题目:输入一个递增排序的数组和一个值k,请问如何在数组中找出两个和为k的数字并返回它们的下标?假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。例如,输入数组[1,2,4,6,10],k的值为8,数组中的数字2与6的和为8,它们的下标分别为1与3。
我们最容易想到的方法是,先扫描到数字i,再依次判断其余数字之和是否等于k,假设数组长度为n,因为每个数字要和其他n-1个数字组合,所以这种解法的时间复杂度是O(n^2)。
利用二分查找优化
同样的,先扫描到数字i,再判断数组中是否存在另一个数字k-i,不过因为数组是递增的,所以我们没必要从头到尾扫描每个数字,这里可以用二分查找进行优化。因为二分查找时间复杂度为O(logn),因此优化后解法的时间复杂度是O(nlogn)。
public static int[] twoSum1(int[] numbers, int target) {
int[] result = new int[2];
for (int i = 0; i < numbers.length; i++) {
//计算k-i的差值
int complement = target - numbers[i];
//在右侧有序部分二分查找差值
int index = binarySearch(numbers, complement, i + 1, numbers.length - 1);
if (index != -1) {
//如果找到了差值
result[0] = i; //当前数字的下标
result[1] = index; //差值的下标
break;
}
}
return result;
}
//二分查找
private static int binarySearch(int[] numbers, int target, int left, int right) {
while (left <= right) {
//获取中间元素的下标
int mid = left + (right - left) / 2;
if (numbers[mid] == target) {
//差值下标
return mid;
} else if (numbers[mid] < target) {
//说明差值在右侧,缩小查找范围到右侧部分
left = mid + 1;
} else {
//说明差值在左侧,缩小查找范围到左侧部分
right = mid - 1;
}
}
return -1;
}
使用哈希表查找
我们可以用空间换时间,使用一个哈希表来存储数组中的数字和对应的下标。我们遍历数组中的每个数字,对于每个数字,我们计算目标值与当前数字的差值complement。然后,我们在哈希表中查找是否存在该差值。哈希表中查找一个数字的时间复杂度为O(1),因此,时间复杂度是O(n),不过,该解法需要一个大小为O(n)的哈希表,因此空间复杂度是O(n)。
public static int[] twoSum2(int[] numbers, int target) {
int[] result = new int[2];
//创建一个哈希表
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < numbers.length; i++) {
//计算与当前数字的差值
int complement = target - numbers[i];
//判断差值是否存在于哈希表中
if (map.containsKey(complement)) {
//如果找到了差值
result[0] = map.get(complement); //获取差值在数组中的下标
result[1] = i; //当前数字在数组中的下标
break;
}
//将当前数字和下标放入哈希表中
map.put(numbers[i], i);
}
return result;
}
使用双指针
我们可以定义一个左指针i指向数组的开头,一个右指针j指向数组的末尾。然后我们不断地向中间移动左指针和右指针,直到找到和为k的两个数字或者左指针大于等于右指针为止。
在每次移动指针时,我们比较当前两个指针指向的数字之和与目标值target的大小。如果sum等于target,则找到了答案。如果sum小于target,则说明左指针的值太小,需要向右移动左指针。如果sum大于target,则说明右指针的值太大,需要向左移动右指针。
public static int[] twoSum3(int[] numbers,int target){
int i = 0; //左指针,开始指向数组第一个(最小)元素
int j = numbers.length - 1; //右指针,开始指向数组最后一个(最大)元素
//循环退出条件左指针大于等于右指针或者两指针指向数字之和等于target
while (i < j && numbers[i] + numbers [j] != target){
if (numbers[i] + numbers[j] < target){
//两指针指向数字之和小于target,我们会希望数字大一些
i++;
}else {
//两指针指向数字之和大于target,我们会希望数字小一些
j--;
}
}
int[] result = new int[]{i,j};
return result;
}
该算法只有一个while循环,循环次数最多为数组长度,因此该解法时间复杂度为O(n),而空间复杂度是O(1)。