算法日志2:二分法之茴字的四种写法

前言

对证明的最透彻理解,那就是你把证明背下来

本文的作者为本人的第二人格,参考了二分从入门到入睡这篇文章,


二分查找本质上是把问题归约到可解的子问题,这点同数学归纳法,动态规划等一致

我感觉的二分法和动态规划,数学归纳法区别:

graph LR START{归约到可解子问题}-->A[二分法]-->B[一个可解子问题] START-->C[动态规划]-->D[超多可解子问题] START-->E[数学归纳法]-->F[无限可解子问题]

注意这里的可解子问题不一定要选最小可解子问题,比如到一个差不多合适的颗粒度就可以了,你懂我意思吗,可以手动对齐颗粒度。

举例来说,二分法数组大小缩小到3,就可以直接枚举了,把这三个元素都检查一下找到就直接返回,没找到就返回错误。

但是要注意数组大小3,2,1,0最好都写一下枚举,虽然可能用不到,但是用不到又不大可能,有些问题你可能一开始没考虑到,但是考虑不到又不大可能

如果要化为最小子问题,则为下面四种情况:

有如下的口诀(口诀来自二分从入门到入睡):

  • 左闭右开,交叉中止

  • 左闭右闭,相等中止

  • 左开右开,相邻中止

  • 左开右闭,相等中止

c++和python中,最符合直觉理解最舒服的时左闭右闭,剩下几种可以练习一下

短暂切换回第一人格:

二分子问题实战

  1. 问题要素

    • l 左边界

    • r 右边界

    • m 中间位置

    • nums 问题数组

    • t 待查找值

    • res 最终返回的序号,也可能是-1

  2. 概念对应表, 同级别的概念放同一行, 不要搞错对应

    l r m res
    nums[l] nums[r] nums[m]
  3. 补充知识:

    • m下取整 = (r-l)/2 + l

    • m 上取整 = m下取整 +1

  4. 题目最后要返回的是是res

  5. 万事俱备,开始拼装

python四种尝试:

左闭右闭

class Solution:
    '''
    尝试左闭右闭,相错中止
    注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
   '''
    def search(self, nums: List[int], taret: int) -> int:
        t = target #浅拷贝,t和target是对同一块内存的引用
        l = 0
        r = len(nums) - 1 
        res = -1  #最终的结果
        while(l <= r): # 相错即为r<l
            m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去  
            if t == nums[m]:
                res = m
                return res
            elif t < nums[m]:
                l = l  
                r = m - 1
            elif t > nums[m]:
                r = r
                l = m + 1  
        return res       

如果复用上面的代码,直接左闭右开,发现有问题

class Solution:
    '''
    尝试左闭右开,相等中止
    注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
   '''
    def search(self, nums: List[int], target: int) -> int:
        t = target #浅拷贝,t和target是对同一块内存的引用
        l = 0
        r = len(nums) - 1 
        res = -1  #最终的结果
        while(l < r): # 相等即为r==l
            m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去  
            if t == nums[m]:
                res = m
                return res
            elif t < nums[m]:
                l = l  
                r = m ## 开区间需要多取一个元素,如果r取m-1, 那开区间[l,r)少包住一个元素m-1,万一正好t = nums(m-1),就寄了
            elif t > nums[m]:
                r = r
                l = m + 1  
        return res               

上面的代码,假设数组大小为1,只有一个元素时,while(l < r) 这个循环会直接进不去,直接返回-1,如下图所示

一个自然的想法是,需要特判吗?

错误的,其实根本因为开始时的设置错了,错误设置了r = len(nums) - 1

我们来具体分析下面两种设置

  • l = 0 , r = len(nums) - 1

  • l = 0, r = len(nums)

其实本质上,第一种设置,就是把最开始的数组当成了闭区间,第二种则是把最开始的数组设成了开区间

所以,我们可以得出的一个原则是:

开始,中间,结束,即整个过程中的区间开闭要保持一致

