本文已参与「新人创作礼」活动,一起开启掘金创作之路。
二分法
二分查找是一个在有序数组中查找特定元素的搜索算法,尤其是面对大量的数据时,其查找效率极高,时间复杂度是log(n)。
什么时候可以用二分法?
- 前提:元素有序 (或数据具有二段性)
- 查找某个值
- 给一个值求插入位置
- 判断是否存在某个值
- 利用二分思路求最大最小值
二分法查找的思路
二分法查找的思路如下 :
- 首先,从数组的中间元素开始搜索,如果该元素正好是目标元素,则搜索过程结束,否则执行下一步。
- 如果目标元素大于/小于中间元素,则在数组大于/小于中间元素的那一半区间查找,然后重复步骤「1」的操作。
- 如果某一步数组为空,则表示找不到目标元素。
二分法的思路很简单,无非就是每次根据中值判断目标值是在哪个区间,然后缩减区间到原来的一半,直至找到结果。二分法最容易出错的地方在于边界和细节处理,大体逻辑并不难写出,我们往往死在细节处理上。
例如:
- 是
while(left < right)还是while(left <= right)? - 是
right = middle还是right = middle - 1?
在二分查找的过程中,要循环不变量规则,就是在while循环里,寻找中每一次边界的处理都要坚持根据区间的定义来操作。
二分法的边界模板
-
左闭右闭的区间写法
[left,right]:while(left <= right),left的改变为left = mid + 1,right的改变为right = mid - 1; -
左闭右开的区间写法
[left,right):while(left < right),left的改变为left = mid + 1,right的改变为right = mid;
解题套路
以leetcode 题目 704.二分查找为例,分别用两种写法解答
题目:给定一个
n个元素有序的(升序)整型数组nums和一个目标值target,写一个函数搜索nums中的target,如果目标值存在返回下标,否则返回-1。
-
左闭右闭区间
[left,right]- 采用左闭右闭区间写法,右区间端点可以取到,
nums[right]是存在的,所以right初始值为nums.length - 1,而不是nums.length。 - 同样的,在闭区间
[left,right]中,left === right是有意义的,要考虑到相等的情况,所以while循环里要用<=,即while(left <= right) - 当
nums[middle] > target时,当前这个nums[middle]一定不是target,所以左区间结束位置的下标是middle - 1,而不是middle, 即right = middle - 1
- 采用左闭右闭区间写法,右区间端点可以取到,
// @lc code=start
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
// 已知数组升序且不重复,寻找目标值
// 考虑使用二分法求解
// 采用写法一,左闭右闭区间
let left = 0 ,right = nums.length - 1
// 区间左闭右闭,所以left === right是有意义的 ,所以要用 <= ,即 left <= right
while(left <= right){
const middle = parseInt((left + right) / 2)
// 分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节
if (nums[middle] === target) {
// 找到目标值
return middle
} else if(nums[middle] > target) {
// 当前这个nums[middle]一定不是target,所以左区间结束位置的下标就是 middle - 1, 即 right = middle - 1
right = middle - 1
} else if (nums[middle] < target){
//
left = middle + 1
}
}
return -1
};
// @lc code=end
-
左闭右开区间
[left,right)- 采用左闭右开区间写法,右区间端点不能取到,
nums[right]不存在,此时right初始值可以为nums.length。 - 同样的,在左闭右开区间
[left,right)中,nums[right]的情况没有意义,也就不用考虑left === right的情况,所以while循环里要用<,即while(left < right) - 当
nums[middle] > target时,此时左区间结束位置的下标可以是middle,而不是middle - 1,因为区间左闭右开,当right = middle,也不会与nums[middle]比较。
- 采用左闭右开区间写法,右区间端点不能取到,
// @lc code=start
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
// 采用写法二,左闭右开区间
let left = 0 ,right = nums.length
// 区间左闭右开,所以left === right是没有意义的 ,所以要用 < ,即 left < right
while(left < right){
const middle = parseInt((left + right) / 2)
// 分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节
if (nums[middle] === target) {
// 找到目标值
return middle
} else if(nums[middle] > target) {
// 循环不变量规则 - 区间是左闭右开
// 此时左区间结束位置的下标就算是 middle,因为区间左闭右开,也不会与nums[middle]比较
right = middle
} else if (nums[middle] < target){
//
left = middle + 1
}
}
return -1
};
// @lc code=end
- 总结
| 两种写法的对比 | 左闭右闭[left,right] | 左闭右开[left,right) |
|---|---|---|
right初始值 | nums.length - 1 | nums.length |
while循环不变量 | while(left ≤ right) | while(left < right) |
当nums[middle] > target时 | right = middle - 1 | right = middle |
相关题目
- 704.二分查找
Easy - 35.搜索插入位置
Easy - 69.x的平方根
Easy - 34.在排序数组中查找元素的第一个和最后一个位置
Medium