已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
- 若旋转
4次,则可以得到[4,5,6,7,0,1,2] - 若旋转
7次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入: nums = [3,4,5,1,2]
输出: 1
解释: 原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入: nums = [4,5,6,7,0,1,2]
输出: 0
解释: 原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入: nums = [11,13,15,17]
输出: 11
解释: 原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length1 <= n <= 5000-5000 <= nums[i] <= 5000nums中的所有整数 互不相同nums原来是一个升序排序的数组,并进行了1至n次旋转
1. 生活案例:寻找折断的尺子
想象你有一把刻度从小到大排列的尺子,结果不小心被折成两段,然后你随便把它们拼接在了一起(比如:[4, 5, 6, 7, 0, 1, 2])。
- 现状:尺子不再是整体递增的,而是变成了两段各自递增的部分。
- 目标:你要在这堆乱序中,瞬间找到那个**“断裂点”**,也就是最小的那个刻度
0。 - 规则:你不能一个一个去数(那是 ),老板要求你用最快的速度(,即二分法)找到它。
2. 代码实现与详细注释
这是你图片中的代码,我为你加上了结合“寻找断裂点”逻辑的详细中文注释:
JavaScript
/**
* @param {number[]} nums - 旋转后的数组
* @return {number} - 数组中的最小值
*/
var findMin = function(nums) {
let left = 0;
let right = nums.length - 1;
// 使用二分查找
while (left < right) {
// 找到中间那个点
let mid = Math.floor(left + (right - left) / 2);
// 【核心逻辑】:拿中间的值和【最右边】的值对比
if (nums[right] < nums[mid]) {
// 情况A:右边的居然比中间还小?
// 说明中间点还在“上半段”(大数区),最小值一定在 mid 的右边
// 所以要把左边界移过来:left = mid + 1
left = mid + 1;
} else {
// 情况B:右边比中间大,说明从 mid 到 right 这一段是正常的升序
// 那么最小值要么是当前的 mid,要么在 mid 的左边
// 所以收缩右边界:right = mid
right = mid;
}
}
// 当 left 和 right 重合时,我们就找到了那个“断裂点”的底端
return nums[left];
};
3. 核心原理解析
为什么是和 nums[right] 比,而不是 nums[left]?
这是二分查找旋转数组的一个巧妙点:
- 如果
nums[mid] > nums[right]:说明mid还在左边那个“较高的山峰”上,最小值肯定在右边。 - 如果
nums[mid] < nums[right]:说明从mid到right是平稳上升的,最小值只可能在mid左边(包含mid本身)。
为什么能达到 ?
二分法的精髓在于**“排除法” 。每一次比较,我们都能确定最小值绝对不在**其中的某一半,直接砍掉一半的数据。这就好比你在查字典,每一页都撕掉一半不符合条件的,速度极快。
运行轨迹追踪:以 [4, 5, 6, 7, 0, 1, 2] 为例
left=0 (4),right=6 (2),mid=3 (7)。nums[3](7) > nums[6](2),中间比右边大!说明最小值在右半区。left变成3 + 1 = 4。此时范围是[0, 1, 2]。- 新
mid对应的值是0。 - 不断收缩,最后锁定在
0。