【算法编程】Trie树(字典树)

  Trie树是一种非常简单且有效的数据结构,其主要用于针对包含大量的字符串,但所有字符串包含字符类型数量较少的情况下,对字符串的存储。最典型的应用就是存储单词,因此也称作字典树

  例如,给定几个单词,则可以用Trie树进行保存:

给定单词 apple, able, app, aboard, far, are,可知这些单词有许多相同的前缀,因此可以通过多叉树的形式将公共的前缀进行合并,如下图所示,红色则为一个标记,用于标记从根结点到该结点是一个单词。

【算法编程】Trie树(字典树)_字典树

由上图可知,Trie树具有几个性质:

  • Trie树是多叉树;
  • Trie始终有一个根结点root。当没有一个单词(字符串)时,Trie树只包含根结点root;
  • Trie的深度(高度)一定是所有单词(字符串)中最长的长度;
  • 可以用二维数组存储trie树。假设已知只有26个字母,则数组可以定义为 【算法编程】Trie树(字典树)_trie树_02 ,每一个结点都最多有26个孩子结点,分别表示字母a-z,其中 【算法编程】Trie树(字典树)_字典树_03 表示第 【算法编程】Trie树(字典树)_结点_04 个字母的下一个字母的编号是 【算法编程】Trie树(字典树)_数据结构_05

注,树中每一个结点都是有唯一的编号 【算法编程】Trie树(字典树)_结点_04,每个结点 【算法编程】Trie树(字典树)_结点_04 都对应某一个字符串中的某个字母,因此这个字母的下一个字母如果是 【算法编程】Trie树(字典树)_数据结构_08 (字母’a-z’ 分别对应 ‘0-25’) 则可以保存在 【算法编程】Trie树(字典树)_字典树_03 上此时 【算法编程】Trie树(字典树)_字典树_03

  • 需要维护一个特殊的数组 【算法编程】Trie树(字典树)_结点_11cnt[idx]$ 可以记为以root为起点,结点idx为终点对应的路径上的字符串出现的数量。

  Trie树有一些典型的应用,比如字符串高效存储和查询、求所有字符串的最大公共前缀等。下面给出几个应用样例

Trie树的最基本的应用就是存储和查询单词:

维护一个字符串集合,支持两种操作:

“I x”向集合中插入一个字符串x; “Q x”询问一个字符串在集合中出现了多少次。 共有N个操作,输入的字符串总长度不超过 【算法编程】Trie树(字典树)_字典树_12,字符串仅包含小写英文字母。
输入输出要求:第一行包含整数N,表示操作数。接下来N行,每行包含一个操作指令,指令为”I x”或”Q x”中的一种。

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int trie[N][26]; // trie树,只有26个小写字母。trie[idx][j]表示第idx个结点的第j个孩子
int cnt[N]; // 每个结点作为一个单词的标记,用于记录每个结点作为单词结尾的次数
int idx = 1; // 新插入的字符的编号,初始化从1开始编号
char str[N]; // 用于终端输入一个单词
// 插入字符
void insert(char* s) {
int p = 0; // 定义一个指针从根结点开始
// 遍历字符串中的每一个字符
for(int i = 0; s[i]; i ++) {
int w = s[i] - 'a';
// 如果当前的字符s[i]并不在结点p的第w个孩子上,则新增一个节点
if(!trie[p][w]) trie[p][w] = idx ++;
p = trie[p][w]; // 将指针转移到下一个结点
}
cnt[p] ++;
}
// 查询是否存在一个单词
int query(char* s) {
int p = 0;
for(int i = 0; s[i]; i ++) {
int w = s[i] - 'a';
if(!trie[p][w]) return 0; // 不存在
p = trie[p][w];
}
return cnt[p];
}

int main() {
int n;
scanf("%d", &n);
while (n -- ) {
char op[2];
scanf("%s%s", op, str);
if (*op == 'I') insert(str);
else printf("%d\n", query(str));
}
return 0;
}

Trie拓展应用——最大异或对:

给定 【算法编程】Trie树(字典树)_trie树_13 个整型数 【算法编程】Trie树(字典树)_算法_14【算法编程】Trie树(字典树)_数据结构_15), 随机挑选两个值(可以是同一个)计算异或,求所有异或结果的最大值。
输入输出要求:输入两行,第一行为一个整数n表示整数个数,第二行输入n个整数。

思路: 先从暴力出发,肯定是要两层for循环,铁定会超时,但是我们发现,对于一个数a[i]的二进制位,则如果要寻找一个最大的数与之异或值最大,则贪心地从最高位开始,寻找与其恰巧位相反的。

假设给定一个整型数的二进制位1001011,则期望与之异或最大的肯定是0110100

【算法编程】Trie树(字典树)_数据结构_16,从高位遍历其二进制位( 【算法编程】Trie树(字典树)_数据结构_17 ),每次寻找一个与 【算法编程】Trie树(字典树)_trie树_18

  如何搜索呢?可以借助trie树因为trie树能够将每个含有相同前缀的字符串挂载到一起,因此,当遍历到第j位二进制位时,只需要从其中寻找与之相反的位即可,若不存在则按照原始位查询。

例子:

【算法编程】Trie树(字典树)_数据结构_19

#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;
const int M = 32 * N; // 假设最多有N个整数,则最多有32*N个二进制位
int a[N];

int trie[M][2], cnt[M]; // 分别表示trie树,以及每个结点作为一个数字的末尾的次数
int idx = 1;

// 向trie树插入一个结点
void insert(int x) {
int p = 0; // 从根结点开始
for(int i = 31; i >= 0; i --) {
int k = x >> i & 1; // 获得整型数的二进制的第i位
if(!trie[p][k]) trie[p][k] = idx ++;
p = trie[p][k];
}
cnt[p] ++;
}

int main() {
int n, res = 0;
scanf("%d", &n);
for(int i = 0; i < n; i ++) scanf("%d", &a[i]);

// 建立trie树
for(int i = 0; i < n; i ++) insert(a[i]);

for(int i = 0; i < n; i ++) {
// 遍历当前的数a[i]的二进制:
int p = 0, r = 0;
for(int j = 31; j >= 0; j --) {
int k = a[i] >> j & 1;
// 每次试图从trie树中查找与当前位相反的数,假设当前数为10010,则当遍历第一个高位1时,
// 希望搜索是否存在0,如果存在,则与之异或的最大值一定在0对应的子树后面。如果没找到,则按照当前位向下搜索;
// 每次寻找到一个不相同位时,则累计异或结果
int t = (k + 1) % 2;
if(!trie[p][t]) p = trie[p][k]; // 如果没有找到与当前位不同的,则只能按照原始的位向下查找
else r += pow(2, j), p = trie[p][t];
}
res = max(res, r);
}
printf("%d", res);
return 0;
}