coverPiccoverPic

所以说二分搜索要怎么写?

前言

做这个题的时候 H-Index,二分搜索算法写来写去都写不对,经过阅读大量文献,和好多技术文章,简单了解了一下二分搜索的写法。

我们不妨假设,下面讨论的二分搜索是在一个以自然数(包含 0)作为下标的单调递增(单调递减同理,比较的时候换个方向就可以了)数组中,寻找与给定目标值相等,或者最大的小于等于(或者大于等于)目标值的数(也可以说此时目标值就成为了一个下(上)界,或者说左侧(右侧)的边界)。

二分搜索中,我们通常会定义左边界 leftleft,右边界 rightright,每次取左右边界的均值 left+floor((rightleft)/2)left + \text{floor}((right - left) / 2),指向的数组元素与目标值 targettarget 比较,然后移动左右边界,最后在 left<rightleft < right 或者 leftrightleft \leq right 的时候终止循环,然后返回目标值的下标。

个人认为二分搜索可能遇到的问题在于:每次搜索边界如何移动,怎么判断终止循环,结果需要怎么取。自己在写二分搜索的时候,感觉对于终止循环的表达式怎么去写还是挺迷惑的,是 left<rightleft < right 还是 leftrightleft \leq right,不同的写法也会影响到边界移动的方式以及如何返回结果下标。

与目标值相等

1. 在 [left,right)[left, right) 中搜索

在数组 numsnums 上面搜索,最初可行区间为 [left,right)[left, right)。这种想法是很自然的,毕竟我们会把左边界设置为 numnum 第一位下标 0,右边界为数组长度 nums.lengthnums.length,但是 nums[nums.length]nums[nums.length] 超出了边界。

此时循环退出条件为:

  1. 找到了 targettarget 返回结果下标;
  2. 找不到,返回 -1,也就是说这个时候 [left,right)[left, right) 空了,于是有 left<rightleft < right 时退出循环;
ts
  1. function binarySearch(nums: number[], left: number, right: number, target: number): number {
  2. while (left < right) {
  3. const mid = left + Math.floor((right - left) / 2)
  4. if (nums[mid] < target) {
  5. left = mid + 1
  6. } else if (nums[mid] === target) {
  7. return mid
  8. } else {
  9. right = mid
  10. }
  11. }
  12. return -1
  13. }

描述一下每次迭代都发生了什么:

  1. 一开始可行域是 [left,right)[left, right)
  2. 如果 nums[mid]<targetnums[mid] < target,就是说 [left,mid][left, mid] 都不满足,因此 left=mid+1left = mid + 1[mid+1,right)[mid + 1, right) 的情况;
  3. 如果 nums[mid]<targetnums[mid] < target[mid,right)[mid, right) 都不满足,因此 right=midright = mid[left,mid)[left, mid) 的情况;
  4. 如果找到了 nums[mid]=targetnums[mid] = target 就返回结果,最后找不到返回 -1;

2. 在 [left,right][left, right] 中搜索

那现在要在 [left,right][left,right] 闭区间进行搜索,需要怎么处理呢,很简单,把上面的右边界 +1 就行了,也可以采用退出循环条件为 leftrightleft \leq right 的写法,因为是闭区间,没有找到结果时终止循环肯定得让区间为空,在 left=rightleft = right 时区间里面还有东西,还得再迭代一步缩小一下。

ts
  1. function binarySearch(nums: number[], left: number, right: number, target: number): number {
  2. while (left <= right) {
  3. const mid = left + Math.floor((right - left) / 2)
  4. if (nums[mid] < target) {
  5. left = mid + 1
  6. } else if (nums[mid] === target) {
  7. return mid
  8. } else {
  9. right = mid - 1
  10. }
  11. }
  12. return -1
  13. }

我们又来描述一下每次迭代都发生了什么:

  1. 一开始可行域是 [left,right][left, right]
  2. 如果 nums[mid]<targetnums[mid] < target[left,mid][left, mid] 都不满足,因此 left=mid+1left = mid + 1[mid+1,right][mid + 1, right] 的情况;
  3. 如果 nums[mid]<targetnums[mid] < target[mid,right][mid, right] 都不满足,因此 right=mid1right = mid - 1[left,mid1][left, mid - 1] 的情况;
  4. 如果找到了 nums[mid]=targetnums[mid] = target 就返回结果,最后找不到返回 -1;

大于等于目标值的最小值

此时,我们需要的索引最靠左的,大于等于目标值 targettarget 的数组元素。算法和与目标值相等的情况类似,关键在于,目标值不一定在数组中,或者有多个目标值,于是需要考虑 nums[mid]=targetnums[mid] = target 时边界如何移动。

1. 在 [left,right)[left, right) 中搜索

ts
  1. function binarySearch(nums: number[], left: number, right: number, target: number): number {
  2. if (target > nums[right - 1]) {
  3. return -1
  4. }
  5. while (left < right) {
  6. const mid = left + Math.floor((right - left) / 2)
  7. if (nums[mid] < target) {
  8. left = mid + 1
  9. } else if (nums[mid] > target) {
  10. right = mid
  11. } else {
  12. right = mid
  13. }
  14. }
  15. return left
  16. }

