仅供自己复习
来源于算法总结的分治a题的更详细解释(当时写算法总结的时候还有点迷糊,这里倒是弄懂了)
关于二分搜索有一篇文章写得很好
以及李威大佬的二分搜索
一、二分搜索详解(leetcode704)
前提是有序数组
通过移动左右两个指针以及中间的那个指针来进行查找
时间复杂度为O(logN)
重点①:查询中间的数经常会看到多种方法,如
let mid = Math.floor((left + right) / 2)
let mid = left + Math.floor((right - left) / 2)
let mid = right - Math.floor((right -left) / 2)
这三个式子中2其实是1的变体,因为在left和right都很大的情况下,left+right可能越界,所以使用第二个,而第二个是更靠近左边界的中位数,第三个是更靠肩右边的中位数,因为这两个都是在搜索区域之内的,所以其实两个都能用,但是,肯定是哪个边界是闭就用哪个,假设左边界闭合,就用第二个,左开右闭用第三个
重点②:搜索区域,也就是我们怎么定left和right的初始值、终止情况、移动情况三个方面
第一种写法:
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0
let right = nums.length
while(left < right) {
let mid = left + Math.floor((right - left) / 2)
if(nums[mid] > target) {
right = mid
} else if(nums[mid] < target) {
left = mid + 1
} else if (nums[mid] === target) {
return mid
}
}
return -1
};
首先,这里left的初始值为0,而right的初始值为nums.length,而在一个数组中很明显是没有一个下标为nums.length的,所以我们很自然地想到,这个区间是左闭右开的,毕竟0是肯定取得到的
说到左闭右开就能想到经常写的东西
for(let i = 0; i < nums.length; i++){}
这种for循环其实就是使用了左闭右开,当下标等于数组的个数就出数组
而我们这里也借用了这种思想,所以使用了
while(left < right)
,这样当变成了[left, left)的时候,肯定搜索区域是空
除此之外,因为有边界是开的,所以我们移动right的时候不需要用right = mid - 1,而是使用right = mid
,毕竟右边是开边界,如果用right = mid - 1,相当于是从mid - 2的位置开始找的(mid - 1就没找到),而如果用right=mid,就是从mid - 1开始找的,能找到mid - 1
但是左边就不需要了,就直接用left = mid + 1
,毕竟是闭边界,可以保证已经找过了mid
第二种写法:
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0
let right = nums.length - 1
while(left <= right) {
let mid = left + Math.floor((right - left) / 2)
if(nums[mid] > target) {
right = mid - 1
} else if(nums[mid] < target) {
left = mid + 1
} else if (nums[mid] === target) {
return mid
}
}
return -1
};
其实是把有边界的right变成了nums.length - 1,这就代表着这是左闭右闭的写法,这里又能很自然地想到
for(let i = 0; i <= nums.length - 1; i++) {}
所以我们这里的循环终止条件其实是while(left <= right)
,就是为了让搜索区间内没有数
而因为是闭,我们移动左右指针都很自然很简单地写出来left = mid + 1
,right = mid - 1
第三种写法
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0
let right = nums.length - 1
while(left < right) {
let mid = left + Math.floor((right - left) / 2)
if(nums[mid] > target) {
right = mid - 1
} else if(nums[mid] < target) {
left = mid + 1
} else if (nums[mid] === target) {
return mid
}
}
return nums[left] === target ? left : -1
};
如果说前两种写法是常规的,考虑了开闭区间后就直接写的,让搜索区域的最后终止条件是搜索区域内没值,那么第三种写法就是要让搜索区域留下那么一个值,这种写法有很多变种,但无论怎么写最后都落在搜索区域中还存在一个值,循环就终止了,所以说必须要判断一下最后这个值,又因为虽然留了一个值,但是这个时候left === right
,所以这里是判断nums[left]还是nums[right]其实无差
其实方法一和方法二是最常用的!!
二、使用情况
其实只要看到有序数组或者是求某个确定的值就可以思考一下二分搜索了(这里不记录那种特别特别特别明显的)
另外,这些题其实还可以使用例如单调栈之类的方法,不过因为这里只讲二分搜索,所以这里有些只给出二分搜索的代码,除非特别经典想不出来的那种会给其他解法
1、在数组中查找符合条件的元素的下标**
a、在排序数组中查找元素的第一个和最后一个位置
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var searchRange = function(nums, target) {
let left = 0
let right = nums.length - 1
while(left <= right) {
let mid = left + Math.floor((right - left) / 2)
if(nums[mid] === target) {
let newLeft = mid, newRight = mid
// 这里 newLeft - 1 可以等于 left
while(newLeft - 1 >= left && nums[newLeft - 1] === target) {
newLeft--
}
// 这里的也可以等
while(newRight + 1 <= right && nums[newRight + 1] === target) {
newRight++
}
return [newLeft, newRight]
} else if(nums[mid] > target) {
right = mid - 1
} else {
left = mid + 1
}
}
return [-1, -1]
};
b、搜索插入位置
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var searchInsert = function(