著作權,轉載請註明出處,謝謝!
一個線性時間下的原地置換排序演算法
排序演算法
現有的線性時間排序(時間複雜度為ο(n)演算法,比如計數排序,基數排序,桶排序等,雖然在時間複雜度上都能保持線性,但不能進行原地置換排序,需要額外的輔助空間.一個演算法能否同時做到時間複雜度為ο(n),空間複雜度為Ω(1)呢?
我們知道,在現有的常規線性演算法(計數排序,桶排序,基數排序等)當中,計數排序和桶排序因為不是原地置換排序,因此都需要額外的輔助空間來完成排序,因此無法達到上述目的,而基數演算法本身是一個複合演算法,如果能保證其位排序的演算法能做到空間複雜度為Ω(1)和時間複雜度ο(n),那顯然可以達到上述要求的演算法目標.對於基數排序中的位排序要求必須具有穩定性.既要滿足線性時間,常量空間,並且穩定的演算法,在現有常用演算法中還沒有這種演算法可以達到這種要求。
我們選基數排序作為實現這個演算法目標的基本演算法,由於我們進行的是整數排序,基數排序的位就是整數二進位表示裡的位,而對於整數的二進位形式,每位上的數就只有0和1(這是個很有利的因素),而對0和1序列排序做到線性時間,只需要利用0作為基元,做一次快排劃分即可,快排劃分的演算法時間複雜度是ο(n),而且也是原地置換排序,空間上的要求可以滿足(Ω(1)),但問題在於對於基數排序,要求其位排序必須是穩定排序,而快排劃分顯然不是穩定的。但如果我們能找到一種方法將這種不穩定帶來的影響消除,使得其不影響最終結果,問題就解決了。那麼而怎樣讓快排劃分不影響最終結果呢?
我們知道基數排序有兩種基本的形式,一種是從低位到高位進行排序,一種是從高位到低位進行,經過分析發現,如果採用從高位到低位的方式進行,是可以讓快排劃分不影響到最終結果的,我們考慮一般情況,設B(k),B(k-1)….,B(1)表示一個基數排序所有的位(對於32位整數k=32),對第i位進行排序,而前m位(m=k-i-1)位已經排好序B(k)…B(i+1),在進行i位排序時,如果我們的快排劃分只針對前m位相同的情況進行,則快排劃分的結果顯然不會影響最終結果.這樣對於第i位上的一次快排,就可以轉換成若干個小區間的快排劃分(用前m位元進行劃分,前m位相同的為一個區間),而且由於前m位已經排好序,那麼顯然前m位相同的都必然緊挨在一起,這種情況下雖然一次快排劃分變成了p次快排劃分(p<n),但整體上的時間複雜度並沒有增加,雖然在具體的演算法技巧處理時會考慮索引回溯,但時間複雜度不會改變,因為最極端的回溯小於等於n,因此時間複雜度最多為2n.而對於整數而言,前m位的擷取和比較都非常快,所以在進行時間複雜度分析時可以不考慮,所以整體上演算法每位上排序時間複雜度還是為32n到64n之間,而回溯通過一定技巧處理也可以消除,那麼演算法的時間複雜度就可以小於32n.由於這種位演算法空間複雜度為Ω(1),因此整個演算法滿足前面提到的演算法目標。
下面是具體的C#語言下的演算法實現(為了減少交換次數,這裡位排序不是嚴格按照快排劃分進行,而是針對0和1的特點,用一個指標i指向序列第1個1,遍曆指標在前,遇到0就與i交換,i隨後右移一位,這可以大大減少交換次數):
private void BitSortAndDelRepeatorsA(int[] A)
{
//擷取數組長度
int theN = A.Length;
//從高位到低位開始排序,這裡從31位開始,32位是符號位不考慮,
//或者單獨考慮。
for (int i = 31; i >= 1; i--)
{
//當前排序之前的值,只有該值相同才進行快排分組,如果不相同,
//則重新開始另外一次快排
//這很關鍵,否則快排的不穩定就會影響最後結果.
int thePrvCB = A[0] >>(i) ;
//快排開始位置,會變化
int theS = 0;
//快排插入點
int theI = theS-1;
//2進位基數,用於測試某一位是否為0
int theBase = 1 << (i-1);
//位基元始終為0,
int theAxBit = 0;
//分段快排,但總體上時間複雜度與快排分組一樣.
for (int j = 0; j < theN; j++)
{
//擷取當前數組值的前面已拍過序的位元值。
int theTmpPrvCB = A[j]>> (i);
//如果前面已排過的位不相同,則重新開始一次快排.
if (theTmpPrvCB !=thePrvCB)
{
theS = j;
theI = theS - 1;
theAxBit = 0;
thePrvCB = theTmpPrvCB;
j--;//重新開始排,回朔一位.
continue;
}
//如果前面的數相同,則尋找第1個1,thI指向其
//如果相同,則按快排處理
int theAJ = (A[j] &(theBase)) > 0 ? 1 : 0;
//如果是重新開始排,則尋找第1個1,並人theI指向其.這可以
//減少交換,加快速度.
if (theI < theS)
{
if (theAJ == 0)
{
continue;
}
theI = j;//Continue保證J從theI+1開始.
continue;
}
//交換.
if (theAJ <= theAxBit)
{
int theTmp = A[j];
A[j] = A[theI];
A[theI] = theTmp;
theI++;
}
}
}
}
演算法分析
時間複雜度雖然有32*n,但由於32是個常數,因此整個演算法的時間複雜度還是ο(n),空間複雜度方面,演算法中只是利用了有限的幾個交換變數(<20),因此空間複雜度為Ω(1)。但演算法的穩定性方面,由於其位演算法是不穩定的,因此整個演算法也是不穩定的。
該演算法經過我大量測試,其複雜度符合演算法分析結果。