排序

最后修改:

对于序列中的相同元素,如果排序之后它们的相对位置没有发生改变,则称该排序算法为「稳定排序」,反之则为「不稳定排序」

原地排序就是指排序过程中不需要额外的辅助空间,只需要常数级别的额外空间,直接操作原数组进行排序。

选择排序

选择排序是最简单朴素的排序算法,但是时间复杂度较高,且不是稳定排序。其他基础排序算法都是基于选择排序的优化。

思想:从小到大排序,从0开始每次遍历数组,找到最小的,最小的和0交换,然后找第二小的,依次排完。

C++实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>

void selectSort(std::vector<int>& nums) {
    int n = nums.size();
    // sortedIndex 是一个分割线
    // 索引 < sortedIndex 的元素都是已排序的
    // 索引 >= sortedIndex 的元素都是未排序的
    // 初始化为 0,表示整个数组都是未排序的
    int sortedIndex = 0;
    while (sortedIndex < n) {
        // 找到未排序部分 [sortedIndex, n) 中的最小值
        int minIndex = sortedIndex;
        for (int i = sortedIndex + 1; i < n; i++) {
            if (nums[i] < nums[minIndex]) {
                minIndex = i;
            }
        }
        // 交换最小值和 sortedIndex 处的元素
        int tmp = nums[sortedIndex];
        nums[sortedIndex] = nums[minIndex];
        nums[minIndex] = tmp;

        // sortedIndex 后移一位
        sortedIndex++;
    }
}

冒泡排序

冒泡算法是对选择排序的一种优化,通过交换 nums[sortedIndex] 右侧的逆序对完成排序,是一种稳定排序算法。

选择排序的几个待优化的问题:

1、选择排序算法是个不稳定排序算法,因为每次都要交换最小元素和当前元素的位置,这样可能会改变相同元素的相对位置。

2、选择排序的时间复杂度和初始数据的有序度完全没有关系,即便输入的是一个已经有序的数组,选择排序的时间复杂度依然是 O(n^2)。

3、选择排序的时间复杂度是 O(n^2),具体的操作次数大概是 n^2/2 次,常规的优化思路无法降低时间复杂度。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>

// 进一步优化,数组有序时提前终止算法
void bubbleSort(std::vector<int>& nums) {
    int n = nums.size();
    int sortedIndex = 0;
    while (sortedIndex < n) {
        // 加一个布尔变量,记录是否进行过交换操作
        bool swapped = false;
        for (int i = n - 1; i > sortedIndex; i--) {
            if (nums[i] < nums[i - 1]) {
                // swap(nums[i], nums[i - 1])
                int tmp = nums[i];
                nums[i] = nums[i - 1];
                nums[i - 1] = tmp;
                swapped = true;
            }
        }
        // 如果一次交换操作都没有进行,说明数组已经有序,可以提前终止算法
        if (!swapped) {
            break;
        }
        sortedIndex++;
    }
}

插入排序

插入排序是基于选择排序的一种优化,将 nums[sortedIndex] 插入到左侧的有序数组中。对于有序度较高的数组,插入排序的效率比较高。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>
// 对选择排序进一步优化,向左侧有序数组中插入元素
// 这个算法有另一个名字,叫做插入排序
void insertSort(std::vector<int>& nums) {
    int n = nums.size();
    // 维护 [0, sortedIndex) 是有序数组
    int sortedIndex = 0;
    while (sortedIndex < n) {
        // 将 nums[sortedIndex] 插入到有序数组 [0, sortedIndex) 中
        for (int i = sortedIndex; i > 0; i--) {
            if (nums[i] < nums[i - 1]) {
                // swap(nums[i], nums[i - 1])
                int tmp = nums[i];
                nums[i] = nums[i - 1];
                nums[i - 1] = tmp;
            } else {
                break;
            }
        }
        sortedIndex++;
    }
}

插入排序的空间复杂度是 O(1),是原地排序算法。时间复杂度是 O(n^2)。具体的操作次数和选择排序类似,是一个等差数列求和,大约是 n^2 /2 次。

插入排序是一种稳定排序,因为只有在 nums[i] < nums[i - 1] 的情况下才会交换元素,所以相同元素的相对位置不会发生改变。

插入排序的综合性能应该要高于冒泡排序。

冒泡排序的操作数大约是 n^2/2,而插入排序的操作数会小于 n^2/2。

希尔排序

希尔排序是基于插入排序的简单改进,通过预处理增加数组的局部有序性,突破了插入排序的O(N^2) 时间复杂度。

