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