要理解二分查找中 while(l < r) 和 while(l <= r) 的区别,核心在于二分的 "目标" 和 "边界收缩逻辑" ------ 前者是 "收敛到唯一解",后者是 "在区间内验证目标是否存在"。下面结合你给出的三个经典案例,拆解背后的核心逻辑:
先明确核心结论
| 循环条件 | 核心目标 | 边界收缩特点 | 适用场景 |
|---|---|---|---|
l < r |
让 l 和 r 收敛到同一个索引(唯一解) |
收缩时永不跳过可能的解(r=mid) |
找 "唯一值"(如峰值、旋转数组最小值) |
l <= r |
在区间内验证目标是否存在,找到则返回 | 收缩时可能跳过(r=mid-1/l=mid+1) |
找 "特定值"(如有序数组查 target) |
逐个分析你的代码案例
案例 1:findPeakElement(找峰值,l < r)
为什么不用 l <= r?
- 目标是 "收敛到唯一解" :我们不需要在循环内判断 "mid 是不是峰值",而是通过趋势收缩,让
l和r最终指向同一个索引(必然是峰值)。 - 越界 / 死循环风险 :若用
l <= r,当l==r时会进入循环:mid = l = r,此时判断nums[mid] < nums[mid+1],若mid是数组最后一个元素,mid+1会越界;- 若
mid不是最后一个元素,right=mid会导致l和r永远相等,陷入死循环。
- 逻辑冗余 :
l==r时已经找到唯一解,继续循环无意义。
案例 2:search(旋转数组查 target,l <= r)
while(l <= r) {
int mid = l + (r - l)/2;
if(nums[mid] == target) return mid; // 找到目标,直接返回
// 收缩边界(跳过mid,因为mid已经验证不是target)
if(nums[l] <= nums[mid]) {
if(nums[mid] > target && target >= nums[l]) r = mid - 1;
else l = mid + 1;
} else {
if(nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
return -1; // 区间为空,未找到
为什么必须用 l <= r?
- 目标是 "验证存在性" :我们需要在循环内逐个验证
mid是否是目标值,找到则返回,没找到则收缩区间。 - 边界收缩逻辑要求 :每次收缩都会跳过
mid(r=mid-1/l=mid+1),因为mid已经验证不是 target。若用l < r,会漏掉最后一个可能的解:- 比如数组
[1],target=1,l=0, r=0,l < r不成立,直接返回 - 1,错误; - 用
l <= r会进入循环,mid=0,找到 target 返回 0,正确。
- 比如数组
- 循环终止条件合理 :当
l > r时,说明区间内所有元素都验证过,没有 target,返回 - 1 即可。
案例 3:findMin(旋转数组找最小值,l < r)
while(l < r) {
int mid = l + (r - l)/2;
if(nums[mid] > nums[r]) {
l = mid + 1; // 最小值在右侧,mid不可能是解,跳过
} else {
r = mid; // 最小值在左侧(含mid),mid可能是解,不跳过
}
}
return nums[l]; // l==r,就是最小值
为什么不用 l <= r?
- 目标是 "收敛到唯一解" :旋转数组的最小值是唯一的,我们通过比较
nums[mid]和nums[r]收缩区间,最终让l和r指向最小值的索引。 - 逻辑冗余 / 死循环 :若用
l <= r,当l==r时进入循环,mid=l=r,nums[mid] > nums[r]不成立,r=mid导致循环永不退出。 - 效率更高 :
l < r循环次数更少,收敛到唯一解后直接返回,无需额外判断。
通用判断法则(记住这 2 点)
什么时候用 l < r?
满足以下任一条件即可:
- 目标是找 "唯一值"(如峰值、旋转数组最小值、有序数组找第一个 / 最后一个元素);
- 边界收缩时不跳过可能的解 (即有
r=mid,而非r=mid-1); - 最终返回
l(或r) (因为l==r)。
什么时候用 l <= r?
满足以下任一条件即可:
- 目标是 "查找特定值是否存在"(如有序数组查 target、旋转数组查 target);
- 边界收缩时跳过已验证的 mid (即
r=mid-1或l=mid+1); - 循环内有 "找到目标直接返回" 的逻辑,最终可能返回 - 1(未找到)。
补充:易混点对比
| 对比项 | l < r |
l <= r |
|---|---|---|
| 循环终止时 | l == r(区间长度为 1) |
l > r(区间为空) |
| 收缩方式 | 至少保留 mid(r=mid) |
必跳过 mid(r=mid-1/l=mid+1) |
| 返回值 | 循环外返回 l/r |
循环内返回目标,循环外返回 - 1 |
| 典型场景 | 找唯一极值 / 边界值 | 找特定值(存在性验证) |
总结
二分循环条件的选择,本质是匹配 "目标" 和 "边界收缩逻辑":
- 若想让区间收敛到唯一索引(找极值 / 唯一值),用
l < r; - 若想在区间内验证目标是否存在(找特定值),用
l <= r。
cpp
int findMin(vector<int>& nums) {
int l=0,r=nums.size()-1;
while(l<r)//为什不是<=,因为总会出现l == r,如果是<=满足条件进入循环会一直循环下去死循环
{//其实l == r 就确定是唯一一个值,就找到了要寻找的值
int mid = l + (r-l)/2;
if(nums[mid]>nums[r])
{
l=mid+1;
}else{
r=mid;//或者更直接的说法是<=的情况每次更新左右区间的时候l=mid+1,r=mid-1
}//跳过了mid的值的判断,最后l == r的时候 mid=l=r,要进入循环才能将mid的位置的数进行判断
}
return nums[r];//r==l
}