前言
本期包括闭区间、左闭右开、左开右开三个区间上的二分查找写法,循环不变量的理解以及查找≥、>、<、≤等内容。
1.闭区间上的二分查找
前提条件,数组排列有序。
核心定义,循环不变量。 根据区间内的元素与target的关系都是未知的,区间外的元素与target的关系都是已知的。
得到定义 left的左边均小于 target,right的右边都是大于等于target的。这句话的代码为
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
# 如果写成下面这样,就是left的左边都是小于等于target的元素, right的右边都是大于target的元素
if nums[mid] > target:
left = mid + 1
else:
right = mid - 1
if 条件会将 大于等于 和 小于 进行分组
下面给出闭区间二分查找的代码。
class Solution:
def search(self, nums: List[int], target: int) -> int:
# [left, right]
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left if left != len(nums) and nums[left] == target else -1
2.左闭右开区间上的二分查找
[left,right)
根据区间内的元素与target的关系都是未知的,区间外的元素与target的关系都是已知的。
得到定义 left的左边均小于 target,right-1的右边都是大于等于target的。故当循环结束时,left和right都是我们要找的答案!
class Solution:
def search(self, nums: List[int], target: int) -> int:
# [left, right)
left, right = 0, len(nums)
# [1,1) 试想一下,这样的区间存在吗? 所以left和right相等循环结束
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left if left != len(nums) and nums[left] == target else -1
3. 左开右开区间上的二分查找
(left,right)
根据区间内的元素与target的关系都是未知的,区间外的元素与target的关系都是已知的。
得到定义 left+1的左边均小于 target,right-1的右边都是大于等于target的。故当循环结束时,left+1和right都是我们要找的答案!
class Solution:
def search(self, nums: List[int], target: int) -> int:
# (left, right)
left, right = -1, len(nums)
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid
else:
right = mid
return right if right < len(nums) and nums[right] == target else -1
4. leetcode 34. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]
示例 3:
输入:nums = [], target = 0 输出:[-1,-1]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums
是一个非递减数组-109 <= target <= 109
解题思路:
在了解了上面的内容以后,这道题就显得非常容易了。 随便选择二分查找1-3的一种写法,然后调用函数,我们就得到了第一次出现的位置。接下来是值得注意的地方,如果返回值start == len(nums),说明数组中不存在>=target的数,如果nums[start] != target 则说明 数组中不存在target。 以上两种情况,均需要返回[-1, -1]。
如果 start存在,接下来 我们要找的就是最后一次出现的位置, 那么只要找到target + 1第一次出现的位置然后 -1,就得到了 target最后出现的位置。
以下是代码
def lower_bound(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
start = lower_bound(nums, target)
if start == len(nums) or nums[start] != target:
return [-1, -1]
end = lower_bound(nums, target+1) - 1
return [start, end]
5. leetcode 35. 搜索插入位置 https://leetcode.cn/problems/search-insert-position/
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5 输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2 输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7 输出: 4
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
为 无重复元素 的 升序 排列数组-104 <= target <= 104
解题思路:
这题非常简单,要找到target的插入位置,就是说找到第一个>=target的位置,直接调用二分查找的函数即可。以下是代码
def lower_bound(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
return lower_bound(nums, target)
6. leetcode 704. 二分查找 https://leetcode.cn/problems/binary-search/
解题思路:直接调用函数即可,需要注意的是如果找不到的话需要返回-1(我这里使用的开区间的写法,如果right >= len(nums) 或者 nums[right] != target 说明nums中不存在target)。
以下是代码
class Solution:
def search(self, nums: List[int], target: int) -> int:
# (left, right)
left, right = -1, len(nums)
while left + 1 < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid
else:
right = mid
return right if right < len(nums) and nums[right] == target else -1
7. leetcode 2529. 正整数和负整数的最大计数 https://leetcode.cn/problems/maximum-count-of-positive-integer-and-negative-integer/
给你一个按 非递减顺序 排列的数组 nums
,返回正整数数目和负整数数目中的最大值。
- 换句话讲,如果
nums
中正整数的数目是pos
,而负整数的数目是neg
,返回pos
和neg
二者中的最大值。
注意:0
既不是正整数也不是负整数。
示例 1:
输入:nums = [-2,-1,-1,1,2,3] 输出:3 解释:共有 3 个正整数和 3 个负整数。计数得到的最大值是 3 。
示例 2:
输入:nums = [-3,-2,-1,0,0,1,2] 输出:3 解释:共有 2 个正整数和 3 个负整数。计数得到的最大值是 3 。
示例 3:
输入:nums = [5,20,66,1314] 输出:4 解释:共有 4 个正整数和 0 个负整数。计数得到的最大值是 4 。
解题思路:
这道题也很简单,需要注意的就是0既不是正整数也不是负整数。
首先找到第一个 >= 1的位置 就是 第一个正整数的位置 记作positive
然后找到 第一个>= 0的位置 -1 就是 第一个负整数的位置 记作negative
len(nums[positive: ]) 正整数的个数
len(nums[0: negative+1]) 负整数的个数
以下是代码
def lower_bound(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
class Solution:
def maximumCount(self, nums: List[int]) -> int:
positive = lower_bound(nums, 1)
negative = lower_bound(nums, 0) - 1
p_n = len(nums[positive:])
n_n = len(nums[0:negative+1])
return max(p_n, n_n)
8. leetcode 2300. 咒语和药水的成功对数 https://leetcode.cn/problems/successful-pairs-of-spells-and-potions/
给你两个正整数数组 spells
和 potions
,长度分别为 n
和 m
,其中 spells[i]
表示第 i
个咒语的能量强度,potions[j]
表示第 j
瓶药水的能量强度。
同时给你一个整数 success
。一个咒语和药水的能量强度 相乘 如果 大于等于 success
,那么它们视为一对 成功 的组合。
请你返回一个长度为 n
的整数数组 pairs
,其中 pairs[i]
是能跟第 i
个咒语成功组合的 药水 数目。
示例 1:
输入:spells = [5,1,3], potions = [1,2,3,4,5], success = 7 输出:[4,0,3] 解释: - 第 0 个咒语:5 * [1,2,3,4,5] = [5,10,15,20,25] 。总共 4 个成功组合。 - 第 1 个咒语:1 * [1,2,3,4,5] = [1,2,3,4,5] 。总共 0 个成功组合。 - 第 2 个咒语:3 * [1,2,3,4,5] = [3,6,9,12,15] 。总共 3 个成功组合。 所以返回 [4,0,3] 。
示例 2:
输入:spells = [3,1,2], potions = [8,5,8], success = 16 输出:[2,0,2] 解释: - 第 0 个咒语:3 * [8,5,8] = [24,15,24] 。总共 2 个成功组合。 - 第 1 个咒语:1 * [8,5,8] = [8,5,8] 。总共 0 个成功组合。 - 第 2 个咒语:2 * [8,5,8] = [16,10,16] 。总共 2 个成功组合。 所以返回 [2,0,2] 。
提示:
n == spells.length
m == potions.length
1 <= n, m <= 105
1 <= spells[i], potions[i] <= 105
1 <= success <= 1010
解题思路:
这题思路不难,但是想过leetcode的样例超级难!
实际上的思路就是 potions这个数组排个序,然后用spell也就是咒语乘过去 记作search,接着用二分查找找到乘完后数组中第一个大于等于target的位置 记作X,然后len(search[x:])。 这样就是基本思路,然并卵,超出时间限制在这折磨你。
看了别人的思路,将spell * potions 改成了 success / spell ,这个时候还是不行,仍然超出时间限制!
又看看了别人的思路,改成了len(potions) - x,这个时候过了样例。
以下是代码
def lower_bound(nums, target):
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
class Solution:
def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
ans = []
potions.sort()
for spell in spells:
# search = [spell *p for p in potions]
x = lower_bound(potions, success / spell)
ans.append(len(potions) - x)
return ans
解释如下(感谢万能的GPT):
这两个方法之所以在性能上有所不同,是因为它们在查找满足条件的配对数时使用了不同的逻辑。让我们来看一下每个方法的行为:
-
ans.append(len(potions[x:]))
:- 这种方法使用二分查找来找到第一个大于等于
success / spell
的元素的索引x
。 - 然后它计算从索引
x
到列表末尾的元素数量,以确定满足条件的配对数。 - 问题在于,
potions[x:]
创建了一个新的列表切片,这需要额外的时间和空间复杂度来创建和遍历切片,尤其当potions
列表很大时,这会导致超出时间限制。
- 这种方法使用二分查找来找到第一个大于等于
-
ans.append(len(potions) - x)
:- 这种方法也使用了二分查找来找到第一个大于等于
success / spell
的元素的索引x
。 - 然后它计算满足条件的配对数,直接使用
len(potions) - x
。 - 这种方法没有创建新的列表切片,因此没有额外的时间和空间开销,因此性能更好。
- 这种方法也使用了二分查找来找到第一个大于等于
综上所述,第二种方法在性能上更优,因为它避免了创建额外的列表切片。
9. Leetcode 275. H 指数 II https://leetcode.cn/problems/h-index-ii/solution/tu-jie-yi-tu-zhang-wo-er-fen-da-an-si-ch-d15k/
给你一个整数数组 citations
,其中 citations[i]
表示研究者的第 i
篇论文被引用的次数,citations
已经按照 升序排列 。计算并返回该研究者的 h 指数。
h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h
指数是指他(她)的 (n
篇论文中)至少 有 h
篇论文分别被引用了至少 h
次。
请你设计并实现对数时间复杂度的算法解决此问题。
示例 1:
输入:citations = [0,1,3,5,6]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。由于研究者有3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。
示例 2:
输入:citations = [1,2,100]
输出:2
提示:
n == citations.length
1 <= n <= 105
0 <= citations[i] <= 1000
citations
按 升序排列
解题思路:
这道题好难。
首先理解一下什么是H指数? 就是说 某位作者的 k篇论文的引用数 都大于K ,那么他的H指数就是k。
例如 A作者发表六篇论文[0,1,2,3,5,6],列表的数字是他每篇论文的引用数。 可以看出他有三篇论文的引用数都>3,所以他的H指数就是3。
这道题的思路是二分答案,如果一位作者有 3篇论文的引用数 > 3, 那么他也肯定有 2篇论文的引用数 >2。 如果一位作者没有 4篇论文的引用数 都 > 4, 那么他肯定也不存在 5篇论文都 > 5。
以下是代码
class Solution:
def hIndex(self, citations: List[int]) -> int:
# 论文的数量是[1,n]
left, right = 1, len(citations)
while left <= right:
mid = (left + right) // 2
# 如果 mid篇论文的引用数 都大于 mid, 那么 mid-1篇论文的引用数都大于mid-1,所以更新左边界
if citations[-mid] >= mid:
left = mid + 1
# 如果没有mid篇论文的引用 都大于mid, 那么 mid+1篇论文的引用数肯定小于 mid+1,所以更新右边界
else:
right = mid - 1
# 根据循环不变量 可知left - 1为 存在h篇论文的引用数>h
# right + 1 为不存在 h篇论文的引用数>h
# 所以返回的答案为 right or left - 1
return right
之前我在想一个问题,就是为什么要返回right?
现在相同了,因为left每次更新为mid + 1 而 right 每次更新为 mid - 1
left的更新前提是 存在 mid篇文章的引用指数 >= mid,更新为 mid + 1
right的更新前提是 不存在mid篇文章的引用指数 >= mid, 更新为 mid - 1
当循环结束时 left > right ,为什么返回right
而不是left
,这是因为在最后一次循环中,left
指针会移动到mid + 1
,而right
指针会保持在mid - 1
。由于left
指针移动到了不满足条件的区域,而right
指针停留在满足条件的区域,因此我们应该返回right
作为答案。
总结
二分查找的思路很简单,但需要把握住循环不变量的核心!还有一句很重要的话,区间内的元素关系位置,区间外的元素关系已知。