力扣350.两个数组的交集II
题目:两个数组的交集 II(返回元素出现次数的交集)
题目描述)
给你两个整数数组 nums1
和 nums2
,以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应当等于该元素在两个数组中出现次数的较小值。结果顺序不限。
示例:
nums1 = [1,2,2,1], nums2 = [2,2]
→ 输出[2,2]
nums1 = [4,9,5], nums2 = [9,4,9,8,4]
→ 输出[4,9]
(顺序可变)
约束:
1 <= nums1.length, nums2.length <= 1000
0 <= nums[i] <= 1000
方法一:哈希表计数(推荐 --- 常用、简洁)
思路
用哈希表(或数组计数)统计 nums1
中每个元素的出现次数,然后遍历 nums2
,当某元素在哈希表中计数 > 0 时,将该元素加入结果并将计数减 1。为节约空间,最好对较短的数组建表。
复杂度
- 时间:
O(n + m)
(n = nums1.length, m = nums2.length) - 空间:
O(min(n, m))
(用于哈希表)
代码(Java)
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
// 确保 nums1 是较短的那个数组,以减少哈希表空间
if (nums1.length > nums2.length) {
return intersect(nums2, nums1);
}
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums1) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
List<Integer> resList = new ArrayList<>();
for (int num : nums2) {
Integer c = count.get(num);
if (c != null && c > 0) {
resList.add(num);
count.put(num, c - 1);
}
}
// 转换为 int[]
int[] res = new int[resList.size()];
for (int i = 0; i < res.length; i++) res[i] = resList.get(i);
return res;
}
}
方法二:排序 + 双指针(适合已排序数组或内存充足时)
思路
先对两个数组排序,然后用两个指针同时从头遍历:当 a[i] == b[j]
,将该元素加入结果并 i++, j++;如果 a[i] < b[j]
,i++,否则 j++。这是经典的归并式扫描。
复杂度
- 时间:
O(n log n + m log m)
(排序时间) - 空间:
O(1)
或O(k)
(结果数组),若排序需额外空间则视排序实现而定
代码(Java)
java
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int i = 0, j = 0;
List<Integer> resList = new ArrayList<>();
while (i < nums1.length && j < nums2.length) {
if (nums1[i] == nums2[j]) {
resList.add(nums1[i]);
i++; j++;
} else if (nums1[i] < nums2[j]) {
i++;
} else {
j++;
}
}
int[] res = new int[resList.size()];
for (int k = 0; k < res.length; k++) res[k] = resList.get(k);
return res;
}
}
方法三:计数数组(当值域有限时非常高效)
思路
题目给出的约束 0 <= nums[i] <= 1000
表明值域较小,可以直接用长度为 1001 的计数数组统计出现次数,比 HashMap
更省常数开销、更快。
复杂度
- 时间:
O(n + m + R)
(R = 值域大小,通常为常数) - 空间:
O(R)
,在本题中R = 1001
,为常数
代码(Java)
java
import java.util.ArrayList;
import java.util.List;
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
int MAXV = 1000; // 根据题目约束,数值在 0..1000
int[] cnt = new int[MAXV + 1];
// 统计 nums1
for (int v : nums1) cnt[v]++;
List<Integer> ans = new ArrayList<>();
// 遍历 nums2,取最小出现次数
for (int v : nums2) {
if (cnt[v] > 0) {
ans.add(v);
cnt[v]--;
}
}
int[] res = new int[ans.size()];
for (int i = 0; i < res.length; i++) res[i] = ans.get(i);
return res;
}
}
进阶问题与优化思路
问题 1:如果给定的数组已经排好序,该如何优化?
答案 :排序好的情况下直接使用 双指针法(方法二) ,时间 O(n + m)
,空间 O(1)
(不计结果数组)。如果数组已排序且你想节省额外拷贝,直接双指针扫描是最优。
问题 2:如果 nums1
的大小比 nums2
小,哪种方法更优?
答案 :当其中一个数组明显更小(比如 nums1
很小),建议对更小的数组建立哈希表(或计数表),然后遍历较大的数组进行查找。原因:哈希/计数的空间与较小数组成正比,时间仍然 O(n + m)
,总体比对两个大数组排序更省时间。
也可以直接调用上面哈希实现中的交换逻辑(先确保对较短数组计数)------代码已经处理了这一点。
问题 3:如果 nums2
的元素存储在磁盘上,内存有限,不能一次加载所有元素怎么办?
这是典型的外存/流式问题,常见解决策略:
-
如果
nums1
可放入内存 :把nums1
建成哈希表(或计数表),然后流式逐块读取nums2
(比如分块读取),对每块中的元素与哈希表匹配并记录结果,写出或累积结果。这只需要哈希表的内存(与nums1
成正比)。 -
如果两个数组都太大、都不能一次加载:使用外部排序(external sort)或哈希分区(hash-based partitioning)。
- 外部排序 + 归并:对磁盘上的数据做外部排序(分块排序然后归并),排序后用归并式扫描(双指针)得到交集。
- 哈希分区:对两个数组按相同哈希函数分成多个块,使得每一对对应分区都能放入内存,然后分别加载对应分区做内存内的哈希或排序对比。这个方法可以并行化,常用于大数据场景。
-
位图/布隆过滤器(仅适用于值域小或允许小概率误判的场景):如果值域非常小,可以用位图;若允许误判,可以用布隆过滤器先过滤候选,然后进一步确认。
边界和实现细节(工程注意事项)
- 返回结果的 顺序不重要,因此你可以按照任意顺序生成结果。
- 当题目没有给出值域限制时,优先选择哈希方案;当值域很小且固定时,计数数组更快且空间可控。
- 在实现时,若需要节省空间,应优先把较短的数组作为构建哈希/计数的对象。
- 如果使用排序方法并需要保持原数组不变,先复制数组再排序。
小结(推荐)
- 一般场景:哈希计数 (对较短数组建表)是最稳妥的选择,时间
O(n+m)
,实现简单。 - 值域受限:计数数组 最快且常数开销小。
- 已排序或可承受排序开销:排序 + 双指针 很简洁且空间低。
- 大规模外存场景:考虑 外部排序 或 哈希分区。