概述
前言
一刷代码随想录,今天的内容是 哈希表。所有题目均源自LeetCode。
欢迎大家在评论区留言发表自己的看法,如文章有不妥之处,请批评指正。
文章目录
- 前言
- 一、哈希表简介
- 二、练习题目
- 三、源码剖析
- 总结
一、哈希表简介
哈希表利用空间换时间,将索引查找时间降到 O(1)
,常见的哈希表结构有三种:数组、set(集合)、map(映射)。
有关哈希表具体的基础理论知识,请看卡哥的讲解 哈希表理论基础。
二、练习题目
- 242. 有效的字母异位词
- 383. 赎金信
- 349. 两个数组的交集
- 202. 快乐数
- 1. 两数之和
- 454. 四数相加 II
- 15. 三数之和
- 18. 四数之和
三、源码剖析
1、242. 有效的字母异位词|★☆☆☆☆
第一版:哈希表,利用数组表示哈希表
时间复杂度:O(n)
,空间复杂度:O(1)
class Solution {
public:
bool isAnagram(string s, string t) {
// 哈希表,利用数组表示哈希表
// 如果是字母异位词,则s和t的长度肯定是相等的,不等的直接返回false
int lenS = s.size(), lenT = t.size();
if (lenS != lenT) return false;
// 26个英文字母,数组长度>=26即可,一定需要初始化
int hash[26] = {0};
// 使用memset初始化也可以
// memset(hash, 0, sizeof(hash));
for (int i = 0; i < lenS; ++i) {
// 将s的元素放入,'a'的放入位置为下标0处, 减'a'是为了求出其他字符的放入位置
++hash[s[i] - 'a'];
}
for (int i = 0; i < lenT; ++i) {
--hash[t[i] - 'a'];
// t中字母在s中没有出现,s中某个字母比t中多的情况也包含在此
// 因为s和t的长度一致,s中某字母比t中多n个,则t中就有n个其他字母
// 这n个其他字母最终会造成 hash[t[i] - 'a'] < 0 的结果。
if (hash[t[i] - 'a'] < 0) return false;
}
return true;
}
};
2、383. 赎金信|★☆☆☆☆
第一版:数组哈希表
时间复杂度:O(n)
,空间复杂度:O(1)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
// 哈希表
// 首先, magazine.size() >= ransomNote.size()
// 然后,第一遍遍历将 magazine 映射到哈希表中统计每个字符出现的次数
// 最后,第二遍遍历时将 ransomNote 的每个字符在 hash 表中减去次数
// 如果造成了 hash[i]<0的情况,则false
int lenR = ransomNote.size(), lenM = magazine.size();
if (lenR > lenM) return false;
int hash[26] = {0};
for (int i = 0; i < lenM; ++i) {
++hash[magazine[i] - 'a'];
}
for (int i = 0; i < lenR; ++i) {
--hash[ransomNote[i] - 'a'];
if (hash[ransomNote[i] - 'a'] < 0) return false;
}
return true;
}
};
3、349. 两个数组的交集|★☆☆☆☆
第一版:哈希表
时间复杂度:O(n)
,空间复杂度:O(1)
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
// 哈希表
// 数组的交集,就是两个数组中将相同元素提取出来组成一个新的集合
// 输出结果中的每个元素一定是 唯一 的,即只要元素相同即可,不考虑个数
int len1 = nums1.size(), len2 = nums2.size();
// 数组长度>=元素的种类,多一点可以避免有时候算错了造成越界
int hash[1010] = {0};
vector<int> ret;
for (int i = 0; i < len1; ++i) {
hash[nums1[i]] = 1;
}
for (int i = 0; i < len2; ++i) {
if (hash[nums2[i]]-- > 0) ret.push_back(nums2[i]);
}
return ret;
}
};
4、202. 快乐数|★★☆☆☆
第一版:不知道哈希表怎么使用,看了一下题解提示
class Solution {
public:
bool isHappy(int n) {
// 看到示例,首先想到的是模拟,如果闷头去模拟,可能会陷入无限循环
// 无限循环,就是平方和出现了重复的情况,只要有一次重复出现,则后面均会重复,陷入循环
// 那么就可以考虑使用hash表来记录是否无限循环了
unordered_set<int> set;
while (n != 1) {
// 遇到重复
if (set.find(n) != set.end()) return false;
set.insert(n);
int temp = n;
n = 0;
while (temp > 0) {
n += pow(temp % 10, 2);
temp /= 10;
}
}
return true;
}
};
5、1. 两数之和|★☆☆☆☆
第一版:map哈希表
时间复杂度:O(n)
,空间复杂度:O(n)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 哈希表
// map的key存nums[i],value存下标
std::unordered_map<int, int> map;
vector<int> ret;
for (int i = 0; i < nums.size(); ++i) {
// 当前hash表中有target - nums[i]的数,即找到了两个整数相加等于target
if (map.find(target - nums[i]) != map.end()) {
// 存入另一个数的下标
ret.push_back(map[target - nums[i]]);
// 存入当前数的下标
ret.push_back(i);
return ret;
}
map[nums[i]] = i;
}
return ret;
}
};
第二版:优化第一版代码,看了题解
时间复杂度:O(n)
,空间复杂度:O(n)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 优化第一版代码
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); ++i) {
auto iter = map.find(target - nums[i]);
if (iter != map.end()) {
return {iter->second, i};
}
map[nums[i]] = i;
}
return {};
}
};
6、454. 四数相加 II|★★☆☆☆
第一版:结果错误。
原因:没想清楚 a+b 和 -(c+d) 的个数关系,在这里是hash表中出现一次 -(c+d) 后就算一次,而且将hash表中的个数都-1了,是错误的做法。其实画画图就能知道,一个 -(c+d) 可以将上面的全部 a+b 都组成元组,因此出现一次 -(c+d) 就需要将hash表中 a+b 的个数累加起来。
第二版:看了题解,解决了第一版的问题
时间复杂度:O(n^2)
,空间复杂度:O(n)
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
// 类比两数之和,使用哈希表
// 两个for循环,将a+b算出来,并且存入哈希表,key存a+b的值,value存a+b的值出现的次数
// 然后再两个for循环,计算c+d,每计算一次就看-(c+d)的值是否在hash表中,如果在,则ret += map[-(c+d)]
//(其实画画图就能知道,一个 -(c+d) 可以将上面的全部 a+b 都组成元组,因此出现一次 -(c+d) 就需要将hash表中 a+b 的个数累加起来)
int ret = 0;
unordered_map<int, int> map;
for (int a : nums1) {
for (int b : nums2) {
++map[a + b];
}
}
for (int c : nums3) {
for (int d : nums4) {
int sum = 0 - (c+d);
if (map[sum] > 0) {
ret += map[sum];
}
}
}
return ret;
}
};
7、15. 三数之和|★★☆☆☆
第一版:哈希表,主要在于怎么去重,没写出来,一直报错
第二版:双指针,去重也是难点,b、c去重部分还是看了题解
时间复杂度:O(n^2)
,空间复杂度:O(1)
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 双指针加一个固定指针
// 先排序
// 再固定指针i,双指针j(left)和k(right),j=i+1,k=nums.size() - 1
// 每一次遍历,首先固定i不动,然后j和k向中间移动,每次都计算 nums[i]+nums[j]+nums[k]的值
// 如果大于0,则k向左移动;如果小于0,则j向右移动;
// 如果等于0,则将nums[i]、nums[j]、nums[k]存入ret,j和k都移动
// 同时,在j,k移动时,如果移动后指向的元素与先前元素相等,则继续移动j,k
// 最后,如果nums[i]>0,则直接break;否则遍历到末尾结束
vector<vector<int>> ret;
int n = nums.size();
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1, right = n - 1;
int a = nums[i];
// 不能等,因为 j != k
while (left < right) {
int b = nums[left], c = nums[right];
if (a + b + c == 0) {
ret.push_back({a, b, c});
--right;
++left;
while (left < right && nums[right] == nums[right + 1]) {
--right;
}
while (left < right && nums[left] == nums[left - 1]) {
++left;
}
}else if (a + b + c > 0) { // 向左移动k
--right;
}else { // 向右移动j
++left;
}
}
}
return ret;
}
};
8、18. 四数之和|★★★☆☆
第一版:看了题解,然后自己写的时候去重部分还是有问题
时间复杂度:O(n^3)
,空间复杂度:O(1)
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
// 双指针,实际是4指针,但是每次固定两个指针
// 方法与三数之和一样,只是在剪枝的时候要注意
// 五数之和、六数之和等都是一样的用双指针
vector<vector<int>> ret;
sort(nums.begin(), nums.end());
int n = nums.size();
for (int a = 0; a < n; ++a) {
// 1级剪枝
// 与三数之和有点区别,因为这里target不确定,如果为负数则直接nums[a] > target就会漏掉结果,如[-4,-3,-2,1,0],target = -9
// 因为nums[a]是四个数中最小的,当nums[a] >= 0时,nums[a] > target后面就一定不会有满足条件的了,
if (nums[a] > target && nums[a] >= 0) {
break;
}
// nums[a] 去重
if (a > 0 && nums[a] == nums[a - 1]) { // a>0是去掉a==0的边界情况
continue;
}
for (int b = a + 1; b < n; ++b) {
// 2级剪枝
if (nums[a] + nums[b] > target && nums[a] + nums[b] >= 0) {
break;
}
// nums[b] 去重
if (b > a + 1 && nums[b] == nums[b - 1]) {
continue;
}
int left = b + 1, right = n - 1;
while (left < right) {
// 分析数据大小,最大可能为4个 10^9 相加,用int就溢出了
if ((long) nums[a] + nums[b] + nums[left] + nums[right] > target) {
--right;
}else if ((long) nums[a] + nums[b] + nums[left] + nums[right] < target) {
++left;
}else {
ret.push_back({nums[a], nums[b], nums[left], nums[right]});
++left;
--right;
// nums[left] 去重
while (left < right && nums[left] == nums[left - 1]) ++left;
// nums[right] 去重
while (left < right && nums[right] == nums[right + 1]) --right;
}
}
}
}
return ret;
}
};
总结
(
1
)
(1)
(1) 哈希表常在 数组 中使用,字符串也是一种数组。
(
2
)
(2)
(2) 一道题能使用哈希表的话,首先分析一下数据量,能使用数组 arr[length]
做哈希表就尽量使用数组做哈希表,要比容器 map、set
更简单有效。
(
3
)
(3)
(3) C++常用的哈希表容器:unordered_map/multimap、unordered_set/multimap
,这四个的底层就是用的 hash table。
(
4
)
(4)
(4) 三数之和、四数之和,如果还有五数之和、六数之和……,他们的思路都是一样的,只是需要注意 剪枝、去重、相加后是否会溢出 三点。
最后
以上就是细腻香氛为你收集整理的《代码随想录》学习笔记:哈希表合集前言一、哈希表简介二、练习题目三、源码剖析总结的全部内容,希望文章能够帮你解决《代码随想录》学习笔记:哈希表合集前言一、哈希表简介二、练习题目三、源码剖析总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复