本文是對 Swift Algorithm Club 翻譯的一篇文章。
Swift Algorithm Club是 raywenderlich.com網站出品的用Swift實現演算法和資料結構的開源專案,目前在GitHub上有18000+⭐️,我初略統計了一下,大概有一百左右個的演算法和資料結構,基本上常見的都包含了,是iOSer學習演算法和資料結構不錯的資源。
?andyRon/swift-algorithm-club-cn是我對Swift Algorithm Club,邊學習邊翻譯的專案。由於能力有限,如發現錯誤或翻譯不妥,請指正,歡迎pull request。也歡迎有興趣、有時間的小夥伴一起參與翻譯和學習?。當然也歡迎加⭐️,?????。
本文的翻譯原文和程式碼可以檢視?swift-algorithm-club-cn/Tree
這個話題已經有個輔導文章
樹表示物件之間的層次關係。 這是一個樹的結構:
樹由節點組成,這些節點彼此連線。
節點可以連線到他們的子節點,也可以連線到他們的父節點。 子節點是給定節點下的節點; 父節點是上面的節點。 節點始終只有一個父節點,但可以有多個子節點。
沒有父節點的節點是 root 節點。 沒有子節點的節點是 leaf 節點。
樹中的指標不能形成迴圈。 這不是樹:
這種結構稱為圖。 樹實際上是一種非常簡單的圖形形式。 (類似地,連結串列是樹的簡單版本。想一想!)
本文討論通用樹。 通用樹對每個節點可能有多少個子節點或樹中節點的順序沒有任何限制。
這是在Swift中的基本實現:
public class TreeNode<T> {
public var value: T
public weak var parent: TreeNode?
public var children = [TreeNode<T>]()
public init(value: T) {
self.value = value
}
public func addChild(_ node: TreeNode<T>) {
children.append(node)
node.parent = self
}
}
複製程式碼
這描述了樹中的單個節點。 它包含泛型型別T
,對parent
節點的引用,以及子節點陣列。
新增description
以便列印樹:
extension TreeNode: CustomStringConvertible {
public var description: String {
var s = "\(value)"
if !children.isEmpty {
s += " {" + children.map { $0.description }.joined(separator: ", ") + "}"
}
return s
}
}
複製程式碼
在 playground 建立樹:
let tree = TreeNode<String>(value: "beverages")
let hotNode = TreeNode<String>(value: "hot")
let coldNode = TreeNode<String>(value: "cold")
let teaNode = TreeNode<String>(value: "tea")
let coffeeNode = TreeNode<String>(value: "coffee")
let chocolateNode = TreeNode<String>(value: "cocoa")
let blackTeaNode = TreeNode<String>(value: "black")
let greenTeaNode = TreeNode<String>(value: "green")
let chaiTeaNode = TreeNode<String>(value: "chai")
let sodaNode = TreeNode<String>(value: "soda")
let milkNode = TreeNode<String>(value: "milk")
let gingerAleNode = TreeNode<String>(value: "ginger ale")
let bitterLemonNode = TreeNode<String>(value: "bitter lemon")
tree.addChild(hotNode)
tree.addChild(coldNode)
hotNode.addChild(teaNode)
hotNode.addChild(coffeeNode)
hotNode.addChild(chocolateNode)
coldNode.addChild(sodaNode)
coldNode.addChild(milkNode)
teaNode.addChild(blackTeaNode)
teaNode.addChild(greenTeaNode)
teaNode.addChild(chaiTeaNode)
sodaNode.addChild(gingerAleNode)
sodaNode.addChild(bitterLemonNode)
複製程式碼
如果你列印出tree
的值,你會得到:
beverages {hot {tea {black, green, chai}, coffee, cocoa}, cold {soda {ginger ale, bitter lemon}, milk}}
複製程式碼
這對應於以下結構:
beverages
節點是根節點,因為它沒有父節點。 葉子是黑色
,綠色
,柴
,咖啡
,可可
,薑汁
,苦檸檬
,牛奶
,因為它們沒有任何子節點。
對於任何節點,您可以檢視parent
屬性, 並按照自己的方式返回到根:
teaNode.parent // this is the "hot" node
teaNode.parent!.parent // this is the root
複製程式碼
在談論樹時,我們經常使用以下定義:
-
樹的高度。 這是根節點和最低葉子之間的連線數。 在我們的示例中,樹的高度為3,因為從根到底部需要三次跳轉。
-
節點的深度。 此節點與根節點之間的連線數。 例如,
tea
的深度為2,因為需要兩次跳躍才能到達根部。 (根本身的深度為0.)
構建樹的方法有很多種。 例如,有時您根本不需要 parent
屬性。 或者,您可能只需要為每個節點提供最多兩個子節點 - 這樣的樹稱為二叉樹。 一種非常常見的樹型別是二叉搜尋樹(或BST),它是二叉樹的更嚴格版本,其中節點以特定方式排序以加速搜尋。
我在這裡展示的通用樹非常適合描述分層資料,但它實際上取決於您的應用程式需要具備哪種額外功能。
下面是一個如何使用TreeNode
類來確定樹是否包含特定值的示例。 首先看一下節點自己的value
屬性,如果沒有匹配,那麼依次看看這個節點所有的子節點。 當然,這些子節點也是TreeNode
,所以它們將遞迴地重複相同的過程:首先看看它們自己的value
屬性,然後看看它們的子節點。 它們的子節點也會再次做同樣的事情,等等...遞迴和樹齊頭並進。
這是程式碼:
extension TreeNode where T: Equatable {
func search(_ value: T) -> TreeNode? {
if value == self.value {
return self
}
for child in children {
if let found = child.search(value) {
return found
}
}
return nil
}
}
複製程式碼
如何使用它的示例:
tree.search("cocoa") // returns the "cocoa" node
tree.search("chai") // returns the "chai" node
tree.search("bubbly") // nil
複製程式碼
也可以使用陣列來描述樹。 陣列中的索引表示不同的節點,然後,建立不同節點之間的連線。 例如,如果我們有:
0 = beverage 5 = cocoa 9 = green
1 = hot 6 = soda 10 = chai
2 = cold 7 = milk 11 = ginger ale
3 = tea 8 = black 12 = bitter lemon
4 = coffee
複製程式碼
那麼我們可以使用以下陣列描述樹:
[ -1, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 6, 6 ]
複製程式碼
陣列中的每個項的值都是指向其父節點的指標。索引3處的項tea
,其值為1,因為其父項為hot
。根節點指向-1
,因為它沒有父節點。您只能將這些樹從一個節點遍歷到根節點,而不是相反。
順便說一句,有時您會看到使用術語 forest 的演算法。 顯而易見,這是給予單獨樹物件集合的名稱。 有關此示例,請參閱 union-find。