【數據結構】 二叉樹

【二叉樹】node

  二叉樹是最爲簡單的一種樹形結構。所謂樹形結構,其特徵(部分名詞的定義就不明確給出了,畢竟不是學術文章。。)在於:python

  1. 若是是非空的樹形結構,那麼擁有一個惟一的起始節點稱之爲root(根節點)算法

  2. 除了根節點外,其餘節點都有且僅有一個「父節點」;除此外這些節點還均可以有0到若干個「子節點」設計模式

  3. 樹中的全部節點都必須能夠經過根節點通過若干次後繼操做到達數組

  4. 節點之間不會造成循環關係,即任意一個節點都不可能從自身出發,通過不重複的徑路再回到自身。說明了樹形結構內部蘊含着一種「序」,可是不是線性表那樣的「全序」緩存

  5. 從樹中的任意兩個節點出發獲取到的兩個任意子樹,要不二者無交集,要不其中一者是另外一者的子集數據結構

  

  限定到二叉樹,二叉樹就是任意一個節點至多隻能有兩個子節點的樹形結構。也就是說,某個節點的子節點數能夠是0,1或2。 因爲能夠有兩個子節點,因此區別兩個子節點能夠將其分別定義爲左子節點和右子節點。可是須要注意的是,若一個節點只有一個子節點,那麼也必須明確這個子節點是左子節點仍是右子節點。不存在「中子節點」或者「單子節點」這種表述。多線程

  因爲上述規則對全部節點都生效,因此二叉樹也是一個遞歸的結構。事實上,遞歸就是二叉樹一個很是重要的特色,後面還會提到不少經過遞歸的思想來創建的例子。app

  對於左子節點做爲根節點的那顆二叉樹被稱爲相對本節點的左子樹,右子樹是同理。ide

  ■  基本概念

  空樹  不包含任何節點的二叉樹,連根節點也沒有

  單點樹  只包含一個根節點的二叉樹是單點樹

  至於兄弟關係,父子關係,長輩後輩關係是一言既明的就不說了。

  樹中沒有子節點的節點被稱爲樹葉(節點),其他的則是分支節點。一個節點的子節點個數被稱爲「度數」。正如上所說,二叉樹任意節點的度數取值多是0,1或2。

  節點與節點之間存在關聯關係,這種關聯關係的基本長度是1。經過一個節點通過若干個關聯關係到達另外一個節點,通過的這些關聯關係合起來被稱爲一個路徑。路徑的長度等於關聯關係的個數。爲了統一,一般把一個節點到其自身的路徑長度爲0。

  二叉樹是一種層級結構,某個節點所在的層數是根節點到達此節點路徑的長度。所以根節點所在第0層。所在第k層的節點,子節點在第k+1層。

  一棵樹能夠取到的最大的路徑長度稱爲樹的高度or深度。單點樹的高度爲0。

  ■  基本性質

  下面列出一些二叉樹的基本性質,證實就不給了,能力有限…

  非空二叉樹的第i層最多能夠有2^i個節點。高度爲h的二叉樹最多能夠有2^(h+1) - 1個節點。

  對於任何非空二叉樹,若其葉節點個數爲m,度數爲2的節點個數爲n。那麼m = n + 1。雖然懶得抄證實了,可是能夠感受到,加上去的這個1,若是算在根節點上是比較合理的。

  ●  滿二叉樹和擴充二叉樹

  全部分支節點的度數都是2的二叉樹是滿二叉樹。須要強調是分支節點。即最終葉節點並不是必定要徹底是2^n個那麼完整的那種。

  對於一個二叉樹T,添加上全部有必要的葉節點,使得T中的全部節點(包括分支和葉節點)的度數都變成2,從而T變成一個滿二叉樹。獲得的這個滿二叉樹相對原樹T而言就是一個T的擴充二叉樹。被新添加上去的這些葉節點被稱爲外部節點,新擴充二叉樹中的全部分支節點(包含原樹的分支節點和葉節點)被稱爲內部節點。根據上述基本性質的第三條,可知外部節點個數m = 內部結點個數n + 1。

  ●  徹底二叉樹

  對於高度爲h的二叉樹T,若0到h-1層全部層都符合,i層的節點個數是2^i個;且第h層節點數小於2^h個時,全部h-1層度數爲1的節點的子節點都是左子節點的話,那麼就稱T爲徹底二叉樹。須要注意區別徹底二叉樹和滿二叉樹之間的區別。好比下面這個就是一個徹底二叉樹但不是滿二叉樹。

 

  第0層有1個,第1層有2個節點,是徹底二叉樹。可是紅圈中的節點度數是1,不符合滿二叉樹要求。

  對於總含有n個節點的徹底二叉樹,其高度h是log2n向下取整。因爲徹底二叉樹在0到h-1層爲止的完整性,有一些很好的性質。好比從根節點開始,把根節點標號爲0,以後按從上到下,從左到右依次給樹中的全部節點編號,那麼能夠看到對於編號爲i的節點,

  1. 其父節點的編號是(i-1)/2,這是int除法,自動向下取整

  2. 若2 * i + 1 < n則其左子節點的序號是2 * i + 1,不然沒有左子節點

  3. 若 2 * i + 2 < n則其右子節點的序號是2 * i + 2,不然沒有右子節點

   也是由於其完整性,能夠將一個完整二叉樹比較好地投影成一個連續表。如按照上面描述的規則那樣對樹節點進行編號的話,那麼在表中僅憑下標就能夠輕鬆得查找到某個特定節點在樹中的父子節點。

  從直觀的圖上來講,完整二叉樹是很豐滿的圖形。一個二叉樹越是豐滿完整(實際上就是指其分支節點中度數爲1的節點佔比比較低),其樹的最長路徑越接近O(log n)的。相反越是單薄的,最長路徑接近O(n)的。極端的狀況,當全部的節點都只有一個子節點,那麼二叉樹其實就變成了一個連續表了。

 

  ■  二叉樹的抽象數據類型

  若是構造一個二叉樹類,可能會須要下面這些方法和屬性:

