1.直接选择排序
时间复杂度:O(n²)
选择排序的基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
c
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1; // begin和end标记当前待排序区间的范围
while (begin < end) // 当区间还有至少两个元素时
{
int mini = begin, maxi = begin; // 假设第一个元素既是最大也是最小
// 遍历整个区间,找出真正的最大值和最小值
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] > a[maxi]) // 找到更大的元素
{
maxi = i;
}
if (a[i] < a[mini]) // 找到更小的元素
{
mini = i;
}
}
// 将最小值放到前面(begin位置)
Swap(&a[begin], &a[mini]);
// 将最大值放到后面(end位置)
Swap(&a[end], &a[maxi]);
++begin; // 缩小范围,前面已排好
--end; // 缩小范围,后面已排好
}
}
关键细节分析:
为什么需要两次交换?
优化版选择排序每趟同时找出最小值和最大值,分别放到两端,这样可以减少一半的遍历次数。
特殊情况处理:当 maxi == begin 时
如果最大值正好在 begin 位置,那么第一次交换(将最小值放到 begin)会把这个最大值移走。
示例:假设区间 [9, 2, 5, 1],begin=0, end=3
-
mini=3(值1),maxi=0(值9)
-
第一次交换:Swap(&a[0], &a[3]) → [1, 2, 5, 9]
-
此时最大值9被移到了下标3(原 mini 的位置),但 maxi 仍然指向0(现在值是1)
-
第二次交换:Swap(&a[3], &a[0]) 会把1又换回来,导致错误
解决方案:在第一次交换后,检查 maxi 是否等于 begin,如果是,则更新 maxi = mini。
c
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[maxi]) maxi = i;
if (a[i] < a[mini]) mini = i;
}
Swap(&a[begin], &a[mini]);
// 如果最大值原本在 begin 位置,它已经被换到 mini 位置了
if (maxi == begin) maxi = mini;
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
gif:

