Trie树
基本概念
Trie树又称字典树,它是用来查询字符串的一种数据结构。一般它每一个节点都有26个子节点,所以是26叉树。优点查询字符串的时候速度快,缺点浪费大量空间。但是也可以实现255个子节点对应ASCII码0~255,具体看需求吧。
当一个字符串长为M的时候,假设数据结构中已经有了N个字符串了。对于平衡树而言,查找一个字符串 O(log^n)。对于字典树来说就O(M)。
Trie树图
例图A
例图B
性质
- 利用每一个字符串的公共前缀来节约内存。
- 每次查找的时候,都是从trie树上的跟节点进行检索
- 根节点不包含字符,其余节点每一个节点都只包含一个字符。
- 由跟节点开始到某一节点,所有路径上的祖先节点的字符与本节点的字符构成的字符串,即为该节点的字符串。
- 每个节点的所有子节点包含的字符各不相同。
时间与空间复杂度分析
- 当存储少量字符串时,Trie消耗空间较大。因为键值并非显式存储的,而是与其他键值共享的字符串,但存储大量字符串的时候,Trie空间明显会降低。
- 查询快,因为查找的时间复杂度。跟当前数据结构中已经存储了多少字符串无关。缺点随着树高的增加空间增长过快,为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;
}
这种设计处于1与2直接,查找效率 O(M) * log26 ,比1节省很多内存,但是没有2节省,
毕竟RBnode大小比单个指针大。
但是实际上我们可以计算一波帐,一个RBNode 三个指针+value 大小起码也30多字节了。
假设第一层
RBT 我们挂26个字符 都 780个字节了。而 数组需要 208个字节。
假设第二层每个子节点都只挂一个字符
RBT 26个map , 每个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
核心思想
它的核心思想就是把一个待插入的字符串分开存储,把有公共部分子串的字符存到树中,把独特的子串存到Tail数组中。
我们把这个树称为 Reduce-Trie ,把Tail 数组 可以看做单独子串的数组。而Reduce-Trie 是用Base数组表达出来的一个抽象的树并非真正意义的存在。
查找的时候根据从root节点开始,根据状态方程查找每一个字符的子节点。即 base[root] + Ch = child , 如果这个child 的base值是无效的,则代表这个字符串没有在DATrie中,如果是负值则代表有部分子串在Tail数组中,剩下的就是跟Tail数组中每个字符做比较了,如果每个字符都相同那么,该字符串就在DATrie中。
插入原理与场景
场景1 正常无冲突无前缀
这种情况,我们插入的时候直接插入即可,也就是找到了一个空的base节点(base数组中的一个位置) 发现它是空的,也就是对应的check数组中的位置中的值是无效值,没人使用它,那么证明这个串 没有公共前缀 是第一次插入, 那么我们把它对应check数组中的位置赋值成上一个节点的下标,这个操作实际上就是 child -> parent = node , 也就是把 父子节点连接起来而已。
把Reduce-trie中处理完后,把该字符串剩下的位置保存到 Tail数组中即可,也就是上图的意思。
场景2 有公共前缀无冲突
第二个情况就是有公共冲突了,假如已经插入 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个串的图
参考文章
http://blog.51cto.com/sbp810050504/1310596
https://linux.thai.net/~thep/datrie/datrie.html