Trie,又经常叫前缀树,字典树等等。它有很多变种,如后缀树,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree。当然很多名字的意义其实有交叉。

定义

在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

trie中的键通常是字符串,但也可以是其它的结构。trie的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。比如,bitwise trie中的键是一串位元,可以用于表示整数或者内存地址

基本性质

1,根节点不包含字符,除根节点意外每个节点只包含一个字符。

2,从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。

3,每个节点的所有子节点包含的字符串不相同。

优点:

可以最大限度地减少无谓的字符串比较,故可以用于词频统计和大量字符串排序。

缺点:
虽然不同单词共享前缀,但其实trie是一个以空间换时间的算法。其每一个字符都可能包含至多字符集大小数目的指针(不包含卫星数据)。

应用场景:

(1) 字符串检索
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。

(2)文本预测、自动完成,see also,拼写检查

(3)词频统计

==》若无内存限制:Trie + “k-大/小根堆”(k为要找到的数目)。

否则,先hash分段再对每一个段用hash(另一个hash函数)统计词频,再要么利用归并排序的某些特性(如partial_sort),要么利用某使用外存的方法。参考

“海量数据处理之归并、堆排、前K方法的应用:一道面试题” http://www.dataguru.cn/thread-485388-1-1.html。

“算法面试题之统计词频前k大”

算法导论笔记——第九章 中位数和顺序统计量

(4)排序

Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
比如给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

(5)字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。
举例:
给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?
解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

  1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
  2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

(6)字符串搜索的前缀匹配
trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。
Trie树检索的时间复杂度可以做到n,n是要检索单词的长度,
如果使用暴力检索,需要指数级O(n^2)的时间复杂度。

(7) 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等

后缀树可以用于全文搜索

转一篇关于几种Trie速度比较的文章:http://www.hankcs.com/nlp/performance-comparison-of-several-trie-tree.html

Trie树和其它数据结构的比较 http://www.raychase.net/1783


【例题】LOJ6469 Magic(trie)

redis 前缀查询 命令行 redis 前缀树_字符串


我们考虑转化问题

假设一个数数位为redis 前缀查询 命令行 redis 前缀树_i++_02(即redis 前缀查询 命令行 redis 前缀树_redis 前缀查询 命令行_03),则贡献为redis 前缀查询 命令行 redis 前缀树_i++_02。我们可以看做这个数对redis 前缀查询 命令行 redis 前缀树_i++_02个位置每个的贡献都为redis 前缀查询 命令行 redis 前缀树_trie树_06,那么对于数位redis 前缀查询 命令行 redis 前缀树_redis 前缀查询 命令行_07,我们只需要求有多少对异或完比redis 前缀查询 命令行 redis 前缀树_字符串_08大的数,那么就是redis 前缀查询 命令行 redis 前缀树_redis 前缀查询 命令行_07这个数位对于答案的贡献了。

可以先建redis 前缀查询 命令行 redis 前缀树_i++_10个数的redis 前缀查询 命令行 redis 前缀树_redis 前缀查询 命令行_11树,然后令redis 前缀查询 命令行 redis 前缀树_redis 前缀查询 命令行_12等于redis 前缀查询 命令行 redis 前缀树_i++_13,将redis 前缀查询 命令行 redis 前缀树_redis 前缀查询 命令行_12的各个数位在redis 前缀查询 命令行 redis 前缀树_i++_15树上爬就好了。

//size x equals to the number who has its prefix
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
inline int read()
{
	int X=0; bool flag=1; char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
	while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+ch-'0'; ch=getchar();}
	if(flag) return X;
	return ~(X-1);
}
int n,a[N],res,cnt;
int trie[N<<6][2],siz[N<<6],f[20];
void insert(int s) {
	int it=0;
	for(int i=59;i>=0;i--) {
		int p=((s>>i)&1);
		if(!trie[it][p]) trie[it][p]=++cnt;
		it=trie[it][p];
		siz[it]++;
	}
}
int search(int s,int t) {
	int it=0,tot=0;
	for(int i=59;i>=0;i--) {
//		printf("|%lld %lld|\n",it,sum);
		int p=((s>>i)&1);
		if((t>>i)&1) {
			if(!trie[it][1^p]) break;
			it=trie[it][1^p];
		}
		else {
			tot+=siz[trie[it][1^p]];
			if(!trie[it][p]) break;
			it=trie[it][p];
		}
	}
	return tot;
}
signed main() {
	f[0]=1;
	for(int i=1;i<=18;i++) f[i]=f[i-1]*10;
	f[19]=f[18];
	n=read();
	for(int i=1;i<=n;i++) {
		a[i]=read();
		insert(a[i]);
	}
	for(int i=18;i>=0;i--) {
		int tot=0;
		for(int j=1;j<=n;j++) {
			tot+=search(a[j],f[i]-1);
		}
		res+=tot;
	}
	printf("%lld",res/2);
}

模板

// string
const int N=1e5+5;
int n,m,cnt,trie[N][26];
int siz[N];
char s[N];
void insert(char *s) {
	int it=0,str=strlen(s);
	for(int i=0;i<str;i++) {
		if(!trie[it][s[i]-'a']) trie[it][s[i]-'a']=++cnt;
		it=trie[it][s[i]-'a'];
	}
	siz[it]++;
}
int search(char *s) {
	int it=0,tot=0,str=strlen(s);
	for(int i=0;i<str;i++) {
		if(!trie[it][s[i]-'a']) trie[it][s[i]-'a']=++cnt;
		it=trie[it][s[i]-'a'];
		tot+=siz[it];
	}
	return tot;
}
//xor
const int N=1e5+5;
int n,cnt,trie[N<<5][2];
int siz[N<<5],res;
void insert(int s) {
	int it=0;
	for(int i=31;i>=0;i--) {
		int p=((s>>i)&1);
		if(!trie[it][p]) trie[it][p]=++cnt;
		it=trie[it][p];
	}
	siz[it]++;
}
int search(int s) {
	int it=0,tot=0;
	for(int i=31;i>=0;i--) {
		int p=((s>>i)&1);
		if(trie[it][1^p]) tot+=(1<<i),it=trie[it][1^p];
		else it=trie[it][p];
	}
	return tot;
}