剑指offer面试题57(java版):和为s的两个数字 所有和为S的连续正数序列

welcome to my blog

剑指offer面试题57(java版):和为s的两个数字

题目1描述

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

思路

  • 见注释
  • 最开始满足条件的两个数就是乘积最小的! 因为left+right=s, z=left*right=left*(s-left), 可以看出z是开口向下的二次函数,第一个满足条件的left使得z最小

复杂度

  • 时间复杂度, left,right从两端向中间扫描数组, 只扫描一次, 时间复杂度为O(n)
第三次做, 有序数组中使用双指针容易确定指针的移动方向, 但是要会通过将例子说明为什么left只能向右, right只能向左; 因为一开始假设left向右,right向左, 此种假设下如果right向右移动, 会重复判断已经判断过的情况
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> res = new ArrayList<>();
        if(array==null || array.length<2)
            return res;
        //双指针
        int left=0, right=array.length-1, curr;
        while(left<right){
            curr = array[left] + array[right];
            if(curr > sum)
                right--;
            else if(curr < sum)
                left++;
            else{
                res.add(array[left]);
                res.add(array[right]);
                return res;
            }
        }
        return res;
    }
}
第二次做, 有序数组中使用双指针, 指针的移动方向容易确定; while条件尽量写得具体些, 别太泛, 见注释
import java.util.ArrayList;
/*
改成O(N)的算法,不适用双层循环, 而是使用双指针
因为数组是有序的,所以指针的移动是固定
*/
public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> res = new ArrayList<>();
        int left=0, right=array.length-1;
        int tempSum=0;
        //while的条件尽量写得具体, 这里不能写成left != right, 写成这样array==null时不能通过,需要加代码
        while(left < right){ 
            tempSum = array[left] + array[right];
            if(tempSum < sum)
                left++;
            else if(tempSum > sum)
                right--;
            else{
                res.add(array[left]);
                res.add(array[right]);
                break;
            }
        }
        return res;
    }
}
第二次做,直接暴力搜索, 利用乘积最小这个条件, 数组两段往里遍历, 找到的第一个结果就是最终结果
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> res = new ArrayList<>();
        //给定的数组中不知道有没有负数, 暂时当做没有吧, 作为面试题的话就问问面试官
        //a+b=sum时, |a - b|越大则a*b越小, 所以从两头遍历到的第一个结果就是最终结果
        for(int i=0; i<array.length-1; i++){
            for(int j=array.length-1; j>=1; j--){
                if(array[i] + array[j] == sum){
                    res.add(array[i]);
                    res.add(array[j]);
                    return res;
                }
            }
        }
        return res;
    }
}
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        /*
        思路: 使用两个指针, left指向开头, right指向结尾;  left只能向右移动, right只能向左移动
        如果array[left] + array[right] == sum, 则保留这两个数
        如果array[left] + array[right] > sum, 则左移right指针(不能左移left, 因为规定left只能右移)
        如果array[left] + array[right] < sum, 要么右移left,要么右移right; 其实只能右移left指针(不能右移right, 因为上一轮循环就是当前right右移一位的情况, 说明不满足条件才到了这一轮循环)
        */
        ArrayList<Integer> al = new ArrayList<Integer>();
        //input check
        if(array.length<2 || array==null)
            return al;
        //execute
        int left=0, right=array.length-1;
        while(left<right){
            if(array[left]+array[right]==sum){
                al.add(array[left]);
                al.add(array[right]);
                break;
            }
            else if(array[left] + array[right] < sum)
                left++;
            else 
                right--;
        }
        return al;
    }
}

题目2描述

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

输出描述

输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序

第四次做; 核心: 1)双指针, left,right都从左往右 2)等差数列求和公式的计算结果是精确的 3)right最多移动到(target+1)/2即可, 如target=8或者target=9; 4)sum==target时, 更新结果, 别忘记移动指针!!! 5)有序数组, 使用双指针; 对双指针不熟
class Solution {
    public int[][] findContinuousSequence(int target) {
        if(target<3){
            return null;
        }
        int left=1, right=2;
        List<int[]> list = new ArrayList<>();
        while(right<=target/2+1){
            int sum = (left+right)*(right-left+1)/2;
            if(sum==target){
                int[] tmp = new int[right-left+1];
                for(int i=0; i<right-left+1; i++){
                    tmp[i] = left+i;
                }
                list.add(tmp);
                left++;
                right++;
            }else if(sum<target){
                right++;
            }else{
                left++;
            }
        }
        int[][] arr = new int[list.size()][];
        for(int i=0; i<arr.length; i++){
            arr[i] = list.get(i);
        }
        return arr;
    }
}
/*
等差数列求和
双指针: left, right都是从左往右
*/
class Solution {
    public int[][] findContinuousSequence(int target) {
        if(target<=2)
            return null;
        List<List<Integer>> list = new ArrayList<>();
        int left=1, right=2;
        while(right<=(target+1)/2){
            int sum = (left + right)*(right-left+1)/2;
            if(sum < target)
                right++;
            else if(sum > target)
                left++;
            else{
                list.add(new ArrayList<>());
                for(int i=left; i<=right; i++){
                    list.get(list.size()-1).add(i);
                }
                //核心:这里别忘记更新
                left++;
                right++;
            }
        }
        
        /*
        这里可以用java1.8的stream获取数组, 代码简洁些
        int[][] res = new int[list.size()][];
        for(int i=0; i<res.length; i++){
            res[i] = list.get(i).stream().mapToInt(k->k).toArray();
        }
        */
        
        int[][] res = new int[list.size()][];
        int index=0;
        for(List<Integer> al : list){
            res[index] = new int[al.size()];
            for(int j=0; j<al.size(); j++){
                res[index][j] = al.get(j);
            }
            index++;
        }
        return res;
    }
}

