- 📚 博客主页:⭐️这是一只小逸白的博客鸭~⭐️
- 👉 欢迎 关注❤️点赞👍收藏⭐️评论📝
- 😜 小逸白正在备战实习,经常更新面试题和LeetCode题解,欢迎志同道合的朋友互相交流~
- 💙 若有问题请指正,记得关注哦,感谢~
往期文章 :
- LeetCode 剑指 Offer II 链表 专题总结
- LeetCode 剑指 Offer II 哈希表 专题总结
- LeetCode 剑指 Offer II 栈 专题总结
- LeetCode 剑指 Offer II 队列 专题总结
- LeetCode 剑指 Offer II 树(上) 专题总结
- LeetCode 剑指 Offer II 树(下) 专题总结
- LeetCode 剑指 Offer II 堆 专题总结
- LeetCode 剑指 Offer II 前缀树(上) 专题总结
第一题:后缀,
第二题:前缀,用哈希更好
第三题:01 Trie树
065. 最短的单词编码
题目:
单词数组
words
的 有效编码 由任意助记字符串s
和下标数组indices
组成,且满足:
words.length == indices.length
- 助记字符串
s
以 ‘#
’ 字符结尾- 对于每个下标
indices[i]
,s
的一个从indices[i]
开始、到下一个 ‘#
’ 字符结束(但不包括 ‘#
’)的 子字符串 恰好与words[i]
相等
给定一个单词数组words
,返回成功对words
进行编码的最小助记字符串s
的长度 。
示例:
输入:words = [“time”, “me”, “bell”]
输出:10
解释:一组有效编码为 s = “time#bell#” 和 indices = [0, 2, 5] 。
words[0] = “time” ,s 开始于 indices[0] = 0 到下一个 ‘#’ 结束的子字符串,如加粗部分所示 “time#bell#”
words[1] = “me” ,s 开始于 indices[1] = 2 到下一个 ‘#’ 结束的子字符串,如加粗部分所示 “time#bell#”
words[2] = “bell” ,s 开始于 indices[2] = 5 到下一个 ‘#’ 结束的子字符串,如加粗部分所示 “time#bell#”
提示:
1 <= words.length <= 2000
1 <= words[i].length <= 7
words[i]
仅由小写字母组成
思路:
前缀树:
题意:如果单词X
是Y
的后缀,那么单词X
就不需要考虑了,因为编码Y
的时候就同时将X
编码了。例如,如果words
中同时有 “me
” 和 “time
”,我们就可以在不改变答案的情况下不考虑 “me
”。如果单词Y
不在任何别的单词X
的后缀中出现,那么Y
一定是编码字符串的一部分。
- 前缀树反向存储单词,这样可以将后缀过滤掉
- 查询过程中找到单词结尾时判断是不是尾节点
- yes,就添加这个单词,并且删除这个结尾,防止一样的元素重复计算 例如:time,time,time,长度为5 --> time#
- no,代表是后缀,直接跳过结束查询
class Trie {
public:
vector<Trie*> next;
bool isEnd;
Trie():next(26, nullptr),isEnd(false){}
void insert(string& word) {
Trie* node = this;
// 反向插入
for(int i = word.length() - 1; i >= 0; i--) {
int index = word[i] - 'a';
if(node->next[index] == nullptr) {
node->next[index] = new Trie();
}
node = node->next[index];
}
node->isEnd = true;
}
int searchLen(string& word) {
Trie* node = this;
for(int i = word.length() - 1; i >= 0; i--) {
int index = word[i] - 'a';
node = node->next[index];
// 找到单词最后一个字符,并且该字符是单词结尾
if(node->isEnd && i == 0) {
// 判断这个字符是不是结尾,不是的话就是后缀,不用处理
for(int i = 0; i < 26; i++) {
if(node->next[i]) {
return 0;
}
}
// 将这个单词删除,将相同元素 adn 后缀元素删除
node->isEnd = false;
// 这个字符是结尾,返回 单词 + #
return word.length() + 1;
}
}
return 0;
}
};
class Solution {
public:
// 思路:本题含义是如果 x 是 y 的后缀,就删除x,不用统计
int minimumLengthEncoding(vector<string>& words) {
Trie* node = new Trie();
for(auto& word : words) {
node->insert(word);
}
int ans = 0;
for(auto& word : words) {
ans += node->searchLen(word);
}
return ans;
}
};
066. 单词之和
题目:
实现一个
MapSum
类,支持两个方法,insert
和sum
:
MapSum()
初始化 MapSum 对象
void insert(String key, int val)
插入key-val
键值对,字符串表示键key
,整数表示值val
。如果键key
已经存在,那么原来的键值对将被替代成新的键值对。
int sum(string prefix)
返回所有以该前缀prefix
开头的键key
的值的总和。
示例:
输入:
inputs = [“MapSum”, “insert”, “sum”, “insert”, “sum”]
inputs = [[], [“apple”, 3], [“ap”], [“app”, 2], [“ap”]]
输出:
[null, null, 3, null, 5]
解释:
MapSum mapSum = new MapSum();
mapSum.insert(“apple”, 3);
mapSum.sum(“ap”); // return 3 (apple = 3)
mapSum.insert(“app”, 2);
mapSum.sum(“ap”); // return 5 (apple + app = 3 + 2 = 5)
提示:
1 <= key.length, prefix.length <= 50
key
和prefix
仅由小写英文字母组成1 <= val <= 1000
- 最多调用
50
次insert
和sum
思路:
这道题用哈希表反而比前缀树简单不止一点半点,而且还快
题意:难点是 sum()
函数,要找到prefix
前缀的单词的val
和
方法一:哈希表
将单词存入哈希表,求和时遍历哈希表,配对前缀为
prefix
的单词
用substr()
进行前缀配对
class MapSum {
public:
unordered_map<string, int> map;
MapSum() {
}
void insert(string key, int val) {
map[key] = val;
}
int sum(string prefix) {
int ans = 0, n = prefix.length();
for(auto& [key, val] : map) {
if(key.length() >= n && key.substr(0, n) == prefix)
ans += val;
}
return ans;
}
};
方法二:前缀树
正常插入,单词结尾时用
wordVal
存储val
值,将bool
标记换成int
标记
计算sum()
时搜索按前缀开头的单词
- 先按prefix前缀顺序搜索
- 接着不确定哪里有字符, 用
dfs
深搜,将26
个可能的后序字符都搜索一遍dfs
过程中用ans
将单词val
加上
class Trie {
private:
int wordVal;
vector<Trie*> next;
public:
Trie():next(26,nullptr),wordVal(0){};
// 插入,将结尾的bool 换成 int 就行
void insert(string& word, int val) {
Trie* node = this;
for(char ch : word) {
int index = ch - 'a';
if(node->next[index] == nullptr) {
node->next[index] = new Trie();
}
node = node->next[index];
}
node->wordVal = val;
}
// 先顺着前缀搜索下去,后面转而遍历每一种可能
void searchPrefix(string& word, int& ans) {
Trie* node = this;
// 找到前缀的最后一个字符
for(auto& ch : word) {
// 没有word这个前缀就直接返回
if(node->next[ch - 'a'] == nullptr)
return ;
node = node->next[ch - 'a'];
}
// 从这个字符开始遍历
dfs(node, ans);
}
void dfs(Trie* node, int& ans) {
if(node == nullptr) return ;
// 如果是单词就加上
if(node->wordVal > 0)
ans += node->wordVal;
// 遍历前缀剩余可能的部分
for(int i = 0; i < 26; i++) {
dfs(node->next[i], ans);
}
}
};
class MapSum {
public:
Trie* node;
MapSum() {
node = new Trie();
}
void insert(string key, int val) {
node->insert(key, val);
}
int sum(string prefix) {
int ans = 0;
node->searchPrefix(prefix, ans);
return ans;
}
};
067. 最大的异或
题目:
给定一个整数数组
nums
,返回nums[i] XOR nums[j]
的最大运算结果,其中0 ≤ i ≤ j < n
。
示例:
输入:nums = [3,10,5,25,2,8]
输出:28
解释:最大运算结果是 5 XOR 25 = 28.
提示:
1 <= nums.length <= 2 * 104
0 <= nums[i] <= 231 - 1
思路:
前缀树:
每个节点有2
个分支:我们把每个元素看成一个32
位的01
串(数值不足 在前面补0
),将32
位二进制串插入一棵trie
树(从高位开始插入)。
insert
:由于我们要求两个元素的异或最大值,所以我们肯定是要从最高位开始考虑的,因此我们存x
时从左向右开始存储在trie
中。
search
:我们先在trie
树中找到能与x
异或取得最大值的另一个数组元素y
,因为异或的运算的法则是相同得0
,不同得1
,所以我们尽可能走与x
当前位相反的字符方向走,才能得到能和x
产生最大值的另一个数组元素y
,然后res=x^y
。
class Trie {
public:
vector<Trie*> next;
Trie():next(2,nullptr){}
void insert(int x) {
Trie* node = this;
for(int i = 30; i >= 0; i--) {
// 取x的第i位
int u = (x >> i) & 1;
if(node->next[u] == nullptr) {
node->next[u] = new Trie();
}
node = node->next[u];
}
}
int search(int x) {
Trie* node = this;
// 存储 x 能够异或的最大值
int res = 0;
for(int i = 30; i >= 0; i--) {
int u = (x >> i) & 1;
// 优先选择取该位反方向为最大 因为 1 ^ 0 = 1
if(node->next[!u]) {
node = node->next[!u];
res = (res << 1) ^ !u;
}else {
// 没有的话取本身
node = node->next[u];
res = (res << 1) ^ u;
}
}
// ans = res ^ x
res ^= x;
return res;
}
};
class Solution {
public:
int findMaximumXOR(vector<int>& nums) {
Trie* node = new Trie();
for(int i : nums) {
node->insert(i);
}
int res = 0;
for(int i : nums) {
res = max(res, node->search(i));
}
return res;
}
};