1.什么是字典树
字典树,又称单词查找树或者键树,或者前缀树,是哈希树的一种变种,典型的应用就是保存大量的字符串的信息,统计和排序大规模字符串,因为采用了前缀的概念,压缩存储了部分的字符串,所以说,查询和查找效率都非常的高,接近于哈希
主要思想就是,利用了空间换时间的思路,将多个字符串的最大公共前缀压缩存储
示例图:
2.字典树的ADT
描述如下:
1.根节点不含有键值
2.每个节点之下有多个指针指向下一个待查键位
3.每个节点的子节点各不相同
主要的核心数据域
1.键值(该节点的键值保存)
2.子指针(如果是孩子兄弟树存储方式还有兄弟指针,子指针还可以作为叶子节点和树节点的区分标志)-如果采用孩子兄弟表示法,我们又称其为双链树
3.标记域(记录该单词是否出现过,因为我们采取了压缩存储公共前缀的方式,所以说,我们有必要对标记域进行声明)
4.kind标记域,标记该节点是叶子节点还是树节点
5.其他(根据题目的具体要求,我们还可以加设其他的结构,方便问题的解决)
操作:
1.插入单词
2.查询单词
3.建树
4.销毁树
5.DFS(深度优先遍历)-如果本树建立之时就已经采用了语序的状态,那么我们深度优先的前序遍历该子树就会得到字典序排列
3.复杂度以及比较
假设双链树的查找基(关键字的基就是我们关键字的键的最大种类数,有点类似于我们的基数排序里面的基)是d
双链树的平均树深是h
1.在双链树中每一位的平均查找长度是
sum(p+p*2+p*3+...+p*d)=sum((1+d)*d*p/2)
p=1/d
sum=(1+d)/2
2.在双链树中查找单词的平均查找长度是
(1+d)*h/2
相对于哈希表的查找来说,我们每次的时间耗费大多都是浪费存在遍历单词上了,哈希表可以做到O(1)的查询,但是还是需要遍历,所以说是O(h)
但是哈希的建树操作时间复杂度其实和字典树是一样的,但是我们完全可以在字典树中实现,查询+建树同时进行,但是哈希表就不行了
采用双链树的话,我们虽然可以节省内存,但是在查询的时候,我们必须对下一层的子指针进行相应的遍历,会比较耗时,但是对于稀疏的状态来说还是非常的优秀的
4.操作解析
为了降低内存的消耗,本文中描述中全部采用双链树的形式
1.查询操作
首先,我们在对单词便利的过程中完成对树的遍历操作,从根节点开始,对于单词的每一位我们在子指针中查找有没有匹配节点
1.没有匹配节点,返回失败
2.有匹配节点继续开始步骤,知道我们单词查询遍历完毕
1.如果单词查询完,标记域是否,代表我们这个单词只是作为其他单词的前缀出现过,之前没有出现过这个单词,我们对单词标记域进行修改,返回失败
2.如果标记域是是,我们进行其他的操作(比如计数什么的)返回成功
2.插入操作(建树)
同上,我们首先从根节点开始
1.子指针中如果对当前的关键字的位有相应的指针,那么继续遍历
2.如果不存在相应的指针,我们开始怼该单词的每一位进行建树操作,对每一次便利的单词的位扩展构建子节点,并在最后的时候,对叶子节点的标记域进行修改
3.销毁
销毁一颗字典树我们需要对数进行后序遍历,这是很简单的
4.有序输出
前序遍历Trie
5.封装代码
#include"iostream"
#include"cstdio"
#include"cstdlib"
using namespace std;
typedef struct node
{
char key; //键值
struct node* son[26]; //以英文字母为例
bool flag; //标记是否存在过
bool lort; //标记是叶子节点还是树节点
}point;
class Trie
{
public:
Trie()
{
root=new point;
root->flag=false;
root->key='#'; //空标记
root->lort=true; //开始是空树,必然是叶子节点
memset(data,0,sizeof(data));
for(int i=0;i<26;i++) root->son[i]=NULL;
}
~Trie()
{
clear(root);
}
friend istream& operator>>(istream&,Trie&);
void clear(point*); //后序遍历清空Trie
void insert(point*,int); //建树+插入
bool find(point*,int); //查询
void scan(point*,int); //前序遍历输出字典序
inline void initdata()
{
memset(data,0,sizeof(data));
}
inline point* returnroot() //返回根节点指针
{
return root;
}
private:
point* root; //空表头指针
char data[20]; //不超过20个字母的单词
char a[20];
};
istream& operator>>(istream& in,Trie& p)
{
cout<<">>> ";
in>>p.data;
return in;
}
void Trie::insert(point* now,int i)
{
if(data[i]=='\0') return ;
if(now->son[data[i]-'a']==NULL)
{
now->lort=false; //不是叶子节点了
now->son[data[i]-'a']=new point;
now->son[data[i]-'a']->key=data[i];
if(data[i+1]=='\0') now->son[data[i]-'a']->flag=true;
else now->son[data[i]-'a']->flag=false;
for(int j=0;j<26;j++) now->son[data[i]-'a']->son[j]=NULL;
insert(now->son[data[i]-'a'],i+1);
}
else insert(now->son[data[i]-'a'],i+1);
}
bool Trie::find(point* p,int i)
{
if(data[i]=='\0'&&p->flag==true) return true;
else
{
if(data[i]=='\0'&&p->flag==false) return false;
else
{
if(p->son[data[i]-'a']==NULL) return false;
else return find(p->son[data[i]-'a'],i+1);
}
}
}
void Trie::scan(point* p,int i)
{
if(p->lort==true)
{
a[i]=p->key;
for(int j=1;j<=i;j++) cout<<a[j];
cout<<endl;
return ;
}
else
{
for(int j=0;j<26;j++)
{
if(p->son[j]!=NULL)
{
a[i]=p->key;
scan(p->son[j],i+1);
}
}
}
}
void Trie::clear(point* p)
{
if(p->lort==true) free(p);
else
{
for(int i=0;i<26;i++)
{
if(p->son[i]!=NULL) clear(p->son[i]);
}
}
}
int main()
{
Trie my;
int t=10;
while(t--)
{
cin>>my;
my.insert(my.returnroot(),0);
my.initdata();
}
t=5;
while(t--)
{
cin>>my;
if(my.find(my.returnroot(),0)) cout<<"Yes!"<<endl;
else cout<<"No!"<<endl;
}
my.scan(my.returnroot(),0);
return 0;
}
6.待解
1.DFA(有限状态机)
2.Double-Array Trie / Tripple-Array Trie(对空间的压缩,基于DFA)
3.叶子节点真的要保存整个剩余的字段吗?
7.OJ例题及解析
1.HDU 1251
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
using namespace std;
typedef struct node
{
char key;
struct node* son[26];
int count; //记录以该字符为后缀的前缀单词的个数
}point;
point* root;
char data[12];
void insert()
{
point* p=root;
int j=0;
while(data[j]!='\0')
{
int k=data[j]-'a';
if(p->son[k]!=NULL) p=p->son[k],p->count++;
else
{
p->son[k]=new point;
p->son[k]->key=data[j];
p->son[k]->count=1;
for(int i=0;i<26;i++) p->son[k]->son[i]=NULL;
p=p->son[k];
}
j++;
}
memset(data,0,sizeof(data));
}
int dosomething()
{
point* p=root;
int j=0;
while(data[j]!='\0')
{
int k=data[j]-'a';
if(p->son[k]!=NULL) p=p->son[k];
else return 0;
j++;
}
return p->count;
}
int main()
{
root=new point;
for(int i=0;i<26;i++) root->son[i]=NULL;
while(gets(data)&&data[0]!='\0') insert();
while(gets(data)!=NULL) cout<<dosomething()<<endl;
return 0;
}
2.1671
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
using namespace std;
typedef struct node
{
int key; //键
struct node* son[10]; //英文字母,如果是其他情况,调整就好
bool flag; //判断是否出现过
bool fornext;
// int count; 前缀的数目等其他的信息
}point;
point* root;
char data[12];
bool ok; //记录当前号码是否存在过
bool insert()
{
bool youxinjiedianshengcheng=false;
int j=0;
point* p=root;
while(data[j]!='\0')
{
int k=data[j]-'0';
if(p->son[k]==NULL)
{
youxinjiedianshengcheng=true;
p->son[k]=new point;
p->fornext=false;
p->son[k]->key=k;
for(int i=0;i<=9;i++) p->son[k]->son[i]=NULL;
p->son[k]->flag=false;
p=p->son[k];
}
else
{
if(p->son[k]->flag==true) ok=true;
p->fornext=false;
p=p->son[k];
}
j++;
}
p->flag=true;
p->fornext=true;
if(ok==true||youxinjiedianshengcheng==false) return false;
else return true;
}
void dfs(point* gen)
{
if(gen->fornext==true)
{
delete gen;
return ;
}
else
{
for(int i=0;i<=9;i++)
{
if(gen->son[i]!=NULL)
{
dfs(gen->son[i]);
gen->son[i]=NULL;
}
}
delete gen;
}
}
int main()
{
int t;
cin>>t;getchar();
root=new point;
root->flag=false;
for(int i=0;i<10;i++) root->son[i]=NULL;
root->key=-1;
while(t--)
{
bool yes=false;
int k;
cin>>k;getchar();
while(k--)
{
ok=false;
gets(data);
if(insert()==false) yes=true;
}
if(yes==false) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
dfs(root);
root=new point;
root->flag=false;
for(int i=0;i<10;i++) root->son[i]=NULL;
}
return 0;
}