農林漁牧網

您現在的位置是:首頁 > 林業

當初我要是這麼學習二叉樹就好了

2022-01-11由 紙鶴視界 發表于 林業

遞迴樹怎麼畫

目錄

1。 樹形結構

1。1 概念(瞭解)

1。2 概念(重要)

1。3 樹的表示形式

1。4 樹的應用

2。 二叉樹(BinaryTree重點)

2。1 概念

2。2 二叉樹的

5種

基本形態

2。3 兩種特殊的二叉樹

2。3。1 斜樹

2。3。2 滿二叉樹

2。3。3 完全二叉樹

2。4 二叉樹的性質

2。4。1

第i層結點個數

2。4。2

樹的所有最大點個數

2。4。3

葉子結點和非葉子結點數量關係

2。4。4

根據結點求樹深度

2。4。5

父子結點編號關係

2。4。6 小練兵

2。5 二叉樹的儲存

2。5。1 順序儲存

2。5。1。1 完全二叉樹的形式儲存

2。5。1。2 一般樹的形式儲存

2。5。2 鏈式儲存

3。 二叉樹由淺入深解析

3。1 遍歷二叉樹

3。1。1 二叉樹遍歷原理

3。1。2 二叉樹遍歷方法

3。1。2。1 前序遍歷

3。1。2。2 中序遍歷

3。1。2。3 後序遍歷

3。1。2。4 層序遍歷

3。1。3 二叉樹遍歷演算法

3。1。3。1 前序遍歷演算法

根左右

3。1。3。2 中序遍歷演算法

左根右

3。1。3。2 後序遍歷演算法

左右根

3。1。3。2 層序遍歷演算法

上至下, 左至右

3。2 二叉樹建立問題

3。2。1 前序中序構建二叉樹

3。2。2 中序後序構建二叉樹

3。2。3 根據二叉樹建立字串

3。2。4 前序遍歷字串構建二叉樹

3。2。5 合併二叉樹

3。2。6 遞增二叉樹

3。3 經典題目解析

3。3。1 結點個數

3。3。2 葉子結點個數

3。3。3 第k層結點個數

3。3。4 查詢 val 所在結點

3。3。5 相同的樹

3。3。6 另一棵樹子樹

3。3。7 二叉樹最大深度

3。3。8 平衡二叉樹

3。3。9 對稱二叉樹

3。3。10 二叉樹的完全性檢驗

3。3。11 二叉樹最近公公祖先

3。3。12 二叉樹最大寬度

3。4。 特殊的二叉樹問題

3。4。1 搜尋二叉樹

3。4。2 赫夫曼樹及其應用

3。4。1。 赫夫曼樹

3。4。2。赫夫曼樹定義與原理

3。4。3 赫夫曼編碼

3。4。3 線索二叉樹

4。 總結回顧

5。 結束語

1。 樹形結構

1。1 概念(瞭解)

樹不再是順序表, 連結串列, 棧, 佇列那樣一對一的線性結構。 而樹則

是一種一對多的資料結構, 由n(n>=0)個互不相交有限節點組層有層次關係的集合。

特點如下:

有一個特殊的節點, 稱為根節點, 根節點沒有前驅節點

除根節點外, 其餘節點被分成M(M > 0)個互不相交的集合T1, T2, …, Tm,其中每一個集合 <= m) 又是一棵與樹類似的子樹。每棵子樹的根節點有且只有一個前驅,可以有0個或多個後繼

樹是遞迴定義的[樹的定義中用到了樹的概念]

錯誤的二叉樹 D和E不應該相交, I節點有兩個父節點

當初我要是這麼學習二叉樹就好了

正確的樹:

當初我要是這麼學習二叉樹就好了

1。2 概念(重要)

用的是上圖中正確的二叉樹來解釋概念

節點的度:

一個節點含有的子樹的個數稱為該節點的度[A的度就是2]

非終端節點或分支節點:度不為0的節點[A, B, C, D, E]

當初我要是這麼學習二叉樹就好了

樹的度:

一棵樹中,最大的節點的度稱為樹的度; 如上圖:樹的度為3

葉子節點或終端節點:

度為0的節點稱為葉節點; 如上圖:G、H、I、J節點為葉節點

雙親節點或父節點:

若一個節點含有子節點,則這個節點稱為其子節點的父節點; 如上圖:A是B的父節點

孩子節點或子節點:

一個節點含有的子樹的根節點稱為該節點的子節點; 如上圖:B是A的孩子節點

兄弟節點:

具有相同父節點的節點互稱為兄弟節點[B和C, E和F, G, H和I]

堂兄弟節點:

雙親在同一層的節點互稱為堂兄弟節點[D和E, D和F]

節點的祖先:

從根到該節點的所有分支經歷所有節點[A]

根結點:

一棵樹中,沒有雙親結點的結點;如上圖:A

森林:

由m(m>=0)棵互不相交的樹的集合稱為森林

當初我要是這麼學習二叉樹就好了

節點的層次:

從根開始定義起,根為第1層,根的子節點為第2層,以此類推

樹的高度或深度:

樹中節點的最大層次; 如上圖:樹的高度為4

當初我要是這麼學習二叉樹就好了

當初我要是這麼學習二叉樹就好了

1。3 樹的表示形式

樹結構相對線性表就比較複雜了,要儲存表示起來就比較麻煩了,實際中樹有很多種表示方式,如:雙親表示法、孩子表示法、孩子兄弟表示法等等。我們這裡就簡單的瞭解其中最常用的

孩子兄弟表示法

【線上OJ常考的就是這種表示形式】

class

Node

{

int

value

// 樹中儲存的資料

Node

child

// 第一個孩子引用

Node

brother

// 下一個兄弟引用

}

當初我要是這麼學習二叉樹就好了

1。4 樹的應用

檔案系統管理(目錄和檔案)

當初我要是這麼學習二叉樹就好了

2。 二叉樹(BinaryTree重點)

2。1 概念

我們從經典的猜數字遊戲開始, 由淺入深介紹二叉樹。

在100以內猜數字, 最多幾次可以猜對呢?答案是:7

下面是根據每次猜的大小來構建二叉樹

當初我要是這麼學習二叉樹就好了

當初我要是這麼學習二叉樹就好了

我們發現利用這個大小關係來查詢指定範圍內某個數字, 效率不是高的一丁半點。 因此對於這種具有對立關係的情況: 0和1, 真與假, 上與下, 正與反, 對與錯都適合用樹來建模。 這種特殊的樹狀結構稱為二叉樹

一棵二叉樹是結點的一個有限集合,該集合或者為空,或者是由一個根節點加上兩棵別稱為左子樹和右子樹的二叉樹組成。

特點:

每個節點最多有2顆子樹, 即不存在度大於2的節點

二叉樹有左右子樹之分, 次序不能顛倒, 因此二叉樹也是一顆有序樹

2。2 二叉樹的

5種

基本形態

空二叉樹

只有一個根節點

根節點只有左子樹

根節點只有右子樹

根節點即有左子樹又有右子樹

當初我要是這麼學習二叉樹就好了

2。3 兩種特殊的二叉樹

2。3。1 斜樹

顧名思義, 斜樹一定要是斜的, 往哪兒斜也是有講究的。

所有的節點都只有左子樹叫做左斜樹; 所有的節點都只有右子樹叫做右斜樹

左斜樹:

當初我要是這麼學習二叉樹就好了

右斜樹:

當初我要是這麼學習二叉樹就好了

斜樹有明顯特點: 每一層都只有一個節點, 結點個數和樹的深度相同。

有些同學就奇怪了: 這也能叫樹嗎, 這不是我們熟悉的線性表嗎?

解釋: 對的, 線性表結構可以理解為樹的一種極其特殊的表現形式

2。3。2 滿二叉樹

在一棵二叉樹中, 如果所有分支結點都存在左子樹和右子樹, 並且所有葉子節點都在同一層上, 這樣的二叉樹稱為滿二叉樹

當初我要是這麼學習二叉樹就好了

它的樣子看得很美, 很勻稱

滿二叉樹特點:

葉子結點只能出現在最下一層, 出現在其它層就不可能達到平衡

非葉子結點的度一定是2, 否賊就是“缺胳膊少腿”

在同樣深度的二叉樹中, 滿二叉樹結點個數最多, 葉子結點最多

2。3。3 完全二叉樹

把一顆具有 n 個節點的二叉樹按層序編號, 編號i(<1=i<=n)的節點與同樣深度的滿二叉樹中編號為i的節點在二叉樹中所在位置完全相同

當初我要是這麼學習二叉樹就好了

這是一種有些難以理解的特殊二叉樹

首先從字面上理解區分, “滿”和“完全”的差異: 滿二叉樹一定是一顆完全二叉樹, 但是完全二叉樹不一定是一顆滿二叉樹

