Swift实现二叉搜索树与字典树

二叉搜索树

二叉搜索树的性质如下:

  • 若任意节点的左子树不空,则左子树所有节点的值小于根节点的值
  • 若任意节点的右子树不空,则左子树所有节点的值大于根节点的值
  • 任意节点的左右子树也为二叉搜索树
  • 没有键值相等的节点

其基本操作有如下四个

  • empty——返回一个空的二叉搜索树
  • isEmpty——检查二叉搜索树是否为空
  • contains——检查二叉搜索树是否包含某元素
  • insert——向二叉搜索树插入元素

具体实现过程

  • 用一个递归枚举定义二叉搜索树的数据结构
indirect enum BinarySearchTree<E:Comparable>{ // indirect 表明是递归枚举
    case Leaf
    case Node(BinarySearchTree<E>, E, BinarySearchTree<E>)
}

通过关联值指定一个节点的左右子树及其自身。

  • 初始化方法,可以初始化一个空树或者一个根节点有值的树
    init(){
        self = .Leaf
    }
    
    init(value:E) {
        self = .Node(.Leaf, value, .Leaf)
    }
  • 打印所有元素的方法
    var elements:[E] {
        switch self {
        case .Leaf:
            return []
        case let .Node(left, x, right):
            return left.elements + [x] + right.elements
        }
    }

这个方法可以按照中序排列方式打印出所有元素

  • 判断是否为空以及是否为二叉搜索树的方法
    var isEmpty:Bool {
        if case .Leaf = self {
            return true
        }
        return false
    }
    
    var isBST:Bool {
        switch self {
        case .Leaf:
            return true
        case let .Node(left, x, right):
            return left.elements.reduce(true, { (result, y) -> Bool in
                result && (y < x)
            }) && right.elements.reduce(true, { (result, y) -> Bool in
                result && (y > x)
            }) && left.isBST && right.isBST
        }
    }

其中 isBST 方法会根据二叉搜索树的前三个特性来检测一个树。

  • 检测是否包含某元素的方法
    func contains(x:E) -> Bool {
        switch self {
        case .Leaf:
            return false
        case let .Node(_, y, _) where x == y:
            return true
        case let .Node(left, y, _) where x < y:
            return left.contains(x: x)
        case let .Node(_, y, right) where x > y:
            return right.contains(x: x)
        default:
            fatalError()
        }
    }

如果树是空的,则 x 不在树中,返回 false。

如果树不为空,且储存在根节点的值与 x 相等,返回 true。

如果树不为空,且储存在根节点的值大于 x,那么如果 x 在树中的话,它一定是在左子树中,所以,我们在左子树中递归搜索 x。

类似地,如果根节点的值小于 x,我们就在右子树中继续搜索。

  • 插入一个元素
    mutating func insert(x:E) {
        switch self {
        case .Leaf:
            self = BinarySearchTree.init(value: x)
        case .Node(var left, let y, var right):
            if x < y {left.insert(x: x)}
            if x > y {right.insert(x: x)}
            self = .Node(left, y, right)
        }
    }

由于需要对二叉搜索树进行修改,所以该方法用到了 mutating 来修饰。方法递归遍历二叉搜索树,直到找到一个空叶子节点就加入。

使用如下

        var binaryTree = BinarySearchTree.init(value: 5)
        binaryTree.insert(x: 1)
        binaryTree.insert(x: 2)
        binaryTree.insert(x: 3)
        binaryTree.insert(x: 4)
        binaryTree.insert(x: 5)
        binaryTree.insert(x: 6)
        binaryTree.insert(x: 7)
        binaryTree.insert(x: 8)
        binaryTree.insert(x: 9)
        binaryTree.insert(x: 10)
        print(binaryTree.elements)
        // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

字典树

字典树 Trie 是一种有序树,用于保存关联数组,通常是字符串,字典树的键不是直接保存在节点里,而是由一串节点的值组合而成。

<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/be/Trie_example.svg/500px-Trie_example.svg.png" width=350>

具体实现过程

  • 用结构体定义一个字典树的数据结构
struct Trie<Element:Hashable>{
    var children:[Element:Trie]
    var isElement: Bool
}

其中 children 就是一个字典,值是一个子字典树,键是子树的前缀值,而 isElement 定义了当前节点为止的组合是否是一个元素

<img src="https://github.com/julycoding/The-Art-Of-Programming-By-July/raw/master/ebook/images/8/8.4/2.gif" width=350>

例如上图中,“at” 节点的 isElement 就是 true,而 ag 由于不是一个有意义的单词所以 isElement 为 false。

  • 初始化方法

首先是两个基础的初始化方法

    init() {
        isElement = false
        children = [:]
    }
    
    init(isElement:Bool, children:[Element:Trie]) {
        self.isElement = isElement;
        self.children = children;
    }

但是正常使用过程中,我们更需要一个能接受一个数组作为入参的初始化方法,然后形成一个单元素的字典树。

