听风是风

学或不学,知识都在那里,只增不减。

导航

JS leetcode 两个数组的交集I II 合集题解分析

壹 ❀ 引

前些日子,在与博客园用户MrSmileZhu闲聊中,我问到了他先前在字节跳动面试中遇到了哪些算法题(又戳到了他的伤心处),因为当时面试的高度紧张,原题描述已经无法重现了,但大概与数组合并、求交集相关。比较巧的是我在今年年初有整理过一份数组常用操作的文章的JS 数组常见操作汇总,结果今天leetcode的每日打卡,也正好是求数组交集,此题一共有两题,我分别试了下,也看了其他用户思路,才发现原来花样有这么多,所以这篇文章是关于这两题的思路汇总。不积跬步,无以至千里;不积小流,无以成江海。那么本文开始。

贰 ❀ 两个数组的交集

此题来自leetcode349. 两个数组的交集,题目描述如下:

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]

说明:

输出结果中的每个元素一定是唯一的。
我们可以不考虑输出结果的顺序。

注意,在例子1中,随便两个数组都有两个2,但是认定交集只有一个2;结合说明中输出结果每个元素都是唯一,我们可知题目要求无非是两点。

  1. 这个元素一定是在nums1中与nums2中都有出现
  2. 不管出现几次,只要是同一个元素都认定为是一个元素。

贰 ❀ 壹 借用哈希表

所以我的想法是,以其中一个数组为遍历参照物,依次查找另一个数组中有没有当前元素,考虑要求2,所以我还需要一个额外哈希表,记录已经出现过的元素,那么直接上代码:

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    // 定义一个哈希表,记录出现过的元素
    let dic = {},
        ans = [];
    for (let i = 0, len = nums1.length; i < len; i++) {
        // 这个元素在两边数组都有出现,其次它还得是第一次向淮安
        if (nums2.includes(nums1[i]) && !dic[nums1[i]]) {
            // 记录出现过的每个元素
            dic[nums1[i]] = true;
            ans.push(nums1[i]);
        };
    };
    return ans;
};

思路很简单,只是额外加了一个哈希表用于缓存出现过的元素,比如示例1中的2,不管你出现几次,我只有将你第一次出现时加入进去。

贰 ❀ 贰 借用set元素独一特性

其实针对第一个例子,如果我们不用hash表保证元素第一次出现,就会出现元素满足几次就记录几次的问题,比如下面的代码:

var intersection = function (nums1, nums2) {
    return nums1.filter(item => nums2.includes(item)));
};

这个实现用于测试示例1,就会得到[2,2]。灵机一动,我们也可以在得到结果之后再加工啊,比如set结构不接受重复元素,所以有了如下实现:

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    return [...new Set(nums1.filter(item => nums2.includes(item)))];
};

我能想到的也就这两点了,其它的思路均来自leetcode用户秦时明月

贰 ❀ 叁 借用set结构

由于我们只是要找两个数组中都有的元素,且只记录第一次,我们完全可以用new Set过滤掉重复元素。

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    // 保证nums1是长度更小那个
    if (nums1.length > nums2.length) {
        [nums1, nums2] = [nums2, nums1];
    };
    let hash = new Set(nums1);
    let res = new Set();
    for (let i = 0; i < nums2.length; i++) {
        // 注意,由于是set结构,这里用has取代了数组的includes
        if (hash.has(nums2[i])) {
            res.add(nums2[i]);
        };
    };
    return [...res];
};

然而我看到这个思路,是这么想的:

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    if (nums1.length > nums2.length) {
        [nums1, nums2] = [nums2, nums1]
    };
    let hash1 = new Set(nums1);
    let hash2 = new Set(nums2);
    let res = new Set();
    for (let num of hash1) {
        if (hash2.has(num)) {
            res.add(num);
        };
    };
    return [...res];
};

我在上篇文章中就提出了一个疑问,数组的includes和set的has查找,到底谁更快,在知乎不精确测试中提到,当数据小于一万,has更快,大于一万时,includes更具优势,闲的无聊,我也做了一个测试,但事实证明不管数据如何,has查找似乎都比includes更具优势:

// 创建一个0-99999的数组
let arr = Array.from(Array(100000), (v, k) => k);
let set = new Set(arr);
// 数组测试,执行100次
console.time('arr');
for (let i = 0; i < 100; i++) {
    arr.includes(10000);
};
console.timeEnd('arr');
// set测试,执行100次
console.time('set');
for (let i = 0; i < 100; i++) {
    set.has(10000);
};
console.timeEnd('set');

大家可以修改数组长度与执行次数,以及需要查找的数,我多次刷新让大家看看时间对比

由于以上例子数组范围是[0,99999],如果我们将查找的数改为100000,由于不存在这个数,也就是每次查找都会从头找到尾,时间差异就更大了。

当然还有二分查找等其它思路,这里我不做一一记录,可点击上方秦时明月查看,关于本题先说到这里。

叁 ❀ 两个数组的交集 II

来看看题二,题二来源leetcode350. 两个数组的交集 II,题目描述如下:

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]

说明:

输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
我们可以不考虑输出结果的顺序。
进阶:

如果给定的数组已经排好序呢?你将如何优化你的算法?
如果 nums1 的大小比 nums2 小很多,哪种方法更优?
如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

与题一不同的是,题二要求,如果两个数组同时存在相同元素,即便元素已重复,仍认为是交集,比如实例1中双方都有两个2,所以输出了[2,2]

叁 ❀ 壹 我的思路,开心爱消除

按照常规的思路[2,2][2]由于2次对比都满足,会输出[2,2],而本题必须要数量上还对等,所以我将其理解成开心爱消除,找到一个消除一个,直接上代码:

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersect = function (nums1, nums2) {
    let ans = [];
    nums1.forEach((item, index) => {
        let sub = nums2.indexOf(item);
        if (sub > -1) {
            ans.push(item);
            // 找到1个删掉一个
            nums2.splice(sub, 1);
        }
    });
    return ans;
};

叁 ❀ 其它优秀思路

我们在题一中利用哈希证明元素是第一次出现,在这里,同理可以借用哈希,记录每个元素出现的次数,满足一次,让次数减一,其实还是同一个道理,这里借用leetcode用户天使爆破组思路:

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersect = function (nums1, nums2) {
    let ans = [];
    let hash = {};
    // 记录每个元素出现次数
    for (let num of nums1) {
        hash[num] ? ++hash[num] : hash[num] = 1;
    };
    // 遍历nums2看看有没有数字在nums1出现过
    for (let num of nums2) { 
        let val = hash[num];
        if (val) {
            ans.push(num); // 推入res数组
            --hash[num]; // 匹配掉一个,就少了一个
        };
    };
    return ans;
};

题目在进阶中,指出如果两数组已排好序如何优化,假设数组已排序,我们大可使用双指针来解决这个问题,大致图示为:

直接上代码:

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersect = function (nums1, nums2) {
    // 先排序,才好使用双指针
    nums1.sort((a, b) => a - b);
    nums2.sort((a, b) => a - b); 
    const ans = [];
    let p1 = 0;
    let p2 = 0;
    while (p1 < nums1.length && p2 < nums2.length) {
        if (nums1[p1] > nums2[p2]) {
            p2++;
        } else if (nums1[p1] < nums2[p2]) {
            p1++;
        } else {
            ans.push(nums1[p1]);
            p1++;
            p2++;
        };
    };
    return ans;
};

那么到这里,本文结束。

posted on 2020-07-14 22:40  听风是风  阅读(969)  评论(0编辑  收藏  举报