字典树
Trie(字典树)又称前缀树
给出字符串集合:code、cook、five、file、fat,我们设根节点为空节点,那么其Trie树如下所示:
字典树又称前缀树,给出一个字符串集合,我们可以通过公共前缀将整个字符串集合存到一个树形结构中,一旦形成了集合中的字符串,我们只需在树上的相应节点打上标记
存储
我们采用双数组存储的方式,其中Next数组是二维数组,第二维之所以是26是因为每个节点都可能连接26个字母,另外一个标记数组可以有两种形式:
- bool类型的vis数组只标记该节点结尾字符串是否存在
- int类型的num数组既可以标记是否存在又能存储在集合中的下标
const int maxn=max_len*num; //字符串最大长度乘以数量
int Next[maxn][26];
bool vis[maxn]; //该结点结尾的字符串是否存在
int num[maxn]; //该结点结尾的字符串是否存在,存在则保存下标
不难发现我们在存储时是将字符转化为0-25的数字下标:
建树
如何建树?我们采用动态开点的方式,这里类似链式前向星那样。Next数组第一维保存的就是动态开点的数,第二维是接下来指向的字符(0-25)
每个字符串的第一个字符保存在下标为0的集合中,而且root实际上是不存在的,也就是说如果有多种字母开头的字典树,那么这实际上是一个森林
建树的过程即插入的过程,时间复杂度为O(n*len)
标记是否存在
int cnt=0;
void insert(char *s) { //插入字符串
int p=0,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
if(!Next[p][c]) Next[p][c]=++cnt; //没有就添加结点
p=Next[p][c];
}
vis[p]=1;
}
标记并保存下标
int cnt=0;
void insert(char *s,int k) { //插入字符串集合的字符串k
int p=0,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
if(!Next[p][c]) Next[p][c]=++cnt; //没有就添加结点
p=Next[p][c];
}
num[p]=k;
}
查找字符串
查找时,首先目标串的每一个前缀必须都出现,也就是Next[p][s[i]-‘a’]都有值,那么查询到最后一个节点时,如果该节点被标记,那么vis[p]就不为0,否则为0,因此直接返回vis[p]
查询的时间复杂度为O(len)
int find(char *s){ //查找字符串
int p=0,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
if(!Next[p][c]) return 0;
p=Next[p][c];
}
return vis[p];
}
简单模板
const int maxn=max_len*num; //每个字符串最大长度乘以字符串数量
struct Trie{
int Next[maxn][26],cnt;
bool vis[maxn]; //该结点结尾的字符串是否存在
//int num[maxn];
void init(){
cnt=0;
memset(vis,0,sizeof vis);
memset(Next,0,sizeof Next);
}
void insert(char *s,int k) { //插入字符串
int p=0,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
if(!Next[p][c]) Next[p][c]=++cnt; //没有就添加结点
p=Next[p][c];
}
vis[p]=1;
}
int find(char *s){ // 查找字符串
int p=0,len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'a';
if(!Next[p][c]) return 0;
p=Next[p][c];
}
return vis[p];
}
};