简介

计算机科学中,trie,又称前缀树或字典树,是一种有序),用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
Trie这个术语来自于retrieval。根据词源学,trie的发明者Edward Fredkin把它读作/ˈtriː/ “tree”。但是,其他作者把它读作/ˈtraɪ/ “try”。

原理

在图示中,键标注在节点中,值标注在节点之下。每一个完整的英文单词对应一个特定的整数。Trie可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含在分支的顺序中的。键不需要被显式地保存在节点中。图示中标注出完整的单词,只是为了演示trie的原理。
trie中的键通常是字符串,但也可以是其它的结构。trie的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。比如,bitwise trie中的键是一串比特,可以用于表示整数或者内存地址。
Tire Tree - 图1

  • 概括

tire树是为了快速查询一组字符串中是否含有一个字符串的结构,是以空间换取时间。tire树的每个节点有26个子节点(对应26个字母,不过也可以是根据实际进行改变) , 根节点不储存数值,而除了根节点以外的节点存储{一个字母,指向儿子的指针,以及一个bool类型判断是否是一个单词的末尾。

  • 插入

从根节点开始,按照需插入字符串从左到右的顺序,第一个字母是第二层(第一层是根节点),第二个字母是第三层,以此类推,如果没有某个字母的节点,就新建一个。在最后的叶节点加一个标志表示其一个单词的末尾。

  • 查找

按照需插入字符串从左到右的顺序,在搜索字符串的末端时,判断节点的标记是否是单词的末尾。

  • 删除

我用的删除方法写起来相对简单(不过可能时间复杂度高一点(再高也是O(n))):
在节点中储存节点(字符)出现的次数,当删除某个字符串时,每个节点出现的次数减一,消除字符串末尾的节点的标志,再从字符串末尾的节点开始把计数(字符出现次数)为零的节点删去。

代码实现(Java版)

image.png

Node.java

  1. import lombok.Data;
  2. import java.util.LinkedList;
  3. /**
  4. * 字典树的节点
  5. */
  6. @Data
  7. public class Node {
  8. private char content; // 存在当前节点的字
  9. private boolean isEnd; // 是否是词的结尾
  10. private int count; // 这个词在这个字下面的分支的个数
  11. private LinkedList<Node> childList;// 子节点
  12. /***
  13. * @Description: 构造方法(初始化节点使用)
  14. */
  15. public Node(char c){
  16. childList=new LinkedList<>();
  17. isEnd=false;
  18. content=c;
  19. count=0;
  20. }
  21. /****
  22. * @Description: 提供一个遍历node中的linkedList中是否有这个字。有就意味着可以继续查找下去,没有就没有
  23. */
  24. public Node subNode(char c){
  25. if(null!=childList&&!childList.isEmpty()){
  26. for (Node node : childList) {
  27. if(node.content==c){
  28. return node;
  29. }
  30. }
  31. }
  32. return null;
  33. }
  34. }

TrieTree.java

  1. /**
  2. * 字典树
  3. */
  4. public class TrieTree {
  5. private Node root;// 根
  6. public TrieTree(){
  7. root=new Node(' '); // 构造一个空的根节点
  8. }
  9. /***
  10. * @Description: 查询
  11. * @Param: word 要判断的词
  12. */
  13. public boolean search(String word){
  14. Node current=root; // 从根节点开始找
  15. if(null!=word){
  16. // 转成字符数组
  17. char[] chars = word.toCharArray();
  18. if(null!=chars&&chars.length>0){
  19. for (char c : chars) {
  20. Node node = current.subNode(c);
  21. if(null==node){ // 如果返回的子节点为空 说明不存在
  22. return false;
  23. }else{
  24. current=current.subNode(c);
  25. }
  26. }
  27. // 判断当前节点是否是结束节点
  28. if(current.isEnd()){
  29. return true;
  30. }else{
  31. return false;
  32. }
  33. }else{
  34. return false;
  35. }
  36. }else{
  37. return false;
  38. }
  39. }
  40. /***
  41. * @Description: 插入方法,先判断是否有这个词,(通过上面的写的查询方法) 如果没有,就一个个按顺序判断里面的字
  42. * 如果有这个字,继续判断下一个,当没有字个字的时候,new Node对象,放到上一个字的LindkedList里面
  43. * @Param: [word] 要插入的分词
  44. */
  45. public void insert(String word){ //华为电脑
  46. // 判断有没有这个词 有就直接说这个词在整个字典数已存在
  47. if(this.search(word)){
  48. return;
  49. }
  50. // 如果不存在 ,就从根节点一个个找
  51. Node current=root;
  52. if(null!=word){
  53. char[] chars = word.toCharArray();
  54. if(null!=chars&&chars.length>0){
  55. for (char c : chars) {
  56. Node child = current.subNode(c);
  57. if(null!=child){
  58. current=child;
  59. }else{
  60. // 构造新的
  61. current.getChildList().add(new Node(c));
  62. current=current.subNode(c);
  63. }
  64. current.setCount(current.getCount()+1); // 分支数加一
  65. }
  66. // 循环结束之后把最后一个字变成isEnd:true
  67. current.setEnd(true);
  68. }
  69. }
  70. }
  71. /***
  72. * @Description: 删除分词
  73. * @Param: [word] 要删除的分词
  74. */
  75. public void deleteWord(String word) {
  76. // 查询一个词在不在字典树
  77. if (this.search(word) == false) {
  78. return;
  79. }
  80. Node current = root;
  81. if (null != word) {
  82. char[] chars = word.toCharArray();
  83. if (null != chars && chars.length > 0) {
  84. for (char c : chars) {
  85. Node node = current.subNode(c);
  86. if (node.getCount() == 1) {
  87. current.getChildList().remove(node);
  88. return;
  89. } else {
  90. current.setCount(current.getCount() - 1); // 分支数减一
  91. current = node;
  92. }
  93. }
  94. current.setEnd(false); // isend设置为false代表当前链路上的字连起来不是一个词了
  95. }
  96. }
  97. }
  98. }

测试类

  1. public class TestTrieTree {
  2. public static void main(String[] args) {
  3. String content="华为-华为手机-华为平板-华为牛逼-鸿蒙-华为鸿蒙操作系统";
  4. // 模拟分词
  5. String[] split = content.split("-");
  6. // 构造字典树
  7. TrieTree trie = new TrieTree();
  8. // 把分词插入
  9. for (String s : split) {
  10. trie.insert(s);
  11. }
  12. System.out.println(trie.search("华为"));
  13. System.out.println(trie.search("华为手"));
  14. // 删除分词
  15. trie.deleteWord("华为");
  16. System.out.println(trie.search("华为"));
  17. System.out.println(trie.search("华为手机"));
  18. }
  19. }

参考

https://zh.wikipedia.org/wiki/Trie
https://www.cnblogs.com/end/archive/2013/01/21/2870161.html