摘要:
本文主要讲解了Trie的基本思想和原理,实现了几种常见的Trie构造方法,着重讲解Trie在编程竞赛中的一些典型应用。
- 什么是Trie?
- 如何构建一个Trie?
- Trie在编程竞赛中的典型应用有些?
- 例题解析
术语取自retrieval中(检索,收回,挽回)的trie,读作“try”,也叫做前缀树或者字典树,是一种有序的树形数据结构。我们常用字典树来保存字符串集合(但不仅限于字符串),如下图就是一个字典树。
它保存的字符集合是{to,te,tea,ted,ten,a,i,in,inn},可以看出从根结点到单词结点所经过的路径上的所有字母所组成的字符串就是该单词结点对应的字符串。从图中我们可以验证字典树的三条性质:
1.根结点不包含字符,除根结点外,其他结点都只包含一个字符;
2.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
3.每个节点的所有子节点包含的字符都不相同;
类比查英文字典的过程就是在字典树上查单词的过程,它的核心思想就是利用字符串的公共前缀来减少查询时间,最大限度地减少无谓字字符比较,从而提高效率。
Trie的典型应用是用于统计、排序和保存大量的字符串(不限于字符串),所以经常被用于搜索引擎的文本词频统计,关键词检索。
如何构建一个Trie?
常用的有三种构建方法,分别是转移矩阵法、链表法和左儿子-右兄弟表示法。
转移矩阵法的基本思想和实现
矩阵转移法的思想是将根结点标号为0,其余结点标号为从1开始的正整数,然后用一个二维数组来保存每个结点的所有子节点,用数组下标进行直接存取。具体来说就是用ch[i][j]来表示结点i编号为j的那个子结点(其中编号为j意为该字符在字符集中的编号,比如a在所有的小写字母集合中的编号为0),可以构建如下图所示的转移矩阵(原谅我这不讲究的画图)。
将初始化、插入和查询封装到一个结构体里代码如下:
1 const int maxn = 1001;//模板数 2 const int sigma_size = 26; 3 4 struct Trie { 5 int ch[maxn][sigma_size]; 6 int val[maxn];//规定非单词结点的附加价值为0 7 int sz;//结点总数 8 void init() {//初始化,只有一个根结点并且没有子结点 9 sz = 1; 10 memset(ch[0], 0, sizeof(ch[0])); 11 } 12 int idx(char c) { 13 return c - 'a'; 14 } 15 void insert(char *s, int v) { 16 int n = strlen(s); 17 int u = 0; 18 for(int i = 0; i < n; i++) {//遍历模板串的每一个字母 19 int c = idx(s[i]);//得到该字母的编号 20 if(!ch[u][c]){//该结点不存在 21 memset(ch[sz], 0, sizeof(ch[sz])); 22 val[sz] = 0;//中间结点的附加信息为0 23 ch[u][c] = sz++;//将结点u编号为j的子结点编号为sz 24 } 25 u = ch[u][c];//往下走 26 } 27 val[u] = v; 28 } 29 bool query(char *t) { 30 int m = strlen(t); 31 int u = 0; 32 for(int i = 0; i < m; i++) { 33 int c = idx(t[i]); 34 if(!ch[u][c])//结点u编号为j的子结点为空表示不存在该串 35 return false; 36 u = ch[u][c]; 37 } 38 return true; 39 } 40 };
上述代码中值得注意的是每遇到一个结点才重置二维数组中的一行,看似麻烦,其实可以起到优化内存,防止内存超限的作用,因为如果按照题目要求一次性置零,判题机会直接检测到内存超限,按照上述的方法,有多少单词就用多少内存,只要不是字符集特别大(一般是小写字母集合,如果真的很大采用后面的第三种方法),并不会直接判内存超限。
来看一个具体的问题,输入单词数n和n个单词,查询m个单词是否在之前的单词表中,存在输出Yes,否则输出No。直接套用模板代码如下:
1 #include <cstdio> 2 #include <cstring> 3 const int maxn = 1001;//模板数 4 const int sigma_size = 26; 5 6 struct Trie { 7 int ch[maxn][sigma_size]; 8 int val[maxn];//规定非单词结点的附加价值为0 9 int sz;//结点总数 10 void init() {//初始化,只有一个根结点并且没有子结点 11 sz = 1; 12 memset(ch[0], 0, sizeof(ch[0])); 13 } 14 int idx(char c) { 15 return c - 'a'; 16 } 17 void insert(char *s, int v) { 18 int n = strlen(s); 19 int u = 0; 20 for(int i = 0; i < n; i++) {//遍历模板串的每一个字母 21 int c = idx(s[i]);//得到该字母的编号 22 if(!ch[u][c]){//该结点不存在 23 memset(ch[sz], 0, sizeof(ch[sz])); 24 val[sz] = 0;//中间结点的附加信息为0 25 ch[u][c] = sz++;//将结点u编号为j的子结点编号为sz 26 } 27 u = ch[u][c];//往下走 28 } 29 val[u] = v; 30 } 31 bool query(char *t) { 32 int m = strlen(t); 33 int u = 0; 34 for(int i = 0; i < m; i++) { 35 int c = idx(t[i]); 36 if(!ch[u][c])//结点u编号为j的子结点为空表示不存在该串 37 return false; 38 u = ch[u][c]; 39 } 40 return true; 41 } 42 }; 43 44 Trie trie;//直接使用封装好的结构体 45 int main() 46 { 47 int n; 48 char word[maxn]; 49 trie.init(); 50 printf("输入单词表的个数和单词表:\n"); 51 scanf("%d", &n); 52 for(int i = 0; i < n; i++) { 53 scanf("%s", word); 54 trie.insert(word, 1); 55 } 56 printf("输入欲查询单词的个数和单词:\n"); 57 int m; 58 scanf("%d", &m); 59 for(int i = 0; i < m; i++) { 60 scanf("%s", word); 61 if(trie.query(word)) 62 printf("Yes\n"); 63 else 64 printf("No\n"); 65 } 66 getchar(); 67 return 0; 68 } 69 70 /*测试样例 71 10 72 asd 73 zxc 74 qwe 75 asdf 76 zxcv 77 qwer 78 asdfgh 79 rewq 80 fdsa 81 vcxz 82 5 83 asdf 84 ghjk 85 zxcv 86 qwer 87 rewq 88 */