【LeetCode 刷题系列】缺失的第一个正数:原地哈希的极致优雅

3 阅读4分钟

前言

Hello 大家好!今天我们来聊聊 LeetCode 上的经典困难题——缺失的第一个正数(First Missing Positive)

这道题是面试中的高频考点,很多同学在看到“未排序数组”和“O(n) 时间复杂度”时,第一反应往往是排序或者使用哈希表。但今天我要带大家用一种非常巧妙、空间复杂度为 O(1) 的解法——原地哈希(置换排序) ,来完美拿下它!


题目描述

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

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

示例:

输入:nums = [3,4,-1,1]
输出:2

输入:nums = [7,8,9,11,12]
输出:1

核心思路:原地哈希(位置重排)

1. 关键结论:答案的范围

对于一个长度为 n 的数组,缺失的第一个正整数必然在 [1, n+1] 范围内

  • 如果数组恰好包含 1 ~ n 的所有正整数,那么缺失的就是 n+1(如 [1,2,3] → 缺失 4)。
  • 如果数组存在 1 ~ n 的缺口,那么该缺口就是答案(如 [3,4,-1,1] → 缺口为 2)。

2. 核心策略:把数组本身当哈希表

既然答案只可能在 [1, n] 或 n+1 中,我们只需要关心在这个范围内的数字。
我们可以利用数组的下标作为哈希键:

  • 数字 1 应该放在索引 0 的位置
  • 数字 2 应该放在索引 1 的位置
  • 数字 k 应该放在索引 k-1 的位置

目标:  遍历数组,将每个在 [1, n] 范围内的数 x,交换到它应该在的位置 nums[x-1] 上。

3. 为什么必须用 while 而不是 if

这是这道题最容易踩坑的地方!
当我们把 nums[i] 交换到目标位置后,换到 i 位置的新元素可能依然不在正确的位置上

  • 如果用 if:每次只交换一次就跳过,换过来的新元素会被“晾在一边”,导致最终结果错误。
  • 如果用 while:持续交换,直到当前位置的元素不在 [1, n] 范围内,或者已经在正确位置为止。

时间复杂度分析:  虽然看起来是双重循环,但每次 swap 操作都会把至少一个元素放到正确的位置上,而每个元素最多被正确放置一次。因此总的交换次数不超过 n,整体时间复杂度依然是 O(n)(摊还分析)。


代码实现

/**
 * @param {number[]} nums
 * @return {number}
 */
var firstMissingPositive = function(nums) {
    const n = nums.length;
    
    // 第一步:将 1~n 范围内的正整数归位
    for (let i = 0; i < n; i++) {
        // 循环条件:当前数在 [1,n] 范围内,且没有放在正确的位置上
        while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] !== nums[i]) {
            // 将 nums[i] 交换到它应该在的位置 nums[i]-1
            const temp = nums[nums[i] - 1];
            nums[nums[i] - 1] = nums[i];
            nums[i] = temp;
        }
    }
    
    // 第二步:扫描第一个位置不对的数
    for (let i = 0; i < n; i++) {
        if (nums[i] !== i + 1) {
            return i + 1;
        }
    }
    
    // 如果 1~n 全都存在,返回 n+1
    return n + 1;
};

代码细节拆解

  1. 循环条件三要素:

    • nums[i] >= 1 && nums[i] <= n:只处理有效范围内的数,负数、0、大于n的数直接忽略。
    • nums[nums[i] - 1] !== nums[i]:防止重复数字导致死循环(比如 [1,1],如果不加这个条件会无限交换)。
  2. 交换操作:  使用临时变量 temp 完成原地交换,不引入额外空间。

  3. 最终扫描:  遍历归位后的数组,第一个 nums[i] !== i+1 的位置,i+1 就是缺失的最小正整数。


复杂度分析

  • 时间复杂度:  O(n)。每个元素最多被交换到正确位置一次,总交换次数 ≤ n。
  • 空间复杂度:  O(1)。只使用了常数级别的额外变量,完美满足题目要求。

总结

这道题的精髓在于打破思维定式:谁说哈希表一定要额外开辟空间?数组本身的下标就可以作为天然的哈希键!