欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 维修 > c语言数据结构——八大排序算法实现

c语言数据结构——八大排序算法实现

2025/4/8 23:18:38 来源:https://blog.csdn.net/2301_79705195/article/details/146783884  浏览:    关键词:c语言数据结构——八大排序算法实现

文章目录

  • 八大排序算法
    • 排序算法种类
    • 选择排序类
      • 堆排序
        • 算法思路
        • 时间复杂度和空间复杂度
      • 选择排序
        • 算法思路
        • 算法优化
        • 时间复杂度和空间复杂度
    • 插入排序类
      • 插入排序
        • 算法思路
        • 时间复杂度和空间复杂度
      • 希尔排序
        • 算法思路
        • 时间复杂度和空间复杂度
    • 非比较排序类
      • 计数排序
        • 时间复杂度和空间复杂度
      • 归并排序
        • 算法思路
        • 递归实现
        • 非递归实现
        • 时间复杂度和空间复杂度
    • 交换排序类
      • 冒泡排序
        • 时间复杂度和空间复杂度
      • 快速排序
        • 三种单趟排序思路
        • 快排递归实现
        • 递归实现的优化
        • 非递归实现
        • 时间复杂度和空间复杂度
    • 排序算法性质与总结
      • 时间复杂度和空间复杂度总结
      • 稳定性
      • 算法适用范围
    • 全文总结

八大排序算法

这篇文章是c语言数据结构章节的最后一个部分——排序算法

在我们的日常生活中,排序是无处不在的,比如打开一个购物网站,会发现网站可能会对其上架的商品有多种排序方式:
在这里插入图片描述
随便输入一个商品,平台对其都有对应的排序功能如销量、价格等。再到中高考成绩的排序决定考生排位,又或是学校内成绩的排序,最终都离不开排序。

所以今天这篇文章将重点讲解八大排序算法的思想以及实现。因为重点是思想以及功能能够成功实现,所以排序的对象选择整形数组。最重要的是学习背后的思想。其他类型的数据排序只需要稍微改动一些逻辑即可。

排序算法种类

八大排序算法正好两两对应一组:
在这里插入图片描述
其中有两种在之前已经接触过了,即堆排序和冒泡排序。所以对应的篇幅会少一些。

接下来,我们将对这几个算法进行深度地剖析及实现。请注意,在这篇文章中,所有的排序算法都已升序进行讲解。

选择排序类

如其名,该类排序最重要的二字就是"选择"。即每一次排序的时候都会在给定数组中选取数据,将选定好的数据放在指定的位置。下面让我们来看看是怎么样操作的。

堆排序

堆排序在之前的堆的实现文章中就已经讲过了,在这里只进行简单的回顾。

感兴趣的读者可以翻阅这篇文章:二叉树概念和堆的实现应用

算法思路

堆本质上是完全二叉树,即逻辑结构是二叉树。但是真正存储的时候,为了效率,其底层选择的是使用数组进行存储。

为了排一个升序的数组,那么应当建大堆。这是之前的文章讲过的。

对于建堆,选择使用向下调整算法建堆的时间复杂度为O(N),代码如下所示:

void AdjustDown(HPDataType* a, int Heapsize, int parent) {//默认调整小堆//向下调整小堆的逻辑:(但是得保证当前已经是一个小堆)//从传入的parent位置开始 找到parent左右孩子较小的那个  (注意:可能会没有左右孩子)//如果大于较小的那个孩子 就交换 反之退出循环 int Parent = parent;while ((Parent * 2 + 1) < Heapsize) { //如果不是叶子节点就向下调整//假设法找到左右孩子较小的那个//如果能进入循环 说明有左孩子 但不一定有右孩子int Child = Parent * 2 + 1;if (Child + 1 < Heapsize && a[Child] < a[Child + 1])  Child = Child + 1; //调为大堆要改第二个符号 第一个不能改if (a[Parent] < a[Child]) {//想要调整为大堆就改符号 找左右孩子大的 swap(&a[Parent], &a[Child]);Parent = Child;}else break;}
}

