循环首次适应算法_数据结构与算法之2——排序问题

排序真的是数据结构与算法中的重中之重啊,无论是对编程能力的提升,以后工作后的应用,或者是应对面试的时候都是经常需要用到的。基本的几个经典排序一定要做到滚瓜烂熟,能够做到给你一个具体的排序算法题,一定能短时间内选出最适合的排序算法(时间和空间复杂度以及根据具体所给的条件),并且不假思索的写出代码。只有这样才能算是技能过关。

(本文所有代码实现都是基于python语言,很多代码是自己设计和手打,如有错误之处欢迎大家指正)

在讲解具体的排序算法之前,先理清一些关于排序的概念:

内排序与外排序:在排序工作中,如果待排序的数据都保存在内存里,称为内排序;针对外存(磁盘、磁带等)数据的排序工作,称为外排序,例如归并排序。

稳定性与适应性原则:排序时,算法的选择必须遵从稳定性和适用性原则。稳定性是指当排序遇到相同值大小的排序码时,虽然随意排序都不会影响排序结果,但是在一些具体的实际情况下,要保持相同排序码的相对位置保持不变。适应性是指选择排序算法的时候要尽可能考虑所有具体排序问题的啊情况,比如遇到接近排好序的队列甚至是已经排好序的队列,是否能够使运行速度更快?而不是对待所有可能的排序队列都是花费一样的时间。在实际工作中需要应对的排序问题很多都是已经接近排好序的。这就是适应性的问题。

1. 插入排序法

36934dc4c6918ee8aac05346638f64c9.png

从图中可以看出插入排序法的基本原理(以升序为例):假设排序列表为

,从
开始依次和之前的数(
)比较,如果
则将
插在
之前,否则在之后。然后
依次和前面的数(
)比较,若
,则插在列首,否则往后继续与
比较,以此类推。

notes: 需要比较的元素成功插入以后,插入位置以后的所有排好序的元素要依次往后移。

def  

算法的空间复杂度为o(1),在表内排序,最佳时间复杂度为o(n)(列表本身为有序),只有外循环n-1次的遍历时间。最差复杂度为o(n^2),假设内循环每次都是插入第一个元素,导致后面所有有序元素都要逐个后移。平均复杂度为o(n^2)。

2. 选择排序法

选择排序法的基本思想:

  • 维护需要考虑的所有记录中最小的i 个记录的已排序序列。
  • 每次从剩余未排序的部分挑选出最小的记录,把它放在已排序部分的后面作为第i+1个记录,已排序序列增长。
  • 以空序列为排序的开始,直到未排序部分只剩一个元素时,直接将其放入已排序部分最后一个,整个排序工作就完成了。

简单选择排序:

顺序扫描列表中的每个元素并找到最小的,将其放入另一个列表中,然后继续扫描剩余的最小的元素继续添加,直到原列表中所有元素都添加完成。时间复杂度为o(n^2)(每个元素每都要扫描整个列表),空间复杂度o(n)(新引用了一个列表)。

def 

直接选择排序:

与简单排序算法不一样的是,直接排序算法直接在表内完成,不需要新引入一个列表进行排序工作,空间复杂度为o(1)。

def 

相比于之前的简单排序,空间复杂度为o(1),但是时间复杂度并没有减少,仍然需要两次循环,复杂度仍然为o(n^2),并且不具有稳定性,不能保证相同元素保持相对空间位置。效果要低于插入排序法,因此选择排序法很少被使用。

堆排序算法:

堆排序算法在之前的《数据结构与算法——树与二叉树》的文章中提及过,它同样也是一种选择排序的方式,与前两种选择排序方法不同的是用的优先队列的思想选出最小值,以此类推实现表内的排序。这种方式空间复杂度同样为o(1),时间复杂度为o(nlogn),优于前两种算法。但是堆排序同样有稳定性和适应性的问题,即不能保证相同元素的相对位置,并且每次取出最小值,由堆的性质可知,会从堆尾取出元素重新堆排列。

def  

3. 交换排序法(冒泡法):

冒泡排序法是一种典型的需要清除逆序对来达到排序效果的排序算法,通过比较相邻的记录,如果存在逆序则反转,通过反复的比较和反转,最终完成顺序的排序。

d1626229cf757d8929357a2fd8090182.png

一次完整的扫描可以保证把最大的元素移动到列表末位,通过一遍遍扫描,表的最后将累积会越来越多排好顺序的大元素。经过n-1次扫描一定能完成所有排序。

def 

仔细分析冒泡排序法可以知道,假如最小元素正好在列表末位,则整个外循环需要n-1遍,当然这也仅限于恰好只是这种情况之下。其他一般情况,扫描并不需要这么多次。为了增加该算法的适应性,做出了一些修改,即若某次扫描过程中未遇到逆序,则是已排好序的,直接返回列表就好了。

def  

冒泡排序法最坏情况时间复杂度为o(n^2),平均时间复杂度也为o(n^2),改进的方法在最好的情况下时间复杂度为o(n)。冒泡排序方法是一种原位排序,空间复杂度为o(1)。

传统的冒泡排序法效率比较低,实际结果劣于复杂度相同的简单插入排序法,主要是因为在反复反转的时候赋值操作较多,累积起来代价比较大,另外一些距离最终位置较远的的记录有可能拖累整个算法进程。要缓解这两个问题,这里主要介绍2种改进的方法,一种是交错冒泡排序法,另一种是快速排序法,此方法将被列为第4种排序方式在后文进行解释

