Trie 树实现与应用 与 Double Array Trie 进阶

Trie树

基本概念

 Trie树又称字典树,它是用来查询字符串的一种数据结构。一般它每一个节点都有26个子节点,所以是26叉树。优点查询字符串的时候速度快,缺点浪费大量空间。但是也可以实现255个子节点对应ASCII码0~255,具体看需求吧。
 当一个字符串长为M的时候,假设数据结构中已经有了N个字符串了。对于平衡树而言,查找一个字符串 O(log^n)。对于字典树来说就O(M)。

Trie树图
例图A

这里写图片描述

例图B

这里写图片描述

性质
  1. 利用每一个字符串的公共前缀来节约内存。
  2. 每次查找的时候,都是从trie树上的跟节点进行检索
  3. 根节点不包含字符,其余节点每一个节点都只包含一个字符。
  4. 由跟节点开始到某一节点,所有路径上的祖先节点的字符与本节点的字符构成的字符串,即为该节点的字符串。
  5. 每个节点的所有子节点包含的字符各不相同。
时间与空间复杂度分析
  1. 当存储少量字符串时,Trie消耗空间较大。因为键值并非显式存储的,而是与其他键值共享的字符串,但存储大量字符串的时候,Trie空间明显会降低。
  2. 查询快,因为查找的时间复杂度。跟当前数据结构中已经存储了多少字符串无关。缺点随着树高的增加空间增长过快,为26的指数倍增长。空间复杂度为 26的n次方。
红黑树与Trie树做字典树比较
红黑树作字典树伪代码
typedef set<std::string>  Dict;
void Init(int Fd)
{
   Dict _dict;
   char buff[SIZE];
   while( read(Fd,buff,SIZE) > 0)
   {
      _dict.Insert(std::string(buff));
   }
}
Bool FindString(Dict & _dict,std::string AimString)
{
   return _dict.count()
}
列如,查询一个长度为5的字符串。只需五次就可以从 
26的五次方个可能中找到该字符串。对于红黑树,需要 log2 26^5 = 23
当然前提是建立在了共有26^5个字符串的长度都小于5,并且不重复。
如果字符串很少在32个以内,红黑树的比较次数小于5次。空间大小最多只有32*sizeof(RBNode)。
字典树在长度为5的时候却达到了,最小 sizeof(TrieNode)*26*6 
最多却有 sizeof(TrieNode) * 475255 Tips:这个数字根据等比数列求和公式而得
上述长度为5最少情况的Trie树图示

这里写图片描述

节点设计
1.
struct TrieNode
{
   int freq;//1.即代表终止符又代表字符串出现的次数,如果为0,则表示从跟到该节点的字符串不存在。
   int node;//2.代表该节点还有多少子节点。用来删除的时候快速得到当前节点的子节点数。
   TrieNode * child[SIZE]; 
}
这种设计就是上面那种结构非常浪费内存
2.
struct TrieNode
{
   int freq;
   int node;
   List<TrieNode*> child;
}
这种设计节省内存,但是每次遍历子节点的时候都要遍历整个链表,效率太低。查找效率变为26 * O(M)
3.struct TrieNode
{
    int freq;
    int node;
    set<TrieNode*> child;
}
这种设计处于12直接,查找效率 O(M) * log26 ,比1节省很多内存,但是没有2节省,
毕竟RBnode大小比单个指针大。
但是实际上我们可以计算一波帐,一个RBNode 三个指针+value 大小起码也30多字节了。
假设第一层 
RBT 我们挂26个字符 都 780个字节了。而 数组需要 208个字节。
假设第二层每个子节点都只挂一个字符
RBT 26map , 每个map (root 指针 + Head指针+RBNode) = 50 字节
26 * 50 =1300 字节 ,  数组 需要 208*26 = 5408  
其实省空间吗,省,真的那么省吗?
没有!!!
功能

1.插入字符串
 思想就是从跟节点开始遍历。如果当前节点的字节点有字符串的首字符,则继续迭代,否则创建新节点。
2.查找字符串
 思想就是从跟节点开始遍历。如果遍历的时候发现某个字符对应的子节点为NULL,代表字符串不存在,否则迭代遍历。如果到最后一个字符了,查看 TrieNode的终止符成员变量是否为真,如果为真则存在。
3.输出字符串词频
  先查找到字符串,找到即这个字符串的最后一个字符,然后输出频数。
