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


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

题解地址:桶排序 + 基于“异或运算”交换两个变量的值(Python 代码、Java 代码)

说明:文本首发在力扣的题解版块,更新也会在第 1 时间在上面的网站中更新,这篇文章只是上面的文章的一个快照,您可以点击上面的链接看到其他网友对本文的评论。

传送门:41. 缺失的第一个正数

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

示例 1:

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

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

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

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

桶排序 + 基于“异或运算”交换两个变量的值(Python 代码、Java 代码)

这道题使用桶排序的思路,即 “一个萝卜一个坑”。在学习“排序算法”的时候,可能会忽略“桶排序”的作用,但它的思想的确可以解决一些特定问题。

“桶排序”的思想,有些地方也把它叫做“抽屉原理”,以下介绍来自[“百度百科”之“抽屉原理”词条]:

抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有 n + 1 个元素放到 n 个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理。

思路分析:可以就使用题目中的例子,在纸上写写画画,就能得出思路,只不过在编码上需要注意一些细节。

41.png

下面是“桶排序”过程。

(温馨提示:下面的幻灯片中,有几页上有较多的文字,可能需要您停留一下,可以点击右下角的后退 “|◀” 或者前进 “▶|” 按钮控制幻灯片的播放。)

如图所示:我们可以把数组进行一次“排序”,“排序”的规则是:如果这个数字 i 落在“区间范围里”,i 就应该放在索引为 i - 1 的位置上,下面具体解释。

1、数字 i 落在“区间范围里”;

例如:[3, 4, -1, 1],一共 4 个数字,那么如果这个数组中出现 “1”、“2”、“3”、“4”,就是我们重点要关注的数字了;
又例如:[7, 8, 9, 11, 12] 一共 5 个数字,每一个都不是 “1”、“2”、“3”、“4”、“5” 中的一个,因此我们无须关注它们;

2、i 就应该放在索引为i - 1 的位置上;

这句话也可以这么说 “索引为 i 的位置上应该存放的数字是 i + 1”。

就看上面那张图,数字 $1$ 应该放在索引为 $0$ 的位置上,数字 $3$ 应该放在索引为 $2$ 的位置上,数字 $4$ 应该放在索引为 $3$ 的位置上。一个数字放在它应该放的位置上,我们就认为这个位置是“和谐”的,看起来“顺眼”的。

按照以上规则排好序以后,缺失的第 $1$ 个正数一下子就看出来了,那么“最不和谐”的数字的索引 $+ 1$,就为所求。那如果所有的数字都不“和谐”,数组的长度 $+ 1$ 就为所求。

参考代码 1

Python 代码:

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:

        size = len(nums)
        if size == 0:
            return 1

        for i in range(size):

            while nums[i] > 0 and nums[i] <= size:
                if nums[nums[i] - 1] == nums[i]:
                    # 如果已经在合适的位置上,就不用交换了
                    break
                # 这里我单独把交换数组两个位置的方法封装起来,是为了不让自己出错,这一行代码有点绕
                # 就要把它放到合适的位置上,i 应该放在索引为 i - 1 的位置上
                self.__swap(nums, i , nums[i] - 1)


        # 从头到尾看一遍
        for i in range(size):
            if nums[i] != i + 1:
                return i + 1
        return size + 1


    def __swap(self, nums, index1, index2):
        if index1 == index2:
            return
        nums[index1], nums[index2] = nums[index2], nums[index1]  

Java 代码:

import java.util.Arrays;

public class Solution {

    // 关键字:桶排序,什么数字就要放在对应的索引上,其它空着就空着
    // 最好的例子:[3,4,-1,1]
    // 整理好应该是这样:[1,-1,3,4],
    // 这里 1,3,4 都在正确的位置上,
    // -1 不在正确的位置上,索引是 1 ,所以返回 2

    // [4,3,2,1] 要变成 [1,2,3,4],*** Offer 上有类似的问题。

    // 这里负数和大于数组长度的数都是"捣乱项"。

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

