# 3. 把待搜索区间分成两个部分(重点、最重要的部分在这里)
根据看到的中间位置的元素的值 nums[mid]
可以把待搜索区间分为两个部分:
- 一定不存在 目标元素的区间:下一轮搜索的时候,不用考虑它;
- 可能存在 目标元素的区间:下一轮搜索的时候,需要考虑它。
由于 mid
只可能被分到这两个区间的其中一个,即:while
里面的 if
和 else
就两种写法:
- 如果
mid
分到左边区间,即区间分成[left..mid]
与[mid + 1..right]
,此时分别设置right = mid
与left = mid + 1
; - 如果
mid
分到右边区间,即区间分成[left..mid - 1]
与[mid..right]
,此时分别设置right = mid - 1
与left = mid
。
并且把 循环可以继续的条件 写成 while (left < right)
。在上面把待搜索区间分成两个部分的情况下,退出循环以后一定会有 left == right
成立,因此在退出循环以后,不需要考虑到底返回 left
还是返回 right
。
这里介绍一个 「重要的经验」:
在 写
if
语句的时候,通常把容易想到的,不容易出错的逻辑写在if
的里面,这样就把复杂的、容易出错的情况放在了else
的部分,这样编写代码不容易出错。
什么情况是容易想到的,不容易出错的呢?我的经验是:题目要我们找符合条件 a 的元素,我们就对条件 a 取反面,这样分析不容易出错。
例如本题(「力扣」第 35 题),题目要我们找出第一个大于等于 target
的元素的下标,那么小于 target
的元素就一定不是我们要找的。因此 if
语句就是
if (nums[mid] < target) {
// 下一轮搜索区间是 [mid + 1..right]
left = mid + 1;
}
2
3
4
剩下的情况放在 else
中,我们 甚至可以不用分析 else
是什么情况。if
的区间是 [mid + 1..right]
,它的反面区间就是 [left..mid]
,此时 else
就应该设置 right = mid
。
因此完整的逻辑就是:
if (nums[mid] < target) {
// 下一轮搜索区间是 [mid + 1..right]
left = mid + 1;
} else {
right = mid;
}
2
3
4
5
6
上面的叙述,总结起来就一句话:我们 总是在区间 [left..right]
里查找元素目标。
注意:我们说的是 左闭右闭区间。为什么不是「左闭右开」呢?「左闭右开」当然可以,但是我们 不想把精力花在思考「右边界是不是可以取到」这件事情上,这是因为 任意一个「左闭右开 [left..right)
」区间一定唯一对应一个「左闭右闭 [left..right - 1]
」区间,所以到底是开区间还是闭区间,前后保持一致就可以。
根据 mid
位置是不是目标元素,进而判断 mid
的左边是否存在目标元素,mid
的右边是否存在目标元素,只把搜索区间分为两个部分,然后设置 left
和 right
,在设置 left
和 right
的时候,左闭右闭区间的形式是最直观的,这是因为如果是开区间,还需要在脑子里反应一下,右端点不包括。如果我们觉得二分问题复杂,复杂问题应该简单做。
我们再说说把区间分成两个部分的好处:
在代码层面,只可能有以下两种情况:
while(left < right)
与left = mid + 1
、right = mid
的搭配;while(left < right)
与left = mid
、right = mid - 1
的搭配;
只有在这两种把区间分成两个部分的划分下,退出循环以后有 left == right
成立,我们不用去讨论返回 left
还是 right
(这句话非常重要,大家可以在做题的过程中慢慢体会)。
补充说明:有的朋友觉得把区间分为三个部分更清晰,但是一旦分成三个部分,有
mid + 1
、mid - 1
出现,退出循环以后就不一定有left
和right
重合。我们完全可以这样做,分类讨论的时候分成三个部分,然后把它们的逻辑合并起来。
在我们的讲解中 while(left < right)
与「定义的区间是 [left..right)
」没有任何关系,请大家不要做多余的解读,我们不讲循环不变量是 [left..right)
的情况,我们只认为区间是「左闭右闭」区间,理由上面也说了,每一个位置的值是不是可以取到,我们需要非常清楚,而不想看到一个开区间,我们在脑子里需要想一下「右端点不包括」。
至于为什么是 left = mid + 1
、 right = mid
搭配使用,而 left = mid
、 right = mid - 1
搭配使用,这一点完全不用记忆,我们画图说明。
{:style="width:500px"}
因此我们再次和大家强调:永远去思考下一轮搜索应该在哪个区间里,就能考虑清楚到底下一轮更新的是 left
还是 right
,到底加不加 1
,到底减不减 1
。