二分查找
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在数据规模的对数时间复杂度内完成查找。但是,二分查找要求线性表具有有随机访问的特点(例如数组),也要求线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果。
查找方式
查找存在的值
用的是左闭右闭区间,所以 l e f t = m i d + 1 ; r i g h t = m i d − 1 ; left=mid+1;\:right=mid-1; left=mid+1;right=mid−1;
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
left, right = 0, n-1
while left <= right:#注意这里是闭区间
mid = (left + right) // 2
if nums[mid] == target: return mid
if nums[mid] > target:
right = mid - 1#闭区间
else:
left = mid + 1#闭区间
return -1
查找一个小于等于该值的索引
使用左闭右开,可以找到小于等于目标值的索引。 r i g h t = n right=n right=n表明开区间。
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
left, right = 0, n
while left < right:#注意这里是开区间
mid = (left + right) // 2
if nums[mid] <= target:
left = mid + 1#闭区间
else:
right = mid#开区间
return left-1
寻找一个相同值的左边界和右边界
使用左闭右闭的区间。首先寻找左边界
- 中间值小于目标值,说明应该右移,由于是左闭右闭: l e f t = m i d + 1 left=mid+1 left=mid+1
- 中间值大于目标值,说明应该左移, r i g h t = m i d − 1 right=mid-1 right=mid−1
- 中间值等于目标值,但是我们是要找左边界,所以区间右端点减一: r i g h t = m i d − 1 right=mid-1 right=mid−1
- 边界处理:如果目标值在首部或者尾部且只有 一个,弹出的时候right值索引比目标值索引小一,所以如果 n u m s [ r i g h t + 1 ] = t a r g e t nums[right+1]=target nums[right+1]=target等于目标值,说明最后一个索引就是目标值,且只有一个。或者首部为目标值,且只有一个。
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums: return 0
n = len(nums)
left, right = 0, n-1
if target < nums[0] or target > nums[-1]: return 0#值不在范围内,排除
while left <= right:#首先搜索左边界
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
right = mid - 1
if nums[right + 1] == target:#目标值在数组最后,或者数组首部的情况
start = right + 1
else:
return 0
同理,搜索右边界也是一样的,只不过在找到目标值之后,要将区间右移查找,即增大左区间: l e f t = m i d + 1 left=mid+1 left=mid+1
- 边界:在弹出的时候left是比目标值大一的,如果 n u m s [ l e f t − 1 ] = t a r g e t nums[left-1]=target nums[left−1]=target说明之前找到了目标值的。
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums: return 0
n = len(nums)
left, right = 0, n-1
if target < nums[0] or target > nums[-1]: return 0#值不在范围内,排除
left, right = 0, n-1
while left <= right:#再搜索右边界
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
if nums[left - 1] == target:
end = left - 1
else:
return 0
return end - start + 1
- 综上所述
- 找左边界:
{ l e f t = m i d + 1 , i f m i d < t a r r i g h t = m i d − 1 , i f m i d ≥ t a r \begin{cases} left = mid + 1,&if \:mid<tar\\ right = mid -1,&if\:mid\ge tar \end{cases} {left=mid+1,right=mid−1,ifmid<tarifmid≥tar - 找右边界:
{ l e f t = m i d + 1 , i f m i d ≤ t a r r i g h t = m i d − 1 , i f m i d > t a r \begin{cases} left = mid + 1,&if \:mid\leq tar\\ right = mid -1,&if\:mid> tar \end{cases} {left=mid+1,right=mid−1,ifmid≤tarifmid>tar
- 找左边界:
相关题目
4. 寻找两个有序数组的中位数
题目:给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。你可以假设 nums1 和 nums2 不会同时为空。
- 分析:数组有序,说明可以使用二分,但是如果合并数组那么肯定不满足时间限制。承接上面的思路,我们可以在两个数组中各自划分一条线,把数组变成左右两边。只要两个数组的左边个数等于右边的个数和。或者两个数组的左边个数和 等于 右边个数和加一,那么我们就可以求的中位数了。
- 奇数个数:奇数个数的时候,分割线左边的个数和始终要大于右边的个数和加一个。我们的中位数就在两个分割线左边值里面去一个最大数。
- 偶数个数:左右两边的个数相等,中位数就是中间的两个数取平均值。但是由于分割线左右两边有四个元素,我们应该取左边的最大值, 右边的最小值计算平均
- 二分法的细节:
- 第一个数组左边的值,肯定要小于第二个数组右边的值
- 第一个数组右边的值要大于第二个数组左边的值
- 如果第一个数组右边的值小于第二个数组左边的值,那么那么说明一个数组的分割线靠左了,应该右移。
- 反之,如果第一个数组的左边的值大于第二个数组的右边的值,说明该分割线取得靠右了,需要左移。
- 在分割线位于左右端点时候,需要特殊判断取正负无穷值。
-解题模板:
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int])->float:
n1 = len(nums1)
n2 = len(nums2)
if n1 > n2:
nums1, nums2 = nums2, nums1
n1, n2 = n2, n1
left, right = 0, n1
while left < right:#进行二分查找
i = (left + right) // 2
j = (n1 + n2 + 1)//2 - i#这里多加一个1,意味着左边个数等于,或者比右边个数多个1
if nums1[i] < nums2[j-1]:
left = i + 1
else:
right = i
i = left
j = (n1 + n2 +1) // 2 -i
#左边越界的时候取负无穷
m1 = nums1[i-1] if i > 0 else -float('inf')
m2 = nums2[j-1] if j > 0 else -float('inf')
if (n1+n2) % 2 == 1:
return max(m1, m2)#为奇数个的时候, 取左边界的最大值
#右边越界的时候取正无穷
m3 = nums1[i] if i < n1 else float('inf')
m4 = nums2[j] if j < n2 else float('inf')
if (n1+n2) % 2 == 0:#为偶数个的时候,取左边界的最大值,右边界的最小值,两者求一个平均
return (max(m1, m2)+min(m3,m4)) / 2
29. 两数相除
题目:给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。返回被除数 dividend 除以除数 divisor 得到的商。整数除法的结果应当截去(truncate)其小数部分。
- 分析:被除数和除数之间有一个倍数关系,我们不知道倍数是多少,但是直到倍数的上下界是[1, dividend],所以我们可以使用二分查找在区间里面找一个合适的倍数。相当于查找一个倍数的左边界。
- 解题模板:
class Solution:
def divide(self, dividend: int, divisor: int) -> int:
#将dividend和divisor转为正数,并确定商的符号
divd,div = abs(dividend),abs(divisor)
sig = (dividend>=0) ^ (divisor<=0)#取异或,True为正,False为负
if divd >= 2**31 and div==1 and not sig :return -2**31
if divd >= 2**31 and div==1 and sig:return 2**31-1
#二分查找(查找1-divd中)
left = 0
right = divd
while left<right:#左闭右开
mid = left + (right-left+1)//2
if mid*div <= divd:#[mid,right]
left = mid
else:#[0,mid)
right = mid-1
return left if sig else -left
33. 搜索旋转排序数组
题目:假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。你的算法时间复杂度必须是 O(log n) 级别。
-
分析:升序序列直接就是对目标值进行判断在左边还是在右边。但是移位后的序列,因为数据的分布原因,不能说直接根据中间值的大小判断目标值在左边或者在右边,所以需要进行判断,中间的那个数值是属于左列表还是属于右列表。
-
例如:[ 0,1,2,3,4] 升序;[3,4,0,1,2] 右列表占优;[2,3,4,0,1] 左列表占优。可以使用mid 和right 比较判断是左占优还是右占优。
-
解题模板:
class Solution:
def search(self, nums: List[int], target: int) -> int:
count = len(nums)
if count == 0: return -1#列表为空,返回-1
left, right = 0, count-1#左闭右闭
while left <= right: #循环跳出条件
mid = (left + right) // 2 #计算中值
if nums[mid] == target: #中间值刚好等于目标值,则输出
return mid
if nums[left] <= nums[mid]:#原始列表为左占优(包含升序列表)
if nums[left]<= target < nums[mid]:#如果目标在列表左边,由于mid已经判断过了
right = mid - 1
else:#如果目标在列表右边
left = mid + 1
else:#列表的顺序为移位后的顺序
if nums[mid] < target <= nums[right]:#判断目标是否在列表右边,右占优
left = mid + 1
else:#目标是否在列表左边
right = mid - 1
return -1
34. 在排序数组中查找元素的第一个和最后一个位置
题目:给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。
- 分析:使用二分查找,但是在找到目标值后不能停止,还需要继续找边界。
- 大于:当mid 大于目标值时,right 就要改变, 由于右闭,所以变成 right = mid-1 。
- 小于:当mid小于目标值时,这个时候就是移动左边界,由于左闭,所以 left = mid + 1
- 等于:当mid与目标值等于的时候,因为要求左边界的值,所以需要收敛右边界,则right =mid-1
- 类似的,求右边界也是如此
- 注意还有一个首位值特判,如果不存在则置-1
- 解题模板:
class Solution:
def searchRange(self, nums: List[int], target: int) -> int:
if not nums: return [-1,-1]
n = len(nums)
left, right = 0, n-1
if target < nums[0] or target > nums[-1]: return [-1,-1]
while left <= right:#首先搜索左边界
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
right = mid - 1
if nums[right + 1] == target:
start = right + 1
else:
return [-1, -1]
left, right = 0, n-1
while left <= right:#再搜索右边界
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
if nums[left - 1] == target:
end = left - 1
else:
return [-1,-1]
return [start, end]
35. 搜索插入位置
题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
- 分析:这道题比简单,就是传统的二分找左索引。
- 解题模板:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
count = len(nums)
left, right = 0, count-1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
50. Pow(x, n)
题目:实现 pow(x, n) ,即计算 x 的 n 次幂函数。
- 分析:传统方法折半搜索: x^n = (x*x)^(n/2)。如果此时n是偶数,直接把上次递归得到的值算个平方返回即可,如果是奇数,则还需要乘上个x的值。还有一点需要引起我们的注意的是n有可能为负数,对于n是负数的情况,我们可以先用其绝对值计算出一个结果再取其倒数即可。我们让i初始化为n,然后看i是否是2的倍数,是的话x乘以自己,否则res乘以x,i每次循环缩小一半,直到为0停止循环。最后看n的正负,如果为负,返回其倒数。
- 解题模板:
class Solution:
def myPow(self, x: float, n: int) -> float:
if n == 0 or x == 1.0:return 1
if (n > 0 and n % 2 == 0): return self.myPow(x * x, n // 2)#折半计算
elif (n > 0): return self.myPow(x, n-1) * x# n为奇数,逐个计算,但是下一次就会为偶数
else: return 1/self.myPow(x, -n)#当 n 为负数的时候
69. x 的平方根
题目:实现 int sqrt(int x) 函数。计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
- 分析:使用二分查找比较
- 解题模板:
class Solution:
def mySqrt(self, x: int) -> int:
if x == 1: return x
left, right = 1, x
while left < right :#左闭右开
mid = (left + right) // 2
m = mid * mid
if m == x: return mid
if m > x: right = mid
else: left = mid + 1
return left - 1
74. 搜索二维矩阵
题目:编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:每行中的整数从左到右按升序排列。每行的第一个整数大于前一行的最后一个整数。
- 分析:矩阵数值的排布很有特点,相当于是做两次二分,行首数值做一次二分,行内数值做一次二分。
- 解题模板:
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
if not matrix or not matrix[0]: return False
m, n = len(matrix), len(matrix[0])
top, down = 0, m-1
while top < down:#二分法,这里取的是左边界
mid = (top + down + 1) // 2#注意这里多加了一个1
if matrix[mid][0] > target:
down = mid - 1
else:
top = mid
left, right = 0, n-1
while left < right:#这里取得是右边界
mid = (left + right) // 2
if matrix[top][mid] < target:
left = mid + 1
else:
right = mid
return (matrix[top][left] == target)
81. 搜索旋转排序数组 II
题目:假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
- 分析:这道题是承接33. 搜索旋转排序数组和剑指 Offer 11. 旋转数组的最小数字这两道题的方法组合而成的。
- 首先是搜索旋转排序数组这道题,提供了一个在区分左右占优区间之后,再一次区分target目标值是在左区间还是右区间的思路。
- 剑指这道题,提供了一个去除重复数字对搜索的影响。那就是对中,右两个数进行判断,区别是左占优,还是右占优。但是在中等于右的时候,这个条件就不成立,因为此时可能除了中右相等,可能左中右都相等。待寻找的值可能在左边,也可能在右边,例如下面这个例子,所以我们只有逐个缩小区间进行进一步分析。
- 例如: [0,0,0,0,1,0] 和 [0,1,0,0,0,0], 比较mid, right不能直到1在左边还是右边。这个时候中右,甚至左中右都相等,我们无法直接知道目标值是在左区间还是右区间,所以只有逐个缩小区间范围。那为什么是右区间减一了? 首先,中右的值是相等的,我们去掉最右边的值,在取中值的时候,也相当于中值左移,并且去掉右边的值,对元素索引没有影响,如果去掉左边的,或者中间的值,对元素的索引会产生
- 解题模板:
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 target == nums[mid]: return True
if nums[mid] < nums[right]:#右占优
if nums[mid] < target <= nums[right]:#目标值在右边
left = mid + 1
else:
right = mid - 1
elif nums[mid] > nums[right]:#左边占优
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:#中右相等,此时缩小区间
right -= 1
return False
153. 寻找旋转排序数组中的最小值
题目:假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。请找出其中最小的元素。你可以假设数组中不存在重复元素。
- 分析:
- 中大于右:最小值在右区间,于是left = mid + 1
- 中小于右:最小值在左区间,或者中间值就刚好是最小值,这个时候right = mid,不能把mid舍去
- 解题模板:
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 target == nums[mid]: return True
if nums[mid] < nums[right]:#右占优
if nums[mid] < target <= nums[right]:#目标值在右边
left = mid + 1
else:
right = mid - 1
elif nums[mid] > nums[right]:#左边占优
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:#中右相等,此时缩小区间
right -= 1
return False
154. 寻找旋转排序数组中的最小值 II
题目:假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。请找出其中最小的元素。注意数组中可能存在重复的元素。
- 分析:使用二分判断做右区间谁占优,由于可能存在重复值在区间的mid和right相等,所以如果出现这种情况只能一个一个缩小右边界。
- 解题模板:
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[left]
162. 寻找峰值
题目:峰值元素是指其值大于左右相邻值的元素。给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
- 分析:寻找峰值,就是找一个数即大于左边,也大于右边。由于极值可能处于列表两端。所以开始就在元数的左右两端加一个负无穷,最后得到的峰值坐标减一就可以了。
- 解题模板:
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
nums = [-float('inf')] + nums + [-float('inf')] #左右加负无穷
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[mid-1] and nums[mid] > nums[mid+1]:return mid - 1
if nums[mid-1] < nums[mid] < nums[mid+1]:#递增数列,峰值在右
left = mid + 1
else:#其他情况峰值在左
right = mid
209. 长度最小的子数组
题目:给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
- 分析:可以使用滑动窗口法进行滑动求解,如果和大于s,那么就需要缩小左边界,并且判断缩小之后 的总和是否满足大于等于s。每次不满足总和大于等于s,我们就要右移right, 加入一个新的数进来。
- 使用二分法:num[i]的值是前i个数的总和, nums[j] - nums[i]就是( j , i ]的总和,所以我们可以使用二分进行区间缩小。区间的上界就是所有数的总和,下界就是s。nums[j]-s就是找一个前缀的左索引
- 解题模板:
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
ans = n + 1
sums = [0]
for i in range(n):
sums.append(sums[-1] + nums[i])
for i in range(1, n + 1):
target = s + sums[i - 1]
bound = bisect.bisect_left(sums, target)
if bound != len(sums):
ans = min(ans, bound - (i - 1))
return 0 if ans == n + 1 else ans
240. 搜索二维矩阵 II
题目:编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:每行的元素从左到右升序排列。每列的元素从上到下升序排列。
- 分析:每行的元素从左到右升序排列。每列的元素从上到下升序排列越往左上角值越小,越往右下角值越大。所以如果但前值比目标值大的话,我们应该减小当前值,选择左移,或者上移。如果目标值比当前值小的话,选择右移或者下移。但是这样会出现两种选择,其实我们可以设定初始点为右上角,或者左下角,这样一来,我们变大或者变小就只需要执行一个方向的操作了。
- 题解模板:
class Solution:
def searchMatrix(self, matrix, target):
"""
:type matrix: List[List[int]]
:type target: int
:rtype: bool
"""
if not matrix: return False
m, n = len(matrix), len(matrix[0])
i, j = 0, n-1
while i < m and j >= 0:
if matrix[i][j] == target:
return True
if matrix[i][j]> target:
j -= 1
else:
i += 1
return False
287. 寻找重复数
题目:给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
- 分析:二分法的思路是先猜一个数(有效范围 [left, right]里的中间数 mid),然后统计原始数组中小于等于这个中间数的元素的个数 cnt,如果 cnt 严格大于 mid,(注意我加了着重号的部分「小于等于」、「严格大于」)。根据抽屉原理,重复元素就在区间 [left, mid] 里;
- 解题模板:
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
left, right = 0, len(nums)-1
while left < right:#每次找一个中位数
mid = (left + right) // 2
cnt = 0#计算得到比中位数小于等于的值的个数
for num in nums:
if num <= mid:
cnt += 1
if cnt > mid:#如果比mid小于等于的值的个数大于mid,那么重复的数肯定在mid左边
right = mid
else:#否则,重复的数在mid右边
left = mid + 1
return left#最后弹出left,就是为重复值
300. 最长上升子序列
题目:给定一个无序的整数数组,找到其中最长上升子序列的长度。
- 分析:这道题可以使用动态规划来做,这里主要是使用二分查找,如果待加入的值比递增栈的最后一个元素小,那么就使用 二分查找找到栈中的一个合适进行元素交换,方便以后出现的值进行计数。
- 解题模板:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
size = len(nums)
# 特判
if size < 2:
return size
# 为了防止后序逻辑发生数组索引越界,先把第 1 个数放进去
tail = [nums[0]]
for i in range(1, size):
# 【逻辑 1】比 tail 数组实际有效的末尾的那个元素还大
# 先尝试是否可以接在末尾
if nums[i] > tail[-1]:
tail.append(nums[i])
continue
# 使用二分查找法,在有序数组 tail 中
# 找到第 1 个大于等于 nums[i] 的元素,尝试让那个元素更小
left = 0
right = len(tail) - 1
while left < right:
# 选左中位数不是偶然,而是有原因的,原因请见 LeetCode 第 35 题题解
# mid = left + (right - left) // 2
mid = (left + right) >> 1
if tail[mid] < nums[i]:
# 中位数肯定不是要找的数,把它写在分支的前面
left = mid + 1
else:
right = mid
# 走到这里是因为【逻辑 1】的反面,因此一定能找到第 1 个大于等于 nums[i] 的元素,因此无需再单独判断
tail[left] = nums[i]
return len(tail)
315. 计算右侧小于当前元素的个数
题目:给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质:counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
- 分析:由于是计算比右边数字大的个数,所以我们翻转一下列表,求比左边数字大的个数。这样就可以转变为二分查找,减少运算量,计算得到的结果再翻转一遍就是原来的比右边数字大的个数结果。
- 从第一个数字开始,使用二分查找找到一个插入位置,插入位置的索引就是比它小的数字的个数。循环使用二分法找到插入位置插入,最后再翻转输出。
- 解题模板:
from typing import List
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
if not nums: return []#计算为空的情况,直接返回
nums.reverse()#翻转原始列表
n = len(nums)
res = [0] #因为最后一个数字比它小的肯定是 0 个
count = [nums[0]]#将第一个数字写入计数的列表
for key in range(1, n):#计算每一个字符比它小的数字个数,使用二分查找
start = 0
end = key-1
#while循环内二分查找出当前数字应该排的位置
while start <= end:
mid = (start + end) // 2
if nums[key] > count[mid]:
start = mid + 1
else:
end = mid - 1
#找出当前数字应该排的位置后插入进排序列表
count.insert(start, nums[key])
res.append(start)#写入当前数字的比它小的值
# print(res)
res.reverse()
return res
if __name__ == "__main__":
s = Solution()
print(s.countSmaller([5,2,6,1]))
410. 分割数组的最大值
题目:给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
- 分析:贪心地模拟分割的过程,从前到后遍历数组,用 sum 表示当前分割子数组的和,cnt 表示已经分割出的子数组的数量(包括当前子数组),那么每当 sum 加上当前值超过了 x,我们就把当前取的值作为新的一段分割子数组的开头,并将 cnt 加 1。遍历结束后验证是否 cnt 不超过 m。
二分的上界为数组 nums 中所有元素的和,下界为数组 nums 中所有元素的最大值 - 解题模板:
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
def check(x: int) -> bool:
total, cnt = 0, 1
for num in nums:
if total + num > x:
cnt += 1
total = num
else:
total += num
return cnt <= m
left = max(nums)#下界
right = sum(nums)#上界
while left < right:#二分法,在上界和下界之间找一个值
mid = (left + right) // 2
if check(mid):
right = mid
else:
left = mid + 1
return left
658. 找到 K 个最接近的元素
题目:给定一个排序好的数组,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。如果有两个数与 x 的差值一样,优先选择数值较小的那个数。
- 分析:如果 x 的值就在长度为 size 区间内(不一定相等),要得到 size - 1 个符合题意的最接近的元素,此时看左右边界:
1、如果左边界与 x 的差值的绝对值较小,删除右边界;
2、如果右边界与 x 的差值的绝对值较小,删除左边界;
3、如果左、右边界与 x 的差值的绝对值相等,删除右边界。 - 细节:由于返回长度为k, 所以“最优区间的左边界”的索引的搜索区间为 [0, size - k]。我们先从 [0, size - k] 这个区间的任意一个位置(用二分法就是当前候选区间的中位数)开始,定位一个长度为 (k + 1) 的区间,根据这个区间是否包含 x 开展讨论。
1、如果区间包含 x,我们尝试删除 1 个元素,好让区间发生移动,便于定位“最优区间的左边界”的索引;
2、如果区间不包含 x,就更简单了,我们尝试把区间进行移动,以试图包含 x,但也有可能区间移动不了(极端情况下)
class Solution:
def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
size = len(arr)
left = 0
right = size - k
while left < right:
# mid = left + (right - left) // 2
mid = (left + right) >> 1
# 尝试从长度为 k + 1 的连续子区间删除一个元素
# 从而定位左区间端点的边界值
if x - arr[mid] > arr[mid + k] - x:
left = mid + 1
else:
right = mid
return arr[left:left + k]
719. 找出第 k 小的距离对
题目:给定一个整数数组,返回所有数对之间的第 k 个最小距离。一对 (A, B) 的距离被定义为 A 和 B 之间的绝对差值。
- 分析:一个数组,两两元数之间就有一个差值,我们需要计算出所有的差值,然后在里面找到第k小的差值。上面说的差值都是差值的绝对值,是一个正整数。使用的方法和上一篇287. 寻找重复数是类似的,通过统计小于等于mid的值的个数,然后可以知道本次比mid小的距离数量有多少个。我们的目的就是找到第k小的距离,每个计算的cnt就是比当前mid距离差值小的个数。
- 具体细节:
- 如果cnt大于等于k,说明我们的预设差值mid过大了。比mid小的个数太多了,需要较小mid的值。于是right = mid。
- 如果cnt<k,说明我们设置的mid距离差值太小了,需要增大我们的距离差值mid,于是left = mid + 1。
-解题模板:
class Solution:
def smallestDistancePair(self, nums: List[int], k: int) -> int:
nums.sort()
left, right = 0, nums[-1] - nums[0]#计算出距离差值k的左右范围
while left < right:
mid = (left + right) // 2
cnt, start = 0, 0
for i in range(len(nums)):
while nums[i] - nums[start] > mid:#找出距离差值小于距离设定中值的个数
#[s, i]的距离大于mid, s增加。当[s, i]距离小于mid,说明[s+1,i],[s+2, i]...
#之间所有距离都会小于mid,他们总的个数为 i-start 个
start += 1
cnt += i - start#统计小于mid的个数有多少个
if cnt < k:#cnt个数偏小,mid需要右移
left = mid + 1
else:
right = mid
return left
875. 爱吃香蕉的珂珂
题目:珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
- 分析:这道题还是使用二分法。首先找到二分法的上界和下界,可以知道上界就是列表中最大的那个数,下界设置为1。
- 注意是左闭右开,所以在取上界的right的时候加了一个一
- 在每次判断使用时间的时候,如果不能完全除尽,剩余的余数还需要加一个小时
- 最后有三种情况,cnt 等于,大于,小于 H。当cnt等于H的时候,我们不能立即输出mid,因为这里要找的是最小值,所以还需要寻找一个左边界。于是,right = mid, 寻找左边界。最后把三种情况合并成为两种。
- 解题模板:
class Solution:
def minEatingSpeed(self, piles: List[int], H: int) -> int:
left, right = 1, max(piles)+1
while left < right:
mid = (left + right) // 2
cnt = 0
for i in range(len(piles)):#计算需要的小时数
if piles[i] > mid:
cnt += (piles[i] // mid)
if piles[i] % mid != 0:#有余数不能除尽,增加一个小时
cnt += 1
else:#不足一个小时的量的,增加一个小时
cnt += 1
if cnt > H:#时间用的比较多,加大进食量
left = mid + 1
else:#时间用的少,可以减少进食量
right = mid
return left
911. 在线选举
题目:在选举中,第 i 张票是在时间为 times[i] 时投给 persons[i] 的。现在,我们想要实现下面的查询函数: TopVotedCandidate.q(int t) 将返回在 t 时刻主导选举的候选人的编号。在 t 时刻投出的选票也将被计入我们的查询之中。在平局的情况下,最近获得投票的候选人将会获胜。[0,1,1,0,0,1,0], [0,5,10,15,20,25,30]表示:第 0 分钟投给 0 号; 第 5 分钟投给 1 号; 第 10 分钟投给 1 号; 第 15 分钟投给 0 号; 第 20 分钟投给 0 号; 第 25 分钟投给 1 号; 第 30 分钟投给 0 号;
- 分析:首先我们需要计算出times时刻的优胜者是谁,然后对输入的t进行查找左边界,输出对应的优胜者。
- 解题模板:
class TopVotedCandidate:
def __init__(self, persons: List[int], times: List[int]):
self.n = len(times)
self.times = times
self.memo = {}
self.win = [[] for _ in range(self.n)]
dic = {}
maxcnt = 0
for i in range(self.n):#找到每一个时刻的优胜者是谁
if persons[i] not in dic:
dic[persons[i]] = 1
else:
dic[persons[i]] += 1
if dic[persons[i]] >= maxcnt:#大于等于,因为票数相同,优胜者为最近的得票者
maxcnt = dic[persons[i]]
self.win[i] = persons[i]
else:#优胜者没变
self.win[i] = self.win[i-1]
# print(self.win)
def q(self, t: int) -> int:
left, right = 0, self.n
if t in self.memo:
return self.memo[t]
while left < right:#进行二人分查找
mid = (left+right)//2
if self.times[mid] == t:#找到时间后弹出
left = mid+1
break
if self.times[mid] > t:
right = mid
else:
left = mid + 1
self.memo[t] = self.win[left-1]
return self.memo[t]
# Your TopVotedCandidate object will be instantiated and called as such:
# obj = TopVotedCandidate(persons, times)
# param_1 = obj.q(t)
1011. 在 D 天内送达包裹的能力
题目:传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力。
-
分析:类似于珂珂吃香蕉,使用二分法。使用二分法,计算一个mid运载能力,然后根据运载能力算出总共需要的天数。
- 如果需要的天数大于D, 那么此时说明我们的mid运载能力小了,应该增大运载能力,即 left = mid + 1。
- 如果需要的天数小于等于D, 说明此时的运载能力超过了或者刚刚好,这时候我们应该缩小运载能力,即right= mid + 1。
在固定运载能力计算需要天数的时候有几种情况:(本次货物重量weight[; 之前的没有运走的为 res)
-
细节:
- res == 0, weight == mid,刚好没有剩余,cnt加一
- res != 0, weight == mid,这个时候只能把之前的作为一批,本次的做一批,算成两批,cnt += 2
- weight < mid, res + weight < mid, 说明还可以继续加,cnt不变
- weight < mid, res + weight == mid, 刚好算一批,cnt加一,记得要res 清零
- weight < mid, res + weight > mid, 之前的算作一批,本次的留作res在计算,cnt 加一
- weight > mid: 这个不可能出现,因为我们定上下界的时候已经定好了,肯定是单个的可以算作一批的
- 最后循环完之后,如果res != 0,说明还有一些剩余的,这时候我们cnt加一,再送走一次。
-
解题模板:
class Solution:
def shipWithinDays(self, weights: List[int], D: int) -> int:
n = len(weights)
left, right = max(weights), sum(weights)#计算上下界
while left < right:#开始进行二分查找
mid = (left + right) // 2
cnt, res = 0, 0
for i in range(n):#每次都对货物进行一次遍历,找出需要的天数
if weights[i] == mid and res != 0:
cnt += 2
res = 0
continue
elif weights[i] == mid and res == 0:
cnt += 1
continue
elif weights[i] < mid and res + weights[i] < mid:
res += weights[i]
continue
elif weights[i] < mid and res + weights[i] == mid:
cnt += 1
res = 0
continue
elif weights[i] < mid and res + weights[i] > mid:
cnt += 1
res = weights[i]
continue
#elif weights[i] > mid:
#cnt = float('inf')
if res != 0: cnt += 1#最后一天还有剩余,再送一个批次
if cnt > D:#天数过多,增加运载能力
left = mid + 1
else:#天数刚好,或者过少,减少运载能力
right = mid
return left
1552. 两球之间的磁力
题目:在代号为 C-137 的地球上,Rick 发现如果他将两个球放在他新发明的篮子里,它们之间会形成特殊形式的磁力。Rick 有 n 个空的篮子,第 i 个篮子的位置在 position[i] ,Morty 想把 m 个球放到这些篮子里,使得任意两球间 最小磁力 最大。已知两个球如果分别位于 x 和 y ,那么它们之间的磁力为 |x - y| 。给你一个整数数组 position 和一个整数 m ,请你返回最大化的最小磁力。
在代号为 C-137 的地球上,Rick 发现如果他将两个球放在他新发明的篮子里,它们之间会形成特殊形式的磁力。Rick 有 n 个空的篮子,第 i 个篮子的位置在 position[i] ,Morty 想把 m 个球放到这些篮子里,使得任意两球间 最小磁力 最大。
已知两个球如果分别位于 x 和 y ,那么它们之间的磁力为 |x - y| 。
给你一个整数数组 position 和一个整数 m ,请你返回最大化的最小磁力。
- 分析:先确定二分的内容是什么?这里的二分内容是磁力。磁力的最大值就是两个端点间的距离,磁力的最小值就是两两篮子之间的最小距离。怎么移动二分值,如果使用当前的mid二分值去篮子里面取值,可以得到的区间数量大于等于给出的(m-1)个区间数量。说明mid偏小。反之,如果得到的区间数量小于(m-1)个区间数量,那么说明二分之偏大
- 解题模板:
class Solution:
def maxDistance(self, position: List[int], m: int) -> int:
position.sort()
n = len(position)
left = min([position[i+1]-position[i] for i in range(n-1)])#取得间隔磁力的最大值,最小值
right = position[-1]-position[0]
def check(dis):
i, j = 0, 0
count = 0
while j < n:#从左到右计算两两之间的长度间隔,如果有大于等于所取的dis, 并且个数为大于等于m-1个
while j < n and position[j]-position[i]<dis:#
j += 1
if j < n:
count += 1
i = j
return count >= m-1#因为m是篮球的个数,所以组成的区间间隔有m-1个
while left <= right:#二分法找到合适的值,注意这里是闭区间
dis = left + (right-left)//2
if check(dis):
left = dis+1
else:
right = dis-1
return left-1