算法介绍
如果需要在一个有序数组中查找某一特定元素,按照常规思路,我们会从头开始遍历一下这个数组,直到找到这个元素。显然,这个过程的算法时间复杂度为 O ( n ) O(n) O(n),当数据范围比较大时,就有可能超时。本文所讲的二分查找就能将该问题的时间复杂度降为 O ( l o g n ) O(logn) O(logn)。其大致过程为每次拿数组中间的值与目标值进行比较,如果数组中间的值比目标值大(假定有序数组顺序为从小到大),说明目标值在数组左边,否则在右边。
算法模板
二分的本质是边界,多用于具有单调性的题目中。如果我们在一个区间上定义某种性质,使得整个区间可以被一分为二,即这个性质在一边区间满足而在另一边区间不满足。那么使用二分就可以找出左边界的右端点或右边界的左端点。
可能你会发现网上二分查找的算法介绍特别多,一般文章中都会提到边界问题,这也是二分查找比较难的地方。因此,下面给出二分查找通用的模板,无需考虑边界问题。
注意:题目可能无解,但二分一定有解。
- 寻找右边界的左端点,区间 [ l , r ] [l, r] [l,r] 被划分成 [ l , m i d ] [l, mid] [l,mid] 和 [ m i d + 1 , r ] [mid + 1, r] [mid+1,r] 时使用:
function binarySearch(l, r) {
while (l < r) {
let mid = l + r >> 1;
if (check(mid)) r = mid; // 满足所定义的性质
else l = mid + 1;
}
return l;
}
- 寻找左边界的右端点,区间 [ l , r ] [l, r] [l,r] 被划分成 [ l , m i d − 1 ] [l, mid - 1] [l,mid−1] 和 [ m i d , r ] [mid, r] [mid,r] 时使用:
function binarySearch(l, r) {
while (l < r) {
// 加 1 是为了防止当 r = l + 1 时 (l + (l + 1)) / 2 = l 进入 [l, l + 1] 死循环
let mid = l + r + 1 >> 1; // 向上取整
if (check(mid)) l = mid; // 满足所定义的性质
else r = mid - 1;
}
return l;
}
实战
- LeetCode704. 二分查找
题意:在有序数组nums
(元素不重复)中找target
,返回下标,不存在则返回-1
。
解法:我们定义一个性质:nums[mid] >= target
,则数组被分为左右两个区间,左边的数都小于target
,右边的数都大于等于target
。我们要寻找的解就是右边区间的左端点,因此,采用第一个模板。
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let l = 0, r = nums.length - 1;
while (l < r) {
let mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
if (nums[l] === target) return l;
else return -1;
};
- LeetCode34. 在排序数组中查找元素的第一个和最后一个位置
该题也是模板题,代码如下:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var searchRange = function(nums, target) {
let res = [];
let l = 0, r = nums.length - 1;
while (l < r) {
let mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
if (nums[l] != target) return [-1, -1];
else {
res.push(l);
l = 0, r = nums.length - 1;
while (l < r) {
let mid = l + r + 1 >> 1;
if (nums[mid] <= target) l = mid;
else r = mid - 1;
}
}
res.push(l);
return res;
};