五月集训(第23天)—字典树(Trie)
字典树(Trie)
1. 211. 添加与搜索单词 - 数据结构设计
思路: 字典树模板
为什么要使用递归查询呢?那是因为存在特殊符号.
,该符号可以代表字典树上当前结点的任意子节点,所以要从多个分支往下搜,递归很容易实现, 但是迭代就很难实现了。
class WordDictionary {
struct TrieNode {
vector<TrieNode *> child;
bool isEnd;
TrieNode() {
this->child = vector<TrieNode *> (26, nullptr);
this->isEnd = false;
}
};
public:
WordDictionary() {
trie = new TrieNode();
}
void insert(TrieNode *root, const string &word) {
for (auto w : word) {
if (root->child[w - 'a'] == nullptr) {
root->child[w - 'a'] = new TrieNode();
}
root = root->child[w - 'a'];
}
root->isEnd = true;
}
void addWord(string word) {
insert(trie, word);
}
bool search(string word) {
int len = word.length();
return dfs(word, 0, trie, len);
}
bool dfs(const string &word, int index, TrieNode *root, int word_length) {
if (index == word_length) return root->isEnd;
char ch = word[index];
if (ch >= 'a' && ch <= 'z') {
TrieNode *child = root->child[ch - 'a'];
if (child != nullptr && dfs(word, index + 1, child, word_length)) {
return true;
}
} else if (ch == '.') {
for (int i = 0; i < 26; i++) {
TrieNode *child = root->child[i];
if (child != nullptr && dfs(word, index + 1, child, word_length)) {
return true;
}
}
}
return false;
}
private:
TrieNode *trie;
};
/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary* obj = new WordDictionary();
* obj->addWord(word);
* bool param_2 = obj->search(word);
*/
2. 1268. 搜索推荐系统
思路:
要查询单词,利用字典树。
要返回前缀,那么在生成字典树时给每个结点保存以当前字母为终点的前缀对应的单词,查询时可直接给出。
此题还要求按照字典序升序返回,那么保存前缀对应单词就使用堆,方便维护顺序。(此题仅保留字典序最小的三个单词,所以用大根堆,在堆中单词多余三个时,弹出堆顶至只剩三个单词即可)
class Solution {
private:
struct Trie {
vector<Trie *> child;
priority_queue<string> words; // 默认为大根堆,即堆顶为字典序大的数
Trie () {
this->child = vector<Trie *> (26, nullptr);
}
};
void add_word(Trie *root, const string &word) {
Trie* node = root;
for (const char &ch : word) {
if (node->child[ch - 'a'] == nullptr) {
node->child[ch - 'a'] = new Trie();
}
node = node->child[ch - 'a'];
node->words.push(word);
if (node->words.size() > 3) node->words.pop();
}
}
// 根节点就是一个空的树根,从下一层开始存字符串
public:
vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
Trie *root = new Trie();
for (const string &p_word : products) {
add_word(root, p_word);
}
vector<vector<string>> ans;
bool fial_flag = false;
Trie *cur = root;
for (const char &ch : searchWord) {
if (fial_flag || cur->child[ch - 'a'] == nullptr) {
/* 如果某一个字母找不到了,之后的全都找不到,答案中添加一个空列表I占位 */
ans.emplace_back();
fial_flag = true;
} else {
vector<string> child_words;
cur = cur->child[ch - 'a'];
while (!cur->words.empty()) { /* 将以字母ch为前缀的三个单词按字典序从大到小放入child_words中 */
child_words.push_back(cur->words.top());
cur->words.pop();
}
reverse(child_words.begin(), child_words.end()); /* 按字典序从大到小排列 */
ans.push_back(child_words); /* 添加答案 */
}
}
return ans;
}
};
3. 421. 数组中两个数的最大异或值
思路: 实现细节在代码中注释
异或性质,同为0,异为1,找到某两个数,让他们不相同的位尽可能多,最后选最大的结果。(当然不是不同的数最多的就是最大的,要考虑每一位的权重问题)
利用字典树存每个数字,每个结点代表一个二进制位的值(0或1),由于“字母”只有0和1,所以直接用二叉树就可
挑一个数与字典树中的数xor,一位一位比,每次尽可能找不一样的位(异为1,会使xor结果更大)。每个数字都和字典树xor一次,选择最大的结果即可。
struct Trie { /* 由于该字典树每个结点只有0和1两个子节点,可以用二叉树表示 */
Trie *left = nullptr; /* 左子节点代表 0 */
Trie *right = nullptr; /* 右子节点代表 1 */
Trie() {}
};
class Solution {
private:
Trie *root = new Trie(); // 字典树的根节点
static const int HIGH_BIT = 30; // 字典树的高度,即每个单词的长度,(此处为每个数字的二进制位长)
public:
void add_word(int num) { /* 将每个数字按二进制加入字典树 */
Trie *cur = root;
int bit_ = 0;
for (int i = HIGH_BIT; i >= 0; i--) {
bit_ = ((num >> i) & 1);
if (!bit_) { /* 0为左节点 */
if (cur->left == nullptr) {
cur->left = new Trie();
}
cur = cur->left;
} else { /* 1为右子结点 */
if (cur->right == nullptr) {
cur->right = new Trie();
}
cur = cur->right;
}
}
}
int get_max_xor(int num) { /* 获取num与已经加入字典树的数字的最大xor结果 */
Trie *cur = root;
int ans = 0;
int bit_ = 0;
for (int i = HIGH_BIT; i >= 0; i--) {
bit_ = ((num >> i) & 1);
if (bit_) { /* bit_为1,则在字典树中找0,即找左子结点,才能使xor结果更大,没有0则只能找1 */
if (cur->left) {
cur = cur->left;
ans = ans * 2 + 1; // 将当前位逐步还原
} else {
cur = cur->right;
ans = ans * 2;
}
} else { /* bit_为0则找1,即找右子结点 */
if (cur->right) {
cur = cur->right;
ans = ans * 2 + 1;
} else {
cur = cur->left;
ans = ans * 2;
}
}
}
return ans;
}
int findMaximumXOR(vector<int>& nums) {
int nums_n = nums.size();
int ans = 0;
for (int i = 0; i < nums_n - 1; i++) { /* 由于异或的性质,可以边往字典树里添加数,边找最大的xor结果,因为每个数xor了前面的数,也被后面的数xor,所以没有遗漏,最后一个数就不用加入字典树了 */
add_word(nums[i]);
ans = max(ans, get_max_xor(nums[i + 1]));
}
return ans;
}
};
4. 1707. 与数组中元素的最大异或值
思路:
和第三题思路相似,只需要对nums[] 和 queries[]
排序,这样就可以利用第三问的方式,边往字典树中加入数字,边得到结果,复杂度降为$O(nlogn + mlogm + nlogC)$, 即两个排序和一个字典树的生成
struct Trie {
Trie *left = nullptr;
Trie *right = nullptr;
Trie() {}
};
class Solution {
private:
Trie *root = new Trie();
static const int HIGH_BIT = 30; // 最大数为 10e9 可以用 int 表示
public:
void add_words(int num) { /* 将每个数字按二进制加入字典树 */
Trie *cur = root;
int bit_ = 0;
for (int i = HIGH_BIT; i >= 0; i--) {
bit_ = ((num >> i) & 1);
if (!bit_) { /* 0为左节点 */
if (cur->left == nullptr) {
cur->left = new Trie();
}
cur = cur->left;
} else { /* 1为右子结点 */
if (cur->right == nullptr) {
cur->right = new Trie();
}
cur = cur->right;
}
}
}
int get_max_xor(int num) { /* 获取num与已经加入字典树的数字的最大xor结果 */
Trie *cur = root;
int ans = 0;
int bit_ = 0;
for (int i = HIGH_BIT; i >= 0; i--) {
bit_ = ((num >> i) & 1);
if (bit_) { /* bit_为1,则在字典树中找0,即找左子结点,才能使xor结果更大,没有0则只能找1 */
if (cur->left) {
cur = cur->left;
ans = ans * 2 + 1; // 将当前位逐步还原
} else {
cur = cur->right;
ans = ans * 2;
}
} else { /* bit_为0则找1,即找右子结点 */
if (!cur->right) {
cur = cur->left;
ans = ans * 2;
} else {
cur = cur->right;
ans = ans * 2 + 1;
}
}
}
return ans;
}
static int cmp (const vector<int> &a, const vector<int> &b) {
return a[1] < b[1];
}
vector<int> maximizeXor(vector<int>& nums, vector<vector<int>>& queries) {
int nums_size = nums.size();
int query_size = queries.size();
vector<int> ans (query_size, 0);
for (int i = 0; i < query_size; ++i) queries[i].push_back(i); /* 添加原来的顺序,方便排序后还原 */
sort(queries.begin(), queries.end(), cmp); /* 按 mi 对访问排序 */
sort(nums.begin(), nums.end());
int x, m, id, idx = 0;
for (int i = 0; i < query_size; ++i) {
x = queries[i][0], m = queries[i][1], id = queries[i][2];
while (idx < nums_size && nums[idx] <= m) {
add_words(nums[idx]);
++idx;
}
/* 一定主义判断 idx == 0 即字典树为空的情况,这时字典树是空的,访问时会访问到非法地址,一直报错,呜呜呜 */
if (idx == 0 || m < queries[0][1]) ans[id] = -1; /* 如果所有数都比 m 大 */
else {
ans[id] = get_max_xor(x); /* 还原为开始的访问顺序 */
}
}
return ans;
}
};
字典树的结点用指针数组和位运算简化一下
struct Trie {
Trie *child[2];
Trie() {
child[0] = nullptr;
child[1] = nullptr;
}
};
class Solution {
private:
Trie *root = new Trie();
static const int HIGH_BIT = 30; // 最大数为 10e9 可以用 int 表示
public:
void add_words(int num) { /* 将每个数字按二进制加入字典树 */
Trie *cur = root;
int bit_ = 0;
for (int i = HIGH_BIT; i >= 0; i--) {
bit_ = ((num >> i) & 1);
if (cur->child[bit_] == nullptr) cur->child[bit_] = new Trie();
cur = cur->child[bit_];
}
}
int get_max_xor(int num) { /* 获取num与已经加入字典树的数字的最大xor结果 */
Trie *cur = root;
int ans = 0;
int bit_ = 0;
for (int i = HIGH_BIT; i >= 0; i--) {
bit_ = ((num >> i) & 1);
if (cur->child[bit_ ^ 1] == nullptr) {
ans = ans * 2;
cur = cur->child[bit_];
} else {
ans = ans * 2 + 1;
cur = cur->child[bit_ ^ 1];
}
}
return ans;
}
static int cmp (const vector<int> &a, const vector<int> &b) {
return a[1] < b[1];
}
vector<int> maximizeXor(vector<int>& nums, vector<vector<int>>& queries) {
int nums_size = nums.size();
int query_size = queries.size();
vector<int> ans (query_size, 0);
for (int i = 0; i < query_size; ++i) queries[i].push_back(i); /* 添加原来的顺序,方便排序后还原 */
sort(queries.begin(), queries.end(), cmp); /* 按 mi 对访问排序 */
sort(nums.begin(), nums.end());
int x, m, id, idx = 0;
for (int i = 0; i < query_size; ++i) {
x = queries[i][0], m = queries[i][1], id = queries[i][2];
while (idx < nums_size && nums[idx] <= m) {
add_words(nums[idx]);
++idx;
}
/* 一定主义判断 idx == 0 即字典树为空的情况,这时字典树是空的,访问时会访问到非法地址,一直报错,呜呜呜 */
if (idx == 0 || m < queries[0][1]) ans[id] = -1; /* 如果所有数都比 m 大 */
else {
ans[id] = get_max_xor(x); /* 还原为开始的访问顺序 */
}
}
return ans;
}
};
东方欲晓,莫道君行早。