242. 有效的字母异位词
题目描述
给定两个字符串 s 和 t,判断 t 是否是 s 的字母异位词(字符种类和数量完全相同,顺序不同)。假设字符串仅包含小写字母。
- 示例 1:输入
s = "anagram", t = "nagaram",输出true - 示例 2:输入
s = "rat", t = "car",输出false
核心思路
利用数组模拟哈希表(小写字母仅26个,数组大小固定为26),统计字符出现次数:
- 遍历
s,统计每个字符出现次数(数组对应索引+1); - 遍历
t,抵消对应字符的统计次数(数组对应索引-1); - 校验数组是否全为0,全0则为异位词,否则不是。
C++代码(增加长度预判,提升效率)
cpp
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.size() != t.size()) return false; // 长度不同直接排除
int record[26] = {0};
for (int i = 0; i < s.size(); i++) record[s[i] - 'a']++;
for (int i = 0; i < t.size(); i++) record[t[i] - 'a']--;
for (int i = 0; i < 26; i++) if (record[i]) return false;
return true;
}
};
关键说明
- 字符映射 :
s[i] - 'a'将小写字母映射到0-25的数组索引(无需记忆ASCII值,仅需相对位置); - 复杂度:时间 O(n)(仅三次线性遍历),空间 O(1)(固定26长度数组,与输入无关);
- 精简点:去掉冗余注释、合并换行,保留核心逻辑,语法极简且不影响可读性。
总结
- 核心是利用数组实现哈希统计,适配小写字母的有限范围;
- 长度预判是工程常用优化,可避免无效的字符统计;
- 代码精简的核心是保留「统计-抵消-校验」的核心逻辑,剔除冗余语法。
349. 两个数组的交集
题目描述
给定两个数组,计算它们的交集,输出结果中每个元素唯一,且不考虑顺序。
核心思路
本题核心是利用哈希表去重+快速查找 ,因题目补充了数值范围(0≤数值≤1000),优先用数组实现哈希(效率更高);无范围限制时用unordered_set(更通用)。
C++代码
方案1:数组哈希(适配数值范围限制,最优)
cpp
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> res;
int hash[1005] = {0};
for (int num : nums1) hash[num] = 1;
for (int num : nums2) if (hash[num]) res.insert(num);
return vector<int>(res.begin(), res.end());
}
};
方案2:unordered_set(通用版,无数值范围限制)
cpp
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> res, nums_set(nums1.begin(), nums1.end());
for (int num : nums2) if (nums_set.count(num)) res.insert(num);
return vector<int>(res.begin(), res.end());
}
};
关键说明
- 数组哈希:利用0-1000的数值范围,用长度1005的数组标记nums1元素,查找时间O(1),空间更省;
- unordered_set:底层哈希表,读写效率高,无需排序,适配任意数值范围;
- 去重逻辑 :结果用
unordered_set存储,天然去重,最后转vector返回。
复杂度
- 时间:O(m+n)(m、n为两数组长度,仅线性遍历+哈希查找);
- 空间:O(n)(哈希存储nums1元素或结果集)。
总结
- 有数值范围限制时,优先用数组哈希(速度快、空间省);
- 无范围限制时,用
unordered_set(通用灵活); - 核心逻辑:哈希标记存在性 → 遍历另一数组筛选交集 → 去重后返回。
1. 两数之和
题目描述
给定整数数组 nums 和目标值 target,找出数组中两个数之和等于 target 的下标,保证有且仅有一个答案,且同一元素不能重复使用。
- 示例:输入
nums = [2,7,11,15], target = 9,输出[0,1]
核心思路
暴力解法时间复杂度 O(n²),最优解用哈希表(unordered_map) 实现 O(n) 复杂度:
- 哈希表存储「已遍历元素值 → 下标」,避免重复遍历;
- 遍历数组时,计算当前元素的「补数」(
target - nums[i]); - 若补数存在于哈希表中,直接返回补数下标和当前下标;若不存在,将当前元素和下标存入哈希表。
C++代码
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp;
for (int i = 0; i < nums.size(); i++) {
auto iter = mp.find(target - nums[i]);
if (iter != mp.end()) return {iter->second, i};
mp[nums[i]] = i; // 简化insert写法,等价于mp.insert({nums[i], i})
}
return {};
}
};
关键说明
- 哈希表选择 :用
unordered_map(哈希表实现)而非map(红黑树),查询/插入效率 O(1),无需有序性; - 核心逻辑:「空间换时间」,通过哈希表记录已遍历元素,避免内层循环;
- 存储设计:key = 数组元素值(用于快速查找补数),value = 元素下标(用于返回结果)。
复杂度
- 时间:O(n)(仅一次遍历,哈希表查找为O(1));
- 空间:O(n)(最坏情况存储n-1个元素)。
总结
- 用哈希表的核心目的:快速判断「补数是否已出现」,而非暴力遍历;
- 选择
unordered_map是因无需key有序,追求最高读写效率; - 哈希表存储「已遍历元素值-下标」,遍历中实时查询补数,找到即返回。
454. 四数相加II
题目描述
给定四个等长数组 A、B、C、D,计算满足 A[i]+B[j]+C[k]+D[l]=0 的元组 (i,j,k,l) 数量。
- 示例:输入 A=[1,2], B=[-2,-1], C=[-1,2], D=[0,2],输出 2
核心思路
暴力解法时间复杂度 O(n⁴),最优解通过哈希表拆分计算,将复杂度降至 O(n²):
- 拆分四数和为「两数和(A+B)」+「两数和(C+D)=0」,即
A+B = -(C+D); - 先用哈希表统计 A+B 所有可能的和及出现次数;
- 遍历 C+D 的所有和,查找哈希表中是否存在「-(C+D)」,存在则累加对应次数。
C++代码
cpp
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> sum_ab;
// 统计A+B的和及出现次数
for (int a : A) for (int b : B) sum_ab[a + b]++;
int count = 0;
// 查找-(C+D)在sum_ab中的次数并累加
for (int c : C) for (int d : D) count += sum_ab[-c - d];
return count;
}
};
关键说明
- 拆分逻辑:将四数和拆分为两组两数和,避免四层循环,时间复杂度从 O(n⁴) 降至 O(n²);
- 哈希表设计:key = A+B 的和,value = 该和出现的次数(统计所有可能的(i,j)组合数);
- 简化查找 :
-c-d等价于0-(c+d),直接作为key查询哈希表,存在则累加对应次数(即满足条件的组合数)。
复杂度
- 时间:O(n²)(两层循环统计A+B,两层循环统计C+D并查询);
- 空间:O(n²)(最坏情况A+B的和互不重复,哈希表存储n²个键值对)。
总结
- 核心巧思:拆分问题+哈希表统计,用空间换时间;
- 与三数之和/四数之和的区别:本题是四个独立数组,无需去重,哈希法适配性极高;
- 关键逻辑:统计A+B的和→遍历C+D找互补和→累加符合条件的组合数。
15. 三数之和
题目描述
给定整数数组 nums,找出所有和为 0 且不重复的三元组 [a,b,c],要求三元组不能重复,且元素下标互不相同。
- 示例:输入
nums = [-1,0,1,2,-1,-4],输出[[-1,0,1],[-1,-1,2]]
核心思路
哈希法需处理大量去重细节,最优解为排序+双指针,时间复杂度 O(n²)、空间复杂度 O(1)(排序的系统空间不计):
- 先排序数组,便于去重和双指针移动;
- 固定第一个数
a = nums[i],用左指针left = i+1、右指针right = 数组末尾找b + c = -a; - 按三数之和的大小移动指针:和>0则右指针左移,和<0则左指针右移,和=0则记录结果并去重。
C++代码(双指针最优版)
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) break; // 正数无法凑出和为0,直接退出
if (i > 0 && nums[i] == nums[i-1]) continue; // 先手去重a
int left = i + 1, right = nums.size() - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) right--;
else if (sum < 0) left++;
else {
res.push_back({nums[i], nums[left], nums[right]});
// 后手去重b和c
while (right > left && nums[right] == nums[right-1]) right--;
while (right > left && nums[left] == nums[left+1]) left++;
// 找到有效组合,双指针同时收缩
right--;
left++;
}
}
}
return res;
}
};
关键说明
- 排序的作用:① 方便去重;② 可通过指针移动调整三数之和的大小;
- 去重细节 :
- a的去重:跳过与前一个元素相同的nums[i](避免重复三元组);
- b/c的去重:找到有效组合后,跳过连续重复的b/c;
- 剪枝优化:若nums[i]>0,因数组已排序,后续数更大,无法凑出和为0,直接退出循环。
复杂度
- 时间:O(n²)(排序 O(n log n) + 双指针遍历 O(n²),整体由O(n²)主导);
- 空间:O(1)(仅额外使用指针和结果数组,排序的系统空间不计入)。
总结
- 核心逻辑:排序固定a + 双指针找b/c,替代暴力三层循环和复杂的哈希去重;
- 去重关键:a对比前一个元素(先手去重),b/c在找到有效组合后跳过重复值(后手去重);
- 剪枝优化:正数直接退出,减少无效遍历。
18. 四数之和
题目描述
给定整数数组 nums 和目标值 target,找出所有和为 target 且不重复的四元组 [a,b,c,d],答案中不可包含重复四元组。
- 示例:输入
nums = [1,0,-1,0,-2,2], target = 0,输出[[-1,0,0,1],[-2,-1,1,2],[-2,0,0,2]]
核心思路
在三数之和的「排序+双指针」基础上嵌套一层for循环,将时间复杂度从暴力O(n⁴)降至O(n³):
- 排序数组,便于去重和指针移动;
- 两层for循环固定前两个数
a=nums[k]、b=nums[i]; - 双指针
left=i+1、right=数组末尾找c+d = target - a - b; - 增加剪枝和去重逻辑,避免无效遍历和重复四元组。
C++代码
cpp
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {//靠left<right约束即可,无需-3
// 一级剪枝:正数且超过target则无需继续
if (nums[k] > target && nums[k] >= 0) break;
// 去重a
if (k > 0 && nums[k] == nums[k-1]) continue;
// 未找到元素时去重用if且永远跟前一个比较过的比↑↑↑
for (int i = k+1; i < nums.size(); i++) {//靠left<right约束即可,无需-2
// 二级剪枝:前两数和超过target且非负则无需继续
if (nums[k]+nums[i] > target && nums[i] >= 0) break;
// 去重b
if (i > k+1 && nums[i] == nums[i-1]) continue;
// 未找到元素时去重用if且永远跟前一个比较过的比↑↑↑
int left = i+1, right = nums.size()-1;
while (right > left) {
// 用long避免溢出
long sum = (long)nums[k] + nums[i] + nums[left] + nums[right];
if (sum > target) right--;
else if (sum < target) left++;
else {
res.push_back({nums[k], nums[i], nums[left], nums[right]});
// 去重c、d(找到元素时去重用while且永远跟后一个比较过的比↓↓↓)
while (right>left && nums[right]==nums[right-1]) right--;
while (right>left && nums[left]==nums[left+1]) left++;
// 收缩指针
right--;
left++;
}
}
}
}
return res;
}
};
关键说明
若第一个数为正且超过target,后续数更大,直接break;
若前两数和为正且超过target,后续数更大,直接break;
- a去重:跳过与前一个相同的nums[k](k>0时);
- b去重:跳过与前一个相同的nums[i](i>k+1时,避免误删k=i-1的情况);
- c/d去重:找到有效组合后,跳过连续重复的c/d;
- 溢出处理 :用
long类型存储四数之和,避免int溢出。
关键说明
- 剪枝优化 :
- 一级剪枝:若第一个数为正且超过 target,因数组已排序后续数更大,无法凑出 target,直接 break;
- 二级剪枝:若第一个数和第二个数的和为正且超过 target,因数组已排序后续数更大,无法凑出 target,直接 break;
- 去重逻辑 :
- a 去重:枚举第一个数时,跳过与前一个相同的 nums [k](k>0 时),避免重复枚举;
- b 去重:枚举第二个数时,跳过与前一个相同的 nums [i](i>k+1 时),避免误删 k=i-1 的合法情况;
- c/d 去重:找到有效四数组合后,用 while 循环跳过 left/right 指针指向的连续重复元素,避免重复添加相同组合;
- 溢出处理 :用long类型存储四数之和,避免四个 int 类型数值相加超出 int 范围导致的计算错误;
循环范围约束:外层 / 内层循环无需限制k < nums.size()-3、i < nums.size()-2,仅靠内部双指针right > left即可约束四数的有效范围,避免漏解;
去重语法差异:枚举阶段(k/i)的去重使用if判断(仅跳过一次重复),找到组合后的去重使用while循环(跳过所有连续重复),逻辑更精准。
复杂度
- 时间:O(n³)(排序O(n log n) + 三层遍历O(n³),整体由O(n³)主导);
- 空间:O(1)(仅额外使用指针和结果数组,排序的系统空间不计入)。
总结
- 核心逻辑:排序 + 两层for固定前两数 + 双指针找后两数,复用三数之和的双指针思路;
- 关键细节:剪枝要考虑target为负数的情况,去重需避免误删有效组合,求和时防止溢出;
- 扩展:五数/六数之和可沿用「多一层for循环 + 双指针」的思路,时间复杂度递增一个量级。