二分查找解题思路

二分查找

二分查找也称折半查找(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=mid1;

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=mid1
  • 中间值等于目标值,但是我们是要找左边界,所以区间右端点减一: r i g h t = m i d − 1 right=mid-1 right=mid1
  • 边界处理:如果目标值在首部或者尾部且只有 一个,弹出的时候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[left1]=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=mid1ifmid<tarifmidtar
    • 找右边界:
      { 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=mid1ifmidtarifmid>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。

  1. 首先是搜索旋转排序数组这道题,提供了一个在区分左右占优区间之后,再一次区分target目标值是在左区间还是右区间的思路。
  2. 剑指这道题,提供了一个去除重复数字对搜索的影响。那就是对中,右两个数进行判断,区别是左占优,还是右占优。但是在中等于右的时候,这个条件就不成立,因为此时可能除了中右相等,可能左中右都相等。待寻找的值可能在左边,也可能在右边,例如下面这个例子,所以我们只有逐个缩小区间进行进一步分析。
  3. 例如: [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] )。请找出其中最小的元素。你可以假设数组中不存在重复元素。

  • 分析:
  1. 中大于右:最小值在右区间,于是left = mid + 1
  2. 中小于右:最小值在左区间,或者中间值就刚好是最小值,这个时候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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值