Tire(前缀树)
一、定义:
-
Trie(发音类似 “try”)或者说前缀树是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。
-
其核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」。
-
Trie结构:
- 与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

其中红色节点表示一个单词的结尾。
- Tire字典树的查找速度主要和它的元素(字符串)的长度相关O(w)。
二、插入字符串
public void insert(String word)
:
我们从字典树的根开始查找前缀,对于当前字符对应的子节点,有两种情况:
- 子节点存在:沿着指针移动到子节点,继续寻找下一个字符。
- 子节点不存在:创建一个新的子节点,记录在children数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
三、查找前缀
public Trie searchPrefix(String prefix)
:
我们从字典树的根开始查找前缀,对于当前字符对应的子节点,有两种情况:
- 子节点存在:沿着指针移动到子节点,继续搜索下一个字符。
- 子节点不存在:说明字典树中不包含该前缀,返回null空指针。
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
- 若搜索到了前缀的末尾,说明该字典树中存在该前缀。
- 另外,若前缀末尾对应的节点
isEnd==true
,则说明字典树中存在该字符串。
三、应用场景:
1、Trie树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构,这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
2、在纯算法领域,前缀树算是一种比较常用的数据结构,但如果是在工程中,不考虑前缀匹配的话,基本上使用hash就可以满足要求了。
- 但考虑前缀匹配的话,工程上一般也不会使用Tire。(工程领域Tire的应用面不广)
- 字符集大小不好确定(如下的题目只考虑了26个字母,字符集的大小限制在较小的26以内),因此可以使用Trie,但在工程中一般会兼容各种字符集,一旦字符集很大,Tire将会带来很大的空间浪费。
- 另外,对于个别的超长字符Tire会进一步变深,如果这时Tire是存储在硬盘中的,Tire结构过深带来的影响是多次随机IO,随机IO成本极高。
- Tire的特殊结构,也会在工程上为分布式存储带来困难。
- 工程上例如【联想输入】、【模糊匹配】、【全文检索】的典型场景主要是通过ES(ElasticSearch)解决的,而ES的实现主要是依靠【倒排索引】。
—>例如:Leetcode「208. 实现 Trie (前缀树)」
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串 word 。boolean search(String word)
如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。boolean startsWith(String prefix)
如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
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
class Trie {
private Trie[] children;//指向子节点的指针数组children
private boolean isEnd;//表明该节点是否为字符串的结尾
public Trie() {
children=new Trie[26];//于本题数组长度为26,即小写英文字母的数量
isEnd=false;//初始化为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';
//子节点不存在,创建一个新的子节点,记录在children数组的对应位置上
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;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix)!=null;
}
public 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;
}
}
/**
* 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);
*/
复杂度分析:
-
时间:初始化为O(1),其余操作为O(|S|),其中|S|为每次插入/查询的字符串长度。
-
空间:O(∣T∣⋅Σ),其中 ∣T∣ 为所有插入字符串的长度之和,Σ为字符集的大小,本题 Σ=26。
四、扩展
实际需求中不可能只有26个字母,也许有大小写,也许有其他的符号,因此我们需要使用其他的数据结构。
第二版:
class Node{
public boolean isWord;
public Map<Character,Node> next;
}
- 此时暂时只考虑英文字符不考虑其他语言(中文等)。
1、根据第二版我们设计的Trie:
public class Trie {
private class Node{
public boolean isWord;
public TreeMap<Character,Node> next;
public Node(boolean isWord){
this.isWord = isWord;
this.next = new TreeMap<>();
}
public Node(){
this(false);
}
}
private Node root;
private int size;
public Trie(){
this.root = new Node();
this.size = 0;
}
/**
* 返回Trie节点数
*/
public int getSize(){
return this.size;
}
}
2、Trie的添加
向字典树中添加节点的步骤如图:

//添加单词
public void add(String word){
Node curr = root;
for(int i=0;i<word.length();i++){
char c = word.charAt(i);
if(curr.next.get(c)==null){
curr.next.put(c,new Node());//没有该节点,直接添加新的
}
curr = curr.next.get(c);//有该节点,移动curr,继续查找下一节点
}
if(!curr.isWord){//如果之前已经存在该单词了size不能加1
curr.isWord = true;
size++;//Trie的节点数
}
}
3、Trie的查找单词:
//查询单词
public boolean contains(String word){
Node curr = root;
for(int i=0;i<word.length();i++){
char c = word.charAt(i);
if(curr.next.get(c)==null){
return false;//无下一个值的节点,直接返回false
}
curr = curr.next.get(c);
}
return curr.isWord;//单词到达末尾,根据isWord标志判断
}
4、判断前缀:
//查询在Trie中是否有单词以prefix为前缀
public boolean isPrefix(String prefix){
Node curr = root;
for(int i=0;i<prefix.length();i++){
char c = prefix.charAt(i);
if(curr.next.get(c)==null){
return false;
}
curr = curr.next.get(c);
}
return true;
}
5、删除单词
删除节点有以下四种情况:

- 情况一:逐一将节点去除即可;
- 情况二:直接将isWord置为false即可;
- 情况三:逐一删除节点,如果待删除节点是另外一个单词结尾则停止删除;
- 情况四:逐一删除节点,但是如果待删除节点还有其他孩子节点则停止删除。
//删除单词
public void delete(String word){
Stack<Node> preNodes = new Stack<>();
//删除前查找是否存在这个单词,存在才能删除
if(!contains(word)){
return;//不存在该单词,直接返回
}
Node curr = root;
for(int i=0;i<word.length();i++){
char c = word.charAt(i);
preNodes.push(curr);//添加前驱节点
curr = curr.next.get(c);
}
if(curr.next.size()==0){//到了单词末尾,节点是叶子节点
for(int i=word.length()-1;i>=0;i--){
char c = word.charAt(i);
Node pre = preNodes.pop();//获得前驱节点
//判断是否为其他单词的结尾,是则停止删除节点【情况3】
//判断待删除节点是否还有其他孩子,有则停止删除节点【情况4】
if((i!=word.length()-1&&pre.next.get(c).isWord)||pre.next.get(c).next.size()!=0){
break;
}
pre.next.remove(c);//删除节点【情况1】
}
}else{
curr.isWord = false;//情况2
}
size--;
}
上面实现的Trie中,我们是使用TreeMap
来保存节点的所有子节点,也可以用HashMap
来保存,效率会更高:
public Node(boolean isWord){
this.isWord = isWord;
this.next = new HashMap<>();
}
- Trie的查询效率非常高,但对空间的消耗还是很大的,是典型的空间换时间。