1. 算法思想
在算法题中,哈希表(Hash Table)的核心作用是高效存储和快速查找键值对,其算法思想可总结为以下几点:
1. 快速判断元素是否存在
- 适用场景:需要判断某个元素是否在集合中,或统计元素出现次数。
- 核心思路:用哈希表记录元素的存在性或频率,将原本 \(O(n)\) 的遍历查找优化为 \(O(1)\) 的哈希表查询。
- 典型例题:两数之和、存在重复元素、字母异位词判断。
2. 分组与映射
- 适用场景:将具有相同特征的元素归为一组(如字母异位词、同构字符串)。
- 核心思路:设计哈希表的键为元素的特征(如排序后的字符串、字符频率数组),值为符合该特征的元素集合。
- 典型例题:字母异位词分组、有效的字母异位词。
3. 缓存中间结果
- 适用场景:避免重复计算,保存子问题的解(如动态规划中的重叠子问题)。
- 核心思路:用哈希表记录已计算的结果,下次需要时直接查询,减少时间复杂度。
- 典型例题:斐波那契数列、爬楼梯问题的优化。
4. 处理冲突与滑动窗口
- 适用场景:在限定范围(如长度为 k 的窗口)内统计元素频率或判断重复。
- 核心思路:结合哈希表与滑动窗口,动态维护窗口内的元素状态,通过哈希表快速检查窗口内是否存在目标元素。
- 典型例题:存在重复元素 II、无重复字符的最长子串。
5. 双向映射与关系记录
- 适用场景:需要维护两个变量之间的双向关联(如字符替换、模式匹配)。
- 核心思路:使用两个哈希表分别记录正向和反向映射关系,确保映射的唯一性。
- 典型例题:同构字符串、单词规律。
6. 空间换时间
- 本质:哈希表通过额外的存储空间,将时间复杂度从 \(O(n)\) 或 \(O(n^2)\) 优化到 \(O(1)\) 或 \(O(n)\)。
- 注意事项:需权衡空间复杂度,避免过度使用导致内存溢出(如哈希表存储整个数组的元素)。
解题步骤总结
- 分析问题:判断是否需要快速查找、去重、分组或缓存结果。
- 设计键值映射:确定哈希表的键(如元素值、排序后的字符串、频率数组)和值(如元素索引、出现次数、元素集合)。
- 处理冲突逻辑:根据题意选择合适的冲突处理方式(如直接覆盖、链表存储)。
- 优化与边界检查:考虑动态扩容、滑动窗口维护、空值或重复元素的特殊处理。
2. 例题
2.1 两数之和

这是经典 "两数之和" 问题的解法,核心思路如下:
1. 利用哈希表优化查找
用 unordered_map
(哈希表)存储数组元素值 和对应的下标 。遍历数组时,通过哈希表快速判断 target - 当前元素
是否存在,将原本暴力枚举两层循环的 \(O(n^2)\) 时间复杂度,优化为 \(O(n)\) (n 是数组长度 )。
2. 遍历 + 查找的流程
- 遍历数组
nums
,对每个元素nums[i]
:- 计算差值
comp = target - nums[i]
,查哈希表是否有comp
。 - 若有,说明找到两个数(
comp
对应数和nums[i]
),返回它们的下标{hash[comp], i}
。 - 若没有,把当前元素
nums[i]
和下标i
存入哈希表,继续遍历。
- 计算差值
这样一趟遍历就能完成查找,高效解决问题,本质是用空间换时间,靠哈希表快速查询特性简化流程。
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); ++i)
{
if(hash.count(target - nums[i])) return {hash[target - nums[i]], i};
hash[nums[i]] = i;
}
return {};
}
};
2.2 面试题 01.02. 判定是否为字符重排
面试题 01.02. 判定是否互为字符重排 - 力扣(LeetCode)

核心思路如下:
1. 预处理:检查长度是否相等
- 如果
s1
和s2
的长度不同,直接返回false
。因为变位词必须由完全相同的字符组成,长度必然相等。
2. 统计字符频率
- 使用一个长度为 26 的数组
hash
统计 小写字母 的出现次数(假设输入仅含小写字母)。 - 遍历
s1
,将每个字符ch
映射到数组下标ch - 'a'
,并增加对应计数。
3. 验证字符频率匹配
- 遍历
s2
,对每个字符ch
:- 如果
hash[ch - 'a'] > 0
,说明s1
中存在该字符,将计数减一。 - 如果
hash[ch - 'a'] == 0
,说明s2
中的该字符在s1
中不存在或已被用完,直接返回false
。
- 如果
4. 返回结果
- 如果遍历完
s2
所有字符都没有提前返回false
,说明s1
和s2
的字符频率完全一致,返回true
。
关键点
- 时间复杂度:\(O(n)\),其中 n 是字符串长度。两次遍历数组即可完成判断。
- 空间复杂度:\(O(1)\),因为数组长度固定为 26,不随输入规模变化。
- 适用条件:输入字符串仅含小写字母。若包含其他字符(如大写字母、数字),需扩大数组范围或改用哈希表。
cpp
class Solution {
public:
bool CheckPermutation(string s1, string s2) {
if(s1.size() != s2.size()) return false;
int hash[26];
for(auto ch : s1) ++hash[ch - 'a'];
for(auto ch : s2)
if(hash[ch - 'a'] > 0) --hash[ch - 'a'];
else return false;
return true;
}
};
2.3 存在重复元素

