20211124_datawhale31期_leetcode刷题_3二分查找

15 篇文章 0 订阅
11 篇文章 0 订阅

三 二分查找

请添加图片描述from: https://realpython.com/

来源

Datewhale31期__LeetCode 刷题 :

1 定义、基本思想及步骤

二分查找又称折半查找,它是一种效率较高的查找方法。

二分查找要求:线性表是有序表,即表中结点按关键字有序,并且要用向量作为表的存储结构。不妨设有序表是递增有序的。

  • 步骤

  • 初始状态下,将整个序列作为搜索区域(假设为 [low, high]);

  • 找到搜索区域内的中间元素 half,和目标元素进行比对。如果相等,则搜索成功;如果中间元素大于目标元素,表明目标元素位于中间元素的左侧,将 [low, half-1] 作为新的搜素区域;反之,若中间元素小于目标元素,表明目标元素位于中间元素的右侧,将 [half+1, high] 作为新的搜素区域;

  • 重复执行第二步,直至找到目标元素。如果搜索区域无法再缩小,且区域内不包含任何元素,表明整个序列中没有目标元素,查找失败。


2 简单二分查找python实现

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        low, high = 0, len(nums) - 1

        while low <= high:
            half = (high + low) // 2
            if target == nums[half]:	
                return half

            elif target > nums[half]:   # 小于目标值,则在 [half + 1 , high ] 中继续搜索
                low = half + 1

            elif target < nums[half]:  # 大于目标值,则在 [low, half - 1 ] 中继续搜索
                high = half - 1

        return -1

