上一篇文章中,分享了关于二分查找的两个模板。
这篇文章,我们运用二分模板来解决 搜索旋转数组 这一类型的专题,共有四道题,并附题解。
在解决搜索旋转数组这一专题之前,先来看一道这个类型题的简化版或者基础版,即力扣34题,也就是旋转数组类型的题是在力扣34题的基础上,增加了若干条件升级得到的。
力扣34--在排序数组中查找元素的第一个和最后一个位置。
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]
示例 3:
输入:nums = [], target = 0 输出:[-1,-1]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums
是一个非递减数组-109 <= target <= 109
题解:
上篇文章中我们有详细的解释过,二分查找答案的性质,即一半满足条件,另一半不满足条件。
什么时候采用二分查找呢?就是答案一半满足条件,一半不满足条件的时候使用,所以二分查找找到的答案也就是一半满足条件,一半不满足条件。
分析一下这道题,找出给定元素的开始位置和结束位置,那和二分查找有啥关系呢?
我们假如 target 存在数组中,target 开始的位置,是不是就相当于找到数组中所有大于等于 target 的元素后,最小的那个元素的下标就是 target 的开始位置,结束的位置就是找到数组中所有小于等于 target 的元素后,最大的那个元素的小标就是 target 的结束位置,这不就是妥妥的二分查找嘛,二分查找找到的答案,就是我们所求的起始位置和结束位置,两个位置正好对应二分查找的两个方法。
如果 target 不存在数组中,那么也不影响我们使用二分查找,我们二分查找的是所有大于等于 target 和所有小于等于 target 的元素,这并不意味着数组中一定存在 target 这个元素,所以我们都要进行判断,判断数组中是否存在 target。
顺便说一句,遇到这样的题并且看到时间复杂度为 O(log n)
,应该考虑是否可以用二分查找来解决此题。
如果上面的题解没有说明白或者看不懂,那么请结合代码理解。
完整的Java代码如下:
class Solution {
public int[] searchRange(int[] nums, int target) {
int []res = new int[] {-1,-1};
int left = 0;
int right = nums.length - 1;
while(left < right){
int mid = left + right >> 1;
if( nums[mid] >= target ){
right = mid;
}
else{
left = mid + 1;
}
}
res[0] = nums.length != 0 && nums[right] == target ? right : -1;
left = 0;
right = nums.length - 1;
while(left < right){
int mid = left + right + 1 >> 1;
if(nums[mid] <= target){
left = mid;
}
else{
right = mid - 1;
}
}
res[1] = nums.length != 0 && nums[left] == target ? left : -1;
return res;
}
}
定义解释:
在这几道专题之前,我先给出一个定义,这个定义是我自己做题过程中总结的一种说法,而且这个定义贯穿每一道题的解题思路和解题过程。
先解释一下,这篇文章中所说的,区间严格递增、区间递增指的是,将数组分为两个区间,每个区间都是严格递增或递增,这个区间递增的概念是针对于数组的首元素来进行判断是否是递增或严格递增的。
例如3,4,5,0,1,2,这个数组的两个区间分别是3,4,5和0,1,2,根据数组搜首元素即3来判定的,即3,4,5都是大于等于3的,0,1,2都小于3的,再如3,3,4,5,5,0,1,1,2,两个区间分别是3,3,4,5,5和0,1,1,2,同理,第一个区间都是大于等于3,第二个区间都是小于3,这就具有区间递增、区间严格递增的性质。
再例如,3,4,5,0,1,2,3 或者 3,3,4,4,5,0,1,2,3,3,像这样的,就不具有区间递增、区间严格递增的性质,因为不能通过数组首元素3来进行划分,比如说区间3,4,5都是大于等于3,而0,1,2,3都是小云等于3,两个区间都能取到3,这和上面的明显不同,这两个区间都包含3,那么我才用二分查找的时候,当元素为3时,我怎么判定它是哪个区间呢?根本判定不了,因为它不具有区间递增、区间严格递增的性质,自然也就用不了二分查找的方法。但是遇到这种情况怎么解决,下面的面试题10.03和154题的讲解中会给出解决办法。
力扣33 -- 搜索旋转排序数组
整数数组 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,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1
示例 3:
输入:nums = [1], target = 0 输出:-1
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
nums
中的每个值都 独一无二- 题目数据保证
nums
在预先未知的某个下标上进行了旋转 -104 <= target <= 104
题解:
有了基础版的解题思路之后,再来看这道题。
转转后的数组仍具有二段性,一部分是严格递增,另一部分也是严格递增,但是第二部分的首元素小于第一部分的最后一个元素,这个就是转折点,转折点可以是第一部分的最后一个元素,也可以是第二部分的首元素,这里我们选择第一部分的首元素,寻找这个转折点我们采用二分查找,因为它具有二分性。
找到这个转折点之后,整个数组就分为两个部分,两个部分都是严格递增关系,那么如何判断 target 是否在数组中呢?答案就是用 target 和 nums[0] 相比较,如果 nums[0] > target,那么答案就可能在第二部分中,否则答案就可能在第一部分中。
为什么是可能呢?因为我们确定了区间,但是 target 不一定存在这个数组中的。
确定了区间之后,仍然采用二分查找方法,因为他仍然具有二分性,如果 target 在这个区间中,那么这个区间肯定是由大于等于 target 和小于 target 组成,或者是由小于等于 target 和大于 target 组成,虽然 target 不一定在这个区间内,但是不影响我们使用二分查找方法,因为最后我们有一个判断,判断 target 是否在这个区间内,即是否在这个数组中。
如果上面的题解没有说明白或者看不懂,那么请结合代码理解。
完整Java代码如下:
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left < right){
int mid = left + right + 1 >> 1;
if(nums[mid] >= nums[0]){
left = mid;
}
else{
right = mid - 1;
}
}
if(nums[0] > target){
left = left + 1;
right = nums.length - 1;
}
else{
left = 0;
}
while(left < right){
int mid = left + right + 1 >> 1;
if(nums[mid] <= target){
left = mid;
}
else{
right = mid - 1;
}
}
return nums[right] == target? right : -1;
}
}
力扣153--寻找旋转排序数组中的最小值
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2] 输出:0 解释:原数组为 [0,1,2,4,5,6,7] ,旋转 3 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17] 输出:11 解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums
中的所有整数 互不相同nums
原来是一个升序排序的数组,并进行了1
至n
次旋转
题解:
原数组是个严格递增的数组,但是经过多此旋转,让我们找出最小值。
整体思路和33题一样,找出两个严格递增区间,但是和33不同的是,我们不找 target,而且经过多次旋转,如果经过 n 次旋转,那么旋转后的数组和原数组一样,只有一个严格递增区间,所以这就需要我们在最后对边界值进行判断,通过判断的结果决定来返回哪个值。
具体的解题思路,请参考下列的代码理解。
完整Java代码如下:
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while(left < right){
int mid = left + right + 1 >> 1;
if(nums[mid] >= nums[0] ){
left = mid;
}
else{
right = mid - 1;
}
}
return right == nums.length - 1 ? nums[0]: nums[right+1];
}
}
34题的解题思路是这个专题的思路的基础,33题和153题是一个解题思路,它们的类型和题中的条件都相似,而下面的面试题10.03和154题是一个类型,并且题中的条件、解题思路都相似,它们都是在34题解题思路基础上进行的改变,即增加了判定条件。
力扣面试题10.03--搜索旋转数组
搜索旋转数组。给定一个排序后的数组,包含n个整数,但这个数组已被旋转过很多次了,次数不详。请编写代码找出数组中的某个元素,假设数组元素原先是按升序排列的。若有多个相同元素,返回索引值最小的一个。
示例1:
输入: arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 5 输出: 8(元素5在该数组中的索引)
示例2:
输入:arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 11 输出:-1 (没有找到)
提示:
- arr 长度范围在[1, 1000000]之间
题解:
这道题更像是33和153的结合体,但是有一点,这道题数组不是严格递增,即它可能含有多个相同的元素,那么问题就来了,如果含有多个相同的元素,旋转后的数组就可能不具有二段性了,即可能不具有两个区间都是递增的,就像上面的定义解释中所说的那样。
例如:3,4,5,0,1,2,3 或者 3,3,4,4,5,0,1,2,3,3,像这样的,就不具有区间递增、区间严格递增的性质,因为不能通过数组首元素3来进行划分,比如说区间3,4,5都是大于等于3,而0,1,2,3都是小于等于3,两个区间都能取到3,所以不能像33和153那样使用二分查找的思路来解决问题,那么该如何解决呢,或者怎么处理才能使用33和153那种采用二分查找的解题思路呢?
其实很简单,既然不具备区间严格递增或区间递增的性质,像这样3,3,4,4,5,0,1,2,3,3的数组,那我就去掉数组末尾和数组首元素相同的元素,数组末尾有几个和数组首元素相同的元素,我就去掉几个,例如3,3,4,4,5,0,1,2,3,3就变为3,3,4,4,5,0,1,2,这样不就具有区间严格递增或者区间递增的性质了。
这样做会不会影响解题的答案呢?肯定是不会的,因为题中要求返回索引最小的一个,还拿原来的例子,3,3,4,4,5,0,1,2,3,3,target = 3,返回的肯定是0,肯定不是数组后面的那几个3的索引,那假如数组是3,3,4,4,5,0,1,2,target=3,那还是返回0,一句话,如果数组前面有与 target 相同的值,那么数组末端有没有与 target 相同的值不影响这道题的答案。
再例如3,3,4,4,5,0,1,2,3,3,target = 2,那我们怎么处理呢?一样还是把数组末尾的3都去掉,变成3,3,4,4,5,0,1,2,为什么呢?因为不去掉就不具有区间递增或者区间严格递增的条件,就不能使用二分查找方法,那么去掉末尾的3之后,是否影响结果呢,肯定还是不影响的,因为target = 2。
具体的操作是加上一行代码:
while(left < right && arr[left] == arr[right]){
right--;
}
判断数组末尾元素和数组首元素是否相同,相同则去掉数组末尾的元素,当然不是真正的去掉,而是将 right--。
如果上面的题解没有说明白或者看不懂,那么请结合代码理解。
完整的Java代码如下:
class Solution {
public int search(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while(left < right && arr[left] == arr[right]){
right--;
}
while(left < right){
int mid = left + right + 1 >> 1;
if(arr[mid] >= arr[0]){
left = mid;
}
else{
right = mid - 1;
}
}
if(arr[0] > target){
left = left + 1;
right = arr.length - 1;
}
else{
left = 0;
}
while(left < right){
int mid = left + right >> 1;
if(arr[mid] >= target){
right = mid;
}
else{
left = mid + 1;
}
}
return arr[left] == target ? left : -1;
}
}
力扣154--寻找旋转排序数组中的最小值||
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,4]
- 若旋转
7
次,则可以得到[0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个可能存在 重复 元素值的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
示例 1:
输入:nums = [1,3,5] 输出:1
示例 2:
输入:nums = [2,2,2,0,1] 输出:0
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums
原来是一个升序排序的数组,并进行了1
至n
次旋转
题解:
这道题和10.03的解题思路是完全一样的,它俩就可以说是一道题。
完整的Java代码如下:
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while(left < right && nums[left] == nums[right]){
right--;
}
while(left < right){
int mid = left + right + 1 >> 1;
if(nums[mid] >= nums[0]){
left = mid;
}
else{
right = mid - 1;
}
}
return right == nums.length - 1 ? nums[0] : nums[right+1];
}
}
以上5道题就是二分查找专题(1)-- 搜索旋转数组的所有例题,其中34为基础版,其余为进阶版,以上内容仅供大家参考学习。
附:以上内容均为自己撰写,若有错误,将在第一时间修改。