二分法的边界处理
0 前言
本文分享了二分法的原理及左闭右闭和左闭右开两种不同写法,看完本文您将对二分法有一个全新的认识。(ps: 有二分基础的可以直接看二分法的两种写法,里面分享的是二分的边界处理细节问题)
1 二分法原理
说到原理,其实我们在上初中的时候就已经接触过它的思想了。

莫慌莫慌😁我放一张图你就会想起来

对于区间[a,b]上连续不断且f(a)·f(b)<0的函数y=f(x),通过不断地把函数f(x)的零点所在的区间一分为二,使区间的两个端点逐步逼近零点,进而得到零点近似值的方法叫二分法。
相信阁下已经想起来第一次接触二分思想的时候了哈哈!
1.1 定义一
其中维基百科上是这样定义二分的
二分法(dichotomy)指的是将一个整体事物分割成两部分。也即是说,这两部分必须是互补事件,即所有事物必须属于双方中的一方,且互斥,即没有事物可以同时属于双方。
1.2 定义二
百度百科是这样定义二分的
二分法(Bisection method) 即一分为二的方法. 设[a,b]为R的闭区间. 逐次二分法就是造出如下的区间序列([an,bn]):a0=a,b0=b,且对任一自然数n,[an+1,bn+1]或者等于[an,cn],或者等于[cn,bn],其中cn表示[an,bn]的中点。
相信读者朋友可以看出来维基百科上的定义更具有普遍性。我们不妨以猜数字游戏引出计算机领域的二分法的用处。
假如这里有1-10的数字,游戏玩家预先抽出一张数字,要求是使用最少的次数猜中抽出的数字 (ps:在猜测期间,游戏玩家会得到所猜数字是猜大了还是猜小了.)
这样我们猜测的时候就可以使用二分的思想进行猜测
- 首先,猜测 1-10 的中间数字 5 ,如果猜大了, 6-10 的数字就不可能是答案,答案只可能存在于 1-4 ,反之,答案存在于 6-10 .
- 接着,我们继续猜测 1-5 (6-10) ,选择 1-5 的中间值 3 ,如果猜大了, 4-5 的数字就不可能是答案,答案就只可能存在于 1-2 中
- 以相同的方式就可以寻找到答案.
下面用图解释一下

从上面猜数字游戏中我们可以尝试使用维基百科上的定义套一下
为什么我们可以折半猜测数字呢? 在猜测 mid == 5 时,若猜大了,为什么 5-10 的数字一定不是答案,其实这里面隐含着一个东西 : 有序 ,维基百科上说的互斥且互补,因为答案一定在 1-5 或 6-10 中,且不会同时存在两个区间当中,1-10 这十个数字是有序排列的,有序满足了互补且互斥,所以可以使用二分猜测数字.
在其他博主中也有这样描述二分法的
- 满足二段性
- 答案在二段性的分界点
其实本质上是一样的.
分享到这,我想说的是二分法的思想很简单,但难在细节的处理上,边界问题,这关系到区间收敛的成功与否,在写本片博客时,我也刚刚认真对待二分的边界问题,之前就是学个思想,只记了一种闭区间二分写法,几乎没有二分边界的意识.下面我就深入分享一下二分的边界问题,这也是二分的精髓所在.
2 二分法的两种写法
以 Leetcode T34 为例.

