排序算法分类详解

按时间复杂度、比较方式、稳定性、空间复杂度多维度分类

Posted by PyroHao on 2025-10-01
Estimated Reading Time 15 Minutes
Words 3.7k In Total
Viewed Times

排序算法可以从多个维度进行分类,理解这些分类有助于在实际应用中选择合适的算法。本文将从时间复杂度、比较方式、稳定性、空间复杂度等多个角度对排序算法进行系统分类和介绍。


一、按时间复杂度分类

1.1 平方阶排序 O(n²)

这类排序算法实现简单,适合小规模数据,但在大数据量时性能较差。

算法 最优 平均 最坏 特点
冒泡排序 O(n) O(n²) O(n²) 最简单,可提前退出
选择排序 O(n²) O(n²) O(n²) 交换次数最少
插入排序 O(n) O(n²) O(n²) 对小数据最快

适用场景:

  • 数据量小于 50:优先使用插入排序
  • 数据量 50-1000:考虑使用希尔排序或更高级算法
  • 教学演示:使用冒泡排序(最直观)

代码示例对比:

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
// 三种 O(n²) 排序的核心操作对比

// 冒泡排序:相邻元素比较交换
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1); // 频繁交换
}
}
}

// 选择排序:选择最值放到已排序区
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j; // 只记录索引,不交换
}
}
swap(arr, i, minIdx); // 每轮只交换一次
}

// 插入排序:将元素插入已排序区
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 移动元素
j--;
}
arr[j + 1] = key; // 插入
}

1.2 线性对数阶排序 O(n log n)

这类排序算法是实际应用中最常用的,适合大规模数据。

算法 最优 平均 最坏 空间 稳定性
快速排序 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) 稳定
堆排序 O(n log n) O(n log n) O(n log n) O(1) 不稳定
希尔排序 O(n log n) O(n^1.3) O(n²) O(1) 不稳定

选择建议:

1
2
3
4
5
6
7
8
9
10
数据规模较大时:
├── 需要稳定性?
│ ├── 是 → 归并排序
│ └── 否 → 继续判断
├── 内存受限?
│ ├── 是 → 堆排序
│ └── 否 → 快速排序
└── 最坏情况敏感?
├── 是 → 堆排序 / 归并排序
└── 否 → 快速排序(平均最快)

性能对比(排序 100 万个随机整数):

算法 耗时 内存占用
快速排序 ~100ms ~log n
归并排序 ~120ms O(n)
堆排序 ~150ms O(1)
希尔排序 ~200ms O(1)

1.3 线性阶排序 O(n)

这类排序算法突破了比较排序的下界,但需要特定数据条件。

算法 时间复杂度 空间复杂度 数据要求 稳定性
计数排序 O(n + k) O(n + k) 整数,范围小 稳定
桶排序 O(n + k) O(n + k) 分布均匀 稳定
基数排序 O(d(n + k)) O(n + k) 固定位数 稳定

k = 数据范围,d = 数字位数

适用条件对比:

1
2
3
4
5
6
7
8
9
10
// 计数排序:适合数据范围小
int[] arr = {4, 2, 2, 8, 3, 3, 1}; // 范围 1-8,适合
int[] arr2 = {1, 1000000, 2}; // 范围大,不适合

// 桶排序:适合数据分布均匀
float[] arr = {0.42f, 0.32f, 0.33f, 0.52f}; // 0-1 均匀分布,适合

// 基数排序:适合固定位数整数
int[] arr = {170, 45, 75, 90, 802}; // 3位数以内,适合
String[] arr = {"cat", "bat", "rat"}; // 等长字符串,适合

二、按比较/非比较分类

2.1 比较排序

定义:通过比较元素之间的大小关系来决定元素的相对位置。

特点

  • 适用范围广:可以排序任何可比较的数据类型
  • 理论下界:平均和最坏情况时间复杂度不低于 O(n log n)
  • 灵活性高:可以实现稳定或不稳定排序

算法列表

  • 冒泡排序、选择排序、插入排序
  • 希尔排序、快速排序、归并排序、堆排序

比较次数分析:

