Leetcode 刷题笔记之:二分查找

目录

基础背景

二分查找主要有几个要素,三个指针分别指向left, right, mid,停止条件等。下面就是一个二分查找的标准模板

之后部分会分别介绍二分查找的三种模板。

704. 二分查找

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)-1
        while left + 1 < right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid
            if nums[mid] < target:
                left = mid
            if nums[mid] > target:
                right = mid
        # 还剩下两种情况,手动判断
        if nums[left] == target:
            return left
        if nums[right] == target:
            return right
        return -1 

什么时候可以采用二分查找?

每当需要查询集合中的元素或者其索引时,都可以采用二分查找。二分查找的前提是集合元素有序,所以对于无序集合需要先排序。

二分查找的三个部分

  1. 排序(有序集合忽略)
  2. 二分查找
  3. 在剩余空间在选取结果(二分查找可能会结束语right-left=1)

模板一

这是一个标准的二分查找模板

def binarySearch(nums, target):
    """
    :type nums: List[int]
    :type target: int
    :rtype: int
    """
    if len(nums) == 0:
        return -1

    left, right = 0, len(nums) - 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
            
    # End Condition: left > right
    return -1

这个模板的好处在于,结束时left>right,所以不需要后续处理。适用于查找通过单个索引访问的元素。

几个关键的点

初始化:left = 0, right = len(nums) - 1
循环条件: left <= right
向左查询: right = mid - 1
向右查询: left = mid + 1

69. x 的平方根

确定左边界,右边界。X的平方根不可能大于X/2,所以以此设计好左右边界

跟模板不同的地方取决于题目性质。需要注意的地方是中点的取值,左右取决于题目的分析

class Solution:
    def mySqrt(self, x: int) -> int:
        if x == 0:
            return 0
       
        left = 1
        right = x // 2
        while left < right:
            mid = (left + right + 1) >> 1 # 取右中点, 否则当left+1=right时left=mid<right会陷入死循环
            temp_square = mid * mid
            if temp_square > x:
                right = mid - 1
            if temp_square <= x:
                left = mid
        return left

374. 猜数字大小

常规的二分查找思路,需要注意的是跟模板之间的差别

# The guess API is already defined for you.
# @param num, your guess
# @return -1 if my number is lower, 1 if my number is higher, otherwise return 0
# def guess(num: int) -> int:

class Solution:
    def guessNumber(self, n: int) -> int:
        left, right = 1, n
        mid = (left + right + 1) >> 1
        ans = guess(mid)
        while ans != 0:
            # print(f"now left {left}, right {right}, mid {mid}")
            if ans == 1:
                # print(f"too small")
                left = mid
                mid = (left + right + 1) >> 1 # 取右边的点
                ans = guess(mid)
            if ans == -1:
                # print("too large")
                right = mid
                mid = (left + right) >> 1
                ans = guess(mid)
        # print(f"correct {mid}")
        return mid

33. 搜索旋转排序数组