h 有序数组(h-sorted array)

定义

一个数组是 h 有序的(h-sorted),是指:

对于任意索引 i,只要 i + h < n,就有 arr[i] <= arr[i + h]

换句话说,将数组按步长 h 分成若干子序列(每个子序列由间隔为 h 的元素组成),每个子序列内部都是有序的

示例

设数组为:

1
[9, 1, 8, 2, 7, 3, 6, 4, 5]

h = 3 时,分成 3 个子序列:

  • 子序列 0: 索引 0, 3, 6 → [9, 2, 6]
  • 子序列 1: 索引 1, 4, 7 → [1, 7, 4]
  • 子序列 2: 索引 2, 5, 8 → [8, 3, 5]

如果每个子序列都各自有序(如 [2, 6, 9], [1, 4, 7], [3, 5, 8]),则整个数组是 3-有序 的。

h 越小,数组越“接近完全有序”;当 h = 1 时,就是普通插入排序,此时若有序,则整体有序。

希尔排序(Shell Sort)

核心思想

希尔排序是 插入排序的改进版,通过以下两步提升效率:

  1. 预处理阶段
    选择一个递减的 h 序列(如 ..., 127, 31, 7, 3, 1),对数组依次进行 h-有序化。
  2. 最终排序
    h = 1 时,执行一次插入排序 —— 此时数组已“基本有序”,插入排序效率接近 O(N)。

💡 关键:大的 h 值让元素快速“跳跃”到大致正确位置,避免小步挪动。

h 序列的选择

常见序列:

  • Shell 原始序列:⌊N/2⌋, ⌊N/4⌋, ..., 1(最简单,但性能一般)
  • Knuth 序列:(3^k - 1)/2,如 1, 4, 13, 40, ...(推荐)
  • Sedgewick 序列:更优,但复杂

由于元素在不同 h 子序列间跳跃,无法保证相等元素的相对顺序,故希尔排序是不稳定排序。

时空复杂度分析

项目复杂度说明
时间复杂度依赖 h 序列
• 最坏:O(N²)(如用 Shell 原始序列)
• 平均:O(N^{1.3}) ~ O(N^{1.5})
• 最好:O(N log N)(某些序列)
Knuth 序列平均约 O(N^{1.25})
空间复杂度O(1)原地排序
比较次数远少于 O(N²)预处理大幅减少逆序对

C++ 实现(使用 Knuth 序列)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <vector>

void shellSort(std::vector<int>& arr) {
    int n = arr.size();
    
    // 1. 生成 Knuth 序列: 1, 4, 13, 40, ...
    int h = 1;
    while (h < n / 3) {
        h = 3 * h + 1;  // h = (3^k - 1) / 2
    }

    // 2. 从大到小使用 h 序列
    while (h >= 1) {
        // 对每个子序列执行插入排序
        for (int i = h; i < n; ++i) {
            int key = arr[i];
            int j = i;
            // 在子序列中向前比较(步长为 h)
            while (j >= h && arr[j - h] > key) {
                arr[j] = arr[j - h];
                j -= h;
            }
            arr[j] = key;
        }
        h /= 3;  // 下一个更小的 h
    }
}

算法说明

  • 内层 while带步长 h 的插入排序
  • 外层 h 从大到小,逐步精细化排序
  • 时间复杂度取决于 h 序列,Knuth 序列实践效果良好

总结

特性说明
h 有序数组每个间隔为 h 的子序列内部有序
希尔排序多轮 h-插入排序,h 递减至 1
稳定性❌ 不稳定
时间复杂度O(N^{1.3}) ~ O(N²),依赖 h 序列
空间复杂度O(1)
适用场景中等规模数据、内存受限环境

✅ 希尔排序虽不如快排/归并高效,但因其 简单、原地、无递归、缓存友好,仍在嵌入式系统或教学中使用。

快速排序

快速排序的核心思想是 “分治 + 原地分区”,其关键在于 partition(分区)操作

类似于二叉树的前置遍历,快速排序的 partition 操作可以看作是 “选择一个基准值(pivot),把小于它的放左边,大于它的放右边,然后递归处理左右两部分”

分区(Partition)过程

  1. 选择一个 pivot(通常选最后一个元素);
  2. 维护一个指针 i,表示“小于 pivot 的区域”的右边界;
  3. 遍历数组,若当前元素 < pivot,则与 i 位置交换,并 i++
  4. 最后将 pivot 与 i 位置交换,此时 pivot 已在最终排序位置;
  5. 返回 pivot 的索引,用于划分左右子数组。

