快速排序
快速排序算法被列为20世纪十大算法之一,由Tony Hoare设计。在c++的STL、Java SDK和.net framework中都有各自的实现版本,可见其应用非常广泛。
基本思想
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。所以快速排序的基本步骤如下:
1. 从数列中挑出一个元素,称为 "基准"(pivot)。
2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分割结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
实现代码
void qsort_z(int sdata[], int low, int high) { int pivot = 0; if (low < high) { pivot = partition_z(sdata, low, high); qsort_z(sdata, low, pivot-1); qsort_z(sdata, pivot+1, high); } } int partition_z(int data[], int low, int high) { int pivotkey = 0; pivotkey = data[low]; while (low < high) { while(low < high && data[high] >= pivotkey) high--; swap_z(data, low, high); while(low < high && data[low] <= pivotkey) low++; swap_z(data, low, high); } return low; } void swap_z(int data[], int i, int j) { if (i == j) return; int temp = data[i]; data[i] = data[j]; data[j] = temp; }
实现代码分析
代码的核心部分是partition_z函数,它的主要作用是选取一个关键字,然后想办法将它放到一个位置,使得它左边的值都比它小,它右边的值都比它大。这个关键字称之为枢轴(pivot)。
时间复杂度
最好情况:O(nlog2n);最坏情况O(n2);平均情况:O(nlog2n)。
优化
1. 优化选取枢轴
如果所选的枢轴正好处于待排序列的中间位置,那正好可以将整个序列分成一个小数集合和一个大数集合,这样快排算法就可以达到最优性能。所以枢轴的选取尤为重要,上面的实现程序中,只是简单的将整个序列最左边的元素作为枢轴,实际上可以通过一些优化方案,使我们选择的枢轴尽可能的是整个序列的中间数。
对于小数组可能会采用三数取中(median-of-three)法,即取三个关键字先进行排序,然后将中间数作为枢轴。由于整个待排序列处于无序状态,所以随机选取其中的三个数,和直接选取左中右三个数是一回事,而且取随机数还要带来时间开销,所以通常这三个关键字选取为序列的左端、中间、右端三个值。
对于非常大的待排序列,可能会使用所谓的九数取中(median-of-nine)法,先从序列中分三次取样,每次取三个数,三个样品各取中数,再将这三个中数中的中数作为枢轴。
2. 优化小数组时的排序方案
如果数组非常小,使用直接插入排序会比使用快速排序算法性能更好,直接插入排序是简单排序中性能最好的。因为快排使用递归操作,如果是对于大数组,这点性能影响相对于它的整体算法优势而言可以忽略不计,但如果数组非常小,就成了大炮打蚊子。所以可以在排序算法中增加一个判断,当high-low不大于某个常数时(有资料认为7比较合适,也有人认为50比较合理),就用直接插入排序,如:
#define MAX_LENGTH_INSERT_SORT 7 void qsort_z(int sdata[], int low, int high) { int pivot = 0; if ((high - low) > MAX_LENGTH_INSERT_SORT) { pivot = partition_z(sdata, low, high); qsort_z(sdata, low, pivot-1); qsort_z(sdata, pivot+1, high); } else InsertSort(); }
【大话数据结构 – 读书笔记】