【算法精讲】LeetCode 41. 缺失的第一个正数:原地哈希的极致运用
🧠 核心知识点:什么是“原地哈希”?
在解决数组问题时,我们通常有两种选择:
- 使用额外空间:创建一个哈希表(Hash Map)或集合(Set)来记录出现的数字。空间复杂度为 。
原地哈希的原理
- 对应关系:数字
1应该在索引0,数字2应该在索引1,以此类推。 - 判断缺失:遍历数组,如果发现某个位置
i上的数字不是i+1,那么i+1就是缺失的最小正整数。
🎯 题目引入:LeetCode 41
题目描述
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。
硬性约束:
- 时间复杂度必须为 。
示例:
- 输入:
[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]:
-
有效范围检查:只有当
nums[i]是正整数,且在数组长度范围内(1 <= nums[i] <= n)时,它才有可能对结果产生影响。负数、0 或大于n的数可以直接忽略(它们占着位置也没用,反正缺失的正数肯定在1到n+1之间)。 -
归位操作:如果
nums[i]是有效数字,比如是3,它应该待在索引2的位置(即nums[2])。- 检查目标位置
nums[3-1]是否已经是3了? - 如果是,说明已经归位,跳过。
- 如果不是,说明目标位置被别的数占了,或者目标位置是空的。我们需要交换
nums[i]和nums[nums[i]-1],把3放到它该去的地方。
- 检查目标位置
-
循环交换:交换后,当前位置
i可能会迎来一个新的数字,我们需要继续检查这个新数字是否需要归位,直到当前位置的数字无法归位为止(使用while循环)。
第二步:查找缺失值
经过第一步的处理,数组中所有能归位的正整数都回到了对应的索引位置。
再次遍历数组:
- 如果发现
nums[i] != i + 1,说明索引i对应的正整数i+1缺失了。 - 返回
i + 1。
第三步:兜底返回
如果遍历完整个数组,发现 1 到 n 都在正确的位置上(数组变成了 [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。
-
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条件,内层循环结束。
- 3 在范围内,且
-
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,内层循环结束。
- 4 在范围内,且
-
i = 2,
nums[2] = 3。- 目标位置
nums[2]已经是 3,无需交换。
- 目标位置
-
i = 3,
nums[3] = 4。- 目标位置
nums[3]已经是 4,无需交换。
- 目标位置
最终数组状态:[1, -1, 3, 4]
查找阶段:
i=0:nums[0] == 1(正常)i=1:nums[1] == -1(异常!期望是 2)- 返回
i + 1即2。
📝 复杂度分析
-
- 我们直接在原数组
nums上进行修改,没有使用任何额外的数组或哈希表,仅使用了几个变量 (i,n,correct_pos)。
- 我们直接在原数组
⚠️ 易错点提示
- 死循环问题:
while循环中必须加上nums[nums[i] - 1] != nums[i]这个判断。如果数组中有重复元素(例如[1, 1]),如果不加这个判断,程序会试图把第二个1不断交换到索引0的位置,导致死循环。 - 索引越界:访问
nums[nums[i] - 1]前,必须先确保nums[i]在1到n之间,否则会导致索引超出范围错误。