一.字典树的定义及实现
1.定义
Trie树,即字典树,是一种哈希树的变种。是一种用于快速查询某个字符串/字符前缀是否存在的数据结构。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
2.实现
树的构造
每个结点都包含一个链接数组(注意数组元素为结点类型),和一个bool变量代表当前到这个结点的路径组成的单词是否存在。
bool IsEnd;//标记当前结点 为结尾的路径 是否为有效字符串
vector<Trie*> children;//在 路径上出现字母 位置存放结点
每拿到一个新的单词,从第一个字母开始遍历,与树的第一个节点对比,看这个字母对应位置是否开辟了下一个结点。
- 如果没有开辟,则在这个位置开辟新的结点。
- 如果已经有结点了,则说明这个单词可以和之前的单词共用同一个前缀,则继续遍历所谓的下一个结点,重复之前的步骤,直到把这个单词遍历完。
void insert(string word) {//构造
Trie* node=this;
for(int i=0;i<word.size();i++)
{
if(node->children[word[i]-'a']==NULL)//若node的孩子结点的数组里面,当前字母对应位置没有结点
{
node->children[word[i]-'a']=new Trie();//构建新节点放入node的children数组,的字母对应位置
}
node=node->children[word[i]-'a'];//node后移
}
node->IsEnd=true;//每遍历完一个单词,该结尾结点IsEnd=true 代表该路径单词存在
}
举例:包含三个单词 “sea”,“sells”,“she” 的 Trie如下左边图所示。实际情况如下右边图所示。
查找单词或者单词前缀
判断一个字母是否存在,只要看当前结点中的链接数组里,这个字母下标对应位置有没有开辟下一个结点。
- 如果没有,表示不存在该字母。
- 如果有,则表示存在, 若下一个节点标志位为true,则表示这个字母是该条路径组成单词的结尾字母。
所以判断一个单词是否存在,就是依次每一个字符,在遍历到结尾结点的时候看当前的标志位是否为true,如果是才代表该单词存在。而判断单词前缀是否存在,只需要把这个单词前缀遍历完,在遍历完之前没有出现单词不匹配的情况就算存在,不需要检查标志位。
bool search(string word) {//搜索单词
Trie* node=this;
for(int i=0;i<word.size();i++)
{
if(node->children[word[i]-'a']==NULL)//如果当前word中字母对应位置没有结点,就代表这个单词不存在
{
return false;
}
node=node->children[word[i]-'a'];
}
if(node->IsEnd==true)//看匹配的最后一个字符是否为单词结尾
{
return true;
}
return false;
}
//搜索单词前缀
bool startsWith(string prefix) {//和 search 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的。
Trie* node=this;
for(int i=0;i<prefix.size();i++)//直到把prefix遍历完就行
{
if(node->children[prefix[i]-'a']==NULL)
{
return false;
}
node=node->children[prefix[i]-'a'];
}
return true;
}
二.字典树的应用
关于 Trie 的应用场景,概括为:一次建树,多次查询。
力扣648 单词替换,不过注意这里的前缀是字典树里的单词,字典树是小集合,外面来的单词是大集合。而上面的例子是字典树大集合,外面来的单词小集合。
class Solution {
struct Tire
{
vector<Tire*> children;
bool IsEnd;
Tire():children(26),IsEnd(false){}//初始化
};
void Insert(Tire*& root,vector<string> dictionary)//插入,构造字典树
{
for(int i=0;i<dictionary.size();i++)
{
string temp=dictionary[i];
Tire* node=root;
for(int j=0;j<temp.size();j++)
{
if(node->children[temp[j]-'a']==NULL)
{
node->children[temp[j]-'a']=new Tire();
}
node=node->children[temp[j]-'a'];
}
node->IsEnd=true;
}
}
string startsWith(Tire* root,string word)//找前缀字符串
{
string res;
Tire* node=root;
for(int i=0;i<word.size();i++)
{
if(node->children[word[i]-'a']==NULL)//第一个字母都不符合
{
return "no";
}
res+=word[i];
node=node->children[word[i]-'a'];
if(node->IsEnd==true)
{
return res;
}
}
return "no";//说明dictionnary中的单词word[i]长
}
public:
vector<string> Cut(string sentence)
{
vector<string> res;
//记录空格位置
vector<int> index;//保存空格位置
for(int i=0;i<sentence.size();i++)
{
if(sentence[i]==' ')
{
index.push_back(i);
}
}
//根据空格位置截取子串
int left=0;//子串左边界
for(auto &e:index)
{
int len=e-left;
res.push_back(sentence.substr(left,len));
left=e+1;
}
//注意这里退出循环时,还有一个单词没有复制过来
res.push_back(sentence.substr(left,sentence.size()-left));
return res;
}
string replaceWords(vector<string>& dictionary, string sentence) {
//1.构造Tire树
Tire* root=new Tire();
Insert(root,dictionary);
//2.切割sentence
vector<string> str=Cut(sentence);
//3.查找
string res;
for(int i=0;i<str.size();i++)
{
if(startsWith(root,str[i])!="no")/如果字典中存在符合要求的前缀
{
res+=startsWith(root,str[i]);
}
else
{
res+=str[i];
}
res+=" ";
}
res.pop_back();
return res;
}
};