二叉樹 Binary Tree

上一篇文章樹 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章