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

Trie树

基本概念

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

Trie树图

例图A

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

例图B

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

性质

  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树图示

《Trie 树实现与应用 与 Double Array 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, int & length)
    {
        int PreRet = 0;
        int max = 0;
        for (auto & i : parent->_child)
        {
            if ((PreRet = MaxPreFix_(i.second, length)) > max)
                max = PreRet;
        }
        if (max > length)
            length = max;
        return max + 1;
    }
    /* 时间复杂度上面的是O(n) n代表节点个数。 MaxPreFix_ 返回的是当前高度。 length 保存的是所有递归中 高度最大的值。 */
    void Clear(TrieNode * root)
    {
        for (auto & i : root->_child)  //类似后序遍历的思想
        {
            Clear(i.second);
        }
        delete root;
    }
protected:
    TrieNode * const  _root;
    long _size;//字典树中字符串的个数
};
void Unit_test()
{
    Trie object;
    std::string buff;
    while (cin >> buff)
    {
        object.Insert(buff); // 这里我输入的参数是 "abcd" "abcde" "bcde"
    }
    int length = object.MaxPreFix();
    cout << length << endl;
    cout << object.Getfreq("abcd") << endl;
    cout << object.Getfreq("abcde") << endl;
    cout << object.Getfreq("bcde") << endl;
    cout << object.size() << endl;
    object.Remove("abcd");
    cout << object.size() << endl;
    object.Remove("abcde");
    cout << object.size() << endl;
    object.Remove("bcde");
    cout << object.size() << endl;
}
int main()
{
    Unit_test();
    return 0;
}

Double-Array-Trie

设计理念

  DATrie的设计就是为了即节省内存,又提高访问效率,并且它把Trie树序列化到了数组中,这样如果把相应的数组添加到了 cache 中,又大大提高了访存效率。综合来看DATrie效率最差也是数组Trie的效率。

设计原理
核心思想方程
Base[parent]+Ch = child
Check[child] = parent
核心思想

《Trie 树实现与应用 与 Double Array Trie 进阶》
  它的核心思想就是把一个待插入的字符串分开存储,把有公共部分子串的字符存到树中,把独特的子串存到Tail数组中。
  我们把这个树称为 Reduce-Trie ,把Tail 数组 可以看做单独子串的数组。而Reduce-Trie 是用Base数组表达出来的一个抽象的树并非真正意义的存在。
  查找的时候根据从root节点开始,根据状态方程查找每一个字符的子节点。即 base[root] + Ch = child , 如果这个child 的base值是无效的,则代表这个字符串没有在DATrie中,如果是负值则代表有部分子串在Tail数组中,剩下的就是跟Tail数组中每个字符做比较了,如果每个字符都相同那么,该字符串就在DATrie中。

插入原理与场景
场景1 正常无冲突无前缀

《Trie 树实现与应用 与 Double Array Trie 进阶》
  这种情况,我们插入的时候直接插入即可,也就是找到了一个空的base节点(base数组中的一个位置) 发现它是空的,也就是对应的check数组中的位置中的值是无效值,没人使用它,那么证明这个串 没有公共前缀 是第一次插入, 那么我们把它对应check数组中的位置赋值成上一个节点的下标,这个操作实际上就是 child -> parent = node , 也就是把 父子节点连接起来而已。
  把Reduce-trie中处理完后,把该字符串剩下的位置保存到 Tail数组中即可,也就是上图的意思。

场景2 有公共前缀无冲突

《Trie 树实现与应用 与 Double Array Trie 进阶》
  第二个情况就是有公共冲突了,假如已经插入 bachelor了,现在要插入badge了,那么ba这俩个字符冲突了,我们需要从tail中把公共字符提取出来,因为现在仅凭字符d无法在 Reduce-Trie中区分开来 bachelor 与 badge 这俩个字符串了,所以需要从 tail数组中把字符c提取出来。
  有的时候不一定非得从Tail数组中的字符就得跟插入中的字符有一样的,有可能已有abcd0 , bcd0 全在 tail中 而 现在要插入 acd0 ,只是因为 a 无法区分俩个字符串了,故我们需要从tail中提取出来 字符 b 而已。后期就可以通过 ab… 与 ac…来区分这俩个字符串了。