        for (int i = 0; i < len; i++) {
            // 前两个是在判断是否成为索引
            // 后一个是在判断,例如 3 在不在索引 2 上
            // 即 nums[i] ?= nums[nums[i]-1] 这里要特别小心
            while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
                // 第 3 个条件不成立的索引的部分是 i 和 nums[i]-1
                swap(nums, nums[i] - 1, i);
            }
        }

        // 调试代码
        // System.out.println(Arrays.toString(nums));

        for (int i = 0; i < len; i++) {
            // [1,-2,3,4]
            // 除了 -2 其它都满足: i+1 = num[i]
            if (nums[i] - 1 != i) {
                return i + 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;
    }

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

复杂度分析

  • 时间复杂度:$O(N)$,这里 $N$ 是数组的长度,其实只要看这个数组一遍,就可以知道每个数字应该放在哪个位置,所以时间复杂度是 $O(N)$。
  • 空间复杂度:$O(1)$,桶排序在原地进行,没有使用额外的存储空间。

补充内容

交换两个整数,有两种比较 tricky 的做法。下面只给出结论,不给出解释(我也解释不了)。

交换两个变量的值,例如 a 和 b,不使用第三个变量,有两种不同的方法:

基于异或运算 基于加减法
a = a ^ b
b = a ^ b
a = a ^ b
a = a + b
b = a - b
a = a - b

我理解的方式就是自己在纸上写几个例子,并且记住这个结论。个人觉得“基于异或运算”交换两个变量的值好记一些,因为右边都一样,左边是 aba

参考代码 2:基于异或运算交换两个变量的值。

Python 代码:

from typing import List


class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:

        size = len(nums)

        # 题目中给出的例子,就是最好的例子
        # [3,4,-1,1]
        # [-1,4,3,1]
        # [-1,1,3,4]
        # 3 应该放在索引为 2 的地方
        # 4 应该放在索引为 3 的地方

        for i in range(size):

            # nums[i] > 0 and nums[i] <= size 的意思是:只要是符合索引的数字
            # 这里的索引应该认为从 1 开始
            # 就要把它放到正确的地方上去,这一步叫 hash
            # nums[i] == nums[nums[i]-1],叫放到了正确的地方
            # 例如,3 应该放在索引为 2 的地方,如果不理解,这句话多读几遍

            # 所以,先判断这个数字是不是索引
            # 然后判断这个数字是不是放在了正确的地方

            while 1 <= nums[i] <= size and nums[i] != nums[nums[i] - 1]:
                # 交换
                self.__swap(nums, i, nums[i] - 1)

        # print(nums)
        # 再遍历一次,没有放对的就是你要找的
        for i in range(size):
            # value: [1,2,3,4]
            # index: [0,1,2,3]
            if i + 1 != nums[i]:
                return i + 1

        return size + 1

    def __swap(self, nums, index1, index2):
        # nums[index1], nums[index2] = nums[index2], nums[index1]
        nums[index1] = nums[index1] ^ nums[index2]
        nums[index2] = nums[index1] ^ nums[index2]
        nums[index1] = nums[index1] ^ nums[index2]

Java 代码:

public class Solution {

    // 关键字:桶排序,什么数字就要放在对应的索引上,其它空着就空着
    // 最好的例子:[3,4,-1,1]
    // 整理好应该是这样:[1,-1,3,4],
    // 这里 1,3,4 都在正确的位置上,
    // -1 不在正确的位置上,索引是 1 ,所以返回 2

    // [4,3,2,1] 要变成 [1,2,3,4],*** Offer 上有类似的问题。

    // 这里负数和大于数组长度的数都是"捣乱项"。

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

        for (int i = 0; i < len; i++) {
            // 前两个是在判断是否成为索引
            // 后一个是在判断,例如 3 在不在索引 2 上
            // 即 nums[i] ?= nums[nums[i]-1] 这里要特别小心
            while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
                // 第 3 个条件不成立的索引的部分是 i 和 nums[i]-1
                swap(nums, nums[i] - 1, i);
            }
        }

        // 调试代码
        // System.out.println(Arrays.toString(nums));

        for (int i = 0; i < len; i++) {
            // [1,-2,3,4]
            // 除了 -2 其它都满足: i+1 = num[i]
            if (nums[i] - 1 != i) {
                return i + 1;
            }
        }

        return len + 1;
    }

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

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

(本节完)


题目地址 题解
LeetCode 第 41 题:缺失的第一个正数 桶排序(Python 代码、Java 代码)
  • 提示:点击上面的题解链接,可以看到我制作的幻灯片轮播图。

这道题使用桶排序的思路,即 “一个萝卜一个坑”,就可以解决。可以就使用题目中的例子,在纸上写写画画,就能得出思路,只不过在编码上需要注意一些细节。

LeetCode 第 41 题:缺失的第一个正数

下面是“桶排序”过程。

LeetCode 第 41 题:缺失的第一个正数


以下是上面 gif 图的静态图。

LeetCode 第 41 题:缺失的第一个正数-1

LeetCode 第 41 题:缺失的第一个正数-2

LeetCode 第 41 题:缺失的第一个正数-3

LeetCode 第 41 题:缺失的第一个正数-4

