题目链接
题目描述

题目解析


这段代码是 两数之和问题的最优解法(哈希表版),核心思路是「用空间换时间」,将时间复杂度从暴力解法的 O (n²) 优化到 O (n)。下面逐行拆解逻辑、核心原理和细节设计:
一、整体逻辑框架
- 哈希表作用:存储「数组元素值 → 元素下标」的映射,利用哈希表 O (1) 的查找效率,快速判断「当前元素的互补值」是否已在数组中出现过。
- 核心流程 :遍历数组时,对每个元素计算「互补值」(目标值 - 当前元素),检查互补值是否在哈希表中:
- 若存在:直接返回「互补值的下标」和「当前元素的下标」(这两个就是和为目标值的两个数)。
- 若不存在:将当前元素和其下标存入哈希表,继续遍历。
二、逐行代码解析
1. 类与函数定义
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
- 遵循 LeetCode 标准接口:
Solution类包含twoSum成员函数。 - 输入参数:
vector<int>& nums:待查找的整数数组(引用传递,避免拷贝开销)。int target:目标和。
- 返回值:
vector<int>:存储两个符合条件的元素下标。
2. 哈希表初始化
cpp
unordered_map<int, int> hash; // 键:数组元素值,值:元素下标
- 选择
unordered_map(C++ 哈希表容器)而非map:unordered_map:基于哈希表实现,查找、插入效率均为 O (1)(最优)。map:基于红黑树实现,查找效率 O (log n),效率低于哈希表。
- 键值对设计:
- 键(key):存数组中的「元素值」------ 因为我们需要通过「互补值」快速查找对应的下标。
- 值(value):存元素的「下标」------ 最终要返回的是下标,而非元素值。
3. 遍历数组
cpp
int n = nums.size();
for (int i = 0; i < n; ++i) {
- 先获取数组长度
n,避免循环中重复调用nums.size()(微小优化,不影响正确性)。 - 循环变量
i:当前遍历元素的下标,从 0 到 n-1 遍历所有元素。
4. 计算互补值
cpp
int x = target - nums[i]; // 计算当前元素的互补值
- 互补值定义:
x = target - nums[i]。含义:如果存在一个数x已经在数组中,且x + nums[i] = target,那么x和nums[i]就是我们要找的两个数。 - 举例:若
nums[i] = 2、target = 9,则x = 7------ 只要数组中存在 7,且 7 的下标不等于i,就满足条件。
5. 检查互补值是否存在
cpp
if (hash.find(x) != hash.end()) {
return {hash[x], i};
}
hash.find(x):在哈希表中查找键为x(互补值)的元素。- 若找到:返回一个迭代器,指向键值对
(x, 下标)。 - 若没找到:返回
hash.end()(哈希表的尾后迭代器,标志查找失败)。
- 若找到:返回一个迭代器,指向键值对
- 逻辑成立时(找到互补值):
hash[x]:通过键x取出对应的值(即互补值在数组中的下标)。i:当前元素的下标。- 直接返回
{hash[x], i}:这两个下标就是答案(顺序不影响,题目允许任意顺序返回)。
6. 存入当前元素到哈希表
cpp
hash[nums[i]] = i;
- 若互补值
x不在哈希表中,说明当前元素暂时没有找到匹配的伙伴,将其「值 - 下标」存入哈希表,供后续元素查找。 - 关键细节:先检查,再存入 (避免重复使用同一元素):
- 例如数组
[3,3]、target=6:- 遍历第一个 3(i=0):互补值 3 不在哈希表,存入
{3:0}。 - 遍历第二个 3(i=1):互补值 3 在哈希表中(下标 0),直接返回
[0,1]。
- 遍历第一个 3(i=0):互补值 3 不在哈希表,存入
- 若反过来「先存入再检查」,会导致用当前元素和自己匹配(如 i=0 时,存入 3 后检查互补值 3,返回
[0,0],违反「不能使用两次相同元素」的规则)。
- 例如数组
三、核心原理与优势
1. 为什么效率高?
- 时间复杂度 O (n):仅遍历数组一次(n 是数组长度),每次遍历中的「查找」和「插入」操作都是 O (1)(哈希表特性)。
- 空间复杂度 O (n):最坏情况下,哈希表需要存储数组中所有元素(如答案在数组最后两个元素),额外占用 O (n) 空间。
2. 如何避免「重复使用同一元素」?
- 关键在于「先检查互补值,再存入当前元素」:
- 遍历到
nums[i]时,哈希表中存储的是「前 i-1 个元素」的映射,因此互补值x一定是来自「之前的元素」,而非当前元素本身。 - 确保两个下标
hash[x]和i是不同的(即不重复使用同一元素)。
- 遍历到
3. 为什么能处理「重复元素」?
- 例如示例 3:
nums = [3,3], target=6:- 第一个 3 存入哈希表后,第二个 3 遍历到的时,互补值 3 已存在(下标 0),直接返回
[0,1]。 - 哈希表会覆盖相同键的值吗?会,但这里不影响:因为题目保证「唯一答案」,重复元素的答案必然是「前一个重复元素 + 后一个重复元素」,不会出现需要前一个重复元素匹配更早元素的情况。
- 第一个 3 存入哈希表后,第二个 3 遍历到的时,互补值 3 已存在(下标 0),直接返回
四、测试用例验证(结合代码流程)
以示例 2 为例:nums = [3,2,4], target=6
- 初始化哈希表
hash = {},n=3。 - 遍历
i=0(nums[0]=3):- 互补值
x = 6-3=3。 hash.find(3)→ 未找到(hash为空)。- 存入
hash[3] = 0,此时hash = {3:0}。
- 互补值
- 遍历
i=1(nums[1]=2):- 互补值
x = 6-2=4。 hash.find(4)→ 未找到。- 存入
hash[2] = 1,此时hash = {3:0, 2:1}。
- 互补值
- 遍历
i=2(nums[2]=4):- 互补值
x = 6-4=2。 hash.find(2)→ 找到(键 2 对应的值是 1)。- 返回
{1, 2},符合示例答案。
- 互补值
五、总结
这段代码的设计非常精炼,核心亮点:
- 用
unordered_map实现 O (1) 查找,将时间复杂度优化到最优。 - 「先检查互补值,再存入当前元素」的逻辑,既避免重复使用同一元素,又能处理重复元素的场景。
- 代码简洁,无冗余操作,完全符合 LeetCode 题目的约束条件(唯一答案、不重复使用元素)。
它是解决「两数之和」问题的工业级最优解法,适用于所有数组规模(包括大规模数组)。
题目链接
题目描述

题目解析


这段代码是 字符计数法的优化版,核心思路和基础版一致(通过统计字符出现次数判断是否可重排),但更简洁高效 ------ 仅用一个哈希数组完成统计与校验,减少了数组占用的空间(从两个数组变为一个),且提前终止无效情况。下面逐行拆解逻辑、优化点和核心细节:
一、整体逻辑框架
- 长度预判 :先判断两个字符串长度是否一致,不一致直接返回
false(字符总数不同,不可能重排相等)。 - 单哈希数组统计 :用一个大小为 26 的数组
hash统计字符出现次数:- 第一步:遍历
s1,对每个字符的计数「加 1」(记录s1中每种字符的总数量)。 - 第二步:遍历
s2,对每个字符的计数「减 1」(用s2的字符去 "抵消"s1的计数)。
- 第一步:遍历
- 实时校验 :遍历
s2时,每次减 1 后检查计数是否为负数:- 若出现负数:说明
s2中该字符的出现次数 超过 了s1,直接返回false。 - 遍历结束无负数:说明
s2与s1的字符种类和数量完全一致,返回true。
- 若出现负数:说明
二、逐行代码解析
1. 长度预判(剪枝优化)
cpp
if(s1.size() != s2.size()) return false;
- 核心逻辑:两个字符串要能通过重排相等,字符总数必须完全相同(长度一致是必要条件)。
- 作用:提前排除无效情况,避免后续无意义的遍历(例如
s1="a"、s2="ab"直接返回false),提升效率。 - 注意:这是「必要不充分条件」------ 长度相同不代表一定可重排,但长度不同一定不可重排。
2. 哈希数组初始化
cpp
int hash[26] = {0};
- 数组大小为 26:对应 26 个小写字母(
a-z),索引与字符的映射关系为ch - 'a'(例如'a'→0、'b'→1、...、'z'→25)。 - 初始值为 0:用栈上的普通数组(而非
vector),占用空间更小(26 个 int 仅 104 字节),访问速度更快。 - 优化点:相比基础版的「两个数组」,这里只用一个数组,减少了空间开销(虽然基础版也是 O (1),但进一步精简)。
3. 遍历 s1 统计字符(计数加 1)
cpp
for(auto ch : s1) {
hash[ch - 'a']++;
}
- 遍历
s1的每个字符ch:- 计算字符对应的索引:
ch - 'a'(将字符a-z转化为 0-25 的整数索引)。 - 计数加 1:
hash[索引]++表示该字符在s1中多出现一次。
- 计算字符对应的索引:
- 示例:
s1="abc"→ 遍历后hash[0]=1('a')、hash[1]=1('b')、hash[2]=1('c'),其余为 0。
4. 遍历 s2 校验字符(计数减 1 + 实时判断)
cpp
for(auto ch : s2) {
hash[ch - 'a']--;
if(hash[ch - 'a'] < 0) return false;
}
- 这是代码的「核心优化点」:边遍历
s2边校验,无需后续单独对比数组,提前终止无效情况。 - 分步解析:
- 对
s2的当前字符ch,计算索引后将计数「减 1」(用s2的字符抵消s1的计数)。 - 检查计数是否小于 0:
- 若
hash[ch-'a'] < 0:说明s2中该字符的出现次数 超过了s1(例如s1="abc"、s2="aab",遍历第二个 'a' 时,hash[0]从 0 减为 -1),此时直接返回false(不可能重排)。
- 若
- 对
- 示例 1(有效):
s2="bca"(s1="abc"):- 遍历 'b':
hash[1]--→ 0(无负数)。 - 遍历 'c':
hash[2]--→ 0(无负数)。 - 遍历 'a':
hash[0]--→ 0(无负数)。遍历结束返回true。
- 遍历 'b':
- 示例 2(无效):
s2="bad"(s1="abc"):- 遍历 'b':
hash[1]--→ 0。 - 遍历 'a':
hash[0]--→ 0。 - 遍历 'd':
hash[3]--→ -1(s1中无 'd',计数直接变负),返回false。
- 遍历 'b':
5. 最终返回
cpp
return true;
- 遍历
s2结束后,若未出现任何计数为负的情况:- 说明
s2中所有字符的出现次数 都不超过s1。 - 又因为两个字符串长度相同(第一步已校验),所以
s2中每个字符的出现次数 必然与s1完全一致(总次数相同,且无字符多出现)。 - 因此返回
true,表示两个字符串可重排互为对方。
- 说明
2. 为什么「计数为负」就能直接返回 false?
- 假设
hash[ch-'a']减 1 后为负,说明s2中ch的出现次数 大于s1中ch的出现次数(例如s1有 1 个 'a',s2有 2 个 'a')。 - 这种情况下,无论怎么重排,
s2的 'a' 都多一个,不可能和s1相等,因此直接提前终止,避免后续无效遍历。
3. 为什么遍历结束后不用再检查数组是否全为 0?
- 因为第一步已保证
s1和s2长度相同(字符总数相同):- 遍历
s1时,hash数组的「总和」等于s1的长度(每个字符加 1,总加次数 =s1.size())。 - 遍历
s2时,hash数组的「总和」减少s2.size()(每个字符减 1,总减次数 =s2.size())。 - 由于
s1.size() == s2.size(),最终hash数组的「总和」必然为 0。
- 遍历
- 又因为遍历
s2时已保证「所有计数都不为负」,总和为 0 且无负数 → 所有计数必然为 0(例如[0,0,...,0])。 - 因此无需额外检查,直接返回
true即可。
四、边界情况验证
- 空字符串 :
s1=""、s2=""→ 长度相同,hash数组始终全 0 → 返回true。 - 重复字符 :
s1="aab"、s2="aba"→ 遍历s1后hash[0]=2、hash[1]=1;遍历s2时:- 'a' →
hash[0]=1(≥0);'b' →hash[1]=0(≥0);'a' →hash[0]=0(≥0)→ 返回true。
- 'a' →
- 字符数量超出 :
s1="aab"、s2="aaa"→ 遍历s2第三个 'a' 时,hash[0] = 2-3 = -1→ 返回false。 - 字符种类不同 :
s1="abc"、s2="abd"→ 遍历s2的 'd' 时,hash[3] = 0-1 = -1→ 返回false。
五、总结
这段代码是「字符计数法」的最优实现之一,核心亮点:
- 空间最优:仅用一个固定大小的数组(O (1) 空间),无额外开销。
- 时间高效:遍历两次字符串(O (n) 时间),且提前终止无效情况,减少不必要的循环。
- 逻辑精简:利用「长度相等」和「计数非负」的隐含条件,省去后续数组校验步骤,代码更简洁。
它充分利用了题目「仅小写字母」的约束,是面试中既高效又易写的最优解。
题目链接
题目描述

题目解析
