# 3. 把待搜索区间分成两个部分(重点、最重要的部分在这里)

根据看到的中间位置的元素的值 nums[mid] 可以把待搜索区间分为两个部分:

  • 一定不存在 目标元素的区间:下一轮搜索的时候,不用考虑它;
  • 可能存在 目标元素的区间:下一轮搜索的时候,需要考虑它。

由于 mid 只可能被分到这两个区间的其中一个,即:while 里面的 ifelse 就两种写法:

  • 如果 mid 分到左边区间,即区间分成 [left..mid][mid + 1..right],此时分别设置 right = midleft = mid + 1
  • 如果 mid 分到右边区间,即区间分成 [left..mid - 1][mid..right],此时分别设置 right = mid - 1left = 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;
}
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;
}
1
2
3
4
5
6

上面的叙述,总结起来就一句话:我们 总是在区间 [left..right] 里查找元素目标

注意:我们说的是 左闭右闭区间。为什么不是「左闭右开」呢?「左闭右开」当然可以,但是我们 不想把精力花在思考「右边界是不是可以取到」这件事情上,这是因为 任意一个「左闭右开 [left..right) 」区间一定唯一对应一个「左闭右闭 [left..right - 1]」区间,所以到底是开区间还是闭区间,前后保持一致就可以。

根据 mid 位置是不是目标元素,进而判断 mid 的左边是否存在目标元素,mid 的右边是否存在目标元素,只把搜索区间分为两个部分,然后设置 leftright在设置 leftright 的时候,左闭右闭区间的形式是最直观的,这是因为如果是开区间,还需要在脑子里反应一下,右端点不包括。如果我们觉得二分问题复杂,复杂问题应该简单做。

我们再说说把区间分成两个部分的好处:

在代码层面,只可能有以下两种情况:

  • while(left < right)left = mid + 1right = mid 的搭配;
  • while(left < right)left = midright = mid - 1 的搭配;

只有在这两种把区间分成两个部分的划分下,退出循环以后有 left == right 成立,我们不用去讨论返回 left 还是 right(这句话非常重要,大家可以在做题的过程中慢慢体会)

补充说明:有的朋友觉得把区间分为三个部分更清晰,但是一旦分成三个部分,有 mid + 1mid - 1 出现,退出循环以后就不一定有 leftright 重合。我们完全可以这样做,分类讨论的时候分成三个部分,然后把它们的逻辑合并起来。

在我们的讲解中 while(left < right) 与「定义的区间是 [left..right) 」没有任何关系,请大家不要做多余的解读,我们不讲循环不变量是 [left..right) 的情况,我们只认为区间是「左闭右闭」区间,理由上面也说了,每一个位置的值是不是可以取到,我们需要非常清楚,而不想看到一个开区间,我们在脑子里需要想一下「右端点不包括」。

至于为什么是 left = mid + 1right = mid 搭配使用,而 left = midright = mid - 1 搭配使用,这一点完全不用记忆,我们画图说明。

image.png{:style="width:500px"}

因此我们再次和大家强调:永远去思考下一轮搜索应该在哪个区间里,就能考虑清楚到底下一轮更新的是 left 还是 right ,到底加不加 1,到底减不减 1