查找和排序
查找
查找和排序时程序设计中常用的算法,查找相对简单,大致有顺序查找、二分查找、哈希查找和二叉树查找,其中二分查找是大多数面试官都会考察的内容。这几个查找都各有特点:
- 顺序查找:是最普通的查找方式,虽然常用,但并不推荐。
- 二分查找:用在排序或者部分排序数组中查找一个数字或者统计某个数字出现的次数。
- 哈希表:可以让我们在 O ( 1 ) O_{(1)} O(1)的时间查找某一元素,是效率最高的查找方式。缺点是需要额外的空间来实现哈希表。
- 二叉树查找:树查找算法对应的数据结构是二叉搜索树,利用搜索树有序结构,查找可以适用于折半查找。
排序
在面试中常见的排序有:插入排序、冒泡排序、归并排序、快速排序等,我们不仅要知道这些排序的写法,还要知道不同算法的优劣,如空间消耗、平均时间复杂度和最差时间复杂度等方面去比较他们的优点。
面试题8:旋转数组的最小数字
题目
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组「3, 4, 5, 1, 2」为「1, 2, 3, 4, 5」的一个旋转,该数组的最小值为1。
分析
该题最简单直观的解法是从头到尾遍历一遍,就能知道最小的元素。这种思路的时间复杂度是 O ( n ) O_{(n)} O(n) 但却没有用到递增这一特性。
我们注意到旋转之后的数组实际上可以划分为两个排序的字数组,前面字数组的元素都大于或等于后面字数组的元素,最小元素刚好是这两个字数组的分界线。考虑到给糊的数组在一定程度上是排序的,因此我们可以利用二分查找的思路来寻找这个最小元素。
- 用两个指针分别只想第一个元素和最后一个元素。
- 找到数组中间的元素,如果中间元素位于前面的递增字数组,那么它应该大于或者等于第一个指针指向的元素,此时数组中最小的元素应该位于该中间元素后面。我们就可以把第一个指针指向该中间元素,这样可以缩小寻找的范围。移动之后的第一个指针认为与前面递增字数组之中。
- 如果中间元素位于后面的递增字数组,那么它应该小雨或者等于第二个指针指向的元素,此时数组中最小的元素应该位于该中间元素后面。我们可以把第一个指针指向该中间元素,这样可以缩小寻找的范围。移动后的第二个指针任然位于后面的递增数组中。
- 不管移动第一个还是第二个指针,查找范围都会缩小到原来的一半。接下来就是循环了。
- 最终第一个指针将只想前面子数组的最后一个元素,而第二个指针会只想后面子数组的第一个元素,即它们最终会指向两个相邻的元素,而第二个指针刚好就是最小的元素,这是循环的结束条件。
解:Java
public static void main(String[] args) {
int[] arr = {3, 4, 5, 1, 2, 3};
System.out.println(min(arr));
}
public static int min(int[] arr) {
if (arr == null || arr.length == 0) {
return -1;
}
int left = 0;
int right = arr.length - 1;
int mid = 0;
while (arr[left] >= arr[right]) {
if (right - left == 1) {
mid = right;
break;
}
mid = (right - left) / 2 + left;
if (arr[mid] >= arr[left]) {
left = mid;
} else if (arr[mid] <= arr[right]) {
right = mid;
}
}
return arr[mid];
}
这样就完了么
题目中提到,在旋转数组中,由于是把递增排序数组前面的若干个数字搬到数组的后面,因此第一个数字总是大于或者等于最后一个数字。再来考虑一下特殊情况:如果是把排序数组的前面0个元素搬到最后面,即排序数组本身,这任然是数组的一个旋转,我们的代码需要支持这种情况。此时,数组中的第一个数字就是最小的数字,可以直接返回。这就是一开始mid = 0
的原因。
在来看一个特殊情况:数组「1, 0, 1, 1, 1」和数组「1, 1, 1, 0, 1」都可以看成是递增排序数组「0, 1, 1, 1, 1」的旋转。
这两种情况下,第一个指针和第二个指针指向的数字都是1, 并且两个指针中间的数字也是1, 三个数字相同时,我们无法确定中间数字是位于前面的字数组还是后面的字数组中,也就无通过移动指针来缩小查找范围了。所以不得不采用顺序查找的方法。
public static void main(String[] args) {
int[] arr = {1, 0, 1, 1, 1, 1};
System.out.println(min(arr));
}
public static int min(int[] arr) {
if (arr == null || arr.length == 0) {
return -1;
}
int left = 0;
int right = arr.length - 1;
int mid = 0;
while (arr[left] >= arr[right]) {
if (right - left == 1) {
mid = right;
break;
}
mid = (right - left) / 2 + left;
if (arr[right] == arr[left] && arr[mid] == arr[right]) {
// 三个数相等时,只能遍历
return minInOrder(arr);
} else if (arr[mid] >= arr[left]) {
left = mid;
} else if (arr[mid] <= arr[right]) {
right = mid;
}
}
return arr[mid];
}
private static int minInOrder(int[] arr) {
if (arr == null || arr.length == 0) {
return -1;
}
int temp = arr[0];
for (int i = 1; i < arr.length; i++) {
if (temp > arr[i]) {
temp = arr[i];
}
}
return temp;
}