单词查找树,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。Trie可以看作是一个确定有限状态自动机(DFA)。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。 Trie这个术语来自于retrieval。根据词源学。本博文主要讲解了单词查找树的相关知识及其实现。

  如下所示为一棵单词查找树:

与二叉查找树不同,Trie树的键不是直接保存在节点中,而是由节点在树中的位置决定。

对于Trie树它有3个基本性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。

  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。

  3. 每个节点的所有子节点包含的字符都不相同。

Trie树效率分析:

Trie树优点是最大限度地减少无谓的字符串比较,查询效率比较高。核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

  1. 插入、查找的时间复杂度均为O(M),其中M为字符串长度。
  2. 对于英文字母的字典树,其空间复杂度是26^n 级别的数字的字典树是10^n 级别的,非常庞大。

Trie树创建:

Trie树的创建要考虑的是父节点如何保存孩子节点,主要有链表和数组两种方式:

  1. 使用节点数组,因为是英文字符,可以用Node[26]来保存孩子节点(如果是数字我们可以用Node[10]),这种方式最快,但是并不是所有节点都会有很多孩子,所以这种方式浪费的空间太多

  2. 用一个链表根据需要动态添加节点。这样我们就可以省下不小的空间,但是缺点是搜索的时候需要遍历这个链表,增加了时间复杂度。

  3. 可以结合hash表来存储其对应的孩子节点,以孩子节点的值为键,指向孩子节点的指针为值。这样在理论上可以做到时间复杂度为O(1)的情况,且空间的浪费程度不高。

Trie树实现:

  对于trie树,其使用得较频繁点的功能是查找和插入,为此,此处主要讲解Trie树的插入和查找功能。

在讲解其相应的功能之前,我们先定义Trie树节点类:

  1. class TrieNode{
  2. /**
  3. * 该节点的值
  4. */
  5. String value;
  6. /**
  7. * 该节点的孩子节点
  8. */
  9. Map<String,TrieNode> children;
  10. /**
  11. *用于判断该孩子节点是否为最终节点,即对应的单词的末尾字符
  12. */
  13. boolean isEnd;
  14. public TrieNode(){
  15. this(null);
  16. }
  17. public TrieNode(String value){
  18. this(value,false);
  19. }
  20. public TrieNode(String value,boolean isEnd){
  21. this.value=value;
  22. this.isEnd=isEnd;
  23. children=new HashMap<String,TrieNode>();
  24. }
  25. /**
  26. * 用于设置当前节点的值
  27. * @param value 当前节点的值
  28. */
  29. public void setValue(String value){
  30. this.value=value;
  31. }
  32. /**
  33. * 用于获取其当前节点的值
  34. * @return 当前节点值
  35. */
  36. public String getValue(){
  37. return this.value;
  38. }
  39. /**
  40. * 用于设置当前节点的字符是否为字符串的最后一个字符
  41. * @param isEnd true表示当前字符为字符串的最后一个字符
  42. */
  43. public void setIsEnd(boolean isEnd){
  44. this.isEnd=isEnd;
  45. }
  46. /**
  47. * 用于获取其当前节点是否为单词末尾字符的判断
  48. * @return 返回其对应的结果
  49. */
  50. public boolean getIsEnd(){
  51. return this.isEnd;
  52. }
  53. /**
  54. * 用于添加当前节点的孩子节点
  55. * @param value 孩子节点的值
  56. * @param node 指向当前节点的孩子节点的指针
  57. */
  58. public void addChild(String value,TrieNode node){
  59. children.put(value,node);
  60. }
  61. /**
  62. * 用于获取当前节点的孩子节点
  63. * @param value 孩子节点的值
  64. * @return 其对应的孩子节点的指针
  65. */
  66. public TrieNode getChild(String value){
  67. return children.get(value);
  68. }
  69. }
  1. 插入:

  对于插入操作,无非是逐一把单词的每个字符插入到对应前缀的后面,使其成为该前缀的孩子节点。需要注意的是,在插入之前,先查看前缀是否存在,如果存在就共享,否则穿件对应的节点和边。

其插入操作的相关代码如下:

  1. public void add(String value){
  2. TrieNode node=head;
  3. for(int i=0;i<value.length();i++){
  4. String v=String.valueOf(value.charAt(i));
  5. TrieNode child=node.getChild(v);
  6. //该字符并没有在相应的孩子节点中
  7. if(child==null){
  8. child=new TrieNode(v);
  9. node.addChild(v,child);
  10. }
  11. //当其为该单词的最后一个字符时
  12. if(i==value.length()-1){
  13. child.setIsEnd(true);
  14. }
  15. node=child;
  16. }
  17. }
  1. 查找:

  对于查找操作而言,其较为简单,只需要沿着字典树的链接,从上往下查找即可。

