主题
方法一:两次二分
首先 153. 寻找旋转排序数组中的最小值,找到
根据旋转排序数组的定义,下标在
根据这一性质,分类讨论:
- 如果
,那么 只可能在子数组 中。由于子数组 是递增的,我们可以在 中二分查找 。 - 如果
,那么 只可能在子数组 中。由于子数组 是递增的,我们可以在 中二分查找 。
注意上述讨论兼容
- 如果
,由于 是递增的,所以 比 中的每个数都要大,所以 不存在 。代码调用 传入的 , nums[0] == target是,最后会返回 。 - 如果
,那么在 中二分也就是在 中二分。
二分基础知识:【基础算法精讲 04】。
本题视频讲解:【基础算法精讲 05】。
下面代码用的开区间二分,用其他二分写法也是可以的。不同二分写法的区别见 我的题解。
python
class Solution:
# 153. 寻找旋转排序数组中的最小值(返回的是下标)
def findMin(self, nums: List[int]) -> int:
left, right = -1, len(nums) - 1 # 开区间 (-1, n-1)
while left + 1 < right: # 开区间不为空
mid = (left + right) // 2
if nums[mid] < nums[-1]:
right = mid
else:
left = mid
return right
# 有序数组中找 target 的下标
def lower_bound(self, nums: List[int], left: int, right: int, target: int) -> int:
while left + 1 < right: # 开区间不为空
mid = (left + right) // 2
# 循环不变量:
# nums[right] >= target
# nums[left] < target
if nums[mid] >= target:
right = mid # 范围缩小到 (left, mid)
else:
left = mid # 范围缩小到 (mid, right)
return right if nums[right] == target else -1
def search(self, nums: List[int], target: int) -> int:
i = self.findMin(nums)
if target > nums[-1]: # target 只可能在第一段
return self.lower_bound(nums, -1, i, target) # 开区间 (-1, i)
# target 只可能在第二段
# 由于此时 target <= nums[-1],所以 lower_bound 中的循环结束后,right < n 一定成立,无需判断 right == n
return self.lower_bound(nums, i - 1, len(nums), target) # 开区间 (i-1, n)cpp
// C++ 版待补充cpp
class Solution {
// 153. 寻找旋转排序数组中的最小值(返回的是下标)
int findMin(vector<int>& nums) {
int left = -1, right = nums.size() - 1; // 开区间 (-1, n-1)
while (left + 1 < right) { // 开区间不为空
int mid = left + (right - left) / 2;
if (nums[mid] < nums.back()) {
right = mid;
} else {
left = mid;
}
}
return right;
}
// 有序数组中找 target 的下标
int lower_bound(vector<int>& nums, int left, int right, int target) {
while (left + 1 < right) { // 开区间不为空
// 循环不变量:
// nums[right] >= target
// nums[left] < target
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid; // 范围缩小到 (left, mid)
} else {
left = mid; // 范围缩小到 (mid, right)
}
}
return nums[right] == target ? right : -1;
}
public:
int search(vector<int>& nums, int target) {
int i = findMin(nums);
if (target > nums.back()) { // target 只可能在第一段
return lower_bound(nums, -1, i, target); // 开区间 (-1, i)
}
// target 只可能在第二段
// 由于此时 target <= nums[n-1],所以 lower_bound 中的循环结束后,right < n 一定成立,无需判断 right == n
return lower_bound(nums, i - 1, nums.size(), target); // 开区间 (i-1, n)
}
};复杂度分析
- 时间复杂度:
,其中 为 的长度。 - 空间复杂度:
。
方法二:一次二分
旋转排序数组有什么性质?
把
- 如果
,那么可以推出以下结论: 由两个递增段组成。 - 第一段的所有元素均大于第二段的所有元素。
在第一段。
- 如果
,那么 在第二段。或者 就是递增数组,此时只有一段。
写法一
设
我们需要判断
把
分类讨论:
- 如果
,那么 在第一段, 在第二段,说明 在 的左边。 - 如果
,那么 在第一段, 在第二段,说明 在 的右边。 - 否则
和 在同一段。和 函数一样,比较 和 的大小,即可区分谁在左谁在右。
下面代码用的开区间二分,用其他二分写法也是可以的。
二分的范围可以是
这是因为,如果
答疑
问:在
答:不需要。如果 nums[right] == target 一定不成立,算法一定会返回
python
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = -1, len(nums) - 1 # 开区间 (-1, n-1)
while left + 1 < right: # 开区间不为空
mid = (left + right) // 2
x = nums[mid]
if target > nums[-1] >= x: # target 在第一段,x 在第二段
right = mid # 下轮循环去左边找
elif x > nums[-1] >= target: # x 在第一段,target 在第二段
left = mid # 下轮循环去右边找
elif x >= target: # 否则,x 和 target 在同一段,这就和方法一的 lower_bound 一样了
right = mid
else:
left = mid
return right if nums[right] == target else -1cpp
// C++ 版待补充cpp
class Solution {
public:
int search(vector<int>& nums, int target) {
int last = nums.back();
int left = -1, right = nums.size() - 1; // 开区间 (-1, n-1)
while (left + 1 < right) { // 开区间不为空
int mid = left + (right - left) / 2;
int x = nums[mid];
if (target > last && x <= last) { // target 在第一段,x 在第二段
right = mid; // 下轮循环去左边找
} else if (x > last && target <= last) { // x 在第一段,target 在第二段
left = mid; // 下轮循环去右边找
} else if (x >= target) { // 否则,x 和 target 在同一段,这就和方法一的 lower_bound 一样了
right = mid;
} else {
left = mid;
}
}
return nums[right] == target ? right : -1;
}
};写法二
下面只讨论
- 如果
,说明 在第一段中,那么 也必须在第一段中(否则 一定在 的右边)且 必须大于等于 。 - 写成代码就是
target > nums[n - 1] && x >= target。
- 写成代码就是
- 如果
,说明 在第二段中(或者 只有一段),那么 可以在第一段,也可以在第二段。 - 如果
在第一段,那么 一定在 左边。 - 如果
在第二段,那么 必须大于等于 。 - 写成代码就是
target > nums[n - 1] || x >= target。
- 如果
根据这两种情况,去判断
python
class Solution:
def search(self, nums: List[int], target: int) -> int:
def check(i: int) -> bool:
x = nums[i]
if x > nums[-1]:
return target > nums[-1] and x >= target
return target > nums[-1] or x >= target
left, right = -1, len(nums) - 1 # 开区间 (-1, n-1)
while left + 1 < right: # 开区间不为空
mid = (left + right) // 2
if check(mid):
right = mid
else:
left = mid
return right if nums[right] == target else -1cpp
// C++ 版待补充python
class Solution:
def search(self, nums: List[int], target: int) -> int:
def check(i: int) -> bool:
x = nums[i]
if x > nums[-1]:
return target > nums[-1] and x >= target
return target > nums[-1] or x >= target
i = bisect_left(range(len(nums) - 1), True, key=check)
return i if nums[i] == target else -1cpp
// C++ 版待补充cpp
class Solution {
public:
int search(vector<int>& nums, int target) {
int last = nums.back();
auto check = [&](int i) -> bool {
int x = nums[i];
if (x > last) {
return target > last && x >= target;
}
return target > last || x >= target;
};
int left = -1, right = nums.size() - 1; // 开区间 (-1, n-1)
while (left + 1 < right) { // 开区间不为空
int mid = left + (right - left) / 2;
(check(mid) ? right : left) = mid; // 更简洁的写法
}
return nums[right] == target ? right : -1;
}
};复杂度分析
- 时间复杂度:
,其中 为 的长度。 - 空间复杂度:
。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府