class BinTree:
    BinTree(data,left,right)    # 構造二叉樹
    is_empty()    # 判斷是不是空二叉樹
    num_nodes()    # 返回總節點數
    data()    # 獲取根節點數據
    left()    # 獲取左子樹
    right()    # 獲取右子樹
    setLeft(binTree)  # 設置左子樹爲指定樹對象
    setRight(binTree)    # 設置右子樹爲指定樹對象
    traversal()    # 遍歷樹用的方法,好比python實現就可讓它返回一個迭代器

 

  做爲一個數據結構,二叉樹的本職工做是存儲數據。樹的節點就是拿來存儲數據用的。由於根節點在一個樹裏處於比較特殊的地位,因此通常用樹的根節點做爲特徵來指定一顆樹。根節點做爲獲取二叉樹中數據的入口而存在。除了根節點,組成一棵樹的另外兩部分就是左子樹和右子樹。這三者組合起來就能夠完整地描述一棵樹了,這也是爲何構造方法中採用了這三個參數,而且類中給出了這三個屬性各自的get和set方法。

  除去這些方法外,最關鍵的方法就是如何遍歷二叉樹了。

  因爲二叉樹是個二維結構,遍歷時到達一個節點以後咱們可能有兩個前進方向,所以遍歷二叉樹分紅兩種模式,分別是深度優先和廣度優先。

  ●  深度優先遍歷

  深度優先表示,從根節點出發遍歷時優先向一個葉節點方向遍歷,不撞南牆不回頭。

  上面說過了一個二叉樹有三個要素即根節點,左右子樹。在遍歷到一個特定的位置時,這三個要素前後如何處理是個問題。首先須要說明,左子樹和右子樹能夠通過樹中心線的鏡面對稱從而互換,所以暫規定左子樹老是先於右子樹處理。另左右子樹分別爲L,R,根節點爲D的話。那麼遍歷順序有下面三種能夠選擇:

  DLR  被稱爲先根序

  LDR  稱爲中根序,也稱爲對稱序

  LRD  稱爲後根序

  選定任意一種順序,從根節點開始判斷,當L或者R存在時則轉入相關子樹判斷,不然進行順序規定的下一個對象。只要整個遍歷過程選定了一個順序不變化,那麼最終就能夠一個很少也很多地遍歷完樹的全部節點。

  針對下面這個二叉樹實例,根據不一樣的遍歷順序能夠獲得三個不一樣的節點序列:

 

  按照DLR順序獲得的是 A B D H E I C F J K G 序列。這個序列對這個二叉樹來講也被稱爲是先根序列

  按照LDR順序獲得的是 D H B E I A J F K C G 序列。這個被稱爲對稱序列。

  按照LRD順序獲得的是 H D E I B J K F G C A 序列。這個被稱爲後根序列。在嘗試寫出序列的過程當中就能夠感覺到,其實二叉樹的遍歷是具備很強的遞歸色彩思想的。二叉樹自己就能夠視爲將兩個自身類對象做爲屬性的類實例。所以遞歸在所不免。

  一樣的一棵樹,咱們能夠根據遍歷順序的不一樣而寫出多種不一樣的序列。因此獲得一個序列以後並不能還原出樹來。事實上,告訴一個序列而且告訴這個序列的遍歷順序,也沒法還原樹。可是能夠證實,若是知道了一棵樹的對稱序列,且知道另外一個序列的話,是能夠惟一肯定一個二叉樹的。

  ●  寬度優先遍歷

  寬度優先遍歷時,因爲來到一個子樹以後不急着立刻處理這個子樹而是回頭看還有哪些平級的子樹。因此寬度優先的遍歷不能用遞歸來作。相對的,咱們能夠設計一個棧,把相關同級的子樹都壓入棧,確認已經沒有同級的子樹以後再從棧中取出各個子樹來處理。

  按照寬度優先的順序遍歷獲得的序列是層次序列,好比上面給出的示例樹,其層次序列是A B C D E F G H I J K,和咱們習慣性的編號順序一致。

 

  不少時候,遍歷的目的並不在於真的要遍歷全部節點,而是爲了搜索咱們一個想要的節點。根據樹的形狀以及實際數據分佈不一樣而採用的不一樣的遍歷模式&遍歷順序,有可能會給最終搜索的效率帶來很大的影響。(好比咱們想要找上例中的J節點,採用兩種模式的四種順序,J每次出現的時間點都不一樣)選擇一個最優的遍歷順序有時候是高效解決問題的關鍵。

 

  ■  Python的二叉樹實現

  建立節點類,再據此實現樹類,雖然可行,可是有些繁瑣。目前咱們僅爲了作一些簡單的演示,因此借用一些現成的數據類型也能夠實現簡單的二叉樹。好比一個多重嵌套的列表。規定一棵樹是一個三元列表tree。tree[0]保存根節點的值,tree[1]和tree[2]分別是左子樹和右子樹,分別也均可能是三元列表。當沒有某個子樹的時候,就用None來代替。

  因而咱們就能夠得到一個簡單的樹了:

['A',
  ['B',
    ['C',None,None],
    ['D',None,None]
  ],
  ['E',None,
    ['F',None,None]
  ]
]

   葉節點是[xxx,None,None],而分支節點後兩個值中至少有一個不爲None。這整個嵌套列表的維度 = 樹的高度 + 1。

   ●  利用這個二叉樹進行簡單應用 —— 表達式二叉樹

  對於簡單的數學表達式(用加減乘除,大於小於號等二元運算符以及相關數字量組成的表達式),因爲每一個運算符都是二元的,因此表達式實際上是能夠很好地對應到一顆二叉樹的。並且這是一棵滿二叉樹。樹中全部葉節點都是數字量,全部分支節點加上根節點是運算符。好比以這個算式爲例:

  ( a - b ) * ( c / d + e ) 總共包含了5個數字量和4個二元運算符(這裏姑且先無論括號的存在)。在這種狀況下,能夠將這個表達式轉化成一個二叉樹以下:

  能夠看到,二叉樹因爲其二維的層級結構,自然就帶來了括號的做用,因此括號能夠不在樹中體現,就能構造出和算數表達式相對應的二叉樹了。

  而後再來看看如何遍歷這棵樹。首先咱們只考慮深度優先模式,而後若是採用中根序遍歷,獲得的是a - b * c / d + e,基本上就是去掉括號的原表達式。可是也能夠看到,因爲遍歷的過程將二維信息一維化,本來的一些計算順序的信息丟失。因此中根序並非好的遍歷表達式二叉樹的辦法。

  採用前根序得 * - a b + / c d e,採用後根序獲得 a b - c d / e + *  。這二者分別是以前在學習棧的時候提到過的,算數表達式的前綴表達法和後綴表達法。根據以前的瞭解,二者無需括號,是自帶了計算順序信息的。

  在獲得一個表達式二叉樹以後,咱們固然能夠得出其後根序列,而後根據以前說過的利用棧的方法來求表達式的值。另外一方面,着眼於二叉樹總體,採用遞歸的辦法也是可行的。這裏的遞歸思想大概是這樣的: 到達一個樹的根節點時,判斷其兩個分支是否都是葉節點了。若是不是,則遞歸進入該子樹進行處理,若是都是葉節點(即數字量了),那麼根據根節點給出的運算符作出相關運算,並將獲得結果做爲本樹(或者說該根節點)的值保存下來,返回上層,直到計算出整個表達式樹根節點的值。

  用函數的語言來講能夠是這樣的:

  假如咱們的樹是這樣的

expTree = ['*', 3, [
    '+', 2, 5
  ]
]

  這個表達式原型是3 * ( 2 + 5 )。爲了表達的方便,全部葉節點沒有寫成[num,None,None]的形式。

def is_basic_exp(ins):
    return not isinstance(ins, list)

def eval_exp(ins):
    if is_basic_exp(ins):
        return ins
    op, a, b = ins[0],eval_exp(ins),eval_exp(ins)
    if op == '+':
        return a + b
    elif op == '-':
        return a - b
    elif op == '*':
        return a * b
    elif op == '/':
        return a / b
    else:
        raise SyntaxError('Wrong choice of operator')

 

   以上就是計算一個由List實現的表達式二叉樹結構的值的函數,採用遞歸的辦法。is_basic_exp是個輔助函數,當目前的「樹」不是List類型,說明已是葉節點(基於上面的那個簡化操做,不然能夠判斷ins[1] == None and ins[2] == None)。此時就能夠直接返回葉節點的值。若是當前不是葉節點,那麼就去除本節點的操做符以及兩個子節點的值。子節點有可能也不是葉節點,所以遞歸。若是兩個子節點都是葉節點,那麼恰好就獲取到了兩個值和一個操做符。接下來根據操做符進行相應的計算便可。

  固然這個函數仍是存在一些缺陷的。好比未對除數爲0的狀況做出判斷,爲驗證傳入參數的類型是否是都是數字類型等等。

  

  ■  優先隊列

  優先隊列是一種重要的緩存結構,基於對二叉樹的認識能夠高效地實現一個優先隊列所以能夠看作是一種對二叉樹的應用。

  ●  基本概念

  優先隊列本質上也是一個線性表。和通常的隊列或者棧不一樣的是,除了每一個元素的值自己外,每一個元素還有一個優先度的值。每次對優先隊列進行訪問或彈出操做時,都一定會獲取到當前隊列中優先值最高的元素。若是隊列中有多個優先值同樣的元素,那麼能夠根據隊列內部自定義的規則給出結果。

  ●  簡單實現

  要用Python實現優先隊列的話,能夠選擇List做爲基本的容器。

  另外,爲了可以實現「獲取時老是獲取到優先度最高的那一個」這樣一個目的,可考慮的實現方式可能有兩種。第一,在建立與插入元素到隊列的時候時刻保持隊列根據優先度的順序排列,這樣在獲取元素的時候只要在隊尾獲取便可;第二,根據簡單的方法建立以及插入元素,在獲取元素的時候再到隊列中去尋找優先度最高的一項。綜合對比兩項來看,分別是把難度放在了插入和獲取上。下面採用第一種方案實現