代码框架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <vector>
#include <algorithm> // for std::swap

// 分区函数:将 arr[lo..hi] 分区,返回 pivot 的最终索引
int partition(std::vector<int>& arr, int lo, int hi) {
    int pivot = arr[hi];          // 选择最后一个元素作为 pivot
    int i = lo;                   // i 是“小于 pivot 区域”的右边界(初始为 lo)

    for (int j = lo; j < hi; ++j) {
        if (arr[j] < pivot) {
            std::swap(arr[i], arr[j]);
            i++;
        }
    }
    std::swap(arr[i], arr[hi]);   // 将 pivot 放到正确位置
    return i;
}

// 快速排序主函数
void quickSort(std::vector<int>& arr, int lo, int hi) {
    if (lo >= hi) return;         // 基线条件:子数组长度 <= 1

    int p = partition(arr, lo, hi); // 前序位置:确定 pivot 位置
    quickSort(arr, lo, p - 1);    // 递归左子数组
    quickSort(arr, p + 1, hi);    // 递归右子数组
}

// 便捷调用接口
void quickSort(std::vector<int>& arr) {
    if (arr.empty()) return;
    quickSort(arr, 0, arr.size() - 1);
}

时间复杂度

情况时间复杂度说明
最好情况O(N log N)每次 pivot 都将数组平分(如随机化 pivot)
平均情况O(N log N)实践中非常高效
最坏情况O(N²)数组已有序,且每次选首/尾为 pivot(退化为冒泡)

优化建议:使用 随机化 pivot三数取中 可避免最坏情况。

空间复杂度

  • O(log N)(平均情况)
    来源于递归调用栈的深度(平衡时深度为 log N)。

  • O(N)(最坏情况)
    当递归退化为链式调用(如已排序数组),栈深度为 N。

💡 可通过 尾递归优化迭代+栈 将最坏空间降至 O(log N),但 C++ 一般不自动优化尾递归。

稳定性

❌ 快速排序是 不稳定 的!

原因:

partition 过程中,相等元素可能因交换而改变相对顺序

总结

项目内容
核心思路分治 + 原地分区,类比二叉树前序遍历
代码框架partition + 递归左右子数组(C++ 实现见上)
时间复杂度平均 O(N log N),最坏 O(N²)
空间复杂度平均 O(log N),最坏 O(N)
稳定性❌ 不稳定

✅ 快速排序是 实践中最快的通用排序算法之一,广泛用于 std::sort(Introsort 混合策略)等底层实现。

归并排序

归并排序的核心思想是 “分治 + 合并”,其关键在于 “先递归排序左右子数组,再合并两个有序数组”

类似于二叉树后续遍历,“把数组从中间劈开,递归地让左右两边有序,然后像拉链一样合并成一个整体有序数组。”

合并(Merge)过程

  1. 创建临时数组 temp 存储 arr[lo..hi]
  2. 用双指针 i(左半)、j(右半)遍历两个有序子数组;
  3. 每次取较小者放入原数组 arr,直到一方耗尽;
  4. 将剩余元素全部拷回。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <vector>

// 合并两个有序子数组 arr[lo..mid] 和 arr[mid+1..hi]
void merge(std::vector<int>& arr, int lo, int mid, int hi) {
    // 1. 复制到临时数组
    std::vector<int> temp(arr.begin() + lo, arr.begin() + hi + 1);
    
    // 2. 双指针合并
    int i = 0;                 // temp 左半起始索引 (对应 arr[lo])
    int j = mid - lo + 1;      // temp 右半起始索引 (对应 arr[mid+1])
    int k = lo;                // 原数组写入位置

    while (i <= mid - lo && j <= hi - lo) {
        if (temp[i] <= temp[j]) {
            arr[k++] = temp[i++];
        } else {
            arr[k++] = temp[j++];
        }
    }

    // 3. 拷回剩余元素(左半或右半)
    while (i <= mid - lo) arr[k++] = temp[i++];
    while (j <= hi - lo) arr[k++] = temp[j++];
}

// 归并排序主函数
void mergeSort(std::vector<int>& arr, int lo, int hi) {
    if (lo >= hi) return;               // 基线条件:子数组长度 <= 1

    int mid = lo + (hi - lo) / 2;       // 防止溢出
    mergeSort(arr, lo, mid);            // 递归左半
    mergeSort(arr, mid + 1, hi);        // 递归右半
    merge(arr, lo, mid, hi);            // 后序位置:合并
}

