「力扣」第 41 题:缺失的第一个正数(困难)

201 阅读4分钟

「力扣」第 41 题: 缺失的第一个正数

给定一个未排序的整数数组,找出其中没有出现的最小的正整数。

示例 1:

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

示例 2:

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

示例 3:

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

说明:你的算法的时间复杂度应为 O(n),并且只能使用常数级别的空间。

思路分析

首先要读懂题意,「找出没有出现的最小的正整数」的意思。我们并不关系负数和零,把数组中所有的正整数拿出来,从 1 开始找,发现只要找不到了,就是最小的正整数。

为此,我们可以把数组中所有的正整数都放进一个数组里,然后从 1 开始找。但是题目要求我们只能使用常数级别的空间。

我们仔细想一下,哈希表里存放的数都是正整数,于是哈希表就可以用布尔数组替代。那这个布尔数组可不可以不使用呢,直接使用原始数组的空间就能达到哈希表的效果。

事实上是可以的。我们观察示例 1 ,我们只要把 1 放在第 1 个位置,2 放在第 2 个位置,0 无处安放,然后遍历一遍数组,发现第 3 个位置上的数字不是 3 ,则 3 就是这个数组缺失的第 1 个正数。

观察示例 2,把 3 放在第 3 个位置,4 放在第 4 个位置,把 1 放在第 1 个位置,-1 无处安放,然后遍历一遍数组,发现第 2 个位置上的数字不是 2 ,则 2 就是这个数组缺失的第 1 个正数。

你是不是已经发现了,这里我们可以使用原始数组充当“桶”的作用,一个桶只放置一个元素,这也叫“抽屉原理”,通俗点说就是一个萝卜一个坑。而放置的过程使用“交换”这个操作完成,这样就无需使用额外空间。这样扫描过一次以后,就一定有元素放在了它应该在的位置,有的位置上的元素无处安放,我们只要从头遍历到第 1 个无处安放的元素,把它的位置返回即可。

细节:1、我们只关心大于 0 和小于等于数组长度的正数,因为它们可以表示索引,我们是读索引,找到题目需要的数;

2、如果遇到两个相同的元素要放置在 1 个位置上怎么办?任意放一个即可,只要这个位置上元素放对了,我们就不用管它,继续遍历下一个位置;

3、数组的索引和要返回的正数相差 1,注意这个编码细节。

这道题的难度为困难,不过如果你知道“桶”的思想,便不难了,只是在编码上有一点点困难。如果你平常注意编码规范,这就是一道很常规的问题。

参考代码

Java 代码:

public class Solution {

    public int firstMissingPositive(int[] nums) {
        int len = nums.length;

        for (int i = 0; i < len; i++) {
            while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
                // 满足在指定范围内、并且没有放在正确的位置上,才交换
                // 例如:数值 3 应该放在索引 2 的位置上
                swap(nums, nums[i] - 1, i);
            }
        }

        // [1, -1, 3, 4]
        for (int i = 0; i < len; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }
        // 都正确则返回数组长度 + 1
        return len + 1;
    }

    private void swap(int[] nums, int index1, int index2) {
        if (index1 == index2) {
            return;
        }
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

复杂度分析

  • 时间复杂度:O(N)这里用到了均摊复杂度分析。在遍历的过程中,有些数直接跳过了,有些数在之前的循环中被交换到了正确的位置。即数组里元素的位置,绝大多数是通过交换来到了正确的位置,交换这个操作是常数级别的。一种可能的情况是,在前面扫描的时候,假设在一个位置上 while 执行循环体的次数很多,在后面的数的遍历中,一定会有一些数不会执行 while 里面的循环体:这些数要么不在扫描的范围内(小于 1 或者大于 len),要么已经在正确的位置上(在之前的 while 循环里被交换过来)。也就是说,最坏的情况不可能每次都发生。

  • 这里 N 是数组的长度,其实只要看这个数组一遍,就可以知道每个数字应该放在哪个位置,所以时间复杂度是 O(N)

  • 空间复杂度:O(1),桶排序在原地进行,没有使用额外的存储空间。