数据结构及算法基础--基本排序(elementary sort)(一)选择排序(selection sort)、插入排序(insertion sort)和希尔排序(shell sort)
其实基本排序并不是一个排序方法,而是集中基本的排序方法的定位,我使用这个名字也是因为《algorithm》书中将下面几种排序方法放在一章中,而这一章节的名字成为基本排序(elementary sort)。
书中包括三种排序方法:选择排序(selection sort)、插入排序(insertion sort)和希尔排序(shell sort)
我们开始一个一个对其进行实现:
在这之前,我们先对一些基本的方法进行定义,这样在实现算法的时候会大大的简化代码,并且能够使代码的可读性大大的提高!
主要的方法如下:
public class Example { private static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } private static void exch(Comparable[] a,int i,int j){ Comparable t=a[i]; a[i]=a[j]; a[j]=t; } private static void show(Comparable[] a){ for(int i=0;i<a.length;i++){ System.out.println(a[i]+" "); } } private static boolean isSorted(Comparable[] a){ for(int i=0;i<a.length;i++){ if(less(a[i],a[i+1]))return false; } return true; } public static void main(String[] args){ Integer[] a={7,145,14,51,413,4,14,134,13,4,42,}; TopDownMergeSort(a); assert isSorted(a); show(a); } }
以上代码很是简单。接下来便是对三种具体的排序算法进行实现
1)选择排序(selection sort)
其具体的工作原理,简单来说便是找到最小的元素放在第一,找到第二小的元素放在第二。去具体的实现可以参照下图:
我们可以看见,对数组的每一个位置进行检查,将后面元素中最小的元素放在这个位置。这便是选择排序的基本模型。
接下来是对选择排序的实现:
private static void SelectionSort(Comparable[] a){ int N=a.length; for(int i=0;i<N;i++){ int min=i; for(int j=min;j<N;j++){ if(less(a[j],a[min]))min=j; } exch(a,i,min); } }
在书中对这段代码的描述如下:
For each i, this implementation puts the ith smallest item in a[i]. The entries to the left of position i are the i smallest items in the array and are not examined again.
将第i个元素左边最小的元素放在a[i]上,便是这段代码的目的。
选择排序分析:
1、算法复杂度
对于选择排序的算法复杂度,主要考虑的是比较的次数和交换的次数。这里便有了书中的定理A:
选择排序的比较次数为~N^2,交换次数为N。对与比较次数,实际上是N*(N-1)/2,所以我们通常表示为~N^2;对于已经排序成功的序列,比较次数不变,交换次数为0;具体证明过程我给出书中原话,有兴趣的同学可以看一下,其实也非常简单:
Proof: You can prove this fact by examining the trace, which is an N-by-N table in which unshaded letters correspond to compares. About one-half of the entries in the table are unshaded—those on and above the diagonal. The entries on the diagonal each correspond to an exchange. More precisely, examination of the code revealsthat,for each i from 0 to N-1,there is one exchange and N-1-i compares, so the totals are N exchanges and (N-1) + (N-2) + . . . + 2 + 1+ 0 = N*(N-1) / 2 ~N^2 / 2 compares.
所以选择排序不管最好或者最差情况,比较次数都是不会变更的,所以它的算法复杂度最差情况和最好情况都为为O(N^2)。
2、稳定性
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
2)插入排序(insertion sort)
我们还是使用最简单的描述方法来描述插入排序:将当前元素插入到前面已经排序的元素组中的合适位置,然后对下一个元素进行同样的操作(原谅我的语言表述能力)。我们还是看图吧:
这个图应该能够很明确的表述插入排序的过程。
接下来来我们来对这个算法进行实现:
private static void InsertionSort(Comparable[] a){ int N=a.length; for(int i=0;i<N;i++){ for(int j=i;j>0&&less(a[j],a[j-1]);j--){ exch(a,j,j-1); } } }
书中对于这段代码的描述如下:
For each i from 0 to N-1, exchange a[i] with the entries that are smaller in a[0] through a[i-1]. As the index i travels from left to right, the entries to its left are in sorted order in the array, so the array is fully sorted when i reaches the right end.
很好理解,就是酱a[i]移动到前面已经排序的a[0]到a[n-1]内。
注意,我们这里实现的是直接插入排序,并没有讨论二分插入排序,后面的复杂度分析也是分析的直接插入排序。
插入排序算法分析
1、算法复杂度
当最好的情况,也就是要排序的表本身就是有序的,比如纸牌拿到后就是{2,3,4,5,6},那么我们比较次数,其实就是代码第6行每个L.r[i]与L.r[i‐1]的比较,共比较了 次,由于每次都是L.r[i]>L.r[i‐1],因此没有移动的记录,时间复杂度为O(n)。
当最坏的情况,即待排序表是逆序的情况,比如{6,5,4,3,2},此时需要比较 次,而记录的移动次数也达到最大值 次。
如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次为~N^2/4次。因此,我们得出直接插入排序法的时间复杂度为O(n^2)。从这里也看出,同样的O(n^2)时间复杂度,直接插入排序法比冒泡和简单选择排序的性能要好一些。
其实总结起来,最好的情况使用n-1次比较,0次移动。最坏情况都是用了~N^2/2次比较和移动。而平均使用了~N^2/4次比较和移动,书中原文如下:
Insertion sort uses N^2/4 compares and N^2/4 exchanges to sort a randomly ordered array of length N with distinct keys, on the average. The worst case is N^2/2 compares and N^2/2 exchanges and the best case is N-1 compares and 0 exchanges.
证明过程给出书中的原文:
Proof: JustasforPropositionA,the number of compares and exchanges is easy to visualize in the N-by-N diagram that we use to illustrate the sort. We count entries below the diagonal—all of them, in the worst case, and none of them, in the best case. For randomly ordered arrays, we expect each item to go about halfway back, on the average, so we count one-half of the entries below the diagonal.
The number of compares is the number of exchanges plus an additional term equal to N minus the number of times the item inserted is the smallest so far. In the worst case (array in reverse order), this term is negligible in relation to the total; in the best case (array in order) it is equal to N-1.
其实就是我在上面中文分析中讲的过程。
2、稳定性
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
3.额外定理
这个定理真的很奇特,首先我们来了解一个概念,就是inversions是什么,我确实不知道该怎么翻译,就先用英文表示了,如果有专业的中文名字,我会进行修改。
inversions简单的翻译就是倒置,在《algorithm》书中,这个名词指的数组中没有顺序的元素对。简单的例子:
E X A M P L E ,这里面invertions的数目为11,分别为:E-A, X-A, X-M, X-P, X-L, X-E, M-L, M-E, P-L, P-E, 和 L-E 。
这里有一个衍生概念,如果invertions的树木是这个数列大小的常数倍数,那么我们称这个数列是部分排序的(partially sorted)。
然后,我们来提出这个额外的定理
The number of exchanges used by insertion sort is equal to the number of inversions in the array, and the number of compares is at least equal to the number of inversions and at most equal to the number of inversions plus the array size minus 1.
插入排序中使用交换的次数等于这个数组中inversions的数目,然后比较的次数最少等于inversions的数目,最多等于inversions的数目加上数组大小-1.
至于证明过程,我没有进行详细了解,这里给出书中的简略证明原文:
Everyexchangeinvolvestwoinvertedadjacententriesandthusreducesthe number of inversions by one, and the array is sorted when the number of inver- sions reaches zero. Every exchange corresponds to a compare, and an additional compare might happen for each value of i from 1 to N-1 (when a[i] does not reach the left end of the array).
3)希尔排序(shell sort)
希尔算法其实是一种增进的插入算法,其思想是将序列每件个h元素分为一个子序列进行排序,图示如下:
我在理解这个概念的时候是没有问题的,但是对过程的实现是出现了很大的疑问。至于具体是什么,我们先来看一下shell排序的实现过程,具体如图所示:
当程序将增量h的值降低到1的时候,便是排序完成的时候。我们给出程序可能会更好的理解这个过程:
private static void ShellSort(Comparable[] a){ int N=a.length; int h=1; while(h<N/3)h=h*3+1; while(h!=0){ for(int i=0;i<N;i++){ for(int j=i;j>=h&&less(a[j],a[j-h]);j-=h){ exch(a,j,j-h); } } h=h/3; } }
我不想用我的问题来迷惑你们,所以我直接讲我遇到问题的知识点是什么:
我们首先明确一个概念,希尔排序是插入排序的一种,我们可以看到第二层循环的内部其实是对单一元素进行插入排序的操作。
所以我们从第h个元素开始进行排序,实际就是分割出来的子序列的第二个元素。(它不是从子序列第一个元素开始进行插入排序,因为第一个元素不需要!!)
上面便是实现shell排序的重点,如果还有疑问,我的解决方法便是,不看别人写的代码,仅仅通过希尔排序的流程和概念,自己编写出希尔排序的程序,然后在来对照上述程序,我们便可以发现问题的所在。
这里再再提醒:希尔排序就是提取间隔h的元素,并对子序列进行插入排序,直到h变为1;
在《algorithm》书中,所规定的h的取值方法如上面程序描述所示,h大于等于序列长度的三分之一,依次递除3;在这种规定下,h的值只能为1,4,13,40,121……
希尔排序分析:
1)算法复杂度分析
希尔算法的复杂度和增量h的选取有关系,如果我们采用的是希尔增量,那么它的算法复杂度便是O(n^2), 但是如果采用Hibbard增量,那么算法复杂度便为O(n^(3/2)),如果采用书中的算法,那么便不能用简单的公式表示,在算法书中其实表示,在希尔排序的表现分析中,这方面的学习已经远超本书的范围(
The study of the performance characteristics of shellsort requires mathematical ar- guments that are beyond the scope of this book. )。
但是书中给出了一个比较重要的定理:
希尔排序中在运用上述增量的时候,使用比较的次数被一个N的小倍数乘以使用的增量数量所限定?
(The number of compares used by shellsort with the increments 1, 4, 13, 40, 121, 364, . . . is bounded by a small multiple of N times the number of incre- ments used. )
说实话,没太懂,并且感觉不是什么重要的定理,希望有些同学通过证明能够得出一些对这个定理的启发,我放弃了T T:
Instrumenting Algorithm 2.3 to count compares and divide by the number of increments used is a straightforward exercise (see Exercise 2.1.12). Ex- tensive experiments suggest that the average number of compares per increment might be N^(1/5), but it is quite difficult to discern the growth in that function unless N is huge. This property also seems to be rather insensitive to the input model.
但是还有一个很重要的点,希尔排序的lower-bound是n*log2n!也就是说希尔排序没有快速排序快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O( n^2)复杂度的算法快得多。但是其实也是看情况的,因为快速排序的最差情况也是O(n^2)。关于快速排序我们会在后面进行讲解。
算法书中的基础排序主要介绍了这三种,其中重点讲解了插入排序和选择排序。这也将是最基础的两个排序算法,也是扩展最多的排序算法,希望大家能够熟练掌握。