Aurora
发布于 2026-07-09 / 17 阅读
0
0

排序

排序算法分析

1. 冒泡排序(Bubble Sort)

原理图

@startuml title 冒泡排序:相邻比较,大的往后"冒" start : 总共需要扫描 n-1 趟; : i = 0; while (i < len - 1 ?) is (是) : 假设这一趟没有交换; : swapped = false; while (j < len - 1 - i ?) is (是) if (相邻的左边 > 右边 ?) then (是) : 交换这两个数; note right: 大的数往后冒 : swapped = true; endif : j++; endwhile (否) if (这一趟没有发生交换 ?) then (是) note right: 已经有序,提前结束 stop endif : i++; endwhile (否) stop @enduml

通俗解释

从头开始,相邻两个数比较,大的往后移。每一轮扫下来,最大的数就"冒"到了末尾。如果某一轮没有发生任何交换,说明已经有序,提前结束。

指标

指标
时间复杂度(最好) O(n) —— 已有序,一趟扫完无交换即结束
时间复杂度(平均) O(n²)
时间复杂度(最差) O(n²) —— 完全倒序
空间复杂度 O(1)
稳定性 稳定

代码

VOID bubble_sort(INT32 *nums, INT32 len)
{
    for (INT32 i = 0; i < len - 1; i++) {
        BOOL swapped = FALSE;
        for (INT32 j = 0; j < len - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                SWAP_TWO_NUMS(nums[j], nums[j + 1]);
                swapped = TRUE;
            }
        }
        if (!swapped) break;
    }
}

2. 插入排序(Insertion Sort)

原理图

@startuml title 插入排序:像打扑克摸牌一样,新牌插到合适位置 start : 手里已经有第 0 张牌(认为它已排好序); : 摸下一张牌 i = 1; while (还有没摸完的牌吗?) is (是) : 看看新牌该插到哪里; : j = i; while (还没到头 且 新牌小于前面的牌?) is (是) : 把前面的牌往后挪一位; note right: 给新牌腾出位置 : j--; endwhile (否) : 新牌落位; note right: 现在手里的牌又是从小到大了 : 摸下一张牌; endwhile (否) stop @enduml

通俗解释

就像打扑克摸牌。左手已经拿了几张排好序的牌,新摸一张,从右往左比,找到合适的位置插进去。数组左侧是"手里的牌",右侧是"还没摸的牌"。

指标

指标
时间复杂度(最好) O(n) —— 已经有序时每次只比一下就结束
时间复杂度(平均) O(n²)
时间复杂度(最差) O(n²) —— 倒序,每次都要挪一堆
空间复杂度 O(1)
稳定性 稳定

代码

VOID insertion_sort(INT32 *nums, INT32 len)
{
    for (INT32 i = 1; i < len; i++) {
        for (INT32 j = i; j > 0; j--) {
            if (nums[j] < nums[j - 1]) {
                SWAP_TWO_NUMS(nums[j], nums[j - 1]);
            } else {
                break;
            }
        }
    }
}

3. 选择排序(Selection Sort)

原理图

@startuml title 选择排序:每轮挑出最小的,放到前面去 start : 从第 0 个位置开始; while (还有没排好的位置吗?) is (是) : 假设当前位置就是最小的; : 记录它的位置 minIdx = i; while (往后找还有更小的吗?) is (是) if (发现更小的数 ?) then (是) : 更新 minIdx; note right: 记住这个更小的位置 endif : 继续往后看; endwhile (否) : 把找到的最小值和当前位置交换; note right: 现在这个位置就是正确的数了 : 去下一个位置; endwhile (否) stop @enduml

通俗解释

每次从剩下的数里"挑"出最小的那个,放到最前面。就像从一堆苹果里挑出最小的放第一个,再从剩下的里面挑最小的放第二个……以此类推。

指标

指标
时间复杂度 O(n²) —— 无论什么情况,都要完整扫一遍找最小值
空间复杂度 O(1)
稳定性 不稳定 —— 挑最小值时可能把相等的数换到后面去

代码

