以前咱們介紹了交換類排序中的冒泡排序,此次咱們介紹另外一種交換類排序叫作快速排序。快速排序的優勢是原地排序,不佔用額外空間,時間複雜度是O(nlogn)
。java
固然,對於快速排序來講,它也是有缺點的,它對於含有大量重複元素的數組排序效率是很是低的,時間複雜度會降爲O(n^2)
。此時須要使用改進的快速排序—雙路快速排序,在雙路快速排序的基礎上,咱們又進一步優化獲得了三路快速排序。數組
快速排序的基本思想是:經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小,而後再按此方法對這兩部分數據分別進行快速排序,整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。
快速排序的步驟以下:dom
l
指向它。v
,一部分大於v
,用j
指向小於v
和大於v
的分界點,用i
指向當前訪問的元素e
,此時,數組arr[l+1...j]<v
,arr[j+1...i-1]>v
。e>v
,那麼直接將e
合併在大於v
那麼部分的後面,而後i++
繼續比較後面的元素。e<v
,那麼將e
移動到j
所指向元素的後一個元素,接着j++
,而後i++
繼續比較後面的元素。v
,中間部分是>v
,右邊部分是<v
。l
指向的元素和j
指向的元素交換,這樣就v
這個元素進行了快速排序,v
左邊元素都小於v
,右邊元素都大於v
。如今咱們使用上述方法對數組[2, 1, 4, 3, 7, 8, 5, 6]
進行快速排序,下圖展現了整個快速排序的過程:性能
快速排序代碼:優化
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } // 遞歸使用快速排序,對arr[l...r]的範圍進行排序 private static void sort(Comparable[] arr, int l, int r) { if (l >= r) { return; } // 對arr[l...r]部分進行partition操做, 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p] int p = partition(arr, l, r); sort(arr, l, p - 1); sort(arr, p + 1, r); } private static int partition(Comparable[] arr, int l, int r) { // 最左元素做爲標定點 Comparable v = arr[l]; int j = l; for (int i = l + 1; i <= r; i++) { if (arr[i].compareTo(v) < 0) { swap(arr, j + 1, i); j++; } } swap(arr, l, j); return j; }
通過上述介紹,咱們能夠發現快速排序不能保證每次切分的子數組大小相等,所以就可能一邊很小,一邊很大。對於一個有序數組,快速排序的時間複雜度就變成了O(n^2)
,至關於樹退化成了鏈表,下圖展現了這種變化:spa
上述咱們是固定使用左邊的第一個元素做爲標定元素,如今咱們隨機挑選一個元素做爲標定元素。此時咱們第一次選中第一個元素的機率爲 1/n,第二次又選中第二個元素 1/n-1,以此類推,發生以前退化成鏈表的機率爲1/n(n-1)(n-2)....,當 n 很大時,這種機率幾乎爲 0。3d
另外一個優化就是對小規模數組使用插入排序,由於遞歸會使得小規模問題中方法的調用過於頻繁,而插入排序對小規模數組排序是很是快的。code
優化的快速排序代碼:blog
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } // 遞歸使用快速排序,對arr[l...r]的範圍進行排序 private static void sort(Comparable[] arr, int l, int r) { // 對於小規模數組, 使用插入排序 if (r - l <= 15) { InsertionSort.sort(arr, l, r); return; } // 對arr[l...r]部分進行partition操做, 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p] int p = partition(arr, l, r); sort(arr, l, p - 1); sort(arr, p + 1, r); } 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]; int j = l; for (int i = l + 1; i <= r; i++) { if (arr[i].compareTo(v) < 0) { swap(arr, j + 1, i); j++; } } swap(arr, l, j); return j; }
對於含有大量重複元素的數組,使用上述的快速排序效率是很是低的,由於在咱們上面的判斷中,若是元素小於v
,則將元素放在<v
部分,若是元素大於等於v
,則放在>v
部分。此時,若是數組中有大量重複元素,>v
部分會變得很長,致使左右兩邊不均衡,性能下降。排序
雙路快速排序的步驟以下:
<v
和>v
兩部分放在數組的兩端,用i
指向<v
部分的下一個元素,用j
指向>v
部分的前一個元素。i
開始向後遍歷,若是遍歷的元素e<v
,則繼續向後遍歷,直到遍歷的元素e>=v
,則中止遍歷。一樣從j
開始向前遍歷,若是遍歷的元素e>v
,則繼續向前遍歷,直到遍歷的元素e<=v
,則中止遍歷。i
指向的元素和j
指向的元素。而後i++
,j--
繼續比較下一個。雙路快速排序的代碼:
public static void sort(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) { InsertionSort.sort(arr, l, r); return; } int p = partition(arr, l, r); sort(arr, l, p - 1); sort(arr, p + 1, r); } 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]; int i = l + 1, j = r; while (true) { // 注意這裏的邊界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0 // 不加等號若是遇到相等的狀況,這時候while循環就會退出,即交換i和j的值,使得對於包含大量相同元素的數組, 雙方相等的數據就會交換,這樣就能夠必定程度保證兩路的數據量平衡 // 從i開始向後遍歷,若是遍歷的元素e<v,則繼續向後遍歷,直到遍歷的元素e>=v,則中止遍歷 while (i <= r && arr[i].compareTo(v) < 0) { i++; } // 從j開始向前遍歷,若是遍歷的元素e>v,則繼續向前遍歷,直到遍歷的元素e<=v,則中止遍歷 while (j >= l + 1 && arr[j].compareTo(v) > 0) { j--; } if (i >= j) { break; } swap(arr, i, j); i++; j--; } // 此時j指向的元素是數組中最後一個小於v的元素, i指向的元素是數組中第一個大於v的元素 swap(arr, l, j); return j; }
三路快速排序的步驟以下:
v
的元素單獨做爲一個部分。lt
指向小於v
部分的最後一個元素,gt
指向大於v
部分的第一個元素。i
開始向後遍歷,若是遍歷的元素e=v
,則e
直接合併到=v
部分,而後i++
繼續遍歷。若是遍歷的元素e<v
,則將e
和=v
部分的第一個元素(lt+1
指向的元素)交換,而後lt++
,i++
繼續遍歷。若是遍歷的元素e>v
,則將e
和>v
部分前一個元素(gt-1
指向的元素)交換,而後gt--
,不過此時i
不須要改變,由於i
位置的元素是和gt
位置前面的空白元素交換過來的。i=gt
,而後將l
指向元素和lt
指向元素交換。<v
部分和>v
部分進行以上操做。三路快速排序相比雙路快速排序的優點在於:減小了對重複元素的比較操做,由於重複元素在一次排序中就已經做爲單獨一部分排好了,以後只須要對不等於該重複元素的其餘元素進行排序。
三路快速排序代碼:
public static void sort(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) { InsertionSort.sort(arr, l, r); return; } // 隨機在arr[l...r]的範圍中, 選擇一個數值做爲標定點pivot 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); }
本文介紹了快速排序、快速排序的優化、雙路快速排序和三路快速排序。
對於快速排序,咱們須要選擇合適的標定點,使得標定點的兩邊平衡;在快速排序中遞歸到小數組時,咱們可使用插入排序替換遞歸,減小沒必要要的開銷。
對於雙路快速排序和三路快速排序,咱們使用的場合是數組中存在大量重複元素。
最後,提示一下 JDK 底層的排序使用的就是插入排序 + 雙路快速排序 + 歸併排序的組合。