八大排序算法详解

前言:从扑克牌到代码,用最直观的方式理解排序算法


目录

  1. 插入排序

  2. 希尔排序

  3. 选择排序

  4. 堆排序

  5. 冒泡排序

  6. 快速排序

  7. 归并排序

  8. 总结对比


一、插入排序

理解思路

就像打扑克牌时整理手牌!

想象你手里有一堆扑克牌,你从左到右一张张整理:

  1. 第1张牌直接拿在手里(已经有序)

  2. 摸到第2张牌,和手里的牌比较,插入到合适位置

  3. 摸到第3张牌,再和前面已经排好的牌比较,插入到合适位置

  4. 重复这个过程,直到所有牌都整理好

核心思想:

  • 把数组分成"已排序"和"未排序"两部分

  • 每次从未排序部分取一个元素,插入到已排序部分的正确位置

  • 就像你玩扑克牌时,每摸一张牌就插入到正确位置

代码实现

cpp

复制代码
// 直接插入排序
// 时间复杂度:O(n²)  空间复杂度:O(1)
// 最好情况:O(n)(数据已经有序)
// 最坏情况:O(n²)(数据逆序)
void InsertSort(int* a, int n)
{
    // i 从1开始,因为第0个元素可以认为已经有序
    // i 表示当前要插入的元素位置
    for (int i = 1; i < n; i++)
    {
        // end 指向已排序部分的最后一个元素
        int end = i - 1;
        
        // tmp 保存要插入的值
        // 因为后面移动元素会覆盖 a[i]
        int tmp = a[i];
        
        // 向前查找插入位置
        // end >= 0:还没到数组开头
        // a[end] > tmp:前面的元素比要插入的大
        while (end >= 0 && a[end] > tmp)
        {
            // 大的元素往后挪,给 tmp 腾位置
            a[end + 1] = a[end];
            end--;  // 继续往前比较
        }
        
        // 找到插入位置了,把 tmp 放进去
        // 因为 end 已经减到比插入位置小1了
        // 所以插入位置是 end + 1
        a[end + 1] = tmp;
    }
}

过程演示

cpp

复制代码
// 对 [5, 3, 4, 1, 2] 排序

初始:5, 3, 4, 1, 2

i=1: tmp=3, end=0 (5)
     5 > 3 → 5往后挪
     [5, 5, 4, 1, 2]
     end=-1, 插入3到位置0
     [3, 5, 4, 1, 2]

i=2: tmp=4, end=1 (5)
     5 > 4 → 5往后挪
     [3, 5, 5, 1, 2]
     end=0 (3)
     3 > 4? 否,停止
     插入4到位置1
     [3, 4, 5, 1, 2]

i=3: tmp=1, end=2 (5)
     5 > 1 → 5往后挪
     [3, 4, 5, 5, 2]
     end=1 (4)
     4 > 1 → 4往后挪
     [3, 4, 4, 5, 2]
     end=0 (3)
     3 > 1 → 3往后挪
     [3, 3, 4, 5, 2]
     end=-1, 插入1到位置0
     [1, 3, 4, 5, 2]

i=4: tmp=2, end=3 (5)
     5 > 2 → 5往后挪
     [1, 3, 4, 5, 5]
     end=2 (4)
     4 > 2 → 4往后挪
     [1, 3, 4, 4, 5]
     end=1 (3)
     3 > 2 → 3往后挪
     [1, 3, 3, 4, 5]
     end=0 (1)
     1 > 2? 否,停止
     插入2到位置1
     [1, 2, 3, 4, 5] ✅

二、希尔排序

理解思路

插入排序的优化版,先让数组"大致有序",再进行最终排序!

想象你要整理一副完全混乱的扑克牌:

  1. 先把牌分成几堆,每堆内部整理(预排序)

  2. 减少堆数,继续整理

  3. 最后全部放在一起整理(最终排序)

核心思想:

  • 插入排序在数据基本有序时效率很高(接近O(n))

  • 希尔排序通过"分组预排序"让数据先大致有序

  • 最后一轮就是直接插入排序,但因为已经基本有序,速度很快

代码实现

