力扣之Trie(字典树、前缀树)系列
Leetcode 0208 实现Trie(前缀树)
分析
-
本题的考点:trie。
-
trie又被称为前缀树、字典树。
-
关于trie可以参考:网址。
代码
- C++
class Trie {
public:
struct Node {
bool is_end;
Node *son[26];
Node() {
is_end = false;
for (int i = 0; i < 26; i++)
son[i] = NULL;
}
} *root;
/** Initialize your data structure here. */
Trie() {
root = new Node();
}
/** Inserts a word into the trie. */
void insert(string word) {
auto p = root;
for (auto c : word) {
int u = c - 'a';
if (!p->son[u]) p->son[u] = new Node();
p = p->son[u];
}
p->is_end = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
auto p = root;
for (auto c : word) {
int u = c - 'a';
if (!p->son[u]) return false;
p = p->son[u];
}
return p->is_end;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string word) {
auto p = root;
for (auto c : word) {
int u = c - 'a';
if (!p->son[u]) return false;
p = p->son[u];
}
return true;
}
};
- Java
public class Trie {
private class Node{
public boolean isWord;
public Node[] next;
public Node(boolean isWord){
this.isWord = isWord;
next = new Node[26];
}
public Node(){
this(false);
}
}
private Node root;
public Trie(){
root = new Node();
}
// 向Trie中添加一个新的单词word
public void insert(String word){
Node cur = root;
for(int i = 0 ; i < word.length() ; i ++){
char c = word.charAt(i);
if(cur.next[c-'a'] == null)
cur.next[c-'a'] = new Node();
cur = cur.next[c-'a'];
}
cur.isWord = true;
}
// 查询单词word是否在Trie中
public boolean search(String word){
Node cur = root;
for(int i = 0 ; i < word.length() ; i ++){
char c = word.charAt(i);
if(cur.next[c-'a'] == null)
return false;
cur = cur.next[c-'a'];
}
return cur.isWord;
}
// 查询是否在Trie中有单词以prefix为前缀
public boolean startsWith(String isPrefix){
Node cur = root;
for(int i = 0 ; i < isPrefix.length() ; i ++){
char c = isPrefix.charAt(i);
if(cur.next[c-'a'] == null)
return false;
cur = cur.next[c-'a'];
}
return true;
}
}
时空复杂度分析
-
时间复杂度:每一个操作都是 O ( l e n ) O(len) O(len),
len
为单词平均长度。 -
空间复杂度: O ( n × l e n ) O(n \times len) O(n×len),
n
为插入的单词数量。
Leetcode 0211 添加与搜索单词
分析
-
本题的考点:trie。
-
trie插入操作不变,查询的时候遇到字母正常处理,遇到通配符直接暴搜。
代码
- C++
class WordDictionary {
public:
struct Node {
bool is_end;
Node *son[26];
Node() {
is_end = false;
for (int i = 0; i < 26; i++)
son[i] = NULL;
}
} *root;
/** Initialize your data structure here. */
WordDictionary() {
root = new Node();
}
void addWord(string word) {
auto p = root;
for (auto c : word) {
int u = c - 'a';
if (!p->son[u]) p->son[u] = new Node();
p = p->son[u];
}
p->is_end = true;
}
bool search(string word) {
return dfs(root, word, 0);
}
// 返回以p为根的trie树中是否存在字符串word[i...)
bool dfs(Node *p, string word, int i) {
if (i == word.size()) return p->is_end;
if (word[i] != '.') {
int u = word[i] - 'a';
if (!p->son[u]) return false;
return dfs(p->son[u], word, i + 1);
} else {
for (int j = 0; j < 26; j++)
if (p->son[j] && dfs(p->son[j], word, i + 1))
return true;
return false;
}
}
};
- Java
public class WordDictionary {
private class Node {
public boolean isWord;
public TreeMap<Character, Node> next;
public Node(boolean isWord) {
this.isWord = isWord;
next = new TreeMap<>();
}
public Node() {
this(false);
}
}
private Node root;
public WordDictionary() {
root = new Node();
}
public void addWord(String word) {
Node cur = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.next.get(c) == null)
cur.next.put(c, new Node());
cur = cur.next.get(c);
}
cur.isWord = true;
}
public boolean search(String word) {
return match(root, word, 0);
}
// 在以node为根节点的字典树中查找是否存在单词word,index表示匹配到第几个字符
private boolean match(Node node, String word, int index) {
if (index == word.length()) return node.isWord;
char c = word.charAt(index);
if (c != '.') {
if (node.next.get(c) == null)
return false;
return match(node.next.get(c), word, index + 1);
} else { // c == '.'
for (char nextChar : node.next.keySet())
if (match(node.next.get(nextChar), word, index + 1))
return true;
return false;
}
}
}
时空复杂度分析
-
时间复杂度:插入与插入字符串长度成正比,查询是指数级别。
-
空间复杂度:和递归深度有关。
Leetcode 0386 字典序排数
题目描述:Leetcode 0386 字典序排数
分析
-
本题的考点:深搜、trie。
-
本题可以将
1~n
这所有的数插入trie
中,例如12
插入trie
则插入两个节点,即1
和2
。然后我们递归遍历这棵树,如果得到的节点对应的值小于等于n
,则加入结果中。 -
实际上,我们并不需要将
trie
建立出来。
代码
- C++
class Solution {
public:
vector<int> res;
vector<int> lexicalOrder(int n) {
for (int i = 1; i <= 9; i++) dfs(i, n);
return res;
}
void dfs(int cur, int n) {
if (cur <= n) res.push_back(cur);
else return;
for (int i = 0; i <= 9; i++) dfs(cur * 10 + i, n);
}
};
- Java
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> lexicalOrder(int n) {
for (int i = 1; i <= 9; i++) dfs(i, n);
return res;
}
private void dfs(int cur, int n) {
if (cur <= n) res.add(cur);
else return;
for (int i = 0; i <= 9; i++) dfs(cur * 10 + i, n);
}
}
时空复杂度分析
-
时间复杂度: O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n))。
-
空间复杂度:考虑结果的存储, O ( n ) O(n) O(n)。
Leetcode 0421 数组中两个数的最大异或值
分析
-
本题的考点:trie(字典树、前缀树)。
-
关于
trie
的讲解可以参考:trie(字典树、前缀树)。这里面就有对本题的讲解。 -
这里题如果暴力求解的话,相当于让任意两个不同的数进行异或运算,然后记录最大的结果输出即可,伪码如下:
int res = 0; // 最小是0
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++)
res = max(res, a[i] ^ a[j]);
}
-
我们分析内层循环,其实在寻找和
a[i]
异或值最大的另一个数据,我们可以使用字典树(trie
)优化这一步,因为所有的数据范围在 [ 0 , 2 31 ) [0, 2^{31}) [0,231)之间,二进制位数最长是31
位,我们可以将这31
位二进制数看做一个字符串,最高位是字符串的第一个字符,然后将所有的这些字符串插入trie
树中。 -
这样操作之后,我们如何得到与某个数据A异或值最大的数呢?数据A可以看成一个31的二进制字符串,从左到右遍历这个字符串,假设当前考察的是字符
u
,则在trie树中我们应该走到!u
的分支上(如果存在的话),这样异或值才能最大(贪心思想)。
代码
- C++
class Solution {
public:
vector<vector<int>> s; // 两维,第一维是idx,第二维决定对应位是0还是1
void insert(int x) {
int p = 0;
for (int i = 30; ~i; i--) {
int u = x >> i & 1;
if (!s[p][u]) s[p][u] = s.size(), s.push_back({0, 0});
p = s[p][u];
}
}
int query(int x) {
int p = 0, res = 0;
for (int i = 30; i >= 0; i--) {
int u = x >> i & 1;
if (s[p][!u]) p = s[p][!u], res = res * 2 + !u;
else p = s[p][u], res = res * 2 + u;
}
return res ^ x;
}
int findMaximumXOR(vector<int>& nums) {
s.push_back({0, 0}); // 创建根节点(空节点)
int res = 0;
for (auto x : nums) {
res = max(res, query(x));
insert(x);
}
return res;
}
};
- Java
class Solution {
static class Node {
Node[] son = new Node[2];
}
Node root = new Node();
public int findMaximumXOR(int[] nums) {
int res = 0;
for (int x : nums) {
res = Math.max(res, query(x));
insert(x);
}
return res;
}
private void insert(int x) {
Node p = root;
for (int i = 30; i >= 0; i--) {
int u = (x >> i) & 1;
if (p.son[u] == null) p.son[u] = new Node();
p = p.son[u];
}
}
private int query(int x) {
Node p = root;
int res = 0;
for (int i = 30; i >= 0; i--) {
int u = (x >> i) & 1;
if (p.son[u ^ 1] != null) {
p = p.son[u ^ 1];
res = res * 2 + 1 - u;
} else {
p = p.son[u];
res = res * 2 + u;
}
// 数组模拟的话,刚开始没有元素时,p会一直是0,因此不会NPE
// 这里不是数组模拟,因此刚开始trie中没有数据时,要让p一直指向root
if (p == null) p = root;
}
return res ^ x;
}
}
时空复杂度分析
- 时间复杂度:
O
(
n
)
O(n)
O(n),
n
为数组长度。 - 空间复杂度: O ( n ) O(n) O(n)。
Leetcode 1707 与数组中元素的最大异或值
分析
-
本题的考点:trie(字典树、前缀树)。
-
本题的由来:Leetcode 0208 实现Trie(前缀树) —> Leetcode 0421 数组中两个数的最大异或值 —> Leetcode 1707 与数组中元素的最大异或值。
-
本题存在两种解法:在线做法,离线做法。所谓的在线做法就是指查询是一个一个来的,我们只能每次处理一个查询;离线做法可以将所有查询存储起来,然后最后一起处理。这里只演示在线做法
在线做法
- 因为
nums
中的数据是小于等于 1 0 9 10^9 109的,将每个数据看成二进制串,只需要0~29
一共30
个比特位即可存储每个数据。 - 本题和
LC421
非常类似,区别在于异或的数据要小于某个数,为此每个节点需要记录额外的信息,这里记录以该节点为根的子的最小值min
即可。 - 向
trie
插入数据的时候,更新每个节点的min
值,查询与x
异或值最大的数时,需要判断两个条件:(1)异或的比特位尽量不同;(2)该节点存在小于给定值的值。
离线做法
-
由于全部询问已经给出,我们不一定要按顺序回答询问,而是按照 m i m_i mi 从小到大的顺序回答。
-
首先将数组
nums
从小到大排序,将询问按照 m i m_i mi 的大小从小到大排序。 -
在回答每个询问前,将所有不超过 m i m_i mi 的
nums
元素插入字典序中,由于nums
已经排好序,我们可以维护一个指向nums
数组元素的下标idx
,初始值为 0,每插入一个元素就将idx
加一。对于每个询问,我们可以不断插入满足 n u m s [ i d x ] ≤ m i nums[idx] \le m_i nums[idx]≤mi 的元素,直至不满足该条件或idx
指向了数组末尾。 此时字典树中的元素就是nums
中所有不超过 m i m_i mi 的元素然后就可以使用LC421
求解。 -
另外注意,由于
queries
被排序了,我要记录每个查询原始对应的位置,因为最后的结果数据中的数据要按查询的顺序给出。
代码
- C++
class Solution {
public:
struct Node {
Node *son[2];
int min; // 以该节点为根的子树存储的最小值
Node() {
son[0] = son[1] = NULL;
min = INT_MAX;
}
} *root;
void insert(int val) {
auto p = root;
p->min = min(p->min, val);
for (int i = 29; i >= 0; i--) {
int u = val >> i & 1;
if (!p->son[u]) p->son[u] = new Node();
p = p->son[u];
p->min = min(p->min, val);
}
}
// 返回数值不大于m的与x异或值最大的结果
int query(int x, int m) {
auto p = root;
if (p->min > m) return -1;
int res = 0;
for (int i = 29; i >= 0; i--) {
int u = x >> i & 1;
if (p->son[!u] && p->son[!u]->min <= m) p = p->son[!u], res = res * 2 + !u;
else p = p->son[u], res = res * 2 + u;
}
return res ^ x;
}
vector<int> maximizeXor(vector<int>& nums, vector<vector<int>>& queries) {
root = new Node();
for (int x : nums) insert(x);
vector<int> res;
for (int i = 0; i < queries.size(); i++) {
int x = queries[i][0], m = queries[i][1];
res.push_back(query(x, m));
}
return res;
}
};
- Java
class Solution {
static class Node {
Node[] son = new Node[2];
int min = Integer.MAX_VALUE;
}
Node root = new Node();
private void insert(int val) {
Node p = root;
p.min = Math.min(p.min, val);
for (int i = 29; i >= 0; i--) {
int u = val >> i & 1;
if (p.son[u] == null) p.son[u] = new Node();
p = p.son[u];
p.min = Math.min(p.min, val);
}
}
private int query(int x, int m) {
Node p = root;
if (p.min > m) return -1;
int res = 0;
for (int i = 29; i >= 0; i--) {
int u = x >> i & 1;
if (p.son[1 ^ u] != null && p.son[1 ^ u].min <= m) {
p = p.son[1 ^ u];
res = res * 2 + 1 - u;
} else {
p = p.son[u];
res = res * 2 + u;
}
}
return res ^ x;
}
public int[] maximizeXor(int[] nums, int[][] queries) {
for (int x : nums) insert(x);
int[] res = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
int x = queries[i][0], m = queries[i][1];
res[i] = query(x, m);
}
return res;
}
}
- Python
# TLE
class Solution:
class Node:
def __init__(self):
self.son = [None] * 2
self.min = int(2e9)
def maximizeXor(self, nums: List[int], queries: List[List[int]]) -> List[int]:
root = self.Node()
for x in nums:
self.insert(root, x)
res = []
for i in range(len(queries)):
x = queries[i][0]
m = queries[i][1]
res.append(self.query(root, x, m))
return res
def insert(self, root, val):
p = root
p.min = min(p.min, val)
for i in range(29, -1, -1):
u = val >> i & 1
if p.son[u] == None:
p.son[u] = self.Node()
p = p.son[u]
p.min = min(p.min, val)
def query(self, root, x, m):
p = root
if p.min > m:
return -1
res = 0
for i in range(29, -1, -1):
u = x >> i & 1
if p.son[1 ^ u] and p.son[1 ^ u].min <= m:
p = p.son[1 - u]
res = res * 2 + 1 - u
else:
p = p.son[u]
res = res * 2 + u
return res ^ x
时空复杂度分析
-
时间复杂度: O ( n + m ) O(n + m) O(n+m),
n
为数组nums
长度,m
为queries
长度。 -
空间复杂度: O ( n ) O(n) O(n)。
AcWing相关题目
-
给定一个数组(元素非负),返回异或值最大的两个元素异或的结果。AcWing 143. 最大异或对。
-
给定一个数组(元素非负),请你在该整数序列中找出一个连续的非空子序列,使得子序列中元素的异或和能够最大。如果存在多个这样的序列,返回区间右端点最小的那个。AcWing 1414. 牛异或。
-
给定一个数组(元素非负),请在所有长度不超过
M
的连续子数组(子数组可以为空)中,找出子数组异或和的最大值。AcWing 3485. 最大异或和。