当前位置:   article > 正文

字典树(Trie/前缀树)

字典树

目录

字典树的概念

字典树的逻辑

字典树的实现

易混点剖析

代码示例


字典树的概念

字典树(Trie)是一种空间换时间的数据结构,是一棵关于“字典”的树。主要用于统计、排序和保存大量的字符串。字典树是通过利用字符串的公共前缀来节约存储空间,因此字典树又叫前缀树。字典树是对于字典的一种存储方式。这个词典中的每个“单词”就是从根节点出发一直到某一个目标节点的路径,路径中每条边的字母连起来就是一个单词。

字典树的逻辑

在学习字典树之前我们先来看这样一个问题:

给你n个单词,并进行x次查找,每次查找随机一个单词word。

问:word是否出现在这n个单词中?

想想看,这像不像平常我们查字典?比如要在字典中查找单词“abandon”,我们一般是找到首字母为'a'的部分,然后再找第二个单词为‘b’的部分······如果字典中存在这个单词,那么我们最后就可以找到这个单词,反之则不能。

接下来,我们通过图解存储了单词{“a”,"abc",“bac”,“bbc”,"ca" }的字典树对上方内容进行解释:

c29ad59fcd7e47ae8d15b0eaaee39f3c.png

非原图,如有侵权,请联系作者

从这张图我们可以看出,这棵字典树的每条边上都有一个字母(可以类比查字典时查找的第k个字母来理解),并且这棵树的一些节点被指定成了标记节点,用于表示到此为止是一个完整的单词。

字典树的实现

通过上面的图不难发现,字典树并不是二叉树,所以字典树的结构定义和二叉树的并不同,这里不要惯性思维了。通常我们定义多叉树时常用孩子表示法,像这样

  1. struct TreeNode {
  2. ELEMENT_TYPE value; //结点值
  3. TreeNode* children[NUM]; //孩子结点
  4. };

字典树也类似:

  1. struct TrieNode {
  2. int isEnd; //该结点是否是一个串的结束
  3. TrieNode* children[SIZE]; //字母映射表
  4. };

常规的多叉树类型一般是,一个成员存放当前节点的内容,如上方的value成员;一个成员存放所有孩子节点的地址,如上方的children[NUM]成员,其数组中存放的是一个个TreeNode*类型的孩子节点的地址。

而字典树却与其有些不同,字典树的一个成员存放的是boo/intl型的isEnd成员,用于判断当前节点是否为一个单词的结束,如果是就标记为true,这样做的目的是在查找的时候方便确定找到最后是不是一个单词,因为并不是一个单词中所有的子串都是一个单词,例如”abandon“是一个单词,而其子串“ab“就不是一个单词。其另一个成员存放的也是一个指向孩子节点的指针,与常规的多叉树理解类似。

这里也许你会发现一些”端倪“:字典树的结构里好像并没有用于存储节点内容的成员,比如常规多叉树中的value成员。其实就是没有,但这并不妨碍我们实现这个功能,例如我们暂定字典中只有小写的26个字母,那么字典树的children[SIZE] 成员中SIZE就可以设置为26,那么我们就可以存储,那么我们将要查找或是存储的单词每一个字母(的ASCII值)减去一个'a' (字符a),那么其对应的值,就是我们要存放的children数组下标。了解过哈希表(散列表)的同学肯定会有一种茅塞顿开的感觉,因为这里其实就是用的哈希表的思路。下标为k的元素如果不为空(数组中的每一个元素都是一个指针)就说明这个元素有孩子节点,进而说明这个元素对应的字母是存在的。其中,一般第n层的children数组的下标k位置如果不为空,就表示这个单词中的第n个位置存在k下标位置所对应的字母。例如包含单词 "sea","sells","she" 的字典树图解如下:

6ec930c8eff14c7c8787eeec0a15dddb.png

非原图,如有侵权,请联系作者


在这里,我们还可以试着讨论一下为什么这个children成员用数组实现。(这一小部分内容与字典树的核心内容并没有多大关系,如果没有兴趣可以直接跳过)

相信有部分人(比如我)第一次接触字典树时对于

TrieNode* children[SIZE]

