二分查找
注意:排序数组中的搜索问题,首先想到二分法。
一、基本的二分查找
最简单的情况就是搜索一个数,如果存在,返回索引;否则返回-1。
func search(nums []int, target int) int {
// 二分查找
left, right := 0, len(nums) - 1 // 注意
for left <= right {
mid := left + (right - left) >> 1 // 注意
if nums[mid] == target {
return mid
} else if nums[mid] > target {
right = mid - 1 // 注意
} else if nums[mid] < target {
left = mid + 1 // 注意
}
}
return -1
}
mid防止溢出
代码mid := left + (right - left) >> 1
目的是为了防止left
和right
太大相加导致溢出。
循环结束条件
循环结束条件是<=
,而不是<
。
是因为初始化right
的赋值是nums.length - 1
,即最后一个元素的索引,而不是nums.length
。两者出现在不同功能的二分查找中,区别是:前者相当于两端都是闭区间[left, right]
,后者相当于左闭右开区间[left, right)
。我们可以把这个区间称作搜索区间。而搜索未找到停止搜索的条件就是搜索区间为空。当left == right
时,搜索区间还有一个值。所以循环结束条件是<=
。
left、right
每次迭代更替的值
此处是left = mid + 1, right = mid - 1
,有时候是right = mid
或者left = mid
。为什么呢?
当我们理解搜索区间的概念后,本算法中搜索区间两端都是闭的。当发现索引mid
不是要找的target
时,下一步的搜索区间应该是[left, mid - 1]
或者[mid + 1, right]
。因为mid
已经搜索过,应该从搜索区间中去除。
二、搜索左边界
常见代码如下:
func search(nums []int, target int) int {
if len(nums) == 0 {
return -1
}
left, right := 0, len(nums) // 注意
for left < right { //注意
mid := left + (right - left) >> 1
if nums[mid] == target {
right = mid
} else if nums[mid] > target {
right = mid // 注意
} else if nums[mid] < target {
left = mid + 1
}
}
return left
}
循环结束条件
此处终止条件是left < right
。是因为前面right = nums.length
,所以搜索区间是[left, right)
左闭右开。所以循环的终止条件等价于left == right
,此时搜索区间已经为空,可以终止。
注意:至于为什么这里写成左闭右开搜索区间形式,是因为这种写法较为普遍。也可以赋值为
right = nums.length - 1
写为闭区间。
返回值解惑
- 为什么没有返回-1操作?如果
nums
中不存在target
这个值,怎么办?
先理解左侧边界有什么特殊含义:
对于上述数组,算反会返回1。这个1的含义可以解读为:nums
中小于2的元素有1个。
比如对于有序数组nums = [2,3,5,7],target = 1
,算法会返回0,含义是:nums
中小于1的元素有0个。
再比如说nums = [2,3,5,7],target = 8
,算法会返回4,含义是:nums
中小于8的元素有4个。
可以看出,函数返回值(即left
变量的值)取值区间是闭区间[0, nums.length]
。所以我们可以在循环结束后面添加如下代码,就能在正确的时候返回-1:
for left < right {
// ...
}
// target 比所有数都大
if left == len(nums) {
return -1
}
// 处理其他情况
if nums[left] == target {
return left
} else {
return -1
}
- 为什么返回
left
而不是right
?
因为循环终止条件是left == right
,所以都是一样的。
left、right
每次迭代更替的值
此处right = mid, left = mid + 1
是因为搜索区间是[left, right)
左闭右开,所以当nums[mid]
被检测之后,下一步搜索区间应该去掉mid
分隔为两个区间,即[left, mid)
和[mid + 1, right)
。
为什么能搜索左侧区间?
关键在于对nums[mid] == target
这种情况的处理:
找到target时不立即返回,而是缩小搜索区间的上届right
,在区间[left, mid)
中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
三、搜索右边界
和搜索左边界类似,依然采用左闭右开搜索区间的写法,有两处不同已标注:
func search(nums []int, target int) int {
if len(nums) == 0 {
return -1
}
left, right := 0, len(nums)
for left < right {
mid := left + (right - left) >> 1
if nums[mid] == target {
left = mid + 1 // 注意
} else if nums[mid] > target {
right = mid
} else if nums[mid] < target {
left = mid + 1
}
}
return left - 1 // 注意
}
为什么能搜索右侧区间?
类似的,关键点在于:
当nums[mid] == target
时,不要立即返回,而是增大搜索区间的下界left
,使得区间不断向右收缩,达到锁定右侧边界的目的。
返回值解惑
- 为什么最后返回
left - 1
而不像左侧边界的函数,返回left
?而且我觉得这里既然是搜索右侧边界,应该返回right
才对。
因为循环终止条件是left == right
,所以left
和right
是一样的。
至于为什么要减一,是搜索右侧边界的一个特殊点,关键在这个条件判断:
因为我们对left
的更新必须是left = mid + 1
,而循环结束时,nums[left]
一定不等于target
了,而nums[left - 1]
可能是target
。
- 为什么没有返回-1操作?如果
nums
中不存在target
这个值,怎么办?
类似搜索左边界,left
的取值范围是[0, nums;length]
,所以添加如下代码,可以正确返回-1:
for left < right {
// ...
}
// target 比所有数都小
if left == 0 {
return -1
}
// 处理其他情况
if nums[left - 1] == target {
return left - 1
} else {
return -1
}