这段代码已经修改为调整大堆的版本。如果想要改为调整小堆的请按注释修改!

既然已经建立大堆了,就可以模仿出堆过程。将最大的数(堆顶数据)出堆,其实就是存储在数组的最后一个位置。但是后续会调整堆的大小减小1,所以就可以近似看成出堆操作。

最后代码的实现如下:

void HeapSort(int* a, int n) {//向下调整建堆/*int begin = (n - 1 - 1) / 2;while (begin >= 0) {AdjustDown(a, n, begin);begin--;}*///当然可以使用for循环for (int i = (n - 1 - 1) / 2; i >= 0; i--) {AdjustDown(a, n, i);}//模仿出堆过程while (n > 0) {int end = n - 1;swap(&a[0], &a[end]);AdjustDown(a, end, 0);n--;}
}
时间复杂度和空间复杂度

在上述提及的那篇文章中介绍过:
向下调整算法建堆的时间复杂度为O(N),出堆过程时间复杂度为O(N * LogN),所以最后该算法属于是N * LogN级别,所以时间复杂度为O(N * LogN)。

由于该算法没有开辟额外的空间堆数组进行处理,所以空间复杂度为O(1)。
由于已经讲过,便不再赘述。

选择排序

算法思路

这个排序十分简单,即给定一个数组,每次都找到最小值位置,然后与第一个位置的元素交换位置。然后让后面部分的数组执行上述操作。

如下图例子所示:
在这里插入图片描述
对于上面这个思路的代码实现:

