转自 http://www.ahathinking.com/archives/14.html
本文讨论一棵最简单的trie树,基于英文26个字母组成的字符串,讨论插入字符串、判断前缀是否存在、查找字符串等基本操作;至于trie树的删除单个节点实在是少见,故在此不做详解。
- Trie原理
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
- Trie性质
好多人说trie的根节点不包含任何字符信息,我所习惯的trie根节点却是包含信息的,而且认为这样也方便,下面说一下它的性质 (基于本文所讨论的简单trie树)
1. 字符的种数决定每个节点的出度,即branch数组(空间换时间思想)
2. branch数组的下标代表字符相对于a的相对位置
3. 采用标记的方法确定是否为字符串。
4. 插入、查找的复杂度均为O(len),len为字符串长度
- Trie的示意图
如图所示,该trie树存有abc、d、da、dda四个字符串,如果是字符串会在节点的尾部进行标记。没有后续字符的branch分支指向NULL
- Trie的优点举例
已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。下面对比3种方法:
1. 最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。
2. 使用hash:我们用hash存下所有字符串的所有的前缀子串。建立存有子串hash的复杂度为O(n*len)。查询的复杂度为O(n)* O(1)= O(n)。
3. 使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)。
解释一下hash为什么不能将建立与查询同时执行,例如有串:911,911456输入,如果要同时执行建立与查询,过程就是查询911,没有,然后存入9、91、911,查询911456,没有然后存入9114、91145、911456,而程序没有记忆功能,并不知道911在输入数据中出现过。所以用hash必须先存入所有子串,然后for循环查询。
而trie树便可以,存入911后,已经记录911为出现的字符串,在存入911456的过程中就能发现而输出答案;倒过来亦可以,先存入911456,在存入911时,当指针指向最后一个1时,程序会发现这个1已经存在,说明911必定是某个字符串的前缀,该思想是我在做pku上的3630中发现的,详见本文配套的“入门练习”。
- Trie的简单实现(插入、查询)
02 usingnamespacestd;
03
04 constintbranchNum=26;//声明常量
05 int i;
06
07 structTrie_node
08 {
09 bool isStr; //记录此处是否构成一个串。
10 Trie_node *next[branchNum];//指向各个子树的指针,下标0-25代表26字符
11 Trie_node():isStr(false)
12 {
13 memset(next,NULL,sizeof(next));
14 }
15 };
16
17 classTrie
18 {
19 public:
20 Trie();
21 void insert(constchar*word);
22 bool search(char*word);
23 void deleteTrie(Trie_node*root);
24 private:
25 Trie_node* root;
26 };
27
28 Trie::Trie()
29 {
30 root = new Trie_node();
31 }
32
33 voidTrie::insert(constchar*word)
34 {
35 Trie_node *location = root;
36 while(*word)
37 {
38 if(location->next[*word-'a']==NULL)//不存在则建立
39 {
40 Trie_node *tmp = new Trie_node();
41 location->next[*word-'a']=tmp;
42 }
43 location = location->next[*word-'a'];//每插入一步,相当于有一个新串经过,指针要向下移动
44 word++;
45 }
46 location->isStr=true;//到达尾部,标记一个串
47 }
48
49 boolTrie::search(char*word)
50 {
51 Trie_node*location=root;
52 while(*word&&location)
53 {
54 location = location->next[*word-'a'];
55 word++;
56 }
57 return(location!=NULL&&location->isStr);
58 }
59
60 void Trie::deleteTrie(Trie_node*root)
61 {
62 for(i=0;i<branchNum;i++)
63 {
64 if(root->next[i]!=NULL)
65 {
66 deleteTrie(root->next[i]);
67 }
68 }
69 deleteroot;
70 }
71
72 voidmain()//简单测试
73 {
74 Triet;
75 t.insert("a");
76 t.insert("abandon");
77 char * c = "abandoned";
78 t.insert(c);
79 t.insert("abashed");
80 if(t.search("abashed"))
81 printf("true\n");
82 }
入门练习 :PKU POJ 3630解题报告
另,附上当时在博客园与daviddang讨论的问题,就是增加一个删除单词的功能,如果单词不存在返回false。
02 {
03 Trie_node * current = root;
04 std::stack<Trie_node*>nodes;//用来记录经过的中间结点,供以后自下而上的删除
05 while (*word!='\0'&¤t!=0)
06 {
07 nodes.push(current);//经过的中间结点压栈
08 current = current->next[*word-'a'];
09 word++;
10 }
11
12 if (current && current->isStr)
13 {
14 current->isStr = false;//此时current指向该word对应的叶子结点
15 while (nodes.size()!=0)
16 {
17 char c = *(--word);
18 current = nodes.top()->next[c-'a'];//取得当前处理的结点
19
20 bool isNotValid = true;
21 for (int i=0;i<26;++i)//判断该结点是否是所有的next为空或只有该word对应字符域不为空
22 {
23 if (current->next[i]!=0)
24 {
25 isNotValid=false;
26 }
27 }
28 if (current->isStr==0&&isNotValid)//当一个节点无效,即它只和该word相关,可以删除
29 {
30 deletecurrent;
31 }
32 else//说明当前这个中间结点也被其他的结点所用,不能删除。
33 {
34 break;
35 }
36 nodes.top()->next[c-'a']=0;//把上层的结点的next中指向current结点的指针清0
37 nodes.pop();
38 }
39 return true;
40 }
41 else
42 {
43 return false;
44 }
45 }
附上一个简单的测试用例
02 {
03 Triet;
04 t.insert("abc");
05 t.insert("d");
06 char * c = "dda";
07 t.insert(c);
08 t.insert("da");
09 if(t.search("da"))
10 {
11 printf("find\n");
12 }
13 t.deleteWord("dda");
14 if(!t.search("dda"))
15 {
16 printf("not find\n");
17 }
18 t.deleteWord("d");
19 if(!t.search("d"))
20 {
21 printf("not find\n");
22 }
23 if(t.search("da"))
24 {
25 printf("find\n");
26 }
27 }
28 //结果应该是
29 find
30 not find
31 notfind
32 find
另外一个个据称没有借助任何数据结构,只有两个指针删除节点的方法,暂时没验证对不对
bool deleteWord(char *s, node *root) {
node *p=root;
node *prev=root;
char *pres=s;
while(*s!='\0'&&p->next[*s-'a']!=NULL){
if(p->isWord){
prev=p;
pres=s;
}
int count=0;
for(int i=0; i<26; i++){
if(p->next[i]!=NULL)
count ++;
}
if(count>1){
prev=p;
pres=s;
}
p=p->next[*s-'a'];
s++;
}
if(*s=='\0'){
for(int i=0; i<26; i++){
if(p->next[i]!=NULL){
p->isWord=false;
return true;
}
}
p=prev;
s=pres;
while(*s!='\0'&&p->next[*s-'a']!=NULL){
prev = p->next[*s-'a'];
p->next[*s-'a']=NULL;
int count=0;
for(int i=0; i<26; i++){
if(prev->next[i]!=NULL)
count++;
}
if(count <=1){
pres = s;
break;
}
p=prev;
s++;
}
s=pres;
p=prev;
while(*s!='\0'&&p->next[*s-'a']!=NULL){
prev = p->next[*s-'a'];
delete p;
p=prev;
s++;
}
return true;
} else
return false;
}
方法一:trie树
有了上面学习的思考与总结,3630用trie树本以为可以水过,可是学习和做题终究是两回事,我很快写出trie树,然后提交,超时了。
后来受discuss提示,我大致计算了一下本题trie树的复杂度,号码个数10000,长度10,树的宽度大概有10000,所以总的节点数大概就有100,000级,即要进行十万次new的操作,确实时间耗费很多,估计这样题目的用时要有1秒到2秒左右的样子。
于是为了纯粹的做题,我将new操作省掉,改为提前申请一个buffer空间,就ac了,时间变为125ms了,不过这样确实挺耗空间的,没办法,为了做题只能用空间换时间。
代码如下:
#include<iostream>
using namespace std;
int cases, count;
int nodenum;
struct node
{
bool isExist;
node * branch[10];
}Node[100000];
class Trie
{
private:
node root;
public:
boo publl insert(char num[])
{
node *location = &root;
int i = 0;
int len = strlen(num);
while(num[i])
{
if(i==len-1 && location->branch[num[i]-'0'] != NULL) //解决没有按照长度排序而存在的问题
{
return false;
}
if(location->branch[num[i]-'0']==NULL)//没有建立
{
location->branch[num[i]-'0'] = &Node[nodenum];
Node[nodenum].isExist = false;
memset(Node[nodenum].branch,NULL,sizeof(Node[nodenum].branch));
nodenum++;
}
if(location->branch[num[i]-'0']->isExist == true)
{
return false;
}
location = location->branch[num[i]-'0'];
i++;
}
location->isExist = true;
return true;
}
};
int main()
{
scanf("%d",&cases);
while(cases--)
{
nodenum = 1;
bool flag = true;
scanf("%d",&count);
char tel[11];
Trie t;
while(count--)
{
scanf("%s",tel);
if(!t.insert(tel))
{
flag = false;
}
}
if(flag)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
}
return 0;
}
方法二:
转成数字存储比较,这样的话用long整形就可以,然用除法+取余的方法核对是否是某个数字的前缀,但是这种方法的复杂度显然是O(n^2)呀,所以就不尝试了。
方法三:
受大雄提示,可以使用字符串排序比较来做,因为通过排序,前缀子串肯定是与父串挨着的,嘿嘿,这样做,思路简单、代码量少,易理解啊,所以很快ac,下面分析一下复杂度。
理论上使用trie的平均复杂度应该是n*len;其中,len是号码的平均长度,n是号码的个数。使用数组进行字符比较,理论上的复杂度有n*len+logn,排序为logn,然后查询是否存在前缀子串是n*len。所以后者应该时间稍微多一点,提交后果然,耗时188ms。
另外值得一提的是使用数组比较的方法有个好处,那就是地址都是连续的,cpu在寻址时会非常快,而用链式结构(即指针),包括使用数组型的trie树则是跳来跳去的,故会有一些开销吧。
呵呵,我所崇拜的排序又一次派上用场了。
#include<iostream>
using namespace std;
int cases, count;
char tel[10005][11];
int i, j;
int cmp(const void *a, const void *b)
{
return strcmp( (char*)a,(char*)b );
}
int main()
{
scanf("%d",&cases);
while(cases--)
{
bool flag = true;
scanf("%d",&count);
for(i = 0; i < count; i++)
{
scanf("%s",tel[i]);
}
qsort(tel,count,sizeof(char)*11,cmp);
int len1, len2;
for(i = 1; i < count; i++)
{
len1 = strlen(tel[i-1]);
len2 = strlen(tel[i]);
j = 0;
if(len1 <= len2)
{
while(tel[i-1][j] == tel[i][j] && j < len1)
{
j++;
}
if(j == len1)
{
flag = false;
}
}
if(!flag)
{
break;
}
}
if(flag)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
}
return 0;
}
贴上自己的代码
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#define _DEBUG 1
#define MAX_N 10
//WA了无数次,发现数组开小了 10000-->100010 AC
//,这应是题目中测试数据问题,因为题目要求1 ≤ n ≤ 10000
#define MAX_NODE 100010
typedef struct Node{
bool isEnd;
int branch[MAX_N];
}TrieNode;
TrieNode nodes[MAX_NODE];
int node_c;//node计数
bool insert(const char *str){
int cur = 0;
while(*str){
if(nodes[cur].isEnd == true){
return false;
}
int v = *str -'0';
if(nodes[cur].branch[v] == -1){
nodes[cur].branch[v] = ++node_c;
}
cur = nodes[cur].branch[v];
str++;
}
for(int i=0;i<=9;++i){
if(nodes[cur].branch[i] != -1)//说明本字符串为别的字符的前缀
return false;
}
nodes[cur].isEnd = true;
return true;
}
int main(){
int t,n;
int i;
#if _DEBUG == 1
freopen("POJ3630.in","r",stdin);
#endif
scanf("%d",&t);
char str[11];
while(t>0){
scanf("%d\n",&n);
bool isConsistent=true;
node_c = 0;//reset to 0
for(int i=0;i<MAX_NODE;++i){
nodes[i].isEnd=false;
memset(nodes[i].branch,-1,sizeof(nodes[i]));
}
for(i=0;i<n;++i){
gets(str);
if(isConsistent && !insert(str)){//插入
isConsistent = false;
}
}
if(isConsistent)
printf("YES\n");
else
printf("NO\n");
t--;
}
return 0;
}