VOID selection_sort(INT32 *nums, INT32 len)
{
    for (INT32 i = 0; i < len; i++) {
        INT32 min = i;
        for (INT32 j = i + 1; j < len; j++) {
            if (nums[j] < nums[min]) {
                min = j;
            }
        }
        SWAP_TWO_NUMS(nums[i], nums[min]);
    }
}

4. 快速排序(Quick Sort)

原理图

@startuml title 快速排序:选个班长,小的站左边,大的站右边 start : 处理数组 fragment (left ~ right); if (这个片段是不是只有 0 或 1 个人?) then (是) : 已经有序,不用再排了; stop else (否) : 选第一个数当"班长"(pivot); : i = left, j = left + 1; while (还有没看过的同学吗?) is (是) if (这个同学比班长小 ?) then (是) : i 往前走一步; : 把这位同学换到 i 的位置; endif : j 往后看下一个; endwhile (否) : 把班长放到 i 位置; note right: 现在左边都是比班长小的\n右边都是比班长大的 : 对左边小组(比班长小的) 重复此流程; : 对右边小组(比班长大的) 重复此流程; endif stop @enduml

通俗解释

选第一个人当"基准",把比它小的都挪到左边,比它大的都挪到右边。然后左右两边各自再选基准、再分组……直到每组只剩一个人。这就是"分而治之"的思想。

指标

指标
时间复杂度(平均) O(n log n)
时间复杂度(最差) O(n²) —— 每次选的基准都是最大或最小
空间复杂度(平均) O(log n) —— 递归栈
空间复杂度(最差) O(n) —— 数组完全有序时递归深度为 n
稳定性 不稳定

代码

VOID quick_sort_proc(INT32 *nums, INT32 left, INT32 right)
{
    if (left < right) {
        INT32 cur = left;
        INT32 i = cur, j = i + 1;
        while (j <= right) {
            if (nums[j] < nums[cur]) {
                i++;
                SWAP_TWO_NUMS(nums[i], nums[j]);
            }
            j++;
        }
        if (cur != i) SWAP_TWO_NUMS(nums[cur], nums[i]);
        quick_sort_proc(nums, left, i - 1);
        quick_sort_proc(nums, i + 1, right);
    }
}

VOID quick_sort(INT32 *nums, INT32 len)
{
    quick_sort_proc(nums, 0, len - 1);
}

5. 堆排序(Heap Sort)

原理图

@startuml title 堆排序:先把数组搭成"金字塔",再从塔顶取最大值 start partition "第一步:搭成最大堆" { : 从最后一个"有孩子"的节点开始; while (还有没处理过的节点?) is (是) : 把这个节点和它的孩子比较; note right: 检查父节点 >= 两个孩子 : 如果父节点不是最大的,就和孩子中大的那个交换; : 交换后继续检查被换下去的孩子; : 处理下一个节点; endwhile (否) note right: 现在数组像个金字塔\n塔顶(nums[0])就是最大值 } partition "第二步:逐个取最大值" { : i = 数组末尾; while (还没取完?) is (是) : 把塔顶(最大值)和位置 i 交换; note right: 最大值"上岸"了 : 对剩余部分重新搭成堆; note right: 让新的塔顶也满足条件 : i 往前移一位; endwhile (否) } stop @enduml

堆化过程

@startuml title 堆化:让一个节点满足"父 >= 子" start : 把 root 记为"最大"; : 左孩子 = root × 2 + 1; : 右孩子 = root × 2 + 2; if (左孩子存在 且 比"最大"还大?) then (是) : "最大" = 左孩子; endif if (右孩子存在 且 比"最大"还大?) then (是) : "最大" = 右孩子; endif if (最大不是 root ?) then (是) : root 和 max 交换; : 对 max 位置继续堆化; note right: 保证调整后下面也一样满足条件 endif stop @enduml

通俗解释

把数组想象成一个"金字塔"(堆):顶点最大,每个节点都比它的孩子大。搭好这个金字塔后,每次把顶点的最大值取走放到数组末尾,然后重新调整金字塔。不断重复,就排好序了。

指标