什麼是按層序編號?

上圖中就是按照從上到下, 從左到右一次按照1, 2, 3, …, n的序號

當初我要是這麼學習二叉樹就好了

完全二叉樹特點:

葉子結點只能出現在最下兩層

最下層的葉子結點一定集中在左部連續位置

倒數第二層, 如果有葉子結點, 一定都在右部連續位置

如果結點的度為1, 則該節點只有左孩子, 即不存在只有右子樹的情況

同樣結點數的二叉樹, 完全二叉樹的深度最小

從上面的列子中, 也給了我們一個判斷某二叉樹是否為

完全二叉樹

的辦法: 看著二叉樹的示意圖, 心中默默給每個節點按照滿二叉樹的結構逐層順序編號, 如果編號出現了空檔, 就說明不是完全二叉樹否則就是完全二叉樹

2。4 二叉樹的性質

2。4。1

第i層結點個數

若規定根節點的層數為1,則一棵非空二叉樹的第i層上最多有

2^(i-1)

(i>0)個結點

2。4。2

樹的所有最大點個數

若規定根節點的層數為1,則深度為K的二叉樹最大節點數是

2^k-1

(k>0)個結點

2。4。3

葉子結點和非葉子結點數量關係

對任何一棵二叉樹, 如果其葉結點個數為 n0, 度為2的非葉結點個數為 n2,則有

n0=n2+1

[最下一層的葉子結點個數=上面的所有非葉子結點個數+1]

2。4。4

根據結點求樹深度

具有n個節點的二叉樹的深度K為

log2^(n+1)向上取整

2。4。5

父子結點編號關係

對於具有n個結點的完全二叉樹,如果按照從上至下從左至右的順序對所有節點從0開始編號,則對於序號為i 的結點有:

若i>0,雙親序號:(i-1)/2;

若i=0,i為根節點編號,無雙親節點

若2i+1

若2i+2

這個規律很重要, 涉及到堆排, 二叉搜尋樹需要用到這個規律

2。4。6 小練兵

設一棵完全二叉樹中總共有1000個節點, 則該二叉樹中

500 個葉子節點, 500個非葉子節點, 1個節點只有左孩子, 0個只有右孩子

500個葉子結點計算步驟

葉子結點總個數=第10層葉子結點個數+第9層葉子結點個數

有同學會想: 為何第9層會有葉子結點呢?

解釋:

因為我們發現此樹共有

1000

個結點, 而滿二叉樹的話會有

2^10-1

個結點, 所以說明

第10層沒滿, 第9層一定有葉子結點

有了這個理解後再做進一步的計算

第10層全是葉子結點個數: 樹的總結點個數

-

9層及以上所有結點個數

因為是一顆完全二叉樹, 所以可以保證第9層一定是滿二叉樹

算式:

1000-(2^9-1)–>1000-511=489

現在我們還差第9層的葉子結點個數

第9層葉子結點個數: 第9層結點個數

-

第10層父節點個數

算式:

2^(9-1)-第10層父節點個數

最後的問題就是求第10層父節點個數

第10層父節點個數求法

如果結點個數n是偶數, 則其父結點個數為 n/2

如果結點個數n是奇數, 則其父結點個數n/2+1

在第一步中我們求得了第10層父節點個數, 求第10層父節點個數就是:

489/2 + 1–>245

現在可以計算第

3

步得出第9層葉子結點個數:

2^(9-1)-245–>256-245=11

現在可以計算第

1

步中的所有整棵樹的葉子結點;

10

層結點數

+

9

葉子結點數: 489+11 = 500

有了葉子結點個數, 很容易知道非葉子節點

總結點個數-葉子結點個數

1000-500

由於完全二叉樹第10層結點個數為489, 為奇數, 所以一定有

1

個節點只有左孩子, 有

0

個節點只有右子樹

將一顆有 100 個結點的完全二叉樹從根這一層開始,每一層從左到右依次對結點進行編號,根節點編號為 1 ,則編號為 98 的節點的父節點編號為:

(98-1)/2 = 48

設非空二叉樹的所有子樹中,其左子樹上的結點值均小於根結點值,而右子樹上的結點值均不小於根結點值,則稱該二叉樹為排序二叉樹。對排序二叉樹的遍歷結果為有序序列的是:

中序遍歷

將一棵二叉樹的根結點放入佇列,然後遞迴的執行如下操作,將出隊結點所有子結點加入隊。以上操作可以實現

層序遍歷

n個節點的完全二叉樹,最多可以有多少層:

log(n+1)上取整

2。5 二叉樹的儲存

2。5。1 順序儲存

二叉樹的順序儲存就使用一維陣列儲存二叉樹中的節點並且儲存的位置應該要能體現節點之間的邏輯關係, 比如孩子雙親, 左右兄弟節點

2。5。1。1 完全二叉樹的形式儲存

當初我要是這麼學習二叉樹就好了

將這顆二叉樹儲存陣列中, 相應的下標對應其相等的位置

當初我要是這麼學習二叉樹就好了

這下看出

完全二叉樹

的優越性了吧, 由於它定義的嚴格, 所以順序結構也能表現出二叉樹的結構來

2。5。1。2 一般樹的形式儲存

對於一般二叉樹而言, 層序編號不能反映邏輯關係, 但是可以按照完全二叉樹編號, 只不過把不存在的結點設施為 ‘^’ 而已。

淺色節點代表不存在

當初我要是這麼學習二叉樹就好了

考慮一下極端情況: 又斜樹【左斜樹】

一顆深度為

k

的右斜樹, 只有

k

個節點, 卻需要分配

2^k-1

個儲存單元, 這顯然是對儲存空間的浪費。

所以順序儲存只適用於完全二叉樹

當初我要是這麼學習二叉樹就好了

2。5。2 鏈式儲存

既然順序儲存適應性不強, 我們就要考慮鏈式儲存結構。 二叉樹每個節點最多有兩個孩子, 所以設計一個數據域和兩個指標域比較理想。 這就是二叉連結串列

孩子表示法

當初我要是這麼學習二叉樹就好了

class

Node

{

int

val

// 資料域

Node

left

// 左孩子的引用,常常代表左孩子為根的整棵左子樹

Node

right

// 右孩子的引用,常常代表右孩子為根的整棵右子樹

}

當初我要是這麼學習二叉樹就好了

孩子雙親表示法主要用在在平衡樹,本文采用

孩子表示法

來構建二叉樹, 大多數的線上OJ題目也都採用的這種方法來構建二叉樹。

3。 二叉樹由淺入深解析

3。1 遍歷二叉樹

3。1。1 二叉樹遍歷原理

導言: 假設有20張100元的和2000張1元的獎券, 同時撒向了空中, 大家比賽看誰最中間的最多。

相信同學都會說: 先撿100元的。 道理非常簡單, 因為撿1張100元的等於撿100張1元的, 效率好的不是一點點?。

所以得出這樣的結論: 同樣是撿獎券, 在有限時間內, 要達到最高效率, 次序非常重要。 對於二叉樹的遍歷來講, 次序同樣顯得也很重要。

二叉樹的遍歷(traversing binary tree): 是指從根結點出發, 按照某種次序

依次

訪問

二叉樹中的所有節點, 使得每一個節點被訪問一次且僅僅被訪問一次

這裡有兩個關鍵詞: 依次和訪問。 不同於線性結構, 最多也就是從頭至尾, 迴圈, 雙向等簡單的遍歷方式。 樹的節點之間不存在唯一的前驅和後繼關係, 在訪問一個節點後, 下一個被訪問的節點面臨著不同的選擇。 就像你人生的道路上, 高考填報志願要面臨哪個城市, 哪所大學, 具體專業等。 選擇方式不同, 遍歷的次序就完全不同

3。1。2 二叉樹遍歷方法

3。1。2。1 前序遍歷

若二叉樹為空, 則返回空操作

否則先訪問根節點, 然後前序遍歷左子樹再前序遍歷右子樹

最終訪問的結果就是: ABDGHCEIF

如下圖就是

根左右

的順序訪問二叉樹示意圖

當初我要是這麼學習二叉樹就好了

3。1。2。2 中序遍歷

樹空, 則返回空操作

否則就從根結點開始(注意並不是先訪問根節點)

中序遍歷根節點的左子樹

然後訪問根節點

最後中序遍歷右子樹

最終訪問結果就是: GDHBAEICF

如下圖就是

左根右

的順序訪問二叉樹示意圖

當初我要是這麼學習二叉樹就好了

3。1。2。3 後序遍歷

樹空, 則返回空操作

否先從左到右線葉子結點後節點的方式遍歷訪問左右子樹, 最後訪問根節點

最終訪問結果是: GHDBIEFCA

如下圖就是

左右根

的順序訪問二叉樹示意圖

當初我要是這麼學習二叉樹就好了

