哈希表理论知识
散列表,Hash Table,哈希表是根据关键码的值而直接进行访问的数据结构。
数组就是一张哈希表,哈希表中关键码就是数组的索引下标,通过下标直接访问数组中的元素。
哈希表一般用来快速判断一个元素是否出现在集合里。
经典题目
242.有效的字母异位词
题目描述
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是s的字母异位词。
示例 1: 输入: s = “anagram”, t = “nagaram” 输出: true
示例 2: 输入: s = “rat”, t = “car” 输出: false
说明: 你可以假设字符串只包含小写字母。
思路
定义一个数组来记录字符串里字符的出现次数,需要把字符映射到数组也就是哈希表的索引下标上,遍历字符串s的时候,使用hash[ s[i] - ‘a’ ]++,检查字符是否在字符串t中出现,遍历字符串t的时候,hash[ t[i] - ‘a’ ]–就可以。最后,再遍历哈希表,如果出现hash[i]是不为零,就说明,s和t中的字母是不相等的,返回false,否则就是字母异位词,就返回true。
C++代码
讲解视频:
学透哈希表,数组使用有技巧!Leetcode:242.有效的字母异位词
代码实现如下:
class Solution {
public:
bool isAnagram(string s, string t) {
int hash[26] = {0};
for(int i = 0; i < s.size(); i++){
hash[s[i] - 'a']++;
}
for(int i = 0; i < t.size(); i++){
hash[t[i] - 'a']--;
}
for(int i = 0; i < 26; i++){
if(hash[i] != 0) return false;
}
return true;
}
};
349.两个数组的交集
题目描述
给定两个数组nums1和nums2,返回 它们的交集 。输出结果中的每个元素一定是唯一的。我们可以 不考虑输出结果的顺序。
思路
首先定义一个哈希表,对nums1进行处理,然后,遍历nums2的每一个元素
本题可以使用set解决,力扣上的题也可以用数组解决
C++代码
1、数组解决:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result;
int hash[1005] = {0};
for(int i = 0; i < nums1.size(); i++)
hash[nums1[i]]=1;
for(int i = 0; i < nums2.size(); i++){
if(hash[nums2[i]] == 1) result.insert(nums2[i]);
}
return vector<int>(result.begin(),result.end());
}
};
2、set解决:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
350.两个数组的交集II
题目描述
力扣题目链接
给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
力扣官方给出的思路
由于同一个数字在两个数组中都可能出现多次,因此需要用哈希表存储每个数字出现的次数。对于一个数字,其在交集中出现的次数等于该数字在两个数组中出现次数的最小值。
首先遍历第一个数组,并在哈希表中记录第一个数组中的每个数字以及对应出现的次数,然后遍历第二个数组,对于第二个数组中的每个数字,如果在哈希表中存在这个数字,则将该数字添加到答案,并减少哈希表中该数字出现的次数。
为了降低空间复杂度,首先遍历较短的数组并在哈希表中记录每个数字以及对应出现的次数,然后遍历较长的数组得到交集。
C++代码
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int,int> map;
vector<int> result;
for(int i = 0; i < nums1.size(); i++){
map[nums1[i]]++;
}
for(int j = 0; j < nums2.size(); j++){
if(map[nums2[j]] >= 1){
result.push_back(nums2[j]);
map[nums2[j]]--;
}
}
return result;
}
};
202.快乐数
题目描述
编写一个算法来判断一个数n是不是快乐数。
「快乐数」定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
思路
代码随想录给出的思路:
https://programmercarl.com/0202.%E5%BF%AB%E4%B9%90%E6%95%B0.html#%E6%80%9D%E8%B7%AF
求每位数字平方之和(写成一个函数),哈希表存储sum,若sum在表中出现过,说明进入循环了,就不是快乐数。
ps set的find函数的返回值:find会挨个查找set,当达到set.end()时,也就是一个都没找到,返回end,set.find(x) == set.end();找到该数了,返回该数的位置。
C++代码
class Solution {
public:
// 获得每个数字的平方和
int getSum(int n){
int sum = 0;
while(n){
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
// 如果sum在哈希表中出现了,说明循环了,也就不是快乐数了
bool isHappy(int n) {
unordered_set<int> set;
while(1){
int sum = getSum(n);
if(sum == 1) return true;
if(set.find(sum) != set.end()) return false;
else set.insert(sum);
n = sum;
}
}
};
1.两数之和
题目描述
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
思路
代码随想录中的思路
什么情况下使用哈希法?当我们需要查询某一个元素是否出现过,或一个元素是否在集合里。
需要定义一个集合存放我们遍历过的元素,然后再遍历数组的时候查找该元素是否出现在这个集合里。本题还需要知道元素的下标,需要使用key value结构存放,key存放元素,value存放下标,使用map。
C++代码
1、之前写了一个两层for循环,有点暴力
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//vector<int> num;
// int {i,j};
for(int i = 0 ; i < nums.size(); i++){
for(int j = i + 1 ; j < nums.size() ; j++)
if(nums[i] + nums[j] == target){
return {i,j};
}
}
return {};
// for(int k = 0 ; k < num.length ; k++)
// cout << num[k];
}
};
2、使用哈希表,数据结构为unordered_map
C++中map,有三种类型:
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。std::map 和std::multimap 的key是有序的。
map的存储结构是{key:数据元素,value:数组元素},遍历数组时,向map查询是否有和目前遍历元素匹配的数值,如果有,就找到匹配对,没有,就把目前遍历的元素放进map当中(insert操作)。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map<int,int> map;
for(int i = 0; i < nums.size(); i++){
int s = target - nums[i];
// int iter = map.find(s);
if(map.find(s) != map.end()) return {map.find(s) -> second, i};
map.insert(pair<int,int>(nums[i],i));
}
return {};
}
};
454.四数相加II
题目描述
力扣题目链接
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
思路
上小破站给出的讲解视频:
学透哈希表,map使用有技巧!LeetCode:454.四数相加II
找的是0-(c+d)有没有在map当中。
首先定义一个unordered_map,key存放的是a和b两数之和,value放a和b之和的次数;
遍历前两个数组,统计两个元素之和以及出现的次数,存储在map当中;
定义变量count,统计a+b+c=0出现的次数;
在遍历后两个数字的时候,找到0-(c+d)在map中出现过,则用count把map中对应的value(出现次数)统计出来;
C++代码
map中,key是a+b的数值,而value是a+b数值出现的次数。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> map;
int count = 0;
for(int a : nums1){
for(int b : nums2){
map[a + b]++;
}
}
for(int c : nums3){
for(int d : nums4){
if(map.find(0 - (c + d)) != map.end())
count += map[0 - (c + d)];
}
}
return count;
}
};
383. 赎金信
题目描述
力扣题目链接
给你两个字符串:ransomNote和magazine,判断ransomNote能不能由magazine里面的字符构成。
如果可以,返回 true;否则返回 false。magazine中的每个字符只能在ransomNote中使用一次。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
思路
杂志的字母不能重复使用;只有小写字母
哈希解法:题目说只有小写字母,那就空间换时间,用长度为26的数组存储magazine中字母出现的次数。
再用ransomNote去验证这个数组是否包含了ransomNote的所有字母
C++代码
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']++;
}
//如果在magazine中有,就做减减操作
for(int j = 0; j < ransomNote.size(); j++){
record[ransomNote[j] - 'a']--;
if(record[ransomNote[j] - 'a'] < 0) return false;
}
return true;
}
};
15. 三数之和
题目描述
力扣题目链接
思路
双指针法,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来移动left 和right, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
C++代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
int left, right;
sort(nums.begin(),nums.end());
for(int i = 0; i < nums.size(); i++){
//排序之后第一个元素大于0,不能凑成三元组了,直接返回result
if(nums[i] > 0) return result;
//第一步去重
if((i > 0) && (nums[i] == nums[i-1])) continue;
left = i + 1;
right = nums.size() - 1;
while(left < right){
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]});
//对c去重
while((right > left) && (nums[right] == nums[right-1])) right--;
//对b去重
while((right > left) && (nums[left] == nums[left+1])) left++;
//找到答案了,双指针同时向中间移动
right--;
left++;
}
}
}
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
你可以按任意顺序返回答案 。
思路
代码随想录的思路:
使用双指针法,在三数之和的基础上再套一层for循环。
C++代码
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
//三层for循环
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int k = 0; k < nums.size(); k++){
if(nums[k] > target && nums[k] >= 0) break;//剪枝
if(k > 0 && nums[k] == nums[k-1]) continue;
for(int i = k + 1; i < nums.size(); i++){
if(nums[i] + nums[k] > target && nums[i] + nums[k] >= 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((long)nums[k] + nums[i] + nums[left] + nums[right] > target){
right--;
}else if((long)nums[k] + nums[i] + nums[left] + nums[right] < target){
left++;
}
else{
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
while(right > left && nums[right] == nums[right - 1]) right--;//对right去重
while(right > left && nums[left] == nums[left + 1]) left++;//对left去重
//向中间缩小
right--;
left++;
}
}
}
}
return result;
}
};