Trie

Trie树又称为字典树、前缀树。如果我们使用一般树结构去查询一个数据集里的单词,它的复杂度是O(log n),但是如果我们使用Trie去查询单词的话,查询的复杂度只与单词的长度有关,与数据的规模无关。比如对于一个Trie - 图1规模的数据集,我们去查一个单词”word”,一般树的复杂度为O(20),而Trie树的复杂度为O(4),其中4是单词的长度,所以Trie树是一种很高效的查询字符串的树结构。
Trie - 图2

  1. class Node {
  2. char c;
  3. Node next[26];
  4. }

但是这样考虑忽略了大小写,并且没有考虑一些特殊的字符,如@等符号或标点符号。所以我们每个节点不再是静态的指向26个节点,而是动态的指向若干个节点

  1. class Node {
  2. char c;
  3. Map<Character,Node> next;
  4. }

另外我们通过某个字符来到一个节点,可以通过Map已经知道了,所以我们不必存储这个字符

  1. class Node {
  2. Map<Character,Node> next;
  3. }

Trie - 图3
另外通过叶子节点是无法区别单词的结尾的,因为有的单词可能为某个单词的前缀,如”pan”为”panda”的前缀,所以我们要增加一个变量isWord来表示是否是单词的结尾

  1. import java.util.TreeMap;
  2. public class Trie {
  3. private class Node {
  4. public boolean isWord;
  5. public TreeMap<Character,Node> next;
  6. public Node(boolean isWord) {
  7. this.isWord = isWord;
  8. next = new TreeMap<>();
  9. }
  10. public Node() {
  11. this(false);
  12. }
  13. }
  14. private Node root;
  15. private int size;
  16. public Trie() {
  17. root = new Node();
  18. size = 0;
  19. }
  20. public int getSize() {
  21. return size;
  22. }
  23. }

实现

添加单词

Trie - 图4

  1. public void add(String word) {
  2. Node cur = root;
  3. for (int i = 0; i < word.length(); i++) {
  4. char c = word.charAt(i);
  5. //判断是否有指向这个字符的节点
  6. if (cur.next.get(c) == null) {
  7. //没有则新建一个节点
  8. cur.next.put(c, new Node());
  9. }
  10. //移动到这个节点
  11. cur = cur.next.get(c);
  12. }
  13. //遍历完毕,判断这个节点是否被标记为单词的结尾 如果没有则标记并且维护size++
  14. if (!cur.isWord) {
  15. cur.isWord = true;
  16. size++;
  17. }
  18. }

查询单词

查询单词的逻辑与添加单词的逻辑高度重复,如果在查询过程中遇到没有指向该字符的节点,则直接返回false,如果遍历完毕都没有发生上面的情况,则判断该节点是否被标记为单词的结尾,如果没有则返回false,否则返回true。

  1. public boolean contains(String word) {
  2. Node cur = root;
  3. for (int i = 0; i < word.length(); i++) {
  4. char c = word.charAt(i);
  5. if (cur.next.get(c) == null) {
  6. return false;
  7. }
  8. cur = cur.next.get(c);
  9. }
  10. return cur.isWord;
  11. }

前缀搜索

查询是否包含某个前缀,与contains()方法几乎一样,不过最后不用判断是否是单词结尾,直接返回true

  1. public boolean isPrefix(String prefix) {
  2. Node cur = root;
  3. for (int i = 0; i < prefix.length(); i++) {
  4. char c = prefix.charAt(i);
  5. if (cur.next.get(c) == null) {
  6. return false;
  7. }
  8. cur = cur.next.get(c);
  9. }
  10. return true;
  11. }

简单字符匹配

对于字符串中的字符.规定它可以匹配任意的字符,那么这样的一个匹配算法如何写,如果我们遇到的字符不是.的话,逻辑和上面一样,如果遇到的是.的话,我们就要去搜索该节点中所有的分叉(子树)

  1. public boolean match(String word) {
  2. return match(root, word, 0);
  3. }
  4. private boolean match(Node node, String word, int index) {
  5. //递归终止条件
  6. if (index == word.length()) {
  7. return node.isWord;
  8. }
  9. char c = word.charAt(index);
  10. //如果不是.
  11. if (c != '.') {
  12. //没有指向该字符的节点 返回false
  13. if (node.next.get(c) == null) {
  14. return false;
  15. } else {
  16. //否则继续匹配
  17. return match(node.next.get(c), word, index + 1);
  18. }
  19. } else {
  20. //如果是. 去该节点的所有分叉中搜索
  21. for (char nextChar : node.next.keySet()) {
  22. //如果有任一个分叉匹配到了,则返回true
  23. if (match(node.next.get(nextChar), word, index + 1)) {
  24. return true;
  25. }
  26. }
  27. //说明上面的没有一个匹配成功了,返回fasle
  28. return false;
  29. }
  30. }

全部代码

  1. import java.util.TreeMap;
  2. public class Trie {
  3. private class Node {
  4. public boolean isWord;
  5. public TreeMap<Character,Node> next;
  6. public Node(boolean isWord) {
  7. this.isWord = isWord;
  8. next = new TreeMap<>();
  9. }
  10. public Node() {
  11. this(false);
  12. }
  13. }
  14. private Node root;
  15. private int size;
  16. public Trie() {
  17. root = new Node();
  18. size = 0;
  19. }
  20. public int getSize() {
  21. return size;
  22. }
  23. public void add(String word) {
  24. Node cur = root;
  25. for (int i = 0; i < word.length(); i++) {
  26. char c = word.charAt(i);
  27. if (cur.next.get(c) == null) {
  28. cur.next.put(c, new Node());
  29. }
  30. cur = cur.next.get(c);
  31. }
  32. if (!cur.isWord) {
  33. cur.isWord = true;
  34. size++;
  35. }
  36. }
  37. public boolean contains(String word) {
  38. Node cur = root;
  39. for (int i = 0; i < word.length(); i++) {
  40. char c = word.charAt(i);
  41. if (cur.next.get(c) == null) {
  42. return false;
  43. }
  44. cur = cur.next.get(c);
  45. }
  46. return cur.isWord;
  47. }
  48. public boolean isPrefix(String prefix) {
  49. Node cur = root;
  50. for (int i = 0; i < prefix.length(); i++) {
  51. char c = prefix.charAt(i);
  52. if (cur.next.get(c) == null) {
  53. return false;
  54. }
  55. cur = cur.next.get(c);
  56. }
  57. return true;
  58. }
  59. public boolean match(String word) {
  60. return match(root, word, 0);
  61. }
  62. private boolean match(Node node, String word, int index) {
  63. //递归终止条件
  64. if (index == word.length()) {
  65. return node.isWord;
  66. }
  67. char c = word.charAt(index);
  68. if (c != '.') {
  69. if (node.next.get(c) == null) {
  70. return false;
  71. } else {
  72. return match(node.next.get(c), word, index + 1);
  73. }
  74. } else {
  75. for (char nextChar : node.next.keySet()) {
  76. if (match(node.next.get(nextChar), word, index + 1)) {
  77. return true;
  78. }
  79. }
  80. //说明上面的没有一个匹配成功了
  81. return false;
  82. }
  83. }
  84. }