// 便捷调用接口
void mergeSort(std::vector<int>& arr) {
    if (arr.empty()) return;
    mergeSort(arr, 0, arr.size() - 1);
}

排序稳定性

✅ 归并排序是 稳定 的!

原因:

merge 过程中,当左右元素相等时,优先取左边的元素if (temp[i] <= temp[j])),从而保持了原始相对顺序。

📌 稳定性定义:相等元素在排序后相对位置不变。
归并排序是 少数同时具备 O(N log N) 时间复杂度和稳定性的排序算法

时间复杂度

情况时间复杂度说明
最好 / 平均 / 最坏O(N log N)无论输入如何,always 分成两半 + 线性合并

✅ 归并排序的时间复杂度非常稳定,无退化情况。

是否是原地排序?

❌ 归并排序 不是原地排序

  • 空间复杂度:O(N)
    主要消耗在 merge 函数中的临时数组 temp(需额外 O(N) 空间)。
  • 虽然递归栈深度为 O(log N),但主导项是 O(N) 的辅助空间。

💡 对比:快速排序是原地排序(O(1) 辅助空间,不计递归栈),但不稳定;
归并排序稳定且时间稳定,但需额外空间。

总结

项目内容
核心思路分治 + 后序合并,类比二叉树后序遍历
排序稳定性✅ 稳定(相等元素保持原序)
时间复杂度O(N log N)(所有情况)
是否原地排序❌ 否,需 O(N) 额外空间
适用场景需要稳定排序、外部排序(如大文件排序)

✅ 归并排序是 理论最优的稳定排序算法,也是 Java Collections.sort()、Python sorted() 等底层实现的基础(Timsort 是其变种)。

堆排序

思路

最直观的做法:把所有元素插入一个最小堆(或最大堆),然后依次弹出,即可得到有序序列。

  • 使用优先级队列(底层是二叉堆)作为辅助数据结构;
  • 插入 N 个元素:O(N log N);
  • 弹出 N 个元素:O(N log N);
  • 缺点:需要 O(N) 额外空间,不符合“原地排序”要求。

