mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2018 字
5 分钟
缺失的第一个正数
2026-07-03

题目描述#

题目链接

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

请你实现时间复杂度为 O(n)O(n) 并且只使用常数级别额外空间的解决方案。

输入#

未排序的整数数组 nums

输出#

没有出现的最小正整数

约束条件#

  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1

进阶约束#

实现时间复杂度 O(n)O(n) 且空间复杂度 O(1)O(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。

解题思路#

题目要求找「缺失的第一个正整数」——对于长度为 nn 的数组,答案一定在 [1,n+1][1, n+1] 这个范围内。为什么?因为:

  • 最坏情况下,数组恰好包含 11nnnn 个正整数,那么缺失的第一个正整数就是 n+1n+1
  • 如果数组中缺失了 [1,n][1, n] 中的某个数,答案就是那个缺失的数
  • 数组中 ≤ 0 或 > n 的数可以被忽略——因为它们不可能成为答案,反而占据了空间

💡 核心直觉:我们只需要关心 [1,n][1, n] 范围内的数。如果能把每一个 x[1,n]x \in [1, n] “放到它该在的位置”(即下标 x1x-1 处),那么第一个”位置不对”的索引就揭示了缺失的正整数。

从暴力枚举出发#

最朴素的想法:从 11 开始逐个检查每个正整数是否在数组中:

class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
s = set(nums)
x = 1
while x in s:
x += 1
return x
  • 时间复杂度:O(n)O(n)(建集合 O(n)O(n),检查最多 n+1n+1 次,每次 O(1)O(1)
  • 空间复杂度:O(n)O(n)(需要一个哈希集合)

虽然时间复杂度已是最优,但空间复杂度为 O(n)O(n),不满足进阶要求。有没有办法把空间降到 O(1)O(1)

瓶颈分析:暴力法额外建了一个哈希集合来支持快速查找。如果能原地利用数组本身作为”哈希表”,就不需要额外空间了。

原地哈希:让数组自己做”集合”#

关键的洞察是:数组的下标天然就是连续的整数 0,1,2,,n10, 1, 2, \dots, n-1。如果我们能让数组满足 nums[i] = i + 1(即下标 i 处存放值 i+1),那么数组本身就隐含了一个”集合”——要判断 xx 是否出现,只需要看 nums[x-1] 是否等于 xx

🧠 直觉理解:想象你在整理一排有编号的储物柜。你有一堆写着号码的包裹,你希望每个包裹 x 被放进编号为 x 的柜子里。如果你按照这个规则逐个整理包裹,那么最终”柜子是空的”或”柜子里放的号码不对”的位置,就告诉你哪个正整数缺席了。

具体来说,分两步走:

第一步:重新摆放(交换)

遍历数组,对于每个位置 i,如果 nums[i][1,n][1, n] 范围内,并且它不在”正确的”位置上(即 nums[i] != nums[nums[i] - 1]),就把它和它”该去的位置”上的元素交换。重复这个过程直到当前位置无法再交换为止。

这里用 while 而不是 if 很关键——交换过来的新元素可能也在 [1,n][1, n] 范围内,需要继续为它寻找正确位置:

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 + 1
return n + 1

执行过程可视化#

nums = [3, 4, -1, 1]n=4n=4)为例,追踪交换过程:

轮次i交换前数组nums[i]目标位置交换后数组说明
[3, 4, -1, 1]初始状态
10[3, 4, -1, 1]3nums[2][-1, 4, 3, 1]3nums[2] = -1 交换
10[-1, 4, 3, 1]-1不变-1 不在 [1,4] 内,停止
21[-1, 4, 3, 1]4nums[3][-1, 1, 3, 4]4nums[3] = 1 交换
21[-1, 1, 3, 4]1nums[0][1, -1, 3, 4]1nums[0] = -1 交换
21[1, -1, 3, 4]-1不变-1 不在 [1,4] 内,停止
32[1, -1, 3, 4]3nums[2]不变nums[2] 已经是 3,跳过
43[1, -1, 3, 4]4nums[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]=1nums[1-1]=nums[0]=1 相同,如果只用 nums[i] != i + 1 作为条件会陷入死循环(一直在原地交换相同的两个 1)。

复杂度#

  • 时间复杂度O(n)O(n)。虽然内层有 while 循环,但每个数最多被交换一次到正确位置,总交换次数不超过 nn 次。
  • 空间复杂度O(1)O(1)。只使用了常数个临时变量,在原地修改数组。

💡 为什么时间复杂度不是 O(n2)O(n^2) 外层循环 nn 次,内层的 while 看似可以多轮交换,但每次交换至少让一个数去到它的最终位置。每个数一旦”落户”就不会再被移动(因为后续 nums[i] != nums[nums[i]-1] 条件不成立)。因此总交换操作 ≤ nn 次,整个算法仍是线性的。这种分析方法叫做均摊分析

解题思路(标记法)#

除了交换法,还有一种原地哈希的实现——用正负号做标记。

核心思想:第一遍遍历,对于每个 x[1,n]x \in [1, n],将 nums[x-1] 标记为负数,表示”xx 出现过”。第二遍遍历,第一个正数的位置就对应缺失的正整数。

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

这个方法同样满足 O(n)O(n) 时间和 O(1)O(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

逐行解读:

  1. n = len(nums):数组长度。答案的范围锁定在 [1,n+1][1, n+1]
  2. 外层循环 for i in range(n):遍历每个位置,尝试让该位置存放正确的数。
  3. 交换条件 while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]
    • 1 <= nums[i] <= n:只关心 [1,n][1, n] 范围内的数,非正数和大于 nn 的数可以留在原地
    • nums[i] != nums[nums[i] - 1]:当前数不在它该在的位置上(避免重复交换导致死循环)
  4. 交换nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]——Python 的元组解包一次性完成交换,不需要临时变量
  5. 第二遍扫描:第一个 nums[i] != i + 1 的位置,说明 i+1 缺失,直接返回
  6. 兜底 return n + 1:所有位置都正确,说明 [1,n][1, n] 全部出现,返回 n+1n+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] 放在赋值语句的左侧第一位。

总结#

「缺失的第一个正数」教会我们一个重要的思维:将数组的索引视为哈希表的键

🧠 核心收获:当你需要在 O(1)O(1) 空间内做「存在性检查」时,考虑原地哈希——把数组本身当哈希表用。前提是你能建立「元素值 → 数组下标」的映射关系。本题中映射关系是 xx1x \to x-1,恰到好处。

这个技巧在很多题目中都有类似的应用:

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

缺失的第一个正数
https://www.hygen.red/posts/hot100/05-普通数组/017缺失的第一个正数/
作者
Hygen
发布于
2026-07-03
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录