《計算機程序設計藝術》數據結構之樹

個人掘金專欄算法

上節是線性結構,這節的樹是「非線性」結構。bash

樹的定義

  1. 有一個特別指定的節點 T,記爲 root(T),它叫作樹的根。
  2. 剩餘的節點被分劃成 m>=0 個「不相交」的集合,T1,...,Tm,每一個集合也是一個樹,稱爲子樹。

這個定義很奇怪,它是一個遞歸定義,由於用樹來定義了樹。post

但這並無什麼問題,由於讓 m = 0,那麼一個節點就是一棵樹。而後咱們能夠用若干個這樣的樹組成更大的樹。spa

樹也有非遞歸形式的定義,但遞歸定義彷佛是最合適的。由於天然界中的樹也是由幼樹的樹芽逐漸長出有本身的樹芽的子樹芽。3d

術語

  • 度:一個節點的子樹的個數,叫作該節點的度(是枝開二度類比來的嗎?)
  • 葉子節點:度數爲零(沒開過)的節點叫作葉子節點
  • 分支節點:非葉子節點
  • 級:root(T) 的級爲 0,它包含的子樹的根節點都比 root(T) 的級別大 1,以此類推。

  • 有序樹:若是子樹 T1,...,Tm 的相對順序是重要的話,咱們就說該樹是有序樹。也有人稱之爲「平面樹」,由於它與在一個平面中嵌入樹的方式有關。T1 叫第一棵子樹,T2 叫第二棵子樹等等。
  • 有向的樹:若是不把僅僅是節點子樹的相對次序不一樣的兩個樹看成不一樣的樹,就說這樣的樹是有向的。由於考慮的僅僅是節點相對的方向,而不是它們的次序。(這個定義很晦澀,學了圖以後有更簡單的定義)。

從如今開始,咱們假定全部的樹都是有序的,除非另有說明。指針

好比下面兩棵樹是不一樣的樹:code

  • 森林:零個或多個不相交的樹的集合。也能夠說樹除了根以外的全部節點造成一個森林。
  • 二叉樹:該樹的每一個節點至多有 2 個子樹。當只有一個子樹時,咱們會區分左子樹和右子樹。

注意:二叉樹不是樹的特殊狀況。二叉樹徹底是另外一個概念,雖然它跟樹有不少聯繫。例以下圖:cdn

對於二叉樹,這是兩棵不一樣的樹;對於樹,這兩個是相同的樹。(在這一點上我我的表示不太理解,對於樹來講它們能夠是不一樣的樹哇)。blog

  • 0-2樹:每一個節點恰有0個或2個子樹的樹。

樹的畫法

畫樹的時候,能夠把根畫在下面,也能夠把根畫在上面,甚至左邊。繼承

最推薦的畫法是把根畫在上面,緣由以下:

  1. 符合閱讀習慣,大部分人都是從上往下閱讀
  2. 符合更多術語的意義,如 subtree、supertree 等。
  3. 方便使用深淺來描述節點。深的在下面,淺的在上面。

還能夠用不少其餘方式來表示樹。

上圖能夠表示爲下面三種不一樣的形式:

一個代數公式也定義了一個隱含的樹,a - b * (c/d + e/f) 表示:

遍歷二叉樹

有三個主要的方法遍歷二叉樹:

  1. 先根序
  2. 中根序(也叫對稱序)
  3. 後根序

中根序遍歷

設 T 存儲了二叉樹的節點的地址(頭指針),A 是一個棧,做爲輔助。步驟以下:

  • 第一步,置棧 A 爲空,置 P <- T
  • 第二步,若是 P 爲空,則跳到第四步;若是 P 不爲空,則跳到第三步。
  • 第三步,置 A <= P(把 P 壓入 A),而後置 P <- LeftLink(P),並返回至第二步。
  • 第四步,若是 A 不爲空,則置 P <= A(彈出棧頂內存放入 P);若是 A 爲空,則算法結束
  • 第五步,訪問 Node(P),而後置 P <- RightLink(P),並返回至第二步。

用人類的話來講,就是

  1. 把全部最左邊的節點依次壓入 A 棧,直到沒有最左的節點。
  2. A 棧依次彈棧入 P,打出 P 節點的值,再看 P 有沒有右子樹,若是有,就讓右子樹經歷一下第 1 步。沒有右子樹就算了。
  3. A 棧爲空則說明打印完畢。

新手乍看會以爲奇怪,這只是先遍歷了左邊,再遍歷了右邊而已啊,哪有在中間訪問父節點?

這個就須要新手本身動手模擬一遍整個過程了,確實有在「中間」訪問父節點哦。

能夠用幾乎相同的算法來描述先根序遍歷,後根序稍微難一點,因此通常來講咱們優先使用前兩種遍歷方式。

