1. 程式人生 > >線性時間排序: 三種非基於比較的內部排序演算法

線性時間排序: 三種非基於比較的內部排序演算法

未完待續。。。。。。

一、基於比較的排序演算法,在最壞情況下,最少都需要用O(nlgn)次比較。

下面來證明一下。

不妨我們假設 排序都是從小到大進行排序。

說明用1、2、3來表示三個元素,用π(1)、π(2)、π(3)來表示該元素對應的值。用尖括號〈1,3,2〉表示π(1) ≤ π(3) ≤ π(2)

對於n個元素而言,由於寫演算法不知道 未來每個待排元素的大小,所以要使排序演算法能正確地工作,其必要條件就是:n個元素的 n! 種排列中的每一種都能夠作為輸出結果。

從本質上來講,比較排序可以被抽象地視為 決策樹。由於比較的結果只有兩種:1.小於等於 2.大於。所以該決策樹是一棵二叉樹。同時,一棵決策樹 表示某種排序演算法作用於給定輸入所做的所有的可能比較。這裡程式的控制結構、資料移動等體力活都被忽略了。我們用圖片來形象地說明一下。


上圖中,決策樹的所有葉子結點都是一種可能的排序結果。再次強調一遍,用1、2、3來表示第1個元素、第2個元素、第3個元素,而元素的大小用π(1)、π(2)、π(3)來表示。

從根結點(即起始輸入)到一個葉子結點,該條路徑對應於比較排序演算法的一次實際執行過程。這條路徑的長度,就表示對應的排序演算法中最壞情況下的比較次數。這個最壞情況下的比較次數 正是 樹的高度。於是證明的題目就變成了 求具有n! 個葉子結點的二叉樹的最小高度。

由於這是計算理論上該二叉樹的最低高度,所以肯定是滿二叉樹。高度為h的二叉樹的葉子結點的數目記為L,所以有 n! ≤ L ≤ 2h  對該式兩邊同時取對數得:

h ≥ lg(n!) = O(nlgn)

這樣,我們從理論上證明了凡是基於比較的排序演算法,其最壞情況下的最少時間複雜度一定不小於O(nlgn)。注意,這裡的對數都是以2為底的。

二、計數排序(Counting Sort)

先說明一下,這種排序演算法本身不會被獨立使用,通常是作為基數排序的子過程來呼叫的。

計數排序假設n個輸入元素中的每一個都是介於0到k-1之間的整數。所以說,使用該演算法時,必須知道待排序的整數的可能的最大值

計數排序的基本思想就是對每一個輸入的元素x,確定出小於等於x的元素的個數。這樣,就可以把x直接放到它的最終位置上了。當然,我們是先將x直接放到一個緩衝陣列中。這個緩衝陣列和原陣列具有同樣的大小。

舉個例子,比如,有17個元素小於等於x,那麼x就屬於第17個位置。當有幾個元素相同時,這個方案要略做修改。因為不能把它們都放在同一位置上。

我們假定輸入陣列為A[0...n-1],那麼輸入陣列的長度為n。為完成此演算法,我們還需要兩個陣列:存放排序結果的B[0...n-1],以及提供臨時儲存區的C[0...k]。k為所有待排序整數中的可能最大值。

直接用C語言實現之,如下所示:

void CountingSort(int A[], int B[], int n, int k)
{
	int* C = new int[k+1]; //因為還有0值要存,共有k+1個桶
	
	//清空記數
	for(int i = 0; i <= k; i++)
		C[i] = 0;
		
	//統計原陣列中每個值出現的次數
	for(int j = 1; j < n; j++)
		C[A[j]]++;
	
	//統計小於等於每個值對應的個數
	for(int i = 0; i < k; i++)
		C[i] += C[i-1];
	
	//將每個值 直接輸出到緩衝陣列中的最終位置上
	//從後向前輸出,保證排序的穩定性
	for(int j = n - 1; j >=0; j--)
	{
		B[C[A[j]]] = A[j];
		C[A[j]]--;
	}
}

下面我們來分析一下時空複雜度。

