二分算法
概念
二分算法(Binary Search),也被称为折半查找,是一种高效的搜索算法,用于在已排序的数据集中查找特定元素。它的工作原理是将数据集分成两半,然后在其中一半中继续查找,以确定目标元素是否存在。
算法流程详述
二分条件
在一个有序的序列中;一个待查找的值x;
过程
取序列的两端,为l,r;
(l,r代表的可能是数组下标或者实在的数值,此处以数组下标为例,数组为a[])一般是闭区间。
取(l+r)/2为mid,a[mid]与x比较。进而更改l,r的值。
接着再求新的mid,以此类推,周而复始。
最终,l或r就为所求值x,或最接近x的值。
细节剖析
a[mid]与x的比较。
主要就两种情况,大于等于(小于),小于等于(大于);
(>=)这个求,不小于x的最小位置。与(<)是等价的,因为模板中的那个else。
(<=),求不大于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; }
过程描述
设置l,r端点。
取中间值mid,如果(<),说明mid在x的左边,因为没有(=),且是闭区间(闭区间,代表端点是可以取的)l=mid+1;
如果是else,说明在x右边,且mid可能就是为x,就不能像l那样+1,所以r=mid;
当在不断折半后,遇到只有两个值的时候,即l+1=r时。(这里,我们假设前提是,这个序列里存在x,那么此时,l,r里,一定有x)。分三种情况,
- l=x,r!=x,则x<r;
那么折半后,mid=l,满足else情况,那,r=mid=l;,之后循环停止。
- l!=x,r=x,l<x;
那么折半后,mid=l,满足if情况,那,l=mid+1=r;,之后循环停止。
- l=x,r=x,l=r=x;
那么折半后,mid=l,满足else情况,那,r=mid=l;,之后循环停止。
三者都能得到,正确的结果,没什么问题。
但在求最大位置的时候,就会出现问题。
这里,循环停止条件是l==r,所以l,r都是一样。
那么为什么这么肯定,l就是x得最小位置呢?
注意到,l每次变动的条件是(<),说明每次,l的左边的元素都不会比l所在的元素大。所以,l才是最小位置。
过程描述
我们先按照最小位置的模板进行,过程推演。只是把check的那个if语句写成,a[mid]<=x,和l和r取值做出相应的改变如下
int l = 0, r = n - 1; while (l < r) { int mid = l + r >> 1; if (a[mid]<=x) l=mid; else r = mid-1; }
照常,我们取mid,接着我直接过度到最后,只有两个元素的情况,即l+1=r。因为问题就是出现在这里。
还是三种情况。
- l=x,r!=x,则x<r;
那么折半后,mid=l,满足if情况,那,l=mid;依旧是l+1=r,之后就是死循环。
- l!=x,r=x,l<x;
那么折半后,mid=l,满足if情况,那,l=mid;,之后死循环循环。
- l=x,r=x,l=r=x;
那么折半后,mid=l,满足if情况,那,l=mid;,之后死循环。
总之,不管如何,总会出现死循环。导致这个模板出问题。
那么为什么会出现这个问题呢?
原因就在(l+r)/2这里,因为计算机是向下取整,导致,每次取得都是mid=l。
导致,每次都是满足if情况,而无法有效的更改l。
为了解决这个死循环问题。
就有的两种模板,还有一种模板我没有吃透,故不列出。
1号模板
最大位置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; }
可以看到,对mid的取值方式进行了改动。
因为死循环主要是不能向上取整导致的,所以我们给它加上个1,就可以在,l+1=r的时候,使得mid=r了;
那么再进行那三种情况的演算,就可以得到正确答案了。
这里不再累述,可以翻上去对照着算。
2号模板
最大位置2
int l1 = l, r1 = n; while (l1 + 1 < r1) { int mid = l1 + r1 >> 1; if (a[mid] <= x) l1 = mid; else r1 = mid; }
这里,改动了两处,循环条件和l,r的赋值。
死循环,是因为两个相邻的元素,取mid的时候,出现的问题。
那么,我们可以使得l,r在最后不相邻,就可以解决问题。
前面,我有提到,我们的l,r是闭区间。这里我们就可以采取左闭右开的区间。
即,r值是取不到。真实的有序区间是l到r-1;
那么当l+1=r的时候,说明就已经到最后一个值了,跟前面的1号模板,的l=r是等价的。
而这里因为,是开一侧的区间,就杜绝了相邻的情况。
这里进行演算一下。
取mid,如果满足if,说明在x的左边,因为有(=),所以可能mid==x,所以,就不+1,直接l=mid;
如果满足else,则说明在x的右边,比x大;且没有(=),本该r=mid-1的。
但r是开放的一侧,是取不到的,则r=mid。
我们再来到,最后时刻,就是只有两个元素的时候。
此时,l+2=r。
mid=(l+r)/2,为l,r中间的值rt;
也就是1号模板中的r了。即最右边的值。
再对照那三种情况
- l=x,rt!=x,则x<r;
那么折半后,mid=rt,满足else情况,那r=rt,因为循环条件为l+1<r,此时,循环就停止了,正确值为l;
- l!=x,rt=x,l<x;
那么折半后,mid=rt,满足if情况,那,l=mid;,循环停止,正确值为l。
- l=x,rt=x,l=rt=x;
那么折半后,mid=rt,满足if情况,那,l=rt;,之后循环停止,正确值为l。
那么为什么,肯定,l就是最大位置呢。这里,三种情况的第三个情况,最能说明问题。当有多个x,总是能取到最右边的那个元素。
进而确定最大位置。
二分特性
使用二分需要满足两个条件。一个是有界,一个是单调。
对于check函数,其作用就是对mid进行检验是否满足条件。
再根据这个结果去调制区间端点。