zoukankan      html  css  js  c++  java
  • 二分查找的越界以及边界值初始化问题

    先介绍一个二分查找写的非常好的回答:https://www.zhihu.com/question/36132386/answer/530313852。
    个人认为看完基本能对二分查找有一个很好的理解了,也非常容易就能写出一个二分查找的模板:

    def binary_search(nums, target):
        lo, hi = 0, len(nums)
        while lo < hi:
            mid = lo + (hi - lo) // 2
            if nums[mid] < target:   
                lo = mid + 1
            else:
                hi = mid
        return lo   
    

    其中,最后返回的lo是大于等于目标值的最小元素所在的位置索引。

    不过,这并不是这次博客的重点。二分查找本身并不复杂,但是在leetcode中的特殊场景结合起来之后也会带来一些困扰,比如162.寻找峰值。这题的思路很简单,在数组nums中,用类似二分查找的方法,通过中间值mid和邻接值mid+1,我们可以判断中间值mid是处于上坡阶段还是下坡阶段,并进而缩小查找范围。具体的解答不再赘述,这里要谈的问题是,邻接值mid+1,直接取nums[mid+1]是否会引起越界?在此之前,还有一个问题是,nums[mid]本身会越界吗?我们分几个部分聊聊。

    循环过程中的越界

    可以看到,访问数组的nums[mid]操作都在循环体中。
    我们先来分析:在循环过lo是不可能超过hi的,因为循环的条件就是lo<hi;而根据mid的计算公式,mid始终满足lo<=mid<hi,因此有0<=lo<=mid<hi<=n,因此循环体过程的数组访问是不存在越界问题。

    循环体外的越界

    在一些情况下,我们跳出循环体之后仍然有访问数组的需求。比如一种常见的要求是:如果元素存在于数组当中,返回出现的索引;如果不存在,返回-1。

    这个时候,仅仅得到索引是不够的。我们还需要检查nums[lo]是否和目标值相等,因为有可能数组中不存在恰好等于目标值的元素。虽然循环过程lo<hi,但是当循环结束的时候,lo=hi。而hi的初始化值是n,如果hi一直没变,lo一直增大,就会出现lo=hi=n,访问数组越界的情况。对应的就是目标值大于数组中的所有元素。因此如果将hi初始化为n,那么在最后返回的时候必须检查lo<n以及nums[lo]=target两个条件,否则可能产生越界的问题。

    边界初始化与应用

    在题目162中,我们会发现,此时情况更加复杂,不仅要考虑nums[mid]是否越界了,而且由于使用到了nums[mid+1],还要考虑mid+1这个索引是否越界了。而这个时候,如果将hi初始化为n,我们也确实会发现,mid+1有可能越界。那么,有没有更好的解决办法呢?

    答案是,将hi初始化为n-1。事实是,即使是标准的二分查找,也可以采用这种初始化方法,只需做一些简单的改动;而如果是元素存在于数组当中,返回出现的索引,否则返回-1这种要求,甚至不需要改动其它部分的代码。

    乍一看有些反直觉,因为边界值是一个敏感问题。根据开头链接的文章,我们可以知道,lo和hi实际上是对数组做了一个切分,i>=hi部分的索引,是要满足nums[i]>=target的,但没检查数组的情况下,将hi设置为n-1相当于假定最后一个元素nums[n-1]>=target,似乎有些不合理。但这一初始化方法有两个好处:

    1. 结果和模板中的初始化方法一致,且循环结束的时候必定不会越界。先来说说结果为什么一致,首先要明确的是,这种初始化方法区别仅仅在于认为数组最后一个元素大于等于目标值。因此可以分两种情况:

      1. 查找元素没有超过数组最大值:即nums[n-1]满足target<=nums[n-1]且,那么事实和假定一致,结果自然也一致。
      2. 查找元素超出数组最大值:即target>nums[n-1]。假设不符合事实了,但代入循环我们会发现,终止循环的时候lo=n-1,此时借助nums[lo]=target即可将最后一个元素是超过数组最大值的假定排除掉;甚至由于循环终止的时候,lo=hi=n-1,我们都不需要检查数组是否越界。当然,这一情况仅仅适用于数组在元素时返回对应索引,不在时候返回指定值这一前提。如果是直接返回大于等于目标值的索引,不需要做最后的检查,那么返回索引就可能产生错误的结果了。
    2. 当然仅仅是这样还不够,这一初始化方法真正使用的场合正是162.寻找峰值这样需要使用nums[mid+1]这种的情况。我们在上面有一个重要结论是在整个循环体的过程中,lo<hi,而当且仅当循环结束的时候,lo=hi。而mid在循环过程中满足lo<=mid<hi,因此:

      1. hi初始化为n:循环过程中,mid<hi<=n,当mid取到n-1的情况时,mid+1最大取到n,产生越界
      2. hi初始化为n-1:循环过程中,mid<hi<=n-1,因此mid最大取到n-2,mid+1最大取到n-1,不会越界

    由于这一结论以及hi初始化为n-1不会影响二分查找的结果,在很多情况下我们可以使用这一初始化方法。除了上面提到的162外,33.搜索排序旋转数组也可以用邻接索引的方法。

  • 相关阅读:
    基于kubernetes v1.17部署dashboard:v2.0-beta8
    kubeadm快速部署Kubernetes单节点
    kafka数据可靠性深度解读
    MySql中4种批量更新的方法
    如何分析Mysql慢SQL
    企业级SSD市场接口之争:SATA会被NVMe取代吗?
    强势回归,Linux blk用实力证明自己并不弱!
    影响性能的关键部分-ceph的osd journal写
    文章汇总(包括NVMe SPDK vSAN Ceph xfs等)
    NVMe over Fabrics:概念、应用和实现
  • 原文地址:https://www.cnblogs.com/lity/p/15591116.html
Copyright © 2011-2022 走看看