[优选算法专题十.哈希表 ——NO.55~57 两数之和、判定是否互为字符重排、存在重复元素]

题目链接

1. 两数之和

题目描述

题目解析

这段代码是 两数之和问题的最优解法(哈希表版),核心思路是「用空间换时间」,将时间复杂度从暴力解法的 O (n²) 优化到 O (n)。下面逐行拆解逻辑、核心原理和细节设计:

一、整体逻辑框架

  1. 哈希表作用:存储「数组元素值 → 元素下标」的映射,利用哈希表 O (1) 的查找效率,快速判断「当前元素的互补值」是否已在数组中出现过。
  2. 核心流程 :遍历数组时,对每个元素计算「互补值」(目标值 - 当前元素),检查互补值是否在哈希表中:
    • 若存在:直接返回「互补值的下标」和「当前元素的下标」(这两个就是和为目标值的两个数)。
    • 若不存在:将当前元素和其下标存入哈希表,继续遍历。

二、逐行代码解析

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,那么 xnums[i] 就是我们要找的两个数。
  • 举例:若 nums[i] = 2target = 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]
    • 若反过来「先存入再检查」,会导致用当前元素和自己匹配(如 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]
    • 哈希表会覆盖相同键的值吗?会,但这里不影响:因为题目保证「唯一答案」,重复元素的答案必然是「前一个重复元素 + 后一个重复元素」,不会出现需要前一个重复元素匹配更早元素的情况。

四、测试用例验证(结合代码流程)

以示例 2 为例:nums = [3,2,4], target=6

  1. 初始化哈希表 hash = {}n=3
  2. 遍历 i=0nums[0]=3):
    • 互补值 x = 6-3=3
    • hash.find(3) → 未找到(hash 为空)。
    • 存入 hash[3] = 0,此时 hash = {3:0}
  3. 遍历 i=1nums[1]=2):
    • 互补值 x = 6-2=4
    • hash.find(4) → 未找到。
    • 存入 hash[2] = 1,此时 hash = {3:0, 2:1}
  4. 遍历 i=2nums[2]=4):
    • 互补值 x = 6-4=2
    • hash.find(2) → 找到(键 2 对应的值是 1)。
    • 返回 {1, 2},符合示例答案。

五、总结

这段代码的设计非常精炼,核心亮点:

  1. unordered_map 实现 O (1) 查找,将时间复杂度优化到最优。
  2. 「先检查互补值,再存入当前元素」的逻辑,既避免重复使用同一元素,又能处理重复元素的场景。
  3. 代码简洁,无冗余操作,完全符合 LeetCode 题目的约束条件(唯一答案、不重复使用元素)。

它是解决「两数之和」问题的工业级最优解法,适用于所有数组规模(包括大规模数组)。


题目链接

面试题 01.02. 判定是否互为字符重排

题目描述

题目解析

这段代码是 字符计数法的优化版,核心思路和基础版一致(通过统计字符出现次数判断是否可重排),但更简洁高效 ------ 仅用一个哈希数组完成统计与校验,减少了数组占用的空间(从两个数组变为一个),且提前终止无效情况。下面逐行拆解逻辑、优化点和核心细节:

一、整体逻辑框架

  1. 长度预判 :先判断两个字符串长度是否一致,不一致直接返回 false(字符总数不同,不可能重排相等)。
  2. 单哈希数组统计 :用一个大小为 26 的数组 hash 统计字符出现次数:
    • 第一步:遍历 s1,对每个字符的计数「加 1」(记录 s1 中每种字符的总数量)。
    • 第二步:遍历 s2,对每个字符的计数「减 1」(用 s2 的字符去 "抵消" s1 的计数)。
  3. 实时校验 :遍历 s2 时,每次减 1 后检查计数是否为负数:
    • 若出现负数:说明 s2 中该字符的出现次数 超过s1,直接返回 false
    • 遍历结束无负数:说明 s2s1 的字符种类和数量完全一致,返回 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 边校验,无需后续单独对比数组,提前终止无效情况。
  • 分步解析:
    1. s2 的当前字符 ch,计算索引后将计数「减 1」(用 s2 的字符抵消 s1 的计数)。
    2. 检查计数是否小于 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
  • 示例 2(无效):s2="bad"s1="abc"):
    • 遍历 'b':hash[1]-- → 0。
    • 遍历 'a':hash[0]-- → 0。
    • 遍历 'd':hash[3]-- → -1(s1 中无 'd',计数直接变负),返回 false
