Vvvrf / DSA.log← 返回全部笔记
LEETCODE 448Easy

找到所有数组中消失的数字

数值范围恰好等于下标范围,把数组本身当哈希桶,用正负号记录谁来过。

问题拆解

数组长度为 n,每个元素都落在 [1, n],可能有重复。要找出 [1, n] 里没在数组中出现过的所有数字。进阶要求不使用额外空间、时间 O(n)

用一个哈希集合记录出现过的数字,再遍历 1..n 查缺,逻辑很直接,但要额外的 O(n) 空间。关键的观察是:数值范围 [1, n] 和下标范围 [0, n-1] 一一对应,数字 v 天然可以映射到下标 v - 1。既然这样,就不必再借外部容器——数组自己就是一张现成的哈希表。

用下标充当哈希桶,把“某个数出现过”的信息编码进那个位置元素的正负号里。

原地哈希:标负法

第一遍遍历:对每个值 v,找到它对应的下标 abs(v) - 1,把那个位置的元素改成负数,表示“v 来过”。第二遍遍历:仍然是正数的下标,说明对应的数字从没被标记过,也就是缺失的数字。

def findDisappearedNumbers(nums):
    for x in nums:
        idx = abs(x) - 1
        if nums[idx] > 0:
            nums[idx] = -nums[idx]
    return [i + 1 for i, x in enumerate(nums) if x > 0]

有两个细节要留意。取下标时必须用 abs(x),因为 x 可能在更早的步骤里已经被标成了负数,它代表的原始数值仍是 abs(x)。判断 nums[idx] > 0 再取反,是为了让重复出现的数字不会把负号又翻回正号。

第二遍里,下标 i 处仍为正,意味着从没有哪个元素映射到 i,于是 i + 1 就是一个缺失的数。整个过程只在原数组上翻转符号,没有申请新空间。

复杂度

指标 复杂度 原因
时间 O(n) 两次线性遍历
空间 O(1) 复用原数组,返回值不计入

可以迁移的模式

  • 数值范围和下标范围能对上,可以拿下标当哈希键;
  • 允许临时“污染”原数组(改符号、加偏移),事后还能还原本值;
  • 想把计数或存在性判断的空间压到常数。

同样的原地哈希思路,也能解“找重复数字”“缺失的第一个正数”这类值域受限的题。