3。1。2。4 層序遍歷

若樹為空, 返回空操作

否則從樹的第一層, 也就是根節點開始訪問

從上而下逐層遍歷

在同一層中, 按左到右的順序對節點逐個訪問

最終訪問結果就是: ABCDEFGHI

當初我要是這麼學習二叉樹就好了

3。1。3 二叉樹遍歷演算法

假設我們有以下一顆二叉樹T, 用連結串列結構儲存

當初我要是這麼學習二叉樹就好了

3。1。3。1 前序遍歷演算法

根左右

OJ題目連結

遞迴前序遍歷

private

static

void

preOrderTraversal

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

System

out

print

root

val

+

“ ”

preOrder

root

left

preOrder

root

right

}

}

private

static

List

<

String

>

preOrderTraversal

BinaryTreeNode

root

{

List

<

String

>

result

=

new

ArrayList

<

>

if

root

==

null

{

return

result

}

else

{

result

add

root

val

result

addAll

preOrderTraversal

root

left

result

addAll

preOrderTraversal

root

right

return

result

}

}

A

B

D

H

K

E

C

F

I

G

J

演示

遞迴前序遍歷

步驟

呼叫

preOrderTraversal(BinaryTreeNode root)

, root 根節點不為空, 所以執行

System。out。print(root。val + “ ”);

列印字母

A

當初我要是這麼學習二叉樹就好了

呼叫

preOrderTraversal(root。left)

, 訪問了

A

節點的左孩子, 不為 null, 執行

System。out。print(root。val + “ ”);

列印字母

B

當初我要是這麼學習二叉樹就好了

再次遞迴呼叫

preOrderTraversal(root。left)

, 訪問了

B

節點的左孩子, 不為 null, 執行

System。out。print(root。val + “ ”);

列印字母

D

當初我要是這麼學習二叉樹就好了

再次遞迴呼叫

preOrderTraversal(root。left)

, 訪問了

D

節點的左孩子, 不為 null, 執行

System。out。print(root。val + “ ”);

列印字母

H

當初我要是這麼學習二叉樹就好了

再次遞迴呼叫

preOrderTraversal(root。left)

, 訪問

H

節點的左孩子, 此時因為

H

節點無左孩子, 所以

root == null

返回此函, 此時遞迴呼叫

preOrderTraversal(root。right);

訪問了

H

節點的右孩子, 執行

System。out。print(root。val + “ ”);

列印字母

K

當初我要是這麼學習二叉樹就好了

再次遞迴呼叫

preOrderTraversal(root。left)

, 訪問

K

節點的左孩子, 返回

呼叫

preOrderTraversal(root。right)

, 訪問了

K

節點的右孩子, 也是 null, 返回。於是函式執行完畢。

返回到上一級遞迴的函式(即列印

H

節點時的函式), 也執行完畢。[根左右都已經遍歷完, 所以執行完畢]

返回列印

D

節點時的函式, 呼叫

preOrderTraversal(root。ri ght)

訪問

D

節點的右孩子, 不存在

返回到

B

節點, 呼叫

preOrderTraversal(root。right)

, 訪問

B

節點的右孩子, 列印字母

E

當初我要是這麼學習二叉樹就好了

由於節點

E

沒有左右孩子, 返回列印節點

B

時的遞迴函式, 遞迴執行完畢。 返回到最初的

preOrderTraversal(root)

, 呼叫

preOrderTraversal(root。right)

, 訪問

A

節點的右孩子, 列印字母

C

當初我要是這麼學習二叉樹就好了

之後類似前面的遞迴呼叫, 一次繼續列印F, I, G, j

知道了遞迴的程式碼如何執行, 那我們再看看非遞迴實現前序遍歷

/* 非遞迴 1。先建立一個棧 2。根節點放進棧裡 3。迴圈取棧頂元素 4。出棧並訪問這個元素值域 5。把當前元素的右子樹入棧, 再左子樹入棧【因為先列印左子樹後列印右子樹所以對於棧而言應該先入右子樹再入左子樹】 */

private

static

void

preOrderTraversalNo

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

Stack

<

BinaryTreeNode

>

stack

=

new

Stack

<

>

stack

push

root

while

stack

empty

{

BinaryTreeNode

top

=

stack

pop

System

out

print

top

val

+

“ ”

if

top

right

!=

null

{

stack

push

top

right

}

if

top

left

!=

null

{

stack

push

top

left

}

}

}

}

A

B

D

H

K

E

C

F

I

G

J

演示

迭代前序遍歷

步驟

當初我要是這麼學習二叉樹就好了

非遞迴思路很巧妙的利用了棧的先進後出特性進行前序遍歷

3。1。3。2 中序遍歷演算法

左根右

OJ題目連結

遞迴中序遍歷

private

static

void

inOrderTraversal

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

preOrder

root

left

System

out

print

root

val

+

“ ”

preOrder

root

right

}

}

H

K

D

B

E

A

I

F

C

G

J

演示

遞迴中序遍歷

步驟

呼叫

inOrderTraversal(BinaryTreeNode root)

, root 的根節點

A

不為 null, 於是呼叫

inOrderTraversal(root。left)

, 訪問節點

B

B

不為空, 繼續呼叫

inOrderTraversal(root。left)

, 訪問節點

D

D

不為空, 繼續呼叫

inOrderTraversal(root。left)

, 訪問節點

H

H

不為空, 繼續呼叫

inOrderTraversal(root。left)

, 訪問節點

H的左孩子

為空, 返回。 列印當前節點

H

當初我要是這麼學習二叉樹就好了

然後呼叫

inOrderTraversal(root。right)

, 訪問

H

節點的右節點

K

。因為

K

無左孩子, 所以列印

K

當初我要是這麼學習二叉樹就好了

因為

K

沒有右節點, 所以返回。 列印

H

節點的函式執行完畢, 返回。 列印字母

D

當初我要是這麼學習二叉樹就好了

節點

D

沒有右孩子, 所以返回。 列印

B

當初我要是這麼學習二叉樹就好了

呼叫

inOrderTraversal(root。right)

, 訪問

B

節點的右節點

E

, 因為

E

沒有左孩子, 所以列印

E

當初我要是這麼學習二叉樹就好了

節點

E

無右孩子, 返滬。 列印

B

的函式執行完畢, 返回到了我們最初執行

inOrderTraversal(root)

的地方, 列印字母

A

當初我要是這麼學習二叉樹就好了

呼叫

inOrderTraversal(root。right)

, 訪問

A

節點的右孩子C,再遞迴訪問

C

的左孩子

F

F

的左孩子

I

, 因為

I

無右孩子, 所以列印

I

。 之後分別列印

F, C, G, J

迭代中序遍歷

/* 1。先建立一個棧 2。根節點入棧 3。迴圈取棧頂元素 4。左子樹 集體入棧後再入右子樹 5。最外層 while 迴圈判斷需要留意。因為 cur= cur。right 會導致 cur == nulll, 因此在加入一個 !stack。empty() 的判斷 */

private

static

void

inOrderTraversalNo

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

Stack

<

BinaryTreeNode

>

stack

=

new

Stack

<

>

BinaryTreeNode

cur

=

root

while

cur

!=

null

||

stack

empty

{

while

cur

!=

null

{

stack

push

cur

cur

=

cur

left

}

cur

=

stack

pop

System

out

print

cur

val

+

“ ”

cur

=

cur

right

}

}

}

H

K

D

B

E

A

I

F

C

G

J

演示

迭代中序遍歷

步驟

當初我要是這麼學習二叉樹就好了

3。1。3。2 後序遍歷演算法

左右根

OJ題目連結

遞迴後序遍歷

/* OJ: 透過 */

class

Solution

{

List

<

Integer

>

list

=

new

ArrayList

<

>

public

List

<

Integer

>

postorderTraversal

TreeNode

root

{

if

root

==

null

{

return

list

}

else

{

postorderTraversal

root

left

postorderTraversal

root

right

list

add

root

val

return

list

}

}

}

private

static

void

postOrderTraversal

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

postOrderTraversal

root

left

postOrderTraversal

root

right

System

out

print

root

val

+

“ ”

}

}

演示

遞迴後序遍歷

步驟

當初我要是這麼學習二叉樹就好了

後序遍歷先遞迴左子樹, 由根節點 A->B->D->H, 節點

H

無左孩子, 再檢視

H

的右孩子

K

, 因為節點

K

無左右孩子, 所以

列印

K

K

列印執行完之後返回到執行列印

H

的程式

System。out。print(root。val + “ ”);

, 列印

H

節點

後續的分析步驟和都是看

返回的節點有無左右子樹是否遍歷完了: 遍歷完了就輸出根節點, 否則就繼續遍歷

迭代後序遍歷

