选择排序算法

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) 时间

相关推荐
2401_873544921 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
add45a1 小时前
C++中的组合模式
开发语言·c++·算法
無限進步D2 小时前
简单贪心算法 cpp
c++·算法·贪心算法·蓝桥杯·入门·竞赛
2501_945423542 小时前
模板编程中的SFINAE技巧
开发语言·c++·算法
AMoon丶2 小时前
Golang--垃圾回收
java·linux·开发语言·jvm·后端·算法·golang
承渊政道2 小时前
【优选算法】(实战感悟二分查找算法的思想原理)
c++·笔记·学习·算法·leetcode·visual studio code
☆5662 小时前
C++中的策略模式应用
开发语言·c++·算法
2401_884563242 小时前
C++中的原型模式变体
开发语言·c++·算法
重生之我是Java开发战士2 小时前
【递归、搜索与回溯】记忆化搜索:斐波那契数列,不同路径,最长递增子序列,猜数字游戏II,矩阵中最长递增路径
算法·leetcode·深度优先