概要
快速排序(Quicksort)是应用最为广泛的排序算法,与归并排序一样,快速排序也是分治思想的典型例子,它的基本思想是:在序列中任选一个切分元素a,利用a将序列分为两部分,使a左边的元素都不大于a,a右边的元素都不小于a,再采用递归的方式分别对左右两部分重复上述操作,当所有的子序列的长度都缩小为1时排序就完成了。
排序过程
- 首先选定的一个切分元素,一般可以选第一个或最后一个都行,用一个变量a将其保存起来,定义两个指针i和j分别指向数组的头部和尾部;
- 尾部指针j向前遍历,找到一个小于a的元素,将其放在i的位置;
- 头部指针i向后遍历,找到一个大于a的元素,将其放在j的位置;
- 重复步骤2、3直至指针i和j在某个位置相遇,将之前暂存的元素a放到这个位置,至此元素a成功将数组分为两部分,前面的元素都不大于a,后面的元素都不小于a,元素a已排定;
- 分别对前后两部分执行步骤1、2、3、4直到排序完成;
时间复杂度:O(n(logn));
算法基本实现
定义了一个排序接口,后面可用其它算法实现。
public interface Sort {
void sort(int[] array);
//交换数组i和j位置的值
default void exchange(int[] array, int i, int j){
int item = array[i];
array[i] = array[j];
array[j] = item;
}
}
快速排序实现
/**
* 快速排序
*/
public class QuickSort implements Sort{
private InsertionSort insertionSort = new InsertionSort();
@Override
public void sort(int[] arr) {
quick_sort(arr,0, arr.length);
}
private void quick_sort(int[] arr,int lo, int hi) {
int m = 0;
if(hi <= lo + m) {
//insertionSort(arr, lo, hi);//用插入排序优化,设m=15
return;
}
int p = partition(arr, lo, hi);
//分别对p点前后两部分递归调用,不包括p点本身
quick_sort(arr,lo, p);
quick_sort(arr,p+1, hi);
}
/**
* 切分方法
*/
private int partition(int[] arr, int lo, int hi) {
int i = lo, j = hi; //左右指针
//int mid = (lo + hi)/2;//取lo和hi的中点作为切分元素
//exchange(arr,lo,mid);
int a = arr[lo]; // 切分元素
while (i < j) { //一直循环,直至i和j相遇
while (i < j && a <= arr[--j]);//让j停在arr[j] < a的元素上
arr[i] = arr[j];
while (i < j && arr[++i] <= a);//让i停在a < arr[i]的元素上
arr[j] = arr[i];
}
arr[i] = a;//i和j相遇时把a放在它们相遇的位置
return i;//返回相遇的位置
}
public static void main(String[] args) {
Sort quick = new QuickSort();
TestUtil.test(100000,quick);
}
}
测试工具类
public class TestUtil {
/**
* 返回一个大小为n的,由1到n之间的随机整数组成的数组
*/
public static int[] getRandomArray(int n){
return new Random().ints(n,1,n).toArray();
}
public void show(int array[]){
System.out.println(Arrays.toString(array));
}
public static void test(int n,Sort sort) {
int [] array = getRandomArray(n);
long startTime = System.currentTimeMillis();
sort.sort(array);
long endTime = System.currentTimeMillis();
//show(array);
System.out.println("程序运行时间:" + (endTime - startTime) + "ms");
}
}
最好情况
快速排序的复杂度取决于数组的切分情况,最好的情况是每次都能在正中间将数组分为两半,即将数组5:5分,这样数组的长度就能以最快的速度降为1,但是这是不可能的,因为切分点元素是随机的,我们不可能知道它将在数组中排的位置,如果特地去找到这个中间点呢,那就得不偿失了,所以只能尽量去接近这个5:5分,或者说避免最坏情况。
最坏情况
上面的基本算法中我们选取第一个元素作为切分点,一般情况下这没什么问题,但如果这个数组已经是有序的或接近有序的,那么它的切分情况可能会变成这样:[],[1,n-1],即左边为一个空数组,元素全在右边,如下图所示:
这种每次都取一个最小值排定的过程有一股选择排序的味道,而它的时间复杂也变得和选择排序一样,降为了O(),更糟糕的是,由于这是一个递归的过程,每拆分一次都会往java方法栈里面压一帧,如果你的栈空间设置的不是很大,在数组具有一定规模时,用不了多久就会得到一个StackOverflowError。
算法改进
1. 切换为插入排序
与归并排序一样,快速排序也可以用插入排序改进,对于小数组,快速排序比插入排序慢,当数组的长度小于15时就转而使用插入,15只是一个估计值,最佳的转换参数和系统有关,大多数情况下5~ 15 之间的任意值在都能令人满意。
2. 随机抽样
对于上面这种最坏情况,我们可以用随机取样的方法来避免,选取切分点时不直接取首元素,而是在数组中随机取一个元素,或者取数组中间的点,将他与首元素交换位置,接下来的操作与之前一样,无需做什么改变,这样就避免了对接近有序的序列排序时总是选到最小的值作为切分点。还有一个办法是采用三取样切分,随机选3个元素取它们的中位数作为切分点,这样做得到的切分效果更好,但代价是需要计算中位数。
性能测试
分别测试了10^5,10^6,10^7,10^8数量级的排序时间
10^5:程序运行时间:13ms
10^6:程序运行时间:75ms
10^7:程序运行时间:872ms
10^8:程序运行时间:8863ms
应用场景
快速排序算法的应用非常广泛,对于大规模的混乱数据,快速排序是最好的选择,虽然归并排序的时间复杂度也是O(nlongn),但是快速排序的交换次数更少,空间复杂度也更低,在实际应用中快速排序往往比归并排序更快。