Leetcode刷题笔记—二分算法篇
链表子串数组题,用双指针别犹豫。
双指针家三兄弟,各个都是万人迷。
快慢指针最神奇,链表操作无压力。
归并排序找中点,链表成环搞判定。
左右指针最常见,左右两端相向行。
反转数组要靠它,二分搜索是弟弟。
滑动窗口老猛男,子串问题全靠它
左右指针滑窗口,一前一后齐头进
自诩十年老司机,怎料农村道路滑。
一不小心滑到了,鼻青脸肿少颗牙。
算法思想很简单,出了bug想升天
一、二分查找
第一题:二分查找
Leetcode74:二分查找:简单题 (详情点击链接见原题)
给定一个
n
个元素有序的(升序)整型数组nums
和一个目标值target
,写一个函数搜索nums
中的target
,如果目标值存在返回下标,否则返回-1
使用二分法的前提条件是数组为有序数组,同时数组中无重复元素【因为一旦有重复元素】使用二分查找法返回的元素下标可能不是唯一的
大家自己写二分法比较混乱的原因是对区间的定义没有想清楚
写二分法,区间定义一般分为两种,左闭右闭即[left, right]
或者左闭右开即[left, right)
二分法的两种写法:
case1:target
定义在一个左闭右闭的区间[left, right]
(即当 right
取 len(arr) - 1
时【初始时左指针 left
指向第一个元素,右指针 right
指向最后一个元素时】)
- 注1:
while left <= right
要使用<=
,因为left == right
是有意义的,即两个指针指向同一个元素 - 注2:如果
nums[middle]:【中间值】> target:【目标值】
,则需更新右指针right
指向中间元素的前一个元素【right = middle - 1
】 - 同理如果
nums[middle]:【中间值】< target:【目标值】
则更新左指针指向中间元素的后一个元素【left = middle + 1
】
适配的python
解法1:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
middle = left + (right - left) // 2
if nums[middle] < target: # 中间元素小于目标元素目标元素在右边,移动左指针
left = middle + 1
elif nums[middle] > target: # 中间元素大于目标元素目标元素在左边,移动右指针
right = middle - 1
else:
return middle
return -1
case2:target
定义在一个左闭右开的区间 [left, right)
,(即当 right=len(arr)
时【初始时左指针指向第一个元素,右指针指向列表中最后一个元素的下一个位置】
- 注1:即目标元素
target
永远不会取到右指针指向的位置 - 注2:
while left < right
中间不能取=
,因为target
不会取到右指针right
指向的位置,所以left==right
【左指针和右指针指向同一个位置是没有意义的】 - 注3:如果
nums[midle]:中间值 > target:目标元素
,右指针right
指向中间元素,虽然目标值不会为middle
指向的元素但可能为middle
指向元素的前一个元素,右指针指向中间元素能保证循环查找的下一个区间仍然是一个左闭右开区间,【by the way,有的书上管这个叫做循环不变量,你初始规定target在一个左闭右开区间里面,所以你循环缩小区间范围查找的时候也得保证这个不变的性质】
适配的python
解法2:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) # 变化1: target在一个[left,right)区间内
while left < right: # 变化2:不能取'=',因为target不会取到right指针指向的元素,left和right指向一个位置是没有意义的
middle = left + (right - left) // 2
if nums[middle] < target:
left = middle + 1
elif nums[middle] > target:
# 变化3:如果中间值大于目标元素,右指针指向中间元素,目标值可能是中间元素的前一个元素
right = middle
else:
return middle
return -1
第二题:搜索插入位置
Leetcode35:搜索插入位置:简单题 (详情点击链接见原题)
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为O(log n)
的算法
python代码解法:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > target:
right = mid - 1
elif nums[mid] < target:
left = mid + 1
else:
return mid
return left + 1 if nums[left] < target else left
二、二分查找进阶练习
第一题:搜索二维矩阵
Leetcode74:搜索二维矩阵:中等题 (详情点击链接见原题)
给你一个满足下述两条属性的
m x n
整数矩阵:
每行中的整数从左到右按非严格递增顺序排列。
每行的第一个整数大于前一行的最后一个整数。
给你一个整数target
,如果target
在矩阵中,返回true
;否则,返回false
解法1:暴力解法
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
for lis in matrix:
left, right = 0, len(lis)
while left < right:
mid = (left + right) // 2
if lis[mid] > target:
right = mid
elif lis[mid] < target:
left = mid + 1
else:
return True
return False
解法2:优化解法
由于二维矩阵固定列的「从上到下」
或者固定行的「从左到右」
都是升序的。
因此我们可以使用两次二分来定位到目标位置
- 时间复杂度:
O(logm+logn)
- 空间复杂度:
O(1)
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 先对列进行二分,找到target可能所在的行
def find_row():
left, right = 0, len(matrix)
while left < right:
mid = (left + right) // 2
if matrix[mid][0] > target:
right = mid
elif matrix[mid][0] < target:
left = mid + 1
else:
return True
return left - 1 if matrix[left - 1][0] < target else False
row = find_row() # 如果找到行接着定位target所在的列
if row is not False:
if row is True:
return row
left, right = 0, len(matrix[row]) - 1
while left <= right:
mid = (left + right) // 2
if matrix[row][mid] > target:
right = mid - 1
elif matrix[row][mid] < target:
left = mid + 1
else:
return True
return False
解法三:将「二维矩阵」
当做「一维矩阵」
来做
因为将二维矩阵的行尾和行首连接,也具有单调性
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
row_num, col_num = len(matrix), len(matrix[0])
left, right = 0, row_num * col_num
while left < right:
mid = (left + right) // 2
if matrix[mid // col_num][mid % row_num] > target:
right = mid
elif matrix[mid // col_num][mid % row_num] < target:
left = mid + 1
else:
return True
return False
第二题:搜索二维矩阵II
Leetcode240:搜索二维矩阵II:中等题 (详情点击链接见原题)
编写一个高效的算法来搜索
m x n
矩阵matrix
中的一个目标值target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列
python代码解法(抽象BST):
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
r, c = 0, len(matrix[0]) - 1
while r < len(matrix) and c >= 0:
if matrix[r][c] < target:
r += 1
elif matrix[r][c] > target:
c -= 1
else:
return True
return False
第三题:搜索旋转排序数组
Leetcode33:搜索旋转排序数组:中等题 (详情点击链接见原题)
整数数组
nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标k(0 <= k < nums.length)
上进行了 旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从0
开始 计数)。例如,[0,1,2,4,5,6,7]
在下标3
处经旋转后可能变为[4,5,6,7,0,1,2]
。
给你旋转后的数组nums
和一个整数target
,如果nums
中存在这个目标值target
,则返回它的下标,否则返回-1
。
你必须设计一个时间复杂度为O(log n)
的算法解决此问题
关键1
:只有在顺序区间内才可以通过区间两端的数值判断 target
是否在其中
关键2
: 判断顺序区间还是乱序区间,只需要对比 nums[left]
和 nums[right]
是否是顺序对即可
关键3
: 每次二分都会至少存在一个顺序区间
python代码解法:
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 左闭右闭区间
while left <= right:
mid = (left + right) // 2
if nums[mid] == target: # 1.找到目标值,直接返回索引
return mid
elif 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
return -1
第三题进阶:搜索旋转排序数组II
Leetcode81. 搜索旋转排序数组 II:中等题 (详情点击链接见原题)
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同
本题和上一题大致类似,主要针对于特殊情况,101111
和 111101
这种情况,分不清是前面有序还是后面有序
python代码解法(暴力解法):
class Solution:
def search(self, nums: List[int], target: int) -> bool:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return True
elif nums[left] == nums[mid] == nums[right]: # 遇到特殊情况直接采用暴力按顺序查找
for i in nums:
if i == target:
return True
return False
elif 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
return False
python代码解法(优化解法):
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 左闭右闭区间
while left <= right:
mid = (left + right) // 2
if nums[mid] == target: # 1.找到目标值,直接返回索引
return True
if nums[left] == nums[mid]: # 在分不清是前面有序还是后面有序的时候,left+1 即可,相当于去掉一个干扰项
left += 1
continue
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 False
第四题:寻找旋转排序数组中的最小值
Leetcode153:寻找旋转排序数组中的最小值:中等题 (详情点击链接见原题)
已知一个长度为
n
的数组,预先按照升序排列,经由1
到n
次 旋转后,得到输入数组。例如,原数组nums = [0,1,2,4,5,6,7]
在变化后可能得到:
若旋转4
次,则可以得到[4,5,6,7,0,1,2]
若旋转7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组[a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组[a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素 。你必须设计一个时间复杂度为O(log n)
的算法解决此问题
解题思路
由上图可知
如果中值 < 右值,说明最小值在左半边,可以收缩右边界
如果 中值 > 右值,说明最小值在右边,可以收缩左边界
通过比较中值和右值即可确定最小值的位置范围,从而确定边界收缩的方向,而 case1
和 case2
中都是左值 小于 中值,但最小值的范围却不同说明只比较左值和中值不能确定最小值的位置范围
所以我们通过比较中值与右值来确定最小值的位置范围从而确定边界收缩的方向
python代码解法1
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1 # 左闭右闭区间,如果用右开区间则不方便判断右值
while left < right:
mid = (left + right) // 2 # 地板除:mid 更靠近 left
if nums[mid] > nums[right]: # 中值 > 右值,最小值在右半边,收缩左边界
left = mid + 1 # 因为中值 > 右值,所以中值肯定不是最小值,左边界可以跨过mid
elif nums[mid] < nums[right]: # 明确中值 < 右值,中值也可能是最小值,右边界只能取到mid处
right = mid
return nums[left]
解题思路
以上图为例,在处理完上述的第二种情况后,始终将 nums[mid] 与最左边界的数 nums[0] 进行比较
python代码解法2
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
if nums[left] < nums[right]:
return nums[left]
while left < right:
mid = (left + right) // 2
if nums[0] > nums[mid]:
right = mid
else:
left = mid + 1
return nums[left]
python代码解法2
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
if nums[left] <= nums[right]: # 如果左边的值<右边的值说明数组本身有序,left指向的即最小值
return nums[left]
if right - left == 1:
return nums[right] if nums[left] > nums[right] else nums[left]
mid = (left + right) // 2
if nums[left] < nums[mid]: # 左指针指向的值<中间值:最小值一定在右边,left可跨过中间值
left = mid + 1
elif nums[mid] < nums[right]: # 中间值小于右指针指向的值,中间值有可能为最小值
right = mid
return nums[right]
第五题:寻找旋转排序数组的最小值II
Leetcode154:寻找旋转排序数组的最小值II:困难题 (详情点击链接见原题)
已知一个长度为
n
的数组,预先按照升序排列,经由1
到n
次 旋转 后,得到输入数组。例如,原数组nums = [0,1,4,4,5,6,7]
在变化后可能得到:
若旋转4
次,则可以得到[4,5,6,7,0,1,4]
若旋转7
次,则可以得到[0,1,4,4,5,6,7]
注意,数组[a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组[a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个可能存在 重复 元素值的数组nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。你必须尽可能减少整个过程的操作步骤。
第六题:在排序数组中查找元素的第一个位置和最后一个位置
Leetcode34:在排序数组中查找元素的第一个位置和最后一个位置:中等题 (详情点击链接见原题)
给你一个按照非递减顺序排列的整数数组
nums
,和一个目标值target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值target
,返回[-1, -1]
。
你必须设计并实现时间复杂度为O(log n)
的算法解决此问题。
这道题其实就是找目标元素,与前面稍微不同的是找目标元素第一次出现的位置,这道题的关键在于find_startIndex
最后的return left
,
- 当最后一次循环
left == right
如果找到目标元素,则left
,right
,mid
三个指针指向同一个位置,即目标值target
第一次出现的位置 - 当最后一次循环
left == right
如果没找到目标元素,则left
,right
,mid
三个指针指向同一个位置,中间元素大于目标值右指针前移一位,中间元素小于目标值左指针后移一位,此时return left
返回的是第一个大于目标元素的元素下标
在找起始位置时如果left
走到了数组末尾或者没找到目标值返回[-1, -1]
在找结束位置的时候找target + 1
即可找到大于目标值的第一个数的起始位置,end - 1
即为目标值的结束位置
python解法
class Solution:
def find_start(self, nums, target):
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] >= target:
right = mid
return left + 1 if target > nums[-1] else left
def searchRange(self, nums: List[int], target: int) -> List[int]:
ans = []
if not nums:
return [-1, -1]
start = self.find_start(nums, target)
if start >= len(nums) or nums[start] != target:
return [-1, -1]
ans.append(start)
end = self.find_start(nums, target + 1)
ans.append(end - 1)
return ans
第七题:寻找峰值
Leetcode162:寻找峰值:中等题 (详情点击链接见原题)
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组nums
,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设nums[-1] = nums[n] = -∞
。
你必须实现时间复杂度为O(log n)
的算法来解决此问题
对于该题,博主本人水平有限,没能想到好的解法,引用leetcode
大神的解题思路,题给前提条件:
case1:数据长度至少为 1
case2:越过数组两边看做负无穷
case3:相邻元素不相等
讨论:
特殊情况:数组长度为 1,由于边界看做负无穷,此时峰值为该唯一元素的下标
一般情况:数组长度大于1,从最左边的元素 nums[0]
开始出发考虑:
- 如果
nums[0] > nums[1]
,那么最左边元素nums[0]
就是峰值 - 如果
nums[0] < nums[1]
, 由于已经存在明确的nums[0]
和nums[1]
大小关系,将nums[0]
看做边界,nums[1]
看做新的最左侧元素,继续向右进行分析- 在达到数组最右侧前若出现
nums[i] > nums[i + 1]
,说明nums[i]
就是我们想要的峰值 - 如果到达数组最右侧还没出现
nums[i] > nums[i + 1]
说明数组严格递增,由于边界视为-∞
,故最后一个元素为峰值元素
- 在达到数组最右侧前若出现
推论:
- 如果
nums[x] > nums[x + 1]
,若nums[x] > nums[x - 1]
则nums[x]
即为峰值,若nums[x] < nums[x - 1]
,对nums[x - 1]
做同样的分析即可知nums[x]
的左边一定存在峰值 - 否则
nums[x] < nums[x + 1]
nums[x]
的右边一定存在峰值
分析
中点所在地方,可能是某座山的山峰,山的下坡处,山的上坡处,如果是山峰,最后会二分终止也会找到,关键是我们的二分方向,并不知道山峰在我们左边还是右边,送你两个字你就明白了,爬山(没错,就是带你去爬山),如果你往下坡方向走,也许可能遇到新的山峰,但是也许是一个一直下降的坡,最后到边界。但是如果你往上坡方向走,就算最后一直上的边界,由于最边界是负无穷,所以就一定能找到山峰,总的一句话,往递增的方向上,二分,一定能找到,往递减的方向只是可能找到,也许没有
python代码解法:
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
elif len(nums) == 2: # 因为题目所给条件对于所有有效的 i 都有 nums[i] != nums[i + 1]
return nums[0] if nums[0] > nums[1] else nums[1]
else:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[mid + 1]: # 如果中间值大于它的后一个值,峰值一定在它的左边,右指针指向中间位置
right = mid # 右指针不能越过中间元素因为中间位置有可能是峰值
else: # 如果中间值小于后一个值,峰值一定在它的右边,左指针指向中间元素的下一个位置
left = mid + 1 # 左指针可以越过中间元素,因为中间位置不可能是峰值
return left
第七题扩展:山脉数组的峰顶索引
Leetcode852. 山脉数组的峰顶索引:中等题 (详情点击链接见原题)
符合下列属性的数组
arr
称为 山脉数组 :
arr.length >= 3
存在i(0 < i < arr.length - 1)
使得:
arr[0] < arr[1] < ... arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
python代码解法:
class Solution:
def peakIndexInMountainArray(self, arr: List[int]) -> int:
left, right = 0, len(arr) - 1
while left < right:
mid = (left + right) // 2
if arr[mid] > arr[mid + 1]:
right = mid
else:
left = mid + 1
return left
第八题:有序数组中的单一元素
Leetcode540:有序数组中的单一元素:中等题 (详情点击链接见原题)
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足O(log n)
时间复杂度和O(1)
空间复杂度。
case1:[1, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6]
如果中间元素的下标为奇数,且单一元素在中间元素左边的话,那么中间元素与下一个元素是相等的
case2: [1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 6]
如果中间元素的下标为奇数,且单一元素在中间元素右边的话,那么中间元素与前一个元素是相等的
case3:[1, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7]
中间元素下标为偶数,单一元素在中间元素的左边,中间元素与前一个元素是相等的
case4: [1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 6, 7, 7]
中间元素下标为偶数,单一元素在中间元素的右边,中间元素与下一个元素是相等的
出发点1: 从中间元素下标的奇偶性出发
插入单一元素之前,如果数组中的所有元素都是两两成对出现的话,nums = [1,1,2,2,3,3,4,4]
,那么所有偶数下标的元素与他的下一个元素必然是相等的
出发点2: 从插入单一元素后影响前一个元素和后一个元素是否相等出发
python代码解法:
class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if mid % 2 == 0: # case1:如果中间元素下标为偶数
if mid + 1 < len(nums) and nums[mid] == nums[mid + 1]: # 1.如果中间元素与下一个元素相等,单一元素一定在中间元素的右边
left = mid + 1
else: # 2.否则单一元素在可能在中间元素的左边(此时中间元素可能为单一元素,不能跳过中间元素)
right = mid
else: # case2:如果中间元素下标为奇数
if mid - 1 >= 0 and nums[mid] == nums[mid - 1]: # 1.如果中间元素与上一个元素相等,单一元素一定在中间元素的右边
left = mid + 1
else: # 2.否则单一元素在可能在中间元素的左边(此时中间元素可能为单一元素,不能跳过中间元素)
right = mid
return nums[left]
`
第九题:寻找两个正序数组的中位数
Leetcode4. 寻找两个正序数组的中位数:困难题 (详情点击链接见原题)
给定两个大小分别为
m
和n
的正序(从小到大)数组nums1
和nums2
。请你找出并返回这两个正序数组的 中位数
解法一:暴力解法,先将两个数组合并,然后根据奇数还是偶数返回中位数
python代码解法(暴力解法)
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
index = len(nums1) + len(nums2)
ans = [0] * index
p1, p2 = 0, 0
i = 0
while p1 < len(nums1) and p2 < len(nums2):
if nums1[p1] < nums2[p2]:
ans[i] = nums1[p1]
p1 += 1
else:
ans[i] = nums2[p2]
p2 += 1
i += 1
if p1 < len(nums1):
while i < index:
ans[i] = nums1[p1]
p1 += 1
i += 1
if p2 < len(nums2):
while i < index:
ans[i] = nums2[p2]
p2 += 1
i += 1
print(ans)
left, right = 0, len(ans) - 1
mid = (left + right) // 2
if len(ans) % 2 == 0:
return (ans[mid] + ans[mid + 1]) / 2
else:
return ans[mid]
解法二:二分法
中位数把数组分割成了两部分,并且左右两部分元素个数相等
单侧元素个数为: (n1 + n2 + 1) // 2 个
, 令 k = (n1 + n2 + 1) // 2
,问题变为如何在两个有序数组中找到前 k
小的元素位置
如果我们从nums1
数组中取出前 m1
个元素,那么从 nums2
就需取出前 m2 = k - m1
个元素,如果我们在 nums1
数组中找到了合适的位置,则 m2
的位置也就确定了
问题进一步转换为:如何从 nums1
数组中取出前 m1
个元素,使得 nums1
第 m1
个元素或者 nums2
第 m2 = k - m1
个元素为中位线位置
我们可以通过二分查找的方法,在数组 nums1
中找到合适的 m1
位置,具体做法如下
- 让
left
指向nums1
的头部位置,right
指向nums1
的尾部位置 - 每次取中间位置作为
m1
,则m2 = k - m1
- 如果
nums1[m1] < nums2[m2 - 1]
,则nums1
的前m1
个元素都不可能是第k
个元素,说明m1
的取值小了,应该将m1
进行右移操作 - 如果
nums1[m1] >= nums2[m2 - 1]
,说明m1
的取值大了,应该将m1
进行左移操作
- 如果
- 找到
m1
的位置之后,还要根据两个数组长度和的奇偶性,以及边界条件来计算对应的中位数
python代码解法
import sys
from typing import List
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
n1 = len(nums1)
n2 = len(nums2)
if n1 > n2:
return self.findMedianSortedArrays(nums2, nums1)
k = (n1 + n2 + 1) // 2 # 单侧元素个数为: (n1 + n2 + 1) // 2 个
left, right = 0, n1
while left < right:
m1 = (left + right) // 2 # 在nums1中取前 m1 个元素
m2 = k - m1 # 在nums2中取前 k - m1 个元素
if nums1[m1] < nums2[m2 - 1]:
left = m1 + 1
else:
right = m1
m1 = left # 找到 m1 的位置后,还要根据两个数字长度和奇偶性以及边界条件来计算对应的中位数
m2 = k - m1
c1 = max(-sys.maxsize if m1 <= 0 else nums1[m1 - 1], -sys.maxsize if m2 <= 0 else nums2[m2 - 1])
if (n1 + n2) % 2 == 1: # 如果两数组长度之和为奇数
return c1
c2 = min(sys.maxsize if m1 >= n1 else nums1[m1], sys.maxsize if m2 >= n2 else nums2[m2])
return (c1 + c2) / 2
if __name__ == '__main__':
s = Solution()
nums1 = [1, 4, 6, 8, 10, 14]
nums2 = [2, 5, 7, 9, 11]
print(s.findMedianSortedArrays(nums1, nums2))
三、二分答案
第一题:爱吃香蕉的珂珂
Leetcode875:爱吃香蕉的珂珂/爱吃香蕉的狒狒:中等题 (详情点击链接见原题)
珂珂喜欢吃香蕉。这里有
n
堆香蕉,第i
堆中有piles[i]
根香蕉。警卫已经离开了,将在h
小时后回来。
珂珂可以决定她吃香蕉的速度k
(单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉k
根。如果这堆香蕉少于k
根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在h
小时内吃掉所有香蕉的最小速度k
(k
为整数)
大家在使用二分法的时候很容易陷入思维定式,一看到二分题上来就对区间进行二分,恰恰这道题就反其道而行之,如果纠结于区间很容易陷入死胡同,顺便提一句,这道题是博主本人在面试中碰到的一道手撕代码题,很多大厂面试官特别喜欢在leetcode
上出原题或者是一个解题方法改编题
这道题求的是珂珂吃香蕉的最小速度,所以我们应该对速度进行二分,珂珂吃香蕉的速度是自己决定的,所以最小速度应该是1
,珂珂吃香蕉的最大速度是以题目所给的n
堆香蕉里面最大的那堆max(piles)
的速度吃香蕉
python代码解法:
import math
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
def check(k): # 检查珂珂吃香蕉的速度是否能赶在饲养员回来之前把所有香蕉吃完
"""
:param k:珂珂选择吃香蕉的速度
:return:珂珂是否能在饲养员回来之前吃完所有香蕉
"""
hours = 0
for pile in piles:
hours += ceil(pile / k) # 每堆香蕉吃完的耗时 =(这堆香蕉的数量/珂珂一小时吃香蕉的速度)[向上取整]
return hours <= h # 珂珂吃完所有香蕉的耗时 hours <= 饲养员离开的时间 h (即珂珂能在饲养员回来之前吃完所有香蕉)
left, right = 1, max(piles) # 开始对珂珂吃香蕉的速度进行二分
while left < right:
mid = (left + right) // 2
if check(mid): # 如果选择的中间值可以在饲养员回来之前吃完所有香蕉,移动右指针减小珂珂吃香蕉的速度,因为题目要找的是最小速度
right = mid
else: # 如果选择的中间值没法在饲养员回来之前吃完所有香蕉,移动左指针增大珂珂吃香蕉的速度
left = mid + 1
return right # 当两个指针指向同一个位置 left==right 时跳出循环,此时两个指针指向的速度值即珂珂能赶在饲养员回来之前吃完所有香蕉的最小速度
注:能否对上面的解法进行算法上面的优化?
其实在求最小速度的时候我们没必要从
1
开始去试,如果给出的piles
为[55, 66, 89, 100, 150]
,最小速度用1
去试探会做很多无用功,所以对于最小速度我们可以在1
和平均速度
二者之间取一个最大值即left= max(1, sum(piles) // h)
第二题:在 D 天内送达包裹的能力
Leetcode1011. 在 D 天内送达包裹的能力:中等题 (详情点击链接见原题)
传送带上的包裹必须在
days
天内从一个港口运送到另一个港口。
传送带上的第i
个包裹的重量为weights[i]
。每一天,我们都会按给出重量(weights)
的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量
python代码解法:
class Solution:
def check(self, weight, weights, days):
capacity = 0
day_count = 0
for w in weights:
capacity += w
if capacity > weight:
capacity = w
day_count += 1
day_count += 1 # 将最后一趟花费的天数加上
return day_count <= days
def shipWithinDays(self, weights: List[int], days: int) -> int:
# 二分答案,left 取单天最大的运载重量(否则一天的包裹得两天装)
left, right = max(weights), sum(weights)
while left < right:
mid = (left + right) // 2
if self.check(mid, weights, days): # 如果可以在mid的运载能力下在days天内完成运输,继续尝试更小的重量
right = mid
else: # 如果不能在mid的运载能力下在days天内完成运输
left = mid + 1
return left
第三题:制作 m 束花所需的最少天数
Leetcode1482. 制作 m 束花所需的最少天数:中等题 (详情点击链接见原题)
给你一个整数数组
bloomDay
,以及两个整数m
和k
。
现需要制作m
束花。制作花束时,需要使用花园中 相邻的k
朵花
python代码解法:
class Solution:
def check(self, time, m, k, bloomDay):
k_count = 0
m_count = 0
for day in bloomDay:
if day <= time:
k_count += 1
if k_count == k:
m_count += 1 # 花束计数+1
k_count = 0 # 花朵计数重新归零
else: # 花朵计数重新归零
k_count = 0
return m_count >= m
def minDays(self, bloomDay: List[int], m: int, k: int) -> int:
if len(bloomDay) < m * k:
return -1
left, right = min(bloomDay), max(bloomDay)
while left < right:
mid = (left + right) // 2
if self.check(mid, m, k, bloomDay):
right = mid
else:
left = mid + 1
return left
第四题:供暖器
Leetcode475. 供暖器:中等题 (详情点击链接见原题)
冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。在加热器的加热半径范围内的每个房屋都可以获得供暖
解题思路:
需要求得最小的加热半径 ans
,使得所有的 house[i]
均被覆盖,在以 ans
为分割点的数轴上具有二段性
- 数值小于
ans
的半径无法覆盖所有的房子 - 数值大于
ans
的半径可以覆盖所有房子
具体实现:
先对 houses
和 heaters
进行排序,使用 i
指向当前处理到的 houses[i]
,j
指向可能覆盖到 house[i]
的最小下标,x
代表当前需要 check
的半径
- 当且仅当
headters[j] + x < houses[i]
时,houses[i]
必然不能被heaters[j]
所覆盖,此时让j
自增 - 找到合适的
j
之后,再检查heaters[j] - x <= houses[i] <= heaters[j] + x
是否满足即可知道houses[i]
的覆盖情况
python代码解法
class Solution:
def check(self, houses, heaters, x): # x 代表当前需要check的半径
i, j = 0, 0
while i < len(houses):
# heaters[j] + x < houses[i]: 此时 houses[i]必然不能被 heaters[j] 所覆盖,此时j自增
# j 指向可能覆盖到 houses[i]的最小下标heaters[j]
while j < len(heaters) and houses[i] > heaters[j] + x:
j += 1
# 找到合适的 j 之后再检查 heaters[j] - x <= houses[i] <= heaters[j] + x 是否满足
if j < len(heaters) and heaters[j] - x <= houses[i] <= heaters[j] + x:
i += 1
continue
return False
return True
def findRadius(self, houses: List[int], heaters: List[int]) -> int:
houses.sort()
heaters.sort()
left, right = 0, 10
while left < right:
mid = (left + right) // 2
if self.check(houses, heaters, mid):
right = mid
else:
left = mid + 1
return right
第五题:你可以安排的最多任务数目
Leetcode2071. 你可以安排的最多任务数目:困难题 (详情点击链接见原题)
给你
n
个任务和m
个工人。每个任务需要一定的力量值才能完成,需要的力量值保存在下标从0
开始的整数数组tasks
中,第i
个任务需要 tasks[i] 的力量才能完成
解题思路:
最大可能完成的任务数量的最大值为 min(工人数量, 任务数量)
,最小值为 0
,将任务数与工人数力量排序
任务力量从 mid
开始从大到小遍历,假设每个工人都吃药丸,去掉吃了药丸也完不成任务的工人,那么只要判断最大力量的工人再不吃药丸的情况下能不能完成,不能的话,只要让最小力量的吃了药丸完成工作,确保工人不重复利用的情况下就能找到最大值
python代码解法
from collections import deque
class Solution:
def check(self, mid, tasks, workers, pills, strength):
queue = deque()
index = len(workers) - 1
for i in range(mid - 1, -1, -1):
task = tasks[i]
while index >= 0 and workers[index] + strength >= task:
queue.append(workers[index])
index -= 1 # 已经入队的不会重新入队
# 吃了药丸都完成不了工作
if len(queue) == 0:
return False
if queue[0] >= task: # 队伍中力量最大的不吃药丸的可以完成工作
queue.popleft()
elif pills > 0: # 需要吃药丸时,让队尾吃药
queue.pop()
pills -= 1
else:
return False
return True
def maxTaskAssign(self, tasks: List[int], workers: List[int], pills: int, strength: int) -> int:
tasks.sort() # 按消耗力量值大小将任务排序
workers.sort() # 根据工人力量值大小进行排序
# right 取值分析
# 任务数量多,工人数量少,完成任务数量以工人数量为准
# 任务数量少,工人数量多
# 对所求答案(即任务完成的数量进行二分)
left, right = 0, min(len(tasks), len(workers))
while left <= right:
mid = (left + right) // 2
if self.check(mid, tasks, workers, pills, strength):
left = mid + 1
else:
right = mid - 1
return right
if __name__ == '__main__':
s = Solution()
tasks = [5, 4]
workers = [0, 0, 0]
pills = 1
strength = 5
print(s.maxTaskAssign(tasks, workers, pills, strength))
四、贪心 + 二分
第一题: 有界数组中指定下标处的最大值
Leetcode1802. 有界数组中指定下标处的最大值:中等题 (详情点击链接见原题)
给你三个正整数
n
、index
和maxSum
。你需要构造一个同时满足下述所有条件的数组nums
(下标 从0
开始 计数)
第二题:安排工作以达到最大收益
你有
n
个工作和m
个工人。给定三个数组:difficulty
,profit
和worker
,其中:
difficulty[i]
表示第i
个工作的难度,profit[i]
表示第i
个工作的收益。
worker[i]
是第i
个工人的能力,即该工人只能完成难度小于等于worker[i]
的工作
python代码解法
class Solution:
def search(self, package, target):
left, right = 0, len(package) - 1
while left < right:
mid = (left + right) // 2
if package[mid][0] > target:
right = mid
elif package[mid][0] <= target:
left = mid + 1
return left + 1 if target > package[left][0] else left
def maxProfitAssignment(self, difficulty: List[int], profit: List[int], worker: List[int]) -> int:
ans = 0
package = list(map(list, zip(difficulty, profit)))
package.sort(key=lambda x: x[0])
max_profit = package[0][1]
for i in range(1, len(package)): # 正序遍历
max_profit = max(package[i][1], max_profit)
package[i][1] = max_profit
for j in range(len(package) - 1, -1, -1): # 倒序遍历
if package[j][0] == package[j - 1][0]:
max_profit = package[j][1]
max_profit = max(max_profit, package[j - 1][1])
package[j - 1][1] = max_profit
worker.sort()
for work in worker:
res = self.search(package, work)
if res < len(package) and package[res][0] == work:
ans += package[res][1]
elif res > 0:
ans += package[res - 1][1]
return ans
if __name__ == '__main__':
s = Solution()
difficulty = [23, 30, 35, 35, 43, 46, 47, 81, 83, 98]
profit = [8, 11, 11, 20, 33, 37, 60, 72, 87, 95]
worker = [95, 46, 47, 97, 11, 35, 99, 56, 41, 92]
print(s.maxProfitAssignment(difficulty, profit, worker))
四、其他应用
第一题:有效三角形的个数
Leetcode611. 有效三角形的个数:中等题 (详情点击链接见原题)
给定一个包含非负整数的数组
nums
,返回其中可以组成三角形三条边的三元组个数
解题思路:
- 首先对数组排序
- 固定最短的两条边,二分查找最后一个小于两边之和的位置,可以求得固定两条边长之和满足条件的结果
python代码解法
class Solution:
def triangleNumber(self, nums: List[int]) -> int:
nums.sort()
n = len(nums)
ans = 0
for i in range(0, n - 2):
for j in range(i + 1, n - 1):
s = nums[i] + nums[j]
left, right = j + 1, n - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < s:
left = mid
else:
right = mid - 1
if nums[right] < s:
ans += right - j
return ans
第二题:有界数组中指定下标处的最大值
给你三个正整数
n
、index
和maxSum
。你需要构造一个同时满足下述所有条件的数组nums
解题思路:
如果我们确定了 nums[index]
的值为 x
,此时我们可以找到一个最小的数组总和,在 index
左侧的数组元素从 x - 1
每次递减 1
,如果减到 1
之后还有剩余元素,那么剩余元素都为 1
,同样右侧也是如此
这样我们可以计算出数组的总和,如果总和小于等于 maxSum
,此时 x
就是合法的,随着 x
的增大,数组的总和也会增大,因此i我们可以使用二分查找的方法发找到一个最大的且符合条件的 x
为了方便计算数组 左侧,右侧的元素之和,我们定义一个函数 sum(x, cnt)
表示一共有 cnt
个元素,最大值为 x
的数组的总和
等差数列求和公式
case1
: 如果 x >= cnt
,那么数组的总和为 [x +(x - cnt + 1) // 2
case2
: 如果 x , cnt
,那么数组的总和为 [(x + 1) * x] // 2 + cnt - x
总结
本文给大家总结了Leetcode
上常见的二分算法题,二分算法在某些题目中的难度会很高,包括博主本人对二分的理解能力也有限,对于二分,最重要的是要注意边界条件,左指针和右指针是否可以指向同一个位置决定了你的循环条件和if
条件中是否需要添加等号,路漫漫其修远兮~,大家一起加油,文中有不足之处欢迎大家评论指正,如果本文对你有帮助的话,还请多多点赞哦