前驅與後繼

前驅就是上一個節點(能夠是子節點也能夠是父節點),後繼就是下一個節點。因爲我認爲這兩個術語十分無聊,因此下文統一以大白話來敘述。

假設 P 指向一個二叉樹,那麼

  • P$ 表示在中根遍歷時節點 Node(P) 的下一個節點的地址
  • $P 表示在中根遍歷時節點 Node(P) 的上一個節點的地址

爲何用 $ 呢?由於 $ 是對稱的,因此能夠表示對稱序(中根序)。

  • P* 表示在先根遍歷時節點 Node(P) 的下一個節點的地址
  • *P 表示在先根遍歷時節點 Node(P) 的上一個節點的地址

後根就不講了,省得記混。

內存表示

咱們能夠把二叉樹中的每一個節點表示爲 [LeftLink, Node, RightLink],那麼樹

就能夠表示爲:

這種表示方法會出現不少空連接,空連接甚至比有用的連接還要多,怎麼解決這個問題呢?

穿線的樹

看圖:

  1. 每一個節點都有兩條指向其餘地方的線
    1. 可能兩條線都是實線,指向本身的子樹,好比 C
    2. 可能兩條線一實一虛,一個是子樹,一個是比它高的節點(規則不明),好比 B 和 E
    3. 可能兩條線都是虛線,好比 D、G、H、J
  2. 只有 D 和 J 的虛線比較特殊,分別指向「最左」和「最右」,後面再說。

那麼虛線具體指向哪個比它高的節點呢?答案是

  • 左虛線指向中根遍歷時的上一個節點
  • 右虛線指向中根遍歷時的下一個節點

從新開圖,能夠得出中根中序遍歷的順序是:

D B A E G C H F J

好了,咱們知道怎麼畫穿線了,那麼怎麼用內存表示呢?

給每一個節點再加兩個 bit:[LeftTag, LeftLink, Node, RightLink, RightTag]

LeftTag 和 RightTag 的值只能是 0 或 1,0 表示沒有虛線(有實線),1 表示有虛線。

由於咱們能夠經過中根遍歷算法肯定具體的位置,因此就不須要再存一個地址了。

穿線的存在使得遍歷二叉樹不須要額外的輔助棧。

類似和等價

  • 類似的樹:若是二叉樹 T 和 T' 結構相同,則它們是類似的
  • 等價的樹:若是二叉樹 T 和 T' 類似,且對應的節點包含相同的信息,則說它們是等價的。

書中對如何證實兩個數類似和等價給出了形式化的步驟,有興趣能夠自行翻開 311 頁查看。

樹的二叉樹表示

樹和二叉樹的區別咱們複習一下:

  1. 樹老是有一個根節點;樹的每一個節點能夠有 0,1,2,3,4,5,6,... 個子樹。
  2. 二叉樹能夠爲空;它的每一個節點能夠有 0,1,2 個子二叉樹;它區分左兒子和右兒子。

這一節研究「咱們是否能把一棵樹表示爲一棵二叉樹」。

考慮有以下兩棵樹

咱們把每棵樹的連接刪掉,而後把全部擁有同一個父親的節點橫向連接(哥哥指向弟弟),而後讓每一個父親只連接它的大兒子(爸爸指向大兒子),再把兩個根節點連起來,就獲得了這樣的圖:

接下來就神奇了,把上圖順時針轉 45 度,而後微調一下,就獲得了二叉樹!

這個過程是可逆的,因此任何二叉樹都對應於惟一片森林。

爲何會這樣呢?由於每一個節點至多有一個大兒子,至多有一個弟弟呀,因此每一個節點的度至多爲 2,這知足二叉樹的定義。

經過數學概括飯,很容易證實三棵樹的森林,也能夠轉化成二叉樹。以此類推。

這個轉換過程叫作「森林和二叉樹之間的天然對應」。

形式化的描述以下:

對於具備 n+1 個節點的樹 T,和具備 n 個節點的二叉樹,令 F = (T1, T2, ..., Tn) 是 T 的子樹組成的森林,二叉樹 B(F) 的定義以下:

  1. 若是 n = 0,則 B(F) 爲空
  2. 若是 n > 0,則 B(F) 的根是 root(T1);B(F) 的左子樹是 B(T11,T12,...,T1m),其中 T11,T12,...,T1m 是 T1 的子樹;B(F) 的右子樹是 B(T2,T3,...,Tn),這裏又是一個遞歸定義。

從新理解遍歷

天然對應轉行過程其實就是對樹進行先根遍歷。

咱們把上面的兩棵樹用另外一種形式從新表示一下:

(A(B,C(K)), D(E(H),F(J),G))

對其進行先根遍歷,獲得 A B C K D E H F J G,竟然就是把括號去掉而已!

