这是我参与「第四届青训营 」笔记创作活动的的第12天
概述
要求
- 必须采用顺序存储结构
- 必须按关键字大小有序排列(后有补充说明)
复杂度
时间复杂度:
本质
二分查找本质是把数组看作左右两部分,然后找到两部分的边界,因此数组有序不是必要条件。只要能把数组分成两个部分,那么就可以使用二分法。
基本原则
- 每次都要缩减搜索范围
- 每次缩减不能排除潜在答案(若题目规定有多个可行答案,则每次缩减不能排除所有潜在答案)
基本模板1
来源
一般流程
满足isBlue(m)函数的情况会导致l移动。因此最终l所在的位置即为最后一个满足isBlue(m)的数组元素的位置或-1,最终r在的位置即为第一个不满足isBlue(m)的数组元素的位置或N。
后处理一般是边界处理,即检查返回的l或r是否越界(即l或r不移动),一般有以下三种情况:
- 数组元素只有一个。
- 不存在“等于”的数字。
- 符合条件的数字恰好在端点处,如:在
[2, 5]中寻找≤ 5的数字,最终l = 1,r = 2,r不移动。
伪代码
l = -1, r = N # l是左边界,r是右边界。l = 数组第一个元素的前一个位置,r = 数组最后一个元素的后一个位置
while l +1 != r
m = l + (r - l) // 2 # 防止溢出,如果直接用m = (l + r) / 2可能会溢出
if isBlue(m)
l = m
else
r = m
checkCondition() # 一些条件处理,如边界检测等
return l or r
时间复杂度:
细节问题
l和r指代什么l表示蓝色区域最后一个元素的下标,r表示红色区域第一个元素的下标。- 为什么
l的初始值为-1而不是0,r的初始值为N而不是N-1? 当数组全为红色区域或蓝色区域时,l和r的定义违背了第一条。 m是否始终处于以内? 当时, 注意:由于m可以为0,说明数组至少含有一个元素,因此只可以为1,而非0(参考第二条)。另外,若,则程序将会立马退出while循环 当时, 注意:为什么不等于N-1?因为当时将会进入while的最后一次循环,此时通过计算得到l的新值l = m = N - 1。之后就由于不满足l + 1 != r无法再进入下一次while循环。所以能进入循环的只能为N - 2。- 更新指针时,能不能写成
l = m + 1或r = m - 1? 对于这种模板来说,不能。若m刚好指向蓝色区域的右边界,那么l = m + 1会导致l进入红色区域。同理,若m刚好指向红色区域的左边界,那么r = m - 1会导致r进入蓝色区域。 - 会不会进入死循环?
当
l + 1 = r时,会退出循环。 当l + 2 = r时,m将指向l和r正中间,此时要么l = m,要么r = m,之后将会进入第一种情况。 其他情况,最后都会进入上述两种情况。因此,程序不会陷入死循环。 isBlue(m)问题isBlue(m)只能是判断第m个元素是否属于蓝色区域,不能判断其是否属于红色区域。
部分常见题型
另外,若要找值为
target的元素,则需令isBlue(m)条件为<= target,然后判断第l个元素是否为target,并且注意边界情况,最后返回l(方法不唯一)。
基本模板2
来源
框架
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 ...;
}
其中...标记的部分是可能出现细节问题的部分。
搜索区间为左闭右闭型。
技巧
- 不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节
- 使用
left + (right - left) / 2防止溢出
寻找一个数(基本的二分搜索)
代码(左闭右闭)
int binarySearch(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)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
细节
- 什么是左闭右闭区间?什么是左闭右开区间?
左闭右闭区间:
left和right指向的都是搜索区间内的元素,即搜索范围是[left, right]。当搜索区间为空时,left = right + 1,此时搜索区间为[right + 1, right],显然不包含任何元素,因此while循环条件是left <= right或left + 1 != right,即退出循环时left = right + 1左闭右开区间:left指向搜索区间的起始位置(第一个元素),right指向搜索区间末端的后一个元素,即搜索范围是[left, right)。当搜索区间为空时,left = right,此时搜索区间为[left, left),显然不包含任何元素,因此while循环条件是left < right或left != right,即退出循环时left = right - 如何判断搜索区间的类型?
规定
mid = left + (right - left) / 2则当while的循环条件是left <= right或left + 1 != right时,搜索区间是左闭右闭型,且right的初值一般为nums.size() - 1,且right = mid - 1。 当while的循环条件是left < right或left != right时,搜索区间是左闭右开型,且right的初值一般为nums.size(),且right = mid。 此处不考虑左开右闭型。 - 为什么
while循环的条件中是<=,而不是<? 因为该模板是左闭右闭型。 - 为什么
left = mid + 1,right = mid - 1?而有的代码是right = mid或者left = mid? 因为该模板是左闭右闭型。当发现mid不是要找的target后,自然下一个要搜索的区间是[left, mid - 1]或[mid + 1, right],因为mid已经搜索过,应该从搜索区间中去除。 若是左闭右开区间,则下一个搜索区间就是[left, mid)或[mid + 1, right),因此应该使用left = mid + 1和right = mid。 而模板1之所以同时使用left = mid和right = mid,是因为思考的角度不同:模板1把数组分为蓝红两个部分,left和right分别指向蓝红区域的边界。数组中任何一个元素只有两种可能:要么是蓝色,要么是红色。所以如果nums[mid]是蓝色,则left = mid,反之亦然。 - 该模板的缺陷
只能找到
target,但不能找到target第一次、最后一次出现的位置,也不能找到第一个大于或小于target的元素的位置。