题目描述:
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0] 输出:3 解释:范围 [1,2] 中的数字都在数组中。
示例 2:
输入:nums = [3,4,-1,1] 输出:2 解释:1 在数组中,但 2 没有。
示例 3:
输入:nums = [7,8,9,11,12] 输出:1 解释:最小的正数 1 没有出现。
提示:
1 <= nums.length <= 105
-2^31 <= nums[i] <= 2^31 - 1
解题准备:
1.了解基本操作:既然是找到缺失的第一个正数,必然涉及遍历(查找),其它的暂时看不出来。
2.模拟操作:
随便提供可能的数据,并得到它们的答案,如下图。
人眼很快能得到结果,目前看不出步骤。
解题难点1分析:
难点1:看不出步骤。
既然我们的目标是找到缺失的第一个正数,那么朴素的思想是:
方案1:
定义变量x=1,对数组元素每次遍历,完成遍历后x++,只要存在x不在数组中,那么它就是答案。
对于【1,2,3……n】这样的数据,最多遍历n*n次即可,所以时间复杂度较高约为O(n^2)
方案2:
如果说遍历的效率比较低,我们可以自然地想到利用Hash表,采取空间换时间的思路,来优化算法。
简单地说,遍历一次数组,把数组的元素全部添加进Hash集合HashSet中,然后利用其方法查找缺失的正数。
查找的思路和方案1类似,都是定义变量x=1,一遍遍查找,只要x不在集合中,就是答案。
不过明显地,且不说遍历一次数组,和遍历一次x,一共出现了两次,时间复杂度在O(n^2)
就单单说使用Hash集合,已经开辟了O(n)的空间复杂度,所以还是有问题。
当然,效率自然高了很多,只是不符合题意罢了。
解题难点2分析:
难点2:如何优化算法,保证其效率的同时,又保证其空间复杂度不过高?
无所谓,先把题目解决了再说,即使时间空间复杂度差一些。
其实很多人也能想到,既然我们的目标是找到缺失的、最小的正数,那么:
问题1:如果这个数组有序,不就很好解决了吗?
比如,对于有序数组【-2,-1,0,1,2,3,4,5,……n】,遍历一遍数组,然后找到不相连的两个数,中间的数不就是答案吗?(当然,0到第一个正数、一直连续到最后一个数、仅一个数的数组、第一个数就比1大等等特殊情况,都值得注意)
但是这确实是一个思路。
解决方案也很简单:首先排序一遍【排序的时间复杂度可能很高,但一般不增加空间复杂度】,随后采用遍历方法,找到第一个非0的正数D(如果没有,返回1),判断一下D和1的关系(如果大于1,返回1),随后进入循环,
判断一下D和下一个数N的关系(如果D+1!=N,说明二者不相连,那么D+1就是答案),如果相连,那么D进一步,直到把整个数组遍历完成。
这个算法的时间复杂度大约在O(NlogN)左右,由于在原数组基础上排序,所以空间复杂度为O(1),复杂例子可能过不去,但是也是一种思路。
排序代码:
class Solution {public int firstMissingPositive(int[] nums) {Arrays.sort(nums); // 排序int L=0, R=0; // 双指针有助于遍历while(R<nums.length && nums[R]<=0 ){R++;}L=R; // 指向第一个正数if(R>=nums.length){return 1; // 可能没有正数,如【-2,-1】}if(L==0){if(nums[L]!=1){return 1; // 可能第一个数就比1大,如【7,8,9】}}else if(nums[L-1]<=0 && nums[L]>1){return 1; // 可能既有负数,但是第一个正数就比1大,如【-2,-1,5,6】}if(R<nums.length-1){R++; // 保证之后的操作数组不越界}while(L<nums.length){if(nums[L]==nums[R]||nums[L]+1==nums[R]){ // 判断前后是否相连,相连有相同、+1相等两种形式,如【1,1】,【1,2】L++;if(R<nums.length-1){R++;}}else{return nums[L]+1; // 不相连,返回首个正数}}return nums[L-1]+1; // 遍历结束,全部相连,返回正数}
}
解题难点3分析:
本题我也是看了答案,如果让我做,我并不能以题目要求做出来,不过提供给我一种新思路:
本题特殊思路:
一个算法,其本质是提供一条路线,使输入转变为输出,如果从输入无法解决,不妨从结果集找思路。
思考“解题难点1分析”中的“方案1”,可以发现这种穷举是最本质的做法,其它的行为都是在其上做优化。
从结果集入手,可以发现,对于一个长度为n的数组,其缺失的正数只有n+1种可能,分别是从1到n+1。(如果是连续的,即为n+1,如【1,2,3】,该例答案为4)。
1.那么,我们干脆定义一个辅助数组flag,其大小为n
2.然后遍历输入数组nums,如果nums【i】满足,>0并且<=n(因为如果>n,它一定不是缺失的最小正数),,就使flag【nums[i]-1】=true;
3.在遍历结束后,再遍历flag,只要flag【i】==false,就说明数组nums中没有这个正数,这就是答案。
代码:
class Solution {public int firstMissingPositive(int[] nums) {boolean[] data=new boolean[nums.length];// 不初始化的原因,是JVM自动把boolean类型的数据置为false。for(int i=0; i<nums.length; i++){if(nums[i]<=nums.length&&nums[i]>0){ // 满足两个条件data[nums[i]-1]=true;}}for(int i=0; i<nums.length; i++){if(!data[i]){return i+1; // 返回答案}}// 如果全都是true,说明答案是n+1return nums.length+1;}
}
真正的答案:
其实该题要求辅助空间为常数级别,所以解题难点3分析中的答案,也不符合题意,不过已经非常接近了。
由于题目不限制修改数组nums,所以可以在数组nums的角度上操作,其原理和难点3类似,不过思路又更加特别。
1.难点3中flag的作用就是映射,将答案映射到第一个false对应的下标上。
2.其实采用一个全为0、大小为n的数组,同样满足该目的。
3.不如使其映射到nums上。
难点4:怎么做到呢?nums中既有正数、又有负数。
步骤:
1.负数不影响结果,不妨映射到n+1上(或者0,随你),也就是:遍历nums,如果nums【i】<=0,使nums【i】=n+1或者0。
2.再一次遍历,正数如果满足nums【i】<=n,那么使nums【nums[i]-1】=0(等于0最好判断)【错误】
问题2:如果nums【i】-1比i更大(比如8和1),那么遍历到i=nums【i】-1即8时,这个数已经被置0了,那怎么处理?
所以,原数据不能乱变,起码要使得后来的操作可以处理。
3.删除2,同样的思路,使nums【nums[i]-1】= - nums【nums[i]-1】,这保证了可以通过绝对值,获得数据。(这也是最难的一步)
4.最后遍历一遍数组,和遍历boolean数组一样,找到第一个非负(boolean中是false)的数,这个数下标+1就是答案。
5.如果全都小于等于0,说明答案是n+1。
代码不在此发布,因为不是我写的。
以上内容即我想分享的关于力扣热题10的一些知识。
我是蚊子码农,如有补充,欢迎在评论区留言。个人也是初学者,知识体系可能没有那么完善,希望各位多多指正,谢谢大家。