提示:本篇的原文已經(jīng)在github上有所更新,想看最新版的朋友們抱歉了...
二叉查找樹(英語:Binary Search Tree),也稱二叉搜索樹、有序二叉樹(英語:ordered binary tree),排序二叉樹(英語:sorted binary tree)。是一種特殊類型的二叉樹(每個(gè)節(jié)點(diǎn)最多有兩個(gè)子節(jié)點(diǎn)的樹),它執(zhí)行插入和刪除,以使樹始終排序。
屬性:“始終排序”
下面是一個(gè)二叉搜索樹的例子:

注意每個(gè)左邊的子節(jié)點(diǎn)是小于它的父節(jié)點(diǎn)的,并且每個(gè)右邊的子節(jié)點(diǎn)都是大于它的父節(jié)點(diǎn)的。這是二叉搜索樹的關(guān)鍵特征。
例子中,2 小于 7 ,所以它在左邊; 5 大于 2 ,所以它在右邊。
插入新節(jié)點(diǎn)
當(dāng)執(zhí)行插入時(shí),我們首先將新值與根節(jié)點(diǎn)進(jìn)行比較。如果新值較小,我們把它放到左分支;如果新值較大,我們把它放到右分支。我們以這種方式在樹種一直尋找,直到找到一個(gè)合適的位置插入新值。
假設(shè)我們要插入新值 9 :
- 我們從樹的根節(jié)點(diǎn)(值為
7的節(jié)點(diǎn))開始,并將其與新值9進(jìn)行比較。 -
9 > 7,所以我們沿著右分支并重復(fù)相同的過程,但這次在節(jié)點(diǎn)10上。 - 因?yàn)?
9 < 10,所以我們走左分支。 - 我們已經(jīng)沒有值可以比較了,新值
9應(yīng)該插入在這里。

新元素插入到樹中的位置只有一種可能。找到這個(gè)位置通常很快。他需要的 O(h) 的時(shí)間,其中h是樹的高度。
注意: 節(jié)點(diǎn)的 高度 是從該節(jié)點(diǎn)到其最低葉所需的步驟數(shù)。整個(gè)樹的高度是從根到最低葉的距離。二叉搜索樹上的許多操作都是用樹的高度表示的。
遵循這個(gè)簡單的規(guī)則 -- 左側(cè)的值較小,右側(cè)的值較大 -- 我們保持樹的排序方式,這樣每當(dāng)查詢時(shí),可以快速檢查一個(gè)值是否在樹中。
搜索樹
為了在樹中找到一個(gè)值,我們執(zhí)行與插入基本上相同的步驟:
- 如果值小于當(dāng)前節(jié)點(diǎn),則取左側(cè)分支。
- 如果值大于當(dāng)前節(jié)點(diǎn),則取右側(cè)分支。
- 如果值等于當(dāng)前節(jié)點(diǎn),我們就找到了!
像大多數(shù)樹操作一樣,這是遞歸執(zhí)行的,直到我們找到要查找的值,或者遍歷完整個(gè)樹。
如果我們在例子中查找值 5 ,它將如下所示:

由于樹的結(jié)構(gòu),搜索真的很快。它的運(yùn)行時(shí)間是 O(h) 。如果你有一個(gè)有100萬個(gè)節(jié)點(diǎn)的平衡樹(well-balanced tree),它只需要大約20個(gè)步驟來找到這棵樹中的任何東西。(這個(gè)想法非常類似于數(shù)組中的二分搜索。)
遍歷樹
有時(shí)你不只想看一個(gè)節(jié)點(diǎn),而是想看所有的節(jié)點(diǎn)。
有三種方法來遍歷二叉樹:
- 按順序(或深度優(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ā)生遞歸。
如果按順序遍歷二叉搜索樹,它會(huì)查看所有節(jié)點(diǎn),就好像它們從低到高排序一樣。遍歷示例中的樹,它會(huì)打印 1, 2, 5, 7, 9, 10 :

刪除節(jié)點(diǎn)
刪除節(jié)點(diǎn)有點(diǎn)棘手。刪除葉節(jié)點(diǎn)很容易,你只需要將它與父節(jié)點(diǎn)斷開:

如果要?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)。為了保持樹排序正確,我們必須用大于節(jié)點(diǎn)的最小子節(jié)點(diǎn)替換這個(gè)節(jié)點(diǎn):

