📌 题目链接:33. 搜索旋转排序数组 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、二分查找
⏱️ 目标时间复杂度:O(log n)
💾 空间复杂度:O(1)
在 LeetCode Hot100 的第 33 题中,我们面对一个看似“打乱”的升序数组——它被旋转了。但题目要求我们仍以 O(log n) 的时间复杂度完成查找,这直接排除了线性扫描的可能,必须使用二分查找。
然而,这不是普通的有序数组,而是一个局部有序的旋转数组。如何在这种结构中依然高效地进行二分?这就是本题的核心考察点,也是面试中高频出现的经典变种题!
🧠 题目分析
给定一个原本严格升序、无重复元素的整数数组 nums,它在某个未知位置 k 被向左旋转(即前 k 个元素移到末尾),形成如 [4,5,6,7,0,1,2] 的结构。
任务:在 O(log n) 时间内判断目标值 target 是否存在,并返回其下标,否则返回 -1。
✅ 关键约束:
- 数组长度 ≥ 1
- 所有元素 互不相同
- 保证是一次旋转后的结果(不是多次打乱)
这意味着:整个数组由两个严格递增的子段拼接而成,且左段的所有元素 > 右段的所有元素(因为原数组升序)。
例如:
- 原数组:
[0,1,2,4,5,6,7] - 旋转后(k=3):
[4,5,6,7,0,1,2]
这种结构虽然整体无序,但每一段内部有序,且可以通过中点判断哪一段是完整有序的——这正是我们能继续使用二分的关键!
⚙️ 核心算法及代码讲解:基于有序段判断的二分查找
📌 为什么还能用二分?
普通二分依赖“整个区间有序”来判断 target 在左还是右。
但在旋转数组中,每次取中点 mid 后,左右两半中必有一半是完全有序的!
💡 证明:
假设旋转点为k,那么数组分为[0, k-1]和[k, n-1]两段升序。
任取mid,若mid < k,则[mid, n-1]跨越了旋转点,无序;但[0, mid]完全在左段 → 有序。
若mid >= k,则[0, mid]跨越旋转点 → 无序;但[mid, n-1]完全在右段 → 有序。
所以总有一侧是有序的!
因此,我们的策略是:
-
计算
mid = (l + r) / 2 -
判断
nums[mid] == target?是 → 返回mid -
判断 左半段
[l, mid]是否有序:通过nums[l] <= nums[mid]-
如果有序:
- 若
target落在[nums[l], nums[mid])区间 → 搜索左半 - 否则 → 搜索右半
- 若
-
如果左半无序 → 则右半
[mid, r]必有序- 若
target落在(nums[mid], nums[r]]区间 → 搜索右半 - 否则 → 搜索左半
- 若
-
⚠️ 注意边界处理:由于元素唯一,可用
<=或<,但需保持逻辑一致。
🧾 C++ 核心算法代码(带逐行注释)
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = (int)nums.size();
if (!n) return -1;
if (n == 1) return nums[0] == target ? 0 : -1;
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target)
return mid; // 找到目标,直接返回
// 判断左半段 [l, mid] 是否有序
if (nums[0] <= nums[mid]) {
// 左半有序:检查 target 是否在左半范围内
if (nums[0] <= target && target < nums[mid]) {
r = mid - 1; // 在左半,缩小右边界
} else {
l = mid + 1; // 不在左半,去右半找
}
} else {
// 左半无序 ⇒ 右半 [mid, r] 必有序
if (nums[mid] < target && target <= nums[n - 1]) {
l = mid + 1; // 在右半,缩小左边界
} else {
r = mid - 1; // 不在右半,去左半找
}
}
}
return -1; // 未找到
}
};
✅ 注:这里用
nums[0]作为左端参考是安全的,因为只要nums[0] <= nums[mid],说明从 0 到 mid 没有跨越旋转点,即左段连续升序。
🧩 解题思路(分步拆解)
-
特判边界:空数组或单元素数组直接处理。
-
初始化双指针:
l = 0,r = n - 1 -
进入二分循环(
while (l <= r)):-
计算中点
mid -
若
nums[mid] == target→ 成功,返回mid -
判断哪一半有序:
-
若
nums[l] <= nums[mid]→ 左半有序- 检查
target是否在[nums[l], nums[mid])→ 决定搜索方向
- 检查
-
否则 → 右半有序
- 检查
target是否在(nums[mid], nums[r]]→ 决定搜索方向
- 检查
-
-
-
循环结束仍未找到 → 返回
-1
💬 面试 Tip:
面试官常会追问:“如果数组中有重复元素怎么办?”
答:此时nums[l] == nums[mid] == nums[r]无法判断哪边有序,最坏退化为 O(n),需特殊处理(如跳过重复)。但本题明确“元素唯一”,无需考虑。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(log n) —— 每次二分排除一半,标准二分效率 |
| 空间复杂度 | O(1) —— 仅用常数额外变量 |
| 稳定性 | 稳定,因元素唯一,无歧义 |
| 适用场景 | 旋转有序数组查找、部分有序结构搜索 |
| 面试价值 | ⭐⭐⭐⭐⭐ 高频!考察对二分本质的理解,能否在“非完全有序”中识别“局部有序” |
💻 完整可运行代码
✅ C++ 版本
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = (int)nums.size();
if (!n) return -1;
if (n == 1) return nums[0] == target ? 0 : -1;
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target)
return mid;
if (nums[0] <= nums[mid]) {
if (nums[0] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[n - 1]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<int> nums1 = {4,5,6,7,0,1,2};
cout << sol.search(nums1, 0) << "\n"; // 输出: 4
// 测试用例 2
vector<int> nums2 = {4,5,6,7,0,1,2};
cout << sol.search(nums2, 3) << "\n"; // 输出: -1
// 测试用例 3
vector<int> nums3 = {1};
cout << sol.search(nums3, 0) << "\n"; // 输出: -1
return 0;
}
✅ JavaScript 版本
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
const n = nums.length;
if (n === 0) return -1;
if (n === 1) return nums[0] === target ? 0 : -1;
let l = 0, r = n - 1;
while (l <= r) {
const mid = Math.floor((l + r) / 2);
if (nums[mid] === target) return mid;
// 判断左半是否有序
if (nums[0] <= nums[mid]) {
if (nums[0] <= target && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
// 右半有序
if (nums[mid] < target && target <= nums[n - 1]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
return -1;
};
// 测试用例
console.log(search([4,5,6,7,0,1,2], 0)); // 4
console.log(search([4,5,6,7,0,1,2], 3)); // -1
console.log(search([1], 0)); // -1
🎯 面试高频追问 & 拓展思考
- Q:如果数组中有重复元素,还能 O(log n) 吗?
A:不能保证。当nums[l] == nums[mid] == nums[r]时,无法判断哪边有序,需l++或r--跳过,最坏 O(n)。 - Q:如何找到旋转点(最小值位置)?
A:类似思路,找nums[mid] > nums[mid+1]的位置,或利用nums[mid] < nums[r]判断右半有序。 - Q:这个思想还能用在哪些题?
A:LeetCode 153(寻找旋转排序数组中的最小值)、81(含重复的搜索)、162(峰值元素)等。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!