核心思路如下:
1. 利用哈希表记录元素出现次数
- 使用
unordered_map<int, int>
(哈希表)统计每个元素的出现次数。 - 键 :数组中的元素值;值:该元素出现的次数。
2. 遍历数组,检查重复
- 遍历数组
nums
,对每个元素n
:- 检查哈希表 :如果
n
已存在(即hash.count(n) > 0
),说明该元素是重复的,直接返回true
。 - 记录元素 :若
n
不存在,将其加入哈希表并计数为 1(即++hash[n]
)。
- 检查哈希表 :如果
3. 返回结果
- 如果遍历完整个数组都没有发现重复元素,返回
false
。
关键点
- 时间复杂度:\(O(n)\),其中 n 是数组长度。遍历一次数组,哈希表的插入和查找操作平均时间为 \(O(1)\)。
- 空间复杂度:\(O(n)\),最坏情况下数组所有元素都不同,需存储整个数组的元素。
- 哈希表的选择 :使用
unordered_map
而非map
是因为前者基于哈希表实现,查找效率更高(平均 \(O(1)\) vs. 红黑树的 \(O(\log n)\))。
cpp
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_map<int, int> hash;
for(auto n : nums)
{
if(hash.count(n)) return true;
else ++hash[n];
}
return false;
}
};
2.4 存在重复元素 2

核心思路如下:
1. 问题理解
判断数组中是否存在重复元素 ,且它们的下标之差不超过给定值 k
。 例如:nums = [1,2,3,1]
,k = 3
,下标 0 和 3 的元素均为 1,差值 3 ≤ 3
,返回 true
。
2. 哈希表维护元素的最新下标
- 使用
unordered_map<int, int>
记录每个元素最近一次出现的下标。 - 键 :元素值;值:该元素在数组中的最新下标。
3. 遍历数组,检查条件
- 遍历数组
nums
,对每个元素nums[i]
:- 检查重复 :若哈希表中已存在
nums[i]
,计算当前下标i
与上次下标hash[nums[i]]
的差值。- 若差值
≤ k
,说明存在满足条件的重复元素,返回true
。
- 若差值
- 更新哈希表 :将当前元素的下标
i
存入哈希表(覆盖旧值),确保记录的是最新位置。
- 检查重复 :若哈希表中已存在
4. 返回结果
- 若遍历结束仍未找到符合条件的重复元素,返回
false
。
关键点
- 时间复杂度:\(O(n)\),一次遍历数组,哈希表操作平均 \(O(1)\)。
- 空间复杂度:\(O(n)\),最坏情况下需存储所有元素的下标。
- 滑动窗口思想 :通过哈希表动态维护元素的最新位置,等价于在长度为
k
的滑动窗口内检查重复。
cpp
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); ++i)
{
if(hash.count(nums[i]) && abs(hash[nums[i]] - i) <= k)
return true;
hash[nums[i]] = i;
}
return false;
}
};
2.5 字母移位次分组

核心思路如下:
1. 问题理解
将字符串数组中的字母异位词 (由相同字母重排列形成的字符串)分为一组。 例如:strs = ["eat", "tea", "tan", "ate"]
→ [["eat","tea","ate"],["tan"]]
。
2. 哈希表映射:排序后的字符串 → 原字符串列表
- 关键观察 :字母异位词排序后得到的字符串相同(如
"eat"
和"tea"
排序后均为"aet"
)。 - 使用
unordered_map<string, vector<string>>
记录:- 键 :排序后的字符串(如
"aet"
)。 - 值 :所有排序后等于该键的原字符串列表(如
["eat", "tea", "ate"]
)。
- 键 :排序后的字符串(如
3. 遍历字符串数组,构建哈希表
- 对每个字符串
s
:- 生成键 :复制
s
为tmp
并排序,得到tmp
(如"eat"
→"aet"
)。 - 分组 :将
s
加入哈希表中tmp
对应的列表(如hash["aet"].push_back("eat")
)。
- 生成键 :复制
4. 提取结果
- 遍历哈希表,将每个键对应的字符串列表(
vector<string>
)收集到结果ret
中。
关键点
- 时间复杂度:\(O(n \cdot k \log k)\),其中 n 是字符串数量,k 是字符串的最大长度(每个字符串排序时间为 \(O(k \log k)\))。
- 空间复杂度:\(O(n \cdot k)\),存储所有字符串及其排序后的键。
- 扩展性:若输入含非小写字母(如大写字母或数字),排序逻辑需调整(如使用计数排序生成键)。
cpp
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> ret;
unordered_map<string, vector<string>> hash;
for(auto s : strs)
{
string tmp = s;
sort(tmp.begin(), tmp.end());
hash[tmp].push_back(s);
}
for(auto& [x, y] : hash)
ret.push_back(y);
return ret;
}
};