《数据结构与算法分析》读书笔记——排序

稳定的

冒泡排序bubble sort — O(n^2

鸡尾酒排序(Cocktail sort,双向的冒泡排序)— O(n^2

插入排序insertion sort— O(n^2)

桶排序bucket sort— O(n); 需要 O(k) 额外空间

计数排序(counting sort) — O(n+k); 需要 O(n+k) 额外空间

并排序merge sort— O(nlog n); 需要 O(n) 额外空间

原地合并排序— O(n^2)

二叉排序树排序Binary tree sort — O(nlog n)期望时间; O(n^2)最坏时间;需要 O(n) 额外空间

鸽巢排序(Pigeonhole sort) — O(n+k); 需要 O(k) 额外空间

基数排序radix sort— O(n·k); 需要 O(n) 额外空间

Gnome 排序— O(n^2)

图书馆排序— O(nlog n) with high probability,需要1+ε)n额外空间

不稳定的

选择排序selection sort— O(n^2)

希尔排序shell sort— O(nlog n)如果使用最佳的现在版本???

组合排序— O(nlog n)

堆排序heapsort— O(nlog n)

平滑排序— O(nlog n)

快速排序quicksort— O(nlog n)期望时间,O(n^2) 最坏情况;对于大的、乱数列表一般相信是最快的已知排序

Introsort— O(nlog n)

Patiencesorting— O(nlog nk) 最坏情况时间,需要额外的 O(nk)空间,也需要找到最长的递增子串行(longest increasing subsequence

不实用的排序算法

Bogo排序— O(n× n!) 期望时间,无穷的最坏情况。

Stupid sort— O(n^3);递归版本需要 O(n^2) 额外存储器

珠排序Bead sort — O(n) or On),但需要特别的硬件

Pancake sorting—O(n),但需要特别的硬件

stooge sort——On^2.7)很漂亮但是很耗时

 

(直接)插入排序(稳定的)

//*********此实现方法是百度百科上的,估计他人总结的。但需要较多(线性)额外空间,数据结构与算法分析的书上的插入排序不是这种描述,而是和wiki百科上的描述一样。

插入排序是这样实现的:

1、首先新建一个空列表,用于保存已排序的有序数列(我们称之为"有序列表")。

2、从原数列中取出一个数,将其插入"有序列表"中,使其仍旧保持有序状态。

3、重复2号步骤,直至原数列为空。

插入排序的平均时间复杂度为平方级的,效率不高,但是容易实现。它借助了"逐步扩大成果"的思想,使有序列表的长度逐渐增加,直至其长度等于原列表的长度。

//*********以下是wiki百科上的描述,和书上一样。

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

1.   从第一个元素开始,该元素可以认为已经被排序

2.   取出下一个元素,在已经排序的元素序列中从后向前扫描

3.   如果该元素(已排序)大于新元素,将该元素移到下一位置

4.   重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

5.   将新元素插入到该位置后

6.   重复步骤2~5

如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找排序

冒泡排序(稳定的)

冒泡排序是这样实现的:

1、从列表的第一个数字到倒数第二个数字,逐个检查:若某一位上的数字大于他的下一位,则将它与它的下一位交换。

2、重复1号步骤(每次截至到下沉点,下沉点及其后面是有序的),直至再也不能交换。

冒泡排序平均时间复杂度插入排序相同,也是平方级的,但冒泡排序是原地排序的,也就是说它不需要额外的存储空间。

选择排序(不稳定的)

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序是这样实现的:

1、设数组内存放了n个待排数字,数组下标从1开始,到n结束。

2、初始化i=1

3、从数组的第i个元素开始到第n个元素,寻找最小的元素。

4、将上一步找到的最小元素和第i位元素交换。

5i++,直到i=n1算法结束,否则回到第3

选择排序平均时间复杂度也是O(n^2)的。

不稳定性:在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中25的相对前后顺序就被破坏了。

希尔排序(Shellsort,不稳定的)

实现:通过比较相距一定间隔的元素来工作;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。也叫缩小增量排序(diminishingincrement sort)。

增量序列(increment sequence): h1, h2,~~hth1=1

希尔排序的重要性质:一个hk-排序的文件保持它的hk-排序性。保证前面的各趟排序不被后面的各趟排序打乱。

一趟hk-排序的作用就是对hk 个独立的子数组执行一次插入排序

增量序列的一种流行但是不好的选择是使用Shell建议的序列(希尔增量):ht=N/2 hk= hk+1/2。最坏情形运行时间为Θ(N2)

希尔增量的问题:增量对未必互素,因此较小的增量可能影响很小。

Hibbard增量:137~~2k-1。最坏运行时间Θ(N3/2)。相邻的增量没有公因子。

Sedgewick增量序列:{151941109~~}

希尔排序的优劣:

