1. 二分查找易错点总结
二分查找中有三个点需要特别注意:
- 搜索范围的左右边界,即
left = 0
还是left = -1
,right = nums.lengh-1
还是right = nums.lengh
; - 搜索停止(循环结束)的条件,即
while(left < right)
与while(left <= right)
的选择问题; - 搜索时中间值能否加入左/右边界,即
right = mid
还是right = mid-1
,left = mid
还是left = mid+1
; mid
怎么计算,即mid = left + (right - left) / 2
还是mid = left + (right - left + 1) / 2
;
注意:我在下面总结的是一个“系统”,如果按照下面的方法确定搜索边界,那么循环终止条件也必须按照下面的思路选择,不然很容易迷糊。其实不按照下面的思路,甚至是相悖的思路也能正确解题,只不过我太菜了所以才这样总结,希望大神勿喷。
1.1 搜索边界的确定
假设有序数组为int[] nums
,有如下三种情况需要考虑:
-
示例一
我们要搜索的是大于target
的最小值索引,而target
比数组中所有的数都要大,那么[0, nums.lengh-1]
内就没有满足条件的索引,右边界必须扩大,即令right = nums.lengh
,此时目标索引就是 nums.lengh。而左边界不用动,因为即使target
比数组中所有的数都要小,目标索引就是0了,所以搜索区间改为[0, nums.lengh]
。可以结合下图思考。
-
示例二
我们要搜索的是小于target
的最大值索引,而target
比数组中所有的数都要小,那么[0, nums.lengh-1]
内就没有满足条件的索引,左边界必须扩大即令left = -1
,此时目标索引就是-1。右边界不用动,因为即使target
比数组中所有的数都要大,小于target
的最大值索引就是nums.lengh-1,所以搜索区间为[-1, nums.lengh-1]
。可以结合下图思考。
-
示例三(一般用于搜索区间内是否有某个值)
我们要搜索的是等于target
的索引,数组中有可能有target
,搜索区间为[0, nums.lengh-1]
,因为当在该区间内搜索不到时,会跳出循环,直接返回-1表示搜索不到就行。其实,若题目要求:如果target
比数组中所有的数都大,返回nums.lengh,如果target
比数组中所有的数都小,返回-1,此时就可以把区间扩充为[-1, nums.lengh]
,不过此时要注意循环结束条件。
一般搜索边界都是令left = 0
,right = nums.lengh-1
,
1.2 循环结束条件
假设有序数组为int[] nums
,求解的是索引,只需考虑两种情况:
- 情况一
如果搜索区间[left, right]
中一定有目标值索引,那么循环截止条件是while(left < right)
,因为当left == right
时目标索引就是left或者right,也就是说1中讨论的情况一和情况二循环终止条件都是while(left < right)
,甚至情况三时,如果我们将搜索区间扩充为[-1, nums.lengh]
,循环终止条件也是while(left < right)
; - 情况二
如果搜索区间[left, right]
中不一定有目标值索引,那么循环截止条件是while(left <= right)
;(一般用于搜索区间内是否有某个值)
注意:
如果我们应根据题目要求选择是否扩充边界,如果按照1种的思路去扩充了,那么循环截止条件一定是while(left < right)
1.3 中间值归属问题
这个问题其实比较灵活,这里我只讨论3种情况,其余情况类似。假设有序数组为int[] nums
,求解的是索引,根据题目要求分为3种情况:
- 情况一
假设我们要搜索的是大于6的最小值索引,如果num[mid] <= 6
,那么这个mid一定不是目标索引,此时left = mid + 1
(因为mid位置的值比目标值还小,而我们找的元素肯定大于目标值),如果如果num[mid] > 6
,此时的mid是有可能是目标索引的,所以令right = mid
; - 情况二
假设我们要搜索的是小于6的最大值索引,如果num[mid] >= 6
,那么这个mid一定不是目标索引,此时right = mid - 1
(因为mid位置的值比目标值还大,而我们找的元素肯定小于目标值),如果如果num[mid] < 6
,此时的mid是有可能是目标索引的,所以令left = mid
; - 情况三(最简单)
假设我们要搜索的是等于6的索引,如果num[mid] > 6
,那么这个mid一定不是目标索引,此时right = mid - 1
;如果如果num[mid] < 6
,此时的mid也肯定不是目标索引,所以令left = mid + 1
;如果num[mid] == 6
,那就找到啦;
1.4 mid的计算方法问题
mid = left + (right - left) / 2
还是mid = left + (right - left + 1) / 2
是完全不同的,一个取得是靠左边的中位数,一个取得是靠右的中位数,具体用哪种计算方法呢?
先说结论,循环体中有令left = mid
,一定选第二种,否则选第一种。所以敲代码时,一般先写上mid = left + (right - left) / 2
,写完循环体再看看需不需要改。
下面说一下为什么是这样。如下图所示,如果有left = mid
这个条件,就意味着左边界有可能是一直不动的,当出现图中left与right紧挨着的时候,mid会始终等于左边界,程序就会陷入死循环,这是编程语言语法导致的。所以为了避免这种情况,使得当left与right紧挨着时left也能向右滑动,必须令mid = left + (right - left + 1) / 2
。而如果有right = mid
这个条件则不必担心这个问题,因为编程语言语法自动选择的是左边的中位数,right天生能左移.
2. 总结
做题时按照上面的思路一步步去考虑,基本不会出错。步骤如下:
- 首先根据题目要求考虑,边界怎么选,是否需要改变搜索区间;
- 然后考虑搜索区间是否一定有目标值(索引),进而确定循环终止条件;
- 暂时写下
mid = left + (right - left) / 2
,根据题目要求判断mid的归属; - 最后,写完循环体,根据left的滑动方法,判断mid是否需要更改。
这里就不举例了,题目太多了,建议大家去刷一刷LeetCode中的二分查找相关问题,将总结中的4个步骤记住,反复练习,很快就能上手大部分中等及以下的题目。