【算法】排序 (三):二叉樹排序&基於散列排序(C++實現)

一. 二叉樹排序

  • 對比前面所述的一些排序算法,之前通常使用數組或者鏈表之類的初級數據結構,二叉樹排序使用的是高級數據結構——樹。實際上是使用二叉搜索樹的機制,對二叉搜索樹中序遍歷即可以得到排序數組。代碼可以直接參考之前的文章[1]

  • 時間複雜度分析
    二叉樹排序主要耗時的兩個環節爲:構建二叉樹和中序遍歷。
    構建二叉樹的主要耗時是在比較和移動指針,而比較和樹的層數有關,最好情況是變成滿二叉樹,此時層數最少,爲log2n 層,時間複雜度爲O(nlog2n) 。如果是最壞情況,樹變成了一條長鏈(則數串一開始就是順序的或者是倒序的)。最後形成的樹有n層,時間複雜度爲O(n2) 。對於平均情況,我們將每個節點向左擴展或者向右擴展看成等可能,則可以列出遞歸式,如下所示

    T(n)=1n[2k=0n1T(k)]

    中序遍歷每次會遍歷一個元素,對於一個長度爲n的數串來說,遍歷整棵樹一次即可得到結果,其時間複雜度爲O(n)
    通過不斷迭代可以大致估算出
    T(n)=1.39nlog2n+O(n)

    因此總的時間複雜度爲O(nlog2n) ,由此可以得到一個推論(正確性可證),二叉樹排序法和固定先導元素的快速排序在時間複雜度上等價。
  • 空間複雜度分析
    空間複雜度取決於樹的層數,綜上最好情況下,遞歸層數約爲log2n 層,最壞情況下遞歸層數爲n層,平均情況下遞歸層數爲klog2n 層,因此空間複雜度也和固定先導元素的快速排序同樣爲O(log2n)

  • 優劣及穩定性
    這個算法的操作比較複雜,對於小數據來說同樣操作鏈表會比數組隨機讀取慢,但是好處就是空間可以開很大。
    對於排序後需要搜索的情況首選基於二叉樹的排序算法。
    但是該算法存在的問題也不少,雖然說時間空間複雜度上與固定先導元素的快速排序等價。然而這種等價只是在級數上等價,而不是完全等於。由於算法設計大量的指針操作和內存分配,因此算法的時間消耗會大不少。其次在遍歷時涉及到大量的遞歸,每次遞歸都需要傳入大量數據,不僅在空間上消耗大,在時間上消耗也大。
    另外,二叉樹排序算法不是穩定排序。

二. 基於散列排序

  • 注意到前兩篇文章說到存在一些排序算法能夠突破基於比較的排序算法的時間複雜度下界O(nlog2n) 。比如計數排序等。但是這些算法存在的一個致命缺點就是空間冗餘過大,這對降低時間複雜度也起到了反作用。所以我們考慮不要使用過大的空間來存儲,此時的問題就是不同的元素會佔用一個空間的情況。所以考慮使用高級存儲結構——散列來存儲數據。
    散列定義了某種映射是輸入數據能夠映射到散列表中的某一塊區域來進行存放。這種存儲結構對於那種大輸入集但實際輸入數量又不大是非常有利的。散列解決衝突的方法有探查法(就是如果起衝突就以一定的規則去看其他位置是否有空位可以放這個元素)。探查法有三種,線性探查、二次探查和隨機探查。線性探查從當前節點不斷往後直到找到第一個空位就可以存放,這樣的好處能夠最大化的利用空間,壞處在於很容易造成數據堆積。二次探查是以d,d+12,d+22,d+32... 的順序進行。這樣的好處在於數據不容易堆積,壞處在於有一些位置到不了。隨機探查利用的是隨機函數(注意:不能使用系統時間這樣變化的參數作爲隨機種子,要保持隨機種子的一致性)。隨機探查則以d, random(d), random(random(d))的方式進行。這樣的好處是數據進一步分散,缺點是如果剩餘空間不大就比較難找到空節點。這些方法在基於數組的情況是好實現的,但是基於鏈表就很難實現。
    此時基於散列的排序算法就可以很好的解決衝突問題。基於散列來達到線性時間排序的方法統稱爲桶排序,基數排序是桶排序的特例。
    使用散列結構的關鍵,一是設定什麼規則來對元素進行映射,二是如何處理好衝突。
    對於第一個關鍵點,我們需要使數據儘量分散,減少每個桶的數據量,但是又不能讓桶的數量過於龐大。另外,我們最終的目的是爲了排序,所以最好桶本身就是有序的。所以我們很容易想到一種方法就是以最高有效位的值作爲地址,凡是最高有效位的值相同就會被扔到同一個桶裏。
    對於第二個關鍵點,可以直接使用鏈式結構實現。桶內數據使用其他排序,如插入排序、分治排序等得到有序數列。

  • 時間複雜度分析
    桶排序兩個主要耗時的環節爲元素分配到桶裏,二是對桶中的元素進行排序。分配一個元素到桶裏的時間爲O(1),因此前者的時間複雜度爲O(n)。對於後者使用基於比較的排序,則時間複雜度爲O(nlog2n) 。對於最好情況,則每個桶裏最多隻有一個元素,則桶內排序幾乎不需要時間,則總體時間複雜度爲O(n)。最壞情況則將全部元素扔到同一個桶裏,則時間複雜度爲O(nlog2n)+O(n) ,因此是O(nlog2n) 的總體時間複雜度。對於平均情況,可以做如下分析。令T(n)爲總時間複雜度,桶排序的時間複雜度表達式爲

    T(n)=O(n)+i=0n1O(nilog2ni)

    對式子進行適當的兩邊放大爲
    T(n)=O(n)+i=0n1O(n2i)

    通過對兩邊求期望得到
    E(T(n))=E[O(n)+i=0n1O(n2i)]

    利用期望的線性性質有
    E(T(n))=O(n)+i=0n1E[O(n2i)]

    上式可以化爲
    E(T(n))=O(n)+i=0n1O(E[n2i])

    接下來分析可知道E[n2i]=21n ,代入原式,可得到
    T(n)=O(n)+i=0n1O(n2i)=O(n)+n×O(21n)=O(n)

    則桶排序的平均時間複雜度爲O(n)。
  • 空間複雜度分析
    空間複雜度可以認爲是O(1),只是前面係數一定大於n。

  • 優劣及穩定性
    注意制定的那個映射規則一定要想辦法將元素全部分散開來,不然時間複雜度會接近於最壞的時間複雜度,而且由於開散列表(創建桶的過程當中需要大量時間)也需要大量的時間,所以實際運行時佔用時間會很長。
    桶排序的優勢在於它確實突破了基於比較的排序算法的下界,達到了線性水平,同時也通過散列,彌補了計數排序算法上的一些不足。
    桶排序的問題在於未知輸入數分佈的情況下很難設計出一個很科學的映射規則使得數據儘可能分散開,同時佔用空間雖然在空間複雜度上看不出來很大,但是實際運行時前面的係數非常大,也很耗費空間。

參考及代碼

[1] 【數據結構】樹(二):二叉樹&二叉搜索樹&平衡二叉樹(C++實現)
[2] 桶排序實現(bucket_sort.cpp)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章