cpp

复制代码
// 希尔排序
// 时间复杂度:O(n^1.3) 平均  空间复杂度:O(1)
// 比插入排序快得多,特别是大数据量时
void ShellSort(int* a, int n)
{
    // gap 表示分组间隔,也就是"跳着数"的步长
    // gap = n 时,每个元素单独一组,没有意义
    // 所以从 gap = n 开始,每次让 gap 缩小
    int gap = n;
    
    // 只要 gap > 1,就继续分组预排序
    // gap > 1 时是预排序,gap == 1 时就是直接插入排序
    while (gap > 1)
    {
        // 让 gap 快速缩小
        // 加 1 是为了保证最后 gap 一定能变成 1
        gap = gap / 3 + 1;
        
        // 这里的 i 从 0 开始,每次 +1
        // 实际上是在"交错"地处理不同组
        // 比如 gap=3 时:
        // i=0 处理第0组,i=1处理第1组,i=2处理第2组
        // i=3 回到第0组处理下一个元素
        // 就像同时整理多叠牌,每次从每叠拿一张比较
        for (int i = 0; i < n - gap; i++)
        {
            // end 是当前组中已排序部分的末尾
            int end = i;
            
            // tmp 是当前组中要插入的元素
            // 它在 i + gap 位置(同组的下一个)
            int tmp = a[end + gap];
            
            // 在组内进行插入排序
            // end >= 0:还没到组开头
            // a[end] > tmp:同组前一个元素比要插入的大
            while (end >= 0 && a[end] > tmp)
            {
                // 同组元素往后挪 gap 步
                a[end + gap] = a[end];
                // 继续往前找同组的元素
                end -= gap;
            }
            
            // 插入到正确位置
            a[end + gap] = tmp;
        }
    }
}

过程演示

cpp

复制代码
// 对 [9, 8, 7, 6, 5, 4, 3, 2, 1] 排序

初始:9, 8, 7, 6, 5, 4, 3, 2, 1

// gap = 9/3+1 = 4
// 分成4组:第0组索引0,4,8;第1组索引1,5;第2组索引2,6;第3组索引3,7
第0组:9, 5, 1 → 插入排序 → 1, 5, 9
第1组:8, 4 → 4, 8
第2组:7, 3 → 3, 7
第3组:6, 2 → 2, 6
一轮后:[1, 4, 3, 2, 5, 8, 7, 6, 9]

// gap = 4/3+1 = 2
// 分成2组:第0组索引0,2,4,6,8;第1组索引1,3,5,7
第0组:1, 3, 5, 7, 9 → 已有序
第1组:4, 2, 8, 6 → 插入排序 → 2, 4, 6, 8
二轮后:[1, 2, 3, 4, 5, 6, 7, 8, 9]

// gap = 2/3+1 = 1
// 直接插入排序,但已经基本有序,很快
最终:[1, 2, 3, 4, 5, 6, 7, 8, 9] ✅

三、选择排序

理解思路

每次从牌堆里找出最大和最小的牌,放到正确位置!

想象你有一堆牌,要从小到大排列:

  1. 找出最大的牌,放到最右边

  2. 找出最小的牌,放到最左边

  3. 剩下的牌重复这个过程

  4. 直到所有牌都排好

核心思想:

  • 每次从待排序区间选出最大和最小元素

  • 最小元素放到区间开头,最大元素放到区间结尾

  • 缩小范围,继续选择

代码实现

cpp

