1、题目描述
【JZ06】把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组 {3,4,5,1,2} 为 {1,2,3,4,5} 的一个旋转,该数组的最小值为 1。
NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。
知识点:数组,二分查找
难度:☆☆
2、解题思路
2.1 暴力遍历
直接遍历数组,找出最小值即可。
2.2 二分查找
已知原数组为非递减序列,输入数组为原数组的一个旋转。“非递减”的意思就是数组的序列整体是由小到大,并且局部可能存在相同的值,但一定不存在递减子序列。
我们对输入数组划分待查找区域,刚开始的待查找区域为 0 到 length - 1。
旋转数组可以看成左右两个非递减数组的拼接,我们的目的是找出右半非递减数组的第一项。
换句话说,我们是假设 mid 为右半非递减数组的第一项,然后依次检验它是否符合要求,如果不符合就不断调整 mid 的值。
(下面算法步骤用到的图没有前后因果关系,只是示意不同情况下的策略。)
算法步骤如下:
1、初始化:first = 0; last = array.length - 1; mid = (first + last) / 2。
2、如果 array[mid] > array[last],比如说:
可以确定 mid 到 last 区间包含了右半非递减数组,因此,我们要缩小搜索区域,不断向右半非递减数组靠近,更新 first = mid +1 ; mid 重新计算,如下:
3、每次缩小待查找范围都要查看一下 array[first] < array[last] 是否成立,若成立,说明刚好 first 到 last 就是非递减数组,直接返回 array[first] 即可;
4、如果 array[mid] < array[last] ,如下图所示:
说明 mid 到 last 已经是一个非递减序列,已经可以保证 mid 到 last 的区域属于右半非递减数组,但是不确定 mid 是否这个右半数组的首项,因此重新调整 last = mid,这样待查找区域就变成如下:
5、如果 array[mid] == array[last] 则可能是以下这个情况:
也可能是以下这个情况:
也就是说,无法 mid 是否在右半非递减数组里面。我们只能缩小待查询区域的右边界,执行 last = last - 1。
6、当 first == last 时,已经找到了目标,返回 array[first]。
3、解题代码
3.1 暴力遍历
package pers.klb.jzoffer.medium;
/**
* @program: JzOffer2021
* @description: 旋转数组的最小数字
* @author: Meumax
* @create: 2020-07-13 11:21
**/
public class MinNumberInRotateArray {
public int minNumberInRotateArray(int[] array) {
if (array.length == 0) {
return 0;
} else {
int min = array[0];
for (int num : array) {
if (min > num) {
min = num;
}
}
return min;
}
}
}
时间复杂度:O(N)
空间复杂度:没有开辟额外空间,为O(1)
3.2 二分查找
package pers.klb.jzoffer.medium;
/**
* @program: JzOffer2021
* @description: 旋转数组的最小数字
* @author: Meumax
* @create: 2020-07-13 11:21
**/
public class MinNumberInRotateArray {
public int minNumberInRotateArray(int[] array) {
if (array.length == 0) return 0;
int first = 0; // 待查找区域的起始索引
int last = array.length - 1; // 待查找区域的末尾索引
int mid = (first + last) / 2; // 待查找区域的中间索引
// 把 array[last] 当成目标值来缩小待查找区域
while (first < last) {
// 如果最左边小于最右边,那么待查找区域已经是有序的了
if (array[first] < array[last]) {
return array[first];
}
// 更新中间索引
mid = (first + last) / 2;
// 三种情况
if (array[mid] < array[last]) {
// 如果中间值比最右边的值小,那么最小值应该在mid的左侧或者array[mid]就是最小值
last = mid; // 重新确定末尾索引,万一 array[mid] 就是最小值,所以 last = mid 而不是 last = mid - 1
} else if (array[mid] > array[last]) {
// 如果中间值比最右边的值大,那么最小值应该在mid的右侧
first = mid + 1; // 重新确定起始索引
} else { // 等于
// 原始数组{1,2,3,3,3} 旋转数组{3,3,3,1,2} 最小值在右边
// 原始数组{0,1,1,1,1} 旋转数组{1,0,1,1,1} 最小值在左边
// 最小值可能在左边或者右边,不能确定,那就缩小 last 一步
last--;
}
}
// 当结束 while 循环时,first == last
return array[first];
}
}
时间复杂度:二分,所以为O(longN), 但是如果是[1, 1, 1, 1],会退化到O(N)
空间复杂度:没有开辟额外空间,为O(1)
4、解题心得
本题的二分查找是比较经典的没有目标值的二分查找。
如果进行一次比较时,一定能够确定答案在 mid 的某一侧,那我们就可以使用二分查找。当没有目标值时,我们通过分析来得出 arr[mid]跟谁比的问题。
例如本题,我们根据题意已经知道输入的数组肯定是属于两个左右非递减数列的拼接,这个数组的最小值就是右半非递减数列的首项,分析到这,我们就可以确定 key 一定是右半非递减数列的其中一个值,mid 已经是要找的右半非递减数列的首项,那么 key 可以确定初步确定为最右侧的值。
当我们的搜索区域不断缩小,key 可以动态保持为待查找区域的最右侧的值。