从零开始刷算法——二分-搜索旋转排序数组
一、题目概述
给你一个旋转排序数组,其中没有重复元素,让你在其中查找指定元素 target。
例如:
nums = [4,5,6,7,0,1,2]
target = 0 → 输出 4
target = 3 → 输出 -1
旋转数组的结构是这样的:
原有序数组:0 1 2 4 5 6 7
旋转 k 次: 4 5 6 7 0 1 2↑ ↑左段仍有序 右段有序
二、我们采用的策略:两次二分
本题最佳方案之一:
✔ 第一次二分:找到旋转点(最小值的位置)
也就是找到整个数组中的 最小值的下标。
例如:
[4,5,6,7,0,1,2]
最小值在 4 号位
第二次二分:选择正确的区间做普通的二分查找
因为旋转后的数组其实是两段独立的有序数组:
[4 5 6 7] | [0 1 2]
找到最小值的下标 i,就能得到两个有序区间:
第一区:
[0 ... i-1]第二区:
[i ... n-1]
然后根据 target 和 nums.back() 的关系,判断 target 落在哪个有序区间,再进行一次普通二分法。
三、第一次二分:寻找最小值(旋转点)
代码如下:
int findMin(vector<int>& nums) {int left = 0;int right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] > nums.back()) {left = mid + 1;}else {right = mid - 1;}}return left;
}
为什么比较 nums[mid] > nums.back()?
因为旋转数组有一个性质:
比末尾大的数字一定在左侧有序段(红区)
比末尾小的数字一定在右侧有序段(蓝区,也就是最小值所在段)
我们用“红蓝染色法”理解:
| 区域 | 特点 | 与 nums.back() 的关系 |
|---|---|---|
| 红区(左段) | 值更大 | nums[mid] > nums.back() |
| 蓝区(右段) | 值更小(包括最小值) | nums[mid] <= nums.back() |
目标:
找到 蓝色区域(右段)中的第一个数,它就是最小值。
所以 right = mid - 1 是为了锁定蓝区的第一个元素。
四、第二次二分:普通二分查找(带红蓝染色法)
为了在有序区间查找 target,我们写了一个精确的 lower_bound:
int lower_bound(vector<int>& nums, int left, int right, int target) {while (left <= right) {int mid = left + (right - left) / 2;if(nums[mid] < target) { // 左边红区left = mid + 1;}else { // 右边蓝区right = mid - 1;}}return nums[left] == target? left : -1;
}
红区:小于 target
蓝区:大于等于 target
最终 left 会来到 蓝区的第一个位置。
如果该位置恰好等于 target,则返回下标,否则返回 -1。
五、主函数逻辑:根据 target 落在哪一段
int search(vector<int>& nums, int target) {int i = findMin(nums);// 第一段if (target > nums.back()){return lower_bound(nums, 0, i - 1, target);}else return lower_bound(nums, i, nums.size() - 1, target);
}
逻辑非常清晰:
若 target > nums.back()
→ target 必在左侧红区[0 ... i-1]否则
→ target 在右侧蓝区[i ... n-1]
这是由旋转数组的结构决定的。
六、完整代码(推荐写法)
class Solution {int findMin(vector<int>& nums) {int left = 0;int right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] > nums.back()) {left = mid + 1;}else {right = mid - 1;}}return left;}int lower_bound(vector<int>& nums, int left, int right, int target) {while (left <= right) {int mid = left + (right - left) / 2;if(nums[mid] < target) { left = mid + 1;}else {right = mid - 1;}}return nums[left] == target? left : -1; }
public:int search(vector<int>& nums, int target) {int i = findMin(nums);if (target > nums.back()){return lower_bound(nums, 0, i - 1, target);}else return lower_bound(nums, i, nums.size() - 1, target);}
};
七、这种两次二分法的优势
① 逻辑清晰,结构稳定
不会出现常见的:
左右边界写反
mid 的判断错乱
死循环 / 越界等问题
② 利用红蓝染色法,可严格证明每一步正确
每个区间都有清晰的数学意义。
③ 时间复杂度依旧是 O(log n)
两次二分仍然是 O(log n)。
④ 适合拓展到更多题目
例如 154, 153, 81 等旋转数组题几乎可以模板化处理。
总结
本题的精髓不是“二分”,而是结构化思维——把问题拆成两个独立的二分:
找旋转点(找蓝区第一个数)
在对应有序段做普通二分(找蓝区第一个等于 target 的数)
用红蓝染色法可以让你彻底理解“为什么 mid > nums.back() 就是红区”。