指标
时间复杂度 O(n log n) —— 建堆一次,取每个数都要调整
空间复杂度 O(1) —— 直接在数组上操作
稳定性 不稳定

代码

VOID heap_create(INT32 *nums, INT32 len, INT32 root)
{
    INT32 max = root;
    INT32 leftChild = root * 2 + 1;
    INT32 rightChild = root * 2 + 2;
    if (leftChild < len && nums[leftChild] > nums[max]) {
        max = leftChild;
    }
    if (rightChild < len && nums[rightChild] > nums[max]) {
        max = rightChild;
    }
    if (max != root) {
        SWAP_TWO_NUMS(nums[root], nums[max]);
        heap_create(nums, len, max);
    }
}

VOID heap_sort(INT32 *nums, INT32 len)
{
    for (INT32 i = len / 2 - 1; i >= 0; i--)
        heap_create(nums, len, i);

    for (INT32 i = len - 1; i > 0; i--) {
        SWAP_TWO_NUMS(nums[i], nums[0]);
        heap_create(nums, i, 0);
    }
}

6. 归并排序(Merge Sort)

原理图

@startuml title 归并排序:先把数组拆成单个,再两两合并成有序 start : 处理数组片段 (begin ~ end); if (是不是只剩一个数了?) then (是) : 一个数不用排; stop else (否) : 从中间切成两半; : mid = (begin + end) / 2; partition "分:左右各自排序" { : 左边一半: merge_sort(begin, mid); note right: 递归,直到只剩一个数 : 右边一半: merge_sort(mid+1, end); } partition "合:把两个有序的片段合并" { : i 指向左半开头, j 指向右半开头; while (左右两边都还有人吗?) is (是) if (左边的数 <= 右边的数 ?) then (是) : 把左边的数放入辅助数组; : i 往后走; else (否) : 把右边的数放入辅助数组; : j 往后走; endif endwhile (否) : 左边剩的都放进去; : 右边剩的都放进去; : 把辅助数组复制回原数组; } endif stop @enduml

分治归并过程

@startuml title 归并排序:拆 → 拆 → 拆 → 合 → 合 → 合 left to right direction rectangle "[5, 3, 8, 6, 2]" as a0 rectangle " 拆成两半 " as a1 rectangle "[5, 3, 8]" as a2 rectangle "[6, 2]" as a3 rectangle " 再拆 " as a4 rectangle " 再拆 " as a5 rectangle "[5, 3]" as a6 rectangle "[8] 不用拆了" as a7 rectangle "[6]" as a8 rectangle "[2]" as a9 rectangle " 再拆 " as a10 rectangle "[5]" as a11 rectangle "[3]" as a12 rectangle " 合并 [3, 5]" as m1 rectangle " 合并 [3, 5, 8]" as m2 rectangle " 合并 [2, 6]" as m3 rectangle " 最终合并 [2, 3, 5, 6, 8]" as m4 a0 --> a1 a1 --> a2 a1 --> a3 a2 --> a4 a2 --> a7 a3 --> a5 a5 --> a8 a5 --> a9 a4 --> a6 a4 --> "[8] 不…" a6 --> a10 a10 --> a11 a10 --> a12 a11 --> m1 a12 --> m1 m1 --> m2 a7 --> m2 a8 --> m3 a9 --> m3 m2 --> m4 m3 --> m4 note right of m4: 有序了! @enduml

通俗解释

把数组不断对半切,直到每个小组只剩一个人(一个人当然是有序的)。然后两两合并:合并的时候,比较两边的数,谁小谁先出来。这样不断合并,最后全部有序。

指标

指标
时间复杂度 O(n log n) —— 稳定发挥,不管什么情况都要走完流程
空间复杂度 O(n) —— 需要额外一个临时数组来合并
稳定性 稳定

代码

