前言:从扑克牌到代码,用最直观的方式理解排序算法
目录
-
插入排序
-
希尔排序
-
选择排序
-
堆排序
-
冒泡排序
-
快速排序
-
归并排序
-
总结对比
一、插入排序
理解思路
就像打扑克牌时整理手牌!
想象你手里有一堆扑克牌,你从左到右一张张整理:
-
第1张牌直接拿在手里(已经有序)
-
摸到第2张牌,和手里的牌比较,插入到合适位置
-
摸到第3张牌,再和前面已经排好的牌比较,插入到合适位置
-
重复这个过程,直到所有牌都整理好
核心思想:
-
把数组分成"已排序"和"未排序"两部分
-
每次从未排序部分取一个元素,插入到已排序部分的正确位置
-
就像你玩扑克牌时,每摸一张牌就插入到正确位置
代码实现
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] ✅
二、希尔排序
理解思路
插入排序的优化版,先让数组"大致有序",再进行最终排序!
想象你要整理一副完全混乱的扑克牌:
-
先把牌分成几堆,每堆内部整理(预排序)
-
减少堆数,继续整理
-
最后全部放在一起整理(最终排序)
核心思想:
-
插入排序在数据基本有序时效率很高(接近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] ✅
三、选择排序
理解思路
每次从牌堆里找出最大和最小的牌,放到正确位置!
想象你有一堆牌,要从小到大排列:
-
找出最大的牌,放到最右边
-
找出最小的牌,放到最左边
-
剩下的牌重复这个过程
-
直到所有牌都排好
核心思想:
-
每次从待排序区间选出最大和最小元素
-
最小元素放到区间开头,最大元素放到区间结尾
-
缩小范围,继续选择
代码实现
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] ✅
四、堆排序
理解思路
把数组想象成一棵完全二叉树,利用"大堆"特性排序!
大堆:每个父节点都比它的孩子大(根最大)
核心思想:
-
把数组建成一个大堆(根节点是最大值)
-
把根节点(最大值)和最后一个元素交换
-
最后一个元素就排好了
-
剩下的元素继续调整成大堆
-
重复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个和第2个,大的往右移动
-
比较第2个和第3个,大的往右移动
-
一直比较到最后,最大的石头就"冒"到了最右边
-
剩下的石头重复这个过程
-
直到所有石头都排好
核心思想:
-
相邻元素两两比较,大的往后移
-
每一轮把最大的元素移到未排序部分的最后
-
就像气泡从水底慢慢升到水面
代码实现
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] ✅
六、快速排序
理解思路
"分治"思想,选择一个基准值,把比它小的放左边,比它大的放右边!
想象你要整理一堆牌:
-
随便选一张牌作为"基准"
-
把比它小的牌放到左边,比它大的放到右边
-
基准牌就找到了正确位置
-
对左边和右边分别重复这个过程
-
直到所有牌都排好
核心思想:
-
每次选一个"基准值"
-
把数组分成三部分:小于基准 + 基准 + 大于基准
-
递归处理左右两部分
代码实现
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] ✅
七、归并排序
理解思路
"分治"思想的完美体现,先拆分成单个元素,再两两合并!
想象你两堆已经排好序的牌,要合并成一堆:
-
比较两堆最上面的牌
-
小的先放到新堆
-
重复直到一堆空
-
把剩下的牌全部放进去
核心思想:
-
把数组分成两半,分别排序
-
把两个有序数组合并成一个有序数组
-
递归到每个子数组只有一个元素(天然有序)
代码实现
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;
}