class Solution {
public:
int findMin(vector<int>& nums) {
int n = nums.size() - 1;
if(n < 0) return -1;
while(n > 0 && nums[n] == nums[0]) n--; //防止二分出问题(首位有相同的元素)
if(nums[n] >= nums[0]) return nums[0];
int l = 0 , r = n;
while(l < r)
{
int mid = l + r >> 1; // [l, mid], [mid + 1, r]
if(nums[mid] < nums[0]) r = mid;
else l = mid + 1;
}
return nums[r];
}
};
现在来讲一下这个首位元素相同可能导致二分不准确的问题:
我们先看一个没有做特殊处理的、常规的旋转数组二分查找代码:
// 一个常规的、但有缺陷的二分查找
int findMin(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
// 中点 > 右端点,说明最小值一定在右半部分
left = mid + 1;
} else {
// 中点 <= 右端点,说明最小值在左半部分(或就是中点)
right = mid;
}
}
return nums[left];
}
这个逻辑的核心是:通过比较 nums[mid]
和 nums[right]
来判断哪一半是单调递增的,从而缩小范围。
失效的例子
现在,我们用一个头尾相等的数组来走一遍这个流程: nums = [2, 2, 2, 0, 2]
-
初始状态:
left = 0
(值为 2)right = 4
(值为 2)nums = [2, 2, 2, 0, 2]
-
第一次循环:
mid = (0 + 4) / 2 = 2
。nums[mid]
(即nums[2]
) 的值是 2。nums[right]
(即nums[4]
) 的值也是 2。- 现在进入判断:
if (nums[mid] > nums[right])
,也就是if (2 > 2)
,这个条件是 false。 - 代码会执行
else
分支:right = mid;
,于是right
变成了2
。
-
此时的状态:
left = 0
right = 2
- 查找范围被缩小到了
[nums[0], nums[1], nums[2]]
,也就是[2, 2, 2]
。
问题出现了!
你看,我们把查找范围缩小到了 [2, 2, 2]
,但是真正的最小值 0
位于被我们丢弃的右半部分 [0, 2]
。
为什么会这样?
因为当 nums[mid] == nums[right]
时,我们无法确定 mid
到底在最小值的左边还是右边。
- 对于
[2, 2, 2, 0, 2]
,mid
在最小值的左边。 - 对于
[2, 0, 2, 2, 2]
,mid
在最小值的右边。
在这两种情况下,nums[mid]
和 nums[right]
都是 2
,我们的 if/else
逻辑无法区分,只能盲目地缩小一边,这就可能把正确答案丢掉。
如何解决?
代码里的那句:
while(n > 0 && nums[n] == nums[0]) n--;
它的作用就是预处理。在进入二分查找之前,它先把数组末尾那些和开头一样的“捣乱分子”去掉。
对于 [2, 2, 2, 0, 2, 2, 2]
,它会先把 n
从末尾一直缩减到 nums[n]
是 0
的那个位置。这样,新的查找范围就变成了 [2, 2, 2, 0]
,头尾不再相等,常规的二分逻辑就不会出错了。
另一种常见的处理方式是在二分循环内部加一个判断:
// 另一种修复方式
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else if (nums[mid] < nums[right]) {
right = mid;
} else { // 当 nums[mid] == nums[right] 时
right--; // 无法判断,只能把右边界缩小一个,再试一次
}
}
这种方法也可以,但效率上可能不如你代码里那种一次性预处理掉尾部重复元素的方式。
总结
当 nums[mid] == nums[right]
时,二分查找的判断依据就失效了,你不知道最小值藏在哪一半。这就像在迷雾里走路,你不知道该往左还是往右。为了走出迷雾,你必须采取特殊策略:
- 你的代码策略:在开始走路前,就把终点附近和起点一样的“迷雾区域”直接排除掉。
- 另一种策略:在走路时,如果发现
mid
和right
一样(看不清路),就保守地向后退一步 (right--
),直到看清为止。
那中间的元素重复为什么不会影响二分呢?
我们还是用二分法的核心逻辑来分析:通过比较 nums[mid]
和 nums[right]
,来确定最小值在哪一边。
场景一:中间元素重复(没有影响)
我们来看一个例子:nums = [3, 4, 5, 5, 1, 2]
-
初始状态:
left = 0
(值为 3)right = 5
(值为 2)nums = [3, 4, 5, 5, 1, 2]
-
第一次循环:
mid = (0 + 5) / 2 = 2
。nums[mid]
(即nums[2]
) 的值是 5。nums[right]
(即nums[5]
) 的值是 2。- 现在进入判断:
if (nums[mid] > nums[right])
,也就是if (5 > 2)
,这个条件是 true。
分析: 即使 nums
中间有重复的 5
,但 nums[mid]
(5) 依然大于 nums[right]
(2)。这个判断准确地告诉我们:从 left
到 mid
这一段 [3, 4, 5]
是有序的,那么最小值(也就是旋转点 1
)肯定在 mid
的右边。
所以,下一步 left = mid + 1
,将范围缩小到 [5, 1, 2]
,程序可以继续正确地运行。
结论:中间的重复元素,并没有破坏 nums[mid]
和 nums[right]
之间的大小关系,我们依然能做出正确的判断。