在长度 2N 的数组中找出重复 N 次的元素 图解题解
这道题到底在问什么
- 输入
- nums=[1,2,3,3]
- 输出
- 3 (3 出现 2 次,其余各 1 次)
- 输入
- nums=[5,1,5,2,5,3,5,4]
- 输出
- 5 (5 出现 4 次)
先想最直接的笨办法
开局,准备一个空集合 seen,用来记录见过的数。指针从最左边开始,一个一个往右扫。(动画第 4 步)
最优解:为什么这么做
一句话答案:LeetCode 961 在长度 2N 的数组中找出重复 N 次的元素:只有这一个数会出现第二次,用哈希集合边扫边查,第一个已在集合里的就是答案,时间 O(n)、空间 O(n)。
长度 2N 的数组里,要揪出哪个数
给一个长度为 2N 的数组 nums,它由 N+1 个各不相同的数拼成:其中 N 个各只出现一次,剩下那一个恰好出现 N 次,要返回的就是这个重复 N 次的数。题面例子 nums=[1,2,3,3] 返回 3(3 出现 2 次,其余各 1 次);nums=[5,1,5,2,5,3,5,4] 返回 5(5 出现 4 次,别的数各 1 次)。
两两比一遍要 O(n²),数组一长就吃力
拿每个数往后面挨个比对,看有没有相同的,一撞上就是答案。数组有 2N 个数,两两比较最坏要比约 (2N)²/2 次,是 O(n²)。N 到几万,这个平方级的比对量就顶不住了,得想个只扫一遍的办法。
为什么第一次撞见重复,就能立刻收手
题目埋了一个很硬的保证:全场只有那个重复数会出现第二次,其余每个数都只露一次面。所以一旦发现某个值『之前见过』,它必然就是那个重复 N 次的数——不会是别人,因为别人根本没有第二次。既然如此,就不必真去数它出没出满 N 次,撞上第一次重复的那一刻直接返回即可。
剩下要解决的只是『之前见过吗』怎么查得快。用一个哈希集合 seen:往里丢值、问某个值在不在都快得几乎不花时间,边往右扫边把见过的数丢进去;每到一个数先问集合『你在里面吗』,在就返回它,不在就加进去接着走。
集合边扫边查,一次遍历怎么走完
备一个空集合 seen,指针从最左往右逐个走。对当前值 x:先查 x 是否已在 seen 里,是就直接返回 x(撞上了);否则把 x 加进 seen,走向下一个。因为重复数出现了 N(≥2)次,扫到它第二次露面时一定会命中,循环保证能返回,不会空手扫到底。
两个题面例子,各在第几个数撞上
先走 [1,2,3,3]。seen 空着,x=1 不在,加进去得 seen={1};x=2 不在,seen={1,2};x=3 不在,seen={1,2,3};最后一个 x=3,一查已经在 seen 里,返回 3。这题的重复数撞在末尾,几乎把数组扫满。
再走 [5,1,5,2,5,3,5,4]。x=5 不在,seen={5};x=1 不在,seen={5,1};第三个 x=5,查 seen 发现已经有了,立刻返回 5,后面的 2、5、3、5、4 根本没碰。同一段代码,撞早撞晚全由数据决定。
别真去数满 N 次,也别怕循环提前结束
复杂度上,最坏也只需从头扫到重复数第二次出现,约 N+2 个位置,一遍线性扫下来时间 O(n);集合里最多攒下那 N 个只出现一次的数,空间 O(n)。
最容易犯嘀咕的是以为非得统计每个数各出现几次、数到 N 才敢下结论——其实只有重复数会有第二次,任何一次重复都来自它,见到即答案。也有人担心循环没扫到底就 return 会漏掉什么,可题目保证这个重复数一定存在,扫到它第二次必然命中,C++、Java 里那种不写终止条件的 for 也会在撞上那刻收手,不会越界。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这套「没见过就放进集合,见过就是答案」,下面每一帧都在套它。
- 4开局,准备一个空集合 seen,用来记录见过的数。指针从最左边开始,一个一个往右扫。
- 5走到第 0 个,值 3。先去集合 seen 里查一查,之前见过 3 吗?
- 6集合里没有 3,这是第一次见到它。
- 7把 3 记进集合,继续往后走。
- 8走到第 1 个,值 1。先去集合 seen 里查一查,之前见过 1 吗?
- 9集合里没有 1,这是第一次见到它。
- 10把 1 记进集合,继续往后走。
- 11走到第 2 个,值 2。先去集合 seen 里查一查,之前见过 2 吗?
- 12集合里没有 2,这是第一次见到它。
- 13把 2 记进集合,继续往后走。
- 14走到第 3 个,值 4。先去集合 seen 里查一查,之前见过 4 吗?
- 15集合里没有 4,这是第一次见到它。
- 16把 4 记进集合,继续往后走。
- 17走到第 4 个,值 6。先去集合 seen 里查一查,之前见过 6 吗?
- 18集合里没有 6,这是第一次见到它。
- 19把 6 记进集合,继续往后走。
- 20走到第 5 个,值 8。先去集合 seen 里查一查,之前见过 8 吗?
- 21集合里没有 8,这是第一次见到它。
- 22把 8 记进集合,继续往后走。
- 23走到第 6 个,值 3。先去集合 seen 里查一查,之前见过 3 吗?
- 24集合里已经有 3 了!这说明 3 出现了第二次,正是那个重复 N 次的数。扫描到此结束,答案就是 3。
- 25回头数一数,3(绿色)一共出现了 5 次,正好是题目说的那个重复 N 次的数。哈希集合让我们在第一次撞上时就锁定了它,根本不用数到底。
⚠️ 容易写错的地方
✗ 错:以为要统计每个数出现几次、数到 N 才确定
✓ 对:第一次撞上就是答案,立刻返回
只有重复的数会出现第二次,任何一次重复都来自它,不必数满 N 次
✗ 错:担心循环没扫完就结束、会不会漏
✓ 对:题目保证一定存在这个重复数
C++、Java 的无限 for 一定会在撞上那一刻 return,不会越界
✗ 错:把 Java 的 add 返回值理解反
✓ 对:add 返回 true=新加入成功(没见过),false=已存在(撞上)
理解反会把第一次见到的数当成答案
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def repeatedNTimes(self, nums: List[int]) -> int:
s = set()
for x in nums:
if x in s:
return x
s.add(x)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int repeatedNTimes(vector<int>& nums) {
unordered_set<int> s;
for (int i = 0;; ++i) {
if (s.count(nums[i])) {
return nums[i];
}
s.insert(nums[i]);
}
}
};Java
import java.util.*;
class Solution {
public int repeatedNTimes(int[] nums) {
Set<Integer> s = new HashSet<>(nums.length / 2 + 1);
for (int i = 0;; ++i) {
if (!s.add(nums[i])) {
return nums[i];
}
}
}
}复杂度
时间
O(n)
最坏约扫到第 N+2 个元素(数组共 2N 个)就撞上
空间
O(n)
集合最多存下约 N 个不同的数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 在长度 2N 的数组中找出重复 N 次的元素 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
非要开集合吗?能不能不用额外空间做到 O(1)?+
能。有个可证明的性质:那个重复数的若干次出现里,一定存在某两次的下标距离不超过 3(注意是『存在』,不是任意两次都这么近)。所以枚举每个 i,只把 nums[i] 和后面三个位置 nums[i+1]、nums[i+2]、nums[i+3] 比一比,一旦相等就是答案,全程不开集合,空间 O(1)。为什么必然有两次靠得这么近:假如重复数每相邻两次出现都隔 ≥4 格,N 次出现至少占 4(N−1)+1 个位置,N≥2 时这就超过 2N、数组塞不下,矛盾。面试里先写哈希集合最直观最稳。
只出现一次的数,会不会也被当成答案返回?+
不会。集合的判定问的是『这个值之前加进去过没有』。只出现一次的数,扫到它时集合里绝不会有它自己,只会被顺手加进去、不触发返回;唯有那个出现 N(≥2)次的重复数,才会在第二次露面时被查到已存在。题目又保证有且只有一个数重复,所以第一个『查到已在集合里』的值必然就是答案,不存在误判。
Java 里想靠 add 的返回值判重,写法上要当心什么?+
HashSet 的 add 返回 true 表示这个数是新加进去的(之前没有),返回 false 表示集合里早就有了(撞上)。所以判重要写成『如果 add 返回 false 就 return 这个数』。这里容易理解反,把 true 当成撞上——那样会把第一次见到的数直接当答案返回,全错。Python 这边用的是 if x in s 先查再 s.add(x),语义更直白,不容易搞混。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 在长度 2N 的数组中找出重复 N 次的元素 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。