欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 锐评 > 排序篇(七大基于比较的排序算法)

排序篇(七大基于比较的排序算法)

2024/11/29 19:16:56 来源:https://blog.csdn.net/L2770789195/article/details/142323125  浏览:    关键词:排序篇(七大基于比较的排序算法)

目录

插入排序

直接插入排序

希尔排序(缩小增量排序)

选择排序

选择排序

堆排序

交换排序

冒泡排序

快速排序

1.挖坑法

2.Hoare版

3.前后指针

快速排序优化

三数取中法 选基准数

2.递归到小的子区间时 可以考虑使用插入排序

非递归快速排序

归并排序

归并排序----非递归

其他非基于比较的排序

计数排序


几种常见的排序算法:

        1.插入排序:直接插入排序、希尔排序

        2.选择排序:选择排序、堆排序

        3.交换排序:冒泡排序、快速排序

        4.归并排序:归并排序

接下来我们一一讲解

插入排序

直接插入排序

思想:

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

简单点说 就是在已排好序的区间中 找到当前元素(无序区间)的应插入位置

代码实例:

特性:

时间复杂度:O(N^2)

空间复杂度:O(1) 它是一种稳定的排序算法

这里我们对稳定做下解释 当一个数组中 当采用某一排序算法时 重复数据原先在前面的仍在前面 这便称之为稳定

关于时间复杂度的分析:

· 最好情况下 也就是有序的时候 我们不需要移动元素 每次只需要比较一次即可 那么最好情况时的时间复杂度为O(N)

· 最坏情况下 也就是待排序区间为逆序的情况

例如: 9  8  7  6  5 (需要排为升序)

此时需要比较 1+2+3+4=10次

如果有n个元素的话 则需要比较1+2+3+....+(n-1)=n*(n-1)/2

所以最坏时间复杂度为:O(N^2)

空间复杂度分析:

插入排序不需要额外的存储空间 所以其空间复杂度为O(1)

稳定性分析:

根据插入排序思想可知 我们只会移动比temp值大的元素 所以我们排序后可以保证相同元素的相对位置不变 所以直接插入排序是稳定的

希尔排序(缩小增量排序)

思想:先选定一个整数gap  把待排序文件中所有记录分成多个组 所有距离为该整数gap的记录分在同一组内 并对每一组内的记录进行排序 然后 重复上述分组和排序的工作

直到该整数=1时 所有记录最后再统一排好序

特性:

1.希尔排序是对直接插入排序的优化

2.当gap>1时 都是预排序 目的是 让数组更接近于有序 当gap==1时 数组已经接近有序 这样就会很快 这样整体而言 可以达到优化的效果

3.希尔排序的时间复杂度不好计算 因为gap的取值方法有很多 导致很难去计算 因此在好些书中给出的希尔排序的时间复杂度都不固定

4.当然 希尔排序是不稳定的

代码实例:

选择排序

选择排序

基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素 存放在序列的起始位置 直到全部待排序的数据元素排完

例如升序:每一次从待排序的数据元素中 找到最小的那个元素 存放在序列的起始位置

代码1:

关于选择排序 我们还有另一种方法:同时找到最大和最小的元素 的下标 每次排好两个元素

代码实例:

时间复杂度:O(N^2)

空间复杂度:O(1)

稳定性:不稳定

堆排序

堆排序(HeapSort)是指利用堆积树(堆:完全二叉树)这种数据结构所设计的一种排序算法 它也是选择排序的一种 它是通过堆来进行选择数据

需要注意的是:排升序要建大堆  排降序要建小堆

现在我们来理解一下 什么是大/小堆

大堆:每个结点的值都不大于其父节点的值(也就是 结点的值较大)

小堆:每个结点的值都不小于其父节点的值(也就是 结点的值较小)

大堆(结点的值较大):

数组{6,7,9,4,5,1,3}建好大堆如下图:

小堆(结点的值较小):

看了这两张图后 我们发现 即使建好大堆/小堆后 仍然需要排序

大概流程:建大堆(升序)---->然后需要将根节点和从最后一个元素进行交换(根节点的值是最大的)  然后再重新建大堆  直到每个元素都与根节点交换过

现在呢 我们先进行建大堆:

首先呢 先创建parent child两个变量

parent指向最后一棵树的根   child指向最后一棵树的子树  然后依次将每棵树置成大堆

即:让child指向子树元素较大的 若child指向位置的元素大于parent处的元素 则交换

然后令parent-- 直到parent走到0下标处

接下来我们给出上述图片转化为大堆的过程图

当child指向超过下标最大值时 siftDown结束

展示代码:

在创建好大堆以后 我们就需要将根节点的数 与end指向的元素 进行交换

然后再重排大堆 让end--

整体代码:

时间复杂度:O(N*logN)

空间复杂度:O(1)

稳定性:不稳定

交换排序

冒泡排序

这里我们就不对冒泡排序进行讲解了 在之前有讲过

如果大家有些遗忘 可以通过下列链接进行复习

https://blog.csdn.net/L2770789195/article/details/137197253

快速排序

