【二分查找-开区间思维】

文章目录

  • 红蓝染色法
      • [1\. 核心逻辑:`(-1, n)`](#1. 核心逻辑:(-1, n))
      • [2\. 代码模板](#2. 代码模板)
      • [3\. 为什么很多人喜欢这种写法?(优势)](#3. 为什么很多人喜欢这种写法?(优势))
      • [4\. 劣势与注意事项](#4. 劣势与注意事项)
  • 开区间和闭区间的区别
      • [1\. 为什么它是"闭区间"写法?](#1. 为什么它是“闭区间”写法?)
      • [2\. 这张图在解释哪段代码?](#2. 这张图在解释哪段代码?)
      • [3\. 和刚才说的"双开区间"有什么区别?](#3. 和刚才说的“双开区间”有什么区别?)
      • [4\. 总结:如何看懂这张图?](#4. 总结:如何看懂这张图?)
  • 开区间表示的是边界的指针!里面的数是未被染色的数!
      • [1. 三个区域的定义](#1. 三个区域的定义)
      • [2. 动态过程演示](#2. 动态过程演示)
      • [3. 对比:闭区间 `[L, R]` 是什么意思?](#3. 对比:闭区间 [L, R] 是什么意思?)
      • 总结

红蓝染色法

这种写法的核心思想是:不维护搜索区间,而是维护两个"边界指针"

1. 核心逻辑:(-1, n)

这种写法把数组想象成两种颜色(比如红色和蓝色):

  • 左指针 L:始终指向"红色"区域(不满足条件的区域)。
  • 右指针 R:始终指向"蓝色"区域(满足条件的区域)。
  • 目标:找到红蓝交界处。

初始化:

  • L = -1 (假想数组最左侧有一个不满足条件的哨兵)
  • R = n (假想数组最右侧有一个满足条件的哨兵)
  • 这样就把所有实际元素 0n-1 都包在 (L, R) 这个开区间里了。

循环条件:

  • while L + 1 != R: (或者 while L + 1 < R:
  • 解释:LR 紧挨着的时候(比如 L=2, R=3),说明中间没有元素了,边界找到了,循环结束。

更新逻辑:

  • 永远不需要 +1-1
  • 如果 mid 是红色的(不满足条件):L = mid
  • 如果 mid 是蓝色的(满足条件):R = mid

2. 代码模板

假设我们要在一个有序数组 中找到第一个 >= target 的数(即 C++ 中的 lower_bound):

python 复制代码
def binary_search_open_interval(nums, target):
    # 1. 初始化在数组范围之外
    left, right = -1, len(nums)
    
    # 2. 循环条件:当左右指针相邻时停止
    while left + 1 != right:
        mid = left + (right - left) // 2
        
        # 3. 染色判断
        if nums[mid] >= target:
            right = mid  # mid 是蓝色的(满足条件),右边界收缩到 mid
        else:
            left = mid   # mid 是红色的(太小了),左边界收缩到 mid
            
    # 4. 结果处理
    # 循环结束时,left 指向最后一个 < target 的数
    # right 指向第一个 >= target 的数
    if right == len(nums):
        return -1 # 没找到,所有数都比 target 小
    return right

3. 为什么很多人喜欢这种写法?(优势)

  1. 思维负担极低(无脑 mid):

    你再也不用纠结 mid + 1 还是 mid - 1,也不用担心因为漏了 +1 导致死循环。因为 LR 总是直接赋值为 mid,且循环条件保证了 mid 永远不会等于 LR(只要 R - L > 1)。

  2. 返回值清晰:

    • 如果你要找第一个满足条件的数,答案永远是 R
    • 如果你要找最后一个不满足条件的数,答案永远是 L
    • 不需要在 return left 还是 return right 之间犹豫。
  3. 天然处理空数组:

    如果数组是空的,left=-1, right=0,循环根本不会进入,直接返回正确结果,不需要特判。

  4. 始终不会越界:

    虽然 LR 初始在界外,但计算出的 mid 永远在 [0, n-1] 范围内(只要数组非空),所以 nums[mid] 是安全的。

4. 劣势与注意事项

  • 初始化的理解: 必须要接受下标 -1n 这种"虚拟节点"的概念。
  • 死循环风险(微小): 如果你错误地写成了 while left < right 且内部还是 left = mid,那就会死循环。必须严格遵守 while left + 1 != right

总结:我该怎么选?

  • 场景 A:单纯查找一个数(是否存在)

    👉 闭区间 [0, n-1] 最简单直接。找到了就 return,找不到拉倒。

  • 场景 B:查找边界(第一个 >= X,最后一个 < X,最大化最小值等)

    👉 双开区间 (-1, n) 是无敌的。

    你只需要定义好什么是"蓝"(满足条件),什么是"红"(不满足条件),最后返回 RL 即可。

建议: 如果你觉得现在的闭区间写法处理"边界问题"经常出错(比如分不清是找左边界还是右边界),强烈建议尝试一下这个**双开区间(红蓝染色)**写法,它能统一所有复杂的二分问题。

这是一个非常棒的问题!这张图其实是在用 "红蓝染色"的思想 来解释 "闭区间写法" (while L <= R) 的原理。

简单来说:是的,这就是闭区间写法。

但是,它和我们刚才讨论的"双开区间(红蓝染色法)"在变量定义的细节上是不一样的。为了不让你晕,我来帮你拆解这张图的逻辑。


开区间和闭区间的区别

1. 为什么它是"闭区间"写法?

请看图中的两个关键细节:

  1. 最终状态是指针错位(Cross Over):

    图中 R R R 在左边(指向 7), L L L 在右边(指向 8)。

    • 这意味着 L > R L > R L>R(具体是 L = R + 1 L = R + 1 L=R+1)。
    • 只有 闭区间 的循环条件 while L <= R 结束时,才会出现 L L L 跑到 R R R 右边的情况。
    • 如果是 双开区间 (while L + 1 != R),结束时 L L L 和 R R R 是紧挨着的,且 L L L 在左, R R R 在右。
  2. 循环不变量的定义(Loop Invariant):

    图中的文字写着:

    • L − 1 L - 1 L−1 始终是红色 → \rightarrow → 这意味着 L 指针刚刚跨过了红色区域(Red),L 左边的全是红色。对应代码 left = mid + 1
    • R + 1 R + 1 R+1 始终是蓝色 → \rightarrow → 这意味着 R 指针刚刚跨过了蓝色区域(Blue),R 右边的全是蓝色。对应代码 right = mid - 1

2. 这张图在解释哪段代码?

这张图对应的就是最经典的 寻找左侧边界(lower_bound) 的闭区间模板:

python 复制代码
# 闭区间写法
left, right = 0, n - 1

while left <= right:
    mid = left + (right - left) // 2
    if nums[mid] >= target:  # 遇到蓝色(满足条件)
        right = mid - 1      # 尝试往左找,R 变小,保证 R+1 及其右边全是蓝色
    else:                    # 遇到红色(太小了)
        left = mid + 1       # 必须跨过去,L 变大,保证 L-1 及其左边全是红色

# 循环结束时:
# right 指向红色区域的最后一个数 (图中指着 7)
# left  指向蓝色区域的第一个数   (图中指着 8)
return left 

3. 和刚才说的"双开区间"有什么区别?

这是最容易混淆的地方,请注意对比:

特性 闭区间 (你发的图) 双开区间 (我刚才推荐的)
指针定义 L, R 是搜索范围的边界。 L 是红军代表,R 是蓝军代表。
更新逻辑 mid 被排除 (+1-1)。 mid 被保留 (L=midR=mid)。
结束条件 L > R (错位) L + 1 == R (相邻)
结束状态 R 在左 (红),L 在右 (蓝) L 在左 (红),R 在右 (蓝)
答案 return L (因为 L 最后停在蓝色起点) return R (R 本身就是蓝军代表)

4. 总结:如何看懂这张图?

这张图的作者其实是在教大家:

即使你写的是传统的 while left <= right,你也可以在脑子里用"红蓝染色"来理解它。

  • 他把 "答案" 定义为 蓝色
  • 循环结束时,left 指针会停在 "蓝色的第一个"
  • right 指针会停在 "红色的最后一个"
  • 所以图中说:R + 1 是我们要找的答案(也就是 L)。

结论:

如果不喜欢双开区间的 -1, n 写法,这张图就是你理解闭区间二分(找边界)的最佳思维模型 。它完美解释了为什么最后 return left 而不是 return right

开区间表示的是边界的指针!里面的数是未被染色的数!

双开区间 (L, R)(也就是红蓝染色法)的写法中:

开区间 (L, R) 里面的元素,就是当前"还未被染色"(也就是还没被检查/判断)的元素区域。

我们可以把整个数组想象成三个区域:

1. 三个区域的定义

  1. [ 0 , L ] [0, L] [0,L](闭区间): 已知的红色区域

    • 这里面的每一个数,我们(或者初始逻辑)都已经确认过,它们是不满足条件的(或者说是"过小"的)。
    • L 是红色区域的最前线哨兵。
  2. [ R , n − 1 ] [R, n-1] [R,n−1](闭区间): 已知的蓝色区域

    • 这里面的每一个数,我们也已经确认过,它们是满足条件的(或者说是"足够大"的)。
    • R 是蓝色区域的最前线哨兵。
  3. ( L , R ) (L, R) (L,R)(开区间): 未知的白色/灰色区域

    • 这就是你说的"还没被判断"的区域。
    • 我们的目标就是不断缩小这个"未知区域",直到它消失。

2. 动态过程演示

假设数组是 [?, ?, ?, ?, ?],长度为 5。

初始状态:
L = -1, R = 5

此时开区间是 (-1, 5)

也就是下标 0, 1, 2, 3, 4 全都在 LR 之间。
含义: "全都没检查过,全是未知区域。"

第一次二分:
mid = 2。我们检查 nums[2]

  • 假设 nums[2] 满足条件(蓝色)。
  • 我们将 R 移动到 mid,即 R = 2
  • 含义: "哦,我看了一眼中间,发现下标 2 是蓝色的。那么 2 右边的肯定也都是蓝色的(已知)。现在的未知区域变成了 (-1, 2),也就是只剩 0, 1 没检查了。"

循环结束条件:
while L + 1 != R

LR 紧挨着(比如 L=1, R=2)时,开区间 (1, 2)没有整数了
含义: "未知区域为空,所有元素都已被归类为红色或蓝色。任务完成。"


3. 对比:闭区间 [L, R] 是什么意思?

为了加深理解,我们对比一下闭区间写法:

  • 开区间 (L, R) LR 是**"墙"(已确定的边界)。我们搜索的是墙中间的空间**。
  • 闭区间 [L, R] LR 是**"待查嫌疑人"。我们搜索的是 包含 LR 在内的整个嫌疑人名单**。

这也是为什么:

  • 开区间 里,mid 检查完后,直接变成新的墙 (L=midR=mid)。
  • 闭区间 里,mid 检查完后,必须被剔除 (L=mid+1R=mid-1),因为它已经不是嫌疑人了。

总结

你理解得完全到位:

开区间 (L, R) 就是那个"待探索的未知世界"。

随着算法进行,红蓝阵营(LR)不断向中间挤压,直到把这个"未知世界"挤得一点不剩(LR 相邻),二分查找就结束了。

相关推荐
Zsy_0510034 小时前
【数据结构】排序
数据结构·算法·排序算法
Swift社区4 小时前
LeetCode 449 - 序列化和反序列化二叉搜索树
算法·leetcode·职场和发展
CoderYanger5 小时前
贪心算法:3.最大数
java·算法·leetcode·贪心算法·1024程序员节
lxmyzzs5 小时前
【图像算法 - 37】人机交互应用:基于 YOLOv12 与 OpenCV 的高精度人脸情绪检测系统实现
算法·yolo·人机交互·情绪识别
muyouking115 小时前
Zig 语言实战:实现高性能快速排序算法
算法·排序算法
CoderYanger5 小时前
贪心算法:5.最长递增子序列
java·算法·leetcode·贪心算法·1024程序员节
慕容青峰5 小时前
【牛客周赛 107】E 题【小苯的刷怪笼】题解
c++·算法·sublime text
算法熔炉5 小时前
深度学习面试八股文(2)——训练
人工智能·深度学习·算法
EXtreme355 小时前
【数据结构】打破线性思维:树形结构与堆在C语言中的完美实现方案
c语言·数据结构·算法··heap·完全二叉树·topk