從上述程式碼分析,時間複雜度為O(k+n+k+n) = O(k+n),空間複雜度也是O(k+n)。其中k是待排序整數的取值範圍內的最大值,n為待排序整數的個數。

我們可以看到,上述程式碼中根本沒有出現輸入元素之間的比較。因為它不是基於比較的排序演算法,所以它的時間複雜度才可能小於O(nlogn)。說排序演算法的時間下界為O(nlogn),是針對於基於比較的排序演算法的。

計數排序說白了,是用輸入元素自身的實際值來確定它在陣列中的最終位置的。

同時,從上面的程式碼看出,只有輸出時是從後向前輸出的,才能保證計數排序的穩定性:具有相同值的元素在輸出陣列中的相對次序與它們在輸入陣列中的次序相同。也就是說,兩個關鍵字相同的資料,在輸入陣列中先出現的,在輸出陣列中也位於前面。穩定性只有在存在衛星資料時才顯得比較重要。

再強調一點,如果在後面要講的基數排序中使用的計數排序子過程不是穩定的話,那麼基數排序也就廢了。

在介紹基數排序之前,先來說明一下為什麼計數排序不好。

比如我們用計數排序來對這三個數進行排序:1,100,9999999。這時,n=3, k = 107。這時,雖然只是對3個數字進行排序,但是時間複雜度卻是千萬量級的,太慢了。用最水的氣泡排序、雞尾酒排序都比它快的多。而基數排序,正好利用了計數排序的優點,同時避免了計數排序的缺點。

三、基數排序(radix sort)

變數說明:n個數字,每個數字都有d位,每位上的數字取值範圍都是0到k-1

先來說明一下什麼是基數。

基數,radix,指的是在某一記數系統中所有的單個數字位。比如在十進位制表示中,基數指的就是0到9這十個數字,而在十二進位制中,基數指的就是0...9、A、B這十二個數字。

知道了什麼是基數,下面開始介紹基數排序(radix sort)。

提到基數排序,很多人馬上會提到LSB和MSB。下面先來講講什麼是LSB和MSB。

MSB是Most Significant Bit的縮寫,意為 最高有效位。在二進位制數中,MSB是最高加權位。與十進位制數字中最左邊的一位類似。同理,LSB是Least Significant Bit的縮寫,意為 最低有效位。通常,MSB位於二進位制數的最左側,LSB位於二進位制數的最右側。

舉個通俗的例子,當比較兩個十進位制數字大小的時候,是MSB,即最高位起決定性作用。91肯定比19要大。但是當判斷一個十進位制數字是否為偶數時,是LSB,即92肯定是偶數,儘管十位數字是9,它也是偶數。

基數排序中的LSB和MSB有相似的意思。LSB就是從低位開始,從低到高,逐位數字進行計數排序。MSB則正好相反。

雖然書上全都是用的LSB基數排序,但是MSB基數排序也是可以實現的。如果數字從高位開始排序,意味著有n位,就得有10^n個桶。因為你先排最高位,然後對於這個最高位又要分出十個桶排下一位。比如現在有13,23,12,22,11這五個數。你先為高位排序,就相當於把十位為1的分在一個桶1裡(13,12,11),十位為2的分在一個桶2(22,23)裡。然後在桶1和桶2之中剩下的元素排序((11),(12),(13))和((22),(23))。這樣如果有很多位數,桶就很多。但是從最低位開始排就只需要10個桶,每移動一位,就用針對那一位排序(把元素扔進桶裡)。所以不會佔用大量的桶。同時,從高位開始排序,就要分段,每排完一位,把分不出大小的幾個當成一段,一段段的排,不能讓排完的資料跨段移動,保證這一段的數都比下一段小,排到最後每段就只有一個數了。這樣就完全沒有利用到每次呼叫計數排序都是穩定排序這一點,所以低位排序優於高位排序。

下面介紹LSB基數排序。

本質上還是計數排序,不過每次比較的都是一位數字,可能取值為0到9這十個數字。所以k = 10。也就是說需要10個桶(其實說成是棧更好)來存放資料。

對於每一位數字的排序,時間複雜度為O(n+k),設每個元素都有d位數字,則總共的時間複雜度為O(d(n+k))。空間複雜度仍為O(n+k)。