高階排序-快速排序詳解
輔助工具
概述
快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。
簡單來說, 就是設定一個標準V, 每次遍歷就將整個陣列分成小於V和大於V的兩部分, 這時,V就處於它在排好序的陣列中應該在的位置, 然後再對左右兩部分分別進行遞迴排序, 這個將陣列分割成兩部分的操作, 稱之為partition, 下面看下程式碼實現 :
程式碼實現
package sort.quick;
import sort.Sort ;
import utils.ArrayUtil ;
public class QuickSort implements Sort {
@Override
public void sort(int [] arr) {
quickSort(arr, 0, arr.length - 1);
}
/**
* 對陣列arr的[l, r]前閉後閉的區間進行快速排序
* 遞迴方法
* @param arr
* @param l
* @param r
*/
private void quickSort(int [] arr, int l, int r){
// 遞迴終止條件
if (l >= r) {
return;
}
int p = partition(arr, l, r);
quickSort(arr, l, p - 1);
quickSort(arr, p + 1, r);
}
/**
* 對arr陣列的前閉後閉區間[l, r]進行partition操作
* @param arr
* @param l
* @param r
* @return 返回分割後作為標記元素的下標位置
*/
private int partition(int [] arr, int l, int r){
// 設定用於對比的元素為arr[l]
int v = arr[l];
// arr[l + 1, j] <v; arr[j+1, i)>v
int j = l;
for (int i = l; i <= r; i++) {
if (arr[i] < v) {
ArrayUtil.swap(arr, i, j + 1);
j++;
}
}
ArrayUtil.swap(arr, l, j);
return j;
}
}
下面我們就可以測試一下快速排序和歸併排序的效能之間的差異, 測試程式碼如下 :
/**
* 測試插入排序和歸併排序效能
*/
@Test
public void testSort(){
// 生成一個隨機陣列
int[] arr = ArrayUtil.generateArray(1000000, 0, 1000000);
int[] arr1 = ArrayUtil.copyArray(arr);
int[] arr2 = ArrayUtil.copyArray(arr);
int[] arr3 = ArrayUtil.copyArray(arr);
int[] arr4 = ArrayUtil.copyArray(arr);
System.out.println("----------------------------------隨機陣列----------------------------------") ;
System.out.println("歸併排序1 : " + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
System.out.println("歸併排序2 : " + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
System.out.println("歸併排序3 : " + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
System.out.println("自底向上的歸併排序 : " + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
/*
* 生成一個近乎有序的陣列
* 100000 : 陣列元素個數
* 10 : 在一個完全有序的陣列上進行多少次元素交換
*/
arr = ArrayUtil.generateNearlyOrderedArray(1000000, 10);
arr1 = ArrayUtil.copyArray(arr);
arr2 = ArrayUtil.copyArray(arr);
arr3 = ArrayUtil.copyArray(arr);
arr4 = ArrayUtil.copyArray(arr);
System.out.println("------------------------------近乎有序的陣列------------------------------") ;
System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
/*
* 生成一個存在大量重複元素的陣列
* 100000 : 陣列元素個數
*/
arr = ArrayUtil.generateArray(1000000, 0, 100);
arr1 = ArrayUtil.copyArray(arr);
arr2 = ArrayUtil.copyArray(arr);
arr3 = ArrayUtil.copyArray(arr);
arr4 = ArrayUtil.copyArray(arr);
System.out.println("------------------------------存在大量重複元素的陣列------------------------------") ;
System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
}
測試結果如下 :
----------------------------------隨機陣列---------------------------------- 歸併排序1 : 0.205s 歸併排序2 : 0.148s 歸併排序3 : 0.154s 自底向上的歸併排序 : 0.162s 快速排序 : 0.109s ------------------------------近乎有序的陣列------------------------------ 歸併排序1:0.082s 歸併排序2:0.047s 歸併排序3:0.016s 自底向上的歸併排序:0.02s 快速排序 : 60.363s ------------------------------存在大量重複元素的陣列------------------------------ 歸併排序1:0.154s 歸併排序2:0.114s 歸併排序3:0.09s 自底向上的歸併排序:0.09s 快速排序 : 3.401s
對於100萬的資料量, 從上面的測試結果來說, 可以得到下面的結論 :
- 對於隨機陣列來說, 快速排序的效能已經高於了歸併排序了
- 對於近乎有序的陣列, 由於這個版本的在partition的過程中每次都取第一個元素作為標準, 這會導致partition分割的兩部分嚴重不均衡, 在極端情況下即陣列完全有序的情況下, 會退化成O(n²)級別的演算法, 所以這個排序花了60多秒, 這個時間顯然是不能接受的, 下面會進行優化
- 對於存在大量重複元素的陣列, 上面的程式碼其實是分成小於等於V和大於V的兩部分, 一樣是會造成partition的不平衡, 下面是對這些問題的優化方案
需要注意的一點是, 在對近乎有序的陣列使用這一版的歸併排序時, 會因為遞迴深度較深而佔用大量棧空間, 如果報StackOVerflow的異常, 可以使用引數-Xss將棧空間設定大一點, 我這裡設定了-Xss128m是沒有問題的
排序優化
優化1
在上一篇歸併排序的文章中就說過, 對於資料量較小的陣列, 使用插入排序的效能反而更快, 所以可以在partition到底層資料量較小的時候, 使用插入排序替換快速排序, 進行第一次優化 :
package sort.quick;
import sort.Sort ;
import utils.ArrayUtil ;
/**
* 快速排序優化
* @author xuxiumeng
*
*/
public class QuickSort2 implements Sort {
@Override
public void sort(int [] arr) {
quickSort(arr, 0, arr.length - 1);
}
/**
* 對陣列arr的[l, r]前閉後閉的區間進行快速排序
* 遞迴方法
* @param arr
* @param l
* @param r
*/
private void quickSort(int [] arr, int l, int r){
// 遞迴終止條件
if ( r - l < 16) {
ArrayUtil.insertSort(arr, l, r);
return;
}
int p = partition(arr, l, r);
quickSort(arr, l, p - 1);
quickSort(arr, p + 1, r);
}
/**
* 對arr陣列的前閉後閉區間[l, r]進行partition操作
* @param arr
* @param l
* @param r
* @return 返回分割後作為標記元素的下標位置
*/
private int partition(int [] arr, int l, int r){
// 設定用於對比的元素為arr[l]
int v = arr[l];
// arr[l + 1, j] <v; arr[j+1, i)>v
int j = l;
for (int i = l; i <= r; i++) {
if (arr[i] < v) {
ArrayUtil.swap(arr, i, j + 1);
j++;
}
}
ArrayUtil.swap(arr, l, j);
return j;
}
}
測試程式碼 :
/**
* 測試插入排序和歸併排序效能
*/
@Test
public void testSort(){
// 生成一個隨機陣列
int[] arr = ArrayUtil.generateArray(1000000, 0, 1000000);
int[] arr1 = ArrayUtil.copyArray(arr);
int[] arr2 = ArrayUtil.copyArray(arr);
int[] arr3 = ArrayUtil.copyArray(arr);
int[] arr4 = ArrayUtil.copyArray(arr);
int[] arr5 = ArrayUtil.copyArray(arr);
System.out.println("----------------------------------隨機陣列----------------------------------") ;
System.out.println("歸併排序1 : " + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
System.out.println("歸併排序2 : " + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
System.out.println("歸併排序3 : " + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
System.out.println("自底向上的歸併排序 : " + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
System.out.println("快速排序2 : " + ArrayUtil.testSort(arr5, new QuickSort2()) + "s") ;
/*
* 生成一個近乎有序的陣列
* 100000 : 陣列元素個數
* 10 : 在一個完全有序的陣列上進行多少次元素交換
*/
arr = ArrayUtil.generateNearlyOrderedArray(1000000, 10);
arr1 = ArrayUtil.copyArray(arr);
arr2 = ArrayUtil.copyArray(arr);
arr3 = ArrayUtil.copyArray(arr);
arr4 = ArrayUtil.copyArray(arr);
arr5 = ArrayUtil.copyArray(arr);
System.out.println("------------------------------近乎有序的陣列------------------------------") ;
System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
System.out.println("快速排序2 : " + ArrayUtil.testSort(arr5, new QuickSort2()) + "s") ;
/*
* 生成一個存在大量重複元素的陣列
* 100000 : 陣列元素個數
*/
arr = ArrayUtil.generateArray(1000000, 0, 100);
arr1 = ArrayUtil.copyArray(arr);
arr2 = ArrayUtil.copyArray(arr);
arr3 = ArrayUtil.copyArray(arr);
arr4 = ArrayUtil.copyArray(arr);
arr5 = ArrayUtil.copyArray(arr);
System.out.println("------------------------------存在大量重複元素的陣列------------------------------") ;
System.out.println("歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
System.out.println("歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
System.out.println("歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
System.out.println("自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;
System.out.println("快速排序 : " + ArrayUtil.testSort(arr, new QuickSort()) + "s") ;
System.out.println("快速排序2 : " + ArrayUtil.testSort(arr5, new QuickSort2()) + "s") ;
}
測試結果 :
----------------------------------隨機陣列---------------------------------- 歸併排序1 : 0.191s 歸併排序2 : 0.143s 歸併排序3 : 0.156s 自底向上的歸併排序 : 0.16s 快速排序 : 0.109s 快速排序2 : 0.102s ------------------------------近乎有序的陣列------------------------------ 歸併排序1:0.082s 歸併排序2:0.047s 歸併排序3:0.014s 自底向上的歸併排序:0.021s 快速排序 : 134.718s 快速排序2 : 133.616s ------------------------------存在大量重複元素的陣列------------------------------ 歸併排序1:0.183s 歸併排序2:0.109s 歸併排序3:0.086s 自底向上的歸併排序:0.09s 快速排序 : 3.495s 快速排序2 : 3.481s
從測試結果看出, 這一版的快速排序比上一版的快速排序效能上要稍微好一點, 但是並沒有借據近乎有序的陣列, 和存在大量重複元素的陣列效能較差的問題, 所以還需要繼續優化
優化2
因為陣列可能是近乎有序的陣列, 所以我們可以不每次取隊首的元素, 一種方案是每次隨機取一個元素, 一種方案是取隊首, 中間, 隊尾三個元素中取大小排在中間的那個, 這裡我們就使用隨機元素的方案, 程式碼如下:
package sort.quick;
import java.util.Random;
import sort.Sort ;
import utils.ArrayUtil ;
/**
* 快速排序優化
* 上面兩個版本的快速排序, 在處理近乎有序的陣列的時候,效能很差,
* 因為每次partition時都取隊首元素, 在陣列近乎有序的時候, 會導致partition的兩部分很不均勻
* 在陣列完全有序的情況下, 快速排序甚至會退化成O(n²)演算法
* 優化方案 : 每次隨機取一個元素, 而不是每次取第一個元素
* @author xuxiumeng
*
*/
public class QuickSort3 implements Sort {
private static Random random = new Random();
@Override
public void sort(int [] arr) {
quickSort(arr, 0, arr.length - 1);
}
/**
* 對陣列arr的[l, r]前閉後閉的區間進行快速排序
* 遞迴方法
* @param arr
* @param l
* @param r
*/
private void quickSort(int [] arr, int l, int r){
// 遞迴終止條件
if ( r - l < 16) {
ArrayUtil.insertSort(arr, l, r);
return;
}
int p = partition(arr, l, r);
quickSort(arr, l, p - 1);
quickSort(arr, p +