LeetCode-41-缺失的第一个正数

24 篇文章 0 订阅

题目

<困难> 缺失的第一个正数
来源:LeetCode.

给你一个未排序的整数数组 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 < = n u m s . l e n g t h < = 5 ∗ 105 1 <= nums.length <= 5 * 105 1<=nums.length<=5105
  • − 231 < = n u m s [ i ] < = 231 − 1 -231 <= nums[i] <= 231 - 1 231<=nums[i]<=2311

接下来看一下解题思路:
    实际上,对于一个长度为 N N N 的数组,其中没有出现的最小正整数只能在 [ 1 , N + 1 ] [1, N+1] [1,N+1] 中。这是因为如果 [ 1 , N ] [1, N] [1,N] 都出现了,那么答案是 N + 1 N+1 N+1,否则答案是 [ 1 , N ] [1, N] [1,N] 中没有出现的最小正整数。
    所以,可以将所有在 [ 1 , N ] [1, N] [1,N] 范围内的数放入哈希表,随后依次枚举 [ 1 , N ] [1, N] [1,N] 正整数,并判断其是否在哈希表中。
    因为哈希表是一个可以支持快速查找的数据结构:给定一个元素,可以在 O ( 1 ) O(1) O(1) 的时间查找该元素是否在哈希表中。所以时间复杂度是 O ( N ) O(N) O(N)满足题目要求,但是空间复杂度也是 O ( N ) O(N) O(N),不满足要求。

思路一:哈希表-负数标记:

    题目要求 「只能使用常数级别的空间」,而且也没有说 数组是不可修改的,所以可以就把原始的数组当做哈希表来使用。
    对数组进行遍历,对于遍历到的数 x x x,如果它在 [ 1 , N ] [1, N] [1,N] 的范围内,那么就将数组中的第 x − 1 x-1 x1 个位置(注意:数组下标从 0 0 0 开始)打上「标记」。在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 N + 1 N+1 N+1,否则答案是最小的没有打上标记的位置加 1 1 1
    那么该如何打这个「标记」呢?由于只在意 [ 1 , N ] [1, N] [1,N] 中的数,因此可以先对数组进行遍历,把不在 [ 1 , N ] [1, N] [1,N] 范围内的数修改成任意一个大于 N N N 的数(例如 N + 1 N+1 N+1)。
    这样一来,数组中的所有数就都是正数了,因此就可以将「标记」表示为「负号」。算法的流程如下:

  • 将数组中所有小于等于 0 0 0 的数修改为 N + 1 N+1 N+1
  • 遍历数组中的每一个数 x x x,它可能已经被打了标记,因此原本对应的数为 ∣ x ∣ ∣x∣ x,其中 ∣   ∣ |\,| 为绝对值符号。如果 ∣ x ∣ ∈ [ 1 , N ] |x| \in [1, N] x[1,N],那么给数组中的第 ∣ x ∣ − 1 |x| - 1 x1 个位置的数添加一个负号。注意如果它已经有负号,不需要重复添加;
  • 在遍历完成之后,如果数组中的每一个数都是负数,那么答案是 N + 1 N+1 N+1,否则答案是第一个正数的位置加 1 1 1
public static int firstMissingPositive(int[] nums) {
    int n = nums.length;
    if(n <= 0) {
        return 1;
    }
    // 对于小于 0 的元素,全部标记为 n + 1
    for (int i = 0; i < n; ++i) {
        if (nums[i] <= 0) {
            nums[i] = n + 1;
        }

    }
    // 把 ∣x∣∈[1,N] 的 x - 1 位置的数字都标记为负数
    for (int i = 0; i < n; ++i) {
        int num = Math.abs(nums[i]);
        if (num <= n) {
            nums[num - 1] = -Math.abs(nums[num - 1]);
        }
    }
    // 遍历数组,第一个不为负数的元素的位置就是最小的正整数
    for (int i = 0; i < n; ++i) {
        if (nums[i] > 0) {
            return i + 1;
        }
    }
    return n + 1;
}
复杂度分析
  • 时间复杂度: O ( N ) O(N) O(N),其中 N N N 是数组的长度。

  • 空间复杂度: O ( 1 ) O(1) O(1)

思路二:置换:

    除了打标记以外,我们还可以使用置换的方法,可以对数组进行一次遍历,对于遍历到的数 x = nums [ i ] x = \textit{nums}[i] x=nums[i],如果 x ∈ [ 1 , N ] x \in [1, N] x[1,N],就知道 x x x 应当出现在数组中的 x − 1 x - 1 x1 的位置,因此交换 nums [ i ] \textit{nums}[i] nums[i] nums [ x − 1 ] \textit{nums}[x - 1] nums[x1],这样 x x x 就出现在了正确的位置。在完成交换后,新的 nums [ i ] \textit{nums}[i] nums[i] 可能还在 [ 1 , N ] [1, N] [1,N] 的范围内,需要继续进行交换操作,直到 x ∉ [ 1 , N ] x \notin [1, N] x/[1,N]

注意到上面的方法可能会陷入死循环。如果 x x x 已经在正确的位置上了,也就是 nums [ i ] \textit{nums}[i] nums[i] 恰好与 nums [ x − 1 ] \textit{nums}[x - 1] nums[x1] 相等,那么就会无限交换下去。所以当 nums [ i ] = x = nums [ x − 1 ] \textit{nums}[i] = x = \textit{nums}[x - 1] nums[i]=x=nums[x1]成立,可以跳出循环,遍历下一个数。

public static int firstMissingPositive(int[] nums) {
    int n = nums.length;
    if(n <= 0) {
        return 1;
    }

    for (int i = 0; i < n; ++i) {
        // 元素还在 [1-n] 的范围内,并且还没有放到合适的位置,就需要继续交换
        while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
            int tmp = nums[nums[i] - 1];
            nums[nums[i] - 1] = nums[i];
            nums[i] = tmp;
        }
    }
    // 第一个元素不正确的位置就是最小正整数
    for (int i = 0; i < n; ++i) {
        if (nums[i] != i + 1) {
            return i + 1;
        }
    }
    // 没有就返回 n + 1
    return n + 1;
}
嵌套了循环体,为什么时间复杂度是 O ( N ) O(N) O(N) ?

    while 循环不会每一次都把数组里面的所有元素都遍历一遍。如果有一些元素在这一次的循环中被交换到了它们应该在的位置,那么在后续的遍历中,就会被跳过。

    平均下来,每个数只需要看一次就可以了,while 循环体被执行很多次的情况不会每次都发生。因此时间复杂度是 O ( N ) O(N) O(N)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值