🚩传送门:牛客题目

题目

给你一个未排序的整数数组 [NC]30. 缺失的第一个正整数 - 图1,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 [NC]30. 缺失的第一个正整数 - 图2 并且只使用常数级别额外空间的解决方案。

示例 1:

输入:nums = [1,2,0] 输出:3

示例 2:

输入:nums = [3,4,-1,1] 输出:2

示例 3:

输入:nums = [7,8,9,11,12] 输出:1

解题思路:数组替代哈希表

下面做法显然可行,但是空间复杂度过大 。

我们可以将数组所有的数放入哈希表,随后从 1 开始依次枚举正整数,并判断其是否在哈希表中。

为了使空间复杂度较小,我们在原数组基础上操作,我们对数组进行遍历,对于遍历到的数 [NC]30. 缺失的第一个正整数 - 图3 ,如果它在 [NC]30. 缺失的第一个正整数 - 图4 的范围内,那么就将数组中的第 [NC]30. 缺失的第一个正整数 - 图5 个位置(注意:数组下标从 0 开始)打上「标记」。在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 [NC]30. 缺失的第一个正整数 - 图6,否则答案是最小的没有打上标记的位置加 **1**

算法的流程如下:

  • 我们将数组中所有小于等于 0 的数修改为 [NC]30. 缺失的第一个正整数 - 图7
  • 我们遍历数组中的每一个数[NC]30. 缺失的第一个正整数 - 图8,如果[NC]30. 缺失的第一个正整数 - 图9,那么我们给数组中的第 [NC]30. 缺失的第一个正整数 - 图10个位置的数添加一个负号。

    注意如果它已经有负号,不需要重复添加,因为它可能已经被打上了标记

  • 在遍历完成后,如果数组中的每一个数都是负数,那么答案是[NC]30. 缺失的第一个正整数 - 图11,否则答案是第一个正数的位置加 **1**

[NC]30. 缺失的第一个正整数 - 图12

复杂度分析

时间复杂度:[NC]30. 缺失的第一个正整数 - 图13,其中 [NC]30. 缺失的第一个正整数 - 图14 指的是数组长度。

空间复杂度:[NC]30. 缺失的第一个正整数 - 图15

官方代码

  1. class Solution {
  2. public int firstMissingPositive(int[] nums) {
  3. int n = nums.length;
  4. //1. 负数变不在范围内的整数
  5. for (int i = 0; i < n; ++i) {
  6. if (nums[i] <= 0) {
  7. nums[i] = n + 1;
  8. }
  9. }
  10. //2. 根据数字标记相应位置
  11. for (int i = 0; i < n; ++i) {
  12. int num = Math.abs(nums[i]);
  13. if (num <= n) {
  14. nums[num - 1] = -Math.abs(nums[num - 1]);
  15. }
  16. }
  17. //3. 检查缺失数字
  18. for (int i = 0; i < n; ++i) {
  19. if (nums[i] > 0) {
  20. return i + 1;
  21. }
  22. }
  23. //4. 未检查到就是缺失的 N+1
  24. return n + 1;
  25. }
  26. }

🚩解题思路:置换

除了打标记以外,我们还可以使用置换的方法。在恢复后,数组应当有 [NC]30. 缺失的第一个正整数 - 图16 的形式,但其中有若干个位置上的数是错误的,每一个错误的位置就代表了一个 缺失的正数

以题目中的示例二 [3, 4, -1, 1] 为例,恢复后的数组应当为 [1, -1, 3, 4],我们就可以知道缺失的数为 2

那么我们如何将数组进行恢复呢 ?
可以对数组进行一次遍历,对于遍历到的数 [NC]30. 缺失的第一个正整数 - 图17,若 [NC]30. 缺失的第一个正整数 - 图18,我们就知道 [NC]30. 缺失的第一个正整数 - 图19 应当出现在数组中的 [NC]30. 缺失的第一个正整数 - 图20 的位置,因此交换 [NC]30. 缺失的第一个正整数 - 图21[NC]30. 缺失的第一个正整数 - 图22,这样 [NC]30. 缺失的第一个正整数 - 图23 就出现在了正确的位置。在完成交换后,新的 [NC]30. 缺失的第一个正整数 - 图24 可能还在 [NC]30. 缺失的第一个正整数 - 图25 的范围内,我们需要继续进行交换操作,直到 [NC]30. 缺失的第一个正整数 - 图26

注意到上面的方法可能会陷入死循环。如果 [NC]30. 缺失的第一个正整数 - 图27 恰好与 [NC]30. 缺失的第一个正整数 - 图28 相等,那么就会无限交换下去。此时我们有 [NC]30. 缺失的第一个正整数 - 图29,说明 [NC]30. 缺失的第一个正整数 - 图30 已经出现在了正确的位置。因此我们可以跳出循环,开始遍历下一个数。

每次的交换操作都会使得某一个数交换到正确的位置,因此交换的次数最多为 [NC]30. 缺失的第一个正整数 - 图31,因此时间复杂度为 [NC]30. 缺失的第一个正整数 - 图32

复杂度分析

时间复杂度:[NC]30. 缺失的第一个正整数 - 图33,其中 [NC]30. 缺失的第一个正整数 - 图34 指的是数组长度。

空间复杂度:[NC]30. 缺失的第一个正整数 - 图35

官方代码

  1. class Solution {
  2. public int firstMissingPositive(int[] nums) {
  3. int n = nums.length;
  4. //1. 不停的交换
  5. for (int i = 0; i < n; ++i) {
  6. while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
  7. int temp = nums[nums[i] - 1];
  8. nums[nums[i] - 1] = nums[i];
  9. nums[i] = temp;
  10. }
  11. }
  12. //2. 检查交换后不符合位置相等的
  13. for (int i = 0; i < n; ++i) {
  14. if (nums[i] != i + 1) {
  15. return i + 1;
  16. }
  17. }
  18. //3. 说明是n+1
  19. return n + 1;
  20. }
  21. }