二分搜索大家都很熟悉,首先我们先来看看基本框架
func binarySearch(nums []int, target int) int {
left, right := 0, ...
for ... {
mid := left + (right-left)/2
if nums[mid] == target {
...
} else if nums[mid] < target {
left = ...
} else if nums[mid] > target {
right = ...
}
}
return ...
}
看完代码之后我们会发现:
- 数组得是有序的才能成立
- 在存在两个及以上的答案时,只能找出一个,至于下标的情况我觉得可以自定义
- 如果数组长度为偶数,初始化时mid在中间偏左的那一格
接下来在实战中看:
寻找一个数
这可以说是最简单的内容
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 // [mid + 1, right]
} else if nums[mid] > target { // 目标值在左半部分,注意
right = mid - 1 // [left, mid - 1]
}
}
return -1 // 未找到
}
这里需要注意一点,为何是 for left <= right
而不是 for left < right
?
我们可以先从最极端的情况入手,假设要寻找的数在最后一个,那么在前一步应该是 left = right - 1
, mid = left
,然后 left = mid + 1 = right
,在进行循环时,循环就会进不去。
从理论上解释,因为在之前设定值时,数组为闭区间 [left, right]
,所以得保证边界值,也就是说,循环的条件设定和搜索区间设定是相关联的
寻找左侧边界
那么接下来看一下右侧开区间的做法:
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 {
left = mid + 1 // [mid + 1, right)
} else if nums[mid] > target {
right = mid // [left, mid)
}
}
return left
}
不难发现,随着区间的修改,在对应条件下的操作也会对应转换。
在这里阐述一点我的个人想法,二分查找的最大特点在于区间设定,而在对应条件下做出的操作有点像递归,基本盘并没有改变
寻找右侧边界
func right_bound(nums []int, target int) int {
left, right := 0, len(nums)
for left < right {
mid := left + (right-left)/2
if nums[mid] == target {
left = mid + 1
} else if nums[mid] < target {
left = mid + 1
} else if nums[mid] > target {
right = mid
}
}
return left - 1
}
这里面的精华在于
if nums[mid] == target {
left = mid + 1
他并没有直接的收网,而是继续直到最右侧的诞生,这也得益于mid的设置,可以把他看成以left为基准向前进
实战
我们来看力扣的34. 在排序数组中查找元素的第一个和最后一个位置
直接把方法摆上既可以解决
func searchRange(nums []int, target int) []int {
left := leftBound(nums, target)
right := rightBound(nums, target)
return []int{left, right}
}
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
}
当然也有另外一种解法,力扣显示这种时间复杂度更低