1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class Trie { private TrieNode root; private char endSymbol; static class TrieNode { char currentChar = ' ' ; Map<Character, TrieNode> children = new HashMap <>(); } public Trie () { root = new TrieNode (); endSymbol = '*' ; } public void insert (String word) { insertSubStringFrom(0 , word); } public boolean search (String word) { TrieNode node = root; for (int i = 0 ; i < word.length(); i++) { if (!node.children.containsKey(word.charAt(i))) return false ; node = node.children.get(word.charAt(i)); } return node.children.containsKey(endSymbol); } public boolean startsWith (String prefix) { TrieNode node = root; for (int i = 0 ; i < prefix.length(); i++) { if (!node.children.containsKey(prefix.charAt(i))) return false ; node = node.children.get(prefix.charAt(i)); } return true ; } public void insertSubStringFrom (int pos, String str) { TrieNode node = root; for (int i = pos; i < str.length(); i++) { if (!node.children.containsKey(str.charAt(i))) { node.children.put(str.charAt(i), new TrieNode ()); } node = node.children.get(str.charAt(i)); } node.children.put(endSymbol, null ); } }
Trie的思路很简单. 每个字母抽象成一个node, 一个string可以看成从左到右字符构成的graph. 比如apple这个词, 每个字符都可以抽象成node, 那么就是a -> p - > p -> l -> e. 如果有另一个词比如bad, 那么就会有b -> a -> d这样的graph. 每一层对应不同位置的字符. 最上层是第0个字符, 往下依次 + 1. 假如有另一个单词比如ape. 我们发现第0层有a, 那么不需要再创建, 然后看a的children都有谁, 发现有p, 于是我们也不用再创建新的, 看p的children有谁, 发现只有p, 没有e, 于是我们创建e这个node在第二层.
现在的问题是如何快速知道某个node都包含有哪些其他node(字符)呢? 是否存在我们想要字符的node. 比如第0个位置是m, 我们如何知道现在的trie里面第0层有没有m这个node呢? 于是我们想到了hashmap, O(1)访问速度. 用一个dummy root来存所有目前有的第0个字符的node(第0层的node). 比如我们想要找mall. 我们就问root是否有m? 有的话它告诉我们m对应的node, m同样有自己的hashmap, 存着它有的node. 然后我们问m这个node你是否有a? 有的话我们找到a问它是否有l? 有的话我们继续问l问它是否有l? 有的话此时我们发现mall四个字符都有, 我们就看此时有没有end symbol, 因为我们要区分开结尾和继续的区别. 比如一个trie里面有a -> p -> p -> l -> y这个单词, 但是如果我们问它有没有app这个单词, 我们在走到第二个p的时候发现已经遍历完app中每个字符了. 那么此时这个trie有没有app这个词呢? 答案是没有, 因为此时第二个p没有存end symbol, 也就是之前没有单词在这里停下. 因此end symbol很重要. 当然有end symbol的话就说明有这个word.
Trie就是把每个字符抽象成node, 然后node和node连成树. 为了能够快速知道某个node自己有的children, 使用hashmap来存储自己的children从而可以快速知道, 快速访问. 每个字符是个node, 有field表示该node代表什么字符以及存储自己的children都是谁的hashmap. 但是由于我们用不到每个node代表什么字符的这个field, 因此每个node就抽象为hashmap, 但我们应该知道每个hashmap都对应某个位置的已经存的某个字符(当然在某个prefix的前提下).
时间复杂度: O(L)我们要遍历字符串中的每个字符. 空间复杂度: O(L) 也就是目前没有和我们要插入的word相同的prefix.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class Trie { TrieNode root; class TrieNode { Map<Character, TrieNode> children; boolean isWord; TrieNode(Map<Character, TrieNode> children) { this .children = children; } } public Trie () { root = new TrieNode (new HashMap <>()); } public void insert (String word) { TrieNode currNode = root; for (int i = 0 ; i < word.length(); i++) { if (!currNode.children.containsKey(word.charAt(i))) { currNode.children.put(word.charAt(i), new TrieNode (new HashMap <>())); } currNode = currNode.children.get(word.charAt(i)); } currNode.isWord = true ; } public boolean search (String word) { TrieNode foundNode = searchPrefix(word); return foundNode != null && foundNode.isWord; } public boolean startsWith (String prefix) { TrieNode foundNode = searchPrefix(prefix); return foundNode != null ; } private TrieNode searchPrefix (String word) { TrieNode currNode = root; for (int i = 0 ; i < word.length(); i++) { if (!currNode.children.containsKey(word.charAt(i))) { return null ; } currNode = currNode.children.get(word.charAt(i)); } return currNode; } }
这个算是标准写法. 单独写一个method叫做searchPrefix, 它能够给我们返回我们给定word最后一个字符所对应的node. 如果找不到返回null. 这是因为starts with和search的内容很相似. 都是从root出发往后找, 一个是找不到了return false, 即使全部找到了返回最后一个node的isWord. 另一个是找不到了return false, 找到了直接返回true, 也就是二者只是最后一行的差距. 因此我们就把逻辑包装进searchPrefix里面, 为了满足search要看最后一个字符对应node的isWord这个field, 我们让searchPrefix的返回值设置为TrieNode, 也就是返回最后一个字符所对应的node.
一个node代表一个字符. node包含是否有一个boolean表示是否word在它这里结尾以及一个hashmap表示自己的下一个字符有哪些.