作为一名刷过上百道LeetCode题目的程序员,我深刻体会到哈希表在算法解题中的重要性。今天我想分享四个经典题目,通过它们来剖析哈希表的不同实现方式和使用技巧。
这四个题目看似简单,却涵盖了哈希表的三种主要实现方式:数组、unordered_set和unordered_map。它们分别对应着不同的使用场景:
接下来,我将逐个分析这些题目,分享我的解题思路和优化过程。
字母异位词是指由相同字母重新排列形成的单词。最直观的解法是对两个字符串排序后比较:
cpp复制bool isAnagram(string s, string t) {
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
这种方法时间复杂度为O(nlogn),空间复杂度O(1)。但我们可以用哈希表优化到O(n)。
使用数组作为哈希表是最佳选择,因为:
cpp复制bool isAnagram(string s, string t) {
if(s.size() != t.size()) return false;
int record[26] = {0};
for(char c : s) record[c - 'a']++;
for(char c : t) {
if(--record[c - 'a'] < 0)
return false;
}
return true;
}
c - 'a':将字符转换为0-25的索引注意:在C++中,字符本质是ASCII码,所以这种算术运算是安全的。但如果是Unicode字符,这种方法就不适用了。
最直接的思路是双层循环遍历两个数组:
cpp复制vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
vector<int> result;
for(int num1 : nums1) {
for(int num2 : nums2) {
if(num1 == num2) {
// 去重逻辑
if(find(result.begin(), result.end(), num1) == result.end()) {
result.push_back(num1);
}
}
}
}
return result;
}
这种方法时间复杂度O(n²),效率很低。
unordered_set的查找时间复杂度为O(1),非常适合这个场景:
cpp复制vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for(int num : nums2) {
if(nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
nums_set(nums1.begin(), nums1.end()):直接通过迭代器构造set,自动去重find()方法:返回迭代器,找不到时等于end()for(int num : nums2)是C++11特性,更简洁实际项目中,如果数据量很大,可以考虑先对两个数组排序,然后用双指针法,空间复杂度可以降到O(1)
快乐数的定义看似简单,但隐藏着一个关键点:如果不是快乐数,计算过程会进入循环。这正是使用哈希表检测循环的理想场景。
cpp复制class Solution {
public:
int getSum(int n) {
int sum = 0;
while(n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> seen;
while(true) {
int sum = getSum(n);
if(sum == 1) return true;
if(seen.count(sum)) return false;
seen.insert(sum);
n = sum;
}
}
};
n % 10获取最后一位,n /= 10去掉最后一位有趣的是,所有非快乐数最终都会进入4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4的循环
最直观的方法是双重循环:
cpp复制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 {};
}
时间复杂度O(n²),显然不够高效。
使用unordered_map存储已经遍历过的数字及其索引:
cpp复制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 {};
}
num_map.count(complement):检查补数是否存在注意:题目假设每种输入只有一个答案,所以找到后可以直接返回。如果有多个解,需要调整算法。
通过这四个题目,我们可以总结出哈希表实现的选择策略:
| 场景 | 推荐实现 | 原因 | 时间复杂度 |
|---|---|---|---|
| 键范围小且连续 | 数组 | 无哈希冲突,访问最快 | O(1) |
| 只需判断存在性 | unordered_set | 自动去重,查找快 | 平均O(1) |
| 需要键值对 | unordered_map | 直接建立映射关系 | 平均O(1) |
| 需要有序遍历 | set/map | 基于红黑树实现 | O(logn) |
cpp复制unordered_set<int> s;
s.insert(1); // 插入元素
s.erase(1); // 删除元素
s.count(1); // 检查存在性
s.find(1) != s.end(); // 查找元素
unordered_map<int, string> m;
m[1] = "one"; // 插入或修改
m.insert({2, "two"}); // 插入
m.count(1); // 检查键是否存在
m.find(1)->second; // 访问值
reserve()减少rehashmax_load_factor()调整为了巩固哈希表的使用,建议尝试以下LeetCode题目:
在实际编码中,我发现哈希表经常与其他算法结合使用,如双指针法、滑动窗口等。掌握好哈希表能显著提升解题效率。