关于二分法的边界问题及两种写法

关于二分法的边界问题及两种写法

二分查找法大家很熟悉了,对于一个有序序列,我们可以通过二分查找法在 O ( l o g N ) O(logN) O(logN) 的时间内找到想要的元素。但是,在代码实现的过程中,如果没有仔细理解清楚,二分法的边界条件有时会让人很头疼,而对边界条件的妥善处理是很能体现一个人的代码功底的,也通常是面试官会很关注的一个点。另外,大佬的题解中的二分法代码也总有几处小细节不同,但是大佬的代码都是怎么测都没问题的,自己却总因为某处细节没有处理好而出现问题。

实际上,二分法通常有两种细节略有不同的实现方式:左闭右闭和左闭右开,本文将简单介绍这两种实现方式,并指出他们之间的不同及适用情况,希望也能够帮助大家彻底理解二分法,从此不再会因为边界条件问题出错。

题目

我们先给定题目要求,就通过 LeetCode 上的一道二分法的题目为例:704. 二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示

  • 你可以假设 nums 中的所有元素是不重复的。
  • n 将在 [1, 10000]之间。
  • nums 的每个元素都将在 [-9999, 9999]之间。

给定的函数签名 (C++) 是这样的:

class Solution {
public:
    int search(vector<int>& nums, int target) {

    }
};

说明:以下两节讲解参考自代码随想录

左闭右闭

第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是 [left, right] (这个很重要非常重要)。

区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1

例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:

关于二分法的边界问题及两种写法

代码:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

左闭右开

如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。

有如下两点:

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]

在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别

关于二分法的边界问题及两种写法

代码:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

搜索插入的位置:不大于target的最大索引

完整功能的二分法除了查找之外,还应该在没有查找到 target 元素时返回 target 应该插入的位置,为接下来可能的插入操作提供便利。

35. 搜索插入位置

注意本题要求保证给定数组是严格单增的,即不存在相等的元素,如果存在相等的元素如 [1,2,3,3,5] 这种情况在插入 3 时就会有多个合理的插入位置。

版本一:左闭右闭

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        while (left <= right) {
            int mid = (right - left) / 2 + left;
            if (nums[mid] > target) right = mid - 1;
            else if (nums[mid] < target) left = mid + 1;
            else return mid;
        }
        return left;		// 注意
    }
};

版本二:左闭右开

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = (right - left) / 2 + left;
            if (nums[mid] > target) right = mid;
            else if (nums[mid] < target) left = mid + 1;
            else return mid;
        }
        return right;		// 注意
    }
};

Ref:

https://leetcode-cn.com/problems/binary-search

https://leetcode-cn.com/problems/search-insert-position/

https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html#%E6%80%9D%E8%B7%AF

上一篇:2019-11-9:phpcms v9.6.0任意文件上传漏洞(CVE-2018-14399)


下一篇:二分查找的两种方法