场景3冲突

  可能随着节点插入的越来越多,后面选取base 位置的时候,发现竞争,也就是一个子节点被俩个节点抢占,这个时候就需要 选项一个子节点少的节点,我们把它称为TroubleNode,我们把该节点的所有子节点重新还一个位置,这样就不存在竞争,就可以正常插入了。

场景3 坑 1 DATrie 的循环冲突

  如果我们每次新找新偏移量的时候都从1开始找的话,这个时候发生冲突的场景是 2个父节点竞争一个子节点,那么冲突解决后。可能另一个节点又冲突了还是同样的2个父节点竞争一个子节点。因为是多个父节点竞争一个子节点。处理冲突的时候我们只能2个2个处理。那么可能新的冲突值在这三个父节点中循环,从而造成死循环。
  所以我们每次选择新偏移量的时候,不能每次从1开始,要使用全局的一个变量来充当新偏移量的初始值。

场景3 坑 2 DATrie 父子节点发生冲突

  存在一种情况,父子节点发生冲突,那么当前节点为子节点的时候,解决冲突的时候因为需要把所以子节点移位,那么当我们继续插入的时候,node原有的位置已经改变,所以需要给node赋值为修改后的位置

场景3 坑3 与场景2共同发生

  当与场景2共同发生的时候,优先处理场景3,否则会出错会导致后续的tail数组中的内容絮乱。

代码

错误代码(正确的在下面,这个代码只是要讲DAT的坑)
#ifndef DATRIE_H
#define DATRIE_H
#include <iostream>
#include <vector>
using namespace std;
#include <string>
#include <assert.h>

/********核心思想*************/
/***Base[parent]+ch = child***/
/***check[child] = parent ****/
/*****************************/
class DATrie
{
public:
    DATrie()
        :base(1024,0), check(1024,1<<31), tail(256,0),_pos(1)
    {
        base[1] = 1; // root 是下标 1 开始的,我们把它初始偏移量初始为1
    }               // check 初始化为 1<<31 代表这些节点都没有父节点 ,选择1<<31是因为,最大的有符号int也比这个小,我们取pos的时候
                   // 都是 -pos , 即不可能取到这个值,那么就用这个值来充当 check中空闲空间的标志是极为合适的
                   // pos 初始化为 1 是因为如果不初始化为1 初始化成 0 的话,那么第一个串插入时候 base 保存 -pos 的时候 还是0
                   // 遍历的时候就会跟base那些初始化的0混


