今天我们讲讲和合并排序一样也是运用分治的排序算法:快速排序。而且它们的时间复杂度都能到达一个O(nlgn)的上界。所以我们都能在n个元素的情况下,是算法以Ω(nlgn)时间运行。
1、基本思想
合并排序和快速排序的不同就在于递归调用发生的地方。合并排序先将集合分成两个子集在把子集分成更小的子集,所以递归调用发生在处理整个集合之前,而快速排序则是先将两个子集独立地排序,所以递归调用方发生在处理整个集合之后。而怎么划分两个子集则是快速排序的关
以最后一个元素划分
快速排序的API
void quickSort(E[] list) | 算法的入口 |
quickSort(E[] list, int first, int last) | 排序从low到high的元素 |
partition(E[] list, int first, int last) | 在first与last之间以某个元素划分 |
2、Normal难度下的快速排序
这里的划分只是普普通通地要求当前集合的第一个元素罢了,所以实现起来也普普通通。
1 private static <E extends Comparable> int partition(E[] list, int first, int last) 2 { 3 E pivot = list[first]; 4 int low = first; 5 int high = last + 1; 6 while(true) 7 { 8 while(list[++low].compareTo(pivot) == -1 && low < last); 9 while(list[--high].compareTo(pivot) == 1); 10 if(low >= high) 11 break; 12 Swap(list, low, high); 13 } 14 15 list[first] = list[high]; 16 list[high] = pivot; 17 18 return high; 19 }
以最后一个元素划分与此类似,只不过要注意限界条件。
quickSort就是简简单单的划分再递归,划分再递归...
1 private static <E extends Comparable> void quickSort(E[] list, int first, int last) 2 { 3 if(first < last) 4 { 5 int pivotIndex = partition(list, first, list.length - 1); 6 quickSort(list, first, pivotIndex - 1); 7 quickSort(list, pivotIndex + 1, last); 8 } 9 }
3、随机取划分元素
这种愣头地以第一个元素划分的方法效率高吗?假设一个已经有序的集合用第一个元素划分的情况,划分会形成两个极不对称子集合。并且每一步划分都会产生这样的结果。这样当然很麻烦,因为每次只是纯下标循环而没有元素交换。然而使用随机划分,这样左右子集合的比例就可以像Luffy的左右手比例一样变化。
在调用partition前,先中随机取得一个元素与第一个元素交换即可
1 private static <E extends Comparable> int randomizedPartition(E[] list, int first, int last) 2 { 3 int i = (int)Math.random() * (first - last + 1) + first; 4 Swap(list, i, first); 5 6 return partition(list, first, last); 7 }
在quickSort方法中做相应的修改
int pivotIndex = randomizedPartition(list, first, last);
3、三取样划分
如果觉得随机化不是避免算法的最坏情况行为,而是设法消除这种最坏情况的行为与特定实例之间的关联性,仅仅只是是时间复杂度得到相对均匀。那么我们就实打实地利用计算中位数来优化快速排序。前人大量的实验数据发现将取样大小设为3并用大小相对居中的元素划分效果最好。
我就选在第一个元素、中间的元素和最后一个元素中找中位数为划分元素。怎么找中位数不在本文叙述范围内
简而言之,就是通过比较一级一级地找到三个数中间那个元素即可。
所以三取样划分如下
4、进一步优化
在实际运用当中经常会出现含有大量重复元素的数组,可以运用基数排序的原理大大减少比较的次数。
并且对于小数组,快速排序比插入排序盘,所以可以再数组范围比较小的时候进行插入排序(什么是插入排序?大家想想打扑克时是怎么抓牌的)。