数据结构--手撕八大排序(超级详细)!!

本文详细介绍了八种常见的排序算法,包括插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序和计数排序,分析了它们的时间复杂度和空间复杂度,以及各自的优缺点和适用场景。
摘要由CSDN通过智能技术生成

目录

前言

1.插入排序

1.1直接插入排序

1.2希尔排序

2.选择排序

2.1直接选择排序

2.2堆排序

2.2.1建堆

2.2.2代码实现

3.交换排序

3.1冒泡排序

3.2快速排序

3.2.1挖坑法

3.2.2Hoare

3.2.3前后指针法

3.2.4非递归实现快速排序

4.归并排序

4.1递归实现归并排序

4.2非递归实现归并排序

5.计数排序


前言

排序是一种重要的算法,需要我们掌握的方法有很多比如最常见的八种排序算法:插入排序,希尔排序,直接选择排序,堆排序冒泡排序,快速排序,归并排序。当然还有一些其他的排序方法如基数排序,桶排序这里就不多做阐述。

1.插入排序

1.1直接插入排序

首先将数组分为有序部分和无序部分,直接插入排序就是将无序部分的数据,插入到有序部分。直到所有数据都排完,整体思想类似于打扑克牌时,将牌给排序好

常用的方法就是(这里拿升序做例子),将无序部分的第一个数据a[m]从后到前依次,和前面有序部分的最后一个数据a[n]进行比较,如果a[m]小于a[n],就和a[n]前面一个数据a[n- 1]进行比较直到遇到比a[m]小的数字a[n-i]就停止,把a[m]插到a[n-i]的后面.如果没有比m小的数字,就把m插到数组的第一位。不过不可能恰好数组的前面就有一个有序数组,所以我们一般将第一个数字当做有序部分,其余全都当成无序部分,这样就可以对任意数组进行插入排序了。

时间复杂度和空间复杂度

直接插入排序的时间复杂度是O(N^2),当所排的数据越接近有序,时间复杂度越第最低是O(N),空间复杂度是O(1);

1.2希尔排序

希尔排序(也叫缩小增量排序)是一种更高效的插入排序,整体思想和插入排序很像但是稍复杂一些。所以我们可以从直接插入排序的角度来先分析一下,我们知道直接插入排序当所排的数据越接近有序,时间复杂度越第最低,但是总会有一些不那么有序的数据,所以我们为了尽量使数据变得有序可以进行预排序,让数据尽量有序。

首先对要排序的数组进行分组,分成gap组每组各数据之间的间隔为gap(如图的第一趟排序),然后只在每组中进行排序,比如下图中每个颜色相同的部分(代表在同一组)是有序的,接着慢慢减小gap的值,当gap的值等于1时(其实就是直接插入排序),就将整个数组排序好了。设置gap的过程就是预排序。

有些朋友们可能会有疑问,这样来回反复的分组排序,不会反而跟麻烦吗,其实不会的。当gap的值越大时,大的数可以越快的排到后面,小的数可以越快的排到前面。

gap越大排完序越不接近有序,gap越小排完序越接近有序,gap==1时就是直接插入排序,但是gap越小排的越慢,gap越大排得越快。

//注意gap不是每次减一或减几,这样对一些数据较小的数组还可以,如果数据量比较大有几百万个数要排那么这样显然是比较鸡肋的。所以我们一般先令gap=n(n是要拍的数据量)然后gap/=2或gap/=3,不过如果gap/=3的话在一些情况下可能最后除不到1,所以建议一般就让gap/=2或gap/=3+1;

代码实现

时间复杂度

希尔排序的时间复杂度是O(N^1.3~N^2)

2.选择排序

2.1直接选择排序

直接选择排序的思想很简单,就是依次遍历数组找出当前最小值放到最左边然后再对剩下的数组进行遍历找到次小的数放到第二个位置(思路很简单,但是看了思想之后也能感觉到,这个排序很鸡肋!)

不过可以给这个排序升级一下,就是在每次遍历的同时不仅找出当前最小值,还找出当前最大值,将最小值放到左边,最大值放到右边,这样是不是就好一点了。不过就算这样他的效率,比较其他排序还是较慢一些的

代码实现(二元改进后的)

时间复杂度

直接选择排序的时间复杂度是O(N^2)

2.2堆排序

