选择排序算法

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(&a0, &a3) → 1, 2, 5, 9

  • 此时最大值9被移到了下标3(原 mini 的位置),但 maxi 仍然指向0(现在值是1)

  • 第二次交换:Swap(&a3, &a0) 会把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)

  • a6=6 > a5=1,所以 child 更新为 6

比较并交换:

  • achild=6 > aparent=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)

  • a3=7 > a4=4,所以 child 保持为 3

比较并交换:

  • achild=7 > aparent=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)

  • a1=7 > a2=6,所以 child 保持为 1

比较并交换:

  • achild=7 > aparent=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)

  • a3=5 > a4=4,所以 child 保持为 3

比较并交换:

  • achild=5 > aparent=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

  • a2=6 > a0=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)

  • a5=1 < a2=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

  • a1=5 > a0=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

  • a4=4 > a1=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

  • a1=4 > a0=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)

  • a3=3 > a1=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

  • a1=3 > a0=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)

  • a1=1 < a0=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) 时间

相关推荐
JieE21220 分钟前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
JieE2121 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
Jack201 天前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法
小小杨树1 天前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
JieE2122 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2122 天前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
vivo互联网技术2 天前
CVPR 2026 | 全新强化学习框架 BeautyGRPO:重塑真实人像
算法·大模型·cvpr·影像
Darling噜啦啦2 天前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试
用户497863050732 天前
(一)小红的数组操作
算法·编程语言