不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法,提高了效率。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O(),但是现今仍然没有人能找出希尔排序的精确下界。希尔排序没有快速排序算法 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O()复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序在实际使用中证明它不够快再改成快速排序这样更高级的排序算法. 插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。原因是,当n值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。正是这两种情况的结合才使希尔排序效率比插入排序高很多。

堆排序(Heapsort,不稳定的)

原地堆排序

假设我们已经读入一系列数据并创建了一个堆,一个最直观的算法就是反复的调用del_max()函数,因为该函数总是能够返回堆中最大的值,然后把它从堆中删除,从而对这一系列返回值的输出就得到了该串行的降序排列。真正的原地堆排序使用了另外一个小技巧。堆排序的过程是:

1.  创建一个堆H[0..n-1]

2.  把堆首(最大值)和堆尾互换

3.  把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置

4.  重复步骤2,直到堆的尺寸为1

堆排序的平均时间复杂度空间复杂度

归并排序(Merge sort,稳定的)

归并排序(Merge sort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法Divide and Conquer)的一个非常典型的应用。时间复杂度O(NlogN),但主要问题是需要线性附加内存(归并算法的空间复杂度为:Θ (n),因此很难用于内存排序。

归并操作的过程如下:

1.  申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

2.  设定两个指针,最初位置分别为两个已经排序序列的起始位置

3.  比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

4.  重复步骤3直到某一指针到达序列尾

5.  将另一序列剩下的所有元素直接复制到合并序列尾

快速排序(quiksort,不稳定的)

快速排序,在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n)算法更快,因为它的非常精炼和高度优化的内部循环(inner loop可以在大部分的架构上很有效率地被实现出来。

和归并排序一样,快速排序也是一种分治的递归算法。

步骤为:

1.   如果S中元素个数是01,则返回。

2.   从数列S中挑出一个元素,称为 "枢纽元"pivot),(三数中值分割法Median-of-Three Partitioning,使用左端、右端和中心位置上的三个元素的中值作为枢纽元)

3.   重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition操作。

4.   递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。

分割策略:

1. 将枢纽元与最后的元素交换使得枢纽元离开要被分割的数据段。i从第一个元素开始,j从倒数第二个元素开始(实际上,如书中的程序描述,i从第二个元素开始比较,因为第一个元素在选枢纽元已经比较过)。

2. ij的左边时,将i右移,移过那些小于枢纽元的元素,并将j左移,移过那些大于枢纽元的元素。当ij停止时,i指向一个大元素而j指向一个小元素。如果ij的左边,那么将这两个元素互换。重复直到ij彼此交错为止。

3. 分割的最后一步是将枢纽元与i所指的元素交换。

4. 如果ij遇到等于枢纽元的关键字,那么就让ij都停止。

修改快速排序以解决选择问题(selection problemk个最大/小值),叫做快速选择(quickselect)。

桶排序(bucket sort,稳定的)

桶排序 (Bucket sort)或所谓的箱排序,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θn))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

桶排序以下列程序进行:

1.   设置一个定量的数组当作空桶子。

2.   寻访串行,并且把项目一个一个放到对应的桶子去。

3.   对每个不是空的桶子进行排序。

  1. 从不是空的桶子里把项目再放回原来的串行中。

以下来自百度百科:

要求:数据的长度必须完全一样;原理:桶排序利用函数的映射关系。

桶排序有其局限性,适合元素值(键值)集合并不大的情况。

桶排序利用函数的映射关系,减少了几乎所有的比较工作。把大量数据分割成了基本有序的数据块()。然后只需要对桶中的少量数据做先进的比较排序即可。

N关键字进行桶排序的时间复杂度分为两个部分:

(1) 循环计算每个关键字的桶映射函数,这个时间复杂度O(N)

(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN))。因此,我们需要尽量做到下面两点:

