作者: 负雪明烛
id: fuxuemingzhu
个人博客: http://fuxuemingzhu.cn/
公众号:负雪明烛
本文关键词:Leetcode, 力扣,211,搜索单词,前缀树,字典树,Trie,Python, C++, Java


题目地址:https://leetcode.com/problems/add-and-search-word-data-structure-design/description/

题目描述

Design a data structure that supports the following two operations:

  1. void addWord(word)
  2. bool search(word)

search(word) can search a literal word or a regular expression string containing only letters a-z or … A . means it can represent any one letter.

  1. For example:
  2. addWord("bad")
  3. addWord("dad")
  4. addWord("mad")
  5. search("pad") -> false
  6. search("bad") -> true
  7. search(".ad") -> true
  8. search("b..") -> true

Note:

  • You may assume that all words are consist of lowercase letters a-z.

题目大意

设计数据结构能接受正则查找。该正则的设计的是 '.' 匹配任意字符。

解题思路

本文写成前缀树入门教程。

从二叉树说起

前缀树(Trie,字典树),也是一种树。为了理解前缀树,我们先从「二叉树」说起。

常见的二叉树结构是下面这样的:

  1. class TreeNode {
  2. int val;
  3. TreeNode* left;
  4. TreeNode* right;
  5. }

可以看到一个树的节点包含了三个元素:该节点本身的值,左子树的指针,右子树的指针

二叉树可视化是下面这样的:


二叉树的每个节点只有两个孩子,那如果每个节点可以有多个孩子呢?这就形成了「多叉树」。多叉树的子节点数目一般不是固定的,所以会用变长数组来保存所有的子节点的指针。多叉树的结构是下面这样:

  1. class TreeNode {
  2. int val;
  3. vector<TreeNode*> children;
  4. }

多叉树可视化是下面这样:

对于普通的多叉树,每个节点的所有子节点可能是没有任何规律的。而本题讨论的「前缀树」就是每个节点的 children 有规律的多叉树。

前缀树

前缀树」是一种特殊的多叉树,它的 TrieNodechidren 是一个大小为 26 的一维数组(当输入只有小写字符),分别对应了26个英文字符 'a' ~ 'z',也就是说形成了一棵「26 叉树」。

前缀树的结构可以定义为下面这样:

  1. class TrieNode {
  2. public:
  3. vector<TrieNode*> children;
  4. bool isWord;
  5. TrieNode() : isWord(false), children(26, nullptr) {
  6. }
  7. ~TrieNode() {
  8. for (auto& c : children)
  9. delete c;
  10. }
  11. };

TrieNode 里面存储了两个信息:

  • children 是该节点的所有子节点。
  • isWord 表示从根节点到当前节点为止,该路径是否形成了一个有效的字符串。

构建

在构建前缀树的时候,按照下面的方法:

  • 根节点不保存任何信息;
  • 关键词放到「前缀树」时,需要把它拆成各个字符,每个字符按照其在 'a' ~ 'z' 的序号,放在 chidren 对应的位置里面。下一个字符是当前字符的子节点。
  • 一个输入字符串构建「前缀树」结束的时候,需要把该节点的 isWord 标记为 true,说明从根节点到当前节点的路径,构成了一个关键词。

下面是一棵「前缀树」,其中保存了 {"am", "an", "as", "b", "c", "cv"} 这些关键词。图中红色表示 isWordtrue

看下面这个图的时候需要注意:

  1. 所有以相同字符开头的字符串,会聚合到同一个子树上。比如 {"am", "an", "as"}
  2. 并不一定是到达叶子节点才形成了一个关键词,只要 isWordtrue,那么从根节点到当前节点的路径就是关键词。比如 {"c", "cv"}

有些题解把字符画在了节点中,我认为是不准确的。因为前缀树是根据 字符在 children 中的位置确定子树,而不真正在树中存储了 'a' ~ 'z' 这些字符。树中每个节点存储的 isWord,表示从根节点到当前节点的路径是否构成了一个关键词。

查询

在判断一个关键词是否在「前缀树」中时,需要依次遍历该关键词所有字符,在前缀树中找出这条路径。可能出现三种情况:

  1. 在寻找路径的过程中,发现到某个位置路径断了。比如在上面的前缀树图中寻找 "d" 或者 "ar" 或者 "any" ,由于树中没有构建对应的节点,那么就查找不到这些关键词;
  2. 找到了这条路径,但是最后一个节点的 isWordfalse。这也说明没有该关键词。比如在上面的前缀树图中寻找 "a"
  3. 找到了这条路径,并且最后一个节点的 isWordtrue。这说明前缀树存储了这个关键词,比如上面前缀树图中的 "am" , "cv" 等。

应用

