代码随想录:哈希表
🎈hash表方法经常使用 auto 来标记 iter
... auto iter = map.find(target - nums[i]); if(iter != map.end()) { return {iter->second, i}; } ...
🎈for循环遍历的两种方式等价
... for (int a : A) { for (int b : B) { umap[a + b]++; } } ...
前言
- 哈希表是通过关键码的值而进行直接访问的数据结构
- 数组也是一张hash表,只不过数组的关键码就是他的下标,或者说是索引
- hash表最强大就在于它可以快速判断一个元素是否在一个集合里面,时间复杂度$O(1)$
- hash方法&hash函数
在密码学里,哈希函数是指对一串消息做计算,根据one-way-function得到一个消息摘要,确保原消息的真实性
将一个数据列表映射倒hash表上,就涉及了hash function,即哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
很显然有个问题,哈希碰撞
避免hash碰撞的方法
-
- 拉链法
- 发生冲突的位置搞一个链表,有冲突的元素放在链表里
- 拉链法
-
-
- 拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间
- 线性探测法
- 保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
- 当出现碰撞时候后来的元素插到碰撞位置后面的空闲位置
-
- hash表常用的几种数据结构
- array 数组
- set 集合
- map 映射
- C++中set及其底层实现
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
- 所以当我们要使用集合来解决hash问题时,优先使用unordered_set
- 如果集合要是有序的,就使用set
- 如果集合不仅要是有序的,还要有重复元素,就使用multiset
- C++中map及其底层实现
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的
map 是一个key - value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
- 为什么都说set和map是哈希法使用的数据结构?
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
- hash_set hash_map与unordered_set,unordered_map有什么关系?
unordered_set,unordered_map是C++标准库中的数据结构,而hash_set hash_map是民间高手自发造的车轮子,其实是一个东西,既然标准都有了,还是用标准里面的东西吧!
- 哈希法也是牺牲了空间换取了时间,因为要使用额外的数组,set或者是map来存放数据,才能实现快速的查找
- 当要判断一个元素是否在集合里,就可以考虑hash表
有效的字母异位词
242. 有效的字母异位词 - 力扣(LeetCode) (leetcode-cn.com)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
说明: 你可以假设字符串只包含小写字母
平庸简单解法:新建一个hash表 int record[26] = {0}
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 i=0; i<26; i++){ if(record[i] != 0){ return false; } } return true; }
优化之后(如果长度不同肯定不是,合并代码)
class Solution { public: bool isAnagram(string s, string t) { int num[26]={0}; if(s.length()!=t.length()) return false; for(int i=0;s[i]!='\0';i++){ num[s[i]-'a']++; num[t[i]-'a']--; } for(int i=0;i<26;i++) if(num[i]!=0) return false; return true; } };
两个数组的交集
349. 两个数组的交集 - 力扣(LeetCode) (leetcode-cn.com)
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
由于数组长度未知,就不太能用数组来做hash表了,有要求去重,没有顺序 所以使用 unordered_set 来做!
注:使用set来做hash表,需要hash计算,还是有额外耗时的。
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { unordered_set<int> ret_set; unordered_set<int> nums_set(nums1.begin(), nums1.end()); for(int num : nums2){ if(nums_set.find(num) != nums_set.end()){ ret_set.insert(num); } } return vector<int>(ret_set.begin(),ret_set.end()); }
python3 一条命令
def intersection(nums1: List[int], nums2: List[int]) -> List[int]: return list(set(nums1) & set(nums2)) # 两个数组先变成集合,求交集后还原为数组
快乐数
写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
输入:19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
问题就是怎么对各个位置上的数求和。
class HappySolution { public: int getsum(int n) { int sum = 0; while (n) { sum += (n % 10) * (n % 10); n = n / 10; } return sum; } bool isHappy(int n) { unordered_set<int> ret; while (1) { int sum = getsum(n); if (sum == 1) { return true; } if (ret.find(sum) != ret.end()) { return false; } else { ret.insert(sum); } n = sum; } } };
两数之和
1. 两数之和 - 力扣(LeetCode) (leetcode-cn.com)
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
- 暴力破解两层循环,复杂度$O(n^2)$
- 用hash方法的话,大小未知,不方便用数组
- 除了存储值,还要存下标,故用set不够
- 所以这题hash方法要使用map来做
- 题目中的key并不要求顺序,使用unordered_map来做
两数之和题解:
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.insert(pair<int,int>(nums[i],i)); } return {}; }
PYTHON中可以使用枚举做
for idx, val in enumerate(nums):
四个数相加和为零有多少种情况
454. 四数相加 II - 力扣(LeetCode) (leetcode-cn.com)
给定四个包含整数的数组列表 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 。
四个数分成两个部分,a+b c+d
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) { unordered_map<int,int> map; //key 是a+b的值,value是a+b的值出现的次数 for(int a:nums1){ for(int b:nums2){ map[a+b]++; } } int count = 0; for(int c:nums3){ for(int d:nums4){ if(map.find(0-(c+d)) != map.end()){ count += map[0-(c+d)]; } } } return count; }
赎金信
383. 赎金信 - 力扣(LeetCode) (leetcode-cn.com)
给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
注意:
你可以假设两个字符串均只含有小写字母。
canConstruct("a", "b") -> false
canConstruct("aa", "ab") -> false
canConstruct("aa", "aab") -> true
解答:
- 数量要够,样式要有,第一个字符串要被第二个字符串包含
- 可以暴力解法,把赎金信删空为止 复杂度高,使用hash表解法,由于数量固定,使用数组做hash表效率高
- 如果杂志不够赎金信用,那么就返回false
bool canConstruct(string ransomNote, string magazine) { //vector<int> count(26); int count[26] = {0}; for(auto c:magazine) ++count[c-'a']; for(auto c:ransomNote) { if(count[c-'a']!=0) --count[c-'a']; else return false; } return true; }
python解法巧妙
# 赎金信 def canConstruct(ransomNote: str, magazine: str) -> bool: c1 = collections.Counter(ransomNote) c2 = collections.Counter(magazine) x = c1 - c2 #x只保留值大于0的符号,当c1里面的符号个数小于c2时,不会被保留 #所以x只保留下了,magazine不能表达的 if(len(x)==0): return True else: return False s1 = "asdddsadas" s2 = "asdadsadadssadsadsadsasdsadsa" print(canConstruct(s2,s1))
三数之和
15. 三数之和 - 力扣(LeetCode) (leetcode-cn.com)
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。 结果中不出现一样的答案
- 首先排个序
- 那么怎么做到去重呢?hash表不是很好做,可以用双指针,
vector<vector<int>> threeSum(vector<int>& nums) { vector<vector<int>> ret; sort(nums.begin(), nums.end()); for(int i=0; i<nums.size();i++){ if(nums[i]>0){ return ret; } //找 nums[j] + nums[k] = - nums[i]符合条件的j 和 k 且 j!=k if(i>0 && nums[i]==nums[i-1]) continue;// i-1避免溢出 int j = i+1; int k = nums.size()-1; while(j<k) { // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组 /* while (right > left && nums[right] == nums[right - 1]) right--; while (right > left && nums[left] == nums[left + 1]) left++; */ int sum = nums[i] + nums[j] + nums[k]; if(sum > 0){ k--; }else if(sum < 0){ j++; }else{ ret.push_back(vector<int>{nums[i],nums[j],nums[k]}); // 去重逻辑应该放在找到一个三元组之后 while (k > j && nums[k] == nums[k - 1]) k--; while (k > j && nums[j] == nums[j + 1]) j++; // 找到答案时,双指针同时收缩 k--; j++; } } } return ret; }
四数之和
18. 四数之和 - 力扣(LeetCode) (leetcode-cn.com)
题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:
答案中不可以包含重复的四元组。
那么只要在三数之和的基础上再套一层循环,用两层for来固定一个数,最后再用双指针做。。。
vector<vector<int>> fourSum(vector<int>& nums, int target) { vector<vector<int>> ret; sort(nums.begin(), nums.end()); for (int t = 0; t < nums.size(); t++) { //去重 if (t > 0 && nums[t] == nums[t - 1]) continue; for (int i = t + 1; i < nums.size(); i++) { //去重 if (i > t + 1 && nums[i] == nums[i - 1]) continue; //找 nums[j] + nums[k] = - nums[i] - nums[t]符合条件的j 和 k 且 j!=k int j = i + 1; int k = nums.size() - 1; while (j < k) { // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组 /* while (right > left && nums[right] == nums[right - 1]) right--; while (right > left && nums[left] == nums[left + 1]) left++; */ //这里使用 int sum = nums[i] + nums[j] + nums[k] + nums[t] 确不行就离谱 if (nums[i] + nums[j] > target - (nums[k] + nums[t])) { k--; } else if (nums[i] + nums[j] < target - (nums[k] + nums[t])) { j++; } else { ret.push_back(vector<int>{nums[t], nums[i], nums[j], nums[k]}); // 去重逻辑应该放在找到一个三元组之后 while (k > j && nums[k] == nums[k - 1]) k--; while (k > j && nums[j] == nums[j + 1]) j++; // 找到答案时,双指针同时收缩 k--; j++; } } } } return ret; }