LeetCode 442中等数组原地哈希
数组中重复的数据 图解题解
这道题到底在问什么
给定整数数组 nums,所有元素都在 1 到 n 之间,每个数出现一次或两次。返回所有出现两次的数字。要求 O(n) 时间、O(1) 额外空间。
- 输入
- nums=[4,3,2,7,8,2,3,1]
- 输出
- [2,3] (2 和 3 各出现两次)
- 输入
- nums=[1,1,2]
- 输出
- [1]
最优解:一步一步想明白
- 3记牢这套「取绝对值 → 看 x-1 格子 → 负则重复、正则翻负打卡」,下面每一帧都在套它。
- 4开局:数组全是正数,没有任何打卡。值域 1 到 8 正好对应下标 0 到 7,这是整套技巧成立的前提。
- 5先看懂动作:读到值 4,就跑去下标 3(值 4 的「专属格子」),等下把那里翻成负号当打卡记号。每个值都去敲自己对应的那一格。
- 6轮到第 0 位,值是 4,所以 x=4,跳去看它的专属格子 下标 3。
- 7下标 3 还是正的,第一次来:把它翻成负号 -7(绿色)当打卡章。以后谁再来这格、看见负号就知道撞上重复了。
- 8轮到第 1 位,值是 3,所以 x=3,跳去看它的专属格子 下标 2。
- 9下标 2 还是正的,第一次来:把它翻成负号 -2(绿色)当打卡章。以后谁再来这格、看见负号就知道撞上重复了。
- 10轮到第 2 位,值是 -2(先前被翻过负,取绝对值),所以 x=2,跳去看它的专属格子 下标 1。
- 11下标 1 还是正的,第一次来:把它翻成负号 -3(绿色)当打卡章。以后谁再来这格、看见负号就知道撞上重复了。
- 12轮到第 3 位,值是 -7(先前被翻过负,取绝对值),所以 x=7,跳去看它的专属格子 下标 6。
- 13下标 6 还是正的,第一次来:把它翻成负号 -3(绿色)当打卡章。以后谁再来这格、看见负号就知道撞上重复了。
- 14轮到第 4 位,值是 8,所以 x=8,跳去看它的专属格子 下标 7。
- 15下标 7 还是正的,第一次来:把它翻成负号 -1(绿色)当打卡章。以后谁再来这格、看见负号就知道撞上重复了。
- 16轮到第 5 位,值是 2,所以 x=2,跳去看它的专属格子 下标 1。
- 17下标 1 的格子已经是负的(红色)——说明 2 早就打过卡了,这就是出现两次的数,收进答案:{ 2 }。
- 18轮到第 6 位,值是 -3(先前被翻过负,取绝对值),所以 x=3,跳去看它的专属格子 下标 2。
- 19下标 2 的格子已经是负的(红色)——说明 3 早就打过卡了,这就是出现两次的数,收进答案:{ 2, 3 }。
- 20轮到第 7 位,值是 -1(先前被翻过负,取绝对值),所以 x=1,跳去看它的专属格子 下标 0。
- 21下标 0 还是正的,第一次来:把它翻成负号 -4(绿色)当打卡章。以后谁再来这格、看见负号就知道撞上重复了。
- 22八个位置全扫完。回头看,凡是负号的格子都被打过卡,整张数组本身就当了哈希表用,没多花一格额外空间。
- 23揭晓:出现两次的是 2 和 3,答案 [2, 3],和开头说的一致。整趟只走了一遍数组、没开额外数组,时间 O(n)、空间 O(1)。
⚠️ 容易写错的地方
✗ 错:直接拿当前值当下标
✓ 对:先取绝对值再用
先前的打卡会把值翻成负数,不取绝对值会算错下标
✗ 错:看错符号的位置
✓ 对:判定看目标格子 nums[x-1] 的符号
不是看当前值的符号,两个位置别搞混
✗ 错:担心一个数被收多次
✓ 对:发现负号收一次即可
题目保证每数最多出现两次,所以只会撞上一次负号
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
ans = []
for x in nums:
idx = abs(x) - 1
if nums[idx] < 0:
ans.append(idx + 1)
else:
nums[idx] = -nums[idx]
return ansC++
#include <cstdlib>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> findDuplicates(vector<int>& nums) {
vector<int> ans;
for (int x : nums) {
int idx = abs(x) - 1;
if (nums[idx] < 0) ans.push_back(idx + 1);
else nums[idx] = -nums[idx];
}
return ans;
}
};Java
import java.util.*;
class Solution {
public List<Integer> findDuplicates(int[] nums) {
List<Integer> ans = new ArrayList<>();
for (int x : nums) {
int idx = Math.abs(x) - 1;
if (nums[idx] < 0) ans.add(idx + 1);
else nums[idx] = -nums[idx];
}
return ans;
}
}复杂度
时间
O(n)
从头到尾扫一遍,每个位置常数次操作
空间
O(1)
不算返回的答案,只在原数组翻符号,无额外结构
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 数组中重复的数据 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么能不用额外空间就统计出现次数?+
因为值域恰好是 1..n,和下标一一对应。于是把「数字 x 是否出现过」编码进「下标 x-1 处的符号」:正没出现过、负出现过。数组同时当数据和哈希表用,省掉计数数组。
这套「值即下标」的原地哈希还能解什么?+
一系列题:找缺失的数(lc448)、找那个重复又缺失的数(lc645)、找第一个缺失的正数(lc41)。共同点都是值域和下标对得上,可以把信息编码进位置或符号。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 数组中重复的数据 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。