思路

  • 双指针, 见注释
  • 要注意的地方是currSum = (left+right)*(right-left+1)/2;, 举个例子: 3/2=1, 2/2=1, 也就是说分子是3或者2时,分式的结果是一样的, 也就是分子是3时,求出的currSum是错的. 为避免这种错误, 可以求2*currSum的值, 比较if(2*currSum == 2*sum). 其实这道题中的分子一定是偶数, 分式结果是精确的, 可以直接使用currSum = (left+right)*(right-left+1)/2;, 简单地证明一下,将分子化简后可以得到-left(left-1)+right(right+1), 其中left(left-1)一定是偶数, right(right+1)一定是偶数, 所以分子一定是偶数!
第三次做; 等差数列求和结果不会出现小数; 当连续序列长度为2时,序列的两个加数最大, 可以用来放缩right
import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
       ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if(sum<3)
            return res;
        int left=1, right=1, curr;
        while(right<(sum+1)/2+1){
            curr = (left + right)*(right - left + 1) / 2;
            if(curr < sum)
                right++;
            else if(curr > sum)
                left++;
            else{
                res.add(new ArrayList<Integer>());
                for(int i=left; i<=right; i++)
                    res.get(res.size()-1).add(i);
                /*
                left的更新不要太激进, 容易漏结果
                如, 15; [[1,2,3,4,5],[4,5,6],[7,8]]
                */
                left++;
                right++;
            }
        }
        return res;
    }
}
第二次做, 这道题的关键就是什么情况下如何更新左右边界,一共三种情况; 可以稍稍分析题意后放缩right的上限
  • 比起一项一项累加得到temp, 使用等差数列求和公式的好处是, 不用在更新左右边界的时候调整temp的值
import java.util.ArrayList;
/*
这次试试用等差数列的求和公式,不再一个一个累加计算sum了
左右边界的更新方式还是一样
*/
        /*
        稍微放缩一下right的边界情况,和为sum的子序列的最大值是几? 是当子序列长度为2的时候
        x + ( x + 1) = sum
        x = (sum - 1) / 2
        x + 1 = (sum + 1) / 2
        sum==10时
        x = 4.5
        x+1 = 5.5
        sum==11时
        x = 5
        x+1 = 6
        
        所以子序列最大取值为Math.ceil((sum+1)/2)
        */
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
        if(sum <= 2)
            return res;
        int left=1, right=2, temp=0;
        while(right<= Math.ceil((sum+1)/2)){
            temp = (left + right)*(right - left + 1) / 2;
            if(temp < sum){
                right++;
            }
            else if(temp > sum){
                left++;
            }
            else{
                ArrayList<Integer> al = new ArrayList<>();
                for(int i=left; i<=right; i++)
                    al.add(i);
                res.add(al);
                left++;
            }
        }
        return res;
    }
}
第二次做, if语句中语句的执行顺序!见注释, 简单好说一下就是因为需要在if中保持计算temp时用的right和right++后的right一样,所以得先right++,再去计算temp; 左右边界如何变化也是关键
import java.util.ArrayList;
/*
这个子数组的左右边界怎么更新?
*/
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if(sum<1)
            return res;
        //
        int left=1, right=1;
        int temp=1;
        while(right<sum){
            if(temp < sum){
                right++; //这两句的顺序互换会怎么样?
                temp += right;//这两句的顺序互换会怎么样?  temp还需要检查是否合格,如果合格的话right才能++,不合格的话不能++,所以这两句不能互换位置, 原因在于temp有多种情况
            }
            else if(temp > sum){
                temp -= left;
                left++;
            }
            else{
                ArrayList<Integer> al = new ArrayList<>();
                for(int i=left; i<=right; i++)
                    al.add(i);
                res.add(al);
                temp -= left;
                left++;
            }
        }
        return res;
    }
}
import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        /*
        思路: 仍然采用双指针, left指向1, right指向2, 这两个指针只能右移
        如果(left+right)*(right-left+1)/2 == sum, 则保存left,...,right
        如果(left+right)*(right-left+1)/2 < sum, 则右移right
        如果(left+right)*(right-left+1)/2 > sum, 则右移left
        */
        ArrayList<ArrayList<Integer>> alal = new ArrayList<ArrayList<Integer>>();
        int left=1, right=2, currSum;
        while(left < right){
            currSum = (left+right)*(right-left+1)/2; //丢弃的小数部分会不会影响结果? 比如3/2=1, 2/2=1
            if(currSum == sum){
                ArrayList<Integer> al = new ArrayList<Integer>();
                for(int i=left; i<=right; i++)
                    al.add(i);
                alal.add(al);
                left++;  // 这一步很关键, 增加下界以减少元素个数从而减少currSum, 进而寻找其他满足条件的序列
            }
            else if(currSum > sum)
                left++;
            else // currSum < sum
                right++;
        }
        return alal;
    }
}

精彩的答案, 利用了等差数列的性质

  • a/b的小数是0.5意味着余数是b的1/2
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值