1、题目要求及分析:
(1)题目要求:
给一个未排序的数组,找出第一个缺失的正整数。
例如,
[1,2,0]
返回 3
,
[3,4,-1,1]
返回 2
。
你的算法应该在 O(n) 的时间复杂度内完成并且使用常数量的空间。
(2)题目分析:
此题是对数组的灵活运用,跟很多数组的题一样,都是循环->比较->输出
问题,但是这里注意时间复杂度为O(n)与常数量的空间(也就是另外定义初始化空间的时候长度是常量,是固定不变的),具体解决办法分析请看下面!
2、解决办法与分析
方法一:关键是找到数组中最小的元素与最大的数,然后从最小元素递增到最大元素为止,逐渐与数组比较,若不在数组中并且此时该数是大于0的立马返回结束
(1)简单算法描述:
(1)找到数组中最小的元素与最大元素(采用方法的方法很多,但尽可能只能循环一次满足时间复杂度)
(2)从最小元素min自增到最大元素max为止,逐一与数组元素比较
(3)如果不相等且min此时大于0则说明该正整数缺失,返回结束
(4)如果相等则说明该数已经在数组中,继续(2)
(2)java代码:
public int firstMissingPositive1(int[] nums) {
//数组为空,数组长度为1(元素小于等于0、元素等于1,、元素大于1)特殊情况直接返回,不用继续循环判断
if (nums.length == 0 || (nums.length == 1 && nums[0] <= 0) || (nums.length == 1 && nums[0] > 1)) {
return 1;
}
else if (nums.length == 1 && nums[0] == 1) {
return 2;
}
//数组变list集合
Integer arr[] = new Integer[nums.length];
for (int i = 0; i < nums.length; i++) {
Integer integer = nums[i];
arr[i] = integer;
}
//数组排序找到最小最大的值
int min = Collections.min(Arrays.asList(arr)) > 0 ? Collections.min(Arrays.asList(arr)) : 0;
int max = Collections.max(Arrays.asList(arr));
//最小的数组元素大于1,直接返回第一个缺失的正整数位1
if (min > 1) {
return 1;
}
//循环找到缺失最小的正整数
while(min != max ) {
if (!Arrays.asList(arr).contains(min) && min > 0) {
return min;
}
min++;
}
return max+1;
}
(3)优缺点分析:
优点:
- 关键循环部分简单,容易理解;
- 时间复杂度较低,关键部分只有一个while循环。
缺点:
- 该方法较为麻烦的就是先将数组变为
List
集合。 - 虽然利用了
Collections
集合工具的的min
与max
方法,但对于[1,2,3,4...,10000]
这样的情况,寻找最大值与最小值的Collections
内部实现就会造成一定的时间浪费,从而对整个程序效率就造成一定的影响。 - 如果最小值是很小的负数,如
[-100,1,100]
,这样也会在min++
递增循环比较过程中造成很大的浪费(我们只需要正数)。
(4)算法优化:
我们发现我们找到最大元素后,执行while(min != max)
一开始会以为这里当max
足够大时会造成很大的浪费,但后来发现我是错的,因为从min
开始一旦找到最小缺失的正整数就会return
返回。
比如:我们输入数组nums = [1,2,3,100]
,则max = 100
,循环体while
内:
while:
min = 1;
min = 2;
min = 3;
min = 4;
end while;
其实后来发现我们只需要将min
自增到nums.length+1
即可,无需理会max
最大值,即while(min != nums.length+1)
事实上这是因为除了[1,2,3,4,5,...n]
这样连续的情况外,大部分缺失的正整数都会在最小值min
附近,并且在num.length+1
长度下min ++
自增得到的数肯定能找到最小缺失正整数。
比如:我们输入数组nums = [1,2,3,100]
,则nums.length + 1 = 5
循环体while
内:
while:
min = 1;
min = 2;
min = 3;
min = 4;
end while
结果是一样,所以我们可以进一步对方法一进行优化修改代码。
public int firstMissingPositive1(int[] nums) {
//数组为空,数组长度为1(元素小于等于0、元素等于1,、元素大于1)特殊情况直接返回,不用继续循环判断
if (nums.length == 0 || (nums.length == 1 && nums[0] <= 0) || (nums.length == 1 && nums[0] > 1)) {
return 1;
}
else if (nums.length == 1 && nums[0] == 1) {
return 2;
}
//数组变list集合
Integer arr[] = new Integer[nums.length];
for (int i = 0; i < nums.length; i++) {
Integer integer = nums[i];
arr[i] = integer;
}
//最小值小于0时直接从0开始,不需要从负数开始
int min = Collections.min(Arrays.asList(arr)) > 0 ? Collections.min(Arrays.asList(arr)) : 0;
//不需要最大元素了,节省了时间
//int max = Collections.max(Arrays.asList(arr));
//最小的数组元素大于1,直接返回第一个缺失的正整数位1
if (min > 1) {
return 1;
}
//循环找到缺失最小的正整数
while(min != nums.length+1 ) {
if (!Arrays.asList(arr).contains(min) && min > 0) {
return min;
}
min++;
}
return min;
}
方法二:其实质为浓缩继承方法一的思想,高效地完成循环比较
(1)简单算法描述:
(1)循环数组比较元素小于nums.length+1并且大于0的元素
(2)若存在这样的元素num[j],则该元素减1作为索引加入boolean数组中,并且记为true
(3)若没有这样的元素则继续(1),直到nums末尾结束返回1
(4)循环遍历boolean数组,直到false结束,返回i+1
(2)java代码:
public int firstMissingPositive2(int[] nums) {
boolean []b = new boolean[nums.length];
if(nums.length == 0){
return 1;
}
for(int j = 0; j < nums.length; j++){
//以为nums.leng+1为界,找到大于0的正整数元素
if(nums[j] > 0 && nums[j] < nums.length+1){
System.out.println("match: "+nums[j]);
b[nums[j]-1] = true;
}
}
int i = 0;
while(i < nums.length && b[i] == true){
i++;//i++还原回nums[j]
}
return i+1;
}
(3)结果分析:
我们以输入[5,1,3,1000]
为例,分析过程代码得到如下的表格。
要彻底理解此算法,则要知道以下两点:
1)为什么要以if(nums[j] > 0 && nums[j] < nums.length+1)
为条件?
- 这是因为方法一中缺点以及算法优化中提及到的我们只需要关注大于0的元素即可;
- 同时当极限特殊情况
[1,2,3,...n]
这样连续的数组nums
出现时,正整数为n+1
,也就是nums.length+1
,那么我们就可以使用极限情况判别,不需要对大于nums.length+1
的元素进行匹配操作,从而节约时间。
2)为什么要创建boolean数组?
利用数组索引递增记录符合条件的每一个元素
nums[j]-1
递增到nums.length-1
是否在nums
数组中。符合条件的nums[1]-1=0
的递增集合为{0,1,2,3}
,nums[2]-1=2
递增集合为{2,3}
,这也是对方法一中利用min
最小值然后递增比较的思想的浓缩应用。
创建boolean数组初始化全部为
false
,这样就只需要对符合条件的更新为true
即可,节省时间提高效率,同时if
判断更快速。
3、思考总结
(1)其中第二种方法经过查资料看到大神写的,但是却没有任何解释的地方,之后进行了研究分析。发现此算法解决问题甚是高效,在这里便加以记录了。
(2)其实两种方法都比较高效,都可以在解决此问题的很多方法中靠前(个人yy的,敬请见谅!)。但是效率最高的就是第二种方法,时间空间上都很高效!
(3)通过此题总结了如何不利用常规的方法求解数组最值问题(即利用相关的工具类),主要有以下四种方法。
- 通过转为
List
集合,然后利用Collections.min(list)
与Collections.max(list)
。 - 通过转为
List
集合,然后利用排序int min = Collections.sort(list).get(0)
与int max = Collections.sort(list).get(list.size()-1)
- 通过
Arrays.sort(array)
排序,得到int min = array[0]
与int max = array[array.length-1]
- 通过利用
Java8 stream
将一个数组放进stream
里面,然后直接调用stream
的min/max
方法,int min = Arrays.stream(a).min().getAsInt();
与int max = Arrays.stream(a).max().getAsInt();