VOID merge_sort_proc(INT32 *nums, INT32 begin, INT32 end, INT32 *tempNums)
{
    if (begin >= end) return;

    INT32 mid = (begin + end) / 2;
    merge_sort_proc(nums, begin, mid, tempNums);
    merge_sort_proc(nums, mid + 1, end, tempNums);

    INT32 i = begin, j = mid + 1, index = begin;
    while (i <= mid && j <= end) {
        if (nums[i] <= nums[j])
            tempNums[index++] = nums[i++];
        else
            tempNums[index++] = nums[j++];
    }
    while (i <= mid)
        tempNums[index++] = nums[i++];
    while (j <= end)
        tempNums[index++] = nums[j++];

    memcpy(nums + begin, tempNums + begin, sizeof(INT32) * (end - begin + 1));
}

VOID merge_sort(INT32 *nums, INT32 len)
{
    INT32 *tempNums = (INT32 *)malloc(sizeof(INT32) * len);
    merge_sort_proc(nums, 0, len - 1, tempNums);
    free(tempNums);
}

7. 希尔排序(Shell Sort)

原理图

@startuml title 希尔排序:先跳着排,再挨着排(插入排序的升级版) start : gap = len / 2; while (gap 还没缩小到 0?) is (是) note right: 当前间隔 = gap(跳着比较) : 从位置 gap 开始; : i = gap; while (还没到数组末尾?) is (是) : 记下当前位置的值 temp; : j = i; while (还能往前跳 gap 步 且 前面那个数比 temp 大?) is (是) : 把前面的数"挪"到当前位置; note right: 跳着后移 gap 步 : j 往前跳 gap 步; endwhile (否) : 把 temp 放回正确位置; note right: 跳着排好了一个数 : i 往后走一步; endwhile (否) : gap 缩小一半; note right: 间隔越来越小\n最后一轮 gap=1 就是普通插入排序 endwhile (否) stop @enduml

间隔变化

