每日算法-250415
每日算法 - 2024-04-15:
今天我们来解决两道 LeetCode 上关于在旋转排序数组中寻找最小值的问题。
153. 寻找旋转排序数组中的最小值
题目描述
思路
核心思想是利用 二分查找。
解题过程
一个升序排序的数组(无重复元素)在经过若干次旋转后,可以看作是被分成了两个递增的子数组。例如 [4, 5, 6, 7, 0, 1, 2]
可以看作 [4, 5, 6, 7]
和 [0, 1, 2]
两部分。关键在于,第一部分的任意元素都会大于第二部分的任意元素。最小值一定是第二部分的第一个元素。
我们可以利用数组的第一个元素 nums[0]
作为参照点来进行二分查找:
- 初始化
left = 0
,right = nums.length - 1
。 - 进入
while (left <= right)
循环:- 计算中间索引
mid = left + (right - left) / 2
。 - 比较
nums[mid]
和nums[0]
:- 如果
nums[mid] < nums[0]
:这意味着mid
位于旋转后的第二部分(较小的数值部分)。最小值可能是nums[mid]
或者在mid
的左侧。因此,我们向左搜索,更新right = mid - 1
。 - 如果
nums[mid] >= nums[0]
:这意味着mid
位于第一部分(较大的数值部分),或者数组根本没有旋转 (mid
就是 0)。最小值一定在mid
的右侧。因此,我们向右搜索,更新left = mid + 1
。
- 如果
- 计算中间索引
- 循环结束条件是
left > right
。此时,left
指向的就是第二部分的起始位置,即最小值所在的位置。 - 处理特殊情况:如果数组完全没有旋转(例如
[1, 2, 3, 4]
),nums[mid]
将始终>= nums[0]
,left
会一直增加直到nums.length
。在这种情况下,最小值就是nums[0]
。 - 所以,最终返回
left >= nums.length ? nums[0] : nums[left]
。
复杂度
- 时间复杂度: O ( log N ) O(\log N) O(logN),其中 N 是数组的长度。每次迭代都将搜索范围减半。
- 空间复杂度: O ( 1 ) O(1) O(1),只使用了常数级别的额外空间。
Code
class Solution {public int findMin(int[] nums) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < nums[0]) {right = mid - 1;} else {left = mid + 1;}}return left >= nums.length ? nums[0] : nums[left];}
}
154. 寻找旋转排序数组中的最小值 II
题目描述
思路
同样采用 二分查找,但需要处理重复元素带来的复杂性。
解题过程
与上一题不同,由于存在重复元素,nums[0]
可能等于 nums[nums.length - 1]
(例如 [1, 0, 1, 1, 1]
或 [1, 1, 1, 0, 1]
),这使得我们无法简单地通过与 nums[0]
或 nums[nums.length - 1]
比较来确定 mid
位于哪个递增段。
考虑比较 nums[mid]
和 nums[right]
:
- 初始化
left = 0
,right = nums.length - 1
。 - 使用
while (left < right)
循环,因为我们最终希望left
和right
指向同一个元素(最小值)。- 计算
mid = left + (right - left) / 2
。 - 比较
nums[mid]
和nums[right]
:- 如果
nums[mid] < nums[right]
:这表明从mid
到right
这部分是递增的。最小值可能在mid
或者mid
的左侧。因此,我们将搜索范围缩小到左半部分,并包含mid
(因为它可能是最小值),更新right = mid
。 - 如果
nums[mid] > nums[right]
:这表明mid
位于左侧较大的递增段,并且旋转点(最小值)一定在mid
的右侧。因此,我们将搜索范围缩小到右半部分,并排除mid
,更新left = mid + 1
。 - 如果
nums[mid] == nums[right]
:这是最棘手的情况。我们无法确定mid
在最小值左侧还是右侧(例如[3, 3, 1, 3]
中mid=1
,nums[mid]=3
,nums[right]=3
,最小在左;[1, 3, 3, 3]
中mid=1
,nums[mid]=3
,nums[right]=3
,最小在左)。为了安全地处理这种情况,我们无法从mid
获取足够信息,只能将right
向左移动一位 (right--
),尝试缩小范围并消除这个重复的nums[right]
。这样做是安全的,因为即使nums[right]
是最小值,与它相等的nums[mid]
仍然保留在[left, right]
(更新后的right
)的潜在搜索范围内(或者说,如果nums[right]
是唯一最小值,那nums[mid]
不可能是这个值)。
- 如果
- 计算
- 循环结束条件是
left == right
。此时,left
(或right
)指向的位置就是数组中的最小值。 - 返回
nums[left]
。
复杂度
- 时间复杂度:
- 平均情况: O ( log N ) O(\log N) O(logN)。
- 最坏情况: O ( N ) O(N) O(N)。当数组包含大量重复元素时(例如
[1, 1, 1, 1, 0, 1, 1]
或[1, 1, 1, 1, 1]
),nums[mid] == nums[right]
的情况可能频繁发生,导致right--
操作线性地缩小范围。
- 空间复杂度: O ( 1 ) O(1) O(1)。
Code
class Solution {public int findMin(int[] nums) {int left = 0, right = nums.length - 1;while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] < nums[right]) {// mid 在右侧递增段,最小值在 mid 或其左侧right = mid;} else if (nums[mid] > nums[right]) {// mid 在左侧递增段,最小值在 mid 右侧left = mid + 1;} else { // nums[mid] == nums[right]// 无法判断 mid 位置,缩小右边界right--;}}// 循环结束时 left == right,指向最小值return nums[left];}
}