leetcode(16)二分查找系列题目
基本框架:
704. 二分查找
两种写法
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
l, r = 0, n - 1
while l <= r:
mid = (l + r) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
l = mid + 1
else:
r = mid - 1
return -1
或者
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
l, r = 0, n # r 初始化为 n
while l < r: # <
mid = (l + r) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
l = mid + 1
else:
r = mid # 不用- 1
return -1
35. 搜索插入位置
两种写法
while left <= right:时
return left
return right + 1都可以
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) //2
if target == nums[mid]:
return mid
elif target > nums[mid]:
left = mid + 1
else:
right = mid - 1
# return left
return right + 1
或者while left < right:时
return right
return left都可以
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) //2
if target == nums[mid]:
return mid
elif target > nums[mid]:
left = mid + 1
else:
right = mid
return right
# return left
34. 在排序数组中查找元素的第一个和最后一个位置
先找到target的idx,然后左右滑动指针,来找到符合题意的区间
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if target == nums[mid]:
return mid
elif target > nums[mid]:
left = mid + 1
else:
right = mid - 1
return -1
idx = search(nums, target)
if idx == -1:
return [-1, -1]
left, right = idx, idx
while left - 1 >= 0 and nums[left - 1] == target: # 向左滑动,找左边界
left -= 1
while right + 1 < len(nums) and nums[right + 1] == target: # 向右滑动,找右边界
right += 1
return [left, right]
69. x 的平方根
return left - 1
return right都可以
class Solution:
def mySqrt(self, x: int) -> int:
left, right = 0, x
while left <= right:
mid = (left + right) // 2
if mid * mid == x:
return mid
elif mid * mid < x:
left = mid + 1
else:
right = mid - 1
return right # 没找到则返回最大的一个平方小于x的即退出循环的right
# return left - 1
367. 有效的完全平方数
class Solution:
def isPerfectSquare(self, num: int) -> bool:
left, right = 0, num
while left <= right:
mid = left + (right - left) // 2
if mid * mid == num:
return True
elif mid * mid < num:
left = mid + 1
else:
right = mid - 1
return False
162. 寻找峰值
通过二分搜索,且选择前或者后对比一个,最终肯定能够找到峰值,因为题目给了一些前提条件:
- 对于所有有效的 i 都有 nums[i] != nums[i + 1]
- 也就是说相邻的数字都是不等的
- 题目条件给出 nums[-1] = nums[n] = -∞
- 这个条件很关键,当只有一个元素的时候,它自己本身就是峰值
- 当有多个元素的时候,如果单调递增,那最后一个元素是峰值,因为 nums[n] = -∞
- 当有多个元素的时候,如果单调递减,那第一个元素是峰值,因为 nums[-1] = -∞
- 当有多个元素的时候,不是单调区间,而且题目说了相邻元素不相等,一定能够在 [0, n-1] 区间内找到峰值的
二分法找最大值,只能while left < right:
举例子[1,2,1],[2,2,1]
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
n = len(nums)
left, right = 0, n - 1
# 在这里再减去 1,主要是为了避免 nums[mid+1] 数组越界,而且又不妨碍最终的答案落在 len(nums) - 1 上
# 巧妙至极
while left < right:
# mid = (left + right) // 2
mid = left + (right - left) // 2
if nums[mid] > nums[mid + 1]:
# 最后比较的mid和mid + 1其实就是left和right
right = mid
# 移动右边界,最右边就是最大值
else:
left = mid + 1
# 移动左边界,最左边就是最大值
return left
633. 平方数之和
class Solution:
def judgeSquareSum(self, c: int) -> bool:
i = 0
while i*i <= c:
i += 1
left, right = 0, i
while left <= right:
sum_ =left*left + right*right
if sum_ == c:
return True
elif sum_ < c:
left += 1
else:
right -= 1
return False
# 暴力法
class Solution:
def judgeSquareSum(self, c: int) -> bool:
nums = set()
i = 0
while i*i <= c:
nums.add(i*i)
i += 1
# print(nums)
for n in nums:
if (c - n) in nums:
return True
return False
33. 搜索旋转排序数组
先判断左边有序且target在左边,则收缩右边界
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if target == nums[mid]:
return mid
elif nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
153. 寻找旋转排序数组中的最小值
2023.06.08腾讯一面
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[right]
154. 寻找旋转排序数组中的最小值 II
2023.06.08腾讯一面
有相同元素就right -= 1
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
elif nums[mid] < nums[right]:
right = mid
else:
right -= 1
return nums[right]
6096. 咒语和药水的成功对数
先排序,再用二分法
类似于找到success插入potions[j] * spells[i]的位置,但返回的是m - left
,即插入位置后面的元素个数
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
n, m = len(spells), len(potions)
potions.sort()
res = [0] * n
for i in range(n):
left, right = 0, m - 1
while left <= right:
mid = left + (right - left) //2
if potions[mid] * spells[i] >= success:
right = mid - 1
else:
left = mid + 1
res[i] = m - left
return res
或者用除法先向上取整,再调用二分查找的库函数bisect.bisect_left()
这题不向上取整好像也可以,直接t = success / spells[i]
bisect_left函数是新元素会被放置于它相等的元素的前面,而 bisect_right返回的则是跟它相等的元素之后的位置。
for i in range(n):
t = (success + spells[i] - 1) // spells[i]
# t = ceil(success / spells[i])
# t = success // spells[i] + (success % spells[i] != 0)
res[i] = m - bisect.bisect_left(potions, t)
bisect.bisect_right(a,x,lo=0,hi=len(a),*,key=None)
在有序数组a中[lo,hi]区间内查找x插入的位置,返回索引值。如果a中有跟x相同的元素,则x插入的位置是右边。
key 指定一个 key function 用于从每个输入元素提取比较关键字的一个参数的。默认值为 None (直接比较元素)。
1235. 规划兼职工作
- 不选第 i 个工作,那么最大报酬等于前 i-1 个工作的最大报酬(转换成了一个规模更小的子问题);
- 选第 i 个工作,由于工作时间不能重叠,设 j 是最大的满足 \(\textit{endTime}[j]\le\textit{startTime}[i]\) 的 j,那么最大报酬等于前 j 个工作的最大报酬加上 \(\textit{profit}[i]\)(同样转换成了一个规模更小的子问题);
class Solution:
def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int:
jobs = sorted(zip(endTime, startTime, profit))
dp = [0] * (len(jobs) + 1)
for i, (end, start, p) in enumerate(jobs):
# j = bisect_right(jobs, (st, inf), hi = i) 等价于下面一行
j = bisect_right(jobs, start, hi = i, key = lambda x:x[0])
# 查询在jobs中插入(st, inf)的位置因为要找最后一个所以用无穷大
# print(j)
dp[i + 1] = max(dp[i], dp[j] + p)
return dp[-1]
1751. 最多可以参加的会议数目 II
注意:与1235. 规划兼职工作 的区别就是多一维记录参加会议的个数k
定义 f[i][j]表示参加前 i 个会议中的 j 个,能得到的会议价值的最大和。
分类讨论:
- 不参加第 i 个会议:f[i][j] = f[i-1][j];
- 参加第 i 个会议:f[i][j]=f[p][j−1]+value[i],其中 p 是最大的满足 \(\textit{endDay}[p]<\textit{startDay}[i]\) 的 p,不存在时为 -1。
class Solution:
def maxValue(self, events: List[List[int]], k: int) -> int:
events.sort(key = lambda x: x[1])
n = len(events)
dp = [[0] * (k + 1) for _ in range(n + 1)]
for i, (start, end, value) in enumerate(events):
p = bisect_left(events, start, hi = i, key = lambda x: x[1])
for j in range(1, k + 1):
dp[i + 1][j] = max(dp[i][j], dp[p][j - 1] + value)
return dp[-1][-1]
2008. 出租车的最大盈利
定义 f[i]表示行驶到 i 时的最大盈利。
考虑状态转移:
- 一方面,我们可以不接终点为 i 的乘客,这样有 f[i]=f[i-1];
- 另一方面,我们可以接所有终点为 i 的乘客中收益最大的,这样有 f[i]=max(f[start]+i−start+tip),二者取最大值。
class Solution:
def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
rides.sort(key = lambda x: x[1])
m = len(rides)
dp = [0] * (m + 1)
for i, (start, end, tip) in enumerate(rides):
j = bisect_right(rides, start, hi = i, key = lambda x:x[1])
profi = end - start + tip
# 可省略
# if j > 0:
# profi += dp[j]
dp[i + 1] = max(dp[i], profi)
return dp[-1]
410. 分割数组的最大值
首先,为什么可以用二分法:
- 如果对这个数组,每个数单独成一数组,那么子数组的各自和的最大值,就是所有数中的最大值。
- 而如果对这个数组不分组,那么子数组的各自和的最大值就是这个数组的和。
- 这两个值对应的就是 left 和 right,即全分组与不分组的结果。
其次,对于二分的每个mid怎么设置判断条件:
- 假设给定了mid,那么需要判断,如果以mid作为最大值,能形成几组,然后和给定的k值作对比。显然,如果这个mid越大,要分出来的组数越少。
- 如果形成的组数比要求的多,说明这个给定的mid太小了,要扩大,而如果形成的组数太少了,说明给定的mid太大了,要缩小。
- 如果相等,假设给定列表[5,123]和m = 2来找出最大值,假如在二分中选择了124,这样子只能分成两个组,而显然124这个数不是正确答案,正确答案是123,所以相等的时候,查找值mid应该缩小(即找左边界)。
class Solution:
def splitArray(self, nums: List[int], k: int) -> int:
def group(cur):
cnt, tmp = 1, 0 # cnt初值为1
for n in nums:
if tmp + n > cur: # 先加上n
cnt += 1
tmp = n
else:
tmp += n
return cnt
left, right = max(nums), sum(nums)
while left < right:
mid = (left + right) // 2
if group(mid) > k:
left = mid + 1
else:
right = mid
return left
374. 猜数字大小
# The guess API is already defined for you.
# @param num, your guess
# @return -1 if num is higher than the picked number
# 1 if num is lower than the picked number
# otherwise return 0
# def guess(num: int) -> int:
class Solution:
def guessNumber(self, n: int) -> int:
left, right = 1, n
while left < right:
num = (left + right) // 2
if guess(num) == 0:
return num
elif guess(num) == -1:
right = num
else:
left = num + 1
return left
6242. 二叉搜索树最近节点查询
根据二叉搜索树的中序遍历一定是有序列表这一性质,先全部存到一个列表st中
然后用bisect_right()查找每个q的插入位置
自己的思路是分4种情况:
1.q在列表中,则cur_min和cur_max 都是q
但是用if q in st:超时了,改成if q == st[index - 1]:就好了
2.q小于列表最小值
3.q大于列表最大值
4.q被两个数夹着的
class Solution:
def closestNodes(self, root: Optional[TreeNode], queries: List[int]) -> List[List[int]]:
st = []
def inorder(root):
if root:
inorder(root.left)
st.append(root.val)
inorder(root.right)
return st
inorder(root)
# print(st)
res = []
for q in queries:
cur_min, cur_max = -1, -1
index = bisect_right(st, q)
# print(index)
if q == st[index - 1]:
cur_min = cur_max = st[index - 1]
elif index == 0:
cur_max= st[index]
elif index == len(st):
cur_min = st[index - 1]
elif 0 < index < len(st):
cur_min, cur_max = st[index - 1], st[index]
res.append((cur_min, cur_max))
return res
大佬的思路:直接用bisect_right(),bisect_left()分别找最小值最大值的位置
class Solution:
def closestNodes(self, root: Optional[TreeNode], queries: List[int]) -> List[List[int]]:
st = []
def inorder(root):
if root:
inorder(root.left)
st.append(root.val)
inorder(root.right)
return st
inorder(root)
# print(st)
res = []
for q in queries:
i = bisect_right(st, q)
cur_min = st[i - 1] if i > 0 else -1
j = bisect_left(st, q)
cur_max = st[j] if j < len(st) else -1
res.append((cur_min, cur_max))
return res
878. 第 N 个神奇数字
class Solution:
def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
left = min(a, b)
right = n * min(a, b) - 1
c = lcm(a, b)
while left <= right:
mid = (left + right) // 2
cur = mid // a + mid // b - mid // c
if cur >= n:
right = mid - 1
else:
left = mid + 1
return (right + 1) % (10 ** 9 + 7)
1201. 丑数 III
注意这题结果在 **[1, 2 * 10^9] 的范围内,并且是+ num // labc **
class Solution:
def nthUglyNumber(self, n: int, a: int, b: int, c: int) -> int:
left = 1
right = min(n * min(a, b, c), 2 * 10 ** 9)
lab, lac, lbc, labc = lcm(a,b), lcm(a,c), lcm(b,c), lcm(a,b,c)
def check(num):
return num // a + num // b + num // c -\
num // lab - num // lac - num // lbc + num // labc
while left <= right:
mid = (left + right) // 2
cur = check(mid)
if cur >= n:
right = mid - 1
else:
left = mid + 1
return right + 1
参考资料:
bisect 二分查找模块
bisect ---数组平分算法
python中的bisect模块与二分查找详情