此时我们首先需要实现一个数组的递归方式,即从最原始的数组逐渐去除一定数量的元素形成子数组,我们将其定义为 Array 的一个拓展方法

extension Array {
    var decompose: (Element, [Element])? {
        return isEmpty ? nil : (self[startIndex], Array(self.dropFirst()))
    }
}

可以看到,如果数组为空则返回 nil,否则返回一个元组,元组第一个元素是当前数组的头元素,第二个元素是去掉第一个元素之后由该数组其余元素组成的新数组。我们可以通过重复调用一个数组的 decompose 方法递归地遍历一个数组。

接下来就可以定义一个接收数组参数的初始化方法了

    init(key:[Element]) {
        if let (head, tail) = key.decompose {
            let children = [head: Trie.init(key: tail)]
            self = Trie(isElement: false, children: children)
        } else {
            self = Trie(isElement: true, children: [:])
        }
    }

可以看到它遍历传入的数组,生成以数组中元素为节点的单元素字典树。

  • 插入操作
    func insert(key:[Element]) -> Trie<Element> {
        guard let (head, tail) = key.decompose else {
            return Trie.init(isElement: true, children: self.children)
        }
        var newChildren = self.children
        if let nextTrie = self.children[head] {
            newChildren[head] = nextTrie.insert(key: tail)
        } else {
            newChildren[head] = Trie.init(key: tail)
        }
        return Trie.init(isElement: isElement, children: newChildren)
    }

遍历传入的数组并检查每一个节点的 children 键组。

如果键组为空,我们将 isElement 设置为 true,然后不再修改剩余的字典树,表明已经插入完毕了。

如果键组不为空,且键组的 head 已经存在于当前节点的 children 字典中,我们只需要递归地调用该函数,将键组的 tail 插入到对应的子字典树中。

如果键组不为空,且第一个键 head 并不是该字典树中 children 字典的某条记录,就创建一棵新的字典树来储存键组中剩下的键。然后,以 head 键和 tail 数组生成对应新的字典树,储存在当前节点中,完成插入操作。

然后我们就可以定义一个方法来将一个关联数组组合成字典树

func buildStringTrie(words:[String]) -> Trie<Character> {
    let emptyTrie = Trie<Character>.init()
    return words.reduce(emptyTrie, { (result, word) -> Trie<Character> in
        result.insert(key: Array.init(word))
    })
}
  • 打印方法,打印出字典树中包含的所有关联元素
    var elements: [[Element]] {
        var result: [[Element]] = isElement ? [[]] : []
        for (key, value) in children {
            result += value.elements.map { [key] + $0 }
        }
        return result
    }

这个方法很精巧,同时也很难懂,首先需要注意下面一些操作的结果

[] + [["r"]] = [["r"]]
[[]] + [["r"]] = [[], ["r"]]

对于一个嵌套的二维数组,直接加一个空的一维数组,这个空的一维数组就被加入为二维数组的一个元素,而两个嵌套的二维数组相加,则会合并为一个二维数组。

现在来看这段代码,它的思路其实是:

  • 对于 isElement 为 true 的节点,返回类似 [[]] 或 [[], ["X"]] 的数组,里面一定包含一个空数组,开始将此节点回溯的所有父节点组合为元素
  • 对于 isElement 为 false 的节点,由于它一定是某个 isElement 为 true 的节点的父节点,所以返回类似 [["X"], ["Y", "Z"]] 的数组,不会生成多余的数组来组合新的元素
  • 依次遍历,最终生成包含所有元素的数组

让我们用一个简单的例子来测试,首先用 ["cart", "car"] 数组来生成一个字典树,然后在 elements 过程中进行打印

{
        let contents = ["cart", "car"]
        let trie = buildStringTrie(words:contents)
        print(trie.elements)
}

{
    var elements: [[Element]] {
        var result: [[Element]] = isElement ? [[]] : []
        for (key, value) in children {
            result += value.elements.map { [key] + $0 }
        }
        print(result)
        return result
    }
}

最终结果如下

[[]]
[[], ["t"]]
[["r"], ["r", "t"]]
[["a", "r"], ["a", "r", "t"]]
[["c", "a", "r"], ["c", "a", "r", "t"]]
[["c", "a", "r"], ["c", "a", "r", "t"]]
  • 检索某个字符串是否在字典树的方法和检索某个前缀对应的字典树的方法
    func lookup(key: [Element]) -> Bool {
        guard let (head, tail) = key.decompose else { return isElement }
        guard let subtrie = children[head] else { return false }
        return subtrie.lookup(key: tail)
    }
    
    func withPrefix(prefix:[Element]) -> Trie<Element>?{
        guard let (head, tail) = prefix.decompose else {return self}
        guard let remainder = children[head] else {return nil}
        return remainder.withPrefix(prefix: tail)
    }

思路大致一样,遍历传入的数组,当遍历结束时返回结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容