前缀树是一种很重要的数据结构,本篇文章将通过Leetcode中的两道与前缀树相关的题目,以及在实际开发中的应用来探讨这个问题。
本篇文章,主要介绍前缀树的概念以及两道经典的前缀树算法题。
1.前缀树概念
Trie(发音为"try")即前缀树是一种数据结构,用于检索字符串数据集中的键。这一高效的数据结构有多种应用:
- 自动补全
- 拼写检查
- IP路由(最长前缀匹配)
- T9(九宫格)打字预测
- 单词游戏
当然了,还有其他的数据结构如平衡术和哈希表,同样可以使我们能够在字符串数据集中搜索单词。为什么我们还需要使用Trie树呢?
尽管哈希表可以在O(1)时间内寻找键值,却无法高效的完成以下操作:
- 找到具有同一前缀的全部键值
- 按词典序枚举字符串的数据集
Trie树优于哈希表的另一个理由是,随着哈希表大小的增加,会出现大量冲突,时间复杂度可能增加到O(n),其中n是插入的键的数量。与哈希表相比,Trie树在存储多个具有相同前缀的键时可以使用较少的空间,此时Trie树只需要O(m)的时间复杂度,其中m为键长。而平衡树中查找键值需要O(mlogn)时间复杂度。
Trie树的结点结构
Trie树是一个有根的树,其结点具有以下字段:
- 最多R个指向子节点的链接,其中每个链接对应字母表数据集中的一个字母。
- 布尔字段,以指定结点是对应键的结尾还是只是键的前缀。
Trie数中最常见的是插入与查找操作。
2.实现Trie(Leetcode208)
题目描述
实现一个 Trie (前缀树),包含 insert
, search
, 和 startsWith
这三个操作。
示例:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 true
trie.search("app"); // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");
trie.search("app"); // 返回 true
说明:
- 你可以假设所有的输入都是由小写字母 a-z 构成的。
- 保证所有输入均为非空字符串。
解题思路
本题可以直接根据Trie的定义来做,对其进行依次实现。
首先需要定义TrieNode。
- 该节点需要具有子节点children = new TrieNode[26]
- 该节点需要具有标识当前字母结尾是否是一个单词的功能
- 由于本题只有26个小写的英文字母,因此,每个节点存储的具体是哪个字母可以通过其上层对应的index确认。
实现Trie的insert
功能
- 首先获取当前Trie的root,因为每一次insert的时候,都应当从root的位置开始算起。
- 遍历0-25的index的children节点。如果为null则new个新的出来,并且将节点指向下一个要遍历的结点。
- 当全部遍历完成时,将最后一个节点的isWord属性置为true。标识着这个字符串完成插入。
search
与startsWith
函数的代码逻辑有些重复,因此两者都可以抽象出一个find
的逻辑,即遍历这个Trie找到指定的word即可,并返回这个单词结尾字幕的TrieNode。
search
逻辑实现
- 调用find方法,如果返回的TrieNode!=null且TrieNode.isWord==true则说明在这个Trie中找到了这个单词。
startWith
- 调用find方法,如果返回的TrieNode!=null则说明已经找到了,直接返回运算结果就好了。
解答代码
class Trie {
/** Initialize your data structure here. */
class TrieNode {
private boolean isWord;
private TrieNode[] children;
public TrieNode(){
this.isWord = false;
this.children = new TrieNode[26];
}
}
private TrieNode root;
public Trie() {
root = new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode p = root;
for(int i=0; i<word.length(); i++) {
int index = (int)(word.charAt(i)-'a');
if(p.children[index]==null) {
p.children[index] = new TrieNode();
}
p = p.children[index];
}
p.isWord = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode w = find(word);
return w!=null && w.isWord == true;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
return find(prefix)!=null;
}
private TrieNode find(String word) {
TrieNode p = root;
for(int i=0; i<word.length(); i++) {
int index = (int)(word.charAt(i)-'a');
if(p.children[index]==null) {
return null;
}
p = p.children[index];
}
return p;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
3.词典中最长的单词(Leetcode720)
题目描述
给出一个字符串数组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"。
注意:
所有输入的字符串都只包含小写字母。
words数组长度范围为[1,1000]。
words[i]的长度范围为[1,30]。
解题思路
一看到词典,就很自然的想到了Trie
但是这个前缀树的结构与上一题208的不可以定义成一样的。因为上一题的定义有个很严重的问题,我们只能通过前一个节点去找到下一个节点对应的是什么值。这中间缺少了很多数据。
因此这里需要改进前缀树TrieNode
以记录更多的信息。
本题所用的前缀树与208的相比进行了以下改进
- 增加了
char c
用于存储该TrieNode
所具体对应的字母 - 将
end
标识由boolean
类型改成了int
类型。并且end
存储的数值为数组中以该字母结尾的字符串的index
值加1
。这么做的目的是为了后续找这个字符串的时候,可以根据这个字母快速的找到这个字母对应的字符串。
这个地方的处理非常的巧妙啊,为什么要加1,因为默认都是0。感觉这个处理很trick! - 最后通过
BFS
进行遍历整个Trie
从而找到对应的值。
遍历的这个过程也用了一些手法,因为我们需要找的是字典顺序靠前的最长字符串。HashMap的遍历是无序的,因此就要比较,当前的String与ans直接的关系:
String word = words[node.end-1];
if(word.length()>ans.length()
|| word.length()==ans.length() && word.compareTo(ans)<0) {
ans = word;
}
在长度一样的时候,巧妙的运用了java自带的字符串比较的compareTo
函数。
最后返回ans
。
当然本题还有其他解法,但是使用的就不是Trie
。因此本文不再赘述。
解答代码
class Solution {
public String longestWord(String[] words) {
Trie trie = new Trie();
int index = 0;
for(String word: words) {
trie.insert(word, ++index);
}
trie.words = words;
return trie.dfs();
}
}
class TrieNode {
char c;
int end;
HashMap<Character, TrieNode> children = new HashMap<>();
public TrieNode(char c) {
this.c = c;
}
}
class Trie {
TrieNode root;
String[] words;
public Trie() {
root = new TrieNode('0');
}
public void insert(String word, int index) {
TrieNode p = root;
for(char c : word.toCharArray()) {
p.children.putIfAbsent(c, new TrieNode(c));
p = p.children.get(c);
}
p.end = index;
}
public String dfs() {
String ans = "";
Stack<TrieNode> stack = new Stack();
stack.push(root);
while(!stack.isEmpty()) {
TrieNode node = stack.pop();
if(node.end>0 || node == root) {
if(node!=root) {
String word = words[node.end-1];
if(word.length()>ans.length()
|| word.length()==ans.length() && word.compareTo(ans)<0) {
ans = word;
}
}
for (TrieNode n : node.children.values()) {
stack.push(n);
}
}
}
return ans;
}
}