两个数组的交集 II 图解题解
这道题到底在问什么
- 输入
- nums1=[4,9,5,9], nums2=[9,4,9,8,4,5]
- 输出
- [9,4,9,5]
最优解:为什么这么做
一句话答案:LeetCode 350 两个数组的交集 II 用哈希计数:先把 nums1 的每个数记进计数表,再扫 nums2 命中就取一个、计数减 1,重复次数天然带上,时间 O(n+m)、空间 O(n)。
结果该留几个 9,带重复的交集
给两个数组 nums1、nums2,返回交集,但和普通求交不一样:一个数在两边都出现,结果要保留它在两数组中出现次数的较小值。题面 nums1=[4,9,5,9]、nums2=[9,4,9,8,4,5],9 在两边各出现 2 次,结果就带 2 个 9;4 和 5 各 1 次,各留 1 个,输出 [9,4,9,5]。
回 nums1 线扫配对,为什么撑不住
拿 nums2 里每个数,回 nums1 从头找一个还没被认领的相同值,配上就把那个位置划掉。nums1 有 n 个数、nums2 有 m 个,每次配对都可能扫遍 nums1,最坏 O(n·m)(大 O 记号,记操作量随规模的放大),两数组一大就吃力。有人想转成集合求交、一行搞定,可去重后 9 只剩一个,丢掉「9 出现两次」这层重复,本题偏要带重复,也不对。
只要知道每个数还剩几个能配
慢就慢在每次都回 nums1 线扫,其实只要知道 nums1 里每个数还剩几个能配就够了。把 nums1 过一遍,用一张计数表(哈希表,键是数值、值是它在 nums1 出现的次数)记下每个数出现几次。为什么记次数、不只记「出现过」——交集要保留较小的出现次数,9 在 nums1 有 2 个,最多配出 2 个 9;只记「出现过」等于把次数拍成 1,多出来那个 9 就配不上。
扫 nums2 配一个、扣一个名额
接着扫 nums2,每个数 x 查 cnt[x]:大于 0 说明 nums1 还留着一个没配的 x,收进答案,再把 cnt[x] 减 1,用掉一个名额;等于 0 说明没有或已配完,跳过不收。减 1 是重复次数不出错的关键——收一个扣一个,同一个 x 在 nums2 出现多次时,只能配到 nums1 里剩的那几个,扣光就再也配不上。扫完 nums2 就得到答案。
nums1=[4,9,5,9] 配 nums2,逐个查表
拿 nums1=[4,9,5,9]、nums2=[9,4,9,8,4,5] 走一遍。先建表:扫 nums1 得 cnt={4:1, 9:2, 5:1}。再扫 nums2。第一个 9,cnt[9]=2>0,收下、cnt[9] 减成 1。第二个 4,cnt[4]=1>0,收下、cnt[4] 减成 0。第三个 9,cnt[9]=1>0,收下、cnt[9] 减成 0。第四个 8,cnt[8]=0,跳过;第五个 4,cnt[4] 已是 0,跳过。第六个 5,cnt[5]=1>0,收下、cnt[5] 减成 0。答案 [9,4,9,5]:9 配 2 个、4 和 5 各 1 个,正是两边较小的出现次数。
减 1 那步漏掉,重复的数被反复领走
建表扫 nums1、配对扫 nums2,两趟都是线性,时间 O(n+m);计数表只装 nums1 出现过的数,空间 O(n)。配对后漏了 cnt[x] 减 1,同一个名额会被 nums2 里重复的数反复领走,答案多出一堆重复。用集合去重再求交能糊弄过「交集 I」,这题会把重复全抹平、9 只剩一个。边界先想清:两数组毫无公共值时答案为空;有一个数组为空时交集必空;两数组相同时结果就是整份数组。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住口诀:nums1 建计数表,nums2 来配对,配一个扣一个。下面每帧都在套它。
- 4先把 nums1 做成计数表。现在读到 nums1[0] = 4,下一帧把它的计数加 1。
- 54 的计数更新成 1。计数表记的是「这个数在 nums1 里还剩几次能配」。
- 6先把 nums1 做成计数表。现在读到 nums1[1] = 9,下一帧把它的计数加 1。
- 79 的计数更新成 1。计数表记的是「这个数在 nums1 里还剩几次能配」。
- 8先把 nums1 做成计数表。现在读到 nums1[2] = 5,下一帧把它的计数加 1。
- 95 的计数更新成 1。计数表记的是「这个数在 nums1 里还剩几次能配」。
- 10先把 nums1 做成计数表。现在读到 nums1[3] = 9,下一帧把它的计数加 1。
- 119 的计数更新成 2。计数表记的是「这个数在 nums1 里还剩几次能配」。
- 12nums1 的计数表全部建好了。接下来扫 nums2,每个数来表里「认领」名额。
- 13扫到 nums2[0] = 9,去计数表里查它还剩几个名额:cnt[9] = 2。
- 14cnt[9] 原来大于 0,说明 nums1 那边还留着一个 9,配上!收进答案,并把 cnt[9] 扣成 1。
- 15扫到 nums2[1] = 4,去计数表里查它还剩几个名额:cnt[4] = 1。
- 16cnt[4] 原来大于 0,说明 nums1 那边还留着一个 4,配上!收进答案,并把 cnt[4] 扣成 0。
- 17扫到 nums2[2] = 9,去计数表里查它还剩几个名额:cnt[9] = 1。
- 18cnt[9] 原来大于 0,说明 nums1 那边还留着一个 9,配上!收进答案,并把 cnt[9] 扣成 0。
- 19扫到 nums2[3] = 8,去计数表里查它还剩几个名额:cnt[8] = 0。
- 20cnt[8] 已经是 0,nums1 那边没有多余的 8 了,跳过这个,不进答案。
- 21扫到 nums2[4] = 4,去计数表里查它还剩几个名额:cnt[4] = 0。
- 22cnt[4] 已经是 0,nums1 那边没有多余的 4 了,跳过这个,不进答案。
- 23扫到 nums2[5] = 5,去计数表里查它还剩几个名额:cnt[5] = 1。
- 24cnt[5] 原来大于 0,说明 nums1 那边还留着一个 5,配上!收进答案,并把 cnt[5] 扣成 0。
- 25nums2 全部扫完,答案是 [9,4,9,5]。9 配到 2 个、4 和 5 各 1 个,正好是两边出现次数的较小值。
⚠️ 容易写错的地方
✗ 错:用集合 set 去重再交
✓ 对:用计数表保留重复次数
set 会丢掉「9 出现两次」这种重复,本题要带重复
✗ 错:配对后忘了把计数减 1
✓ 对:收入答案就 cnt[v] 减 1
不减会让同一个名额被反复领取,结果偏多
✗ 错:以为必须挑某个数组才对
✓ 对:本题固定统计 nums1 就对
统计哪个数组都能得正确答案;想更省空间可改统计较短数组,属可选优化
完整代码(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 intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
cnt = Counter(nums1)
ans = []
for x in nums2:
if cnt[x]:
ans.append(x)
cnt[x] -= 1
return ansC++
#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:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> cnt;
for (int x : nums1) {
++cnt[x];
}
vector<int> ans;
for (int x : nums2) {
if (cnt[x]-- > 0) {
ans.push_back(x);
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
int[] cnt = new int[1001];
for (int x : nums1) {
++cnt[x];
}
List<Integer> ans = new ArrayList<>();
for (int x : nums2) {
if (cnt[x]-- > 0) {
ans.add(x);
}
}
return ans.stream().mapToInt(Integer::intValue).toArray();
}
}复杂度
时间
O(n + m)
建表扫 nums1,配对扫 nums2,各一遍
空间
O(n)
计数表存 nums1 里出现的数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 两个数组的交集 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
两个数组如果已经排好序,还用得着哈希表吗?+
用不着,排好序后更省:两个指针分别从 nums1、nums2 开头走,谁指的数小谁往后挪一格,相等就同时收进答案、两个指针一起前进。因为有序,相等的数会在两边同一段撞上,重复次数自然对齐,全程只扫一遍、不额外开哈希表,空间能压到 O(1)(不算答案本身)。哈希计数胜在无序也能做,双指针胜在有序时省掉那张表。
如果 nums2 是磁盘上的超大文件、内存装不下,怎么配对?+
把小的那个数组(比如 nums1)建成计数表放进内存,大文件那边流式逐块读、边读边查表配对:某个数 cnt 大于 0 就收进答案并减 1。这样内存只和小数组大小相关,和大文件多大无关,是面试里常见的「大文件求交集」追问。反过来若两个都是大文件,就先各自外部排序,再用双指针归并。
这题和「两个数组的交集 I」差在哪,能套同一份代码吗?+
不能直接套。交集 I 不带重复,结果里每个公共数只出现一次,用两个集合求交、或一个集合加一次去重就行;这题交集 II 要保留较小出现次数,必须用计数表逐个配对、配一个扣一个。识别的关键就一句:结果要不要带重复——要,就上计数表;不要,集合去重更省事。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 两个数组的交集 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。