引言
本篇博客,我们将深入探讨选择排序这一经典算法。我们将使用C语言这一历史悠久且强大的编程语言,来详细讲解选择排序的实现过程。从算法的定义和思想出发,到具体的代码实现,再到时间复杂度的分析,我们将一步步带您揭开选择排序的神秘面纱。无论您是正在学习数据结构的初学者,还是想巩固算法基础的进阶者,相信都能从本篇文章中有所收获。让我们一起,用C语言编写出优雅而实用的选择排序算法吧!
一、选择排序的核心思想
选择排序的核心思想是:每一次从待排序的数据元素中选出最小(最大)的一个元素,然后存放在序列的起始位置,直到待排数据元素排完
常见的选择排序有直接选择排序和堆排序
二、直接选择排序——以升序为例
2.1、基本框架图解
直接选择排序,从每次的序列范围内选择一个最小的值和序列头部交换位置,然后再更改下一次操作序列的范围。哎,等等,选择排序为什么要交换呢?该算法是从序列中选择一个最值,放到序列头部或者尾部,而交换是为了使数据不丢失的最好办法。
这是以升序为例,挑选最小值放头部的例子:
那么,如何实现上述操作呢?让我们分析分析:
首先,需要确定每次挑选最值的范围,如上述动图,每次挑选最小值放在头部,那么下一次挑选就不能算上头部,这里用for循环颇为合适,进入循环,寻找最小值,然后交换,再进入下一次循环,如下:
//直接选择排序
void SelectionSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
//找最小值
//交换最小值与序列头部
Swap(&arr[i], &arr[mini]);
}
}
那么,如何寻找最小值呢?
2.2、寻找最小值详解
寻找最小值很简单,这里说明以下,我们寻找的是最小值的下标。
首先,先默认最小值为第一个元素,然后依次遍历后续元素,如果遇见比最小值还要小的元素,那么就更新最小值,直到遍历完成。
代码如下:
int mini = i;
for (int j = i + 1; j < n; j++)
{
if (arr[j] < arr[mini])
{
mini = j;
}
}
2.3、完整代码
//直接选择排序
void SelectionSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
//找最小值
int mini = i;
for (int j = i + 1; j < n; j++)
{
if (arr[j] < arr[mini])
{
mini = j;
}
}
//交换最小值与序列头部
Swap(&arr[i], &arr[mini]);
}
}
2.4、直接选择排序的时间复杂度及优化
通过完整代码不难看出,该算法内外循环都是n,因此时间复杂度为O(N^2),那么,有没有什么办法可以优化呢?有的。
在找最小值的时候,可以最大值和最小值一起寻找,将最小值放在头部,将最大值放在尾部。这个时候,我么可以使用 begin 和 end 来限制寻找的范围。
那就开始写代码:
void SelectionSort(int* arr, int n)
{
assert(arr);
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1 ; i <= end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[begin], &arr[mini]);
Swap(&arr[maxi], &arr[end]);
begin++;
end--;
}
}
注意:在交换的时候前面有个 if 条件,这是为什么呢?
因为会出现这种情况:
当 maxi 等于 begin ,mini 等于 end 的时候,两次交换恰好相互抵消,因此需要判断调整。
那么为什么条件非是 maxi == begin 呢?那如果maxi == begin 而 mini != end 呢?
根本原因就是因为先交换的 arr[begin] 和 arr[mini] ,而后交换的 arr[maxi] 和 arr[end]!那么如果 maix 在 begin 位置的话,就相当于那个位置被交换了两次!不管 mini 在什么地方。
像这种情况,下标为2的地方被交换了两次,显然是不符合设定的。到现在,条件 maxi == begin 搞清楚了,那为什么要执行 maxi = mini 呢?
首先,交换完 mini 和 begin ,maxi 会随着begin的交换跑到 mini 去,因此,将maxi赋值为mini是正确的。
三、堆排序
3.1、堆排序框架介绍
堆是一种完全二叉树,只不过该完全二叉树节点的值的分布是有要求的。
堆的特点:(1)堆是一颗完全二叉树
(2)堆中的某个节点总是不大于或不小于其父节点
(3)堆顶是最小值或最大值
利用堆顶是最值来对序列进行排序,我们只需要每次取堆顶,再重新建堆即可。
所以具体框架就是这样:
void HeapSort(int* arr, int n)
{
//建堆
//取堆顶
//调整建堆
}
3.2、向下调整法建堆
在介绍堆的时候,我们计算过在堆排序中,向上和向下调整法的时间复杂度,最后得出向下调整法更好,因此,堆排序通常采用向下调整法建堆。本文堆采用顺序表表示。
但是,如果为了一个堆排序而去专门写一个堆的结构,就显得小题大作了,因此,堆排序只是借鉴堆的思想,然后在原数组上改造。
如图,这是原序列,对应的二叉树为:
这是一个乱序的二叉树,要想将其变为堆,就需要进行向下调整,但是,向下调整一次只能调整一个分支,由此可见,向下调整需要进行多次。还有一个问题,从哪里开始调整,根节点,还是叶节点?向下调整,由上而下,自然是把根节点向下调整,但是有一个前提:子树都是排列好的。因此,我们需要挨个将子树调整好,然后再进行根节点的调整。
如图:是从编号为2的节点开始调整。(因为叶子节点没必要调整)
接下来调整编号为1 的节点,但是发现该节点位置很合适,不用调整。接下来调整编号为0 的节点,也就是根节点:
到此,原序列就变成了最小堆。
下面是建堆的代码:
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
//向下调整建堆---最小堆
void AdjustDown(int* arr, int parent ,int n)
{
assert(arr);
int child = parent * 2 + 1;
while (child < n)
{
if (child+1 < n && arr[child + 1] < arr[child])
{
child++;
}
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
3.3、取堆顶,继续调整堆
现在建完堆之后,就需要取堆顶,然后继续建堆。但是我们需要明确堆的范围,因此引入变量 end 来规范堆的大小。
那么,堆顶取完之后放到哪呢?为了避免开拓新的空间,我们就放在堆的末尾。
int end = n - 1;
while (end > 0)
{
Swap(&arr[end], &arr[0]);
AdjustDown(arr, 0, end);
end--;
}
3.4、完整代码和时间复杂度
void HeapSort(int* arr, int n)
{
//建堆
for (int i = (n - 1-1) / 2; i >= 0; i--)
{
AdjustDown(arr, i, n);
}
ArrPrint(arr, n);
//取堆顶
int end = n;
while (end > 0)
{
Swap(&arr[end-1], &arr[0]);
end--;
AdjustDown(arr, 0, end);
}
}
外层的时间复杂度为 O(N) ,下来就要看内层的向下调整的时间复杂度了。而向下调整算法最坏的情况也只不过是由根节点到叶子结点,时间复杂度为 O(logN) .因此,向下调整算法的堆排序的时间复杂度为: O(NlogN)
结语
至此,我们已经完整地探讨了选择排序算法的原理、C语言实现以及性能分析。选择排序作为一种基础的排序算法,虽然在效率上并非最佳,但它简单易懂的特性,使其成为我们学习排序算法的良好起点。
通过本篇博客,相信您已经对选择排序有了更深入的理解。您不仅掌握了如何用C语言实现选择排序,还了解了其时间复杂度,并能够将其与其他排序算法进行比较。
在未来的学习中,您可以尝试将选择排序应用于实际场景,并思考如何优化其性能。例如,在数据量较小的情况下,选择排序仍然是一个不错的选择。
感谢您的阅读!希望本篇博客能帮助您更好地理解选择排序。如果您有任何问题或建议,欢迎在评论区留言讨论。让我们一起在数据结构与算法的道路上不断学习和进步!