上面说了这么多前缀树,那前缀树有什么用呢?

其实我们生活中就有应用。

  • 比如我们常见的电话拨号键盘,当我们输入一些数字的时候,后面会自动提示以我们的输入数字为开头的所有号码。
  • 比如我们的英文输入法,当我们输入半个单词的时候,输入法上面会自动联想和补全后面可能的单词。
  • 再比如在搜索框搜索的时候,输入"负雪",后面会联想到 负雪明烛

等等。

代码

本题是前缀树的变种: '.' 可以表示任何一个小写字符。

在匹配的过程中,如果遇到了 '.' ,则需要对当前节点的所有子树都进行遍历,只要有任何一个子树能最终匹配完成,那么就代表能匹配完成。

代码中的 match() 函数表示在以 root 为根节点的前缀树中,能不能匹配到 word[index:]

下面的 Python 解法和 C++ 解法定义的前缀树略有不同:

  • Python 解法中,保存 children 是使用的字典,它保存的结构是 {字符:Node} ,所以可以直接通过 children['a'] 来获取当前节点的 'a' 子树。
  • C++ 解法中,保存 children 用的题解分析时讲的大小为 26 的数组实现的。而且我的 C++ 解法中写出了很多人容易忽略的一个细节,就是 TrieNode 析构的时候,需要手动释放内存。

Python 代码如下:

  1. class Node(object):
  2. def __init__(self):
  3. self.children = collections.defaultdict(Node)
  4. self.isword = False
  5. class WordDictionary(object):
  6. def __init__(self):
  7. """
  8. Initialize your data structure here.
  9. """
  10. self.root = Node()
  11. def addWord(self, word):
  12. """
  13. Adds a word into the data structure.
  14. :type word: str
  15. :rtype: void
  16. """
  17. current = self.root
  18. for w in word:
  19. current = current.children[w]
  20. current.isword = True
  21. def search(self, word):
  22. """
  23. Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter.
  24. :type word: str
  25. :rtype: bool
  26. """
  27. return self.match(word, 0, self.root)
  28. def match(self, word, index, root):
  29. if root == None:
  30. return False
  31. if index == len(word):
  32. return root.isword
  33. if word[index] != '.':
  34. return root != None and self.match(word, index + 1, root.children.get(word[index]))
  35. else:
  36. for child in root.children.values():
  37. if self.match(word, index + 1, child):
  38. return True
  39. return False
  40. # Your WordDictionary object will be instantiated and called as such:
  41. # obj = WordDictionary()
  42. # obj.addWord(word)
  43. # param_2 = obj.search(word)

C++ 代码如下:

  1. class TrieNode{
  2. public:
  3. vector<TrieNode*> child;
  4. bool isWord;
  5. TrieNode() : child(26, nullptr), isWord(false) {};
  6. ~TrieNode() {
  7. for (auto c : child) delete c;
  8. }
  9. };
  10. class WordDictionary {
  11. public:
  12. /** Initialize your data structure here. */
  13. WordDictionary() {
  14. root = new TrieNode();
  15. }
  16. ~WordDictionary() {
  17. delete root;
  18. }
  19. /** Adds a word into the data structure. */
  20. void addWord(string word) {
  21. TrieNode* p = root;
  22. for (char c : word) {
  23. int i = c - 'a';
  24. if (!p->child[i])
  25. p->child[i] = new TrieNode();
  26. p = p->child[i];
  27. }
  28. p->isWord = true;
  29. }
  30. /** Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter. */
  31. bool search(string word) {
  32. return match(word, root, 0);
  33. }
  34. bool match(string& word, TrieNode* p, int start) {
  35. if (!p) return false;
  36. if (start == word.size()) return p->isWord;
  37. char c = word[start];
  38. if (c != '.') {
  39. return match(word, p->child[c - 'a'], start + 1);
  40. } else {
  41. for (const auto& child : p->child) {
  42. if (match(word, child, start + 1))
  43. return true;
  44. }
  45. }
  46. return false;
  47. }
  48. private:
  49. TrieNode* root;
  50. };
  51. /**
  52. * Your WordDictionary object will be instantiated and called as such:
  53. * WordDictionary obj = new WordDictionary();
  54. * obj.addWord(word);
  55. * bool param_2 = obj.search(word);
  56. */

复杂度分析

  • 时间复杂度:添加单词为

    O

    (

    )

    O(字符串长度)

    O(字符串长度),查询为

    O

    (

    2

    6

    )

    O(26 ^ {字符串长度})

    O(26字符串长度)。

  • 空间复杂度:

    O

    (

    26

    )

    O(所有添加了单词的字符串长度 * 26)

    O(所有添加了单词的字符串长度∗26)。