這總右子樹里的最左邊的子節(jié)點(diǎn)。需要額外搜索,最多耗時(shí) O(h) 來找到這個(gè)孩子。
其他涉及二叉搜索樹的代碼相當(dāng)簡單(如果你理解遞歸),但刪除節(jié)點(diǎn)有點(diǎn)棘手。
代碼(方案1)
有如此多的理論。讓我們看看如何能迅速實(shí)現(xiàn)二叉搜索樹。你可以用不同的方法。首先,我將向您展示如何創(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è)樹。這是一個(gè)泛型類型,因此,節(jié)點(diǎn)可以存儲(chǔ)任何類型的數(shù)據(jù)。它還引用其左、右子節(jié)點(diǎn)和父節(jié)點(diǎn)。
這樣創(chuàng)建它:
let tree = BinarySearchTree<Int>(value: 7)
count 屬性決定了樹和子樹有多少節(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è)樹中有多少個(gè)節(jié)點(diǎn)。 最初,count = 0。
注意:因?yàn)?
left,right和parent是可選的,所以我們可以很好地利用 Swift 的可選鏈接 (?) 和 nil-coalescing運(yùn)算符(??)。你也可以用if let,但是不那么簡潔。
插入節(jié)點(diǎn)
一個(gè)樹節(jié)點(diǎn)本身毫無用處,這是如何將新節(jié)點(diǎn)添加到樹:
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í)現(xiàn)。我們將新值與現(xiàn)有節(jié)點(diǎn)的值進(jìn)行比較,并決定是將其添加到左側(cè)分支還是右側(cè)分支。
如果沒有更多的左、右子節(jié)點(diǎn)要查看,我們?yōu)樾鹿?jié)點(diǎn)創(chuàng)建一個(gè)BinarySearchTree對(duì)象,并通過設(shè)置其父節(jié)點(diǎn)屬性將其連接到樹。
注意: 因?yàn)槎嫠阉鳂涞脑谧筮叺墓?jié)點(diǎn)較小,在右邊的節(jié)點(diǎn)較大,你應(yīng)該總是在根節(jié)點(diǎn)插入元素,以確保這是一個(gè)有效的二叉樹!
根據(jù)例子建立完整的樹:
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ù)字。如果你按順序插入,樹不會(huì)有正確的形狀。
為了方便起見,我們添加一個(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è)值成為樹的根節(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)于以下樹:

順便說一下,你可能想知道當(dāng)你插入重復(fù)項(xiàng)目會(huì)發(fā)生什么? 我們總是將它們插入到正確的分支中。 試試看!
搜索
我們現(xiàn)在做什么,我們在樹中有一些值?搜索他們!能夠快速找到項(xiàng)目是二叉搜索樹的整個(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))處開始,并比較這些值。如果搜索值小于節(jié)點(diǎn)的值,我們在左分支中繼續(xù)搜索;如果搜索值更大,我們在右分支中繼續(xù)搜索。
當(dāng)然,如果沒有更多的節(jié)點(diǎn)可查看 -- 當(dāng)左子節(jié)點(diǎn)或右子節(jié)點(diǎn)為空,那么我們返回 nil 以指示搜索值不在樹中。
注意: 在Swift中,使用可選鏈接非常方便;當(dāng)你寫下
left?.search(value)時(shí),如果left是nil,它將自動(dòng)返回nil。 沒有必要使用if語句顯式檢查這一點(diǎn)。
搜索是一個(gè)遞歸過程,但您也可以使用簡單的循環(huán)來實(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è)人而言,我喜歡使用迭代代碼而不是遞歸代碼,但你的意見可能不同。 ;-)
以下是測試搜索的方法:
tree.search(value: 5)
tree.search(value: 2)
tree.search(value: 7)
tree.search(value: 6) // nil
前三行都返回相應(yīng)的 BinaryTreeNode 對(duì)象。 最后一行返回 nil ,因?yàn)闆]有值為 6 的節(jié)點(diǎn)。
注意: 如果樹中有重復(fù)項(xiàng),
search()總是返回“最高”節(jié)點(diǎn)。 這是有道理的,因?yàn)槲覀冮_始從根節(jié)點(diǎn)向下搜索。
遍歷
記得有3種不同的方法來查看樹中的所有節(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)沒有左或右子節(jié)點(diǎn)時(shí),對(duì) traverseInOrder() 等的調(diào)用會(huì)被忽略。
要打印從低到高排序的樹中的所有值,可以這樣寫:
tree.traverseInOrder{ value in print(value) }
這將打印以下內(nèi)容:
1
2
5
7
9
10
你還可以向樹中添加 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)用樹中每個(gè)節(jié)點(diǎn)上的公式,并將結(jié)果收集到數(shù)組中。 map() 按順序來遍歷樹。
一個(gè)非常簡單的使用 map() 的例子:
public func toArray() -> [T] {
return map { $0 }
}
這將樹的內(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)可能很棘手。 我們可以通過定義一些輔助函數(shù)使代碼更具可讀性。
private func reconnectParentToNode(node: BinarySearchTree?) {
if let parent = parent {
if isLeftChild {
parent.left = node
} else {
parent.right = node
}
}
node?.parent = parent
}
對(duì)樹進(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)看下面的樹:

