A: 希尔排序因计算机科学家Donald L.Shell而得名,他在1959年发现了希尔排序算法。html
A: 希尔排序基于插入排序,可是增长了一个新的特性,大大地提升了插入排序的执行效率。java
A: 回忆以前的简单排序的“插入排序”一节,在插入排序执行一半的时候,标记位i左边这部分数据项都是排过序的,而标记位右边的数据项则没有排过序。这个算法取出标记位所指的数据项,把它存储在一个临时变量里,接着,从刚刚被移除的数据项的左边第一个元素开始,每次把有序的数据项向右移动一个元素,直到存储在临时变量里的数据项可以有序回插。git
A: 假设一个很小的元素在很靠近右端的位置,要把这个很小的元素移动到在左边的正确位置上,全部的中间元素都必须向右移动一位。这个步骤对每个元素都执行了近N次的复制,虽不是全部的元素都必须移动N个位置,可是数据项平均移动了N/2个位置,就至关于执行了N次N/2个移位,总共是N2/2次复制,所以插入排序的执行效率是O(N2)。算法
A: 若是能以某种方式没必要一个一个地移动全部中间的数据项,就能把较小的数据项移动到左边,那么这个算法的执行效率就会有很大的改进。数组
A: 希尔排序经过加大插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能大跨度地移动。当这些数据项排过一趟序后,希尔排序算法减小数据项的间隔再进行排序,依次进行下去。数据结构
A: 进行这些排序时数据项以前的间隔被称为增量,而且习惯上用字母h表示。
下图显示了增量为4时对包含10个数据项的数组进行排序的第一个步骤状况,在0、4和8号的位置上的数据项已经有序了。
当对0、4和8号数据项完成排序以后,算法向右游一步,对一、5和9号数据项进行排序,这个排序过程持续进行,直到全部的数据项已经完成了增量为4的排序。这个过程以下图所示:
在完成增量为4的希尔排序以后,数组能够当作是有4个子数组组成:(0, 4, 8), (1, 5, 9), (2, 6), (3, 7)。这4个子数组分别彻底有序,这些子数组相互交错排列,然而彼此独立。数据结构和算法
A: 上面图解了以4为增量对包含10个数据项的数组进行排序的状况。对于更大的数组,开始的间隔也应该更大,而后间隔不断减少,直到间隔变成1。接下来就是对于任意大小的数组,如何选择间隔呢?ide
A: 举例来讲,含有1000个数据项的数组可能先以364为增量,而后以121为增量,而后以40为增量,接着以13为增量,再接着以4为增量,最有以1为增量进行希尔排序。用来造成间隔的数列(121,40,13,4,1)被称为间隔序列。这里所表示的间隔序列由Knuth提出。函数
A: 数列以逆向的形式从1开始,经过递归表达式h = 3 * h + 1来产生,初始值为1。下表的前两栏显示了这个公式的序列。 性能
A: 在排序算法中,首先在一个短小的循环中使用序列的生成公式来计算出最初的间隔。h值最初被赋值为1,而后用公式h = 3 * h + 1生成序列1,4,13,40,121,364等等。当间隔大于数组大小的时候这个过程中止。
A: 对于一个含有1000个数据项的数组,序列的第七个数字1093太大了。所以使用序列的第6个数字364做为最大的数字来开始这个排序过程,做增量为364的排序。而后,每完成一次排序全程的外部循环,用前面提供的此公式倒推式来减少间隔: h = (h - 1) / 3
。这个倒推的公式生成逆置的序列364,121,40,13,4,1。从364开始,以每个数字做为增量进行排序。当数组用增量为1排序后,算法结束。
A: 示例:ShellSort.java
A: 选择间隔序列能够称得上是一种魔法,除了h = h * 3 + 1生成间隔序列外,还有其余间隔序列。这些间隔只有一个绝对条件,就是逐渐减少的间隔最后必定要等于1,所以最后一趟排序是一次普通的插入排序。
A: 在最开始的时候,希尔排序初始的间隔为N/2,简单地把每一趟排序分红两半,所以对于大小为100的数组逐渐减少的间隔序列为50,25,12,6,3,1。这个方法的好处是不须要开始排序前为找到初始的间隔而计算序列,而只需用2整除N。可是这种证实并非最好的数列。尽管对于大多数的数据来讲这个方法仍是比插入排序效果好,可是这种方法有时会使运行时间降到O(2)。
A: Flaming间隔的代码以下:
if (h < 5) { h = 1; } else { h = (5 * h - 1) / 11; }
这个方法是用2.2而非2来整除每个间隔。对于n=100的数组,会产生序列45,20,9,4,1。这比用2整除好多了,由于这样避免了某些致使时间复杂度O(N2)的最坏状况发生。
A: 间隔序列的数字互质一般被认为很重要,也就是说除了1以外它们没有公约数,这个约束条件使每一趟排序更有可能保持前一趟排序已排好的效果。而以N/2为间隔的低效性就是归咎于它没有遵循这个准则。
A: 或许还能够设计出像上面讲述的间隔序列同样好甚至更好的序列。可是无论怎么样,都应该可以快速地计算,而不会下降算法的执行速度。
A: 迄今为止,除了在一些特殊的状况下,尚未人可以从理论上分析希尔排序的效率。有各类各样基于试验的评估,估计它的时间级是从O(N3/2)到O(N7/6) 。
A: 下表对比了速度较慢的插入排序和速度较快的快速排序,中间还列出了希尔排序的一些估计的大O值。注意Nx/y表示N的x方的y次方根(N等于100,N3/2就是1003的平方根,结果是1000)。另外(logN)2表示N对数的平方,一般协做log2N。
A: 划分(partitioning)是后面讨论的快速排序的根本基础,所以把它做为单独的一节来说解。
A: 划分数据就是把数据分为两组,使全部关键字大于特定值的数据项在一组,使全部关键字小于特定值的数据项在另外一组。
A: 划分算法:当leftPointer遇到比枢纽小的数据项时,它继续右移,由于这个数据项的位置已经处在数组的正确一边了。可是,当遇到比枢纽大的数据项时,它就停下来。同理rightPointer。两个内层的while循环,第一个应用于leftPointer,第二个应用于rightPointer,控制这个扫描过程,由于指针退出了while循环,因此它中止移动。下面是一段扫描不在适当位置上的数据项的简化代码:
while (leftPointer < right && mLArray[++leftPointer] < pivot) {} while (rightPointer > left && mLArray[--rightPointer] > pivot) {} swap(leftPointer, rightPointer);
当这两个循环都退出以后,leftPointer和rightPointer都指着在数组的错误一方位置上的数据项,因此交换这两个数据项。交换以后,继续移动这两个数据项。当两个指针最终相遇的时候,划分过程结束,而且退出外层while循环。
A: 划分算法的运行时间为O(N)。
A: 毫无疑问,快速排序是最流行的排序算法,由于有充足的理由,在大多数状况下,快速排序都是最快的,执行时间为O(N * logN)级。快速排序是在1962年由C.A.RHoare发现的。
A: 有了前面划分算法的介绍,再来理解快速排序就很容易了。快速排序算法本质上经过把一个数组划分为两个子数组,而后递归地调用自身为每个子数组进行快速排序。
A: 基本的递归的快速排序算法代码很简单,下面是一个示例:
public void recQuickSort(int left, int right) { if (right - left <= 0) { // if size is 1, it's already sorted return; } else { // size is 2 or larger // partition range int partitionIndex = partitioning(left, right); // sort left side recQuickSort(left, partitionIndex - 1); // sort right side recQuickSort(partitionIndex + 1, right); } }
有三个基本的步骤:
1) 把数组或者子数组划分左边和右边;
2) 调用自身对左边的进行排序;
3) 调用自身对右边的进行排序。
通过一次划分以后,全部在左边子数组的数据项都小于在右边子数组的。
只要对左边子数组和右边子数组分别进行排序,整个数组就是有序的了。
A: 如何对子数组进行排序呢?经过递归来实现。这个方法首先检查数组是否只包含一个数据项,若是数组只包含一个,那么数组就已经有序,方法当即返回,这个就是递归过程当中的基值条件。
若是数组包含两个或者更多的数据项,算法就调用前面讲过的partitioning()方法对这个数组进行划分。方法返回分割边界的下标index。划分pivot给出两个子数组的分界,以下图所示。
对数组进行划分以后,recQuickSort()递归地调用自身,数组左边的部分调用一次(从left到partitionIndex - 1位置上的数据项进行排序),数组右边的部分也调用一次(从partitionIndex + 1到right位置上的数据项进行排序)。注意这两个递归调用都不包含数组下标partitionIndex的数据项。为何不包含这个数据项呢?难道下标为partitionIndex的数据项不须要排序?
A: 那么partitioning()方法如何选择枢纽呢?如下是一些相关思想:
1) 应该选择具体的一个数据项的关键字的值做为枢纽:成这个数据项为pivot(枢纽);
2) 能够选择任意一个数据项做为枢纽。为了简便,咱们假设老是选择待划分的子数组最右端的数据项做为pivot;
3) 划分完成以后,若是枢纽被插入到左右子数组之间的分界处,那么枢纽就落在排序以后的最终位置上了。
下图显示了用关键字为36的项做为枢纽的状况。由于不能真正像图中显示的那样把一个数组分开,因此这个图只是一个想象的状况。那么怎样才能把枢纽移动到它正确的位置上来呢?
能够把右边子数组的全部数据项都像右移动一位,以腾出枢纽的位置。可是,这样作即低效又没必要要。记住尽管右边子数组的全部数据项都大于枢纽,但它们都尚未排序,因此它们能够在右边子数组内部移动,而没有任何影响。所以,为了简化把枢纽插入正确位置的操做,只要交换枢纽和右边子数组的最左边的数据项(目前是63)便可。
这个交换操做把枢纽放在了正确的位置上,也就是左右子数组之间。63跳到了最右边,以下图所示:
当枢纽被换到分界的位置时,它落在它最后应该在的位置上。之后全部的操做或者发生在左边或者右边,枢纽自己不会再移动了。
示例: QuickSort.java
A: 若是数据是逆序的,而后采用上面的程序进行排序,就会发现算法运行得至关缓慢。
A: 问题出在枢纽的选择上,理想状态下,应该选择被排序的数据项的中值数据项做为枢纽。也就是说,应该由一半的数据项大于枢纽,一半的数据项小于枢纽。对快速排序算法来讲拥有两个大小相等的子数组是最优的状况。若是快速排序算法必需要对划分的一大一小两个子数组排序,那么将会下降算法的效率,这是由于较大的子数组必需要被划分更屡次。
A: N个数据项数组的最坏的划分是一个子数组只有一个数据项,另外一个子数组含有N-1个数据项。
A: 在这种状况下,划分所带来的好处就没有了,算法的执行效率下降到O(N2)。除了慢,还有另一个潜在的问题,当划分的次数增长时,递归方法的调用次数也增长,每个方法调用都要增长所需递归工做栈的大小。若是调用次数太多,递归工做栈可能会发生溢出,从而使系统瘫痪。那么可否改进选择枢纽的方法呢?
A: 方法应该简单但能避免出现选择最大或者最小数据项做为枢纽的状况。能够检测全部的数据项,而且实际计算哪个数据项是中值数据项,这应该是理想的枢纽,但是因为这个过程须要比排序自己更长的时间,所以它不可行。
A: 折衷的解决方案是找到数组的第一个、最后和中间元素的中间值,并将其用于枢纽。这个方案被称为“三数据项取中”,以下图:
查找三个数据项的中值数据项天然比查找全部数据项的中值数据项快得多,同时这也有效地避免了在数据已经有序或者逆序的状况下,选择最大的或者最小的数据项做为枢纽的机会。
A: 固然极可能存在一些很特殊的数据排列使得三数据项取中的执行效率很低,可是一般状况下,对于选择枢纽它都是一个又快又有效的好方法。
A: 由于在选择的过程当中使用三数据项取中的方法不只选择了枢纽,并且还对三个数据项进行了排序。这时就能够保证子数组最左端的数据项小于枢纽,最右端的数据项大于枢纽,这就意味着即使取消了leftPointer > right
和rightPointer < left
的检测,leftPointer和rightPointer也不会分别越过数组。以下图:
A: 三数据项取中的另外一个好处是,对左端、中间以及右端的数据项排序以后,划分过程就不须要再考虑这三个数据项了。划分能够从left + 1和right - 1开始,由于left和right已经被有效地划分了。
A: 示例:QuickSort.java
A: 若是使用三数据项取中划分的方法,则必需要遵循快速排序不能执行三个或者少于三个数据项的划分规则,在这种状况下,数字3则被称为切割点(cutoff)。在上面的示例中,是用一段代码手动地对两个或者三个数据项的子数组进行排序。那么这个是最好的方法吗?
A: 处理小划分的另外一个选择是使用插入排序。当使用插入排序的时候,不用限制以3为切割点。能够把界限定为十、20或者其余任何数。Knuth推荐使用9做为切割点。可是最好的选择值取决于计算机、操做系统、编译器(或者解释器)等。
A: 示例:QuickSort.java
A: 另外一个选择是对数组整个使用快速排序。当快排结束时,数组已是基本有序了,而后能够对整个数组应用插入排序。插入排序对基本有序的数组执行效率很高,并且不少专家都提倡使用这个方法。
A: 示例:QuickSort.java
A: 不少人提倡对快速排序的算法采用循坏代替递归来执行子数组的划分,这个思想源于早起的编译器以及计算机体系结构,对于每一次方法调用那种旧的系统都会致使大量的时间消耗。对于如今的系统来讲,消除递归所带来的改进不是很明显,由于如今的系统能够更为有效地处理方法调用。
A: 快速排序的时间复杂度为O(N*logN)。对于分治算法整体都是这样的,递归的方法把一列数据项分为2组,而后调用自身来分别处理每一组数据项。这种状况下,算法实际以2为底,运行时间和N*log2N成正比。
A: 基数排序(Radix Sort)也称为桶排序,是一种当关键字为整数类型时很是高效的排序方法。
A: 设待排序的数据元素的关键字是m位d进制整数(不足m位的关键字在高位上补0),设置d个桶,令其编号为0,1,2,3,...,d-1。
A: 首先,按关键字最低位的数值依次把各数据元素放在对应的桶中。而后,按照桶号从小到大和进入桶中的前后次序收集分配在个桶中的数据元素,这样就造成了数据元素集合的一个新的排列。称这样的依次排序过程为一次基数排序。
A: 再对一次基数排序所获得的数据元素序列按关键字次低位的数值依次把各数据元素放到对应的桶中,而后按照桶号从小到大和进入桶中数据元素的前后次序收集分配在各桶中的数据元素。
A: 这样的过程重复进行,当完成了第m次基数排序后,就能够获得了排好序的数据元素的序列。
A: 下面是一个例子,有7个数据项{421, 240, 035, 532, 305, 430, 124},每一个数据项都有三位。
A: 分析基数排序算法,由于要求进出桶中的数据元素序列知足FIFO原则,所以这里所说的桶实际就是队列。队列有顺序队列和链式队列,所以在实现中就有这两种方式。
A: 考虑到个位,十位,百位…每一位数值的个数不可能彻底相同,所以很难肯定队列的大小,所以采用链式队列最好,由于它能够任意扩展。请参阅:用链表实现的队列
A: 基于链式队列基数排序算法的存储结构示意图:
A: 一个十进制关键字K的第i位数值Ki的计算公式:
其中,int()函数为取整函数,如int(3.5) = 3。
设k = 6321, K1, K2, K3, K4的计算结果以下:
K1 = int(6321 / 100) - 10 * (int(6321 / 101)) = 6321 - 6320 = 1;
K2 = int(6321 / 101) - 10 * (int(6321 / 102)) = 632 - 630 = 2;
K3 = int(6321 / 102) - 10 * (int(6321 / 103)) = 63 - 60 = 3;
K4 = int(6321 / 103) - 10 * (int(6321 / 104)) = 6 - 0 = 6;
A: 示例:RadixSort.java
A: 全部要作的只是把原始的数据项从数组拷贝到链表,而后再拷贝回来。若是有10个数据项,则有20次拷贝。拷贝的次数和数据项的个数成正比,即O(N)。
A: 对每一位重复一次这个过程,假设对5位的数字排序,就须要20*5次拷贝。位数咱们设为M。所以基于链式队列的基数排序算法的时间复杂度为O(MN)。
A: 尽管从数字中提取出每一位须要花费时间,可是没有比较。现代计算机中位提取操做要快于比较操做。