记住,所有二分法都由模板改进,所有的终点都在于左右指针究竟哪个变换到mid

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        def isOrder(left, right): # 看是不是排序的数组
           return nums[left] < nums[right]

        if not nums:
            return -1

        left, right = 0, len(nums)-1
        while left <= right:
            mid = (left + right) // 2
            if target == nums[mid]:
                return mid
            if isOrder(left, 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 -1

模板二

def binarySearch(nums, target):
    """
    :type nums: List[int]
    :type target: int
    :rtype: int
    """
    if len(nums) == 0:
        return -1

    left, right = 0, len(nums)
    while left < right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid

    # Post-processing:
    # End Condition: left == right
    if left != len(nums) and nums[left] == target:
        return left
    return -1

这个不好用,我们忽略它

模板三

def binarySearch(nums, target):
    """
    :type nums: List[int]
    :type target: int
    :rtype: int
    """
    if len(nums) == 0:
        return -1

    left, right = 0, len(nums) - 1
    while left + 1 < right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid
        else:
            right = mid

    # Post-processing:
    # End Condition: left + 1 == right
    if nums[left] == target: return left
    if nums[right] == target: return right
    return -1

大部分场景下我们可以使用模板三

关键点:确保每个步骤都有3个或者3个以上元素,结束时剩下left和right两个元素

初始条件: left = 0, right = length - 1
循环条件: left + 1 < right
向左右查找都是直接赋值mid

278. 第一个错误的版本

模板三的标准应用

# The isBadVersion API is already defined for you.
# @param version, an integer
# @return an integer
# def isBadVersion(version):

class Solution:
    def firstBadVersion(self, n):
        """
        :type n: int
        :rtype: int
        """
        if n == 1:
            return 1
        
        left, right = 1, n
        while left + 1 < right:
            mid = (left + right) // 2
            if isBadVersion(mid):
                right = mid
            else:
                left = mid
        if isBadVersion(left):
            return left
        else:
            return right

162. 寻找峰值

这道题仍然使用模板三,注意判断峰值的两种特殊情况

class Solution:
    def findPeakElement(self, nums: List[int]) -> int:
        def isPeak(i): # 判断是不是顶峰元素,注意有首尾两种特殊情况
            if i == 0:
                return nums[1] < nums[0]
            if i == len(nums):
                return nums[len(nums)-1] < nums[len(nums)]

            return nums[i-1] < nums[i] and nums[i+1] < nums[i]
        
        if len(nums) == 1:
            return 0

        left, right = 0, len(nums)-1
        while left + 1 < right:
            mid = (left + right) // 2
            if isPeak(mid):
                return mid
            if nums[mid+1] > nums[mid]:
                left = mid
            else: 
                right = mid 

        if isPeak(left):
            return left
        return right

153. 寻找旋转排序数组中的最小值

class Solution:
    def findMin(self, nums: List[int]) -> int:
        def isOrder(left, right):
            return nums[left] <= nums[right]

        left, right = 0, len(nums)-1
        if len(nums) == 1:
            return nums[0]  
        if isOrder(left, right):
            return nums[0]
        while left + 1 < right:
            mid = (left + right) // 2
            # print(f"left {left}, right {right}, mid {mid}")
            if isOrder(left, mid):
                left = mid
            else:
                right = mid
        # print(left, right)
        if isOrder(left, right):
            return nums[left]
        else:
            return nums[right]

154. 寻找旋转排序数组中的最小值 II

相比之前多了限制条件,就是数字可能会有重复。遇到这种题不应该深入细节去思考数字重复的几种情况,而是应该思考这样的限制条件会对原始的模板造成什么样的影响。

原始的模板答案,针对mid与right的比较来确定mid在左边序列还是右边序列,从而移动指针。

加入重复的限制,我们会发现,当mid=right的时候无法判断左右序列,这个时候我们要移动指针,就可以考虑移动单步,左边或者右边,经过验证我们发现移动右边并不会对结果造成影响,所以右指针左移一步即可使得mid继续改变。

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]: # mid在右边序列
                # 搜索范围[left, mid]
                right = mid
            elif nums[mid] > nums[right]: # mid在左边序列
                # 搜索范围[mid+1, right]
                left = mid + 1
            else: # 无法判断在哪个序列
                # 无论是在右序列还是左序列,right -= 1不会改变最小值的存在
                # 搜索范围[left, right-1]
                right = right - 1
        return nums[left]

34. 在排序数组中查找元素的第一个和最后一个位置

这道题是二分查找的一个变种,如果按照传统的二分法,会发现当nums[mid]==target的时候仍然不知怎么做,因为mid可能是在左右边界中,所以需要把这道题转换成一个求左边界,再求右边界的问题,运用两个二分查找。

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        if not nums:
            return [-1,-1]
            
        left, right = 0 ,len(nums)-1
        # 第一次二分查找,找左边界, 使用模板一
        while left < right:
            mid = (left + right) // 2
            if nums[mid] < target:
                # 搜寻范围为[mid+1, right]
                left = mid + 1
            elif nums[mid] == target:
                # 搜寻范围为[left, mid],因为左边还可能有target
                right = mid
            else:
                # 搜寻范围为[left, mid-1]
                right = mid - 1
        if nums[left] == target:
            # 这里要确认一下左边界是否存在
            left_bd = left
        else:
            # 不存在说明数组中没有target值,直接返回[-1,-1]
            return [-1, -1]
          
        # 第二次二分查找,找右边界, 使用模板一
        right = len(nums)-1 # 搜寻范围[left, n-1]
        while left < right:
            mid = (left + right + 1) // 2
            # nums[mid]只有可能>=target
            if nums[mid] > target:
                # 搜寻范围为[left, mid-1]
                right = mid - 1
            else:
                # 搜寻范围为[mid, right]
                left = mid
        if nums[right] == target:
            right_bd = right
            
        return [left_bd, right_bd]

658. 找到 K 个最接近的元素

这道题有几个要素。

  1. 将找到K个元素确定为找到答案的最左边界,利用二分法
  2. 确定最后的答案是什么样?最后的答案一定包含了x(或者说离x最近的元素),其次一定是左边开始第一个达到(右边比左边离x更加)的条件。由于我们的二分法可以确定是left是在从0开始慢慢右移,所以第一个满足要求的左边界就是我们的答案。
