题目
输入描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
输出描述
对应每个测试案例,输出两个数,小的先输出。
解析
预备知识
首先,我们需要知道一个知识,就是对于一组总和相同的整数对集合,2个整数越靠近,乘积越大;相反,2个整数差越大,乘积越小。以下给出证明,若整数对为a,b,总和为s = a + b
, 同时有另一对整数(a + m),(b - m),很明显他们的总和也是s。
a∗b=ab
a
∗
b
=
a
b
(a+m)(b−m)=ab+(b−a)m−m2
(
a
+
m
)
(
b
−
m
)
=
a
b
+
(
b
−
a
)
m
−
m
2
(a+m)(b−m)−ab=(b−a)m−m2=(b−a−m)∗m
(
a
+
m
)
(
b
−
m
)
−
a
b
=
(
b
−
a
)
m
−
m
2
=
(
b
−
a
−
m
)
∗
m
b−a>m
b
−
a
>
m
(b−a−m)∗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
进行以下方法夹逼:
- 若left < right,开始循环
- 当 array[left] + array[right] > sum 时:查找区间应缩小为[left, right - 1],不可能为[left + 1, right],因为这样总和更大了。
- 当 array[left] + array[right] < sum 时:查找区间应缩小为[left + 1, right],不可能为[left, right - 1],因为这样总和更小了。
- 当 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;
}
总结
有序 + 查找,可首选二分。之后可以进一步优化。