复制代码
// 选择排序
// 时间复杂度:O(n²)  空间复杂度:O(1)
// 无论数据是否有序,时间复杂度都是 O(n²)
void SelectSort(int* a, int n)
{
    // begin 指向待排序区间的开头
    int begin = 0;
    // end 指向待排序区间的结尾
    int end = n - 1;
    
    // 当 begin < end 时,说明还有至少两个元素待排序
    while (begin < end)
    {
        // mini 记录最小值的下标,初始为 begin
        int mini = begin;
        // maxi 记录最大值的下标,初始为 begin
        int maxi = begin;
        
        // 遍历待排序区间 [begin, end]
        // 从 begin+1 开始,因为 begin 位置已经作为初始值
        for (int i = begin + 1; i <= end; i++)
        {
            // 如果当前元素比记录的最小值还小
            if (a[i] < a[mini])
            {
                mini = i;  // 更新最小值下标
            }
            // 如果当前元素比记录的最大值还大
            if (a[i] > a[maxi])
            {
                maxi = i;  // 更新最大值下标
            }
        }
        
        // 把最小值放到 begin 位置
        // 注意:要使用交换,不能直接赋值
        // 因为直接赋值会覆盖原数据
        Swap(&a[begin], &a[mini]);
        
        // 如果 begin 位置刚好是最大值
        // 由于上面交换了,最大值被换到 mini 位置了
        // 需要修正 maxi
        if (maxi == begin)
        {
            maxi = mini;
        }
        
        // 把最大值放到 end 位置
        Swap(&a[end], &a[maxi]);
        
        // 缩小范围
        begin++;
        end--;
    }
}

过程演示

cpp

复制代码
// 对 [4, 2, 8, 1, 9, 3] 排序

初始:[4, 2, 8, 1, 9, 3]
      begin=0, end=5

// 第1轮:找最小和最大
最小值1(索引3),最大值9(索引4)
交换 begin(0) 和 mini(3):[1, 2, 8, 4, 9, 3]
交换 end(5) 和 maxi(4):[1, 2, 8, 4, 3, 9]
begin=1, end=4

// 第2轮:在 [2, 8, 4, 3] 中找
最小值2(索引1),最大值8(索引2)
交换 begin(1) 和 mini(1):[1, 2, 8, 4, 3, 9]
交换 end(4) 和 maxi(2):[1, 2, 3, 4, 8, 9]
begin=2, end=3

// 第3轮:在 [3, 4] 中找
最小值3(索引2),最大值4(索引3)
交换 begin(2) 和 mini(2):[1, 2, 3, 4, 8, 9]
交换 end(3) 和 maxi(3):[1, 2, 3, 4, 8, 9]
begin=3, end=2

begin > end,结束
最终:[1, 2, 3, 4, 8, 9] ✅

四、堆排序

理解思路

把数组想象成一棵完全二叉树,利用"大堆"特性排序!

大堆:每个父节点都比它的孩子大(根最大)

核心思想:

  1. 把数组建成一个大堆(根节点是最大值)

  2. 把根节点(最大值)和最后一个元素交换

  3. 最后一个元素就排好了

  4. 剩下的元素继续调整成大堆

  5. 重复2-4步,直到所有元素排好

就像:

  • 你有一堆奖牌,最重的是金牌(堆顶)

  • 每次把金牌拿走放一边

  • 剩下的继续找最重的

  • 重复直到拿完

代码实现

cpp

复制代码
// 向下调整算法(大根堆)
// n: 数组长度,parent: 要调整的父节点下标
// 作用:让 parent 向下调整,保持大堆特性
void AdjustDown(int* a, int n, int parent)
{
    // 左孩子下标
    int child = parent * 2 + 1;
    
    // 还有孩子(没有超过数组范围)
    while (child < n)
    {
        // 找出左右孩子中更大的那个
        // child+1 是右孩子,要确保右孩子存在
        if (child + 1 < n && a[child + 1] > a[child])
        {
            child++;  // 右孩子更大,选右孩子
        }
        
        // 如果孩子比父亲大,需要交换
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            // 继续向下调整,因为交换后可能破坏了下面的堆结构
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            // 父亲已经比孩子大,满足堆条件,停止
            break;
        }
    }
}

