【LeetCode Hot100 刷题日记 (67/100)】153. 寻找旋转排序数组中的最小值 —— 二分查找的精妙应用🧠

10 阅读5分钟

📌 题目链接: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,指向最小值
    }
};

✅ 该代码与官方题解完全一致,逻辑严谨,边界处理得当。


🧭 解题思路(分步详解)

  1. 初始化双指针

    • low = 0high = n - 1,覆盖整个数组
  2. 进入二分循环(while (low < high)

    • 使用 low < high 而非 <=,因为当两者相等时已找到答案
  3. 计算中点 pivot

    • 使用 low + (high - low) / 2 避免整数溢出(虽本题 n ≤ 5000 不会溢出,但为良好习惯)
  4. 比较 nums[pivot]nums[high]

    • nums[pivot] < nums[high]:说明从 pivothigh 是递增的,最小值不可能在 (pivot, high],故 high = pivot
    • 否则:pivot 位于左半段,最小值一定在 pivot 右侧,故 low = pivot + 1
  5. 循环结束,返回 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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!