/* 和中序遍歷類似 1。外層迴圈依舊需要加上 !stack。empty()來帶動迴圈 2。被列印完的節點置為 null, 並且判斷該節點是否被列印過 3。判斷 超時 */

private

static

void

postOrderTraversalNo

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

Stack

<

BinaryTreeNode

>

stack

=

new

Stack

<

>

BinaryTreeNode

cur

=

root

BinaryTreeNode

prev

=

null

while

cur

!=

null

||

stack

empty

{

while

cur

!=

null

{

stack

push

cur

cur

=

cur

left

}

cur

=

stack

peek

if

cur

right

==

null

||

cur

right

==

prev

{

stack

pop

System

out

print

cur

val

+

“ ”

prev

=

cur

cur

=

null

}

else

{

cur

=

cur

right

}

}

}

}

K

H

D

E

B

I

F

J

G

C

A

演示

迭代後序遍歷

步驟

當初我要是這麼學習二叉樹就好了

3。1。3。2 層序遍歷演算法

上至下, 左至右

OJ題目連結

class

Solution

{

public

List

<

List

<

Integer

>

>

levelOrder

TreeNode

root

{

// 1。返回空盒子

List

<

List

<

Integer

>

>

list

=

new

ArrayList

<

>

if

root

==

null

{

return

list

}

else

{

// 2。佇列村粗

Queue

<

TreeNode

>

queue

=

new

LinkedList

<

>

queue

offer

root

while

queue

isEmpty

{

int

size

=

queue

size

List

<

Integer

>

tmp

=

new

ArrayList

<

>

while

size

——

>

0

{

TreeNode

top

=

queue

poll

tmp

add

top

val

if

top

left

!=

null

{

queue

offer

top

left

}

if

top

right

!=

null

{

queue

offer

top

right

}

}

list

add

tmp

}

return

list

}

}

}

A

B

C

D

E

F

G

H

I

J

K

演示

OJ練習題中的程式碼執行

步驟

當初我要是這麼學習二叉樹就好了

正常的層序輸出

/* 1。先建立一個佇列 2。把根節點放在佇列 3。迴圈取佇列元素全部 4。內部 list 新增 當前元素 4。再把當前元素的左子樹入佇列, 再入右子樹 5。大的 ret 新增每次新添元素後的 list */

private

static

void

levelOrder

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

Queue

<

BinaryTreeNode

>

queue

=

new

LinkedList

<

>

queue

offer

root

while

queue

isEmpty

{

BinaryTreeNode

top

=

queue

poll

System

out

print

top

val

+

“ ”

if

top

left

!=

null

{

queue

offer

top

left

}

if

top

right

!=

null

{

queue

offer

top

right

}

}

}

}

A

B

C

D

E

F

G

H

I

J

K

3。2 二叉樹建立問題

3。2。1 前序中序構建二叉樹

OJ題目連結

class

Solution

{

private

int

findInorderIndex

int

inorder

int

inbegin

int

inend

int

key

{

for

int

i

=

inbegin

i

<=

inend

++

i

{

if

inorder

i

==

key

{

return

i

}

}

return

-

1

}

private

TreeNode

buildTreeChild

int

preorder

int

inorder

int

inbegin

int

inend

{

if

inbegin

>

inend

{

return

null

}

else

{

TreeNode

root

=

new

TreeNode

preorder

preIndex

++

int

rootIndex

=

findInorderIndex

inorder

inbegin

inend

root

val

root

left

=

buildTreeChild

preorder

inorder

inbegin

rootIndex

-

1

root

right

=

buildTreeChild

preorder

inorder

rootIndex

+

1

inend

return

root

}

}

private

int

preIndex

=

0

public

TreeNode

buildTree

int

preorder

int

inorder

{

if

preorder

==

null

||

inorder

==

null

{

return

null

}

else

{

return

buildTreeChild

preorder

inorder

0

inorder

length

-

1

}

}

}

其時建立二叉樹也是利用了遞迴的原理。 只不過在當初列印節點的程式碼換成了生成節點, 給節點賦值的操作而已。

前序遍歷的第一個節點一定是

根節點

, 所以我們在中序遍歷中劃分根節點的左右子樹, 然後遞迴找根節點再劃分左右子樹, 遞迴的終止條件是

區間的左一定要大於區間的右

程式碼思路:

判斷前序或中序是否有一個為

null

【特殊情況處理】

利用

buildTreeChild

函式返回根節點

先構造根節點, 再遞迴構建左子樹和右子樹

所有遞迴完之後返回最後的

root

就是樹的根節點

演示圖

第一查詢的是

preorder[preIndex]

的根節點【preIndex值為0】

當初我要是這麼學習二叉樹就好了

由於第二次劃分的時候, 新的

inend

rootIndex-1

所以導致的

if(inbegin>inend) return;

程式返回執行

root。right = buildTreeChild(preorder, inorder, rootIndex+1, inend);

當初我要是這麼學習二叉樹就好了

第三次劃分

當初我要是這麼學習二叉樹就好了

綜合

當初我要是這麼學習二叉樹就好了

3。2。2 中序後序構建二叉樹

OJ題目連結

class

Solution

{

private

int

postIndex

=

0

private

int

findInorderIndex

int

inorder

int

inbegin

int

inend

int

key

{

for

int

i

=

inbegin

i

<=

inend

++

i

{

if

inorder

i

==

key

{

return

i

}

}

return

-

1

}

private

TreeNode

buildTreeChild

int

inorder

int

postorder

int

inbegin

int

inend

{

if

inbegin

>

inend

{

return

null

}

else

{

TreeNode

root

=

new

TreeNode

postorder

postIndex

——

int

rootIndex

=

findInorderIndex

inorder

inbegin

inend

root

val

root

right

=

buildTreeChild

inorder

postorder

rootIndex

+

1

inend

root

left

=

buildTreeChild

inorder

postorder

inbegin

rootIndex

-

1

return

root

}

}

public

TreeNode

buildTree

int

inorder

int

postorder

{

if

inorder

==

null

||

postorder

==

null

{

return

null

}

else

{

postIndex

=

postorder

length

-

1

return

buildTreeChild

inorder

postorder

0

postIndex

}

}

}

後序遍歷和前序變一樣想法一樣, 但是應該注意順序【後序遍歷最後一是根, 而前序遍歷第一個就是根】

演示圖

因為從後往前看的話, 根節點過來就是右節點, 所以先構建右子樹在構建左子樹。 遍歷遞迴思路和上述一樣, 這裡就不多述

當初我要是這麼學習二叉樹就好了

總結出一個二叉樹的性質

已知前序遍歷和中序遍歷, 可以確定唯一的一顆二叉樹

已知中序遍歷和後序遍歷, 可以確定唯一的一顆二叉樹

思考: 已知前序遍歷和後序遍歷能否構建唯一的一顆二叉樹

不能, 原因很簡單。 比如一棵二叉樹前序遍歷是

ABC

後序遍歷是

CBA

我們可以確定根節點是

A

但不能確定

A

的左右子樹。 這棵樹就有如下的四種可能

當初我要是這麼學習二叉樹就好了

3。2。3 根據二叉樹建立字串

OJ題目連結

解法1

先處理左子樹, 再處理右子樹

按照題目要求進行一個一個新增括號

class

Solution

{

private

void

tree2strChild

TreeNode

root

StringBuffer

stringBuffer

{

if

root

==

null

{

return

}

else

{

stringBuffer

append

root

val

// 1。處理左子樹

if

root

left

==

null

{

if

root

right

==

null

{

return

}

else

{

stringBuffer

append

“()”

}

}

else

{

stringBuffer

append

“(”

tree2strChild

root

left

stringBuffer

stringBuffer

append

“)”

}

// 2。處理右子樹

if

root

right

==

null

{

return

}

else

{

stringBuffer

append

“(”

tree2strChild

root

right

stringBuffer

stringBuffer

append

“)”

}

}

}

public

String

tree2str

TreeNode

root

{

if

root

==

null

{

return

“”

}

else

{

StringBuffer

stringBuffer

=

new

StringBuffer

tree2strChild

root

stringBuffer

return

stringBuffer

toString

}

}

}

解法2

程式碼少, 容易理解, 但執行效率低

遞迴前序遍歷的形式新增括號

新增完畢之後再刪除第一個和最後一個括號即可

class

Solution

{

private

StringBuffer

stringBuffer

=

null

private

void

tree2strChild

TreeNode

root

{

if

root

==

null

{

return

}

else

{

stringBuffer

append

“(”

+

root

val

tree2strChild

root

left

if

root

left

==

null

&&

root

right

!=

null

{

stringBuffer

append

“()”

}

tree2strChild

root

right

stringBuffer

append

“)”

}

}

public

String

tree2str

TreeNode

root

{

if

root

==

null

{

return

“”

}

else

{

stringBuffer

=

new

StringBuffer

tree2strChild

root

stringBuffer

deleteCharAt

0

stringBuffer

deleteCharAt

stringBuffer

length

-

1

return

stringBuffer

toString

}

}

}

