题目描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
题目分析
任何一道编程题,拿到题目都是从题干入手,思考题干的信息向我们透露出什么含义。这道题的题干中最有用的信息就是就是递增序列。显然,如果不把这个递增用上的话,就无异于是暴力查找,即便是普通的查找,需要找到满足特定条件的两个数最坏情况复杂度是
的,在其他时候接触到编程题目需要分析复杂度这一点也是要清楚的,过于暴力的方法(及不改变复杂度的优化)在此不赘述。
题干中还提到如果有多组,输出数组中乘积最小的,可以证明,如果两个数中较小者在多组数中最小,则这组数的乘积最小,以下有详细证明,可(不)以(感)理(兴)解(趣)的直接跳过。
乘积最小证明
a
。
暴力解法优化
暴力解法之所以暴力是在于没有将数组递增这个条件用上,以至于在查找的过程中无法根据查找的相关性来减少查找次数,首先知道就是两个数之和是S,所以两个数是有相关性的,如果我们查找到第一个数a,那么第二个数我们就知道是
了,所以我们直接去判断
是否在数组中即可。
我们要判断
是否在数组中,此时需要用到数组是递增的特点,相当于有序数组的查找,显然使用二分查找可以降低复杂度,同时,在编写代码的时候,第一个数字a我们不确定,只能从前往后(从小往大)遍历,在二分查找
时,前面的数组已经不需要遍历了,所以查找的时候,只需要从当前数字的后面到数组末尾进行二分查找即可。找到的第一组数字其中较小者肯定是最小的,所以该组数字乘积肯定是最小的。
核心代码是一个循环加二分查找,代码过于简单,就不需要写了。当然查找可以比较一下第一个数找到
就可以不继续往下找了,这些都是小技巧。
技巧解法
最好的解法一定是把所有题干信息都用到了,而且往往还要用到一定的特殊技巧。至于特殊技巧的部分可以当成定式然后去记忆,遇到类似的题目能够套用即可。
本题的特殊技巧是双指针,具体的操作流程是这样:判断两个指针如果不重合则一直循环,每次循环的过程是一个指针在数组中从前往后扫描,一个从后往前,如果两个指针指向的数字之和小于S,则前面的指针往后(大)移,如果两数之和大于S,则后面的指针往前(小)移动,如果两数之和等于S,则返回。
很多人对这个流程的原理可能不理解,下面我给出理由,同样,理解的可以跳过。
双指针原理探究
最差情况下,两边指针重合,相当于把整个数组都扫描了一遍,复杂度为
。如果存在两个数和为S,则扫描整个数组,一定可以找到其中的一个,不失一般性,假设先找到的是两个数中较小者,较小者指针为i,较大者指针为j。先找到较小者此时i指针确定,因为较大者的指针是从大向小扫描,则还没找到,此时j指向的数字一定是比较大者大的,然后j不断的减1直到找到该数字。为什么j指向的数字一定是比较大者大呢,因为如果j指向的数字比较大者小的话,那么与我们假设的先找到较小的数字矛盾。如果较大指针指向的数比较大数大,那么较大指针会持续减小,直到等于较大数。同样,先找到较大者也是一样的道理。
再看乘积最小的条件,之前证明过较小的数越小,则乘积越小,因为两数之和是相等的,同样说明较大的数越大,则乘积越小,乘积越小的两个数越是靠两边的,所以不管是较小的数还是较大的数总是比其他的一组数发现的更早。
综上,如果存在正确答案一定会找到,且满足乘积最小。
C++代码实现
class Solution {
public:
vector<int> FindNumbersWithSum(vector<int> array, int tsum) {
int i = 0, j = array.size() - 1;
while (i < j) {
i += (array[i] + array[j] < tsum);
j -= (array[i] + array[j] > tsum);
if (array[i] + array[j] == tsum)break;
}
vector<int>res;
if (i < j) {
res.push_back(array[i]);
res.push_back(array[j]);
}
return res;
}
};
最后再举一个用到这个方法思路的题目,大家可以自行思考。给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。