标签:https ast 其他 调用次数 重要 子数组和 割边 注意 之间
A: 希尔排序因计算机科学家Donald L.Shell而得名,他在1959年发现了希尔排序算法。
A: 希尔排序基于插入排序,但是增加了一个新的特性,大大地提高了插入排序的执行效率。
A: 回忆之前的简单排序的“插入排序”一节,在插入排序执行一半的时候,标记位i左边这部分数据项都是排过序的,而标记位右边的数据项则没有排过序。这个算法取出标记位所指的数据项,把它存储在一个临时变量里,接着,从刚刚被移除的数据项的左边第一个元素开始,每次把有序的数据项向右移动一个元素,直到存储在临时变量里的数据项能够有序回插。
A: 假设一个很小的元素在很靠近右端的位置,要把这个很小的元素移动到在左边的正确位置上,所有的中间元素都必须向右移动一位。这个步骤对每一个元素都执行了近N次的复制,虽不是所有的元素都必须移动N个位置,但是数据项平均移动了N/2个位置,就相当于执行了N次N/2个移位,总共是N2/2次复制,因此插入排序的执行效率是O(N2)。
A: 如果能以某种方式不必一个一个地移动所有中间的数据项,就能把较小的数据项移动到左边,那么这个算法的执行效率就会有很大的改进。
A: 希尔排序通过加大插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能大跨度地移动。当这些数据项排过一趟序后,希尔排序算法减少数据项的间隔再进行排序,依次进行下去。
A: 进行这些排序时数据项之前的间隔被称为增量,并且习惯上用字母h表示。
下图显示了增量为4时对包含10个数据项的数组进行排序的第一个步骤情况,在0、4和8号的位置上的数据项已经有序了。
当对0、4和8号数据项完成排序之后,算法向右游一步,对1、5和9号数据项进行排序,这个排序过程持续进行,直到所有的数据项已经完成了增量为4的排序。这个过程如下图所示:
在完成增量为4的希尔排序之后,数组可以看成是有4个子数组组成:(0, 4, 8), (1, 5, 9), (2, 6), (3, 7)。这4个子数组分别完全有序,这些子数组相互交错排列,然而彼此独立。
A: 上面图解了以4为增量对包含10个数据项的数组进行排序的情况。对于更大的数组,开始的间隔也应该更大,然后间隔不断减小,直到间隔变成1。接下来就是对于任意大小的数组,如何选择间隔呢?
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: 当然很可能存在一些很特殊的数据排列使得三数据项取中的执行效率很低,但是通常情况下,对于选择枢纽它都是一个又快又有效的好方法。
标签:https ast 其他 调用次数 重要 子数组和 割边 注意 之间
原文地址:https://www.cnblogs.com/fireway/p/9393889.html