1. 程式人生 > >深入淺出數據結構C語言版(22)——排序決策樹與桶式排序

深入淺出數據結構C語言版(22)——排序決策樹與桶式排序

不改變 自然 只需要 都是 變種 限定 style buck oid

  在(17)中我們對排序算法進行了簡單的分析,並得出了兩個結論:

  1.只進行相鄰元素交換的排序算法時間復雜度為O(N2)

  2.要想時間復雜度低於O(N2),算法必須進行遠距離的元素交換

  

  而今天,我們將對排序算法進行進一步的分析,這一次的分析將針對“使用比較進行排序”的排序算法,到目前為止我們所討論過的所有排序算法都在此範疇內。所謂“使用比較進行排序”,就是指這個算法實現排序靠的就是讓元素互相比較,比如插入排序的元素與前一個元素比較,若反序則交換位置,再比如快速排序小於樞紐的元素分為一組,大於樞紐的元素分為另一組。它們都是依靠“比較”來完成排序工作。

  要對使用比較進行排序的算法進行分析,我們首先要引入一個概念:決策樹。

  決策樹就是這樣的二叉樹:樹的根結點表示“元素的所有可能順序”,樹的每一條邊表示“一種可能的比較”,一條邊連接的孩子結點則是“父結點經過該邊所代表的比較後剩余的可能順序”。這樣的解釋很難理解,但有圖搭配就可以好很多:

  技術分享

  上圖是一棵三元素排序決策樹,根結點處表示所有可能的順序,而從根延伸下來的兩條邊分別表示了兩種“決策”,或者說“比較”,經過該“決策”後就可以得出剩余的可能情況,比如根結點的左孩子是經歷決策“a<b”後剩余的可能。顯然,葉子代表只剩一種可能順序。

  註意,決策樹並沒有代表任何排序算法,即沒有哪個排序算法是這樣工作的。但是決策樹可以給我們這樣一個信息:通過比較來排序的算法,本質上就是沿著決策樹從根到某個葉子的路徑比較下去。

  因此,分析這條“路徑”平均經過多少條邊,就相當於分析使用比較的排序算法平均需要多少次比較。這也是本次分析與(17)的不同之處,在(17)中我們的分析針對的是排序算法的“交換”次數,這次我們分析的是“比較”次數,而比較次數顯然更為關鍵,因為不論元素是否遠距離交換,比較總是存在的。

  要分析使用比較進行排序的算法平均進行幾次比較,我們就必須知曉以下定理。

  定理1:深度為d的二叉樹,最多擁有2d個葉子

  證明很簡單:二叉樹的深度d即二叉樹中深度最大的葉子的深度d,若存在某個葉子深度不是d,則可以在該葉子下添加兩個孩子而不改變樹的深度,因此深度為d的二叉樹要有最多的葉子則必為滿二叉樹,此時有葉子2d

個(深度為d的層最多有2d個結點)

  定理2:有y個葉子的二叉樹,深度至少為[logy](底數默認為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的情況,就可以得出元素的順序:

//size為數組src的大小,也即元素個數
void BucketSort(unsigned int *src,unsigned int size)
{
    //MAX為宏,表示src中元素不會大於等於的值
    unsigned int bucket[MAX] = { 0 };
    
    //將元素們“扔進桶裏”
    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];
}

  顯然,桶式排序的局限性在於要求元素必須是自然數,必須存在上限且上限不可過分大,因為元素的上限決定了桶的數量,而桶的數量並不是想要多少有多少,比如我的電腦就不支持分配一個大小為INT_MAX的數組。

  桶式排序還有一種變種,只需要10個桶即可,感興趣的可以去搜索“桶式排序”或“基數排序”,此處不做介紹。

  本篇博文就是有關排序的最後一篇博文了,下一篇博文開始,我將會介紹圖論算法,並不難,至少理解起來是不難。

深入淺出數據結構C語言版(22)——排序決策樹與桶式排序