class Solution:
    def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
        # 以划窗的思维确定左边界的范围
        left, right = 0, len(arr)-k
        while left < right:
            mid = (left + right) // 2
            if x - arr[mid] > arr[mid + k] - x:
                left = mid + 1
            else:
                right = mid
        return arr[left:left + k]

50. Pow(x, n)

这道题的一个要点就是如果将n分解为可以重复利用的模式:

Pow(n) = Pow(n//2) * P(n//2) (* x )

以及需要注意的是负数的情况

class Solution:
    def myPow(self, x: float, n: int) -> float:
        def computePow(n):
            if n == 1:
                return x
            P = computePow(n//2)
            if n % 2 == 1:
                return  P * P * x
            return P * P
        
        if n == 0:
            return 1
        if n < 0:
            return 1 / computePow(-n)
        return computePow(n)

367. 有效的完全平方数

class Solution:
    def isPerfectSquare(self, num: int) -> bool:
        if num == 0 or num == 1:
            return True
        left = 2
        right = num // 2
        while left + 1 < right:
            mid = (left + right) // 2
            print(left, right, mid)
            sqrt = mid * mid
            if sqrt == num:
                return True
            if sqrt < num:
                left = mid + 1
            else:
                right = mid - 1
        if left * left == num or right * right == num:
            return True
        return False

744. 寻找比目标字母大的最小字母

class Solution:
    def nextGreatestLetter(self, letters: List[str], target: str) -> str:
        left, right = 0, len(letters)-1
        while left + 1 < right:
            mid = (left + right) // 2
            if letters[mid] <= target:
                left = mid
            else:
                right = mid
        if letters[left] > target:
            return letters[left]
        if letters[right] > target:
            return letters[right]
        return letters[0]

349. 两个数组的交集

# 二分法
# 时间复杂度: 排序O(mlogm) + 查找O(n*logm)

class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        def binary_search(x):
            left, right = 0, len(nums1)-1
            while left <= right:
                mid = (left + right) // 2
                if nums1[mid] == x:
                    return nums1[mid]
                if nums1[mid] < x:
                    left = mid + 1
                else:
                    right = mid - 1
            return -1

        if not nums1:
            return []
        results = set()
        nums1 = sorted(nums1)
        for num in nums2:
            res = binary_search(num)
            if res != -1:
                results.add(res)
        return list(results)

350. 两个数组的交集 II

如果使用二分法,跟上一题一模一样,除了储存结果不需要去重

class Solution:
    def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
        def binary_search(x):
            left, right = 0, len(nums1)-1
            while left <= right:
                mid = (left + right) // 2
                if nums1[mid] == x:
                    return nums1[mid]
                if nums1[mid] < x:
                    left = mid + 1
                else:
                    right = mid - 1
            return -1

        if not nums1:
            return []
        results = []
        nums1 = sorted(nums1)
        for num in nums2:
            res = binary_search(num)
            if res != -1:
                results.append(res)
        return list(results)

167. 两数之和 II - 输入有序数组

# 双指针法更好,时间复杂度O(n)
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left, right = 0, len(numbers)-1
        while left < right:
            res =  numbers[left] + numbers[right]
            # print(f"{numbers[left]} + {numbers[right]} = {res}")
            if res == target:
                # print("bingo")
                return [left+1, right+1]
            if res < target:
                # print("too small")
                left += 1
            else:
                # print("too big")
                right -= 1
# 但我们还是用二分查找来做
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        def binary_search(i, num):
            left = i+1
            right = len(numbers) - 1
            while left <= right:
                mid = (left + right) // 2
                res = numbers[mid] + num
                if res == target:
                    return [i+1, mid+1]
                if res < target:
                    left = mid + 1
                else:
                    right = mid - 1
            return -1
        
        for i, num in enumerate(numbers):
            res = binary_search(i,num)
            if res != -1:
                return res

287. 寻找重复数

抽屉原理,如果有10个抽屉,11个苹果,那么一定有至少一个抽屉放了2只或2只以上的苹果

假设现在我们有5个抽屉,每个位置只能存放一种数字,且这个数字小于等于5。但是现在我们发现这5个位置放了6个或者7个数字,说明有一个位置放了不止一个数字,换句话来说,这个数字是重复的数字。

由此二分法雏形出现,我们需要找到这个具体的抽屉的数量。

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        left = 1
        right = len(nums) - 1
        while left < right:
            mid = (left + right) // 2
            count = 0
            for n in nums:
                if n <= mid:
                    count += 1
            if count > mid: # 重复数在[left,mid]
                right = mid
            else: # 重复数在[mid+1, right]
                left = mid + 1
        return left 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值