1
2
3
4
5
6
7
8
9
10
决策树模型:
- n 个元素有 n! 种排列
- 决策树高度至少为 log(n!) ≈ n log n
- 因此比较排序至少需要 O(n log n) 次比较

实际比较次数:
- 冒泡排序:~n²/2
- 快速排序:~1.39n log n 次(平均)
- 归并排序:~n log n
- 堆排序:~2n log n

2.2 非比较排序

定义:不通过直接比较元素大小,而是利用数据的特定特征进行排序。

特点

  • 时间复杂度可突破 O(n log n),达到 O(n)
  • 适用范围受限:需要数据满足特定条件
  • 通常需要额外的空间

算法列表

  • 计数排序、桶排序、基数排序

工作原理对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 计数排序:统计每个值的出现次数
值: 1 2 2 3 3 3 4
计数: 0 1 2 3 1 ...

排序: 1, 2, 2, 3, 3, 3, 4

// 桶排序:将数据分到多个桶,桶内再排序
数据: [0.42, 0.32, 0.33, 0.52, 0.37, 0.47]
0: [0.32, 0.33, 0.37] → 排序
1: [0.42, 0.47] → 排序
2: [0.52] → 已有序

合并: [0.32, 0.33, 0.37, 0.42, 0.47, 0.52]

// 基数排序:按位排序(从低位到高位)
原始: [170, 45, 75, 90, 802, 24, 2, 66]
个位: [170, 90, 802, 2, 24, 45, 75, 66]
十位: [802, 2, 24, 45, 66, 170, 75, 90]
百位: [2, 24, 45, 66, 75, 90, 170, 802] ✓

三、按稳定性分类

3.1 稳定排序

定义:相等元素的相对顺序在排序前后保持不变。

稳定排序算法

  • 冒泡排序 ✓
  • 插入排序 ✓
  • 归并排序 ✓
  • 计数排序 ✓
  • 桶排序 ✓
  • 基数排序 ✓

稳定性示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 学生记录,先按班级排序,再按成绩排序
class Student {
String className; // 班级
int score; // 成绩
}

// 原始数据(已按班级排序)
[张三-A班-85, 李四-A班-90, 王五-B班-85, 赵六-B班-90]

// 使用稳定排序按成绩排序后:
[张三-A班-85, 王五-B班-85, 李四-A班-90, 赵六-B班-90]
// ↑ A班在前 ↑ A班在前(稳定性保证)

// 使用不稳定排序可能结果:
[王五-B班-85, 张三-A班-85, 赵六-B班-90, 李四-A班-90]
// ↑ B班在前(相对顺序被打乱)

实现稳定性的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 归并排序的稳定性保证
while (i < leftSize && j < rightSize) {
// 使用 <= 而不是 <,保证相等元素左边先放入
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
}
}

// 基数排序的稳定性保证
// 从后向前遍历,保证相同值的元素保持原有顺序
for (int i = n - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}

3.2 不稳定排序

定义:相等元素的相对顺序可能改变。

不稳定排序算法

  • 选择排序 ✗
  • 希尔排序 ✗
  • 快速排序 ✗
  • 堆排序 ✗

为什么不稳定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 选择排序的不稳定性示例
[4a, 4b, 3] // 4a 和 4b 是值相等但身份不同的元素

选择最小值 3,与 4a 交换

[3, 4b, 4a] // 4a 和 4b 的相对顺序改变了!

// 快速排序的不稳定性来源
// 分区过程中,相等元素的交换会打乱原有顺序
[3, 2, 4a, 5, 4b]
↓ 分区(pivot=4b)
[3, 2, 4a, 4b, 5]
↓ 递归排序
// 4a 和 4b 的相对位置可能改变

// 堆排序的不稳定性
// 堆调整过程中的元素交换会跨越多个位置

四、按空间复杂度分类

4.1 原地排序 O(1)

定义:除了存储输入数据外,只需要常数级别的额外空间。

原地排序算法

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 希尔排序
  • 堆排序

优势

  • 内存占用低
  • 适合嵌入式系统
  • 缓存友好