具体代码如下:

  1. /**
  2. * 用于判断其对应单词是否在其对应的字典树中
  3. * @param value 需要查找的单词
  4. * @return 是否在字典树中的判断
  5. */
  6. public boolean search(String value){
  7. TrieNode node = head;
  8. for(int i=0;i<value.length();i++){
  9. String v=String.valueOf(value.charAt(i));
  10. TrieNode child=node.getChild(v);
  11. if(child==null){
  12. return false;
  13. }
  14. //当为最后一个字符,且其不为截止字符的时候,表示该单词不在字典树中
  15. if(i==value.length()-1&&!child.getIsEnd()){
  16. return false;
  17. }
  18. node=child;
  19. }
  20. return true;
  21. }

完整代码如下:

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. /**
  4. * @author 学徒
  5. * 用于实现Trie树
  6. */
  7. public class Trie {
  8. /**
  9. * 该trie树的根节点
  10. */
  11. private TrieNode head=new TrieNode();
  12. /**
  13. * Trie树对应的节点类
  14. */
  15. private class TrieNode{
  16. /**
  17. * 该节点的值
  18. */
  19. String value;
  20. /**
  21. * 该节点的孩子节点
  22. */
  23. Map<String,TrieNode> children;
  24. /**
  25. *用于判断该孩子节点是否为最终节点,即对应的单词的末尾字符
  26. */
  27. boolean isEnd;
  28. public TrieNode(){
  29. this(null);
  30. }
  31. public TrieNode(String value){
  32. this(value,false);
  33. }
  34. public TrieNode(String value,boolean isEnd){
  35. this.value=value;
  36. this.isEnd=isEnd;
  37. children=new HashMap<String,TrieNode>();
  38. }
  39. /**
  40. * 用于设置当前节点的值
  41. * @param value 当前节点的值
  42. */
  43. public void setValue(String value){
  44. this.value=value;
  45. }
  46. /**
  47. * 用于获取其当前节点的值
  48. * @return 当前节点值
  49. */
  50. public String getValue(){
  51. return this.value;
  52. }
  53. /**
  54. * 用于设置当前节点的字符是否为字符串的最后一个字符
  55. * @param isEnd true表示当前字符为字符串的最后一个字符
  56. */
  57. public void setIsEnd(boolean isEnd){
  58. this.isEnd=isEnd;
  59. }
  60. /**
  61. * 用于获取其当前节点是否为单词末尾字符的判断
  62. * @return 返回其对应的结果
  63. */
  64. public boolean getIsEnd(){
  65. return this.isEnd;
  66. }
  67. /**
  68. * 用于添加当前节点的孩子节点
  69. * @param value 孩子节点的值
  70. * @param node 指向当前节点的孩子节点的指针
  71. */
  72. public void addChild(String value,TrieNode node){
  73. children.put(value,node);
  74. }
  75. /**
  76. * 用于获取当前节点的孩子节点
  77. * @param value 孩子节点的值
  78. * @return 其对应的孩子节点的指针
  79. */
  80. public TrieNode getChild(String value){
  81. return children.get(value);
  82. }
  83. }
  84. /**
  85. * 用于Trie树的插入操作
  86. * @param value
  87. */
  88. public void add(String value){
  89. TrieNode node=head;
  90. for(int i=0;i<value.length();i++){
  91. String v=String.valueOf(value.charAt(i));
  92. TrieNode child=node.getChild(v);
  93. //该字符并没有在相应的孩子节点中
  94. if(child==null){
  95. child=new TrieNode(v);
  96. node.addChild(v,child);
  97. }
  98. //当其为该单词的最后一个字符时
  99. if(i==value.length()-1){
  100. child.setIsEnd(true);
  101. }
  102. node=child;
  103. }
  104. }
  105. /**
  106. * 用于判断其对应单词是否在其对应的字典树中
  107. * @param value 需要查找的单词
  108. * @return 是否在字典树中的判断
  109. */
  110. public boolean search(String value){
  111. TrieNode node = head;
  112. for(int i=0;i<value.length();i++){
  113. String v=String.valueOf(value.charAt(i));
  114. TrieNode child=node.getChild(v);
  115. if(child==null){
  116. return false;
  117. }
  118. //当为最后一个字符,且其不为截止字符的时候,表示该单词不在字典树中
  119. if(i==value.length()-1&&!child.getIsEnd()){
  120. return false;
  121. }
  122. node=child;
  123. }
  124. return true;
  125. }
  126. }

Trie树应用场景:

1. 字符串检索

  事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

举例:

  给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

2. 字符串最长公共前缀

  Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。

举例:

  给出N个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?

   解决方案: 首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

  1. 利用并查集(Disjoint Set),可以采用经典的Tarjan 算法;

  2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

3. 排序

  Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。

比如给你N个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

4. 作为其他数据结构和算法的辅助结构

  如后缀树,AC自动机等

5. 词频统计

  trie树在这里的应用类似哈夫曼树,

比如词频统计使用哈希表或者堆都可以,但是如果内存有限,就可以用trie树来压缩空间,因为trie树的公共前缀都是用一个节点保存的。

