1 理论基础
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
数组
set (集合)
map(映射)
这里数组就没啥可说的了,我们来看一下set。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
2 力扣242. 有效的字母异位词
题目描述:
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
class Solution {
public:
bool isAnagram(string s, string t) {
unordered_map<char, int> map;
if (s.size() != t.size()) return false;
for (int i = 0; i < s.size(); i++) {
++map[s[i]];
--map[t[i]];
}
for (unordered_map<char, int>::iterator it = map.begin(); it != map.end(); it++) {
if (it->second != 0) return false;
}
return true;
}
};
3 力扣349.两个数组的交集
题目描述:
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> set1(nums1.begin(), nums1.end());
for (int num : nums2) {
if (set1.find(num) != set1.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
4 力扣1.两数之和
题目描述:
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int>m;
for (int i = 0; i < nums.size(); ++i) {
m.insert(pair<int, int>(nums[i], i));
}
for (int i = 0; i < nums.size(); ++i) {
auto it = m.find(target - nums[i]);
if (it != m.end() && it->second != i) return{ it->second,i };
}
return{};
}
};
5 力扣454.四数相加II
题目描述:
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> m;
for (int a : A) for (int b : B) m[a + b]++;
int count = 0;
for (int c : C) for (int d : D)
if (m.find(0 - (c + d)) !=m.end())
count += m[0 - (c + d)];
return count;
}
};
6 力扣15. 三数之和
题目描述:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
双指针:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
if (nums[0] > 0 || nums[nums.size() - 1] < 0) return {};
vector<vector<int>> result;
for (int i = 0; i < nums.size(); ++i) {
//-2 -2 -2 4 ; -2 -2 -1 3
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else { result.push_back(vector<int>{nums[i], nums[left], nums[right]});
//-4 2 2 2 可省略
if (nums[left] == nums[right]) break;
//-2 -1 3 3
while (right > left && nums[right] == nums[right - 1]) right--;
//-2 -1 -1 3
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
};
其中五种可能的重复情况:
对i去重:
if (i > 0 && nums[i] == nums[i - 1]) continue;
-2 [-2] -2 ... 4
-2 [-2] -1 ... 3
i l r
P.S. []为i去重后的起始位置
对l去重:
while (right > left && nums[left] == nums[left + 1]) left++;
-2 -1 [-1] ... 3
i l r
然后再 left++;
对r去重:
while (right > left && nums[right] == nums[right - 1]) right--;
-2 -1 ... [3] 3
i l r
然后再 right--;
对l和r去重:(可省略)
if (nums[left] == nums[right]) break;
-4 2 2 2
i l r
既然nums[l]==nums[r],那么这个i已经被“充分利用”
否则会存在-4 1 2 2 2 3中的-4+1+3的情况,l更左,r更右,i未“充分利用”
哈希法:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
7 力扣18. 四数之和
题目描述:
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {
if (nums[k] > target && (nums[k] >= 0 || target >= 0)) break;
//排除nums元素和target均负,且nums[k]<=target
if (k > 0 && nums[k] == nums[k - 1]) continue;
for (int i = k + 1; i < nums.size(); i++) {
if (nums[k] + nums[i] > target && (nums[k] + nums[i] >= 0 || target >= 0))
break;
if (i > k + 1 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
if (nums[k] + nums[i] > target - (nums[left] + nums[right])) {
right--;
while (left < right && nums[right] == nums[right + 1]) right--;
}
else if (nums[k] + nums[i] < target - (nums[left] + nums[right])) {
left++;
while (left < right && nums[left] == nums[left - 1]) left++;
}
else {
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
}
return result;
}
};
8 力扣383. 赎金信
题目描述:
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int re[26] = { 0 };
for (int i = 0; i < magazine.length(); i++) re[magazine[i] - 'a'] ++;
for (int j = 0; j < ransomNote.length(); j++) {
re[ransomNote[j] - 'a']--;
if (re[ransomNote[j] - 'a'] < 0) return false;
}
return true;
}
};
9 力扣202.快乐数
题目描述:
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
class Solution {
public:
bool isHappy(int n) {
unordered_set<int>s;
while (1) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
if (sum == 1)return true;
if (s.find(sum)!=s.end())return false;
else s.insert(sum);
n = sum;
}
}
};
与力扣18.四数之和的哈希法类似