堆就是用数组实现的完全二叉树(不是平衡二叉树),所以他在物理上没有父节点或子节点。堆在物理结构上是一个数组,在逻辑结构上是一个数组(类似于二叉树的顺序存储)

在逻辑结构上每个父节点(parent)和子节点(child)的下标存在一定数量关系

leftchild=parent*2+1;

rightchild=parent*2+2;

parent=(leftchild-1)/2;

堆可以分为大根堆(最大堆)和小根堆(最小堆)

  • 小根堆:根结点的值最小,且父结点的值比每一个子节点的值都小
  • 大根堆:根结点的值最大,且父结点的值比每一个子节点的值都大

以升序为例(注意升序要建大堆)

2.2.1建堆

向下调整法

前提:根结点的左右子树必须是小堆

从根节点开始,将跟节点和其两个子节点中较小的那个进行比较,如果比这个字节点小,就交换父节点和子节点的值,依次向下递归调到叶子就中止,但是这种方法有一个致命的缺点,就是左右子树必须是小堆,在大部分情况下不会刚好左右子树都是小堆,那么就要自己创建。


以上图为例,我们可以发现dhi这棵树的左右子树都是小堆,可以利用向下调整法进行排序,之后,cdehI,这棵树的左右子树也变成了小堆,可以再次利用向下调整法进行排序,就这样一次向上调整,最后把整棵树的左右子树都变成了小堆,就可以利用向下调整法进行排序了。那么我们还有一个问题。就是怎么找到dhi这棵树,其实这棵树的根节点就是整棵树的最后一个非叶子节点

2.2.2代码实现

时间复杂度

堆排序的时间复杂度是N(N*logN),空间复杂度是N(1)

3.交换排序

3.1冒泡排序

冒泡排序是一个非常经典的排序,很多人第一个接触的排序就是这个,所以这个排序的思想也是挺简单的,而且实现也不难。

将每个数进行两两比较把最小(大)的放到最后面,然后在对剩下的数进行两两比较将最小(大)的放到后面。

冒泡排序的具体步骤如下:

  1. 从数组的第一个元素开始,依次比较相邻的两个元素。如果前一个元素大于后一个元素,就交换它们的位置。
  2. 继续向后比较,直到最后一个元素。一轮比较完成后,最大的元素将会“冒泡”到数组的最后位置。
  3. 重复上述步骤,但这次比较的次数少一次(因为最后一个元素已经排好序)。
  4. 重复进行多轮比较和交换,直到整个数组排序完成。

代码实现

时间复杂度

冒泡排序的时间复杂度是O(N^2)

3.2快速排序

快速排序核心思想就是,选出一个数key,将数组中比key小的数放到key左边(不用有序),将比key大的数放到key右边,快排一般也是利用分治递归的思想,将大问题变成小问题,首先将数组不断递归分组为若干个小区间,当一个区间有<=3个数选出key,这时左边有一个或没有数且比key小右边也一样,这时这个小区间就有序了,最后再回归返回。

3.2.1挖坑法

  1. 选择基准元素(key)。通常选择序列的最左边或最右边的元素作为基准(pivot)。12
  2. 设定“坑”的位置。将基准元素(key)放置在序列中的某个位置,将其视为一个“坑”。12
  3. 移动指针。使用两个指针,一个从序列的左边开始,另一个从右边开始。目标是找到一个比基准元素小(或大)的元素,并将其与“坑”中的元素交换。
  4. 交换元素。当左指针遇到比基准小的元素或右指针遇到比基准大的元素时,停止移动,并将找到的元素与“坑”中的元素交换。这会将基准元素移动到其正确的位置。
  5. 重复过程。继续移动指针,直到左指针和右指针相遇。此时,将基准元素放置在相遇的位置,确保其左边所有元素都比它小,右边所有元素都比它大。
  6. 递归排序。对基准元素左边的子序列和右边的子序列递归执行相同的排序过程。

这种方法的关键在于正确地选择基准元素和设置“坑”,以及有效地移动指针以找到需要交换的元素。通过这种方式,快速排序可以在原地对数组进行排序,不需要额外的存储空间。

代码实现

3.2.2Hoare

  1. 首先和挖坑一样选一个key,将区间排为左边比key小右边比key大,实现的方法是
  2. 定义两个指针begin,end分别指向数组的最左边和最右边
  3. 然后开始找数,因为我们的key是选的左边第一个,所以要先从右边开始找大,在右边找到一个比key小的数后,再在左边找一个比key大的数
  4. 交换两个指针所指向的位置,再继续找数
  5. 直到begin和end相遇,接着把key指向的值换到begin和end相遇的位置
  6. 然后递归对begin和end相遇的位置的左右区间再做操作,最后回归