4.字符串排序
 前序遍历Trie树即可。
5.找到所有单词的公共前缀及其长度
 取一个length,前序递归遍历一下即可。

代码实现
#include<iostream>
#include <map>
#include <set>
#include <string>
#include <stack>
#include <assert.h>
using namespace std;


struct TrieNode
{
    TrieNode()
    :_c(0), freq(0), _child()
    {}
    TrieNode(char c)
        :_c(c), freq(0), _child()
    {}
    char _c;
    int freq;
    map<char,TrieNode*> _child; 
};



class Trie
{
public:
    Trie()
        :_root(new TrieNode), _size(0)
    {}
    void Insert(std::string word)
    {
        //这个Insert 空串也可以插入
        TrieNode * Node = _root;
        auto begin = word.begin();
        auto end = word.end();
        while (begin != end)
        {
            if (Node->_child.count(*begin) == 0)
                Node->_child[*begin] = new TrieNode(*begin); // Node->_child.insert(pair<char, TrieNode*>(*begin, new TrieNode(*begin)));  
             Node = Node->_child[*begin++];
        }
        ++Node->freq;
        ++_size;
    }
    bool Remove(std::string word)
    {
        stack<TrieNode*> scon;
        TrieNode * Node = _root;
        TrieNode * parent = NULL;
        auto begin = word.begin();
        auto end = word.end();
        while (begin != end)
        {
            if (Node->_child.count(*begin) == 0)
                return false;
            parent = Node; //第一次到这的时候 parent 等于 _root ,_root成功压栈没问题 
            scon.push(parent);
            Node = Node->_child[*begin++]; // 这里Node 必不为空,为空 从上面的判断就返回了
        }
        /*
         假设字符串 abc  共循环3次
         遍历  a    b   c 
                |   |   |
                       Node
               root a   b 
                |   |   |       
                       parent       
        */
        if (parent == NULL || Node->freq==0)
        {
             return   (parent == NULL && Node->freq > 0) ? Node->freq--,_size-- : false;

            /*
              parent == NULL  的时候 Node 为 _root 也就是空串的情况
              这句意思是当parent == NULL && Node->freq >0  ,说明被删除的是空串 .
              因为如果频率大于1,说明字典树里面之前插入了空串,将频率减一即可。
              但是如果 parent == NULL && Node->freq == 0 那么它就是 Node->freq==0 的子情况
              也就是匹配完后,发现这个串根本不在字典树中,所以谈不上删除,返回 false即可。
            */
        }
        --Node->freq,_size--;
        if (Node->freq == 0 && Node->_child.size() == 0)
        {
            parent->_child.erase(Node->_c);
            delete Node;  // 必须先从父节点哪里把  该节点的元素删除掉, 即从父节点的红黑树中查找到保存相应子节点的 RbNode
        }                  // 然后先删除RbNode , 然后再删除 我们自己从堆上申请的 关于 TrieNode的内存。
        if (parent == _root)
            return true;
        TrieNode * DelParent = NULL; //删完该节点后,可能parent也是 freq==0 && child.size()==0 我们必须也得把 parent删除。
        while (scon.top() != _root)  //因为这不是删除某个节点而是删除某个字符串
        {
            DelParent = scon.top();
            scon.pop();
            if (DelParent->freq != 0 || DelParent->_child.size() != 0)
                break;
            scon.top()->_child.erase(DelParent->_c);
            delete DelParent;
        }
        return true;
        /*
            if (parent->freq == 0 && parent->_child.size()==0)
               return Remove(word.sub_str(0,word.size()-1)) 如果递归时间复杂度从O(N) 就到了 O(N^2)
            故使用栈来保存路径节点 将时间复杂度降到 O(N) 
        */
    }
    int Getfreq(const std::string  & word)
    {
        auto begin = word.begin();
        auto end = word.end();
        TrieNode * Node = _root;
        while (begin != end)
        {
            if (Node->_child.count(*begin) == 0)
                return -1;
            Node = Node->_child[*begin++];
        }
        return Node->freq;
    }
    int MaxPreFix()
    {
        int ret = 0;
        MaxPreFix_(_root, ret);
        return ret;
    }
    long size()
    {
        assert(_size >= 0);
        return _size;
    }
    ~Trie()
    {
        Clear(_root);
    }
protected:
    int MaxPreFix_(TrieNode * parent, 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值