快速排序(二) 隨機化——C#實現
一 快速排序隨機化
在快速排序(一)中提到,儘管快速排序的最差時間複雜度是θ(n^2),但是其平均時間複雜度是θ(nlgn),在文末的測試中也反映了這一點。但是作出這一判斷的前提是,我們認為輸入陣列的所有排列都是等概率的。但是這一假設並非往往都成立。
考慮一個已經是按照升序排序的陣列作為快速排序的輸入陣列,則排序中每一次劃分都會將元素劃分成元素個數分別為n-1和0的兩個陣列,如此一來,如果用快速排序演算法進行元素排序,則其時間複雜度將會是θ(n^2)。而如果在某種應用場景的情況下,所有的待排序輸入都是這種已經排序或者近似已排序的陣列,則利用快速排序演算法進行排序的平均時間複雜度也會是θ(n^2)。
在《演算法導論》一書的習題中就提到過一個這樣的場景:銀行會按照交易時間來記錄賬戶的交易情況,但是客戶收到銀行對賬單時卻希望能夠按照支票號碼的順序來排列,這就需要銀行按照支票號對原本的交易記錄進行重新排列。這實際上是對大量近似有序的陣列元素進行重排列。因此在這種應用場景的情況下如果用原始的快速排序演算法,其耗時甚至比插入排序都更差。
我們可以通過一種被稱為隨機化抽樣的隨機化技術來解決這一問題。與原始版本的快速排序中始終以A[r]作為劃分待排陣列的主元不同,隨機化版本是從A[p..r]中隨機取一個元素,將其與A[r]交換後作為主元。這樣一來,無論輸入陣列的原始順序如何,即使是已經排好序的陣列,在平均請下隨機抽樣選擇的主元都能使陣列的劃分更平衡。
二 快速排序隨機化的實現
與原始版本相比,隨機化的快速排序只在選取主元的時候有改動,其他地方不變。
1 用於整數陣列的升序排序
private static int Partition(int[] array, int p, int r) { int x = array[r]; int i = p - 1; for (int j = p; j < r; j++) { if (array[j] < x) { i++; Exchange(ref array[i], ref array[j]); } } i++; Exchange(ref array[i], ref array[r]); return i; } public static int RandomizedParition(int[] array, int p, int r) { Random random = new Random(); int i = random.Next(p, r + 1); Exchange(ref array[i], ref array[r]); return Partition(array, p, r); } private static void RandomizedQuickSort(int[] array, int p, int r) { if (p < r) { int q = RandomizedParition(array, p, r); RandomizedQuickSort(array, p, q - 1); RandomizedQuickSort(array, q + 1, r); } } public static void RandomizedQuickSort(int[] array) { RandomizedQuickSort(array, 0, array.Length - 1); }
2 泛型版本
private static int Partition(T[] array, int p, int r, Comparison comparison)
{
T x = array[r];
int i = p - 1;
for (int j = p; j < r; j++)
{
if (comparison(array[j], x) < 0)
{
i++;
Exchange(ref array[i], ref array[j]);
}
}
i++;
Exchange(ref array[i], ref array[r]);
return i;
}
private static int RandomizedPartition<T>(T[] array, int p, int r, Comparison<T> comparison)
{
Random random = new Random();
int i = random.Next(p, r + 1);
Exchange<T>(ref array[i], ref array[r]);
return Partition<T>(array, p, r, comparison);
}
private static void RandomizedQuickSort<T>(T[] array, int p, int r, Comparison<T> comparison)
{
if (p < r)
{
int q = RandomizedPartition<T>(array, p, r, comparison);
RandomizedQuickSort<T>(array, p, q - 1, comparison);
RandomizedQuickSort<T>(array, q + 1, r, comparison);
}
}
public static void RandomizedQuickSort<T>(T[] array, Comparison<T> comparison)
{
RandomizedQuickSort<T>(array, 0, array.Length - 1, comparison);
}
三 執行結果
按照快速排序(一)中的測試方法進行測試。
為測試隨機化快速排序的效果,我們分三種情況來執行演算法,並與原始版本的快速排序做比較。
1 輸入測試陣列隨機化,測試結果:
n = 10, 原始快排:averageCount = 23 隨機化快排:averageCount = 24
n = 100, 原始快排:averageCount = 641 隨機化快排:averageCount = 647
n = 1000, 原始快排:averageCount = 10926 隨機化快排:averageCount = 10922
n = 10000, 原始快排:averageCount = 155615 隨機化快排:averageCount = 155740
由此可見,在輸入陣列隨機化的情況下,兩種演算法時間複雜度幾乎一樣。
2 輸入測試陣列為有序陣列,測試結果:
n = 10, 原始快排:averageCount = 45 隨機化快排:averageCount = 26n = 100, 原始快排:averageCount = 4950 隨機化快排:averageCount = 761
n = 1000, 原始快排:averageCount = 499500 隨機化快排:averageCount = 12905
n = 10000, 原始快排:averageCount = 49995000隨機化快排:averageCount = 178202
由此可見,在輸入陣列為有序陣列時,原始版快速排序的時間複雜度為θ(n^2),而隨機化版的快速排序演算法,儘管其比較次數比隨機化輸入的要稍大,但仍然滿足平均時間複雜度為θ(nlgn)。
3 輸入測試陣列中元素相同,測試結果:
n = 10, 原始快排:averageCount = 45 隨機化快排:averageCount = 45
n = 100, 原始快排:averageCount = 4950 隨機化快排:averageCount = 4950
n = 1000, 原始快排:averageCount = 499500 隨機化快排:averageCount = 499500
由此可見,在輸入陣列中元素相同時,原始快速排序演算法和隨機化快速排序演算法的時間複雜度均為θ(n^2)。即輸入陣列元素相同時,即使是隨機化快速排序演算法,其效能也比插入排序更差。
四 針對相同元素值的隨機化快速排序
針對相同元素值的陣列排序的思路是改進劃分機制,在原有的劃分中,我們把除主元之外的元素分為兩部分,小於主元的在左邊,大於或等於主元的在右邊。而新的劃分方法同過比較待劃分元素與主元的相對大小被劃分為三部分:小於主元的在左邊、等於主元的在中間、大於主元的在右邊。對陣列A[p..r]的劃分返回兩個下標q,t,使得p<=q<=t<=r,其中A[q..t]中的元素都相等且等於主元,A[p..q-1]中的元素都小於主元,A[t+1..r]中的元素都大於主元。在遞迴解決子問題時,只需要對A[p..q-1]和A[t+1..r]兩個子陣列進行排序。A[q..t]已經被排好序,且處在陣列的正確的位置上。
注意到一個細節,在執行劃分的過程中,如果q == t,則說明此時A[q..t]中沒有和主元相同的元素,此時,如果下一個被比較的元素小於主元,則只需要將q和t加1後,交換A[q]和A[i]。而如果q != t,則A[q+1..t]為與主元相同的元素,此時如果下一個被比較的元素小於主元,則在q和t加1的情況下,需要進行兩次交換。例如:
1 3 2 8 6 3 5 8 7 24 => 1 3 2 36 8 5 8 7 2 4 以4為主元,判斷第六個元素時,此時已劃分的部分沒有與主元相等的元素,故只需要進行一次交換。
1 3 2 4 4 8 6 3 5 8 7 2 4 => 1 3 2 3 4 8 6 4 5 8 7 2 4 =>1 3 2 3 4 46 8 5 8 7 2 4 以4為主元,判斷第八個元素時,此時已劃分部分已經有與主元相等的元素,故需要執行兩次交換。
五 針對相同元素值的隨機化快速排序的實現(C#)
演算法的泛型版本C#實現:
public static int[] RandomizedPartionEx<T>(T[] array, int p, int r, Comparison<T> comparison)
{
//隨機抽樣獲得主元
Random random = new Random();
int index = random.Next(p, r + 1);
Exchange<T>(ref array[index], ref array[r]);
T key = array[r];
int q = p - 1;
int t = p - 1;
for (int i = p; i < r; i++)
{
if (comparison(array[i], key) < 0)
{
q++;
t++;
if (q == t)
{
if (i != q)
{
Exchange<T>(ref array[i], ref array[q]);
}
}
else
{
if (i != q)
{
Exchange<T>(ref array[i], ref array[q]);
}
if (i != t)
{
Exchange<T>(ref array[i], ref array[t]);
}
}
}
else if (comparison(array[i], key) == 0)
{
t++;
if (i != t)
{
Exchange<T>(ref array[i], ref array[t]);
}
}
}
q++;
t++;
Exchange<T>(ref array[r], ref array[t]);
int[] partionResult = new int[2];
partionResult[0] = q;
partionResult[1] = t;
return partionResult;
}
private static void RandomizedQuickSortEx<T>(T[] array, int p, int r, Comparison<T> comparison)
{
if (p < r)
{
int[] partionResult = RandomizedPartionEx(array, p, r, comparison);
RandomizedQuickSortEx<T>(array, p, partionResult[0] - 1, comparison);
RandomizedQuickSortEx<T>(array, partionResult[1] + 1, r, comparison);
}
}
public static void RandomizedQuickSortEx<T>(T[] array, Comparison<T> comparison)
{
RandomizedQuickSortEx(array, 0, array.Length - 1, comparison); ;
}
六 針對相同元素的隨機化快速排序演算法的執行結果
輸入測試陣列中元素相同,測試結果:
n = 10, averageCount = 18
n = 100, averageCount = 198
n = 1000, averageCount = 1998
n = 10000, averageCount = 19998
輸入測試陣列中元素隨機化,測試結果:
n = 10, averageCount = 34
n = 100, averageCount = 950
n = 1000, averageCount = 16289
n = 10000, averageCount = 232345
由此可知,該演算法在輸入結果相同的情況下能夠有θ(n)的時間複雜度,而當輸入為隨機排序時,仍然是滿足θ(nlgn)的時間複雜度,只是係數稍微比原始版本大一些。