哈希表
基础知识
哈希表常用于快速判断一个元素是否在集合中,空间换时间
哈希表是根据key
(如数组的索引下标)直接进行访问的数据结构
哈希函数:将key
映射到哈希表上的索引 index = hashFunction(key) = (hashCode(key) % tableSize) mod tableSize
hashCode()
通过特定编码方式把key
转化为数值
哈希碰撞:不同的key
引射到了相同的下标,解决方法:拉链法 和 线性探测法
-
拉链法:将发生冲突的元素存储在链表中,不需要tableSize大于dataSize
-
线性探测法:向下找空位,需要tableSize一定大于dataSize
常见的哈希结构
- 数组
常用于key
的数值十分受限,密度高的情况,相比起set
时间和空间复杂度低(set
把数值映射到key
上要做hash计算)
-
set(集合)
常用于
key
较少、分散且数值跨度大的情况(使用数组会造成空间的极大浪费)
一般来说,当要使用集合来解决哈希问题的时候,优先使用unordered_set
,因为它的查询和增删效率是最优的;如果需要集合是有序的则用set
;如果要求不仅有序还要有重复数据则用multiset
-
map(映射)
map
是一个key-value
的数据结构
map
中对key
是有限制,而对value
没有限制,因为key
的存储方式是用红黑树实现的
hash_set
和hash_map
与unordered_set
和unordered_map
的关系:功能相同, 但
unordered_set
在C++11
被引入标准库,因此建议使用unordered_set
数组
常用于key
的数值十分受限,密度高的情况,相比起set
时间和空间复杂度低(set
把数值映射到key
上要做hash计算)
242# 有效的字母异位词
给定两个字符串
s
和t
,编写一个函数来判断t
是否是s
的 字母异位词。字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次。
示例 1:
输入: s = "anagram", t = "nagaram" 输出: true
示例 2:
输入: s = "rat", t = "car" 输出: false
提示:
1 <= s.length, t.length <= 5 * 104
s
和t
仅包含小写字母
由于字符串只有小写字母,因此定义一个长度为26的数组record
记录字符串中字符出现的次数
// 哈希数组
// O(n) 0ms; O(1) 9.51MB
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 i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) return false;
}
return true;
}
};
空间复杂度:一个常量大小的辅助数组O(26),即 O(1) 或 O(S)
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
提示:
1 <= ransomNote.length, magazine.length <= 105
ransomNote
和magazine
由小写英文字母组成
// 哈希数组
// O(n) 0ms; O(1) 11.46MB
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
if (ransomNote.size() > magazine.size()) return false;
for (int i = 0; i < magazine.size(); i++) record[magazine[i] - 'a']++;
for (int i = 0; i < ransomNote.size(); i++) record[ransomNote[i] - 'a']--;
for (int i = 0; i < 26; i++) {
if (record[i] < 0) return false;
}
return true;
}
};
Set
常用于key
较少、分散且数值跨度大的情况(使用数组会造成空间的极大浪费)
基础语句
// 创建
unordered_set<int> hashset;
unordered_set<int> hashset(vector.begin(), vector.end()); // 范围构造函数,参数为两个迭代器,将vector的所有元素插入hashset中,遍历O(n),插入所有元素到set的时间复杂度为O(n)~O(n^2)(哈希冲突严重)
// 查询
unordered_set.find(key) // O(1) 如果key存在,返回一个指向该元素的迭代器;不存在返回特殊值unordered_set.end()
// 插入
unordered_set.insert(key)
// 删除
unordered_set.erase(key)
// 长度
unordered_set.size()
// 桶数量
unordered_set.bucket_count() // unordered_set会动态扩容(rehashing)
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] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
唯一且无序,选择unordered_set
先将nums1
转化为unordered_set
,再取unordered_set
与nums2
的交集
// unordered_set
// O(n+m) 8ms; O(n) 14.43MB
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> nums1_set(nums1.begin(), nums1.end());
unordered_set<int> result_set; // 使用set为最终结果去重
for (int num : nums2) {
if (nums1_set.find(num) != nums1_set.end()) result_set.insert(num);
}
return vector<int>(result_set.begin(), result_set.end());
}
};
由于本题力扣对数组的数值进行了大幅限制,因此也可使用哈希数组
// 哈希数组 & unordered_set
// O(n+m) 4ms; O(n) 14.58MB
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
int hash[1001] = {0};
for (int num : nums1) {
hash[num] = 1;
}
for (int num : nums2) {
if (hash[num] == 1) 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
提示:
1 <= n <= 2^31 - 1
该题的突破点在于无限循环(结果会收敛到1 或 无限循环)
无限循环的原因:对于一个非负整数,其每位平方和范围有限(如三位数的每位平方和最大为243)
// unordered_set
// O(logn) 0ms; O(logn) 8.25MB
class Solution {
public:
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> hashset;
while (n != 1) {
if (hashset.find(n) != hashset.end()) return false;
hashset.insert(n);
n = getSum(n);
}
return true;
}
};
用集合记录可能导致集合过大,递归的层次较深也会导致调用栈崩溃
另一种方法是快慢指针法:
每位平方和的过程是一个隐式链表,则转换为链表中是否有环的问题,fast
走两步,slow
走一步(相对速度为1),二者相等即存在环
由于该题的特殊性——为1时1自身形成环,因此只需判断fast
和slow
相遇时(环中)是否为1
// 快慢指针法
// O(logn) 0ms; O(1) 7.6MB
class Solution {
public:
int bitSquareSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
int slow = n, fast = n;
do {
slow = bitSquareSum(slow);
fast = bitSquareSum(bitSquareSum(fast));
} while (slow != fast);
return slow == 1;
}
};
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 。
提示:
3 <= nums.length <= 3000
-10^5 <= nums[i] <= 10^5
三元组不可重复(三元组内的元素可以重复),而最后再对三元组去重非常耗时,在寻找三元组的过程中去重(部分借助set
)
// unordered_set
// O(n^2) 1897ms; O(n) 437.43MB
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
int length = nums.size();
sort(nums.begin(), nums.end()); // 排序简化问题
for (int i = 0; i < length - 2; i++) {
if (nums[i] > 0) break;
if(i > 0 && nums[i] == nums[i - 1]) continue; // 去重a
unordered_set<int> hashset; // 存储去重的b,由于b使用后擦除,因此在b!=c时做到去重b和c
for (int k = i + 1; k < length; k++) {
if (k > i + 2 && nums[k] == nums[k - 1] && nums[k - 1] == nums[k - 2])
continue; // 去重b=c时的b和c
int target = 0 - (nums[i] + nums[k]);
if (hashset.find(target) != hashset.end()) {
result.push_back({nums[i], target, nums[k]}); // nums[k]为c
hashset.erase(target);
} else {
hashset.insert(nums[k]); // nums[k]为b
}
}
}
return result;
}
};
双指针法:将寻找b
和c
的过程转换为双指针查找
两数之和需要返回索引下标(三数之和不需要),而双指针法一定要排序,因此在两数之和中不使用双指针法
// 双指针法
// O(n^2) 55ms; O(1) 28.47MB
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
int n = nums.size();
for (int i = 0; i < n - 2; i++) {
if (nums[i] > 0) break;
if(i > 0 && nums[i] == nums[i - 1]) continue; // 去重a
int left = i + 1, right = n - 1;
while (left < right) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--; // 这里无论去不去重right依旧--,因此不需要去重
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
} else {
result.push_back({nums[i], nums[left], nums[right]});
left++;
right--;
// 去重b和c
while (left < right && nums[left] == nums[left - 1]) left++;
while (left < right && nums[right] == nums[right + 1]) right--;
}
}
}
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]]
提示:
1 <= nums.length <= 200
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
// 双指针法
// O(n^3) 27ms; O(1) 17.18MB
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
int n = nums.size();
for (int i = 0; i < n - 3; i++) {
if (nums[i] > target && nums[i] >= 0) break; // nums[i]>=0保证四数之和不会减小
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重a
for (int j = i + 1; j < n - 2; j++) {
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;
if (j > i + 1 && nums[j] == nums[j - 1]) continue; // 去重b
int left = j + 1, right = n - 1;
while(left < right) {
// nums[i] + nums[j] + nums[left] + nums[right] 会溢出
if ((long) nums[i] + nums[j] + nums[left] + nums[right] < target) {
left++;
} else if ((long) nums[i] + nums[j] + nums[left] + nums[right] > target) {
right--;
} else {
result.push_back({nums[i], nums[j], nums[left], nums[right]});
left++;
right--;
// 去重c和d
while (left < right && nums[left] == nums[left - 1]) left++;
while (left < right && nums[right] == nums[right + 1]) right--;
}
}
}
}
return result;
}
};
Map
基础语句
// 创建
unordered_map<int, int> hashmap;
// 查询
auto iter = unordered_map.find(key); // O(1),不存在返回 unordered_map.end()
iter->first // 查key
iter->second // 查value
auto iter = M.begin(); // 第一个元素
iter+1 // 下一个元素
// 插入
unordered_map[key] = value;
unordered_map.insert(pair<int, int>(key, value));
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]
提示:
2 <= nums.length <= 104
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
- 只会存在一个有效答案
需要查询元素的同时,还需要元素对应的下标,因此用map
,key
可以无需,因此用unordered_map
{key:数据元素,value:对应下标}
// unordered_map
// O(n) 3ms; O(n) 14.59MB
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hashmap;
for (int i = 0; i < nums.size(); i++) {
auto iter = hashmap.find(target - nums[i]);
if (iter != hashmap.end()) return {iter->second, i};
hashmap[nums[i]] = i;
// hashmap.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
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
示例 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
提示:
n == nums1.length
n == nums2.length
n == nums3.length
n == nums4.length
1 <= n <= 200
-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228
对问题拆分为两个两数相加,使用unordered_map
,key
记录两数之和,value
记录两数之和出现的次数(求次数)
# unordered_map
# O(n^2) 130ms; O(n^2) 27.71MB
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> hashmap;
int count = 0;
for (int num1 : nums1) {
for (int num2 : nums2) {
hashmap[num1 + num2]++;
}
}
for (int num3 : nums3) {
for (int num4 : nums4) {
int diff = 0 - num3 - num4;
if (hashmap.find(diff) != hashmap.end()) count += hashmap[diff];
}
}
return count;
}
};
本文参考了 LeetCode官方题解 及 代码随想录