【Weekly Algorithm】 算法周记之《代码随想录》哈希表
学习小结
本周跟随《代码随想录》学习哈希表部分,较深入地了解了哈希表的相关算法题,结合数组、set、map完成题目,并利用双指针解决部分题目。
哈希表
简评
究其本质而言,就是数组,但可以通过给元素编码得其索引,以 O(1) 的效率访存,最适合检测该元素是否存在。
有效的字母异位词
- 代码随想录 (programmercarl.com)
- 即检测一个字符串是否为另一个字符串的字母打散重组的单词
- 字符可以通过 ASCII 码表来存储,遍历某个字符串,对应字符++,再遍历另一个对应字符–,最后所有字符都为 0 即为字母异位词
1 | class Solution { |
两个数组的交集
本题与 [[有效的字母异位词]],有点类似,不过检测的元素从字符变成数字,而且需要输出重合的部分
大致有两思路
- 陆爻齐想到,用 unordered_map 先把某一个数组中有的数字记 1,随后遍历另一数组时,把当前 map 里有且值为 1 的部分改值到 2 并放到数组里;
- 代码随想录用两个 set,其中一个用来存储某个数组唯一的所有元素,另一个存放结果,由于 set 会保证元素唯一,一旦找到交集就可以无脑 insert 进去
这里选择记录陆爻齐的思路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int,int> num_rec;
vector<int> res;
// 遍历某个数组,记录其中存在数字
for (auto c : nums1) {
if (num_rec.find(c) == num_rec.end()) {
num_rec[c] = 1;
}
}
// 遍历另一个数组,记录交集数字加入结果
for (auto c : nums2) {
if (num_rec.find(c) != num_rec.end() && num_rec[c] == 1) {
num_rec[c]++;
res.push_back(c);
}
}
return res;
}
};
快乐数
- 代码随想录 (programmercarl.com)
- 该题就是一个数字游戏,寻找一个整数是否存在一个其各元素平方和为 1 的情况,可能会无限循环
- 解决方法就是创建一个哈希表,把过程中产生的数字记录,若有重复,则陷入死循环,否则到了 1,则为快乐数
- 参考代码随想录思路的解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class Solution {
public:
bool isHappy(int n) {
// 记录过程中产生的数
unordered_map<int, int> record;
// map 中判断是否记录过的方法
while (record.find(n) == record.end()) {
// 做记录
record[n] = 0;
n = get_happy(n);
// 得到快乐数的情况
if (n == 1) {
return true;
}
//cout << n << endl;
}
return false;
}
// 递归获取各位平方和
int get_happy(int n) {
if (n >= 10) {
return (n % 10) * (n % 10) + get_happy(n / 10);
}
return n * n;
}
};
两数之和
- 代码随想录 (programmercarl.com)
- 该题给予了一个整数数组和目标数字,要求找到数组中一组两数和为目标值的下标
- 暴力解法为 O(n^2) 遍历两层数组,而运用哈希值就能快很多,通过 map,可以把遍历过的数字作为索引,其对应下标作为值存入 map 中,如此在得到数组中的一个数字时,就计算其与目标差值,在 map 中查找差值是否存在,存在则找到解,不存在则存入当前正在遍历的数字及其下标。
1 | class Solution { |
四数相加||
- 代码随想录 (programmercarl.com)
- 与[[两数之和]]类似,不过这次有四个数组,要求找到四个数组各取一数之和为 0 的元组个数
- 方法是,把前两数组之和放 map,和值为 key,出现次数为 value,然后遍历后两数组和,对于其负数若 map 中有,则结果多一个该负数在 map 对应的 value 值
1 | class Solution { |
赎金信
本题的意思与此前的 [[有效的字母异位词]],很相近,不过 [[有效的字母异位词]] 需要两个字符串可以互相组成,而本题只需要字符串 a 能由字符串 b 组成即可
方法反而简单,可以用 map 记录字母对应次数,但由于本题目已说明字母串中皆为小写字母,所以通过 ASCII 码转换下标记录数组的效率更高,更简单
陆爻齐的思路是增加一个整形数字记录所需字母的总个数,一旦字符串 b 中出现所需字母,且所需字母的需要个数非零,则该数字减一,最后秩序检查该数字是否为零即可得知题意是否满足
代码随想录更直接些,先统计字符串b的字母,再用 a 的减去,若出现小于零的位,则说明有没有满足的字母存在
陆爻齐的解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41class Solution {
public:
int need_num = 0; // 记录需要字母的总个数
int word_record[26] = {0}; // 记录需要字母的个数
bool canConstruct(string ransomNote, string magazine) {
// 记录需要字母个数
for (auto c : ransomNote) {
add_rec(get_char_num(c));
}
// 削减需要字母个数
for (auto c : magazine) {
cut_rec(get_char_num(c));
}
// 若仍有需要字符未消减干净,则不满足题目条件
if (need_num > 0){
return false;
}
return true;
}
// 记录需要的字母及其数量
void add_rec(int index) {
word_record[index]++;
need_num++;
}
// 削减magazine有的字符,但只有是需要的情况下才削减数量
void cut_rec(int index) {
if (word_record[index] > 0) {
word_record[index]--;
need_num--;
}
}
// 获取字母对应的下标
inline int get_char_num(char c) {
return int(c - 97);
}
};代码随想录的解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
//add
if (ransomNote.size() > magazine.size()) {
return false;
}
for (int i = 0; i < magazine.length(); i++) {
// 通过record数据记录 magazine里各个字符出现次数
record[magazine[i]-'a'] ++;
}
for (int j = 0; j < ransomNote.length(); j++) {
// 遍历ransomNote,在record里对应的字符个数做--操作
record[ransomNote[j]-'a']--;
// 如果小于零说明ransomNote里出现的字符,magazine没有
if(record[ransomNote[j]-'a'] < 0) {
return false;
}
}
return true;
}
};
三数之和
即在一个数组中,找到下标互不重复且三个值不重复的所有三数之和为零的三元组
看起来与[[两数之和]]相近,可以把前两数之和放 map,最后一个数匹配,但由于题目要求返回不重三元组,所以去重将是大难题
所以本题采用双指针,先把数组排好序,用 i 确认第一个数 a,再用left 和 right 从 a 的右侧两旁逼近答案
思路不难,但去重难
首先是 a 的去重,由于排好序了,只要让 a 与前一个值不同,就可以遍历到不同的 a;那么可以让 a 与后面一个不同吗?不能,因为可能有{1,1,-2} 这样的情况,若让 a 与后一个不同,那么由于前两个 1,该答案会被排除。
然后是 b 和 c 的去重,由于排好序,在取到一个可用值后,让 left 和 right 各向中心去重
参考代码随想录思路的解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int size = nums.size();
vector<vector<int>> result;
sort(nums.begin(), nums.end());// 先排好序
for (int i = 0; i < size; i++) {
// 若最小的数都为正数,那么三数之和不可能为零
if (nums[i] > 0) {
break;
}
// 对第一个 a 的去重,若前一个与当前相等,那么有三数之和为零的话一定重用,没的话三数之和定不为零
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 双指针遍历
int left = i+1, right = size - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
// 若三数之和小了,让 left 变大
if (sum < 0) {
left++;
}
// 大了,就让 right 变小
else if(sum > 0) {
right--;
}
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 对于 left 和 right 的去重
while (right > left && nums[left] == nums[left + 1]) {
left++;
}
while(right > left && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
return result;
}
};
四数之和
- 代码随想录 (programmercarl.com)
- 本地算[[三数之和]]的拓展,习得这个拓展套路,便可解决类似的“五数之和”等
- 与 [[四数相加||]] 不同,该题要在与 [[三数之和]] 的一个数组下,找到四数之和为 target 的四元组
- 注意 target 可能为负,所以剪枝不能大于target就剪就差不多了罢
- 参考代码随想录思路的解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end()); // 确保数组有序
int size = nums.size();
for (int k = 0; k < size; k++) { // 第一个数
if (nums[k] >= 0 && nums[k] > target) { //第一层剪枝,若往后只会更大则剪
break;
}
if (k > 0 && nums[k-1] == nums[k]) { // 第一层去重
continue;
}
for (int i = k + 1; i < size; i++) {
int i_k_sum = nums[i] + nums[k];
if (i_k_sum >= 0 && i_k_sum > target) { // 第二层剪枝
break;
}
if (i > k + 1 && nums[i - 1] == nums[i]) { //第二层去重
continue;
}
int left = i + 1, right = size - 1;
while (left < right) {
if ((long long)i_k_sum + nums[left] + nums[right] > target) {
right--;
}
else if((long long)i_k_sum + nums[left] + nums[right] < target) {
left++;
}
else {
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
while(left < right && nums[left] == nums[left+1]) {
left++;
}
while(left < right && nums[right] == nums[right-1]) {
right--;
}
left++;
right--;
}
}
}
}
return result;
}
};