

文章目录
摘要
这道题看起来很像"找重复数字"的基础题,但限制条件非常关键:
你必须在 O(n) 时间、O(1) 额外空间 内找出所有出现两次的数字。
因为 nums 的范围是 1 到 n ,数组长度也是 n ,这其实暗示了一个非常经典的技巧------"原地标记法"。
这个方法特别适合处理"范围可控且和数组下标对应"的问题。

描述
给你一个长度为 n 的数组 nums,并保证其中所有数字都在 1 到 n 的区间内。
每个数字最多出现两次,你需要找出所有 恰好出现两次 的数字。
例子:
txt
输入:[4,3,2,7,8,2,3,1]
输出:[2,3]
要求:
- 不能用额外的数组(O(1) 空间)
- 必须 O(n) 时间
题解答案
核心思路是:
利用元素与下标一一对应的关系,通过"取负号"标记已经出现过的数字。
做法:
- 遍历数组,对每个数字 x,我们取其对应下标
abs(x) - 1 - 如果对应位置 nums[index] 是 正数 → 说明第一次遇到,将其置为负数(标记已出现)
- 如果对应位置 nums[index] 是 负数 → 第二次遇到,加入答案
整个过程无需额外空间,因为我们直接在数组本体做标记。

可运行 Demo 代码(Swift)
以下代码可以直接在 Xcode / Swift Playground 运行:
swift
import Foundation
class Solution {
func findDuplicates(_ nums: [Int]) -> [Int] {
var nums = nums // 需要可变数组
var result: [Int] = []
for i in 0..<nums.count {
let index = abs(nums[i]) - 1 // 映射到 0-based 下标
if nums[index] < 0 {
// 第二次遇到
result.append(index + 1)
} else {
// 第一次遇到,取负号标记
nums[index] = -nums[index]
}
}
return result
}
}
// Demo 测试
let solution = Solution()
let tests = [
[4,3,2,7,8,2,3,1],
[1,1,2],
[1],
[2,2],
[1,2,3,4,5]
]
for nums in tests {
print("输入: \(nums) → 输出: \(solution.findDuplicates(nums))")
}
题解代码分析
下面我们详细拆解一下关键点。
1. 为什么可以用"取负号标记"?
因为数字范围是:
1...n
对应下标是:
0...n-1
所以每个数字都能唯一映射到一个下标位置。
如果 nums[x-1] 被取过一次负数,则说明 ( x ) 已经出现过一次,再次遇到同样的数时,就能判断它是重复的。
2. 为什么不会越界?
因为题目保证:
txt
1 <= nums[i] <= n
取 abs(nums[i]) - 1 永远合法。
3. 为什么不会影响最终结果?
我们只要保证:
- 修改数组值不影响算法判断(我们每次都用 abs())
- 输出不依赖修改后的 nums
所以原地修改完全没问题。
4. 使用绝对值 abs()
因为被标记的数字已经变成负数,我们需要用 abs() 来找到它的初始值。
这是必须的,否则第二次访问时可能下标会错位。
示例测试及结果
运行 Demo 后,你会看到类似输出:
txt
输入: [4, 3, 2, 7, 8, 2, 3, 1] → 输出: [2, 3]
输入: [1, 1, 2] → 输出: [1]
输入: [1] → 输出: []
输入: [2, 2] → 输出: [2]
输入: [1, 2, 3, 4, 5] → 输出: []
你会发现:
- 出现两次的数成功被捕捉
- 没有重复的数组也能正常输出空数组
时间复杂度
算法只遍历一次数组:
txt
O(n)
不论输入大小如何,永远只做 n 次操作。
空间复杂度
除了输入数组本身(允许修改),我们只使用了:
- 一个结果数组(输出必须)
- 常量变量
额外空间为:
txt
O(1)
完全符合题目要求。
总结
这道题的最佳解法就是:
利用数组下标作为"哈希表",通过取负号做出现标记。
优势:
- 不用额外空间(O(1))
- 单次遍历(O(n))
- 技巧性非常强,是数组题的经典套路之一