常见的三种哈希结构分为:数组、set、map
在 C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set 底层实现为哈希表,std::set 和 std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以 key 值是有序的,但 key 不可以修改,改动 key 值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key 有序 | key 不可重复 | key 不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key 有序 | key 可重复 | key 不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key 无序 | key 不可重复 | key 不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和 std::multimap 的底层实现是红黑树。同理,std::map 和 std::multimap 的 key 也是有序的。
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
#include
#include
using namespace std;
int main() {
unordered_set<int> uset;
// 插入元素
uset.insert(1);
uset.insert(2);
uset.insert(3);
// 删除元素
uset.erase(2);
// 查找元素
auto it = uset.find(3);
if (it != uset.end()) {
cout << "Found " << *it << endl;
}
// 获取元素个数
cout << "Size: " << uset.size() << endl;
// 判断元素是否存在
if (uset.count(2)) {
cout << "2 exists" << endl;
} else {
cout << "2 does not exist" << endl;
}
// 清空元素
uset.clear();
// 判断是否为空
if (uset.empty()) {
cout << "Empty set" << endl;
}
// 遍历元素
for (auto x : uset) {
cout << x << " ";
}
cout << endl;
return 0;
}
#include
#include // 引入 unordered_map 头文件
using namespace std;
int main() {
// 创建一个空的 unordered_map
unordered_map<int, string> myMap;
// 添加键值对(insert 或 emplace)
myMap.insert({1, "apple"});
myMap.emplace(2, "banana");
// 使用 [] 运算符访问元素,注意:如果元素不存在,会自动创建一个默认值
cout << myMap[1] << endl; // 输出:apple
// 查找并访问元素,使用 find 方法获取迭代器进行访问,注意:如果没有找到,返回值为 end()
auto it = myMap.find(2);
if (it != myMap.end()) {
cout << it->second << endl; // 输出:banana
}
// 遍历 unordered_map
for (auto& [key, value]: myMap) {
cout << "key: " << key << ", value: " << value << endl;
}
// 判断 unordered_map 是否为空
if (myMap.empty()) {
cout << "myMap is empty" << endl;
} else {
cout << "myMap has " << myMap.size() << " elements" << endl;
}
// 删除元素,使用 erase 方法传入键
myMap.erase(1);
return 0;
}
本节对应代码随想录中:代码随想录,讲解视频:学透哈希表,数组使用有技巧!Leetcode:242.有效的字母异位词_哔哩哔哩_bilibili
题目链接:242. 有效的字母异位词 - 力扣(LeetCode)
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词,s 和 t 仅包含小写字母。
示例 1:
输入: s = “anagram”, t = “nagaram”
输出: true
示例 2:
输入: s = “rat”, t = “car”
输出: false
进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
先对两个字符串排序,然后就可以直接比较两个字符串是否相等
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
};
既然 s 和 t 都仅包含小写字母,那就可以分别定义两个数组,用来存对应字符的出现次数,如果两个数组对应位置的数值一样那就是 true。同时如果两个字符串长度不同,可以直接返回 false
class Solution {
public:
bool isAnagram(string s, string t) {
// 两个字符若长度不等直接返回false
int size = s.length();
if (size != t.length()) {
return false;
}
int num_s[26] = {0};
int num_t[26] = {0};
// 遍历字符串,两个计数的数组计数
for (int i = 0; i < size; i++) {
num_s[int(s[i]) - 97] += 1;
num_t[int(t[i]) - 97] += 1;
}
// 一旦某一个字符的次数不一样直接返回false
for (int i = 0; i < 26; i++) {
if (num_s[i] != num_t[i]) {
return false;
}
}
return true;
}
};
可以优化的点
int(s[i]) - 97
可以替换为 s[i] - 'a'
,这样无需记住字符 a 的 ASCII其实我的解法也属于哈希表,不过其实不需要两个数组来计数,只需要一个数组统计其中一个字符串,然后遍历另一个字符串的时候对应位置的数值减1,如果对应位置数值<0,说明 t 的这个位置的元素比 s 的多,则返回 false
例如,s 为 aab,t 为 aaa。遍历到 t 的第二个 a 的时候,table[0]已经等于0了,当遍历到 t 的第三个 a 的时候,table[0]=-1<0,说明 t 中的 a 比 s 中的 a 至少多一个(后面可能还会有更多的 a,但没必要再去遍历了)。
代码随想录中是先遍历 t,对应位置减1,再判断数组是否全为0,但下面这种解法只需使用两个 for 循环,比代码随想录上的3个 for 循环更优
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
int table[26] = {0};
// 数组计数
for (int i = 0; i < s.length(); i++) {
table[s[i] - 'a']++;
}
for (int i = 0; i < t.length(); i++) {
// 遍历字符串t对应位置减1
table[t[i] - 'a']--;
// 如果<0说明这个位置t的比s的多,即多减了一次
if (table[t[i] - 'a'] < 0) {
return false;
}
}
return true;
}
};
如果输入字符串包含 unicode 字符,那么可能的字符就不再是26个,没法使用大小为26的数组进行计数。此时就需要使用 unordered_map 了。
思路和上面的哈希表的差不多,先遍历 s,将其每个元素放进哈希表中。再遍历字符串 t,如果某个元素的次数<0说明 t 的这个位置的元素比 s 的多,则返回 false。只不过哈希表解法计数是根据字符-a 找索引,而这个是根据 key 直接找索引
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.size() != t.size()) { // 先判断两个字符串长度是否相同
return false;
}
unordered_map<char, int> hash; // 定义哈希表
for (int i = 0; i < s.size(); i++) { // 遍历一个字符串
hash[s[i]]++; // 把s的每个字符放进哈希表中
}
for (int i = 0; i < t.size(); i++) {
hash[t[i]]--; // 在hash表中抵消该字符次数
if (hash[t[i]] < 0) {
return false;
}
}
return true; // 若出现次数都一致,则返回true
}
};
本节对应代码随想录中:代码随想录,讲解视频:学透哈希表,set使用有技巧!Leetcode:349. 两个数组的交集_哔哩哔哩_bilibili
题目链接:349. 两个数组的交集 - 力扣(LeetCode)
给定两个数组 nums1 和 nums2 ,返回它们的交集。输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
这题我是用的上题进阶解法用到的 unordered_map,只不过由于取交集,如果有重复的只输出一次即可。所以我在存的时候让 nums1对应的次数都为1,遍历 nums2时,如果次数为1则 push_back 并且次数+1,这样就不会重复输出。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> hash;
vector<int> res;
// 不同的数只放一次
for (int i = 0; i < nums1.size(); i++) {
hash[nums1[i]] = 1;
}
for (int i = 0; i < nums2.size(); i++) {
// 次数为1说明重叠,加1后后面就不会push_back同样的元素
if (hash[nums2[i]] == 1) {
res.push_back(nums2[i]);
hash[nums2[i]]++;
}
}
return res;
}
};
这道题由于去重,所以其实用 set 更合适。
代码随想录中用了两个 set,一个遍历 nums1,另一个存放结果(这样结果就会去重)。但其实只用一个 set 即可,用一个 set 遍历 nums1后,再去遍历 nums2时,判断元素是否出现,若出现则 push_back 并且 erase 即删除这个元素。这样例如示例1中后面即使有重复的2,但我们在遍历第一个2进行 push_back 后就将 set 中的2删除了,遇到第二个2的时候其实就是找不到的状态了,所以就不会重复输出。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> nums_set(nums1.begin(), nums1.end());
vector<int> res;
for (int num : nums2) {
// 如果nums_set包含num,则num是交集之一,加入结果并从集合中删除num
if (nums_set.erase(num)) {
res.push_back(num);
}
}
return res;
}
};
本节对应代码随想录中:代码随想录,讲解视频:暂无
题目链接:202. 快乐数 - 力扣(LeetCode)
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
示例 1:
输入:n = 19 输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
刚开始看到题目后,一直纠结于题目中说的可能无限循环。拆分数字并判断和是否为1并不困难,但如果程序可能陷入无限循环,那一直找不到结果就会超时,但是和一直不为1,又无法确定后面的和是否会为1。
没有思路的时候可以输出下每个数的和,看看整个过程,代码如下
int n = 12;
for (int i = 0; i < 20; i++) {
int sum = 0;
while (n != 0) {
sum += (n % 10) * (n % 10);
n /= 10;
}
cout << sum << "#";
n = sum;
}
每次会输出数字的和,共输出20次
当 n=12时,会输 5#25#29#85#89#145#42#20#4#16#37#58#89#145#42#20#4#16#37#58#
,注意看数字出现了循环。#89#145#42#20#4#16#37#58#
从89开始就有了这一段循环。既然出现循环,说明当计算的和为89的时候,程序计算一段时间和后又会回到89。也就是说,如果一个数字出现了第二次,那么在这个数前面一定存在一段循环,那这个数字 n 一定不是快乐数,因此就可以用 unordered_set
来判断元素是否重复出现,若重复出现则可以返回 false,终止这个无限循环。
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> mySet;
while (1) {
int sum = 0;
// 计算每个位置上的数字的平方和
while (n != 0) {
sum += (n % 10) * (n % 10);
n /= 10;
}
// 和为1则说明是快乐数
if (sum == 1) {
return true;
}
// 如果set中存在sum,说明一定会陷入循环中,即不可能是快乐数
if (mySet.count(sum)) {
return false;
}
mySet.insert(sum);
n = sum; // 将sum赋值给n,继续计算数字n每个位置上的数字的平方和
}
}
};
本节对应代码随想录中:代码随想录,讲解视频:梦开始的地方,Leetcode:1.两数之和,学透哈希表,map使用有技巧!_哔哩哔哩_bilibili
题目链接:1. 两数之和 - 力扣(LeetCode)
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案,并且只会存在一个有效答案
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
作为 LeetCode 的第一题,题意很明确,就是数组中找和为 target 的两个数,返回他们的下标,并且一定能找到这俩数,并且答案唯一(顺序可以不一样)。
最直接的就是两个 for 循环遍历一个数的时候看看剩余的数里面有没有满足和为 target 的
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int size = nums.size();
vector<int> res;
for (int i = 0; i < size; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target) {
res.push_back(i);
res.push_back(j);
return res;
}
}
}
return res;
}
};
可以优化的点
return {i,j}
即可,这样空间复杂度就优化成 O(1)上面的暴力解法,就是先遍历一个数,然后再查找剩余的数中有没有 target-nums[i]。那就简化成了在数组中查找一个元素是否存在的问题,所以就可以使用哈希表,因为哈希表查找元素的时间复杂度为 O(1)。
而哈希表有3种:数组、set 和 map,只有 set 和 map 是有 O(1)的 find 函数的。但是 set 只存储 key,而题目要求返回的是下标,set 无法直接得到原来的数的下标。
STL 中有个 distance 函数可以计算两个元素之间的距离,计算找到的元素和 set.begin()之间的距离就可以获得下标吗?
答:这种方法无法获得原有的下标,使用 unordered_set 插入元素时不会保留原始顺序,而是根据哈希表中元素的内部顺序随机排序。也就是说,即使向 set 中添加与 vector 中相同的元素,它们在 set 中的顺序也可能与在 v1中的顺序不同。例如示例中的{2,7,11,15}在 unordered_set 中就会变成{11,7,15,2}的顺序
map 中只有 unordered_map 的查询效率为 O(1),那么 key 和 value 存什么呢?map 查找的时候查的是 key,而我们要找的是 target-nums[i]即查的是元素本身。那么 key 存的就是数组中的元素,而 value 存的就是对应的下标
我的写法如下
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 创建一个无序哈希表map,key为整数型,value为整数型
unordered_map<int, int> map;
// 遍历nums中的元素,将元素值作为键,元素下标作为值插入到map中
for (int i = 0; i < nums.size(); i++) {
map.insert({nums[i], i});
}
for (int i = 0; i < nums.size(); i++) {
int tmp = target - nums[i];
// 判断map中是否含有该差值,如果存在且满足条件则返回下标值
if (map.find(tmp) != map.end()) {
if (i != map[tmp]) {
return {i, map[tmp]};
}
}
}
return {};
}
};
而看过题解后,上面的写法可以有优化的地方。我是先将所有元素插入到 map 中,而实际上没有必要先插入所有元素。只需要当没有 target - nums[i]的时候再将当前 nums[i]插入到 map 中,这样也避免了和自己匹配即2*nums[i]=target。如2,7,11,15,target=9,当 i=0时,虽然没有找到 map 中含有7。但是将2插入 map 后,当 i=1即去找 map 中是否含有2的时候就可以找到了。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); ++i) {
auto it = map.find(target - nums[i]);
if (it != map.end()) {
// 如果找到了,返回这两个元素的下标
return {it->second, i};
}
// 否则将当前的数插入哈希表
map[nums[i]] = i;
}
return {};
}
};
本节对应代码随想录中:代码随想录,讲解视频:学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili
题目链接:454. 四数相加 II - 力扣(LeetCode)
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
暴力解法会超时,这里只作为参考
题目意思简单来说就是四个数组分别找出一个数,让它们相加为0。最直接的就是四个 for 循环遍历每个数组,只要和为0,那就加1。但是四重 for 循环的时间复杂度是 O(n^4),肯定会超时。
那我们可不可以把四重 for 循环降到三重 for 循环?
我们这章学的是哈希表,而哈希表查找指定元素是否出现过的时间复杂度为 O(1)。第四层 for 循环实际上就是查找有没有0-nums1-nums2-nums3的数存在,存在的话,它的次数是多少。那我们就可以使用一个 unordered_map 来存放一个数组如 nums4,key 存放 nums4的元素,value 存放其出现次数。这样就可以把时间复杂度降到 O(n^3),不过即使优化到三重 for 循环但还是会超时
注意不能用 set,set 只能存放 key,即不能统计出现次数。虽然用 multiset 然后再用 count 函数计数也能统计次数,但是 multiset 的查找时间复杂度为 O(logn)。
class Solution {
public:
int fourSumCount(vector<int>& nums1,vector<int>& nums2,vector<int>& nums3,vector<int>& nums4) {
int size = nums1.size();
int res = 0;
unordered_map<int, int> hash_map;
// key存放nums4的元素,value存放出现的次数
for (int i = 0; i < size; i++) {
hash_map[nums4[i]]++;
}
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
for (int m = 0; m < size; m++) {
// 如果能在hash_map中找到-(nums1+nums2+nums3),则将res+出现的次数
auto it = hash_map.find(0 - nums1[i] - nums2[j] - nums3[m]);
if (it != hash_map.end()) {
res += it->second;
}
}
}
}
return res;
}
};
既然上面的三重 for 循环会超时,那解法一定是小于三重 for 循环的。比如,可不可以只使用两重 for 循环即 O(n^2)
上面的解法中,我们是用 map 存放一个数组的元素,然后遍历剩下的三个数组。那如果我们先遍历两个数组,让 map 存放的是它们的和,然后再遍历另外两个数组。就可以查找 map 中是否含有-(nums3+nums4),有的话加上它的次数即可。这样我们就可以继续优化到 O(n^2)。
同理,如果是六个数组,那就可以存放三个数组的和,再三重 for 循环,即存放一半数组的和
class Solution {
public:
int fourSumCount(vector<int>& nums1,vector<int>& nums2,vector<int>& nums3,vector<int>& nums4) {
int size = nums1.size();
int res = 0;
unordered_map<int, int> hash_map;
// key存放nums1+nums2的和,value存放和出现的次数
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
hash_map[nums1[i] + nums2[j]]++;
}
}
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
// 如果能在hash_map中找到-(nums3+nums4),则将res+出现的次数
auto it = hash_map.find(0 - nums3[i] - nums4[j]);
if (it != hash_map.end()) {
res += it->second;
}
}
}
return res;
}
};
本节对应代码随想录中:代码随想录,讲解视频:暂无
题目链接:383. 赎金信 - 力扣(LeetCode)
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例 1:
输入:ransomNote = “aa”, magazine = “ab”
输出:false
示例 2:
输入:ransomNote = “aa”, magazine = “aab”
输出:true
提示:
这道简单题和之前的有效的字母异位词那题差不多,是写的最快的一道题了。
判断 ransomNote 能不能由 magazine 构成,只需要先遍历下 magazine 将每个字符出现的次数存起来,然后再遍历 ransomNote,遇到每个字符将其计数-1,如果计数小于0说明 ransomNote 中的这个字符比 magazine 的至少多一个,即不能由 magazine 构成。而如果遍历完 ransomNote 也没能找到计数小于0的则说明可以由 magazine 构成。
查找元素那肯定是用哈希表,解题的关键是选择数组、set、map 中的哪一个更优?首先,set 只能存放 key,而我们还要计数,那肯定是不合适的。而题目中说了 ransomNote 和 magazine 由小写英文字母组成,这就说明最多有26个字符,那么用一个26的数组就更合适。当然,也可以用 map,但是 map 要维护哈希表比数组费时,所以数组更优,后面我会给出两种解法的代码和比较。
首先是数组的解法
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int nums[26] = {0};
// 先遍历magazine计算每个字符的出现次数
for (int i = 0; i < magazine.length(); i++) {
nums[magazine[i] - 'a']++; // 根据-'a'计算相对位置
}
// 再遍历ransomNote
for (int i = 0; i < ransomNote.length(); i++) {
// 先将次数-1,这样如果<0(0-1<0)说明比magazine至少多一个
nums[ransomNote[i] - 'a']--;
if (nums[ransomNote[i] - 'a'] < 0) {
return false;
}
}
return true;
}
};
优化:可以在最前面加一个判断,如果 ransomNote 比 magazine 更长,那一定无法由 magazine 构成,返回 false
map 的解法如下
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
unordered_map<char, int> hash_map;
// 先遍历magazine计算每个字符的出现次数
for (int i = 0; i < magazine.length(); i++) {
hash_map[magazine[i]]++; // 直接将相应元素做为key
}
// 再遍历ransomNote
for (int i = 0; i < ransomNote.length(); i++) {
// 先将次数-1,这样如果<0(0-1<0)说明比magazine至少多一个
hash_map[ransomNote[i]]--;
if (hash_map[ransomNote[i]] < 0) {
return false;
}
}
return true;
}
};
可以看到两个解法其实就是存的 key 不太一样,数组用相对位置当 key,而 map 直接用对应元素当 key。下面是 LeetCode 两种解法的提交记录,从图中也可以看出数组的解法更优
本节对应代码随想录中:代码随想录,讲解视频:梦破碎的地方!| LeetCode:15.三数之和_哔哩哔哩_bilibili
题目链接:15. 三数之和 - 力扣(LeetCode)
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
这道题目难点主要在"不可重复",这样的话如果直接使用哈希表就会出现重复,但去重的操作又很麻烦。实际上使用双指针更合适。我在写的时候也想到了先排序,再对第一个数去重,但一直没想明白怎么对第二个和第三个数去重。实际上就因为这章题目是哈希表,所以一直想着用哈希表来解决。
在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]
为什么先进行排序?如动画上的,排序后,这样数组就是有序的。比如对 a 去重,当 i 为第二个-1时,由于和前面的-1相同,所以就可以直接跳过即 nums[i] == nums[i - 1]
。注意这里不能用 i 和 i+1进行比较,因为三元组内部是可以重复的,如-1 -1 2。
对于 i 指针,是遍历三元组的第一个数,用上面的方法可以对 a 去重,那剩下的两个数该怎么选择呢?
如果 nums[i] + nums[left] + nums[right] > 0 就说明此时三数之和大了,因为数组是排序后了,所以 right 下标就应该向左移动,这样才能让三数之和小一些。如果 nums[i] + nums[left] + nums[right] < 0 说明此时三数之和小了,left 就向右移动,才能让三数之和大一些,直到 left 与 right 相遇为止。当 left=right 时,第二个数和第三个数为同一个数,不符合题意,因此边界条件是 right > left
。
对于 b 和 c 去重,当找到一个合适的3元组后,如果 nums[right] == nums[right - 1]
时就让 right 一直–,因为数组是有序的,知道找到一个不在重复的数为止即可完成去重操作。同理,left 一直++
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector<int>{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 result;
}
};
vector result
中,该二维向量的大小最大为 C(n,3),即 n 个元素中取出三个的组合数。因此空间复杂度为 O(n)本节对应代码随想录中:代码随想录,讲解视频:难在去重和剪枝!| LeetCode:18. 四数之和_哔哩哔哩_bilibili
题目链接:18. 四数之和 - 力扣(LeetCode)
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
这题的解法和上面的三数之和类似。在三数之和中,我们使用一个 i 指针遍历第一个元素,然后使用 left 和 right 指针寻找合适的第二个和第三个元素。在这题中,还是用 left 和 right 指针去查找合适的最后两个元素,不过要多加一层 for 循环去遍历第二个元素
对于第一个元素去重,我们的判断条件是 i>0&&nums[i]==nums[i-1]
,而对于第二个元素去重,还是 nums[j]==nums[j-1]
只不过这里是 j>i+1
不能写成 j>0
,比如0 0 0 0,target =0 ,i=0,j=1时 nums[j]==nums[j-1]
这其实就是用 nums[j]和前面的 nums[i]进行了对比。当 j=i+1
的时候是可能满足四元组的条件的,因此,判断条件是 j>i+1&&nums[j]==nums[j-1]
,其实就是要大于它循环的初始值
另外,还需要注意的是,四个数直接相加,在 LeetCode 给的测试用例中会有整数溢出的情况,因此要转换成 long。可以写四条赋值语句将四个数都转为 long,但更好的写法是在相加的时候将其中一个数转为 long,其余的 int 类型的元素也会被自动升级为 long 类型进行计算,从而避免了整数溢出的问题
一样的道理,五数之和、六数之和等等都采用这种解法
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c + d = target
// a = nums[i], b = nums[j],b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 对a去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < nums.size(); j++) {
// 对b去重
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1;
int right = nums.size() - 1;
while (right > left) {
// nums[i] + nums[j] + nums[left] + nums[right] > target 会溢出
if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
right--;
// nums[i] + nums[j] + nums[left] + nums[right] < target会溢出
} else if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
left++;
} else {
result.push_back(
{nums[i], nums[j], 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 result;
}
};