LeetCode hot 100 —41. 缺失的第一个正数

题目信息

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例 1:

复制代码
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。

示例 2:

复制代码
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。

示例 3:

复制代码
输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数 1 没有出现。

提示:

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

思考与自写代码

依旧是能写且可以通过题目考察,但不符合要求

这题的难点在于空间复杂度,我写的代码没有满足常数级别的空间要求。自写代码如下:

python 复制代码
class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        # 最小的正整数肯定是1
        # 要求时间复杂度为O(N),所以这里不能进行排序. 排序的时复杂度O(Nlog N)高于(也就是慢于)O(N)
        ans = 1
        seen = set()

        for num in nums: seen.add(num)

        flag = True
        while flag:
            if ans in seen: ans += 1
            else: flag = False
        return ans

标准代码

先要想明白一个事情 / 发现一个边界:长度为 n 的数组,缺失的最小正整数一定在 [1, n + 1] 中。 如果 1~n 中存在缺失值,答案就在其中;如果全部存在,答案就是 n + 1。所以小于等于 0 或大于 n 的数字都可以忽略。

为了不用 set,可以把原数组本身当作哈希表:

复制代码
数字 1 应该放在下标 0
数字 2 应该放在下标 1
数字 3 应该放在下标 2
......
数字 x 应该放在下标 x - 1

代码如下:

python 复制代码
class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums) # 必定在[1, n + 1]范围内

        for i in range(n):
            # 大循环是下标遍历,确保每个位置的数字被"合理"放置. nums[x - 1] = x (nums[0] = 1, nums[1] = 1, ...)
            while(
                1 <= nums[i] <= n    # 在答案区间范围内
                and nums[nums[i] - 1] != nums[i] # 位置不合规
            ):
                target_index = nums[i] - 1
                nums[i], nums[target_index] = nums[target_index], nums[i] # 交换两个位置的数字
        
        for i in range(n):
            # 所有的x都被放到了index为x - 1的位置,所以遍历过去,如果nums[x - 1] != x,证明x没有出现过
            if nums[i] != i + 1:
                return i + 1
        return n + 1 # 否则相当于一次循环排序,也即nums本身是[1, 2, 3, ..., n]的一个乱序排列

代码分析 / 解析

看了代码以后我的第一反应是:逻辑我可以理解,但是我自己撰写的时候肯定会被这个while和交换给困住,我不知道如何不重复、不遗漏的,将每个数字放在我认为的"正确"的位置上

所以思维过程应该是,首先不要想完整代码应该如何写,先定义好目标状态是什么:

先不要想完整代码,只定义目标状态:

复制代码
如果数字 x 在 [1, n] 中,就尽量把它放到下标 x - 1。

例如数字 3 的目标位置是:

复制代码
nums[2]
为什么需要交换

为了确保数字不会被覆盖 / 丢失。

当开始遍历,我会将遇到的数字 x(当前的处理位) 放到 index 为 x - 1的位置,如果我直接覆盖,那么原有的numsx - 1会丢失。

为了不引入额外的空间去存放numsx - 1,这里进行交换。那么此时 x 去到了正确位置,但当前处理位置变成了一个新的数字 numsx - 1

为什么是 while 不是 if

while循环确保对当前位置的处理是彻底的。当前位置被处理得当,才会进行下标的移动。

如果只使用一次 if,处理完 x 并且完成交换以后就会进入下一个下标,但当前位置的 numsx - 1 仍然没有被处理,或者说不能确定 一定被放到了正确的位置。

所以这就是while的来源,一次交换之后,当前位置会出现一个新元素;只要这个新元素还能被有效安置,就继续处理当前位置。

相关推荐
302wanger8 小时前
DeepSeek-V4.1-Flash-1017 免费用到1024,速来
算法
lisw058 小时前
微分与积分的本质区别解析
算法·数学建模·极限编程
xys_6789 小时前
深挖数据结构:树和二叉树(上)
数据结构·算法
BeiGuTing_code9 小时前
Java动态数组(一)
java·数据结构·windows
Interview Aid1129 小时前
Google SWE Intern VO 面经:两轮 Coding,Follow-up 考察 BFS 状态
算法
琳达爱分享10 小时前
从单机令牌桶到集群限流:一次配额超发的复盘
后端·算法
range10 小时前
带TTL的LRU缓存的实现-字节手撕-26/10/19
算法
糖果豆豆10 小时前
n log n 到底从哪来?排序下界、2-SAT 与 3-SAT、停机问题,卡的是同一件事
算法·架构
地平线开发者10 小时前
【玩转 征程6 算法工具链:PTQ 工具篇】hb_verifier
算法·自动驾驶
171320330计算机毕设编程10 小时前
计算机毕业设计 基于springboot+vue的在线观鸟视频播放网站 LW PPT
java·大数据·vue.js·spring boot·算法·课程设计