上一篇文章樹 Tree 基本信息及實現介紹了 Tree 的基本信息,一個節點可以有多個子節點。二叉樹(Binary Tree)每個節點至多有兩個子節點,被稱爲左子樹(left)、右子樹(right)。
二叉樹是很多樹結構和算法的基礎。這篇文章將實現一個二叉樹,並介紹三種常見的遍歷算法。
1. 實現一個二叉樹
在 playground 中添加一個文件,名稱爲BinaryNode.swift
。其中代碼如下:
public class BinaryNode<Element> {
public var value: Element
public var leftChild: BinaryNode?
public var rightChild: BinaryNode?
public init(value: Element) {
self.value = value
}
}
在 playground page 添加以下代碼:
var tree: BinaryNode<Int> = {
let zero = BinaryNode(value: 0)
let one = BinaryNode(value: 1)
let five = BinaryNode(value: 5)
let seven = BinaryNode(value: 7)
let eight = BinaryNode(value: 8)
let nine = BinaryNode(value: 9)
seven.leftChild = one
one.leftChild = zero
one.rightChild = five
seven.rightChild = nine
nine.leftChild = eight
return seven
}()
上述代碼定義的 binary tree 如下所示:
建立樹數據結構模型對理解樹非常有幫助。爲此,實現了二叉樹
description
方法,以便在控制檯可視化二叉樹。你可以在源碼中獲取其具體實現。
打印二叉樹後,控制檯輸出如下:
--- Example of tree diagram ---
┌──nil
┌──9
│ └──8
7
│ ┌──5
└──1
└──0
2. 遍歷算法
樹 Tree 基本信息及實現介紹了層序遍歷,對該算法稍作調整,即可用於二叉樹。這裏不會重新實現層序遍歷,而是介紹中序、前序、後續三種遍歷算法。
2.1 中序遍歷 In-order traversal
中序遍歷從根節點開始,按照以下順序遍歷:
- 如果當前節點有左子樹,先遞歸訪問左子樹。
- 訪問當前節點。
- 如果當前節點有右子樹,遞歸訪問右子樹。
下圖是中序遍歷順序示意圖:
使用中序遍歷上述二叉樹時,節點按照升序順序輸出。如果節點按照一定規則排布,in-order traversal 會以升序順序訪問節點,下一篇文章二叉搜索樹將會介紹這些內容。
添加以下中序遍歷代碼:
/// 中序遍歷
public func traversalInOrder(visit: (Element) -> Void) {
leftChild?.traversalInOrder(visit: visit)
visit(value)
rightChild?.traversalInOrder(visit: visit)
}
先查找到最左節點,再訪問其值,最後遍歷最右節點。
使用以下代碼進行中序遍歷:
example(of: "in-order traversal") {
tree.traversalInOrder(visit: {
print($0)
})
}
控制檯輸出如下:
--- Example of in-order traversal ---
0
1
5
7
8
9
2.2 前序遍歷 Pre-order traversal
前序遍歷先訪問當前節點,再遞歸訪問左子樹、右子樹。
前序遍歷算法如下:
/// 前序遍歷
public func traversalPreOrder(visit: (Element) -> Void) {
visit(value)
leftChild?.traversalPreOrder(visit: visit)
rightChild?.traversalPreOrder(visit: visit)
}
運行後輸出如下:
--- Example of pre-order traversal ---
7
1
0
5
9
8
2.3 後續遍歷 Post-order traversal
後續遍歷先遞歸遍歷左子樹、右子樹,最後訪問當前節點。
也就是先訪問子節點,後訪問自身。根節點永遠是最後訪問的節點。
後序遍歷算法如下:
/// 後序遍歷
public func traversalPostOrder(visit: (Element) -> Void) {
leftChild?.traversalPostOrder(visit: visit)
rightChild?.traversalPostOrder(visit: visit)
visit(value)
}
使用後序遍歷控制檯輸出如下:
--- Example of post-order traversal ---
0
5
1
8
9
7
二叉樹的前序遍歷、中序遍歷、後序遍歷時間複雜度、空間複雜度都是O(n)
。二叉樹通過在插入時增加一些規則,可以使中序遍歷按照升序輸出。下一篇文章二叉搜索樹將更進一步介紹這些內容。
3. 二叉樹算法題
3.1 二叉樹高度
給定一個二叉樹,計算它的高度。
二叉樹的高度爲根節點到最遠葉子節點的距離。如果只有一個節點,則高度爲零。因爲它既是根節點,又是葉子節點。
算法如下:
func height<T>(of node: BinaryNode<T>?) -> Int {
guard let node = node else { return -1 }
return 1 + max(height(of: node.leftChild), height(of: node.rightChild))
}
3.2 二叉樹的序列化與反序列化
序列化是將一個數據結構或者對象轉換爲連續比特位的操作,進而可以將轉換後的數據存儲在文件、內存中,同時可通過網絡傳輸到其他計算機。通過相反的方式(即反序列化)可以得到原數據。
常用序列化的地方就是 JSON 轉換。你的任務就是將二叉樹序列化爲數組,並將數組反序列化爲之前的二叉樹。
二叉樹的序列化本質上是對其值進行編碼,更重要的是對其結構進行編碼。通過遍歷樹來進行序列化,有兩種遍歷策略:
- 廣度優先(Breadth First Traversal,簡寫爲 BFT):按照層次的順序從上到下遍歷所有節點。
- 深度優先(Depth First Traversal,簡寫爲 DFT):從根節點開始,一直延伸到某個葉子節點,然後回到根節點,再遍歷另一個分支。根據其相對順序,可以將 DFT 進一步分爲:
- 前序遍歷
- 中序遍歷
- 後序遍歷
假設有以下二叉樹:
1
/ \
2 5
/ \
3 4
使用前序遍歷的方式序列化該二叉樹,如下所示:
從根節點1
開始,然後跳到根節點的左子樹2
,然後跳到2
的左子樹3
,然後查看3
的左右子樹是否存在,然後跳到2
的右子樹4
。最終。序列化數組爲[1, 2, 3, nil, nil, 4, nil, nil, 5, nil, nil]
。
即序列化二叉樹時,遇到空節點序列爲爲nil
,否則繼續遞歸序列化。
3.2.1 先序遍歷
爲BinaryNode
添加以下方法:
/// 前序遍歷二叉樹,會遍歷空節點。
public func traversePreOrder(visit: (Element?) -> Void) {
visit(value)
if let leftChild = leftChild {
leftChild.traversePreOrder(visit: visit)
} else {
visit(nil)
}
if let rightChild = rightChild {
rightChild.traversePreOrder(visit: visit)
} else {
visit(nil)
}
}
上述方法遍歷節點的每一個 node,所以它的時間複雜度是O(n)
。
3.2.2 序列化
序列化時訪問每一個節點,並將其添加到數組。數組元素爲T?
類型,因爲其可能爲nil
。序列化方法如下:
/// 序列化二叉樹
func serialize<T>(_ node: BinaryNode<T>) -> [T?] {
var array: [T?] = []
node.traversePreOrder(visit: {
array.append($0)
})
return array
}
該算法時間複雜度爲O(n)
。由於需要創建一個數組容納所有元素,該算法空間複雜度爲O(n)
。
3.2.3 反序列化
序列化時使用前序遍歷,將值放入數組。反序列化時將值從數組中取出,重新組裝爲二叉樹。
反序列化算法如下:
/// 反序列化
func deserialize<T>(_ array: inout [T?]) -> BinaryNode<T>? {
// value 爲空時表示節點沒有子樹,遞歸結束。
guard let value = array.removeFirst() else { return nil }
// 使用 value 創建節點,並設置左子樹、右子樹。
let node = BinaryNode(value: value)
node.leftChild = deserialize(&array)
node.rightChild = deserialize(&array)
return node
}
inout
表示可以在函數內修改數組元素,這樣允許遞歸調用時修改數組,並獲得修改後的數組。
對二叉樹先序列化,再反序列化後,打印二叉樹。如下所示:
print(tree)
var array = serialize(tree)
if let node = deserialize(&array) {
print(node)
}
控制檯輸出如下:
┌──nil
┌──9
│ └──8
7
│ ┌──5
└──1
└──0
┌──nil
┌──9
│ └──8
7
│ ┌──5
└──1
└──0
經序列化、反序列化操作後,二叉樹沒有發生變化。
由於每次移除數組元素都調用了removeFirst()
,反序列化算法時間複雜度爲O(n²)
。
可以在反序列化開始前先反轉數組,需要移除元素時,使用removeLast()
即可。由於removeLast()
的複雜度爲O(1)
,可以讓整個算法時間複雜度降低到O(n)
。由於需要反轉數組,它的空間複雜度爲O(n)
。
你可以自行優化下反序列化,如果遇到問題,可以在文章底部獲取源碼查看。
總結
二叉樹是很多重要數據結構的基礎。例如,二叉搜索樹(Binary Search Tree,簡寫爲 BST)和平衡二叉樹(AVL)。前序遍歷、中序遍歷、後序遍歷不僅對二叉樹很重要,處理其它類型樹時也會頻繁使用。
Demo名稱:BinaryTree
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/BinaryTree
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/二叉樹%20Binary%20Tree.md