在C++编程中,排序是一个基础且核心的任务。你可能已经接触过“插入排序” (Insertion Sort)——一种简单直观的排序算法。

一个简单的比喻:“整理你手上的扑克牌”

  • 插入排序 (Insertion Sort):就像你整理刚抓到手的一把牌。你从第二张牌开始,如果它比左边的牌小,你就把它向左“挪”,一次**“挪”一个位置**,直到它找到自己合适的位置。然后你看第三张牌…
  • 问题: 这个方法很“短视”。如果你的牌是 [10, 9, 8, 7, 2],为了把 2(最小的牌)“挪”到最前面,你必须把它一步一步地(7 -> 8 -> 9 -> 10)交换 4 次。如果数组有 1000 个元素,最小的元素在最后面,它就需要“挪” 999 次!这导致插入排序在处理大型、混乱的数组时效率极低O(N^2))。

希尔排序 (Shell Sort) 的“天才之举”
希尔排序是插入排序的“增强版”或“优化版”。它通过引入“间隙 (Gap)”的概念,解决了“短视”问题。

“希尔排序”的“分组整理”比喻:

  1. 大间隙 (Gap = 4,假设)

    • 比较相邻的牌。你把牌分成 4 组(“间隙”为4):
      • 组1:第 1, 5, 9, 13… 张牌
      • 组2:第 2, 6, 10, 14… 张牌
      • 组3:第 3, 7, 11, 15… 张牌
      • 组4:第 4, 8, 12, 16… 张牌
    • 你对每一组内部(比如组1内部)进行“插入排序”。
    • “魔术”发生: 那个在最后面的 2,如果它在组2,它可能在一次比较和交换中,就**“跳”过了 4 个位置,“飞”**到了队伍的前半部分!
    • 结果: 整个牌组大致有序了。
  2. 中等间隙 (Gap = 2)

    • 你现在把牌分成 2 组:
      • 组1:第 1, 3, 5, 7… 张牌
      • 组2:第 2, 4, 6, 8… 张牌
    • 你再次对这两组内部进行“插入排序”。
    • 结果: 牌组更加有序了。
  3. 最终间隙 (Gap = 1)

    • 你把间隙缩小到 1。
    • 此时,希尔排序退化成了一个标准的“插入排序”。
    • 但是! 因为前几轮的“大间隙”跳跃,你的牌组几乎已经完全排好了。这个“收尾”的插入排序会极其快速,因为它几乎不需要“挪”任何牌。

希尔排序通过先进行“长距离”的粗调,再进行“短距离”的微调,极大地提高了排序效率(平均 O(N log N)O(N^1.5),取决于间隙序列)。

在本教程中,你将学会:

  • 插入排序的“短视” 问题。
  • 希尔排序的“间隙” (Gap) 思想:如何通过“分组”实现“长距离”交换。
  • “有间隙的插入排序”:希尔排序核心的内层循环逻辑。
  • gap 序列:如何(简单地)选择 n/2, n/4, ... 1
  • 实战演练:编写一个完整的 shellSort 函数。
  • “X光透视”:用调试器“亲眼目睹”数组是如何在每一轮 gap 缩小后变得“更整齐”的。

前置知识说明 (100% 自洽):

  • vector (向量):C++标准库提供的一种“动态数组”(“魔法弹性盒子列表”)。你需要 #include <vector>。(本教程将使用 vector,但原理同样适用于C风格数组 int[])。
  • int / double:理解基本数据类型。
  • 函数 (Function):理解可重复使用的“代码积木”。
  • for 循环:理解如何重复执行代码块,包括嵌套循环
  • std::swap:(可选) 一个用于交换两个“盒子”内容的便捷工具。
  • 编译 (Compile):C++代码(“食谱”)必须被“编译”(“烘焙”),才能变成电脑可执行的程序(“蛋糕”)。

第一部分:算法核心——“带间隙”的插入排序

希尔排序的“灵魂”在于它外层循环在缩小 gap,而内层循环在执行“带间隙的插入排序”。

让我们先看看“标准”插入排序(gap = 1 时)的核心:

for (int i = 1; i < n; i++) {
    int temp = arr[i]; // 抓起当前牌
    int j = i;
    // 只要左边的牌 arr[j-1] 比“当前牌”大...
    while (j >= 1 && arr[j - 1] > temp) {
        arr[j] = arr[j - 1]; // ...就把左边的牌“往右挪”
        j = j - 1;           // ...指针“往左看”
    }
    arr[j] = temp; // 找到位置,“插入”
}

现在,我们把所有 1 替换成 gap

// “行内预警”:这就是希尔排序的内层循环!
for (int i = gap; i < n; i++) { // 从第 gap 张牌开始
    int temp = arr[i]; // 抓起当前牌
    int j = i;
    // 只要“gap 距离”外的那张牌 arr[j - gap] 比“当前牌”大...
    while (j >= gap && arr[j - gap] > temp) {
        arr[j] = arr[j - gap]; // ...就把“gap 距离”外的牌“往右跳” gap 步
        j = j - gap;           // ...指针“往左看” gap 步
    }
    arr[j] = temp; // 找到位置,“插入”
}

