二分查找
二分查找的基本情况不过多介绍,非常基础。
有难度的是找到适它适用的场景,用它或者它的变种来解决复杂问题。几个值得关注的点,也是容易犯错的点
- 最基本的原则是,二分查找只能针对有序的数组
- 边界条件,每个情况到底是用<还是≤(或>还是≥)
- 当出现重复的值,到底是取左边第一个为结果,还是取右边最后一个为结果,也就是左边界问题,和右边界问题
最基础的二分查找模板
func binarySearch(nums []int, target int) int {
left := 0 // 左指针
right := len(nums) - 1 // 右指针,注意
for left <= right {
mid := left + (right - left) / 2 // 中间位置
if nums[mid] == target { // 找到目标值
return mid
} else if nums[mid] < target { // 目标值在右半部分,注意
left = mid + 1
} else if nums[mid] > target { // 目标值在左半部分,注意
right = mid - 1
}
}
return -1 // 未找到
}
值得关注的循环条件中为什么是≤ 而不是<?
因为数组最后一个元素的索引是len(nums)-1, 而不是len(nums),索引为len(nums)是越界的。
可以理解为:
- 当right值为len(nums)-1时,相当于左右都是闭区间,可以取到,[left,right],故循环条件是left≤right,可以取到right 的值
- 当right值为len(nums)时,相当于是左开右闭区间,只能取到左边,取不到右边,[left,right),故循环条件是left<right, 此时right的索引值 实际为数组最后一个元素的索引值+1。
这个非常简单,但是就是因为太简单而非常容易忽略导致犯错,我遇到过在面试中白板写代码卡在这个地方几次,高压环境下又没有定位到问题。
第二个值得关注的点就是 不同情况下,有时用 left = mid + 1, right = mid -1,有时却是用right = mid 或者left = mid。
其实很简单,因为mid已经搜索过了,所以我们要在新区间里剔除掉mid的索引,让新区间取不到mid,接着去搜索[left,mid-1]左右闭区间,[mid+1,right]左右闭区间,就这么简单。
左右边界问题
比如有序数组[1,2,2,2,3],target为2,在第一个判断中nums[mid] ==target就返回了索引2,也就是三个2元素里中间的元素。那如果要求返回第一个2,最左边界的元素呢,或者最右边界的元素呢?
这也就是二分查找的一个缺陷,所以大部分场景是无重复元素的数组,或者特殊情况用特殊的办法来缩小区间,最后让问题变成在一个无重复元素的小区间里查找的问题。
最左边界
func leftBound (nums []int, target int) int {
left:=0
right := len(nums)
for left < right{
mid := left + (right- left) /2
if nums[mid] == target{
right = mid
}else if nums[mid] > target{
right = mid
}else if nums[mid] < target {
left = mid +1
}
}
if left < 0 || left >= len(nums) {
return -1
}
if nums[left] == target{
return left
}
return -1
}
为什么nums[mid] >target情况下 right = mid ,因为我们用的right为len(nums) ,所以条件为left<right,所以是左闭右开区间,right = mid之后,在新子区间 nums[mid]就已经取不到了。
如果right为len(nums)-1,当然这种情况下right就该等于mid-1,因为左右都是闭区间了。
可以再写一版
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 中
// 如果越界,target 肯定不存在,返回 -1
if left < 0 || left >= len(nums) {
return -1
}
// 判断一下 nums[left] 是不是 target
if nums[left] == target {
return left
}
return -1
}
为什么这样可以搜索左边界呢?
因为在nums[mid] == target时,我们不立即返回 mid,而是继续缩小 搜索区间的 上边界或者说左边界,在区间[left,mid)中继续搜索,就可以锁定左边界。
再来看右边界问题,同理只需要在nums[mid] == target时,不返回mid,继续增大左边界(向右压缩)。
func binarySearch(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 {
// 直接返回
return mid
}
}
// 直接返回
return -1
}
func leftBound(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 {
// 别返回,锁定左侧边界
right = mid - 1
}
}
// 判断 target 是否存在于 nums 中
if left < 0 || left >= len(nums) {
return -1
}
// 判断一下 nums[left] 是不是 target
if nums[left] == target {
return left
}
return -1
}
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
}
}
// 判断 target 是否存在于 nums 中
// if left - 1 < 0 || left - 1 >= len(nums) {
// return -1
// }
if right < 0 || right >= len(nums) {
return -1
}
if nums[right] == target {
return right
}
return -1
}