题目描述
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 并且只使用常数级别额外空间的解决方案。
输入
未排序的整数数组 nums
输出
没有出现的最小正整数
约束条件
1 <= nums.length <= 10^5-2^31 <= nums[i] <= 2^31 - 1
进阶约束
实现时间复杂度 且空间复杂度 的解法。
示例1
输入:nums = [1,2,0]输出:3解释:范围 [1,2] 中的数字都在数组中,因此缺失的最小正整数是 3。示例2
输入:nums = [3,4,-1,1]输出:2解释:1 在数组中,但 2 缺失,因此答案是 2。示例3
输入:nums = [7,8,9,11,12]输出:1解释:最小的正整数 1 缺失,因此答案是 1。解题思路
题目要求找「缺失的第一个正整数」——对于长度为 的数组,答案一定在 这个范围内。为什么?因为:
- 最坏情况下,数组恰好包含 到 这 个正整数,那么缺失的第一个正整数就是
- 如果数组中缺失了 中的某个数,答案就是那个缺失的数
- 数组中 ≤ 0 或 > n 的数可以被忽略——因为它们不可能成为答案,反而占据了空间
💡 核心直觉:我们只需要关心 范围内的数。如果能把每一个 “放到它该在的位置”(即下标 处),那么第一个”位置不对”的索引就揭示了缺失的正整数。
从暴力枚举出发
最朴素的想法:从 开始逐个检查每个正整数是否在数组中:
class Solution: def firstMissingPositive(self, nums: List[int]) -> int: s = set(nums) x = 1 while x in s: x += 1 return x- 时间复杂度:(建集合 ,检查最多 次,每次 )
- 空间复杂度:(需要一个哈希集合)
虽然时间复杂度已是最优,但空间复杂度为 ,不满足进阶要求。有没有办法把空间降到 ?
瓶颈分析:暴力法额外建了一个哈希集合来支持快速查找。如果能原地利用数组本身作为”哈希表”,就不需要额外空间了。
原地哈希:让数组自己做”集合”
关键的洞察是:数组的下标天然就是连续的整数 。如果我们能让数组满足 nums[i] = i + 1(即下标 i 处存放值 i+1),那么数组本身就隐含了一个”集合”——要判断 是否出现,只需要看 nums[x-1] 是否等于 。
🧠 直觉理解:想象你在整理一排有编号的储物柜。你有一堆写着号码的包裹,你希望每个包裹
x被放进编号为x的柜子里。如果你按照这个规则逐个整理包裹,那么最终”柜子是空的”或”柜子里放的号码不对”的位置,就告诉你哪个正整数缺席了。
具体来说,分两步走:
第一步:重新摆放(交换)
遍历数组,对于每个位置 i,如果 nums[i] 在 范围内,并且它不在”正确的”位置上(即 nums[i] != nums[nums[i] - 1]),就把它和它”该去的位置”上的元素交换。重复这个过程直到当前位置无法再交换为止。
这里用 while 而不是 if 很关键——交换过来的新元素可能也在 范围内,需要继续为它寻找正确位置:
for i in range(n): while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]: nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]第二步:寻找缺失
交换完成后,再遍历一次数组。第一个不满足 nums[i] == i + 1 的位置就对应着缺失的正整数:
for i in range(n): if nums[i] != i + 1: return i + 1return n + 1执行过程可视化
以 nums = [3, 4, -1, 1]()为例,追踪交换过程:
| 轮次 | i | 交换前数组 | nums[i] | 目标位置 | 交换后数组 | 说明 |
|---|---|---|---|---|---|---|
| — | — | — | — | — | [3, 4, -1, 1] | 初始状态 |
| 1 | 0 | [3, 4, -1, 1] | 3 | nums[2] | [-1, 4, 3, 1] | 3 和 nums[2] = -1 交换 |
| 1 | 0 | [-1, 4, 3, 1] | -1 | — | 不变 | -1 不在 [1,4] 内,停止 |
| 2 | 1 | [-1, 4, 3, 1] | 4 | nums[3] | [-1, 1, 3, 4] | 4 和 nums[3] = 1 交换 |
| 2 | 1 | [-1, 1, 3, 4] | 1 | nums[0] | [1, -1, 3, 4] | 1 和 nums[0] = -1 交换 |
| 2 | 1 | [1, -1, 3, 4] | -1 | — | 不变 | -1 不在 [1,4] 内,停止 |
| 3 | 2 | [1, -1, 3, 4] | 3 | nums[2] | 不变 | nums[2] 已经是 3,跳过 |
| 4 | 3 | [1, -1, 3, 4] | 4 | nums[3] | 不变 | nums[3] 已经是 4,跳过 |
交换结束后,数组变为 [1, -1, 3, 4]。遍历检查:
i=0:nums[0]=1✅i=1:nums[1]=-1 ≠ 2❌ → 返回2
答案正是 2。
⚠️ 注意:交换条件中
nums[i] != nums[nums[i] - 1]不能写成nums[i] != i + 1。考虑[1, 1]这种情况——nums[0]=1已经在正确位置,但nums[1]=1与nums[1-1]=nums[0]=1相同,如果只用nums[i] != i + 1作为条件会陷入死循环(一直在原地交换相同的两个 1)。
复杂度
- 时间复杂度:。虽然内层有
while循环,但每个数最多被交换一次到正确位置,总交换次数不超过 次。 - 空间复杂度:。只使用了常数个临时变量,在原地修改数组。
💡 为什么时间复杂度不是 ? 外层循环 次,内层的
while看似可以多轮交换,但每次交换至少让一个数去到它的最终位置。每个数一旦”落户”就不会再被移动(因为后续nums[i] != nums[nums[i]-1]条件不成立)。因此总交换操作 ≤ 次,整个算法仍是线性的。这种分析方法叫做均摊分析。
解题思路(标记法)
除了交换法,还有一种原地哈希的实现——用正负号做标记。
核心思想:第一遍遍历,对于每个 ,将 nums[x-1] 标记为负数,表示” 出现过”。第二遍遍历,第一个正数的位置就对应缺失的正整数。
class Solution: def firstMissingPositive(self, nums: List[int]) -> int: n = len(nums)
# 第一步:将非正数设为 n+1(不影响后续标记) for i in range(n): if nums[i] <= 0: nums[i] = n + 1
# 第二步:用负号做标记 for i in range(n): x = abs(nums[i]) if 1 <= x <= n and nums[x - 1] > 0: nums[x - 1] = -nums[x - 1]
# 第三步:找第一个正数 for i in range(n): if nums[i] > 0: return i + 1
return n + 1这个方法同样满足 时间和 空间,但它修改了数组中元素的符号,且需要恢复或接受破坏。相比而言,交换法更直观——数组的最终状态就是”整理好的柜子”,不需要额外的”负号”语义。
| 对比项 | 交换法 | 标记法 |
|---|---|---|
| 核心操作 | 交换元素到正确位置 | 用负号标记出现过的数 |
| 数组最终状态 | 部分有序(nums[i]=i+1 或不在范围内的数) | 元素可能被取反,需要 abs() 恢复 |
| 可读性 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 稳定性 | 不改变元素绝对值 | 改变了元素符号 |
代码
最终代码(交换法)
class Solution: def firstMissingPositive(self, nums: List[int]) -> int: n = len(nums) for i in range(n): while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]: nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1] for i in range(n): if nums[i] != i + 1: return i + 1 return n + 1逐行解读:
n = len(nums):数组长度。答案的范围锁定在 。- 外层循环
for i in range(n):遍历每个位置,尝试让该位置存放正确的数。 - 交换条件
while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]:1 <= nums[i] <= n:只关心 范围内的数,非正数和大于 的数可以留在原地nums[i] != nums[nums[i] - 1]:当前数不在它该在的位置上(避免重复交换导致死循环)
- 交换:
nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]——Python 的元组解包一次性完成交换,不需要临时变量 - 第二遍扫描:第一个
nums[i] != i + 1的位置,说明i+1缺失,直接返回 - 兜底
return n + 1:所有位置都正确,说明 全部出现,返回
⚠️ 交换语句的顺序要留意:
nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]是正确的。如果写成nums[i], nums[nums[i] - 1] = nums[nums[i] - 1], nums[i],Python 会先计算右侧,再从左到右赋值——这时nums[i]先被修改,后续nums[nums[i] - 1]中的nums[i]已经变了,导致赋值到错误的位置。所以务必把nums[nums[i] - 1]放在赋值语句的左侧第一位。
总结
「缺失的第一个正数」教会我们一个重要的思维:将数组的索引视为哈希表的键。
🧠 核心收获:当你需要在 空间内做「存在性检查」时,考虑原地哈希——把数组本身当哈希表用。前提是你能建立「元素值 → 数组下标」的映射关系。本题中映射关系是 ,恰到好处。
这个技巧在很多题目中都有类似的应用:
- 442. 数组中重复的数据:同样是 范围的整数,同样可以用原地交换或正负标记找出重复项
- 448. 找到所有数组中消失的数字:本题的”兄弟题”,找的是所有缺失的数而非第一个
- 287. 寻找重复数:利用下标映射 + 快慢指针,将数组问题转化为链表环检测问题
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









