二分查找详解
Posted beeblog72
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分查找详解相关的知识,希望对你有一定的参考价值。
看到一个大佬的博客详解二分查找算法,有一段内容让我深有感触:
我周围的人几乎都认为二分查找很简单,但事实真的如此吗?二分查找真的很简单吗?并不简单。看看 Knuth 大佬(发明 KMP 算法的那位)怎么说的:
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky...
这句话可以这样理解:思路很简单,细节是魔鬼。
这两个月刷题遇到不少要用到二分查找的题。当年学数据结构的时候觉得这是一个相当直观且好理解的算法,但是真正刷题时觉得这个算法需要注意的坑还是挺多的。最普通的应用就是找某个元素的索引(数组有序且不重复),再复杂一些的还有找某个元素最左边或最右边的索引。更高端的有对数组的索引或者数组中整数的取值范围进行二分查找,不过这一块还是万变不离其宗,查找的范围依旧是[left, right]
,难点在于要怎么找到二分查找的对象。
二分查找基本框架
def binarySearch(arr: List[int], target: int):
n = len(arr)
left, right = 0, ... # 左右边界的赋值可变
while left ... right: # 需要注意有没有等号
mid = left + (right - left) // 2
if arr[mid] == target:
... # 要不要直接return
elif arr[mid] < target:
left = ... # 要不要加一
elif arr[mid] > target:
right = ... # 要不要减一
return ... # 有返回mid的,有left的各种
上面一段代码中的...
部分是需要根据题目需要修改的地方,也就是二分查找的细节所在。另外,计算mid
的公式也可以写成mid = (left + right) // 2
,按上面那样写是为了防止溢出(虽然在Python里并不会有整型溢出的问题,不过最好养成这个习惯)。
找一个数的索引
这是二分查找最简单的一种应用,只要学习过数据结构肯定闭着眼睛都能写出来。
def binarySearch(arr: List[int], target: int):
n = len(arr)
left, right = 0, n - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
elif arr[mid] > target:
right = mid - 1
return -1 # 没有找到,返回 -1
这里有几个地方需要注意。
左右指针的赋值
左右指针的初始化决定了搜索区间是开区间还是闭区间
左右指针初始化为left, right = 0, n - 1
,也就是说搜索区间是一个闭区间,即[0, n - 1]
。而当mid
处的值不是目标值时,就要把mid
从搜索区间中去除,继续在两边的某个闭区间中搜索。因此,左右指针的更新规则为left = mid + 1
和right = mid - 1
。
终止条件
搜索区间为空时就应该跳出循环
上面的代码中,while循环的条件是left <= right
,也就是说当left == right + 1
时跳出while循环。实际上,这个终止条件与前面说的闭区间相对应,当left == right
时,闭区间内仍有一个索引的位置需要搜索;当left == right + 1
时,[right + 1, right]
已经是一个空集,意味着已经没有索引需要搜索了,因此就跳出循环。
开区间
爷就是喜欢开区间,那咋办嘛
开区间情况下,左右指针应初始化为left, right = 0, n
,对应的搜索区间是一个开区间,即[0, n)
。
- 当
arr[mid] < target
时,同样地,要把mid
从搜索区间中去除,注意此时是开区间。于是left = mid + 1
,对应的搜索区间为[mid + 1, right)
; - 当
arr[mid] > target
时,right = mid
, 对应的搜索区间为[left, mid)
。
此时,while循环的条件应为left < right
,也就是说当left == right
时跳出while循环。对应的搜索区间为[left, left)
,显然这个区间为空,即搜索完毕。完整代码如下。
def binarySearch(arr: List[int], target: int):
n = len(arr)
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
elif arr[mid] > target:
right = mid
return -1 # 没有找到,返回 -1
局限性
这个版本的二分查找至少还有两个需求无法满足:
- 如果
target
在数组中多次出现,我们想要找到它的左边界(即最早出现时的索引)或者右边界要怎么改呢? - 如果
target
在数组中不出现,我们想要找到它插入到该数组中应该在的位置要怎么改呢?
寻找左侧边界
基于开区间版本,修改几个位置即可。
def binarySearch(arr: List[int], target: int):
n = len(arr)
if n == 0: # 特判
return -1
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if arr[mid] == target:
right = mid # 修改
elif arr[mid] < target:
left = mid + 1
elif arr[mid] > target:
right = mid
return left # 修改
代码还可以进一步简化,但是本文主要是为了弄清原理,这么写看起来更直观一些。基于这三处修改,我们来看看为什么这个版本可以返回左侧边界。
为什么需要特判
实际上也不是所有情况都需要特判,因为很多时候我们不会在空数组中搜索,那不是吃饱了撑的吗。主要是为了和返回的索引区分开来,避免返回值与空数组的情况发生混淆,具体往下看就明白了。
返回值的意义
这个版本的代码除了当target
在数组中多次出现时返回它的左侧边界,它的返回值还有一个含义就是当前数组中比target
小的元素个数。因此返回值的范围是[0, n]
,如果没有特判,数组为空时也返回0
,会发生混淆。
为什么可以找到左边界
关键在于这段代码:
if arr[mid] == target:
right = mid
当找到target
时,不返回索引,而是继续向左搜索,即在[left, mid)
中搜索。
那么问题来了,如果此时mid
已经是左边界了,继续在[left, mid)
中搜索不会出错吗?事实上,每次左指针的更新规则为left = mid + 1
且while循环的条件是left < right
。也就是说最终仍然会搜索到[left, mid)
,此时left == mid
。
寻找右侧边界
基于上面的代码,修改一下就行。
def binarySearch(arr: List[int], target: int):
n = len(arr)
if n == 0: # 特判
return -1
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if arr[mid] == target:
left = mid + 1 # 修改
elif arr[mid] < target:
left = mid + 1
elif arr[mid] > target:
right = mid
return left - 1 # 修改
为什么要返回left - 1
注意到,当找到target
时,不返回索引,而是继续向右搜索,left = mid + 1
。因此,while循环结束时的左指针一定指向target
右边的第一个元素。因此要返回left - 1
。
寻找插入位置
上面提到,寻找左侧边界版本的返回值同样代表了数组中比target
小的元素个数,索引不就来了吗?
def binarySearch(arr: List[int], target: int):
n = len(arr)
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if arr[mid] == target:
right = mid # 修改
elif arr[mid] < target:
left = mid + 1
elif arr[mid] > target:
right = mid
return left # 修改
需要注意的是,这里不需要特判,数组为空时直接往里放就完事了。
以上是关于二分查找详解的主要内容,如果未能解决你的问题,请参考以下文章