提示:本篇的原文已經(jīng)在github上有所更新,想看最新版的朋友們抱歉了...
二叉查找樹(shù)(英語(yǔ):Binary Search Tree),也稱二叉搜索樹(shù)、有序二叉樹(shù)(英語(yǔ):ordered binary tree),排序二叉樹(shù)(英語(yǔ):sorted binary tree)。是一種特殊類型的二叉樹(shù)(每個(gè)節(jié)點(diǎn)最多有兩個(gè)子節(jié)點(diǎn)的樹(shù)),它執(zhí)行插入和刪除,以使樹(shù)始終排序。
屬性:“始終排序”
下面是一個(gè)二叉搜索樹(shù)的例子:
注意每個(gè)左邊的子節(jié)點(diǎn)是小于它的父節(jié)點(diǎn)的,并且每個(gè)右邊的子節(jié)點(diǎn)都是大于它的父節(jié)點(diǎn)的。這是二叉搜索樹(shù)的關(guān)鍵特征。
例子中,2
小于 7
,所以它在左邊; 5
大于 2
,所以它在右邊。
插入新節(jié)點(diǎn)
當(dāng)執(zhí)行插入時(shí),我們首先將新值與根節(jié)點(diǎn)進(jìn)行比較。如果新值較小,我們把它放到左分支;如果新值較大,我們把它放到右分支。我們以這種方式在樹(shù)種一直尋找,直到找到一個(gè)合適的位置插入新值。
假設(shè)我們要插入新值 9
:
- 我們從樹(shù)的根節(jié)點(diǎn)(值為
7
的節(jié)點(diǎn))開(kāi)始,并將其與新值9
進(jìn)行比較。 -
9 > 7
,所以我們沿著右分支并重復(fù)相同的過(guò)程,但這次在節(jié)點(diǎn)10上。 - 因?yàn)?
9 < 10
,所以我們走左分支。 - 我們已經(jīng)沒(méi)有值可以比較了,新值
9
應(yīng)該插入在這里。
新元素插入到樹(shù)中的位置只有一種可能。找到這個(gè)位置通常很快。他需要的 O(h) 的時(shí)間,其中h是樹(shù)的高度。
注意: 節(jié)點(diǎn)的 高度 是從該節(jié)點(diǎn)到其最低葉所需的步驟數(shù)。整個(gè)樹(shù)的高度是從根到最低葉的距離。二叉搜索樹(shù)上的許多操作都是用樹(shù)的高度表示的。
遵循這個(gè)簡(jiǎn)單的規(guī)則 -- 左側(cè)的值較小,右側(cè)的值較大 -- 我們保持樹(shù)的排序方式,這樣每當(dāng)查詢時(shí),可以快速檢查一個(gè)值是否在樹(shù)中。
搜索樹(shù)
為了在樹(shù)中找到一個(gè)值,我們執(zhí)行與插入基本上相同的步驟:
- 如果值小于當(dāng)前節(jié)點(diǎn),則取左側(cè)分支。
- 如果值大于當(dāng)前節(jié)點(diǎn),則取右側(cè)分支。
- 如果值等于當(dāng)前節(jié)點(diǎn),我們就找到了!
像大多數(shù)樹(shù)操作一樣,這是遞歸執(zhí)行的,直到我們找到要查找的值,或者遍歷完整個(gè)樹(shù)。
如果我們?cè)诶又胁檎抑?5
,它將如下所示:
由于樹(shù)的結(jié)構(gòu),搜索真的很快。它的運(yùn)行時(shí)間是 O(h) 。如果你有一個(gè)有100萬(wàn)個(gè)節(jié)點(diǎn)的平衡樹(shù)(well-balanced tree),它只需要大約20個(gè)步驟來(lái)找到這棵樹(shù)中的任何東西。(這個(gè)想法非常類似于數(shù)組中的二分搜索。)
遍歷樹(shù)
有時(shí)你不只想看一個(gè)節(jié)點(diǎn),而是想看所有的節(jié)點(diǎn)。
有三種方法來(lái)遍歷二叉樹(shù):
- 按順序(或深度優(yōu)先):首先查看節(jié)點(diǎn)的左子節(jié)點(diǎn),然后查看節(jié)點(diǎn)本身,最后查看其右子節(jié)點(diǎn)。
- 按節(jié)點(diǎn):先看一個(gè)節(jié)點(diǎn),然后看它的左右子節(jié)點(diǎn)。
- 反序: 先看左右子節(jié)點(diǎn),最后處理節(jié)點(diǎn)本身。
這里再一次發(fā)生遞歸。
如果按順序遍歷二叉搜索樹(shù),它會(huì)查看所有節(jié)點(diǎn),就好像它們從低到高排序一樣。遍歷示例中的樹(shù),它會(huì)打印 1, 2, 5, 7, 9, 10
:
刪除節(jié)點(diǎn)
刪除節(jié)點(diǎn)有點(diǎn)棘手。刪除葉節(jié)點(diǎn)很容易,你只需要將它與父節(jié)點(diǎn)斷開(kāi):
如果要?jiǎng)h除的節(jié)點(diǎn)只有一個(gè)子節(jié)點(diǎn),我們可以將該子節(jié)點(diǎn)鏈接到父節(jié)點(diǎn)。 所以我們只需拉出節(jié)點(diǎn):
棘手部分是,當(dāng)刪除節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn)。為了保持樹(shù)排序正確,我們必須用大于節(jié)點(diǎn)的最小子節(jié)點(diǎn)替換這個(gè)節(jié)點(diǎn):
這總右子樹(shù)里的最左邊的子節(jié)點(diǎn)。需要額外搜索,最多耗時(shí) O(h) 來(lái)找到這個(gè)孩子。
其他涉及二叉搜索樹(shù)的代碼相當(dāng)簡(jiǎn)單(如果你理解遞歸),但刪除節(jié)點(diǎn)有點(diǎn)棘手。
代碼(方案1)
有如此多的理論。讓我們看看如何能迅速實(shí)現(xiàn)二叉搜索樹(shù)。你可以用不同的方法。首先,我將向您展示如何創(chuàng)建一個(gè)基于類的版本,但我們還將介紹如何使用枚舉創(chuàng)建一個(gè)版本。
這是第一次嘗試 BinarySearchTree
類:
public class BinarySearchTree<T: Comparable> {
private(set) public var value: T
private(set) public var parent: BinarySearchTree?
private(set) public var left: BinarySearchTree?
private(set) public var right: BinarySearchTree?
public init(value: T) {
self.value = value
}
public var isRoot: Bool {
return parent == nil
}
public var isLeaf: Bool {
return left == nil && right == nil
}
public var isLeftChild: Bool {
return parent?.left === self
}
public var isRightChild: Bool {
return parent?.right === self
}
public var hasLeftChild: Bool {
return left != nil
}
public var hasRightChild: Bool {
return right != nil
}
public var hasAnyChild: Bool {
return hasLeftChild || hasRightChild
}
public var hasBothChildren: Bool {
return hasLeftChild && hasRightChild
}
public var count: Int {
return (left?.count ?? 0) + 1 + (right?.count ?? 0)
}
}
這個(gè)類只描述了一個(gè)節(jié)點(diǎn),而不是整個(gè)樹(shù)。這是一個(gè)泛型類型,因此,節(jié)點(diǎn)可以存儲(chǔ)任何類型的數(shù)據(jù)。它還引用其左、右子節(jié)點(diǎn)和父節(jié)點(diǎn)。
這樣創(chuàng)建它:
let tree = BinarySearchTree<Int>(value: 7)
count
屬性決定了樹(shù)和子樹(shù)有多少節(jié)點(diǎn)。這不僅僅計(jì)算節(jié)點(diǎn)的直接子節(jié)點(diǎn),而且計(jì)算他們的子節(jié)點(diǎn)和他們的子節(jié)點(diǎn)的子節(jié)點(diǎn),等等。
如果這個(gè)特定對(duì)象是根節(jié)點(diǎn),則它計(jì)算整個(gè)樹(shù)中有多少個(gè)節(jié)點(diǎn)。 最初,count = 0。
注意:因?yàn)?
left
,right
和parent
是可選的,所以我們可以很好地利用 Swift 的可選鏈接 (?
) 和 nil-coalescing運(yùn)算符(??
)。你也可以用if let
,但是不那么簡(jiǎn)潔。
插入節(jié)點(diǎn)
一個(gè)樹(shù)節(jié)點(diǎn)本身毫無(wú)用處,這是如何將新節(jié)點(diǎn)添加到樹(shù):
public func insert(value: T) {
insert(value: value, parent: self)
}
private func insert(value: T, parent: BinarySearchTree) {
if value < self.value {
if let left = left {
left.insert(value: value, parent: left)
} else {
left = BinarySearchTree(value: value)
left?.parent = parent
}
} else {
if let right = right {
right.insert(value: value, parent: right)
} else {
right = BinarySearchTree(value: value)
right?.parent = parent
}
}
}
像許多其他樹(shù)操作一樣,插入是最簡(jiǎn)單的遞歸實(shí)現(xiàn)。我們將新值與現(xiàn)有節(jié)點(diǎn)的值進(jìn)行比較,并決定是將其添加到左側(cè)分支還是右側(cè)分支。
如果沒(méi)有更多的左、右子節(jié)點(diǎn)要查看,我們?yōu)樾鹿?jié)點(diǎn)創(chuàng)建一個(gè)BinarySearchTree對(duì)象,并通過(guò)設(shè)置其父節(jié)點(diǎn)屬性將其連接到樹(shù)。
注意: 因?yàn)槎嫠阉鳂?shù)的在左邊的節(jié)點(diǎn)較小,在右邊的節(jié)點(diǎn)較大,你應(yīng)該總是在根節(jié)點(diǎn)插入元素,以確保這是一個(gè)有效的二叉樹(shù)!
根據(jù)例子建立完整的樹(shù):
let tree = BinarySearchTree<Int>(value: 7)
tree.insert(value: 2)
tree.insert(value: 5)
tree.insert(value: 10)
tree.insert(value: 9)
tree.insert(value: 1)
注意: 以后就會(huì)明白這么做的原因,你應(yīng)該插入隨機(jī)的數(shù)字。如果你按順序插入,樹(shù)不會(huì)有正確的形狀。
為了方便起見(jiàn),我們添加一個(gè) init 方法,為數(shù)組中的所有元素調(diào)用 insert()
:
public convenience init(array: [T]) {
precondition(array.count > 0)
self.init(value: array.first!)
for v in array.dropFirst() {
insert(value: v, parent: self)
}
}
現(xiàn)在你可以這樣做:
let tree = BinarySearchTree<Int>(array: [7, 2, 5, 10, 9, 1])
數(shù)組中的第一個(gè)值成為樹(shù)的根節(jié)點(diǎn)。
調(diào)試輸出
當(dāng)使用像這樣的有些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)時(shí),有人類可讀的調(diào)試輸出是很有用的。
extension BinarySearchTree: CustomStringConvertible {
public var description: String {
var s = ""
if let left = left {
s += "(\(left.description)) <- "
}
s += "\(value)"
if let right = right {
s += " -> (\(right.description))"
}
return s
}
}
當(dāng)你調(diào)用 print(tree)
,輸出如下:
((1) <- 2 -> (5)) <- 7 -> ((9) <- 10)
根節(jié)點(diǎn)在中間。想象一下,你應(yīng)該看到這確實(shí)對(duì)應(yīng)于以下樹(shù):
順便說(shuō)一下,你可能想知道當(dāng)你插入重復(fù)項(xiàng)目會(huì)發(fā)生什么? 我們總是將它們插入到正確的分支中。 試試看!
搜索
我們現(xiàn)在做什么,我們?cè)跇?shù)中有一些值?搜索他們!能夠快速找到項(xiàng)目是二叉搜索樹(shù)的整個(gè)目的。 :-)
這是 search()
的實(shí)現(xiàn):
public func search(value: T) -> BinarySearchTree? {
if value < self.value {
return left?.search(value: value)
} else if value > self.value {
return right?.search(value: value)
} else {
return self // 找到了!
}
}
我希望邏輯清晰:這在當(dāng)前節(jié)點(diǎn)(通常是根節(jié)點(diǎn))處開(kāi)始,并比較這些值。如果搜索值小于節(jié)點(diǎn)的值,我們?cè)谧蠓种е欣^續(xù)搜索;如果搜索值更大,我們?cè)谟曳种е欣^續(xù)搜索。
當(dāng)然,如果沒(méi)有更多的節(jié)點(diǎn)可查看 -- 當(dāng)左子節(jié)點(diǎn)或右子節(jié)點(diǎn)為空,那么我們返回 nil
以指示搜索值不在樹(shù)中。
注意: 在Swift中,使用可選鏈接非常方便;當(dāng)你寫(xiě)下
left?.search(value)
時(shí),如果left
是nil
,它將自動(dòng)返回nil
。 沒(méi)有必要使用if
語(yǔ)句顯式檢查這一點(diǎn)。
搜索是一個(gè)遞歸過(guò)程,但您也可以使用簡(jiǎn)單的循環(huán)來(lái)實(shí)現(xiàn):
public func search(value: T) -> BinarySearchTree? {
var node: BinarySearchTree? = self
while case let n? = node {
if value < n.value {
node = n.left
} else if value > n.value {
node = n.right
} else {
return node
}
}
return nil
}
驗(yàn)證一下,你明白這兩個(gè)實(shí)現(xiàn)是等價(jià)的。就個(gè)人而言,我喜歡使用迭代代碼而不是遞歸代碼,但你的意見(jiàn)可能不同。 ;-)
以下是測(cè)試搜索的方法:
tree.search(value: 5)
tree.search(value: 2)
tree.search(value: 7)
tree.search(value: 6) // nil
前三行都返回相應(yīng)的 BinaryTreeNode
對(duì)象。 最后一行返回 nil
,因?yàn)闆](méi)有值為 6
的節(jié)點(diǎn)。
注意: 如果樹(shù)中有重復(fù)項(xiàng),
search()
總是返回“最高”節(jié)點(diǎn)。 這是有道理的,因?yàn)槲覀冮_(kāi)始從根節(jié)點(diǎn)向下搜索。
遍歷
記得有3種不同的方法來(lái)查看樹(shù)中的所有節(jié)點(diǎn)嘛? 他們是:
public func traverseInOrder(process: (T) -> Void) {
left?.traverseInOrder(process: process)
process(value)
right?.traverseInOrder(process: process)
}
public func traversePreOrder(process: (T) -> Void) {
process(value)
left?.traversePreOrder(process: process)
right?.traversePreOrder(process: process)
}
public func traversePostOrder(process: (T) -> Void) {
left?.traversePostOrder(process: process)
right?.traversePostOrder(process: process)
process(value);
}
他們都做同樣的事情,但順序不同。 再次注意,所有的工作都是遞歸完成的。 由于Swift可選鏈接的特性,當(dāng)沒(méi)有左或右子節(jié)點(diǎn)時(shí),對(duì) traverseInOrder()
等的調(diào)用會(huì)被忽略。
要打印從低到高排序的樹(shù)中的所有值,可以這樣寫(xiě):
tree.traverseInOrder{ value in print(value) }
這將打印以下內(nèi)容:
1
2
5
7
9
10
你還可以向樹(shù)中添加 map()
和 filter()
。 例如,下面是一個(gè)map的實(shí)現(xiàn):
public func map(formula: (T) -> T) -> [T] {
var a = [T]()
if let left = left { a += left.map(formula: formula) }
a.append(formula(value))
if let right = right { a += right.map(formula: formula) }
return a
}
這個(gè)閉包將調(diào)用樹(shù)中每個(gè)節(jié)點(diǎn)上的公式,并將結(jié)果收集到數(shù)組中。 map()
按順序來(lái)遍歷樹(shù)。
一個(gè)非常簡(jiǎn)單的使用 map()
的例子:
public func toArray() -> [T] {
return map { $0 }
}
這將樹(shù)的內(nèi)容變?yōu)榕藕眯虻臄?shù)組。 在 playground 上試試:
tree.toArray() // [1, 2, 5, 7, 9, 10]
作為練習(xí),看看你是否可以實(shí)現(xiàn) filter 和 reduce 。
刪除節(jié)點(diǎn)
您已經(jīng)看到刪除節(jié)點(diǎn)可能很棘手。 我們可以通過(guò)定義一些輔助函數(shù)使代碼更具可讀性。
private func reconnectParentToNode(node: BinarySearchTree?) {
if let parent = parent {
if isLeftChild {
parent.left = node
} else {
parent.right = node
}
}
node?.parent = parent
}
對(duì)樹(shù)進(jìn)行更改涉及更改一系列 parent
和 left
和 right
節(jié)點(diǎn)。這個(gè)功能有助于,它需要當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)(即 self
),并將其連接到另一個(gè)節(jié)點(diǎn)。通常,另一個(gè)節(jié)點(diǎn)是 self
的孩子之一。
我們還需要一個(gè)返回節(jié)點(diǎn)最左邊的后代的方法:
public func minimun() -> BinarySearchTree {
var node = self
while case let next? = node.left {
node = next
}
return node
}
要了解這是如何工作的,請(qǐng)看下面的樹(shù):
例如,如果我們看節(jié)點(diǎn) 10
,其最左邊的子節(jié)點(diǎn)是 6
。我們通過(guò)跟隨所有的左子節(jié)點(diǎn)到達(dá)那里,直到?jīng)]有更多的左子節(jié)點(diǎn)。根節(jié)點(diǎn) 7
的最左邊的子孫是 1
。因此,1
是整個(gè)樹(shù)中的最小值。
我們不需要它刪除,但為了完整性,這里是相反的 minimum()
:
public func maximum() -> BinarySearchTree {
var node = self
while case let next? = node.right {
node = next
}
return node
}
它返回節(jié)點(diǎn)的最右邊的后代。我們通過(guò)跟隨右子節(jié)點(diǎn)找到它,直到結(jié)束。在上面的例子中,節(jié)點(diǎn) 2
的最右邊的后代是 5
。整個(gè)樹(shù)中的最大值為 11
,因?yàn)檫@是根節(jié)點(diǎn) 7
的最右邊的后代。
最后,我們可以編寫(xiě)從樹(shù)中刪除節(jié)點(diǎn)的代碼:
public func remove() -> BinarySearchTree? {
let replacement: BinarySearchTree?
if let left = left {
if let right = right {
replacement = removeNodeWithChildren(left: left, right: right) // 1
} else {
replacement = left // 2
}
} else if let right = right { // 3
replacement = right
} else {
replacement = nil // 4
}
reconnectParentToNode(node: replacement)
parent = nil
left = nil
right = nil
return replacement
}
它看起來(lái)不那么可怕。 ;-) 有四種情況要處理:
1.此節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn)。
2.此節(jié)點(diǎn)只有一個(gè)左子節(jié)點(diǎn)。 左子節(jié)點(diǎn)替換該節(jié)點(diǎn)。
3.此節(jié)點(diǎn)只有一個(gè)右子節(jié)點(diǎn)。 右子節(jié)點(diǎn)替換該節(jié)點(diǎn)。
4.此節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)。 我們只是斷開(kāi)它和父節(jié)點(diǎn)的聯(lián)系。
首先,我們確定哪個(gè)節(jié)點(diǎn)將替換我們要?jiǎng)h除的節(jié)點(diǎn),然后我們調(diào)用reconnectParentToNode()
來(lái)更改左,右和父指針,使之發(fā)生。 由于當(dāng)前節(jié)點(diǎn)不再是樹(shù)的一部分,我們通過(guò)將其指針設(shè)置為nil來(lái)清除它。 最后,我們返回已替換已刪除節(jié)點(diǎn)的節(jié)點(diǎn)(如果這是葉節(jié)點(diǎn),則返回nil)。
這里唯一棘手的是情況1,這個(gè)邏輯有自己的幫助方法:
private func removeNodeWithTwoChildren(left: BinarySearchTree, right: BinarySearchTree) -> BinarySearchTree {
let successor = right.minimun()
let _ = successor.remove()
successor.left = left
left.parent = successor
if right !== successor {
successor.right = right
right.parent = successor
} else {
successor.right = nil
}
return successor
}
如果要?jiǎng)h除的節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn),則必須由大于此節(jié)點(diǎn)值的最小子節(jié)點(diǎn)替換。 這恰好總是是右子節(jié)點(diǎn)的最左邊的子節(jié)點(diǎn),即 right.minimum()
。 我們將該節(jié)點(diǎn)從樹(shù)中的原始位置取出,放到要?jiǎng)h除的節(jié)點(diǎn)的位置。
試試看:
if let node2 = tree.search(value: 2) {
print(tree) // 刪除前
let _ = node2.remove()
print(tree) // 刪除后
}
首先,要使用 search()
找到要?jiǎng)h除的節(jié)點(diǎn),然后在該對(duì)象上調(diào)用 remove()
。 在刪除之前,樹(shù)打印如下:
((1) <- 2 -> (5)) <- 7 -> ((9) <- 10)
在 remove()
之后,你將得到:
((1) <- 5) <- 7 -> ((9) <- 10)
正如你所看到的,節(jié)點(diǎn) 5
取代了 2
。
注意:如果刪除根節(jié)點(diǎn)會(huì)發(fā)生什么? 在這種情況下,
remove()
告訴你哪個(gè)節(jié)點(diǎn)已經(jīng)成為新的根。 試試看:調(diào)用tree.remove()
并看看會(huì)發(fā)生什么。
深度和高度
回想一下,節(jié)點(diǎn)的高度是到其最低葉的距離。 我們可以用以下函數(shù)計(jì)算:
public func height() -> Int {
if isLeaf {
return 0
} else {
return 1 + max(left?.height() ?? 0, right?.height() ?? 0)
}
}
我們看看左右分支的高度,取最高的一個(gè)。 同樣,這是一個(gè)遞歸過(guò)程。 因?yàn)榻Y(jié)果看起來(lái)像遍歷節(jié)點(diǎn)的所有子節(jié)點(diǎn),性能是 O(n) 。
注意: Swift的空合并運(yùn)算符(
??
)可以快速處理 值為 nil 的左或右節(jié)點(diǎn)。 你可以用這個(gè),或者用if let
,但這是一個(gè)更簡(jiǎn)潔。
試試看:
print(tree.height()) // 2
您還可以計(jì)算節(jié)點(diǎn)的深度,這是到根的距離。 這里是代碼:
public func depth() -> Int {
var node = self
var deges = 0
while case let parent? = node.parent {
node = parent
deges += 1
}
return edges
}
它向上遍歷樹(shù),順找父節(jié)點(diǎn),直到我們到達(dá)根節(jié)點(diǎn)(其父節(jié)點(diǎn)為nil)。 這需要 O(h) 時(shí)間。 例:
if let node9 = tree.search(value: 9) {
print(node9.depth()) // 2
}
前任和后繼
二叉搜索樹(shù)總是“排序”,但這并不意味著連續(xù)的數(shù)字在樹(shù)中彼此相鄰。
注意,你只看左子節(jié)點(diǎn)的話,是找不到 7
的。 左子節(jié)點(diǎn)是 2
,而不是 5
。同樣不是 7
后面的數(shù)字。
predecessor()
函數(shù)以順序返回其值在當(dāng)前值之前的節(jié)點(diǎn):
public func predecessor() -> BinarySearchTree<T>? {
if let left = left {
return left.maximum()
} else {
let node = self
while case let parent? = node.parent {
if parent.value < value {
return parent
}
}
return nil
}
}
如果我們有一個(gè)左子樹(shù)很容易。 在這種情況下,直接調(diào)用 predecessor()
是該子樹(shù)中的最大值。 你可以在上面的圖片中驗(yàn)證 5
確實(shí)是 7
的左分支中的最大值。
然而,如果沒(méi)有左子樹(shù),那么我們必須查看我們的父節(jié)點(diǎn),直到我們找到一個(gè)較小的值。 因此,如果我們想知道節(jié)點(diǎn) 9
的前任是什么,我們繼續(xù)向上,直到我們找到具有較小值的第一個(gè)父節(jié)點(diǎn),即 7
。
successor()
的代碼工作方式完全相同:
public func successor() -> BinarySearchTree<T>? {
if let right = right {
return right.minimum()
} else {
var node = self
while case let parent? = node.parent {
if parent.value > value {
return parent
}
node = parent
}
return nil
}
}
這兩個(gè)方法的時(shí)間復(fù)雜度都是 O(h) 。
注意: 有一個(gè)很酷的變體稱為“螺紋”二叉樹(shù),其中“未使用”的左和右指針被重用以在前任節(jié)點(diǎn)和后繼節(jié)點(diǎn)之間建立直接鏈接。 非常聰明!
搜索樹(shù)是否有效?
如果你想搞破壞,你可以通過(guò)調(diào)用 insert()
在一個(gè)不是根的節(jié)點(diǎn),將二叉搜索樹(shù)變成一個(gè)無(wú)效樹(shù),像這樣:
if let node1 = tree.search(value: 1) {
node1.insert(value: 100)
print(tree)
}
根節(jié)點(diǎn)的值為 7
,因此值為 100
的節(jié)點(diǎn)應(yīng)該在樹(shù)的右分支中。 但是,你不是在根的插入,而是在樹(shù)的左側(cè)分支的葉節(jié)點(diǎn)。 所以新的 100
節(jié)點(diǎn)在樹(shù)的錯(cuò)誤的地方!
結(jié)果,tree.search(100)
返回 nil 。
您可以使用以下方法檢查樹(shù)是否是有效的二叉搜索樹(shù):
public func isBST(minValue: T, maxValue: T) -> Bool {
if value < minValue || value > maxValue {
return false
}
let leftBST = left?.isBST(minValue: minValue, maxValue: value) ?? true
let rightBST = right?.isBST(minValue: value, maxValue: maxValue) ?? true
return leftBST && rightBST
}
這驗(yàn)證左分支確實(shí)包含小于當(dāng)前節(jié)點(diǎn)的值,并且右分支僅包含較大的值。
調(diào)用如下:
if let node1 = tree.search(value: 1) {
print(tree.isBST(minValue: Int.min, maxValue: Int.max)) // true
node1.insert(value: 100) //破壞!!!
print(tree.search(value: 100)) // nil
print(tree.isBST(minValue: Int.min, maxValue: Int.max)) // false
}
代碼(解決方案2)
我們已經(jīng)將二叉樹(shù)節(jié)點(diǎn)實(shí)現(xiàn)為類,但也可以使用枚舉。
區(qū)別在于參考語(yǔ)義與價(jià)值語(yǔ)義。 對(duì)基于類的樹(shù)進(jìn)行更改將更新內(nèi)存中的同一個(gè)實(shí)例。 但是基于枚舉的樹(shù)是不可變的 - 任何插入或刪除都會(huì)給你一個(gè)全新的樹(shù)的副本。 哪一個(gè)最好完全取決于你想要使用哪個(gè)。
這里是如何使用枚舉二叉搜索樹(shù):
public enum BinarySearchTreeEnum<T: Comparable> {
case Empty
case Leaf(T)
indirect case Node(BinarySearchTreeEnum<T>, T, BinarySearchTreeEnum<T>)
}
枚舉有三種情況:
-
Empty
空標(biāo)記分支的結(jié)束(基于類的版本為此使用了nil
引用)。 -
Leaf
葉為沒(méi)有孩子的葉節(jié)點(diǎn)。 -
Node
具有一個(gè)或兩個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)的節(jié)點(diǎn)。 這是使用關(guān)鍵字indirect
,以便它可以保存BinarySearchTree
值。 沒(méi)有indirect
,你不能使遞歸枚舉。
注意: 此二叉樹(shù)中的節(jié)點(diǎn)沒(méi)有對(duì)其父節(jié)點(diǎn)的引用。 這不是一個(gè)重要的障礙,但它會(huì)使某些操作略為繁瑣。
像往常一樣,我們將遞歸地實(shí)現(xiàn)大多數(shù)功能。 我們將對(duì)每個(gè)枚舉的情況略有不同。 例如,這是如何計(jì)算樹(shù)中的節(jié)點(diǎn)數(shù)和樹(shù)的高度:
public var count: Int {
switch self {
case .Empty:
return 0
case .Leaf:
return 1
case let .Node(left, _, _right):
return left.count + 1 + _right.count
}
}
插入新節(jié)點(diǎn)如下所示:
public func insert(newValue: T) -> BinarySearchTreeEnum {
switch self {
case .Empty:
return .Leaf(newValue)
case .Leaf(let value):
if newValue < value {
return .Node(.Leaf(newValue), value, .Empty)
} else {
return .Node(.Empty, value, .Leaf(newValue))
}
case .Node(let left, let value, let right):
if newValue < value {
return .Node(left.insert(newValue: newValue), value, right)
} else {
return .Node(left, value, right.insert(newValue: newValue))
}
}
}
在 playground 里試試看:
var tree = BinarySearchTreeEnum.Leaf(7)
tree = tree.insert(2)
tree = tree.insert(5)
tree = tree.insert(10)
tree = tree.insert(9)
tree = tree.insert(1)
注意,每次插入后,你會(huì)得到一個(gè)全新的樹(shù)對(duì)象。這就是為什么你需要將結(jié)果賦值給 tree
。
這里是最重要的搜索功能:
public func search(x: T) -> BinarySearchTreeEnum? {
switch self {
case .Empty:
return nil
case .Leaf(let y):
return (x == y) ? self : nil
case let .Node(left, y, right):
if x < y {
return left.search(x)
} else if y < x {
return right.search(x)
} else {
return self
}
}
}
如你所見(jiàn),這些函數(shù)中的大多數(shù)具有相同的結(jié)構(gòu)。
在 playground 中試試看:
tree.search(10)
tree.search(1)
tree.search(11) // nil
要打印樹(shù)以進(jìn)行調(diào)試,可以使用以下方法:
extension BinarySearchTreeEnum: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .Empty: return "."
case .Leaf(let value): return "\(value)"
case .Node(let left, let value, let right):
return "(\(left.debugDescription) <- \(value) -> \(right.debugDescription))"
}
}
}
當(dāng)你調(diào)用 print(tree)
時(shí),將會(huì)看到這樣的輸出:
((1 <- 2 -> 5) <- 7 -> (9 <- 10 -> .))
根節(jié)點(diǎn)在中間; .
表示在該位置沒(méi)有子節(jié)點(diǎn)。
當(dāng)樹(shù)變得不平衡...
當(dāng)二叉搜索樹(shù)的左和右子樹(shù)包含大致相同數(shù)量的節(jié)點(diǎn)時(shí),它是平衡的。 在這種情況下,樹(shù)的高度是 log(n),其中 n 是節(jié)點(diǎn)的數(shù)量。 這是理想的情況。
然而,如果一個(gè)分支比另一個(gè)分支長(zhǎng)得多,搜索將變得非常慢。 我們最終檢索比我們理想情況下更多的值。 在最壞的情況下,樹(shù)的高度可以變?yōu)?n 。 這樣的樹(shù)比二叉搜索樹(shù)更像鏈表,性能降級(jí)到 O(n) 。 這可不好!
使二叉搜索樹(shù)平衡的一種方法是以完全隨機(jī)的順序插入節(jié)點(diǎn)。 平均來(lái)說(shuō),應(yīng)該可以保持樹(shù)平衡。 但它不保證成功,也不總是實(shí)用。
另一個(gè)解決方案是使用自平衡二叉樹(shù)。 此類型的數(shù)據(jù)結(jié)構(gòu)會(huì)在插入或刪除節(jié)點(diǎn)后調(diào)整樹(shù)以保持平衡。 有關(guān)示例,請(qǐng)參閱AVL樹(shù)和紅黑樹(shù)。
也可以看看
由 Nicolas Ameghino 和 Matthijs Hollemans 寫(xiě)的Swift算法俱樂(lè)部。