@startuml title 间隔逐步缩小的过程 skinparam rectangle { BackgroundColor #FEFEFE BorderColor #333333 } rectangle "gap = 数组长度的一半 → 不断减半 → 直到 1" as seq rectangle "比如长度8: gap=4 → gap=2 → gap=1" as eg rectangle "gap=4: 位置0和4比、1和5比、2和6比、3和7比" as g4 rectangle "gap=2: 位置0、2、4、6 一组;1、3、5、7 一组" as g2 rectangle "gap=1: 所有数挨个比(普通插入排序)" as g1 seq --> eg eg --> g4 g4 --> g2 g2 --> g1 note right of g4: 数据快速"大略"有序 note right of g2: 更有序了 note right of g1: 已经接近有序,\n插入排序很快 @enduml

通俗解释

插入排序的升级版。先让相距较远的元素比较交换(比如隔 4 个位置比一比),让数组快速变得"大体有序"。然后不断缩小间隔,直到间隔为 1 时就是普通的插入排序。这时候数组已经基本有序了,插入排序飞快。

指标

指标
时间复杂度(最好) O(n)
时间复杂度(平均/最差) O(n) ~ O(n²) —— 取决于 gap 序列怎么选
空间复杂度 O(1)
稳定性 不稳定

代码

VOID shell_sort(INT32 *nums, INT32 len)
{
    for (INT32 gap = len / 2; gap > 0; gap /= 2) {
        for (INT32 i = gap; i < len; i++) {
            INT32 temp = nums[i];
            INT32 j;
            for (j = i; j >= gap && nums[j - gap] > temp; j -= gap) {
                nums[j] = nums[j - gap];
            }
            nums[j] = temp;
        }
    }
}

8. 基数排序(Radix Sort)

原理图

@startuml title 基数排序:按个位、十位、百位……一次次排队 start : 找到数组中最大的数(知道最多有几位); : exp = 1(先从个位开始); while (还有更高位没处理?) is (是) note right: 当前处理的是"个位"或"十位"…… : 准备 10 个桶(0 ~ 9); : 遍历数组,看每个数当前位是几; note right 比如 exp=1: 数字 12 → 个位是 2 → 丢进 2 号桶 end note : 统计每个桶里有多少个数; : 计算每个桶的"截止位置"; note right: 决定每个数应该放到临时数组的哪个位置 : 从右往左遍历,按桶位置放入临时数组; note right: 从右往左遍历是为了保证"稳定" : 把临时数组复制回原数组; : exp ×= 10(升到下一位); endwhile (否) stop @enduml

按位排序示例

@startuml title 举例:[12, 43, 38, 25, 7] 的排序过程 skinparam rectangle { BackgroundColor #FEFEFE BorderColor #333333 } rectangle "原始: [12, 43, 38, 25, 7]" as s0 rectangle "先按个位排队: [12, 43, 25, 7, 38]" as s1 rectangle "再按十位排队: [7, 12, 25, 38, 43]" as s2 s0 --> s1 : 第一轮:看个位 s1 --> s2 : 第二轮:看十位 note right of s1 按个位排好后: 个位 2→3→5→7→8 (个位有序了) end note note right of s2 按十位排好后: 十位 0→1→2→3→4 (十位也有序了) 最终整体有序 end note @enduml

通俗解释

不比较大小,而是按"位"来排队。先看个位,把个位是 0 的放一起、是 1 的放一起……个位排好后,再看十位排……直到最高位。就像整理一沓试卷,先按科目分,再按班级分,最后自然就整理好了。

指标

指标
时间复杂度 O(n·k) —— n 是数字个数,k 是最大位数
空间复杂度 O(n + k)
稳定性 稳定

k 为最大数字的位数

代码

VOID radix_sort_proc(INT32 *nums, INT32 len, INT32 exp)
{
    INT32 *tempNums = (INT32 *)malloc(sizeof(INT32) * len);
    INT32 cnt[10] = {0};

    for (INT32 i = 0; i < len; i++)
        cnt[(nums[i] / exp) % 10]++;

    for (INT32 i = 1; i < 10; i++)
        cnt[i] += cnt[i - 1];

    for (INT32 i = len - 1; i >= 0; i--) {
        INT32 digit = (nums[i] / exp) % 10;
        tempNums[cnt[digit] - 1] = nums[i];
        cnt[digit]--;
    }

    memcpy(nums, tempNums, sizeof(INT32) * len);
    free(tempNums);
}

VOID radix_sort(INT32 *nums, INT32 len)
{
    if (len <= 0) return;

    INT32 maxNum = nums[0], minNum = nums[0];
    for (INT32 i = 1; i < len; i++) {
        if (nums[i] > maxNum) maxNum = nums[i];
        if (nums[i] < minNum) minNum = nums[i];
    }

    INT32 offset = (minNum < 0) ? -minNum : 0;
    if (offset > 0) {
        for (INT32 i = 0; i < len; i++) nums[i] += offset;
        maxNum += offset;
    }

    for (INT32 exp = 1; maxNum / exp > 0; exp *= 10)
        radix_sort_proc(nums, len, exp);

    if (offset > 0) {
        for (INT32 i = 0; i < len; i++) nums[i] -= offset;
    }
}

总结对比

一眼看懂选哪个

算法 一句话概括 时间复杂度 空间 稳定
冒泡排序 相邻比较,大的往后"冒" O(n)~O(n²) O(1) Y
插入排序 像打扑克摸牌插牌 O(n)~O(n²) O(1) Y
选择排序 每轮挑最小的放前面 O(n²) O(1) N
快速排序 选基准,小的左大的右 O(n log n) O(log n)~O(n) N
堆排序 搭成堆,取塔顶 O(n log n) O(1) N
归并排序 拆成单个,两两合并 O(n log n) O(n) Y
希尔排序 先跳着排再挨着排 O(n)~O(n²) O(1) N
基数排序 按个十百位排队 O(n·k) O(n+k) Y

分类

  • 比较大小来排序(7种):冒泡、插入、选择、快排、堆排、归并、希尔
  • 不比较大小(1种):基数排序(靠位运算)
  • 在原数组上排(原地):冒泡、插入、选择、快排、堆排、希尔
  • 需要额外空间:归并(需要 O(n) 临时数组)、基数(需要计数数组)
  • 稳定的:冒泡、插入、归并、基数
  • 不稳定的:选择、快排、堆排、希尔

评论