实现(使用 std::priority_queue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <vector>
#include <queue>

// 升序排序:使用最大堆,从后往前填
void heapSortSimple(std::vector<int>& nums) {
    std::priority_queue<int> maxHeap; // 默认是最大堆

    // 1. 所有元素入堆
    for (int x : nums) {
        maxHeap.push(x);
    }

    // 2. 从堆顶取出最大值,倒序放入数组
    for (int i = nums.size() - 1; i >= 0; --i) {
        nums[i] = maxHeap.top();
        maxHeap.pop();
    }
}

✅ 正确性高,代码简洁
❌ 空间复杂度 O(N),非原地排序

优化(原地堆排序)

核心思想

不创建额外堆,而是直接将输入数组视为完全二叉树,通过 sink 操作原地构建最大堆,并完成排序。

两个关键步骤:

  1. 原地建堆(Heapify)

    • 从最后一个非叶子节点开始(索引为 n/2 - 1),自底向上执行 sink
    • 时间复杂度仅为 O(N)(不是 O(N log N)!)。
  2. 原地排序(Sort)

    • 将堆顶(最大值)与堆尾交换;
    • 缩小堆范围(排除已排序部分);
    • 对新堆顶执行 sink,恢复堆性质;
    • 重复直到堆只剩一个元素。

💡 这就是标准的 原地堆排序(In-place Heap Sort),空间复杂度 O(1)。


辅助函数:sink(下沉操作)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 维护最大堆性质:将索引 k 的元素下沉到合适位置
// n 表示当前堆的有效大小([0, n) 范围内)
void sink(std::vector<int>& arr, int k, int n) {
    while (2 * k + 1 < n) {       // 存在左子节点
        int j = 2 * k + 1;        // 左子节点索引
        if (j + 1 < n && arr[j] < arr[j + 1]) {
            j++;                  // 选择较大的子节点(右子节点)
        }
        if (arr[k] >= arr[j]) break; // 已满足堆性质
        std::swap(arr[k], arr[j]);
        k = j;                    // 继续下沉
    }
}

优化版堆排序(原地,O(1) 空间)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <vector>
#include <algorithm> // for std::swap

void heapSort(std::vector<int>& nums) {
    int n = nums.size();

    // 1. 原地建堆:从最后一个非叶子节点开始 sink
    for (int i = n / 2 - 1; i >= 0; --i) {
        sink(nums, i, n);
    }

    // 2. 排序:每次将堆顶移到末尾
    for (int i = n - 1; i > 0; --i) {
        std::swap(nums[0], nums[i]); // 最大值放到已排序区
        sink(nums, 0, i);           // 对 [0, i) 重建堆
    }
}

✅ 时间复杂度:O(N log N)
✅ 空间复杂度:O(1)(无递归,纯迭代)
✅ 原地排序,符合堆排序标准定义


堆排序的稳定性

堆排序无法保证相等元素的相对顺序,故不稳定。

完整可运行示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <vector>
#include <algorithm>

void sink(std::vector<int>& arr, int k, int n) {
    while (2 * k + 1 < n) {
        int j = 2 * k + 1;
        if (j + 1 < n && arr[j] < arr[j + 1]) j++;
        if (arr[k] >= arr[j]) break;
        std::swap(arr[k], arr[j]);
        k = j;
    }
}

void heapSort(std::vector<int>& nums) {
    int n = nums.size();
    // 建堆
    for (int i = n / 2 - 1; i >= 0; --i)
        sink(nums, i, n);
    // 排序
    for (int i = n - 1; i > 0; --i) {
        std::swap(nums[0], nums[i]);
        sink(nums, 0, i);
    }
}

总结

项目内容
简单实现使用 priority_queue,逻辑清晰但需 O(N) 空间
优化实现原地建堆 + sink,O(1) 空间,标准堆排序
时间复杂度建堆 O(N) + 排序 O(N log N) → 总 O(N log N)
空间复杂度O(1)(迭代实现,无递归栈)
稳定性不稳定

✅ 堆排序是少数能在 最坏情况 O(N log N) 下运行的原地排序算法,适用于内存受限或对最坏性能有要求的场景。

计数排序

通用的计数排序(支持负数)

核心思想

计数排序适用于 整数范围有限 的场景。对于包含负数的数组,不能直接用元素值作 count 数组下标(因为下标不能为负)。

解决方案
引入 偏移量(offset),将所有元素映射到非负区间。

设:

  • min_val = min(nums)
  • max_val = max(nums)
  • range = max_val - min_val + 1

则任意元素 x 对应的计数数组索引为:

1
index = x - min_val  // 保证 index >= 0

处理负数和自定义类型

1. 负数处理 ✅

如上所述,通过 offset = -min_val 将负数“平移”到非负域。

示例:nums = [-2, -1, 0, 1, 2]
min = -2, offset = 2 → 映射为 [0, 1, 2, 3, 4]

2. 自定义类型(需以整数为键)

计数排序要求:

  • 元素可转换为整数键(key)
  • 键的范围不能太大

因此:

  • 不能直接排序任意结构体/对象
  • 但若对象有整数 ID 且范围小,可基于 ID 排序(需额外存储原始对象)。

⚠️ 计数排序仅适用于整数或可离散化为小范围整数的类型

累加 count 数组(实现稳定排序)

关键技巧:前缀和(Prefix Sum)

为了使计数排序 稳定(相同元素保持原相对顺序),需对 count 数组做前缀和累加,使其表示“小于等于该值的元素个数”。

步骤:

  1. 统计频次:count[x - min]++
  2. 累加前缀和:count[i] += count[i-1](从左到右)
  3. 从后往前遍历原数组,将元素放入结果数组的 count[index] - 1 位置,并 count[index]--

💡 从后往前遍历 + 前缀和 → 保证稳定性!

实现(通用、稳定、支持负数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
std::vector<int> countingSort(const std::vector<int>& nums) {
    if (nums.empty()) return {};

    int min_val = *std::min_element(nums.begin(), nums.end());
    int max_val = *std::max_element(nums.begin(), nums.end());
    int range = max_val - min_val + 1;

    std::vector<int> count(range, 0);
    for (int x : nums) {
        count[x - min_val]++;
    }

    for (int i = 1; i < range; ++i) {
        count[i] += count[i - 1];
    }

    std::vector<int> result(nums.size());
    for (int i = nums.size() - 1; i >= 0; --i) {
        int idx = nums[i] - min_val;
        result[count[idx] - 1] = nums[i];
        count[idx]--;
    }

    return result;
}

✅ 支持负数
✅ 稳定排序
✅ 原地?否(返回新数组,符合函数式风格;也可改写为 in-place)


时空复杂度分析

项目复杂度说明
时间复杂度O(n + k)n = nums.size(), k = max - min + 1(值域大小)
空间复杂度O(k + n)count 数组 O(k),结果数组 O(n)
是否原地需要额外 O(n) 输出空间(若要求稳定)

⚠️ 当 k >> n(如 max=1e9, min=0),计数排序效率极低,甚至内存溢出。

✅ 计数排序是典型的 非比较排序

  • 不通过元素间比较 决定顺序;
  • 而是通过 统计频次 + 数学映射 直接确定位置;
  • 因此可以突破 比较排序的 O(n log n) 下界,达到 O(n + k)

📌 其他非比较排序:桶排序、基数排序。

总结

特性说明
通用性支持负数(通过偏移量),不支持大范围或浮点数
稳定性✅ 通过前缀和 + 逆序填充实现
时间复杂度O(n + k),k 为值域大小
空间复杂度O(n + k)
排序类型非比较排序
适用场景整数、值域小(如成绩 0100、颜色 02)

桶排序

桶排序的关键点

桶排序的核心思想是 “分而治之 + 归并”,其关键在于以下三点:

  1. 将待排序数组划分为若干个“桶”(子区间)

    • 每个桶对应一个值域范围(如 [0,1), [1,2) 等);
    • 元素通过映射函数(如 floor(x * k))分配到对应桶中。
  2. 对每个桶内部单独排序

    • 可使用任意排序算法(常用插入排序,因桶内数据少且近似有序);
    • 也可递归使用桶排序(适用于浮点数或大范围数据)。
  3. 按顺序合并所有桶

    • 因桶的值域互不重叠且有序,直接拼接即可得到全局有序序列;
    • 合并时间复杂度为 O(n)

💡 桶排序可看作 归并排序(分治) + 计数排序(值域划分) 的结合体。

映射函数(将元素分配到 k 个桶中)

对于 k 个桶,每个桶覆盖区间长度为 1.0 / k
元素 x ∈ [0, 1) 映射到桶索引:

1
2
int bucketIndex = static_cast<int>(x * k);
// 注意:当 x == 1.0 时需特殊处理(但通常 x < 1)

✅ 保证:bucket[i] 中所有元素 ∈ [i/k, (i+1)/k)

使用插入排序的桶排序(推荐)

C++ 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#ifndef BUCKET_SORT_H
#define BUCKET_SORT_H

#include <vector>
#include <algorithm>
#include <cmath>

// 插入排序(用于小数组)
inline void insertionSort(std::vector<double>& arr) {
    for (size_t i = 1; i < arr.size(); ++i) {
        double key = arr[i];
        int j = static_cast<int>(i) - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            --j;
        }
        arr[j + 1] = key;
    }
}

// 桶排序(支持任意范围浮点数)
inline void bucketSort(std::vector<double>& nums) {
    if (nums.empty()) return;

    // 1. 找到最大最小值
    double min_val = nums[0];
    double max_val = nums[0];
    for (double x : nums) {
        if (x < min_val) min_val = x;
        if (x > max_val) max_val = x;
    }

    if (min_val == max_val) return;

    int n = nums.size();
    std::vector<std::vector<double>> buckets(n); // 创建 n 个桶

    // 2. 分配元素到桶
    double range = max_val - min_val;
    for (double x : nums) {
        // 映射到 [0, n-1]
        int idx = static_cast<int>((x - min_val) / range * n);
        if (idx >= n) idx = n - 1;
        if (idx < 0) idx = 0;
        buckets[idx].push_back(x);
    }

    // 3. 对每个桶排序
    for (auto& bucket : buckets) {
        insertionSort(bucket);
    }

    // 4. 合并桶
    int index = 0;
    for (const auto& bucket : buckets) {
        for (double x : bucket) {
            nums[index++] = x;
        }
    }
}

#endif // BUCKET_SORT_H

✅ 时间复杂度期望 O(n),空间 O(n)
✅ 稳定(若插入排序稳定)

使用递归的桶排序(适用于更广范围)

当数据范围大或分布不均时,可对非空桶递归调用桶排序

C++ 实现(递归版)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#ifndef RECURSIVE_BUCKET_SORT_H
#define RECURSIVE_BUCKET_SORT_H

#include <vector>
#include <algorithm>
#include "bucket_sort.h" // Need insertionSort

inline void recursiveBucketSort(std::vector<double>& nums, int depth = 0) {
    const int MAX_DEPTH = 5; // 防止无限递归
    if (nums.size() <= 1 || depth >= MAX_DEPTH) {
        insertionSort(nums);
        return;
    }

    // 1. 找到最大最小值
    double min_val = nums[0];
    double max_val = nums[0];
    for (double x : nums) {
        if (x < min_val) min_val = x;
        if (x > max_val) max_val = x;
    }

    if (min_val == max_val) return;

    int n = nums.size();
    std::vector<std::vector<double>> buckets(n);

    double range = max_val - min_val;
    for (double x : nums) {
        int idx = static_cast<int>((x - min_val) / range * n);
        if (idx >= n) idx = n - 1;
        if (idx < 0) idx = 0;
        buckets[idx].push_back(x);
    }

    nums.clear();
    for (auto& bucket : buckets) {
        if (!bucket.empty()) {
            recursiveBucketSort(bucket, depth + 1);
            nums.insert(nums.end(), bucket.begin(), bucket.end());
        }
    }
}

#endif // RECURSIVE_BUCKET_SORT_H

⚠️ 递归版适用于数据分布不均匀的场景,但需控制深度防止栈溢出。

排序的稳定性

✅ 桶排序可以是 稳定的

  • 前提:桶内排序算法是稳定的(如插入排序);
  • 原因
    • 相同值的元素会被分到同一个桶;
    • 桶内稳定排序保持其相对顺序;
    • 合并时按桶顺序拼接,不改变跨桶相同值的顺序(但跨桶不会有相同值,因值域不重叠)。

📌 结论:若桶内排序稳定,则整体桶排序稳定。

时空复杂度分析

项目复杂度说明
时间复杂度(平均)O(n + k)k 为桶数,通常取 k = n;桶内排序总代价期望 O(n)
时间复杂度(最坏)O(n²)所有元素落入同一桶,退化为插入排序
空间复杂度O(n + k)存储 k 个桶,总元素数 n
是否原地❌ 否需要额外 O(n) 空间

✅ 当输入 均匀分布 时,桶排序性能接近线性,优于 O(n log n) 的比较排序。

适用场景 vs 限制

优点缺点
✅ 平均时间复杂度 O(n)❌ 依赖输入分布(需近似均匀)
✅ 稳定(配合稳定子排序)❌ 最坏情况退化为 O(n²)
✅ 适合浮点数、小范围整数❌ 不适合值域过大或未知的数据

💡 经典应用:对 [0,1) 浮点数排序、成绩分布统计等。

总结

内容说明
关键点分桶 → 桶内排序 → 合并
分配到 k 桶idx = floor(x * k)(x ∈ [0,1))
桶内排序常用插入排序(小数据高效)
递归桶排序应对非均匀分布
稳定性✅ 可稳定(依赖子排序)
时间复杂度平均 O(n),最坏 O(n²)
空间复杂度O(n)
排序类型非比较排序

✅ 桶排序是 理论优美、实践受限 的算法——在合适场景下(如均匀分布浮点数),它是最快的排序之一。

基数排序

基数排序(Radix Sort)是一种 非比较排序算法,其核心思想是:

将整数按“位”拆分(如个位、十位、百位…),从低位到高位(或高位到低位)依次对每一位进行稳定排序,最终得到全局有序序列。

  • 它是 计数排序的扩展:对每一位使用计数排序;
  • 要求待排序数据为 整数(或可映射为整数,如字符串);
  • 关键依赖:每一位的排序必须是稳定的

✅ 示例(LSD 方式):

1
2
3
4
初始: [329, 457, 839, 439, 720, 355, 350]
按个位排序 → [720, 350, 355, 457, 329, 839, 439]
按十位排序 → [720, 329, 839, 439, 350, 355, 457]
按百位排序 → [329, 350, 355, 439, 457, 720, 839] ✅

为什么对每一位必须使用稳定排序?

假设对十位排序时使用不稳定排序

  • 在个位排序后,350355 前(因 0 < 5);
  • 若十位排序时两者十位相同(都是 5),但不稳定排序交换了它们的位置;
  • 最终结果中 355 会排在 350 前 → 破坏整体顺序

结论
只有稳定排序能保证 “高位相同时,低位已排好的顺序不被破坏”

使用什么稳定排序比较好?

推荐:计数排序(Counting Sort)

  • 时间复杂度 O(n + k),k = 10(0~9 数字)→ 实际为 O(n);
  • 空间开销小(仅需长度为 10 的 count 数组);
  • 天然支持数字位排序。

其他稳定排序(如插入排序)也可用,但效率远低于计数排序。

位数不同怎么办

解决方案:统一按最大位数处理

  • 找出数组中最大值 maxVal,计算其位数 d = floor(log10(maxVal)) + 1
  • 位数不足的数视为高位补 0(如 5 视为 005);
  • 计数排序天然支持(取某位数字时,高位不存在即为 0)。

💡 无需显式补零,只需在取第 k 位时做数学运算即可。

有负数怎么办?

解决方案:偏移法(Offset)

  1. 找出最小值 minVal
  2. 将所有元素加上 offset = -minVal,转为非负整数;
  3. 执行基数排序;
  4. 排序后减去 offset,恢复原始值。

⚠️ 注意:此方法仅适用于整数,且需确保加法不溢出。

必须从低位到高位吗?(LSD vs MSD)

类型全称顺序特点
LSDLeast Significant Digit从低位 → 高位✅ 实现简单,常用;要求排序算法稳定
MSDMost Significant Digit从高位 → 低位适合字符串排序;递归分治,实现复杂

整数排序通常用 LSD(因为位数固定,LSD 更高效);
字符串/变长数据用 MSD(可提前终止比较)。

MSD 算法逻辑(简要)

  1. 从最高位开始;
  2. 按当前位将数组分到 10 个桶(0~9);
  3. 对每个非空桶,递归处理下一位
  4. 合并所有桶(自然有序)。

⚠️ MSD 需处理前导零、位数不一致等问题,且递归深度大,实践中较少用于整数排序。

LSD 基数排序 C++ 实现(支持负数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <vector>
#include <algorithm>
#include <climits>

// 对第 exp 位(1, 10, 100...)进行计数排序
void countingSortForRadix(std::vector<int>& arr, int exp) {
    int n = arr.size();
    std::vector<int> output(n);
    std::vector<int> count(10, 0); // 0~9

    // 统计当前位频次
    for (int i = 0; i < n; ++i) {
        int digit = (arr[i] / exp) % 10;
        count[digit]++;
    }

    // 前缀和(变为“<= digit”的个数)
    for (int i = 1; i < 10; ++i) {
        count[i] += count[i - 1];
    }

    // 从后往前填充(保证稳定)
    for (int i = n - 1; i >= 0; --i) {
        int digit = (arr[i] / exp) % 10;
        output[count[digit] - 1] = arr[i];
        count[digit]--;
    }

    // 写回原数组
    for (int i = 0; i < n; ++i) {
        arr[i] = output[i];
    }
}

// 基数排序(支持负数)
void radixSort(std::vector<int>& nums) {
    if (nums.empty()) return;

    // 1. 处理负数:偏移至非负
    int min_val = *std::min_element(nums.begin(), nums.end());
    int offset = (min_val < 0) ? -min_val : 0;
    if (offset > 0) {
        for (int& x : nums) x += offset;
    }

    // 2. 找最大值以确定位数
    int max_val = *std::max_element(nums.begin(), nums.end());

    // 3. LSD: 从个位开始
    for (int exp = 1; max_val / exp > 0; exp *= 10) {
        countingSortForRadix(nums, exp);
    }

    // 4. 恢复负数
    if (offset > 0) {
        for (int& x : nums) x -= offset;
    }
}

时空复杂度及稳定性

项目复杂度/性质说明
时间复杂度O(d × (n + k))d = 位数,k = 10(数字基数),通常简化为 O(d·n)
空间复杂度O(n + k)计数排序需要 O(k) 辅助空间,输出数组 O(n)
稳定性稳定因每一位都使用稳定排序(计数排序)
是否比较排序非比较排序不依赖元素比较,突破 O(n log n) 下界

💡 当 d 为常数(如 32 位整数最多 10 位十进制数),时间复杂度为 O(n)

总结

问题答案
原理按位拆分,逐位稳定排序
为何要稳定保持低位已排好的顺序
推荐子排序计数排序(高效、稳定)
位数不同按最大位数处理,高位补 0
负数处理偏移法转为非负
LSD vs MSD整数用 LSD,字符串用 MSD
稳定性✅ 稳定
时间复杂度O(d·n)
空间复杂度O(n)

✅ 基数排序是 线性时间排序 的代表,在整数范围可控时性能极佳,广泛用于数据库、嵌入式系统等场景。

Reference

https://labuladong.online/algo/data-structure-basic/sort-basic/

山重水复疑无路,柳暗花明又一村
使用 Hugo 构建
主题 StackJimmy 设计