    void ReSizeBaseCheck(int size )
    {
        base.resize(2*size,0);
        check.resize(2*size,1<<31);
    }
    void InSert( string  str)
    {
        size_t  ret = CharSize(str);
        if(ret > base.size())
         {
            ReSizeBaseCheck(base.size());
         }
        if(tail.size()-_pos < str.size())
           tail.resize(2*tail.size()); 
        str.push_back(0);
        string::iterator begin = str.begin();
        string::iterator end = str.end();
        int node = 1;
        int child;
        unsigned char ch;
        int pos;
        while (begin != end)
           {
             ch = *begin++;
             child = base[node] + ch;
             if(check[child]== 1<<31)
               {
                    check[child] = node;
                    break;
               }
             if(check[child] > 0 && base[child]<0)
              {
                  /*有冲突当前节点不再是单独节点了,单独节点表示从当前节点开始能够区分多个有共同前缀的字符串*/
                  int i = 0;
                  pos = -base[child];
                  node = child;
                  while((ch=tail[pos++])!=0 && begin!=end)
                  {
                   // ch = 1;
                     if(ch == *begin)
                    {
                         for( i=1;check[i+ch/*base[i+ch]*/]!=1<<31;++i);  /*****冲突的四种情况**********/
                         base[node] = i;                           /*****************************/
                         check[i+ch] = node;                       /* 1. abcde / abcgh**********/
                         ++begin;                                  /* 2. abc / abcde **********/
                         node = i+ch;                              /* 3. abcdef / abc **********/
                    }                                              /* 4. abc0 / abc0 ***********/
                    else                                           /*****************************/
                    {                                             //第四种情况是reduce trie中只有a
                                                                 //节点,tail中有 "bc0" 现在插入"abc0" 
                       break;
                    }
                  }
                  if(ch == 0 && *begin == 0)
                  {
                      return ;// 情况4
                  }
                  else if(*begin==0) // begin == end 
                  {
                      /* for(i=1;check[i+ch]==1<<31;++i); check[ch+i] = node; base[node] = i; base[ch+i] = -pos; ch = 0; for(i=1;check[ch+i]==1<<31;++i); base[node]=i; base[ch+i]=0; check[ch+i]=node; return ; // break; {情况3 那么 'd'与 0 已经插入到了 reduces Trie中} 这段可能所错的 */
                      for(i=1;check[i+ch]!=1<<31||check[i]!=1<<31;++i); // check[i+ch] == check[i+0] == 1<<31
                      check[i]=check[ch+i] = node;
                      base[node] = i;
                      base[ch+i] = -pos; // 没问题这里 对于情况3 当 d 与 0 逻辑到这里 , pos 指向 e 
                                         //这里base[S1_child] = -pos 
                      ch = 0;// 替换ch 这里 s1串的处理完了,交给下面公共代码去处理S2串
                  }
                  else
                  {  //情况1 与 2 
                     unsigned char S2Ch = *begin; 
                     for( i =1 ;check[S2Ch+i]!=1<<31||check[ch+i]!=1<<31;++i); // 这俩个for循环一个是处理第一个串的区分节点的对应的相应字符
                     base[node] = i; // 一个是处理第二个串的字符区分节点的相应字符
                     check[ch+i] = node;
                     check[S2Ch+i] = node;
                     if(ch == 0 )
                       base[ch+i] = 0;
                     else
                       base[ch+i] = -pos;
                     ch = S2Ch;//替换ch 这里 s1串的处理完了,交给下面公共代码去处理s2串
                     ++begin;// 这里存到tail数组的字符应该是 区分节点表示字符的下一个字符, 
                            // abcde abcgh 这里S2串 区分节点代表的字符是 g 可存到tail中的应该是 h之后的字符 
                   /* ch = *begin; for( i=1;check[ch+i]==1<<31;++i); base[node] = i; check[ch+i] = node;*/
                  }
                  break;    
              }
             if(check[child]!=node)
             {
               /*****位置冲突,此情况是2个父节点在竞争一个子节点*****/
               int TroubleNode = check[child];
               vector<unsigned char>l1,l2;
               for(unsigned short i = 0; i < 256 ;++i)
               {
                 if(check[base[TroubleNode]+i]==TroubleNode) // base[base[TroubleNode]+i] 这里子节点的值无论大于0还是小于0我们都得到了
                   l1.push_back(i); // base[TroubleNode]+i 就是child啦 我们看child对应的 
                 if(check[base[node]+i]==node)//base是否有效即可, 有效就代表着那条边上的字符是被插入的字符
                   l2.push_back(i);
               }
               if(l1.size() > l2.size())
               {
                  l1.swap(l2);    //得到最少边的容器,并且确定到底修改那个节点
                  TroubleNode = node ;
               }
               /* else { TroubleNode = node;//如果L1 的边数少于L2, 那么被修改点 Trouble就该是 node了,而不是check[node]这个 } //冲突节点。总之我们在这里要选择出边数最少的去修改,这样开销最少 */
               // cout <<"Troublenode : " <<TroubleNode <<endl;
               auto size = l1.size();
               decltype (size) i = 1;
               bool flag = true; // 这个flag值代表我们找到了适合的数字q ,使得所有子节点check[q+child] == 0 
               while(1) 
               {
                   unsigned int  aim = 0; 
                   for(decltype(size) j = 0; j < size;++j)
                  {
                     aim = i+l1[j];
                     if(aim > check.size())
                     {
                        ReSizeBaseCheck(aim); // 用来防止当寻找的位置超过check的长度的时候我们需要扩容
                     }
                     if(check[i+l1[j]] != 1<<31)
                     {
                        flag = false;
                        break;
                     }
                  }
                  if(flag)
                     break;  
                  ++i,flag = true; 
               }
               /*******现在把TroubleNode的所有子节点都更新到新的位置处*******/
               int ChildTemp = 0;
               int NewChild = 0;
               for(auto & element : l1)
               {
                  /****** i 等于 base[TroubleNode]的新偏移量*******/
                  ChildTemp = base[TroubleNode]+element;
                  assert(ChildTemp < (int)base.size()); // 这个不强转的话,就会无符号与有符号比较,太危险了,负数直接会溢出成为正数,所以必须把 size_t 转换为 int 
                  NewChild = element + i;
                  base[NewChild]=base[ChildTemp];//新位置接管每个子孩子旧位置的初始偏移量
                  check[NewChild] = TroubleNode;
                  if(base[ChildTemp]>0)
                  {
                      /****如果大于0说明该子节点也有自己孩子,那么我们需要让新的子节点接管老的子节点孩子****/
                      /****小于0那就说明没有子节点,直接把pos下标赋值给新节点即可就不用进这段逻辑了********/
                      int grandson = 0;
                      for(unsigned short k = 0; k < 256 ; ++k)
                      {
                           if(check[grandson=base[ChildTemp]+k]==ChildTemp)
                              {
                                    base[NewChild] = base[ChildTemp];
                                    check[grandson] = NewChild;
                              }
                      }
                  }

                  base[ChildTemp] = 0;
                  check[ChildTemp]= 1<<31;
               }
               base[TroubleNode] = i; 
               --begin;
               continue; // 冲突解决重新再插入冲突字符
             }
             node = child; 
           }
           /********* 从 Tail 中把共同字符插入到reduce trie中完毕 ,现在处理tail 数组中的遗留问题*/
           /*情况3这种特殊的已经处理了 剩下的可以都当一种搞,而且原有的已经都处理完毕,只剩下待插入s2的处理*/
          pos = _pos;
          base[ch+base[node]] = -pos; 
          while(begin != end)
          {
             tail[pos++] = *begin++;
          } 
          _pos = pos; 
    }
    bool Find(string & str)
    {
        cout << str << endl;
        if(str.size()==0)
            return false;
        int node = 1;
        string::iterator begin = str.begin();
        string::iterator end = str.end();
        unsigned char ch = 0;
        while(begin!=end)
        {
           ch = *begin;
           if(base[node=base[node]+ch]==0)
             return false;
           else if(base[node] < -1)
           {
               /***逻辑走到这里表明有一个相同字符的节点是区分节点,剩余的字符在tail中,需要去tail中比较****/
               /*****能做到这里说明 区分节点的字符 与 str 对应的字符 都是相同的,否则base[x] == 0 了***/
               /*****所以我们要 让 begin 向后走一位 才对,因为区分节点的字符已经比较过了*****/
               ++begin;
               int pos = -base[node];
               while((ch=tail[pos++])&&begin!=end)
               {
                  if(ch!=*begin++)
                    return false;
               }
               return true;
           }
          ++begin;
        }
        return true;
     }

#ifdef DEBUG
void Fun()
{
  for(auto & i:base)
  {
     cout<< i <<" ";
  }
  cout << endl;
  auto   size = check.size();
  decltype(size) i = 0;
  int start = 0;
  for( i = 0; i < size; ++i)
   {
      if(check[i] > 0 && base[i]<0)
      {
          cout<<check[i] << " " << i <<" ";
          start = -base[i];
          cout << start << endl;
          while(tail[start]) cout<<tail[start++]<<" ";
          cout << endl;
      }
   }

}
#endif 
protected:
  size_t CharSize(string & str)
{
    size_t ret = 0;
    for(auto & i : str)
      ret +=i;
    return ret;
}

private:
    vector<int> base;
    vector <int> check;
    string tail;
    unsigned long _pos;
};

