哈希表基础
主要的三个哈希结构
-
数组:存放的数据较少时可用数组结构
-
set
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
| std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n)|
| std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
| std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
- map
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
| 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) |
当遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
经典例题
有效的字母异位词(242)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
思路
判断字母是否出现过,用哈希法,英文字母一共就26个,所以可以使用数组来做,要判断次数,将26个英文字母与数组相映射,每个元素对应一个字母,出现一次,相应字母对应的元素位置就加一,判断第二个字符串时,相应字母的元素减一,若数组每个位置的元素都为0,则两个字符串出现的字母次数都相同。 代码
#include <string>
using namespace std;
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++)
{
record[s[i] - 'a']++;
}
for (int j = 0; j < t.size(); j++)
{
record[t[j] - 'a']--;
}
for (int k = 0; k < 26; k++)
{
if (record[k] != 0)
{
return false;
}
}
return true;
}
};
两个数组的交集(349)
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是唯一 的。我们可以不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
思路
这道题看有没有交集,有没有出现过,用哈希法,这道题并没有限制大小,所以不用数组,用set,题目要求元素唯一,所以要去重,并且可以无序,所以使用unordered_set。
代码
#include <vector>
#include <unordered_set>
using namespace std;
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2)
{
// 找到了返回该元素迭代器, 没找到返回end()迭代器
if (nums_set.find(num) != nums_set.end())
{
result_set.insert(num);
}
}
return vector<int> (result_set.begin(), result_set.end());
}
};
快乐数(202)
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
示例 2:
输入:n = 2
输出:false
思路
如果一个数是快乐数的话,那么sum就不会重复,如果sum重复,就意味着将陷入无限循环,也就意味着该数不是快乐数,所以可以用unordered_set存放sum的结果,若是结果与集合中的重复,那么就返回false。
代码
#include <unordered_set>
using namespace std;
class Solution
{
public:
bool isHappy(int n)
{
unordered_set<int> result;
while (1)
{
int sum = getSum(n); //计算和
if (sum == 1) //sum为1, 是快乐数
{
return true;
}
if (result.find(sum) != result.end()) //在sum集合中找到了一样的,进入死循环,不是快乐数
{
return false;
}
else //将新的sum加入到集合中
{
result.insert(sum);
}
n = sum; //更新n
}
}
int getSum(int n) //计算sum
{
int sum = 0;
while (n)
{
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
};
两数之和(1)
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
思路
要找两数之和,先将遍历的一个数保存下来,然后看当前值能不能找到与遍历过的数相加等于target,但是最后要返回的是下标,所以这里使用键值对来存储元素,题目要求不能重复出现,任意顺序返回,所以可以使用unordered_map来保存元素。
代码
#include <unordered_map>
#include <vector>
using namespace std;
class Solution
{
public:
vector<int> twoSum(vector<int> &nums, int target)
{
unordered_map<int, int> result;
for (int i = 0; i < nums.size(); i++)
{
auto iter = result.find(target - nums[i]);
if (iter != result.end())
{
return {iter->second, i}; //return vector
}
else
{
result.insert(make_pair(nums[i], i));
}
}
return {};
}
};
四数相加II(454)
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1
思路
这道题返回的是满足条件的有多少个数组,所以可以用unordered_map,先遍历nums1和nums2,将两个数组和保存到map中,然后再遍历nums3和nums4,找到能使和map中的数相加等于0的组合。
代码
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> umap;
for (int a : nums1)
{
for (int b : nums2)
{
umap[a + b]++;
}
}
int count = 0;
for (int c : nums3)
{
for (int d : nums4)
{
if (umap.find((0 - c - d)) != umap.end())
{
count += umap[0 - c - d];
}
}
}
return count;
}
};
赎金信(383)
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入:ransomNote = "a", magazine = "b"
输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab"
输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab"
输出:true
都是小写字母
思路
这道题是看字符串A能不能由字符串B构成,小写字母一共就26位,所以可以用数组节省空间,如果字符串A比字符串B还要长那么就直接return false,创建一个数组record用来保存字母的出现次数,先记录B的每个字符的出现次数,然后再减掉A每个字符出现的次数,若A所对应的字符小于0,则说明A不能由B组成。 代码
#include <iostream>
#include <string>
using namespace std;
class Solution
{
public:
bool canConstruct(string ransomNote, string magazine)
{
int record[26] = {0};
if (ransomNote.size() > magazine.size()) //A比B长,不可能
{
return false;
}
for (int i = 0; i < magazine.size(); i++) //记录B出现次数
{
record[magazine[i] - 'a']++;
}
for (int j = 0; j < ransomNote.size(); j++)
{
record[ransomNote[j] - 'a']--;
if (record[ransomNote[j] - 'a'] < 0) //A的某个字符出现次数比B的多
{
return false;
}
}
return true;
}
};
三数之和(15)
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
思路
题目要求不能包含重复的三元组,所以得要有去重的操作,这道题目是在同一个数组nums中找到三个数使得三数之和为0,所以可以设置三个指针分别代表三个数a,b,c在数组上滑动。首先将数组排序,将a从头开始遍历,b为a+1,c在最后位置上,若nums[a]+nums[b]+nums[c]>0,则说明要变小,所以c向做移动,若<0,则说明要变大,b向右移动,直到b和c相遇。
代码
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
vector<vector<int>> threeSum(vector<int> &nums)
{
vector<vector<int>> result;
sort(nums.begin(), nums.end()); //排序
for (int a = 0; a < nums.size(); a++)
{
if (nums[a] > 0) //若第一个元素就比0大那么怎么也不可能三数之和为0
{
return result;
}
//对a进行去重,若判断a和a+1,则相当于去除掉了三元组内重复的情况 EX[0, 0, 0]
if (a > 0 && nums[a] == nums[a - 1])
{
continue;
}
int b = a + 1;
int c = nums.size() - 1;
while (b < c)
{
if (nums[a] + nums[b] + nums[c] > 0)
{
c--;
}
else if (nums[a] + nums[b] + nums[c] < 0)
{
b++;
}
else
{
result.push_back(vector<int> {nums[a], nums[b], nums[c]});
while (b < c && nums[b] == nums[b + 1]) //b去重
{
b++;
}
while (b < c && nums[c] == nums[c - 1]) //c去重
{
c--;9
}
b++;
c--;
}
}
}
return result;
}
};
四数之和(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
- 你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
思路
这道题和之前的三数之和还不太一样,之前的是要找到一个三元组使得和为0,这道题是找到四元组和为target,其为不确定的值,所以这里不能直接判断当数组的第一个数大于target就直接pass,逻辑可以改为:num[i] > target && (target > 0 || nums[i] > 0)。在用双指针遍历数组时,先要用两层for循环确定 nums[k] + nums[i] 的值,循环内有双指针left和right,找到nums[k] + nums[i] + nums[left] + nums[right] == target。时间复杂度O(n3)
代码
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end()); //排序
for (int a = 0; a < nums.size(); a++) //遍历第一个数
{
if (nums[a] > target && nums[a] >= 0)
{
break;
}
if (a > 0 && nums[a] == nums[a - 1]) //对a去重
{
continue;
}
for (int b = a + 1; b < nums.size(); b++) //遍历第二个数
{
if (nums[a] + nums[b] > target && nums[a] + nums[b] >= 0)
{
break;
}
if (b > a + 1 && nums[b] == nums[b - 1]) //对b去重
{
continue;
}
//定义双指针
int left = b + 1;
int right = nums.size() - 1;
while (left < right)
{
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
{
result.push_back(vector<int>{nums[a], nums[b], nums[left], nums[right]});
while(right > left && nums[left] == nums[left + 1]) //对c去重
{
left++;
}
while (right > left && nums[right] == nums[right - 1]) //对d去重
{
right--;
}
left++;
right--;
}
}
}
}
return result;
}
};