在使用二分法的过程中,最让我迷惑的就是边界的界定,这里总结一下使用二分法时,可行的边界条件
例一:704. 二分查找
版本一:(错误版本)
这题是一道非常简单的二分查找题,但是我却并没有一次做对,原因出在没有处理好退出二分查找的边界导致超出时间限制的错误。以我最后出错的测试用例来分析,在【2,5】数组中,由于我的while循环退出条件是left<=right,所以只有当left>right才会退出循环,但是在我的代码中,当nums[mid]>target和nums[mid]<target时直接将mid赋值给right和left,且mid的更新式子为mid=(left+right)//2,导致在【2,5】数组中mid一直为零。而由于target为5,需要输出的序号为1,所以整个程序卡在了while循环里。
正确的版本介绍在版本二、版本三
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
n=len(nums)
left,right=0,n-1
if target not in nums:
return -1
while left<=right:
mid=(left+right)//2
if nums[mid]==target:
return mid
elif nums[mid]>target:
right=mid
else:
left=mid
版本二:(正确版本)
相较版本一,对边界条件稍微修改,while退出循环的判断条件不变,还是是left>right退出循环。但是修改nums[mid]<target和nums[mid]>target时left和right的赋值。right=mid-1和left=mid+1。还是以【2,5】数组、target=5为例,最后left==right且都为1,mid也等于1,这时的代码逻辑可以得到正确的结果
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
left,right=0,len(nums)-1
if target not in nums:
return -1
while left<=right:
mid=(left+right)//2
if nums[mid]==target:
return mid
elif nums[mid]>target:
right=mid-1
else:
left=mid+1
版本三:(正确版本)
除了以上两种情况之外,还有一种可以得到正确结果的情况。这种情况下,假设right所指向的数是取不到的,所以初始化为len(nums),更新时把已经判断过不是target的mid直接赋给right。
class Solution:
def search(self, nums: List[int], target: int) -> int:
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
left,right =0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid+1
elif nums[mid] > target:
right = mid
else:
return mid
return -1
例二:162. 寻找峰值
162. 寻找峰值
这题要求使用时间复杂度为 O(log n) 的算法来解决此问题,需要用到二分查找,但是题目里给出的数组并不是有序数组,所以比较难想出解决方法。
最重要的点在于理解,由于题目中给出了nums[-1] = nums[n] = -∞,可以形象地想象成数组中首尾都是洼地,所以只要数组中存在一个元素比相邻元素大,那么沿着它一定可以找到一个峰值。这里mid和mid+1用来表示数组中的某个元素和它相邻的元素。
如果nums[mid]>nums[mid+1],说明在数组的左边一定存在一个峰值,right=mid-1;反之,left=mid+1
还有需要注意判断当前元素是否时峰值的条件(get(mid-1)<get(mid) and get(mid)>get(mid+1))
考虑到[mid-1,mid+1]属于[-1,n],所以定义一个def get(idx):来取数组中的数,if idx==-1 or idx==n: return float(“-inf”)
class Solution(object):
def findPeakElement(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n=len(nums)
def get(idx):
if idx==-1 or idx==n:
return float("-inf")
else:
return nums[idx]
# left<=right,等于right的情况
left,right,ans=0,n-1,-1
while left<=right:
mid=(left+right)//2
if (get(mid-1)<get(mid) and get(mid)>get(mid+1)):
return mid
if get(mid)<get(mid+1):
left=mid+1
else:
right=mid-1
例三:33. 搜索旋转排序数组
二分法,虽然旋转后的数组本身不是增序,但是依然有增序的部分,可以通过mid和left\right的对比找到这些部分
如果target在增序的部分,继续二分;如果不是,那把left=mid+1或right=mid-1,就是整个问题的递归
以nums = [4,5,6,7,0,1,2], target = 0为例,mid第一轮指向7,先判断7不是target,且>=nums[left],此时判断target=0即小于nums[mid]也小于nums[left],所以把left=mid+1,缩小搜索的范围;若target是5,target<nums[mid] and target>=nums[left]为真,则right=mid-1来减小搜索的范围。
相比直接的二分查找,这题要讨论的条件更多一点
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
n=len(nums)
left,right=0,n-1
while left<=right:
mid=(left+right)//2
if nums[mid]==target:
return mid
if nums[mid]>=nums[left]:
if target<nums[mid] and target>=nums[left]:
right=mid-1
else:
left=mid+1
if nums[mid]<=nums[right]:
if target>nums[mid] and target<=nums[right]:
left=mid+1
else:
right=mid-1
return -1
二分法的实际应用题
例四:2187. 完成旅途的最少时间
给你一个数组 time ,其中 time[i] 表示第 i 辆公交车完成 一趟旅途 所需要花费的时间。
每辆公交车可以 连续 完成多趟旅途,也就是说,一辆公交车当前旅途完成后,可以 立马开始 下一趟旅途。每辆公交车 独立 运行,也就是说可以同时有多辆公交车在运行且互不影响。
给你一个整数 totalTrips ,表示所有公交车 总共 需要完成的旅途数目。请你返回完成 至少 totalTrips 趟旅途需要花费的 最少 时间。
输入:time = [1,2,3], totalTrips = 5
输出:3
解释:
- 时刻 t = 1 ,每辆公交车完成的旅途数分别为 [1,0,0] 。
已完成的总旅途数为 1 + 0 + 0 = 1 。 - 时刻 t = 2 ,每辆公交车完成的旅途数分别为 [2,1,0] 。
已完成的总旅途数为 2 + 1 + 0 = 3 。 - 时刻 t = 3 ,每辆公交车完成的旅途数分别为 [3,1,1] 。
已完成的总旅途数为 3 + 1 + 1 = 5 。
所以总共完成至少 5 趟旅途的最少时间为 3 。
class Solution(object):
def minimumTime(self, time, totalTrips):
"""
:type time: List[int]
:type totalTrips: int
:rtype: int
"""
#至少 totalTrips 趟旅途需要花费的最少时间。
#最多花费min(time)*totaltrip的时间
#l=0 r=min(time)*totaltrip
#check(time)#测试T时刻是否可以完成旅途
def check(T):
res=0
for t in time:
res+=T//t
return res>=totalTrips
l,r=1,min(time)*totalTrips #为了防止除数为0,且时刻最少也是1
ans=0
while l<=r: #想想为什么是<=,例子 time = [2], totalTrips = 1
mid=(l+r)//2
if check(mid):
ans=mid
r=mid-1
else:
l=mid+1
return ans
例五:2226. 每个小孩最多能分到多少糖果
给你一个 下标从 0 开始 的整数数组 candies 。数组中的每个元素表示大小为 candies[i] 的一堆糖果。你可以将每堆糖果分成任意数量的 子堆 ,但 无法 再将两堆合并到一起。
另给你一个整数 k 。你需要将这些糖果分配给 k 个小孩,使每个小孩分到 相同 数量的糖果。每个小孩可以拿走 至多一堆 糖果,有些糖果可能会不被分配。
返回每个小孩可以拿走的 最大糖果数目 。
输入:candies = [5,8,6], k = 3
输出:5
解释:可以将 candies[1] 分成大小分别为 5 和 3 的两堆,然后把 candies[2] 分成大小分别为 5 和 1 的两堆。现在就有五堆大小分别为 5、5、3、5 和 1 的糖果。可以把 3 堆大小为 5 的糖果分给 3 个小孩。可以证明无法让每个小孩得到超过 5 颗糖果。
输入:candies = [2,5], k = 11
输出:0
解释:总共有 11 个小孩,但只有 7 颗糖果,但如果要分配糖果的话,必须保证每个小孩至少能得到 1 颗糖果。因此,最后每个小孩都没有得到糖果,答案是 0 。
class Solution(object):
def maximumCandies(self, candies, k):
"""
:type candies: List[int]
:type k: int
:rtype: int
"""
#相同数量的糖果,只能分不能和
#当分配i个糖果时,是否符合要求
def check(i):
res=0
for c in candies:
res+=c//i
return res>=k
l,r=1,max(candies) #不设为0,max(candies)的原因是防止mid=(0+1)//2=0,导致check函数的除数为0
ans=0
while l<=r:
mid=(l+r)//2
if check(mid):
ans=mid
l=mid+1
else:
r=mid-1
return ans