#endif

正确代码

// Copyright (c) 2018 QIHOO Inc
// Author: Chengyuxuan (chengyuxuan@360.cn)

#ifndef DAT_DATRIE_H
#define DAT_DATRIE_H
#include <iostream>
#include <vector>
using namespace std;
#include <string>
#include <unistd.h>
#include <assert.h>
#define INVALID  0x8000000000000000
/********核心思想*************/
/***Base[parent]+ch = child***/
/***check[child] = parent ****/
/*****************************/
class DATrie
{
public:
    DATrie()
        :base(1024,0), check(1024,INVALID), tail(256,0),_pos(1),size_(0)
    {
        base[0] = 1; // root 是下标 0 开始的,我们把它初始偏移量初始为1.这是因为如果从1开始那么可能在后面我们插一个二进制串0,第一个字符是0,base[1]+0 为1 ,因为初始的时候base[1]的base值初始化了,但是却没有check选择归主,这个时候插入应该是走
//冲突逻辑的但是。因为没有设置check却走了 一般逻辑会造成bug ,所以从0开始。
    }               // check 初始化为 INVALID 代表这些节点都没有父节点 ,选择INVALID是因为,最大的有符号long int 也比这个小,我们取pos的时候
                   // 都是 -pos , 即不可能取到这个值,那么就用这个值来充当 check中空闲空间的标志是极为合适的
                   // pos 初始化为 1 是因为如果不初始化为1 初始化成 0 的话,那么第一个串插入时候 base 保存 -pos 的时候 还是0
                   // 遍历的时候就会跟base那些初始化的0混

