两数之和、三数之和、四数之和解决方案
这篇文章详细总结力扣网站中两数之和、三数之和、四数之和的解决方案
两数之和
两数之和题目🔗
本题的要求是找到两个数
a
a
a,
b
b
b,使其满足
a
+
b
=
t
a
r
g
e
t
a+b=target
a+b=target
简单的解法是两重for循环分别找出
a
a
a和
b
b
b,这种方法效率比较低,这里我们使用哈希表解题。
(一)主要思想
哈希表的一个重要功能是快速判断一个元素是否出现集合里,根据这一特点,我们可以设置一个for循环遍历
a
a
a,然后判断
b
b
b 是否在哈希表里出现过。
如果
b
b
b 没有出现在哈希表里,则把
a
a
a 作为key,a对应的下标
i
i
i 为value加入到哈希表中;
如果
b
b
b 出现在哈希表里,则返回二者下标即可。
以
n
u
m
s
=
[
2
,
7
,
11
,
15
]
,
t
a
r
g
e
t
=
9
nums = [2,7,11,15], target = 9
nums=[2,7,11,15],target=9 为例:
初始化一个HashMap,从下标为0遍历数组
n
u
m
s
nums
nums:
当 i = 0 i=0 i=0 时, n u m s [ i ] = 2 nums[i]=2 nums[i]=2 ,此时 t a r g e t − a = 7 target-a=7 target−a=7,查询hashMap发现7不在表中,于是将(2,0)加入到hashMap中:
key | Value |
---|---|
2 | 0 |
当 i = 1 i=1 i=1 时, n u m s [ i ] = 7 nums[i]=7 nums[i]=7 ,此时 t a r g e t − a = 2 target-a=2 target−a=2,查询hashMap发现2在hashMap中,符合题意,返回2和7的下标0,1即可。
(二)代码实现
Java版:
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
Map<Integer, Integer> map= new HashMap();
for(int i=0;i<nums.length;i++){
int remain = target-nums[i];
if(map.containsKey(remain)){
res[0] = i;
res[1] = map.get(remain);
}else{
map.put(nums[i],i);
}
}
return res;
}
}
C语言版(需要自己定义hashMap结构):
typedef struct {
int key;
int value;
UT_hash_handle hh; // make this structure hashable
} map;
map* hashMap = NULL;
void hashMapAdd(int key, int value){
map* s;
// key already in the hash?
HASH_FIND_INT(hashMap, &key, s);
if(s == NULL){
s = (map*)malloc(sizeof(map));
s -> key = key;
HASH_ADD_INT(hashMap, key, s);
}
s -> value = value;
}
map* hashMapFind(int key){
map* s;
// *s: output pointer
HASH_FIND_INT(hashMap, &key, s);
return s;
}
void hashMapCleanup(){
map* cur, *tmp;
HASH_ITER(hh, hashMap, cur, tmp){
HASH_DEL(hashMap, cur);
free(cur);
}
}
void hashPrint(){
map* s;
for(s = hashMap; s != NULL; s=(map*)(s -> hh.next)){
printf("key %d, value %d\n", s -> key, s -> value);
}
}
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
int *ans = malloc(sizeof(int) * 2);
map* res = NULL;
hashMap = NULL;
for(int i=0;i<numsSize;i++){
res = hashMapFind(target-nums[i]);
if(res){
ans[0] = res->value;
ans[1] = i;
*returnSize = 2;
return ans;
}else{
hashMapAdd(nums[i],i);
}
}
hashMapCleanup();
return NULL;
}
(三)总结
在此题中,对数组进行遍历并使用hashMap记录遍历过的元素,在遍历的过程中不断查询hashMap,判断已经遍历过的元素中是否存在满足 a + b = t a r g e t a+b=target a+b=target 的元素。
三数之和
三数之和题目🔗
参照两数之和,这道题目也可以用两重for循环+hashMap来解决。
a
+
b
+
c
=
0
a+b+c=0
a+b+c=0,使用一重for循环来确定a,一重for循环来确定b,然后通过
0
−
a
−
b
0-a-b
0−a−b 来确定
c
c
c 即可。但是这种方式需要复杂地去重。
例如: n u m s = [ − 1 , − 1 , − 1 , 0 , 0 , 0 , 1 , 1 , 1 ] nums=[-1,-1,-1,0,0,0,1,1,1] nums=[−1,−1,−1,0,0,0,1,1,1],其去重的复杂程度可想而知
so,这道题我们不妨换一种思路,通过排序+双指针的方式实现。
(一)主要思路
由于此题并未要求返回下标,我们先将数组按照升序排序。
然后设置一个for循环遍历排序后的数组,设置两个指针
l
e
f
t
left
left 和
r
i
g
h
t
right
right ,
l
e
f
t
left
left 指向
i
+
1
i+1
i+1 的位置,
r
i
g
h
t
right
right指向数组最后一个位置,如下图所示:
判断
s
u
m
=
n
u
m
s
[
i
]
+
n
u
m
s
[
l
e
f
t
]
+
n
u
m
s
[
r
i
g
h
t
]
sum=nums[i]+nums[left]+nums[right]
sum=nums[i]+nums[left]+nums[right] 与0的大小关系:
若
s
u
m
=
0
sum=0
sum=0,则将三个数加入到结果集中,
l
e
f
t
+
+
,
r
i
g
h
t
−
−
left++,right--
left++,right−−;
若
s
u
m
>
0
sum>0
sum>0,则
r
i
g
h
t
−
−
right--
right−−;
若
s
u
m
<
0
sum<0
sum<0,则
l
e
f
t
−
−
left--
left−−;
直到
l
e
f
t
>
r
i
g
h
t
left>right
left>right。
以
n
u
m
s
=
[
−
1
,
0
,
1
,
2
,
−
1
,
−
4
]
nums = [-1,0,1,2,-1,-4]
nums=[−1,0,1,2,−1,−4]为例:
首先对数组进行排序,排序后:
n
u
m
s
=
[
−
4
,
−
1
,
−
1
,
0
,
1
,
2
]
nums=[-4,-1,-1,0,1,2]
nums=[−4,−1,−1,0,1,2],指针的初始状态:
指针的移动过程如下图:
第二轮的指针移动过程如下:
第三轮指针移动过程如下:
后面几轮的移动过程类似,在此不再赘述。
根据距离可知,我们的方法行之有效,但也存在结果重复的问题,比如第二轮第三步得到的结果
(
−
1
,
0
,
1
)
(-1,0,1)
(−1,0,1)与第三轮第二步得到的结果重复。因此我们进行去重操作。
首先分析重复结果出现的原因:
在本例中,出现重复是因为
n
u
m
s
[
i
]
nums[i]
nums[i] 与
n
u
m
s
[
i
+
1
]
nums[i+1]
nums[i+1]值相同,导致出现了两次
(
−
1
,
0
,
1
)
(-1,0,1)
(−1,0,1)的三元组。
因此我们在第二轮以后的每一轮的指针移动前,判断
n
u
m
s
[
i
]
nums[i]
nums[i] 是否等于
n
u
m
s
[
i
−
1
]
nums[i-1]
nums[i−1] 即可。若二者相等,则跳过本轮循环。
代码如下:
if(nums[i]>0) break; //如果i指向的数已经大于0了,那么后续的三元组均不满足题意
if(i>0&&nums[i]==nums[i-1]) continue;
那么问题来了,这里为什么不用 n u m s [ i ] nums[i] nums[i]与 n u m s [ i + 1 ] nums[i+1] nums[i+1] 来做判断?
假设给出的数组 n u m s = [ − 1 , − 1 , 2 ] nums=[-1,-1,2] nums=[−1,−1,2],那么这个数值元素组成的三元组刚好是满足题意的,如果我们用 n u m s [ i ] = n u m s [ i + 1 ] nums[i]=nums[i+1] nums[i]=nums[i+1]来判断,在第一轮开始之前,判断出 n u m s [ 0 ] = n u m s [ 1 ] nums[0]=nums[1] nums[0]=nums[1],跳过本轮循环,那么结果集将会是空集,不满足题意。用 n u m s [ i ] nums[i] nums[i] 和 n u m s [ i − 1 ] nums[i-1] nums[i−1]则不会出现上述情况
除了
n
u
m
s
[
i
]
nums[i]
nums[i] 会重复,
l
e
f
t
left
left 和
r
i
g
h
t
right
right 对应的值也会重复:
比如
n
u
m
s
=
[
−
1
,
0
,
0
,
1
,
1
]
nums=[-1,0,0,1,1]
nums=[−1,0,0,1,1],则会出现两个
(
−
1
,
0
,
1
)
(-1,0,1)
(−1,0,1)的三元组,因此我们需要分别对
l
e
f
t
left
left 和
r
i
g
h
t
right
right 进行去重。
方法与对
n
u
m
s
[
i
]
nums[i]
nums[i] 的处理类似,每次保存结果后,我们只需让
l
e
f
t
left
left 不断
+
+
++
++,直到
l
e
f
t
left
left 与其前一个值不相等,然后让
r
i
g
h
t
right
right 不断
−
−
--
−− ,使其与后一个值不相等即可。
代码如下:
if(nums[i]+nums[left]+nums[right]==0){
List<Integer> sublist = new ArrayList<>();
sublist.add(nums[i]);
sublist.add(nums[left]);
sublist.add(nums[right]);
res.add(sublist);
left++;
right--;
while(left<right&&(nums[left]==nums[left-1])) left++;
while(left<right&&(nums[right]==nums[right+1])) right--;
}
这里考虑一个问题,为什么是在保存结果后再对 l e f t left left 和 r i g h t right right 去重,而不是先去重再保存结果呢?
考虑数组 n u m s = [ 0 , 0 , 0 , 0 , 0 ] nums=[0,0,0,0,0] nums=[0,0,0,0,0],显然,三元组 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)是符合题意的,如果是先去重,则会将此结果排除掉。
(二)代码实现
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>>res =new ArrayList<List<Integer>>();
Arrays.sort(nums);
int left,right;
for(int i=0;i<nums.length;i++){
if(nums[i]>0) break; //如果i指向的数已经大于0了,那么后续的三元组均不满足题意
if(i>0&&nums[i]==nums[i-1]) continue;
left = i+1;
right = nums.length-1;
while(left<right){
if(nums[i]+nums[left]+nums[right]==0){
List<Integer> sublist = new ArrayList<>();
sublist.add(nums[i]);
sublist.add(nums[left]);
sublist.add(nums[right]);
res.add(sublist);
left++;
right--;
while(left<right&&(nums[left]==nums[left-1])) left++;
while(left<right&&(nums[right]==nums[right+1])) right--;
}
else if(nums[i]+nums[left]+nums[right]>0){
right--;
}else{
left++;
}
}
}
return res;
}
}
(三)总结
本题排序+双指针的思想比较好理解,关键在于三元组中每个数的去重处理,以及去重操作的时机。
四数之和
四数之和题目🔗
三书之和满足的条件是
a
+
b
+
c
=
0
a+b+c=0
a+b+c=0,四数之和慢则的条件是
a
+
b
+
c
+
d
=
t
a
r
g
e
t
a+b+c+d=target
a+b+c+d=target,万变不离其宗,我们照着葫芦画瓢即可。
(一)主要思想
首先对数组按照升序排序,设置两个for循环,第一个负责确定
a
a
a,第二个负责确定
b
b
b,剩下的
c
c
c 和
d
d
d 依然由
l
e
f
t
left
left 和
r
i
g
h
t
right
right 指针确定。判断逻辑与三数之和相同。
以
n
u
m
s
=
[
1
,
0
,
−
1
,
0
,
−
2
,
2
]
,
t
a
r
g
e
t
=
0
nums = [1,0,-1,0,-2,2], target = 0
nums=[1,0,−1,0,−2,2],target=0 为例,我们看一下指针的移动过程:
当
l
e
f
t
>
r
i
g
h
t
left>right
left>right 跳出循环后,
j
+
+
j++
j++。
其余指针移动过程与本轮类似,不再赘述。
然后我们考虑一下去重操作,与三数之和类似,我们需要分别对 a , b , c , d a,b,c,d a,b,c,d 四个数去重:
- 对 a a a 的去重操作与三数之和一样:
if(i>0&&nums[i]==nums[i-1]) continue;
- 对 b b b的去重稍有不同:
if((j-i)>1&&nums[j]==nums[j-1]) continue;
这里判断条件为什么是
j
−
i
>
1
j-i>1
j−i>1?
考虑数组
n
u
m
s
=
[
−
1
,
−
1
,
2
,
2
]
,
t
a
r
g
e
t
=
0
nums=[-1,-1,2,2],target=0
nums=[−1,−1,2,2],target=0即可,若直接判断
n
u
m
s
[
j
]
与
n
u
m
s
[
j
−
1
]
nums[j]与nums[j-1]
nums[j]与nums[j−1]的大小关系而不考虑
j
j
j的位置,满足条件的四元组
(
−
1
,
−
1
,
2
,
2
)
(-1,-1,2,2)
(−1,−1,2,2)将会直接被排除。
- 对 c c c和 d d d的去重与三数之和一样:
if(sum==target){
res.add(Arrays.asList(nums[i], nums[j],nums[left], nums[right]));
left++;
right--;
while(left<right&&nums[left]==nums[left-1]) left++;
while(left<right&&nums[right]==nums[right+1]) right--;
}
这道题的测试用例中有int型数值相加溢出的情况,因此我们在计算时先将int转成long,代码如下:
long sum = (long)nums[i]+nums[j]+nums[left]+nums[right];
(二)代码实现
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
int left,right;
Arrays.sort(nums);
Boolean isbreak = true;
for(int i=0;i<nums.length-3&&isbreak;i++){
if (nums[i] > 0 && nums[i] > target) { //先做第一步的剪枝,注意与三数之和区别开来
isbreak = false;
break;
}
for(int j=i+1;j<nums.length-2;j++){
left = j+1;
right = nums.length-1;
if(i>0&&nums[i]==nums[i-1]) continue;
if((j-i)>1&&nums[j]==nums[j-1]) continue;
while(left<right){
long sum = (long)nums[i]+nums[j]+nums[left]+nums[right];
if(sum==target){
res.add(Arrays.asList(nums[i], nums[j],nums[left], nums[right]));
left++;
right--;
while(left<right&&nums[left]==nums[left-1]) left++;
while(left<right&&nums[right]==nums[right+1]) right--;
}
else if(sum>target){
right--;
}else{
left++;
}
}
}
}
return res;
}
}
总结
本篇文章记录了力扣三道典型题目的思路,除了要掌握双指针+排序的思想外,去重剪枝的处理也十分关键。在做题的过程中应多写多练,提高自己思维的严谨性~