【码道初阶】Leetcode81旋转数组的元素搜索:二分查找在旋转排序数组搜索中的应用——处理重复元素的实现与原理
·
二分查找在旋转排序数组搜索中的应用:处理重复元素的实现与原理
1. 问题背景
旋转排序数组搜索是一个经典问题。给定一个原本按非降序排列的数组,在某个未知位置旋转后,要求判断目标值是否存在于数组中。例如,数组 [0,1,2,4,4,4,5,6,6,7] 旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]。问题难点在于:
- 数组包含重复元素
- 时间复杂度需控制在
O(log n)级别(暴力遍历别来碰瓷) - 必须处理旋转点的未知性
2. 核心挑战
2.1 旋转数组的特性
旋转后的数组由两个非降序子数组构成,例如:
[4,5,6,6,7](左半部分)和 [0,1,2,4,4](右半部分)
2.2 重复元素的影响
当数组中存在大量重复元素时,无法直接通过比较中间值与边界值确定有序区间,例如:
nums = [1,1,1,1,1,2,1,1,1]
此时,传统的二分查找策略会失效。
3. 算法设计思路
3.1 基础二分查找的改进
标准的二分查找要求数组有序,但旋转数组局部有序。我们需要通过以下步骤动态判断有序区间:
- 确定中间点
mid - 比较
nums[mid]与左右边界 - 根据比较结果缩小搜索范围
3.2 处理重复元素的核心策略
当 nums[mid] == nums[l] 或 nums[mid] == nums[r] 时,无法确定哪一侧是有序的,但可以安全地跳过边界值,因为:
nums[l]或nums[r]已被检查且不等于target- 移动指针不会错过潜在解
4. 算法实现详解
4.1 完整代码
bool search(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) return true;
// Case 1: 无法判断有序区间
if (nums[mid] == nums[l]) {
++l; // 跳过重复的左边界
}
else if (nums[mid] == nums[r]) {
--r; // 跳过重复的右边界
}
// Case 2: 右半部分有序
else if (nums[mid] < nums[r]) {
if (target > nums[mid] && target <= nums[r]) {
l = mid + 1; // 目标在右侧有序区
} else {
r = mid - 1; // 目标在左侧可能无序区
}
}
// Case 3: 左半部分有序
else {
if (target >= nums[l] && target < nums[mid]) {
r = mid - 1; // 目标在左侧有序区
} else {
l = mid + 1; // 目标在右侧可能无序区
}
}
}
return false;
}
4.2 关键逻辑分解
4.2.1 直接命中目标
if (nums[mid] == target) return true;
- 立即返回:找到目标值,终止搜索
4.2.2 处理无法判断有序区间的情况
if (nums[mid] == nums[l]) {
++l; // 关键点1:跳过无效左边界
}
else if (nums[mid] == nums[r]) {
--r; // 关键点2:跳过无效右边界
}
- 数学保证:
当nums[mid] == nums[l]时,由于nums[mid] != target(已在前面的判断排除),可以确定nums[l] != target - 操作意义:
逐步收缩搜索范围,直到进入可判断有序性的区间
4.2.3 右半部分有序时的处理
else if (nums[mid] < nums[r]) {
if (target > nums[mid] && target <= nums[r]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
- 有序性判断:
nums[mid] < nums[r]→ 右半部分[mid+1, r]有序 - 范围筛选:
当target在有序区间内时向右搜索,否则向左
4.2.4 左半部分有序时的处理
else {
if (target >= nums[l] && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
}
- 有序性判断:
nums[mid] >= nums[r]→ 左半部分[l, mid-1]有序 - 范围筛选:
当target在有序区间内时向左搜索,否则向右
5. 复杂度分析
5.1 时间复杂度
- 最佳情况:
O(log n)
当数组无重复元素时,化为标准二分查找 - 最坏情况:
O(n)
当数组全为重复元素时(如[1,1,1,1]),需要线性扫描
5.2 空间复杂度
- O(1):仅使用常数级别的额外空间
6. 示例演练
6.1 示例1:存在目标值
nums = [2,5,6,0,0,1,2], target = 0
- 初始范围
[0,6],mid=3(值0) - 直接找到目标,返回
true
6.2 示例2:不存在目标值
nums = [2,5,6,0,0,1,2], target = 3
- 初始范围
[0,6],mid=3(值0) - 进入右半有序判断,3不在
(0,2]区间 - 逐步收缩范围,最终返回
false
6.3 极端案例:全重复元素
nums = [1,1,1,1,1], target = 2
- 每次比较都会进入
nums[mid] == nums[l]分支 - 通过
++l逐步收缩,时间复杂度O(n)
7. 算法优化方向
7.1 预处理去重
// 去除首尾重复元素(伪代码)
while (l < r && nums[l] == nums[r]) {
++l;
--r;
}
- 优点:减少后续二分查找的干扰项
- 缺点:增加预处理时间
7.2 混合策略
- 当重复元素超过阈值时,切换为线性搜索
- 保持理论时间复杂度为
O(log n)
8. 总结
我们通过巧妙的边界处理策略,在保证正确性的前提下,最大限度地利用了二分查找的效率优势。笔者认为有几个核心点在于:
- 安全边界跳跃:利用
nums[mid]与边界的比较结果,安全地排除不可能区域 - 动态有序判断:根据中间值与边界的比较结果,动态选择搜索方向
- 鲁棒性处理:兼容包含大量重复元素的极端情况
这种改进后的二分查找算法设计体现了以下重要思想:
- 信息最大化利用:即使无法确定整体有序性,仍通过局部信息指导搜索
- 渐进式处理:通过逐步缩小问题规模应对复杂情况
- 健壮性优先:在最坏情况下仍能保证正确性,同时优化平均性能
大伙掌握了这种改进型二分查找技巧,对解决其他变形的搜索问题(如山脉数组查找、波浪数组处理等)就有了重要的参考价值。
更多推荐


所有评论(0)