LeetCode 第 41 题:缺失的第一个正数-5

LeetCode 第 41 题:缺失的第一个正数-6

LeetCode 第 41 题:缺失的第一个正数-7

LeetCode 第 41 题:缺失的第一个正数-8

如图所示:我们可以把数组进行一次“排序”,“排序”的规则是:如果这个数字 i 落在“区间范围里”,i 就应该放在索引为 i - 1 的位置上,下面具体解释。

1、数字 i 落在“区间范围里”;

例如:[3, 4, -1, 1],一共 4 个数字,那么如果这个数组中出现 “1”、“2”、“3”、“4”,就是我们重点要关注的数字了;
又例如:[7, 8, 9, 11, 12] 一共 5 个数字,每一个都不是 “1”、“2”、“3”、“4”、“5” 中的一个,因此我们无须关注它们;

2、i 就应该放在索引为i - 1 的位置上;

这句话也可以这么说 “索引为 i 的位置上应该存放的数字是 i + 1”。

就看上面那张图,数字 $1$ 应该放在索引为 $0$ 的位置上,数字 $3$ 应该放在索引为 $2$ 的位置上,数字 $4$ 应该放在索引为 $3$ 的位置上。一个数字放在它应该放的位置上,我们就认为这个位置是“和谐”的,看起来“顺眼”的。

按照以上规则排好序以后,缺失的第 $1$ 个正数一下子就看出来了,那么“最不和谐”的数字的索引 $+ 1$,就为所求。那如果所有的数字都不“和谐”,数组的长度 $+ 1$ 就为所求。

Python 代码:

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:

        size = len(nums)
        if size == 0:
            return 1

        for i in range(size):

            while nums[i] > 0 and nums[i] <= size:
                if nums[nums[i] - 1] == nums[i]:
                    # 如果已经在合适的位置上,就不用交换了
                    break
                # 这里我单独把交换数组两个位置的方法封装起来,是为了不让自己出错,这一行代码有点绕
                # 就要把它放到合适的位置上,i 应该放在索引为 i - 1 的位置上
                self.__swap(nums, i , nums[i] - 1)


        # 从头到尾看一遍
        for i in range(size):
            if nums[i] != i + 1:
                return i + 1
        return size + 1


    def __swap(self, nums, index1, index2):
        if index1 == index2:
            return
        nums[index1], nums[index2] = nums[index2], nums[index1]  

Java 代码:

import java.util.Arrays;

public class Solution {

    // 关键字:桶排序,什么数字就要放在对应的索引上,其它空着就空着
    // 最好的例子:[3,4,-1,1]
    // 整理好应该是这样:[1,-1,3,4],
    // 这里 1,3,4 都在正确的位置上,
    // -1 不在正确的位置上,索引是 1 ,所以返回 2

    // [4,3,2,1] 要变成 [1,2,3,4],*** Offer 上有类似的问题。

    // 这里负数和大于数组长度的数都是"捣乱项"。

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

        for (int i = 0; i < len; i++) {
            // 前两个是在判断是否成为索引
            // 后一个是在判断,例如 3 在不在索引 2 上
            // 即 nums[i] ?= nums[nums[i]-1] 这里要特别小心
            while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
                // 第 3 个条件不成立的索引的部分是 i 和 nums[i]-1
                swap(nums, nums[i] - 1, i);
            }
        }

        // 调试代码
        // System.out.println(Arrays.toString(nums));

        for (int i = 0; i < len; i++) {
            // [1,-2,3,4]
            // 除了 -2 其它都满足: i+1 = num[i]
            if (nums[i] - 1 != i) {
                return i + 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;
    }

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

复杂度分析:

  • 时间复杂度:$O(N)$,这里 $N$ 是数组的长度,其实只要看这个数组一遍,就可以知道每个数字应该放在哪个位置,所以时间复杂度是 $O(N)$。
  • 空间复杂度:$O(1)$,桶排序在原地进行,没有使用额外的存储空间。

文章作者: liweiwei1419
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 liweiwei1419 !
评论
 上一篇
「力扣」第 66 题:加 1 「力扣」第 66 题:加 1
「力扣」第 66 题:加 1链接:https://leetcode-cn.com/problems/plus-one 给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。 最高位数字存放在数组的首位, 数组中每个元素只存储单
下一篇 
「力扣」第 27 题:移动元素 「力扣」第 27 题:移动元素
「力扣」第 27 题:移动元素传送门:英文网址:27. Remove Element ,中文网址:27. 移除元素 。 给定一个数组 nums 和一个值 val,你需要*原地**移除所有数值等于 *val 的元素,返回移除后数组的新长度。
  目录