这种的定义是很难认同的,因为我们可能会觉得这会浪费很多空间,为什么不用链表等其它顺序结构来实现呢?

        首先,如果将数组替换为链表或其它线性结构,那么实现起来会非常繁琐,而且即使实现了,每次查找字母时都要进行一次链表的遍历,相较于数组的直接下标访问,是一个很费时的过程。

        其次,这里的数组其实就是一个哈希表,哈希表的一大优点就是用空间换时间,而且一个良好的哈希表结构是必须要留有一定的空闲位置的,所以这种实现方式也并非不妥。

        也许会有人想到用C++中的map/unordered_map来实现,这确实是可行的。首先,如果用map容器替换数组的话,实现起来并不是很难,理论上相较于哈希表确实会节省空间,但map的底层实现是红黑树,用一个红黑树去实现一个字典树,是不是有点小题大做了呢?其次,对于unordered_map来说,其本身就是一个哈希容器,与使用数组的本质是一样的,所以当然可行啦。

易混点剖析

       1、 字典树的实现是一种哈希表和树的结合。字典树的每次插入和查找操作的时间复杂度都是O(NlogN)的,而且为了防止删除操作对树操作不可逆的损坏,所以我们一般都是在结构体中额外增加一个成员,用以表示当前节点是否被删除(专业术语叫“惰性删除”)。

        2、不要将当前字母和当前节点弄混淆了。字典树的每一个节点都是一个“字母表”,并不是代指哪一个具体的字母。同时这也是字典树的特点:像字典一样,每次遍历到字符 ch 时,当前字典树节点不是直接代表这个 ch 的,而是表示当前节点的下一个字母是ch。不过,我们可以将其子节点看作代表一个固定的字符,如果是这样,那么字典树的根节点不代表任何字符。所以如果word字符串和trieTree(字典树)同时遍历的话,那么当前节点就表示在“字典”中,当前字符是否存在,而真正代表这个字符的是当前节点对应的一个字节点(next)。

       3、 字典树的增加操作很好解决,但删除和析构的维护成本就比较大了。因为C/C++不想java那样是完全封装好的,需要自己手动释放掉被删除的节点,而字典树的节点又异常的庞大(一般至少有26个next指针),所以字典树的释放就极大的增加了字典树的维护难度。

