33. 搜索螺旋数组
题目描述:
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
问题分析
旋转排序数组具有以下特点:
- 数组的某一部分是升序排列的,但另一部分因为旋转的原因可能并不是升序排列。
- 通过找到旋转点,我们能够把数组划分成两个部分,一部分是递增的,另一部分在旋转后也保持递增但次序不同。
在这种情况下,传统的二分查找算法无法直接使用,因为整个数组并不是完全有序的。不过,可以通过观察发现,每次将数组一分为二时,总有一部分是有序的,这是我们可以利用的特性。(这个特性十分重要,无论MID在哪里,都会遵守这个特性--MID总有一侧的数据是有序的!)
思路分析
-
确定二分查找的基础:二分查找的核心是在每一步将搜索空间减半。我们每次通过将数组分成两部分,分别检查目标值是否在其中一部分的有序区间内,从而决定搜索的方向。
-
如何判断哪部分是有序的:
- 每次通过比较 nums[left] 和 nums[mid] 来判断左半部分是否有序。
- 如果 nums[left] <= nums[mid],则说明左半部分是有序的。因为旋转不会影响这一部分,nums[left] 到 nums[mid] 保持递增。
- 否则,右半部分一定是有序的,即 nums[mid] <= nums[right]。
-
在有序区间内查找目标值:
- 如果左半部分是有序的,并且目标值 target 落在这个有序区间中,那么我们缩小搜索范围到左半部分,即更新 right = mid - 1。
- 如果目标值不在左半部分有序区间内,则我们将搜索范围缩小到右半部分,即更新 left = mid + 1。
-
反之亦然:如果发现右半部分是有序的,执行同样的判断。如果目标值在右半部分有序区间内,就在右侧继续查找;否则,继续在左侧查找。
-
终止条件:每次通过调整 left 和 right 指针,最终要么找到目标值,要么当 left > right 时,确定目标值不存在。
思路示例
以数组 [4, 5, 6, 7, 0, 1, 2] 和 target = 1 为例,具体操作如下:
-
初始状态:
- left = 0,right = 6,mid = 3,nums[mid] = 7。
- 比较 nums[left] = 4 和 nums[mid] = 7,确定左半部分 [4, 5, 6, 7] 是有序的。
- 目标 1 不在这个有序区间内,搜索右半部分 [0, 1, 2]。
-
第二次查找:
- 更新 left = 4,right = 6,mid = 5,nums[mid] = 1。
- 找到目标值 1,返回 mid = 5。
这个过程利用了旋转数组的有序性特征,确保了每次都能在正确的区间内查找目标值。
点击查看代码
func search(nums []int, target int) int {
left, right := 0, len(nums) - 1
while left <= right {
mid := left + (right - left) / 2
// 找到目标值
if nums[mid] == target {
return mid
}
// 判断左半部分是否有序
if nums[left] <= nums[mid] {
// 左侧已经有序,判断目标值是否在左半部分的有序区间内(target)
if target >= nums[left] && target < nums[mid] {
right = mid - 1
} else {
left = mid + 1
}
} else {
// 右半部分有序,判断target是否在右侧的有序范围内,否则在左侧去搜索
if target > nums[mid] && target <= nums[right] {
left = mid + 1
} else {
right = mid - 1
}
}
}
// 目标值不存在
return -1
}