📌 题目链接:153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、二分查找
⏱️ 目标时间复杂度:O(log n)
💾 空间复杂度:O(1)
🧠 题目分析
给定一个升序排列且元素互不相同的数组,经过若干次“旋转”后,形成一个新的数组。所谓“旋转”,是指将数组末尾的元素不断移到开头。例如:
- 原数组:
[0,1,2,4,5,6,7] - 旋转 4 次后:
[4,5,6,7,0,1,2]
题目要求我们在这个旋转后的数组中,找出最小值。
关键约束条件:
- 必须使用 O(log n) 时间复杂度的算法 → 暗示使用 二分查找
- 数组中无重复元素 → 避免了边界相等带来的歧义
- 最小值一定存在于某个“断点”处,即从递增变为更小值的位置
🧩 核心算法及代码讲解
✅ 核心思想:利用旋转数组的单调性 + 二分查找
虽然数组整体不是有序的,但它由两个严格递增的子数组拼接而成。例如 [4,5,6,7,0,1,2] 可拆分为 [4,5,6,7] 和 [0,1,2]。
关键观察:
最小值一定位于右半部分的起始位置,且整个数组满足:
- 左半部分所有元素 > 右半部分所有元素
- 数组最后一个元素
nums[high]是右半部分的最大值
因此,我们可以用 nums[mid] 与 nums[high] 比较,来判断 mid 落在左半段还是右半段:
| 情况 | 条件 | 含义 | 操作 |
|---|---|---|---|
| 1️⃣ | nums[mid] < nums[high] | mid 在右半段(包含最小值) | 缩小右边界:high = mid |
| 2️⃣ | nums[mid] > nums[high] | mid 在左半段(最小值在其右侧) | 缩小左边界:low = mid + 1 |
⚠️ 注意:由于无重复元素,不会出现
nums[mid] == nums[high]的情况(除非区间长度为 1,此时循环已结束)
📜 C++ 代码(带逐行注释)
class Solution {
public:
int findMin(vector<int>& nums) {
int low = 0;
int high = nums.size() - 1;
// 当 low == high 时,区间只剩一个元素,即为最小值
while (low < high) {
int pivot = low + (high - low) / 2; // 防止溢出的中点计算
if (nums[pivot] < nums[high]) {
// pivot 在最小值右侧(含最小值),最小值在 [low, pivot]
high = pivot;
} else {
// pivot 在最小值左侧,最小值在 [pivot + 1, high]
low = pivot + 1;
}
}
return nums[low]; // 此时 low == high,指向最小值
}
};
✅ 该代码与官方题解完全一致,逻辑严谨,边界处理得当。
🧭 解题思路(分步详解)
-
初始化双指针
low = 0,high = n - 1,覆盖整个数组
-
进入二分循环(
while (low < high))- 使用
low < high而非<=,因为当两者相等时已找到答案
- 使用
-
计算中点
pivot- 使用
low + (high - low) / 2避免整数溢出(虽本题 n ≤ 5000 不会溢出,但为良好习惯)
- 使用
-
比较
nums[pivot]与nums[high]- 若
nums[pivot] < nums[high]:说明从pivot到high是递增的,最小值不可能在(pivot, high],故high = pivot - 否则:
pivot位于左半段,最小值一定在pivot右侧,故low = pivot + 1
- 若
-
循环结束,返回
nums[low]- 此时
low == high,指向唯一候选——最小值
- 此时
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(log n) :每次迭代将搜索区间缩小一半 |
| 空间复杂度 | O(1) :仅使用常数个额外变量 |
| 适用场景 | 无重复元素的旋转排序数组找极值 |
| 面试高频点 | 二分查找的变种、边界判断、为何不与 nums[0] 比较? |
💡 为什么和
nums[high]比较,而不是nums[0]?
因为nums[0]可能就是最小值(如未旋转或旋转 n 次),此时无法区分左右段。而nums[high]始终属于右半段(或整个数组未旋转时就是最大值),具有稳定的参考价值。
💡 若数组有重复元素?
此题无重复,但若存在(如 LeetCode 154 题),则可能出现nums[mid] == nums[high],此时无法判断方向,需high--缩小范围,最坏退化为 O(n)。
💻 完整可运行代码
✅ C++ 版本
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int findMin(vector<int>& nums) {
int low = 0;
int high = nums.size() - 1;
while (low < high) {
int pivot = low + (high - low) / 2;
if (nums[pivot] < nums[high]) {
high = pivot;
}
else {
low = pivot + 1;
}
}
return nums[low];
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<vector<int>> testCases = {
{3,4,5,1,2},
{4,5,6,7,0,1,2},
{11,13,15,17},
{2,1}
};
vector<int> expected = {1, 0, 11, 1};
for (int i = 0; i < testCases.size(); i++) {
int res = sol.findMin(testCases[i]);
cout << "Test " << i+1 << ": " << (res == expected[i] ? "✅ PASS" : "❌ FAIL")
<< " | Input: [";
for (int j = 0; j < testCases[i].size(); j++) {
cout << testCases[i][j];
if (j != testCases[i].size()-1) cout << ",";
}
cout << "] → Output: " << res << "\n";
}
return 0;
}
✅ JavaScript 版本
var findMin = function(nums) {
let low = 0;
let high = nums.length - 1;
while (low < high) {
const pivot = low + Math.floor((high - low) / 2);
if (nums[pivot] < nums[high]) {
high = pivot;
} else {
low = pivot + 1;
}
}
return nums[low];
};
// 测试用例
const testCases = [
[3,4,5,1,2],
[4,5,6,7,0,1,2],
[11,13,15,17],
[2,1]
];
const expected = [1, 0, 11, 1];
testCases.forEach((nums, i) => {
const res = findMin(nums);
console.log(`Test ${i+1}: ${res === expected[i] ? '✅ PASS' : '❌ FAIL'} | Input: [${nums}] → Output: ${res}`);
});
🌟 结语 & 下期预告
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!