【LeetCode】33. 搜索旋转排序数组
文章目录
- 33. 搜索旋转排序数组
- 描述
- 示例 1:
- 示例 2:
- 示例 3:
- 示例 4:
- 提示:
- 解题思路
- 方法一:二分查找(推荐)
- 方法二:先找旋转点,再二分查找
- 方法三:线性搜索
- 代码实现
- 复杂度分析
- 算法图解
- 二分查找过程图
- 旋转数组结构图
- 二分查找决策树
- 边界情况处理
- 测试用例
- 完整题解代码
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) 的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
示例 4:
输入:nums = [3,1], target = 1
输出:1
提示:
- 1 <= nums.length <= 5000
- -10^4 <= nums[i] <= 10^4
- nums 中的每个值都 独一无二
- 题目数据保证 nums 在预先未知的某个下标上进行了旋转
- -10^4 <= target <= 10^4
解题思路
方法一:二分查找(推荐)
核心思想:
- 旋转数组可以分成两个有序部分
- 通过比较中间元素与边界元素,确定哪一半是有序的
- 在有序的一半中进行二分查找,在无序的一半中继续递归
算法步骤:
- 初始化左右指针 left = 0, right = len(nums) - 1
- 计算中间位置 mid = (left + right) / 2
- 如果 nums[mid] == target,返回 mid
- 判断左半部分是否有序:
- 如果 nums[left] <= nums[mid],左半部分有序
- 如果 target 在左半部分范围内,在左半部分搜索
- 否则在右半部分搜索
- 否则右半部分有序:
- 如果 target 在右半部分范围内,在右半部分搜索
- 否则在左半部分搜索
时间复杂度:O(log n)
空间复杂度:O(1)
方法二:先找旋转点,再二分查找
核心思想:
- 先找到旋转点(最小值的位置)
- 根据 target 与边界值的比较,确定在哪个有序区间搜索
- 在确定的有序区间中进行二分查找
时间复杂度:O(log n)
空间复杂度:O(1)
方法三:线性搜索
核心思想:
- 直接遍历数组查找目标值
- 适用于小规模数据或调试
时间复杂度:O(n)
空间复杂度:O(1)
代码实现
// 二分查找解法
func search(nums []int, target int) int {left, right := 0, len(nums)-1for left <= right {mid := left + (right-left)/2if nums[mid] == target {return mid}// 判断左半部分是否有序if nums[left] <= nums[mid] {// 左半部分有序if nums[left] <= target && target < nums[mid] {right = mid - 1} else {left = mid + 1}} else {// 右半部分有序if nums[mid] < target && target <= nums[right] {left = mid + 1} else {right = mid - 1}}}return -1
}
复杂度分析
方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
二分查找 | O(log n) | O(1) | 推荐,最优解 |
找旋转点+二分 | O(log n) | O(1) | 思路清晰 |
线性搜索 | O(n) | O(1) | 小规模数据 |
算法图解
二分查找过程图
graph TDA[开始: nums=[4,5,6,7,0,1,2], target=0] --> B[第一次二分: left=0, right=6, mid=3]B --> C{检查 nums[mid]=7}C --> D[左半部分有序: nums[0:3]=[4,5,6,7]]D --> E{target=0 在左半部分范围内?}E -->|否| F[在右半部分搜索: left=4, right=6]F --> G[第二次二分: left=4, right=6, mid=5]G --> H{检查 nums[mid]=1}H --> I[左半部分有序: nums[4:4]=[0]]I --> J{target=0 在左半部分范围内?}J -->|是| K[在左半部分搜索: left=4, right=4]K --> L[第三次二分: left=4, right=4, mid=4]L --> M{检查 nums[mid]=0}M --> N[找到目标值! 返回索引4]style N fill:#90EE90style A fill:#E6F3FFstyle B fill:#FFF2CCstyle G fill:#FFF2CCstyle L fill:#FFF2CC
旋转数组结构图
二分查找决策树
graph TDA[开始二分查找] --> B{检查 nums[mid] == target?}B -->|是| C[返回 mid]B -->|否| D{左半部分有序?<br/>nums[left] <= nums[mid]}D -->|是| E{target 在左半部分范围内?<br/>nums[left] <= target < nums[mid]}E -->|是| F[在左半部分搜索<br/>right = mid - 1]E -->|否| G[在右半部分搜索<br/>left = mid + 1]D -->|否| H{target 在右半部分范围内?<br/>nums[mid] < target <= nums[right]}H -->|是| I[在右半部分搜索<br/>left = mid + 1]H -->|否| J[在左半部分搜索<br/>right = mid - 1]F --> K{left <= right?}G --> KI --> KJ --> KK -->|是| BK -->|否| L[返回 -1]style C fill:#90EE90style L fill:#FFB6C1style A fill:#E6F3FF
边界情况处理
- 数组长度为1:直接比较
- 数组长度为2:分别比较两个元素
- 没有旋转:数组完全有序
- 目标值不存在:返回-1
- 目标值在边界:正确处理边界条件
测试用例
func main() {// 测试用例1nums1 := []int{4, 5, 6, 7, 0, 1, 2}target1 := 0fmt.Printf("测试用例1: nums=%v, target=%d, 结果=%d\n", nums1, target1, search(nums1, target1))// 测试用例2nums2 := []int{4, 5, 6, 7, 0, 1, 2}target2 := 3fmt.Printf("测试用例2: nums=%v, target=%d, 结果=%d\n", nums2, target2, search(nums2, target2))// 边界测试nums3 := []int{1}target3 := 0fmt.Printf("边界测试1: nums=%v, target=%d, 结果=%d\n", nums3, target3, search(nums3, target3))nums4 := []int{3, 1}target4 := 1fmt.Printf("边界测试2: nums=%v, target=%d, 结果=%d\n", nums4, target4, search(nums4, target4))
}
完整题解代码
package mainimport "fmt"// 方法一:二分查找解法(推荐)
// 时间复杂度:O(log n),空间复杂度:O(1)
func search(nums []int, target int) int {left, right := 0, len(nums)-1for left <= right {mid := left + (right-left)/2if nums[mid] == target {return mid}// 判断左半部分是否有序if nums[left] <= nums[mid] {// 左半部分有序if nums[left] <= target && target < nums[mid] {right = mid - 1} else {left = mid + 1}} else {// 右半部分有序if nums[mid] < target && target <= nums[right] {left = mid + 1} else {right = mid - 1}}}return -1
}// 方法二:先找旋转点,再二分查找
// 时间复杂度:O(log n),空间复杂度:O(1)
func searchFindPivot(nums []int, target int) int {if len(nums) == 0 {return -1}// 找到旋转点(最小值的位置)pivot := findPivot(nums)// 根据target与边界值的比较,确定搜索区间if pivot == 0 {// 数组没有旋转,直接二分查找return binarySearch(nums, target, 0, len(nums)-1)}// 在左半部分搜索if nums[0] <= target && target <= nums[pivot-1] {return binarySearch(nums, target, 0, pivot-1)}// 在右半部分搜索return binarySearch(nums, target, pivot, len(nums)-1)
}// 找到旋转点(最小值的位置)
func findPivot(nums []int) int {left, right := 0, len(nums)-1for left < right {mid := left + (right-left)/2if nums[mid] > nums[right] {left = mid + 1} else {right = mid}}return left
}// 标准二分查找
func binarySearch(nums []int, target, left, right int) int {for left <= right {mid := left + (right-left)/2if nums[mid] == target {return mid} else if nums[mid] < target {left = mid + 1} else {right = mid - 1}}return -1
}// 方法三:线性搜索(用于验证和调试)
// 时间复杂度:O(n),空间复杂度:O(1)
func searchLinear(nums []int, target int) int {for i, num := range nums {if num == target {return i}}return -1
}// 方法四:优化的二分查找解法
// 时间复杂度:O(log n),空间复杂度:O(1)
func searchOptimized(nums []int, target int) int {if len(nums) == 0 {return -1}left, right := 0, len(nums)-1for left <= right {mid := left + (right-left)/2if nums[mid] == target {return mid}// 处理重复元素的情况(虽然题目保证元素互不相同)if nums[left] == nums[mid] && nums[mid] == nums[right] {left++right--continue}// 判断左半部分是否有序if nums[left] <= nums[mid] {// 左半部分有序if nums[left] <= target && target < nums[mid] {right = mid - 1} else {left = mid + 1}} else {// 右半部分有序if nums[mid] < target && target <= nums[right] {left = mid + 1} else {right = mid - 1}}}return -1
}// 辅助函数:打印数组和搜索过程
func printSearchProcess(nums []int, target int) {fmt.Printf("数组: %v, 目标值: %d\n", nums, target)// 找到旋转点pivot := findPivot(nums)fmt.Printf("旋转点位置: %d (值: %d)\n", pivot, nums[pivot])// 显示有序区间if pivot == 0 {fmt.Println("数组没有旋转,完全有序")} else {fmt.Printf("左半部分有序区间: [%d, %d]\n", 0, pivot-1)fmt.Printf("右半部分有序区间: [%d, %d]\n", pivot, len(nums)-1)}result := search(nums, target)fmt.Printf("搜索结果: %d\n", result)fmt.Println()
}func main() {fmt.Println("=== 33. 搜索旋转排序数组 ===")// 测试用例1nums1 := []int{4, 5, 6, 7, 0, 1, 2}target1 := 0fmt.Printf("测试用例1: nums=%v, target=%d\n", nums1, target1)fmt.Printf("二分查找解法结果: %d\n", search(nums1, target1))fmt.Printf("找旋转点+二分解法结果: %d\n", searchFindPivot(nums1, target1))fmt.Printf("线性搜索解法结果: %d\n", searchLinear(nums1, target1))fmt.Printf("优化二分查找解法结果: %d\n", searchOptimized(nums1, target1))fmt.Println()// 测试用例2nums2 := []int{4, 5, 6, 7, 0, 1, 2}target2 := 3fmt.Printf("测试用例2: nums=%v, target=%d\n", nums2, target2)fmt.Printf("二分查找解法结果: %d\n", search(nums2, target2))fmt.Printf("找旋转点+二分解法结果: %d\n", searchFindPivot(nums2, target2))fmt.Printf("线性搜索解法结果: %d\n", searchLinear(nums2, target2))fmt.Printf("优化二分查找解法结果: %d\n", searchOptimized(nums2, target2))fmt.Println()// 边界测试用例testCases := []struct {nums []inttarget intdesc string}{{[]int{1}, 0, "单个元素,目标不存在"},{[]int{1}, 1, "单个元素,目标存在"},{[]int{3, 1}, 1, "两个元素"},{[]int{3, 1}, 3, "两个元素,目标在开头"},{[]int{1, 3}, 3, "两个元素,未旋转"},{[]int{1, 3}, 1, "两个元素,未旋转,目标在开头"},{[]int{1, 2, 3, 4, 5}, 3, "完全有序数组"},{[]int{5, 1, 2, 3, 4}, 1, "旋转一个位置"},{[]int{2, 3, 4, 5, 1}, 1, "旋转到末尾"},}for _, tc := range testCases {result := search(tc.nums, tc.target)fmt.Printf("%s: nums=%v, target=%d, 结果=%d\n", tc.desc, tc.nums, tc.target, result)}// 详细搜索过程演示fmt.Println("\n=== 详细搜索过程演示 ===")demoCases := []struct {nums []inttarget int}{{[]int{4, 5, 6, 7, 0, 1, 2}, 0},{[]int{4, 5, 6, 7, 0, 1, 2}, 6},{[]int{3, 1}, 1},{[]int{1, 2, 3, 4, 5}, 3},}for _, demo := range demoCases {printSearchProcess(demo.nums, demo.target)}// 算法正确性验证fmt.Println("=== 算法正确性验证 ===")verifyNums := []int{4, 5, 6, 7, 0, 1, 2}verifyTarget := 5fmt.Printf("验证数组: %v, 目标值: %d\n", verifyNums, verifyTarget)fmt.Printf("二分查找解法: %d\n", search(verifyNums, verifyTarget))fmt.Printf("找旋转点+二分解法: %d\n", searchFindPivot(verifyNums, verifyTarget))fmt.Printf("线性搜索解法: %d\n", searchLinear(verifyNums, verifyTarget))fmt.Printf("优化二分查找解法: %d\n", searchOptimized(verifyNums, verifyTarget))// 验证所有解法结果一致if search(verifyNums, verifyTarget) == searchFindPivot(verifyNums, verifyTarget) &&search(verifyNums, verifyTarget) == searchLinear(verifyNums, verifyTarget) &&search(verifyNums, verifyTarget) == searchOptimized(verifyNums, verifyTarget) {fmt.Println("✅ 所有解法结果一致,算法正确!")} else {fmt.Println("❌ 解法结果不一致,需要检查!")}// 性能测试fmt.Println("\n=== 性能测试 ===")largeNums := make([]int, 10000)for i := range largeNums {largeNums[i] = i}// 旋转数组rotated := append(largeNums[5000:], largeNums[:5000]...)largeTarget := 7500fmt.Printf("大数组测试: 长度=%d, 目标值=%d\n", len(rotated), largeTarget)result := search(rotated, largeTarget)fmt.Printf("二分查找解法结果: %d\n", result)
}