旋转数组的最小数字
1 查找排序概述
通常排序和查找是面试时考查算法的重点。在准备面试的时候,我们应该重点掌握二分查找、归并排序和快速排序,做到能随时正确、完整地写出它们的代码。
面试小提示:
如 果 面 试 题 是 要 求 在 排 序 的 数 组 (或 者 部 分 排 序 的 数 组 )中查找一个数字或者统计某个数字出现的次数,那么我们都可以尝试用二分查找算法。
哈希表和二叉排序树查找的重点在于考查对应的数据结构而不是算法。哈希表最主要的优点是我们利用它能够在0(1)时间内查找某一元素,是效率最高的查找方式;但其缺点是需要额外的空间来实现哈希表。面试题 50 “第一个只出现一次的字符”就是用哈希表的特性来实现高效查找的。
排序比查找要复杂-些。面试官会经常要求应聘者比较插入排序、冒泡排序、归并排序、快速排序等不同算法的优劣。强烈建议应聘者在准备面试的时候,一定要对各种排序算法的特点烂熟十胸,能够从额外空间消耗、平均时间复杂度和最差时间复杂度等方面去比较它们的优缺点。需要特别强调的是,很多公司的面试官喜欢在面试环节要求应聘者写出快速排序的代码。应聘者不妨自己写一个快速排序的函数并用各种数据进行测试。当测试都通过之后,再和经典的实现进行比较,看看有什么区别。
不同的排序算法适用的场合也不尽相同。快速排序虽然总体的平均效率是最好的,(但也不是任何时候都是最优的算法。比如数组本身己经排好序了,而每一轮排序的时候都以最后一个数字作为比较的标准,此时快速排序的效率只有〇( n 2)。因此,在这种场合快速排序就不是最优的算法。在面试的时候,如果面试官要求实现一个排序算法,那么应聘者一定要问清楚这个排序应用的环境是什么、有哪些约束条件,在得到足够多的信息之后再选择最合适的排序算法。
在面试的时候应聘者不要怕问面试官问题,只有多提问,应聘者才有可能明了面试官的意图。在上面的例子中,该应聘者通过几个问题就弄清楚了需排序的数字在一个较小的范围内,并且可以用辅助内存。知道了这些限制条件,就不难写出如下代码了:
2 问题
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
3 思路
我们注意到旋转之后的数组实际上可以划分为两个排序的字数组,而且前面的字数组的元素大于或者等于后面字数组的元素。我们还注意到最小的元素刚好是这两个字数组的分界线。在排序的数组中可以用二分查找实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此我们可以试着用二分查找法的思路来寻找这个最小的元素。
- 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
- 接着我们可以找到数组中间的元素。如果中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时最小元素应该位于该中间元素之后,然后我们把第一个指针指向该中间元素,移动之后第一个指针仍然位于前面的递增子数组中。
- 同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时最小元素应该位于该中间元素之前,然后我们把第二个指针指向该中间元素,移动之后第二个指针仍然位于后面的递增子数组中。
- 第一个指针总是指向前面递增数组的元素,第二个指针总是指向后面递增数组的元素。最终它们会指向两个相邻的元素,而第二个指针指向的刚好是最小的元素,这就是循环结束的条件。
示意图如下:
特殊情况:
- 如果把排序数组的0个元素搬到最后面,这仍然是旋转数组,我们的代码需要支持这种情况。如果发现数组中的一个数字小于最后一个数字,就可以直接返回第一个数字了。
- 下面这种情况,即第一个指针指向的数字、第二个指针指向的数字和中间的数字三者相等,我们无法判断中间的数字1是数以前面的递增子数组还是后面的递增子数组。正样的话,我们只能进行顺序查找。
代码
public int minNumberInRotateArray(int [] array) {
if(array == null ||array.length ==0){
// throw new RuntimeException("输入有问题");
return -1;
}
int start =0;
int end = array.length -1;
if(array[start] < array[end]){
return array[start];
}
int mid = (start + end)/2;
if(array[start] == array[mid] && array[mid] == array[end]){
for(int i =start;i<end;i++){
if(array[start] > array[i]){
array[start] = array[i];
}
}
return array[start];
}
while(start < end){
mid = (start + end)/2;
if(array[mid] > array[end]){
start = mid +1;
}else{
end = mid;
}
}
return array[start];
}
public class Test {
public static void main(String[] args) {
// 数组中数字都相同
int[] array7 = {5, 6, 7, 1, 2,3, 4};
System.out.println(min(array7));
// // 输入NULL
// System.out.println(min(null));
}
/**
* 把一个数组最开始的若干个元素搬到数组的末尾, 我们称之数组的旋转。
* 输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。
* 例如数组{3, 4, 5, 1, 2}为{l ,2, 3, 4, 5}的一个旋转,该数组的最小值为
*
* @param numbers 旋转数组
* @return 数组的最小值
*/
public static int min(int[] numbers) {
// 判断输入是否合法
if (numbers == null || numbers.length == 0) {
System.out.println("Invalid input.");
}
// 开始处理的第一个位置
int lo = 0;
// 开始处理的最后一个位置
int hi = numbers.length - 1;
// 设置初始值
int mi = lo;
// 确保lo在前一个排好序的部分,hi在排好序的后一个部分
while (numbers[lo] >= numbers[hi]) {
// 当处理范围只有两个数据时,返回后一个结果
// 因为numbers[lo] >= numbers[hi]总是成立,后一个结果对应的是最小的值
if (hi - lo == 1) {
return numbers[hi];
}
// 取中间的位置
mi = lo + (hi - lo) / 2;
// 如果三个数都相等,则需要进行顺序处理,从头到尾找最小的值
if (numbers[mi] == numbers[lo] && numbers[hi] == numbers[mi]) {
return minInorder(numbers, lo, hi);
}
// 如果中间位置对应的值在前一个排好序的部分,将lo设置为新的处理位置
if (numbers[mi] >= numbers[lo]) {
lo = mi;
}
// 如果中间位置对应的值在后一个排好序的部分,将hi设置为新的处理位置
else if (numbers[mi] <= numbers[hi]) {
hi = mi;
}
}
// 返回最终的处理结果
return numbers[mi];
}
/**
* 找数组中的最小值
*
* @param numbers 数组
* @param start 数组的起始位置
* @param end 数组的结束位置
* @return 找到的最小的数
*/
public static int minInorder(int[] numbers, int start, int end) {
int result = numbers[start];
for (int i = start + 1; i <= end; i++) {
if (result > numbers[i]) {
result = numbers[i];
}
}
return result;
}
4 LeetCode寻找旋转排序数组中的最小值
LeetCode稍微简单的原因是第一是有序的,第二个是因为肯定发生了翻转,所以
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
示例 1:
输入: [3,4,5,1,2]
输出: 1
示例 2:
输入: [4,5,6,7,0,1,2]
输出: 0