leetcode刷题笔记——二分查找
目前完成的贪心相关的leetcode算法题序号:
中等:80,81
困难:4
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reconstruct-itinerary
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
文章目录
算法理解
二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为n的数组,二分查找的时间复杂度为O(log n)。
具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。
这里提供两个小诀窍:
1)第一是尝试熟练使用一种写法,比如左闭右开(满足C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;
2)第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。
二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。
一、33题:搜索选择排序数组
1.题干
升序排列的整数数组 nums 在预先未知的某个点上进行了旋转(例如, [0,1,2,4,5,6,7] 经旋转后可能变为 [4,5,6,7,0,1,2] )。
请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
说明:
nums 中的每个值都 独一无二
nums 肯定会在某个点上旋转
2.思路
本题给出条件,nums中不存在重复值。
数组经过旋转,说明数组是两段升序数组构成的,因此每次进行二分时,必然有一半是有序数组。
如此一来,就可以通过寻找有序数组,并判断二分的点对应的元素值是否在有序数组中,如果在,将区间更新到有序数组的范围中,如果不在,则将区间更新到另一半的范围中。
解题思路:
1)将数组进行二分,寻找有序数组(认为右区间>=左区间即可能为有序数组);
2)对两部分分别进行判断,如果可能是有序数组,就判断目标值是否在区间中;
3)如果在(由于没有重复值),则将区间更新为该数组的范围,如果不在更新到另一半数组区间;
3.代码
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)-1
#初始化区间为数组的整个区间
while right >= left:
#注意二分法的循环终止条件,需要与循环和的判断条件进行配合
mid = int((left + right) / 2)
#如果找到目标值,则返回索引
if nums[mid] == target:
return mid
#如果二分后的左半区间可能有序,则判断目标值是否在区间内,在则将右边界更新为mid-1
#不在则说明目标值如果存在数组,也只能在右半区间,将左边界更新为mid+1
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
#右半区间的处理逻辑与左半区间对称
elif nums[right] >= nums[mid]:
if nums[mid] < target <= nums[right]:
left = mid+1
else:
right = mid - 1
#二分循环结束的情况有2种:
#1)找到了目标值,这是循环直接返回索引结果,不需要后续处理;
#2)right < left,二分法最终也没能够找到目标值,需要进行判断,返回-1
if right < left:
#right < left还能够同时覆盖数组长度为0的情况
return -1
return None
4. 问题点思考
二分法的问题往往在于区间的边界设置和判断条件的匹配上,配置的不好往往会漏解,甚至陷入死循环。
采取的策略:
1)建立一套自己习惯的边界设置方法和判断条件,熟悉这套设置方法下的特性;
2)根据实际情况在此基础上进行调整;
二、81题:搜索旋转排序数组II
1.题干
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
数组中可能包含重复元素
2.解题思路
这题是33题的进阶问题,注意增加的问题点就是,数组中可能存在重复元素。
33题解法在数组中出现重复元素情况下,会遇到一个问题:
如果重复元素刚好出现在二分之后,二分点的元素与左边界/右边界的元素重复,则就不能够有效判断出两半部分是否为有序数组
针对这个问题对33题的解法进行改进:
1)二分后,判断完二分点的元素不是目标值之后,对当前区间的左边界和右边界进行检测;
2)如果该元素与二分点的元素是重复值,则将重复值去除;
3)直到左右边界的值不等于二分点的元素值,或者左右边界的索引达到了二分点的位置;
3.代码
class Solution:
def search(self, nums: List[int], target: int) -> bool:
left, right = 0, len(nums) - 1
while right >= left:
mid = int((left + right) / 2)
if nums[mid] == target:
return True
#33题算法的改进:
# 1)如果检测到左边界与二分点的元素值重复,左边界右移,直到左边界取值与二分点不等,或者左边界到达二分点位置
# 2)如果检测到右边界与二分点的元素值重复,右边界左移,直到右边界取值与二分点不等,或者右边界到达二分点位置
while nums[mid] == nums[left] and left < mid:
left += 1
while nums[mid] == nums[right] and mid < right:
right -= 1
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
elif nums[mid] <= nums[right]:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
if right < left:
return False
4. 注意点
三、4题:寻找两个正序数组的中位数
1. 题干
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。
进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?
2. 思路
对两个有序数组寻找中位数,即为查找两个数组的联合分割点,使得分割线两边的元素数量一致:
以长度较小的数组为基准进行二分:
1)由于两个数组的长度已知,因此知道了某个数组的分割点之后,另一个数组的分割点也就能得到;
2)以长度较小的数组为基准,进行二分法划分数组,之后就能确定长度较长的数组的分割点,判断两个数组在分割点处是否满足交叉小于等于条件,如果不满足,做相应调整(左边最大值大于右边最小值,则小数组的分割点需要左移),直至找到正确的分割点;
3)找到分割点之后,还需要根据分割点的位置,考虑几种特殊情况下的处理方法;
3. 代码
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
#将较小的数组作为nums1,以nums1为基准进行二分法
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m = len(nums1)
n = len(nums2)
#记录分割点左边需要包含的元素数量:
#如果两个数组的总长度为奇数,则分割线左边要比右边多一个元素,最终的中位数即为分割后左边的最大值
#如果两个数组的总长度为偶数,则分割线两边的元素数量相等,最终的中位数为左边最大值和右边最小值的均值
totalLen = (m + n + 1) // 2
#left和right表示的是小数组在分割线左右两边的元素数量
left, right = 0, len(nums1)
#分割线位置需要满足的条件:
#左边的最大值 <= 右边的最小值,即nums1[i-1] <= nums2[j]或者nums1[i] >= nums2[j-1]
#当left == right时,即为找到了满足条件的分割点
while left < right:
i = left + (right - left + 1) // 2
j = totalLen - i
if nums1[i-1] > nums2[j]:
right = i - 1
else:
left = i
i = left
j = totalLen - i
#如果两个数组的分割线中,有一个左边没有元素(不会同时为0),则分割后的整体的左边最大值就是另一个数组的左边的最大值
if i == 0:
leftmax = nums2[j-1]
elif j == 0:
leftmax = nums1[i-1]
else:
leftmax = max(nums1[i-1], nums2[j-1])
#如果两个数组的分割线中,有一个右边没有元素(有可能出现右边都没有元素的情况,数组长度为0和1时)
#因此这里需要特殊处理一下,利用mid值规避划分后右边都没有元素的情况
mid = int(-1*(m+n-1)//2)
if i == len(nums1):
rightmin = nums2[mid]
elif j == len(nums2):
rightmin = nums1[mid]
else:
rightmin = min(nums1[i], nums2[j])
#判断数组总长的奇偶,为奇数则中位数是分割线左边的最大值,偶数则为分割线左边最大值和右边最小值的平均值
if (m + n) % 2 == 1:
return float(leftmax)
else:
return (leftmax + rightmin) / 2
二分法的难点一致都是细节,如何确定循环条件,如何确定索引计算方法以防止超出列表的索引范围等等
四、牛客网——牛牛打气球
1. 题干
有n个气球,每个气球都有一个坚韧度,牛牛有一把全屏武器,可以使每一个气球的坚韧度都下降b(坚韧度不会为负数),特别的:每次释放武器的时候,牛牛可以选择一个气球,使得这个气球多承受a点伤害。
牛牛想知道,最少释放几次武器,可以使得所有气球的坚韧度都变成0呢?
2. 解题思路
设定一个足够大的武器释放次数的上下边界,二分判断该范围内,各个数是否能够满足条件,直到范围压缩到1,即得结果。
3. 代码
import math
def check(n, nums, a, b):
rest = 0
weaken = n * b
for val in nums:
#注意:这里需要,每次二分法遍历的数,经过基础全屏攻击之后,打掉各个气球所需要的剩余次数(额外打击次数)
#不能简单求和后,再与n*a比较,因为这样计算得到的就不是打掉各个气球所需要的剩余次数之和
#可能出现不同气球剩余次数互相补充的情况,一定要严谨
rest += math.ceil(max(0, val - weaken) / a)
return rest <= n
if __name__ == "__main__":
n, a, b = [int(x) for x in input().strip().split()]
nums = [int(x) for x in input().strip().split()]
max_n = max(nums)
low, high = 1, 10 ** 9
while low < high:
mid = low + (high - low) // 2
if check(mid, nums, a, b):
high = mid
else:
low = mid + 1
print(low)