Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1478 字
7 分钟
二分查找:为什么你的边界条件总是写错

二分查找的”入门易,精通难”#

二分查找大概是每个编程入门者最先接触的算法之一。从原理上看,它再简单不过:在一组有序数据中,每次排除一半,直到找到目标。很多人一看就”懂了”,但真到写代码的时候,循环条件是 left <= right 还是 left < rightright 更新到底是 mid 还是 mid - 1?mid 怎么算才不溢出?

我见过太多人(包括以前的我)在面试的时候,明明知道是二分,结果边界条件调半天调不出来。

根子上的问题在于:二分查找看起来是同一个算法,实际上因为区间定义的不同,衍生出了至少三种写法。你在一篇文章里学了一半,另一篇文章又学了另一半,混在一起自然出 bug。


核心理解:区间决定了边界#

二分查找的所有变体,本质上只取决于你如何定义”搜索区间”。

闭区间写法 [left, right]#

区间两端都包含。初始 left = 0right = n - 1

def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right: # 等号不能省:left==right 时区间还有一个元素
mid = left + (right - left) // 2 # 防溢出写法
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1 # mid 已经检查过,所以 +1
else:
right = mid - 1 # mid 已经检查过,所以 -1
return -1

为什么 while left <= right?因为 [left, right]left == right 时区间还有一个元素 [x, x],得检查它。当 left > right 时空区间 [3, 2] 才意味着没找到。

为什么 left = mid + 1right = mid - 1?因为 mid 已经跟 target 比较过了,确认不相等,所以下次直接跳过它。

动画中的关键点: 每一步用 mid 比较后,不相等的那半边直接灰掉(排除),剩下的区间缩小一半。括号里的 left 和 right 值看熟了,面试手写就再也不会犹豫。

开区间写法 [left, right)#

right 设为 n(不在区间内),循环条件变成 left < right

def binary_search2(nums, target):
left, right = 0, len(nums) # right 不包含在区间内
while left < right: # left==right 时空区间
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid # mid 没被排除,但作为新区间的右边界(不包含)
return -1

两种写法都能用,选一种吃透就行。我个人习惯闭区间写法,因为它和数组下标的直觉保持一致。


两个最容易栽的坑#

mid 越界#

很多人写 mid = (left + right) // 2。当 left + right 超过 int 上限时,就溢出了。Python 没这个问题(大整数自动扩展),但 Java、C++、Go 等语言都会炸。

统一写法:

mid = left + (right - left) // 2

死循环#

两个元素时 left = 0, right = 1,如果用 mid = (left + right) // 2 且更新写成 left = mid,那 mid 永远是 0,left 永远不前进——死循环。解决方法:每次更新 left 或 right 时必须跳过 mid


实战#

704. 二分查找#

最标准的二分查找,直接套上面的闭区间模板就能过:

class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1

33. 搜索旋转排序数组#

这道题是二分查找的经典变种。数组先升序排列,再从某个位置旋转了一次(比如 [0,1,2,4,5,6,7] 旋转成 [4,5,6,7,0,1,2])。要求 O(log n) 查找 target。

关键观察: 随便切一刀 mid,左右至少有一半仍然是有序的。利用那一半判断 target 在不在里面,从而决定收缩方向。

class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
# 左半有序
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1 # target 在左半
else:
left = mid + 1 # target 在右半
# 右半有序
else:
if nums[mid] < target <= nums[right]:
left = mid + 1 # target 在右半
else:
right = mid - 1 # target 在左半
return -1

本质上还是二分查找,只是判断条件不再是单纯比大小,而是”先判断哪半有序,再看 target 在不在有序那半里”。


二分查找的通用框架#

把上面的思路抽象出来,二分查找的所有变种都长这样:

def binary_search_template(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
# 根据题目定义"check"逻辑
if check(mid, target):
return mid # 找到了
elif need_go_right(mid, target):
left = mid + 1 # 向右搜
else:
right = mid - 1 # 向左搜
return -1 # 或 left(找插入位置等变种)

不同的题只需要改 checkneed_go_right 两个逻辑。题做多了之后,你手里握着的不再是几十个变种的分散记忆,而是一个框架 + 两个判断。


更多二分变种#

场景LeetCode关键变化
标准查找704
旋转数组33先判断有序区间
查找左边界34找到 target 后继续往左缩
查找右边界34找到 target 后继续往右缩
搜索插入位置35返回 left 而不是 -1
山脉数组峰值852check 不再比 target,而是比 nums[mid+1]
x 的平方根69check 是 mid*mid <= x
寻找峰值162往”上坡”方向走

这些都基于同一个框架,换汤不换药。


二分查找写到一定程度,你会发现自己不再去背 <= 还是 < 了——只要想清楚区间是怎么定义的,边界条件就是自然推导的结果,而不是死记硬背的规则。

二分查找:为什么你的边界条件总是写错
https://qiandaos.top/posts/binary-search/
作者
千岛寒流
发布于
2025-05-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00