关于二分法的边界问题及两种写法
二分查找法大家很熟悉了,对于一个有序序列,我们可以通过二分查找法在 O(logN)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)/2if (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] == targetreturn 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] == targetreturn 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; // 注意}
};
在排序数组中搜索元素的第一个和最后一个位置
- 在排序数组中查找元素的第一个和最后一个位置
由于数组已经排序,因此整个数组是单调递增的,我们可以利用二分法来加速查找的过程。
考虑 targettargettarget 开始和结束位置,其实我们要找的就是数组中「第一个等于 targettargettarget 的位置」(记为 leftIdxleftIdxleftIdx )和「第一个大于 targettargettarget 的位置减一」(记为 rightIdxrightIdxrightIdx )。
二分查找中,寻找 leftIdxleftIdxleftIdx 即为在数组中寻找第一个大于等于 targettargettarget 的下标,寻找 rightIdxrightIdxrightIdx 即为在数组中寻找第一个大于 targettargettarget 的下标,然后将下标减一。两者的判断条件不同,为了代码的复用,我们定义 binarySearch(nums, target, lower)
表示在 numsnumsnums 数组中二分查找 targettargettarget 的位置,如果 lowerlowerlower 为 truetruetrue,则查找第一个大于等于 targettargettarget 的下标,否则查找第一个大于 targettargettarget 的下标。
最后,因为 targettargettarget 可能不存在数组中,因此我们需要重新校验我们得到的两个下标 leftIdxleftIdxleftIdx 和 rightIdxrightIdxrightIdx,看是否符合条件,如果符合条件就返回 [leftIdx,rightIdx][leftIdx,rightIdx][leftIdx,rightIdx],不符合就返回 [−1,−1][-1,-1][−1,−1]。
class Solution {
private:int binSearch(vector<int>& nums, int target, bool lower) {int left = 0, right = nums.size() - 1;int res = nums.size();while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] > target || (lower && nums[mid] >= target)) {right = mid - 1;res = mid;}else left = mid + 1;}return res;}
public:vector<int> searchRange(vector<int>& nums, int target) {int left = binSearch(nums, target, true);int right = binSearch(nums, target, false) - 1;if (left <= right && nums[left] == target && nums[right] == target && right <= nums.size()) return {left, right};else return {-1, -1};}
};
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