二分法——二分查找

Posted 程序员Henchamps

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分法——二分查找相关的知识,希望对你有一定的参考价值。

#0

从今天开始,这个专题进入新的分支——「二分法」。二分法找跟「双指针」很相似,都是使用两个指针利用线性容器可以随机访问的特性。二分法的分支将以二分查找算法展开。因为二分法是在双指针上演化而来的,因此双指针的套路依然适用。

二分查找每次查找都能够将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为0。这便是二分思想,不过要运用恰当,在套用双指针的套路时还有几个问题需要注意。

在以二分查找切入二分法后,将会讨论运用二分法的变种问题。

#1

首先把要求限制严格,如果要在线性容器上使用二分查找,那么这个线性容器必须要满足两个条件:

  • 这个线性容器中的元素是有序的
  • 这个线性容器中不存在重复的元素

同样对照双指针的四个步骤,看在运用二分法的时候有哪些需要解决的问题。

以nums代表「线性容器」,以left和right代表「两个指针」,target表示「查找的目标值」

  • 确定两个指针指向元素的含义
    • 针对二分查找,这里的两个指针指的是查找的范围
  • 确定两个指针移动与停止的条件
    • 移动条件根据nums[mid] 和 target的比较决定
    • 停止条件是left <= right
  • 确定指针移动时的step
    • 当需要移动left指针时,left = mid + 1
    • 当需要移动right指针时,right = mid - 1
  • 确定指针移动时的额外执行逻辑
    • 计算mid,mid代表查找范围的中点
    • 比较nums[mid] 和 target 的大小

实现的代码请参考

public int binarySearch(int[] nums,int target) {
  int len = nums.length;
  int left = 0,right = len - 1;

  while (left <= right) {
    int mid = left + ((right - left) >> 1);
    if (a[mid] == target) {
      return mid;
    } else if (a[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return -1;
}

上面的代码规避了使用二分查找容易出现的问题,事实上,除了第一个步骤,其他三个步骤都有「额外需要注意的地方」。

mid的计算方式

二分法之所以会被单独拉出来独立成一个分支,其原因是在每次查找时计算查找范围的中点并缩小一半的查找范围。容易出错的地方在于如果使用int mid = (left + right) / 2这个直观的计算方式,如果left+right超过了数值范围会发生溢出,而使用left + (right - left) / 2则不会。当然采用left + ((right - left) >> 1)利用移位加速计算过程则属于锦上添花了。

指针的停止条件与移动时的step

指针的停止条件与指针在移动时步长(step)的选择其实是一个问题:考虑边界情况。

停止条件

在线性容器中查找目标值的时候都是假设目标值在线性容器的「中间部分」,这样确定很好理解。但是假如停止条件是left < right并且目标值在线性容器的「最右端」。

当进行二分查找时,最后一个步骤是「将left指针移动至最右端」,这个时候由于触发了终止条件left < right退出循环体,则无法找到目标值,算法的行为与预期不符。

停止条件设置不当

移动时的step

这部分容易出问题的是将移动指针的代码left = midright = mid

left == right并且nums[left] != target时,由于无法移动指针一直满足条件left <= right。本应该退出循环的算法却陷入了死循环中。

#10

这部分开始进入实际的LeetCode题目。

No.35

具体题目的描述请登录LeetCode网站查看。

这道题目中的线性容器完全符合两个要求,因此参考二分查找的代码就能解答。不过这种题目还是需要多多考虑边界情况,可以先代码一把梭然后根据提交的结果判断,不过更好的办法是事先考虑并准备相应的测试用例,待跑通测试用例后再提交。

代码请参考。

class Solution {
    public int searchInsert(int[] nums, int target) {
        // 步骤一
        int len = nums.length;
        int left = 0,right = len - 1;
        // 边界条件的处理
        if (target > nums[right]) return right + 1;
        if (target < nums[left]) return left;
        // 步骤二,停止条件
        while(left <= right) {
            // 步骤四,计算mid
            int mid = left + ((right - left) >> 1);
            // 步骤四,与目标值进行比较
            if (nums[mid] < target)
                // 步骤三,移动指针
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid - 1;
            else
                return mid;
        }
        // 返回值需要根据题目要求分析
        return right + 1;
    }
}

很明显,除了解题框架四个步骤和边界情况的处理,还有一个返回值也容易出问题。这个时候需要明确,当循环体退出两个指针指向元素的含义,再充分理解题意。

这道题目的要求是在没有找到目标值的时候返回它会被按顺序插入的位置。不考虑边界情况,这个位置就是原有数组中第一个大于目标值所对应的下标位置。

二分查找还可以通过递归的方式实现,根据上面的代码改造。

class Solution {
    public int searchInsert(int[] nums, int target) {
        // 步骤一
        int len = nums.length;
        int left = 0,right = len - 1;
        // 边界条件的处理
        if (target > nums[right]) return right + 1;
        if (target < nums[left]) return left;
        return binarySearch(nums,left,right,target);
    }
    private int binarySearch(int[] nums,int left,int right,int target) {
        // 停止条件
        if(left <= right) {
            // 步骤四,计算mid
            int mid = left + (right - left) / 2;
            // 步骤四,与目标值进行比较
            // 步骤三,移动指针
            if(nums[mid] < target) return binarySearch(nums,mid + 1,right,target);
            else if (nums[mid] > target) return binarySearch(nums,left,mid - 1,target);
            else return mid;
        }
        // 返回值需要根据题目要求分析
        return right + 1;
    }
}

#11

本文以二分查找为切入点阐述了二分法的思想,并且针对实现二分查找算法容易遇到的三个问题进行了描述,分别是「mid的计算方式」,「指针移动条件和终止条件」。二分法分支一共会出四篇文章,在剩下的文章中将介绍几个二分法的变种问题。分别是条件一(「这个线性容器中的元素是有序的」)被打破,条件二(「这个线性容器中不存在重复的元素」)以及两个条件「都被打破」的情况下,如何运用二分法的问题。


以上是关于二分法——二分查找的主要内容,如果未能解决你的问题,请参考以下文章

二分查找常见套路与分析

C语言二分查找

「算法笔记」一文摸秃二分查找

二分法查找

二分法查找

代码题(12)— 二分查找