二分查找总结——二分查找细节分析 + 红蓝染色法(Leetcode 34)
1. 写在前面
本文为个人学习总结,二分算法参考:B站Up:灵茶山艾府(二分查找 红蓝染色法)
视频链接:https://www.bilibili.com/video/BV1AP41137w7/
Leetcode题目:34. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
测试样例1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
测试样例2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
2. 二分查找的区间细节分析
二分查找(Binary Search)的算法思想是十分简单的,但二分法的细节却惊人的多。以下是一份二分查找的代码示例,在敲定二分法的代码时,我们通常会遇到注释的细节问题:
// 下方代码的作用:查找有序正整数数组nums[]中第一个≥target的数的位置(下标)
// 待查找数组 nums[], 待查找数 target
int left = 0; // 数组最左端元素下标
int right = nums.length - 1; // 数组最右端元素下标
while(left <= right){ // 是left<right呢还是left<=right呢
int mid = (left + right) / 2;
if(nums[mid] >= target){ // 可以是nums[mid] > target吗
right = mid - 1; // 是right = mid - 1呢还是right = mid呢?
}else{
left = mid + 1; // 同理,可以是left = mid 吗?
}
}
return left;
其实,上面的情况都是可能存在的,而具体的代码写法要取决于我们对于查找区间的开闭选定,这个区间其实指的就是我们查找的范围,即left 和 right圈定的范围,常见的区间选定方法有:
- 左闭右闭 [left, right]
- 左闭右开 [left, right)
- 左开右开 (left, right)
当我们在写二分查找的代码的时候,一定要先明确我们选定的区间的闭合情况,在整个二分查找的过程中都应该去遵循这个区间,才能保证我们的二分查找代码不出错。
什么意思呢?我们来对不同情况分析一下。
① 左闭右闭[left, right]
区间左闭右闭,也就是说我们每次二分查找的时候是包含left和right索引指向的那个数的,那么,我们对left和right的初始化应该如下所示:
// 待查找数组 nums[], 待查找数 target
// 假设nums为[1,2,5,8,11,12,13]
int left = 0; // 数组最左端元素下标
int right = nums.length - 1; // 数组最右端元素下标
while(left <= right){
int mid = (left + right) / 2;
if(nums[mid] >= target){
right = mid - 1;
}else{
left = mid + 1;
}
}
return left;
在代码中,left指向nums的第一个数,right指向nums的最后一个数,此时查找范围涵盖了整个数组,没有问题,对于其他的编写细节:
i) while(left<=right)
前文提及,我们需要一直遵守我们选择的区间,那么,我们就需要去判断left=right对于当前选择的区间是不是合法的。
当left=right时,显然,对于[left, right]这个区间有意义,因此我们此时还不能结束循环。循环条件为left<=right
ii) right = mid - 1 || left = mid + 1
由于我们的区间是左闭右闭区间,包含两端,如果right = mid,我们的下一次查找区间为:[left, mid],此时mid指向的元素又被包括进来了,但是在之前的if条件判断中,我们知晓了nums[mid]与target的关系,所以下一次查找没有必要将mid指向的元素包括进来了,因此right = mid - 1。
left = mid + 1的情况同理。
// nums为[1,2,5,8,11,12,13], target为2
第一次查找区间索引:[0, 6], mid = 3
nums[mid] = 8 > 2, right = mid - 1 = 2
下一次查找区间索引:[0, 2] (1,2,5)
//假设right = mid 则下一次查找区间索引:[0, 3],包括的元素为1,2,5,8
//但我们之前已经知道了mid指向的8是不符合条件的了,所以没有必要包含到下一次查找了
① 左闭右开[left, right)
左闭右开,即不包含right指向的数,那么我们的代码会发生如下变化:
// 待查找数组 nums[], 待查找数 target
// 假设nums为[1,2,5,8,11,12,13]
int left = 0;
int right = nums.length; // 数组长度!这里变了!
while(left < right){ // 循环条件变了!
int mid = (left + right) / 2;
if(nums[mid] >= target){
right = mid; // 变啦!
}else{
left = mid + 1;
}
}
return left;
i) 变化一:right的值
int right = nums.length;
区间为左闭右开,即不包含right指向的元素。因此,为了将查找范围涵盖整个数组,right不能为nums.length - 1,否则会丢失数组最后一个元素。
ii) 变化二:循环条件
在左闭右开区间 [left, right) 中,left = right是没有意义的(如:(1,1]是没有意义的),因此在left = right时就可以结束循环了。循环条件为left<right,而非left<=right。
** iii) 变化三:下一次查找的right值**
right = mid, 即下一次查找的区间为:[left, mid),下一次查找不会包含mid了,是符合我们之前的做法的。left = mid + 1是因为区间左闭,假如left = mid,下一次查找又会将mid指向的元素包括进来了,没有必要。
3. ☆☆红蓝染色法☆☆
红蓝染色法,即将我们二分查找的数组划分为蓝色、红色两个区域,蓝色区域表示符合条件的数(True)的范围,红色区域表示不符合条件的数(False)的范围