// 堆排序
// 时间复杂度:O(n log n)  空间复杂度:O(1)
void HeapSort(int* a, int n)
{
    // 1. 建大堆
    // 从最后一个非叶子节点开始调整
    // (n-1-1)/2 是最后一个非叶子节点
    // 为什么从这开始?因为叶子节点不需要调整
    for (int i = (n - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(a, n, i);
    }
    
    // 2. 排序
    // end 指向未排序部分的最后一个元素
    int end = n - 1;
    while (end > 0)
    {
        // 把堆顶(最大值)和最后一个元素交换
        Swap(&a[0], &a[end]);
        
        // 把剩下的元素重新调整成大堆
        // end 就是剩余元素个数
        AdjustDown(a, end, 0);
        
        // 缩小范围
        end--;
    }
}

过程演示

cpp

复制代码
// 对 [3, 7, 5, 9, 1, 6, 2] 排序

// 1. 建大堆
初始:[3, 7, 5, 9, 1, 6, 2]
       3
      / \
     7   5
    / \ / \
   9  1 6 2

建堆后:[9, 7, 6, 3, 1, 5, 2]
       9
      / \
     7   6
    / \ / \
   3  1 5 2

// 2. 排序
end=6: 交换9和2 → [2, 7, 6, 3, 1, 5, 9]
       向下调整 → [7, 3, 6, 2, 1, 5, 9]
end=5: 交换7和5 → [5, 3, 6, 2, 1, 7, 9]
       向下调整 → [6, 3, 5, 2, 1, 7, 9]
end=4: 交换6和1 → [1, 3, 5, 2, 6, 7, 9]
       向下调整 → [5, 3, 1, 2, 6, 7, 9]
end=3: 交换5和2 → [2, 3, 1, 5, 6, 7, 9]
       向下调整 → [3, 2, 1, 5, 6, 7, 9]
end=2: 交换3和1 → [1, 2, 3, 5, 6, 7, 9]
       向下调整 → [2, 1, 3, 5, 6, 7, 9]
end=1: 交换2和1 → [1, 2, 3, 5, 6, 7, 9]
       向下调整 → [1, 2, 3, 5, 6, 7, 9]

最终:[1, 2, 3, 5, 6, 7, 9] ✅

五、冒泡排序

理解思路

就像水中的气泡,大的元素慢慢"冒"到水面!

想象你有一排不同大小的石头:

  1. 从左边开始,比较第1个和第2个,大的往右移动

  2. 比较第2个和第3个,大的往右移动

  3. 一直比较到最后,最大的石头就"冒"到了最右边

  4. 剩下的石头重复这个过程

  5. 直到所有石头都排好

核心思想:

  • 相邻元素两两比较,大的往后移

  • 每一轮把最大的元素移到未排序部分的最后

  • 就像气泡从水底慢慢升到水面

代码实现

cpp

复制代码
// 冒泡排序
// 时间复杂度:O(n²)  空间复杂度:O(1)
// 最好情况:O(n)(已经有序,优化版)
void BubbleSort(int* a, int n)
{
    // 外层循环:需要几轮
    // i 表示已经排好了几个元素
    for (int i = 0; i < n - 1; i++)
    {
        // flag 优化:如果某一轮没有发生交换,说明已经有序
        bool swapped = false;
        
        // 内层循环:每轮比较的次数
        // j < n - 1 - i:因为每轮后 i 个元素已经排好
        for (int j = 0; j < n - 1 - i; j++)
        {
            // 如果前一个比后一个大,交换
            if (a[j] > a[j + 1])
            {
                Swap(&a[j], &a[j + 1]);
                swapped = true;
            }
        }
        
        // 如果这一轮没有交换,说明全部有序,提前结束
        if (!swapped)
        {
            break;
        }
    }
}

过程演示

cpp

复制代码
// 对 [5, 3, 8, 1, 4, 2] 排序

初始:[5, 3, 8, 1, 4, 2]

// 第1轮:把最大的冒到最后
比较 5和3 → 交换 [3, 5, 8, 1, 4, 2]
比较 5和8 → 不交换 [3, 5, 8, 1, 4, 2]
比较 8和1 → 交换 [3, 5, 1, 8, 4, 2]
比较 8和4 → 交换 [3, 5, 1, 4, 8, 2]
比较 8和2 → 交换 [3, 5, 1, 4, 2, 8]
                           ↑ 8排好了

// 第2轮:在剩下的 [3,5,1,4,2] 中继续
比较 3和5 → 不交换 [3, 5, 1, 4, 2, 8]
比较 5和1 → 交换 [3, 1, 5, 4, 2, 8]
比较 5和4 → 交换 [3, 1, 4, 5, 2, 8]
比较 5和2 → 交换 [3, 1, 4, 2, 5, 8]
                        ↑ 5排好了

// 第3轮:在 [3,1,4,2] 中继续
比较 3和1 → 交换 [1, 3, 4, 2, 5, 8]
比较 3和4 → 不交换 [1, 3, 4, 2, 5, 8]
比较 4和2 → 交换 [1, 3, 2, 4, 5, 8]
                     ↑ 4排好了

// 第4轮:在 [1,3,2] 中继续
比较 1和3 → 不交换 [1, 3, 2, 4, 5, 8]
比较 3和2 → 交换 [1, 2, 3, 4, 5, 8]
                  ↑ 3排好了

// 第5轮:在 [1,2] 中继续
比较 1和2 → 不交换 [1, 2, 3, 4, 5, 8]

最终:[1, 2, 3, 4, 5, 8] ✅

六、快速排序

理解思路

"分治"思想,选择一个基准值,把比它小的放左边,比它大的放右边!

想象你要整理一堆牌:

  1. 随便选一张牌作为"基准"

  2. 把比它小的牌放到左边,比它大的放到右边

  3. 基准牌就找到了正确位置

  4. 对左边和右边分别重复这个过程

  5. 直到所有牌都排好

核心思想:

  • 每次选一个"基准值"

  • 把数组分成三部分:小于基准 + 基准 + 大于基准

  • 递归处理左右两部分

代码实现

cpp

复制代码
// 快速排序主框架
// 时间复杂度:O(n log n) 平均  空间复杂度:O(log n)
// 最好情况:O(n log n)  最坏情况:O(n²)(已经有序)
void QuickSort(int* a, int left, int right)
{
    // 递归终止条件:区间为空或只有一个元素
    if (left >= right)
    {
        return;
    }
    
    // 划分函数:返回基准值的位置
    int meet = PartSort(a, left, right);
    
    // 递归排序左半部分 [left, meet-1]
    QuickSort(a, left, meet - 1);
    
    // 递归排序右半部分 [meet+1, right]
    QuickSort(a, meet + 1, right);
}

// ============================================
// 版本1:挖坑法(最推荐,最容易理解)
// ============================================
// 参数:left 左边界,right 右边界
// 返回值:基准值的最终位置
int PartSort_WaKeng(int* a, int left, int right)
{
    // 选择左边的值作为基准值
    int key = a[left];
    // 坑的位置,初始在 left
    int hole = left;
    
    // left 和 right 指针移动,直到相遇
    while (left < right)
    {
        // 右边找比 key 小的,填到左边坑里
        while (left < right && a[right] >= key)
        {
            right--;
        }
        // 把找到的值填入坑,right 位置变成新坑
        a[hole] = a[right];
        hole = right;
        
        // 左边找比 key 大的,填到右边坑里
        while (left < right && a[left] <= key)
        {
            left++;
        }
        // 把找到的值填入坑,left 位置变成新坑
        a[hole] = a[left];
        hole = left;
    }
    
    // 最后把 key 填入最终坑
    a[hole] = key;
    return hole;
}

// ============================================
// 版本2:前后指针法(代码最简洁)
// ============================================
int PartSort_3Pointers(int* a, int left, int right)
{
    // key 是基准值
    int key = left;
    // prev 指向最后一个小于 key 的位置
    int prev = left;
    // cur 是遍历指针
    int cur = left + 1;
    
    while (cur <= right)
    {
        // 找到比 key 小的值,且 prev 要移动
        if (a[cur] < a[key] && ++prev != cur)
        {
            Swap(&a[cur], &a[prev]);
        }
        cur++;
    }
    
    // 把 key 换到中间位置
    Swap(&a[key], &a[prev]);
    return prev;
}

// ============================================
// 版本3:非递归版(防止递归栈溢出)
// ============================================
void QuickSortNonR(int* a, int left, int right)
{
    // 用栈存储待排序的区间
    Stack st;
    StackInit(&st);
    
    // 先入整个区间(先右后左,因为栈是先进后出)
    StackPush(&st, right);
    StackPush(&st, left);
    
    while (!StackEmpty(&st))
    {
        // 取出一个区间
        int begin = StackTop(&st);
        StackPop(&st);
        int end = StackTop(&st);
        StackPop(&st);
        
        // 对区间进行划分
        int keyi = PartSort_WaKeng(a, begin, end);
        
        // 如果右区间存在,入栈
        if (keyi + 1 < end)
        {
            StackPush(&st, end);
            StackPush(&st, keyi + 1);
        }
        
        // 如果左区间存在,入栈
        if (begin < keyi - 1)
        {
            StackPush(&st, keyi - 1);
            StackPush(&st, begin);
        }
    }
    
    StackDestroy(&st);
}

过程演示(挖坑法)

cpp

复制代码
// 对 [6, 1, 2, 7, 9, 3, 4, 5, 10, 8] 排序

初始:[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
      key=6, hole=0, left=0, right=9

// 第1步:右边找小
right找比6小的:right=7, a[7]=5
填坑:a[0]=5, hole=7
[5, 1, 2, 7, 9, 3, 4, 5, 10, 8]
                          ↑坑

// 第2步:左边找大
left找比6大的:left=3, a[3]=7
填坑:a[7]=7, hole=3
[5, 1, 2, 7, 9, 3, 4, 7, 10, 8]
             ↑坑

// 第3步:右边找小
right找比6小的:right=6, a[6]=4
填坑:a[3]=4, hole=6
[5, 1, 2, 4, 9, 3, 4, 7, 10, 8]
                   ↑坑

// 第4步:左边找大
left找比6大的:left=4, a[4]=9
填坑:a[6]=9, hole=4
[5, 1, 2, 4, 9, 3, 9, 7, 10, 8]
                ↑坑

// 第5步:右边找小
right找比6小的:right=5, a[5]=3
填坑:a[4]=3, hole=5
[5, 1, 2, 4, 3, 3, 9, 7, 10, 8]
                   ↑坑

// 第6步:左边找大
left现在5, right=5, left<right不成立
结束循环,把key填入坑
a[5]=6
[5, 1, 2, 4, 3, 6, 9, 7, 10, 8]
                ↑
            基准6归位

递归处理:[5,1,2,4,3] 和 [9,7,10,8]
继续...最终得到 [1,2,3,4,5,6,7,8,9,10] ✅

七、归并排序

理解思路

"分治"思想的完美体现,先拆分成单个元素,再两两合并!

想象你两堆已经排好序的牌,要合并成一堆:

  1. 比较两堆最上面的牌

  2. 小的先放到新堆

  3. 重复直到一堆空

  4. 把剩下的牌全部放进去

核心思想:

  • 把数组分成两半,分别排序

  • 把两个有序数组合并成一个有序数组

  • 递归到每个子数组只有一个元素(天然有序)

代码实现

cpp

复制代码
// 归并排序主函数
// 时间复杂度:O(n log n)  空间复杂度:O(n)
// 稳定排序,适合链表和外部排序
void MergeSort(int* a, int n)
{
    // 开辟临时数组,用于合并
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        return;
    }
    
    _MergeSort(a, 0, n - 1, tmp);
    
    free(tmp);
}

// 归并排序的核心递归函数
// left:左边界,right:右边界,tmp:临时数组
void _MergeSort(int* a, int left, int right, int* tmp)
{
    // 递归终止:区间只有一个元素或为空
    if (left >= right)
    {
        return;
    }
    
    // 取中间位置,分成两半
    int mid = (left + right) / 2;
    
    // 递归排序左半部分
    _MergeSort(a, left, mid, tmp);
    
    // 递归排序右半部分
    _MergeSort(a, mid + 1, right, tmp);
    
    // ============================================
    // 合并两个有序区间:[left, mid] 和 [mid+1, right]
    // ============================================
    
    // 左区间的范围
    int begin1 = left, end1 = mid;
    // 右区间的范围
    int begin2 = mid + 1, end2 = right;
    // tmp 数组写入的位置
    int i = left;
    
    // 同时遍历两个有序数组
    // 谁小就先把谁放进 tmp
    while (begin1 <= end1 && begin2 <= end2)
    {
        // 使用 <= 保证稳定性(相等时左区间优先)
        if (a[begin1] <= a[begin2])
        {
            tmp[i++] = a[begin1++];
        }
        else
        {
            tmp[i++] = a[begin2++];
        }
    }
    
    // 如果左区间还有剩余,全部拷贝到 tmp
    while (begin1 <= end1)
    {
        tmp[i++] = a[begin1++];
    }
    
    // 如果右区间还有剩余,全部拷贝到 tmp
    while (begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }
    
    // 把 tmp 中排好序的数据拷贝回原数组
    // 注意:是 [left, right] 区间
    for (i = left; i <= right; i++)
    {
        a[i] = tmp[i];
    }
}

过程演示

cpp

复制代码
// 对 [3, 7, 5, 9, 1, 6, 2, 4] 排序

初始:[3, 7, 5, 9, 1, 6, 2, 4]

// ========== 分解阶段 ==========
// 不断对半拆分,直到每个子数组只有一个元素

拆分过程:
[3, 7, 5, 9, 1, 6, 2, 4]
      ↓
[3, 7, 5, 9]    [1, 6, 2, 4]
   ↓                ↓
[3, 7]  [5, 9]   [1, 6]  [2, 4]
  ↓      ↓        ↓       ↓
[3][7]  [5][9]   [1][6]  [2][4]

// ========== 合并阶段 ==========
// 两两合并,边合并边排序

第1次合并:
[3] 和 [7] → [3, 7]
[5] 和 [9] → [5, 9]
[1] 和 [6] → [1, 6]
[2] 和 [4] → [2, 4]

第2次合并:
[3, 7] 和 [5, 9]
比较:3<5 → tmp[0]=3
     7<5? 否 → tmp[1]=5
     7<9 → tmp[2]=7
     左空,右剩9 → tmp[3]=9
得到:[3, 5, 7, 9]

[1, 6] 和 [2, 4]
比较:1<2 → tmp[0]=1
     6<2? 否 → tmp[1]=2
     6<4? 否 → tmp[2]=4
     左空,右剩6 → tmp[3]=6
得到:[1, 2, 4, 6]

第3次合并:
[3, 5, 7, 9] 和 [1, 2, 4, 6]
比较:3<1? 否 → tmp[0]=1
     3<2? 否 → tmp[1]=2
     3<4 → tmp[2]=3
     5<4? 否 → tmp[3]=4
     5<6 → tmp[4]=5
     7<6? 否 → tmp[5]=6
     左剩7,9 → tmp[6]=7, tmp[7]=9
得到:[1, 2, 3, 4, 5, 6, 7, 9] ✅

八、总结对比

性能对比表

算法 最好 平均 最坏 空间 稳定性
插入排序 O(n) O(n²) O(n²) O(1) ✅ 稳定
希尔排序 O(n) O(n^1.3) O(n²) O(1) ❌ 不稳定
选择排序 O(n²) O(n²) O(n²) O(1) ❌ 不稳定
堆排序 O(n log n) O(n log n) O(n log n) O(1) ❌ 不稳定
冒泡排序 O(n) O(n²) O(n²) O(1) ✅ 稳定
快速排序 O(n log n) O(n log n) O(n²) O(log n) ❌ 不稳定
归并排序 O(n log n) O(n log n) O(n log n) O(n) ✅ 稳定

选择建议

cpp

复制代码
// 1. 数据量小(n < 100)
// 使用插入排序,简单高效,常数小
if (n < 100) InsertSort(a, n);

// 2. 数据量大且随机
// 使用快速排序,平均最快
QuickSort(a, 0, n-1);

// 3. 要求稳定性
// 使用归并排序
MergeSort(a, n);

// 4. 数据基本有序
// 使用插入排序或冒泡排序(接近O(n))
if (isAlmostSorted) InsertSort(a, n);

// 5. 链表排序
// 使用归并排序(快排对链表不友好)
MergeSortList(head);

// 6. 内存受限
// 使用堆排序(O(1)空间)
HeapSort(a, n);

完整代码

cpp

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// ============================================
// 辅助函数
// ============================================
void Swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void PrintArray(int* a, int n)
{
    for (int i = 0; i < n; i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
}

// ============================================
// 1. 插入排序
// ============================================
void InsertSort(int* a, int n)
{
    for (int i = 1; i < n; i++)
    {
        int end = i - 1;
        int tmp = a[i];
        while (end >= 0 && a[end] > tmp)
        {
            a[end + 1] = a[end];
            end--;
        }
        a[end + 1] = tmp;
    }
}

// ============================================
// 2. 希尔排序
// ============================================
void ShellSort(int* a, int n)
{
    int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        for (int i = 0; i < n - gap; i++)
        {
            int end = i;
            int tmp = a[end + gap];
            while (end >= 0 && a[end] > tmp)
            {
                a[end + gap] = a[end];
                end -= gap;
            }
            a[end + gap] = tmp;
        }
    }
}

// ============================================
// 3. 选择排序
// ============================================
void SelectSort(int* a, int n)
{
    int begin = 0, end = n - 1;
    while (begin < end)
    {
        int mini = begin, maxi = begin;
        for (int i = begin + 1; i <= end; i++)
        {
            if (a[i] < a[mini]) mini = i;
            if (a[i] > a[maxi]) maxi = i;
        }
        Swap(&a[begin], &a[mini]);
        if (maxi == begin) maxi = mini;
        Swap(&a[end], &a[maxi]);
        begin++;
        end--;
    }
}

// ============================================
// 4. 堆排序
// ============================================
void AdjustDown(int* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while (child < n)
    {
        if (child + 1 < n && a[child + 1] > a[child])
        {
            child++;
        }
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

void HeapSort(int* a, int n)
{
    for (int i = (n - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(a, n, i);
    }
    int end = n - 1;
    while (end > 0)
    {
        Swap(&a[0], &a[end]);
        AdjustDown(a, end, 0);
        end--;
    }
}

// ============================================
// 5. 冒泡排序
// ============================================
void BubbleSort(int* a, int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        bool swapped = false;
        for (int j = 0; j < n - 1 - i; j++)
        {
            if (a[j] > a[j + 1])
            {
                Swap(&a[j], &a[j + 1]);
                swapped = true;
            }
        }
        if (!swapped) break;
    }
}

// ============================================
// 6. 快速排序
// ============================================
int PartSort(int* a, int left, int right)
{
    int key = a[left];
    int hole = left;
    while (left < right)
    {
        while (left < right && a[right] >= key) right--;
        a[hole] = a[right];
        hole = right;
        while (left < right && a[left] <= key) left++;
        a[hole] = a[left];
        hole = left;
    }
    a[hole] = key;
    return hole;
}

void QuickSort(int* a, int left, int right)
{
    if (left >= right) return;
    int meet = PartSort(a, left, right);
    QuickSort(a, left, meet - 1);
    QuickSort(a, meet + 1, right);
}

// ============================================
// 7. 归并排序
// ============================================
void _MergeSort(int* a, int left, int right, int* tmp)
{
    if (left >= right) return;
    int mid = (left + right) / 2;
    _MergeSort(a, left, mid, tmp);
    _MergeSort(a, mid + 1, right, tmp);
    
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    int i = left;
    
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] <= a[begin2])
            tmp[i++] = a[begin1++];
        else
            tmp[i++] = a[begin2++];
    }
    while (begin1 <= end1) tmp[i++] = a[begin1++];
    while (begin2 <= end2) tmp[i++] = a[begin2++];
    
    for (i = left; i <= right; i++)
        a[i] = tmp[i];
}

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL) return;
    _MergeSort(a, 0, n - 1, tmp);
    free(tmp);
}

// ============================================
// 测试
// ============================================
void TestSort()
{
    int a[] = {3, 7, 5, 9, 1, 6, 2, 4};
    int n = sizeof(a) / sizeof(int);
    
    printf("原数组:");
    PrintArray(a, n);
    
    InsertSort(a, n);
    printf("插入排序:");
    PrintArray(a, n);
}

int main()
{
    TestSort();
    return 0;
}