哈希算法题目:缺失的第一个正数
这道题要求我们在一个未排序的整数数组中,找出没有出现的最小的正整数。你的算法时间复杂度应为 O(n),并且只能使用常数级别的额外空间。这是一个将哈希思想与数组下标就地结合使用的经典问题。
题目描述
给你一个未排序的整数数组 nums。请你找出其中没有出现的最小的正整数。
你必须实现时间复杂度为 O(n)、空间复杂度为 O(1) 的算法来解决此问题。
示例 1:
输入:nums = [1,2,0]
输出:3
解释:最小的正整数 1 和 2 都在数组中,所以没有出现的最小正整数是 3。
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有出现。
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:所有数都大于 0,但最小的正整数 1 没有出现。
解题思路
如果允许使用额外空间,我们可以很容易地用哈希集合(HashSet)存储所有正数,然后从 1 开始逐个检查是否在集合中。但题目要求 O(1) 空间,所以我们需要一种“原地哈希”的技巧:利用数组自身的下标来标记某个正整数是否出现过。
核心观察:
- 对于长度为 n 的数组,没有出现的最小的正整数只可能在 [1, n+1] 这个范围内。
- 最理想情况:数组正好包含 1 到 n 的所有数,那么答案是 n+1。
- 其他情况:在 [1, n] 范围内至少缺失一个数,答案就是那个缺失的最小数。
- 因此,我们只需要关心数组中值在 [1, n] 范围内的数。
原地哈希策略:
我们可以遍历数组,对于每个遍历到的值 x,如果 x 在 [1, n] 范围内,我们就把它放到数组下标 x-1 的位置上(因为数组下标从 0 开始)。这样,如果数组中包含 1,它最终会在下标 0 处;包含 2,会在下标 1 处,以此类推。
遍历完成后,我们再次检查数组。如果某个下标 i 处的值不是 i+1,那么就说明 i+1 这个正整数缺失了。如果所有 [1, n] 都出现在正确位置,那么答案就是 n+1。
循序渐进讲解解题步骤
步骤 1:理解数据范围与目标
- 数组长度为 n。
- 答案只可能是 1 到 n+1 之间的整数。
- 我们要做的是“归位”:让数字 1 放到索引 0 处,数字 2 放到索引 1 处,……,数字 n 放到索引 n-1 处。
步骤 2:第一次遍历——原地交换归位
我们从头到尾遍历数组,对于每个位置 i:
- 设当前值
x = nums[i]。 - 如果
x在 [1, n] 范围内,并且x不在它应该在的位置(即nums[x-1] != x),那么我们就交换nums[i]和nums[x-1],把x放到正确的位置。 - 交换后,新的
nums[i]可能还是一个需要归位的数,所以我们需要继续处理当前位置i,直到nums[i]不在 [1, n] 范围内,或者已经在正确位置。
为什么是 while 循环而不是 if?
因为交换后,被换到位置 i 的新数字可能也属于 [1, n] 且不在正确位置,需要继续归位。
为什么条件要加 nums[x-1] != x?
避免重复交换导致死循环。例如数组 [1, 1],如果第一个 1 已经在正确位置,第二个 1 过来时,如果不检查就直接交换,就会和自身交换,无限循环。
示例推演:
数组 nums = [3, 4, -1, 1],n = 4。
- i=0, x=3,它应该在索引 2(因为 3-1=2)。交换
nums[0]和nums[2],数组变为[-1, 4, 3, 1]。新nums[0] = -1不在 [1,4] 内,继续 i=1。 - i=1, x=4,应在索引 3。交换
nums[1]和nums[3],数组变为[-1, 1, 3, 4]。新nums[1] = 1,应在索引 0,且nums[0] != 1,所以继续 while:交换nums[1]和nums[0],数组变为[1, -1, 3, 4]。新nums[1] = -1,结束。 - i=2, x=3,已在正确位置(因为
nums[2] = 3),跳过。 - i=3, x=4,已在正确位置,跳过。
最终数组:[1, -1, 3, 4]。
步骤 3:第二次遍历——查找第一个缺失的正数
遍历索引 0 到 n-1:
- 如果
nums[i] != i+1,那么i+1就是缺失的最小正整数,直接返回。 - 如果遍历完都没有找到,说明数组包含了 1 到 n 的所有数,返回 n+1。
在上面的示例数组 [1, -1, 3, 4] 中:
- i=0: nums[0] = 1,正确。
- i=1: nums[1] = -1,不等于 2,所以缺失的最小正整数是 2,返回 2。
步骤 4:边界情况与复杂度分析
- 时间复杂度:每个数字最多被交换一次放到正确位置,所以尽管有 while 循环,总交换次数不超过 n,时间复杂度 O(n)。
- 空间复杂度:只用了几个变量,O(1)。
- 边界情况:空数组?题目未明确,但按定义最小正整数是 1,所以空数组应返回 1。
- 包含重复数字:我们的条件
nums[x-1] != x能防止重复数字导致的死循环,重复数字最终会让正确位置放上该数字,其他重复数字留在外面(不在 [1, n] 内或不在正确位置)。
完整代码示例(Python)
def firstMissingPositive(nums):
n = len(nums)
if n == 0:
return 1
i = 0
while i < n:
# 当前值在 [1, n] 范围内,且不在它应该在的位置
if 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
# 交换到正确位置
correct_idx = nums[i] - 1
nums[i], nums[correct_idx] = nums[correct_idx], nums[i]
else:
i += 1
# 第二次遍历,查找第一个位置不匹配的
for i in range(n):
if nums[i] != i + 1:
return i + 1
return n + 1
测试运行:
print(firstMissingPositive([3, 4, -1, 1])) # 输出 2
print(firstMissingPositive([1, 2, 0])) # 输出 3
print(firstMissingPositive([7, 8, 9, 11, 12])) # 输出 1
总结
这道题的关键在于利用数组下标本身作为哈希表,将数值 x 映射到下标 x-1 的位置上,从而在 O(1) 空间内标记数字是否出现。这种“原地哈希”技巧在限制空间复杂度的问题中非常有用,是哈希思想的一种巧妙变体。通过两次遍历(一次归位,一次检查),我们就能在 O(n) 时间、O(1) 空间内解决问题。