1
2
3
4
5
6
7
8
9
10
11
// 所有原地排序的共同特点
public static void sort(int[] arr) {
// 只使用几个临时变量
int temp, i, j;

// 直接操作原数组
for (i = 0; i < arr.length; i++) {
// ... 排序逻辑
}
// 不需要额外的数组空间
}

4.2 需要额外空间

算法 空间复杂度 空间用途
快速排序 O(log n) 递归栈空间
归并排序 O(n) 临时数组
计数排序 O(n + k) 计数数组 + 输出数组
桶排序 O(n + k) 桶数组
基数排序 O(n + k) 计数/桶数组

空间换时间的权衡:

1
2
3
4
5
6
7
8
// 归并排序:用 O(n) 空间换取稳定性和 O(n log n) 保证
// 适合:内存充足,需要稳定排序

// 计数排序:用 O(n + k) 空间换取 O(n) 时间
// 适合:数据范围小,追求极致速度

// 快速排序:用 O(log n) 栈空间换取平均最快性能
// 适合:通用场景,内存不敏感

五、按算法思想分类

5.1 交换排序

思想:通过交换逆序元素来达到有序。

算法 交换方式 特点
冒泡排序 相邻元素交换 简单,可提前退出
快速排序 分区交换 高效,分治思想
1
2
3
4
5
6
7
8
冒泡排序的交换过程:
[5, 3, 8, 6, 2] 初始
[3, 5, 8, 6, 2] 交换 5,3
[3, 5, 6, 8, 2] 交换 8,6
[3, 5, 6, 2, 8] 交换 8,2(第一轮结束,8到位)
[3, 5, 2, 6, 8] 交换 6,2
[3, 2, 5, 6, 8] 交换 5,2
[2, 3, 5, 6, 8] 交换 3,2(完成)

5.2 选择排序

思想:每次选择最值元素放到已排序区。

算法 选择策略 特点
选择排序 线性扫描选最值 交换次数最少
堆排序 堆结构选最值 O(n log n) 保证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
选择排序 vs 堆排序的选择过程:

选择排序:
[64, 25, 12, 22, 11]
↑ ↓
扫描找到最小值 11,交换
[11, 25, 12, 22, 64] 11 到位
↑ ↓
扫描找到最小值 12,交换
[11, 12, 25, 22, 64] 12 到位
...

堆排序:
构建最大堆:
64
/ \
25 12
/ \
22 11

提取堆顶 64,与末尾交换,堆化...

5.3 插入排序

思想:将元素插入到已排序序列的合适位置。

算法 插入策略 特点
插入排序 逐个插入 对小数据最快
希尔排序 分组插入 突破 O(n²)
1
2
3
4
5
6
7
8
9
10
11
12
13
插入排序过程:
[12], 11, 13, 5, 6 已排序 | 未排序
[11, 12], 13, 5, 6 插入 11
[11, 12, 13], 5, 6 插入 13(已在正确位置)
[5, 11, 12, 13], 6 插入 5
[5, 6, 11, 12, 13] 插入 6

希尔排序(gap=2)过程:
[12, 11, 13, 5, 6, 7]
↓ ↓ ↓ 分组 112, 13, 6 → 排序 → 6, 12, 13
↓ ↓ ↓ 分组 211, 5, 7 → 排序 → 5, 7, 11
[6, 5, 12, 7, 13, 11] gap=2 排序后
[5, 6, 7, 11, 12, 13] gap=1(普通插入排序)

5.4 归并排序

思想:分治法,先分后合。

算法 分割策略 特点
归并排序 二分分割 稳定,O(n log n) 保证
快速排序 按基准分割 平均最快
1
2
3
4
5
6
7
8
9
10
11
12
13
14
归并排序的分治过程:
[38, 27, 43, 3, 9, 82, 10]
/ \
[38, 27, 43] [3, 9, 82, 10]
/ \ / \
[38] [27,43] [3,9] [82,10]
/ \ / \ / \
[27] [43] [3] [9] [82] [10]
\ / \ / \ /
[27,43] [3,9] [10,82]
\ / \ /
[27, 38, 43] [3, 9, 10, 82]
\ /
[3, 9, 10, 27, 38, 43, 82]

5.5 分布排序