我们二分查找主要有如下情况:
① 查找 ≥x 的第一个元素
② 查找 >x 的第一个元素
③ 查找 <x 的最后一个元素
④ 查找 ≤x 的最后一个元素
但系!这上面这四种情况都可以用一个代码来解决,我们后续会进行分析。
我们先来看第一种情况,即查找≥x的第一个元素。我们用红蓝染色法来分析一下过程:
示例数组nums[]:5 7 7 8 8 10
目标:查找 ≥8 的第一个元素的下标
定义:L的左边为红色区域,R的右边为蓝色区域
初始时,红色和蓝色区域未扩展,分别在两侧
红色区域:[]
蓝色区域:[]
第一次: M = (L + R) / 2 = 2 (下取整)
红| | 蓝
5 7 7 8 8 10
↑ ↑ ↑
L M R
由于nums[M]<8, L = M + 1
-----------------------------------------
红色区域:[5,7,7]
蓝色区域:[]
第二次: M = (L + R) / 2 = 4 (下取整)
红 | | 蓝
5 7 7 8 8 10
↑ ↑ ↑
L M R
由于nums[M]≥8, R = M - 1
-----------------------------------------
红色区域:[5,7,7]
蓝色区域:[10]
第三次: M = (L + R) / 2 = 3 (下取整)
红 | | 蓝
5 7 7 8 8 10
↑↑↑
LMR
由于nums[M]≥8, R = M - 1
-----------------------------------------
最终:循环结束时,R在L的左边,如下:
红色区域:[5,7,7]
蓝色区域:[8,8,10]
红|蓝
5 7 7 8 8 10
↑ ↑
R L
此时,返回R+1位置的元素即为所求,或者返回L位置的元素也可以。
// 采用左闭右闭区间,代码如下(其实就是我们上面一直在分析的代码):
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] res = new int[2];
// 寻找第一个 ≥target 的数的位置
int start = binarySearch1(nums, target);
// 数组中的数都<target 或 找到的数并不满足条件
if(start==nums.length||nums[start]!=target) return new int[]{-1,-1};
// 找 ≥target 的最后一个元素的位置 => 等价于找≥target+1 的第一个元素的左边一个数
// 如:5 7 7 8 8 10,找8的最后一个,即找满足≥9的第一个(10)的左边一个数
int end = binarySearch1(nums, target+1)-1;
res[0] = start;
res[1] = end;
return res;
}
// 二分搜索 红蓝染色法 左闭右闭区间~~
int binarySearch1(int[] nums, int target){
int left = 0, right = nums.length-1;
while(left<=right){
int mid = (right-left)/2 + left;
if(nums[mid]<target){
left = mid + 1;
}else{
right = mid - 1;
}
}
return left;
}
}
PS:二分查找的情况转换
在上面,我们给出了查找 ≥x 的第一个元素的代码,对于其他情况:
① 查找 >x 的第一个元素
可转换为:查找满足≥(x+1)的第一个元素
② 查找 <x 的最后一个元素
可转换为:查找满足≥x的第一个元素的左边那个数
③ 查找 ≤x 的最后一个元素
可转换为:查找满足>x的第一个元素的左边那个数

浙公网安备 33010602011771号