二分查找的基本问题
二分查找是一种算法,其输入必须是有序的元素列表,对于包含n
个元素的列表,使用二分查找最多需要$\log_2^n$
步
对数运算是幂运算的逆运算
log_2^n = a → 2^a=n
在使用大O表示法讨论运行时间时,
$\log$
指的都是$\log_2$
(以2
为底)
二分法的核心思想是逐渐缩小问题规模,在练习和学习的时候,要着眼于掌握算法的思想,而不该去纠结二分的几种写法的区别和细节
二分查找的基本问题在LeetCode上是704题,二分查找
var search = function (nums, target) {
let left = 0,
right = nums.length - 1;
while (left <= right) {
const mid = ~~((left + right) / 2);
if (nums[mid] === target) {
return mid;
}
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
};
二分查找的思路就是根据待搜索区间的中间元素nums[mid]
与target
的值的大小关系,判断下一轮搜索需要在哪个空间进行查找,进而设置left
和right
的值,分为三种情况
nums[mid] === target
,找到了目标元素,直接返回nums[mid] > target
,说明mid
以及mid
右边的所有元素,一定比target
大,下一轮搜索的区间改为[left, mid - 1]
,因此需要设置right = mid - 1
nums[mid] < target
,与上一条相反,下一轮搜索的区间改为[mid + 1, right]
,因此需要设置left = mid + 1
如果while
循环结束,函数仍未退出,说明不存在目标元素,返回-1
二分查找的问题变种
大多数二分查找的题目都不是这么简单,一般不是找与target
相等的值,比如找到小于等于target
的下标最小的元素,这样的题目的特点是,如果nums[mid]
等于target
时,还需要继续查找
可以使用如下的思路和模板进行思考:
区间划分和缩小
根据中间位置元素的值nums[mid]
把待搜索区域分为两个部分:
- 一定不存在目标元素的区间,下一轮搜索不需要考虑
- 可能存在目标元素的区间,下一轮搜索需要考虑
mid
只可能存在于这两个区间的一个,即while
里面的if...else
有两种写法:
- 如果
mid
被分到左边区间,区间被分为[left, mid]
和[mid + 1, right]
,此时分别设置right = mid
和left = mid + 1
- 如果
mid
被分到右边区间,区间被分为[left, mid - 1]
和[mid, right]
,此时分别设置right = mid - 1
和left = mid
循环条件
while
的循环条件写成left < right
,在上面把待搜索区间分为两个部分的情况下,退出循环一定left === right
,这样就不需要考虑最终返回left
还是right
的问题了
if...else
的条件
把容易想到的、不容易出错的逻辑写在if
里面,什么是容易想到、不容易出错的逻辑呢,题目要求我们需要符合条件A的元素,那么我们就将条件A的反面作为if
的逻辑
练习
以LeetCode的35题搜索插入位置为例,实际上,题目要求的是找到第一个大于等于target
的下标,那么我们if
的条件去取反,那么就是nums[mid] < target
在这个条件下,区间缩小的规则就是left = mid + 1
,与它配套的反面空间就是right = mid
,所以就可以写出代码了:
var searchInsert = function (nums, target) {
const length = nums.length;
// right 初始值取的是 length,而不是 length - 1,这样 left 就可以去到最后一个有效元素 length- 1
// 如果 right 初始值是 length - 1,需要针对插入到最后的情况进行特殊处理
let left = 0,
right = length;
while (left < right) {
const mid = ~~((left + right) / 2);
if (nums[mid] < target) {
// 下一轮搜索空间 [mid + 1, right]
left = mid + 1;
} else {
// 下一轮搜索空间 [left, mid]
right = mid;
}
}
return left;
};
总结
- 想清楚题目是否可以使用二分法,题目中包含单调性或者可以缩减问题规模的特点,常见的应用有在有序或者半有序的数组中找下表,确定一个有范围的整数
- 确定搜索范围
- 从『中间元素什么时候不是解』开始反向思考
if
的条件 - 把区间分为两个部分,
while
循环的条件不包含等号 - 根据
mid
所在区域,缩小问题规模 - 考虑
right
的取值是不是要增加一位 - 如果遇到
left = mid
的情况,mid
需要向上取值