这段时间,决心复习并深入研究常见的那几种排序算法,首先是快排。
以下内容来自 Sedgewick 的 Algorithms (Fourth Edition):
快排是原地算法( 见wiki:
),给长度为N的数组排序,平均花(Nlog N)的时间。所以是综合考虑到空间和时间的算法,优于众多排序算法。但它有一个重要缺点是:很“脆弱”,必须要防止“坏”情况的出现。在优化版本将看到。
基础实现:
快排是种分而治之的排序方法。 它把数组划分为两个子数组,再将两个子数组分别独立地排序。
快排是归并排序的补充:对于归并,我们把数组分为两个子数组,然后分别排序,然后再归为整体进行排序;对于快排,当我们把两个子数组排好序之后,整个数组自然就排好序了。
以下是书上给出的代码,是大概的实现,如果要跑起来的话,还要自己去实现未实现的类和方法:
public class QuickSort {
public static void sort(Comparable [] a){
StdRandom.shuffle(a); // eliminate dependence on input
sort(a, 0, a.length - 1);
}
private static void sort (Comparable[] a, int lo, int hi){
if(hi <= lo) return;
int j = partition(a, lo, hi);
sort(a, lo, j-1); // sort left part a[lo .. j-1]
sort(a, j+1, hi); // sort right part a[j+1 .. hi]
}
private static int partition(Comparable[] a, int lo, int hi){
// Partition into a[lo..i-1], a[i], a[i+1..hi]
int i = lo, j = hi+1;
Comparable v = a[lo]; // partitioning item
while (true){
// Scan right, scan left, check for scan complete, and exchange
while (less(a[++i], v)) if (i == hi) break;
while (less(v, a[--j])) if (j == lo) break;
if (i >= j) break;
exch(a, i, j);
}
exch(a, lo, j); // Put v = a[j] into position
return j; // with a[lo..j-1] <= a[j] <= a[j+1..hi]
}
}
以上算法实现有几点考虑:
原地划分:如果不是原地,即我们另外使用一个数组来实现划分,比较容易但是一来消费空间,二来还要拷回给原有的数组。
不要超出范围:如果那个被选作划分标兵的元素恰巧是最小值或者最大值,那我们得小心,别让指针跑出左右范围。上面的partition方法中使用明显的测试条件来防御出界。不过,测试条件(j == lo)其实是多余的,因为标兵自身起到一个guard的效果,即标兵自己不会小于自己。所以,其实可以用同样的办法来防御右边出界:做完shuffle,就将数组最大值放在最右边来守卫。
保持随机性:上面的方法是将数组重新随机排一遍来保证随机性,另一个方法是在partition方法里,随机选取一个元素作为标兵。
终止循环:其实呢,上面的算法实现有可能无限循环下去=.= 如果数组中有元素与标兵元素相同,那么指针可能就不会到交叉的那一步。
处理与标兵元素相同的元素:最好在大于等于标兵元素的时候停止左边的扫描,在小于等于标兵元素的时候停止右边的扫描,哪怕会有看上去不必要的交换元素,但不这么做的话,会导致n平方的时间。具体证明是作为习题的,就不展开。稍后的版本将看到更好的处理含有相同元素的数组的快排算法。
终止递归:专业点的程序员会特别留心以保证递归方法终止。快排算法常见的一个错误是没有确定一个元素的最终位置,而陷入无限递归(当标兵元素是最小或最大时,考虑这种情况的出现)。
ok,下面是算法改进:
快排是C.A.R Hoare 在上世纪60年代发明的,后人是各种学习研究和改进呐。如果你的算法代码将使用很多次,或者更进一步将被使用为库函数的话,那么很有必要研究一下算法优化。
今天在快排花的时间已经蛮多了,待续。