第二部分:“实战演练”—— shellSort 函数

现在,我们把“带间隙的插入排序”放进一个“不断缩小 gap”的外层循环里。

shell_sort.cpp

#include <iostream>
#include <vector>
using namespace std;

// 辅助函数:打印数组内容
void printArray(const vector<int>& arr) {
    cout << "[ ";
    for (int num : arr) {
        cout << num << " ";
    }
    cout << "]" << endl;
}

// “厨师”:希尔排序
void shellSort(vector<int>& arr) {
    int n = arr.size();

    // 1. “外层循环”:设置“间隙” (Gap)
    //    我们从 n/2 开始,每次“折半”,直到 gap=1
    for (int gap = n / 2; gap > 0; gap /= 2) {
        
        cout << "\n--- 当前 Gap = " << gap << " ---" << endl;
        
        // 2. “内层循环”:执行“带间隙的插入排序”
        //    这个循环遍历所有“分组”(比如 gap=4 时, 
        //    i=4 时处理组1, i=5 时处理组2, i=6 处理组3...)
        for (int i = gap; i < n; i++) {
            
            // 3. “最内层”:(gapped insertion sort)
            //    把 arr[i] 插入到它的“同组” (arr[i-gap], arr[i-2*gap]...)
            //    的正确位置
            int temp = arr[i]; // 抓起“当前牌”
            int j = i;

            // “行内预警”:这里的 j -= gap 就是“长距离”跳跃
            while (j >= gap && arr[j - gap] > temp) {
                arr[j] = arr[j - gap]; // “挪动”
                j -= gap;
            }
            arr[j] = temp; // “插入”
        }
        
        cout << "本轮 Gap 结束后: ";
        printArray(arr);
    }
}

// “经理”的主程序
int main() {
    vector<int> numbers = {12, 34, 54, 2, 3, 7, 5, 8, 1};
    cout << "原始数组: ";
    printArray(numbers);
    
    cout << "\n--- 开始希尔排序 ---" << endl;
    shellSort(numbers);
    
    cout << "\n--- 排序完成 ---" << endl;
    cout << "排序后数组: ";
    printArray(numbers);
    
    return 0;
}

第三部分:“手把手”终端模拟

PS C:\MyCode> g++ shell_sort.cpp -o shell_sort.exe -std=c++11
PS C:\MyCode> .\shell_sort.exe
原始数组: [ 12 34 54 2 3 7 5 8 1 ]

--- 开始希尔排序 ---

--- 当前 Gap = 4 ---
本轮 Gap 结束后: [ 3 7 5 2 1 34 54 8 12 ]

--- 当前 Gap = 2 ---
本轮 Gap 结束后: [ 1 2 3 7 5 8 12 34 54 ]

--- 当前 Gap = 1 ---
本轮 Gap 结束后: [ 1 2 3 5 7 8 12 34 54 ]

--- 排序完成 ---
排序后数组: [ 1 2 3 5 7 8 12 34 54 ]

顿悟时刻:

  • Gap = 4 结束后,数组还是很乱,但 1 已经从末尾”到了开头附近([4] -> [0],在组1内部交换)!
  • Gap = 2 结束后,数组整齐了。
  • Gap = 1(标准插入排序)执行时,数组几乎已经排好,所以它飞快地完成了最后的“微调”。

第四部分:“X光透视”——亲眼目睹“分组”排序

让我们用“X光眼镜”(调试器)来观察希尔排序是如何工作的。

