本文会汇总常见的二分查找题型并给出相关Leetcode题目的题解
二分查找框架:
基础二分算法:
二分查找是特定对于有序的数组中寻找特定元素或者特定元素位置的算法,我一直使用的是[left,right] 闭区间的算法方式,所以我们循环退出的条件是while (l<=r) ,也就是二者错开才退出循环,当l=r的时候仍然可能搜索到数据。
Go
func search(nums []int, target int) int {
l,r:=0,len(nums)-1
//定义区间为l-r的闭区间,每一个元素都可能被搜索到
for l<=r{
//防止超过最大范围取中点
mid:=(r-l)/2+l
if nums[mid]==target{
return mid
}else if nums[mid]>target{
//当前值大于target,缩小范围,也就是动右指针
r=mid-1
}else{
l=mid+1
}
}
return -1
}
下面针对mid的定义是为了防止溢出设置的,而针对指针的移动,因为mid在不匹配的情况下就证明了从l或者r的区间到mid是没有目标数据的,那么直接移动,使得l=mid+1或者r=mid-1。
搜索左侧边界的二分算法:
只搜索对应元素还是太基础了,我们经常会遇见搜索某一个数字的最左侧或者最右侧边界的问题,那么这类问题和上方的不同点在于数组中会含有多个target目标值。
Go
// 将给定的数字 nums 搜索 target 的左侧边界
func left_bound(nums []int, target int) int {
left := 0
right := len(nums) - 1
// 搜索区间为 [left, right]
for left <= right {
mid := left + (right - left) / 2
if nums[mid] < target {
// 搜索区间变为 [mid+1, right]
left = mid + 1
} else if nums[mid] > target {
// 搜索区间变为 [left, mid-1]
right = mid - 1
} else if nums[mid] == target {
// 收缩右侧边界
right = mid - 1
}
}
// 判断 target 是否存在于 nums 中
if left < 0 || left >= len(nums) {
return -1
}
// 如果越界,target 肯定不存在,返回 -1
if nums[left] == target {
// 判断一下 nums[left] 是不是 target
return left
}
return -1
}
在这里,我们针对mid=target的时候需要不同的处理,因为我们在搜索边界值,那么当二者相等的时候,我们不可以直接返回,需要不断收缩右边界,看看l到mid-1还有没有目标值了。在整个循环的过程中,假设数组为[1,2,3,3,4,5,6],最后一次循环的时候,右侧指针还要向左移动到2指向的位置,而left一直指向的是最左侧3的位置,所以我们需要返回left。
搜索右侧边界的二分算法:
同理,搜索右侧边界的代码如下:
Go
func rightBound(nums []int, target int) int {
left, right := 0, len(nums) - 1
for left <= right {
mid := left + (right - left) / 2
if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid - 1
} else if nums[mid] == target {
// 这里改成收缩左侧边界即可
left = mid + 1
}
}
// 最后改成返回 left - 1
if left - 1 < 0 || left - 1 >= len(nums) {
return -1
}
if nums[right] == target {
return right
}
return -1
}
本题就是对于上面搜索左右边界的使用,只需要把两个代码拼起来即可解题。
Go
func searchRange(nums []int, target int) []int {
res:=[]int{-1,-1}
left,right:=0,len(nums)-1
for left<=right{
mid:=left+(right-left)/2
if target<=nums[mid]{
right=mid-1
}else{
left=mid+1
}
}
if left<len(nums)&&nums[left]==target{
res[0]=left
}
left, right = 0, len(nums)-1
for left<=right{
mid:=left+(right-left)/2
if target>=nums[mid]{
left=mid+1
}else{
right=mid-1
}
}
if right>=0&&nums[right]==target{
res[1]=right
}
return res
}
二分查找习题运用:
对于本题,数组非严格递增,如果有多个符合条件的目标成绩需要找到左右端点,下标相减+1就是总共的成绩数。
Go
func searchLeft(scores []int,target int)int{
l,r:=0,len(scores)-1
for l<=r{
mid:=(r-l)/2+l
if scores[mid]==target{
r=mid-1
}else if scores[mid]<target{
l=mid+1
}else{
r=mid-1
}
}
if l<0||l>=len(scores){
return -1
}
if scores[l]==target{
return l
}
return -1
}
func searchRight(scores []int,target int)int{
l,r:=0,len(scores)-1
for l<=r{
mid:=(r-l)/2+l
if scores[mid]==target{
l=mid+1
}else if scores[mid]<target{
l=mid+1
}else{
r=mid-1
}
}
if r<0||r>=len(scores){
return -1
}
if scores[r]==target{
return r
}
return -1
}
func countTarget(scores []int, target int) int {
res:=0
left:=searchLeft(scores,target)
right:=searchRight(scores,target)
if left==-1&&right==-1{
return 0
}
res=right-left+1
return res
}
观察题目给定数据,我们发现在旋转点前和旋转点之后的两个数组都是有序的,那么只需要对这两个区间分别进行二分查找,再重新旋转返回对应下标即可。
Go
func binsearch(nums []int,target int)int{
l,r:=0,len(nums)-1
for l<=r{
mid:=(r-l)/2+l
if nums[mid]==target{
return mid
}else if nums[mid]<target{
l=mid+1
}else{
r=mid-1
}
}
return -1
}
func search(nums []int, target int) int {
index:=0
for i:=0;i<len(nums)-1;i++{
if nums[i]>nums[i+1]{
//index找到的是下标K
index=i+1
}
}
leftNum:=nums[:index]
rightNum:=nums[index:]
ll:=binsearch(leftNum,target)
rl:=binsearch(rightNum,target)
if ll!=-1{
return ll
}
if rl!=-1{
return (rl+index)%len(nums)
}
return -1
}
题目给的条件是二维数组每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。所以直接遍历存在nums当中二分查找。
Go
func searchMatrix(matrix [][]int, target int) bool {
m,n:=len(matrix),len(matrix[0])
nums:=[]int{}
for i:=0;i<m;i++{
for j:=0;j<n;j++{
nums=append(nums,matrix[i][j])
}
}
l,r:=0,m*n-1
for l<=r{
mid:=(r-l)/2+l
if nums[mid]==target{
return true
}else if nums[mid]<target{
l=mid+1
}else{
r=mid-1
}
}
return false
}