(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。

(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的比较排序操作。当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:

O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)

桶排序应用:海量数据

一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。

分析:对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件: 100=<score<=900。那么我们就可以考虑桶排序这样一个投机取巧的办法、让其在毫秒级别就完成500万排序。

方法:创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有***人,501分有***人。

实际上,桶排序对数据的条件有特殊要求,如果上面的分数不是从100-900,而是从0-2亿,那么分配2亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。

典型

在一个文件中有10G个整数,乱序排列,要求找出中位数。内存限制为2G。只写出思路即可(内存限制为2G意思是可以使用2G空间来运行程序,而不考虑本机上其他软件内存占用情况。)关于中位数:数据排序后,位置在最中间的数值。即将数据分成两部分,一部分大于该数值,一部分小于该数值。中位数的位置:当样本数为奇数时,中位数=(N+1)/2 ; 当样本数为偶数时,中位数为N/21+N/2的均值(那么10G个数的中位数,就第5G大的数与第5G+1大的数的均值了)。

分析:既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法。

思想:将整型的每1byte作为一个关键字,也就是说一个整形可以拆成4keys,而且最高位的keys越大,整数越大。如果高位keys相同,则比较次高位的keys。整个比较过程类似于字符串的字典序

第一步:10G整数每2G读入一次内存,然后一次遍历这536,870,912即(1024*1024*1024*2 /4个数据。每个数据用位运算">>"取出最高8(31-24)。这8bits(0-255)最多表示256个桶,那么可以根据8bit的值来确定丢入第几个桶。最后把每个桶写入一个磁盘文件中,同时在内存中统计每个桶内数据的数量NUM[256]

代价:(1) 10G数据依次读入内存的IO代价(这个是无法避免的,CPU不能直接在磁盘上运算)(2)在内存中遍历536,870,912个数据,这是一个O(n)的线性时间复杂度(3)256个桶写回到256个磁盘文件空间中,这个代价是额外的,也就是多付出一倍的10G数据转移的时间。

第二步:根据内存中256个桶内的数量NUM[256],计算中位数在第几个桶中。很显然,2,684,354,560个数中位数是第1,342,177,280个。假设前127个桶的数据量相加,发现少于1,342,177,280,把第128个桶数据量加上,大于1,342,177,280。说明,中位数必在磁盘的第128个桶中。而且在这个桶的第1,342,177,280-N(0-127)个数位上。N(0-127)表示前127个桶的数据量之和。然后把第128个文件中的整数读入内存。(平均而言,每个文件的大小估计在10G/128=80M左右,当然也不一定,但是超过2G的可能性很小)。注意,变态的情况下,这个需要读入的第128号文件仍然大于2G,那么整个读入仍然可以按照第一步分批来进行读取。

代价:(1)循环计算255个桶中的数据量累加,需要O(M)的代价,其中m<255(2)读入一个大概80M左右文件大小的IO代价。

第三步:继续以内存中的某个桶内整数的次高8bit(他们的最高8bit是一样的)进行桶排序(23-16)。过程和第一步相同,也是256个桶。

第四步:一直下去,直到最低字节(7-0bit)的桶排序结束。我相信这个时候完全可以在内存中使用一次快排就可以了。

整个过程的时间复杂度O(n)的线性级别上(没有任何循环嵌套)。但主要时间消耗在第一步的第二次内存-磁盘数据交换上,即10G数据分255个文件写回磁盘上。一般而言,如果第二步过后,内存可以容纳下存在中位数的某一个文件的话,直接快排就可以了

 

基数排序(radix sort,稳定的)

基数排序Radix sort,也叫卡式排序card sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。(每次可以是1位,也可以是多位)。

基数排序的方式可以采用LSDLeastsignificant digital)或MSDMostsignificant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。基数排序的策略是多趟桶式排序。

基数排序的时间复杂度是 O(k·n),其中n是排序元素个数,k是排序趟数。这个时间复杂度不一定优于O(n·log(n))k的大小取决于数字位的选择(比如比特位数),和待排序数据所属数据类型的全集的大小;k决定了进行多少轮处理,而n是每轮处理的操作数目。

空间复杂度O(k·n)

计数排序(Counting sort,稳定的)

计数排序的基本思想:对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。

当输入的元素是 n 0 k 之间的整数时,它的运行时间是Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1的原因。算法的步骤如下:

1. 找出待排序的数组中最大和最小的元素

2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i

3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)

4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

外部排序(external sorting)

外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。

外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行归并排序

一般来说外排序分为两个步骤:预处理和合并排序。即首先根据内存的大小,将有n个记录的磁盘文件分批读入内存,采用有效的内存排序方法进行排序,将其预处理为若干个有序的子文件,这些有序子文件就是初始顺串,然后采用合并的方法将这些初始顺串逐趟合并成一个有序文件。

多路合并:在初始顺串构造之后,使用k-路合并所需要的趟数为logkN/M)。(k-路合并方法使用2k盘磁带)。

多相合并:(k-路合并方法使用k+1盘磁带)。顺串的个数是一个斐波那契数Fn,分配这些顺串的最好方式是把他们分裂成两个斐波那契数Fn-1Fn-2

顺串的构造:多路合并中最初的顺串含有M个记录,另一种顺串的构造方法是替换选择(replacement selection)。M个记录读入内存并建立堆。执行一次DeleteMin,把该最小记录写到输出磁带,再从输入磁带读入下一个记录。如果它比刚刚写出的记录大,则放入堆中堆根位置,调整堆序;否则,将该新元素存入堆的死区(dead space),用于下一顺串。继续执行DeleteMin,直到堆大小为零,此时顺串构建完成,死区元素(等于堆大小)作为一个新堆,重复。

替换选择产生平均长度为2M的顺串,顺串数约为一半。替换选择的特殊价值:输入数据常常从排序或几乎排序开始,此时替换选择产生少数非常长的顺串。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值