“《算法导论》之‘排序’”:快速排序
简介
快速排序是由C.A.R Hoare于1960年发明的。
快速排序可能是应用最广泛的排序算法。快速排序流行的原因是它实现简单、适用于各种不同的输入数据且在一般应用中比其他排序算法都要快得多。快速算法引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为N的数组排序所需的时间和NlgN成正比。之前的几个排序算法都无法将这两个优点结合起来。另外,快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。已经有无数例子显示许多钟错误都能致使它在实际中的性能只有平方级别。
快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:
1)归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序,而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了;
2)在归并排序中,递归调用发生在处理整个数组之前,而在快速排序中,递归调用发生在处理整个数组之后;
3)在归并排序中,一个数组被等分为两半,而在快速排序中,切分(partition)的位置取决于数组的内容。
挖坑填数法
下边引用自该博文:
快速排序算法采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
该方法的基本思想是:
1)先从数列中取出一个数作为基准数。
2)分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3)再对左右区间重复第二步,直到各区间只有一个数。
虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此博主对快速排序作了进一步的说明:挖坑填数+分治法。
先看一个实例:以一个数组作为示例,取区间第一个数为基准数。
初始时,i = 0、j = 9,X = a[i] = 72。
由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。
1)从 j 开始向前找一个比X小或等于X的数。当 j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8],i++;
这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?
2)简单,再找数字来填a[8]这个坑。这次从 i 开始向后找一个大于X的数,当 i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3], j--。
因此,数组变为:
此时,i = 3、j = 7、X = 72。
再重复上面的步骤,先从后向前找,再从前向后找。
1)从 j 开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5], i++。
2)从 i 开始向后找,当i=5时,由于 i == j 退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将 X 填入a[5]。
因此,数组变为:
可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
对“挖坑填数法”进行总结
1)i =low、j = high, 将基准数挖出形成第一个坑a[i];
2)j-- 由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中;
3)i++ 由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中;
4)再重复执行2、3二步,直到i==j,将基准数填入a[i]中。
具体程序代码如下:
1 void QuickSort::sort() 2 { 3 sort(0, len - 1); 4 } 5 6 void QuickSort::sort(int low, int high) 7 { 8 if (high <= low) return; 9 int j = partition(low, high); 10 sort(low, j - 1); 11 sort(j + 1, high); 12 } 13 14 int QuickSort::partition(int low, int high) 15 { 16 int i = low, j = high; 17 int temp = arr[low]; // 即 arr[low] 就是第一个坑 18 while (i < j) 19 { 20 // 从右向左找小于 temp 的数来填 arr[i] 21 while (i < j && less(temp, arr[j])) 22 { 23 j--; 24 } 25 if (i < j) 26 { 27 arr[i] = arr[j]; 28 i++; 29 } 30 31 // 从左向右找大于或等于 temp 的数来填 arr[j] 32 while (i < j && less(arr[i], temp)) 33 { 34 i++; 35 } 36 if (i < j) 37 { 38 arr[j] = arr[i]; 39 j--; 40 } 41 42 } 43 //退出时,i等于j。将 temp 填到这个坑中。 44 arr[j] = temp; 45 46 return j; 47 }
算法改进
对快速排序有诸多种方法,详见该书P187。
切换到插入排序
和大多数递归排序算法一样,改进快速排序性能的一个简单办法基于以下两点:
1)对于小数组,快速排序比插入排序慢;
2)因为递归,快速排序的sort()方法在小数组中也会调用自己。
因此,在排序小数组时应该切换到插入排序。只需将函数“sort(int low, int high)”中的
if (high <= low) return;
切换成
1 if (high <= low + M) 2 { 3 Insertion.sort(low, high); 4 return; 5 };
就可以了。
转换系数M的最佳值是和系统相关的,但是5~15之间的任意值在大多数情况下都能令人满意。
三点中值(见《STL源码剖析》6.7节,P393)
在快速排序中,任何一个元素都可以被选来当作枢轴(pivot),但是其合适与否却会影响排序的效率。为了避免“元素当初输入时不够随机”所带来的恶化效应,最理想最稳当的方法就是取整个序列的头、尾、中央三个位置的元素,以其中值(median)作为枢轴。这种做法称为meidian-of-three partitioning,或称为medium-of-three-QuickSort。
具体代码如下:
template<class T> inline const T& __median(const T& a, const T& b, const T& c) { if (a < b) if (b < c) // a < b < c return b; else if (a < c) // a < b, b >= c, a <c return c; else return a; else if (a < c) // c > a >= b return a; else if (b < c) // a >= b, a >= c, b < c return c; else return b; }
完整程序请见Github.
参考资料
《算法》