三数取中法

有人可能会想到,当每次递归后key的值选到的都是最小值那么每次分的左右区间的大小会相差很大,换句话说就是要递归很多次(就是遇到有序数组时),这时排序的时间复杂度会变成O(N^2),三数取中就是把左边,右边,中间三个数拿出来作比较。将中间值的那个交换到数组开头的位置,这样就可以保证每次选到的key大小都是较为中间的。

3.2.3前后指针法

  1. 首先定义两个指针prev(在后),cur(在前)让cur去找比key要小的数
  2. 当cur找到比key小的数后,让prev++然后交换两个指针所指向的值(因为prev在前,cur只会找比key小的数,所以cur走过的地方都是比key大或等于key的,也可以理解为cur在筛选比key大的数来让prev交换,除非cur与prev的下一个重合)
  3. 最后cur走到数组末尾交换prev与key指向的值

代码实现

3.2.4非递归实现快速排序

非递归的方法,要借助栈(数据结构里的)(用挖坑法举例)

每次分割数组后,把左右两边的区间的左右下标压栈,依次取出再分割排序压栈,以此循环直到栈为空就结束。具体思路在图片和代码注释里

时间复杂度

最优的时间复杂度是O(nlogn),最差的O(n^2) ,因为用了三数取中,不存在最差情况。

4.归并排序

归并排序需要用到分治的思想,就是把大问题分治成若干个小问题,再把小问题分治成更小的问题知道无法在分,然后再将小问题合并成大问题,那么看到这里,是不是很容易就联想到了递归的思想。

4.1递归实现归并排序

递归是一个比较常见的方法,在讲归并排序之间,先来探讨这个问题,如何把两个有序数str1,str2组合并成一个有序数组?我们可以用这样一个方法,创建一个新数组str,然后从str1,和str2的第一个位置开始依次向后比较,当str1的值小于str2时就将str1的值放到新数组中,反之将str2的值放到新数组中(就是依次比较将两个数组,将较小的那个数放到新数组中),直到一个数组中的所有数都放到了新数组中,最后将另一个数组中剩下的所有数都放到新数组,这样就完成了合并。这就是归并排序中,并的思想。

然后再回来,如果一个数组的左右两边都是有序的话,那么是不是就很好排序了,归并排序就是,先将数组分成两份,当这两份是有序的时候,就可以并了。对这两个部分进行递归一直分,分成更小的部分,当数组两边都只有一个数时就可以认为,这两个部分是有序的然后递归回去,开始合并再返回。

代码实现

4.2非递归实现归并排序

这里我们利用循环来实现,整体思想和递归一样都是先分再并,但是有不同的是,我们不需要再创建一个新数组,直接在原数组操作就可以。而且递归是从一个数组慢慢分成,若干个小数组,迭代是直接分成若干个小数组,慢慢合并成一个有序数组。

假如要排序的是[8,7,6,5,4,3,2,1],那么首先定义一个步长gap(每组数据个数),gap==1时[8,7][6,5][4,3][2,1]两两合并(一个区间),[7,8][5,6][3,4][1,2],gap==2时[7,8,5,6][3,4,1,2]]两两合并(一个区间)[5,6,7,8][1,2,3,4],gap==4时[5,6,7,8,1,2,3,4]合并[1,2,3,4,5,6,7,8]。

//不过,要注意边界和特殊情况,比如:

  • 没有右区间,在合并时某一对只有左区间[7,6,5,4,3,2,1]在最后1没有区间和他合并(只会在最后)。
  • 右区间不够gap,比如[8,9,6,7][4,5,3]
  • 左区间不够gap 比如[8,9,6,7][5]

具体解决方法写在代码注释里了

代码实现

时间复杂度
O(NlogN),可以看出他的递归过程中每次都将一组平均分,分完后大概是logN,空间复杂度O(N)

小区间优化

小区间优化可以减少递归次数,当要排序的数据很大时,用递归快排就会递归的比较深,效率方面先不说,递归次数太多可能会造成栈空间不够,所以当数据量少时可以用插入排序等,反而比递归更好。所以当快排到一定深度时这时,数组已经接近有序而且数据量不多,可以用插排来减少递归次数。

