问题描述:
Given an array S of n integers, are there elements a, b, c, and d in S such that a + b + c + d = target? Find all unique quadruplets in the array which gives the sum of target.
Note:
- Elements in a quadruplet (a,b,c,d) must be in non-descending order. (ie, a ≤ b ≤ c ≤ d)
- The solution set must not contain duplicate quadruplets.
For example, given array S = {1 0 -1 0 -2 2}, and target = 0. A solution set is: (-1, 0, 0, 1) (-2, -1, 1, 2) (-2, 0, 0, 2)
原问题链接:https://leetcode.com/problems/4sum/
问题分析
这个问题的解决方法有好几种,一种是基于前面3sum的办法再加一个循环来解决。还有一种是基于前面一篇文章讨论过的方法。
虽然前面讨论的很多方法在理论上比较好,在实际问题结合具体的情况时还有更多可以优化改进的地方。像前面提到的3Sum等问题,我们都是针对数组排序,然后在一定的范围内循环取值来规避一些重复的情况。因为题意的要求需要去除重复的项,这也是这个问题里可以优化的地方。我们先不按照前面的3Sum办法套循环,而是根据这个问题的情况来具体分析一下。
方法一
对于4Sum来说,它需要有4个元素a + b + c + d = target。所以我们这里相当于找到两个数的和,然后再找是否有另外两个不同的数字它们的和和前面这两个加起来等于target。而要求得所有数字的和的组合,我们无非就是用一个两重的循环就可以得出来。在这里我们可以借鉴一下2Sum的解决办法。
我们可以用一个map来保存两个元素的和。其中map的key是两个数的和,value则保存这两个数在数组里的索引。因为要保存两个数字的结构,这里可以用现成的AbstractMap.SimpleEntry。也可以自己定义一个Pair的类型。由于存在有不同的两个数的和是同一个值的情况,所以我们要用一个List来保存所有的这些数字对。这样我们每次取得一个数字组合的时候就去map里查找是否存在有target - nums[i] - nums[j]的元素。如果有的话,则说明找到了符合条件的元素。
这样的话,我们就需要把map里这个元素对应的list元素都拿出来,和i, j 组成一个数组。这里还有一个需要判断过滤的地方。就是要防止list里面元素对和i, j有相同的。因为可能有元素索引1, 2它的和正好和索引1, 3它们的和构成target,但是这是不符合条件的。在构成这个数组后我们还需要在最终的结果里判断去除重复的数组。这样才得到最终的结果。
按照这个思路,我们可以得到如下的代码:
import static java.util.AbstractMap.SimpleEntry; public class Solution { public List<List<Integer>> fourSum(int[] num, int target) { if(num.length < 4) return new ArrayList(); Map<Integer, List<SimpleEntry<Integer, Integer>>> map = new HashMap<Integer, List<SimpleEntry<Integer, Integer>>>(); List<List<Integer>> result = new ArrayList<List<Integer>>(); Arrays.sort(num); for(int i = 0; i < num.length; i++) { for(int j = i + 1; j < num.length; j++) { if(map.containsKey(target - num[i] - num[j])) { for(SimpleEntry<Integer, Integer> entry : map.get(target - num[i] - num[j])) { if(entry.getKey() != i && entry.getKey() != j && entry.getValue() != i && entry.getValue() != j) { List<Integer> list = new ArrayList<Integer>(); list.add(num[entry.getKey()]); list.add(num[entry.getValue()]); list.add(num[i]); list.add(num[j]); Collections.sort(list); if(!result.contains(list)) result.add(list); } } } if(map.containsKey(num[i] + num[j])) { map.get(num[i] + num[j]).add(new SimpleEntry(i, j)); } else { List<SimpleEntry<Integer, Integer>> item = new ArrayList<SimpleEntry<Integer, Integer>>(); item.add(new SimpleEntry(i, j)); map.put(num[i] + num[j], item); } } } return result; } }
上述代码的逻辑还是比较简单的。它的时间复杂度为O(N^3)。
方法二
和上述方法的思路不同,我们这里如果换一种思路,稍微借鉴了一部分3Sum的办法。我们可以这么来想。既然我们是要找4个不同的元素,而且希望最终的结果是从小到大的方式来排列。如果事先将数组排序之后。我们要求的这4个元素必然会构成一个从小到大的顺序。我们不需要关注哪个在前面哪个在后面。那么我们可以考虑一种取两头,然后往中间凑的方式。
这就好比是首先我们取数组的两头的两个元素,假设它们是我们找到的两个元素nums[i] , nums[j] (i < j)。如果真的有构成4个元素的和等于target的话,必然有两个元素在i, j之间。这个时候我们就只需要从i + 1到j - 1的范围内去排查就可以了。这个排查的过程就和3sum里的思路差不多。考虑到从i, j两头凑的方法最终要满足有4个元素,所以必然要有j > i + 2。我们要针对所有可能的i, j范围去遍历,i的取值范围是0到nums.length - 4。而j则取i + 3到 nums.length - 1。
在实际的实现代码里,我们还有一些可以优化的地方,比如当nums[i] * 4 > target的时候,我们可以直接退出。因为这里i就是最小的元素,它的4倍已经大于target了那么就没有查找的必要了。同样,如果nums[j] * 4 < target 也可以退出当前循环。另外还有一些小的优化手段,像3Sum里的那样,当找到两个元素的和等于目标值了。针对这两个元素它和它后面的值作比较,跳过一些相同的值。
按照这个思路,可以得到如下的代码:
public class Solution { public List<List<Integer>> fourSum(int[] nums, int target) { List<List<Integer>> list = new ArrayList<>(); Arrays.sort(nums); for(int i = 0, len = nums.length; i < len - 3; i++) { if(nums[i]<<2 > target) return list; // return immediately for(int j = len - 1; j > i + 2; j--) { if(nums[j]<<2 < target) break; // break immediately int rem = target - nums[i] - nums[j]; int l = i + 1, r = j - 1; while(l < r) { int sum = nums[l] + nums[r]; if(sum > rem) --r; else if(sum < rem) ++l; else { list.add(Arrays.asList(nums[i], nums[l], nums[r], nums[j])); while(++l <= r && nums[l - 1] == nums[l]); // avoid duplicate results while(--r >= l && nums[r] == nums[r + 1]); // avoid duplicate results } } while(j >= 1 && nums[j] == nums[j - 1]) --j; // skip inner loop } while(i < len - 1 && nums[i] == nums[i + 1]) ++i; // skip outer loop } return list; } }
虽然从时间复杂度来说,这种思路也是理论上达到O(N^3)的结果。但是由于这里省略了要对结果的排序和去除重复。所以执行的效率大大提高。
自己的最初实现
一般来说,到这里就应该结束了。不过觉得在这里将自己最初的一个实现效率比较低的方法列出来作为比较发现一些原有实现上效率低下的地方也是很好的。
原有的方法和前面文章里讨论4Sum的思路比较一致,首先根据所有元素的和的情况去得到一个n * (n - 1) / 2的数组。这个数组里保存的是原nums数组所有元素任意两个值的和的所有情况。然后我们再对这个数组排序。然后再按照3Sum里的思路去查找合适的对,再去除重复的索引以及数组。
这种实现的代码如下:
import static java.util.AbstractMap.SimpleEntry; public class Solution { private Map<Integer, List<SimpleEntry<Integer, Integer>>> map = new HashMap<>(); private int[] medium; private List<List<Integer>> result = new ArrayList<>(); public List<List<Integer>> fourSum(int[] nums, int target) { makeMap(nums); Arrays.sort(medium); int l = 0, r = medium.length - 1; while(l < r) { if(medium[l] + medium[r] == target) { mergeList(medium[l], medium[r], nums); l++; r--; } else if(medium[l] + medium[r] < target) { l++; while(l < r && medium[l] == medium[l - 1]) l++; } else { r--; while(l < r && medium[r] == medium[r + 1]) r--; } } return result; } public void makeMap(int[] nums) { int n = nums.length, k = 0; int size = n * (n - 1) / 2; medium = new int[size]; for(int i = 0; i < nums.length - 1; i++) { for(int j = i + 1; j < nums.length; j++) { int key = nums[i] + nums[j]; SimpleEntry<Integer, Integer> entry = new SimpleEntry<>(i, j); if(map.containsKey(key)) { map.get(key).add(entry); } else { List<SimpleEntry<Integer, Integer>> list = new ArrayList<>(); list.add(entry); map.put(key, list); } medium[k++] = key; } } } public void mergeList(int l, int r, int[] nums) { for(SimpleEntry<Integer, Integer> entry : map.get(l)) { for(SimpleEntry<Integer, Integer> e : map.get(r)) { List<Integer> list = new ArrayList<>(); if(entry.getKey() == e.getKey() || entry.getKey() == e.getValue() || entry.getValue() == e.getKey() || entry.getValue() == e.getValue()) continue; list.add(nums[entry.getKey()]); list.add(nums[entry.getValue()]); list.add(nums[e.getKey()]); list.add(nums[e.getValue()]); Collections.sort(list); if(!result.contains(list)) result.add(list); } } } }
很显然,这种方法的思路是好的。希望将所有和的情况都记录下来分析。而这里导致效率低下的地方在于一方面要保存一个map,里面有所有和以及元素对列表。同时还要建立一个长的和值数组,并对这个n * n级别的数组排序。而这里很多情况在上述的实现里是可以很好避免的。
总结
仅仅知道一些问题的思路还是远远不够的,结合具体问题的分析和实现往往能发现一些更加高效的实现。
相关推荐
4 : 两个排序数组的中位数 (40_Hard) LeetCode 5 : 最长回文子串 (251_Medium) LeetCode 6 : ZigZag 对话 (15_Medium) LeetCode 7 : 反转整数 (174_Easy) LeetCode 9 : 回文数 (248_Easy) LeetCode 11 : 盛水最多的...
4 Median of Two Sorted Arrays 5 Longest Palindromic Substring 8 String to Integer 11 Container with Most Water 14 Longest Common Prefix 15 Three Sum 16 Three Sum Closest 20 Valid Parentheses 26 Remove...
LeetCode :laptop: :check_mark_button: 一步步! LeetCode: ://leetcode.com/ 程式语言:C ++ # 标题 解决方案 困难 1个 简单的 2个 中等的 3 中等的 4 难的 5 中等的 6 中等的 7 反整数 7反向整数 简单的 ...
Leetcode\TwoSum\TwoSum.cs 问题: 业绩报告: 反转整数 代码: Leetcode\ReverseInteger\ReverseInteger.cs 问题: 业绩报告: 回文数 代码: Leetcode\PalindromeNumber\PalindromeNumber.cs 问题: 从排序数组中...
2sum leetcode leetcode 方法 哈希: 2Sum->3Sum(hash) 2Sum Less Than K->3Sum Closet->3Sum Smaller(两个指针接近) 找到重复的号码 两个指针: 链表循环 BF: DP: 数学: i^0=i;i^i=0 单号->单号2->单号3 n&n...
#4:Median of Two Sorted Arrays 地图 #1:Two Sum #3:Longest Substring Without Repeating Characters #5:Longest Palindromic Substring 链表 #2:Add Two Numbers 分而治之 #53:Maximum Subarray 队列/集 #3:...
leetcode 轮廓 1_count_and_say.cpp - super_ugly_number.cpp - Detect_Pattern.cpp - degree_of_array.cpp - 键盘.cpp - 2Sum_Data_Structure_Design.cpp - shuffle_array.cpp - permutations.cpp - kth_missing_...
3sum # Leetcode 解决方案以下是我在 Leetcode 上解决的一些问题,如果你喜欢/找到任何你的答案,请留下一个星。 如果你想与我联系,我在下面分享了我的 Linkedin URL。 问题 # 标题 # 解决方案 困难 1 二和 简单...
Tip4: backtrace or K sum remove duplicates if i != 0 and n == nums[i-1]: (#15. 3Sum) if idx > start and nums[idx] == nums[idx-1]: continue (#40. Combination Sum II) Tip5: 鸽笼原理要记得,如果题目说要...
1. **Two Sum (两数之和)**: 这是LeetCode上的一个基础题目,要求在给定的整数数组中找到两个数,使得它们的和等于一个特定的目标值。这个题目主要涉及数组的遍历和哈希表的应用。在C++中,可以利用`unordered_map`...
Sum II - Input array is sorted LeetCode 344 Reverse String LeetCode 345 Reverse Vowels of a String 2 字符串 编号 题目 LeetCode 3 Longest Substring Without Repeating Characters LeetCode 13 Roman to ...
leetcode 1004 力码 去做 : array , dynamic-programming : link , math : string , sliding-window : array , median : string : string : number , reverse , bit : string : math , bit : string , ...
https://leetcode-cn.com/problems/two-sum/ 难度 : 简单 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。...
sum c LeetCode:heart_suit: 好!!!!!!!!!!!!!!!!!!!! LeetCode 题解(C++实现)(持续更新中...) # Title Solution Difficulty label date 1 Easy 2 medium 3 medium 滑动窗口,hashmap 4 hard ...
这些文件通常以问题的ID或标题命名,例如“TwoSum.java”、“MergeIntervals.java”等,这有助于追踪和理解代码对应的具体问题。 在学习和使用这个压缩包时,你可以期待以下几个方面的知识收获: 1. 数据结构:...
【LeetCode答案解析——力码探索】 LeetCode是一个广受欢迎的在线编程挑战平台,它提供了大量的算法题目,旨在帮助程序员提升编程技能、算法理解和问题解决能力。"LeetCode:力码"这个项目可能是某位开发者在...
例如,"Two Sum"(两数之和)问题,通过哈希表可以在O(n)的时间复杂度内解决,这展示了哈希表在快速查找上的优势。而"Merge Intervals"(合并区间)则涉及到排序和区间处理,有助于理解如何高效地操作和合并数据结构...
3sum 力码 力扣算法 (注:“ :locked: " 表示你需要来自 Leetcode) # 标题 解决方案 困难 001 简单的 002 中等的 003 中等的 004 难的 005 中等的 006 简单的 007 简单的 008 简单的 009 简单的 010 难的 011 中等...
Leetcode\3Sum(15).swift Leetcode\k 站内最便宜的飞行(787).swift Leetcode\combination sum(39).swift Leetcode\count number of team(1395).swift Leetcode\counting bits(338).swift Leetcode \find 数组中的...