
题目 1:搜索旋转排序数组
链接:33. 搜索旋转排序数组 - 力扣(LeetCode)
题目分析
原始数组是升序数组,在某个下标位置进行旋转,数组被拆成两段升序子数组(例如 [4,5,6,7,0,1,2])。 要求:给一个目标值 target,如果 target 在数组中返回下标,不存在返回 -1;数组无重复元素,时间复杂度要求 \(O(logn)\),提示我们必须使用二分查找,不能直接遍历。 难点:数组不再全局有序,直接二分无法判断目标在哪一侧,需要先判断 mid 落在左半有序区还是右半有序区。
算法原理
旋转数组一定有一半区间是有序的:
- 取
mid = left + (right - left) / 2 - 判断左区间
[left, mid]是否有序:nums[left] <= nums[mid]- 如果左区间有序:再看 target 是否落在
[nums[left], nums[mid]]之间,落在里面就收缩右边界;否则去右边查找
- 如果左区间有序:再看 target 是否落在
- 如果左区间无序,说明右区间
[mid, right]一定有序- 判断 target 是否落在右有序区间内,落在里面收缩左边界;否则去左边查找
- 循环直到
left > right,找不到返回 -1
代码编写
cpp
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target) return mid;
// 左半段有序
if(nums[left] <= nums[mid]) {
if(target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 右半段有序
if(target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
题目 2:寻找旋转排序数组中的最小值
链接:153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
题目分析
同样是无重复升序数组旋转后的数组,例如 [3,4,5,1,2],我们要找到数组里面最小元素。 最小值就是旋转点,旋转点的特点:最小值比它右边元素小,同时比左边元素小。 全局有序时(旋转 0 次)最小值就是第一个元素。要求 \(O(logn)\),二分。
算法原理
核心判断:比较 nums[mid] 和 nums[right]
- 如果
nums[mid] > nums[right]:最小值一定在 mid 的右侧,left = mid + 1 - 如果
nums[mid] < nums[right]:最小值在 mid 或者 mid 左侧,right = mid
注意:这里不能写
right = mid - 1,因为 mid 本身有可能就是最小值 循环结束条件left < right,最后 left == right,就是最小值下标。
代码编写
cpp
int findMin(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while(left < right) {
int mid = left + (right - left) / 2;
if(nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
本篇总结
本篇两道题是二分查找的高频变种:旋转有序数组二分,不再依赖全局有序,利用「旋转数组必有一半有序」这个性质二分。
- 搜索旋转排序数组:区分左右哪一段有序,再判断 target 是否落在有序区间,收缩窗口;适合查找指定值
- 寻找旋转排序数组最小值:只比较 mid 和右边界,不断向旋转拐点收敛,循环条件
left < right,最终左右重合就是答案 - 易错点:边界判断、
mid计算防止溢出、区分while(left <= right)和while(left < right)的使用场景 - 前提条件:这两题默认数组无重复元素,如果存在重复数字,逻辑还要再改动
