【刷题之路】不一样的二分法
一、题目描述
原题连接: NC71 旋转数组的最小数字
描述:
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
数据范围:1 ≤ n ≤ 10000,数组中任意元素的值:0 ≤ val ≤ 10000
要求:空间复杂度:O(1),时间复杂度:O(longn)
示例1:
输入:[3,4,5,1,2]
返回值 1
示例2:
输入:[3, 100, 200, 3]
返回值 3
二、解题
1、方法1——暴力法
最简单也最通用的方法就是直接遍历整个数组找到最小值:
1.1、代码实现
int minNumberInRotateArray1(int* rotateArray, int rotateArrayLen) {
// 特殊情况特殊处理
if (rotateArray == NULL) {
return -1;
}
if (1 == rotateArrayLen) {
return rotateArray[0];
}
int min = rotateArray[0];
int i = 0;
for (i = 1; i < rotateArrayLen; i++) {
if (rotateArray[i] < min) {
min = rotateArray[i];
}
}
return min;
}
时间复杂度:O(n)
空间复杂度:O(1)
2、方法2——二分法
2.1、思路解析
由题目描述可知:
旋转后的数组一定会被分成两部分:“大部分”和“小部分”,
例如:{ 4, 5, 6, 7, 1, 2, 3 } 中4567就是“大部分”,123就是“小部分”:
在“大部分”中最小的数也比“小部分”种最大的数要大,在“小部分”中最大的数也比“大部分”最小的数要小。
毋庸置疑,我们要找的最小值一定是在“小部分”中的,所以我们解题的关键就是要将范围缩小到“小部分”
所以我们可以看到,这个二分法和其他的二分法不太一样,这次我们需要通过mid来帮我们将范围缩小在“小部分中”,那具体怎么缩小呢?
我们可以想到,数组在经过旋转之后,right一定是指向“小部分”最大的值的,
所以我们有以下判断:
当arr[mid] > arr[right]时,就可以肯定mid的位置一定就在“大部分” 中:
这时候就要将范围向右缩小,执行:left = mid + 1;
当arr[mid] < arr[tight]时,就可以肯定mid的位置一定在“小部分”中:
这时候我们就要将范围往左缩小,执行right = mid,
这里可能有人就要有疑惑了: 为什么向右缩小范围时,可以执行left = mid + 1;而向左缩小范围时,却只能执行right = mid呢?这里的mid为什么不能减1呢?
其实这也是为了防止特殊情况的发生,就如上面的例子:
当mid在“大部分”中时,就算是mid已经指向了“大部分”中的最大值,那执行mid + 1跳过的也只是“大部分”中最大的之而已,对于我们找最小值没有任何影响。
但是,请看下例:
若是mid刚好就指向了“小部分”最小的值,那么执行mid - 1就将跳过这个最小值,right指向的就是“大部分”中的最大值了:
这样查找就出错了……
2.2、代码实现
有了上面的思路,那我们写代码就水到渠成了:
int minNumberInRotateArray2(int* rotateArray, int rotateArrayLen) {
int left = 0;
int right = rotateArrayLen - 1;
int mid = 0;
while (left < right) {
mid = left + (right - left) / 2;
if (rotateArray[mid] > rotateArray[right]) {
left = mid + 1;
}
else if (rotateArray[mid] > rotateArray[right]) {
right = mid;
}
else {
right--; // 数组中可能有重复值,有重复值就要向左缩小范围
}
}
return rotateArray[left];
}
时间复杂度:O(logn)
空间复杂度:O(1)