例如,如果我們看節(jié)點(diǎn) 10 ,其最左邊的子節(jié)點(diǎn)是 6 。我們通過跟隨所有的左子節(jié)點(diǎn)到達(dá)那里,直到?jīng)]有更多的左子節(jié)點(diǎn)。根節(jié)點(diǎn) 7 的最左邊的子孫是 1。因此,1 是整個(gè)樹中的最小值。
我們不需要它刪除,但為了完整性,這里是相反的 minimum() :
public func maximum() -> BinarySearchTree {
var node = self
while case let next? = node.right {
node = next
}
return node
}
它返回節(jié)點(diǎn)的最右邊的后代。我們通過跟隨右子節(jié)點(diǎn)找到它,直到結(jié)束。在上面的例子中,節(jié)點(diǎn) 2 的最右邊的后代是 5 。整個(gè)樹中的最大值為 11 ,因?yàn)檫@是根節(jié)點(diǎn) 7 的最右邊的后代。
最后,我們可以編寫從樹中刪除節(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
}
它看起來不那么可怕。 ;-) 有四種情況要處理:
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)沒有子節(jié)點(diǎn)。 我們只是斷開它和父節(jié)點(diǎn)的聯(lián)系。
首先,我們確定哪個(gè)節(jié)點(diǎn)將替換我們要?jiǎng)h除的節(jié)點(diǎn),然后我們調(diào)用reconnectParentToNode() 來更改左,右和父指針,使之發(fā)生。 由于當(dāng)前節(jié)點(diǎn)不再是樹的一部分,我們通過將其指針設(shè)置為nil來清除它。 最后,我們返回已替換已刪除節(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)從樹中的原始位置取出,放到要?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() 。 在刪除之前,樹打印如下:
((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è)遞歸過程。 因?yàn)榻Y(jié)果看起來像遍歷節(jié)點(diǎn)的所有子節(jié)點(diǎn),性能是 O(n) 。
注意: Swift的空合并運(yùn)算符(
??)可以快速處理 值為 nil 的左或右節(jié)點(diǎn)。 你可以用這個(gè),或者用if let,但這是一個(gè)更簡潔。
試試看:
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
}
它向上遍歷樹,順找父節(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
}
前任和后繼
二叉搜索樹總是“排序”,但這并不意味著連續(xù)的數(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è)左子樹很容易。 在這種情況下,直接調(diào)用 predecessor() 是該子樹中的最大值。 你可以在上面的圖片中驗(yàn)證 5 確實(shí)是 7 的左分支中的最大值。
然而,如果沒有左子樹,那么我們必須查看我們的父節(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è)很酷的變體稱為“螺紋”二叉樹,其中“未使用”的左和右指針被重用以在前任節(jié)點(diǎn)和后繼節(jié)點(diǎn)之間建立直接鏈接。 非常聰明!
搜索樹是否有效?
如果你想搞破壞,你可以通過調(diào)用 insert() 在一個(gè)不是根的節(jié)點(diǎn),將二叉搜索樹變成一個(gè)無效樹,像這樣:
if let node1 = tree.search(value: 1) {
node1.insert(value: 100)
print(tree)
}
根節(jié)點(diǎn)的值為 7 ,因此值為 100 的節(jié)點(diǎn)應(yīng)該在樹的右分支中。 但是,你不是在根的插入,而是在樹的左側(cè)分支的葉節(jié)點(diǎn)。 所以新的 100 節(jié)點(diǎn)在樹的錯(cuò)誤的地方!
結(jié)果,tree.search(100) 返回 nil 。
您可以使用以下方法檢查樹是否是有效的二叉搜索樹:
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)將二叉樹節(jié)點(diǎn)實(shí)現(xiàn)為類,但也可以使用枚舉。
區(qū)別在于參考語義與價(jià)值語義。 對(duì)基于類的樹進(jìn)行更改將更新內(nèi)存中的同一個(gè)實(shí)例。 但是基于枚舉的樹是不可變的 - 任何插入或刪除都會(huì)給你一個(gè)全新的樹的副本。 哪一個(gè)最好完全取決于你想要使用哪個(gè)。
這里是如何使用枚舉二叉搜索樹:
public enum BinarySearchTreeEnum<T: Comparable> {
case Empty
case Leaf(T)
indirect case Node(BinarySearchTreeEnum<T>, T, BinarySearchTreeEnum<T>)
}
枚舉有三種情況:
-
Empty空標(biāo)記分支的結(jié)束(基于類的版本為此使用了nil引用)。 -
Leaf葉為沒有孩子的葉節(jié)點(diǎn)。 -
Node具有一個(gè)或兩個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)的節(jié)點(diǎn)。 這是使用關(guān)鍵字indirect,以便它可以保存BinarySearchTree值。 沒有indirect,你不能使遞歸枚舉。
注意: 此二叉樹中的節(jié)點(diǎn)沒有對(duì)其父節(jié)點(diǎn)的引用。 這不是一個(gè)重要的障礙,但它會(huì)使某些操作略為繁瑣。
像往常一樣,我們將遞歸地實(shí)現(xiàn)大多數(shù)功能。 我們將對(duì)每個(gè)枚舉的情況略有不同。 例如,這是如何計(jì)算樹中的節(jié)點(diǎn)數(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è)全新的樹對(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
}
}
}
如你所見,這些函數(shù)中的大多數(shù)具有相同的結(jié)構(gòu)。
在 playground 中試試看:
tree.search(10)
tree.search(1)
tree.search(11) // nil
要打印樹以進(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)在中間; . 表示在該位置沒有子節(jié)點(diǎn)。
當(dāng)樹變得不平衡...
當(dāng)二叉搜索樹的左和右子樹包含大致相同數(shù)量的節(jié)點(diǎn)時(shí),它是平衡的。 在這種情況下,樹的高度是 log(n),其中 n 是節(jié)點(diǎn)的數(shù)量。 這是理想的情況。
然而,如果一個(gè)分支比另一個(gè)分支長得多,搜索將變得非常慢。 我們最終檢索比我們理想情況下更多的值。 在最壞的情況下,樹的高度可以變?yōu)?n 。 這樣的樹比二叉搜索樹更像鏈表,性能降級(jí)到 O(n) 。 這可不好!
使二叉搜索樹平衡的一種方法是以完全隨機(jī)的順序插入節(jié)點(diǎn)。 平均來說,應(yīng)該可以保持樹平衡。 但它不保證成功,也不總是實(shí)用。
另一個(gè)解決方案是使用自平衡二叉樹。 此類型的數(shù)據(jù)結(jié)構(gòu)會(huì)在插入或刪除節(jié)點(diǎn)后調(diào)整樹以保持平衡。 有關(guān)示例,請(qǐng)參閱AVL樹和紅黑樹。
也可以看看
由 Nicolas Ameghino 和 Matthijs Hollemans 寫的Swift算法俱樂部。