字典树

关于 Trie Tree 的介绍

Background

Trie树(又称前缀树、字典树)是一种高效的数据结构,用于处理字符串的查询、插入和删除操作。它主要用于实现词典、搜索引擎的自动补全、拼写检查等功能。Trie树的特点是每个节点表示一个字符,从根节点到任意一个叶子节点的路径表示一个字符串。

Trie树的基本组成如下:

  1. 节点:每个节点代表一个字符,通常包含一个指向子节点的指针数组,以及一个布尔值,表示该节点是否是一个完整字符串的结尾。
  2. 根节点:树的根节点,不包含任何字符,但可以指向其他节点。
  3. 子节点:一个节点可以有多个子节点,每个子节点代表一个字符。

Trie树的主要操作包括:

  1. 插入:将一个字符串插入Trie树中。从根节点开始,遍历字符串的每个字符,沿着对应的子节点路径向下搜索。如果路径不存在,创建新节点。当到达字符串的最后一个字符时,将节点的布尔值设为True,表示到此为止的路径构成一个完整的字符串。
  2. 查询:检查一个字符串是否存在于Trie树中。从根节点开始,遍历字符串的每个字符,沿着对应的子节点路径向下搜索。如果找不到对应的子节点,说明字符串不存在。如果遍历到最后一个字符,检查节点的布尔值。如果为True,表示该字符串存在。
  3. 删除:从Trie树中删除一个字符串。首先查询该字符串是否存在。如果存在,从最后一个字符的节点开始,将其布尔值设为False,然后逐级向上删除没有子节点的节点。
  4. 前缀查询:查询某个前缀的所有字符串。从根节点开始遍历前缀的每个字符,沿着对应的子节点路径向下搜索。到达前缀的最后一个字符时,遍历其所有子树,收集所有布尔值为True的节点,将它们的路径组合成完整的字符串。

Trie树的优点:

  1. 查询、插入和删除操作的时间复杂度为O(m),其中m是字符串的长度。与字符串的数量无关,因此在处理大量字符串时具有很高的性能。
  2. 前缀查询非常高效,适用于自动补全、拼写检查等场景。

Trie树的缺点:

  1. 空间复杂度较高,因为每个节点需要存储一个指向子节点的指针数组。在实际应用中,可以采用压缩或者优化的数据结构来减小空间开销,如压缩字典树(Radix Tree)、Ternary Search Tree等。

Implementation

ts
1class TrieNode {
2    children: Map<string,TrieNode>;
3    endOfWord: boolean;
4
5    constructor() {
6        this.children = new Map();
7        this.endOfWord = false;
8    }
9}
10
11class Trie {
12    private root;
13
14    constructor() {
15        this.root = new TrieNode();
16    }
17
18    insert(word: string): void {
19        let current = this.root;
20        for (const char of word) {
21           const children = current.children;
22           if (!children.has(char)) {
23               children.set(char, new TrieNode());
24           }
25           current = children.get(char);
26        }
27        current.endOfWord = true;
28    }
29
30    search(word: string): boolean {
31        let current = this.root;
32        for (const char of word) {
33           const children = current.children;
34           if (children.has(char)) {
35               current = children.get(char);
36           } else {
37               return false;
38           }
39        }
40        return current.endOfWord;
41    }
42
43    startsWith(prefix: string): boolean {
44        let current = this.root;
45        for (const char of prefix) {
46           const children = current.children;
47           if (children.has(char)) {
48               current = children.get(char);
49           } else {
50               return false;
51           }
52        }
53        return true;
54    }
55}

Examples

