壹 ❀ 引
前些日子,在与博客园用户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;结合说明中输出结果每个元素都是唯一,我们可知题目要求无非是两点。
- 这个元素一定是在nums1中与nums2中都有出现
- 不管出现几次,只要是同一个元素都认定为是一个元素。
贰 ❀ 壹 借用哈希表
所以我的想法是,以其中一个数组为遍历参照物,依次查找另一个数组中有没有当前元素,考虑要求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;
};
那么到这里,本文结束。