字典树or前缀树的实现

定义

        前缀树,又称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);
    }
}

参考资料:

        字典树(前缀树)_越看越喜欢啊

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值