樹 Tree 基本信息及實現

樹(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

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