剑指Offer-44-和为S的两个数字

题目

输入描述

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

输出描述

对应每个测试案例,输出两个数,小的先输出。

解析

预备知识

首先,我们需要知道一个知识,就是对于一组总和相同的整数对集合,2个整数越靠近,乘积越大;相反,2个整数差越大,乘积越小。以下给出证明,若整数对为a,b,总和为s = a + b, 同时有另一对整数(a + m),(b - m),很明显他们的总和也是s。

ab=ab a ∗ b = a b

(a+m)(bm)=ab+(ba)mm2 ( a + m ) ( b − m ) = a b + ( b − a ) m − m 2

(a+m)(bm)ab=(ba)mm2=(bam)m ( a + m ) ( b − m ) − a b = ( b − a ) m − m 2 = ( b − a − m ) ∗ m

ba>m b − a > m

(bam)m>0 ( b − a − m ) ∗ m > 0

经过以上证明,可以发现(a + m)(b - m)的乘积一定大于ab,所以对于题目中输出两个数乘积最小的,只需从最小开始遍历递增的数组,那么第一组总和为S的整数对必为乘积最小的结果。

思路一

我们已经得出了只需从最小开始遍历,寻找一组整数对总和为S即可。那么当我们遍历到i时,常规的思路,可能是遍历剩余数组元素看看是否存在对应数使得他们的总和为S,但这样复杂度为O(n^2)。
既然时间都消耗在查找在另一半数字上了,那么我们可以采用空间换时间的做法。预遍历一半数组,把数组元素全部存在set中,再次遍历数组,对于当前元素i,判断集合中是否存在(S - i)即可。

    /**
     * 空间换时间
     * @param array
     * @param sum
     * @return
     */
    public ArrayList<Integer> FindNumbersWithSum3(int [] array, int sum) {
        ArrayList<Integer> result = new ArrayList<>();
        if(array == null || array.length < 2) {
            return result;
        }
        Set<Integer> sets = new HashSet<>();
        for(int i = 0; i < array.length; i++) {
           sets.add(array[i]);
        }
        for(int i = 0; i < array.length - 1; i++) {
           int num = sum - array[i];
           if(sets.contains(num)) {
               result.add(array[i]);
               result.add(num);
           }
        }
        return result;
    }
思路二

从思路一得出,我们只需高效的解决如何确定另一半是否在数组中即可。由于此题的数组是有序的,看到有序数组加上查找问题,肯定二分查找啊。所以废话不多说,快速手撸一个二分算法(二分查找简介,见这篇博客的预备知识):

    //不存在返回-1
    public int binarySearch(int[] array, int num) {
        int left = 0, right = array.length - 1;
        while(left <= right) {
            int mid = left + (right - left) / 2;
            if(array[mid] > num) {
                right = mid - 1;
            } else if(array[mid] < num) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

    /**
     * 二分查找
     * @param array
     * @param sum
     * @return
     */
    public ArrayList<Integer> FindNumbersWithSum(int [] array, int sum) {
        ArrayList<Integer> result = new ArrayList<>();
        if(array == null || array.length < 2) {
            return result;
        }
        for(int i = 0; i < array.length - 1; i++) {
            int num = sum - array[i];
            if(binarySearch(array, num) != -1) {
                result.add(array[i]);
                result.add(num);
                return result;
            }
        }
        return result;
    }
思路三

左右夹逼法。
left表示第一个数的索引,right表示第二个数的索引,初始:left = 0, right = array.length - 1
进行以下方法夹逼:

  1. 若left < right,开始循环
  2. 当 array[left] + array[right] > sum 时:查找区间应缩小为[left, right - 1],不可能为[left + 1, right],因为这样总和更大了。
  3. 当 array[left] + array[right] < sum 时:查找区间应缩小为[left + 1, right],不可能为[left, right - 1],因为这样总和更小了。
  4. 当 array[left] + array[right] == sum 时,即满足条件,结束循环
    /**
     * 左右夹逼
     * @param array
     * @param sum
     * @return
     */
    public ArrayList<Integer> FindNumbersWithSum2(int [] array, int sum) {
        ArrayList<Integer> result = new ArrayList<>();
        if(array == null || array.length < 2) {
            return result;
        }
        int left = 0, right = array.length - 1;
        while(left < right) {
            int temp = array[left] + array[right];
            if(temp < sum) {
                left++;
            } else if(temp > sum) {
                right--;
            } else {
                result.add(array[left]);
                result.add(array[right]);
                return result;
            }
        }
        return result;
    }

总结

有序 + 查找,可首选二分。之后可以进一步优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值