"一听就会,一写就废"的二分法
上文分析了左闭右闭 [left,right] 区间模型: 重识二分法(上),本文开始分析左闭右开 [left,right) 区间模型。
左闭右开的区间模型
同理,既然选择了 [left,right) ,那接下来的二分区间都要遵循这个"初心"。

1、区间初始定义
区间初始值定义,left=0,right=len,其中 len 是数组 nums 的长度。
2、循环条件选择
依然要判断 left==right 是否有意义?显然,对于 [left,right) 区间, left==right 是有没有意义的。因为 left 不会有越界的情况,所以永远不会有 left==right 的情况。故,循环条件选择:left<right
3、mid指针讨论
当 left 与 right 之间的元素个数是偶数时:
举例说明:设 nums 数组的长度为 2n+1(n 是正整数)。索引从 0 到 n−1 共 n 个元素,索引从 n+1 到 2n 共 n 个元素,所以索引为 n 的元素恰为中间元素。初始情况下 ,left=0,right=2n+1,left 与 right 之间的元素个数为 2n(一定为偶数)。
👉使用 mid=(left+right)/2 会使 mid 指向恰为中心的元素。
此时 mid=(0+2n+1)/2=n(向下取整)
👉使用 mid=(left+right+1)/2 会使 mid 指向中间偏右的元素。
此时 mid=(0+2n+1+1)/2=n+1
👉使用 mid=(left+right−1/2 会使 mid 指向恰为中心的元素。
此时 mid=(0+2n+1−1)/2=n(向下取整)
当 left 与 right 之间的元素个数是偶数时:
举例说明:nums 数组的长度为 2n (n 是正整数)。索引从 0 到 n−2 共 n−1 个元素,索引从 n+1 到 2n−1 共 n−1 个元素,所以索引为 n−1(中间偏左) 或 n(中间偏右) 的元素即为中间元素。初始情况下 left=0,right=2n, left 与 right 之间的元素个数为 2n−1(一定为奇数)。
👉使用 mid=(left+right)/2 会使 mid 指向中间偏右的元素。
此时 mid=(0+2n)/2=n
👉使用 mid=(left+right+1)/2 会使 mid 指向中间偏右的元素。
此时 mid=(0+2n+1)/2=n+1
👉使用 mid=(left+right−1)/2 会使 mid 指向中间偏左的元素。
此时 mid=(0+2n−1)/2=n−1(向下取整)
截止目前的讨论,对于 [left,right) 区间模型,无论 mid 选择哪种计算模型,均可指向中间元素,且左右区间的长度大体均等。因此,要选择出合适的计算方法,还需要看计算出的落盘新区间,方可确定 mid 的计算模型。
4、计算落盘新区间
通过比较 nums[mid] 与 target,从而计算 target 新的落盘区间,即
-
nums[mid]==target,直接返回 mid 即可;
-
nums[mid]>target, 说明 target 一定在 [left,mid) 区间内。我们发现,此区间模型恰与我们的“初心”保持一致,即 [left,right)。故,此时应将新的右开区间赋值给 right 指针,即 right=mid;
-
nums[mid]<target,说明 target 一定在 (mid,right)区间内。我们发现,此区间模型违背了我们的“初心”,因此新的落盘区间应为 [mid+1,right)。故,此时应将新的左闭区间赋值给 left 指针,即 left=mid+1。
💥 注意啦!我们发现:
在每次计算落盘新区间时,如果 nums[mid]<target, left 指针都会发生改变(每次循环后的 left 会比前一次循环的多1),而如果 nums[mid]>target, right 指针也不一定会发生变化(有可能当次计算出来的 mid 与上一次的 right 值相同)。
💥 故我们猜想,在某种 mid 指针的计算方法下,可能会出现“死循环”。
先说结论: 当 left 指向第 k 个元素,right 指向第 k+1 个元素,其中 k∈[0,len−1),采用 mid=(left+right+1)/2 计算方式,并且存在 nums[mid]>target,此时 right 和 mid 会反复指向最后第一个元素,进入死循环。
证明一下: 当 left 指向第 k 个元素,right 指向第 k+1 个元素,
- 代入 mid=(left+right+1)/2,可得:mid=k+1=right。
而 nums[mid]>target 时,mid 指针要赋值给 right,这样就导致 mid 和 right 反复赋值,故一旦命中上述情况,二分法将陷入“死循环”中。
而 mid=(left+right)/2 或 mid=(left+right−1)/2 均可以满足要求(可自行证明)。为防止计算精度溢出问题,mid 计算方式选择为: mid=left+(right−left)>>1
证明一下: 当 left 指向第 k 个元素,right 指向第 k+1 个元素,采用 mid=(left+right)/2 计算方式时,mid=k+1/2,向下取整取 k,即 mid=k
-
此时 nums[mid]==target,直接返回 mid 即可;
-
此时 nums[mid]>target,会将 mid(上次循环的 left) 赋值给 right,此时 left == right, 循环退出,返回 −1;
-
此时 nums[mid]<target,会将 mid+1(上次循环的 right) 赋值给 left,此时 left == right, 循环退出,返回 −1;
证明一下: 当 left 指向第 k 个元素,right 指向第 k+1 个元素,采用 mid=(left+right−1)/2 计算方式时,mid=k,这种计算方式与 mid=(left+right)/2 的结果一致,不再赘述。
5、最终返回值
如果 target 的确存在于 nums 数组中,一定会在第四步中 nums[mid]==target 时返回,否则返回 −1 即可。
6、代码参考
迭代法
function search(nums: number[], target: number): number {
let left = 0;
let right = nums.length;
while(left < right) {
const mid = left + ~~((right - left) / 2);
const midVal = nums[mid];
if (midVal === target) {
return mid;
}
if (midVal > target) {
right = mid;
} else {
left = mid + 1;
}
}
return -1;
}
递归法
function search(nums: number[], target: number): number {
return searchHelper(nums, target, 0, nums.length);
}
function searchHelper(nums: number[], target: number, left: number, right: number) {
if (left >= right) {
return -1;
}
const mid = left + ~~((right - left) / 2);
if(nums[mid] === target) {
return mid;
}
if (nums[mid] > target) {
return searchHelper(nums, target, left, mid);
}
return searchHelper(nums, target, mid + 1, right);
}
总结
| 区间模型 | 循环条件 | mid 指针取值 |
|---|
| [left,right) | left<right | mid=(left+right)/2、mid=(left+right−1)/2 |
-
如果 [nums[mid]==target,直接返回 mid 提前结束二分搜索;
-
如果 [nums[mid]>target,left 指针不动,right=mid 后继续二分;
-
如果 [nums[mid]<target,right 指针不动,left=mid+1 后继续二分;
接下来继续分析其他两种区间模型,即左开右闭 (left,right] 、左开右开 (left,right)