二分查找及其变种

本文涉及 4 道「搜索旋转排序数组」题:

  • LeetCode 33 题:搜索旋转排序数组
  • LeetCode 81 题:搜索旋转排序数组-ii
  • LeetCode 153 题:寻找旋转排序数组中的最小值
  • LeetCode 154 题:寻找旋转排序数组中的最小值-ii

可以分为 3 类:

  • 33、81 题:搜索特定值
  • 153、154 题:搜索最小值
  • 81、154 题:包含重复元素

33搜索旋转排序数组

题目要求时间复杂度 O(logn),显然应该使用二分查找。二分查找的过程就是不断收缩左右边界,而怎么缩小区间是关键。

如果数组「未旋转」,在数组中查找一个特定元素 target 的过程为:

  • 若 target == nums[mid],直接返回
  • 若 target < nums[mid],则 target 位于左侧区间 [left,mid) 中。令 right = mid-1,在左侧区间查找
  • 若 target > nums[mid],则 target 位于右侧区间 (mid,right] 中。令 left = mid+1,在右侧区间查找

但是这道题,由于数组「被旋转」,所以左侧或者右侧区间不一定是连续的。在这种情况下,如何判断 target 位于哪个区间?

根据旋转数组的特性,当元素不重复时,如果 nums[i] <= nums[j],说明区间 [i,j] 是「连续递增」的。

i、j 可以重合,所以这里使用的比较运算符是「小于等于」

因此,在旋转排序数组中查找一个特定元素时:

1.若 target == nums[mid],直接返回
2.若 nums[left] <= nums[mid],说明左侧区间 [left,mid]「连续递增」。此时:
若 nums[left] <= target <= nums[mid],说明 target 位于左侧。令 right = mid-1,在左侧区间查找
否则,令 left = mid+1,在右侧区间查找。
3.否则,说明右侧区间 [mid,right]「连续递增」。此时:
若 nums[mid] <= target <= nums[right],说明 target 位于右侧区间。令 left = mid+1,在右侧区间查找
否则,令 right = mid-1,在左侧区间查找
注意:区间收缩时不包含 mid,也就是说,实际收缩后的区间是 [left,mid) 或者 (mid,right]

func search(nums []int, target int) int {
if len(nums) == 0 {
return -1
}
left, right, mid := 0, len(nums)-1, 0
for left <= right {
mid = (left + right) / 2
if nums[mid] == target {
return mid
}
// [left,mid] 连续递增
if nums[left] <= nums[mid] {
if nums[left] <= target && target <= nums[mid] {
right = mid - 1 // 在左侧 [left,mid) 查找
} else {
left = mid + 1
}
} else { // [mid,right] 连续递增
if nums[mid] <= target && target <= nums[right] {
left = mid + 1 // 在右侧 (mid,right] 查找
} else {
right = mid - 1
}
}
}
return -1
}

81. 搜索旋转排序数组

这道题是 33 题的升级版,元素可以重复。当 nums[left] == nums[mid] 时,无法判断 target 位于左侧还是右侧,此时无法缩小区间,退化为顺序查找。

顺序查找的一种方法是直接遍历 [left,right] 每一项:

if nums[left] == nums[mid] {
for i := left; i <= right; i++ {
if nums[i] == target {
return i
}
}

另一种方法是令 left++,去掉一个干扰项,本质上还是顺序查找:

if nums[left] == nums[mid] {
    left++
    continue
}

153. 搜索旋转排序数组中的最小值

如果数组没有翻转,即 nums[left] <= nums[right],则 nums[left] 就是最小值,直接返回。

如果数组翻转,需要找到数组中第二部分的第一个元素:

下面讨论数组翻转的情况下,如何收缩区间以找到这个元素:

若 nums[left] <= nums[mid],说明区间 [left,mid] 连续递增,则最小元素一定不在这个区间里,可以直接排除。因此,令 left = mid+1,在 [mid+1,right] 继续查找

否则,说明区间 [left,mid] 不连续,则最小元素一定在这个区间里。因此,令 right = mid,在 [left,mid] 继续查找。
[left,right] 表示当前搜索的区间。注意 right 更新时会被设为 mid 而不是 mid-1,因为 mid 无法被排除。这一点和「33 题 查找特定元素」是不同的

func findMin (nums []int) int {
left, right := 0, len(nums)-1
for left <= right { // 实际上是不会跳出循环,当 left==right 时直接返回 if nums[left] <= nums[right] { // 如果 [left,right] 递增,直接返回 return nums[left] } mid := left + (right-left)>>1
if nums[left] <= nums[mid] { // [left,mid] 连续递增,则在 [mid+1,right] 查找
left = mid + 1
}else {
right = mid // [left,mid] 不连续,在 [left,mid] 查找
}
}
return -1
}

154. 搜索旋转排序数组中的最小值

这道题是 153 题的升级版,元素可以重复。和 81 题一样,当 nums[left] == nums[mid] 时,退化为顺序查找。

81 题提供了两种方法:

  • 一种是直接遍历 [left,right] 每一项
  • 另一种是 left++,跳过一个干扰项


154 题只能使用第一种方法。因为如果 left 是最小元素,那么 left++ 就把正确结果给跳过了。

总结

在旋转排序数组中进行二分查找时,无论是搜索特定值,还是搜索最小值,都需要在左右两个区间里,找到「连续递增」的那个区间。

判断区间是否「连续递增」,只需比较区间边界值:如果 nums[left] <= nums[mid],则区间 [left,mid] 连续递增;反之,区间 [mid,right] 连续递增。但是上述判断仅适用于数组中不含重复元素的情况,如果数组中包含重复元素,那么在 nums[left]==nums[mid] 时将退化为线性查找

找到「连续递增」的区间后,问题就变得简单了许多:

  • 33 题,查找特定值:只需要判断目标值在「连续递增」区间内还是区间外。比如当区间 [left,mid] 连续递增时,若目标值位于该区间内,则 right = mid-1;若目标值位于该区间外,则 left = mid+1。如果是区间 [mid,right] 连续递增,也可以用类似的方法收缩区间
  • 153 题,查找最小值:只需要排除左侧或者右侧的一段「连续区间」,使得 [left,right] 不连续,就可以找到最小值

更多内容欢迎访问我的个人博客网站:www.zpf0000.com 

  • 26
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值