定义
前缀树,又称Trie树 或 字典树,所以当然是一个树,并且是多叉树。要点:
1)树从0号节点开始,即根节点。
2)每一条边上都标识有一个字符,这些字符可以是任意一个字符集中的字符。比如,对于都是小写字母的字符串,字符集就是'a' - 'z';对于都是数字的字符串,字符集就是'0' - '9';对于二进制字符串,字符集就是0和1。
3)终结点的通路与集合中的字符串是一一对应的。
下图就是一个典型的前缀树,它包含的字符串集合是{in, inn, int, tea, ten, to}。其中,每个节点的编号是我们为了描述方便加上的。图中,3号节点对应的路径“0123”上的字符串是inn,8号节点对应的路径“0568”上的字符串是ten。
原理
下面讲一下对于给定的字符串集合{W1, W2, W3, ..., Wn},如何创建对应的Trie树。事实上,Trie树的创建都是从根节点(0号节点)开始,通过依次将W1、W2、W3、...、Wn插入Trie中实现的。所以关键就是之前提到的Trie的插入操作。
具体来说,Trie一般支持两个操作:
1)Trie.insert(W):插入操作,就是将一个字符串W加入到集合中。
2)Trie.search(S):查询操作,就是查询一个字符串S是不是在集合中。
插入
假设我们要插入字符串“in”。我们一开始位于根(0号节点),用P=0表示。首先,看点P是不是有一条标识着i的连向子节点的边。没有这条边,我们就新建一个节点,也就是1号节点;然后,把1号节点设置为P点的子节点,并且将边标识为i。最后,我们移动到1号节点,同时,令点P=1。
这样,我们就把字符串“in”中的‘i’字符插入到Trie中了。然后,我们再插入字符n。也是先找点P=1有没有标记为'n'的边。还是没有,也是就新建一个节点2,将其设置为P=1的子节点,并且把边标识为字符‘n’。之后,令P=2。这样,我们就把n插入到前缀树中了。这里需要注意的是,由于‘n’是“in”的最后一个字符,所以我们还需要将点P=2这个节点标记为终结点。
可以看出,单个字符的插入就是一个简单的单元步骤,可以重复循环。前缀树中插入一个新的字符串的过程都是按照这个逻辑进行。综上所述,在Trie中插入一个字符串W的伪代码如下:
Insert(W):
P=ROOT
For i=1 ... W.len:
If P.thru(W[i]) == NULL://没有标识为W[i]的边
P.addChild(W[i], new Node())
P=P.thru(W[i])
P.markEndPoint() //标记为P为终结点
查询
下面讲一下如何查询Trie树中是不是包含字符串S,也就是之前提到的查找操作search。查找其实比较简单,我们都要从根节点开始,沿着标识着S[1] -> S[2] -> S[3] -> ... -> S[S.len]的边移动,如果最后成功到达一个终结点,就说明S在Trie树中。如果最后无路可走,或者到达一个不是终结点的节点,就说明S不在Trie树中。(清晰看出对单个字符的判断,是一个简单的单元函数)
对于上图已经建好的Trie树,如果是查找字符串"te",就会从0号节点开始经过5最后到达6.但是6不是终结点,所以te没在Trie树中。如果查找的是字符串"too",就会从0开始经过5和9,然后发现之后无路可走:9号节点没有标记为'o'的边连出去。所以too也不在Trie中。
综上所述,在Trie树中查找一个字符串的伪代码如下:
Search(S):
P = ROOT
For i = 1..S.len:
If P.thru(S[i]) == NULL: //如果没有S[i]这个子节点
Return False
P = P.thru(S[i])
If P.isEndPoint:
Return True
else
Return False
代码实现
这里以剑指OfferⅡ上的典型习题062为例,讲解Trie树的实现。题目同LeetCode208,题目要求如下。
代码如下,这里需要特别说明以下几点:
1)Trie树的根节点就是当前类的this指针
2)注意search函数与startswith函数的区别:search是判断形参word是否已经在Trie树中存在,即word的尾节点所处的节点必须isEndPoint为1;而startswith函数不同,它是判断形参prefix是否在前缀树中,对isEndPoint不要求。
class Trie{
public:
vector<Trie*> child;
bool isEndPoint;
private:
//必须返回最后一个字符所在的孩子指针,因为它要被用于判断是否终结点
Trie* searchPrefix(const string &prefix)
{
Trie* node=this;
for(char &ch:prefix)
{
int idx=ch-'a';
if(node->child[idx]==nullptr)
return nullptr;
node=node->child[idx];
}
return node;
}
public:
Trie()
{
child.clear();
child.resize(26);
isEndPoint=false;
}
//插入一个新的字符串
void insert(string word)
{
Trie* node=this;
for(char &ch:word)
{
int idx=ch-'a';
if(node->child[idx]==nullptr) //子节点不存在
node->child[idx]=new Trie();
node=node->child[idx];
}
node->isEndPoint=true;
}
//判断字符串word是否已经存在
void search(string word)
{
Trie* trailNode=searchPrefix(word);
return trailNode && trailNode->isEndPoint;
}
//判断字符串prefix,是否是前缀树中的前缀
bool startsWith(string prefix)
{
return searchPrefix(prefix);
}
}
参考资料: