在深入探讨二分查找的边界处理之前,我们先来看看一些常见的错误场景。这些场景往往是因为对二分查找的原理理解不够深入,或者在编写代码时没有考虑到一些特殊情况而导致的。
一、90%的错误来自这两个场景
1. 边界值处理不当(重复元素陷阱)
错误本质:找到目标立即返回,忽略重复元素的连续分布特性
js
// 错误:直接返回中间索引
function searchWrong(arr, target) {
let left=0, right=arr.length-1
while(left <= right) {
const mid = (left+right)>>1
if(arr[mid] === target) return mid // 可能返回中间位置
else if(arr[mid] < target) left=mid+1
else right=mid-1
}
return -1
}
// 正确:持续压缩右边界[2]
function searchLeftBound(arr, target) {
let left=0, right=arr.length-1
while(left <= right) {
const mid = left + ((right-left)>>1)
if(arr[mid] >= target) right=mid-1 // 关键!
else left=mid+1
}
return arr[left]===target ? left : -1 // 最后校验
}
2. 区间缩小异常导致死循环
错误本质:区间收缩不彻底,形成无限震荡
js
// 错误:right=mid导致区间无法缩小
while(left < right) {
const mid = (left+right)>>1
if(arr[mid] > target) right=mid // 应改为right=mid-1
else left=mid+1
}
// 正确:严格保证区间收缩[7]
while(left <= right) {
const mid = left + ((right-left)>>1)
if(arr[mid] > target) right=mid-1 // 区间必须缩小
else left=mid+1
}
二、六大边界处理要点(必读避坑指南)
1. 初始值陷阱
-
闭区间 必须初始化
right = arr.length-1
,否则会漏查最后一个元素 -
开区间 必须初始化
right = arr.length
,否则无法处理插入数组末尾的场景
2. 循环条件选择
区间类型 | 循环条件 | 搜索终止条件 | 典型错误案例 |
---|---|---|---|
左闭右闭 | left <= right |
left > right |
漏查最后一个元素 |
左闭右开 | left < right |
left == right |
导致死循环 |
3. 中间值计算防溢出
javascript
// 错误写法:可能导致整数溢出
mid = (left + right) / 2
// 正确写法:通过偏移量计算
mid = left + ((right - left) >> 1) // 位运算优化[1,6]
4. 边界校验不可少
左边界查找后必须验证:
javascript
if (left >= arr.length || arr[left] !== target) return -1 // [1,4]
右边界查找后必须回退:
javascript
return right >=0 && arr[right] === target ? right : -1 // [2,4]
5. 更新策略一致性
区间类型 | 条件判断 | 左指针更新 | 右指针更新 |
---|---|---|---|
闭区间 | arr[mid] < target |
left = mid + 1 |
right = mid - 1 |
开区间 | arr[mid] >= target |
right = mid |
不更新左指针 |
6. 极值处理方案
特殊场景 | 处理方案 | LeetCode对应题目 |
---|---|---|
空数组 | 预处理返回-1 | #34 |
全相同元素 | 边界校验后二次验证 | #34 |
目标值在两端 | 初始化时扩展虚拟边界 | #35 |
二、两种核心写法精讲(附LeetCode实战案例)
写法1:左闭右闭区间(精确查找)
适用场景:判断元素是否存在、精确匹配查找
javascript
function binarySearch(arr, target) {
let left=0, right=arr.length-1 // [7]
while(left <= right) { // 允许left=right的合法区间
const mid = left + ((right-left)>>1) // 防溢出写法[1]
if(arr[mid] === target) return mid // 直接命中
arr[mid] < target ? left=mid+1 : right=mid-1 // 区间严格收缩
}
return -1 // 未找到
}
LeetCode实战:
- 704.二分查找 通过率:72%
- 时间复杂度:O(log n),空间复杂度:O(1)
写法2:左闭右开区间(边界查找)
适用场景:查找插入位置、左右边界定位
javascript
function findBound(arr, target, isLeft) {
let left=0, right=arr.length
while(left < right) { // 终止时left=right
const mid = left + ((right-left)>>1)
if(arr[mid] < target || (!isLeft && arr[mid] === target)) {
left = mid + 1 // 左区间收缩
} else {
right = mid // 右区间收缩[4]
}
}
return isLeft ? left : left-1 // 左右边界转换
}
LeetCode实战:
- 34.在排序数组中查找元素的第一个和最后一个位置 通过率:45%
- 时间复杂度:O(2 log n),空间复杂度:O(1)
三、红蓝染色法:可视化边界处理(应对面试追问)
操作原理
-
染色规则:
- 红色区域:确定不符合条件(
arr[i] < target
) - 蓝色区域:可能符合条件(
arr[i] >= target
)
- 红色区域:确定不符合条件(
-
指针移动策略:
javascript
function redBlueSearch(arr, target) {
let left=-1, right=arr.length // 初始化虚拟边界
while(left+1 < right) { // 保证有中间元素
const mid = left + ((right-left)>>1)
arr[mid] < target ? left=mid : right=mid // 染色规则
}
return arr[right]===target ? right : -1 // 最终校验
}
技术优势
- 无需处理
mid±1
的边界问题 - 循环终止条件更直观(
left+1 == right
) - 可扩展性强,适合处理复杂边界问题
四、高频面试考点
-
如何防止死循环?
-
核心:确保每次循环区间严格缩小(
left
或right
必须移动) -
案例:当
left=3, right=4
且mid=3
时,必须让left=mid+1
或right=mid
-
-
如何处理重复元素?
-
左边界:找到目标后继续左移
right
指针 -
右边界:找到目标后继续右移
left
指针
-
-
为什么推荐位运算?
-
位运算
>>1
比除法运算/2
快2-3倍 -
避免JS引擎的浮点数转换开销
-