這就是爲何把這種遍歷叫作「天然對應」了,由於真的很天然。

再舉個例子:

對書的章節號組成的樹進行先根遍歷,獲得的結果是

1
1.1
1.1.1
1.1.2
1.2
1.2.1
1.3
1.3.1
1.3.2
複製代碼

夠不夠天然?

再舉個例子,歐洲的王位繼承。

  1. 國王死了,傳位給大兒子,大兒子死了就傳位給大兒子的大兒子。
  2. 若是很不幸,大兒子全家都死了,就傳給其餘兒子中最大的一個。以此類推。

因此,只須要把皇室的族譜先根遍歷一下,就能夠獲得王位繼承順序了。

夠不夠天然?

因此先根序是列出樹中節點最天然的方法。

接下來是後根序。

後根序

(A(B,C(K)), D(E(H),F(J),G))

的先根序是

A B C K D E H F J G

後根序則是

B K C A H E J F G D

這其實就是把原樹 (A(B,C(K)), D(E(H),F(J),G)) 的書寫方式改一下,把父節點寫在括號右邊,而不是左邊:

((B, (K)C)A, ((H)E,(J)F,G)D)

經過把對應的定義作對比,能夠觀察出:

  1. 先根遍歷森林與先根遍歷對應二叉樹徹底相同
  2. 後根遍歷森林與中根遍歷對應二叉樹徹底相同

樹的基本數學性質

擴充的二叉樹

在二叉樹全部的空節點的地方加上特殊節點,就獲得了一個擴充的二叉樹。

假設有 n 個圓圈節點,s 個方形節點,那麼邊的數量就是 n + s - 1

因爲每一個圓圈對應兩條邊,因此邊的數量也能夠表示成 2n

因而咱們獲得 n + s - 1 = 2n,由此推出 s = n + 1

s = n + 1 意味着,特殊的方形節點老是比圓形節點多一個。

即便 n = 0,這個公式也成立。

通路長度

圖:通常把圖定義爲點(叫作頂點) + 鏈接某些不一樣頂點的線(叫作邊)的集合。簡而言之,圖 = 頂點 + 邊。一對頂點之間最多有一條邊。若是兩個頂點被一條邊鏈接,則稱這兩個頂點是「相鄰」的。

通路:若是 VV' 是兩個頂點,V_0 = VV_k 相鄰與 V_{k+1}V_n = V',那麼咱們就說 (V_0, V_1, ..., V_n) 是從 V 到 V' 的一條通路,長度爲 n。

簡單通路:若是 V_0, V_1, ..., V_{n-1} 都不相同,並且 V_1, V_1, ..., V_n 也都不相同,則這條通路的簡單的。這句話的意思是 V_0V_n 能夠相同,只要中途的點都不相同便可。

擴充二叉樹的外部通路長度 E 的定義是「從根到每一個方形節點的通路長度之和」。

擴充二叉樹的內部通路長度 I 的定義是「從根到每一個圓形節點的通路長度之和」。

上圖中的 E = 3 + 3 + 2 + 3 + 4 + 4 + 3 + 3 = 25I = 2 + 1 + 0 + 2 + 3 + 1 + 2 = 11

而且能夠證實,E = I + 2n

徹底二叉樹

圖中從左到右給全部節點編了號。你很容易發現

  • 節點 k 的父親是 k/2
  • 節點 k 的兒子是 2k 和 2k+1
  • n 是圓形節點個數,那麼方形節點的編號就是 n + 1 到 2n + 1

所以,徹底二叉樹能夠順序存儲,且結構隱含在節點的下標中。

帶權通路

假設有 m 個實數 w_1 w_2 ... w_m,把這些實數做爲擴充二叉樹的方形節點,能夠畫出不一樣的擴充二叉樹。求如何找出「帶權通路長度」最小的二叉樹(其中帶權通路長度 \Sigma w_jl_j 是指 w_j 乘以其到根節點的通路長度 l_j 的積的累加之和)。

假設這 m 個數字是 2 3 4 11,那麼就有三種擴充二叉樹:

三者的帶權通路長度爲別是 4*2+2*3+3*3+11*1=343*2+4*3+11*3+2*1=532*2+11*2+3*2+4*2=40。最小的是第一個。

求最小帶權通路長度的樹的漂亮算法是由 D.Huffman(哈夫曼)發現的。

  1. 首先求最小的兩個值 w_iw_j 組成的二叉樹(顯然一個放左邊一個放右邊長度最小)
  2. 而後把這個數看作一個總體,值爲 w_i + w_j,與剩下的數字放一塊兒,遞歸地帶入第 1 步便可,直到只剩兩個

舉例:求 2,3,4,7,11,13,17,19,23,29,31,41 的最優樹

相關文章
相關標籤/搜索