提示:本题是系列LeetCode的150道高频题,你未来遇到的互联网大厂的笔试和面试考题,基本都是从这上面改编而来的题目
互联网大厂们在公司养了一大批ACM竞赛的大佬们,吃完饭就是设计考题,然后去考应聘人员,你要做的就是学基础树结构与算法,然后打通任督二脉,以应对波云诡谲的大厂笔试面试题!
你要是不扎实学习数据结构与算法,好好动手手撕代码,锻炼解题能力,你可能会在笔试面试过程中,连题目都看不懂!比如华为,字节啥的,足够让你读不懂题
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
提示:
1 <= nums.length <= 5 * 105
-231 <= nums[i] <= 231 - 1
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/first-missing-positive
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
这是关键,否则你就需要排序o(nlog(n))
然后你再去遍历o(n)查找
这是不行的
你得一遍过,然后找到那个第一个没有出现的正数,就是缺谁
这里有一个很重要的知识点:
目前0–i-1范围上能组合出最远的的最右右边界range
i位置来了一个[i],然后你就能表示一个新的最远右边界range+=[i]
还没有出现的最小那个正数就range+1;
比如下图,range目前是1,来了一个i是2,那最远可以求和表示到3,不能表示的就是4
依次类推
还不一样,所以要重新思考
[7,8,9,11,12]
可能是要用排挤的方式,将i下标放i+1值,这样的话,才能表示正数1—N都放好了
啥时候一直i一直没有i+1,说明i+1就是那个没出现的最小正数
定义0–L-1范围中每一个位置i都放好了i+1,说明前面都是合格的
啥时候L那没法放i+1,就是i+1没出现
比如:arr=120
发现0和1下标i,都放了i+1,2那个位置放了个0,说明2+1没出现
[7,8,9,11,12]
再看这个例子,当7已经超过了arr的下标返回,咱们,把它放到最右边就行了,说明左边一定有小的数没有出现。
我们看L=7这个地方
7-1超过4这个小标了,只能交换到最后位置R
R–
然后12-1超过4这个小标了,只能交换到R
R–
然后11-1超过4这个小标了,只能交换到R
R–
然后9-1超过4这个小标了,只能交换到R
R–
发现L=0=R了,看看L处是否达标?因为8不是0+1,所以L也不达标
此时L+1=1就是第一个没出现的正数
反正,
(1)如果[i]可以取[i]-1位置,就去,这样L++,说明0–L-1范围内都是达标的(i处放i+1)
(2)如果,如果[i]不可以取[i]-1位置,就得放到[i] - 1那个位置,或者[i] - 1越界就得把[i]放到此时的R,交换,
当然你还需要查[i] - 1位置人家原本是否已经达标了,即[i] - 1放了[i]了
当然了,我们要换的是[i]>0的情况,那些负数,0,绕过了就
(3)直到L=R碰到,最后看看L处是否达标,不是,L+1就是最小的那个没有出现的正数
这就是一个比较巧妙的排挤方法
手撕代码问题不大的,逻辑最重要了
//复习:
//定义0--L-1范围中每一个位置i都放好了i+1,说明前面都是合格的
public int firstMissingPositiveReview(int[] nums) {
int L = 0;
int N = nums.length;
int R = N - 1;
while (L <= R){
//如果L处放了L+1,就跳过,合法不管
if (nums[L] == L + 1) L++;//说明0--L-1范围内都是达标的(i处放i+1)
//如果,如果[i]不可以取[i]-1位置,就得放到[i] - 1那个位置,或者[i] - 1越界就得把[i]放到此时的R,交换,
//当然你还需要查[i] - 1位置人家原本是否已经达标了,即[i] - 1放了[i]了
//同时我们只处理大于0的数
else if (nums[L] > 0 && nums[L] - 1 < N && nums[nums[L] - 1] != nums[L])
swap(nums, L, nums[L] - 1);
//如果上面没有满足,那就要把[i]放到R处
else swap(nums, L, R--);//然后R--
}
//L=R碰到,最后看看L处是否达标,不是,L+1就是最小的那个没有出现的正数
return L + 1;//
}
测试一把:
public static void test(){
Solution solution = new Solution();
int[] arr = {7,8,9,11,12};
System.out.println(solution.firstMissingPositive(arr));
System.out.println(solution.firstMissingPositiveReview(arr));
int[] arr2 = {1,2,0};
System.out.println();
System.out.println(solution.firstMissingPositive(arr2));
System.out.println(solution.firstMissingPositiveReview(arr2));
int[] arr3 = {3,2,-1,4};
System.out.println();
System.out.println(solution.firstMissingPositive(arr3));
System.out.println(solution.firstMissingPositiveReview(arr3));
int[] arr4= {1};
System.out.println();
System.out.println(solution.firstMissingPositive(arr4));
System.out.println(solution.firstMissingPositiveReview(arr4));
}
public static void main(String[] args) {
test();
}
1
1
3
3
1
1
2
2
这个排挤的方法真的非常巧!
LeetCode测试:
绝对的牛
这么搞:
L含义不变,0–L-1上,i位置放i+1
(1)只要你来到L处,数字放的是L+1,L++,不管,继续
注意:R有含义了哦!!!最开始R=N,代表我非常期待1–N连续数字出现
对吧
arr = 1 2 0,N=3,我期待1–3连续数字出现R=3就是这个意思
如果你中途发现有些数字压根不会让我的预期满足,就要跟R交换,让R–1
为啥呢,我希望连续出现1–R,
(2)但是中间你要是重复出现俩数,那就是不需要的
长度N就只能放N个数,你多了重复的数出现,就占据了多余空间,让R预期降低了,R–
2 2 1,俩2,最开始R=3,一旦碰到2个2,说明R=2,你最多只能得到1 2这种连续数字,不可能有3出现,重复的2占据了3的空间
对吧?
这种重复怎么表示呢?
就是[L]本来应该去[L]-1位置,但是你发现[L]-1位置竟然已经是[L]了
那多出来了[L]
L=3,但是[3]=6你应该去6-1位置,但是5那已经放了[L]=6了
6重复出现了,必然R预期应该降低
(3)还有,万一你出现0和负数,也是我们不要的,也不能出现<=L的数,因为我期待L处放L+1,L左边已经好了的
由于L的含义是0–L-1就是达标的,0–L-1都让i放好了i+1值,所有你要是L处出现<=L的值,也是不行的
比如:前面有了123了,现在L=3,你L处绝对不能出现负数或者0,也不能出现<=L的数,因为我希望L放L+1,来了个左边的数,就是重复
因此让L和R那交换,进入垃圾区
这必然导致R–,也就是我预期出现连续的1–R,R变小了,被你这种重复的值搞的
(4)还有,压根你出现L上的数,竟然比我R还大,你不是扯淡么?
我期待1–R连续出现,都放N长度数组中【而且最次也是R-1位置放R】,你来了一个大于R的数,不够你放啊,你不是捣乱吗?
所以当L处数字大于R也不行
(5)剩下的,就正常,就看看只要是把[L]放到[L-1]位置,继续看L新来的数是否达标
直到L>R,此时的L=R,R+1就是我们那个没法达标的数,也是L+1
手撕代码:
//定义0--L-1范围中每一个位置i都放好了i+1,说明前面都是合格的
//与此同时,我们期待1--R范围上的连续数字,出现不合格的数字就让R--,
public int firstMissingPositiveReview2(int[] nums) {
int L = 0;
int R = nums.length;
while (L < R){
//L含义不变,0--L-1上,i位置放i+1
//(1)只要你来到L处,数字放的是L+1,L++,不管,继续
if (nums[L] == L + 1) L++;
//(2)但是中间你要是重复出现俩数,那就是不需要的
//就是[L]本来应该去[L]-1位置,但是你发现[L]-1位置竟然已经是[L]了
//(3)万一你出现0和负数,也是我们不要的,也不能出现<=L的数,因为我期待L处放L+1,L左边已经好了的
//(4)还有,压根你出现L上的数,竟然比我R还大,你不是扯淡么?
else if (nums[L] <= L || nums[L] > R || nums[nums[L] - 1] == nums[L])
nums[L] = nums[--R];//直接让R处数过来,不叫换了,否则越界
//(5)剩下的,就正常,就看看只要是把[L]放到[L-1]位置,继续看L新来的数是否达标
else swap(nums, L, nums[L] - 1);
}
//L=R时,R左边都已经达标了,我们期待的1--R上的数已然出现了,
return L + 1;
}
这个方法,绕过了很多交换,也就是上面我那个方法之所以逊色的原因
测试:
public static void test(){
Solution solution = new Solution();
int[] arr = {7,8,9,11,12};
System.out.println(solution.firstMissingPositive(arr));
System.out.println(solution.firstMissingPositiveReview(arr));
System.out.println(solution.firstMissingPositiveReview2(arr));
int[] arr2 = {1,2,0};
System.out.println();
System.out.println(solution.firstMissingPositive(arr2));
System.out.println(solution.firstMissingPositiveReview(arr2));
System.out.println(solution.firstMissingPositiveReview2(arr2));
int[] arr3 = {3,2,-1,4};
System.out.println();
System.out.println(solution.firstMissingPositive(arr3));
System.out.println(solution.firstMissingPositiveReview(arr3));
System.out.println(solution.firstMissingPositiveReview2(arr3));
int[] arr4= {1};
System.out.println();
System.out.println(solution.firstMissingPositive(arr4));
System.out.println(solution.firstMissingPositiveReview(arr4));
System.out.println(solution.firstMissingPositiveReview2(arr4));
}
public static void main(String[] args) {
test();
}
1
1
1
3
3
3
1
1
1
2
2
2
提示:重要经验:
0)最重要的是理解,我们预期要1–R上的连续数字,出现不达标的就放垃圾区,R–,合格的L++,否则将L和[L]-1位置交换
1)关键在于我来到i位置,想让i放好i+1,如果不行,我就要把[i]放到[i-1]那里去,交换出来,如果[i]-1位置越界了,直接放到此时的R处,R–
2)这一路,保证0–L上是满足i处放i+1的,否则就往后面交换,然后继续检查L处,当L处是小数或者0,不处理,最后L>R之后,L+1就是那个没法达标的数字
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。