思想:根据数据的分布特征直接定位。

算法 分布方式 特点
计数排序 统计频率 整数,范围小
桶排序 均匀分桶 分布均匀
基数排序 按位分桶 固定位数

六、综合选择指南

6.1 决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
开始选择排序算法

├── 数据规模?
│ ├── n < 50 → 插入排序
│ ├── 50 ≤ n < 1000 → 希尔排序 / 快速排序
│ └── n ≥ 1000 → 继续判断

├── 需要稳定性?
│ ├── 是 → 归并排序 / 计数排序 / 基数排序
│ └── 否 → 继续判断

├── 内存受限?
│ ├── 是 → 堆排序
│ └── 否 → 快速排序

├── 数据特征?
│ ├── 整数且范围小 → 计数排序
│ ├── 分布均匀 → 桶排序
│ ├── 固定位数 → 基数排序
│ └── 基本有序 → 插入排序

└── 默认推荐 → 快速排序(Dual-Pivot)

6.2 各语言/库的排序实现

语言/库 实现算法 备注
Java Arrays.sort() Dual-Pivot QuickSort(基本类型)
Timsort(对象类型)
基本类型追求速度
对象类型追求稳定
Python sorted() Timsort 归并排序的优化版本
C++ std::sort() Introsort 快速排序 + 堆排序混合
C qsort() QuickSort 标准实现
JavaScript sort() QuickSort / Timsort 引擎相关
Go sort.Sort() QuickSort 标准实现

6.3 实际应用案例

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
// 案例 1:大规模用户数据排序(需要稳定性)
// 选择:归并排序或 Timsort
List<User> users = getUsers(); // 100万用户
users.sort(Comparator.comparing(User::getAge)); // Java 使用 Timsort

// 案例 2:实时数据流排序(数据逐个到达)
// 选择:插入排序
void onDataArrive(int value) {
insertIntoSortedArray(arr, value); // O(n) 插入
}

// 案例 3:考试成绩排序(0-100分)
// 选择:计数排序
int[] scores = getScores(); // 范围小
CountingSort.sort(scores); // O(n)

// 案例 4:浮点数坐标排序(均匀分布)
// 选择:桶排序
float[] coordinates = getCoordinates();
BucketSort.sort(coordinates); // 均匀分布时接近 O(n)

// 案例 5:内存极度受限的嵌入式系统
// 选择:堆排序或希尔排序
void sortInLimitedMemory(int[] arr) {
HeapSort.sort(arr); // O(1) 额外空间
}

七、总结

7.1 分类总览表

分类维度 类别 包含算法
时间复杂度 O(n²) 冒泡、选择、插入
O(n log n) 快速、归并、堆、希尔
O(n) 计数、桶、基数
比较方式 比较排序 冒泡、选择、插入、希尔、快速、归并、堆
非比较排序 计数、桶、基数
稳定性 稳定 冒泡、插入、归并、计数、桶、基数
不稳定 选择、希尔、快速、堆
空间复杂度 O(1) 冒泡、选择、插入、希尔、堆
O(log n) 快速
O(n) 归并
O(n + k) 计数、桶、基数
算法思想 交换 冒泡、快速
选择 选择、堆
插入 插入、希尔
归并 归并
分布 计数、桶、基数

7.2 学习建议

  1. 入门阶段:理解冒泡、选择、插入三种基础排序
  2. 进阶阶段:掌握快速排序和归并排序的分治思想
  3. 深入阶段:理解堆结构和堆排序,学习希尔排序的增量序列
  4. 拓展阶段:了解线性时间排序的适用场景

7.3 面试重点

  • 必会算法:快速排序(手写)、归并排序(手写)、堆排序(理解)
  • 常考问题
    • 快速排序的优化(三数取中、小数组插入排序)
    • 归并排序的迭代实现
    • 堆排序的堆化过程
    • 各种排序算法的稳定性和时间复杂度
    • 实际场景下的算法选择

理解排序算法的分类,不仅有助于记忆各种算法的特点,更重要的是能够在实际开发中根据具体场景选择最合适的算法,达到最佳的性能表现。


如果您喜欢此博客或发现它对您有用,欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !