已知存在一个按非降序排列的整数数组 nums
,数组中的值不必互不相同。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7]
在下标 5
处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]
。
给你 旋转后 的数组 nums
和一个整数 target
,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums
中存在这个目标值 target
,则返回 true
,否则返回 false
。
你必须尽可能减少整个操作步骤。
示例 1:
输入:nums = [2,5,6,0,0,1,2]
, target = 0
输出:true
示例 2:
输入:nums = [2,5,6,0,0,1,2]
, target = 3
输出:false
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
- 题目数据保证
nums
在预先未知的某个下标上进行了旋转 -104 <= target <= 104
进阶:
- 这是 搜索旋转排序数组 的延伸题目,本题中的
nums
可能包含重复元素。 - 这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
通过次数
208.8K
提交次数
509.1K
通过率
41.0%
我的答案:
一、信息
题目描述了一个整数数组 nums
,它首先按非降序排列,然后在未知下标 k
处进行了旋转。数组中的元素可以重复。要求编写一个函数,判断给定的目标值 target
是否存在于数组中。
二、分析
- 数组旋转:数组在某个下标
k
处旋转意味着数组被分为两部分,并且每部分内部仍然保持原有的排序。 - 存在重复元素:与普通的二分查找不同,重复元素的存在使得我们无法直接判断哪部分是有序的。
- 二分查找的应用:虽然数组被旋转,我们仍然可以尝试使用二分查找,但需要添加额外的条件来处理重复元素。
三、算法设计
- 初始化左右指针:设定左右边界
left
和right
。 - 二分查找:
- 循环直到
left
大于right
。 - 计算中间位置
mid
。 - 如果
nums[mid]
等于target
,返回true
。 - 如果
nums[left]
、nums[mid]
和nums[right]
相等,不能判断哪部分有序,此时将left
和right
分别向内移动。 - 如果左侧有序(
nums[left] <= nums[mid]
):- 判断
target
是否在左侧,调整right
。 - 否则,调整
left
。
- 判断
- 如果右侧有序:
- 判断
target
是否在右侧,调整left
。 - 否则,调整
right
。
- 判断
- 循环直到
- 找不到目标值时返回
false
。
四、代码实现(C++)
#include <vector>
using namespace std;
class Solution {
public:
bool search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return true;
if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
++left; --right;
} else if (nums[left] <= nums[mid]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return false;
}
};
五、实现代码过程中可能遇到的问题
- 边界处理:在处理边界条件时需要非常小心,尤其是在调整
left
和right
时,以免错过目标值。 - 重复元素的处理:当遇到
nums[left]
、nums[mid]
和nums[right]
相等的情况,需要特别处理,此时简单地移动边界可能会影响查找效率。 - 时间复杂度:由于重复元素的存在,最坏情况下的时间复杂度可能退化到 O(n)。
Leetcode官方题解:
class Solution { public: bool search(vector<int> &nums, int target) { int n = nums.size(); if (n == 0) { return false; } if (n == 1) { return nums[0] == target; } int l = 0, r = n - 1; while (l <= r) { int mid = (l + r) / 2; if (nums[mid] == target) { return true; } if (nums[l] == nums[mid] && nums[mid] == nums[r]) { ++l; --r; } else if (nums[l] <= nums[mid]) { if (nums[l] <= target && target < nums[mid]) { r = mid - 1; } else { l = mid + 1; } } else { if (nums[mid] < target && target <= nums[n - 1]) { l = mid + 1; } else { r = mid - 1; } } } return false; } };
学到了什么?
从题目「搜索旋转排序数组 II」中,我们可以学到一些重要的思想、思维方法和二分查找的技巧:
1. 灵活应用二分查找
- 二分查找的适应性:通常,二分查找适用于严格有序的数组。然而,这个题目教会我们即使在部分有序的情况下,二分查找也可以被灵活应用。关键在于能够准确地识别出数组中的有序部分。
2. 处理复杂场景的思维训练
- 应对重复元素:在数组中存在重复元素时,传统的二分查找可能无法直接应用。这要求我们对原有算法进行修改和调整,以适应更复杂的场景。
- 增强判断逻辑:在无法直接判断哪部分是有序时,我们需要采取额外的步骤(如逐步缩小搜索范围),这增强了我们的逻辑判断能力。
3. 细节处理的重要性
- 边界条件的关注:正确处理边界条件(如数组为空或仅有一个元素的情况)对于实现一个健壮的算法至关重要。
- 安全的中点计算:学习到了如何安全地计算中点,避免
(left + right) / 2
可能导致的整数溢出问题。
4. 复杂度分析的意识
- 最坏情况的分析:虽然二分查找通常具有 O(log n) 的时间复杂度,但在某些情况下,它可能会退化。了解算法在不同情况下的性能变化是重要的。
5. 灵活应对算法的限制
- 算法退化情况的应对:了解到即使在算法可能退化的情况下,如何通过适当的策略来尽可能保持算法的有效性。
6. 质疑和验证的重要性
- 不断验证假设:在实施算法的每一步,都需要验证当前的假设是否正确,特别是在有重复元素影响判断时。
通过这个题目,我们不仅学习了二分查找的技巧,还提高了我们处理复杂问题、细节关注和逻辑判断的能力。这些技巧和思维方法在解决其他算法问题时也非常有用。