2.堆排序
c
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; // 已经满足堆性质
}
}
}
c
//小堆
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;
}
}
关于堆排序我们在之前的文章有过讲解,不过在这里我们再来进行一次吧。
在这里,为了更加直观的展示排序过程,我将使用ds辅助画图来展示一步步的排序:
2.1建堆过程详解:
以数组 [3, 5, 2, 7, 4, 1, 6] 为例,n=7。
初始完全二叉树:
c
3 (下标0)
/ \
5 (1) 2 (2)
/ \ / \
7 (3) 4 (4) 1 (5) 6 (6)
找到最后一个非叶子节点:
-
最后一个节点下标:n-1 = 6
-
最后一个非叶子节点下标:(6-1)/2 = 2(值为2的节点)
调整节点2(parent=2,值为2)
初始状态:
c
parent=2 (值2)
child = 2*2+1 = 5 (值1)
c
3
/ \
5 2 (parent)
/ \ / \
7 4 1 6
↑ ↑
child 右孩子
检查较大孩子:
-
左孩子 child=5 (值1)
-
右孩子 child+1=6 (值6)
-
a[6]=6 > a[5]=1,所以 child 更新为 6
比较并交换:
- a[child]=6 > a[parent]=2,交换
c
3
/ \
5 6
/ \ / \
7 4 1 2
数组:[3, 5, 6, 7, 4, 1, 2]
继续向下调整(parent 更新为 child=6):
- parent=6,child = 6*2+1 = 13 ≥ n,退出循环
节点2调整完成。
调整节点1(parent=1,值为5)
c
parent=1 (值5)
child = 1*2+1 = 3 (值7)
c
3
/ \
5 (parent) 6
/ \ / \
7 4 1 2
↑
child
检查较大孩子:
-
左孩子 child=3 (值7)
-
右孩子 child+1=4 (值4)
-
a[3]=7 > a[4]=4,所以 child 保持为 3
比较并交换:
- a[child]=7 > a[parent]=5,交换
c
3
/ \
7 6
/ \ / \
5 4 1 2
数组:[3, 7, 6, 5, 4, 1, 2]
继续向下调整(parent 更新为 child=3):
-
parent=3 (值5)
-
child = 3*2+1 = 7 ≥ n,退出循环
节点1调整完成。
调整节点0(parent=0,值为3)
初始状态:
c
parent=0 (值3)
child = 0*2+1 = 1 (值7)
c
3 (parent)
/ \
7 (child) 6
/ \ / \
5 4 1 2
检查较大孩子:
-
左孩子 child=1 (值7)
-
右孩子 child+1=2 (值6)
-
a[1]=7 > a[2]=6,所以 child 保持为 1
比较并交换:
- a[child]=7 > a[parent]=3,交换
c
7
/ \
3 6
/ \ / \
5 4 1 2
数组:[7, 3, 6, 5, 4, 1, 2]
继续向下调整(parent 更新为 child=1):
c
parent=1 (值3)
child = 1*2+1 = 3 (值5)
c
7
/ \
3 (parent) 6
/ \ / \
5 4 1 2
↑
child
检查较大孩子:
-
左孩子 child=3 (值5)
-
右孩子 child+1=4 (值4)
-
a[3]=5 > a[4]=4,所以 child 保持为 3
比较并交换:
- a[child]=5 > a[parent]=3,交换
c
7
/ \
5 6
/ \ / \
3 4 1 2
数组:[7, 5, 6, 3, 4, 1, 2]
继续向下调整(parent 更新为 child=3):
-
parent=3 (值3)
-
child = 3*2+1 = 7 ≥ n,退出循环
建堆完成!
2.2排序过程详解:
建堆后的数组:[7, 5, 6, 3, 4, 1, 2]
第一轮(end=6)
交换堆顶和堆尾:
c
交换前:[7, 5, 6, 3, 4, 1, 2]
交换后:[2, 5, 6, 3, 4, 1, 7]
堆大小变为6(只考虑前6个元素)
调整堆顶(parent=0,值2):
c
2 (parent)
/ \
5 6
/ \ /
3 4 1
-
parent=0, child=1 (值5)
-
较大孩子:右孩子 child+1=2 (值6) > 左孩子5,所以 child=2
-
a[2]=6 > a[0]=2,交换
c
6
/ \
5 2
/ \ /
3 4 1
数组:[6, 5, 2, 3, 4, 1, 7],parent=2
继续向下调整:
-
parent=2 (值2),child=5 (值1)
-
右孩子 child+1=6 超出范围(n=6,最大下标5)
-
a[5]=1 < a[2]=2,不交换,退出
第二轮(end=5)
交换堆顶和堆尾(前6个元素):
c
交换前:[6, 5, 2, 3, 4, 1, 7]
交换后:[1, 5, 2, 3, 4, 6, 7]
堆大小变为5(前5个元素:[1,5,2,3,4])
调整堆顶:
c
1 (parent)
/ \
5 2
/ \
3 4
-
parent=0, child=1 (值5)
-
较大孩子:右孩子 child+1=2 (值2) < 5,所以 child=1
-
a[1]=5 > a[0]=1,交换
c
5
/ \
1 2
/ \
3 4
数组:[5, 1, 2, 3, 4, 6, 7],parent=1
继续向下调整:
-
parent=1 (值1),child=3 (值3)
-
右孩子 child+1=4 (值4) > 3,所以 child=4
-
a[4]=4 > a[1]=1,交换
c
5
/ \
4 2
/ \
3 1
数组:[5, 4, 2, 3, 1, 6, 7],parent=4
继续向下调整:
- parent=4 (值1),child=9 ≥ n,退出
第三轮(end=4)
交换堆顶和堆尾(前5个元素):
c
交换前:[5, 4, 2, 3, 1, 6, 7]
交换后:[1, 4, 2, 3, 5, 6, 7]
堆大小变为4(前4个元素:[1,4,2,3])
调整堆顶:
c
1 (parent)
/ \
4 2
/
3
-
parent=0, child=1 (值4)
-
较大孩子:右孩子 child+1=2 (值2) < 4,所以 child=1
-
a[1]=4 > a[0]=1,交换
c
4
/ \
1 2
/
3
数组:[4, 1, 2, 3, 5, 6, 7],parent=1
继续向下调整:
-
parent=1 (值1),child=3 (值3)
-
右孩子 child+1=4 超出范围(n=4,最大下标3)
-
a[3]=3 > a[1]=1,交换
c
4
/ \
3 2
/
1
数组:[4, 3, 2, 1, 5, 6, 7],parent=3
继续向下调整:
- parent=3 (值1),child=7 ≥ n,退出
第四轮(end=3)
交换堆顶和堆尾(前4个元素):
c
交换前:[4, 3, 2, 1, 5, 6, 7]
交换后:[1, 3, 2, 4, 5, 6, 7]
堆大小变为3(前3个元素:[1,3,2])
调整堆顶:
c
1 (parent)
/ \
3 2
-
parent=0, child=1 (值3)
-
较大孩子:右孩子 child+1=2 (值2) < 3,所以 child=1
-
a[1]=3 > a[0]=1,交换
c
3
/ \
1 2
数组:[3, 1, 2, 4, 5, 6, 7],parent=1
继续向下调整:
- parent=1 (值1),child=3 ≥ n,退出
第五轮(end=2)
交换堆顶和堆尾(前3个元素):
c
交换前:[3, 1, 2, 4, 5, 6, 7]
交换后:[2, 1, 3, 4, 5, 6, 7]
堆大小变为2(前2个元素:[2,1])
调整堆顶:
c
2 (parent)
/
1
parent=0, child=1 (值1)
- a[1]=1 < a[0]=2,不交换,退出
第六轮(end=1)
交换堆顶和堆尾(前2个元素):
c
交换前:[2, 1, 3, 4, 5, 6, 7]
交换后:[1, 2, 3, 4, 5, 6, 7]
堆大小变为1,排序完成!

总结:
堆排序的核心是 AdjustDown 函数:
-
从父节点开始,找到较大的孩子
-
如果孩子大于父亲,交换,父亲下沉到孩子位置
-
重复直到满足堆性质
建堆时从最后一个非叶子节点开始,自底向上调整;排序时每次交换堆顶与堆尾,然后重新调整堆顶。
建堆为什么从最后一个非叶子节点开始?
-
叶子节点没有孩子,天然满足堆性质,不需要调整
-
第一个非叶子节点是最后一个叶子节点的父节点
-
从下往上调整,可以确保上层调整时下层已经满足堆性质
-
最后一个非叶子节点下标:(n-1-1)/2 = (n-2)/2
常见问题:
Q1:为什么用大根堆得到升序,小根堆得到降序?
-
大根堆:堆顶是最大值,交换到末尾,逐渐形成升序
-
小根堆:堆顶是最小值,交换到末尾,逐渐形成降序
Q2:建堆时从最后一个非叶子节点开始,为什么不是从根开始?
-
如果从根开始向下调整,下面的子堆可能还没建好,无法保证正确性
-
从下往上建堆,可以确保每次调整时子树已经是堆
Q3:堆排序和选择排序的关系?
-
选择排序每次找最大值需要 O(n) 时间
-
堆排序利用堆结构,每次找最大值只需要 O(log n) 时间