Swift算法俱樂(lè)部中文版 -- 二叉搜索樹(shù)

提示:本篇的原文已經(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ù):

  1. 按順序(或深度優(yōu)先):首先查看節(jié)點(diǎn)的左子節(jié)點(diǎn),然后查看節(jié)點(diǎn)本身,最后查看其右子節(jié)點(diǎn)。
  2. 按節(jié)點(diǎn):先看一個(gè)節(jié)點(diǎn),然后看它的左右子節(jié)點(diǎn)。
  3. 反序: 先看左右子節(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,rightparent 是可選的,所以我們可以很好地利用 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í),如果 leftnil ,它將自動(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)行更改涉及更改一系列 parentleftright 節(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ù)。

也可以看看


維基百科上的二叉搜索樹(shù)

Nicolas Ameghino 和 Matthijs Hollemans 寫(xiě)的Swift算法俱樂(lè)部。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 樹(shù)的概述 樹(shù)是一種非常常用的數(shù)據(jù)結(jié)構(gòu),樹(shù)與前面介紹的線性表,棧,隊(duì)列等線性結(jié)構(gòu)不同,樹(shù)是一種非線性結(jié)構(gòu) 1.樹(shù)的定...
    Jack921閱讀 4,484評(píng)論 1 31
  • 基于樹(shù)實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu),具有兩個(gè)核心特征: 邏輯結(jié)構(gòu):數(shù)據(jù)元素之間具有層次關(guān)系; 數(shù)據(jù)運(yùn)算:操作方法具有Log級(jí)的平...
    yhthu閱讀 4,315評(píng)論 1 5
  • 1 概述 二叉搜索樹(shù),顧名思義,其主要目的用于搜索,它是二叉樹(shù)結(jié)構(gòu)中最基本的一種數(shù)據(jù)結(jié)構(gòu),是后續(xù)理解B樹(shù)、B+樹(shù)、...
    CodingTech閱讀 3,141評(píng)論 5 35
  • 二叉搜索樹(shù),平衡樹(shù),B,b-,b+,b*,紅黑樹(shù) 二叉搜索樹(shù) ? 1.所有非葉子結(jié)點(diǎn)至多擁有兩個(gè)兒子(Le...
    raincoffee閱讀 3,907評(píng)論 3 18
  • 入住酒店,晚間洗澡的時(shí)候發(fā)現(xiàn)浴室里的瓶子上寫(xiě)著三效合一:洗發(fā),護(hù)發(fā),沐浴。同住的好友直說(shuō)這酒店也太不上心了,這...
    西瓜x(chóng)igu閱讀 559評(píng)論 0 0