LeetCode 448 - 找到所有数组中消失的数字


文章目录

摘要

这题可以说是数组题里最典型的"原地标记法"应用之一。题目要求我们找出 1...n 中没出现过的数字,而且还给了个进阶要求:在 O(n) 时间、O(1) 额外空间内完成

听起来好像有点限制,但只要利用"数组本身可作为标记空间"的特性,就能用非常干净利落的方式解决。这类技巧专项训练非常重要,因为真实开发中有很多场景都可以借鉴这种思路。

描述

输入是一个长度为 n 的数组,里面的每个数字都在 [1, n] 区间内。我们要返回所有没有出现在数组里的数字。

比如:

txt 复制代码
nums = [4,3,2,7,8,2,3,1]
缺失的是:5 和 6

或者:

txt 复制代码
nums = [1,1]
缺失的是:2

进阶要求我们不能使用额外空间(返回结果除外),也就是说不能随便用哈希表、map 之类的辅助结构。

题解答案(直觉方法)

利用这样一个事实:

数组中的值都在 [1, n] 范围内,那么每个值 x 对应数组位置 x-1 必定存在。

我们可以利用"把出现过的数字对应的位置标记为负数"这种方式,把数组本身变成一个"出现记录表"。

整体思路:

  1. 遍历数组,对于每个数字 x

    • 我们让数组中 x-1 的位置变成负数(如果还没负)
  2. 遍历标记后的数组

    • 所有仍然是正数的位置 i,说明数字 i+1 没出现过

这是典型的"使用符号作为标记"的技巧。

题解代码(Swift 可运行 Demo)

swift 复制代码
import Foundation

class Solution {
    func findDisappearedNumbers(_ nums: [Int]) -> [Int] {
        var nums = nums  // 复制一份,便于原地修改
        let n = nums.count
        
        // 第一步:把出现过的数字对应位置标记成负数
        for i in 0..<n {
            let index = abs(nums[i]) - 1
            if nums[index] > 0 {
                nums[index] = -nums[index]
            }
        }
        
        // 第二步:正数的位置就是缺失的数字
        var result = [Int]()
        for i in 0..<n {
            if nums[i] > 0 {
                result.append(i + 1)
            }
        }
        
        return result
    }
}

// MARK: - Demo

let solution = Solution()
print("示例 1:", solution.findDisappearedNumbers([4,3,2,7,8,2,3,1]))  // [5, 6]
print("示例 2:", solution.findDisappearedNumbers([1,1]))              // [2]

题解代码分析

为什么要用"负数标记"?

这是这题最精妙的地方。

  • 数组长度为 n
  • 所有数字范围都在 1...n

那么数字和索引之间刚好可以一一映射。

例子:

txt 复制代码
数字 4 ------ 对应索引 3
数字 1 ------ 对应索引 0
数字 7 ------ 对应索引 6

利用这点,我们可以把"某个数字出现过"这件事记录到 nums[x-1] 中。

最简单的标记方式就是:

把 numsx-1 变成负数

负数代表"已经被访问过了"。

为什么不直接改成 0 或者某个标志值?

因为之后我们仍然需要判断原本的值,而负数保留了数值本身的信息,且不会和 [1,n] 冲突。

为什么需要用 abs(nums[i])

因为标记过程中,有些位置已经变成负数了,所以必须取绝对值。

第二轮遍历为什么能找到缺失值?

因为:

  • 出现过的数字对应位置已经被标记为负数
  • 没出现过的位置仍然是正数

所以:

txt 复制代码
第 i 个位置是正数 → i + 1 没出现过

就是缺失值。

示例测试及结果

我们用题目给的例子跑一下:

示例 1

txt 复制代码
输入: [4,3,2,7,8,2,3,1]
中间标记过程如下:
原始:   4  3  2  7  8  2  3  1
标记后: -4 -3 -2 -7  8 -2 -3 -1
                ↑  ↑
                正  正
缺失数字为:[5, 6]

示例 2

txt 复制代码
输入: [1,1]
标记后:[-1, 1]
缺失的是 2

运行 Demo 即可得到相同结果。

时间复杂度

O(n)

  • 一次遍历标记出现过的数字
  • 一次遍历找出没被标记的位置
  • 两次线性扫描,总共 O(n)

在大数据量下仍然非常稳定。

空间复杂度

O(1) 额外空间

注意:返回的结果数组不算额外空间。

其余操作都在原数组上原地完成,不需要新开结构。

总结

这题真正想让你掌握的是:

"用符号(正负)作为额外标记信息"的技巧

它在很多 O(1) 空间的题里都能出现,比如:

  • 找重复数字
  • 找缺失数字
  • 数组原地哈希
  • 原地 bucket 标记

在实际业务逻辑中,这种技巧也能用于:

  • 用户状态表中"是否访问过"的标记(即使在共享数据结构中)
  • 工具链中处理序列时进行轻量级标记
  • 内存敏感场景的原地修改技巧
相关推荐
CHHH_HHH32 分钟前
【C++11】深入解析C++可变参数模板
开发语言·c++·算法·stl·c++11
hurrycry_小亦33 分钟前
洛谷题目:P1215 [USACO1.4] 母亲的牛奶 Mother‘s Milk 题解(本题简)
算法
KJ_BioMed1 小时前
从PDB到高亲和力分子:De novo生成式计算化学Pipeline剖析
算法·生物医药·生物科研·科研干货·化合物设计
一个王同学2 小时前
从零到一 | CV转多模态大模型 | week17 | LLM 推理优化 & vLLM 详解
人工智能·深度学习·算法·机器学习·计算机视觉·vllm
旖-旎4 小时前
《LeetCode 53 最大子数组和 || LeetCode 918 环形子数组的最大和》
c++·算法·leetcode·动态规划
变量未定义~4 小时前
单调栈+倍增思想 皇家守卫【算法赛】、单调队列 附近最小
算法
QN1幻化引擎4 小时前
给 AI 做一次「意识体检」——基于 QN1 幻化引擎的灵鉴意识识别框架与 DalinX V5 实测
大数据·数据结构·人工智能·算法·架构
拂拉氏4 小时前
【知识讲解】 AVL树从基本成员的介绍到核心接口的实现(插入、判断、删除等等)
数据结构·算法·avl树
可靠的仙人掌5 小时前
SAC(Soft Actor-Critic)算法底座
开发语言·算法·php
c238565 小时前
《动态规划:从“傻傻穷举”到“过目不忘”的修仙之路》
c++·算法·动态规划