经典算法题之 n 数之和问题
1、 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
方法一:暴力枚举
思路及算法
最容易想到的方法是枚举数组中的每一个数 x,寻找数组中是否存在 target - x。
当我们使用遍历整个数组的方式寻找 target - x 时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x。
1 class Solution { 2 public int[] twoSum(int[] nums, int target) { 3 int n = nums.length; 4 for (int i = 0; i < n; ++i) { 5 for (int j = i + 1; j < n; ++j) { 6 if (nums[i] + nums[j] == target) { 7 return new int[]{i, j}; 8 } 9 } 10 } 11 return new int[0]; 12 } 13 }
复杂度分析
时间复杂度:O(N2),其中N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。
空间复杂度:O(1)。
方法二:哈希表
思路及算法
注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。
使用哈希表,可以将寻找 target - x 的时间复杂度降低到O(1)。
这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。
1 class Solution { 2 public int[] twoSum(int[] nums, int target) { 3 Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>(); 4 for (int i = 0; i < nums.length; ++i) { 5 if (hashtable.containsKey(target - nums[i])) { 6 return new int[]{hashtable.get(target - nums[i]), i}; 7 } 8 hashtable.put(nums[i], i); 9 } 10 return new int[0]; 11 } 12 }
复杂度分析
时间复杂度:O(N),其中N 是数组中的元素数量。对于每一个元素 x,我们可以O(1) 地寻找 target - x。
空间复杂度:O(N),其中N 是数组中的元素数量。主要为哈希表的开销。
说明,这道题本身并不难,但是是这一类题目思想的基础,暴力解法是通用的方法,但是当数据量很大的时候,就会存在时间开销巨大的问题,因此,使用空间换时间,使用hash表将target - x的结果存储。后续的题目都是这个思想
2、找出数组中两数之和为指定值的所有整数对
问题描述
给定一个整型数组(数组中的元素可重复),以及一个指定的值。打印出数组中两数之和为指定值的 所有整数对
方法一:排序加双指针
先将整型数组排序,排序之后定义两个指针left和right。left指向已排序数组中的第一个元素,right指向已排序数组中的最后一个元素
将 arr[left] + arr[right] 与 给定的元素比较,若前者大,right--;若前者小,left++;若相等,则找到了一对整数之和为指定值的元素。
此方法采用了排序,排序的时间复杂度为O(NlogN),排序之后扫描整个数组求和比较的时间复杂度为O(N)。故总的时间复杂度为O(NlogN)。空间复杂度为O(1)
1 public class ExpectSumOfTwoNumber { 2 3 public static void expectSum_bySort(int[] arr, int expectSum) { 4 List<List<Integer>> res = new ArrayList<>(); 5 if (arr == null || arr.length == 0) { 6 return; 7 } 8 Arrays.sort(arr); 9 int left = 0, right = arr.length - 1; 10 11 while (left < right) { 12 if (arr[left] + arr[right] > expectSum) { 13 right--; 14 } else if (arr[left] + arr[right] < expectSum) { 15 left++; 16 } else//equal 17 { 18 res.add(Arrays.asList(left, right)); 19 left++; 20 right--; 21 } 22 } 23 for (List<Integer> l : res) { 24 System.out.println(l.get(0) + ":" + arr[l.get((0))] + ", " + l.get(1) + ":" + arr[l.get((1))]); 25 } 26 } 27 }
方法二、哈希表(注意是有问题的)
依次遍历整型数组,对整型数组中的每一个元素,求解它的suplement(expectedSum-arr[i]).suplement就是指定的值减去该数组元素。
如果该元素的 suplement不在HashSet中,则将该元素添加到HashSet。
如果该元素的suplement在HashSet中,说明已经找到了一对整数之和为指定值的元素。
该方法使用了HashSet,故空间复杂度为O(N),由于只需要扫描一遍整型数组,故时间复杂度为O(N)
1 public class ExpectSumOfTwoNumber { 2 3 public static void expectSum_bySet(int[] arr, int expectSum) { 4 List<List<Integer>> res = new ArrayList<>(); 5 if (arr == null || arr.length == 0) { 6 return; 7 } 8 HashSet<Integer> intSets = new HashSet<Integer>(arr.length); 9 10 Integer suplement; 11 for( int i=0; i<arr.length; i++ ){ 12 suplement = expectSum - arr[i]; 13 if (!intSets.contains(suplement)) { 14 intSets.add(arr[i); 15 } 16 else{ 17 res.add(Arrays.asList(suplement, arr[i])); 18 intSets.remove(suplement); 19 } 20 } 21 for (List<Integer> l: res) { 22 System.out.println(l.get(0) +", "+ l.get(1)); 23 } 24 } 25 26 public static void main(String[] args) { 27 int[] arr = {2, 7, 4, 9, 3}; 28 int expectSum = 11; 29 expectSum_bySet(arr, expectSum); 30 System.out.println("----------------"); 31 int[] arr2 = {3, 7, 9, 1, 2, 8, 5, 6, 10, 5}; 32 int expectSum2 = 10; 33 expectSum_bySet(arr2, expectSum2); 34 System.out.println("----------------"); 35 int[] arr3 = {2, 5, 1, 5, 5, 6, 3, 3}; 36 int expectSum3 = 8; 37 expectSum_bySet(arr3, expectSum3); 38 } 39 } 40 41 ------------------------------------------------------------------------ 42 >>> 43 2, 9 44 ---------------- 45 1, 9 46 5, 5 47 4, 6 48 0, 10 49 ---------------- 50 3, 5 51 2, 6
可以看到,对于第三个测试用例,当数据出现重复时,结果会出现问题,原因是set会将相同数据去重,解决方法是用list代替set,注意remove函数的重载
1 public class ExpectSumOfTwoNumber { 2 3 public static void expectSum_bySet(int[] arr, int expectSum) { 4 List<List<Integer>> res = new ArrayList<>(); 5 if (arr == null || arr.length == 0) { 6 return; 7 } 8 List<Integer> intList = new ArrayList<>(); 9 10 Integer suplement; 11 for( int i=0; i<arr.length; i++ ){ 12 suplement = expectSum - arr[i]; 13 if (!intList.contains(suplement)) { 14 intList.add(arr[i]); 15 } 16 else{ 17 res.add(Arrays.asList(suplement, arr[i])); 18 // 注意,此处remove的是值,由于remove函数重载的问题,如果使用int suplement, 19 // remove函数会删除指定位置的index,可能造成数组越界 20 intList.remove(suplement); 21 } 22 } 23 for (List<Integer> l: res) { 24 System.out.println(l.get(0) +", "+ l.get(1)); 25 } 26 } 27 28 public static void main(String[] args) { 29 int[] arr = {2, 7, 4, 9, 3}; 30 int expectSum = 11; 31 expectSum_bySet(arr, expectSum); 32 System.out.println("----------------"); 33 int[] arr2 = {3, 7, 9, 1, 2, 8, 5, 6, 10, 5}; 34 int expectSum2 = 10; 35 expectSum_bySet(arr2, expectSum2); 36 System.out.println("----------------"); 37 int[] arr3 = {2, 5, 1, 5, 5, 6, 3, 3}; 38 int expectSum3 = 8; 39 expectSum_bySet(arr3, expectSum3); 40 } 41 } 42 43 ------------------------------------------------------------------------ 44 >>> 45 7, 4 46 2, 9 47 ---------------- 48 3, 7 49 9, 1 50 2, 8 51 5, 5 52 ---------------- 53 2, 6 54 5, 3 55 5, 3
3、三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:输入:nums = []
输出:[]
排序加双指针
1 class Solution { 2 public List<List<Integer>> threeSum(int[] nums) { 3 int n = nums.length; 4 Arrays.sort(nums); 5 List<List<Integer>> ans = new ArrayList<List<Integer>>(); 6 // 枚举 a 7 for (int first = 0; first < n; ++first) { 8 // 需要和上一次枚举的数不相同 9 if (first > 0 && nums[first] == nums[first - 1]) { 10 continue; 11 } 12 // c 对应的指针初始指向数组的最右端 13 int third = n - 1; 14 int target = -nums[first]; 15 // 枚举 b 16 for (int second = first + 1; second < n; ++second) { 17 // 需要和上一次枚举的数不相同 18 if (second > first + 1 && nums[second] == nums[second - 1]) { 19 continue; 20 } 21 // 需要保证 b 的指针在 c 的指针的左侧 22 while (second < third && nums[second] + nums[third] > target) { 23 --third; 24 } 25 // 如果指针重合,随着 b 后续的增加 26 // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环 27 if (second == third) { 28 break; 29 } 30 if (nums[second] + nums[third] == target) { 31 List<Integer> list = new ArrayList<Integer>(); 32 list.add(nums[first]); 33 list.add(nums[second]); 34 list.add(nums[third]); 35 ans.add(list); 36 } 37 } 38 } 39 return ans; 40 } 41 }
4、四数之和
给你一个由 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]]
排序加双指针
思路与算法
最朴素的方法是使用四重循环枚举所有的四元组,然后使用哈希表进行去重操作,得到不包含重复四元组的最终答案。假设数组的长度是n,则该方法中,枚举的时间复杂度为O(n4),去重操作的时间复杂度和空间复杂度也很高,因此需要换一种思路。
为了避免枚举到重复四元组,则需要保证每一重循环枚举到的元素不小于其上一重循环枚举到的元素,且在同一重循环中不能多次枚举到相同的元素。
为了实现上述要求,可以对数组进行排序,并且在循环过程中遵循以下两点:
- 每一种循环枚举到的下标必须大于上一重循环枚举到的下标;
- 同一重循环中,如果当前元素与上一个元素相同,则跳过当前元素。
使用上述方法,可以避免枚举到重复四元组,但是由于仍使用四重循环,时间复杂度仍是O(n4)。注意到数组已经被排序,因此可以使用双指针的方法去掉一重循环。
使用两重循环分别枚举前两个数,然后在两重循环枚举到的数之后使用双指针枚举剩下的两个数。假设两重循环枚举到的前两个数分别位于下标i和j,其中i<j。初始时,左右指针分别指向下标j+1 和下标n−1。
每次计算四个数的和,并进行如下操作:
- 如果和等于target,则将枚举到的四个数加到答案中,然后将左指针右移直到遇到不同的数,将右指针左移直到遇到不同的数;
- 如果和小于target,则将左指针右移一位;
- 如果和大于target,则将右指针左移一位。
- 使用双指针枚举剩下的两个数的时间复杂度是O(n),因此总时间复杂度是O(n3),低于O(n4)
具体实现时,还可以进行一些剪枝操作:
- 在确定第一个数之后,如果nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target,说明此时剩下的三个数无论取什么值,四数之和一定大于target,因此退出第一重循环;
- 在确定第一个数之后,如果nums[i]+nums[n−3]+nums[n−2]+nums[n−1]<target,说明此时剩下的三个数无论取什么值,四数之和一定小于target,因此第一重循环直接进入下一轮,枚举ums[i+1];
- 在确定前两个数之后,如果nums[i]+nums[j]+nums[j+1]+nums[j+2]>target,说明此时剩下的两个数无论取什么值,四数之和一定大于target,因此退出第二重循环;
- 在确定前两个数之后,如果nums[i]+nums[j]+nums[n−2]+nums[n−1]<target,说明此时剩下的两个数无论取什么值,四数之和一定小于target,因此第二重循环直接进入下一轮,枚举nums[j+1]。
1 class Solution { 2 public List<List<Integer>> fourSum(int[] nums, int target) { 3 List<List<Integer>> quadruplets = new ArrayList<List<Integer>>(); 4 if (nums == null || nums.length < 4) { 5 return quadruplets; 6 } 7 Arrays.sort(nums); 8 int length = nums.length; 9 for (int i = 0; i < length - 3; i++) { 10 if (i > 0 && nums[i] == nums[i - 1]) { 11 continue; 12 } 13 if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) { 14 break; 15 } 16 if ((long) nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) { 17 continue; 18 } 19 for (int j = i + 1; j < length - 2; j++) { 20 if (j > i + 1 && nums[j] == nums[j - 1]) { 21 continue; 22 } 23 if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) { 24 break; 25 } 26 if ((long) nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) { 27 continue; 28 } 29 int left = j + 1, right = length - 1; 30 while (left < right) { 31 int sum = nums[i] + nums[j] + nums[left] + nums[right]; 32 if (sum == target) { 33 quadruplets.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right])); 34 while (left < right && nums[left] == nums[left + 1]) { 35 left++; 36 } 37 left++; 38 while (left < right && nums[right] == nums[right - 1]) { 39 right--; 40 } 41 right--; 42 } else if (sum < target) { 43 left++; 44 } else { 45 right--; 46 } 47 } 48 } 49 } 50 return quadruplets; 51 } 52 }
5、统计特殊四元组
给你一个 下标从 0 开始 的整数数组 nums ,返回满足下述条件的 不同 四元组 (a, b, c, d) 的 数目 :
nums[a] + nums[b] + nums[c] == nums[d] ,且
a < b < c < d示例 1:
输入:nums = [1,2,3,6]
输出:1
解释:满足要求的唯一一个四元组是 (0, 1, 2, 3) 因为 1 + 2 + 3 == 6 。
示例 2:输入:nums = [3,3,6,4,5]
输出:0
解释:[3,3,6,4,5] 中不存在满足要求的四元组。
示例 3:输入:nums = [1,1,1,3,5]
输出:4
解释:满足要求的 4 个四元组如下:
- (0, 1, 2, 3): 1 + 1 + 1 == 3
- (0, 1, 3, 4): 1 + 1 + 3 == 5
- (0, 2, 3, 4): 1 + 1 + 3 == 5
- (1, 2, 3, 4): 1 + 1 + 3 == 5
方法一:直接枚举
思路与算法
最简单的方法是直接枚举四个下标 a,b,c,da,b,c,d 并进行判断。
1 class Solution { 2 public int countQuadruplets(int[] nums) { 3 int n = nums.length; 4 int ans = 0; 5 for (int a = 0; a < n; ++a) { 6 for (int b = a + 1; b < n; ++b) { 7 for (int c = b + 1; c < n; ++c) { 8 for (int d = c + 1; d < n; ++d) { 9 if (nums[a] + nums[b] + nums[c] == nums[d]) { 10 ++ans; 11 } 12 } 13 } 14 } 15 } 16 return ans; 17 } 18 }
复杂度分析
时间复杂度:O(n4)
空间复杂度:O(1)。
方法二:使用哈希表存储nums[d]
思路与算法
如果我们已经枚举了前三个下标a,b,c,那么就已经知道了等式左侧
nums[a]+nums[b]+nums[c] 的值,即为nums[d] 的值。对于下标 d 而言,它的取值范围是 c<d<n,那么我们可以使用哈希表统计数组 nums[c+1] 到 nums[n−1] 中每个元素出现的次数。
这样一来,我们就可以直接从哈希表中获得满足等式的 d 的个数,而不需要在[c+1,n−1] 的范围内进行枚举了。
细节
在枚举前三个下标 a,b,c 时,我们可以先逆序枚举 c。在 c 减小的过程中,d 的取值范围是逐渐增大的:即从 c+1 减小到 c 时,d 的取值范围中多了 c+1 这一项,而其余的项不变。因此我们只需要将 nums[c+1] 加入哈希表即可。
在这之后,我们就可以枚举 a,b 并使用哈希表计算答案了。
class Solution { public int countQuadruplets(int[] nums) { int n = nums.length; int ans = 0; Map<Integer, Integer> cnt = new HashMap<Integer, Integer>(); for (int c = n - 2; c >= 2; --c) { cnt.put(nums[c + 1], cnt.getOrDefault(nums[c + 1], 0) + 1); for (int a = 0; a < c; ++a) { for (int b = a + 1; b < c; ++b) { ans += cnt.getOrDefault(nums[a] + nums[b] + nums[c], 0); } } } return ans; } }
方法三:使用哈希表存储 nums[d]−nums[c]
思路与算法
我们将等式左侧的 nums[c] 移动到右侧,变为: nums[a]+nums[b]=nums[d]−nums[c]
如果我们已经枚举了前两个下标 a, b,那么就已经知道了等式左侧 nums[a]+nums[b] 的值,即为 nums[d]−nums[c] 的值。对于下标 c, d 而言,它的取值范围是 b<c<d<n,那么我们可以使用哈希表统计满足上述要求的每一种 nums[d]−nums[c] 出现的次数。这样一来,我们就可以直接从哈希表中获得满足等式的 c, d 的个数,而不需要在 [b+1,n−1] 的范围内进行枚举了。
细节
在枚举前两个下标 a, b 时,我们可以先逆序枚举 b。在 b 减小的过程中, c 的取值范围是逐渐增大的:即从 b+1 减小到 b 时,c 的取值范围中多了 b+1 这一项,而其余的项不变。
因此我们只需要将所有满足 c=b+1 且 d>c 的 c, d 对应的 nums[d]−nums[c] 加入哈希表即可。在这之后,我们就可以枚举 a 并使用哈希表计算答案了。
1 class Solution { 2 public int countQuadruplets(int[] nums) { 3 int n = nums.length; 4 int ans = 0; 5 Map<Integer, Integer> cnt = new HashMap<Integer, Integer>(); 6 for (int b = n - 3; b >= 1; --b) { 7 for (int d = b + 2; d < n; ++d) { 8 cnt.put(nums[d] - nums[b + 1], cnt.getOrDefault(nums[d] - nums[b + 1], 0) + 1); 9 } 10 for (int a = 0; a < b; ++a) { 11 ans += cnt.getOrDefault(nums[a] + nums[b], 0); 12 } 13 } 14 return ans; 15 } 16 }