排序(Sorting)相关算法(理论篇)

排序(Sorting)相关算法(理论篇)

排序(Sorting)是将一组数据序列按照关键字的非减或非增的顺序排列起来的操作。将待排序内容全部放在内存中的排序叫做内部排序,一次放不进来需要多次读取外存的叫外部排序,现在介绍内部排序。

内部排序有许多方法,常见的有:Straight Insertion Sort,Shell Sort,Quick Sort等。可以分为如下几类:

——————— TABLE STARTS ———————-

插入类排序

直接插入排序(straight insertion sort)
折半插入排序(Binary insertion sort)
希尔排序(Shell sort)

交换类排序

冒泡排序(bubble sort)
快速排序(quick sort)

选择类排序

简单选择排序(simple selection sort)
树形选择排序 (tree selection sort),或 锦标赛排序(tournament sort)
堆排序 (heap sort)

归并类排序

归并排序(merge sort)

分配类排序

基数排序(radix sort)

———————– TABLE ENDS ———————–

下面分别简要介绍其排序原理(无代码):

插入类排序

设想在打牌的时候,在牌堆里摸一张牌后,我们需要对新加入的牌和已有的牌进行比较,将新牌插入手中的牌中。插入类排序基本就是基于这样的思想,将乱序的部分看成牌堆,有序的部分看成手中的牌,然后依次从无序的中选择一个进行插入。主要有下面几种实现:

直接插入排序(straight insertion sort)【 O(n^2) ,O(1) 】

这是最简单的方法,首先,从第2个元素开始,把现在的元素左边的元素作为已经排序好的(后面的叙述中默认我们要求的排序是升序排列),如果这个元素比左边有序序列的最后一个,也就是该元素的前一个大,那么就不管它,继续向右移动,如果比有序的最后一个小,那么说明要进行插入操作,那么从有序序列的右边向左边依次比较,找到第一个比待插入元素小的元素,那么待插入元素就应该插在它的右边。依次对所有元素进行,就可以实现直接插入排序。

为了排序方便,我们常将数组的第一个元素设置成【哨兵】,即如果需要插入操作的话,首先将待插入元素放入哨兵,然后每比较一次如果比哨兵大,那么可以直接右移,因为最右边的已经在哨兵中暂存了。如此直到一个小鱼哨兵的位置,这时候我们把哨兵的元素放到它的右边就可以了。

直接插入排序的时间复杂度为 O(n^2),空间复杂度O(1)。是稳定排序。适合初始记录基本有序的情况。

折半插入排序(Binary insertion sort)【 O(n^2) ,O(1) 】

折半插入排序是将直接插入的查找过程通过二分查找实现,利用了已经排序好的序列部分的有序的性质,可以减少比较次数,但是由于移动次数不变,因此时间复杂度和空间复杂度与直接插入一样。也是稳定排序。适合初始记录无序,n较大的情况。

希尔排序(Shell sort)【 低于O(n^2) ,O(1) 】

希尔排序本质上是一种【分组插入排序】, 希尔排序维护一个增量序列【d1,…,dn】,每次从增量序列中递增地取一个值dk,然后以dk为间隔,在要排序的数列中取值,得到间隔为dk的一组值,然后进行插排,然后同样间隔右移一个单位,重复上述操作,等都重复完了以后,再取dk+1间隔,仍然以这样的一组为单位进行插排。因此直接插排可以看成是增量序列为【1】的特殊的Shell 排序。

可以这样理解,shell sort把数据分成了d1,d2,。。。,dn个组(间隔数就是组数,因为考虑到要移动 0 – dk-1 )然后分组插排,这样的好处就是通过大间隔的插排,使得数列已经基本有序,这样对于普通的插排可以提高效率。

增量序列的取法要满足:所有序列中的数字没有除了1以外的公因数。并且最后增量为1。

以及,这个排序法的时间复杂度和增量序列有关,不太容易计算。总之要比直接插排好。而空间复杂度仍为O(1)。

交换类排序tongguo

所谓交换类排序的思路是,遇到一个不符合前小后大的两个数,都进行一次交换,这样就可以把所有的都排整齐。

实际上,这种思路归结起来就是四个字:消除逆序。只要逆序全部消除,排序就完成了。最常见也是思路和实现最简单的是冒泡,bubble sort,有的地方也叫起泡排序。还有另一种特殊的复杂一点的交换方法,就是QuickSort,快排。下面分别说明:

冒泡排序(bubble sort)【 O(n^2),O(1) 】

实际上,这个排序方法就是依次进行相邻两个数的逆序消除,从无序部分的第一个到最后一个的依次遍历就是一趟,(确切的说,所谓一趟的定义是,使得有序序列中增加一个元素),因为每次这样都可以把最大的从左到右沉下去,而遍历多趟以后慢慢将小数冒出来,这就是冒泡排的名字的由来。设想如果从左到右,最大值每次都会和右边比较一下,所以它一定会一路把右边的值都推上去,从而自己沉在最下面,每次拿出一个最大值就把无序的长度减一,这样再把次大的拿出来,依次类推,即可全部排好。

冒泡是稳定的,由于两两比较,然后要比较的序列长度也是线性减少,所以明显为O(n^2),空间复杂度就是交换的那一临时变量,所以是O(1)。

快速排序(quick sort)【O(nlogn),O(logn) ~ O(n)】

QuickSort是一种较为特殊的交换排序,instead of 冒泡的相邻两两交换,快排可以一次性消除多个逆序。比如对于一个数列【4 2 3 1】,冒泡的话需要 42 43 41 才能交换到【2 3 1 4】,然后到【2 1 3 4】,再到【1 2 3 4】,而快排的想法是,如果我们直接把 1和4调换位置,那么可以只用一次,就消除了所有逆序。