6. 字符串搜索的前缀匹配

  trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。

  Trie树检索的时间复杂度可以做到O(M),M是要检索单词的长度,如果使用暴力检索,需要指数级O(M^2)的时间复杂度。

回到目录|·(工)·)

博文参考自:字典树(Trie树)的实现及应用

K:单词查找树(Trie)的更多相关文章

  1. cogs 293. [NOI 2000] 单词查找树 Trie树字典树

    293. [NOI 2000] 单词查找树 ★★☆   输入文件:trie.in   输出文件:trie.out   简单对比时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需 ...

  2. codevs 1729 单词查找树

    二次联通门 : codevs 1729 单词查找树 /* codevs 1729 单词查找树 Trie树 统计节点个数 建一棵Trie树 插入单词时每新开一个节点就计数器加1 */ #include ...

  3. Trie树,又称单词查找树、字典

    在百度或淘宝搜索时,每输入字符都会出现搜索建议,比如输入“北京”,搜索框下面会以北京为前缀,展示“北京爱情故事”.“北京公交”.“北京医院”等等搜索词.实现这类技术后台所采用的数据结构是什么?[中国某 ...

  4. 【数据结构】关于前缀树(单词查找树,Trie)

    前缀树的说明和用途 前缀树又叫单词查找树,Trie,是一类常用的数据结构,其特点是以空间换时间,在查找字符串时有极大的时间优势,其查找的时间复杂度与键的数量无关,在能找到时,最大的时间复杂度也仅为键的 ...

  5. COGS 293.[NOI2000] 单词查找树

    ★   输入文件:trie.in   输出文件:trie.out   简单对比 时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高 ...

  6. [NOI2000] 单词查找树

    ★★   输入文件:trie.in   输出文件:trie.out   简单对比 时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提 ...

  7. 293. [NOI2000] 单词查找树——COGS

    293. [NOI2000] 单词查找树 ★★   输入文件:trie.in   输出文件:trie.out   简单对比时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检 ...

  8. 解题报告:luogu P5755 [NOI2000]单词查找树

    题目链接:P5755 [NOI2000]单词查找树 曾几何时,NOI 也有这么水的题( 裸的\(Trie\),只用维护插入即可,记得\(+1\)就好了,真没用讲的. \(Code\): #includ ...

  9. 【NOI2000】 单词查找树

    问题描述 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高查找和定位的速度,通常都画出与单词列表所对应的单词查找树,其特点如下: 根结点不包含字母,除根结点外每一个结点都仅包 ...

随机推荐

  1. chrome 插件学习笔记(一)

    主要是屏蔽cnbeta中屏蔽广告之后的弹出层 manifest.json文件 { "js": ["jquery-1.7.2.min.js","cnbe ...

  2. 隐藏导航栏,偏移20PX

    1:让ViewController在NavigationBar下面开始布局. automaticallyAdjustsScrollViewInsets作用 当Controller上存在唯一一个UISc ...

  3. jenkins 配置。

    为了避免在jenkins操作过程中的权限问题. 将安装在/Users/Shared/jenkins目录下的卸载. sudo launchctl unload /Library/LaunchDaemon ...

  4. 近期js

    1 var value1 = 0, value2 = 0, value3 = 0; for ( var i = 1; i <= 3; i++) { var i2 = i; (function() ...

  5. 刚才在windows下发现拖拽不了文件了

    百度了下  摁了两下esc就可以了.以下是百度得到的答案 按几下Esc目的是:没按前ESC键[接触不好]或[键不灵]或其他原因导致ESC处于[按下]状态,这样鼠标就会拖不了文件,点击菜单也会马上消失, ...

  6. centos6安装最新syslog-ng推送hdfs

    可参考以下网址: installhttps://www.syslog-ng.com/community/b/blog/posts/latest-syslog-ng-available-rhel-6-c ...

  7. webpack快速入门——实战技巧:优雅打包第三方类库

    下面说两种方法: 一. 1.引入jQuery,首先安装: cnpm install --save jquery 2.安装好后,在我们的entry.js中引入: import $ from 'jquer ...

  8. 用 TensorFlow 实现 k-means 聚类代码解析

    k-means 是聚类中比较简单的一种.用这个例子说一下感受一下 TensorFlow 的强大功能和语法. 一. TensorFlow 的安装 按照官网上的步骤一步一步来即可,我使用的是 virtua ...

  9. javascript数据结构与算法--二叉树遍历(中序)

    javascript数据结构与算法--二叉树遍历(中序) 中序遍历按照节点上的键值,以升序访问BST上的所有节点 代码如下: /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * ...

  10. JVM-垃圾收集算法、垃圾收集器、内存分配和收集策略

    对象已死么? 判断一个对象是否存活一般有两种方式: 1.引用计数算法:每个对象都有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1.计数为0时可以回收. 2.可达性分析算法(Reachab ...