力扣350.两个数组的交集II

力扣350.两个数组的交集II

题目:两个数组的交集 II(返回元素出现次数的交集)

题目描述)

给你两个整数数组 nums1nums2,以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应当等于该元素在两个数组中出现次数的较小值。结果顺序不限。

示例:

  • 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 的元素存储在磁盘上,内存有限,不能一次加载所有元素怎么办?

这是典型的外存/流式问题,常见解决策略:

  1. 如果 nums1 可放入内存 :把 nums1 建成哈希表(或计数表),然后流式逐块读取 nums2(比如分块读取),对每块中的元素与哈希表匹配并记录结果,写出或累积结果。这只需要哈希表的内存(与 nums1 成正比)。

  2. 如果两个数组都太大、都不能一次加载:使用外部排序(external sort)或哈希分区(hash-based partitioning)。

    • 外部排序 + 归并:对磁盘上的数据做外部排序(分块排序然后归并),排序后用归并式扫描(双指针)得到交集。
    • 哈希分区:对两个数组按相同哈希函数分成多个块,使得每一对对应分区都能放入内存,然后分别加载对应分区做内存内的哈希或排序对比。这个方法可以并行化,常用于大数据场景。
  3. 位图/布隆过滤器(仅适用于值域小或允许小概率误判的场景):如果值域非常小,可以用位图;若允许误判,可以用布隆过滤器先过滤候选,然后进一步确认。


边界和实现细节(工程注意事项)

  • 返回结果的 顺序不重要,因此你可以按照任意顺序生成结果。
  • 当题目没有给出值域限制时,优先选择哈希方案;当值域很小且固定时,计数数组更快且空间可控。
  • 在实现时,若需要节省空间,应优先把较短的数组作为构建哈希/计数的对象。
  • 如果使用排序方法并需要保持原数组不变,先复制数组再排序。

小结(推荐)

  • 一般场景:哈希计数 (对较短数组建表)是最稳妥的选择,时间 O(n+m),实现简单。
  • 值域受限:计数数组 最快且常数开销小。
  • 已排序或可承受排序开销:排序 + 双指针 很简洁且空间低。
  • 大规模外存场景:考虑 外部排序哈希分区

相关推荐
rzjslSe2 小时前
【JavaGuide学习笔记】理解并发(Concurrency)与并行(Parallelism)的区别
java·笔记·学习
青柠编程3 小时前
基于Spring Boot的竞赛管理系统架构设计
java·spring boot·后端
꒰ঌ 安卓开发໒꒱3 小时前
Java面试-并发面试(二)
java·开发语言·面试
Mr.Aholic3 小时前
Java系列知识之 ~ Spring 与 Spring Boot 常用注解对比说明
java·spring boot·spring
月夕·花晨4 小时前
Gateway-断言
java·开发语言·分布式·spring cloud·微服务·nacos·sentinel
西贝爱学习4 小时前
【JDK 11 安装包免费下载 免登录Oracle 】jdk11与jdk8有什么区别?
java·开发语言
元亓亓亓4 小时前
LeetCode热题100--994. 腐烂的橘子--中等
算法·leetcode·职场和发展
(●—●)橘子……4 小时前
记力扣2516.每种字符至少取k个 练习理解
算法·leetcode·职场和发展
宠友信息5 小时前
类似小红书垂直社区APP小程序源码
java·开发语言·微信小程序·小程序·uni-app·开源·web app