十个二分九个错?教你如何优雅地写二分查找

俗话说,十个二分九个错,二分算法整个过程了解不困难,但各种边界条件的判断错误常常是我们WA的原因,二分真的这么难写吗?如果没有对应的方法,确实是的。于是我在网上查了一些资料,总结了一些自己认为简单有效,容易理解的优雅的二分写法

参考博客你真的会写二分吗

一,小数二分

这个比较简单,模板也很单一,没什么好说的

 1 const double eps = 1e-8
 2 
 3 double search(double low, double high) {
 4     while (high - low > eps)
 5     {
 6         double mid = (low + high) / 2;
 7         if ( judge(mid) )
 8             high = mid;
 9         else 
10             low = mid;
11     }
12     return (low + high) / 2;
13 }

二,整数二分

首先得确定,二分一般是用来查找一段有序的连续数字中的某个位置的数字,注意:有序且连续,也就是说二分不一定是在数组上进行操作的,它非常灵活,不过虽然不同题目二分的要求不同,但是他们的思路是一样的

我们把问题抽象为在一组有序连续数列中,根据一个key值寻找该数列某一位置的问题,

那么根据key值通常可以把数列分为两部分(以下全部假设为非递减数列)

第一种:

......a1,a2,a3,b1,b2,b3,b4,b5......     //红色部分表示,b1和b1往后的数字都大于或等于key值,而a3和a3前面的数字都小于key值

第二种:

......b1,b2,b3,b4,b5,a1,a2,a3.....    //红色部分表示,b5和b5前面的数字都小于或等于key值,而a1和a1后面的数字都大于key值

 

 

为什么要这么分呢,因为我们二分是通过不断与key值判断来划分区间的,也就是说我们要找的位置就在a3与b1(或b5和a1)这个分界点的两边,

举个例子,有一组数列 1 2 3 4 4 4 4 5 6 7.假设让key值为4,根据第一种分法可以得到下面的序列

1 2 3 | 4 4 4 4 5 6 7 

现在我们想找小于key的最后一个元素的位置,是不是就是黑色数字和红色数字分界线的左边的第一个数字3?

又比如我们想找第一个等于key的元素的位置,那就是分界线右边的第一个数字对吧

 

但是如果我想找大于key的第一个数字怎么办?那么就要用到我们的第二种分法了

1 2 3 4 4 4 4 | 5 6 7

显然大于key的第一个数字的位置就是分界线的右边那个位置。

再比如等于key的最后一个位置就是分界线左边的那个位置

 

虽然种种问法互不相同,什么大于key小于key,最后一个第一个的,我们发现不管怎么问都能回归到到底是求分界线左边的位置还是分界线右边的位置,所以我们没必要去记,也容易记错

我们只需要考虑到底是用第一种分法还是第二种分法,到底是求分界线左边位置还是右边位置就好了

下面给出通用代码

1 while (left <= right) {
2         mid = (left + right)>>1;
3         if (key ? arr[mid])     //第一个问号决定了,你是用哪种分段法
4             right = mid - 1;
5         else 
6             left = mid + 1;
7     }
8     return ?;               //第二个问号决定了,你是选分界线左边还是右边的位置

第一个问号:

首先我们可以确定的是,key<arr[mid]的时候,显然我们选当前区间的左半边,当key>arr[mid]的时候就选右半边,

现在的问题就是,当key=arr[mid]时候选哪半边,实际上根据前面的分析很容易得知,当我们让key=arr[mid]时取right=mid-1(相当于选当前区间的左半边),就是让分界线不断左移,比如假设一个有很多key值2的序列1 2 2 2 2 2 2 2 2 2 3,

如果二分的时候当key=arr[mid]取左半边,不断取不断取,最后就会取到最前面的1 2 这个分界,

反之如果key=arr[mid]的时候取右半边(left = mid+1),不断取不断取,分界线不断向右移动直到2 3这个分界,这样很容易理解吧?

所以第一个问号取<=,就对应了前面所说的第一种分段法,取<就对应第二种

 

第二个问号:

根据while循环的条件我们知道,循环结束的标志是left > right,更具体一点来说,每次循环结束,left = right+1 记住!每次结束left在right右边一个位置

而这个left 和 right,就恰好在分界线的两边,还是拿第一个例子来说数列1 2 3 4 4 4 4 5 6 7,

第一种分段法: 1 2 3 | 4 4 4 4 5 6 7 

显然,结束的时候,right指向分界线左边,即arr[right] = 3, 而arr[left]自然就等于4了

第二种分段法: 1 2 3 4 4 4 4 | 5 6 7

同理,结束的时候,right指向分界线左边,即arr[right] = 4,  而arr[left] = 5

 

看完这些,有人可能会问,如果数列中没有等于key的元素怎么办?实际上如果数列中没有等于key的元素,第一种分段法和第二种分段法就是相同的了,即:小于key的元素在分界线左边,大于key的元素在分界线右边, 这样处理其实和含key元素是一样的(这个时候第一个问号填<= 和 < 效果都是一样的)

注意!!当你的key值比边界值还要大或者小的时候,left或right的值是可能超过数组范围的,

比如数列1 2 3 4 5 6 7,你要求比0大的第一个数时,分界线在最左边,即 | 1 2 3 4 5 6 7

这时候left = 0,而right = -1 所以arr[left] = 1, 而arr[right]是越界的!换句话说,如果你在这个序列中求最后一个比0小的数字时,得到的结果是arr[right]未定义,即没有比0小的数字,这一点有时要在程序中特殊判定一下

 

posted @ 2018-05-10 17:32  LBNOQYX  阅读(962)  评论(0编辑  收藏  举报