题目一
题干
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
数组样式如下:
1 2 3 4
2 3 4 5
3 4 5 6
解题思路一(×)
这道题目拿到手上,自然而然想到的是,一个一个的遍历,比如我们想要寻找这个数组里有没有数字7,那么首先我们遍历第一行:1 2 3 4,发现没有,那么紧接着遍历第二行,以此类推,这样可不可以解决,当然可以,但是并不是最优解,回到题干中去,我们发现题目中的“每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序”这个条件并没有使用,那么很明显我们需要把这个条件运用起来,那么就引申出第二个解题思路。
解题思路二(√)
其实查找的过程也是筛选的过程。这句话什么意思?查找是寻找一个数字(或者是一类数字),而筛选就是将不是这个数字的数字给剔除掉,那么是筛选一个数字快还是一部分数字快呢?有同学肯定会说:那不是废话嘛!肯定是筛选一部分数字快,其实解题思路一中就是一个一个的遍历,接着一个一个的筛选,那么这个效率自然而然的就很低了,而当使用题干中的每行每列的顺序条件时,效率就很高了(可以筛选一部分的数字)。具体操作如下:
在这个数组中的右上角的数字:4,是这一行中的最大数,是这一列的最小数,如果当**target(目标数字)<4(右上角的数字)时,那么表明在这一列中没有我们要找的数字,则可以把这一列都去掉,如果当target(目标数字)>4(右上角的数字)**时,那么表明在这一行中没有我们要找的数字,则可以把这一行都去掉,这样一来我们筛选的效率就大大提高了,代码也很好写。效果如下:
代码
C++:
class Solution{
public:
bool Find(int target, vector<vector<int> > array) {
int i = 0;
int j = array[0].size()-1;
while( i < array.size() && j >= 0){
if(target < array[i][j]){ //array[i][j]一定是当前行最大的,当前列最小的
//target < array[i][j] 排除当前列
j--;
}
else if(target > array[i][j]){
//target > array[i][j] 排除当前行
i++;
}
else{
//找到
return true;
}
}
}
}
Java:
public class Solution {
public boolean Find(int target, int [][] array) {
if(array == null){
return false;
}
int i = 0;
int j = array[0].length - 1;
while( i < array.length && j >= 0){
if(target < array[i][j]){//array[i][j]一定是当前行最大的,当前列最小的
//target < array[i][j] 排除当前列
j--;
}
else if(target > array[i][j]){
//target > array[i][j] 排除当前行
i++;
}
else{
//找到
return true;
}
}
return false;
}
}
题目二
题干
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋
转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的
所有元素都大于0,若数组大小为0,请返回0。
解题思路一(×)
这条题目乍一看其实就是求数组中的最小元素,那么最先想到的肯定是遍历整个数组,首先将数组的第一个元素记下来(result),然后依次往后遍历,发现有比result更小的数字,那么进行替换,直到最后一个数字。这个思路和想法是没有问题的,但是依然没有用到题干中的其他条件,那么这个思路并不是最优解。
解题思路二(×)
通过题干我们可以了解到旋转数组可以分为两个正常的非递减排序的数组,例如:
因此,跟思路一一样,我们也是遍历整个数组,只不过发生一次替换的情况,我们就停止循环,因为第一替换发生就是最小的数字,后面的数字都不会比第一次替换的那个数字小。
解题思路三(√)
这道题目其实可以采用二分法的思想来解决,通过上面的分析我们知道旋转数组可以分为两个正常的非递减排序的数组,也就是说最左边(left)的一个数字和最右边(right)的一个数字一定是分在两个非递减排序的数组中的,那么通过中间的数字(mid)与left和right对比,我们就可以判定最小的数字在mid的左边还是mid的右边,解释永远没有例子更直观,我们通过例子来分析:
在例1当中,mid(5)>left(3),那么我们自然就会知道,mid和left应该是属于同一个非递减排序的数组,又因为是旋转数组,left一定是属于较大的数组(指的是该数组中所有元素都不会小于另一个数组,反之为较小数组),那么较小数组就会在mid的右边,同理在例2中,mid(2)<left(5),由于是旋转数组,那么mid就是在较小数组中(且最小值只会在mid的左边或者就是mid)。如此一来我们就可以通过二分查找的方法逐步筛选出旋转数组的最小值。当然有同学会问,那么当mid=left的时候呢?这个时候我们需要分为两种情况:
第一种情况
如果left和mid处于较大数组中时,并且mid(3)=left(3),那么我们可知较小数组在mid的右边,因此我们可以把这种情况归到mid>left中去。
第二种情况
当left=mid并且mid处于较小数组中时,那么right一定也等于mid,也就是说left=mid=right,有的同学一看:啊,这个简单,随便选一个left,mid,right当最小值不就行了?可事实真的是这样吗?我们再看一种情况
我们发现虽然left=mid=right,但是最小值并不是left,mid或者是right,拿这种情况怎么办呢?那就只能用解题思路二中的方法,逐个遍历直到出现一次替换的时候。总结:
left,right,mid之间的关系 | 如何解决 |
---|---|
left=mid=right | 逐个比较 |
mid≥left | mid赋值给left |
mid<left | mid赋值给right |
代码
C++:
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
if(rotateArray.empty()){
return 0;
}
int left = 0;
int right = rotateArray.size() - 1;
int mid = 0;
//要一直满足该条件,以证明旋转特性
while(rotateArray[left] >= rotateArray[right]){
if(right - left == 1){
//两个下标已经相邻了
mid = right;
break;
}
mid = left + ((right - left) >> 1); //注意操作符优先级
if(rotateArray[mid] == rotateArray[left] && rotateArray[left] == rotateArray[right]){
//无法判定目标数据在mid左侧,还是右侧我们采用线性遍历方式
int result = rotateArray[left];
for(int i = left+1; i < right; i++){
if(result > rotateArray[i]){
result = rotateArray[i];
}
}
return result;
}
if(rotateArray[mid] >= rotateArray[left]){
//试想两者相等, 隐含条件 rotateArray[left] >= rotateArray[right]
//说明mid在前半部分
left = mid;
}else{
//说明mid在后半部分
right = mid;
}
}
return rotateArray[mid];
}
}
Java:
public static int minNumberInRotateArray (int[] nums) {
int left = 0;
int right = nums.length - 1;
int mid = 0;
while(left <= right) {
if(right - left == 1){
return nums[right];
}
mid = (left + right)>>1;
if(nums[left] == nums[mid] && nums[left] == nums[right]){
int result = left + 1;
while(left < right){
if(nums[result]>nums[left]){
result = left;
}
left++;
}
return nums[result];
}
if(nums[mid] >= nums[left]){
left = mid;
}
else {
right = mid;
}
}
return nums[mid];
}
题目一
题干
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位
于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
解题思路一(×)
在这个题目当中如果没有这句话“并保证奇数和奇数,偶数和偶数之间的相对位置不变。”这条题目的难度就大大降低了,我们可以用两个头尾指针(left和right)指向数组的左边与右边,left指针找偶数,right指针找奇数,当left指针找到偶数时,right指针找到奇数时,将左右指针的内容进行互换。如图:
我们发现虽然用这个方法可以完成奇数在前半部分,偶数在后半部分,但是他们的相对位置发生变化。因此这个方法不可行,那么在不改变相对位置的情况下如何解决呢?
解题思路二(√)
我们可以采用插入排序的思想,首先从第一个位置开始向后找,当找到奇数的时候,将前面所有的偶数都往后移动一个位置,再把找到的奇数放到这些偶数的前面,直到循环结束。我们还是直接上例子:
初始情况如图所示,当i在1时,我们发现是奇数,但是由于i在第一个位置,那么不需要变化,当i在2和6时,发现是偶数,那么直接往后移动。
当i在3时,由于3是奇数,从1开始把后面的所有偶数(2和6)都往后移动一格位置,如此一来数组的顺序就变成了1,3,2,6,4,5,紧接着i继续往后移动,当i在4的时候由于是偶数,不予考虑,接着往后移动。
当i在3时,由于5是奇数,从3开始把后面的所有偶数(2、6和4)都往后移动一格位置,如此一来数组的顺序就变成了1,3,5,2,6,4,此时循环结束,完成。
代码
C++:
class Solution {
public:
void reOrderArray(vector<int> &array) {
int k = 0;
for(int i = 0; i < array.size(); ++i){
if(array[i] & 1){ //从左向右,每次遇到的,都是最前面的奇数,一定将来要被放在k下标处
int temp = array[i]; //现将当前奇数保存起来
int j = i;
while(j > k){
//将该奇数之前的内容(偶数序列),整体后移一个位置
array[j] = array[j-1];
j--;
}
array[k++] = temp; //将奇数保存在它将来改在的位置,因为我们是从左往右放的,没有跨越奇 数,所以一定是相对位置不变的
}
}
}
};
Java:
public static void reOrderArray(int[] array) {
if (array.length == 0) {
return;
}
int k = 0;
for (int i = 0; i < array.length; i++) {
if ((array[i] & 1) == 1) {
int tmp = array[i];
int j = i;
while (j > k) {
array[j] = array[j - 1];
j--;
}
array[k] = tmp;
k++;
}
}
}