    DATrie(const vector<int64_t> & _base , const vector<int64_t> & _check , const string & _tail,uint64_t pos )
      :base(_base),check(_check),tail(_tail),_pos(pos),size_(0)
    {}
    void ReSizeBaseCheck(int size )
    {
        base.resize(2*size,0);
        check.resize(2*size,INVALID);
    }
    /* void InSert( string str) */
    bool InSert( string  str)
    {
        assert(str.size()>0);
        size_t  ret = CharSize(str);
        if(ret > base.size())
         {
            ReSizeBaseCheck(base.size());
         }
        if(tail.size()-_pos < str.size())
           tail.resize(2*tail.size()); 
        str.push_back(0); // 因为string 中的结束符不是 \0 , 所以这里加入一个\0用来区分 tail中的不同字符串
        string::iterator begin = str.begin();
        string::iterator end = str.end();
        volatile int64_t node = 0;
        int64_t child;
        unsigned char ch;
        int64_t pos;
        static uint64_t  NewOffset = 1; // 这里把offset 变成了静态的那么 多线程下是不安全的
        while (begin != end)
           {
             ch = *begin++;
             child = base[node] + ch;
             assert(child > 0);
             if(child >= base.size())
             {
                ReSizeBaseCheck(child);
             }
             if(check[child]== INVALID)
               {
                    check[child] = node;
                    break;
               }
             if(check[child] == node && check[child] >= 0 && base[child]<0)
              {
                  /*有冲突当前节点不再是单独节点了,单独节点表示从当前节点开始能够区分多个有共同前缀的字符串*/
                  int64_t i = 0;
                  pos = -base[child];
                  node = child;
                  while((ch=tail[pos++])!=0 && begin!=end)
                  {
                     if(ch == *begin)
                    {
                         for(NewOffset;check[NewOffset+ch/*base[i+ch]*/]!=INVALID;++NewOffset)
                           {
                              if(NewOffset+ch > check.size())
                                ReSizeBaseCheck(NewOffset+ch);
                           }                                               /*****冲突的四种情况**********/
                         base[node] = NewOffset;                           /*****************************/
                         check[NewOffset+ch] = node;                       /* 1. abcde / abcgh**********/
                         ++begin;                                          /* 2. abc / abcde **********/
                         node = NewOffset+ch;                              /* 3. abcdef / abc **********/
                    }                                                      /* 4. abc0 / abc0 ***********/
                    else                                                   /*****************************/
                    {                   /*第四种情况是reducetrie中只有a节点,tail中有 "bc0" 现在插入"abc0"*/ 
                       break;
                    }
                  }
                  if(ch == 0 && *begin == 0)
                  {
                      for(NewOffset;check[NewOffset]!=INVALID;++NewOffset)
                      {
                          if(NewOffset > check.size())
                             ReSizeBaseCheck(NewOffset);
                      } 
                        base[node] = NewOffset;
                        check[NewOffset] = node;
                        base[NewOffset] = 0;
                     // cout << str.data()<< pos << endl;
                      ++size_;
                      return true;// 情况4
                  }
                  else if(*begin==0) // begin == end 情况3 
                  {
                      for(NewOffset;check[NewOffset+ch]!=INVALID||check[NewOffset]!=INVALID;++NewOffset) 
                      {
                               cout << NewOffset << endl;
                               if(NewOffset+ch > check.size())
                                  ReSizeBaseCheck(NewOffset+ch);
                      }
                      check[NewOffset]=check[ch+NewOffset] = node;
                      base[node] = NewOffset;
                      base[ch+NewOffset] = -pos; // 没问题这里 对于情况3 当 d 与 0 逻辑到这里 , pos 指向 e 
                      base[NewOffset+0] = 0;
                      ++size_;
                      return true;
                     // ch = 0;// 替换ch 这里 s1串的处理完了,交给下面公共代码去处理S2串
                  }
                  else
                  {  //情况1 与 2 
                     unsigned char S2Ch = *begin;
                    if(S2Ch+NewOffset > check.size())
                        ReSizeBaseCheck(S2Ch+NewOffset);
                     for(NewOffset;check[S2Ch+NewOffset]!=INVALID||check[ch+NewOffset]!=INVALID;++NewOffset)
                     {
                                 if(S2Ch+NewOffset > check.size())
                                   ReSizeBaseCheck(S2Ch+NewOffset);
                     }                                        
                                     // 这俩个for循环一个是处理第一个串的区分节点的对应的相应字符
                     base[node] = NewOffset; // 一个是处理第二个串的字符区分节点的相应字符
                     check[ch+NewOffset] = node;
                     check[S2Ch+NewOffset] = node;
                     if(ch == 0 )
                       base[ch+NewOffset] = 0;
                     else
                       base[ch+NewOffset] = -pos;
                     ch = S2Ch;//替换ch 这里 s1串的处理完了,交给下面公共代码去处理s2串
                     ++begin;// 这里存到tail数组的字符应该是 区分节点表示字符的下一个字符, 
                            // abcde abcgh 这里S2串 区分节点代表的字符是 g 可存到tail中的应该是 h之后的字符 
                  }
                  break;    
              }
             if(check[child]!=node)
             {
               /*****位置冲突,此情况是2个父节点在竞争一个子节点*****/
               int64_t TroubleNode = check[child];
               vector<unsigned char>l1,l2;
               for(unsigned short i = 0; i < 256 ;++i)
               {
                 if(check[base[TroubleNode]+i]==TroubleNode) // base[base[TroubleNode]+i] 这里子节点的值无论大于0还是小于0我们都得到了
                   l1.push_back(i); // base[TroubleNode]+i 就是child啦 我们看child对应的 
                 if(check[base[node]+i]==node)//base是否有效即可, 有效就代表着那条边上的字符是被插入的字符
                   l2.push_back(i);
               }
               // 保证 base[root]不可被修改 TroubleNode == 0
               if(l1.size() > l2.size()||TroubleNode == 0 )
               {
                  l1.swap(l2);    //得到最少边的容器,并且确定到底修改那个节点
                  TroubleNode = node ;
               }
               auto size = l1.size();
               bool flag = true; // 这个flag值代表我们找到了适合的数字q ,使得所有子节点check[q+child] == 0 
               while(1) 
               {
                   uint64_t   aim = 0; 
                   for(decltype(size) j = 0; j < size;++j)
                  {
                     aim = NewOffset+l1[j];
                     if(aim > check.size())
                     {
                        ReSizeBaseCheck(aim); // 用来防止当寻找的位置超过check的长度的时候我们需要扩容
                     }
                     if(check[aim] != INVALID)
                     {
                        flag = false;
                        break;
                     }
                  }
                  if(flag)
                     break;  
                  ++NewOffset,flag = true; 
               }
               /*******现在把TroubleNode的所有子节点都更新到新的位置处*******/
               int64_t ChildTemp = 0;
               volatile int64_t NewChild = 0;
               for(auto & element : l1)
               {
                  /****** NewOffset 等于 base[TroubleNode]的新偏移量*******/
                  ChildTemp = base[TroubleNode]+element;
                  assert(ChildTemp < (int64_t)base.size()); // 这个不强转的话,就会无符号与有符号比较,太危险了,负数直接会溢出成为正数,所以必须把 size_t 转换为 int 
// 这里没有选择判断childtemp是否超过就扩容是因为,正确逻辑就孩子是不会超过base界限的,所以这里assert下
                  NewChild = element + NewOffset;
                  if(NewChild > base.size())
                       ReSizeBaseCheck(NewChild);
                  base[NewChild]=base[ChildTemp];//新位置接管每个子孩子旧位置的初始偏移量
                  check[NewChild] = TroubleNode;
                  if(base[ChildTemp]>0)
                  {
                      /****如果大于0说明该子节点也有自己孩子,那么我们需要让新的子节点接管老的子节点孩子****/
                      /****小于0那就说明没有子节点,直接把pos下标赋值给新节点即可就不用进这段逻辑了****/
                      int64_t grandson = 0;
                      for(unsigned short k = 0; k < 256 ; ++k)
                      {
                           if(check[grandson=base[ChildTemp]+k]==ChildTemp)
                              {
                                    check[grandson] = NewChild;
                              }
                      }
                  }
                  /* 这个if是用来处理特殊情况,就是TroubleNode 和 node 是父子关系的时候,也就是ChildTemp 为node的时候需要更新node的新位置*/
                  if(ChildTemp == node)
                  {
                       node  = NewChild ;
                       cout << "NewChild :" << NewChild <<endl;
                       cout << "node :" << node << endl;
                       cout << "ChildTemp :" << ChildTemp <<endl;
                  }
                  base[ChildTemp] = 0; // 设为无效
                  check[ChildTemp]= INVALID;
               }
               base[TroubleNode] = NewOffset; 
               --begin;
               continue; // 冲突解决重新再插入冲突字符
             }
             node = child; 
           }
           if(base[node] == 0)
              return false; // 走到这里了 插入了相同的串,导致最后一个node被赋值的时候,被赋值成0,这里直接返回就好
           /* 从 Tail 中把共同字符插入到reduce trie中完毕 ,现在处理tail 数组中的遗留问题*/
           /*情况3这种特殊的已经处理了 剩下的可以都当一种搞,而且原有的已经都处理完毕,只剩下待插入s2的处理*/
          pos = _pos;
          base[ch+base[node]] = -pos;

          while(begin != end)
          {
             tail[pos++] = *begin++;
          } 
          _pos = pos;
          ++size_;
          return true;
    }
    void OutPut(int fd)
    {
         assert(fd>0);
         size_t  size = base.size();
         write(fd,&size,sizeof(size_t));
         write(fd,&base[0],sizeof(int)*base.size());
         size = check.size();
         write(fd,&size,sizeof(size_t));
         write(fd,&check[0],sizeof(int)*check.size());
         size = tail.size();
         write(fd,&size,sizeof(size_t));
         write(fd,&tail[0],sizeof(unsigned char));
         write(fd,&_pos,sizeof(_pos));
    }
    /************************************************************************************************/
    /*** 当有 abc / abcdef插入的时候这俩个字符串都可以插入进去,并区分*******************************/
    /***是这样区分的,从 C节点开始,abc串的查找是 base[node+0] 与 base[node+'d'] 这里node为第节点c***/
    /***因为偏移量不同,所以查找的child就不同,故可以区分开******************************************/
    bool Find(string  str)
    {
        if(str.size()==0)
            return false;
        int64_t  node = 0;
        str.push_back(0);
        string::iterator begin = str.begin();
        string::iterator end = str.end();
        unsigned char ch = 0;
        //str.push_back(0); 写到这里会出错迭代器失效
        while(begin!=end)
        {
           ch = *begin;
           if(base[node=base[node]+ch]==0 && check[node] == INVALID)
             return false;
           else if(base[node] <= -1)
           {
               /***逻辑走到这里表明有一个相同字符的节点是区分节点,剩余的字符在tail中,需要去tail中比较****/
               /*****能做到这里说明 区分节点的字符 与 str 对应的字符 都是相同的,否则base[x] == 0 了***/
               /*****所以我们要 让 begin 向后走一位 才对,因为区分节点的字符已经比较过了*****/
               ++begin;
               int64_t pos = -base[node];
               while((ch=tail[pos++])&&begin!=end)
               {
                  if(ch!=*begin++)
                    return false;
               }
               if(++begin!=end)
                 return false;   //说明给的串 给 已有串长,已有串只是查找串的子串
               else 
                 return true;
               /* 上面++begin的原因是我们先前多push了个0进去.故我需要 ++begin使其先后走一步才正确*/
           }
          ++begin;
        }
        if(base[node]!=0)
           return false;
        else 
           return true;
        //因为我们把 \0插入到里面了,这里就是判断如果是\0结尾则正确
        //否则说明没插入过这个串。可能插入了abcde但是没插入abc
    }
 unsigned long Size()
 {
   return size_;
 }
void Fun()
{
  /* for(auto & i:base) { cout<< i <<" "; } cout << endl; */
  auto   size = check.size();
  decltype(size) i = 0;
  int64_t start = 0;
  for( i = 0; i < size; ++i)
   {
      if(check[i] > 0 && base[i]<0)
      {
          cout<<check[i] << " " << i <<" ";
          start = -base[i];
          cout << start << endl;
          while(tail[start]) cout<<tail[start++]<<" ";
          cout << endl;
      }
   } 
}
protected:
  size_t CharSize(string & str)
{
    size_t ret = 0;
    for(auto & i : str)
      ret +=i;
    return ret;
}

private:
    vector<int64_t> base;
    vector <int64_t> check;
    string tail;
    uint64_t  _pos; // tail 中下一个的位置
    uint64_t  size_;
};

