参考:labuladong的算法小抄https://labuladong.online/algo/essential-technique/binary-search-framework/
1. 二分搜索框架
def binarySearch(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = left + (right-left)//2 # 这里和(left+right)//2的效果一样,但可以防止溢出
if (nums[mid] == target):
...
elif (nums[mid] < target):
left = ...
elif (nums[mid] > target):
right = ...
return ...
二分查找的技巧一:不要有else,把所有的可能性都用else if列举出来。
2. 基本的二分搜索:寻找一个数
寻找一个数,如果存在返回其索引,否则返回-1
def binarySearch(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = left + (right-left)//2 # 这里和(left+right)//2的效果一样,但可以防止溢出
if (nums[mid] == target):
return mid
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
return ...
几个要点
- 循环条件是left <= right,不是left < right.
其实两个都可以,看写法。这种写法下两端都是闭区间,right初始化为len(nums)-1而不是len(nums) - 为什么elif是left = mid + 1而不是mid?(right同理)
因为我们搜索的是闭区间。已知nums[mid] != target, 下一步搜索就要把mid这个位置排除 - 该方式的缺点
当有重复元素的时候,比如[1,2,2,2,3],target = 2,我想得到左侧边界的索引1,或者右侧边界的索引3,就无法做到
3. 寻找左侧边界的二分搜索
def binarySearch(nums, target):
left, right = 0, len(target) #注意
while (left < right):
mid = left + (left+right)//2
if (nums[mid] == nums[target]):
right = mid #注意
elif (nums[mid] < nums[target]):
left = mid + 1
elif (nums[mid] > nums[target]):
right = mid #注意
if (left < 0 or left > len(nums)-1): # left<0可以不要,但这里都写上,形式更统一,也更方便记忆
return -1
# 注意返回时判断是否相等
if (nums[left] == target):
return left
else:
return -1
- 这个方法是左闭右开区间,所以right初始化为len(nums), 二分搜索是right = mid
- 为什么能搜索到左侧边界:找到 target 时不会立即返回,而是缩小「搜索区间」的上界 right
- 为什么返回left而不是right?
都是一样的,因为while的终止条件是left == right. 可以这么记:因为是搜索左侧边界所以返回left。
下面尝试统一写法,写成左右都闭区间的形式
def binarySearch(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right-left)//2
if (nums[mid] == target):
right = mid - 1
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
if (left < 0 or left > len(nums)-1): # left<0可以不要,但这里都写上,形式更统一,也更方便记忆
return -1
if (nums[left] == target):
return left
else:
return -1
3. 寻找右侧边界的二分搜索
直接套用公式。因为是右侧边界最终返回right即可
def binarySearch(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = left + (right-left)//2
if nums[mid] == target:
left = mid + 1
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
if right < 0 or right > len(nums)-1:
return -1
if nums[right] == target:
return right
else:
return -1
leetcode实战
leetcode 34 在排序数组中查找元素的第一个和最后一个位置
这道题是需要我们寻找target的左侧边界和右侧边界。最直观的解法,写两个函数lowerBound和upperBounder, 分别计算。
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
start = self.lowerBound(nums, target)
end = self.upperBound(nums, target)
return [start, end]
def lowerBound(self, nums, target):
left, right = 0, len(nums)-1
while (left <= right):
mid = left + (right-left)//2
if (nums[mid] == target):
right = mid - 1
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
if left < 0 or left > len(nums) - 1:
return -1
if nums[left] == target:
return left
else:
return -1
def upperBound(self, nums, target):
left, right = 0, len(nums)-1
while (left <= right):
mid = left + (right-left)//2
if (nums[mid] == target):
left = mid + 1
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
if right < 0 or right > len(nums) - 1:
return -1
if nums[right] == target:
return right
else:
return -1
但是这种写法比较冗余,可以只写一个lowerBound. 返回参数只返回left,不判断其他情况。此时upperBound的含义就变了,它的作用是:
- 如果target在nums中存在,则返回target下界的index;
- 如果target在nums中不存在,返回的是第一个大于target的数字的index;
(如果target比nums中所有元素都大,则返回len(nums))
在主函数中判断,如果下界存在,则上界也必存在,再调用upperBound(nums, target+1)计算出target+1的下界index,再-1就是target的上界. 完整代码如下:
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
start = self.lowerBound(nums, target)
if start >= 0 and start <= len(nums) - 1 and nums[start] == target:
end = self.lowerBound(nums, target+1)-1
return [start, end]
else:
return [-1, -1]
def lowerBound(self, nums, target):
left, right = 0, len(nums)-1
while (left <= right):
mid = left + (right-left)//2
if (nums[mid] == target):
right = mid - 1
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
return left
看了题解区另一位老哥的解法,简单粗暴
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
lo, hi = 0, len(nums)-1
res = [-1, -1]
# 找第一个等于target的位置
while lo <= hi:
mid = lo + (hi-lo)//2
if nums[mid] < target:
lo = mid + 1
elif nums[mid] > target:
hi = mid - 1
else:
hi = mid - 1
res[0] = mid # 重点
lo, hi = 0, len(nums)-1
// 找最后一个等于target的位置
while lo <= hi:
mid = lo + (hi-lo)//2
if nums[mid] < target:
lo = mid + 1
elif nums[mid] > target:
hi = mid - 1
else:
lo = mid + 1
res[1] = mid # 重点
return res
leetcode704 二分查找
https://leetcode.cn/problems/binary-search/
没啥好说的
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
left, right = 0, len(nums)-1
while (left <= right):
mid = left + (right-left)//2
if (nums[mid] == target):
return mid
elif (nums[mid] < target):
left = mid + 1
elif (nums[mid] > target):
right = mid - 1
return -1
最长递增子序列
力扣300题:https://leetcode.cn/problems/longest-increasing-subsequence/description/
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
这道题的常见解法是动态规划,O(n^2); 用贪心+二分查找的方式,可以达到时间复杂度O(nlogn).
核心思路参考动态规划的文章,这里贴一下代码:
class Solution(object):
def lengthOfLIS(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
def upperBound(arr, target):
lo, hi = 0, len(arr)-1
while lo <= hi:
mid = lo + (hi-lo)//2
if arr[mid] < target:
lo = mid + 1
elif arr[mid] > target:
hi = mid - 1
elif arr[mid] == target:
hi = mid - 1
return lo
d = []
for n in nums:
if not d or n > d[-1]:
d.append(n)
else:
idx = upperBound(d, n)
# 一般来说,当n>d[-1]时,idx = len(d),会越界;但上面if已经判断了n>d[-1],这里不会出现越界的情况
d[idx] = n
return len(d)
二分查找的应用:吃香蕉
class Solution(object):
def minEatingSpeed(self, piles, h):
"""
:type piles: List[int]
:type h: int
:rtype: int
"""
def helper(piles, k):
hour = 0
for i in range(len(piles)):
hour += piles[i]//k
if piles[i]%k > 0:
hour += 1
return hour
# 注意lo和hi的起始位置
lo, hi = 1, max(piles)
res = 0
# 注意不用新建一个数组,否则会内存超限
while lo <= hi:
mid = lo + (hi-lo)//2
if helper(piles, mid) < h:
hi = mid - 1
elif helper(piles, mid) > h:
lo = mid + 1
elif helper(piles, mid) == h:
hi = mid - 1
res = lo
return res