LeetCode之二分查找系列题解

676 阅读7分钟

二分查找题解系列

广告: 最近github上新开了一个仓库May-Nodes,包括但不限于之前面试遇到的相关数据库,计算机操作系统,Java基础知识,计算机网络以及LeetCode等算法题解等知识。届时也会整理学习使用的PDF文档与资源。有需要的小伙伴 可以点个关注和star。在持续更新中,总会遇到你想要的。

前言二分查找是算法系列文章的第一篇尝试题解,之前也没有真正尝试过写算法系列的题解,今天也是在借鉴前人总结基础上进行自己的理解与更新。首先是阅读本文章需要有基础的二分查找的理解,知道什么叫做二分查找,大体是一个什么模板与样子,然后本文章具体讲解一些比较细致的部分,例如对于while(left < right)while(left <= right)的区别以及在判断语句中 right = midright = mid -1等左右判断系列。

基础版本

示例一

首先来看一下对于基础版本的二分查找的模板:

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;
    while(...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

需要注意的第一点:

  • 对于 right的判断,我们一般会写上right = nums.length-1也是我们的习惯所在,但是在某些情况下我们也可能会写上right = nums.length ,后面会讲解具体的区别所在。
  • 对于right = mid 或者是说 right = mid -1 等情况,或者是对于 left的判断也是需要情况具体判断。

对于最为基础的二分查找系列可以见:LeetCode 经典二分查找

 public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length-1;
        while(left <= right){
            int mid = left + (right - left)/2;
            if(nums[mid]<target)
            left = mid+1;
            else if(nums[mid]>target)
            right = mid-1;
            else if(nums[mid] == target)
            return mid;
        }
        return -1;
    }

对于这个题目来说,题目中明确说明,是进行值的查找系列:要么值存在于数组中,要么不存在于数组中。同时也是经典的在循环体里面对元素进行查找

示例二

对于这个题目可以参见35- 搜索插入位置模板系列:

 public int searchInsert(int[] nums, int target) {

        int left = 0;
        int right =... ;
        while(...){
            int mid = left+ (right-left)/2;
            if(nums[mid]>target)
                right = mid;
            else if (nums[mid]<target)
                left = mid + 1;
            else if(nums[mid]  == target)
                return   mid;
        }
        return left;
    }

注意这个题目,我们计算的是,先行判断是否存在于目标数组中,然后不存在时候,并不是返回-1 值,而是返回对应的插入位置,想象以下,对于 在数组中存在 [...,2,4...],此时我们的taget 是3,这个时候在判断4不满足情况时候,我们并不能够直接进行 right = mid -1,这样的话,就直接将位于 2 和 4 之间的元素跳过,这也是不符合常理的地方。对于这类的题解,我们就是 在循环体中 排除不存在目标元素的区间,对于一半是不存在的,对于另外一半(包括当前中值在内)是可能存在的

循环体中元素查找

对于在循环体中的元素查找而言,我们把搜索的区间分成三个部分,左半部分中间值部分(我们正在进行判断的部分),和右半部分,对于这种在循环体中进行元素的查找一般都是直接查找到某个具体的值,例如上面的简单的二分查找,或者说是如下:二叉搜索树的查找,我们可以发现的是这种思想都是讲中间值进行舍弃,因为中间值已经不满足我们的情况,所以剩下的就是 left = mid+1right = mid-1,在两边进行值的查询。

即将大规模问题转化成小规模问题。减而治之是分而治之的特例,将大问题划分成若干个子问题以后,最终答案只在其中一个子问题里。

在循环体中排除一定不存在目标元素的区间

对于此类的问题:根据看到的 mid 位置的元素,排除掉一定不可能存在目标元素的区间,而下一轮在可能存在目标的子区间里继续查找

具体做法

  1. 对于此类问题,我们先把循环写作 while(left< right) 这样就会保证在推出循环时候,对于[left,right]区间里面只有一个元素,有可能就是我们的目标元素。

  2. 平时时候,我们思考什么时候不是解情况下会比较容易解决问题:如果一个数要满足多个条件,只需要对其中一个条件取反,就可以达到缩小搜索范围的目的

  3. 避免出现死循环时候对于 left = mid + 1,right = mid 时候,效果会更好一些,可以在进行调试的过程中,将具体的 left,mid,right 值都打印出来,进行具体的查看。

  4. 一般都采用到的是 进行一侧进行对mid进行加一和减一处理,以免会漏掉必要的值。

遇到的核心问题

为什么 while 循环中的条件判断是 <=,而不是 <?

:和我们的right的设置值有关系,因为我们对于 right的设置是 nums.length - 1而不是nums.length。 是因为这两个适用于不同的二分查找的情况下,还需要具体的情况具体讨论(对于大多数直接求某个具体值判断是否存在问题一般还是使用到<=)。

区别也就在于:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。

对于如上的基础的实现而言使用到的就是两端都关闭的区间。这个区间其实就是每次进行搜索的区间

我们在找到具体值的时候进行停止下来:

 else if(nums[mid] == target)
            return mid;

下面我们来具体测试说为什么对于在不同的right值时候要进行不同的判断:

首先我们对代码进行如下的更改:将 <= 更改成为 < 时候,进行如下的案例测试,会发现不能够正确输出3

image-20200808111520076

若是我们将right 设置为nums.length搭配上while(left < right),也是能够正常输入对应的结果

原因如下

while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [4, 3],可见这时候区间为空,因为没有数字既大于等于4 又小于等于 3 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。

while(left < right) 的终止条件是 left == right,写成区间的形式就是 [left, right],或者带个具体的数字进去 [3, 3]这时候区间非空,还有一个数 3,但此时 while 循环终止了。也就是说这区间 [3, 3] 被漏掉了,索引 3 没有被搜索,也就是我们的5没有被检索到,如果这时候直接返回 -1 就是错误的。

为什么 这里是**left = mid + 1right = mid - 1**,而有些情况下确实 right = mid ,left = mid + 1? 该如何进行区分。

正如前面所讲的在循环体中排除一定不存在目标元素区间时候,要根据具体的情况定论,查看具体我们所求的值和当前的判断中值是必须相等,还是说在范围内进行查询做定论。

小结

  • 对于具体的情况具体下定论,对于 right = nums.length -1 和 while(left < right) 也是推荐使用(不能够适用于所有的情况)因为对于这样的判断时候,就一定会有 left = right的情况出现,这些情况下,我们之间返回 left 或者 (right) 都是可行的
  • while (left <= right) 事实上是把待搜索区间「三分」,if else 有三个分支,它直接面对目标元素,在目标元素在待搜索数组中有只有 1 个的时候,可能提前结束查找。但是如果目标元素没有在待搜索数组中存在,则不能节约搜索次数;
  • 对于 有可能会出现的情况,上面的匹配还是要看自己平时的积累和总结经验,对于在编码过程中出现的死循环问题,要多进行将leftrightmid的中值打印出来。具体情况具体分析。