前言
这是唯一一个我到现在写了的不能做A+B的数据结构,如能,还请神犇指教……
想一想,百度的数据总量早已超过 1 T B = 1024 G B = 1048576 M B 1TB=1024GB=1048576MB 1TB=1024GB=1048576MB,是怎么为你迅速地查出来字典树的?
当然是使用字典树。
简介
这是一棵字典树的图片:
在这个字典树中有两种点,黑点和白点,后面都会解释。
这个字典树中存的就是abc, abcd, abd, b, bcd, efg, hij
7个(单词??)字符串。在这棵树里面,如果搜索a
或ab
,就可以给出abc, abcd, abd
三个字符串的结果。
可以看出,a
和ab
是这三个单词的前缀,所以字典树是一种前缀树。
可以看看这个基本信息表:
学名 | 别名 | 类型 |
---|---|---|
字典树 | 单词查找树,Trie树 | 树形数据结构 |
基本性质
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符;2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
- 每个节点的所有子节点包含的字符都不相同。
可以对照一下上图中的字典树看一看是否符合。
实现
定义
首先,我们要明确字典树是一棵树,树是一种图,所以在字典树的定义上,我们采用类似邻接表的方法。
因为在竞赛中一般字典树记录的都是字母或数字,所以可以用二维数组记录,第一维记录当前节点的编号,最大就是所有字符串的长度和(百度:我有几个TB我也不知道),第二维记录边权,也就是这条边所表示的字符,值记录连接到的另外一个点。
一个最多有 1 0 5 10^5 105个节点的记录小写字母的字典树如下定义:
int trie[100001][26], k;
//理解trie[p][c]:编号为p的点走到编号为trie[p][c]的点需要c的路程
k记录的是当前这棵树有多少个节点。
又因为有黑点和白点之分,就还需要一个数组来记录,即:
bool col[100001];
至于这个区别在哪里,请听下回讲解。
插入
再回顾一下前面那张图:
这个字典树中存的就是abc, abcd, abd, b, bcd, efg, hij
7个字符串。
容易发现黑点表示的是一个字符串结束的点。
下面演示在这个字典树中插入字符串bcg
的过程(红点表示当前遍历到的点):
首先,找到根节点。
然后,跟着原先已有的路径向下遍历。
走到了点7,已经完成插入的已经有bc
这个前缀子串了,但是,c
到g
没有路!
怎么办?再创建一个新点,即点15。
注意这个时候我们是站在点7的角度上创建点15这个新点,至于怎么创建新点,搞懂了定义就很简单。
再把点15标黑,因为点15是字符串bcg
的结束节点。于是最终完成时的图长这样:
从图片模拟到代码实现:
考虑将当前遍历到的点用一个数字 p p p来表示,那么我们知道了边权,也就是当前点连接到的字符 c c c,我们也就可以得到连接的另一个点,也就是 t r i e [ p ] [ c ] trie[p][c] trie[p][c]。
当然,因为有效字符有96个之多,这样很容易爆空间,所以不毒瘤的出题人一般都会给出字符的范围,假设最小为 x x x,那么另一个点也就是 t r i e [ p ] [ c − x ] trie[p][c-x] trie[p][c−x]。
因为开始 t r i e trie trie的所有值都为0,又因为没有一个点会连到根,所以如果 t r i e [ p ] [ c ] trie[p][c] trie[p][c]为0,就证明原先没有从 p p p点经过字符 c c c的路径,这个时候就可以建一个新点了。
最后,对插入的字符串的每一个字符进行遍历,依次插入每一个字符,最后记得将字符串的最后一个字符对应的点标黑。
还是那一棵记录小写字母构成的字符串的字典树:(注意insert
是关键字)
inline void Insert(char* s) {
int len = strlen(s), p = 0;
for(int i = 0; i < len; i++) {
char c = s[i] - 'a';
if(!trie[p][c]) trie[p][c] = ++k;
p = trie[p][c];
}
col[p] = 1;
}
inline void Insert(string s) {
int len = s.length(), p = 0;
for(int i = 0; i < len; i++) {
char c = s[i] - 'a';
if(!trie[p][c]) trie[p][c] = ++k;
p = trie[p][c];
}
col[p] = 1;
}
附:这样一个板子几乎可以解决任何的字典树题:
inline Type funtion(char* s) {
int len = s.length(), p = 0;
for(int i = 0; i < len; i++) {
char c = s[i] - x;//x为最小的可能字符
//do sth. ...
p = trie[p][c];
}
//do sth. ...
}
查询
一般的查询有3种:查询是否存在这个字符串,查询是否有这个字符串的前驱字符串,查询是否有这个字符串的后缀字符串。
因为都可以用板子,下面一一亮出代码:
inline bool Search1(char* s) {
//查询是否存在这个字符串
int p = 0