- 非递减(说明是有序的,可以使用二分来做)
- 寻找区间(目标值在数组中的开始位置和结束位置)
相信读者见过有些代码的while循环终止条件是 \(left <= right, [left,right]\); 有的是 \(left < right,[left,right)\) , 其实这就是今天要分享的重点之一 收敛点的处理方式 .
2.1 写法一 闭区间 \(left <= right\)
def binarySearch(nums:List[int], target:int) -> int:
left, right = 0, len(nums)-1
while left <= right: # 不变量:左闭右闭区间
mid = left + (right-left) //2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left # 若存在target,则返回第一个等于target的值
- 为什么 \(left <= right\) 时就是闭区间呢?
因为当 \(left = right\) 时仍然有意义, 此时 \(mid\) 被排除在区间意外,不会包括。 - 为什么 \(nums[mid] >= target\) 时 \(right = mid - 1\) 如果这样操作不是把等于正确答案的也排除在外了吗?
非也非也。当 \(nums[mid] = target\) 那一刻,$ right $ 确实变成 $ mid - 1 $了,这也就意味着 \(right\) 此时在 \(target\) 的左边,紧挨着 \(target\) ,此时 \(right\) 就不会在动了,\(left\) 会一直向 \(right\) 赶来,直到二者相遇,又因为 \(left = right\) 有意义,比较完之后,因为此时的 \(nums[right] < target\) ,故 \(left = mid + 1\) 也就是 \(left\) 再次向前移动一位,移到了 \(target\) 位置,此时 \(left <= right\) 不成立, \(return\) \(left\) 即是正确答案。

2.2 左闭右开区间 \(left < right\)
def binarySearch(nums:List[int], target:int) -> int:
left, right = 0, len(nums)-1
while left < right: # 不变量:左闭右闭区间
mid = left + (right-left) //2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left # 若存在target,则返回第一个等于target的值
- 为什么 \(left < right\) 时就是左闭右开区间呢?
因为当 \(left = right\) 时没有意义(不满足while循环条件), 此时 \(mid\) 包含在区间内,即 \(nums[mid]\) 有可能是答案。

当 \(nums[mid] = target\) 那一刻,$ right $ 确实变成 $ mid$ 了,这也就意味着 \(right\) 此时在 \(target\) 的位置(可能是第一个,可能不是),此时如果再遇到 \(nums[mid] = target\) , \(right\) 还会再动,\(left\) 和 \(right\) 会一直靠近,直到二者相遇
if nums[mid] < target:
left = mid + 1
大家再看这一句,$ left $ 把边界把的死死的,只要是小于 \(target\) 的值一律排除在外,所以当 \(left\) 和 \(right\) 相遇时,所指一定是 \(target\) (当然,前提是数组里面存在 \(target\))
大家会发现笔者是这样判断的
if nums[mid] < target:
left = mid + 1
else:
right = mid
- 即当 \(nums[mid] < target\) 时,\(left = mid + 1\) ,大家会发现 $ left $ 把边界把的死死的,只要是小于 \(target\) 的值一律排除在外。
- 当 \(nums[mid] >= target\) 时,$right = mid $,大家会发现 $ right $ 更加宽容一些,相等时也会移动,这就意味着 $ right $ 在向着 $ left $ 靠近,没错,这其实倾向于寻找存在重复元素时,第一个出现的元素。
如果找最后一个呢?
莫慌莫慌,这可以转换为寻找 \(target + 1\) 的第一个出现的位置,然后下标减一,这不就是 \(target\) 的最后一个元素了吗哈哈哈🌞
end = binarySearch(nums, target + 1) - 1
2.3 我们再来反证下面两个问题
2.3.1 为什么 \(while\) \(left <= right:\) 搭配 \(right = mid + 1\)
如果使用循环终止条件 \(while\) \(left <= right:\), 且 \(right = mid\) ,当 \(left\) 和 \(right\) 同时指向同一个 \(target\) 的时候,就死循环了。
2.3.2 为什么 \(while\) \(left < right:\) 搭配 \(right = mid - 1\)
如果使用循环终止条件 \(while\) \(left < right:\), 且 \(right = mid + 1\) ,当出现下图所示的情况时,就把 \(target\) 完美漏掉了。而且由于循环终止条件是 \(while\) \(left < right:\),无论如何 \(left\) 也不会跑到 \(right\) 右边去,因为二者相等时就 \(return\) 了。