#endif
测试代码
#include "DATrie.h"
#include <iostream>
#include <fstream>
using namespace std;

int main()
{
     DATrie object;
     fstream fileIO("pation1");
     if(!fileIO)
       cout << "cant open" << endl;
     std::string line;
     while(getline(fileIO,line))
     {
        object.InSert(line);
     }
     line.assign("0000616e1fe615a4232dcf50b837a0ec\tCggKBgir1Ds4DA==");
     cout << object.Find(line) << endl;
     return 0;
}
测试数据
000013a61c3ce28910ec5a6e61f745b5 EiAKHggMEgcIARVjfxk+EgcIahVjfxk+EggI6VIVY38ZPhoLIgkI254GFYHQCj4= 00002aaa3db487e5dde91501ce107748 ChAKBgjJ1jw4CAoGCNq6PDgIEkgKCwgNEgcIAhUAAIA/ CikIDBIHCAEVRItsPhIICPpVFUSLbD4SCQisk0MVRItsPhIHCG4VRItsPgoOCBsSCghYGgbplKTlrZAaPyIJCPudBhWF6xE/ KgsIod3JpwsVhesRPyoLCJ/eqYILFYXrET8qCwjkgKbUCBWF6xE/KgsIgKzPsQYVhesRPw== 00002bc1e2a049aa33456183eeaf807c GjIiCQj7nQYVUS+4PSoLCJ/eqYILFVEvuD0qCwjkgKbUCBVRL7g9KgsIw/Ly7wUVUS+4PQ==00002ead14813003f739b1a10fb43750 Eg0KCwgNEgcIARUAAIA/GhYiBwgPFU0VzD0qCwi/na+bCRVNFcw9 0000323b6cd6017e62470911956f13c0 CggKBgjj7Dw4CA== 00003a2b188745a2999fc1c2611bd656 Gj0iBwgPFU0VzD0qCwjy7p2ZAxVNFcw9KgsIid+9iwMVTRXMPSoLCNXS3+ QKFU0VzD0qCwjXx7+dARVNFcw900003d51048fc16986e3f07b96bd94a5 Eg0KCwgNEgcIARUAAIA/ 000047474c0fb600f4f24b0156ce462d ChAKDgiE8TkQme2U0AU4CUAB 000050da9743f4f4b2aba9198e59a9a5 ChAKBgiz8js4CAoGCJqBPDgI 0000616e1fe615a4232dcf50b837a0ec CggKBgir1Ds4DA== 000061b4e39bb62daa686d1aa21cac3c Cg4KDAjHtToQ4fvX1gU4BxINCgsIDRIHCAIVAAAAPw== 
前8个串的图

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

参考文章

http://blog.51cto.com/sbp810050504/1310596
https://linux.thai.net/~thep/datrie/datrie.html

    原文作者:Trie树
    原文地址: https://blog.csdn.net/sdoyuxuan/article/details/79155189
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