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

「力扣」第 41 题:“缺失的第一个正数” 问题描述

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

示例 1

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

示例 2

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

示例 3

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

说明:

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

方法一:哈希表(空间复杂度不符合要求)

  • 按照刚才我们读例子的思路,其实我们只需从最小的正整数 1 1 开始,依次判断 2 2 3 3 4 4 直到数组的长度 N 是否在数组中;

  • 如果当前考虑的数不在这个数组中,我们就找到了这个缺失的最小正整数;

  • 由于我们需要依次判断某一个正整数是否在这个数组里,我们可以先把这个数组中所有的元素放进哈希表。接下来再遍历的时候,就可以以 O ( 1 ) O(1) 的时间复杂度判断某个正整数是否在这个数组;

  • 由于题目要求我们只能使用常数级别的空间,而哈希表的大小与数组的长度是线性相关的,因此空间复杂度不符合题目要求。

Java 代码:

import java.util.HashSet;
import java.util.Set;

public class Solution {

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

        Set<Integer> hashSet = new HashSet<>(len);
        for (int num : nums) {
            hashSet.add(num);
        }

        for (int i = 1; i <= len ; i++) {
            if (!hashSet.contains(i)){
                return i;
            }
        }

        return len + 1;
    }
}

复杂度分析

  • 时间复杂度: O ( N ) O(N) N N 是数组的长度;
  • 空间复杂度: O ( N ) O(N) ,不符合题目的要求。

方法二:二分查找(时间复杂度不符合要求)

  • 根据刚才的分析,这个问题其实就是要我们查找一个元素,而查找一个元素,如果是在有序数组中查找,会快一些;

  • 因此我们可以将数组先排序,再使用二分查找法从最小的正整数 1 1 开始查找,找不到就返回这个正整数;

  • 这个思路需要先对数组排序,而排序使用的时间复杂度是 O ( N log N ) O(N \log N) ,是不符合这个问题的时间复杂度要求。

Java 代码:

import java.util.Arrays;

public class Solution {

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

        for (int i = 1; i <= len; i++) {
            int res = binarySearch(nums, i);
            if (res == -1) {
                return i;
            }
        }
        return len + 1;
    }

    private int binarySearch(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = (left + right) >>> 1;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1;
    }
}

复杂度分析

  • 时间复杂度: O ( N log N ) O(N \log N) ,这个算法的时间复杂度主要集中在排序算法的时间复杂度上,是不符合题目要求的;
  • 空间复杂度: O ( 1 ) O(1)

方法三:将数组视为哈希表

  • 由于题目要求我们“只能使用常数级别的空间”,而要找的数一定在 [1, N + 1] 左闭右闭(这里 N 是数组的长度)这个区间里。因此,我们可以就把原始的数组当做哈希表来使用。事实上,哈希表其实本身也是一个数组;
  • 我们要找的数就在 [1, N + 1] 里,最后 N + 1 这个元素我们不用找。因为在前面的 N 个元素都找不到的情况下,我们才返回 N + 1
  • 那么,我们可以采取这样的思路:就把 1 1 这个数放到下标为 0 0 的位置, 2 2 这个数放到下标为 1 1 的位置,按照这种思路整理一遍数组。然后我们再遍历一次数组,第 1 1 个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。
  • 这个思想就相当于我们自己编写哈希函数,这个哈希函数的规则特别简单,那就是数值为 i 的数映射到下标为 i - 1 的位置。

我们来看一下这个算法是如何应用在示例 2 上的。

「力扣」第 41 题:缺失的第一个正数(哈希表)题解-1

  • 我们一眼就可以看出来,最后那个数组,看起来最不顺眼的那个位置就是下标为 1 的位置,它应该放置的是数值 2 2 ,因此它就是缺失的第一个正数就是 2 2

  • 我们在编码的时候,需要从左到右做一次扫描,来找到这个数。

这个思路是比较典型的:思路简单,但是编码并没有那么容易的问题。

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) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }

    public static void main(String[] args) {
        int[] nums = {4, 3, 2, 1};
        Solution solution = new Solution();
        int firstMissingPositive = solution.firstMissingPositive(nums);
        System.out.println(firstMissingPositive);
    }
}

特别说明:被交换过来的那个数,我们只是判断了它不等于 nums[i],但是它应该放在哪一个位置,还需要继续做判断。因此 for 循环里应该写 while

复杂度分析

  • 时间复杂度: O ( N ) O(N) ,这里 N N 是数组的长度。

说明:while 循环不会每一次都把数组里面的所有元素都看一遍。如果有一些元素在这一次的循环中被交换到了它们应该在的位置,那么在后续的遍历中,由于它们已经在正确的位置上了,代码再执行到它们的时候,就会被跳过。

最极端的一种情况是,在第 1 个位置经过这个 while 就把所有的元素都看了一遍,这个所有的元素都被放置在它们应该在的位置,那么 for 循环后面的部分的 while 的循环体都不会被执行。

平均下来,每个数只需要看一次就可以了,while 循环体被执行很多次的情况不会每次都发生。这样的复杂度分析的方法叫做均摊复杂度分析

  • 空间复杂度: O ( 1 ) O(1)
发布了455 篇原创文章 · 获赞 348 · 访问量 126万+

猜你喜欢

转载自blog.csdn.net/lw_power/article/details/104544590
今日推荐