3 二分查找细节总结

  1. 初始区间问题.
    初始化赋值时, 区间取左闭右闭、左闭右开都可写出相应解法及代码, 但左闭右开这种写法在解决问题的过程中,需要考虑的情况更加复杂,所以建议 全部使用「左闭右闭」区间, 即起始区间为[0, len(nums)], 而非[0, len(nums)-1]

  2. half的取值问题
    常见的 half 取值就是 half = (low + high) // 2 或者 half = low + (high - low) // 2 。前者是最常见写法,后者是为了防止整型溢出。式子中 // 2 就代表的含义是中间数「向下取整」------- 取左。
    half = (low + high + 1) // 2,或者 half = low + (high - low + 1) // 2可以做到取右, 但一般来说,取中间位置元素在平均意义下所达到的效果最好。同时这样写最简单。而对于 half 值是向下取整还是向上取整,大多数时候是选择不加 1.

  3. 边界条件问题----------low <= highlow < high

    • low <= high,且查找的元素不存在,则 while 判断语句出界条件是 low == high + 1,写成区间形式就是 [high + 1, high],此时待查找区间为空,待查找区间中没有元素存在,所以此时终止循环可以直接return -1是正确的
    • 如果判断语句为low < high,且查找的元素不存在,则 while 判断语句出界条件是 low == high,写成区间形式就是 [high, high]。此时区间不为空,待查找区间还有一个元素存在,并不能确定查找的元素不在这个区间中,此时终止循环return -1是错误的。 >>>>>> 改为 return low if nums[low] == target else -1, 且不用判断返回low 还是 high.
  4. 搜索区间范围的选择
    区间范围3种情况:
    low = half + 1、high = half - 1 -----对应直接找法
    low = half + 1 、high = half---- 对应排除法half向下取整(half = low + (high - low) // 2)的情况
    low = half、high = half - 1 ---- 对应排除法half向上(half = low + (high - low + 1) // 2)取整的情况


4 二分法两种思路

  • 即上面提到的直接找法和排除法
    • 直接找法: 在循环体中找到元素就直接返回结果----若nums[half]值大于或小于目标值, 分别取区间[low, half - 1 ][half + 1 , high ]>>>>>>>适用用于简单的,非重复的元素查找值问题
    • 排除法: >>>> 适用于可能不存在的元素,找边界问题等复杂问题
      (1) 取两个节点中心位置 half,根据判断条件先将目标元素一定不存在的区间排除
      (2) 然后在剩余区间继续查找元素,继续根据条件排除不存在的区间。
      (3) 直到区间中只剩下最后一个元素,然后再判断这个元素是否是目标元素。

排除法思路有两种代码:

  • 一、区间分为[half + 1, high][low, half]; 而 half = low + (high - low) // 2:
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        low = 0
        high = len(nums) - 1
        # 在区间 [low, high] 内查找 target
        while low < high:
            # 取区间中间节点
            half = low + (high - low) // 2
            # nums[half] 小于目标值,排除掉不可能区间 [low, half],在 [half + 1, high] 中继续搜索
            if nums[half] < target:
                low = half + 1 
            # nums[half] 大于等于目标值,目标元素可能在 [low, half] 中,在 [low, half] 中继续搜索
            else:
                high = half
        # 判断区间剩余元素是否为目标元素,不是则返回 -1
        return low if nums[low] == target else -1
  • 二、区间分为[low, half - 1][half, high]; 而 half = low + (high - low + 1) // 2:
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        low = 0
        high = len(nums) - 1
        # 在区间 [low, high] 内查找 target
        while low < high:
            # 取区间中间节点
            half = low + (high - low + 1) // 2
            # nums[half] 大于目标值,排除掉不可能区间 [half, high],在 [low, half - 1] 中继续搜索
            if nums[half] > target:
                high = half - 1 
            # nums[half] 小于等于目标值,目标元素可能在 [half, high] 中,在 [half, high] 中继续搜索
            else:
                low = half
        # 判断区间剩余元素是否为目标元素,不是则返回 -1
        return low if nums[low] == target else -1
  • 区分被分为两部分: [low, half - 1] 与 [half, high] 时,half 取值要向上取整。即 half = low + (high - low + 1) // 2。因为如果当区间中只剩下两个元素时(此时 high = low + 1),一旦进入low = half 分支,区间就不会再缩小了,下一次循环的查找区间还是[low, high],就陷入了死循环。

5 相关题目

5.1 374 . 猜数字大小

  • 1到n随机取数>>>>>序列递增有序>>>>>二分查找
class Solution:
    def guessNumber(self, n: int) -> int:

        low, high = 1, n

        while low <= high:
            half = low + (high - low)//2
            if guess(half) == 0:
                return half

            elif guess(half) == -1:
                high = half - 1


            elif guess(half) == 1:
                low = half + 1
        

5.2 35 . 搜索插入位置

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        low, high = 0, len(nums)-1

        while low <= high:
            half = low + (high - low) // 2
            if nums[half] == target:
                return half

            elif nums[half] > target:
                high = half - 1 

            else:
                low  = half + 1
        return low

5.3 69 . Sqrt(x)

  • 法一----------二分
    • 思路: 对于给定的 x , 有 0 ≤ x < x 2 0\le x<x^2 0x<x2 >>>> 对于在区间 [0,x] 判断 x 位于哪个[ n u m 2 num^2 num2
      , ( n u m + 1 ) 2 (num+1)^2 (num+1)2]区间即可, return num即可
class Solution:
    def mySqrt(self, x: int) -> int:
        low = 0
        high = x

        while low < high:
            mid = low + (high - low + 1) // 2
            if mid*mid == x:
                return mid
                
            elif mid*mid > x:
                high = mid - 1

            elif mid*mid < x:
                low = mid  

        return low

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

  • 法一------------优先使用二分查找-------O(NlogN)
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:

        for i in range(len(numbers)):
            goal = target - numbers[i]
            low = i + 1
            high = len(numbers) - 1

            while low <= high:
                mid = low + (high - low) // 2
                if numbers[mid] == goal:
                    return [ i + 1, mid + 1]

                elif numbers[mid] > goal:
                    high = mid - 1

                else:
                    low = mid + 1
  • 法二>>>>>>>>以left和right指针, 和大right左移, 和小left右移
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left, right = 0, len(numbers) - 1
        while left <= right: 
            sum_num = numbers[left] + numbers[right]
            if sum_num == target:
                return [left + 1, right + 1]
            elif sum_num < target:
                left += 1
            else:
                right -= 1

在这里插入图片描述

5.5 1011 . 在 D 天内送达包裹的能力

  • 法一: -----------参考答案------二分
    • 思路: 船最小的运载能力,最少也要等于或大于最重的那件包裹,即 max(weights)。最多的话,可以一次性将所有包裹运完,即 sum(weights)。船的运载能力介于 [max(weights), sum(weights)] 之间。 >>>> 同时先计算运载能力mid = (high + low) // 2 的运输天数, 再与之D比较进而缩小区间:
class Solution:
    def shipWithinDays(self, weights: List[int], days: int) -> int:
        low, high = max(weights), sum(weights)
        while low < high:
            mid = low + (high - low) // 2
            # 计算载重为mid 需要多少天运完
            cur = 0         # 目前天数的重量
            day_mid = 1     # 目前天数
            for weight in weights:
                if weight + cur > mid:
                    day_mid += 1
                    cur = 0
                cur += weight


            if day_mid > days:  #如果天数超了, 则增加载重
                low = mid + 1

            else:
                high = mid
        return low

5.6 278 . 第一个错误的版本

  • 二分法>>>>>>>>> 获得第一个错误版本
class Solution:
    def firstBadVersion(self, n):
        """
        :type n: int
        :rtype: int
        """

        low, high = 1, n

        while low < high:
            mid = low + (high - low) // 2

            if isBadVersion(mid):
                high = mid
            else:
                low = mid  + 1
        return low
  • 若想获得最后一个正确版本可用mid向上取整, 区间在[mid, high][low, mid-1]的方法

5.7 33 . 搜索旋转排序数组

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        k = self.compute_K(nums)
        n = len(nums)
        if target >= nums[0]:   # [0, n- k -1]  [n - k, n - 1]两个递增区间分别二分
            left = 0
            right = n - k -1
        else:
            left = n - k
            right = n - 1

        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid

            elif nums[mid] > target:
                right = mid - 1
            else:
                left = mid + 1
        return -1

# 先二分求k的值
    def compute_K(self, nums):
        low, high = 0, len(nums) - 1
        reference = nums[0]
        while low < high:
            mid = low + (high - low + 1) // 2
            if nums[mid] > reference:
                low = mid
            else: 
                high = mid - 1
        return len(nums) - 1 - low

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

  • 与上题类似用二分法求k
class Solution:
    def findMin(self, nums: List[int]) -> int:
        k = self.compute_K(nums)
        if k == 0:
            return nums[0]

        else:
            return nums[len(nums) - k]

    
    
    
    
    def compute_K(self, nums):
        low, high = 0, len(nums) - 1
        reference = nums[0]
        while low < high:
            mid = low + (high - low + 1) // 2
            if nums[mid] > reference:
                low = mid
            else: 
                high = mid - 1
        return len(nums) - 1 - low

参考资料

  1. 二分法的细节加细节 你真的应该搞懂!!!_小马的博客
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值