3。2。4 前序遍歷字串構建二叉樹

OJ題目連結

import

java

util

Scanner

import

java

util

Stack

class

TreeNode

{

char

val

TreeNode

left

TreeNode

right

TreeNode

char

val

{

this

val

=

val

}

}

public

class

Main

{

// 1。採用的是迭代的方式進行中序遍歷

private

static

void

inOrderTraversal

TreeNode

root

{

if

root

==

null

{

return

}

else

{

Stack

<

TreeNode

>

stack

=

new

Stack

<

>

TreeNode

cur

=

root

while

cur

!=

null

||

stack

empty

{

// 2。 左子樹集體入棧

while

cur

!=

null

{

stack

push

cur

cur

=

cur

left

}

cur

=

stack

pop

System

out

print

cur

val

+

“ ”

cur

=

cur

right

}

}

}

private

static

int

index

=

0

// 記錄列印的字串下標, 因為是遞迴, 所以應該使用全域性變數而不是區域性變數

private

static

TreeNode

createTree

String

str

{

if

str

==

null

{

return

null

}

else

{

TreeNode

root

=

null

// 1。越過空節點

if

str

charAt

index

==

‘#’

{

++

index

}

else

{

// 2。 按照前序根左右的形式遞迴構建二叉樹

root

=

new

TreeNode

str

charAt

index

++

root

left

=

createTree

str

root

right

=

createTree

str

}

return

root

}

}

public

static

void

main

String

args

{

Scanner

scanner

=

new

Scanner

System

in

while

scanner

hasNext

{

String

str

=

scanner

next

TreeNode

root

=

createTree

str

inOrderTraversal

root

System

out

println

}

}

}

3。2。5 合併二叉樹

OJ題目練習

class

Solution

{

public

TreeNode

mergeTrees

TreeNode

root1

TreeNode

root2

{

// 1。如果某個節點為null, 則返回另一個節點

if

root1

==

null

{

return

root2

}

else

if

root2

==

null

{

return

root1

}

else

{

// 2。如果兩個節點都不為null, 則開始合併二叉樹

TreeNode

root

=

new

TreeNode

root1

val

+

root2

val

root

left

=

mergeTrees

root1

left

root2

left

root

right

=

mergeTrees

root1

right

root2

right

return

root

}

}

}

3。2。6 遞增二叉樹

OJ練習題目

class

Solution

{

private

TreeNode

cur

=

null

private

void

inOrderTraversal

TreeNode

root

{

if

root

==

null

{

return

}

else

{

//1。左

inOrderTraversal

root

left

// 2。根

cur

right

=

root

root

left

=

// 3。右

inOrderTraversal

root

right

}

}

public

TreeNode

increasingBST

TreeNode

root

{

if

root

==

null

{

return

null

}

else

{

// 1。傀儡節點用來儲存cur節點的位置用於返回

TreeNode

newRoot

=

new

TreeNode

-

1

// 2。移動節點

cur

=

newRoot

inOrderTraversal

root

return

newRoot

right

}

}

}

3。3 經典題目解析

3。3。1 結點個數

/* 求結點個數 遍歷思路: 遞迴每次遍歷就自增 1[線上 OJ 可能會翻車: 先拿一個小樹呼叫 gerSize1 方法, 然後再拿一個大的樹來呼叫 getSize1] 子問題思路: return 1+func()。。。自帶返回值 */

private

static

int

size

=

0

private

static

void

getSize1

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

++

size

getSize1

root

left

getSize1

root

right

}

}

private

static

int

getSize2

BinaryTreeNode

root

{

if

root

==

null

{

return

0

}

else

{

return

1

+

getSize2

root

left

+

getSize2

root

right

}

}

3。3。2 葉子結點個數

/* 求葉子結點個數 遍歷思路: 遞迴遍歷 子問題思路: return 1+func()。。。 */

private

static

int

leaftSize

=

0

private

static

void

getLeafSize1

BinaryTreeNode

root

{

if

root

==

null

{

return

}

else

{

if

root

left

==

null

&&

root

right

==

null

{

++

leaftSize

}

else

{

getLeafSize1

root

left

getLeafSize1

root

right

}

}

}

private

static

int

getLeafSize2

BinaryTreeNode

root

{

if

root

==

null

{

return

0

}

else

{

if

root

left

==

null

&&

root

right

==

null

{

return

1

}

else

{

return

getLeafSize2

root

left

+

getLeafSize2

root

right

}

}

}

3。3。3 第k層結點個數

private

static

int

getKLevelSize

BinaryTreeNode

root

int

k

{

if

root

==

null

||

k

<

1

{

return

0

}

else

{

if

k

==

1

{

return

1

}

else

{

return

getKLevelSize

root

left

k

-

1

+

getKLevelSize

root

right

k

-

1

}

}

}

3。3。4 查詢 val 所在結點

private

static

BinaryTreeNode

find

BinaryTreeNode

root

String

val

{

if

root

==

null

{

return

null

}

else

{

if

root

val

equals

val

{

return

root

}

else

{

BinaryTreeNode

ret

=

find

root

left

val

if

ret

!=

null

&&

ret

val

equals

val

{

return

ret

}

ret

=

find

root

right

val

if

ret

!=

null

&&

ret

val

equals

val

{

return

ret

}

return

null

}

}

}

3。3。5 相同的樹

OJ題目練習

當初我要是這麼學習二叉樹就好了

class

Solution

