概念
Trie这个名字取自“retrieval”,检索,因为Trie可以只用一个前缀便可以在一部字典中找到想要的单词。
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是 用于统计和排序大量的字符串(但不仅限于字符串) ,所以经常被搜索引擎系统 用于文本词频统计 。它的优点是: 最大限度地减少无谓的字符串比较 。
Trie的核心思想是 空间换时间 。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
前缀树的3个基本性质:
-
根节点不包含字符,除根节点外每一个节点都只包含一个字符。
-
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
-
每个节点的所有子节点包含的字符都不相同。
Trie树应用:
- 串的快速检索:
给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
在这道题中,我们可以用数组枚举,用哈希,用字典树,先把熟词建一棵树,然后读入文章进行比较,这种方法效率是比较高的。
- “串”排序:
给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出。
用字典树进行排序,采用数组的方式创建字典树,这棵树的每个结点的所有儿子很显然地按照其字母大小排序。对这棵树进行先序遍历即可。
- 最长公共前缀
对所有串建立字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,于是,问题就转化为当时公共祖先问题。
前缀树查询和哈希查询的比较:
- 通常字典树的查询时间复杂度是O(logL),L是字符串的长度。所以效率还是比较高的。
虽然网上说的都是 字典树的效率比hash表高。但是还是相对来看比较好,各有个的特点吧。
- hash表,通过hash函数把所有的单词分别hash成key值,查询的时候直接通过hash函数即可,都知道hash表的效率是非常高的为O(1),直接说字典树的查询效率比hash高,难道有比O(1)还快的-_-。
hash:
- 当然对于单词查询,如果我们hash函数选取的好,计算量少,且冲突少,那单词查询速度肯定是非常快的。那如果hash函数的计算量相对大呢,且冲突律高呢?
这些都是要考虑的因素。且hash表不支持动态查询,什么叫动态查询,当我们要查询单词apple时,hash表必须等待用户把单词apple输入完毕才能hash查询。
当你输入到appl时肯定不可能hash吧。
字典树(tries树):
- 对于单词查询这种,还是用字典树比较好,但也是有前提的,空间大小允许,字典树的空间相比较hash还是比较浪费的,毕竟hash可以用bit数组。
那么在空间要求不那么严格的情况下,字典树的效率不一定比hash弱,它支持动态查询,比如apple,当用户输入到appl时,字典树此刻的查询位置可以就到达l这个位置,那么我在输入e时光查询e就可以了(更何况如果我们直接用字母的ASCII作下标肯定会更快)!字典树它并不用等待你完全输入完毕后才查询。
举个栗子
给你100000个长度不超过10的单词。对于每一个单词,我们要判断他出没出现过,如果出现了,求第一次出现在第几个位置。
如果我们使用一般的方法,没查询一个单词都去遍历一遍,那么时间复杂度将为O(n^2),这对于100000这么大的数据是不能够接受的。假如我们要查找单词student。那我们 通过前缀树只需要查找s开头的即可,然后接下来查询t开头的即可,对于大量的数据可以省去不小的时间。
树结构:
- 其中count表示 以当前单词结尾的单词数量。
- prefix表示 以该处节点之前的字符串为前缀的单词数量。
public class TrieNode {
int count;
int prefix;
TrieNode[] nextNode=new TrieNode[26];
public TrieNode(){
count=0;
prefix=0;
}
}
- 前缀树的创建
好比假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词,那我们创建trie树就得到
Java代码示例:
// 插入一个新单词
public static void insert(TrieNode root,String str){
if(root==null||str.length()==0){
return;
}
char[] c=str.toCharArray();
for(int i=0;i<str.length();i++){
//如果该分支不存在,创建一个新节点
if(root.nextNode[c[i]-'a']==null){
root.nextNode[c[i]-'a']=new TrieNode();
}
root=root.nextNode[c[i]-'a'];
root.prefix++;//注意,应该加在后面
}
//以该节点结尾的单词数+1
root.count++;
}
- 查询以str开头的单词数量,查询单词str的数量
// 查找该单词是否存在,如果存在返回数量,不存在返回-1
public static int search(TrieNode root,String str){
if(root==null||str.length()==0){
return -1;
}
char[] c=str.toCharArray();
for(int i=0;i<str.length();i++){
//如果该分支不存在,表名该单词不存在
if(root.nextNode[c[i]-'a']==null){
return -1;
}
//如果存在,则继续向下遍历
root=root.nextNode[c[i]-'a'];
}
//如果count==0,也说明该单词不存在
if(root.count==0){
return -1;
}
return root.count;
}
// 查询以str为前缀的单词数量
public static int searchPrefix(TrieNode root,String str){
if(root==null||str.length()==0){
return -1;
}
char[] c=str.toCharArray();
for(int i=0;i<str.length();i++){
//如果该分支不存在,表名该单词不存在
if(root.nextNode[c[i]-'a']==null){
return -1;
}
//如果存在,则继续向下遍历
root=root.nextNode[c[i]-'a'];
}
return root.prefix;
}
- 在主函数中测试
public static void main(String[] args){
TrieNode newNode=new TrieNode();
insert(newNode,"hello");
insert(newNode,"hello");
insert(newNode,"hello");
insert(newNode,"helloworld");
System.out.println(search(newNode,"hello"));
System.out.println(searchPrefix(newNode,"he"));
}
/**
输出:3 4
**/
CPP代码示例:
#include<cstdio>
#include<cstdlib>
using namespace std;
char s[11];
struct Trie{
Trie* next[26]; //结构体指针 有26种字符
int sum; //单词出现的次数
Trie(){ //构造函数 便于初始化
for(int i=0;i<26;i++){ //初始化
next[i]=NULL; //初始时,每个字符所对应数组下标中的指针为空
}
sum=0;
}
}root;
void insert(char* s) //创建字典树 在字典树上插入结点
{
Trie* p=&root; //从根结点开始遍历
for(int i=0;s[i];i++){ //遍历单词的每一个字符
if(p->next[s[i]-'a']==NULL){ //判断字符所对应结构体指针数组下标中的指针是否为空
p->next[s[i]-'a']=new Trie; //如果为空 就新建一个结点
}
p=p->next[s[i]-'a']; //将指针指向当前字符所对应的结构体指针数组的下标所对应的地址
p->sum++;
}
}
int find(char* s) //查找单词
{
Trie* p=&root; //从根结点开始遍历
for(int i=0;s[i];i++){ //遍历单词的每一个字符
if(p->next[s[i]-'a']==NULL)return 0; // 如果下标所对应的指针为空 查找失败
else p=p->next[s[i]-'a']; //如果不为空 遍历下一个字符 直至遍历结束
}
return p->sum; //返回遍历完的最后一个结点中所对应的数据 代表当前当前单词出现的次数
}
int main(){
while(gets(s)&&s[0]!='\0') //读取字典中的单词 将其插入字典树中
insert(s);
while(scanf("%s",s)==1){ //在字典树中查找单词出现的次数
printf("%d\n",find(s));
}
return 0;
}
如有不同见解,欢迎留言讨论~~