概述
前缀树又名字典树,单词查找树,Trie树,是一种多路树形结构,是哈希树的变种,和hash效率有一拼,是一种用于快速检索的多叉树结构。
典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
栗子
下图是一个前缀树,这个前缀树是由[“abc”,”abd”,”abcd”,”b”bcd,”efg”,”hij”]组成的。
第一行代表一个根节点,不存放任何值
根节点下的每一条路径,都可以代表一个前缀字符串,红色实心代表当前节点为某个前缀字符串的结尾,比如最左侧的路径 a -> b -> c -> d,可以代表abc和abcd
从上面可以发现一些Trie树的特性: 1)根节点不包含字符,除根节点外的每一个子节点都包含一个字符。 2)从根节点到某一节点的路径上的字符连接起来,就是该节点对应的字符串。 3)每个节点的所有子节点包含的字符都不相同。 4)每条边对应一个字母。每个节点对应一项前缀。叶节点对应最长前缀,即单词本身。
Java实现
示例代码
/*** @author xuyj* @date 2022年01月19日 23:21*/public class Trie {/*** 子节点*/private Trie[] children;/*** 是否叶子节点*/private boolean isEnd;/*** 无参构造函数,初始化*/public Trie() {children = new Trie[26];isEnd = false;}/*** 将单词插入前缀树* @param word*/public void insert(String word) {Trie node = this;for (int i = 0; i < word.length(); i++) {char ch = word.charAt(i);int index = ch - 'a';// 如果字母不在此路径下,则在改路径上增加一个子节点if (node.children[index] == null) {node.children[index] = new Trie();}// 增加子节点node = node.children[index];}// 标记叶子结点node.isEnd = true;}/*** 查找单词在前缀树中是否存在* @param word* @return*/public boolean search(String word) {Trie node = searchPrefix(word);// 找到该前缀并且以叶子结点结尾// 比如存在路径abcde,查找abc,则返回falsereturn node != null && node.isEnd;}public boolean startsWith(String prefix) {return searchPrefix(prefix) != null;}private Trie searchPrefix(String prefix) {Trie node = this;for (int i = 0; i < prefix.length(); i++) {char ch = prefix.charAt(i);int index = ch - 'a';// 存在字符不在前缀树下,直接返回空if (node.children[index] == null) {return null;}node = node.children[index];}return node;}
这里只是创建了一个基本的前缀树形结构,如果需要调整,增加成员变量即可。
例题
leetcode上的例题648需要用到前缀树的结构,leetcode648. 单词替换

思路和算法
把所有的词根放入前缀树中,在树上查找每个单词的最短词根,该操作可在线性时间内完成。
示例代码
class Solution {//前缀树数据结构是一个字符多叉树,用一个数组来保存其子节点, isValid用来标记截止到该节点是否为一个完整的单词class TrieNode{TrieNode[] kids;boolean isValid;public TrieNode(){kids = new TrieNode[26];}}TrieNode root = new TrieNode();public String replaceWords(List<String> dictionary, String sentence) {String[] words = new String[dictionary.size()];for(int i = 0; i < words.length; ++i) words[i] = dictionary.get(i);//建树过程for(String word : words){insert(root, word);}String[] strs = sentence.split(" ");for(int i = 0; i < strs.length; ++i){//如果可以在树中找到对应单词的前缀,那么将这个单词替换为它的前缀if(search(root, strs[i])){strs[i] = replace(strs[i], root);}}//用StringBuilder来把字符串数组还原成原字符串句子的转换目标字符串StringBuilder sb = new StringBuilder();for(String s : strs){sb.append(s).append(" ");}sb.deleteCharAt(sb.length()-1);return sb.toString();}//建前缀树模版public void insert(TrieNode root, String s){TrieNode node = root;for(char ch : s.toCharArray()){if(node.kids[ch - 'a'] == null) node.kids[ch - 'a'] = new TrieNode();node = node.kids[ch - 'a'];}node.isValid = true;}//查询是否存在传入的字符串的前缀public boolean search(TrieNode root, String s){TrieNode node = root;for(char ch : s.toCharArray()){if(node.isValid == true) break;if(node.kids[ch - 'a'] == null) return false;node = node.kids[ch - 'a'];}return true;}//将传入的字符串替换为它在前缀树中的前缀字符串public String replace(String s, TrieNode root){TrieNode node = root;StringBuilder sb = new StringBuilder();for(char ch : s.toCharArray()){if(node.isValid || node.kids[ch - 'a'] == null) break;node = node.kids[ch - 'a'];sb.append(ch);}return sb.toString();}}
复杂度分析
时间复杂度:O(N)O(N),其中 NN 是 sentence 的长度。每次查询操作为线性时间复杂度。
空间复杂度:O(N)O(N),前缀树的大小。