代码示例

  1. #include <iostream>
  2. #include <unordered_map>
  3. #include <string>
  4. using namespace std;
  5. class trieTree
  6. {
  7. private:
  8. int passBy;
  9. bool isEnd;
  10. trieTree* nexts[26];
  11. public:
  12. //构造函数
  13. trieTree() :passBy(0), isEnd(0), nexts() {}
  14. //插入单词
  15. void insert(string word);
  16. //查找单词是否存在
  17. bool contains(string word);
  18. //查找前缀数量
  19. int pre_size(string pre);
  20. //删除单词
  21. void erase(string word);
  22. };
  23. void test1()
  24. {
  25. trieTree* my_trie = new trieTree();
  26. my_trie->insert("apple");
  27. my_trie->insert("apple");
  28. my_trie->insert("banana");
  29. my_trie->insert("orange");
  30. my_trie->insert("applogise");
  31. my_trie->insert("app");
  32. my_trie->erase("applogise");
  33. cout << my_trie->contains("apple") << endl;
  34. cout << my_trie->contains("appl") << endl;
  35. cout << my_trie->contains("banana") << endl;
  36. cout << my_trie->contains("pear") << endl;
  37. cout << my_trie->pre_size("") << endl;
  38. cout << my_trie->pre_size("app") << endl;
  39. cout << my_trie->pre_size("appl") << endl;
  40. cout << my_trie->pre_size("ore") << endl;
  41. }
  42. class trieTree_byHash
  43. {
  44. private:
  45. int pass;
  46. bool end;
  47. unordered_map<char, trieTree_byHash*> nexts;
  48. public:
  49. trieTree_byHash() :pass(0), end(0), nexts() {}
  50. //插入单词
  51. void insert(string word);
  52. //查找单词是否存在
  53. bool contains(string word);
  54. //查找前缀数量
  55. int pre_size(string pre);
  56. //删除单词
  57. void erase(string word);
  58. };
  59. void test2()
  60. {
  61. trieTree_byHash* my_trie = new trieTree_byHash();
  62. my_trie->insert("apple");
  63. my_trie->insert("apple");
  64. my_trie->insert("banana");
  65. my_trie->insert("orange");
  66. my_trie->insert("applogise");
  67. my_trie->insert("app");
  68. my_trie->erase("applogise");
  69. cout << my_trie->contains("app") << endl;
  70. cout << my_trie->contains("appl") << endl;
  71. cout << my_trie->contains("banana") << endl;
  72. cout << my_trie->contains("pear") << endl;
  73. cout << my_trie->pre_size("") << endl;
  74. cout << my_trie->pre_size("app") << endl;
  75. cout << my_trie->pre_size("appl") << endl;
  76. cout << my_trie->pre_size("ore") << endl;
  77. }
  78. int main()
  79. {
  80. test1();
  81. cout << endl;
  82. test2();
  83. return 0;
  84. }
  85. /*固定数组实现前缀树*/
  86. //插入单词
  87. void trieTree::insert(string word)
  88. {
  89. //不重复插入单词
  90. if (this->contains(word))
  91. return;
  92. this->passBy++;
  93. trieTree* cur = this;
  94. for (char ch : word)
  95. {
  96. if (cur->nexts[ch - 'a'] == nullptr)
  97. cur->nexts[ch - 'a'] = new trieTree();
  98. cur = cur->nexts[ch - 'a'];
  99. cur->passBy++;
  100. }
  101. cur->isEnd = true;
  102. }
  103. //查找单词是否存在
  104. bool trieTree::contains(string word)
  105. {
  106. trieTree* cur = this;
  107. for (char ch : word)
  108. {
  109. if (cur->nexts[ch - 'a'] == nullptr)
  110. return false;
  111. cur = cur->nexts[ch - 'a'];
  112. }
  113. return cur->isEnd;
  114. }
  115. //查找前缀数量
  116. int trieTree::pre_size(string pre)
  117. {
  118. trieTree* cur = this;
  119. for (char ch : pre)
  120. {
  121. if (cur->nexts[ch - 'a'] == nullptr)
  122. return 0;
  123. cur = cur->nexts[ch - 'a'];
  124. }
  125. return cur->passBy;
  126. }
  127. //删除单词
  128. void trieTree::erase(string word)
  129. {
  130. if (!this->contains(word))
  131. return;
  132. trieTree* cur = this;
  133. cur->passBy--;
  134. int index = 0;
  135. while (--cur->nexts[word[index] - 'a']->passBy != 0)
  136. {
  137. cur = cur->nexts[word[index++] - 'a'];
  138. }
  139. //由于不会出现一样的单词,所以最后至少有一个节点的passBy为1
  140. //接着从index位置开始,将后面的节点全部delete
  141. trieTree* next = cur->nexts[word[index] - 'a'];
  142. cur->nexts[word[index] - 'a'] = nullptr;
  143. cur = next;
  144. while (cur != nullptr)
  145. {
  146. next = index + 1 < word.size() ? cur->nexts[word[++index] - 'a'] : nullptr;
  147. delete cur;
  148. cur = next;
  149. }
  150. }
  151. /*哈希表实现前缀树*/
  152. //插入单词
  153. void trieTree_byHash::insert(string word)
  154. {
  155. if (this->contains(word))
  156. return;
  157. this->pass++;
  158. trieTree_byHash* cur = this;
  159. for (char ch : word)
  160. {
  161. if (cur->nexts.find(ch) == cur->nexts.end())
  162. cur->nexts[ch] = new trieTree_byHash();
  163. cur = cur->nexts[ch];
  164. cur->pass++;
  165. }
  166. cur->end = true;
  167. }
  168. //查找单词是否存在
  169. bool trieTree_byHash::contains(string word)
  170. {
  171. trieTree_byHash* cur = this;
  172. for (char ch : word)
  173. {
  174. if (cur->nexts.find(ch) == cur->nexts.end())
  175. return false;
  176. cur = cur->nexts[ch];
  177. }
  178. return cur->end;
  179. }
  180. //查找前缀数量
  181. int trieTree_byHash::pre_size(string pre)
  182. {
  183. trieTree_byHash* cur = this;
  184. for (char ch : pre)
  185. {
  186. if (cur->nexts.find(ch) == cur->nexts.end())
  187. return 0;
  188. cur = cur->nexts[ch];
  189. }
  190. return cur->pass;
  191. }
  192. //删除单词
  193. void trieTree_byHash::erase(string word)
  194. {
  195. if (!this->contains(word) && this->pass == 0)
  196. return;
  197. trieTree_byHash* cur = this;
  198. cur->pass--;
  199. int index = 0;
  200. while (index < word.size() && --cur->nexts[word[index]]->pass != 0)
  201. {
  202. cur = cur->nexts[word[index++]];
  203. }
  204. trieTree_byHash* next = cur->nexts[word[index]];
  205. cur->nexts[word[index]] = nullptr;
  206. cur = cur->nexts[word[index]];
  207. while (cur != nullptr)
  208. {
  209. next = index + 1 < word.size() ? cur->nexts[word[++index]] : nullptr;
  210. delete cur;
  211. cur = next;
  212. }
  213. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/994766
推荐阅读
相关标签
  

闽ICP备14008679号