点击上方“五分钟学算法”,选择“星标”公众号
重磅干货,第一时间送达
2 Sum 这题是 Leetcode 的第一题,相信大部分小伙伴都听过的吧。
作为一道标着 Easy 难度的题,它真的这么简单吗?
我在之前的刷题视频里说过,大家刷题一定要吃透一类题,为什么有的人题目做着越来越少,有的人总觉得刷不完的题,就是因为没有分类吃透。
单纯的追求做题数量是没有意义的,Leetcode 的题目只会越来越多,就像高三时的模考试卷一样做不完,但分类总结,学会解决问题的方式方法,才能遇到新题也不手足无措。
2 Sum
这道题题意就是,给一个数组和一个目标值,让你在这个数组里找到两个数,使得它俩之和等于这个目标值的。
比如题目中给的例子,目标值是 9,然后数组里 2 + 7 = 9
,于是返回 2 和 7 的下标。
方法一
在我多年前还不知道时空复杂度的时候,我想这还不简单嘛,就每个组合挨个试一遍呗,也就是两层循环。
后来我才知道,这样时间复杂度是很高的,是 O(n^2)
;但另一方面,这种方法的空间复杂度最低,是 O(1)
。
所以,面试时一定要先问面试官,是希望优化时间还是优化空间。
一般来说我们追求优化时间,但你不能默认面试官也是这么想的,有时候他就是想考你有没有这个意识呢。
如果一个方法能够兼具优化时间和空间那就更好了,比如斐波那契数列这个问题中从递归到 DP 的优化,就是时间和空间的双重优化,不清楚的同学后台回复「递归」快去补课~
我们来看下这个代码:
class Solution {public int[] twoSum(int[] nums, int target) {for (int i &#61; 0; i < nums.length; i&#43;&#43;) {for (int j &#61; i &#43; 1; j < nums.length; j&#43;&#43;) {if (nums[i] &#43; nums[j] &#61;&#61; target) {return new int[]{i, j};}}}return new int[]{-1, -1};}
}
时间复杂度&#xff1a;O(n^2)
空间复杂度&#xff1a;O(1)
喏&#xff0c;这速度不太行诶。
方法二
那在我学了 HashMap
这个数据结构之后呢&#xff0c;我又有了新的想法。
HashMap
或者 HashSet
的最大优势就是能够用 O(1)
的时间获取到目标值&#xff0c;那么是不是可以优化方法一的第二个循环呢&#xff1f;
有了这个思路&#xff0c;假设当前在看 x
&#xff0c;那就是需要把 x
之前或者之后的数放在 HashSet
里&#xff0c;然后看下 target - x
在不在这个 hashSet
里&#xff0c;如果在的话&#xff0c;那就匹配成功&#xff5e;
诶这里有个问题&#xff0c;这题要求返回这俩数的下标&#xff0c;可是 HashSet
里的数是无序的...
那就用升级版——HashMap
嘛&#xff5e;&#xff5e;还不了解 HashMap
的原理的同学快去公众号后台回复「HashMap」看文章啦。
HashMap
里记录下数值和它的 index
这样匹配成功之后就可以顺便得到 index
了。
这里我们不需要提前记录所有的值&#xff0c;只需要边过数组边记录就好了&#xff0c;为了防止重复&#xff0c;我们只在这个当前的数出现之前的数组部分里找另一个数。
总结一下&#xff0c;
HashMap
里记录的是下标 i
之前的所有出现过的数&#xff1b;
对于每个 nums[i]
&#xff0c;我们先检查 target - nums[i]
是否在这个 map
里&#xff1b;
如果在就直接返回了&#xff0c;如果不在就把当前 i
的信息加进 map
里。
class Solution {public int[] twoSum(int[] nums, int target) {int[] res &#61; new int[2];Map map &#61; new HashMap<>();for (int i &#61; 0; i < nums.length; i&#43;&#43;) {if (map.containsKey(target - nums[i])) {res[0] &#61; map.get(target - nums[i]);res[1] &#61; i;return res;}map.put(nums[i], i);}return res;}
}
时间复杂度&#xff1a;O(n)
空间复杂度&#xff1a;O(n)
喏&#xff0c;速度提升至 beat 99.96%
拓展
这是最基本的 2 Sum
问题&#xff0c;这个题可以有太多的变种了&#xff1a;
如果这个数组里有不止一组结果&#xff0c;要求返回所有组合&#xff0c;该怎么做&#xff1f;
如果这个数组里有重复元素&#xff0c;又该怎么做&#xff1f;
如果这个数组是一个排好序了的数组&#xff0c;那如何利用这个条件呢&#xff1f;- Leetcode 167
如果不是数组而是给一个 BST
&#xff0c;该怎么在一棵树上找这俩数呢&#xff1f;- Leetcode 653
...
这里讲一下排序数组这道题&#xff0c;之后会在 BST
的文章里会讲 653 这题。
排序数组
我们知道排序算法中最快的也需要 O(nlogn)
&#xff0c;所以如果是一个 2 Sum
问题&#xff0c;那没必要专门排序&#xff0c;因为排序会成为运算的瓶颈。
但如果题目给的就是个排好序了的数组&#xff0c;那肯定要好好收着了呀&#xff01;
因为当数组是排好序的时候&#xff0c;我们可以进一步优化空间&#xff0c;达到 O(n)
的时间和 O(1)
的空间。
该怎么利用排好序这个性质呢&#xff1f;
那就是说&#xff0c;在 x
右边的数&#xff0c;都比 x
要大&#xff1b;在 x
左边的数&#xff0c;都比 x
要小。
这也就是典型的 Two pointer
算法&#xff0c;两个指针相向而行的情况&#xff0c;我之后也会出文章详细来讲哒。
class Solution {public int[] twoSum(int[] numbers, int target) {int left &#61; 0;int right &#61; numbers.length - 1;while (left < right) {int sum &#61; numbers[left] &#43; numbers[right];if (sum &#61;&#61; target) {return new int[]{left &#43; 1, right &#43; 1}; //Your returned answers are not zero-based.} else if (sum < target) {left &#43;&#43;;} else {right --;}}return new int[]{-1, -1};}
}
3 Sum
3 Sum
的问题其实就是一个 2 Sum
的升级版&#xff0c;因为 1 &#43; 2 &#61; 3 嘛。。
那就是外面一层循环&#xff0c;固定一个值&#xff0c;在剩下的数组里做 2 Sum
问题。
反正 3 Sum
怎么着都得 O(n^2)
&#xff0c;就可以先排序&#xff0c;反正不在乎排序的这点时间了&#xff0c;这样就可以用 Two pointer
来做了。
还需要注意的是&#xff0c;这道题返回的是数值&#xff0c;而非 index
&#xff0c;所以它不需要重复的数值——The solution set must not contain duplicate triplets.
class Solution {public List> threeSum(int[] nums) {List> res &#61; new ArrayList<>();Arrays.sort(nums);for (int i &#61; 0; i &#43; 2 < nums.length; i&#43;&#43;) {if (i > 0 && nums[i] &#61;&#61; nums[i - 1]) {// skip same resultcontinue;}int j &#61; i &#43; 1;int k &#61; nums.length - 1; int target &#61; -nums[i];while (j < k) {if (nums[j] &#43; nums[k] &#61;&#61; target) {res.add(Arrays.asList(nums[i], nums[j], nums[k]));j&#43;&#43;;k--;while (j < k && nums[j] &#61;&#61; nums[j - 1]) {j&#43;&#43;; // skip same result}while (j < k && nums[k] &#61;&#61; nums[k &#43; 1]) {k--; // skip same result}} else if (nums[j] &#43; nums[k] > target) {k--;} else {j&#43;&#43;;}}}return res;}
}
4 Sum
最后就是 4 Sum
问题啦。
这一题如果只是 O(n^3)
的解法没什么难的&#xff0c;因为就是在 3 Sum
的基础上再加一层循环嘛。
但是如果在面试中只做出 O(n^3)
恐怕就过不了了哦????
这 4 个数&#xff0c;可以想成两两的 2 Sum
&#xff0c;先把第一个 2 Sum
的结果存下来&#xff0c;然后在后续的数组中做第二个 2 Sum
&#xff0c;这样就可以把时间降低到 O(n^2)
了。
这里要注意的是&#xff0c;为了避免重复&#xff0c;也就是下图的 nums[x] &#43; nums[y] &#43; nums[z] &#43; nums[k]
&#xff0c;其实和 nums[z] &#43; nums[k] &#43; nums[x] &#43; nums[y]
并没有区别&#xff0c;所以我们要限制第二组的两个数要在第一组的两个数之后哦。
看下代码吧&#xff1a;
class Solution {public List> fourSum(int[] nums, int target) {Set> set &#61; new HashSet<>();Map>> map &#61; new HashMap<>();Arrays.sort(nums);// 先处理第一对&#xff0c;把它们的sum存下来for(int i &#61; 0; i < nums.length - 3; i&#43;&#43;) {for(int j &#61; i &#43; 1; j < nums.length - 2; j&#43;&#43;) {int currSum &#61; nums[i] &#43; nums[j];List> pairs &#61; map.getOrDefault(currSum, new ArrayList<>());pairs.add(Arrays.asList(i, j));map.put(currSum, pairs);}}// 在其后做two sumfor(int i &#61; 2; i < nums.length - 1; i&#43;&#43;) {for(int j &#61; i &#43; 1; j < nums.length; j&#43;&#43;) {int currSum &#61; nums[i] &#43; nums[j];List> prevPairs &#61; map.get(target - currSum);if(prevPairs &#61;&#61; null) {continue;}for(List pair : prevPairs) {if(pair.get(1) < i) {set.add(Arrays.asList(nums[pair.get(0)], nums[pair.get(1)], nums[i], nums[j]));}}}}return new ArrayList<>(set);}
}
好啦&#xff0c;以上就是 2 Sum
相关的所有问题啦&#xff0c;如果有收获的话&#xff0c;记得给我点个赞哦&#xff5e;
---
爱分享&#xff0c;爱开源&#xff0c;GitHubPorn 现已正式上线&#xff01;专注于为大家分享优质的计算机学习资源与开发者工具。
如果今天的推荐符合你的口味&#xff0c;请在文章点赞&#xff0c;以表示对我的支持&#xff0c;你们的点赞和转发关注&#xff0c;是我持续更新的动力^_^