二分查找一般用来最一个有序的数组,快速查找到某个值出现的位置。
算法很好理解,但是算法里面的一些细节点之前一直困扰自己。现在借这个机会梳理一下。
零、二分查找递归实现的逻辑
有序数组中查找目标值。则目标值有三种情况
1、目标值在有序数组的中间位置;
2、目标值在有序数组的左边位置;
3、目标值在有序数组的右边位置。
假设有一个函数传入的是有序数组array,返回的是目标值target在有序数组中的位置index。 则
f(array)== array[mid] || f(0, mid - 1) || f(mid + 1, length)
一、问题
一直很疑惑的问题有
1、二分查找中传入的尾元素下标应该是lenght -1,还是length?
2、二分查找算法中对子数组左右边界的处理,到底是让right=mid,还是让right=mid-1;或到底是让left=mid,还是让left=mid=1?
二、解答
之所以会对最开始传入二分查找函数的参数尾元素下标的值产生疑惑,是因为在求mid=(left + right)的时候,给right传不同的值会影响mid的大小,从而进一步影响结果的正确性。
最开始传入二分查找函数的参数首元素,很清晰的知道要传数组起始下标0。
1、递归函数传入数组长度值length作为参数
假设我们传入的是length, 则二分查找算法如下(我自己写的,不知道有没有问题)
//先二分查找k在数组中出现的位置,再从该位置左右遍历出k出现的数量
int length = array.length;
int index = binarySearch(array, k, 0, length);
//二分查找算法
public int binarySearch(int [] array, int k, int start, int end) {
if (start == end) {
return -1;
}
int mid = (start + end) / 2;
if (k == array[mid]) {
return mid;
} else if (k < array[mid]) {
end = mid;
} else {
start = mid + 1;
}
int index = binarySearch(array, k, start, end);
return index;
}
可以看到直接传入length, 那么表示end是数组array中最后一个元素的下一个位置。
1、那么在算法中,如果start==end, 即说明没有找到k元素(因为end代表的是数组中尾元素的下一个位置,end其实不在数组中了);---递归失败退出条件。
2、接着求mid=(start + end) / 2。这其实对mid的求值是会有影响的。
假如传入的是(0, 5),则mid=(0+5)/2=2, 这和(0+(5-1))/2 = 2值是一样的。
假如传入的是(0, 4),则mid=(0+4)/2=2, 这和(0+(4-1))/2 = 1值是不一样的。
所以直接传入length会影响mid的求值。
当length为奇数的时候,(0 + 奇数)/2 ==(0+奇数-1)/2;
当length为偶数的时候,(0 + 偶数)/2 = (0+偶数-1)/2 + 1;
这意味着:
传入的数是奇数,则长度为奇数的数组会存在一个中间数字,那么mid=(0 + 奇数)/2= (0+奇数-1)/2,mid指向的刚好是数组最中间位置的数;
传入的数是偶数,则长度为偶数的数组会存在两个中间数字。mid=(0 + 偶数)/2是右边那个中间数,(0+偶数-1)/2是左边那个中间数。
总结一下这里,可以看到传入lengh和lengh-1,有可能mid值相同,或相差1。但是mid返回的都是中间位置的元素。
3、接下来就是用中间值和目标值进行判断了:注意这里给start和end赋新值时要清楚这两个变量分别代表的含义。
当k == array[mid]时,表明找到了元素k,直接返回下标mid;
当k < array[mid]时,表明k在数组的左边位置。需要获取mid左边左边子数组。只需要改变end值,因为end值代表的是数组中尾元素的后面一个值,所以end=mid;
当k > array[mid]时,表明k在数组的右边位置。需要获取mid位置处右边字数组。只需要改变start值,因为start值代表的就是数组中第一个元素的下标,所以start=mid + 1;
如果当前数组中间位置元素不是目标值,则拆分成左右子数组,再对目标子数组重新利用二分查找计算目标值出现位置,并返回其结果。 这里就引入了递归调用了。递归调用过程单独写一篇文章来梳理,这里先不管它。
2、递归函数传入(数组长度值length-1)作为参数
下面我们再来看看二分查找函数中传入的是(length-1),它表示的是数组的尾元素的下标值。那么我们传入的start和end就正好分别对应数组的起始和结尾元素下标。
public class MidFind {
public static void main(String[] args) {
int[] array = {1, 4, 6, 8, 10, 15, 49, 98};
int index = MidSearch(0, array.length - 1, array, 49);
System.out.println(index);
}
private static int MidSearch(int left, int right, int[] array, int target) {
if (left>=right){//当左右下标出现重合的时候,说明找不到,直接返回-1
return -1;//
}
int mid = (right + left) / 2;
if (array[mid] < target) {//向右递归查找
return MidSearch(mid + 1, right, array, target);
} else if (array[mid] > target) {//向左递归查找
return MidSearch(left, mid - 1, array, target);
} else {
return mid;//不满足大于、小于,那就是等于
}
}
}
这种情况感觉好理解一些了,每次递归中没有找到目标值,则让left=mid+1;或让right=mid-1。就把mid元素排除在下一次查找范围了。感觉更好理解一点。强烈推荐以后写二分查找就用这种写法,理解简单,实现也简单
三、二分查找进阶 之 循环实现二分查找
递归方式实现二分查找,逻辑非常好理解但是效率不高。其实递归本质上是可以利用循环来实现的,下面贴上循环实现的代码。
可以看到在一个循环中不断地求出mid值,然后先判断目标值是否等于array[mid]值,不等于的话把数组分成左右两边,进入下一次循环,每次循环重新计算mid值。while循环退出条件是l >r。默认返回-1表示没找到。
import java.util.*;
public class Solution {
public int search (int[] nums, int target) {
int l = 0;
int r = nums.length - 1;
//从数组首尾开始,直到二者相遇
while(l <= r){
//每次检查中点的值
int m = (l + r) / 2;
if(nums[m] == target)
return m;
//进入左的区间
if(nums[m] > target)
r = m - 1;
//进入右区间
else
l = m + 1;
}
//未找到
return -1;
}
}