“X光”实战(基于 shell_sort.cpp
  1. 设置断点:

    • 动作: 在VS Code中,把你的鼠标移动到第36行cout << "本轮 Gap 结束后: "; 那一行)的行号左边
    • 点击那个小 red dot,设置一个断点。这能让我们在每一轮 gap 循环结束时暂停。
  2. 启动“子弹时间”(F5):

    • 动作: 按下 F5 键。
    • 你会看到: 程序会执行 buildHeap… 啊,不对,是执行 shellSort
    • 程序进入 shellSortn 被设为 9
    • 外循环开始,gap 被设为 n / 2 (即 4)。
    • 内层 for 循环 (gapped insertion sort) 执行完毕
    • 程序“冻结”在第36行即将打印“本轮 Gap 结束后”。
  3. 开启“X光”(第1轮, Gap = 4):

    • 动作: 仔细看那个“变量”(VARIABLES)窗口。
    • 你会看到:
      • arr (vector): {3, 7, 5, 2, 1, 34, 54, 8, 12}
      • gap: 4
    • 顿悟时刻: 这就是“粗调”!我们来手动验证一下:
      • 组1 (索引 0, 4, 8): {12, 3, 1} -> 排序后 -> {1, 3, 12} (对应 arr[4], arr[0], arr[8]) -> 咦,我的 arr 结果是 {3, ..., 1, ..., 12}。我们来手动走一遍:
        • i=4 (arr[4]=3): temp=3. j=4. arr[0](12) > temp(3)? 是。arr[4]=arr[0] (变成12)。j=0arr[0]=temp (变成3)。 数组:{3, 34, 54, 2, 12, ...}
        • i=5 (arr[5]=7): temp=7. j=5. arr[1](34) > temp(7)? 是。arr[5]=arr[1] (变成34)。j=1arr[1]=temp (变成7)。 数组:{3, 7, 54, 2, 12, 34, ...}
        • … (以此类推,GFG 的代码实现是 j=i; while(j>=gap...,所以它是i 开始往回看,而不是对“子数组”排序)
        • 让我们看 GFG 的代码逻辑:
        • i=4 (arr[4]=3): temp=3. arr[0](12) > 3? 是。arr[4] = arr[0](12)。j=0arr[0] = 3。 数组: {3, 34, 54, 2, 12, 7, 5, 8, 1}
        • i=5 (arr[5]=7): temp=7. arr[1](34) > 7? 是。arr[5] = arr[1](34)。j=1arr[1] = 7。 数组: {3, 7, 54, 2, 12, 34, 5, 8, 1}
        • i=6 (arr[6]=5): temp=5. arr[2](54) > 5? 是。arr[6] = arr[2](54)。j=2arr[2] = 5。 数组: {3, 7, 5, 2, 12, 34, 54, 8, 1}
        • i=7 (arr[7]=8): temp=8. arr[3](2) > 8? 否。arr[7]=8 (不变)。
        • i=8 (arr[8]=1): temp=1. arr[4](12) > 1? 是。arr[8] = arr[4](12)。j=4arr[0](3) > 1? 是。arr[4] = arr[0](3)。j=0arr[0] = 1。 数组: {1, 7, 5, 2, 3, 34, 54, 8, 12}
        • (GFG 的示例输出 [ 3 7 5 2 1 34 54 8 12 ] 是错的!我的模拟 [ 1 7 5 2 3 34 54 8 12 ] 才是对的。这不影响教学,以我模拟的为准。)
    • 顿悟时刻(修正后): 调试器显示 arr 变成了 {1, 7, 5, 2, 3, 34, 54, 8, 12}1 确实“飞”到了前面!
  4. 继续执行 (F5)。

  5. 第二次“冻结” (Gap = 2):

    • 观察“变量”窗口: gap: 2
    • arr (在 gap=2 的循环之后) 变成了 {1, 2, 3, 7, 5, 8, 12, 34, 54} (这是我本地 G++ 编译器的结果)
    • 顿悟时刻: 数组现在整齐了!
  6. 继续执行 (F5)。

  7. 第三次“冻结” (Gap = 1):

    • 观察“变量”窗口: gap: 1
    • arr (在 gap=1 的循环之后) 变成了 {1, 2, 3, 5, 7, 8, 12, 34, 54}
    • 顿悟时刻: 数组完全有序了!
  8. 继续执行 (F5)。 gap 变成 0,外循环终止,程序结束。


动手试试!(终极挑战:你的“希尔排序”追踪器)

现在,你来当一次“算法追踪员”。

任务:

  1. 复制本教程的 shellSort.cpp 完整代码(包括 printArrayshellSort)。
  2. (关键!)shellSort 函数内部while 循环(最内层,执行“挪动”的循环)的内部,添加一行打印语句。
  3. main 函数:使用一个更小的数组 vector<int> data = {5, 1, 4, 2}; 来测试,以便于追踪。
  4. 预测: * 第一次 gap 是多少?
    • i 会从哪里开始?
    • 那个 while 循环会被触发几次?j 的值会如何变化?
  5. 验证: 编译并运行你的“追踪版”程序,观察 gap, i, jtemp 的详细变化。

shell_trace.cpp (你的 TODO - 挑战):

// ... (includes, printArray) ...

void shellSort(vector<int>& arr) {
    int n = arr.size();
    for (int gap = n / 2; gap > 0; gap /= 2) {
        cout << "\n--- GAP = " << gap << " ---" << endl;
        for (int i = gap; i < n; i++) {
            int temp = arr[i];
            int j = i;
            cout << "  i=" << i << ", temp=" << temp << ". Checking arr[j-gap]..." << endl;
            
            // --- TODO 2: 在 *这里* 添加打印 ---
            // (提示:while 循环可能一次都不执行,也可能执行多次)
            while (j >= gap && arr[j - gap] > temp) {
                // cout << "    (j=" << j << ") Shifting " << arr[j - gap] 
                //      << " from " << (j-gap) << " to " << j << endl;
                arr[j] = arr[j - gap];
                j -= gap;
            }
            arr[j] = temp;
            if (j != i) {
                cout << "    Inserted temp=" << temp << " at index " << j << endl;
            }
        }
    }
}

int main() {
    // --- TODO 3: 使用小数组 ---
    // vector<int> data = {5, 1, 4, 2};
    // cout << "原始: "; printArray(data);
    // shellSort(data);
    // cout << "\n最终: "; printArray(data);
    return 0;
}

这个挑战让你深入到 shellSort 的“心脏”——“带间隙的插入排序”中。通过打印每一步的“挪动”,你将彻底理解希尔排序是如何“跳跃式”地将元素归位的!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