基本思想:任取待排序元素序列中的某元素作为基准值 按照该基准值 将待排序集合分割成两个子序列 左子序列中所有元素均小于基准值 右子序列中所有元素均大于基准值 然后重复该过程 直到所有元素都排列在相应位置上为止

1.挖坑法

我们先给个排序过程图:

代码实现

2.Hoare版

这个跟挖坑法也很相似 只是没有坑 当选择L处的元素作为基准值时 先让R移动 找到小于基准值的数 让L移动 找到大于基准值的数 然后让L、R两处的元素进行交换 当L、R相遇时 让相遇处的元素与基准数进行交换 然后在L、R相遇处的左右两侧接着重复上述步骤

3.前后指针

初始时 prev指针指向序列开头  cur指针指向prev指针的后一个位置

cur指针向后移动 寻找比基准值小的元素

当找到比基准值小的元素时 如果 ++prev和cur不相等 则交换prev和cur位置处的元素

这样做的原因是:cur和prev本身相差一个距离 当cur碰到几个大于基准值的元素时 prev就会与cur的距离增加几个距离 prev会停留在大于基准值处  当cur再次找到比基准值小的元素时 则可以把小于基准值的元素与大于基准值的元素进行交换 直到cur走到数组之外

代码实例:

总结:在这三种方法中 我们使用挖坑法最多 其次是Hoare法 至于前后指针已经使用的很少了

对于快速排序的过程 其实可以形象地用递归树来表示

具体来说 快速排序的递归树中的每个结点代表一次递归调用 左子树代表 对小于基准值的子数组进行排序的递归调用 右子树代表对大于基准值的子数组进行排序的递归调用

快速排序优化

  1. 三数取中法 选基准数

我们前面的基准数 选择的都是Left处的元素(也就是每个数组的最左侧元素)

但基准数当选择的过大或者过小时 会让数组排序的次数增加

如下图:

我们可以看到当选择的基准数过大时 会导致每次只排好一个元素或者很少的元素

而如果当选择的基准数为中间的数时 每次会排好一半的元素  大大减少了时间

所以我们三数取中法是 在最左侧、最右侧和中间的数中 选择某一位于两者区间的数

代码实例:

2.递归到小的子区间时 可以考虑使用插入排序

我们知道快速排序 每次会数组分成两个部分 类似于二叉树

这里 我们借用下之前的图:

在这里我们更加明显的看到了二叉树的样子

我们把每个圈当成一个数组 每次排序就会分成两个数组

当某一数组被分成另外几个数组时 也就是某一次递归 递归到剩余的几次 我们就可以采取直接插入排序  

我们在前面都是用递归实现快速排序的

现在我们来学习一下非递归的快速排序

非递归快速排序

我们知道在使用递归的快速排序中 递归起到了将数组一分为二的作用

所以我们的非递归排序的核心就是 如何将数组一分为二

这里我们需要一个栈来配合

先利用自已写过的代码(partition)找到基准数要替换到的位置--pivot

注意:partition在找到基准值要替换的位置的同时 也进行着将小于基准数的划分到左边 将大于基准数的划分到其右边

让基准数左右两侧数组的起始和结尾的下标入栈

注意:当基准数某一侧只剩下一个元素时 代表着已经有序 不需要再进行排序  

当pivot满足pivot>left+1或pivot<right-1 代表着至少有两个元素---》即需要入栈 排序

代码如下:

时间复杂度:O(N*logN)

空间复杂度:O(logN)

稳定性:不稳定

归并排序

基本思想:

归并排序(Merge Sort)是一种分而治之的排序算法。它将一个大的列表分成两个小列表,分别进行排序,然后将排序好的小列表合并成一个最终的排序列表。这个过程递归地进行,直到子列表的大小为1(因为只有一个元素的列表自然是排序好的)

归并排序算法主要包含两个主要步骤:

分解:将数组分解成两个较小的子数组,直到子数组的大小为1。

合并:将已排序的子数组合并成一个大的有序数组,直到合并为1个完整的数组

过程实例:

代码实现:

归并排序----非递归

在递归的归并排序中 递归的作用在于将数组进行分解

所以我们在 非递归中主要是将数组进行分解

我们利用gap当作每个子数组的大小

利用left mid right来指示要合并的区间

代码实现:

时间复杂度:O(N*logN)

空间复杂度:O(N)

稳定性:稳定

其他非基于比较的排序

计数排序

思想:计数排序又称为鸽巢原理 是对哈希直接定址法的变形应用

操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

例如:

但也有一个问题 假如数组arr存放的是 {90,95,91,92,90,92}  tmp数组难道还要申请100个空间吗?

其实不然  我们仍然只需要申请max-min+1个空间 即:6个空间(下标:0~5)

只需要在放入tmp数组时 让 下标=元素-min

在tmp数组中 取出元素放入arr的时候 让元素=下标+min 即可

代码实现:

计数排序总结:

  1. 计数排序在数据范围集中时 效率很高 但是适用范围及场景有限
  2. 时间复杂度:O(N+(arr的长度))
  3. 空间复杂度:O(arr的长度)
  4. 稳定性:稳定

排序算法总结:

版权声明:

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

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