《剑指offer》刷题——【查找和排序】面试题11:旋转数组的最小数字
一、面试考点
1. 查找:
(1)顺序查找;
(2)二分查找:重点,必须能手写完整的代码(循环、递归)
(3)哈希表查找:考察重点在于对应的数据结构而不是算法
- 优点:利用它能够在O(1)时间内查找某一元素,市销率最高的查找方式;
- 缺点:需要额外的空间来实现哈希表
(4)二叉排序树查找:考察重点在于对应的数据结构而不是算法
- 数据结构:二叉搜索树
2. 排序:
(1)面试经常会要求比较:插入排序、冒泡排序、归并排序、快速排序等不同算法的优劣;
(2)对各种排序算法的特点烂熟于心,能够从额外空间消耗、平均时间复杂度、最差时间复杂度等方面比较他们的优缺点
(3)面试经常会要求应聘者写:快排
3. 快排实现:
4. 员工年龄排序:
import java.util.Arrays;
public class AgeSort {
public static void main(String[] args) {
int[] ages = new int[] { 23, 45, 32, 43, 21, 24, 25, 23, 22, 22, 21 };
System.out.println("原数组为:" + Arrays.toString(ages));
AgeSort as = new AgeSort();
as.sortAge(ages);
System.out.println("排序后的数组为:" + Arrays.toString(ages));
}
private void sortAge(int[] ages) {
if (ages == null || ages.length < 1) {
return;
}
int oldAge = 80;
int youngAge = 20;
// 初始化一个odlAge+1的数组
int[] timeOfAge = new int[oldAge + 1];
// 将数组元素都置为0
for (int i = 0; i < timeOfAge.length; i++) {
timeOfAge[i] = 0;
}
// 某个年龄出现了多少次,就在timeOfAge数组对应年龄的位置设置多少次
for (int j = 0; j < ages.length; j++) {
int a = ages[j];
timeOfAge[a]++;
}
int index = 0;
for (int i = youngAge; i <= oldAge; i++) {// 按照年龄从小到大依次遍历timeOfAge
for (int j = 0; j < timeOfAge[i]; j++) {// 在timeOfAge中取得各个年龄位置记录的出现次数
ages[index] = i;// 将新数组从头设置出现的年龄,已经排好序
index++;
}
}
}
}
员工年龄(0-99),数组timeOfAge用来统计每个年龄出现的次数,某个年龄出现了多少次,就在数组age里设置几次该年龄,这就相当于给数组排序了,该方法用长度100的整数数组作为辅助空间换来 O(n) 的时间效率
二、题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的
小元素,如:数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1
三、解题思路
1. 旋转数组特点:
(1)旋转之后的数组可以划分为两个排序的子数组:前面的子数组都大于或者等于后边子数组的元素
(2)最小的元素刚好是这两个子数组的分界线
(3)在排序的数组中可以使用二分查找法实现O(logn)
2. 使用二分查找法:
(1)两个指针,分别指向数组的首尾
(2)找到数组中间的元素:
- 当它大于或者等于第一个指针指向的元素时,该中间元素位于前面的递增子数组,此时最小的元素应该位于该中间元素的后面,所以将第一个指针指向该中间元素;
- 当它小于或者等于第二个指针指向的元素时,该中间元素位于后面的递增子数组,此时最小的元素应该位于该中间元素的前面,所以将第二个指针指向该中间元素;
- 第一个指针总是指向前面递增数组的元素,第二个指针总是指向后面递增数组的元素,最终第一个指针将指向前面子数组的最后一个元素,第二指针会指向后面子数组的第一个元素
- 循环条件:两个指针相邻,第二个指针指向的刚好是最小的元素
3. 代码实现:
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
//异常情况
if(array==null || array.length<=0)
return 0;
int preIndex = 0;//第一个指针,总是指向前面递增子数组
int endIndex = array.length-1;//第二个指针,总是指向后面递增子数组
//如果把排序数组前面0个元素搬到最后面,即排序数组本身,此时数组中的第一个数字就是最小的数字,可以直接返回,因此mid初始化为第一个指针
int mid = preIndex;//中间元素
while(array[preIndex] >= array[endIndex]){
//循环结束条件:若两个指针相邻,第二个指针指向的刚好是最小的元素
if(endIndex - preIndex ==1){
mid = endIndex;
break;
}
//求中间元素
mid = (preIndex + endIndex)>>1;
//如果中间元素大于第一个指针指向元素,则最小元素肯定位于中间元素之后,让第一个指针指向中间元素
if(array[mid] >= array[preIndex])
preIndex = mid;
//如果中间元素小于第二个指针指向元素,则最小元素肯定位于中间元素之前,让第二个指针指向中间元素
else if(array[mid] <= array[endIndex])
endIndex = mid;
}
return array[mid];
}
}
4. 特殊情况:
(1)前面认为,在旋转数组中,由于是把递增数组排序数组前面的若干个数字搬到数组的后面,因此第一个数字总是大于或者等于最后一个数字
(2)特例:如果把排序数组的前面的0个元素搬到最后面,即排序数组本身,此时数组中的第一个数字就是最小的数字,可以直接返回,因此mid初始化为第一个指针
(3)特例:当两个指针指向的数字及它们中间的数字三者相同,无法判断中间的数字是位于前面的子数组还是后面的子数组,也就无法移动两个指针来缩小查找的范围。此时不得不采用顺序查找
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
//异常情况
if(array==null || array.length<=0)
return 0;
int preIndex = 0;//第一个指针,总是指向前面递增子数组
int endIndex = array.length-1;//第二个指针,总是指向后面递增子数组
//如果把排序数组前面0个元素搬到最后面,即排序数组本身,此时数组中的第一个数字就是最小的数字,可以直接返回,因此mid初始化为第一个指针
int mid = preIndex;//中间元素
while(array[preIndex] >= array[endIndex]){
//循环结束条件:若两个指针相邻,第二个指针指向的刚好是最小的元素
if(endIndex - preIndex ==1){
mid = endIndex;
break;
}
//求中间元素
mid = (preIndex + endIndex)>>1;
//如果两个指针指向元素和中间元素,三个相等,则只能顺序查找
if(array[preIndex]==array[endIndex] && array[mid]==array[preIndex])
return minInOrder(array, preIndex, endIndex);
//如果中间元素大于第一个指针指向元素,则最小元素肯定位于中间元素之后,让第一个指针指向中间元素
if(array[mid] >= array[preIndex])
preIndex = mid;
//如果中间元素小于第二个指针指向元素,则最小元素肯定位于中间元素之前,让第二个指针指向中间元素
else if(array[mid] <= array[endIndex])
endIndex = mid;
}
return array[mid];
}
public int minInOrder(int[] array, int preIndex, int endIndex){
int result = array[preIndex];//存最小元素
//遍历两个指针之间元素
for(int i = preIndex+1; i<=endIndex; i++){
if(result > array[i])
result = array[i];
}
return result;
}
}