这是二分查找里最核心的内容之一。
关键不是「左闭右闭」还是「左闭右开」,而是:
mid 是否已经被证明不可能是答案。
左闭右闭[left, right]
区间定义:
[left, right]表示:
left 和 right 都是可能答案例如:
left = 0; right = n - 1;假设我们在找:
第一个 >= target 的位置当:
nums[mid] >= target时,说明:
mid 可能就是答案例如:
nums = [1,2,2,2,5] target = 2第一次:
mid = 2 nums[2] = 2此时:
mid 可能是第一个2吗?不知道。
因为左边还有:
nums[1] = 2所以我们要继续往左找。
但是:
mid 本身不能丢理论上应该写:
right = mid;但是如果这样写:
while(left <= right)可能死循环。
例如:
left = 1 right = 1 mid = 1执行:
right = mid;得到:
left = 1 right = 1区间没缩小。
永远循环。
因此左闭右闭模板必须:
right = mid - 1;把 mid 排除掉。
虽然排除了 mid,
但别忘了:
return left;最终 left 会停在第一个满足条件的位置。
左闭右开[left, right)
区间定义:
[left, right)右边界不属于搜索区间。
例如:
left = 0; right = n;循环:
while(left < right)当:
nums[mid] >= target时:
mid 可能是答案所以不能丢。
写:
right = mid;为什么不会死循环?
因为这里区间是:
[left,right)假设:
left = 3 right = 4则:
mid = 3执行:
right = mid;得到:
left = 3 right = 3此时:
left < right已经不成立。
循环结束。
不会死循环。
本质区别
左闭右闭:
区间包含 right所以:
right = mid可能导致区间大小不变。
必须:
right = mid - 1保证缩小。
左闭右开:
区间不包含 right所以:
right = mid已经缩小了区间。
因为:
[mid,right) → [mid,mid)区间长度直接变成 0。
用图理解
假设:
left=3 right=4 mid=3左闭右闭:
[3,4] ^ mid执行:
right = mid;得到:
[3,3]下一轮还是:
mid=3可能卡住。
所以要:
right = mid - 1;变成:
[3,2]循环结束。
左闭右开:
[3,4) ^ mid执行:
right = mid;得到:
[3,3)区间为空。
直接结束。
所以可以记住:
| 区间 | 循环条件 | 左移右边界 |
|---|---|---|
[left,right] | left <= right | right = mid - 1 |
[left,right) | left < right | right = mid |
原因是:
- 左闭右闭中,
mid仍在区间内,必须用-1保证区间缩小。 - 左闭右开中,
right本来就不在区间内,right = mid已经完成缩小,同时保留了mid作为候选答案。
这也是为什么你现在的findFirst():
while (left <= right) { ... if (nums[mid] < target) left = mid + 1; else right = mid - 1; } return left;能够正确返回第一个 ≥ target 的位置(lower_bound)。