JZ6、旋转数组的最小数
题目描述
把一个数组最开始的若干个元素
搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序
的数组的一个旋转,输出旋转数组的最小元素。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
示例1
输入
[3,4,5,1,2]
返回值
1
注:
1、非递减排序
:这里有些歧义,暂且当做递增排序
来处理。
2、这里定义的旋转
:比如说{3,4,5,1,2}是{1,2,3,4,5}的一个旋转,旋转后第一个元素大于等于
最后一个元素。
3、最开始的若干个元素
:若干个可以是0个
,若是0个,则旋转后的数组就是原递增数组,就不存在:第一个元素大于等于
最后一个元素。
题解:
本题是找出最小数,即首先是一个查找题
,
又因为旋转后的数组实际上可划分为两个(递增)排序子数组
(比如:示例的[3,4,5]和[1,2]),即,本题的数组在一定程度上是排序
的,而排序的数组可以用二分法
实现查找。
或者说:
如果能够明确二分之后,答案存在于二分的某一侧,就可以使用二分。
关键点:
1、二分查找的关键是arr[mid]跟谁比。原则是arr[mid]跟某个值比后能产生二分效果
(即,一次比较后能够确定答案在mid的某一侧)
2、一般的比较原则有:
—如果有目标值target,那么直接让arr[mid] 和 target 比较即可。
—如果没有目标值,就看arr[mid]跟某个值比后能产生二分效果,一般可以考虑 端点
下面以右端点看作target
来进行分析(也可将左端点 或 左右两个端点作为target )
1、情况1:
arr[mid] > target
比如旋转数组4 5 6 1 2 3 :arr[mid] 为 6, target为右端点 3, arr[mid] > target,说明arr[mid]处于前边的递增子数组, 说明[first … mid] 都是 >= target 的,可以确定最小数在arr[mid]右侧,在[mid+1…last]区间内,所以 first = mid + 1
2、情况2:
arr[mid] < target,
比如旋转数组5 6 1 2 3 4:说明arr[mid]处于后边的递增子数组,可以确定最小数在arr[mid]及其左侧,说明答案肯定不在[mid+1…last],但是arr[mid] 有可能是答案,所以答案在[first, mid]区间,所以last = mid;
3、情况3:
arr[mid] == target:
如果是 1 0 1 1 1, arr[mid] = target = 1, 显然答案在左边
如果是 1 1 1 0 1, arr[mid] = target = 1, 显然答案在右边
所以这种情况,不能确定答案在左边还是右边,那么就让last = last - 1;慢慢缩少区间,同时也不会错过答案。
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {if (rotateArray.size() == 0) return 0;
int first = 0, last = rotateArray.size() - 1;
while (first < last) { // 最后剩下一个元素,即为答案
if (rotateArray[first] < rotateArray[last]) { // 提前退出:将前0个元素搬到数组末尾的情况;
return rotateArray[first];
}
int mid = (first + last)/2;
if (rotateArray[mid] > rotateArray[last]) { // 情况1
first = mid + 1;
}
else if (rotateArray[mid] < rotateArray[last]) { //情况2
last = mid;
}
else { // 情况3
--last;
}
}
return rotateArray[first];
}
};
时间复杂度:二分,所以为O(longN), 但是如果是[1, 1, 1, 1],会退化到O(n)(last = last - 1:一个一个的缩小,缩小n次)
空间复杂度:没有开辟额外空间,为O(1)
面试题53-I 数字在排序数组中出现的次数
题目:
统计一个数字在排序数组
中出现的次数。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
题解:
模式识别:
排序数组+一次数出现次数(即,查找一个数出现次数)----------->二分查找
思路一:
使用二分查找查找出一个给定数(O(logn)),然后从这个数开始用向两边遍历,统计重复的次数,因给定数在长度为n的数组中可能出现n次,所以相连变扫描的时间复杂度为O(n),这和直接从头扫描整个数组一样为O(n)。
思路二:(分别查找重复数字的第一个和最后一个)
记重复数字的第一个为左边界,最后一个为右边界;
具体步骤(以查右边界为例):
1、nums[mid]<target:右边界在后半段,即left=mid+1;
2、nums[mid]>target:右边界在前半段,即right=mid-1;
3、nums[mid]==target:此时需判断这个值是不是右边界,
3.1 若nums[mid]位于数组的右边界(即,mid ==(nums.size()-1),或者nums[mid]和它后一位不相等(nums[mid]!=nums[mid+1]),此时nums[mid]是右边界
3.2 否则,nums[mid]不是右边界,右边界在后半段left=mid+1;
代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.size()==0) return 0;//判特
int left=0, right=nums.size()-1, mid;
//找右边界,跳出循环后mid代表右边界
while(left<=right){
mid=left+(right-left)/2;
if(nums[mid]<target) left=mid+1;
else if(nums[mid]>target) right=mid-1;
else{
if(mid==(nums.size()-1) ||nums[mid]!=nums[mid+1]) break;
else left=mid+1;
}
}
// 若数组中无 target ,则提前返回
//mid指向最后查找的位置,若nums[mid]!=target则表明数组没有target值;
if(nums[mid]!=target) return 0;
//记录右边界值
int r=mid;//下面还会进行一次查找,mid会变,所以这里记录右边界值
left=0,right=nums.size()-1;
//找左边界,跳出循环后mid代表左边界
while(left<=right){
mid=left+(right-left)/2;
if(nums[mid]<target) left=mid+1;
else if(nums[mid]>target) right=mid-1;
else{
if(mid==0 ||nums[mid]!=nums[mid-1]) break;
else right=mid-1;
}
}
//记录左边界值
int l=mid;
return r-l+1;
}
};
时间复杂度 O(log N): 二分法为对数级别复杂度。
空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
面试题53-II. 0~n-1中缺失的数字
题目:
一个长度为n-1的递增排序数组
中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出
这个数字。
限制:
1 <= 数组长度 <= 10000
示例 1:
输入: [0,1,3]
输出: 2
示例 2:
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
题解:
模式识别
:排序数组+找出(查找)------>二分查找
思路:
即,
查找第一个下标与其值不同的元素,其下标就是缺失的值;
有一种特殊情况:
当 nums 缺失的是第 n-1 个数时,比如 nums = [0, 1],此时 nums 缺失的是 2,但所有元素都是nums[index]==index,所以没有第一个下标和值不同的元素;
代码:C++,使用带等号的二分查找while(i<=j)
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n=nums.size();
int left=0,right=n-1,mid;
while(left<=right){
mid=left+(right-left)/2;
if(nums[mid]==mid) left=mid+1;
else{
if(mid==0 || nums[mid-1]==mid-1) return mid;
else right=mid-1;
}
}
//循环结束也没查找成功,原因是当 nums 缺失的是第 n-1 个数时,比如 nums = [0, 1],此时 nums 缺失的是 2,但所有元素都是nums[mid]==mid,所以查找不到第一个下标和值不同的元素;循环到最后mid指向最后一个元素,而left=mid+1则是缺失的元素;
return left;
}
};
间复杂度 O(log N): 二分法为对数级别复杂度。
空间复杂度 O(1): 几个变量使用常数大小的额外空间。
Java,不使用带等号的二分查找while(i < j)
class Solution {
public int missingNumber(int[] nums) {
int i = 0 , j = nums.length - 1;
int mid;
while(i < j){
mid = (i + j)/2;
if(nums[mid] == mid) i++;
else j = mid;
}
if(nums[i] != i) return i;
else return nums.length;
}
}
- 更好理解的写法:
class Solution {
public int missingNumber(int[] nums) {
if(nums[nums.length - 1] == nums.length - 1) return nums.length;
if(nums[0] != 0) return 0;
int low = 0 , high = nums.length - 1 , mid = 0;
while(low <= high){
mid = low + (high - low) / 2;
if(nums[mid] == mid) low = mid + 1;
else{
if(nums[mid - 1] == mid - 1) break;
else high = mid - 1;
}
}
return mid;
}
}
注:
二分查找循环条件带不带等号的区别,即while(i<=j)和while(i < j)
- while(i<=j),区间缩小到 i =j,可能会陷入到死循环,所以,需要return 语句或
break
退出while循环; - while(i < j),区间缩小到 i =j,不满足 while(i < j),即自动退出循环,while循环内一般不需要return语句。