Binary Search | 二分查找

Albert Wang / 2026-03-15 / 600 Words/has been Read   Times


二分查找的边界条件之所以容易让人困惑,是因为循环的终止条件搜索区间的定义以及左右边界的更新方式三者必须严格保持一致。如果其中一个地方搞错了,就会导致死循环或者漏查元素。

在开始之前,请记住一个原则:区间不变性。 也就是说,你定义的 [left, right] 或者 [left, right),在每一轮循环开始的时候,都要保证目标值(如果存在)一定在这个范围内。边界更新的目的,就是根据中间值的比较结果,缩小区间但保持这个性质

闭区间写法 [left, right] #

这是最直观的写法。

  • 区间定义:left 和 right 都是有效的数组下标,即搜索范围包含两端。
  • 循环条件:left <= right。因为当 left == right 时,区间 [left, right]内还有一个元素,需要继续查找。
  • 右边界更新:right = mid - 1。因为 mid 已经检查过了,且比目标值大(假设场景),所以新的右边界应该是 mid 前面的那个位置。
  • 左边界更新:left = mid + 1。同理,mid 已经检查过,新的左边界是 mid 后面的位置。
def binary_search_closed(nums, target):
   if not nums:
       return -1
   
   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  # mid 在左边,排除它
       else:
           right = mid - 1 # mid 在右边,排除它
           
   return -1

左闭右开区间写法 [left, right) #

这是 C++ STL 标准库中常用的写法,也是很多算法题解中常见的风格。

  • 区间定义: left 是有效的下标, right 是无效的下标(即搜索范围不包含 right )。
  • 循环条件: left < right 。因为当 left == right 时,区间 [left, right) 已经为空,没有元素了。
  • 右边界更新: right = mid 。因为当前区间不包含 right ,所以将 right 设置为 mid ,相当于把新搜索区间设置为 [left, mid) ,正好排除了已经检查过的 mid 。
  • 左边界更新: left = mid + 1 。因为 left 是包含的,要排除 mid ,就得从 mid + 1 开始。
def binary_search_left_closed(nums, target):
    if not nums:
        return -1
    
    left, right = 0, len(nums)  # 注意 right 是 len(nums),不包含
    
    while left < right:  # 注意是 <
        mid = left + (right - left) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1   # 进入区间 [mid+1, right)
        else:
            right = mid      # 进入区间 [left, mid)
            
    return -1

左开右闭区间写法 (left, right] #

这种写法相对少见,主要为了对称性理解。由于数组下标从 0 开始,左开区间需要处理 -1 这个边界。

  • 区间定义: left 是无效下标(不包含), right 是有效下标(包含)。
  • 初始化: left = -1 , right = len(nums) - 1 。
  • 循环条件: left < right 。因为当 left + 1 == right 时,区间 (left, right] 内还有一个元素(即 right 本身),所以不能取等号。
  • 右边界更新: right = mid - 1 。因为 mid 被检查过,且右区间是闭的,所以新右边界要跳过 mid 。
  • 左边界更新: left = mid 。因为左区间是开的,将 left 更新为 mid ,相当于新的区间不包含旧的 left ,但包含 mid 吗?等等,这里容易混淆,我们需要仔细推演。实际上,这种写法为了保证区间不变性,更新规则与闭区间相反
def binary_search_right_closed(nums, target):
    if not nums:
        return -1
    
    left, right = -1, len(nums) - 1  # 左开右闭 (left, right]
    
    while left < right:
        # 因为左开,为了确保 mid 落在有效区间内,通常需要向上取整
        # 否则可能陷入死循环
        mid = left + (right - left + 1) // 2  
        
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid      # target 在右边,更新左边界为 mid (开区间)
        else:
            right = mid - 1 # target 在左边,更新右边界为 mid-1 (闭区间)
            
    return -1

为什么 mid 要向上取整? 因为在左开右闭的写法中,如果只剩两个元素(例如 left = 0 , right = 1 ,实际指向索引1),向下取整得到 mid = 0 。如果 nums[0] < target ,我们会执行 left = mid = 0 ,区间仍然是 (0, 1] ,导致死循环。向上取整得到 mid = 1 ,就能避免这个问题。

开区间写法 (left, right) #

这种写法极端情况更多,主要用于理论探讨,实际工程中很少使用。

  • 区间定义: left 和 right 都是无效下标。
  • 初始化: left = -1 , right = len(nums) 。
  • 循环条件: left + 1 < right 。表示区间内至少有一个元素时继续循环。
  • 边界更新
    • left = mid (排除 mid 左侧)
    • right = mid (排除 mid 右侧)
def binary_search_open(nums, target):
    if not nums:
        return -1
        
    left, right = -1, len(nums)  # 开区间 (left, right)
    
    while left + 1 < right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid   # 进入区间 (mid, right)
        else:
            right = mid  # 进入区间 (left, mid)
            
    return -1

总结与对比 #

区间类型 区间表示 初始化 循环条件 左边界更新 右边界更新 中间值取法
闭区间 [left, right] 0, len-1 left <= right left = mid + 1 right = mid - 1 向下取整
左闭右开 [left, right) 0, len left < right left = mid + 1 right = mid 向下取整
左开右闭 (left, right] -1, len-1 left < right left = mid right = mid - 1 向上取整
开区间 (left, right) -1, len left + 1 < right left = mid right = mid 向下取整

为什么边界难判断? 因为只要记住一条原则,就可以推导出所有写法:确保每次循环后,搜索区间在缩小的同时,仍然覆盖所有可能包含目标值的元素,并且不会重复检查已排除的元素。 如果你忘了这个原则,只死记硬背代码,就很容易出错。

Last modified on 2026-03-15