Trie Tree(字典树)是一种树形数据结构,它用于高效地存储和查找字符串。Trie Tree 的每个节点表示一个字符,而从根节点到叶子节点所经过的所有字符组成一个字符串。Trie Tree 可以在 O(长度) 的时间内实现字符串的插入、删除和查询操作。下面列举了一些 Trie Tree 的应用:

  1. 自动补全:Trie Tree 常用于搜索引擎、文本编辑器和输入法的自动补全功能。当用户输入前缀时,Trie Tree 可以快速找到以该前缀开头的所有字符串。
  2. IP 路由查找:Trie Tree 可以用于查找网络中 IP 地址的最长匹配前缀。通过最长匹配前缀,路由器可以确定下一跳的地址,从而实现 IP 路由。
  3. 词频统计:Trie Tree 可用于统计文本中单词的出现次数。将文本中的单词插入到 Trie Tree 中,然后在每个单词的末尾节点记录单词出现的次数。
  4. 前缀匹配:Trie Tree 可以快速找到以某个前缀开头的所有字符串。这在文本分类、过滤和搜索中非常有用。
  5. 词典查找:Trie Tree 可用于实现词典查找功能,用户可以通过输入单词的前缀来快速查找到完整的单词。
  6. 拼写检查:Trie Tree 可用于实现拼写检查功能。将正确拼写的单词存储在 Trie Tree 中,然后通过比较输入的单词与 Trie Tree 中的单词来判断拼写是否正确。
  7. 字符串排序:Trie Tree 可以对字符串进行排序。将字符串插入到 Trie Tree 中,然后通过深度优先搜索(DFS)遍历 Trie Tree,输出排序后的字符串。

总之,Trie Tree 是一种非常实用的数据结构,可以应用于许多字符串处理场景。

find_maximum_xor

rust
1use std::collections::HashSet;
2
3pub fn find_maximum_xor(nums: Vec<i32>) -> i32 {
4    let mut max_xor = 0;
5    let mut mask = 0;
6
7    for i in (0..=31).rev() {
8        mask |= 1 << i;
9        let prefixes: HashSet<i32> = nums.iter().map(|num| num & mask).collect();
10        let temp_max_xor = max_xor | (1 << i);
11        for &prefix in &prefixes {
12            // if a ^ b = c, then a ^ c = b and b ^ c = a
13            if prefixes.contains(&(temp_max_xor ^ prefix)) {
14                max_xor = temp_max_xor;
15                break;
16            }
17        }
18    }
19
20    max_xor
21}

这是一个Rust语言的函数,名为find_maximum_xor,其目的是从给定的整数向量(nums: Vec<i32>)中找出两个数,使得它们的异或(XOR)值最大。

在这个函数中,异或运算的特点被充分利用:如果a ^ b = c,那么a ^ c = b并且b ^ c = a。

让我详细解释一下这段代码的逻辑:

  1. let mut max_xor = 0; let mut mask = 0;:创建并初始化变量max_xor(用来记录目前找到的最大异或值)和mask(用于生成前缀)。
  2. for i in (0..=31).rev():这是一个循环,从31到0,覆盖了一个32位整数的所有位。
  3. mask |= 1 << i;:将mask的第i位设为1(i从31到0),用于在下一步中生成所有可能的i位前缀。
  4. let prefixes: HashSet<i32> = nums.iter().map(|num| num & mask).collect();:使用mask来获取数组nums中所有数字的前i位前缀,并将这些前缀存储到HashSet中。HashSet用于高效查找。
  5. let temp_max_xor = max_xor | (1 << i);:尝试将max_xor的第i位设为1,看看是否有两个数能使它们的异或结果为这个值。
  6. for &prefix in &prefixes { if prefixes.contains(&(temp_max_xor ^ prefix)) {...}:对于每个前缀,我们检查temp_max_xor ^ prefix是否也在前缀集合中。如果存在,那么说明我们可以在数组中找到两个数,它们的前i位异或结果为temp_max_xor
  7. max_xor = temp_max_xor; break;:如果我们找到这样的两个数,我们就把temp_max_xor赋值给max_xor,并跳出当前的for循环。
  8. max_xor:在遍历所有的位之后,我们返回max_xor,这就是数组中两个数能得到的最大异或结果。

这个函数的主要思想是从最高位开始,试图让结果的每一位都尽可能地为1。通过使用贪心的策略,我们可以在O(n)的时间复杂度内求解这个问题,n为nums的长度。