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: