1. 快速排序(Quick Sort)
基本算法流程
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
1. 从数列中挑出第一个元素,称为”基准”(pivot),
2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码分析
public class QuickSort {
public QuickSort(Comparable[] arr){
sort(arr,0,arr.length-1);
}
private void sort(Comparable[] arr, int l, int r){
if (l >= r){
return;
}
int p = partition(arr,l,r);
sort(arr,l,p-1);
sort(arr,p+1,r);
}
private int partition(Comparable[] arr, int l, int r) {
Comparable v = arr[l];
Comparable temp;
int j = l;
for (int i = l+1; i <= r ; i++) {
if (arr[i].compareTo(v) < 0){
temp = arr[i];
arr[i] = arr[j+1];
arr[j+1] = temp;
j++;
}
}
temp = arr[j];
arr[j] = arr[l];
arr[l] = temp;
return j;
}
}
时间复杂度
在平均状况下,排序n个项目要 O(n\log n)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常比其他 O(n\log n)算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
运行结果
从图中我们可以看出,在随机数组中,快速排序会比归并排序快一些。在近乎有序的数组中,快速排序用的时间大概是归并排序的一百倍。这是为什么呢?
在归并排序和快速排序中,其实都是将数组不断拆成两部分。在归并排序
中,数组是平均的分成两组。而快速排序
则是不平均的两个数组,在选择第一个数为基准,那极致的情况下可能会出现一个数组只有一个数,而另一个数组是全部剩余的数,从而将排序退化为O(n^2)的排序。
其实也很简单,只要随机制定快速排序基准数,那么效率就会大大增加。
2. 双路快速排序
基本算法流程
双路快速排序和之前的快速排序有些不同,之前快速排序是通过遍历比较每个数和基准的大小,小的交换至基准的左边,大的在基准的另一边。
- 下面介绍一下双路快速排序的步骤。
1. 从数列中挑出第一个元素,称为”基准”(pivot),
2. 分别从数列的两头开始遍历。正向遍历,如果遇到大于等于基准的数字则停止,反向遍历如果遇到小于等于基准的数字则停止。
3. 将停止时的两个数字交换位置。交换后保证左侧小于基准,右侧大于基准,两侧再继续遍历。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
4. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码分析
public class QuickSort2Ways {
public QuickSort2Ways(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v
int i = l+1, j = r;
while( true ){
while( i <= r && arr[i].compareTo(v) < 0 )
i ++;
while( j >= l+1 && arr[j].compareTo(v) > 0 )
j --;
if( i > j )
break;
swap( arr, i, j );
i ++;
j --;
}
swap(arr, l, j);
return j;
}
private static void sort(Comparable[] arr, int l, int r){
// 对于小规模数组, 使用插入排序
if( r - l <= 15 ){
new InsertionSort(arr, l, r);
return;
}
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
运行结果
从图中我们可以看出,双路快排相比快速排序和插入排序更快,而且在近乎有序的数组中发挥也很不错。
3. 三路快速排序
基本算法流程
三路快速排序,简而言之就是双路快排的升级版。三路快排要做的事情,其实就是将数组分成三部分:小于基准,等于基准和大于基准,之后递归的对小于基准和大于基准部分进行排序就好了。而这种做法就会使等于基准的数在排序中减小操作,进而增加排序效率。
代码分析
public class QuickSort3Ways {
public QuickSort3Ways(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void sort(Comparable[] arr, int l, int r){
if( r - l <= 15 ){
new InsertionSort(arr, l, r);
return;
}
swap( arr, l, (int)(Math.random()*(r-l+1)) + l );
Comparable v = arr[l];
int lt = l; // arr[l+1...lt] < v
int gt = r + 1; // arr[gt...r] > v
int i = l+1; // arr[lt+1...i) == v
while( i < gt ){
if( arr[i].compareTo(v) < 0 ){
swap( arr, i, lt+1);
i ++;
lt ++;
}
else if( arr[i].compareTo(v) > 0 ){
swap( arr, i, gt-1);
gt --;
}
else{ // arr[i] == v
i ++;
}
}
swap( arr, l, lt );
sort(arr, l, lt-1);
sort(arr, gt, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
运行结果
三路快排能够很好的解决了近乎有序的数组和有大量重复数组的元素排序问题,以至于在很多语言的标准库中,排序接口使用的就是三路快排的思路,比如Java语言。
总感觉博客写的有些问题,无论就写作布局还是排版内容,希望各位看后能不吝赐教。