1. 二维数组中的查找
题目描述:
在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。
数据范围:矩阵的长宽满足 0 \le n,m \le 5000≤n,m≤500 , 矩阵中的值满足 0 \le val \le 10^90≤val≤10
9
进阶:空间复杂度 O(1)O(1) ,时间复杂度 O(n+m)O(n+m)
题目链接
题目分析:
首先我们想到的思路就是遍历整个数组查找,但是这样的话时间复杂度就会比较高,因为遍历查找的方式一次只能排除一个,要想提高效率那就得想如何才能一次排除多个元素呢? 查找的本质其实就是在排除,既然是排除,那么排除过程中就能分为查找范围内和查找范围外两部分。此题的二维数组是一个从左往右从上到下的顺序排列的,我们每次把二维数组右上角的元素就能够产生一次比较排除多个元素的效果。
与查找范围内的右上角元素比较有三种情况
(1)如果在比较范围内的右上角元素大于targe,那么就排除最右边一列
(2)如果在比较范围内的右上角元素小于targe,那么就排除最上边一行
(3)如果等于targe返回true,找到了。
//Java代码
class Solution {
public boolean Find(int target, int [][] array) {
//i, j分别为当前锁定的元素的横纵下标,也是查找范围的最大上边界和有边界
int i = 0;
int j = array[0].length - 1;
//只要i,j合法就进入循环
while(i < array.length && j >= 0){
//如果当前元素小于目标值则排除有效范围内的最上面一行
if(array[i][j] < target){
i++;
} else if (array[i][j] > target) {
j--; //如果当前元素小于目标值则排除有效范围内的最右边一列
}else {
//找到则返回true
return true;
}
}
return false; //越界跳出循环说明没找到,返回false
}
}
2. 旋转数组的最小数字
题目描述:
描述
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
数据范围:1 \le n \le 100001≤n≤10000,数组中任意元素的值: 0 \le val \le 100000≤val≤10000
要求:空间复杂度:O(1)O(1) ,时间复杂度:O(logn)O(logn)
题目链接
题目分析:
这个题目首先我们可能会想到的是先假设一个最小值,然后遍历数组保存最小的元素,但是这样的效率是比较低的。此题其实仍然是一个查找问题,查找的元素是最小值且要查找的数组具备以下特征:
根据这些特征我们可以知道,如果我们确定了一个元素的值通过与前半部分最小值和后半部分最大值进行比较可以确定此元素是在前半部分还是后半部分,确定了这个元素在哪个部分我们就能进一步排除一部分元素缩小查找范围。这里我们采用的是二分查找,由题意我们可知要找的元素是后半部分的第一个,我们用left和right分别维护查找区间的左边界和右边界,初始值分别为0和arr.length - 1用mid的值与left或right所指的值比较从而判断mid所在哪个部分(以与left的值比较举例):
(1)如果arr[mid] < arr[left],则arr[mid]在后半部分,然后right = mid,排除mid右边所有元素
(2)如果arr[mid] > arr[left],则arr[mid]在前半部分,然后left = mid,排除mid左边所有元素
这样下去,left一直向右移动且一直在前半部分,right一直向左移动且一直在后半部分,当right - left = 1时说明left所指的元素为前半部分最后一个元素,righ所指t的元素为后半部分的第一个元素,要找的元素正是right所指的元素。
到这,此题已经解决的差不多了,但是假如说遇到arr[mid] = arr[left]这样的特殊情况的话以上通过与边界值比较大小来缩小查找范围的思路就行不通了。
在这种特殊情况我们需要我们只能通过遍历的方式进行查找了。
public class Solution {
public int minNumberInRotateArray(int [] array) {
int left = 0; //查找范围的左边界
int right = array.length - 1; //查找范围的有边界
int mid = 0; //保存当前要与目标比较的元素下标,数值上等于(left+right)/2
while(array[left] >= array[right]){
//right - left == 1是right为要找的元素
if(right - left == 1){
mid = right;
break;
}
mid = left + (right - left) >>> 1; //mid = (left + right) / 2
//如果出现array[left] == array[mid]的情况只能通过遍历的方式去找
if(array[left] == array[mid]){
int min = array[left];
for (int i = left + 1; i <= right; i++) {
if(array[i] < min){
min = array[i];
break;
}
}
return min;
}
if(array[mid] > array[left]){
//这种情况说明要找的值在数组的后半部分
left = mid;
} else if (array[mid] < array[left]) {
right = mid;
}
}
return array[mid];
}
}
调整数组顺序使奇数位于偶数前面
题目描述:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
示例:
输入:[2,5,7,8,9,3,4]
结果:[5,7,9,3,2,8,4]
题目链接
题目分析:
此题很多人可能会想用双指针的方法来解决,两指针分别从两边开始向中间移动,如果左指针遇到偶数,右指针遇到了奇数就把两个元素调换,然后继续向中间移动直到两指针错开,但是这种方法会导致改变奇数与奇数,偶数与偶数之间的相对位置。就比如说题中的示例如果用这种方法那么结果会是[5,7,9,8,2,4],这明显与题意不符。此题正确的方法有很多种,例如新创建一个数组,遍历原数组把奇数放在前面偶数放在后面,又如采取归并的思想。这里我们采取一种简单且高效的方法,此方法类似于插入排序:
我们需要一个变量k保存当前应该放入的奇数的位置,在定义一个变量temp用于保存已找到的一个奇数,然后定义一个循环变量从0开始遍历一直到结尾,如果arr[i]是奇数就用temp保存这个奇数,再把[k, i - 1]范围内的所有元素整体向右移一位,然后把temp放在k位置上,k++。遍历完成时数组就会是前面是奇数后面是偶数并且不会改变相对位置。
对于题中示例, 图解如下:
…
…
class Solution {
public void reOrderArray(int [] array) {
int k = 0;
for(int i = 0; i < array.length; i++){
if((array[i] & 1) == 1){//从左向右,每次遇到的,都是最前面的奇数,一定将来要被放在k下标处
int temp = array[i];//现将当前奇数保存起来
int j = i;
while(j > k){//将该奇数之前的内容(偶数序列),整体后移一个位置
array[j] = array[j-1];
j--;
} array[k++] = temp;//将奇数保存在它将来改在的位置,因为我们是从左往右放的,没有跨越奇数,所以一定是相对位置不变的
}
}
}
}