二分查找
推荐刷题
- leetcode704.二分查找
- leetcode35.搜索插入位置
- leetcode34. 在排序数组中查找元素的第一个和最后一个位置
- leetcode69. sqrt(x)
- leetcode367. 有效的完全平方数 二分的关键词:搜索一个数在数组的位置、默认数组是有序数组、时间复杂度等于O(log2n)
在做过上面的题后,我发现了二分查找的一个可以公用的做法
target目标值
left左边界
right右边界
leetcode704.二分查找 :找到target目标值的下标,没有则返回-1
leetcode35.搜索插入位置:找到target目标值的下标,没有则返回它应该在数组中的位置
leetcode34. 在排序数组中查找元素的第一个和最后一个位置:找到target目标值的首次和最后一次出现的下标值,没有则返回-1,-1
leetcode69. sqrt(x):返回一个数的算数平方根,向下取舍
leetcode367. 有效的完全平方数:判断一个数是否是有效的完全平方数
可以看到所有的问题都存在着target不存在的情况
我们可以把所有的问题从寻找target变成寻找寻找第一个大于或者小于target的下标值
而且例如34题,是寻找target的首次出现、最后一次出现的下标值,这就等同于我们变化后的正常解法。
我们最后这种的做法最后得出的是target应该在数组中存在的位置
再根据题目要求返回相应的东西即可
leetcode704.二分查找
常规做法
二分查找的关键点有几个:一个是中间值的求导,一个是循环判断,以及左右边界值的赋值
二分查找的步骤:
- 找出区间、遍历循环(反复循环数组的一半长度)
- 求出中间值
- 中间值与目标值判断、左右边界重新赋值
中间值的求导
二分查找需要用到left = 0 、right = list.length - 1,两个边界值
// 位运算
let mid = list.length >> 1
用这个方法时要注意,在二分查找中,找中间值指的是所选区间的中间索引值,上诉方法只能求数组的中间值,如果要用次方法求所选区间的中间值,用下列的表达式
let mid = ((right - left) >> 1) + left
// left + (right-left) / 2 这种写法可以避免发生越界问题
//向下取值
// Math.floor()
let mid = Math.floor(left + (right - left) / 2)
循环判断
声明时right的赋值
while()循环判断
循环内部right的赋值
左闭右和
let l = 0, r = nums.length - 1
while(l <= r){
let mid = Math.floor(l + (r - l) / 2)
if(nums[mid] === target){
return mid - 1
}else if(nums[mid] > target){
r = mid
}else{
l = mid + 1
}
}
return -1
// whlie()里使用等号,
左闭右开
let l = 0, r = nums.length
while(l < r){
let mid = Math.floor(l + (r - l) / 2)
if(nums[mid] === target){
return mid
}else if(nums[mid] > target){
r = mid
}else{
l = mid + 1
}
}
return -1
// whlie()里使用等号,
变化后的解法
let l = 0;
let r = nums.length - 1
while(l <= r){
mid = Math.floor(l + (r-l)/2)
if(nums[mid] >=target){
r = mid - 1
}else {
l = mid + 1;
}
}
let mm = l > r ? l : r
return nums[mm] === target ? mm : -1
分析一下这样的做法:
当我们内部if判断条件变成nums[mid] >=target后,这样出现的情况就是我们循环的最后一定是剩下两个数
假如存在target,倒数第二次循环剩余的两个值剩余的两个值是[小于target,target],最后一次是[target,target]
假如不存在target,剩余的两个值是[大于target,大于target]
并且注意到我们求mid值的一个细节,Math.floor()是向下取舍,所以最后一次循环只有两个数的时候,假设是l,r
right = r ,left = l, mid = l,mid值一定是等于此时的left
最后的结果:
假如target存在,倒数第二次循环剩余的两个值[小于target,target],此时的nums[mid]是小于target
那么l = mid + 1,仍满足l<=r循环条件,剩余的两个值是[target,target],下一次判断就是r = mid - 1
此时left就是target的位置,而right就是第一个小于target最后出现的位置。
分析:
假如target不存在,剩余的两个值[大于target,大于target],此时的nums[mid]是大于target
那么r = mid - 1,不满足循环条件,退出循环,此时left就是target应该存在位置
因为没有target,现在就是第一个大于target的数值的位置
而right就是第一个小于target最后出现的位置。
这样来看,又等同于target存在的情况了
结论:
所以不管target存在与否,循环结束后,right的位置就是第一个小于target最后出现的位置
left就是target应该存在的位置,假如没有target,则代表是第一个大于target的数值的位置
leetcode35.搜索插入位置
常规做法
假如数组中没有存在target,我们循环的最后一定是剩下两个数
并且注意到我们求mid值的一个细节,Math.floor()是向下取舍,所以最后一次循环,只有两个数的时候,mid值一定是等于此时的left
list[mid]如果比target小,那么right = mid - 1, 如果比target大,那么left = mid + 1,我们只需要在最后return的时候比较left与right的大小就知道target应该插入的位置。
let l = 0, r = nums.length - 1
let mid = 0
while(l <= r){
mid = Math.floor(l + (r-l)/2)
if(nums[mid]===target){
return mid
}else if(nums[mid] > target){
r = mid - 1;
}else{
l = mid + 1
}
}
return l > r ? l : r
变化后的做法
let l = 0, r = nums.length - 1
let mid = 0
while(l <= r){
mid = Math.floor(l + (r-l)/2)
if(nums[mid] >=target){
r = mid - 1
}else {
l = mid + 1;
}
}
return l > r ? l : r
leetcode34. 在排序数组中查找元素的第一个和最后一个位置
leetcode34. 在排序数组中查找元素的第一个和最后一个位置
常规做法
这个问题实现数组的原生api(indexOF、lastIndexOF)一样
当数组中不止一个target的时候,如何找到第一个和最后一个值
经历过leetcode35.搜索插入位置,我们应该知道,当数组中不存在当前值时,**循环到最后一定会剩下两个数值**
现在我们改变一下比较的条件,如果当前值**大于等于**target时,right = mid - 1,并设置返回值为 mid
设置返回值的原因是,当我们最后只剩下两个数时,mid 一定等于 left此时的值,而这个值不管是大于小于等于target
其实可以将问题拆开分析
1. 求元素的最后一个位置,不就等于求大于此元素的第一个位置再减去一
2. 而求元素的第一个位置其实和上面的右边界一样求法
const findBr = (nums,target,judge) => {
let l = 0 , r = nums.length -1,ans = nums.length
while(l <= r){
let mid = Math.floor(l + (r - l) / 2)
if(nums[mid] > target || (judge && nums[mid] >= target)){
r = mid - 1;
}else{
l = mid + 1
}
}
return l > r ? l : r
}
let res = [-1,-1]
let leftBr = findBr(nums,target,true)
let rightBr = findBr(nums,target,false) - 1
if(leftBr <= rightBr && rightBr < nums.length && nums[leftBr] === target && nums[rightBr] === target)
{
res = [leftBr,rightBr]
}
return res
变化后的做法
let l = 0 , r = nums.length -1,ans = nums.length
while(l <= r){
let mid = Math.floor(l + (r - l) / 2)
if(nums[mid] >= target){
r = mid - 1;
}else{
l = mid + 1
}
}
return l > r ? l : r
}
const findRight = (nums,targe) => {
let l = 0 , r = nums.length -1,ans = nums.length
while(l <= r){
let mid = Math.floor(l + (r - l) / 2)
if(nums[mid] > target){
r = mid - 1;
}else{
l = mid + 1
}
}
return l > r ? l : r
}
let leftBr = findLeft(nums,target)
let rightBr = findRight(nums,target) - 1
return nums[leftBr] === target ? [leftBr,rightBr] : [-1,-1]
由于这种做法找到target的应存在(实存在)的位置,和第一个小于target的值的最后一次出现的位置,所以要求target最后一次出现的位置时,需要更换一下循环条件,(nums[mid] > target)
leetcode69. sqrt(x)
常规做法
let l = 0;
let r = x;
while(l <= r){
let mid = Math.floor(l + (r-l) / 2)
if(mid * mid > x){
r = mid - 1;
}else{
l = mid + 1
}
}
return r
变化后做法
let r = x;
while(l <= r){
mid = Math.floor(l + (r-l)/2)
if(mid * mid >=x){
r = mid - 1
}else {
l = mid + 1;
}
}
return l * l > x ? l - 1: l
在上面结论说到了 left就是target应该存在的位置,假如没有target,则代表是第一个大于target的数值的位置,素以最后做一个比较,然后取值
leetcode367. 有效的完全平方数
常规做法
let l = 0;
let r = num
while(l <= r){
let mid = l + ((r - l) >> 1)
if(mid * mid === num){
return true
}else if(mid * mid < num){
l = mid + 1
}else{
r = mid - 1
}
}
return false
变化后的做法
let l = 0;
let r = num;
while(l <= r){
mid = Math.floor(l + (r-l)/2)
if(mid * mid >=num){
r = mid - 1
}else {
l = mid + 1;
}
}
return l * l > num ? false: true