算法 --- 哈希表
哈希表
哈希表算法的核心题目类型是那些需要快速判断一个元素是否出现过、出现过多少次,或者需要建立两个元素之间映射关系的题目。
具体来说,以下几类题目非常适合使用哈希表(通常包括 set
集合 和 map
字典/映射):
1. 需要快速查找和判重(“是否存在”)
当题目需要频繁地检查某个数据是否已经存在于一个集合中时,哈希集合(HashSet
)的 O(1) 时间复杂度查找是无可替代的。
-
典型问题:
-
两数之和:给定一个数组和一个目标值,找出数组中两个数之和等于目标值。哈希表用来存储“已经遍历过的数字及其索引”,从而在遍历时能快速检查“目标值减去当前数”的结果是否在表中。
-
存在重复元素:判断数组中是否存在重复元素。只需将元素依次加入集合,如果在加入前发现已存在,即返回 true。
-
快乐数:判断一个数是否是快乐数。哈希表用来记录计算过程中出现过的所有和,如果某个和重复出现,则说明进入了循环,不是快乐数。
-
2. 需要统计频率(“出现了多少次”)
当题目需要统计元素出现的次数时,哈希映射(HashMap
)是完美的工具,可以用键(key)存储元素,值(value)存储其出现的频次。
-
典型问题:
-
两个数组的交集:找出两个数组中都出现过的元素。可以用哈希表统计其中一个数组所有数字的出现频率,再遍历另一个数组进行匹配。
-
有效的字母异位词:判断两个字符串的字符组成是否相同。用哈希表统计两个字符串中每个字符的出现次数,然后比较两个表是否一致(或者用一个表统计第一个字符串,然后用第二个字符串去递减计数)。
-
数组中出现次数超过一半的数字:统计每个数字的频率,找到频率超过数组长度一半的那个。
-
但是我们不要忘记了对于频繁的查找某一个数的时候,二分也是可以的,尽管有些局限,但是能用二分就用二分!
3. 需要建立映射关系(“对应关系是什么”)
当题目要求建立两个集合或两个维度之间的对应关系,并根据一个键来快速查找其关联的值时,使用哈希映射。
-
典型问题:
-
两数之和(同样适用):这里它建立了“数值”到“索引”的映射关系。
-
单词规律:判断字符串是否遵循某种特定的模式。例如
pattern = "abba"
,str = "dog cat cat dog"
。需要建立字符到单词的双向映射,哈希表可以轻松完成。 -
同构字符串:原理与上一题完全相同,判断两个字符串能否通过字符映射一一对应。
-
4. 需要记录状态或前缀信息
有些问题需要记录之前遍历过的子数组的状态,而哈希表可以高效地存储这些状态及其出现的位置或次数。
-
典型问题:
-
和为 K 的子数组:计算数组中连续子数组和为 k 的个数。利用“前缀和”思想,哈希表用来记录“某个前缀和”出现的次数,从而快速计算出当前前缀和与目标 k 的差值是否存在。
-
最长连续序列:给定无序数组,找出数字连续的最长序列(如
[100,4,200,1,3,2]
的最长连续序列是[1,2,3,4]
)。哈希集合用于存储所有数字以实现 O(1) 访问,然后对于每个数,检查其“减一”是否不存在(说明它是序列起点),然后开始逐个检查“加一”是否存在来延长序列。
-
总结:什么题目适用哈希表?
当你看到题目中包含以下关键词时,就应该优先考虑哈希表:
-
“是否出现/包含”
-
“重复”
-
“次数/频次”
-
“总和/差值”(常与前缀和结合)
-
“映射/对应关系”
其核心思想就是 “用空间换时间”,通过额外的哈希表存储来将原本需要 O(n) 的查找操作降低到 O(1),从而优化整个算法的时间复杂度。
题目练习
1. 两数之和 - 力扣(LeetCode)
解法(哈希表):
算法思路:
-
如果我们可以事先将「数组内的元素」和「下标」绑定在一起存入「哈希表」中,然后直接在哈希表中查找每一个元素的
target - nums[i]
,就能快速的找到「目标和的下标」。 -
这里有一个小技巧,我们可以不用将元素全部放入到哈希表之后,再来二次遍历(因为要处理元素相同的情况)。而是在将元素放入到哈希表中的「同时」,直接来检查表中是否已经存在当前元素所对应的目标元素(即
target - nums[i]
)。如果它存在,那我们已经找到了对应解,并立即将其返回。无需将元素全部放入哈希表中,提高效率。 -
因为哈希表中查找元素的时间复杂度是 O(1),遍历一遍数组的时间复杂度为 O(N),因此可以将时间复杂度降到 O(N)。
这是一个典型的「用空间交换时间」的方式。(固定一个数,从之前的数中查找!)
class Solution {
public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> hash;for(int i = 0; i < nums.size(); ++i) {int tmp = target - nums[i];if(hash.count(tmp)) return {hash[tmp], i};hash[nums[i]] = i;}return {-1, -1};}
};
面试题 01.02. 判定是否互为字符重排 - 力扣(LeetCode)
解法(哈希表):
算法思路:
-
当两个字符串的长度不相等的时候,是不可能构成互相重排的,直接返回 false;
-
如果两个字符串能够构成互相重排,那么每个字符串中「各个字符」出现的「次数」一定是相同的。因此,我们可以分别统计出这两个字符串中各个字符出现的次数,然后逐个比较是否相等即可。这样的话,我们就可以选择「哈希表」来统计字符串中字符出现的次数。
第一种写法:
class Solution {
public:bool CheckPermutation(string s1, string s2) {if(s1.size() != s2.size()) return false;int hash1[26] = {0};int hash2[26] = {0};for(int i = 0; i < s1.size(); ++i){hash1[s1[i] - 'a']++;hash2[s2[i] - 'a']++;}for(int i = 0; i < 26; ++i){if(hash1[i] != hash2[i]) return false;}return true;}
};
第二种写法:
class Solution {
public:bool CheckPermutation(string s1, string s2) {if(s1.size() != s2.size()) return false;int hash[26] = {0};for(auto ch : s1) hash[ch - 'a']++;for(auto ch : s2){int tmp = ch - 'a';hash[tmp]--;if(hash[tmp] < 0) return false;}return true;}
};
217. 存在重复元素 - 力扣(LeetCode)
解法(哈希表):
算法思路:
-
分析一下题目,出现「至少两次」的意思就是数组中存在着重复的元素,因此我们可以无需统计元素出现的数目。仅需在遍历数组的过程中,检查当前元素「是否在之前已经出现过」即可。
-
因此我们可以利用哈希表,仅需存储数「组内的元素」。在遍历数组的时候,一边检查哈希表中是否已经出现过当前元素,一边将元素加入到哈希表中。
class Solution {
public:bool containsDuplicate(vector<int>& nums) {unordered_map<int, int> hash;for(auto x : nums){if(hash.find(x) != hash.end()) return true;hash[x]++;}return false;}
};
219. 存在重复元素 II - 力扣(LeetCode)
解法(哈希表):
算法思路:
解决该问题需要我们快速定位到两个信息:
-
两个相同的元素;
-
这两个相同元素的下标。
因此,我们可以使用「哈希表」,令数组内的元素做 key 值,该元素所对应的下标做 val 值,将「数组元素」和「下标」绑定在一起,存入到「哈希表」中。
思考题:
如果数组内存在大量的「重复元素」,而我们判断下标所对应的元素是否符合条件的时候,需要将不同下标的元素作比较,怎么处理这个情况呢?
答: 这里运用了一个「小贪心」。
我们按照下标「从小到大」的顺序遍历数组,当遇到两个元素相同,并且比较它们的下标时,这两个下标一定是距离最近的,因为:
-
如果当前判断符合条件直接返回 true,无需继续往后查找。
-
如果不符合条件,那么前一个下标一定不可能与后续相同元素的下标匹配(因为下标在逐渐变大),那么我们可以大胆舍去前一个存储的下标,转而将其换成新的下标,继续匹配。
换成>=k的话,就需要加一个判断了,防止前面的相同的数被覆盖!!!
class Solution {
public:bool containsNearbyDuplicate(vector<int>& nums, int k) {unordered_map<int, int> hash;for(int i = 0; i < nums.size(); ++i){int x = nums[i];if(hash.find(x) != hash.end() && (i - hash[x]) <= k) return true;hash[x] = i;}return false;}
};
49. 字母异位词分组 - 力扣(LeetCode)
解法(哈希表 + 排序):
算法思路:
互为字母异位词的单词有一个特点:将它们「排序」之后,两个单词应该是「完全相同」的。
所以,我们可以利用这个特性,将单词按照字典序排序,如果排序后的单词相同的话,就划分到同一组中。
这时我们就要处理两个问题:
-
排序后的单词与原单词需要能互相映射;
-
将排序后相同的单词,「划分到同一组」;
利用语言提供的「容器」的强大的功能就能实现这两点:
-
将排序后的字符串(string)当做哈希表的 key 值;
-
将字母异位词数组(string[])当成 val 值。
定义一个「哈希表」即可解决问题。
class Solution {
public:vector<vector<string>> groupAnagrams(vector<string>& strs) {unordered_map<string, vector<string>> hash;for(auto& s : strs){string tmp = s;sort(tmp.begin(), tmp.end());hash[tmp].push_back(s);}vector<vector<string>> ret;for(auto& [x, y] : hash){ret.push_back(y);}return ret;}
};