class PiorQueue:
    def __init__(self,elist=[]):
        self._elems = list(elist)
        self._elems.sort(reverse=True)
    def enqueue(self, e):
        i = len(self._elems) - 1
        while i >= 0:
            if sellf._elem[i] <= e:
                i -= 1
            else:
                break
        self._elems.insert(i+1,e)
    def peek(self):
        return self._elems[-1]
    def dequeue(self):
        if self.is_empty():
            raise Exception('empty queue')
        return self._elems.pop()

 

  這個類建立的時候接受了一個可迭代對象做爲參數(這裏使用了可變對象做爲參數的默認值,這是不值得提倡的。由於方法/函數參數的默認值只隨函數定義在內存中保存一次,以後每次調用該方法/函數時默認值都是引用到這個地址。使用可變對象有可能致使不一樣時間調用方法/函數的默認值不一樣。),而且按照從小到大排序。

  enqueue是新增元素入隊列的方法,能夠看到咱們默認將元素值做爲其優先度的值,從隊尾開始逐漸遍歷,找到一個比新增元素值(或者說優先度)大的,而後把這個元素插入到這個元素位置前面。

  peek和dequeue方法都很簡單。不難看出,插入時方法的複雜度是O(n)而另兩個都是O(1)的。

  ●  用樹形結構實現

  若是評價上述用線性表/連續表實現優先隊列效率比較低的話,分析一下,主要的瓶頸就在於插入時要進行一個O(n)的遍歷。如何將這個很「線性」的遍歷變得更加高效稱爲關鍵、

  正如前面說過,一個線性結構能夠很好地映射到一個對應的徹底二叉樹結構中。天然,一個有序的線性表也能夠。一個有序的線性表對應出來的徹底二叉樹結構被稱爲堆。堆的根節點是堆頂,從堆頂向葉節點方向呈現出來值上的順序是堆序。堆序是從小到大,即堆頂的值是最小值的狀況,成爲小頂堆,反之爲大頂堆。

  如將一個承載着優先隊列的線性表對應成一個堆,那麼能夠想象,因爲堆自己的徹底二叉樹的性質,將本來一維的,從上到下依次遍歷的方式改爲了二維的方式。這使得遍歷的效率大大提升。本來O(n)複雜度的操做將會變成O(logn)。相似的,經過堆來實現優先隊列一樣要解決插入和刪除的問題。即如何在合適的位置插入一個新元素以及如何在刪除一個元素以後調整原堆,保持它的堆序。

  下面將更深地講述堆和堆的性質,以及用堆實現優先隊列的種種。

 

■  堆

  正如上面所說,堆比較重要的特色是它有特定的堆序。從定義上來講,堆對應的是一個徹底二叉樹,其最下層的葉子節點能夠不滿。同時因爲堆序,其保證任意一個節點都小於or大於其兩個子節點中的值。以小頂堆爲例,從堆中任意選取一條路徑,那麼獲得的就必定是從上至小依次遞增的一條有序路徑(暫時先不考慮不一樣路徑之間的節點值的大小比較關係)。

  堆還具備如下的性質:

  1. 在堆的最後append一個元素,整個結構仍然保持是一個徹底二叉樹,可是並不必定是堆(新加入的元素不知足堆序)

  2. 堆頂必定是整個堆中最小or最大的值。堆中的任意一個節點下的兩個子樹也都是堆。

  3. 改變堆頂的值,整個堆保持徹底二叉樹結構,可是不必定是堆

  4. 去除堆中最後一個元素,整個結構保持是一個堆

  

  若是將堆做爲優先隊列的載體。那麼顯而易見有兩個問題須要解決。第一,把一個新元素入堆的時候如何將其放到合適的位置;第二,從堆中取出最優先元素以後,剩餘元素如何編排成一個堆。在下面的實現分析中咱們能夠看到,這兩個操做都是O(log2n)的,相比於以前插入時要O(n)的操做,顯然堆做爲載體的效率更高

  ●  向上和向下篩選

  下面以小頂堆爲例講述。

  插入一個元素到堆,如何才能將它準確放入到合適的位置。其實這個解決辦法很樸素,首先咱們將元素append到堆末尾,此時因爲堆序不必定獲得保持,堆稱爲一個普通的徹底二叉樹。接下來就是找回堆序,最簡單的辦法就是將位於堆末尾的那個新元素依次向上和父元素比較。對於大於父元素的狀況,則交換二者位置。對於小於的狀況,則表示當前新元素已經處於合適的位置了。這一整個過程就叫向上篩選。其巧妙之處在於注目於單條路徑,維護其堆序的正確性,對於其餘的路徑保持不變。

  相對的,若是是從堆中彈出最優先(就是堆頂)元素,那麼堆頂此時空了。咱們能夠從堆末尾彈出堆的最後一個元素M賦值給堆頂。相似的,堆序崩壞,須要找回堆序。此時就能夠將M和兩個子節點比較,選擇其中最小的那個做爲堆頂。若是此時M已是三者中最小,就能夠不用變了;不然交換最小者和M的位置。接下來就是繼續關注M,進行遞歸的操做。這整個過程相似於上述過程的反過來,因此是向下篩選。

  下面開始基於堆作一個簡單的有限隊列的實現:

class PriorQueue():
    def __init__(self,elist):
        self._elems = list(elist)
        if elist:
            self.buildHeap()

    def is_empty(self):
        return not self._elems

    def peek(self):
        if self.is_empty():
            raise ValueError('Queue is empty')
        return self._elems[0]

    def enqueue(self,elem):
        self._elems.append(elem)
        self.shiftup(elem,len(self._elems)-1)

    def dequeue(self):
        if self.is_empty():
            raise ValueError('Queue is empty')
        elem0 = self._elems[0]
        lastElem = self._elems.pop()
        if not self.is_empty():
            self.shiftdown(lastElem,0,len(self._elems))
        return elem0

  這個類和以前經過list實現的消息隊列是相似的。不過目前這裏還有三個方法,buildHeap,shiftup和shiftdown沒有實現。第一個是用來構建堆的,後二者則分別是向上和向下篩選。下面首先看shiftup和shiftdown兩個方法:

    def shiftup(self,element,lastIndex):
        currentIndex,parentIndex = lastIndex,lastIndex//2
        while currentIndex > 0:
            if self._elems[parentIndex] > element:
                self._elems[currentIndex] = self._elems[parentIndex]
                currentIndex = parentIndex
            else:
                self._elems[currentIndex] = element
                break

    def shiftdown(self,element,begin,end):
        currentIndex,child1,child2 = begin,begin*2+1,begin*2+2
        while child2 <= end:
            minChild,minChildIndex = (self._elems[child1],child1) if self._elems[child1] < self._elems[child2]\
                else (self._elems[child2],child2)
            if element > minChild:
                self._elems[currentIndex] = minChild
                currentIndex = minChildIndex
                child1,child2 = currentIndex*2+1,currentIndex*2+2
            else:
                self._elems[currentIndex] = element

 

  閱讀代碼以後其實能夠發現,雖然在描述中說的是將父子節點進行互換,可是實際上沒有進行x,y = y,x這樣的操做。這也算是一個小優化點吧,其實是先將要進行篩選的值「握在手中」,逐漸進行篩選,碰到須要調換的,好比向上篩選過程當中碰到父節點比我手裏的值大的狀況,那麼就將父節點那個值賦值給當前節點,再將當前節點標記爲父節點,此時並不直接將父節點賦值爲我手中的值,而是選擇繼續往上看,這樣到頭來我只須要直接把手中的值放到合適的位置,避免了不少沒必要要的賦值操做。

   相比於向上篩選,向下篩選稍微複雜一些,主要是要作一些哪一個子節點的元素更小之類的判斷。此外兩個方法都留出了begin,end之類的邊界指定接口,這樣不只僅是篩選到堆頂/最下層的狀況,任意層級指定的篩選均可以實現。

 

  接下來讓咱們來考慮一下buildHeap方法如何實現。在這個類的構造方法中咱們能夠看到,實際上是將外部傳入的elist參數做爲構造樹的一個基石。順便一提將self._elems = list(elist)能夠避免可變對象參數默認值陷阱以及防止修改原對象等等。這個列表自己其實已經對應了一個徹底二叉樹結構了。假如其長度爲end = len(self._elems),那麼從 end // 2 的那個下標(包括其自身)開始的全部節點都是葉子節點。(請注意,葉子節點不必定是最下層的,還有多是倒數第二層的)

  有兩種方案能夠選擇。1. 咱們另聲明一個空堆,而後將列表中數字逐個append入堆,並進行向上篩選,最終造成堆。

  2. 咱們也能夠基於現有堆直接做出調整。因爲堆要求從堆中取出任意一個子樹,均可以造成一個堆。對於那些葉子節點來講沒問題,一個節點確定是一個堆,而對於下標爲end // 2 - 1的節點開始一直到整個堆的堆頂,有沒有堆序卻仍是不必定的。因此咱們要作的工做就是從下至上,從右至左按照下標遞減順序依次對各個還沒有造成堆序的徹底二叉樹造成堆序,即進行向下篩選(不向上篩選是由於向上篩選走的每每是整個堆中的一條路而忽視了其餘路的檢查,而向下篩選因爲篩選時會進行子節點大小的判斷,算是兼顧了全部路線,所以能夠保證最終總體堆序的造成)。基於這樣的思想很快就可寫出代碼了:

