Two Sum
友情提示:篇幅较长,找题目的话,右边有目录,幸好我会MarkDown语法。
改成了系列模式,因为类似的题不少,本质上都是换壳,所以在同一篇文章里面把这类问题汇总一下。先说2 Sum。这道题非常出名,因为这是leetcode的第一道题。很多人说一些公司招聘的时候,这道题专门出给他们想招进来的人,因为这不是放水,简直就是洪水。要做的就是已知一个数组,和一个target值。返回两个目标的index,这两个目标求和就是target值。
解决思路
这题不难,只需要熟悉hashtable即可。在hashtable里面,key是差,value是index。比如例子中的[2,7,11,15]
,target是9。那么在2的时候就存入7 0
,下一位找到7的时候,之前有个差值是7,那么就返回7对应的index,0,以及当前这个7的index,就是1。
code
public class Solution {
public int[] twoSum(int[] nums, int target) {
//创建一下数组,要存两个index的数组。
int[] result = new int[2];
//这里用hashtable也行,看心情。
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
//扫一遍数组,一边扫一边存
for(int i = 0; i < nums.length; i++){
int cur = nums[i];
//这里搞出来个差值,其实差值是在没找到之后添加到map里面的。
int toFind = target - cur;
//如果发现之前需要这个差值,那就找index。
if(map.containsKey(cur)){
result[0] = map.get(cur);
result[1] = i;
return result;
}
//如果没有,就put到map里面
else{
map.put(toFind, i);
}
}
return result;
}
}
复杂度分析
就是O(n),因为只扫了一遍数组。
最后再说两句
虽然这题非常简单,但是14年秋天第一次看到这题的时候感觉还是难到爆炸,无从下手。因为一丝编程基础都没有,现在两年过去了,用脚趾都能敲出来。其实行业之间努力其次,路径非常重要,在这里感谢一下点拨我的老乡和兄弟们。而且重新写了几次,连变量命名都是一样的。
Two Sum II - Input array is sorted
解决思路
这题用sorted
当做题目,好比路边的一些职业勾引男性行人,非常直接的就意味着二分搜索。一次查一半,所以刚开始只用到了二分搜索。但是有个问题,二分搜索的步子太大,可能把目标值跳过,那么还要借鉴双指针的全盘扫描的特点。
code
public class Two_Sum_II {
public int[] twoSum(int[] numbers, int target) {
int[] result = new int[2];
//这里用二分搜索,我常用start和end来命名两头,middle是中间。
int start = 0;
int end = numbers.length-1;
//这个while循环条件很巧妙,二分搜索建议固定一个模板,这个就挺好固定的。
while (start + 1 < end) {
//看,我刚说的是实话,而且这里middle的计算方法是防止越界。
int middle = start + (end-start)/2;
if (numbers[start] + numbers[end] < target) {
//这里需要判断,到底是跳一半还是走一步,就再加个判断。
if (numbers[middle] + numbers[end] < target) {
start = middle;
}
else {
start++;
}
}
else if(numbers[start] + numbers[end] > target) {
if (numbers[middle] + numbers[start] > target) {
end = middle;
}
else {
end--;
}
}
else {
break;
}
}
//题目中保证了有结果,还不是zero-based,那么就把result两项都续一秒。
result[0] = start+1;
result[1] = end+1;
return result;
}
}
复杂度分析
当然就是最坏情况O(n)了,也是标准的双指针复杂度。不过二分搜索方法是它最优情况是O(nlgn)。
最后再说两句
不得不说,这个题目把二分搜索和双指针拼在一起非常有创意。只用二分搜索让我多交了一个submit,好题一个。
Two Sum III - Data structure design
Design and implement a TwoSum class. It should support the following operations: add and find.
add - Add the number to an internal data structure.
find - Find if there exists any pair of numbers which sum is equal to the value.
For example,
add(1); add(3); add(5);
find(4) -> true
find(7) -> false
锁住的题,罕见的一道design题目和Google没关系,tag只有Linkedin,怪不得被收购了。
解决思路
这是一道入门级别的design题目,当然第一次遇到design这个词我就像脑血栓般浑身发抖。不过好在它脱胎于Two Sum,本质上还是不难的,我们要做的就是套上design的外壳,解决掉它。值得注意的是,一个数字只能用1次,所以还是要记录一下数字出现的次数的。
code
public class TwoSumIII {
//用一个List当容器装数字,用Map来记录数字出现的次数
List<Integer> list = new ArrayList<>();
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
// Add the number to an internal data structure.
public void add(int number) {
list.add(number);
//非常常规的往map里记录出现个数的写法
if (map.containsKey(number)) {
map.put(number, map.get(number)+1);
}
else {
map.put(number,1);
}
}
// Find if there exists any pair of numbers which sum is equal to the value.
public boolean find(int value) {
for (int i = 0; i < list.size(); i++) {
int cur = list.get(i);
int toFind = value - cur;
//这里还是Two Sum的求法,取一个,找另一个。值得注意的是需要看求和的两个数是不是相等。
if (cur != toFind) {
//根据leetcode测试,在map里面找比在list找目标数字更快一些。
if (map.containsKey(toFind)) {
return true;
}
}
else {
if (map.get(cur) > 1) {
return true;
}
}
}
return false;
}
}
复杂度分析
这种题应该不太需要分析复杂度吧,能实现就行。每次都是遍历一遍List,所以就是O(n)。
最后再说两句
写的时候发现其实遍历一下Map也行,可以省去一个list。但我偏偏不省,因为List不要钱,能加就加,而且看着也方便,一个map用于不同的用途,可能会引起线程冲突。出来混,多一事不如少一事。
3 Sum
这题用脚后跟看都是2Sum的follow up。就是在一个数组里面挑3个数字,这三个数字的和为0就行。需要注意的是triplet这个单词的拼写和发音,还有不能有重复的triplet,不能重复这一点还是有点儿小麻烦的
解决思路
既然是follow up,解决思路也就是follow up。follow up是什么意思呢,我们翻译一下,follow就是跟随,up就是上面。就是跟随上面的意思,我们往上看,上面只有2Sum一题,那我们就跟随一下。A+B
是2Sum,A+B+C
是3Sum,那么稍加修改A+(B+C)就成了这两道题连接的桥梁。所以这题的基本思路就是套了个壳子而已。
值得一提的是,此题可能有重复数字,而且要求不能有重复结果,所以使用双指针法。前面这句的不是很理所当然,在这里就当经验记录一下了,强行解释就是指针可以跳过重复的数字,而且求和也很容易。
code
public class ThreeSum {
public List<List<Integer>> threeSum(int[] nums) {
//首先把输出写出来
List<List<Integer>> result = new ArrayList<>();
//双指针出马之前数组通常需要排序
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
int cur = nums[i];
//这个本意是重复数字的话可以跳过。因为之前的数字已经打头过了,重复的就没必要打头了。
if (i > 0 && cur == nums[i-1]) {
continue;
}
//双指针出马,这里注意了我一般命名成left和right。
int left = i+1;
int right = nums.length-1;
//这里注意了开始2Sum过程。
while (left < right) {
int two_sum = nums[left] + nums[right];
if (two_sum < -1*cur) {
//说明加和小了,那把左指针往右移动
left++;
}
else if (two_sum > -1 * cur) {
//大了那就往左移动
right--;
}
else {
List<Integer> list = new ArrayList<>();
list.add(cur);
list.add(nums[left]);
list.add(nums[right]);
//把这次记录的结果加到result里面
result.add(list);
//下回测试corner case的时候就一群0,这次4个0就吃亏了,忘了这个while循环,所以要跳过重复数字。
while(left+1 < right && nums[left] == nums[left+1]){
left++;
}
while (right-1 > left && nums[right] == nums[right-1]) {
right--;
}
//跳过之后,继续挪动一下。
left++;
right--;
}
}
}
return result;
}
}
复杂度分析
这个排序的复杂度是O(nlgn),循环的复杂度就是O(n^2),所以就是循环的复杂度n方。
最后再说两句
其实这种数组题,无论多么的熟练,都要在纸上先勾画一下思路,尤其是这道题里面重复数字的处理,其实也可以用个set来去重,但那样毕竟颜值不行,不符合我自拍的一贯水准。跳过相等数字,最后再挪动一下,code里面不好分析,在纸上画画一目了然。
3 Sum Smaller
Given an array of n integers nums and a target, find the number of index triplets i, j, k with 0 <= i < j < k < n that satisfy the condition nums[i] + nums[j] + nums[k] < target.
For example, given nums = [-2, 0, 1, 3], and target = 2.
Return 2. Because there are two triplets which sums are less than 2:
[-2, 0, 1] [-2, 0, 3]
Follow up:
Could you solve it in O(n2) runtime?
这题居然是锁住的,company tag只有一个Google,所以把题目内容贴上来。
解决思路
这题说老实话让我很困惑,为啥这题都能当一道题出了。其实就是3Sum稍微变一下,然后返回个个数而不是一群triplet。而且要求是O(n2),那类比3Sum的双指针方法可以满足。
code
public class Three_Sum_Smaller {
public int threeSumSmaller(int[] nums, int target) {
//双指针是一定要排序的,否则后面根据大小挪动指针就没有意义了。
Arrays.sort(nums);
int result = 0;
for (int i = 0; i < nums.length-2; i++) {
int left = i+1;
int right = nums.length-1;
int cur = nums[i];
while (left < right) {
int two_sum = nums[left] + nums[right];
//这里需要注意如果满足条件,接下来不需要移动指针,直接把中间所有的可能性都加起来就可以
if (cur + two_sum < target) {
result += right - left;
left++;
}
else {
//只有和大了,才要让右边指针左移,让整体小一些。
right--;
}
}
}
return result;
}
}
复杂度分析
直接O(n2)了,就是两重循环,多说一句,双指针就是把O(n2)降成O(n)的,外面再套一层循环,就是O(n2)了。
最后再说两句
这题会做了,Google是不是能过一轮了啊。就注意小于的情况直接求result就行。
3 Sum Closet
还是一个数组,还有个目标数。返回距离目标最近的一个和,这个和是3个数的和。
解决思路
和上面一样,双指针,看大小。
code
public class ThreeSumClosest {
public int threeSumClosest(int[] nums, int target) {
if(nums == null || nums.length < 3){
return 0;
}
int res = 0;
int diff = Integer.MAX_VALUE;
Arrays.sort(nums);
for(int cur = 0; cur < nums.length-2; cur++){
int left = cur+1;
int right = nums.length-1;
int tempTar = target-nums[cur];
while(left < right){
int sum = nums[left] + nums[right];
int value = Math.abs(sum-tempTar);
//找到更小的就更新。
if(value <= diff){
diff = value;
res = nums[cur] + nums[left] + nums[right];
}
if(sum < tempTar){
left++;
}
else if(sum > tempTar){
right--;
}
else{
return target;
}
}
}
return res;
}
}
复杂度分析
还是O(n2).
最后再说两句
所以可以看出,很多题目思路一致,换汤不换药。都是双指针扫数组,非常容易。
4 Sum
这次是4个,就是找四个数,它们的和是目标数。
解决思路
这次就是3 Sum套了个壳而已,方法都是一样的。
code
public class FourSum {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
//象征性的说,如果没有4个数,那还玩个球啊
if(nums.length < 4) return res;
//双指针排序,都看腻了吧
Arrays.sort(nums);
for(int i = 0; i < nums.length-3; i++){
//去掉重复的数字,就是打头不能相同
if(i > 0 && nums[i] == nums[i-1]) continue;
for(int j = i+1; j < nums.length-2; j++){
//再去掉一遍
if(j > i+1 && nums[j] == nums[j-1]) continue;
//实力打脸,以后还是要left和right,low和high太low了。
int low = j+1, high = nums.length-1;
while(low < high){
int sum = nums[i] + nums[j] + nums[low] + nums[high];
if(sum == target){
//这里新建一个list也行。
res.add(Arrays.asList(nums[i],nums[j], nums[low], nums[high]));
while(low+1 < high && nums[low+1] == nums[low]){
low++;
}
while(high-1 > low && nums[high-1] == nums[high]){
high--;
}
low++;
high--;
}
else if(sum < target){
low++;
}
else{
high--;
}
}
}
}
return res;
}
}
复杂度分析
O(n3),因为是三重循环。
最后再说两句
这个系列结束了,没想到从2 Sum可以延伸这么长。不过核心思想都差不多,一些细节处,比如去掉重复数字这种手法需要多加熟练。
这个9月加油找了。