引言
前缀树,也叫字典树,我们成为 Trie树(发音类似 "try"),是一种多路树形结构,是哈希树的一种延伸。
效率方面与hash树差不多,也是一种快速检索的多叉树,用于统计和排序大量的字符串,经常用于搜索引擎的文本词频统计。
最大的优点就是减少无用的字符串比较,查询速度快,核心思想就是用空间换时间,利用查找存储的公共前缀降低时间开销,
这样缺点也很明显,因为需要提前定义存储各种情况的公共前缀,所以内存开销非常大。
实现前缀树
前缀树是一棵有根树,其每个节点包含以下字段:
指向子节点的指针数组 children。对于实际情况而言,数组长度订为 26,即小写英文字母的数量。
此时 children[0] 对应小写字母 a,children[1] 对应小写字母 b,…,children[25] 对应小写字母 z。
布尔字段 isEnd,表示该节点是否为字符串的结尾。
插入字符串(insert)
我们从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:
子节点存在。沿着指针移动到子节点,继续处理下一个字符。
子节点不存在。创建一个新的子节点,记录在 children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
查找前缀(search)
我们从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:
子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
子节点不存在。说明字典树中不包含该前缀,返回空指针。
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
若搜索到了前缀的末尾,就说明字典树中存在该前缀。
此外,若前缀末尾对应节点的 isEnd 为真,则说明字典树中存在该字符串。
class Trie {
private final Trie[] children;
private boolean isEnd;
// 初始化
public Trie() {
children = new Trie[26];
isEnd = false;
}
// 插入新元素
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;
}
// 查询元素是否存在
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
// 查找前缀是否存在
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;
}
}
题目描述
题干:
给出一个字符串数组 words 组成的一本英语词典。
返回 words 中最长的一个单词,该单词是由 words 词典中其他单词逐步添加一个字母组成。
若其中有多个可行的答案,则返回答案中字典序最小的单词。若无答案,则返回空字符串。
示例 1:
输入:words = ["w","wo","wor","worl", "world"]
输出:"world"
解释: 单词"world"可由"w", "wo", "wor", 和 "worl"逐步添加一个字母组成。
示例 2:
输入:words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
输出:"apple"
解释:"apply" 和 "apple" 都能由词典中的单词组成。但是 "apple" 的字典序小于 "apply"
题解思路
这里采用前缀树最明显的提示就是该单词由其他单词组成,这样用前缀树模型只需判断序号即可。
如果不采用前缀树的方法,直接用哈希表存储来代替也可以实现,而且速度上也相差不多,
这样就印证了开头我们所说的效率问题,具体思路还是数组的排序和遍历,排序之后保证长度和序号正确,
之后无论是用前缀树依次添加还是用哈希表存储出现过的单词判断当前遍历的单词是否由其他的单词组成皆可。
public String longestWord(String[] words) {
Arrays.sort(words, (a, b) -> {
if (a.length() != b.length()) {
return a.length() - b.length();
} else {
return b.compareTo(a);
}
});
String longest = "";
Set<String> set = new HashSet<>();
set.add("");
for (String word : words) {
if (set.contains(word.substring(0, word.length() - 1))) {
set.add(word);
longest = word;
}
}
return longest;
}
public String longestWord01(String[] words) {
Trie trie = new Trie();
for (String word : words) {
trie.insert(word);
}
String longest = "";
for (String word : words) {
if (trie.search(word)) {
if (word.length() > longest.length() || (word.length() == longest.length() && word.compareTo(longest) < 0)) {
longest = word;
}
}
}
return longest;
}
总结
虽然题目上有点明显展示前缀树的嫌疑,不过确实是前缀树的经典例题,能够加深对前缀树的理解和感受。
当然有人会觉得这里的前缀树和哈希表过于浪费空间,所以可以用Stack判断往里pop和push。
如果文章存在问题欢迎在评论区斧正和评论,各自努力,你我最高处见。