Leetcode刷题笔记—二分算法篇

Leetcode刷题笔记—二分算法篇

素材来自网络

链表子串数组题,用双指针别犹豫。
双指针家三兄弟,各个都是万人迷。
快慢指针最神奇,链表操作无压力。
归并排序找中点,链表成环搞判定。
左右指针最常见,左右两端相向行。
反转数组要靠它,二分搜索是弟弟。

滑动窗口老猛男,子串问题全靠它
左右指针滑窗口,一前一后齐头进
自诩十年老司机,怎料农村道路滑。
一不小心滑到了,鼻青脸肿少颗牙。
算法思想很简单,出了bug想升天

一、二分查找

第一题:二分查找

Leetcode74:二分查找:简单题 (详情点击链接见原题)

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

使用二分法的前提条件是数组为有序数组,同时数组中无重复元素【因为一旦有重复元素】使用二分查找法返回的元素下标可能不是唯一的

大家自己写二分法比较混乱的原因是对区间的定义没有想清楚
写二分法,区间定义一般分为两种,左闭右闭即[left, right]或者左闭右开即[left, right)

二分法的两种写法:
case1:target定义在一个左闭右闭的区间[left, right](即当 rightlen(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(log⁡m+log⁡n)
  • 空间复杂度: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 ,数组中的值不必互不相同

本题和上一题大致类似,主要针对于特殊情况,101111111101 这种情况,分不清是前面有序还是后面有序

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 的数组,预先按照升序排列,经由 1n 次 旋转后,得到输入数组。例如,原数组 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) 的算法解决此问题

解题思路
在这里插入图片描述

由上图可知
如果中值 < 右值,说明最小值在左半边,可以收缩右边界
如果 中值 > 右值,说明最小值在右边,可以收缩左边界
通过比较中值和右值即可确定最小值的位置范围,从而确定边界收缩的方向,而 case1case2 中都是左值 小于 中值,但最小值的范围却不同说明只比较左值和中值不能确定最小值的位置范围
所以我们通过比较中值与右值来确定最小值的位置范围从而确定边界收缩的方向

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 的数组,预先按照升序排列,经由 1n 次 旋转 后,得到输入数组。例如,原数组 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. 寻找两个正序数组的中位数:困难题 (详情点击链接见原题)

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

解法一:暴力解法,先将两个数组合并,然后根据奇数还是偶数返回中位数
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 个元素,使得 nums1m1 个元素或者 nums2m2 = k - m1 个元素为中位线位置

我们可以通过二分查找的方法,在数组 nums1 中找到合适的 m1 位置,具体做法如下

  1. left 指向 nums1 的头部位置, right 指向 nums1 的尾部位置
  2. 每次取中间位置作为 m1,则 m2 = k - m1
    • 如果 nums1[m1] < nums2[m2 - 1],则 nums1 的前 m1 个元素都不可能是第 k 个元素,说明 m1 的取值小了,应该将 m1 进行右移操作
    • 如果 nums1[m1] >= nums2[m2 - 1],说明 m1 的取值大了,应该将 m1 进行左移操作
  3. 找到 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 小时内吃掉所有香蕉的最小速度 kk 为整数)

大家在使用二分法的时候很容易陷入思维定式,一看到二分题上来就对区间进行二分,恰恰这道题就反其道而行之,如果纠结于区间很容易陷入死胡同,顺便提一句,这道题是博主本人在面试中碰到的一道手撕代码题,很多大厂面试官特别喜欢在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,以及两个整数 mk
现需要制作 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 的半径可以覆盖所有房子

具体实现:
先对 housesheaters 进行排序,使用 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. 有界数组中指定下标处的最大值:中等题 (详情点击链接见原题)

给你三个正整数 nindexmaxSum 。你需要构造一个同时满足下述所有条件的数组 nums(下标 从 0 开始 计数)

第二题:安排工作以达到最大收益

Leetcode826. 安排工作以达到最大收益

你有 n 个工作和 m 个工人。给定三个数组: difficulty, profitworker ,其中:
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 ,返回其中可以组成三角形三条边的三元组个数

解题思路:

  1. 首先对数组排序
  2. 固定最短的两条边,二分查找最后一个小于两边之和的位置,可以求得固定两条边长之和满足条件的结果

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

第二题:有界数组中指定下标处的最大值

Leetcode1802. 有界数组中指定下标处的最大值

给你三个正整数 nindexmaxSum 。你需要构造一个同时满足下述所有条件的数组 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条件中是否需要添加等号,路漫漫其修远兮~,大家一起加油,文中有不足之处欢迎大家评论指正,如果本文对你有帮助的话,还请多多点赞哦

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_lover_forever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值