刷题心得

  • 前缀树是挺有意思的应用。
  • 不过面试和力扣题目都考察不多,建议大家理解掌握,不必深究。

类似题目:

参考资料:


祝大家 AC 多多,Offer 多多!我们明天再见!

日期

2018 年 2 月 27 日
2018 年 12 月 22 日 —— 今天冬至
2021 年 10 月 19 日

【LeetCode】211. Add and Search Word - Data structure design 添加与搜索单词 - 数据结构设计的更多相关文章

  1. [LeetCode] 211. Add and Search Word - Data structure design 添加和查找单词-数据结构设计

    Design a data structure that supports the following two operations: void addWord(word) bool search(w ...

  2. 211 Add and Search Word - Data structure design 添加与搜索单词 - 数据结构设计

    设计一个支持以下两个操作的数据结构:void addWord(word)bool search(word)search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z . ...

  3. Leetcode211. Add and Search Word - Data structure design 添加与搜索单词 - 数据结构设计

    设计一个支持以下两种操作的数据结构: void addWord(word) bool search(word) search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a- ...

  4. [LeetCode] Add and Search Word - Data structure design 添加和查找单词-数据结构设计

    Design a data structure that supports the following two operations: void addWord(word) bool search(w ...

  5. [leetcode]211. Add and Search Word - Data structure design添加查找单词 - 数据结构设计

    Design a data structure that supports the following two operations: void addWord(word) bool search(w ...

  6. Java for LeetCode 211 Add and Search Word - Data structure design

    Design a data structure that supports the following two operations: void addWord(word)bool search(wo ...

  7. (*medium)LeetCode 211.Add and Search Word - Data structure design

    Design a data structure that supports the following two operations: void addWord(word) bool search(w ...

  8. leetcode@ [211] Add and Search Word - Data structure design

    https://leetcode.com/problems/add-and-search-word-data-structure-design/ 本题是在Trie树进行dfs+backtracking ...

  9. leetcode 211. Add and Search Word - Data structure design Trie树

    题目链接 写一个数据结构, 支持两种操作. 加入一个字符串, 查找一个字符串是否存在.查找的时候, '.'可以代表任意一个字符. 显然是Trie树, 添加就是正常的添加, 查找的时候只要dfs查找就可 ...

随机推荐

  1. Linux—yum的python版本错误——初级解决方案

    为了安装rrdtool,发现不是少这个就是少那个,最后发现yum也不能用. 从网上找的解决yum问题. 转自:http://doarthon.blog.51cto.com/3175384/728809 ...

  2. 学习Java的第十八天

    一.今日收获 1.java完全学习手册第三章算法的3.1比较值 2.看哔哩哔哩上的教学视频 二.今日问题 1.在第一个最大值程序运行时经常报错. 2.哔哩哔哩教学视频的一些术语不太理解,还需要了解 三 ...

  3. Tomcat中的Server.xml配置详解

    Tomcat中的Server.xml配置详解 Tomcat Server的结构图如下: 该文件描述了如何启动Tomcat Server <Server> <Listener /> ...

  4. 双向链表——Java实现

    双向链表 链表是是一种重要的数据结构,有单链表和双向链表之分:本文我将重点阐述不带头结点的双向链表: 不带头结点的带链表 我将对双链表的增加和删除元素操作进行如下解析 1.增加元素(采用尾插法) (1 ...

  5. ReactiveCocoa操作方法-重复

    retry重试      只要失败,就会重新执行创建信号中的block,直到成功. __block int i = 0; [[[RACSignal createSignal:^RACDisposabl ...

  6. 深入 char

    深入 char * ,char ** ,char a[ ] ,char *a[] 内核分类: c语言 2013-02-23 15:34 15176人阅读 评论(8) 收藏 举报Charcharchar ...

  7. MyBatis绑定Mapper接口参数到Mapper映射文件sql语句参数

    一.设置paramterType 1.类型为基本类型 a.代码示例 映射文件: <select id="findShopCartInfoById" parameterType ...

  8. 深度解析Spring Cloud Ribbon的实现源码及原理

    Ribbon的核心作用就是进行请求的负载均衡,它的基本原理如下图所示.就是客户端集成Ribbon这个组件,Ribbon中会针对已经配置的服务提供者地址列表进行负载均衡的计算,得到一个目标地址之后,再发 ...

  9. C#中继承和多态

    1.继承的概念 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用已存在的类的功能. 为了提高软件模块的可复用性和可扩充性,以便提高软件的开发效率,我们总 ...

  10. Mysql脚本 生成测试数据

    使用: ./xie.sh -uroot -p'123456' #!/bin/bash #混合测试数据库脚本 #将创建一个single数据库,其中创建一个s1表 #如果数据库存在,将会写入数据,可以在写 ...