目录
-
一、介绍
Trie,又称字典树或前缀树,是用来存储字符串的N叉树。该Tire中每一个结点都代表一个前缀或者字符串,而这个前缀或者字符串是由通往该结点的所有路径决定的。
图1.字典树
如上图所示,是一棵字典树。
从根结点出发,走最右边的分支,先到达y,此时代表前缀y;再到达e,此时代表前缀ye;最后走到s,代表字符串yes。
再从根出发,走最左边的分支,先到达a,此时代表前缀a;再到达b,此时代表前缀ab;之后ab处分支出两条路,分布代表字符串abc和abd。这种情况下具有公共前缀的字符串只需要存储其不同的子字符串,而前缀只存储一次,这也是前缀树的由来。
其中,根节点表示空字符串。
-
二、如何表示Trie的结点
-
1.方法一:数组
如果假设我们的字符串只包含26个小写字母,那么每个节点最多有26个子结点,因此我们可以用指针数组(nextBranch)来表示其子节点。如下:
class TrieNode {
TrieNode* nextBranch[N];
};
如果子节点包含某个字母,那么对应的指针不为空。以图1的根节点为例,其指针数组中只有nextBranch[0]、nextBranch[3]和nextBranch[24]不为NULL。
-
2.方法二:Map
数组方法的缺点在于必须事先确定字符串中字符种类的数量,而且如果字符种类的数量太多,那么会十分浪费空间。此时可以使用Hashmap来实现。这样更省空间也更灵活,但是可能比数组稍慢一些。
class TrieNode {
unordered_map<char, TrieNode*> nextBranch;
};
-
三、性质
1.由于字符串的公共前缀会统一保存,故节约内存;
2.在trie上进行检索总是从根结点出发;
3.根结点不包含字符,除根结点外的每一个结点都只代表一个字母;
4.从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串;
5.每个结点的所有子结点包含的字符都不相同;
-
四、Trie结点的定义
/*字典树结点*/
class TrieNode{
public:
/*结点最大分支树(由于这里假设所有输入是由26个小写字母组成,故MaxBranchNum为26)*/
const static int MaxBranchNum = 26;
/*指针数组,代表26个字母,如果包含第i个字母,那么nextBranch[i]!=NULL*/
TrieNode* nextBranch[MaxBranchNum];
/*是否是一个单词的结尾*/
bool isEnd;
/*当isEnd==true,该值保存对应的单词*/
string word;
/*该单词的出现次数*/
int count;
TrieNode():count(0),isEnd(false){
memset(nextBranch,NULL,sizeof(TrieNode*)*MaxBranchNum);//初始化nextBranch数组
};
/*插入字母ch(相当于将ch在nextBranch中对应的指针实例化)*/
void put(char ch){
if(nextBranch[ch-'a']==NULL)nextBranch[ch-'a'] = new TrieNode();
}
/*获得字母ch对应的结点指针,当ch不存在时返回NULL*/
TrieNode* get(char ch){return nextBranch[ch-'a'];}
/*判断当前结点是否包含字母ch(相当于判断ch在nextBranch中对应的指针是否为NULL)*/
bool containsKey(char ch){return nextBranch[ch-'a']!=NULL;}
/*设置isEnd为true*/
void setEnd(){isEnd = true;}
/*判断是否是单词*/
bool isWord(){return isEnd;}
};
-
五、Trie接口定义
/*字典树*/
class Trie{
public:
Trie(){root = new TrieNode();}
~Trie(){destory(this->root);}
/*插入单词*/
void insert(const string word);
/*查找前缀prefix,如果查找成功则返回最后的结点,否则返回NULL*/
TrieNode* searchPrefix(const string prefix);
/*查找单词word,如果查找成功返回word的出现次数,否则返回0;*/
int search(const string word);
/*如果存在任意单词以prefix为前缀,则返回true,否则返回false*/
bool startsWith(const string prefix);
/*打印当前字典树中的所有单词*/
void printALL();
/*打印以prefix为前缀的所有单词*/
void printPre(const string prefix);
private:
TrieNode* root;
/*销毁以root为根的子树*/
void destory(TrieNode* root);
/*打印以root为根的所有单词*/
void print(TrieNode* root);
};
-
六、插入字符串
/*插入单词*/
void Trie::insert(const string word){
if(word.size()==0)return;
for(int i=0;i<word.size();i++)//出现异常字符时直接返回
if(word[i]<'a'||word[i]>'z')return;
TrieNode* node = root;
for(int i=0;i<word.size();i++){
if(!node->containsKey(word[i]))
node->put(word[i]);
node = node->get(word[i]);
}
//此时的node指向最终的结点
if(node->word.size()==0){
node->word.assign(word);
}
node->count++;
node->setEnd();
}
时间复杂度:如果插入的字符串长度为m,那么时间复杂度为O(m).
-
七、搜索字符串
-
1.搜索前缀
由于所有结点的后代都与该结点相对应字符串有着公共的前缀。因此很容易搜索到有公共前缀的任意单词。所以,可以根据给定的前缀沿着树向下搜索,如果有找不到的结点,那么搜索失败;否则,搜索成功。
/*查找前缀prefix,如果查找成功则返回最后的结点,否则返回NULL*/
TrieNode* Trie::searchPrefix(const string prefix){
TrieNode* node = root;
for(int i=0;i<prefix.size();i++){
if(prefix[i]<'a'||prefix[i]>'z')return NULL;
if(node->containsKey(prefix[i]))
node = node->get(prefix[i]);
else
return NULL;
}
return node;
}
-
2.搜索单词
像搜索前缀一样,当搜索完成后会返回搜索的最终结点,可以通过方法isWord()进行判断其是否是单词,也可以通过判断count>0来判断是否是单词。
/*查找单词word,如果查找成功返回word的出现次数,否则返回0;*/
int Trie::search(const string word){
TrieNode* node = searchPrefix(word);
return node?node->count:0;
}
-
3.时间复杂度
无论是查找前缀还是单词,其时间复杂度与插入相同,即O(m).
-
八、完整代码
/*数据结构:字典树(前缀树)*/
#include <iostream>
#include <string.h>
using namespace std;
/*字典树结点*/
class TrieNode{
public:
/*结点最大分支树(由于这里假设所有输入是由26个小写字母组成,故MaxBranchNum为26)*/
const static int MaxBranchNum = 26;
/*指针数组,代表26个字母,如果包含第i个字母,那么nextBranch[i]!=NULL*/
TrieNode* nextBranch[MaxBranchNum];
/*是否是一个单词的结尾*/
bool isEnd;
/*当isEnd==true,该值保存对应的单词*/
string word;
/*该单词的出现次数*/
int count;
TrieNode():count(0),isEnd(false){
memset(nextBranch,NULL,sizeof(TrieNode*)*MaxBranchNum);//初始化nextBranch数组
};
/*插入字母ch(相当于将ch在nextBranch中对应的指针实例化)*/
void put(char ch){
if(nextBranch[ch-'a']==NULL)nextBranch[ch-'a'] = new TrieNode();
}
/*获得字母ch对应的结点指针,当ch不存在时返回NULL*/
TrieNode* get(char ch){return nextBranch[ch-'a'];}
/*判断当前结点是否包含字母ch(相当于判断ch在nextBranch中对应的指针是否为NULL)*/
bool containsKey(char ch){return nextBranch[ch-'a']!=NULL;}
/*设置isEnd为true*/
void setEnd(){isEnd = true;}
/*判断是否是单词*/
bool isWord(){return isEnd;}
};
/*字典树*/
class Trie{
public:
Trie(){root = new TrieNode();}
~Trie(){destory(this->root);}
/*插入单词*/
void insert(const string word);
/*查找前缀prefix,如果查找成功则返回最后的结点,否则返回NULL*/
TrieNode* searchPrefix(const string prefix);
/*查找单词word,如果查找成功返回word的出现次数,否则返回0;*/
int search(const string word);
/*如果存在任意单词以prefix为前缀,则返回true,否则返回false*/
bool startsWith(const string prefix);
/*打印当前字典树中的所有单词*/
void printALL();
/*打印以prefix为前缀的所有单词*/
void printPre(const string prefix);
private:
TrieNode* root;
/*销毁以root为根的子树*/
void destory(TrieNode* root);
/*打印以root为根的所有单词*/
void print(TrieNode* root);
};
/*插入单词*/
void Trie::insert(const string word){
if(word.size()==0)return;
for(int i=0;i<word.size();i++)//出现异常字符时直接返回
if(word[i]<'a'||word[i]>'z')return;
TrieNode* node = root;
for(int i=0;i<word.size();i++){
if(!node->containsKey(word[i]))
node->put(word[i]);
node = node->get(word[i]);
}
//此时的node指向最终的结点
if(node->word.size()==0){
node->word.assign(word);
}
node->count++;
node->setEnd();
}
/*查找前缀prefix,如果查找成功则返回最后的结点,否则返回NULL*/
TrieNode* Trie::searchPrefix(const string prefix){
TrieNode* node = root;
for(int i=0;i<prefix.size();i++){
if(prefix[i]<'a'||prefix[i]>'z')return NULL;
if(node->containsKey(prefix[i]))
node = node->get(prefix[i]);
else
return NULL;
}
return node;
}
/*查找单词word,如果查找成功返回word的出现次数,否则返回0;*/
int Trie::search(const string word){
TrieNode* node = searchPrefix(word);
return node?node->count:0;
}
/*如果存在任意单词以prefix为前缀,则返回true,否则返回false*/
bool Trie::startsWith(const string prefix){
return searchPrefix(prefix)!=NULL;
}
/*打印以root为根的所有单词*/
void Trie::print(TrieNode* root){
if(root==NULL)return;
TrieNode* node = root;
if(root->word.size()!=0)cout<<root->word<<endl;
for(int i=0;i<root->MaxBranchNum;i++)
print(root->nextBranch[i]);
}
/*打印当前字典树中的所有单词*/
void Trie::printALL(){
print(this->root);
}
/*打印以prefix为前缀的所有单词*/
void Trie::printPre(const string prefix){
if(prefix.size()==0)printALL();
else{
TrieNode* node = root;
for(int i=0;i<prefix.size();i++){
if(node->containsKey(prefix[i]))node = node->get(prefix[i]);
else return;
}
print(node);
}
}
/*销毁以root为根的子树*/
void Trie::destory(TrieNode* root){
if(root==NULL)return;
for(int i=0;i<root->MaxBranchNum;i++)
destory(root->nextBranch[i]);
delete root;
root = NULL;
}
int main(){
Trie* a = new Trie();
a->insert("hello");
a->insert("hello");
a->insert("hi");
a->insert("beauty");
a->insert("helloworld");
a->insert("word");
cout<<a->search("hello");//2
a->printALL();
a->printPre("h");
delete a;
return 0;
}