您现在的位置是:首页 >技术教程 >LeetCode刷题(ACM模式)-03哈希表网站首页技术教程
LeetCode刷题(ACM模式)-03哈希表
- 注:每道 LeetCode 题目都使用 ACM 代码模式,可直接在本地运行,蓝色字体为题目超链接
0. 哈希表理论基础
0.1 哈希表
- 哈希表(Hash table,也称散列表)是根据关键码的值而直接进行访问的数据结构,通俗来讲,数组就是一张哈希表,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
- 那么哈希表能解决什么问题呢?一般哈希表都是用来快速判断一个元素是否出现在集合里
- 例如要查询一个名字是否在这所学校里。枚举的时间复杂度是 O(n),而哈希表只需要 O(1):只用初始化把这所学校里学生的名字都存在哈希表里,查询的时候通过索引直接就可以知道这位同学在不在这所学校里
0.2 哈希函数
-
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后查询索引下标就可快速知道该学生是否在这所学校
-
哈希函数如下图所示,通过 hashCode 把名字转化为数值,一般 hashcode 是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字
-
如果 hashCode 得到的数值大于哈希表的大小 tableSize,怎么办呢?
- 为保证映射出来的索引数值都落在哈希表上,会再次对数值做一个取模操作,这样就保证了学生姓名一定可以映射到哈希表上
-
此时问题又来了,如果学生数量大于哈希表的大小怎么办?此时就算哈希函数计算的再均匀,也难免会有几位学生的名字同时映射到哈希表同一个索引下标的位置,此时就叫哈希碰撞
0.3 哈希碰撞
- 如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞
0.3.1 哈希碰撞解决方法:拉链法和线性探测法
- 拉链法
- 小李和小王在索引 1 的位置发生冲突,发生冲突的元素都被存储在链表中,这样就可通过索引找到他俩
- 其实拉链法就是要选择适当的哈希表大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间(数据规模是 dataSize, 哈希表的大小为 tableSize)
- 线性探测法
- 使用线性探测法,一定要保证 tableSize > dataSize。需要依靠哈希表中的空位来解决碰撞问题:例如冲突的位置放了小李,那么就向下找一个空位放置小王的信息。所以要求 tableSize 一定要大于 dataSize,否则哈希表上就没有空位来存放冲突的数据了。如图所示
- 使用线性探测法,一定要保证 tableSize > dataSize。需要依靠哈希表中的空位来解决碰撞问题:例如冲突的位置放了小李,那么就向下找一个空位放置小王的信息。所以要求 tableSize 一定要大于 dataSize,否则哈希表上就没有空位来存放冲突的数据了。如图所示
0.4 常见的三种哈希结构
-
数组、set(集合)、map(映射)
-
C++ 中 set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示
-
std::unordered_set 底层实现为哈希表,std::set 和 std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以 key 值是有序的,但 key 不可修改,改动 key 值会导致整棵树错乱,所以只能增删
-
std::unordered_map 底层实现为哈希表,std::map 和 std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的 key 也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)
-
-
当要使用集合来解决哈希问题的时候,优先使用 unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用 set,如果要求不仅有序还要有重复数据的话,那么就用 multiset;而 map 是一个 key value 的数据结构,map 中对 key 是有限制,对 value 没有限制,因为 key 的存储方式使用红黑树实现的
总结
- 虽然 std::set 和 std::multiset 的底层实现是红黑树,std::set 和 std::multiset 使用红黑树来索引和存储,但还是哈希法的使用方式即 key 和 value。所以使用这些数据结构来解决映射问题依然称之为哈希法,map 同理
- 当遇到要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。但哈希法也是牺牲空间换取时间,因为要使用额外的数组 set 或 map 来存放数据,才能实现快速的查找
1. 有效的字母异位词
242. 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词
- 示例 1
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]- 示例 2
输入: s = “rat”, t = “car”
输出: false- 提示
1 <= s.length, t.length <= 5 * 104
你可以假设字符串 s 和 t 仅包含小写字母
1.1 思路
- 数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,故可以定义一个数组来记录字符串 s 里字符出现的次数。需要定义一个多大的数组呢?定义一个数组 record,大小为 26 就可以了,初始化为 0,因为 a~z 的 ASCII 也是 26 个连续的数值。为了方便举例,判断一下字符串 s= “aee”, t = “eae”,动画如下
1.1.1. 定义一个数组 record 记录字符串 s 里字符出现的次数
- 需要把字符映射到数组也就是哈希表的索引下标上,因为字符 a~z 的 ASCII 是 26 个连续的数值,所以字符 a 映射为下标 0,相应的字符 z 映射为下标 25
- 再遍历字符串 s 的时候,只需要将 s[i] - ‘a’ 所在的元素做 +1 操作即可,并不需要记住字符 a 的 ASCII,只要求出一个相对数值就可以了。这样就可将字符串 s 中字符出现的次数统计出来
1.1.2. 检查字符串 t 中是否出现了这些字符
- 同样在遍历字符串 t 时,对 t 中出现的字符映射哈希表索引上的数值再做 -1 的操作
1.1.3. 检查 record 数组如果有元素不为 0
- 说明字符串 s 和 t 一定是谁多了字符或者谁少了字符,return false
1.1.4. 如果 record 数组所有元素都为 0
- 说明字符串 s 和 t 是字母异位词,return true
1.2 代码实现
// 时间复杂度为:O(n)
// 空间度复杂度:O(1)
#include <iostream>
#include <string>
using namespace std;
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0}; // 用来记录每个字符出现的次数,数组初始化为 0
// 将字符减去'a'得到一个相对数值,然后将该相对数值作为 record 数组的下标
// 将对应元素加 1,表示该字符出现了 1 次
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符 a 的ASCII,只要求出一个相对数值就可以
record[s[i] - 'a']++;
}
// 方法同上,只不过将对应元素减 1,表示该字符出现了 1 次
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
// 遍历 record 数组,如果存在元素不为 0
// 说明该元素对应的字符在字符串 s 和 t 中出现的次数不一致
// 因此 s 和 t 不是字母异位词,return false
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
return false;
}
}
// record 数组所有元素都为零 0,说明字符串 s 和 t 是字母异位词
return true;
}
};
int main() {
string s = "anagram";
string t = "nagaram";
Solution solution;
cout << solution.isAnagram(s, t) << endl;
return 0;
}
2. 两个数组的交集
349. 两个数组的交集
给定两个数组 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] 也是可通过的- 提示
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
2.1 思路
-
如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。此时就要使用另一种结构体 set
- std::set 和 std::multiset 底层实现都是红黑树,std::unordered_set 的底层实现是哈希表,使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择 unordered_set
- std::set 和 std::multiset 底层实现都是红黑树,std::unordered_set 的底层实现是哈希表,使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择 unordered_set
-
但是直接使用 set 不仅占用空间比数组大,而且速度要比数组慢,set 把数值映射到 key 上都要做 hash 计算的,根据题目提示,可以使用数组来做哈希表,因为数组都是 1000 以内的
2.2 代码实现
- 使用 unordered_set 做哈希表
// 时间复杂度: O(mn)
// 空间复杂度: O(n)
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
// 使用了无序集合来存储结果集 result_set,保证了结果集中不会有重复元素
unordered_set<int> result_set;
// nums_set 使用了 nums1 容器中的元素来初始化
// 包含了 nums1 中所有不重复的整数元素,且顺序不确定
unordered_set<int> nums_set(nums1.begin(), nums1.end());
// 范围 for 循环:依次取出 nums2 中的每个元素并将其赋值给循环变量 num
// 判断它是否在 nums_set 中出现过,若是,则将它插入到 result_set 中
for (int num : nums2) {
// nums_set.find(num) 表示在 nums_set 集合容器中查找是否存在值为 num 的元素
// 如果存在,则返回该元素的迭代器;否则,返回 nums_set.end(),表示未找到该元素
// 因此 nums_set.find(num) != nums_set.end() 表示可以找到
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
int main() {
vector<int> nums1 = {1, 2, 2, 1};
vector<int> nums2 = {2, 2};
Solution solution;
vector<int> result = solution.intersection(nums1, nums2);
for (int num : result) {
cout << num << " ";
}
cout << endl;
return 0;
}
- 使用数组做哈希表
// 时间复杂度: O(m + n),其中 m 和 n 分别为两个输入数组的长度
// 空间复杂度: O(m + n),因为最坏情况下,两个输入数组没有任何公共元素,此时结果集大小为 0,result_set 占用空间为 O(m+n)
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
class Solution {
public:
vector<int> intersection(vector<int> &nums1, vector<int> &nums2) {
// 使用了无序集合来存储结果集 result_set,保证了结果集中不会有重复元素
unordered_set<int> result_set;
int hash[1005] = {0};
// 遍历 nums1 数组,将其中的每个元素作为下标
// 在 hash 数组相应位置上标记为 1,表示该元素存在于 nums1 中
for (int num : nums1) {
hash[num] = 1;
}
// 遍历 nums2 数组中每个元素 num,判断其在 hash 数组中对应位置上的值是否为 1
// 若是,则说明该元素同时存在于 nums1 和 nums2 中,并插入到 result_set 集合
for (int num : nums2) {
if (hash[num] == 1) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
int main(int argc, char *argv[]) {
vector<int> nums1 = {1, 2, 2, 1};
vector<int> nums2 = {2, 2};
Solution solution;
vector<int> result = solution.intersection(nums1, nums2);
for (int num : result) {
cout << num << " ";
}
cout << endl;
return 0;
}
3. 快乐数
202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
- 快乐数定义
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和
然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1
如果这个过程结果为 1,那么这个数就是快乐数
如果 n 是快乐数就返回 true ;否则返回 false- 示例 1
输入:n = 19
输出:true
解释:
1 2 + 9 2 = 82 1^2 + 9^2 = 82 12+92=82
8 2 + 2 2 = 68 8^2 + 2^2 = 68 82+22=68
6 2 + 8 2 = 100 6^2 + 8^2 = 100 62+82=100
1 2 + 0 2 + 0 2 = 1 1^2 + 0^2 + 0^2 = 1 12+02+02=1- 示例 2
输入:n = 2
输出:false- 提示
1 < = n < = 2 31 − 1 1 <= n <= 2^{31} - 1 1<=n<=231−1
3.1 思路
- 题目说了会无限循环,也就是说求和过程中 sum 会重复出现,所以这道题使用哈希法判断这个 sum 是否重复出现,如果重复就 return false,否则一直找到 sum = 1 为止,判断 sum 是否重复出现可以使用 unordered_set
3.2 代码实现
// 时间复杂度: O(logn)
// 空间复杂度: O(logn)
#include <iostream>
#include <unordered_set>
using namespace std;
class Solution {
public:
// 计算 n 每个位上数字的平方和。具体实现:
// 通过 while 循环不断将 n 除以 10 取余数
// 然后计算这个余数的平方并加到 sum 中,继续将 n 除以 10
// 直到 n 为 0 时停止,这样最终就能得到这个数每个位上数字的平方和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set; // 用来记录每次计算结果 sum,避免重复数字出现
while (1) {
int sum = getSum(n);
// 如果等于 1,则说明这个数字是快乐数
if (sum == 1) {
return true;
}
// 判断 sum 是否已经在 set 中出现过
// 如果出现过,则表明陷入了循环而不是得到了 1
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum; // 将计算结果 sum 赋值给变量 n,以便继续进行下一轮计算
}
}
};
int main(int argc, char *argv[]) {
Solution solution;
cout << solution.isHappy(19) << endl; // 输出 1
cout << solution.isHappy(2) << endl; // 输出 0
return 0;
}
4. 两数之和
1. 两数之和
定义一个整数数组 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]- 提示
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案
4.1 思路
-
1. 什么时候使用哈希法
- 当需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。本题就需要一个集合来存放遍历过的元素,然后在遍历数组的时候去查询某元素是否出现在这个集合,那么就应该想到使用哈希法
-
2. 哈希表为什么用 map
- 因为本题不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value 结构来存放,key 来存元素,value 保存元素所在的下标,那么使用 map 正合适,此外这道题目中并不需要 key 有序,所以选择 std::unordered_map 效率更高
本题使用数组或 set 来做哈希法的局限
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费
- set 是一个集合,里面放的元素只能是一个 key,而本题不仅要判断 y 是否存在而且还要记录 y 的下标位置,因为要返回 x 和 y 的下标,所以 set 也不能用
-
3. 本题 map 是用来存什么的
- map 用来存放访问过的元素,因为遍历数组的时候,需要记录之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的元素和对应下标
-
4. map 中 key 和 value 分别表示什么
- 判断元素是否出现,这个元素就要作为 key,所以数组中的元素作为 key,有 key 对应的就是 value,value 用来存下标
- 所以 map 中的存储结构为 {key:数据元素,value:数据元素对应的下标}
- 在遍历数组的时候,只需要向 map 去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进 map 中,因为 map 存放的就是访问过的元素,过程如下
4.2 代码实现
// 时间复杂度: O(n)
// 空间复杂度: O(n)
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int> &nums, int target) {
// 定义无序映射(哈希表)map,用于存储每个元素及其下标
unordered_map<int, int> map;
// 遍历当前元素,并在 map 中寻找是否有匹配的 key
for (int i = 0; i < nums.size(); ++i) {
// 声明一个迭代器 iter,并指向 map 中等于 (target - nums[i]) 的元素
auto iter = map.find(target - nums[i]);
if (iter != map.end()) {
return {iter->second, i}; // 如果找到,则返回这两个元素的下标
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
// 如果遍历完整个数组后仍未找到符合条件的两个整数,则返回一个空的整数向量
return {};
}
};
int main(int argc, char *argv[]) {
vector<int> nums = {2, 7, 11, 15};
int target = 9;
Solution solution;
vector<int> result = solution.twoSum(nums, target);
cout << "[" << result[0] << ", " << result[1] << "]" << endl;
return 0;
}
5. 四数相加 II
454. 四数相加 II
给你四个整数数组 A、B、C 和 D,数组长度都是 n,请计算有多少个元组 (i, j, k, l) 能满足
- 0 <= i, j, k, l < n
- A[i] + B[j] + C[k] + D[l] == 0
- 示例 1
输入:A = [1,2], B = [-2,-1], C = [-1,2], D = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0- 示例 2
输入:A = [0], B = [0], C = [0], D = [0]
输出:1- 提示
n == A.length == B.length == C.length == D.length
1 <= n <= 200
-228 <= A[i], B[i], C[i], D[i] <= 228
5.1 思路
- 本题是使用哈希法的经典题目,只要找到 A[i] + B[j] + C[k] + D[l] = 0 就可以,不用考虑有重复的四个元素相加等于 0 的情况。本题解题步骤:
- 1、首先定义一个 unordered_map,key 放 a 和 b 两数之和,value 放 a 和 b 两数之和出现的次数
- 2、遍历大 A 和大 B 数组,统计两个数组元素之和以及和出现的次数,放到 map 中
- 3、定义 int 变量 count,用来统计 a+b+c+d = 0 出现的次数
- 4、再遍历大 C 和大 D 数组,找到如果 0-(c+d) 在 map 中出现过的话,就用 count 把 map 中 key 对应的 value 也就是出现次数统计出来
- 5、最后返回统计值 count 就可以了
5.2 代码实现
// 时间复杂度: O(n^2)
// 空间复杂度: O(n^2),最坏情况下 A 和 B 的值各不相同,相加产生的数字个数为 n^2
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
int fourSumCount(vector<int> &A, vector<int> &B, vector<int> &C, vector<int> &D) {
unordered_map<int, int> umap;
// 循环遍历 A 和 B 数组,将每个元素对的和添加到哈希表中,同时记录它们出现的次数
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
// 遍历 C 和 D 数组,并在哈希表中查找是否存在等于 0-(c+d) 的键值
int count = 0;
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)]; // 如果存在,则将该元素的出现次数加入计数器中
}
}
}
return count;
}
};
int main(int argc, char *argv[]) {
vector<int> A = {1, 2};
vector<int> B = {-2,-1};
vector<int> C = {-1, 2};
vector<int> D = {0, 2};
Solution solution;
int result = solution.fourSumCount(A, B, C, D);
cout << result << endl;
return 0;
}
6. 赎金信
383. 赎金信
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。如果可以,返回 true;否则返回 false。为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。magazine 中的每个字符只能在 ransomNote 中使用一次
- 示例 1
输入:ransomNote = “a”, magazine = “b”
输出:false- 示例 2
输入:ransomNote = “aa”, magazine = “ab”
输出:false- 示例 3
输入:ransomNote = “aa”, magazine = “aab”
输出:true- 提示
1 <= ransomNote.length, magazine.length <= 105
可以假设 ransomNote 和 magazine 均只由小写字母组成
6.1 思路
- 本题判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成,但是这里需要注意两点
- 第一点 “为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思” 这里说明杂志里面的字母不可重复使用
- 第二点 “你可以假设两个字符串均只含有小写字母” 说明只有小写字母
- 因为只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为 26 的数组记录 magazine 里字母出现的次数,然后再用 ransomNote 去验证这个数组是否包含了 ransomNote 所需要的所有字母,这是数组在哈希法中的应用
本题的情况下,使用 map 的空间消耗要比数组大一些,因为 map 要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了,所以数组更加简单直接有效
6.2 代码实现(哈希实现)
// 时间复杂度: O(n)
// 空间复杂度: O(1)
#include <iostream>
#include <string>
using namespace std;
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
// 如果 ransomNote 的长度大于 magazine,则肯定无法由 magazine 组成
if (ransomNote.size() > magazine.size()) {
return false;
}
// 遍历 magazine 字符串中的每个字符,将其对应的 record 数组中的元素加一
// 表示该字符在 magazine 中出现了一次
for (int i = 0; i < magazine.length(); ++i) {
record[magazine[i] - 'a']++;
}
// 遍历 ransomNote 字符串中的每个字符,将其对应的 record 数组中的元素减一
// 表示尝试用该字符来构建 ransomNote
for (int j = 0; j < ransomNote.length(); ++j) {
record[ransomNote[j] - 'a']--;
// 如果发现某个字符在 ransomNote 中的出现次数超过了在 magazine 中的出现次数
// 则说明无法由 magazine 构成 ransomNote,返回 false
if (record[ransomNote[j] - 'a'] < 0) {
return false;
}
}
return true;
}
};
int main() {
string ransomNote = "aa";
string magazine = "aab";
Solution solution;
bool result = solution.canConstruct(ransomNote, magazine);
if (result) {
cout << "Ransom note can be constructed from magazine." << endl;
} else {
cout << "Ransom note cannot be constructed from magazine." << endl;
}
return 0;
}
7. 三数之和
15. 三数之和
给你一个整数数组 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] 。
注意,输出的顺序和三元组的顺序并不重要- 示例 2
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0- 示例 3
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0- 提示
3 <= nums.length <= 3000
− 1 0 5 -10^5 −105 <= nums[i] <= 1 0 5 10^5 105
7.1 思路
这道题目使用哈希法并不合适,因为在去重操作中有很多细节需要注意,而且哈希法在使用两层 for 循环时,能做的剪枝操作很有限,虽然时间复杂度是 O ( n 2 ) O(n^2) O(n2),也可在 LeetCode 上通过,但程序执行时间较长
- 下面介绍另一个解法:双指针法,这道题目使用双指针法要比哈希法高效一些
- 1、拿这个 nums 数组来举例,首先将数组排序,然后有一层 for 循环,i 从下标 0 的地方开始,同时定义一个下标 left 在 i+1 的位置上,定义下标 right 在数组结尾的位置上
- 2、在数组中找到 abc 使得 a + b + c = 0(这里相当于 a = nums[i],b = nums[left],c = nums[right])
- 3、接下来如何移动 left 和 right 呢?
- 如果 nums[i] + nums[left] + nums[right] > 0 说明此时三数之和大了,因为数组是排序后了,所以 right 下标就应该向左移动,这样才能让三数之和小一些
- 如果 nums[i] + nums[left] + nums[right] < 0 说明此时三数之和小了,left 就向右移动,才能让三数之和大一些,直到 left 与 right 相遇为止
7.2 代码实现
// 时间复杂度: O(n^2)
// 空间复杂度: O(1)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> threeSum(vector<int> &nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end()); // 首先将数组排序
// 外层循环遍历整个数组,以每个元素作为第一个数(即假设该数在三元组中最小)
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); ++i) {
// 排序之后如果第一个元素已经 > 0,那么不可能凑成三元组
if (nums[i] > 0) {
return result;
}
// 对 a 去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1; // left 指向当前元素的下一个位置
int right = nums.size() - 1; // right 指向数组末尾
// 内层循环使用双指针技巧,在已排序的数组中查找剩余两个数
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--; // right 下标向左移动才能让三数之和小一些
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++; // 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;
}
};
int main(int argc, char *argv[]) {
vector<int> nums = {-1, 0, 1, 2, -1, -4};
Solution solution;
vector<vector<int>> ans = solution.threeSum(nums);
cout << "[";
for (int i = 0; i < ans.size(); ++i) {
cout << "[";
for (int j = 0; j < ans[i].size(); ++j) {
cout << ans[i][j];
if (j != ans[i].size() - 1) {
cout << ", ";
}
}
cout << "]";
if (i != ans.size() - 1) {
cout << ", ";
}
}
cout << "]" << endl;
return 0;
}
// 输出
[[-1, -1, 2], [-1, 0, 1]]
7.3 a 去重逻辑思考
// 错误:把三元组中出现重复元素的情况直接 pass 掉了
// 例如 {-1, -1, 2} 这组数据,当遍历到第一个 -1 的时候
// 判断下一个也是 -1,那这组数据就 pass 掉了
// 题意是不能有重复的三元组,但三元组内的元素是可以重复的
if (nums[i] == nums[i + 1]) {
continue;
}
// 正确:这么写就是当前使用 nums[i],判断前一位是不是一样的元素
// 再看 {-1, -1, 2} 这组数据,当遍历到第一个 -1 的时候,只要前一位没有 -1
// 那么 {-1, -1, 2} 这组数据一样可以收录到结果集里
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
8. 四数之和
18. 四数之和
给你一个由 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]]
8.1 思路
- 四数之和与三数之和是一个思路,都是使用双指针法, 基本解法就是在三数之和的基础上再套一层 for 循环。但是有一些细节需要注意,例如
- 不要判断 nums[k] > target 就返回了,三数之和可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target 是任意值。比如:数组是 [-4, -3, -2, -1],target 是 -10,不能因为 -4 > -10 而跳过。但依旧可以去做剪枝,逻辑变成 nums[i] > target && (nums[i] >=0 || target >= 0) 就可以了
- 四数之和的双指针解法是两层 for 循环 nums[k] + nums[i] 为确定值,依然是循环内有 left 和 right 下标作为双指针,找出 nums[k] + nums[i] + nums[left] + nums[right] == target 的情况
8.2 代码实现
// 时间复杂度: O(n^3)
// 空间复杂度: O(1)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> fourSum(vector<int> &nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 剪枝处理
for (int k = 0; k < nums.size(); ++k) {
if (nums[k] > target && nums[k] >= 0) {
break; // 这里使用 break,统一通过最后的 return 返回
}
// 对 nums[k] 去重
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.size(); ++i) {
// 二级剪枝处理
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
// 对 nums[i] 去重
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
if ((long)nums[k] + nums[i] + nums[left] + nums[right] > target) {
right--;
} else if ((long)nums[k] + nums[i] + nums[left] + nums[right] < target) {
left++;
} else {
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
// 对 nums[left] 和 nums[right] 去重
while (right > left && nums[right] == nums[right - 1]) {
right--;
}
while (right > left && nums[left] == nums[left + 1]) {
left++;
}
// 找到结果时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}
};
int main(int argc, char *argv[]) {
vector<int> nums = {-1, 0, 1, 2, -1, -4};
int target = -1;
Solution solution;
vector<vector<int>> ans = solution.fourSum(nums, target);
cout << "[";
for (int i = 0; i < ans.size(); ++i) {
cout << "[";
for (int j = 0; j < ans[i].size(); ++j) {
cout << ans[i][j];
if (j != ans[i].size() - 1) {
cout << ", ";
}
}
cout << "]";
if (i != ans.size() - 1) {
cout << ", ";
}
}
cout << "]" << endl;
return 0;
}
// 输出结果
[[-4, 0, 1, 2], [-1, -1, 0, 1]]