Binary Search
Binary Search 总结
什么时候使用二分法?
当要求使用比 O(n)
还要低的时间复杂度时,只能是 O(lgn)
。通常对应二分法和倍增法。
二分法模板:
首先看一个经典的二分查找问题:
在一个排序数组中找一个数,返回该数出现的任意位置,如果不存在,返回 -1
样例:
给出数组 [1, 2, 2, 4, 5, 5]
.
对于
target = 2
, 返回1
或者2
.对于
target = 5
, 返回4
或者5
.对于
target = 6
, 返回-1
.
代码:
上面模板中需要注意:
while
循环条件start + 1 < end
可以保证循环内start
和end
不相邻,从而避免死循环。mid = start + (end - start) / 2
可以防止溢出。每次的更新要使用
start = mid
或end = mid
。前面的
while
循环目的是缩小查找区间,直到start
和end
相邻,因此while
里面可以不return
。熟练此模板,对于其他的二分查找问题,只需要根据题意改变缩小区间的条件即可。
本总结参考/复制/修改了很多综合帖的内容,全部参考文档会赋予文章底部。
作者:公瑾
Binary Search | 基础
用途:针对Sorted数组查找的算法 时间复杂度:
O(log(n))
二分查找法的前置条件要拥有一个已经Sorted好的序列,这样在查找所要查找的元素时, 首先与序列中间的元素进行比较, 如果大于这个元素, 就在当前序列的后半部分继续查找, 如果小于这个元素, 就在当前序列的前半部分继续查找, 直到找到相同的元素, 或者所查找的序列范围为空为止.
伪代码:
Binary Search | 痛点
十个二分九个错:编程珠玑的作者Jon Bentley,布置作业让他的学生们写二分查找,然后他一个个来看。结果呢,他发现 90%
是错的。
按照上面的伪代码写出了下列的Python代码
接下来说说二分查找的一些痛点。
痛点 1: 溢出
mid = (l + r) // 2
在对两个Signed 32-bit数字进行相加时,有可能出现溢出,例如下面这种情况:
当left 与 right 之和超过了所在类型的表示范围的话, 这个和就会成为一个很随机的值, 那么 middle 就不会得到正确的值. 所以, 更稳妥的做法应该是这样的
痛点 2: 边界错误
二分查找算法的边界, 一般来说分两种情况, 一种是左闭右开区间, 类似于 [left, right)
, 一种是左闭右闭区间, 类似于 [left, right]
.
等等,区间是个什么鬼,左闭右开,左闭右闭又是个什么鬼?
区间的定义: 区间有开区间和闭区间,其中又分为全开区间 ( )
,全闭区间 [ ]
,左开右闭区间 ( ]
和左闭右开区间 [ )
, 开区间的意思是区间两处的端点值取不到,而闭区间的端点值就可以取到。
例如区间 [2,6)
,他是一个左闭右开的区间,那么在这 2~6
之间的数字我都可以取到,而且可以取到 2
,但不可以取到 6
.
需要注意的是, 循环体外的初始化条件, 与循环体内的迭代步骤, 都必须遵守一致的区间规则, 也就是说, 如果循环体初始化时, 是以左闭右开区间为边界的, 那么循环体内部的迭代也应该如此. 如果两者不一致, 会造成程序的错误. 比如下面就是错误的二分查找算法:
这个算法的错误在于, 在循环初始化的时候, 初始化 r = len(arr)
, 也就是采用的是左闭右开区间, 而当满足 target < arr[mid]
的条件是, target 如果存在的话应该在 [left, mid)
区间中, 但是这里却把 r
赋值为 mid - 1
了, 这样, 如果恰巧 mid-1 就是查找的元素, 那么就会找不到这个元素.
下面给出两个算法, 分别是正确的左闭右闭和左闭右开区间算法, 可以与上面的进行比较:
左闭右闭:包括End区间,end inclusive
左闭右开,不包括End区间, end exclusive
痛点 3 : 死循环
上面的情况还只是把边界的其中一个写错, 也就是右边的边界值写错, 如果两者同时都写错的话, 可能会造成死循环, 比如下面的这个程序:
这个程序采用的是左闭右闭的区间. 但是,
当 target < arr[mid]
的时候, 那么下一次查找的区间应该为 [mid + 1, right]
, 而这里变成了 [mid, right];
当 target > arr[mid]
的时候, 那么下一次查找的区间应该为 [left, mid - 1]
, 而这里变成了 [left, mid]
. 两个边界的选择都出现了问题, 因此, 有可能出现某次查找时始终在这两个范围中轮换, 造成了程序的死循环.
Binary Search | 模板选择
@gengwuli
提出了模板一致性的观点,我也十分赞同。公瑾的策略是 95%
的情况下都用以下模板:
极个别情况会采用以下的模板:
至于为什么不采用一个模板,是因为在大部分题的时候,运用第一种模板能够有更好的可读性 而第二个模板专门针对的是第一个模板的短板:当要access数组边界的数,如果边界的数在运行中出现更改,可能越界。虽然这种情况也可以用Edge Case来写,但太过麻烦。这点我们后面具体说来。
这里插一句话:用的惯的模板才是最适合你的,切记
Binary Search | 题型
题目类型分为三类:
有明确Target
没有明确Target
没有明确Target (可越界类型)
第一类题:有明确 Target 的题型
这一类题,会要求你找一个 Target
值,一般找到就返回 Target
值的下标或者Boolean函数。基础题目举两个例子:
LeetCode 374. Guess Number Higher or Lower
告知
Target
是1
到N
之间的一个数,然后返回这个数的下标
LeetCode 367. Valid Perfect Square
给定一个数值,判断是不是
Perfect Square
,这里只需要从0
到Target
中选择折中点,然后不停乘方和Target
比对即可,因为返回是Boolean
,同样不需要考虑边界
由于这一类题基本上就是套公式,直接考公式的几率很小。所以Medium以上的题目会对边界的选定模糊化,我们要做的是明确边界,然后再套公式。下面举例二分经典题:
33. Search in Rotated Sorted Array
取中间值以后会发现,左边或者右边,至少有一边是sorted的,根据这个特性,搭配下面这个神图,就能理解下一段的代码
第二类题:没有明确 Target 的题型
该神器的使用方法:把代码拷过去,然后initiatize一个object,随便传入一个Test Case,然后点运行。代码会一步一步的跑,你就可以看你 left
和 right
在每一层迭代之后的值了。
这一类的题比较多变,可能会要你找
比Target大的最小值
比Target小的最大值
满足要求的第一个值
不满足要求的最后一个值
...
在刷这类题前,我们先看看模板,在迭代退出的时候,left
和 right
的位置:
Case 1:
首先这个程序肯定返回
-1
。我们的模板对While Loop定义如下:while l <= r:
那么只有当
l > r
的时候,While Loop才会终止。重点来了,当迭代结束后,假设
Target
为4, L和R的位置分别对应着两个条件:L
对应的是:第一个比4大的坐标, 根据这道题的定义就是比Target大的最小值R
对应的是:最后一个比4小的坐标,根据这道题的定义就是比Target小的最大值
Case 2:
根据我们 While Loop
的特性,有一个 Edge
的情况就是,L最大可以等于len(array)
这里可以看到,如果我们要返回 array[l]
,系统是会报错的,因为我们的L已经越界了,这个局限性是这套模板的小短板,当然,只要大家记住这一点,以后就可以写出相对应的Edge Case处理。
l
和 r
的位置和定义清楚了以后,我们来做做题。
返回 l 的情况: 33. Search in Rotated Sorted Array
给了一个Target值,找到在Array里,Target按顺序应该插入的位置。
这里我们同样可以用镇楼法宝模拟下 L
, R
在迭代结束后的下标
l
的下标是1, 定义是第一个满足条件的最小值。r
的下标是0,定义是最后一个不满足条件的最大值。
我们返回l
即可,另外还要说一点,这个模板对于这道题得天独厚的好处就是,不需要考虑当target插入的下标等于len(arr)
的Edge Case,因为l
本身就自带这个特性。
返回r的情况:458. Last Position of Target (Lintcode)
给了一个Target值,找到在Array里,该Target值出现最后的坐标
这道题的Array里面会有重复,去重的方式就是当 nums[mid] == target
的时候,对left进行增值。这样就可以去掉左边重复的数。
举例:
l
的下标是3, 定义是第一个不满足条件的值r
的下标是2,定义是最后一个满足条件的值
所以返回r
楼上两题的合体:34. Search for a Range
分别找到第一个位置,和最后一个位置,返回一个区间。
第三类题:没有明确Target的题型,可越界类型
这种类型的题目,用 l <= r
的模板可能会越界,我们可以填写个别的Edge Case处理,或者套用其他比如 l < r
或者 l + 1 < r
的模板解决。
162. Find Peak Element
找峰值,mid
比对的区间是 nums[mid + 1]
,这种情况当 l
越界等于 len(nums)
就会报错,所以可以选择用 while l + 1 < r
的区间,最终对 l
和r
进行比对。当然也可以写Edge处理。
153. Find Minimum in Rotated Sorted Array
这道题最终要返回的 nums[l]
。同样,可以写 Edge Case 处理,也可以使用 while l < r
或者 while l + 1 < r
来解。
参考
Last updated