motivation
写之前的碎碎念,不知不觉已经拿到了Leetcode的3月份刷题打卡徽章(实际上最后几天就已经在注意了哈哈哈)。同时LeetCode刷题系列已经很久没有更新,加上最近看到的一个面试题目,是属于二分查找的思想在做,但是我们平时使用二分查找时却大多数只用在有序数组中特定元素的查找、有序数组等于特定元素的最小索引、或者是有序数组中满足条件的最小值。
看起来是比较局限,可能是前人已经把问题精简到对一个有序数组进行查找这样的地步,但是在一些问题中,可能并没有直接给出这样的有序数组,而是暗含在某种计算中。
作者目前对二分查找也是在学习之中,掌握基本思想和算法,需要进一步的代码题目熟练。
二分查找适用情况
搜索范围:(begin, end)
单调性:对于属于begin到end中的任意x,y,如果x<y,则有utility(x) <= utility(y)
这里是用utility(;)来表示对应点进行判断是否满足条件时所用到的一个评估值,读者可以和有序数组来进行对应。
二分搜索模板
INIT:
left = begin, right = end
ITERATION:
while begin < end :
calculate(mid) // 不同的mid计算方法
if ( judge(mid) == LABEL1 ): // 判断mid是否符合条件
UPDATE( left )
if ( judge(mid) == LABEL2 ):
UPDATE( right )
if ( judge(mid) == LABEL3 ):
blahblah
上边的比较泛化,且不是很完整,目的是想要指出:二分查找是一种针对有序数组的快速查找思想,其判断条件是根据问题而异的。
下边以有序数组中查找特定目标值为例子,进行表述。
经典例子:查找排序数组的目标值
问题表述和算法
Pre:
arr : 有序数组
goal: 目标值(数组中的值)
Post:
数组中目标值的下标,如果没有找到目标值,则返回-1
// 实际上下边这种判断和更新方式是找到数组中最左侧等于目标值的下标
Algorithm:
left = 0, right = arr.size()
while ( left < right ):
mid = left + (right-left)/2
if ( arr[mid]<goal ):
left = mid+1
else :
right = mid
return (arr[left]==goal) ? left : -1
这里的搜索范围是从0到数组长度,是否满足条件找到目标值judge判断方式是 arr[mid]与goal的相对大小。
单调性: 数组有序
对于本问题来说utility(x) 就是数组中对应下标的值:arr[x]
二分搜索的几个细节
- inter 的计算方式
- 当arr[mid]=goal为真时,元素的更新
分析
mid的计算方式主要有两种:向下取整和向上取整
mid = (left+right)/2 ;
mid = (left+right)/2 + 1 ;
根据将arr[mid]=goal也有两种分配情况:
// 1.
if ( arr[mid] <= goal ){
left 更新
}else{
right 更新
}
// 2.
if ( arr[mid] < goal ){
left 更新
}else{
right 更新
}
总共有4种组合方式:
// 寻找到arr中最左侧等于goal的index
int binary_search( vector<int>& arr, int goal ){
int left = 0, right = arr.size()-1 ;
while ( left < right ){
int mid = (left+right)/2 ;
if ( arr[mid] < goal ){
left = mid + 1 ;
}else{
right = mid ;
}
}
return left ;
}
// !error! , 由于arr[mid]==goal时,该值直接被略过了:begin = inter + 1
int binary_search( vector<int>& arr, int goal ){
int left = 0, right = arr.size()-1 ;
while ( left < right ){
int mid = (left+right)/2 ;
if ( arr[mid] <= goal ){
left = mid + 1 ;
}else{
right = mid ;
}
}
return left ;
}
// !error! 当搜索范围的长度只剩下2,arr[inter]==goal时,end不再更新逼近
int binary_search( vector<int>& arr, int goal ){
int left = 0, right = arr.size()-1 ;
while ( left < right ){
int mid = (left+right)/2 + 1 ;
if ( arr[mid] < goal ){
left = mid + 1 ;
}else{
right = mid ; // [2,2]将保持不变
}
}
return left ;
}
// 找到arr中最右侧的值为goal的index
int binary_search( vector<int>& arr, int goal ){
int left = 0, right = arr.size()-1 ;
while ( left < right ){
int mid = (left+right)/2 + 1 ;
if ( arr[mid] <= goal ){
left = mid ;
}else{
right = mid -1 ;
}
}
return left ;
}
总结
mid的更新方式,影响着left和right的更新方式(由于收敛的要求,要最终left>=right)
- 当使用 mid= (left+right)/2 时,由于mid是向下压缩的,则left在更新是要使用left=mid+1来避免上半部分区间长度在只有两个元素时不再减小的情况出现。即arr[mid]不为goal,因此:arr[mid]=goal 与 > goal 的判断合并(right更新的判断)
- 当使用 mid=(left+right)/2+1时,由于mid是向上压缩的,则right在更新时要使用right=mid-1来避免下半部分区间长度在只有两个元素时不再减小的情况出现。即arr[mid]不为goal,因此:arr[mid]=goal 与 < goal的判断合并(left更新的判断)
其他例子
utility(x) = x*x
utility(x) : 数组从begin到x的累积和
PS.
相比较于其他已有的leetcode刷题笔记,我希望能够提供相关的解题分析和部分相关问题的链接,使得大家能获得一个较好的分析与相关问题的比较。
偶尔会进行类似题目的总结工作,有需要的读者可以对这份工作进行关注。
如果你发现有相关的错误或者表述不当不清晰的地方,请进行评论,我会尽快进行核对勘正。