快排是这样操作的,首先,选中一个数字,把它作为枢轴(pivot),将小鱼pivot的放到pivot的左边,大于的放到右边,这就是我们的目的。那么如何放呢?首先,我们维护两个指针,一个low,一个high,如果low小于high,也就是说还可以移动,那么当low指向的key小鱼pivot的话,就把low往高出移动,而同样的,如果high大于pivot,则向低处移动。这样很明显,如果都满足的话,它们会在pivot在有序数组中应在的位置相遇。那么如果不满足,即比如low指向的反而大于pivot,或者high指向的小鱼pivot,那么我们将这两个数交换,就可以消除这种逆序。

具体的实现方法是这样:首先,把最左边的元素作为pivot,然后把low放在最左边的pivot位置,high放到最右边,当然这个过程中还要维护一个pivotkey变量来暂存这个值,为交换腾出空间。这样先移动high,直到找到一个比pivotkey小的值,于是high停下来,把指向的内容赋予low,然后low开始右移,直到比pivotkey大的,low停下,把内容给high,然后high在继续。。。如此下去,知道low==high,此时的位置就是pivotkey的位置。为甚么捏?因为low移动的条件是,high指向的某个小鱼pivotkey的元素已经被交换到low此时的位置上了,所以low指向的位置小鱼pivotkey了,所以low才能走。也就是说,low移动到了某个位置,说明它前面的位置必然都小鱼pivotkey,high指针同理,所以这个相遇点就是pivot应在的位置,于是把pivotkey赋予这个位置。

实际上,每次我们都能以pivot为中心,左边小鱼pivotkey,右边大于。那么递归调用,把high用pivot,low用上次的low,以及low用pivot+1,high用上次的high。由此下去,即可得到全部的排列。

快排由于移动或者交换是非顺次交换的,所以不稳定,适合于初始无序,n较大,在平均情况下最快。

时间复杂度为O(nlogn),这是平均。空间复杂度就是调栈的深度,最好就是logn,最坏是n。

选择类排序

选择类排序的基本思路是:把要排序的数组的最小值选出来,放在最小的位置,然后从第二个开始,再选次小的,。。。以此下去就可以排好。

简单选择排序(simple selection sort)【 O(n^2),O(1) 】

基本思路很简单,就是从左到右,依次选择后面无序序列中最小的,放在有序的后面,使得有序长度加1。具体实现是这样:首先把想要放到的位置记做i,也就是无序的第一个位置,然后在后面找到最小的返回其位置k,if not k == i,则需要把i和k对应的元素交换,两重循环下去就可以排好,外循环用来移动i,内循环找min的下标。

时间复杂度为O(n^2),空间为O(1),就是交换时用的那个临时变量。

树形选择排序 (tree selection sort),或 锦标赛排序(tournament sort)【 O(nlogn),O(n) 】

既然选择排序中含有选择min 的过程,因此可以就这个进行优化,如何选择最小值,不需要逐个遍历,我们可以利用之前选择最小值时得到的信息,来简化后面的选择。树形选择就是这个思想的一个实现。我们把要排序的n个数作为叶子,然后两两比较,将最小的放到上面做子树的根,这样就可以得到最小的。得到后,把最小的那个数在叶子上置为inf,从这个点网上,重新比较这一条路径,直到root,这时候就是次小,以此类推。

这个比较一次就是tree的深度,要n次比较,所以O(nlogn),但是需要额外的空间,叶子n个,所以总共node也是n的线性倍数。

堆排序 (heap sort)【 O(nlogn),O(1) 】

堆排序利用了最大堆的调整过程,通过堆来实现在O(1)里对list进行选择排序。首先,建立最大堆,也叫大根堆,顾名思义就是根节点的key大于两个儿子的key,建立好以后,我们就得到了最大值,然后把最大值放在最后,也就是和堆的最后一个元素交换,并且不考虑这个元素,把剩下的n-1个元素重新调整为堆,依次下去,就可以得到一个排序。

堆排序的时间复杂度取决于筛选法调整堆的过程,筛选n-1次,每次都有logn的级别的操作,所以时间复杂度是O(nlogn),而空间复杂度显然是常数,就是交换根和最后一个元素的时候的中间变量。

堆排是不稳定的。

归并类排序

归并类排序是针对两个已经排好的序列,merge一下,重新整合成一个有序序列的过程。

归并排序(merge sort)【 O(nlogn),O(n) 】

利用递归,把merge sort 的过程一直下放到两两merge,然后逐层递归回来,就可以得到多个数的排序结果。

每趟归并不超过n个key,共需要logn趟,所以为O(nlogn)。归并需要中间变量,就是merge 的输出的存放的空间,所以空间为O(n)。

分配类排序

对于特殊情况的排序,可以突破排序的O(nlogn)的界限,达到O(n)。

基数排序(radix sort)【 O(d(n+rd)),O(n+rd) 】

先根据个位数分配,在收集,在根据十位数分配,再收集,。。。依次下去,就可以得到每个位置都是顺序的结果。利用了多关键词排序的方法。

时间复杂度和空间复杂度如上,其中d是每个记录关键字的个数,r是每个关键词的取值范围。

理论部分到此位置,之后整理堆排序中关于堆的相关操作,以及所有排序的代码。

ps:
这个网页可以看各种排序算法的动画演示,效果不错:http://www.atool.org/sort.php

2018年03月11日23:58:40

美妙的假设被丑陋的事实扼杀,这是科学之大悲剧 —— 博物学家, 托马斯 亨利 赫胥黎

posted @ 2018-03-11 23:07  毛利小九郎  阅读(339)  评论(0编辑  收藏  举报