搞清了这点,可将代码修改如下:

class Solution:
    '''
    修改了r 的初始值
    尝试左闭右开,相等中止
    注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
   '''
    def search(self, nums: List[int], target: int) -> int:
        t = target #浅拷贝,t和target是对同一块内存的引用
        l = 0
        r = len(nums) #######  将初始值从len(nums)-1 改为 len(nums)
        res = -1  #最终的结果
        while(l < r): # 相等即为r==l
            m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去  
            if t == nums[m]:
                res = m
                return res
            elif t < nums[m]:
                l = l  
                r = m ## 开区间需要多取一个元素,如果r取m-1, 那开区间[l,r)少包住一个元素m-1,万一正好t = nums(m-1),就寄了
            elif t > nums[m]:
                r = r
                l = m + 1  
        return res         

可以看到,代码被一把抓住,顷刻炼化!

左开右开也是如此,需要保持前后区间一致

class Solution:
    '''
    尝试左开右开,相临中止
    注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
   '''
    def search(self, nums: List[int], target: int) -> int:
        t = target #浅拷贝,t和target是对同一块内存的引用
        l = -1 ## 将初始值从0改为-1, 保证开始,过程,结束中区间一致
        r = len(nums) 
        res = -1  #最终的结果
        while(l + 1 < r): # 相临即为r==l+1
            m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去  
            if t == nums[m]:
                res = m
                return res
            elif t < nums[m]:
                l = l  
                r = m ## 开区间需要多取一个元素,如果r取m-1, 那开区间(l,r)少包住一个元素m-1,万一正好t = nums(m-1),就寄了
            elif t > nums[m]:
                r = r
                l = m ## 开区间需要多取一个元素,如果l取m+1, 那开区间(l,r)少包住一个元素m+1,万一正好t = nums(m+1),就寄了
        return res         

下面要进行最神人的代码书写,左开右闭, 按照刚才的套路进行重写

class Solution:
    '''
    尝试左开右闭,相等中止
    注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
   '''
    def search(self, nums: List[int], target: int) -> int:
        t = target #浅拷贝,t和target是对同一块内存的引用
        l = -1
        r = len(nums)-1 
        res = -1  #最终的结果
        while(l < r): 
            m = (r-l)//2 + l # //为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去  
            if t == nums[m]:
                res = m
                return res
            elif t < nums[m]:
                l = l  
                r = m - 1
            elif t > nums[m]:
                r = r
                l = m ## 开区间需要多取一个元素,如果l取m+1, 那开区间(l,r]少包住一个元素m+1,万一正好t = nums(m+1),就寄了
        return res         

发现又寄了,发生肾么事了?

我们来看左闭右开和左开右闭的情况

假设在[5]里找5

左闭右开

[5, 假想)-->5的序号是0,假想的序号是1,由于向下取整,则访问(1-0)/2+0 == 0号元素,可以访问到,且就是5

左开右闭

(假想,5]-->假想的序号是-1,5的序号是0,由于向下取整,我们访问了-1号元素,即nums[-1],但这个元素不存在,是假想的!导致访问超出地址!

解决方案就是向上取整!

class Solution:
    '''
    尝试左开右闭,相等中止
    注意对应 m对应序号,nums[m]对应值,和target进行比较是值比较
   '''
    def search(self, nums: List[int], target: int) -> int:
        t = target #浅拷贝,t和target是对同一块内存的引用
        l = -1
        r = len(nums)-1 
        res = -1  #最终的结果
        while(l < r): 
            m = (r-l)//2 + l + 1 # //改成向上取整,为整数除法, 这句代码为下取整,因为正数除法会将小数部分舍去  
            if t == nums[m]:
                res = m
                return res
            elif t < nums[m]:
                l = l  
                r = m - 1
            elif t > nums[m]:
                r = r
                l = m ## 开区间需要多取一个元素,如果l取m+1, 那开区间(l,r]少包住一个元素m+1,万一正好t = nums(m+1),就寄了
        return res         

