【力扣-41. 缺失的第一个正数 ✨】Python笔记

5 阅读5分钟

【算法精讲】LeetCode 41. 缺失的第一个正数:原地哈希的极致运用


🧠 核心知识点:什么是“原地哈希”?

在解决数组问题时,我们通常有两种选择:

  1. 使用额外空间:创建一个哈希表(Hash Map)或集合(Set)来记录出现的数字。空间复杂度为 O(N)O(N)

原地哈希的原理

  • 对应关系:数字 1 应该在索引 0,数字 2 应该在索引 1,以此类推。
  • 判断缺失:遍历数组,如果发现某个位置 i 上的数字不是 i+1,那么 i+1 就是缺失的最小正整数。

🎯 题目引入:LeetCode 41

题目描述

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

硬性约束:

  1. 时间复杂度必须为 O(N)O(N)

示例:

  • 输入: [1, 2, 0] -> 输出: 3 (1, 2 都在,缺 3)
  • 输入: [3, 4, -1, 1] -> 输出: 2 (1 在,但 2 不在)
  • 输入: [7, 8, 9, 11, 12] -> 输出: 1 (1 都不在)

💡 解题思路详解

既然不能用额外的空间存数据,我们就把输入数组 nums 本身当作哈希表来用

第一步:数据清洗与归位

我们的目标是让数组尽可能变成 [1, 2, 3, ...] 的形式。
遍历数组,对于每个元素 nums[i]

  1. 有效范围检查:只有当 nums[i] 是正整数,且在数组长度范围内(1 <= nums[i] <= n)时,它才有可能对结果产生影响。负数、0 或大于 n 的数可以直接忽略(它们占着位置也没用,反正缺失的正数肯定在 1n+1 之间)。

  2. 归位操作:如果 nums[i] 是有效数字,比如是 3,它应该待在索引 2 的位置(即 nums[2])。

    • 检查目标位置 nums[3-1] 是否已经是 3 了?
    • 如果是,说明已经归位,跳过。
    • 如果不是,说明目标位置被别的数占了,或者目标位置是空的。我们需要交换 nums[i]nums[nums[i]-1],把 3 放到它该去的地方。
  3. 循环交换:交换后,当前位置 i 可能会迎来一个新的数字,我们需要继续检查这个新数字是否需要归位,直到当前位置的数字无法归位为止(使用 while 循环)。

第二步:查找缺失值

经过第一步的处理,数组中所有能归位的正整数都回到了对应的索引位置。
再次遍历数组:

  • 如果发现 nums[i] != i + 1,说明索引 i 对应的正整数 i+1 缺失了。
  • 返回 i + 1

第三步:兜底返回

如果遍历完整个数组,发现 1n 都在正确的位置上(数组变成了 [1, 2, ..., n]),那么缺失的最小正整数就是 n + 1


💻 代码实现与逐行解析

from typing import List

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums)
        
        # 1. 原地哈希:将数值 x 放到索引 x-1 的位置
        for i in range(n):
            # while 循环条件解析:
            # 1. 1 <= nums[i] <= n : 只关心 1 到 n 之间的数
            # 2. nums[nums[i] - 1] != nums[i] : 目标位置上的数还不是当前数
            #    (如果不加这个判断,遇到重复元素如 [1, 1] 会死循环)
            while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
                # 计算目标位置
                correct_pos = nums[i] - 1
                
                # 交换当前元素和目标位置的元素
                # Python 特有的元组解包交换,无需临时变量
                nums[i], nums[correct_pos] = nums[correct_pos], nums[i]
        
        # 2. 遍历寻找第一个不符合规则的位置
        for i in range(n):
            # 如果索引 i 处的值不是 i+1,说明 i+1 缺失
            if nums[i] != i + 1:
                return i + 1
        
        # 3. 如果 1 到 n 都存在,则缺失的是 n+1
        return n + 1

关键点图解

假设输入 nums = [3, 4, -1, 1],长度 n=4

  1. i = 0, nums[0] = 3

    • 3 在范围内,且 nums[3-1] (即 nums[2]) 是 -1,不等于 3。
    • 交换 nums[0]nums[2]
    • 数组变为:[-1, 4, 3, 1]
    • 此时 nums[0]-1,不满足 while 条件,内层循环结束。
  2. i = 1, nums[1] = 4

    • 4 在范围内,且 nums[4-1] (即 nums[3]) 是 1,不等于 4。
    • 交换 nums[1]nums[3]
    • 数组变为:[-1, 1, 3, 4]
    • 此时 nums[1]1
    • 继续检查1 在范围内,且 nums[1-1] (即 nums[0]) 是 -1,不等于 1。
    • 再次交换 nums[1]nums[0]
    • 数组变为:[1, -1, 3, 4]
    • 此时 nums[1]-1,内层循环结束。
  3. i = 2, nums[2] = 3

    • 目标位置 nums[2] 已经是 3,无需交换。
  4. i = 3, nums[3] = 4

    • 目标位置 nums[3] 已经是 4,无需交换。

最终数组状态[1, -1, 3, 4]
查找阶段

  • i=0: nums[0] == 1 (正常)
  • i=1: nums[1] == -1 (异常!期望是 2)
  • 返回 i + 12

📝 复杂度分析

    • 我们直接在原数组 nums 上进行修改,没有使用任何额外的数组或哈希表,仅使用了几个变量 (i, n, correct_pos)。

⚠️ 易错点提示

  1. 死循环问题while 循环中必须加上 nums[nums[i] - 1] != nums[i] 这个判断。如果数组中有重复元素(例如 [1, 1]),如果不加这个判断,程序会试图把第二个 1 不断交换到索引 0 的位置,导致死循环。
  2. 索引越界:访问 nums[nums[i] - 1] 前,必须先确保 nums[i]1n 之间,否则会导致索引超出范围错误。