二分查找的本质是边界效应;
给定一个区间,在上面定义一种性质,使得左右区间一分为二,左边的区间都满足性质A,而右边的区间都不满足性质A,运用二分查找可以找出这个边界;
具体过程
先取mid = (l+r)/2,然后检验mid的性质,
如果mid具有性质A(即mid在左边区间),则边界一定在mid的右边,可已修改区间,令左边界l = mid,再重复上面的过程;
如果mid不具有性质A,则边界一定在mid的左边;
整数的二分的难点
难点在于存在复杂的边界分析,下面我们来看一下代码;
代码的目的
是为了确定在一个有序数组中x的其实位置和结束位置;
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (a[mid] < x) l = mid + 1;
else r = mid;
}
注意:
这里的的check函数是a[mid] < x;
这样的话,mid一定不会是边界,所以左边界的话,要取mid+1;
*如果存在多个边界(存在多个相等的边界值)的话:*上面的代码所求出的边界就会是左边界;
下面我们来分析下如何确定右边界:
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
分析一下两个代码的不同之处:
首先由于不同的目的,两个函数中的check函数并不一样,如果满足if语句,则边界一定在mid及其右边,如果不满足mid,则一定落在mid的左边;
下面是一种错误的写法
while (l < r)
{
int mid = l + r >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
这一个代码的错误在与会造成死循环,原因如下:
如果mid经运算等于l且满足if语句,则会发现l会赋值为mid,造成 l 的取值并没有发生改变,即要确定的范围并没有发生改变,下一次循环还是和这一次一样;
我们能采用二分的的目的就是为了不断的缩小范围来确定边界的范围,所以不修改边界是十分危险的;在第一个代码中我们用对mid是用的向下取整,但对 l 的修改中我们并没有让 l = mid所以不存在危险;
在这里类比上面的思想,我们可以采用想上取整来转移风险;因为r的赋值语句是 r = mid -1;