5. 最终返回
cpp 复制代码
return true;
  • 遍历 s2 结束后,若未出现任何计数为负的情况:
    • 说明 s2 中所有字符的出现次数 都不超过 s1
    • 又因为两个字符串长度相同(第一步已校验),所以 s2 中每个字符的出现次数 必然与 s1 完全一致(总次数相同,且无字符多出现)。
    • 因此返回 true,表示两个字符串可重排互为对方。
2. 为什么「计数为负」就能直接返回 false
  • 假设 hash[ch-'a'] 减 1 后为负,说明 s2ch 的出现次数 大于 s1ch 的出现次数(例如 s1 有 1 个 'a',s2 有 2 个 'a')。
  • 这种情况下,无论怎么重排,s2 的 'a' 都多一个,不可能和 s1 相等,因此直接提前终止,避免后续无效遍历。
3. 为什么遍历结束后不用再检查数组是否全为 0?
  • 因为第一步已保证 s1s2 长度相同(字符总数相同):
    • 遍历 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 即可。

四、边界情况验证

  1. 空字符串s1=""s2="" → 长度相同,hash 数组始终全 0 → 返回 true
  2. 重复字符s1="aab"s2="aba" → 遍历 s1hash[0]=2hash[1]=1;遍历 s2 时:
    • 'a' → hash[0]=1(≥0);'b' → hash[1]=0(≥0);'a' → hash[0]=0(≥0)→ 返回 true
  3. 字符数量超出s1="aab"s2="aaa" → 遍历 s2 第三个 'a' 时,hash[0] = 2-3 = -1 → 返回 false
  4. 字符种类不同s1="abc"s2="abd" → 遍历 s2 的 'd' 时,hash[3] = 0-1 = -1 → 返回 false

五、总结

这段代码是「字符计数法」的最优实现之一,核心亮点:

  1. 空间最优:仅用一个固定大小的数组(O (1) 空间),无额外开销。
  2. 时间高效:遍历两次字符串(O (n) 时间),且提前终止无效情况,减少不必要的循环。
  3. 逻辑精简:利用「长度相等」和「计数非负」的隐含条件,省去后续数组校验步骤,代码更简洁。

它充分利用了题目「仅小写字母」的约束,是面试中既高效又易写的最优解。


题目链接

217. 存在重复元素

题目描述

题目解析

相关推荐
稚辉君.MCA_P8_Java2 小时前
Gemini永久会员 go数组中最大异或值
数据结构·后端·算法·golang·哈希算法
会员果汁2 小时前
双向链式队列-C语言
c语言·数据结构
AI科技星3 小时前
张祥前统一场论:引力场与磁矢势的关联,反引力场生成及拉格朗日点解析(网友问题解答)
开发语言·数据结构·经验分享·线性代数·算法
C雨后彩虹3 小时前
最少交换次数
java·数据结构·算法·华为·面试
-森屿安年-3 小时前
二叉平衡树的实现
开发语言·数据结构·c++
稚辉君.MCA_P8_Java3 小时前
Gemini永久会员 Go 返回最长有效子串长度
数据结构·后端·算法·golang
TL滕3 小时前
从0开始学算法——第五天(初级排序算法)
数据结构·笔记·学习·算法·排序算法
Ayanami_Reii3 小时前
进阶数据结构应用-线段树扫描线
数据结构·算法·线段树·树状数组·离散化·fenwick tree·线段树扫描线
浅川.254 小时前
xtuoj 素数个数
数据结构·算法