喜欢用第四种方法的小馋猫家里得请哈基高了

总结

模版 初始值 循环条件 中间值下标 ( c ) 左右界
模版一
相错终止
( l = 0, r = n - 1 )
( [l, r] ) 左闭右闭
( while(l <= r) ) ( c = l + (r - l) / 2 )
下取整
( l = c + 1 )
( r = c - 1 )
模版二
相等终止
( l = 0, r = n )
( [l, r) ) 左闭右开
( while(l < r) ) ( c = l + (r - l) / 2 )
下取整
( l = c + 1 )
( r = c )
模版三
相等终止
( l = -1, r = n - 1 )
( (l, r] ) 左开右闭
( while(l < r) ) ( c = l + (r - l + 1) / 2 )
上取整
( l = c )
( r = c - 1 )
模版四
相邻终止
( l = -1, r = n )
( (l, r) ) 左开右开
( while(l + 1 < r) ) ( c = l + (r - l) / 2 )
下取整
( l = c )
( r = c )

循环不变量分析

在循环中,要抓住不变量,就是l的左侧始终小于target, r的右侧始终大于target

首先对于任意数组nums,我们进行如下扩充

num[-1] = -inf, num[len(nums)] = inf

例如数组[1,2,3,4] --> [-inf, 1,2,3,4, inf]

开始时l = 0, r = len(num)-1, 也满足l 的左侧始终小于target, r的右侧始终大于target,

假设找5, 则最后 变成 r- > nums[r] = 3 -> 4, l -> nums[l] = 4 -> inf,仍然满足 l左侧始终小于target, r右侧始终大于target

假设找2.5,最后变成 r -> nums[r] = 1 -> 2 , l -> nums[r] = 2 -> 3, 循环不变量仍然成立

假设找3,则能直接找到,并且在找的过程中 l从0 变成了2,r始终没动是3, 仍然满足循环不变量

如果要找大于等于target的范围

循环不变量为,l左侧始终小于target, r的右侧始终大于等于target,

最后变成 [..., r, l, ...] ,由循环不变得出

[...,r]每个元素小于target

[l,...]每个元素大于等于target, 即 target <= [l, ...]

又由于数组是单调不减的, 则 l为 target的数组上界

l 的情况又分为两种,因为我们扩充了数组,所以不可能越界,具体为

  • l < len(num) --> nums[l] in origin nums --> 上界为具体序号

  • l == len(num) --> nums[l] == inf -->返回未找到(找到的上界是我们自己定义的序号)

大于等于,小于,小于等于也是如此这般

练习题

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

python代码

class Solution(object):
    def searchRange(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        l= 0
        r = len(nums)- 1
        lower_edge = len(nums)
        while l<=r :
            m = l+(r-l)/2
            if(nums[m]<target):
                l = m+1 ## l左侧的数都小于target
            else:
                r = m-1 ## r 右侧的数都大于等于target
                ####    [..., r, l, ...], 故 [l, ...] 都大于等于target, 即找到了下界l
            lower_edge = l
        l = 0
        r = len(nums)-1
        upper_edge = -1
        while l<=r:
            m = l+(r-l)/2
            if(nums[m]>target):
                r = m-1 ## r的右侧都大于target
            else:
                l = m+1 ## l的左侧都小于等于target
                #### [...,r,l,...], 故 [...,r]都小于等于target,找到了上界r
            upper_edge = r
        if lower_edge == -1 or lower_edge == len(nums):
            return [-1,-1]
        if upper_edge == -1 or upper_edge == len(nums):
            return [-1,-1]
        if nums[lower_edge] == target and nums[upper_edge] == target:
            return [lower_edge, upper_edge]
        else:
            return [-1,-1]

posted @ 2025-03-20 18:09  玉米面手雷王  阅读(23)  评论(0)    收藏  举报