trie树
介绍
Trie🌳(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
一个例子形象化了解trie树
引入一个问题,存储多个单词,给定一个单词,查找是已存储,给定一个前缀(前缀也可以是个单词)判断该前缀是否是已存储的某个单词的前缀。
我们用trie树就可以巧妙地解决,想象以下,包含三个单词 “sea”,“sells”,“she” 的 Trie 会长啥样呢?
这个样子滴!
该trie树中有大量的空连接(大量的next指针并没有指向什么)因此我们可以这么表示
看到这里的小伙伴们是不是已经对trie树有了一个形象化的理解了呢,接下来我们介绍一下这个26叉树的具体操作(对于上文提出的问题而言,说这是个26叉树也不奇怪,哈哈,)学会了这个什么二叉树一定不在话下了把。继续读下去吧
Trie树的基本操作(存储单词,查找单词,查找前缀)
一下都以C++语言进行描述,在文末会给出整合的java语言代码
首先我们给出trie树的结构体(类)
class Trie {
private:
bool isEnd;
vector<Trie*> next;
public:
Trie():next(26),isEnd(false){}
//方法将在下文实现...
};
接着分析三个方法,不想听我啰嗦的同学可以直接看代码呀。
第一个方法是存储单词,存储单词我们从根节点开始,比如word。从根节点开始,第一个字母是w,因此们根节点的26个next数组中对应下标为w-'a’的改指针一定有指向,因此我们new一个节点,让根节点的next[w-‘a’]指向新的节点,并且新节点不是最后一个单词,isEnd=false,依此类推直到最后一个节点把该节点的isEnd置于true这样一个单词就插入完毕啦。
void insert(string word){
Trie* node = this;
for (char ch:word){
if(node->next[ch-'a']==NULL)
node->next[ch=-'a']=new Trie();
node=node->next[ch-'a'];
}
node->isEnd=true;
}
第二三个方法很像,查找单词和查找前缀,他们共同的部分都是根据给出的字符串遍历trie树,遍历的方法为,把字符串对应的字母转化为数组下标,然后如果该字母下标有所指向就继续,如果没有则直接判断该单词没有存储。不同的部分为,查找单词需要验证最后一个节点的isEnd是否为true如果是true才能判断该单词已经存储,而查找前缀就不需要判断(这里认为整个单词就是该单词的前缀)。因此二三方法可以这么写
Trie* searchprefix(string prefix){
Trie* node=this;
for (char ch:prefix){
ch-='a';
if(node->next[ch]==nullptr)
return nullptr;
node=node->next[ch];
}
return node;
}
bool search(string word){
Trie* node= this->searchprefix(word);
return node!=nullptr && node->isEnd;
}
bool startsWith(string profix){
Trie* node = this->searchprefix(word);
return node!=nullptr;
}
总结
通过上述分析和代码我们可以发现trie的一些性质
- trie的形状和单词的插入或删除无关(删除的话就是按单词找下去没有就不用删除,有的话就把最后一个节点的isEnd=false此时如果是叶子节点就删除该节点,然后回滚直至删除所有节点)也就是说对于任意给定的一组单词,Trie 的形状都是唯一的。
- 查找或插入一个长度为 L 的单词,访问 next 数组的次数最多为 L+1,和 Trie 中包含多少个单词无关。(这是非常重要的,保证了查询的效率)
- Trie 的每个结点中都保留着一个字母表,这是很耗费空间的。如果 Trie 的高度为 n,字母表的大小为 m,最坏的情况是 Trie 中还不存在前缀相同的单词,那空间复杂度就为 O(m^n)。在如今的大多算法优化中我们都是用空间换时间。
全部代码
C++语言
class Trie {
private:
bool isEnd;
vector<Trie*> next;
public:
Trie():next(26),isEnd(false){}
void insert(string word){
Trie* node = this;
for (char ch:word){
if(node->next[ch-'a']==nullptr)
node->next[ch=-'a']=new Trie();
node=node->next[ch-'a'];
}
node->isEnd=true;
}
Trie* searchprefix(string prefix){
Trie* node=this;
for (char ch:prefix){
ch-='a';
if(node->next[ch]==nullptr)
return nullptr;
node=node->next[ch];
}
return node;
}
bool search(string word){
Trie* node= this->searchprefix(word);
return node!=nullptr && node->isEnd;
}
bool startsWith(string profix){
Trie* node = this->searchprefix(word);
return node!=nullptr;
}
};
java语言
calss Trie{
private Trie[] next;
private boolean isEnd;
public Trie(){
next=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.next[index]==null)
node.next[index]=new Trie();
}
node.isEnd=true;
}
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.next[index]==null)
return null;
node=node.next[index];
}
return node;
}
public boolean search(String word){
Trie node=searchprefix(word);
return node!=null&& node.isEnd;
}
public boolean startsWith(String prefix){
return searchprefix(prefix)!=null;
}
}