{

public

boolean

isSameTree

TreeNode

p

TreeNode

q

{

// 1。都為null: 說明之前的節點和節點值相等, 所以相等; 或者兩個空節點也是相等的

if

p

==

null

&&

q

==

null

{

return

true

// 2。某個節點遍歷完了但另一個節點不為null, 所以是false

}

else

if

p

==

null

||

q

==

null

{

return

false

}

else

{

// 3節點值不相等就 false

if

p

val

!=

q

val

{

return

false

}

else

{

// 4。 遞迴進行下一次比較

return

isSameTree

p

left

q

left

&&

isSameTree

p

right

q

right

}

}

}

3。3。6 另一棵樹子樹

OJ題目練習

當初我要是這麼學習二叉樹就好了

class

Solution

{

public

boolean

isSameTree

TreeNode

p

TreeNode

q

{

// 1。都為null: 說明之前的節點和節點值相等, 所以相等; 或者兩個空節點也是相等的

if

p

==

null

&&

q

==

null

{

return

true

// 2。某個節點遍歷完了但另一個節點不為null, 所以是false

}

else

if

p

==

null

||

q

==

null

{

return

false

}

else

{

// 3節點值不相等就 false

if

p

val

!=

q

val

{

return

false

}

else

{

// 4。 遞迴進行下一次比較

return

isSameTree

p

left

q

left

&&

isSameTree

p

right

q

right

}

}

}

public

boolean

isSubtree

TreeNode

root

TreeNode

subRoot

{

// 1。兩個 nullnull 則是子樹; 遍歷到了葉子結點的子樹【雖然不存在】, 說明之前遍歷的都是相等的

if

root

==

null

&&

subRoot

==

null

{

return

true

}

else

if

root

==

null

&&

subRoot

!=

null

||

root

!=

null

&&

subRoot

==

null

{

return

false

}

else

{

// 1。如果兩個節點不為空, 且相等, 則是子樹

if

isSameTree

root

subRoot

{

return

true

// 2。遞迴判斷是否為子樹

}

else

if

isSubtree

root

left

subRoot

{

return

true

}

else

if

isSubtree

root

right

subRoot

{

return

true

}

else

{

return

false

}

}

}

}

3。3。7 二叉樹最大深度

OJ練習題目

當初我要是這麼學習二叉樹就好了

class

Solution

{

public

int

maxDepth

TreeNode

root

{

if

root

==

null

{

return

0

}

else

{

// 1。計算左子樹的高度

int

left

=

maxDepth

root

left

+

1

// 2。計算右子樹的高度

int

right

=

maxDepth

root

right

+

1

if

left

>

right

{

return

left

}

else

{

return

right

}

}

}

}

3。3。8 平衡二叉樹

OJ練習題目

當初我要是這麼學習二叉樹就好了

class

Solution

{

private

int

maxDepth

TreeNode

root

{

if

root

==

null

{

return

0

}

else

{

int

left

=

1

+

maxDepth

root

left

int

right

=

1

+

maxDepth

root

right

return

left

>

right

left

right

}

}

public

boolean

isBalanced

TreeNode

root

{

if

root

==

null

{

return

true

}

else

{

int

left

=

maxDepth

root

left

int

right

=

maxDepth

root

right

if

Math

abs

left

-

right

>

1

{

return

false

}

else

{

return

isBalanced

root

left

&&

isBalanced

root

right

}

}

}

}

// 1。 最佳化後的程式碼: 邊求高度邊計算左右子樹高度差, 不用遞迴兩次

class

Solution

{

private

int

maxDepth

TreeNode

root

{

if

root

==

null

{

return

0

}

else

{

int

left

=

maxDepth

root

left

int

right

=

maxDepth

root

right

if

left

>=

0

&&

right

>=

0

&&

Math

abs

left

-

right

<=

1

{

return

Math

max

left

right

+

1

}

else

{

return

-

1

}

}

}

public

boolean

isBalanced

TreeNode

root

{

return

maxDepth

root

>

-

1

}

}

3。3。9 對稱二叉樹

OJ練習題目

當初我要是這麼學習二叉樹就好了

class

Solution

{

private

boolean

isSameTree

TreeNode

leftTree

TreeNode

rightTree

{

if

leftTree

==

null

&&

rightTree

==

null

{

return

true

}

else

if

leftTree

!=

null

&&

rightTree

==

null

||

leftTree

==

null

&&

rightTree

!=

null

{

return

false

}

else

{

if

leftTree

val

!=

rightTree

val

{

return

false

}

else

{

/* 1。 之前的判斷步驟和 比較二叉樹是否相同 有點類似 2。 之後應該比較 左樹的左 和 右樹的右; 左樹的右 和 右樹的左 是否相同 */

return

isSameTree

leftTree

left

rightTree

right

&&

isSameTree

leftTree

right

rightTree

left

}

}

}

public

boolean

isSymmetric

TreeNode

root

{

if

root

==

null

{

return

true

}

else

{

return

isSameTree

root

left

root

right

}

}

}

3。3。10 二叉樹的完全性檢驗

OJ題目練習

當初我要是這麼學習二叉樹就好了

利用佇列特性進行全部入佇列(包含 null 節點)再進行出佇列判斷

/* 1。 當遇到 null 節點時候說明已經遍歷所有完全二叉樹就退出 2。 在佇列末尾慢慢彈出元素, 如果彈出了不為 null 則說明不是一個完全二叉樹 */

class

Solution

{

public

boolean

isCompleteTree

TreeNode

root

{

if

root

==

null

{

return

true

}

else

{

Queue

<

TreeNode

>

queue

=

new

LinkedList

<

>

queue

offer

root

// 1。直到遇到 null 節點就停止入佇列

while

queue

isEmpty

{

TreeNode

top

=

queue

poll

if

top

!=

null

{

queue

offer

top

left

// 程式走到葉子結點, 則會把 null 節點也帶入佇列, 後續的 peekpeek 探測的時候如果遇到了不為 null 的節點的話就代表在遇到 null 節點之後還有其它葉子結點, 此時一定不是完全二叉樹

queue

offer

top

right

}

else

{

break

}

}

//程式執行到這兒, 說明佇列中已經儲存所有的節點

// 2。節點出佇列, peek探測元素是否為 null

while

queue

isEmpty

{

TreeNode

top

=

queue

peek

if

top

!=

null

{

return

false

}

else

{

queue

poll

//出佇列

}

}

return

true

}

}

}

最佳化: 簡單高效的透過對比節點數和下標數判斷

class

Solution

{

private

int

n

=

0

p

=

0

private

boolean

dfs

TreeNode

root

int

k

{

if

root

==

null

{

return

true

// 1。節點遍歷完畢

}

else

if

k

>

100

{

return

false

// 2。題目提示: 樹中將會有 1 到 100 個結點

}

else

{

++

n

// 3。每遍歷一次, 結點個數就自增一, 記錄當前節點下標

p

=

Math

max

p

k

// 4。因為有左右子樹, 所以每次應該選取最大下標才可

return

dfs

root

left

2

*

k

&&

dfs

root

right

2

*

k

+

1

}

}

public

boolean

isCompleteTree

TreeNode

root

{

if

root

==

null

{

return

true

}

else

{

if

dfs

root

1

{

return

false

// 1。 超過 100 節點就返回 false

}

else

{

return

n

==

p

// 2。未超過 100 節點, 判斷節點數和最大下標數是否相等

}

}

}

}

3。3。11 二叉樹最近公公祖先

OJ題目練習

當初我要是這麼學習二叉樹就好了

/* left == null, right == null: return null left != null, right != null: return root left == null, right != null: return right left != null, right == null: return left */

class

Solution

{

public

TreeNode

lowestCommonAncestor

TreeNode

root

TreeNode

p

TreeNode

q

{

if

root

==

null

||

root

==

p

||

root

==

q

{

return

root

}

else

{

TreeNode

left

=

lowestCommonAncestor

root

left

p

q

TreeNode

right

=

lowestCommonAncestor

root

right

p

q

if

left

==

null

&&

right

==

null

{

return

null

}

else

if

left

!=

null

&&

right

==

null

{

return

left

}

else

if

left

==

null

&&

right

!=

null

{

return

right

}

else

{

return

root

}

}

}

}

3。3。12 二叉樹最大寬度

OJ題目練習

當初我要是這麼學習二叉樹就好了

class

Solution

{

public

int

widthOfBinaryTree

TreeNode

root

{

if

root

==

null

{

return

0

}

else

{

LinkedList

<

TreeNode

>

queue

=

new

LinkedList

<

>

queue

offer

root

int

max_width

=

0

while

queue

isEmpty

{

int

cur_width

=

queue

getLast

val

-

queue

getFirst

val

+

1

// 1。利用結點的值域記錄當前佇列寬度

// 2。頭節點的左右子樹入佇列, 為下一次寬度計算做準備

int

count

=

queue

size

// 3。佇列大小時刻變化著的, 我們應該保留入佇列之前的佇列大小

for

int

i

=

0

i

<

count

++

i

{

TreeNode

top

=

queue

poll

// 4。出佇列元素且保留, 為後續左右子樹入佇列做鋪墊

if

top

left

!=

null

{

queue

offer

top

left

top

left

val

=

top

val

<<

1

// 5。依據的是二叉樹的性質5——左孩子:2*i 根:i 右孩子: 2*i+1

}

if

top

right

!=

null

{

queue

offer

top

right

top

right

val

=

top

val

<<

1

+

1

}

}

if

max_width

<

cur_width

{

max_width

=

cur_width

}

}

return

max_width

}

}

}

其它語言要考慮下方提示: 整形溢位問題

3。4。 特殊的二叉樹問題

3。4。1 搜尋二叉樹

static

class

BinaryTreeNode

{

// 鍵值對的形式儲存

int

key

int

value

BinaryTreeNode

left

BinaryTreeNode

right

public

BinaryTreeNode

int

key

int

value

{

this

key

=

key

this

value

=

value

}

}

// root: 表示樹的根節點, 空樹就是用 null 來表示

private

BinaryTreeNode

root

=

null

//查詢: 根據 Key 找 Value

private

Integer

search

int

key

{

BinaryTreeNode

cur

=

root

while

cur

!=

null

{

if

key

<

cur

key

{

cur

=

cur

left

}

else

if

key

>

cur

key

{

cur

=

cur

right

}

else

{

return

key

}

}

return

null

}

// 插入

private

void

insert

int

key

int

value

{

if

root

==

null

{

root

=

new

BinaryTreeNode

key

value

}

else

{

BinaryTreeNode

cur

=

root

BinaryTreeNode

parend

=

null

while

cur

!=

null

{

parend

=

cur

if

key

<

cur

key

{

cur

=

cur

left

}

else

if

key

>

cur

key

{

cur

=

cur

right

}

else

{

return

// 二叉搜尋樹不存在相等, 如果相等也沒必要賦值

}

}

// 程式執行到這兒, 說明 parent 已經是 cur 的父節點, 修改父節點即可

if

key

<

parend

key

{

parend

left

=

new

BinaryTreeNode

key

value

}

else

{

parend

right

=

new

BinaryTreeNode

key

value

}

}

}

// 刪除: 按照 key 刪除

private

void

remove

int

key

{

// 1。先查詢 key 對應的位置

BinaryTreeNode

cur

=

root

BinaryTreeNode

parent

=

null

while

cur

!=

null

{

parent

=

cur

if

key

<

cur

key

{

cur

=

cur

left

}

else

if

key

>

cur

key

{

cur

=

cur

right

}

else

{

/* 2。核心刪除 考慮當前 cur。left 為空 是否為根節點 root 是: 連結右子樹 否: 接著判斷當前刪除的子節點 cur 是其父節點 parent 節點的左右子樹關係 考慮當前 cur。right 為空 緊接 cur。left 根節點判斷一樣的思路 考慮當前 cur。right 和 cur。left 都不為空 移形換影刪除 */

if

cur

left

==

null

{

if

cur

==

root

{

root

=

cur

right

}

else

if

cur

==

parent

left

{

parent

left

=

cur

right

}

else

if

cur

==

parent

right

{

parent

right

=

cur

right

}

}

else

if

cur

right

==

null

{

if

cur

==

root

{

root

=

cur

left

}

else

if

cur

==

parent

left

{

parent

left

=

cur

left

}

else

if

cur

==

parent

right

{

parent

right

=

cur

left

}

}

else

{

// 具體移形換影步驟

// 1。右子樹找最小值, 並記錄其父節點

BinaryTreeNode

targetParent

=

cur

BinaryTreeNode

target

=

cur

right

while

target

left

!=

null

{

// 最小值特點是: 左子樹為空

targetParent

=

target

target

=

target

left

}

// 2。程式執行到這兒: targetParent 是 target 父節點; target 是右子樹中最小值

// 3。就把 替罪羊target 節點的值賦值給刪除節點

cur

key

=

target

key

cur

value

=

target

value

// 4。斷開末尾 最小值節點 的關係, 建立新的關係

if

target

==

targetParent

left

{

targetParent

left

=

target

right

// 最小值左節點為 null

}

else

if

target

==

targetParent

right

{

targetParent

right

=

target

right

}

}

}

}

}

3。4。2 赫夫曼樹及其應用

3。4。1。 赫夫曼樹

什麼是

哈夫曼樹

哈夫曼樹的目的是為了字串的壓縮, 我們經常用的 zip, tar, ara…等等壓縮都是基於 哈夫曼樹的原理

當初我要是這麼學習二叉樹就好了

舉箇中小學通用的評分標準

if

a

<

60

{

b

=

“不及格”

}

else

if

a

<

70

{

b

=

“及格”

}

else

if

a

<

80

{

b

=

“中等”

}

else

if

a

<

90

{

b

=

“良好”

}

else

{

b

=

“優秀”

}

粗看沒什麼問題, 結果是對的。 一張好的試卷應該讓學生成績大部分處於

中等

良好

範圍,

優秀

不及格

應該很少才對。 而上面的程式, 就需要讓所有的成績判斷是否及格, 再逐級而上得到結果。 資料量大的時候, 演算法效率很低下

當初我要是這麼學習二叉樹就好了

實際生活中學生成績分佈規律如下表

當初我要是這麼學習二叉樹就好了

那麼

70

分以上大約佔總數80%的成績都需要經過

3

次以上的判斷才可以得到結果, 這顯然不合理。

仔細觀察發現: 中等成績(70~79)比例最高, 其次是良好成績, 不及格所佔比例最少。 對上述的二叉樹重新分配如下(有點像搜尋二叉樹, 其實並不是的, 仔細往後面的文章看):

當初我要是這麼學習二叉樹就好了

3。4。2。赫夫曼樹定義與原理

把兩顆二叉樹簡化成葉子結點帶權的二叉樹【A: 不及格, B: ,及格C: 中等,D:良好, E:優秀】

當初我要是這麼學習二叉樹就好了

從樹中一個節點到另一個節點之間的分支結構狗曾兩個雞誒但之間的路徑, 路徑上的分支樹木稱作路徑長度

二叉樹a中根節點到D節點路徑長度就是4, 二叉樹b中根節點到D節點長度就是2

二叉樹a總長度: 1+1+2+2+3+3+4+4=20

二叉樹b總長度: 1+1+2+2+2+2+3+3=16

如果考慮帶權的節點

: 該節點到樹根之間的路徑長度與節點上權的乘積。 樹的帶權路徑長度作為作為樹中所有葉子結點的帶權路徑長度的之和, 用

WPL

來表示。 其中最小的

WPL

的樹稱為

赫夫曼樹

二叉樹a WPL: 5 * 1 + 15 * 2 + 40 * 2 + 30 * 4 + 10 * 4 = = 315

二叉樹b WPL: 5 * 3 + 15 * 3 + 40 * 2 + 30 * 2 + 10 * 2 = 220

這樣的結果意味著如果有 1_0000 個學生的百分制資料計算五級分製成績, 二叉樹a需要計算31500次比較, 二叉樹b需要2200次比較. 差不多少了1/3的量, 效能上提高不是一點點

該如何構建赫夫曼樹呢?

先把有權值的葉子結點按照從小到大的順序排列成一個有序序列[A5, E10, B15, D30, C40]

取頭兩個最小全值的節點作為一個新節點 N1 的兩個位元組點, A 是 N1 左子樹, E 是 N1 右子樹。 注意左右子樹大小關係

當初我要是這麼學習二叉樹就好了

將 N1 替換 A 與 E, 插入有序序列中, 保持從小到大排列[N1 15, B15, D30, C40]

重複步驟2。 將 N1 與 B 作為一個新節點 N2 的兩個位元組點

當初我要是這麼學習二叉樹就好了

重複步驟3的序列替換序列最小值, 再重複步驟2生成新的節點…

當初我要是這麼學習二叉樹就好了

最終效果:

當初我要是這麼學習二叉樹就好了

WPL=40 * 1 + 30 * 2 + 15 * 3 + 10 * 4 + 5 * 4 = 205, 比搜尋二叉樹還少了15, 因此這才是最優的赫夫曼樹

3。4。3 赫夫曼編碼

赫夫曼樹更大作用是為解決當年遠距離通訊(主要是電報)的資料傳輸的最最佳化問題。

比如有一段文字內容為 “BADCADFEED” 要網路傳輸給別人, 顯然用二進位制的資料(0 和 1)來表示是很自然的想法, 這段文字有 6 個字母ABCDEF。

相應的二進位制資料表示就是:

當初我要是這麼學習二叉樹就好了

這樣真正傳輸的就是編碼後的 “001000011010000011101100100011”, 對方接受可以按照

3

位一分來譯碼。 如果文章內容很長, 這樣的二進位制串也很可怕。

此時就應該使用

赫夫曼樹

假設6個字母的頻率: A:27, B:8, C:15, D:15, E:30, F:5

合起來正好是100%。

左圖構造赫夫曼樹的過程權值顯示; 右圖是將權值左子樹改為0, 右子樹分制改為1後的赫夫曼樹

當初我要是這麼學習二叉樹就好了

此時我們對 6 個字母重新編碼

當初我要是這麼學習二叉樹就好了

原編碼: 001000011010000011101100100011[30個字元]

新編碼: 1001010010101001000111100[25個字元]

少了5個字元, 也就是大約 17%[5/30=0。167] 的成本。

字元愈多, 這種壓縮效果更明顯

問題來了: 收到了赫夫曼編碼又該如何解析呢?

編碼中非0即1, 長短不等的話其實很容易混淆的, 所以要設計長短不等的編碼: 任一字元的編碼都不是另一個字元編碼的字首, 這種比那嗎稱為字首編碼[01, 1001, 101, 00, 11, 1000都是不同長短長度, 因此不容易混淆]

這樣並不足以, 我們去方便的解碼, 因此在解碼的時候接收方和傳送方都必須要約定好相同的

赫夫曼編碼規則

當收到

1001010010101001000111100

時, 約定好的

赫夫曼編碼規則

可知 1001 得到第一個字母是

B

, 接下來的誒

01

意味著第二個字母

A

當初我要是這麼學習二叉樹就好了

一般地,設需要編碼的字符集為{ d1,d2, …, dn },各個字元在電文中出現的次數或頻率集合為{ w1,w2, …, wn}, 以d1,d2,…, dn作為葉子結點,以w1,w2, …, wn作為相應葉子結點的權值來構造一棵赫夫曼樹。

規定赫夫曼樹的左分支代表0,右分支代表1,則從根結點到葉子結點所經過的路徑分支組成的0和1的序列便為該結點對應字元的編碼,這就是赫夫曼編碼。

3。4。3 線索二叉樹

當初我要是這麼學習二叉樹就好了

^

: 表示空指標, 圖中二叉樹有許多空指標, 發現這些空間都沒有利用。 這其實不是一個好現象, 應該合理利用起來。

首先那我們來看看有多少個多少個空指標呢?

對於一個有

n

個節點的二叉樹有

2n

個指標, 而

n

個節點的二叉樹一共有

n-1

條分支線數, 也就是說存在

2n-(n-1)=n+1

個空指標域。 對應到題目中就有

11

個空指標的浪費。

另一方面, 我們中序遍歷二叉樹得出

HDIBJEAFCG

, 這樣的字元序列, 我們知道

J

的前驅是

B

後繼是

E

, 也就是說從

中序遍歷二叉樹我們可以知道任意一個節點的前驅和後繼

問題: 上邊找到的規律是建立在已經遍歷過的基礎上, 在二叉連結串列上我們只知道每個左右孩子的地址, 而不知道後繼是誰, 後繼是誰要想知道必須要再次遍歷一次

解決方案

我們在遍歷的時候就建立好前驅後繼。

我們把這種前驅和後繼的指標稱為線索, 加上線索的二叉連結串列稱為線索連結串列, 相應的二叉樹稱為

線索二叉樹(Threaded Binary Tree)

我們把這棵二叉樹進行中序遍歷, 將所有空指標的

rchild

, 改為指向它的後續結點。 於是就可以透過指標知道

H

的後繼是

D

(1),

I

的後繼是

B

(2),

J

的後繼是

E

(3)(E。next = A(4), F。next = C(5), G。next = null(6))。 此時共有

6

個空指標被利用。

當初我要是這麼學習二叉樹就好了

再看圖, 把這棵二叉樹中的所有空指標域中的

lchild

, 改為指向當前結點的前驅。 因此

H

的前驅是

NULL

(1),

I

的前驅是

D

(2)[J。prev = B(3), F。prev = A(4), G。prev = C(5)]。一共有

5

個空指標被利用, 加上上面的

右子樹

利用後一共有

11

個空指標被利用

當初我要是這麼學習二叉樹就好了

實線空心箭頭為前驅 prev, 虛線實心箭頭為後繼 next

就更容易看出, 線索二叉樹, 等於是把一棵二叉樹變成了一個雙向連結串列, 這樣對於我們的插入刪除節點, 查詢某個節點都帶來了方便。 所以我們對

二叉樹以某種次序遍歷使其變為線索二叉樹的過程為

線索化

當初我要是這麼學習二叉樹就好了

二叉搜尋樹與雙向連結串列

OJ題目練習

當初我要是這麼學習二叉樹就好了

Java: 構建程式碼

/** public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int val) { this。val = val; } } */

public

class

Solution

{

private

TreeNode

prev

=

null

// 1。 用來記錄前驅節點

private

void

ConvertChild

TreeNode

root

{

if

root

==

null

{

return

}

else

{

// 2。根絕二叉搜尋樹性質: 中序遍歷就是有序結果

ConvertChild

root

left

/* 3。建立前後指向 節點的左子樹是前驅, 右子樹是後繼 對於最左端葉子結點 4 這種沒有左右子樹的結點要特殊考慮: 保留的前驅節點的右子樹指向當前函式呼叫的節點【4。right = 6】, 但對於這種情況還需要判斷它是否為 null, 否則就會 空指標異常 4。left = null null。right = 4;空指標異常 null = 4; 加上 if 判斷後: 4。left = null; null = 4; 程式 return 返回到 6節點函式 6。left = 4; 4。right = 6; 4 = 6; */

root

left

=

prev

// 第一次遞迴結束返回到 root 是4這個節點

if

prev

!=

null

{

prev

right

=

root

}

prev

=

root

ConvertChild

root

right

}

}

public

TreeNode

Convert

TreeNode

pRootOfTree

{

if

pRootOfTree

==

null

{

return

null

}

else

{

ConvertChild

pRootOfTree

while

pRootOfTree

left

!=

null

{

pRootOfTree

=

pRootOfTree

left

}

return

pRootOfTree

}

}

}

Java 語言在定義節點類的時候只需要:

資料域val, 左子樹left, 右子樹right

, 在實現雙向連結串列的時候需要額外一個

prev節點

來存放當前節點的前驅【左子樹】

當初我要是這麼學習二叉樹就好了

C: 線索二叉樹結構體思路

lchild:

左子樹,

rchild:

右子樹

對於C語言而言, 我們如何知道某一節點的

lchild

是指向它的左孩子還是指向前驅呢?

rchild

是指向右孩子還是後繼呢?

比如:

E

節點的

lchild

是指向它的左子樹

J

, 而

rchild

卻是指向它的後繼

A

。 顯然我們在決定

lchild

是指向左孩子還是前驅,

rchild

是指向右孩子還是後繼上是需要一個區分標誌的。 因此我們給每個節點在增設兩個標誌域

ltag

rtag

注意

ltag

rtag

只是存放

0

1

的布林型變數, 其佔用的記憶體空間要小於像

lchild 和 rchild 的指標變數

當初我要是這麼學習二叉樹就好了

ltag == 0時指向該節點的左孩子, 為1時指向該節點的後繼

rtag == 0時指向該節點的右孩子, 為1時指向該節點的後繼

當初我要是這麼學習二叉樹就好了

typedef

int

DataType

// 資料型別

typedef

enum

{

Link

//Link == 0表示只想做還有子樹指標

Thread

//Thread == 1表示只想前驅或後繼的線索

}

PointerTag

typedef

struct

BiThrNode

{

// 二叉樹儲存節點結構

DataType data

// 資料域

struct

BiThrNode

*

lchild

*

rchild

// 左右子樹

PointerTag LTag

// 左標誌

PointerTag RTag

// 右標誌

}

BiThrNode

*

BiThrTree

當初我要是這麼學習二叉樹就好了

那麼瞭解了C語言的結構體構造線索二叉樹原理後我們該如何實現呢?

參考前邊 Java程式碼的實現, 我們發現可以規律無非是

左根右

的方式處理資料。 無非是中間那部分

的資料如何處理

BiThrNode

*

prev

// 全域性變數, 始終指向剛剛訪問過的節點

void

InThreading

BiThrNode

*

pb

{

if

pb

{

/* * 中序遍歷線索化二叉樹遞迴函式 */

// 1。 左

InThreading

pb

->

lchild

/* * 2。中間部分建立聯絡 * 因為中序遍歷之後第一個元素就是 連結串列的頭節點, 所以應該選取左節點為空的節點就是 連結串列頭節點 * * 前驅分析: * if(!pb->lchild): 表示如果某結點的左子樹域為空, 因為其前驅節點剛剛訪問過 * 賦值給了 prev, 所以可以把 prev 賦值給 pb->lchild, 並修改 pb->Ltag=Thread。完成了前驅節點的線索化 * 後繼分析: * 此時 pb 節點的後繼還沒有訪問到, 只能透過它的前驅節點 prev 的右子樹 rchild 來判斷: * if(!prev-rchild)為空, 則 pb 就是 prev 的後繼, 於是 prev-Thread, prev->rchild = pb 完成後繼節點的線索化 * 完成前驅和後繼的判斷後把 pb 給 prev, 便於下一次遍歷 */

if

pb

->

lchild

{

// 如果沒有左孩子

pb

->

LTag

=

Thread

// 前驅線索

pb

->

lchild

=

prev

// 左孩子指標指向前驅

}

if

prev

->

rchild

{

// 如果沒有右孩子

prev

->

LTag

=

Thread

// 後繼線索

prev

->

lchild

=

pb

// 右孩子指標指向後繼

}

prev

=

pb

// 3。右

InThreading

pb

->

rchild

}

}

當初我要是這麼學習二叉樹就好了

4。 總結回顧

二叉樹每個結點最多兩棵子樹,有左右之分。提到了斜樹,滿二叉樹、完全二叉樹等特殊二叉樹的概念

我們接著談到它的各種性質,這些性質給我們研究二叉樹帶來了方便。

二叉樹的儲存結構由於其特殊性使得既可以用順序儲存結構又可以用鏈式儲存結構表示

遍歷是二叉樹最重要的一門學問,前序、中序、後序以及層序遍歷都是需要熟練掌握的知識。要讓自己要學會用計算機的執行思維去模擬遞迴的實現,可以加深我們對遞迴的理解。不過,並非二叉樹遍歷就一定要用到遞迴, 只不過遞迴的實現比較優雅而已。這點需要明確

二叉樹的建立自然也是可以透過遞迴來實現

搜尋二叉樹能實現查詢效率較大化, 如果遇到

斜樹

則會變的很慢

研究中也發現,二叉連結串列有很多浪費的空指標可以利用,查詢某個結點的前驅和後繼為什麼非要每次遍歷才可以得到,這就引出瞭如何構造一棵線索二叉樹的問題。線索二叉樹給二叉樹的結點查詢和遍歷帶來了高效率

最後,我們知道了關於二叉樹的另一個應用,赫夫曼樹和赫夫曼編碼,對於帶權路徑的二叉樹做了詳盡地講述,讓你初步理解資料壓縮的原理,並明白其是如何做到無最後,我們提到了關於二叉樹的-一個應用,赫夫曼樹和赫夫曼編碼,對於帶權路徑的二叉樹做了詳盡地講述,讓你初步理解資料壓縮的原理,並明白其是如何做到無損編碼和無錯解碼

中間插入的一些

力扣, 牛克

線上OJ就當練手與解析, 有興趣的可以去刷一刷。

5。 結束語

二叉樹相較於連結串列, 佇列, 棧顯然更難一層樓。路漫漫求修遠兮, 吾將上下而求索。勉勵自己頁面每一個來看我部落格的人共同進步, 學習更高質量的知識為目標。

,https://blog。csdn。net/weixin_45364220/article/details/120621141