分治法在排序演算法中的應用(JAVA)--快速排序(Lomuto劃分、Hoare劃分、隨機化快排)
分治法在排序演算法中的應用
快速排序:時間複雜度O(nlogn)
如果說歸併排序是按照元素在陣列中的位置劃分的話,那麼快速排序就是按照元素的值進行劃分。劃分方法由兩種,本節將主要介紹Huare劃分,在減治法在查詢演算法中的應用(JAVA)--快速查詢這篇文章中講述了Lomuto劃分用於快速查詢演算法,下面會給出基於Lomuto的快排程式碼。
1、基於Lomuto劃分的快速排序演算法
public class Main { static int[] a= {5, 3, 1, 9, 8, 2, 4, 7}; public static void main(String[] args) { fastsort(0, a.length-1); for (int i = 0; i < a.length; i++) { System.out.print(a[i] + " "); } } private static int Lomuto(int l, int r) { int p = a[l]; int s = l; for (int i = l+1; i <= r; i++) { if (a[i] < p) { s = s+1; int temp = a[s]; a[s] = a[i]; a[i] = temp; } } int temp = a[l]; a[l] = a[s]; a[s] = temp; return s; } private static void fastsort(int l, int r) { if (l < r) { int s = Lomuto(l, r); fastsort(l, s-1); fastsort(s+1, r); } } }
2、基於Hoare劃分的快速排序
Hoare劃分是一種更為複雜的劃分方式,我們假設有一個數組a[0, n-1],其子陣列為a[l, r](0 <= l <= r <= n-1),假定首個元素為樞軸p,下面從陣列兩端進行掃描,並將掃描到的元素與樞軸比較。從左到右掃描(用指標i來表示),掃描到第一個大於等於樞軸p的,停止;從右到左掃描(用指標j表示,遇到第一個小於等於樞軸的元素)。這裡注意等於樞軸的元素也要進行處理,這樣可以保證陣列分的更加平均。如果遇到相等元素繼續掃描,對於一個具有n個相同元素的陣列來說,劃分後得到的兩個子陣列長度可能為n-1和0。
兩側的掃描都停止之後,根據掃描指標是否相交會有三種情況:若 i < j,則交換a[i]與a[j],i+1,j-1;若 i > j,交換a[p]與a[j];若i = j,則交換a[p]與a[j]。眼尖的讀者估計看出來了,後兩種情況其實是一種。
下圖為Hoare劃分的示意圖:
public class Main {
static int[] a= {5, 3, 1, 9, 8, 2, 4, 7};
public static void main(String[] args) {
fastsort(0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static int Hoare(int l, int r) {
int p = a[l];
int i = l-1;
int j = r+1 ;
while (true) {
do {
j--;
} while (a[j] > p);
do {
i++;
} while (a[i] < p);
if (i < j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
} else
return j;
}
}
private static void fastsort(int l, int r) {
if (l < r) {
int s = Hoare(l, r);
fastsort(l, s);
fastsort(s+1, r);
}
}
}
一般來說,如果我們要考慮一個演算法的實用性,我們需要討論的不是最壞情況下的效率,而應該是平均情況下的效率。實際經過分析,快速排序平均情況下的操作僅比最優情況多執行39%的比較過程而已。所以在處理隨機序列時,速度要強於歸併排序和堆排序。
缺點:快速排序並非穩定性排序方式,空間複雜度為O(logn),不及堆排序的空間複雜度O(1).
提出問題:對於陣列非常小的情況下(對於大多數計算機來說,元素個數為5-15),使用插入排序會更快
解決思路1:判斷元素個數,較小時採用插入排序,較大時採用快速排序。
解決思路2:不再對劃分出來的較小陣列排序,而是在快速排序結束後再使用插入排序的方法對整個接近有序的陣列進行細微調節。
提出問題:如果我們每次都選取第一個元素為樞軸,這自然會有諸多不便,不能應對一些特殊的情況。
解決思路:隨機化快速排序、三平均劃分法,下面給出程式碼,基本思想還是劃分,有興趣的讀者可以研究一下。
隨機化快排:使用隨機元素作為樞軸。
public class Main {
static int[] a= {5, 3, 1, 9, 8, 2, 4, 7};
public static void main(String[] args) {
fastsort(0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static int random_partition(int l, int r) {
int i = (int) (l + Math.random() % (r - l + 1));
int temp = a[i];
a[i] = a[r];
a[r] = temp;
return partition(l, r);
}
private static int partition(int l, int r) {
int p = a[r];
int i = l - 1;
for (int j = l; j < r; j++) {
if (a[j] <= p) {
i++;
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
int temp = a[i+1];
a[i+1] = a[r];
a[r] = temp;
return i+1;
}
private static void fastsort(int l, int r) {
if (l < r) {
int s = random_partition(l, r);
random_partition(l, s-1);
random_partition(s+1, r);
}
}
}
三平均劃分法快排:以最左元素、最右元素、最中間元素的中位數為樞軸。
public class Main {
static int[] a= {5, 3, 1, 9, 8, 2, 4, 7};
public static void main(String[] args) {
fastsort(0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
public static int mid_partition(int l, int r)
{
int range = r - l + 1;
int mid1 = (int) (l + Math.random() % range);
int mid2 = (int) (l + Math.random() % range);
int mid3 = (int) (l + Math.random() % range);
int mid = (a[mid1] < a[mid2]) ?
(a[mid2] < a[mid3] ? mid2 : (a[mid1] < a[mid3] ? mid3 : mid1)):
(a[mid1] < a[mid3] ? mid1 : (a[mid2] < a[mid3] ? mid2 : mid3));
int temp = a[mid];
a[mid] = a[r];
a[r] = temp;
return partition(l, r);
}
private static int partition(int l, int r) {
int p = a[r];
int i = l - 1;
for (int j = l; j < r; j++) {
if (a[j] <= p) {
i++;
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
int temp = a[i+1];
a[i+1] = a[r];
a[r] = temp;
return i+1;
}
private static void fastsort(int l, int r) {
if (l < r) {
int s = mid_partition(l, r);
fastsort(l, s-1);
fastsort(s+1, r);
}
}
}
提出問題:在劃分方式上Lomuto劃分和Hoare劃分也可以進行改進,使用諸如三路劃分的方式,將陣列劃分3段,每段元素分別小於、等於、大於樞軸元素等等。當然,這些就僅供研究學習了,平時我們使用最簡單版本的快速排序是沒有任何問題的。