Swift 算法俱乐部: Trie

好久没更新Blog了,真是一毕业就克制不住自己的懒惰,以后要做个不仅高产,而且有优质文章的“农民”。加油啦,新起点就尝试翻译以下Swift Algorithm Club的文章,算法很重要,Swift是一门很优雅的语言,要跟上节奏,夯实基础,学习新知😂,不扯皮了。

翻译自:https://www.raywenderlich.com/139410/swift-algorithm-club-swift-trie-data-structure

开始

字符存储在树的每个节点的 n-元(n-ary)树称作单词查找树。单词查询树是一种可以促进英文高效地前缀匹配的键值结构。

SwiftAlgClub_TrieData-trie-1.png

上述例子:“Cat”,“Cut”,“Cute”,“To”,“B”

为什么使用单词查找树

单词查找树对某些情况非常实用。除了非常好地存储英文,它同样可以作为‘哈希表’的替代, 它有以下几个优势:

  • 查找值时通常有一个更好的最坏的时间复杂度
  • 不像哈希表,单词查找树不用担心键冲突问题
  • 不需要哈希算法保证每个元素有一个独一无二的路径
  • 单词树可以按照字母顺序排序

实现

你的实现将由一个TrieNode和一个Trie类构成,每个TrieNode代表单词 一个字符。例如,“cute”将有以下的节点构成:c->u->t->e。Trie将管理节点的插入逻辑并且一直关联着这些节点

我们首先实现简单的TrieNode类

TrieNode

class TrieNode<T: Hashable> {
    var value: T?
    weak var parent: TrieNode?
    var children: [T: TrieNode] = [:]
    init(value: T? = nil, parent: TrieNode? = nil) {
        self.value = value
        self.parent = parent
    }
}

上面是TrieNode类的一般实现。它存储着一个值(对于英文而言就是字符)并且有一个父节点和孩子们子节点的引用。这里有以下几点需要注意:

  • class TrieNode<T:Hashable> ,泛型类型,这说明TrieNode适用于任何遵循Hashable协议的类型

  • parent节点使用weak,防止出现循环引用问题,因为对于某个节点而言它的父节点中的children数组肯定包含该节点, 如果这里在使用默认的strong引用父节点就出现了循环引用。某个节点包含父类的引用是必须的,对于移除操作remove会使用的到

  • TrieNode必须遵循Hashable协议。因为你将会使用这个值作为children字典的key,在Swift中只要能作为能作为字典的key就必需遵循Hashable协议

接下来在TrieNode中添加add操作

func add(child: T) {
    // 1. 确保插入的“字符”子节点在当前子节点数组中不存在
    guard children[child] == nil else { return }
    // 2. 创建一个新节点将传入的值插入
    children[child] = TrieNode(value: child, parent: self)
 }

Trie

Trie类的职责是管理节点。

class Trie {
    fileprive let root: TrieNode<Character>
    init() {
        root = TrieNode<Character>()
    }
}

以上就是Trie类的基础,声明一个root属性去关联你的 ”字典树“ 的根节点。因为我们要实现一个特定于英文的“字典树”所以我们使用的节点类型是Character,它是Hashable的子协议,初始化方法实现一个简单的初始化实现一个空节点

Typealiasing

再继续往下之前将Trie类做如下更新:

class Trie {
    typealias Node = TrieNode<Character>
    fileprivate let root: Node
    init() {
        root = TrieNode<Character>()
    }
}

上面添加了一个Node typealias, 它允许你使用Node去替代TrieNode,除了能够缩短语法,他还可以使程序变得更加“健壮”;

Insertion

Trie类管理者“单词树”Trie的操作,当实现insertion方法时,记住Trie是高效的,因为它总是使用已经存在的节点去完成一个排序。例如有两个单词“Cut”,“Cute”,仅需要使用4个节点,因为“Cut”是“Cute”的前缀

extension Trie {
    func insert(word: String) {
        // 如果word是个空字符串,直接返回
        guard !word.isEmpty else {
            return
        }
        // 将从根节点开始执行迭代
        var currentNode = root
        // 每个单词在Trie中都表现为一连串的字符节点
        // 例如cute:c->u->t->e
        // 因为你要一个一个地插入字符,将单词装换成一个字符数组能更方便地保留字符的插入的轨迹
        let charcters = Array(word.lowercased().characters)
        var currentIndex = 0
        //TODO:
        while currentIndex < charcters.count {
            // 得到当前将要插入的字符
            let character = charcters[currentIndex]
            // 判断该字符是否在当前节点的“孩子们”子节点数组中是否存在
            if let child = currentNode.children[character] {
                currentNode = child // 如果存在,只需简单地将当前节点移动到这个存在的节点即可开始下一个字符的插入操作
            } else {
                // 如果不存在, 那就添加个,并移动当前节点的指针到新创建的节点上
                currentNode.add(child: character)
                currentNode = currentNode.children[character]!
            }
            currentIndex += 1
        }
    }
}
SwiftAlgClub_TrieData-trie-2.png

Teminating Nodes

到现在为止,insert方法可以很好到执行插入操作。但是这里会有个问题,例如如果你插入的是“cute”,该这样确定“cut”已经存在呢??

如果没有一些分类的指示器好像很难去确定。回到TrieNode,添加如下属性

var isTerminating = false   // 负责指示一个单词是否结束

回到“cute”那个例子,如果插入“cute”,isTerminating是这样标识的

SwiftAlgClub_TrieData-trie-3.png

小黑点标识cute的结束;接下来如果插入“cut”就会将“t”标识为结束点

SwiftAlgClub_TrieData-trie-4.png

在insert的while中添加如下代码

 // 如果currentIndex等于单词字符的数量,就说明已经到达单词的结尾,可以将结束标识置为true
if currentIndex == characters.count {
    currentNode.isTerminating = true
}

Contains

接下来处理contains函数,这个方法的职责是检查单词是否存在

func contains(word: String) -> Bool {
      guard !word.isEmpty else {
          return false
      }
      var currentNode = root
      let characters = Array(word.lowercased().characters)
      var currentIndex = 0
      // 这里将尝试遍历基于传递的word字符的节点

      // 1. 创建一个while循环, 有两个条件
      // (1). 没有到达单词的结尾
      // (2). 存在字符对应的节点
      while currentIndex < characters.count,
          let child = currentNode.children[characters[currentIndex]] {
              // 当while成功执行结束时,currentNode将会指向最终的节点, currentIndex并且是字符串的长度
              currentIndex += 1
              currentNode = child
      }
      // 成功遍历完所有的字符,并且该结束节点是某个字符串的结尾,则返回true, 否则返回false
      if currentIndex == characters.count &&
          currentNode.isTerminating {
          return true
      } else {
          return false
      }
 }

至此Trie完成
源码:https://github.com/X-Liang/Swift-Algorithm

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

推荐阅读更多精彩内容