交错冒泡排序法

具体的做法是一遍从左往右扫描,一遍从右往左扫描,交替进行。下图只用了两遍向右和两遍向左就完成了排序。

3570c559637750254cba7ba6a6a295ec.png
def 

4. 快速排序法

快速排序法曾经被称为“20世纪最具影响力的算法之一”,在各种基于关键码比较的内排序算法中,快速排序是实践中平均速度最快的算法之一。

快速排序方法的基本操作过程为:

  • 选择一种标准,将未排序序列中的记录按照此标准分为大小两组,此时,两组的顺序已定,较小一组的记录排在前面。
  • 采用同样方式,递归的划分这两组记录,并继续递归的划分下去。
  • 划分总是得到越来越小的分组,如此下去直到每个组最多包含一个记录时停止,整个序列排序完成。

采用递归的方式:

def  

最近在秋招,(话说今年计算机竞争实在是太大了,本非科班菜鸡留下了没有技术的泪水),看了很多面经,发现链表的快排实现是一种高频率的手撕算法面试题,因此必须得掌握,其思想和列表的快排一样,只不过操作略微复杂。

链表的快排

class 

如果每次划分的时候两个部分相等,则只需要logn层的划分就可以使每层记录数为1,每划分一次的比较次数不超过序列的长度,因此综合来说,时间复杂度为o(nlogn)。快速排序最坏情况复杂度为o(n^2),即每次分组都有一个是空组,另一组只比本层划分前少一个记录。空间复杂度最坏的情况为o(n),但是递归的深度一般为o(logn)。因此空间复杂度一般为o(logn)。但是这种算法不算太稳定,可以很明显看出,这种算法不会因为序列本身有效而更高效,不具有适应性。

5. 归并排序法

归并排序即将两个或多个有序序列合并为一个有序序列,其基本方法为:

  • 初始时,将待排序序列的n个记录看成n个有序子序列(只包含一个记录的序列一定是有序的),每个子序列长度为1。
  • 把当时序列组的有序子序列两两归并,完成一遍后序列组里的排序序列个数减半,每个子序列的长度加倍。
  • 对加长的有序子序列重复上面的操作,最终得到长度为n的有序序列。

归并排序适合处理存储在外存的大量数据,许多实际的外村数据集排序算法都是基于归并排序实现的。因为外存数据比较适合顺序处理,但不适合随机访问。此外,归并操作过程中对数据访问具有局限性,适合外存数据交换的特点。特别适合处理一组记录形成的数据块。

顺序表的归并排序

59f12bd5350ba7096155e6bebf5b314d.png

二路归并排序,在初始状态时,每个记录自成一个有序数列。第1遍将这些序列归并为一组长度为2的有序序列,注意其中的44没有归并对象,原样留到下一步,第2步归并出3个长度为4的有序子序列,最后一个子序列中记录数不足4个,再经过两次归并,最后得到全排序序列。

归并排序不太适合原地排序,即实现空间复杂度为o(1)的实现方法,为了实现该方法,必须必须付出空间代价,即开辟一片同样大小的存储区,也就建立了另一个同样大小的表,把一遍归并的结果放入那里。原来的表闲置,下一次归并操作,将结果再放入闲置的表里,这样交错的循环放置,直到最后完成排序。

def  

这里是《数据结构与算法》提供的官方代码,感觉会有点晦涩难懂,这里提供一份比较简洁的代码以供参考。[1]

def 

自然的,既然列表的归并排序需要掌握,那么链表的归并排序自然也需要掌握(面试高频题),这里贴出代码

链表的归并排序

class 

做完k遍归并后,有序子序列长度为2^k,完成整个序列的归并不超过

次,每遍归并做的比较次数为o(n),总的时间复杂度为o(nlogn)。空间复杂度上因为引用了一个新的表,因此空间复杂度为o(n)。上述算法具有稳定性但是没有适应性,无论什么序列都需要做出
遍归并。

python自建的sorted函数

python自带的sorted函数所使用的排序算法为蒂姆排序,结合使用了归并排序和插入排序,最坏时间复杂度为o(nlogn)。该算法具有适应性,在排序数组几乎排好序的情况下时间复杂度远小于o(nlogn),可能达到线性时间。蒂姆排序最坏情况需要n/2空间,因此空间复杂度为o(n)。蒂姆排序经过大量实验证明平均性能超过快速排序,是目前最好的排序方法。蒂姆排序主要是克服了归并排序适应性的缺陷,又保持了其稳定性的关系,尽可能利用实际数据情况。

蒂姆排序基本过程如下:

  • 考察待排序序非严格单调上升或严格单调下降的片段,反转其中严格下降片段。
  • 采用插入排序,对连续出现的几个特别短的上升序列排序,使整个序列变成一系列单调上升的纪录片段,每个片段长于某个特定值。
  • 通过归并产生更长的片段,控制这一归并过程,保证片段的长度尽可能的均匀,归并中采用一些策略,尽可能减少临时空间的使用。

6. 几种经典排序算法时间和空间复杂度的归纳总结

8fb668397b57bf2ea8aa018e4c581ee8.png

参考书籍:《数据结构与算法—python语言描述》—裘宗燕

参考博客:

链表排序算法的python实现 - 公梓小白 - 博客园​www.cnblogs.com

参考

  1. ^归并排序详解(python实现) https://www.cnblogs.com/piperck/p/6030122.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值