樹(Tree)是一種很重要的數據結構,在軟件開發的多方面都有使用:
- 表示層級結構。
- 計算機語言的抽象語法樹。
- 解析人類語言的樹。
- XML 和 HTML 文檔對象模型。
- 處理 JSON 和YAML 文檔。
- 快速查找數據。
- 管理有序數據。
樹的類型有很多,其有不同的形狀和大小。這一篇文章將介紹樹的基本信息,以及如何實現樹。
1. 術語
下面是一些與樹相關常見術語。
1.1 節點 Node
與鏈表相似,樹也由節點構成。
每個節點包含數據和指向子節點的引用。
1.2 父節點 Parent、子節點 Child
Tree 從頂部向下看,和植物中的樹很像,只是方向相反。
每個節點向上都有一個節點(最頂部節點除外),上面的節點稱爲父節點(parent node)。節點的下面,與其直接相連的稱爲子節點(child node)。在樹中,每個child只有一個parent。
1.3 根節點 Root
樹最頂部的節點稱爲根節點(root),根節點是唯一沒有父節點的節點。
1.4 葉子節點 Leaf
如果節點沒有子節點,則稱爲葉子節點(leaf)。
此外,還有以下常用術語:
- 節點的度(degree):該節點子樹的個數稱爲該節點的度。
- 樹的度:所有節點中,度的最大值稱爲樹的度。
- 非葉子節點:度不爲零的節點。
- 高度(height):當前節點到最遠葉子節點的路徑長,所有樹葉的高度爲零。
- 深度(depth):對於任意節點n,n的深度爲從根到n的唯一路徑長。有些地方認爲根深度爲0,有些地方認爲根深度爲1。
- 兄弟節點:具有相同父節點的節點互相稱爲兄弟節點。
- 節點的層數(level):從根開始定義,根爲第一層,根的子節點爲第二層。以此類推。
- 堂兄弟節點:父節點在同一層的節點互爲堂兄弟。
- 節點的祖先(ancestor):從根到該節點所經分支上的所有節點。
- 子孫(descendant):以某節點爲根的子樹中任一節點都稱爲該節點的子孫。
- 森林:由m(m >= 0)棵互不相交的樹的集合稱爲森林。
2. 實現
Tree 由 node 構成,這裏先創建TreeNode
類。
創建一個 playground,並在 Sources 中添加TreeNode.swift
文件,TreeNode
代碼如下:
public class TreeNode<T> {
public var value: T
public var children: [TreeNode] = []
public init(_ value: T) {
self.value = value
}
}
每個 node 存儲一個值和一個數組,數組裏存儲着 children。
在TreeNode
中添加以下代碼:
/// 向當前節點添加子節點
/// - Parameter child: 要添加的子節點
public func add(_ child: TreeNode) {
children.append(child)
}
在 playground page 中添加以下代碼:
example(of: "creating a tree") {
let beverages = TreeNode("Beverages")
let hot = TreeNode("Hot")
let cold = TreeNode("Cold")
beverages.add(hot)
beverages.add(cold)
}
樹可以很方便的用於層次結構問題。上述代碼定義了三個節點,並組織成邏輯層次,其對應於以下結構:
3. 遍歷算法 Traversal Algorithms
線性表(如數組、鏈表、隊列)有明確的開始、結束位置,遍歷很簡單。
與線性表相比,遍歷樹會變得複雜些。
遍歷時左側節點有優先權嗎?深度與優先權有關聯嗎?在實際應用時,應根據要解決的問題決定遍歷策略。不同類型樹、不同類型問題,有不同遍歷方式。
3.1 深度優先
在TreeNode.swift
底部添加以下方法:
extension TreeNode {
/// 深度優先遍歷
public func forEachDepthFirst(visit: (TreeNode) -> Void) {
visit(self)
children.forEach({
// 使用遞歸遍歷
$0.forEachDepthFirst(visit: visit)
})
}
}
如果不想使用遞歸,可以使用棧實現。
在 playground page 添加以下代碼,檢驗深度優先遍歷:
func makeBeverageTree() -> TreeNode<String> {
let tree = TreeNode("Beverages")
let hot = TreeNode("hot")
let cold = TreeNode("cold")
let tea = TreeNode("tea")
let coffee = TreeNode("coffee")
let chocolate = TreeNode("cocoa")
let blackTea = TreeNode("black")
let greenTea = TreeNode("green")
let chaiTea = TreeNode("chai")
let soda = TreeNode("soda")
let milk = TreeNode("milk")
let gingerAle = TreeNode("ginger ale")
let bitterLemon = TreeNode("bitter lemon")
tree.add(hot)
tree.add(cold)
hot.add(tea)
hot.add(coffee)
hot.add(chocolate)
cold.add(soda)
cold.add(milk)
tea.add(blackTea)
tea.add(greenTea)
tea.add(chaiTea)
soda.add(gingerAle)
soda.add(bitterLemon)
return tree
}
上述代碼創建的樹如下:
添加以下代碼:
example(of: "depth-first traversal") {
let tree = makeBeverageTree()
tree.forEachDepthFirst(visit: {
print($0.value)
})
}
輸出如下:
--- Example of: depth-first traversal
Beverages
hot
tea
black
green
chai
coffee
cocoa
cold
soda
ginger ale
bitter lemon
milk
3.2 層序遍歷
層序遍歷根據 node depth,先遍歷同一層節點。
extension TreeNode {
/// 層序遍歷
public func forEachLevelOrder(visit: (TreeNode) -> Void) {
visit(self)
var queue = Queue<TreeNode>()
children.forEach({
queue.enqueue($0)
})
while let node = queue.dequeue() {
visit(node)
node.children.forEach{ queue.enqueue($0)}
}
}
}
這裏使用隊列而非棧確保節點讀取順序。
Queue是隊列這篇文章創建的數據結構,你可以在文章底部的源碼中獲取。
forEachLevelOrder
函數按照層序遍歷:
使用以下代碼進行層序遍歷:
example(of: "level-order traversal") {
let tree = makeBeverageTree()
tree.forEachDepthFirst(visit: {
print($0.value)
})
}
運行後輸出如下:
--- Example of: level-order traversal
Beverages
hot
cold
tea
coffee
cocoa
soda
milk
black
green
chai
ginger ale
bitter lemon
3.3 搜索
前面已經介紹了深度優先、層序遍歷兩種遍歷方式,想要搜索 tree 是否包含指定元素就變得簡單了。
extension TreeNode where T: Equatable {
public func search(_ value: T) -> TreeNode? {
var result: TreeNode?
forEachDepthFirst { (node) in
if node.value == value {
result = node
}
}
return result
}
}
使用以下代碼搜索指定值:
example(of: "searching for a node") {
let tree = makeBeverageTree()
if let searchResult1 = tree.search("ginger ale") {
print("Found node: \(searchResult1.value)")
}
if let searchResult2 = tree.search("WKD Blue") {
print(searchResult2.value)
} else {
print("Couldn't find WKD Blue")
}
}
運行後控制檯輸出如下:
--- Example of: searching for a node
Found node: ginger ale
Couldn't find WKD Blue
深度優先遍歷時,如果有多個匹配值,則會取最後一個。
4. 樹算法題
4.1 同一層節點打印到同一行
根據節點所在層,將同一層節點打印到同一行。如下圖:
輸出應如下所示:
15
1 17 20
1 5 0 2 5 7
使用層序遍歷可以解決按照層打印的問題,只需判斷出何時換行。如下所示:
func printEachLevel<T>(for tree: TreeNode<T>) {
var queue = Queue<TreeNode<T>>()
var nodesLeftInCurrentLevel = 0
queue.enqueue(tree)
while !queue.isEmpty {
nodesLeftInCurrentLevel = queue.count
// 循環打印一層
while nodesLeftInCurrentLevel > 0 {
guard let node = queue.dequeue() else { break }
print("\(node.value)", terminator: " ")
node.children.forEach { queue.enqueue($0) }
nodesLeftInCurrentLevel -= 1
}
// 換行
print()
}
}
使用nodesLeftInCurrentLevel
記錄隊列當前元素數量,此時打印隊列元素時不打印換行符,直到該層打印結束才換行。
該算法時間複雜度爲O(n)
。由於初始化了隊列作爲節點的容器,空間複雜度也是O(n)
。
4.2 爲樹添加父節點
樹的定義中只包含了子節點,刪除、旋轉等操作時需要用到父節點,應如何爲樹添加父節點?
可以按照下面方式爲樹添加一個父節點屬性:
public class TreeNode<T> {
public weak var parent: TreeNode?
...
}
根節點沒有父節點,因此這裏使用了可選類型。節點對子節點強引用,對父節點需爲弱引用,否則會產生循環引用。
總結
樹有以下這些要點:
- 樹與鏈表有一些相似的地方,但鏈表只能有一個子節點,樹可以有多個子節點。
- 除根節點外,所有節點都只有一個父節點。
- 葉子節點沒有子節點。
- 像層序遍歷、深度優先遍歷算法,適用於所有類型樹。但可能因樹類型不同,具體實現有所差異。
Demo名稱:Tree
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Tree
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/樹%20Tree%20基本信息及實現.md