在(17)中我們對排序演算法進行了簡單的分析,並得出了兩個結論:
1.只進行相鄰元素交換的排序演算法時間複雜度為o(n2)
2.要想時間複雜度低於o(n2),排序演算法必須進行遠距離的元素交換
而今天,我們將對排序演算法進行進一步的分析,這一次的分析將針對「使用比較進行排序」的排序演算法,到目前為止我們所討論過的所有排序演算法都在此範疇內。所謂「使用比較進行排序」,就是指這個演算法實現排序靠的就是讓元素互相比較,比如插入排序的元素與前乙個元素比較,若反序則交換位置,再比如快速排序小於樞紐的元素分為一組,大於樞紐的元素分為另一組。它們都是依靠「比較」來完成排序工作。
要對使用比較進行排序的演算法進行分析,我們首先要引入乙個概念:決策樹。
決策樹就是這樣的二叉樹:樹的根結點表示「元素的所有可能順序」,樹的每一條邊表示「一種可能的結果」,一條邊連線的孩子結點則是「父結點經過該邊所代表的比較結果後剩餘的可能順序」。這樣的解釋很難理解,但有圖搭配就可以好很多:
上圖是一棵三元素排序決策樹,根結點處表示所有可能的順序,而從根延伸下來的兩條邊分別表示了兩種「決策結果」,或者說「比較結果」,若符合該「決策結果」就可以得出剩餘的可能情況,比如根結點的左孩子是經歷決策「a
注意,決策樹並沒有代表任何排序演算法,即沒有哪個排序演算法是這樣工作的。但是決策樹可以給我們這樣乙個資訊:通過比較來排序的演算法,本質上就是沿著該元素集合的決策樹從根到某個葉子的路徑比較下去。
因此,分析這條「路徑」平均經過多少條邊,就相當於分析使用比較的排序演算法平均需要多少次比較。這也是本次分析與(17)的不同之處,在(17)中我們的分析針對的是排序演算法的「交換」次數,這次我們分析的是「比較」次數,而比較次數顯然更為特殊,因為不論元素是否遠距離交換,比較總是存在的。
要分析使用比較進行排序的演算法平均進行幾次比較,我們就必須知曉以下定理。
定理1:深度為d的二叉樹,最多擁有2d個葉子
證明很簡單:二叉樹的深度d即二叉樹中深度最大的葉子的深度d,若存在某個葉子深度不是d,則可以在該葉子下新增兩個孩子而不改變樹的深度,因此深度為d的二叉樹要有最多的葉子則必為滿二叉樹,此時有葉子2d個(深度為d的層最多有2d個結點)
定理2:有y個葉子的二叉樹,深度至少為[log
y](底數預設為2)
證明:由定理1可以直接推出。
這個證明可能有點難懂,我們可以觸類旁通一下:假如1元錢最多可以買5個糖,那麼5個糖最少需要多少錢?答案是1元,恰好是反函式的關係。類似的,深度為x的二叉樹最多有y個葉子,那麼有y個葉子的二叉樹最少有多少深度?答案就是x了。
定理3:n元素排序的決策樹有n!個葉子結點
證明:n元素排序的可能順序共有n!個,而決策樹的葉子就是表示「僅剩的可能性」即某一種可能順序,所以n元素排序的決策樹共有n!個葉子
定理4:使用元素比較的排序演算法至少需要o(logn!)次比較
證明:由定理2可知,有y個葉子的決策樹,深度至少為[logy],而n元素排序決策樹葉子數量必為n!,所以n元素排序決策樹深度至少為[logn!],也即n元素排序決策樹的任一葉子深度至少為[logn!],而葉子的深度就表示了從根到該葉子的路徑上經過的邊的數量,也就是「比較」的次數,因此定理4成立。
定理5:使用元素比較的排序演算法至少需要ω(n*logn)次比較
證明:根據定理4進行繼續計算:
logn!=log(n*(n-1)*(n-2)*……*2*1)
=logn+log(n-1)+log(n-1)+……+log2+log1
>=logn+log(n-1)+……log(n/2)
>=(n/2)*log(n/2)=(n/2)*log(n*1/2)=(n/2)*logn+(n/2)*log(1/2)
>=(n/2)*logn-n/2
=ω(n*logn)
定理5就是我們這次分析的最終結果,並且我們可以將定理5進行乙個推廣:假設存在x種可能情形,確定具體情形的方法是不斷地問「是或否」型的問題,那麼累計需要問的次數至少是[logx]。
那麼根據定理5,堆排序、合併排序和快速排序是否已經代表了排序的最快境界呢?不是的,因為定理5依然是有「限定」的,那就是通過比較進行排序的演算法才符合,也就是說不是通過比較來完成排序的話,是可能突破這個界限的。
不通過比較來完成排序,是個什麼樣子?我們這裡可以舉乙個簡單的例子:桶式排序。其時間複雜度是o(n)。
現實生活中桶式排序的思想是不少見的,舉個例子感受一下:
假設我們有很多硬幣,一分、二分、五分、一角、五角和一元都有,現在我們想要將它們按從小到大排好序,該怎麼做?手工模擬任意排序演算法都可以完成這項工作,但沒有人會這麼傻。大部分人的做法都是:準備6個「桶」,分別存放這6種硬幣,一分的扔進一分桶,一元的扔進一元桶,所有硬幣扔進桶裡了,再按順序從桶裡倒出來,排序就完成了。
將上述思想轉換到計算機中就是這樣:假設我們的元素都是自然數,且一定小於max,那我們只要準備max個空桶,即定義乙個整形陣列bucket[max],並將其全部初始化為0。然後遍歷所有元素,若元素為i,則令bucket[i]加1,最後統計陣列bucket的情況,就可以得出元素的順序:
//顯然,桶式排序的侷限性在於要求元素必須是自然數,必須存在上限且上限不可過分大,因為元素的上限決定了桶的數量,而桶的數量並不是想要多少有多少,比如我的電腦就不支援分配乙個大小為int_max的陣列。size為陣列src的大小,也即元素個數
void bucketsort(unsigned int *src,unsigned int
size)
;
//將元素們「扔進桶裡」
for (unsigned int i = 0;i < size;++i)
++bucket[src[i]];
//將桶裡的元素「倒出來」
unsigned int j = 0
;
for (unsigned int i = 0;i < max;++i)
for (unsigned int x = 0;x < bucket[i];++x)
src[j++] =bucket[i];
}
桶式排序還有一種變種,只需要10個桶即可,感興趣的可以去搜尋「桶式排序」或「基數排序」,此處不做介紹。
淺入淺出資料結構(18) 希爾排序
而希爾排序就是 簡單地 將這個道理應用到了插入排序中,將插入排序小小的公升級了一下。那麼,希爾排序是怎麼將這個道理應用於插入排序的呢?我們先來回顧一下插入排序的 void insertionsort int a,unsigned int size 不難看出,在插入排序中,對於每乙個元素,我們都令其執...
淺入淺出資料結構(21) 合併排序
在講解合併排序之前,我們先來想一想下面這個問題如何解決 有兩個陣列a和b,它們都已各自按照從小到大的順序排好了資料,現在我們要把它們合併為乙個陣列c,且要求c也是按從小到大的順序排好,請問該怎麼做?這個問題非常容易解決,我們將a b和c都視為佇列,然後不斷比較a和b的首部,取出其中更小的資料出隊,然...
淺入淺出資料結構(16) 插入排序
從這一篇博文開始,我們將開始討論排序演算法。所謂排序演算法,就是將給定資料根據關鍵字進行排序,最終實現資料依照關鍵字從小到大或從大到小的順序儲存。而這篇博文,就是要介紹一種簡單的排序演算法 插入排序 insertion sort 為了使精力專注於排序演算法本身,而不是對資料的分析 處理,若無特殊說明...