数组
前言
本系列文章是博主本菜鸡自己刷题的一些思考和总结,内容参考了《剑指offer》和@CyC2018大神在牛客写的刷题总结。由于本人比较菜,第一遍看剑指和大神博客时有很多都不懂,所以就四处查资料,思考,写了这个总结,是我自己的学习笔记,有我自己总结的知识点和刷题套路,也希望能帮助和我一样的小白们少走弯路。
题1. 数组中重复的数字(剑指第3题)
在一个长度为
n
n
n 的数组里的所有数字都在 0 到
n
n
n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
Input:
{2, 3, 1, 0, 2, 5}
Output:
2
解题思路
思路1: 暴力比较(是的就是你能想到的唯一办法,在此不赘述):两层循环,一个一个比,时间复杂度O(n^2)(不熟悉时间复杂度概念的先看这儿)。
思路2: 哈希表(不知道啥是哈希表的看这儿,看完赶紧回来,先不用深究)新建一个哈希表,从头遍历这个数组,判断数组中的每一个数是否已经在哈希表中(哈希表可以在O(1)时间内实现查找,空间换时间),如果表中没有这个数,那么就将它放进去,如果有了,就找到了一个重复的数字。这样做的时间复杂度是O(n),但是是以一个大小为O(n)的哈希表为代价的。
还有什么解法呢?
如果要求时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序或是暴力比较的方法,也不能使用哈希。
这里就涉及到了第一个本菜总结的小套路:
套路1
对于长为n的数组,元素又在 [0, n-1] 范围内,有一个特殊的排序方法:从第一个数开始扫描数组,比较
i
i
i 位置上数字(用
m
m
m 表示)和它的下标
i
i
i, 如果
m
=
i
m = i
m=i, 那么接着扫描下一个;如果
m
m
m 不等于
i
i
i,那么就交换位置
i
i
i上的数字和 位置
m
m
m上的数字(把
m
m
m放到属于它的位置上去)
所以以后看到题干有这种限制条件,就要第一时间想到这种方法。
为什么这种数组可以这么做?因为如果没有重复的数字,那么数组排序后数字i将会出现在下标为i的位置。因为这个特点,可以在O(n)时间内对这个数组进行排序,因为每个数最多只用交换(操作)两次就能回到自己的位置。
最优解思路详解
思路3: 对于这道题,以 (2, 3, 1, 0, 2, 5) 为例,按照上面的方法从2开始遍历并交换数字,第一次,把第一个数字2和下标为2的1交换(下标从0开始),数组变成了(1, 3, 2, 0, 2, 5);
由于1还是不等于下标0,将1和下标为1的3交换,数组变成(3, 1, 2, 0, 2, 5);
3还是不等于下标0,将3和下标为3的0交换,数组变成(0, 1, 2, 3, 2, 5);
现在看0,1,2,3都在它们自己的位置上了;
看下标为4的位置上的数字,是2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复,输出重复的数字2
end
我写的答案:(Java (javac 1.8) 运行时间:29ms, 占用内存:9468k)
public class Solution {
public boolean duplicate(int[] numbers,int length,int[] duplication) {
//判断输入数组是否合法
if(numbers == null || length <= 0 ){
return false;
}
for(int i = 0; i < length; i++){
if(numbers[i] < 0 || numbers[i] > length -1){
return false;
}
}
//对合法数组进行遍历
for(int i = 0; i < length; i++){
while(numbers[i] != i){
if(numbers[i] == numbers[numbers[i]]){
duplication[0] = numbers[i];
return true;
}
//交换数字
int temp = numbers[i];
numbers[i] = numbers[temp];
numbers[temp] = temp;
}
}
return false;
}
}
题2. 不修改数组找出重复的数字(剑指第3题附加)
在一个长度为 n n n+1的数组里的所有数字都在1~ n n n范围内,所以数组中至少有一个数组是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3.
解题思路
思路1:因为要求不能修改输入的数组(read only),那么如果创建一个一样长的辅助数组就可以了。逐一把原数组中的数字复制到辅助数组中去,如果原数组中被复制的数字是
m
m
m,那么就把它放到辅助数组中下标为
m
m
m的位置上,这样很容易就能找到重复的数字。
但是这种方案需要O(n)的辅助空间,时间复杂度也是O(n)。
思路2:如果不使用这个辅助空间,那么该怎么做呢?
由于限制条件里有:长度为
n
n
n+1的数组 ,以及所有数字都在1~
n
n
n范围内, 所以自然(很难)能想到这个问题中数字的范围是解题的关键。
借鉴二分法的思路:
把1 ~
n
n
n 的数字从中间数字
m
m
m 分成两部分,然后遍历这个数组,统计前一半1~
m
m
m 的数字出现的数目,如果超过
m
m
m 个,说明重复数字在前一半区间。否则,在后半区间
m
m
m +1 ~
n
n
n, 每次在区间中都一分为二并遍历查找数字出现的频率,直到找到重复数字。
这是剑指给出的解法,时间复杂度是O(nlogn),空间复杂度是O(1),相当于是用时间换空间。
但,这种方法不能保证找出所有重复数字,因为无法确认每个区间内的每个数字是各出现一次还是某个数字出现了两次。
我按照这个思路写的代码:(Java 执行用时:5 ms 内存消耗:39.6 MB)
class Solution {
public int findDuplicate(int[] nums) {
if (nums == null || nums.length < 0 ){
return -1;
}
for(int n:nums){
if(n<0||n>nums.length-1){
return -1;
}
}
int low = 1;
int high = nums.length-1;
while(low<high){
int middle = (high + low)>>1;
int count1 = 0;
int count2 = 0;
for(int n:nums){
if(n>=low & n<= middle){
count1++;
}
if(n>middle & n <= high){
count2++;
}
}
if(count1>middle-low+1){
high = middle;
}else{
low = middle+1;
}
}
return low;
}
}
思路3,4…: 这道题虽然看上去很简单,但是有很多非常巧妙的解法,需要用到稍微复杂些的数据结构知识,感兴趣可以看leetcode上大神们给出的解法,有个关于快慢指针的解法可以达到时间复杂度为O(n),空间复杂度为O(1)的。
(小结:对于不同的题干限制(数组长度,范围,能不能修改输入),以及空间和时间复杂度的要求,每道题都有不同的解法。面试时要问清楚面试官具体的要求是什么)
题3. 二维数组中的查找(剑指第4题)
给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。
Consider the following matrix:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
Given target = 5, return true.
Given target = 20, return false.
思路: 这题没什么好说的。常规思路,二维数组可以看成是一个方形(不一定是正方形)的表。由于每行每列都是递增的数据,问题就变成是寻找一个锚点,将目标数与这个锚点比较,不断的缩小比较空间的问题。
那么如何寻找这个锚点呢?
一开始可能想到的是中间的某个点,但是如果选择方形表中间的点,如果目标数字比这个锚点数大,我们都只能在原比较空间中剔除以锚点为右下顶点的一块方形区域,剩下一块不规则区域,那么如果这个锚点正好处在整个方形表的顶点,那么一次比较就能排除整行/整列的空间了。
一个方形表有四个顶点,显然,左上顶点是整张表最小数,右下顶点是最大数,显然都不适合,能选的只有右上角和左下角了。
比如右上角,每次比较,若目标数比右上角数字大,那么就只需要比较最右边的一列,如果小,那就把这个表的最右一列排除。
我写的代码(Java 运行时间:211ms 占用内存:17552k)
public class Solution {
public boolean Find(int target, int [][] array) {
if(array == null){
return false;
}
int le = array[0].length;
for(int i=1; i<=array.length-1; i++){
if(array[i].length != le){
return false;
}
}
boolean found = false;
for(int i = le-1; i>=0; i--){
if(target == array[0][i]){
found= true;
break;
}
if(target > array[0][i]){
for(int j=1; j<=array.length-1; j++){
if(target==array[j][i]){
found =true;
break;
}
}
}
if(target < array[0][i]){
continue;
}
}
return found;
}
}