壹 ❀ 引
前些日子,在與博客園用戶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;
};
那么到這里,本文結束。