5.计数排序

计数排序的思想相较于前几个并不难,就是把数组中所有的数出现的次数都记录下来,存在一个数组中然后再依次按照每个数出现的次数,将每个数从小到大依次打印。具体方法如下

首先统计每个数出现的次数,然后创建一个数组(数组大小和原数组中数据的最大值一样,这样是为了可以记录的下每个数),把待排序数组中的元素值作为下标,接着遍历原数组,使新数组在该下标下的值加一,这样就可以记录每个数出现的次数,其实在统计过程中,就相当于再排序了,最后只需要,将新数组的值从小到大依次打印就行。

不过有时候,数组中会出现较大的数这时,创建的新数组的空间就会比较大,不仅浪费空间,还会影响效率。这时我们可以利用相对位置来创建新数组,就是我们要排序的部分其实就是最大值到最小值这段区间就行,在创建新数组时数组大小只需大于等于(最大值-最小值)就行。所以在排序计数前,要先算出最值之差。不过再计数时也要把每个数都减去一个最小值(就是该数在新数组中的相对位置)才可以,在最后打印时再加上一个最小值。

//不过这个排序有一个致命的缺点,因为数组下标只能是整数,所以这个排序只能拍整型数据

代码实现

时间复杂度和空间复杂度

计数排序的时间复杂度是O(N+range)range是最值之差加1,所以当range较大时效率就会变低,空间复杂度是O(max-min),就是我们开的数组是这个区间的范围差。

6.总结对比

6.1八大排序的效率对比

1. 冒泡排序和选择排序均为简单直观的排序算法,但是在大规模数据下效率较低。

2. 插入排序在部分有序序列下具有较高的效率,但在一般情况下效率低于其他算法。

3. 希尔排序通过分组插入排序的方式提高了效率,但在最坏情况下仍然是较慢的排序算法。

4. 归并排序和快速排序都是基于分治思想的排序算法,具有较高的效率。其中,快速排序是最常用的排序算法之一,但在最坏情况下会退化为较慢的排序算法。

5. 堆排序利用堆这种数据结构的特性进行排序,虽然在时间复杂度上与归并排序和快速排序相近,但由于对数据的访问方式不连续,对缓存的利用不够充分,所以实际运行时间可能较长。

6. 计数排序是一种特殊的排序算法,适用于某些特定场景。

综上所述,选择合适的排序算法需要根据不同的排序需求及数据规模来评估各个算法的性能。对于小规模数据,简单的排序算法已经足够高效;而对于大规模数据,通常选择归并排序、快速排序或堆排序等高效算法会更加合适。同时,也应根据实际情况考虑算法的稳定性、额外空间的需求等因素。

6.2八大排序稳定性对比

稳定性判断的依据是,在排完序后看相同的值的相对的位置是否发生改变,若不变则是稳定的比如:1,4,7,3,5,3,9在排完序后加粗的3是否还在没加粗的3前面

冒泡排序,可以人为设定相同的值不交换,所以稳定。

直接选择排序,在交换过程中,可能两个数中间有个数和要交换的两个数中的一个相同这样就会发生改变,所以不稳定。

直接插入排序,当要插入的数和前面的数相同就插在它后面所以稳定

希尔排序,因为它是分组与排序,在与排序过程中很有可能会发生改变所以不稳定

堆排序,虽然它也是排数组,但是它的逻辑结构是一颗二叉树,很容易就发生较大的位置改变,所以不稳定。

归并排序,可以人为设计当两个数相同时先放左区间还是右区间,所以稳定

快速排序,当有和key相同的数时,因为key会移动到后面的位置(一般key选第一个),所以不稳定

计数排序,这个是统计每个数出现的次数,不会在意数的位置,所以也不稳定

稳定性的一个作用是保证相同数据之间也有一个顺序。

6.3空间复杂度和时间复杂度对比

八大排序的算法到这里就完了,感谢你的阅读!!,也希望这篇博客对你有帮助,能让你有所收获,谢谢。最后也希望给小编一个三连(点赞,收藏,评论),如果认为小编写的还不错,也可以关注小编写的其他文章,或者推荐给其他有需要的人(小编真的做了很多功课尽量讲好每一个细节)。(QAQ)!!!,如果有什么疑问或者介绍的不好以及错的地方也欢迎大家在下方留言或者私信小编,万分感谢!!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值