文章目录
-
- 哈希的奥义:在混沌中建立瞬间响应
- [一、 两数之和:一切梦开始的地方 (Easy)](#一、 两数之和:一切梦开始的地方 (Easy))
-
- [1.1 题目描述](#1.1 题目描述)
- [1.2 算法思路:边存边找](#1.2 算法思路:边存边找)
- [1.3 C++ 代码实战](#1.3 C++ 代码实战)
- [二、 判定是否互为字符重排 (Easy)](#二、 判定是否互为字符重排 (Easy))
-
- [2.1 题目描述](#2.1 题目描述)
- [2.2 算法思路:计数抵消法](#2.2 算法思路:计数抵消法)
- [2.3 C++ 代码实战](#2.3 C++ 代码实战)
- [三、 存在重复元素 I & II:查重与距离判断](#三、 存在重复元素 I & II:查重与距离判断)
-
- [3.1 题目描述](#3.1 题目描述)
- [3.2 深度拆解:贪心的下标更新](#3.2 深度拆解:贪心的下标更新)
- [3.3 C++ 代码实战 (Q2 为例)](#3.3 C++ 代码实战 (Q2 为例))
- [四、 字母异位词分组:哈希套容器的高阶玩法 (Medium)](#四、 字母异位词分组:哈希套容器的高阶玩法 (Medium))
-
- [4.1 题目描述](#4.1 题目描述)
- [4.2 算法思路:特征归一化](#4.2 算法思路:特征归一化)
- [4.3 C++ 代码实战](#4.3 C++ 代码实战)
- [五、 总结:哈希表的威力](#五、 总结:哈希表的威力)
哈希的奥义:在混沌中建立瞬间响应
一、 两数之和:一切梦开始的地方 (Easy)
1.1 题目描述
题目链接 :1. 两数之和
描述 :
给定一个整数数组
nums和一个目标值target,找出和为目标值的那两个整数,并返回它们的数组下标。
1.2 算法思路:边存边找
-
传统做法 :先全部存进哈希表,再遍历一遍找
target - x。这需要处理同一个元素被用两次的问题。 -
贪心优化 :遍历数组,每到一个数
nums[i]:- 计算"另一半":
x = target - nums[i]。 - 去哈希表查
x是否在之前出现过。 - 如果在 :找到了!返回
{hash[x], i}。 - 如果不在 :把当前的
nums[i]存进哈希表,继续往后走。
- 计算"另一半":
ASCII 逻辑图:
bash
nums = [2, 7, 11, 15], target = 9
i=0: nums[0]=2, target-2=7. Map空. 把 {2:0} 存入.
i=1: nums[1]=7, target-7=2. Map中有2! 返回 {0, 1}.
1.3 C++ 代码实战
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// key: 数值, value: 下标
unordered_map<int, int> hash;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
// 检查"另一半"是否已经在之前的记录里
if (hash.count(complement)) {
return {hash[complement], i};
}
// 没找到就存入当前值,给后面的数备查
hash[nums[i]] = i;
}
return {}; // 基本不会走到这
}
};
二、 判定是否互为字符重排 (Easy)
2.1 题目描述
题目链接 :面试题 01.02. 判定是否互为字符重排
描述 :
给定两个字符串
s1和s2,请编写一个程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。
2.2 算法思路:计数抵消法
所谓"重排",就是字母种类和个数完全一样。
-
长度检查 :长度不相等,直接返回
false。 -
哈希计数:用数组模拟哈希表(因为只有字母,范围小,效率极高)。
- 遍历
s1:每个字母对应的位置+1。 - 遍历
s2:每个字母对应的位置-1。 - 关键点 :如果在减的过程中,某个字母的计数值小于 0,说明
s2中该字母多出来了,直接false。
- 遍历
2.3 C++ 代码实战
cpp
class Solution {
public:
bool CheckPermutation(string s1, string s2) {
if (s1.size() != s2.size()) return false;
// 只有26个字母或128个ASCII码,直接开数组
int hash[128] = {0};
// 1. 统计第一个字符串
for (char ch : s1) hash[ch]++;
// 2. 抵消第二个字符串
for (char ch : s2) {
hash[ch]--;
// 如果减到负数,说明 s2 出现了 s1 没有或多出的字符
if (hash[ch] < 0) return false;
}
return true;
}
};
三、 存在重复元素 I & II:查重与距离判断
3.1 题目描述
Q1 :217. 存在重复元素 ------ 只要有重复就返回
true。Q2 :219. 存在重复元素 II ------ 重复元素的下标差 ≤ k \le k ≤k 返回
true。
3.2 深度拆解:贪心的下标更新
- Q1 简单查重 :直接用
unordered_set记录见过的数。 - Q2 距离查重 :
我们需要记录每个数最后一次出现的位置 。
为什么只需记录"最后一次"?
假设我们找nums[i] == nums[j]。如果i比较靠后,那么它和j越靠近,下标差就越容易 ≤ k \le k ≤k。所以,如果我们遇到一个新的重复元素,即使它不满足 ≤ k \le k ≤k,我们也要更新它的下标,因为它更有潜力满足后续的判断。
3.3 C++ 代码实战 (Q2 为例)
cpp
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int, int> hash; // val -> index
for (int i = 0; i < nums.size(); i++) {
// 如果查到了重复元素
if (hash.count(nums[i])) {
// 且距离满足条件
if (i - hash[nums[i]] <= k) return true;
}
// 无论满不满足,都更新为最新的下标(贪心策略)
hash[nums[i]] = i;
}
return false;
}
};
四、 字母异位词分组:哈希套容器的高阶玩法 (Medium)
4.1 题目描述
题目链接 :49. 字母异位词分组
描述 :
给你一个字符串数组,请你将字母异位词(字符相同但顺序不同)组合在一起。
4.2 算法思路:特征归一化
如何让哈希表识别出 "eat" 和 "tea" 是一家人?
-
找共性 :把它们各自按字母序排序(
sort),结果都是"aet"。 -
哈希映射:
- Key :排序后的字符串(
"aet")。 - Value :一个列表(
vector<string>),存放所有排序后等于"aet"的原单词。
- Key :排序后的字符串(
这是哈希表最强大的地方:它的 Value 可以是任何容器!
4.3 C++ 代码实战
cpp
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// key: 排序后的特征字符串, value: 原字符串集合
unordered_map<string, vector<string>> hash;
for (string& s : strs) {
string key = s;
sort(key.begin(), key.end()); // 排序,让全家人长得一样
hash[key].push_back(s); // 自动归类
}
// 提取结果
vector<vector<string>> ret;
for (auto& [key, group] : hash) {
ret.push_back(group);
}
return ret;
}
};
五、 总结:哈希表的威力
💬 复盘:哈希表不仅能存数,更能存"规律"。
- 数组 vs Map :字符集小用数组,离散数据大用
unordered_map。 - 单次遍历:两数之和、重复元素 II 告诉我们,不需要先存后找,边存边找更高效。
- 特征化(Normalization):异位词分组告诉我们,通过排序或计数提取"特征",可以将复杂对象映射到同一个桶里。
这些题目虽然简单,但它们是构建大型系统(如数据库索引、缓存系统)的逻辑基石。