void SelectSort(int* a, int n) {int begin = 0;for(int i = 0; i < n - 1; i++){int min = i;for(int j = i + 1; j < n; j++){if(a[j] < a[min]) min = j; }swap(&a[min],&a[i]);//交换操作}
}

很容易看见,选择排序算法的本质就是暴力查找数据。但是为了排序的效率,能不能再对这个部分的算法进行一定程度的优化呢?

答案是可以的

算法优化

既然是暴力查找,那就可以每一次暴力查找的时候同时找到当前数组中最大和最小的,然后放在数组的头尾,然后不断地查找下去直到无法查找即可。

这样子相比于上面的暴力查找,虽然方法类似,但是效率高了很多,所需要的时间可以近似看成上面那个算法的一半。

代码实现:

void SelectSort(int* a, int n) {//begin从0往后走 end从n-1往前走//每一次都要找到最大和最小的,最小的放begin位置,最大的放end位置//然后begin++   end--//直到begin>=end就出这个循环int begin = 0, end = n - 1;while (begin < end) {int maxi = begin, mini = begin;//最大数和最小数的下标for (int i = begin + 1; i <= end; i++) {if (a[i] > a[maxi]) {maxi = i;}if (a[i] < a[mini]) {mini = i;}}//交换操作if (begin == maxi && end == mini) swap(&a[begin], &a[end]);else if (begin == maxi) {swap(&a[end], &a[maxi]);swap(&a[begin], &a[mini]);}else {swap(&a[begin], &a[mini]);  swap(&a[end], &a[maxi]);}begin++;end--;}
}

但是为什么后续的交换操作如此麻烦呢?

这是因为当一次性找两个数据的时候,有如下几种可能:
1.最大的数字的坐标与寻找数组段的第一个位置元素重合,最小的数字坐标与数组段最后一个位置元素重合
如果此时先swap(&a[begin], &a[mini]);,再swap(&a[end], &a[maxi]);,就会导致数组实际上是没有交换。所以要特殊处理

2.最大的数字的坐标与寻找数组段的第一个位置元素重合,最小的数字坐标与数组段最后一个位置元素不重合
这一种情况下,如果先swap(&a[begin], &a[mini]);,会导致maxi的位置的值发生改变,指向的是被换后的最小值。那么再执行swap(&a[end], &a[maxi]);的时候,就无法将最大的数放在数组的尾部了,所以也要特殊处理。

3.最大的数字的坐标与寻找数组段的第一个位置元素不重合,最小的数字坐标与数组段最后一个位置元素重合及其余情况
第三种情况的前面半个部分和第2条正好相反,那就与上述操作相反即可。对于其余情况,无论先执行哪一条语句都是正确的,所以不用另外再进行判断。

当然,可以再特殊情况下通过调整maxi和mini的位置。但是在这里为了确保正确,选择逻辑判断。感兴趣的读者可以尝试使用调整坐标方法。

时间复杂度和空间复杂度

无论是算法优化前又或是优化后,这个算法总是会出现两层循环嵌套的逻辑。且每一层循环的次数都与数组长度N有关。所以时间复杂度是很明显的O(N^2),由于比较简单,不进行计算。

没有开辟新的数组对原数组进行处理,所以该算法的时间复杂度是O(1)。

插入排序类

本部分算法的思想就在于”插入“二字。

插入排序

算法思路

一个数组可以看作成已排序区和未排序区。然后从未排序区的第一个数据(记作a)开始,从已排序区的最后一个元素开始向前比较。直到找到第一个小于等于a的数b的时候,就将a插入在b后面的位置。(如果已排序区的数字全部都比a小,那就将a放在第一个位置)

直到已排序区为整个数组的时候,就不用再执行插入算法了。

如图所示:
在这里插入图片描述

代码实现非常简单:

void InsertSort(int* a, int n) {//思路:把前面的部分当作已排序区(升序) //然后让已排序区的下一个位置的元素跟已排序区内比较//直到找到一个小于这个元素的位置,在这个位置后面插入该元素for (int i = 0; i < n - 1; i++) {int end = i; int tmp = a[end + 1]; while (end >= 0) { if (a[end] > tmp) { a[end + 1] = a[end]; end--; }else break;}a[end + 1] = tmp; }
}

比tmp小的数会往后挪,直到找到插入位置放入tmp。

注意:这里插入tmp数据尽量不要写在第二层循环内。因为插入排序算法的核心思想就是与前一个位置的数据比较。但是当tmp走到数组第一个位置比较的时候,前面是没有数据的。会退出循环。如果在第二层循环内写插入tmp操作,那么对于刚刚讲到的这个情况需要进行特殊判断。但是写在当前代码显示位置就可以将所有插入情况都满足。故建议写在当前展示的位置。

时间复杂度和空间复杂度

假设最坏的情况下:每一个数字往前插入都要将已排序区的数字全部比较一遍,那么语句执行的次数是一个很明显的等差数列,时间复杂度为O(N^2)。

空间复杂度为O(1),因为没有额外开辟新的空间。

希尔排序

对于刚刚写的插入排序,我们发现,当数组为倒序的时候,每个数据往前插入的时候,都要与已排序区中的所有元素进行排序。那么效率会退化。当数组中元素较多的时候,那么排序的效率并不高。而当算法是升序的时候,每个元素只要比较一遍就可以不用再执行该算法。

也就是说:对于插入排序来讲,数组越接近升序,那排序效率就越高。

针对这个特点,就提出了希尔排序这个算法。这个算法本质上就是对插入排序算法的优化。

算法思路

希尔排序是这样操作的,既然数组越接近升序插入排序效率越高,那就可以先对数组进行若干次预排序,使其较为接近升序。然后再执行上面实现的插入排序操作,这样效率就高得多。

希尔排序会有一些抽象:
它先规定了一个范围叫gap,需要将这个数组分为gap组。

为了方便讲述,我们先假定gap = 3,数组中有十个元素进行讲解:
在这里插入图片描述

如图所示操作,我们会发现所有的数据都被分到了分配的gap组上,且没有重复。

现在这个数组被分为了红、绿、紫三组。希尔排序就是对这红、绿、紫三组指向的元素分别进行预排序。使用的算法就是上面的插入排序算法。只不过此时相邻的数据举例变为了gap = 3,而不是之前的1。

连续对三组预排序的代码编写很困难,不妨先写对红色的预排序,然后再来推广:

//对红色排
for (int end = 0; end < n - gap; end += gap) {int tmp = a[end + gap];//插入排序while (end >= 0) { if (tmp < a[end]) {  a[end +

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词