二分法介绍
二分查找对具有指定左索引和右索引的连续序列进行操作。这就是所谓的查找空间。二分查找维护查找空间的左、右和中间指示符,并比较查找目标或将查找条件应用于集合的中间值;如果条件不满足或值不相等,则清除目标不可能存在的那一半,并在剩下的一半上继续查找,直到成功为止。如果查以空的一半结束,则无法满足条件,并且无法找到目标。时间复杂度为O(logn)。
总而言之,二分查找法直观简单,在很多题中二分查找是求解的重要组成部分或者其中的一个步骤。虽然二分法原理清晰,但是很多人使用起来头绪就很乱(道理都懂,但自己就总是出问题)。其根本原因就是对二分法的关键点还理解不够深入,只有理解透彻了才能灵活地运用起来。
1 、二分法的关键点
要理解好二分法需要注意的要三点:
1)初始条件:搜索空间(也叫做解空间)的确定
如常见的有,left = 0, right = len(nums) - 1。
**2)终止条件
left > right 或 left >= right
3) 查找的过程:搜索空间的调整
二分的中心:mid = (left+right)/2
nums[mid] < target时:left = mid + 1
nums[mid] > targe时t:right = mid - 1,常看到还有的是right = mid,这与初始条件的设置有关,也就是left = 0, right = len(nums)。
nums[mid] == target时视情况而定。
2 、二分法的应用
2.1 二分查找
1)二分查找: 在数组nums查找target,如果存在返回所在索引,否则返回-1。
Leetcode 704
下面使用的是二分查找,很好地对应了上面的三个关键点,具体见代码注释。
def binarySearch(nums, target):
l, r = 0, len(nums)-1 # 初始条件:搜索空间的确定
while l <= r: # 这里地终止条件是l <= r,这是因为初始条件的右边设为:r = len(nums)-1
# 当l,r相加很大时,为了防止整型溢出,这与mid = (r+l) / 2 是一样的
mid = l + (r - l) // 2 # 二分的中心
if nums[mid] == target:
return mid
elif nums[mid] > target:
# 搜索空间的调整:搜索空间变为[l, mid - 1]
r = mid - 1
else:
# 搜索空间的调整:解空间变为[mid+1, r]
l = mid + 1
return -1
nums = [1, 3, 4, 6, 11, 12, 13]
target = 13
print(binarySearch(nums, target))
与上一题类似的的是Leetcode 74,主要是把二维数组根据索引之间的运算转换为一维数组。
2) 搜索二维矩阵 Leetcode 74
编写一个高效的算法来判断m x n矩阵中,是否存在一个目标值。该矩阵具有如下特性:每行中的整数从左到右按升序排列。每行的第一个整数大于前一行的最后一个整数。
class Solution:
def searchMatrix(self, matrix, target):
if matrix is None or len(matrix) == 0:
return False
row = len(matrix)
col = len(matrix[0])
l = 0
r = row * col - 1
while l <= r:
m = l + (r - l)//2 # 二分的中心
element = matrix[m//col][m%col] #
if element == target:
return True
elif element > target:
r = m - 1
else:
l = m + 1
return False
matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]]
target = 7
print(Solution().searchMatrix(matrix,target))
更多类似地题目还有Leetcode162. 寻找峰值
2.2 左侧边界的⼆分查找
在d数组中二分查找,找到第一个d比target大的数d[i] (也就是左边界),返回d[i]所在索引。
如d = [1, 3, 4, 6, 11, 12, 13],target = 9,返回4。如果d最后的数都不满足条件,那么返回-1。这个索引可以认为是d数组中比target小的元素个数是多少。
def left_bound(d, target):
l, r = 0, len(d) - 1
loc = r
while l <= r:
mid = (l + r) // 2
if d[mid] > target:
r = mid - 1
elif d[mid] < target:
l = mid + 1
elif d[mid] == target:
r = mid - 1
if if l >= len(d) and d[l - 1] != target: # 如果d最后的数都不满足条件,那么返回-1
return -1
else:
return l
d = [1, 3, 6, 8, 11]
target = 16
# d = [1, 3, 4, 6, 11, 12, 13]
# target = 9
print("------寻找左侧边界-------")
print(left_bound(d, target))
与之类似地有 Leetcode 35. 搜索插入位置
注意: 当d = [1, 2, 3, 3, 3, 11],target = 3时,返回的是2。那么为什么不是5,这个就是下面的寻找右侧边界问题。
2.3 右侧边界的⼆分查找
def right_bound(d, target):
l, r = 0, len(d) - 1
loc = r
while l <= r:
mid = (l + r) // 2
if d[mid] > target:
r = mid - 1
elif d[mid] < target:
l = mid + 1
elif d[mid] == target:
# r = mid - 1
l = mid + 1 # 改变这里
# 改变这里
if l < 0 and d[r-1] != target: # 如果d最后的数都不满足条件,那么返回-1
return -1
else:
return l
# d = [1, 3, 6, 8, 11, 20, 22,22,22]
# target = 22
# d = [1, 3, 4, 6, 11, 12, 13]
# target = 9
d = [1, 2, 3, 3, 3, 11]
target = 3
print("------寻找右侧边界-------")
print(right_bound(d, target))
2.4 最长递增子序列 Leetcode300
方法:贪心算法+二分查找。考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
基于上面的贪心思路,我们维护一个数组d[i] ,表示长度为 i的最长上升子序列的末尾元素的最小值,起始时 d[0]=nums[0]。这里的二分就是寻找左边界的过程。
class Solution:
def lenghOfLIS(self, nums):
d = []
for num in nums:
# 如果nums[i] > d[-1],则直接加入到d数组末尾,并其长度加1
if not d or num > d[-1]:
d.append(num)
# 否则,在d数组中二分查找,找到第一个比nums[i]大的数d[k],并更新d[k+1]=nums[i]
# 这里就是相当于寻找左边界问题
else:
l, r = 0, len(d) - 1
while l <= r:
mid = (l+r) // 2
if d[mid] >= num:
# loc = mid
r = mid - 1
else:
l = mid + 1
d[l] = num
return len(d) # 返回d的长度就是所求的最长的递增子序列长度
# nums = [0, 8, 4, 12, 2]
nums = [8, 2, 3, 1, 9, 8, 11]
print(Solution().lenghOfLIS(nums))
2. 5 能力检测问题
Leetcode875 爱吃香蕉的珂珂
方法:二分查找
思路:如果珂珂能以 K 的进食速度最终吃完所有的香蕉(在 H 小时内),那么她也可以用更快的速度吃完。当珂珂能以 K 的进食速度吃完香蕉时,我们令 possible(K) 为 true,那么就存在 X 使得当 K >= X 时, possible(K) = True。
class Solution(object):
def minEatingSpeed(self, piles, h):
"""
:type piles: List[int]
:type h: int
:rtype: int
"""
# 向上取整,先计算以速度K吃每一堆苹果的时间,然后再把全部时间相加,最后判断t<=h
def possibale(K):
t = sum((p-1) // K + 1 for p in piles)
return t <= h
# 每次最少吃一个,最多吃苹果里数量最大的
left, right = 1, max(piles)
while left <= right:
mid = left + (right-left) // 2
# 如果以mid速度能吃完,那么减小速度
if possibale(mid):
right = mid - 1
# 如果以mid速度不能吃完,那么增大速度
else:
left = mid + 1
return left
明白了这一题,还有一题相似的Leetcode 1011 在 D 天内送达包裹的能力。
以上就是我对二分法的简单介绍和应用二分法解题,起到了抛砖引玉的作用。除了上述的三个场合,还有很多都会使用二分,更多详见⻄法的刷题秘籍:https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/thinkings/binary-search-1
此外还有对于二分法的固定模板问题,labuladong的算法小抄官方完整版(字节)一书有详细地讲解。https://mp.weixin.qq.com/s/M1KfTfNlu4OCK8i9PSAmug
参考文章
[1] 我作了首诗,保你闭着眼睛也能写对二分查找——labuladong
[2] 二分专题上、下——⻄法的刷题秘籍
[3] https://leetcode-cn.com/leetbook/read/binary-search/xehmn2/