哈希表(Hash Table)作为数据结构中的瑞士军刀,在算法面试和实际工程中都有着举足轻重的地位。今天我将通过LeetCode上5个经典题目,带大家深入理解哈希表的核心思想与应用技巧。不同于教科书式的讲解,我会结合自己刷题300+的经验,分享那些只有实战中才能获得的"肌肉记忆"。
哈希表本质上是通过哈希函数将键(key)映射到存储位置的数据结构,其平均时间复杂度可以达到O(1)。但实际应用中,我们需要考虑哈希冲突、负载因子、扩容机制等工程细节。在算法题中,我们更多关注其快速查找和去重的特性。下面这组题目由浅入深,覆盖了哈希表最典型的应用场景。
字母异位词(Anagram)是指由相同字母重新排列形成的不同单词。判断两个字符串是否为字母异位词,本质上就是检查它们的字符组成是否完全相同。
cpp复制class Solution {
public:
bool isAnagram(string s, string t) {
int arr[26]={0};
for(char c : s) arr[c-'a']++;
for(char c : t) arr[c-'a']--;
for(int count : arr)
if(count != 0) return false;
return true;
}
};
关键点解析:
s[i]-'a'将字符转换为0-25的索引(ASCII码相减)注意:这种解法假设输入只有小写字母。如果包含Unicode字符,需要使用真正的哈希表(如unordered_map)
这道题是字母异位词的变种,判断magazine中的字符能否组成ransomNote,区别在于字符数量要求"大于等于"而非"等于"。
cpp复制class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int arr[26]={0};
for(char c : magazine) arr[c-'a']++;
for(char c : ransomNote)
if(--arr[c-'a'] < 0) return false;
return true;
}
};
优化技巧:
求两个数组的交集,结果需要去重。这里展示了两种截然不同的解法,体现了数据结构选择对算法的影响。
数组解法:
cpp复制class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
bool arr[1001]={false};
for(int num : nums1) arr[num] = true;
vector<int> res;
for(int num : nums2)
if(arr[num]) {
res.push_back(num);
arr[num] = false; // 避免重复添加
}
return res;
}
};
集合解法:
cpp复制class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> set1(nums1.begin(), nums1.end());
unordered_set<int> res;
for(int num : nums2)
if(set1.count(num)) res.insert(num);
return vector<int>(res.begin(), res.end());
}
};
性能对比:
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 数组 | O(n+m) | O(1) | 数据范围小且已知 |
| 集合 | O(n+m) | O(n) | 通用解法 |
经验:当数据范围已知且有限时(如本题0-1000),数组解法往往更高效。其他情况优先选择集合。
快乐数的判定需要检测计算过程中是否出现循环,这正是哈希表的拿手好戏。
cpp复制class Solution {
public:
int getSum(int n) {
int sum = 0;
while(n) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> seen;
while(n != 1) {
if(seen.count(n)) return false;
seen.insert(n);
n = getSum(n);
}
return true;
}
};
关键细节:
数学优化:
实际上所有不快乐的数最终都会进入4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4的循环。可以利用这个性质进一步优化。
这道经典题目完美展示了哈希表如何将O(n²)的暴力解法优化为O(n)的优雅解法。
暴力解法:
cpp复制class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
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 {};
}
};
哈希表优化:
cpp复制class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> num_map;
for(int i = 0; i < nums.size(); ++i) {
int complement = target - nums[i];
if(num_map.count(complement))
return {num_map[complement], i};
num_map[nums[i]] = i;
}
return {};
}
};
为什么使用哈希表:
常见错误:
经过这5道题目的锤炼,我总结出哈希表应用的几个关键判断点:
进阶技巧:
在实际面试中,当问题涉及上述场景时,可以优先考虑哈希表解法。记住,优秀的算法工程师不仅要知道怎么做,更要清楚为什么这样做以及各种解法的权衡取舍。