2.4 当使用左闭右开区间 \(left < right\) 时,向上(下)取整问题
下面以两种情况为例。
2.4.1 求“满足条件”的最小值
把一些会玩王者荣耀的小朋友按照水平由低到高排好序,想要从中找到能够打败小明的水平最差的那个小朋友。(这里假设水平高一些的一定可以打败水平低一些的,大家都知道,队友坑人的话,那 \(5v5\)哪是 \(5v5\) 啊,那分明是 \(1 v(5+n))\)
while l < r :
mid = (l+r) >> 1
if check(mid) :
r = mid
else :
l = mid + 1
其中 $check(mid)¥是一个返回值为 \(bool\)类型的函数,当序号为 \(mid\) 的小朋友可以打败小明时,返回为 $ true$ ;否则返回 \(false\)。
我们仔细分析一下这几行代码,首先是第一条原则:答案一定在 \([ l,r ]\) 中。
- 当 \(check(mid)为true\) 时,说明 \(mid\) 可以打败小明,那么序号大于 \(mid\) 的一定也可以,而题目要找的是能打败他的水平最差的,所以正确答案一定在序号小于等于 \(mid\)(可能是\(mid\))的一侧,因此令 \(r = mid\) 而不是 \(r = mid - 1\)
- 当 \(check(mid)为false\) 时,说明 $ mid$ 已经无法打败小明,那么序号小于等于mid的小朋友都无法打败小明,正确答案一定在 \([mid + 1,r]\) 这个区间内,所以置 \(l = mid + 1\),而不是 \(l = mid\).
2.4.2 求“满足条件”的最大值
把一些会玩王者荣耀的小朋友按照水平由低到高排好序,想要从中找到不能打败小明的水平最高的那个小朋友。(这里仍然假设水平高一些的一定可以打败水平低一些的)
while l < r :
mid = (l + r + 1) >> 1
if check(mid) :
r = mid - 1
else :
l = mid
注意其中的改变了的地方,我们来分析为什么要变为这样,首先仍然是第一条原则:答案一定在\([ l,r ]\)中。
- 当 \(check(mid)为true\)时,说明 \(mid\) 可以打败小明,那么序号大于 \(mid\) 的一定也可以,而题目要找的是不能打败他的水平最高的,所以正确答案一定在序号小于mid(不包括mid)的一侧,因此令 $ r = mid - 1$ 而不是 $ r = mid$.
- 当 \(check(mid)为false\) 时,说明 \(mid\) 已经无法打败小明,那么序号小于等于 \(mid\) 的小朋友都无法打败小明,但是我们又要找不能打败他的水平最高的那个,所以正确答案一定在\([mid,r]\) 这个区间内(可能是 \(mid\)),所以置 \(l = mid\),而不是 \(l = mid + 1\).
注意这里变成了向上取整的方式 \(mid = (l + r + 1) >> 1\) , 原因如下:
如果 \(l、r\) 在某轮循环中分别是 \(2、3\),则 \(mid = 2\) ,若 \(check(mid)为false\),则 \(l = mid\),你会发现,咦?此轮循环后,\(l\) 和 \(r\) 的值都没变,不仅如此,从此往后每一次循环它们都不会再变了!死循环他来了!
其实不仅是第二种情况不正确的mid取值方式可能会陷入死循环,第一种也会,如果我们让第一种的mid取值方式改为 \(mid = (l + r + 1) >> 1\),同样存在陷入死循环的可能,仍然用上面2、3的例子,此时 \(mid =(2 + 3 + 1)>> 1\),即 \(mid = 3\),而如果 \(check(3)为true\),则\(r = mid = 3\),再次陷入死循环。
也就是说,如果是r=mid,则mid应该向下取整;如果是l=mid,则要向上取整.
3 总结
好了,以上就是笔者想要分享的关于二分法的全部内容了,若有错误,请大佬们不吝评论区赐教,小生当俯身倾耳以请😁。希望笔者的分享能够帮助到您!
参考
- https://zhuanlan.zhihu.com/p/275995132?utm_id=0
- https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%B3%95
- https://blog.csdn.net/qq_45734984/article/details/120331469?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-120331469-blog-122775097.235%5Ev38%5Epc_relevant_anti_t3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-120331469-blog-122775097.235%5Ev38%5Epc_relevant_anti_t3&utm_relevant_index=2

浙公网安备 33010602011771号