二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是要求线性表必须有序
。
一、 查找过程
- 如果目标值等于中间元素,则找到目标值。
- 如果目标值较小,继续在左侧搜索。
- 如果目标值较大,则继续在右侧搜索。
- 重复上面步骤,直到找到,或者遍历结束。
二、核心解题注意点
1. 开闭区间选择不同处理方式不同
这里开闭区间有两种选择:左闭右闭[]
和左闭右开[)
;
// 左闭右闭型
function fn(arr, target) {
let left = 0;
let right = arr.length - 1; // ①
while (left <= right) { // ②
let middle = Math.floor((left + right) / 2);
if (target > arr[middle]) {
left = middle + 1; // ③
} else if (target < arr[middle]){
right = middle - 1;
} else {
return middle;
}
}
return -1;
}
// 左闭右开型
function fn(arr, target) {
let left = 0;
let right = arr.length; // ①
while (left < right) { // ②
let middle = Math.floor((left + right) / 2);
if (target > arr[middle]) {
left = middle + 1;
} else if (target < arr[middle]){
right = middle; // ③
} else {
return middle;
}
}
return -1;
}
这个区间的选择要非常注意(上面标注的①②③是几个不同的地方),不然很容易出错。
出现以上情况的说明
②:
这个是导致出现①③的原因
①:
因为判断条件只有小于,会有这种情况需要注意:当查找的target
是最后一个;这是经过多次查找,left
到达倒是第二位,这时获取到的中间值就是倒是第二个,肯定小于target
,没找到,循环结束。只要给right
设置为数组长度就可以解决这个问题,因为最后一位无论怎样都会被middle
获取到。
③:
这个是由于②和Math.floor
导致的,会有这种情况:当剩下未查找刚好是left
,middle
和right
并且left
刚好就是target
的时候,如果还是移动右指针到middle-1
就会导致循环结束查找失败。
左闭右闭明显比左闭右开简单明了。采用左闭右开需要特别注意3处不同的地方,多练习练习,掌握了其中的原因就好了。
三、几道常见题目
1. 数组中查找某个值
从有序数组中查找某个值,找到返回坐标,找不到返回-1
左闭右闭处理
function fn(arr, target) {
let leftIndex = 0;
let rightIndex = arr.length - 1;
let middle;
while (leftIndex <= rightIndex){
middle = Math.floor((leftIndex + rightIndex) / 2);
// 这里中间值还可以如下获取
// middle = (leftIndex + rightIndex) >> 1;
if(target === arr[middle]){
return middle
}
if(target < arr[middle]){
rightIndex = middle - 1
}else{
leftIndex = middle + 1
}
}
return -1
}
console.log(fn([8, 11], 9))
左开右闭处理
function fn(arr, target) {
let leftIndex = 0;
let rightIndex = arr.length;
let middle;
while (leftIndex < rightIndex){
// 有符号右移,这个使用的是二进制右移,推出最右边的一个,刚好等于Math.floor()
middle = (leftIndex + rightIndex) >> 1;
if(target === arr[middle]){
return middle
}
if(target < arr[middle]){
rightIndex = middle
}else{
leftIndex = middle + 1
}
}
return -1
}
console.log(fn([8, 11], 11))
这里还需要注意一下middle的取值
,别让他在rightIndex=middle
的时候取值等于rightIndex
,这里使用无符号位移或math.floor只会等于leftIndex,都符合。
2. 找到第一个符合要求的值
从有序数组中找到某个值,如果有多个,返回最左边的坐标。找不到返回-1
例如查找[8,8,9]
中的8时应该返回0而不是1
解题思路
1.这时没有等于中间值直接返回。
2.大于时候右边找,小于时候左边找,等于时候左边以及中间找(有可能等于的这个就只有一个)
左闭合右闭解法
function fn(arr, target) {
let leftIndex = 0;
let rightIndex = arr.length - 1;
while (leftIndex <= rightIndex){
let middle = (leftIndex+rightIndex) >> 1;
if(arr[middle] >= target){
rightIndex = middle
}else if(arr[middle] < target){
leftIndex = middle + 1
}
if(leftIndex === rightIndex){
return leftIndex
}
}
return leftIndex
}
console.log(fn([1,2,3],3))
左闭右开解法
function fn(arr, target) {
let leftIndex = 0;
let rightIndex = arr.length;
while (leftIndex < rightIndex){
let middle = (leftIndex+rightIndex) >> 1;
if(arr[middle] >= target){
rightIndex = middle
}else if(arr[middle] < target){
leftIndex = middle + 1
}
}
return leftIndex
}
console.log(fn([1,2,3,4,4,5],2))
这里有几个点要注意:
1.因为有rightIndex = middle
,所以使用Math.floor
或者>>
2.这个二分查找是查找了所有该查找的,最后leftIndex就是查找值的位置。
3.如果使用的是右闭方法(右索引为最后一位),那么循环里面需要后面加上相等判断,停止循环(全部都查了就出结果了)
3. 数组中查找或插入某个数
给定有序数组arr和目标值target,如果数组中存在target,返回坐标,如果不存在,将target按照数组顺序插入数组,返回插入位置。
解题思路
1.这个只需要在上面的基础上,当判断有值时候返回。
2.大于中间值右边找,小于左边找。
3.最后的左值(右值)就是需要插入的位置。
4.实际上就是二分查找一个值得最坏情况。找到leftIndex===rightIndex。
function fn(arr, target) {
let leftIndex = 0;
let rightIndex = arr.length - 1;
let middle;
while (leftIndex <= rightIndex){
middle = Math.floor((leftIndex + rightIndex) / 2);
if(target === arr[middle]){
return middle
}
if(target > arr[middle]){
leftIndex = middle + 1
}else{
rightIndex = middle - 1
}
}
arr.splice(leftIndex, 0, target);
return leftIndex
}
console.log(fn([8], 7))
四、总结
- 有序数组,查找位置可以用二分查找。
- 根据自己选择右边是开区间还是闭区间(闭区间包含)调整while判断条件和if判断处理。
- 如果存在
leftIndex=middle
这种直接赋值的需要注意Math.ceil
和Math.floor
的选择,别让等号两边存在相等。