def buildHeap(self):
    end = len(self._elems)
    for i in range(end//2, -1, -1):    #range生成的是end//2到0的倒序列表
        self.shiftdown(self._elems[i], i, end)

 

   咱們實現的shiftdown方法剛纔一直用於彈出堆頂,可能會給咱們形成必定思惟定式的困擾。其實,shiftdown方法自己並無作過任何減少self._elems的操做,也就是說它不包含「彈出」的操做。它作的,無非是接受外界的一個指標(element)和起始節點(begin)和結束節點(end),而後在起始節點和結束節點之間尋找一個合適的位置插入element這個值而已。因此構造堆方法中作的工做,是對每個待調整的節點,取出其值,而後看將它放在它的子樹中的什麼地方最合適。合適的判斷標準則是要求最終能造成一個堆。

  另外能夠計算得知,構造堆操做的複雜度是O(n)的。可是在整個堆的使用過程當中這個操做只作一次,相比於每次插入都是O(n)的list實現而言是效率更高的。

 

  ●  堆排序

  咱們說的排序一般是指對一維數組的排序。堆的有序性是二維的,而若是將其轉化爲一個一維結構則不必定有序。不過利用堆在堆頂是最小(或最大)的性質,依次彈出堆中全部的元素,那麼按照彈出的順序就能夠獲得一個有序的線性列表了。

  爲了最大限度獲得牛逼的算法,還能夠考慮一下空間的使用。好比每次從堆中獲取出堆頂後,因爲接下來要把堆最下右的值給pop出來作向下篩選,因此堆最後會空出一個格子,正好能夠用來放堆頂的這個值。此後再進行彈出,向下篩選的過程時只要不把最後一個已經保存了有效值的格子不算進去就好了。如此周而復始,最終能夠不用一點外部空間就解決排序的問題。

  下面是對於堆排序實現的一段代碼(抄書的),感受十分牛逼,將代碼精簡作的很好

def heap_sort(elems):
    def shiftdown(elems,e,begin,end):
        i,j = begin,begin*2+1
        while j < end:
            if j+1<end and elems[j+1]<elems[j]:
                j += 1
            if e<elems[j]:
                break
            elems[i] = elems[j]
            i, j = j, 2*j+1
        elems[i] = e

    end = len(elems)
    for i in range(end//2,-1,-1):    # 進行堆構建
        shiftdown(elems,elems[i],i,end)
    for i in range(end-1,0,-1):
        tmp = elems[i]
        elems[i] = elems[0]
        shiftdown(elems,tmp,0,i)    #只到i不到end,不影響已經完成排序部分

   這個排序的複雜度是這樣, 首先堆構建是O(n)的,後面進行排序的循環,循環體中是O(logn)而總共進行了n次,因此總的時間複雜度是O(nlogn)。因爲基本沒有使用額外的空間,空間複雜度是O(1)的。有關排序的更多方法,之後也會提到。

 

■  基於優先隊列進行離散事件模擬

  這部份內容和二叉樹沒有什麼關係,可是書上寫在這裏,並且以爲它的設計模式比較有趣,就簡單記錄一下。

  首先要確認什麼是離散事件。離散事件是指那些不定時發生的,且發生以後會影響到接下來事件的事件。好比書提到的,一個海關要檢查車輛,有N條通道,每條通道在一個時間點上只檢查一輛車。車是一輛一輛隨機時間間隔地到海關門口接受海關檢查。細節上,若是當前N條通道都有車接受檢查,那麼車在海關門口排隊直到有通道空閒,每輛車接受檢查的時間也不固定,所以有可能會出現某條通道相比於另外一個通道先被佔據然後被釋放的狀況。某輛車經過檢查後離開,當前通道獲得釋放能夠接受下輛車。

  

  看了上述描述,若是要用一個計算機過程來模擬這個離散事件過程,咱們最容易想到的就是線程池+隊列模型。每一個通道做爲線程池中的一個工做線程,而外部的一個隊列維護還在排隊等待接受檢查的車輛。簡單明瞭且和場景切合度很高。

  而書中提供了一種不用多線程來模擬的方案。其核心要義是將車輛到達&離開抽象爲「事件類」Event,Event類帶有屬性c_time表示事件開始的時間,同時實現了__lt__和__le__方法,這樣就能夠利用優先隊列模型將各個事件維護入隊,而優先隊列的優先度就是按照事件實際發生的開始時間,越早的越優先。外部的排隊等待的車輛,用一個簡單隊列維護起來就好了。而後模擬過程當中,模擬主對象不斷地從優先隊列中取出事件進行處理。因爲事件按照發生時間排序了,因此能夠存在後入隊先執行的可能性。另外爲了保證事件不中斷地發生,能夠考慮另外定義一個EventFactory之類的類或者在Arrive以及Leave事件的執行方法中直接生成下一個Arrive/Leave事件。

  具體的代碼就不寫了,很長也很繞,就當作是一個思惟練習吧。

 

■  二叉樹的類實現

  前面Python中使用二叉樹時默認載體每每是list或者tuple,仍是一種比較抽象的概念,可是很好地體現出了二叉樹和線性列表間的關係。接下來要說的是如何經過自定義二叉樹類來實現一個二叉樹。經過類實現的數顯然能夠更加直觀,使用起來更加方便。  

  ●  節點類

  二叉樹有不少節點構成,節點類是二叉樹類的基礎。若是將子節點做爲屬性加入到節點類中,那麼很明顯,二叉樹類自己也是一個節點類。或者說,二叉樹類和節點類是沒有區別的。一個簡答的節點類能夠寫成這樣:

class BinTNode:
    def __init__(self,data,left=None,right=None):
        self.data = data
        self.left = left
        self.right = right

 

  那麼一個簡單的二叉樹就能夠表示爲BinTNode(1,BinTNode(2),BinTNode(3))。基於這種類結構,以及前面提到過的不少關於遞歸進行樹遍歷的思想,能夠寫出下面兩個方法:

def countNodes(t):
    if t is None:
        return 0
    return 1 + countNodes(t.left) + countNodes(t.right)

def sumNodes(t):
    if t is None:
        return 0
    return t.data+ sumNodes(t.left) + sumNodes(t.right)

 

  ●  遍歷算法

  一般遍歷,咱們但願作的是對每個節點執行一個相似的遍歷單元操做。下面以深度優先的遍歷,中根序爲例,

def preorder(t, proc):    # proc是一個遍歷處理函數,對每一個節點作操做
    if t is None:
        return
    proc(t)
    preorder(t.left, proc)
    preorder(t.right, proc)

  這是一個簡單的遍歷函數的示例,實際運用過程當中咱們還能夠進行中根序後根序的遍歷,只要調整函數體中調用preorder和proc的順序便可。另外若是要實際寫這麼一個功能的函數,一般還須要一些要素好比檢查t是不是一個合法的二叉樹對象等等。

  若是基於這樣的遍歷思想作一個簡單的應用,那麼咱們能夠考慮設計一個打印樹結構的函數。這裏說的打印不是說縱向上的打印(那種還要考慮當前層有多少節點,節點之間間距如何安排等問題),而是簡單地構造出一個相似以前list實現方法時的樣子。代碼以下:

def print_BinTNode(t):
    if t is None:
        print('^',end='')
        return
    print('('+str(t.data),end='')
    print_BinTNode(t.left)
    print_BinTNode(t.right)
    print(')',end='')

 

  *這裏代碼用的是Python3了。主要考慮到能夠指定end參數,這樣打印的時候就不會很噁心地一行一行都分開了。

  好比二叉樹實例 BinTNode(1,BinTNode(2,BinTNode(5)),BinTNode(3))調用這個方法的話獲得的就是(1(2(5^^)^)(3^^))。遇到沒有子樹的狀況,並非直接跳過而是輸出^字符。這主要是考慮到,當只有一個子樹的狀況,若是不打印^的話會致使最終打印出的結構中,沒法判斷單獨的子樹是左子樹仍是右子樹。

  

  接下來實現二叉樹的寬度優先遍歷。寬度優先須要一個隊列做爲棧記錄還沒有處理的其餘節點的信息。以前在用list實現二叉樹(或者把二叉樹看作是一個list的時候)其實寬度優先遍歷就是對這個list的簡單順序遍歷。而咱們如今在處理的東西變成了二叉樹類的對象,所以須要必定的調整。

def levelorder(t,proc):
    qu = SQueue()
    qu.enqueue(t)
    whie not qu.is_empty():
        t = qu.dequeue()
        if t.left is not None:
            qu.enqueue(t.left)
        if t.right is not None:
            qu.enqueue(t.right)
        proc(t.data)

 

  SQueue是一種FIFO隊列,每來到一個子樹,先從隊列中拿出這個子樹對象,先判斷其左子樹和右子樹是否存在,存在則入隊,而後再去處理這個子樹根節點的數據,而後進行下一個同層or下一層子樹的處理。

 

  接下來講一下非遞歸的深度優先遍歷。前面的遞歸深度優先遍歷很好理解也很好寫。若是強行非遞歸,那麼就要在代碼中引入一個棧了。大體的算法咱們能夠設想一下:

  仍是以先根序爲例,首先咱們處理一個樹的根節點,而後左子樹,而後右子樹。左子樹還好說,大不了再處理下左子樹根節點就好了,右子樹怎麼辦?由於它要在我整個左子樹都處理完後再處理,因此先入棧。隨後咱們就能夠安心處理左子樹。此時處理左子樹的流程也是這樣的。以此類推的話,也就是說咱們的總體流程其實是先沿着樹的最左邊逐步向下,直到最左下葉子節點。處理完它以後,如今能夠從棧中取出一個樹。這個樹應該是最左下葉子節點的兄弟右子樹。那麼這個樹又該怎麼處理,其實仍是同樣,沿最左邊往下,走相似的流程。

  最終,呈現爲代碼就差很少是這種感受:

def preorder_nonrec(t,proc):
    stack = Queue.LifoQueue()
    while t is not None or stack.not_empty():
        # 若t是None,說明上一個處理的節點沒有右子樹,此時就去看棧中是否還剩下沒處理的子樹
        # 若是stack也空了,說明沒有要再處理的東西了,所以能夠直接給出結果
        while t is not None:
            # 這個循環實際上是從某個節點一直沿着最左邊走下去
            # t爲None表示上個處理掉的節點已是最左下的節點(沒有左子樹)此時能夠跳出循環,獲取上個節點的右子樹處理
            # 每跳到一個右子樹,其實流程同樣,仍是沿最左邊往下走
            proc(t.data)
            stack.put(t.right)
            t = t.left
        t = stack.get()

 

   * 估計也不會有人看到這裏,可能我本身都不太會…我想記錄一下如今的心情。最近內心亂的很,聽着アヤノの幸福理論有種回到大一的感受,迷茫焦躁,對將來充滿畏懼同時也沒有勇氣向前一步。細想起來,忽然冒出想去日本留學的念頭,好像是由於在jcinfo上面遇到了很多堅決去日本留學的中國學生和堅決來中國留學的日本學生。而後略微深刻地瞭解了下,發現留學的成本並無我想象得那麼高,反過來,以我已有的成績,不去留學一下反倒以爲有些惋惜。就在這樣一種神奇的心境的驅動下,這兩天發瘋似的收集留學相關的信息,就好像明天就要去留學了同樣,好笑的是時時刻刻心裏深處又有一個小惡魔在提醒我現實是什麼樣的。它讓我以爲我就像是一個小丑,爲了逃避現實而好高騖遠,向四周宣稱着我那不太可能實現的計劃。尤爲是寫到這個算法的時候,我想小小考驗下本身看能不能獨立寫出來,可是沒想到在有教科書的文字提醒下,我仍是寫了個錯的算法,而看到書上的算法才深知本身沒有這方面的才能。。但是又能怎麼辦呢,入了這一行了。留學這兩個字背後,我看到的是金錢,能力,將來可能要付出的辛苦,各類各樣的阻礙和困難;鼓勵別人的時候,一句簡單的「加油」多輕鬆啊,但是誰又能知道這背後須要多麼複雜的糾葛和心緒。

  感受很對不起爸媽,在家裏處於這種狀況的時候又半分心血來潮地說出這種話,將他們好不容易整理好的情緒又打亂。可是個人心裏卻又真的是很想實現這個願望。差很少也該回家了,剩下的我想寫到日記本上。

 

  昨天好像是有點累了的緣故吧,整我的都變得很悲觀。今天一覺醒來感受好些了,雖然目標仍然很遠,可是努力開始吧。

 

  下面這個是非遞歸的中根序遍歷,是我本身寫的,也不知道對不對… 大概思想是將右子樹和根節點分別前後入棧。當沿最左邊一直處理到左下角以後,再回頭到棧中取出內容進行處理。因爲棧中混合保存着根節點數據和右子樹兩種形式,因此還須要有個is_data()方法判斷,若是是根節點的數據,那就proc一下。因爲可能存在某節點的父節點沒有右子樹,此時處理完父節點數據後,棧中的下一個就是父父節點的數據而非父節點的右子樹,因此在處理數據那邊又加了一個循環,直到棧中取出的不是單純數據而是樹時,再賦值給t,回到上一層循環,去處理獲取到的這個右子樹。

def midorder_norecursive(t,proc):
    stack = Queue.LifoQueue()
    while t is not None:
        while t is not None:
            if t.right is not None:
                stack.put(t.right)
            stack.put(t.data)
            t = t.left
        ele = stack.get()
        while ele.is_data():
            proc(ele)
            try:
                ele = stack.get()
            except Queue.Empty,e:
                ele = None
        t = ele

 

   

  接下來再來看看非遞歸的後根序序列。稍微改造下上面的中根序函數,好比修改一下t.right和t.data入棧的順序,就能夠實現後根序了。(很是不肯定是否正確… 我本身試了幾棵樹均可以,但估計還有一些漏洞沒注意到)。下面的函數則是書中給出的一種解決方案。

def postorder_norecursive(t,proc):
    stack = Queue.LifoQueue()
    while t is not None or not stack.empty():
    # 1. t是None多是右子樹處理完了或者沒有右子樹
    # 2. 棧爲空代表已沒有未處理的內容,能夠結束程序
        while t is not None:
        # 沿着最左邊(不是嚴格地了)先入棧一批子樹
            stack.put(t)
            t = t.left if t.left is not None else t.right  # 這個表達式不一樣於以前的入棧策略,是有左子樹則左子樹入棧,不然如有右子樹,右子樹也可入棧。
        t = stack.get()
        proc(t.data)
        if not stack.empty() and stack.top().left == t:
            # 1. 若是棧爲空,則表示沒有須要剩餘須要處理的內容,能夠返回了
            # 2. 目前t是棧頂的左子節點,說明其右子節點尚未處理。由於後根序,先去處理右子樹。
            t = stack.top().right
        else:
            t = None    

 

  入棧策略發生變化的主要緣由,是由於在先根序中,很明確沿着左分支下行並將全部右分支都入棧,整體而言覆蓋到了整個樹。可是在這個場合中,結合下面的子樹處理邏輯,若是某節點沒有左子樹,至關於其沒有了訪問右子樹的鑰匙(右子樹的訪問必定要經過其兄弟左子樹,在if stack.top().left == t這個入口進行訪問),因此要把左子樹爲None的狀況單獨拎出來維護到棧裏。

  ●  生成器構造遍歷函數

  這是一個小點。上面的遍歷函數實際上是給了proc作了一個處理。但不少時候,咱們會但願經過遍歷獲取到樹的線性序列形式。此時,生成器就是一個很好的方案。可是用yield在這裏有個坑,以前在Python的生成器學習文章中也提到過,就是在遞歸的時候,yield獲取到的單個數據是隻保存在本層遞歸的函數調用棧中,並不會自動充入到上層遞歸的調用棧裏。若是是這樣的話,那麼就須要進行一些特殊處理才行。固然非遞歸狀況就很方便了:

def preorder_elements(t):
    stack = LifoQueue()
    while t is not None or not stack.empty():
        while t is not None:
            stack.put(t.right)
            yield t.data
            t = t.left
        t = stack.get()

 

   若是是遞歸,那麼能夠這樣:

def tree_elements(t):
    if t is None:
        return
    yield t.data
    for data in tree_elements(t.left):
        yield data
    for data in tree_elements(t.right):
        yield data
    # 這裏就不能只是tree_elements(t.left),不然這只是在這裏聲明瞭一個生成器,其中內容保存在各自層遞歸的調用棧中,最高層的調用棧無那些內容

 

 

  ●  非遞歸遍歷的複雜度

  光看某個具體算法的程序可能比較難看出來算法的複雜度,但從一個更加宏觀的視角來看待,整個樹的全部節點都是隻被訪問一次,不考慮proc函數的具體複雜度的話,那麼遍歷整個樹的時間複雜度就是O(n)。空間複雜度根據具體算法不一樣可能不一樣。若是出現的是比較極端的那種單線條樹,好比最左邊所有都有一個右子節點且是葉子節點的狀況,這棵樹自己的空間複雜度達到O(n)(高度具體值是n/2,也是O(n)量級的),而先根序遍歷的時候全部的右子樹有都要入棧,因此棧的大小也到達n/2,即O(n)的。

 

■  真·二叉樹類

  上面基於樹節點的二叉樹類(自己也就是節點類)在寫出來的時候就具備了遞歸結構。表現形式上其實和list實現的差很少。另外還有一些小問題好比空樹用None表示,並非二叉樹類。解決這些問題的方法就是在這個基於節點的構造外面再包一層真·二叉樹類,作一個更高層次的封裝而把節點的那種結構維護在實例的內部。

   目前還沒太搞明白這樣一個類的用法,姑且給出書上的定義:

class BinTree:
    def __init__(self):
        self._root = None

    def is_empty(self):
        return self._root is None

    def root(self):
        return self._root

    def leftChild(self):
        return self._root.left

    def rightChild(self):
        return self._root.right

    def setRoot(self, binTNode):
        self._root = binTNode

   能夠看到的一個特色是,這個類沒有涉及到相似於getParent()的方法,即沒法獲取到這個樹實例的父節點在哪裏。要想實現這一點能夠考慮將BinTNode類擴充一個self.parent屬性,這樣至關於一個節點擁有self.root,self,left,self.right和self.parent共四個屬性。

 

【哈夫曼樹】

  ■  定義

  以前咱們講到過一種二叉樹叫作擴充二叉樹。把任意一顆二叉樹補充稱滿二叉樹的話,那麼樹就變成了一棵擴充二叉樹,新補充上去的那些節點叫作外部節點。擴充二叉樹的一個屬性叫作外部路徑長度,指的是從其根節點開始到全部外部節點的路徑的和。顯然,外部路徑長度E = (l1+l2+l3...+lm),其中m是外部節點的個數而ln表明根節點到特定外部節點n的路徑長度。

  如今咱們考慮爲每一個外部節點賦一個權值。,這樣能夠獲得一個屬性叫作帶權擴充二叉樹外部路徑長度。以WPL表示。這個值WPL = (w1l1+w2l2+...+wmlm)。 w1,w2...wm是指從第一個外部節點開始到第m個外部節點的全部權值。好比下圖的兩個樹的WPL值:

  左邊的樹WPL = (2+3+6+9) * 2 = 38,右邊的樹是WPL = 9+(6*2)+(2+3)*3 = 36。

  

  好,如今來看下定義。若是個人手上有一個實數集合W,其中有實數w0,w1,....,w(m-1)。將它們做爲外部節點的權值,填充到一顆擴充二叉樹的外部節點中。顯然這樣的二叉樹形態有多種多樣的。其中一種二叉樹樣式T能夠作到WPL值最小,此時咱們把T稱做結合W上的最優二叉樹,或者哈夫曼樹。

  以上面兩個兩個樹爲例,對於一樣的數據集{2,3,6,9},左邊樹的形態和右邊樹不一樣。而對於一種形態來講,不一樣的權值安排也會致使WPL值的不一樣。實際上,右邊這種形態和這種權值安排方法是WPL值最小的,因此右邊這樣一顆樹是{2,3,6,9}的哈夫曼樹。

  

■  構造

  構造哈夫曼樹的算法就叫哈夫曼算法。首先能夠明確,哈夫曼算法的輸入是一個實數集W,最終要生成的是W對應的哈夫曼樹。其大概描述是這樣的:

  首先將W中元素當作是隻有一個根節點的小樹,而後一字排開,他們都將做爲最終哈夫曼樹的外部節點。選擇其中根節點值最小的兩個樹(若是最小值有重複則任選兩個便可,這也代表一個實數集W的哈夫曼樹並不惟一,可是全部哈夫曼樹的WPL都應該相等),經過必定手段將它們放到相鄰的位置,而後將它們分別做爲左右子節點,在他們上面構造一個根節點。根節點的值取二者的和。根據哈夫曼樹的定義,這些非葉子節點的值並不計入WPL計算,這裏只是作一個輔助做用。

  經過這樣的一次鏈接操做,咱們將原數據集中的兩個元素構形成了一個小樹。如今將原有的兩個原生元素去掉,再把這棵新的小樹加入集合。接下來,將新加入的小樹做爲集合的一員,而後從新執行上述操做。選擇兩個值最小的樹(剛纔的小樹也能夠加入比較了),放到相鄰位置,構造新樹… 如此周而復始,最終集合中會只剩下一個樹,此樹就是集合W的哈夫曼樹。

  要證實這樣的算法獲得的確實是哈夫曼樹有些困難,可是能夠注意到,哈夫曼樹的任意左子樹和右子樹交換位置,獲得的仍然是哈夫曼樹(構造新小樹時並未指定兩個已有樹哪一個在左哪一個在右)。

最後一幅圖中下面的數字是各個外部節點的路徑長度

 

■  實現

  上述描述略複雜一些,具體編碼實現的時候還能夠設計一些更合理的邏輯,來去掉一些沒必要要的步驟。書上是這樣設計的一個算法:

  構造過程的兩個關鍵點是1. 找到集合中兩個最小樹;2. 判斷集合是否只剩下一棵樹。對於第一點,咱們能夠構造一個定根節點值爲優先值的優先隊列,這樣每次只要在隊列前頭取出兩個值,獲得的就必定是最小樹,直接構造便可。至於第二點更簡單,只要給這個優先隊列實現一個統計還有多少元素的方法就能夠了。

  結合以前已經實現過的一些類,能夠寫代碼以下:

class HTNode(BinTNode):
    def __lt__(slef,othernode):
        return self.data < othernode.data

class HuffmannPrioQ(PrioQueue):
    def number(self):
        return len(self._elem)

 

  有了類定義做爲基礎,很方便地就能寫出Huffmann樹的構造過程了。

def HuffmannTree(weights):
    trees = HuffmanPrioQ()
    for w in weights:
        trees.enqueue(HTNode(w))
    # 構造完了優先隊列,下面開始正式哈夫曼樹的構造
    while trees.number() > 1:
        t1 = trees.dequeue()
        t2 = trees.dequeue()
        newTreeRoot = t1.data + t2.data
        newTree = HTNode(x,t1,t2)
        trees.enqueue(newTree)

    return trees.dequeue()

 

  下面來簡單看一下哈夫曼樹構建過程的複雜度。假設實數集weights中有n個元素。這裏主要是兩個循環,第一個循環若是採用的是構建堆,而後基於堆排序進行優先隊列的構建,那麼複雜度是O(n),這裏代碼默認採用的是通常的基於列表的優先隊列,即enqueue一次的複雜度是O(logn),總共進行n次,因此這裏的複雜度是O(nlogn)的。

  第二個循環進行了n-1次,而裏面主要耗時的部分也是enqueue的O(logn),因此第二個循環也是O(nlogn)的。所以總的時間複雜度就是O(nlogn)。至於空間複雜度,最初的隊列有n個元素,後面雖然新增了不少不計入WPL統計的節點,可是整棵樹最後的總結點樹是2n-1個,因此是O(n)。

 

■  哈夫曼樹的重要應用

  其實哈夫曼發現了這種算法的契機,是爲了要作出一種哈夫曼編碼。咱們知道通信的時候須要將文字信息轉換爲二進制編碼進行傳輸。那麼文字(好比26個英文字母)如何和一個二進制編碼進行對應。可是這個對應還不是那麼簡單的。首先是功能上的考慮: 一個重要要求就是每一個字母的編碼都不是其餘任何一個字母編碼的前綴。好比A編碼爲0,B編碼爲01,那麼當接收到型號01的時候,是翻譯成A1仍是B呢因此這個須要進行考量。第二點是性能上的考慮,即如何讓一樣多的二進制數據中傳遞更加多的信息。一個優化方法是找到字母中詞頻(對應哈夫曼樹中的權值)相對高的好比E,S等字母,將它們編碼得短一些;對於不經常使用的X,Z編碼得長一些,這樣整體能夠加快信息收送效率。

  哈夫曼樹正是解決了上面兩個問題的一個解法。這也是哈夫曼樹重要的應用,而獲得的編碼對應關係就是所謂的哈夫曼編碼了。

  

  首先,咱們將須要進行哈夫曼編碼的全部字母一字排開而且賦予它們各自的權值(即出現詞頻,一般能夠經過詞頻機率或者單純的出現次數),基於這個權值進行哈夫曼樹的構造。構造完成後,從整棵樹的根節點開始給每個樹鏈接到左子樹上寫0,鏈接到右子樹上寫1。從根節點出發,沿着路徑走到某個葉節點,將一路上的0和1鏈接起來組成的二進制數,就是本葉子節點對應字母的編碼了。

  經過哈夫曼樹獲得的哈夫曼編碼,能夠保證1. 不一樣編碼之間不會存在一個是另外一個前綴,從而引發信息解讀混亂的問題 2. 高頻的字母編碼儘可能短

  具體實現寫在下面(只能想到一些笨辦法…),簡單來講就是將各個字母和各自的頻度做爲一個tuple的兩項,而後整個tuple做爲節點的值保存。因爲最終要獲取到根節點到各個葉子節點的路徑(且左右還有區別)因此要改造一下BinTNode類。個人辦法是給節點類加上了parent和type兩個屬性。parent指向父節點,type可取值'l','r'或者None來代表這個節點屬於父節點的左右節點仍是這個節點沒有父節點。

  由於優先隊列涉及到節點之間比大小,因此還須要順便重載一下__lt__方法。生成哈夫曼樹以後,從根節點開始遍歷,若是碰到了葉子節點,那麼就從這個節點開始逆推到根節點的路徑,而且根據type屬性判斷路徑該加上0仍是1,最終得到本節點對應的編碼。因爲節點數據維護了字母,也就獲得了字母的編碼。把全部的這些編碼輸出便可。逆推的時候因爲parent節點都已經有了明確地址,因此單條路徑就像鏈表的遍歷同樣,總長是O(logn),總的來講逆推過程是O(nlogn)的(我想是這樣吧…不肯定對不對)

  代碼:

'''
Heap,BinTree,BinTNode類前面實現過了,包括parent和type屬性已經寫在BinTNode類中
'''

class WeightTNode(BinTNode):
    def __lt__(self, other):
        return self.data[1] < other.data[1]

def huffman(_dataSet):
    dataSet = [WeightTNode((item[0],item[1])) for item in _dataSet]
    heap = HeapQueue(dataSet)
    while len(heap) > 1:
        t1,t2 = heap.dequeue(),heap.dequeue()
        newNode = BinTNode(('',t1.data[1]+t2.data[1]),t1,t2)
        t1.parent = newNode
        t2.parent = newNode
        t1.type = 'l'
        t2.type = 'r'
        heap.enqueue(newNode)

    return BinTree(heap.dequeue())

if __name__ == '__main__':
    ofteness = {'a':12,'b':2,'c':5,'d':10,'e':11,'f':4,'g':3,'h':7}
    tree = huffman([(k,v) for k,v in ofteness.iteritems()])
    code_map = {}
    for node in tree.preorderIter():
        tmp = []
        if node.left is None and node.right is None:  # 到達葉子節點
            char = node.data[0]
            while node.parent is not None:
                if node.type == 'l':
                    tmp.append('0')
                elif node.type == 'r':
                    tmp.append('1')
                else:
                    raise Exception('!')
                node = node.parent
            code_map[char] = ''.join(reversed(tmp))

    print code_map
View Code

 

  

 

【樹】

  二叉樹說完這麼多,接下來來看一下更加通常的樹。相比於二叉樹,通常的樹去掉了每一個節點最多隻能有兩個子節點的性質。可是樹也有不少共性,以前在講二叉樹的時候以爲理所固然的東西,這裏再重點提要一下。

  1. 樹具備較爲明顯的層次性

  2. 只容許高層元素對低層元素有關,不容許逆向關係,也不容許任何非垂直方向的關係。即同層元素之間互相獨立無關係

  上面用了不少的「圓圈直線」表示樹是一種很經常使用的方法。小學奧數中經常使用的韋恩圖也能夠用於這種表示上下層級包含的關係。

  不少概念都和二叉樹相似,就很少說了。比較不一樣的是,泛泛的樹對於子節點的順序能夠考量也可不考量。當不考量的時候這種樹是無序樹,反之則是有序樹。可是一個坑是,最大度數爲2的有序樹並非二叉樹。子節點有序並不意味着子節點必定要是左子節點或右子節點。因此說,二叉樹中當一個節點只有一個子節點的時候必須指明它是左子節點仍是右子節點;而在最大度數爲2的有序樹中,一個節點只有一個子節點的時候能夠無論其是左仍是右。

  因爲在計算機中的存儲自己就是有順序這個特色的,因此在數據結構中主要考察的都是有序樹這一類。

  

  樹林是指若干棵互不相交的樹的集合。由於只須要知足不想交便可,因此一棵大樹的各個子樹也能夠組成一個樹林(這也是常見的研究方法,畢竟研究徹底獨立的幾棵樹之間的關係意義不大)。根據有序和無序性,樹林也能夠分紅有序樹林和無序樹林。

  下面就是一個神奇的事情了: 任意一片樹林,均可以一對一地轉化成一棵二叉樹。要作的操做是這樣的:

  1. 將樹林中各樹的根節點水平對齊後一字排開。

  2. 將各樹中全部同一層中擁有同一個父節點的兄弟節點鏈接起來,另外將全部樹的根節點也看作是同一層的兄弟節點,所有鏈接起來

  3. 同層的兄弟節點中,除了最左邊的一個,其他連向父節點的全部連線都去除。因爲2中有兄弟節點之間的連線,因此不會有節點孤懸。

  4. 將全部2步驟中添加的連線看作是父節點和右子節點之間的連線,全部原來就存在於樹中的連線看做是父節點和左子節點之間的連線。以左邊第一棵樹的根節點做爲整個新二叉樹的根節點

  這樣就將原先的樹林改形成了一棵二叉樹。

 

  反過來,如何經過二叉樹還原一個樹林。步驟是:

  1. 若是碰到某個節點C0,其有左子節點C,且左子樹含有最右邊,則這條最右邊上的全部節點C',C'',C'''...都和C0進行連線

  2. 去除二叉樹中全部連向右子節點的邊

  能夠注意到這樣的分隔只要二叉樹有右子樹,那麼這樣一顆二叉樹就可能被分紅多棵樹,即造成樹林。再說一次,這樣的樹林和二叉樹之間是一一對應的。

  左邊是樹林變二叉樹,右邊是二叉樹變樹林。虛線是去掉的線,粗線是過程當中新加的線

 

■  樹的廣泛性質

  直接泛泛地討論樹意義不大,能夠稍加限制好比定義樹的度數k。一棵樹的度數爲k表示這棵樹的任意一個節點,最多隻能有k個子節點。所以能夠獲得下面一些關於樹比較普適的性質。

  1. 樹的第i(i>=0)層至多有k^i個節點

  2. 度數爲k高爲h的樹中,至多有 (k^(h+1) - 1)/(k-1) 個節點(等比數列求和)

  以上「至多」的狀況的樹也是k度徹底樹

  3. n個節點的k度徹底樹的高度是不大於logk n的整數

  4. n個節點的樹中有n-1條邊

  

  一棵樹的抽象數據類型和二叉樹的抽象類型十分類似,不過也有些出入的地方:

class Tree:
    def Tree(self,data,forest)

    def is_empty(self)

    def num_nodes(self)

    def data(self)

    def first_child(self,node)  # 返回樹中某個節點的第一個子節點  

    def children(self,node)    # 返回樹中某個節點的各個子節點的迭代器

    def set_first(self,tree)    # 用tree對象替代本樹的第一棵子樹

    def insert_child(self,i,tree)    # 將某棵樹插入到i的位置

    def traversal(self)    # 返回一個能夠遍歷樹中全部節點的迭代器

    def forall(self,op)    # 對樹中全部節點執行op操做(一般一個函數)

  其中較爲重要的方法多是如何進行樹的遍歷。其實通常樹的遍歷大致上和二叉樹相似的。也分紅寬度優先和深度優先遍歷,深度優先也分紅先中後根序的遍歷。

  比較有意思的一點,不知道還記不記得以前棧和隊列那一片中曾經提到過基於棧和基於隊列進行迷宮探索的時候,能夠發現基於棧的探索是深度優先,而基於隊列的探索是寬度優先的。其實在進行迷宮探索的時候整個邏輯流程走的就是一棵樹的流程。這是一顆看不見的樹叫作搜索樹。基於隊列進行探索時其實就是寬度優先地遍歷這棵樹,基於棧探索時就是深度優先(根據上次的具體實現應該仍是中根序的)非遞歸的遍歷。

 

 ■  實現

  通常樹的實現,因爲其結構不如二叉樹那麼有規則,可能會牽扯到不少麻煩,所以要根據樹來選擇適當的實現形式而不是簡單地套用一個數據結構。下面介紹幾種書上提到的

  ●  子節點引用表示法

  這個方法實際上是對二叉樹線性表實現的一個擴展。有一個先決條件,那就是針對最大度數固定爲m的樹。首先,每一個節點仍是有存儲數據的地方,其次它還有一塊空間用來記錄我這個節點有多少個子節點,由於最大度數是m,因此這個值不能大於m。而後再在這個對象中開闢新的m-1塊子空間用來存放樹。對於放不滿的狀況,那些多出來的空間就讓它放着。

  這種方法的優缺點也很顯然,優勢是數據結構比較規整,取數據很方便;缺點就是可能存在大量的被浪費的空間。

  ●  父節點表示法

  父節點表示法的概念是建立一個連續表,這個表中的每一個元素有兩塊空間,一塊用來存儲本身的數據,另外一塊用來指出其父節點的位置(好比位於這個數組中的下標值)。這樣,只須要有O(n)的空間即可以將整個樹完整地記錄進內存當中。

  固然這麼作的缺點就是進行節點的增刪查改時比較麻煩。由於這樣的一個結構只容許咱們從下往上地尋找關係,若是想要統一全局地考察樹就比較麻煩了。

  ●  子節點表表示法

  這種實現方法的意思在於建立一個連續表用來存儲各個節點的數據,另外每一個元素還須要有一塊空間,用來關聯這個節點的全部子節點的位置,好比在連續表中的下標。這個想法其實和父節點表示法相似,只不過每一個節點的父節點只有一個,因此父節點表示法能夠明確只使用O(n)的空間,而每一個節點的子節點數目不定,因此子節點表表示法涉及的數據結構可能會更復雜。可是相對子節點引用表示法,其用了相對少的空間自上而下地維護了樹的結構信息和數據信息。

 

■    樹的Python實現

  下面的實現,核心上來講是一種嵌套列表的實現,有點相似於最開始咱們基於List實現二叉樹的那種。其實能夠這麼幹是得益於Python自己具備了不少高級的數據結構能夠直接使用。

  咱們將某個節點的子節點數據直接寫在表示這個節點的列表中。固然若是是個子樹那就是又一個列表。因此:

class SubtreeIndexError(ValueError):
    pass

def Tree(data,*subtrees):
    return [data].extend(subtrees)

def is_empty_tree(tree):
    return tree is None

def root(tree):
    return tree[0]

def subtree(tree,i):  # 獲取某個子節點/子樹
    if i < 0 or i > len(tree):
        raise SubtreeIndexError
    return tree[i+1]

 

  能夠看到,當咱們嚴格限制Tree方法的subtree參數長度只能是2的時候,獲得的就是一個List實現的二叉樹了。

  同時咱們使用的Python列表結構的話,能夠比較自由地擴縮容,也方便了樹中節點的增刪改。

  而後根據二叉樹中獲得的經驗,再稍微用類將這個嵌套列表給包裝起來,成一個正式的樹類

class TreeNode:
    def __init__(self,root,*subtrees):
        self._root = root
        self._subtrees = subtrees

    def root(self):
        return self._root

    def set_child(self,i,node):
        if i < 1 or i >= len(self._subtrees):
            raise IndexError
        self._subtrees[i] = node

class Tree:
    def __init__(self,rootNode=None):
        self.rootNode = rootNode
    
    def is_empty(self):
        return self.rootNode is None

    def subtrees(self):
        for subtree in self.rootNode._subtrees:
            yield subtree

    def set_subtree(self,i,tree):
        if not isinstance(tree,'Tree'):
            raise ValueError
        self.rootNode.set_child(tree.rootNode)        

 

 

  長長一篇重要寫完了。其實關於樹這麼大一塊內容,還有不少不少書上都沒提到,好比紅黑樹,線段樹什麼的。目前我也仍是看不太懂這些內容… 要學的東西還有不少,很難有時間和精力再去進階地深究一些內容,總之先這樣吧。天也不早了,差很少該睡了。

相關文章
相關標籤/搜索