每次迭代都发生了什么:

  1. 如果 targettarget 比所有数都大返回 -1;
  2. 一开始可行域是 [left,right)[left, right)
  3. 如果 nums[mid]targetnums[mid] \not= target
    3.1. 如果 nums[mid]<targetnums[mid] < target,就是说 [left,mid][left, mid] 都小于 targettarget,因此 left=mid+1left = mid + 1[mid+1,right)[mid + 1, right) 的情况;
    3.2. 如果 nums[mid]<targetnums[mid] < target,就是说 [mid,right)[mid, right) 都大于 targettarget,因此 right=midright = mid[left,mid)[left, mid) 的情况;
  4. 如果数组元素都不等于 targettarget,循环结束时,left=rightleft = right,返回最不小于目标值的下标 leftleftrightright 也行,下同)
  5. 如果找到了 nums[mid]=targetnums[mid] = target(mid,right)(mid, right) 都大于等于 targettarget,但是 midmid(mid,right)(mid, right) 都更“左”,是更优的选项,可以让 right=midright = mid
    5.1. 如果 [left,mid)[left, mid) 中没有等于 targettarget 的数,则 leftleft 向右移动直至 left=rightleft = right,循环结束,返回大于等于目标值的最小值的下标 leftleft;
    5.2. 如果 [left,mid)[left, mid) 中有等于 targettarget 的数,则在迭代过程中 rightright 最终向右移动直至 [left,mid)[left, mid) 为空或者其中数组元素都小于 targettarget ,然后见 5.1;

2. 在 [left,right][left, right] 中搜索

我们当然也可以把右边界 + 1,但是可以有另一种写法

ts
  1. function binarySearch(nums: number[], left: number, right: number, target: number): number {
  2. if (target > nums[right]) {
  3. return -1
  4. }
  5. while (left <= right) {
  6. const mid = left + Math.floor((right - left) / 2)
  7. if (nums[mid] < target) {
  8. left = mid + 1
  9. } else if (nums[mid] > target) {
  10. right = mid - 1
  11. } else {
  12. right = mid - 1
  13. }
  14. }
  15. return left
  16. }

类似地:

  1. 如果 targettarget 比所有数都大返回 -1;
  2. 一开始可行域是 [left,right][left, right]
  3. 如果 nums[mid]targetnums[mid] \not= target
    3.1. 如果 nums[mid]>targetnums[mid] > target,就是说 [left,mid][left, mid] 都小于 targettarget,因此 left=mid+1left = mid + 1[mid+1,right][mid + 1, right] 的情况;
    3.2. 如果 nums[mid]<targetnums[mid] < target,就是说 [mid,right][mid, right] 都大于 targettarget,因此 right=mid1right = mid - 1[left,mid1][left, mid - 1] 的情况;
  4. 如果数组元素都不等于 targettarget,循环结束时,left=right+1left = right + 1nums[right]<target<nums[right+1nums[right] < target < nums[right + 1,返回最不小于目标值的下标 leftleft
  5. 如果找到了 nums[mid]=targetnums[mid] = target[mid,right][mid, right] 都大于等于 targettarget,但是 midmid[mid,right][mid, right] 都更“左”,是更优的选项,可以让 right=mid1right = mid - 1 (即使 nums[mid1]<targetnums[mid - 1] < target也可以获得正确结果,参考第 4 步);
    5.1. 如果 [left,mid)[left, mid) 中没有等于 targettarget 的数,则 leftleft 向右移动直至 left=right+1left = right + 1,循环结束,返回下标 leftleft;
    5.2. 如果 [left,mid)[left, mid) 中有等于 targettarget 的数,则在迭代过程中 rightright 最终向右移动直至 [left,mid)[left, mid) 为空或者其中数组元素都小于 targettarget ,然后见 5.1;

小于等于目标值的最大值

需要搜索小于等于目标值的下标最后的元素:

1. 在 [left,right)[left, right) 中搜索

ts
  1. function binarySearch(nums: number[], left: number, right: number, target: number): number {
  2. if (target < nums[0]) {
  3. return -1
  4. }
  5. while (left < right) {
  6. const mid = left + Math.floor((right - left) / 2)
  7. if (nums[mid] < target) {
  8. left = mid + 1
  9. } else if (nums[mid] > target) {
  10. right = mid
  11. } else {
  12. left = mid + 1
  13. }
  14. }
  15. return left - 1
  16. }

为什么结果是 left1left - 1 ?

因为当nums[mid]targetnums[mid] \leq target 时,mid=mid+1mid = mid + 1,那结束时,nums[left]nums[left] 就成为第一个大于 targettarget 的元素了。

2. 在 [left,right][left, right] 中搜索

ts
  1. function binarySearch(nums: number[], left: number, right: number, target: number): number {
  2. if (target < nums[0]) {
  3. return -1
  4. }
  5. while (left <= right) {
  6. const mid = left + Math.floor((right - left) / 2)
  7. if (nums[mid] < target) {
  8. left = mid + 1
  9. } else if (nums[mid] > target) {
  10. right = mid - 1
  11. } else {
  12. left = mid + 1
  13. }
  14. }
  15. return left - 1
  16. }

总结

类型 初始可行域 结束条件 小于目标值 等于目标值 大于目标值 成功返回 失败条件
与目标值相等 [left,right)[left, right) left < right left = mid + 1 return mid right = mid mid 目标值不在数组中
[left,right][left, right] left <= right left = mid + 1 return mid right = mid - 1 mid 目标值不在数组中
大于等于目标值的最小值 [left,right)[left, right) left < right left = mid + 1 right = mid right = mid left 目标值大于数组任何元素
[left,right][left, right] left <= right left = mid + 1 right = mid - 1 right = mid - 1 left 目标值大于数组任何元素
小于等于目标值的最大值 [left,right)[left, right) left < right left = mid + 1 left = mid + 1 right = mid left - 1 目标值小于数组任何元素
[left,right][left, right] left <= right left = mid + 1 left = mid + 1 right = mid - 1 left - 1 目标值小于数组任何元素
0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run