题目:34. 在排序数组中查找元素的第一个和最后一个位置
解法:
获取目标值 target 的左端点索引,并将该值赋值给 start ,获取 (target + 1) 的左端点索引 - 1,并将该值赋值给end,[start,end]也即目标 target 在数组(列表)中的索引区间。如果 start 存在,end 就一定存在,返回 [start, end] 。当 start == len(nums) or nums[start] != target ,说明数组中不存在目标值 target ,返回 [-1,-1] 即可
形式变换:
题目中求 ≥ target 的切片区间,当进行需求变更,有如下三种方式: > target 可转化为 ≥ (target + 1), < target 可转化为 (≥ target) - 1, ≤ target 可转化为 (≥ (target + 1)) - 1
也即:
数组元素为整数的情况下,求解不同边界有如下四种情况,情况2、3、4均可以看作情况1的变形:
情况 1、求 '≥ target' 的左边界,按照lower_bound(self, nums, target)进行搜寻即可
情况 2、求 '> target' 的左边界,相当于求'≥ (target + 1)'的左边界,按照lower_bound(self, nums, target + 1)进行搜寻即可
情况 3、求'< target' 的右边界,相当于求'≥ target'的左边界,-1 就是<target的右边界,按照 lower_bound(self, nums, target) - 1 进行搜寻即可
情况 4、求'≤ target' 的右边界,相当于求'≥ (target + 1)'的左边界,-1 就是≤target的右边界,按照lower_bound(self, nums, target + 1) - 1
时间复杂度:O(log n)
空间复杂度:O(1)
代码实现:
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
start = self.lower_bound4(nums, target)
if start == len(nums) or nums[start] != target: # nums[start] != target 这个条件该怎么理解
return [-1, -1]
end = self.lower_bound4(nums, target + 1) - 1
return [start, end]
# 方法一:闭区间 [left, right]
def lower_bound(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 闭区间 [left, right]
while left <= right:
mid = (left + right) // 2 # 向下取整
if nums[mid] >= target:
right = mid - 1 # [left, mid - 1]
else:
left = mid + 1 # [mid + 1, right]
return right + 1 # 此时 right - 1 = left, 返回left 结果一样
# 方法二:开区间 (left, right)
def lower_bound2(self, nums: List[int], target: int) -> int:
left, right = -1, len(nums) # 开区间 (left, right)
while left + 1 < right:
mid = (left + right) // 2 # 向下取整
if nums[mid] >= target:
right = mid # (left, mid)
else:
left = mid # (mid, right)
return right # 或 left + 1
# 方法三:左闭右开区间:[left, right)
def lower_bound3(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) # 开区间 [left, right)
while left < right:
mid = (left + right) // 2 # 向下取整
if nums[mid] >= target:
right = mid # [left, mid)
else:
left = mid + 1 # [mid + 1, right)
return right # 循环终止条件 left == right, 返回left 或 right均可
# 方法四:左开右闭区间:(left, right]
def lower_bound4(self, nums: List[int], target: int) -> int:
left, right = -1, len(nums) - 1 # 左开右闭区间 (left, right]
while left < right:
mid = (left + right + 1) // 2 # 向下取整 -> 向上取整
if nums[mid] >= target:
right = mid - 1# (left, mid - 1]
else:
left = mid # (mid, right]
return right + 1 # 循环终止条件 left + 1 == right, 返回left + 1 或 right均可
题目:2529. 正整数和负整数的最大计数
解法:
1、负整数区间需要求解 <0 的右边界,转换为:求解≥0的左边界 - 1(如情况3),也即:lower_bound(self, nums, target) - 1
2、正整数区间需要求解 ≥1 的左边界,带入函数即可,也即:lower_bound(self, nums, 1)
也即:
数组元素为整数的情况下,求解不同边界可以遵循如下规则:
情况 1、求 '≥ target' 的左边界,按照lower_bound(self, nums, target)进行搜寻即可
情况 2、求 '> target' 的左边界,相当于求'≥ (target + 1)'的左边界,按照lower_bound(self, nums, target + 1)进行搜寻即可
情况 3、求'< target' 的右边界,相当于求'≥ target'的左边界,-1 就是<target的右边界,按照 lower_bound(self, nums, target) - 1 进行搜寻即可
情况 4、求'≤ target' 的右边界,相当于求'≥ (target + 1)'的左边界,-1 就是≤target的右边界,按照lower_bound(self, nums, target + 1) - 1
时间复杂度:O(log n)
空间复杂度:O(1)
代码实现:基础写法要会,但更要关注二分法!
class Solution:
def maximumCount(self, nums: List[int]) -> int:
# 正整数区间:找到 >0 的位置,[right, len(nums)]
left_bound_index = self.lower_bound(nums, 1)
left_value = len(nums) - left_bound_index
# 负整数区间:找到 <0 的位置,[-1, left]
right_bound_index = self.lower_bound(nums, 0) - 1
right_value = right_bound_index + 1
return max(left_value, right_value)
def lower_bound(self, nums: List[int], target) -> int:
left, right = 0, len(nums) - 1 # 闭区间 [left, right]
while left <= right:
mid = (left + right) // 2 # 向下取整
if nums[mid] >= target:
right = mid - 1
else: # nums[mid] < target
left = mid + 1
return right + 1 # 终止条件 right < left,此时返回left 或 right + 1均可
暴力解法:
class Solution:
def maximumCount(self, nums: List[int]) -> int:
# 负整数 求右边界 right
# 正整数 求左边界 left
right = cnt0 = 0
left = len(nums) - 1
for i in range(len(nums)):
if nums[i] < 0:
right = i
elif nums[i] == 0:
cnt0 += 1
else:
left = i
break
if cnt0 == len(nums):
return 0
return max(right + 1, len(nums) - left)
题目:
解法:
时间复杂度:O(n log n),接近10^6,时间复杂度测评一般内置是10^7~10^8.
空间复杂度:O(1)
代码实现:
# 方法一:暴力解法,超时
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
# 两层for 循环
pairs = [0] * len(spells)
for i, x in enumerate(spells):
cnt = 0
for j, y in enumerate(potions):
if x * y >= success:
cnt += 1
pairs[i] = cnt
return pairs
# 方法二:for 循环 + 二分法(前提是排好序)
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
# 整体思路:for循环 + 二分查找
# 定义数组 pairs
pairs = [0] * len(spells)
# 数组排序,二分查找的前提
potions.sort()
for i, x in enumerate(spells):
# 二分查找 potions,找到符合条件的左边界 right + 1 / left
left, right = 0, len(potions) - 1 # 闭区间 [left, right]
while left <= right:
mid = (left + right) // 2
if x * potions[mid] >= success:
right = mid - 1
else:
left = mid + 1
# 左边界 right + 1 / left
pairs[i] = len(potions) - (right + 1)
return pairs
题目:1385. 两个数组间的距离值
解法:
暴力:略
for循环遍历 + 二分查找(推荐)。问题是|arr1[i]-arr2[j]| <= d ,可转化为arr2[j] 是否存在在区间[arr1[i] - d, arr1[j] + d],不存在ans += 1,获取区间 ≥ arr1[i] - d的最小值y,如果不存在该最小值i == len(arr2),或者最小值y 大于区间最大值 arr[i] + d, 则结果进行 +1
# 思路:for循环 + 二分查找(排序)
# 问题:|arr1[i]-arr2[j]| <= d
# 转化为:arr2[j] 是否存在∈ [arr1[i] - d, arr1[i] + d],如果不存在,符合题意 ans += 1
时间复杂度:O(n log m)
空间复杂度:O(1)
代码实现:
# 方法一:暴力解法,两层for循环
class Solution:
def findTheDistanceValue(self, arr1: List[int], arr2: List[int], d: int) -> int:
# 暴力解法,两层for循环
cnt = 0
for i, x in enumerate(arr1):
flag = 0
for j, y in enumerate(arr2):
if abs(x - y) <= d:
flag = 1
break
if flag == 0:
cnt += 1
else:
continue
return cnt
# 方法二:for循环 + 二分查找 (推荐)
class Solution:
def findTheDistanceValue(self, arr1: List[int], arr2: List[int], d: int) -> int:
# for循环 + 二分查找(排序)
# 问题:|arr1[i]-arr2[j]| <= d
# 转化为:arr2[j] 是否存在∈ [arr1[i] - d, arr1[i] + d],如果不存在,符合题意 ans += 1
arr2.sort()
ans = 0
for x in arr1:
# 获取arr2中arr1[i] - d,也即 x-d 的最小值的左边界
i = bisect_left(arr2, x - d)
if i == len(arr2) or arr2[i] > x + d:
ans += 1
return ans
题目:2080. 区间内查询数字的频率
解法:
1、创建空字典,底层是列表的形式
2、遍历将元素值作为key,索引作为value存储到字典中,遍历是有序的,则每个元素对应的索引也是有序的(二分查找的前提)
3、在query()中获取value的列表,返回元素为value的在[left, right]的元素个数,
bisect_right 筛选的是>value时的下标,
bisect_left 筛选的是 ≥value的左边界的索引,
两个索引之差即为value值在[left,right]区间出现的次数
时间复杂度:O(log n)
空间复杂度:O(1)
代码实现:
class RangeFreqQuery:
def __init__(self, arr: List[int]):
pos = defaultdict(list) # 创建空字典,内部是列表的形式
for i, x in enumerate(arr):
pos[x].append(i) # 将元素值为x的索引下标存储到pos中,由于按顺序遍历,索引列表天然有序(这是二分查找的前提!)
self.pos = pos # # self.pos 属于对象实例,每个对象有自己的 pos
def query(self, left: int, right: int, value: int) -> int:
a = self.pos[value] # 获取值为value的列表a,a中存储着在arr中的所有索引(有序);如果value不存在,返回空列表[]
# 求value 在区间[left,right]的索引个数
# bisect_left(a, left) → 第一个 >= left 的位置
# bisect_right(a, right) → 第一个 > right 的位置
# 两者相减 = 索引在 [left, right] 范围内的个数
return bisect_right(a, right) - bisect_left(a, left)
# Your RangeFreqQuery object will be instantiated and called as such:
# obj = RangeFreqQuery(arr)
# param_1 = obj.query(left,right,value)
题目:2563. 统计公平数对的数目
解法:for循环 + 二分查找(提前排序,且排序不影响结果)
时间复杂度:O(nlog n)
空间复杂度:O(1)
代码实现:
# for循环 + 二分查找
class Solution:
def countFairPairs(self, nums: List[int], lower: int, upper: int) -> int:
# 排序不影响结果,加法满足交换定律
# for + 二分查找
ans = 0
nums.sort()
n = len(nums)
for i in range(n):
x = nums[i]
j = i + 1
ans += bisect_right(nums, upper - x, j) - bisect_left(nums, lower - x, j)
return ans
方法一:暴力解法,超时,(10^5)^2,远大于平台每秒执行的时间复杂度能力:10^7~10^8。
class Solution:
def countFairPairs(self, nums: List[int], lower: int, upper: int) -> int:
ans = 0
for i in range(len(nums) - 1):
x = nums[i]
for j in range(i + 1, len(nums)):
y = nums[j]
s = x + y
if s >= lower and s <= upper:
ans += 1
return ans
题目:875. 爱吃香蕉的珂珂
解法:
AI释义:www.qianwen.com/share/chat/…
时间复杂度:O(log n)
空间复杂度:O(1)
代码实现:
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
n = len(piles)
left = 0 # 恒为 False
right = max(piles) # 恒为 True
while left + 1 < right: # 开区间不为空
mid = (left + right) // 2
if sum((p - 1) // mid for p in piles) <= h - n:
right = mid # 循环不变量:恒为 True
else:
left = mid # 循环不变量:恒为 False
return right
题目:2187. 完成旅途的最少时间
解法:见代码实现
时间复杂度:O(log n)
空间复杂度:O(1)
代码实现:
class Solution:
def minimumTime(self, time: List[int], totalTrips: int) -> int:
min_t = min(time)
left = min_t - 1 # 循环不变量:sum >= totalTrips 恒为 False
right = min_t * totalTrips # 循环不变量:sum >= totalTrips 恒为 True
while left + 1 < right: # 开区间 (left, right) 不为空
mid = (left + right) // 2
if sum(mid // t for t in time) >= totalTrips:
right = mid # 缩小二分区间为 (left, mid)
else:
left = mid # 缩小二分区间为 (mid, right)
# 终止循环,此时 left 等于 right - 1
# sum(left) < totalTrips 且 sum(right) >= totalTrips
return right
题目:275. H 指数 II
解法:见代码实现
时间复杂度:O(log n)
空间复杂度:O(1)
代码实现:
class Solution:
def hIndex(self, citations: List[int]) -> int:
# 在开区间 (left, right) 内问询
left = 0 # 恒为 True
right = len(citations) + 1 # 恒为 False
while left + 1 < right: # 区间不为空
# 循环不变量:
# left 的回答一定为:是
# right 的回答一定为:否
mid = (left + right) // 2
# 引用次数最多的 mid 篇论文,引用次数均 >= mid
if citations[-mid] >= mid:
left = mid # 询问范围缩小到 (mid, right)
else:
right = mid # 询问范围缩小到 (left, mid)
# 根据循环不变量,left 现在是最大的回答为 是 的数
return left
题目:2861. 最大合金数
解法:见代码实现
时间复杂度:O(nklog n)
空间复杂度:O(1)
代码实现:
class Solution:
def maxNumberOfAlloys(self, n: int, k: int, budget: int, composition: List[List[int]], stock: List[int], cost: List[int]) -> int:
# 二分搜索边界:left最小1个,right最大2×10^8个,ans记录答案
left, right, ans = 1, 2 * 10 ** 8, 0
# 闭区间二分搜索,left>right时结束
while left <= right:
# 当前尝试制造的合金数量
mid = (left + right) // 2
# 标记是否存在机器能在预算内制造mid个合金
valid = False
# 遍历每台机器,检查是否可行
for i in range(k):
# 当前机器制造mid个合金的总花费
spend = 0
# 遍历每种金属,计算花费
for j, (composition_j, stock_j, cost_j) in enumerate(zip(composition[i], stock, cost)):
# 花费 += 需购买数量 × 单价(库存足够则不买)
spend += max(composition_j * mid - stock_j, 0) * cost_j
# 当前机器花费不超过预算,说明可行
if spend <= budget:
valid = True
break # 找到一台可行机器即可,无需继续检查 # 只要有一台机器i,满足制造mid个合金即可,不需要要几个机器的数量
# 根据可行性调整搜索范围
if valid:
ans = mid # 更新答案
left = mid + 1 # 尝试更多合金
else:
right = mid - 1 # 减少合金数量
# 返回最大可制造合金数
return ans
题目:2439. 最小化数组中的最大值
解法:见代码实现
时间复杂度:O(log n)
空间复杂度:O(1)
代码实现:
class Solution:
def minimizeArrayValue(self, nums: List[int]) -> int:
'''
题目核心规则:
- 只能将 nums[i] 的值转移给 nums[i - 1] (从右向左)
- 不能将 nums[i-1] 的值转移给 nums[i] (从左向右)
- 目标:让数组中最大值尽可能小
'''
# 方法一:二分搜索 + 检查函数
def check(limit: int) -> bool:
'''
检查是否能让所有元素都不超过 limit
思路:从右向左遍历,把超过limit的值向左转移
'''
extra = 0 # 需要向左转移的多余值
for i in range(len(nums) - 1, -1, -1): # 从倒数第二个元素开始,从右向左遍历
current = nums[i] + extra # 当前位置的实际值(包含从右边转移过来的)
if current > limit: # 超过 limit, 多出的部分向左转移
extra = current - limit
else:
extra = 0 # 没超过,不需要转移
return extra == 0
# 二分搜索框架
left, right = 0, max(nums) # 答案范围:0 到 最大值
while left <= right:
mid = (left + right) // 2
if check(mid): # mid 可行,尝试更小的值
right = mid - 1
else: # mid 不可行,需要更大的值
left = mid + 1
return right
class Solution:
def minimizeArrayValue(self, nums: List[int]) -> int:
'''
贪心解法:前缀和平均值
核心思想L:
- 前 i 个元素的总和是固定的(只能从右向左转移)
- 前 i 个元素的平均值就是这部分的理论最小值
- 答案 = 所有前缀平均值中的最大值
'''
ans = 0
prefix_sum = 0
for i, num in enumerate(nums):
prefix_sum += num # 累加前缀和
# 前i+1个元素的平均值(向上取整)
# (prefix_sum + i) // (i + 1) 等价于 ceil(prefix_sum / (i + 1))
avg = (prefix_sum + i) // (i + 1)
# 更新答案
ans = max(ans, avg)
return ans
题目:2517. 礼盒的最大甜蜜度
解法:见代码实现
时间复杂度:O(nlogn+nlogC),其中 n 是数组 price 的长度,C 是数组 price 的最大值与最小值之差。排序的时间是 O(nlogn),二分查找的次数是 O(logC),每次查找的时间是 O(n)。
空间复杂度:O(logn),其中 n 是数组 price 的长度。为排序的空间复杂度。
代码实现:
class Solution:
def maximumTastiness(self, price: List[int], k: int) -> int:
def f(d: int) -> int:
cnt = 1 # 计数器, 至少选第一个糖果
pre = price[0] # 第一个价格(甜度),赋值给pre 记录上一个选中的糖果价格(初始化第一个)
for p in price: # 遍历所有糖果价格
if p - pre >= d: # 如果当前糖果与上一个选中的糖果价格差 >= 目标甜蜜度 d
cnt += 1 # 可以选当前糖果
pre = p # 更新上一个选中的糖果价格
return cnt # 返回能选出的糖果总数
price.sort() # 第一步:对价格数组排序,排序后便于贪心选择,从小到大依次考虑
left = 0 # 第二步:确定二分搜索的边界 ,left为最小可能的甜蜜度(0 表示可以选价格相同的糖果)
right = (price[-1] - price[0]) // (k - 1) # 最大可能的甜蜜度上界,总间隔为 right
while left <= right: # 第三步:二分搜索寻找最大可行甜蜜度
mid = (left + right) // 2 # 计算中间值作为当前尝试的甜蜜度
if f(mid) >= k: # 检查甜蜜度为 mid 的甜蜜度时,能否选出至少 k 个糖果
left = mid + 1 # 可行:当前甜蜜度可以达到,尝试更大的甜蜜度
else:
right = mid - 1 # 不可行,当前甜蜜度太高,需要降低
return right # 返回最大甜蜜度,循环结束时,right 是最后一个满足 f(right) >= k 的值