如何表示一棵樹呢?有上述四種方法,不過這邊我們只細談第一種
假設有下面這棵樹:
我們可以這樣表示樹的節點,然後連接節點
使用 C++ 表示節點結構的話:
你會注意到,當 tree 的 degree 最大為 \(k\) 時,我們就要讓每個節點都準備 \(k\) 條 links 的空間
如果有 \(n\) 個節點,那就需要 \(k\cdot n\) 條 links
但是根據 tree 本身的數學性質我們知道,非空 links 數只會有 \(n-1\) 條
因此,這個方法的空間浪費率為 \(\frac{nk-(n-1)}{nk}\),大概是 \(\frac{k-1}{k}\)
如果 \(k=1\) 就退化成普通的 linked list 了我們就不討論,所以對 tree 來說 \(k=2\) 時浪費率最低,但也達到約 \(50\%\)
不過這個表示方法很直觀,而且表達 tree 上較有彈性
所以在表達 binary tree (也就是 \(k = 2\)) 時,我們更習慣用這種方法
我們可以簡單記得 binary tree 就是每個節點的 degree \(\le 2\) 的 tree
不過要提醒,在資料結構這邊,binary tree 與 tree 還有其他不同的地方,其實不能說 binary tree 是 tree
Tree | Binary Tree |
---|---|
Cannot be empty | Can be empty |
Node degree ≥ 0 | 0 ≤ Node degree ≤ 2 |
Subtrees have no specific order | Subtrees are ordered as left and right |
假設定 root 所在的 level 為 \(1\)
Caution
要注意的是離散在這邊的定義與資結有點不同
令 BT 如下:
\(L\) 跟 \(R\) 代表左右子樹,\(D\) 代表該樹的 root 則遍歷順序:
Preorder | Inorder | Postorder |
---|---|---|
DLR | LDR | LRD |
其實很好記:
而 level order 就是 level 從上往下由左向右遍歷
例如給你下面的 BT
則遍歷結果:
Preorder | Inorder | Postorder | Level Order |
---|---|---|---|
ABDECFG | DBEAFCG | DEBFGCA | ABCDEFG |
如果今天反過來給你遍歷順序,那麼怎樣的組合能決定唯一的 BT 呢?
Inputs | Can Determine the Unique BT? |
---|---|
inorder + {preorder, postorder, or level order} | YES |
preorder + postorder | NO |
level order + {preorder or postorder} | NO |
full BT + {preorder, inorder, postorder, level order} | YES |
complete BT + {preorder, inorder, postorder, level order} | YES |
BST + {preorder, postorder, level order} | YES |
使用 linked list 的方式來表示 tree node 則 BT 的遍歷怎麼寫呢?
下面是節點的結構
為了表達方便,我們使用 enum class 來表達要選擇哪一種遍歷方式
則以 preorder 為例,我們可以很輕易寫出其遞迴演算法:
Tip
nullptr
視為 false
,所以 !root
為 true
的情況即 root == nullptr
(空樹)
記得我們前面講的遍歷記憶法嗎?沒錯,只要記得 DLR 你就能輕鬆寫出 code 了!
同理,inorder 跟 postorder :
至於 level order 用 BFS:
如此,我們便能簡單寫出一個列印 BT 的 method:
測試一下
一些 BT 有可能會考的應用題
現在我們可以進一步補充我們的 constructor 跟 =
operator
可以想像是 compiler 建立 parsing tree or syntax tree 的作法
原則:
例如給 a + b * c - d / e
把這想成是 BT inorder 的結果
則 postorder 為 abc * + de / -
只要有這兩個遍歷結果就能建構回樹了
我覺得這邊只要會手寫作答的部份就好了
這邊附上截止至本文撰寫時,本人印象中有解過,LeetCode 上跟 BT 有關的題目
是 BT ,故可以為空
若不為空:
Tip
BST 的 inorder traversal 必定是由小到大排列輸出
我們知道 BST 的 inorder 就是 sorted order
故 inorder successor 即 sorted order 中的下一位; inorder predecessor 同理為上一位
對應我們 Iterator
實作便是 ++
跟 --
運算
這邊我大致提供一個簡單的實作以供參考
我們給 TreeNode
增加 parent
指標
Iterator
更新後如下:
我們就依照 BST 的規則找正確的位置放置新節點即可
多次 insert()
便可建立 BST
BST 建立之結構受 input 順序影響,如果給 sorted input 則會得 skewed BST
範例:
input: 5, 8, 9, 3, 1, 4, 6, 2, 7
x
x
is leaf: 直接砍掉deg(x) == 1
把 x
的 parent 指向 x
的 child 再砍掉 x
deg(x) == 2
找左子樹最大值 or 右子樹最小值 令為 y
以 y
取代 x
再 delete(y)
(原本位置的 y
)例如原本的 BST
砍 5 ,假設採用左子樹最大值則過程:
因為普通 BST 的 insert()
相對簡單,所以對 iterator 的影響很好處理
但是 erase()
就不一樣了,我們要注意該操作對 iterator 有效性的破壞
下面是我大致 debug 用的 code:
因為每次 erase()
會破壞 iterator 之有效性,所以我們要將 it
更新成 erase()
回傳的下個位置
有成功砍完整棵 BST ,最後 bst
變回了空樹
看到這邊,相信各位算是基礎認識 BST 了
那各位會發現,best case 跟 average case 下 find()
的時間複雜度是 \(O(\lg n)\) 沒有什麼問題
但如果 BST 是 skewed tree 的話,就會有 worst case \(O(n)\)
insert()
跟 erase()
也跟 find()
一樣
這問題就是 BST 的高度並未受到控制,而都叫 binary search tree 了,find()
的時間複雜度我們肯定是希望能好點,不要退化到線性時間
在未來,我們將會討論高度平衡的 BST 來解決這問題
最直覺的方法當然是按照 inorder 等第 k
個出現
但我們有其他作法
關鍵是我們在多紀錄每個節點其左子樹的節點個數
如此一來我們便可以輕鬆找出第 k
小了
大致 pseudocode 長這樣:
k == r
的 case 不必多說
k < r
時,表示要找的目標在當前根節點的左子樹裡;k > r
時,表示左子樹裡沒有想要的,前 r
個後面都不用看了,前 k
個少了 r
個,所以所求是剩下元素中前 k - r
小的那個
這個可以記一下大概演算法怎麼寫
不難,但經典
前面講過 complete BT 會發現這種 BT 其實節點長很滿
這種時候其實反而使用 array 保存更適合
Heap 可以用來製作 priority queue 也能用來 heap sort
Operations | Time Complexity |
---|---|
Insert | \(O(\lg n)\) |
Delete-Max (Delete-Min) | \(O(\lg n)\) |
Heapifty | \(O(\lg n)\) |
Find-Max (Find-Min) | \(O(1)\) |
Build | \(O(n)\) |
Decrease key | \(O(\lg n)\) |
Merge | \(O(n)\) |
以下提供基礎的 binary heap 實作
我們有兩種方式來初始建構 heap:
top-down 很簡單,就是依照輸入順序一個個 push()
(insert) 進去
可是這樣的時間複雜度其實是 \(O(n\lg n)\)
\[ \sum\limits_{i=1}^{n}\lg i = \lg (n!) = O(n\lg n) \]
另一種方式即 bottom-up
我們是先讓所有元素都放進 array (可以想成先填進 complete BT 中)
接著使用 heapify
的方式調整陣列使其符合 heap 的性質
這樣講有點抽象 具體來說就是從最後一個 parent 開始往上使每棵子樹皆符合 heap 性質直到 root
這樣的時間複雜度就是 \(O(n)\) 了