我堅信很多開發者依舊與這些基本的全域性物件打交道:numbers,strings,objects,arrays 和 booleans。
大部分業務場景,以上這些已經夠用了。但是,如果你想讓你的程式碼執行的儘可能快、可擴充套件性儘可能的好,那麼這些基本型別並不夠優秀。
在這篇文章,我們將要討論如何利用 JS 的 Set
物件讓你的程式碼執行的更快——尤其是在它所處理的資料量大的時候。Array
和 Set
在處理資料時,兩則有太多的相似。但是使用 Set
所帶來的執行時優勢,是 Array
無法完成的。
Set
有何不同?
根本的區別就是 Array
是 索引集合(index collection)
。這意味著,資料的值是以 索引(index)
排序的。
const arr = [A, B, C, D];
console.log(arr.indexOf(A)); // Result: 0
console.log(arr.indexOf(C)); // Result: 2
複製程式碼
而 Set
則是 鍵集合(keyed collection)
。相比使用 索引
,Set
使用 鍵
來組織它的資料。一個 Set
中所有項都是按插入順序可迭代的,它不會有重複值。換句話說,Set
中的每一項都是獨一無二的。
最主要的收益是什麼?
Set
相比 Array
有些優勢,特別是考慮到需要更快的執行時間:
- 查詢項: 使用
indexOf()
或includes()
去檢查一個項是否在陣列中很慢。 - 刪除項: 在
Set
中,你可以使用值
去刪除一項。而在Array
中,相同的功能需要使用項的索引
使用splice()
方法。使用索引
是很慢的 - 插入項: 在
Set
中新增一項比Array
使用push()
或者unshift()
等方法新增一項要快的多。 - 排序
NaN
值: 你無法使用Array
的indexOf()
或者includes()
去定位NaN
值,但是Set
可以並且能夠儲存這個值 - 去重:
Set
物件只儲存獨一無二的值,如果你想避免儲存重複值,這是比Array
更好的選擇,因為使用Array
,你需要使用額外的程式碼去處理這種情況。
Note: 更多
Set
內建方法,請查閱 MDN Web Docs
什麼是時間複雜度?
使用 Array
去查詢是一個為 O(N)
的線性時間複雜度。換句話說,隨著資料量的提高,執行時間隨著增加。
相比而言,使用 Set
去查詢,不管是刪除還是插入的時間複雜度都僅僅是 O(1)
——這意味著,執行時間不隨著數量的提高而增加。
Note: 想要了解更多關於時間複雜度的內容,請查閱我的文章 Understanding Big O Notation
那麼 Set 究竟有多快呢?
雖然執行時間受使用的作業系統、資料的大小和其它的一些變數的影響,我希望我的測試結果能讓你對 Set
的速度有個直觀的感受。
準備測試
在開始執行之前,我們簡單的將 Array
和 Set
填充 1000000
個值(0~999999)
let arr = [], set = new Set(), n = 1000000;
for (let i = 0; i < n; i++) {
arr.push(i);
set.add(i);
}
複製程式碼
測試1:查詢
查詢值 123123
:
let result;
console.time('Array');
result = arr.indexOf(123123) !== -1;
console.timeEnd('Array');
console.time('Set');
result = set.has(123123);
console.timeEnd('Set');
複製程式碼
Array
: 0.173msSet
: 0.023msSet
快了 7.54 倍
測試2: 新增
新增一個值,變數為 n
:
console.time('Array');
arr.push(n);
console.timeEnd('Array');
console.time('Set');
set.add(n);
console.timeEnd('Set');
複製程式碼
Array
: 0.018msSet
: 0.003msSet
快了 6.73 倍
測試3:刪除
最後,我們刪除一項(就刪除我們剛新增的)。因為 Array
沒有原生刪除方法,我們寫一個 helper
來完成這個功能:
const deleteFromArr = (arr, item) => {
let index = arr.indexOf(item);
return index !== -1 && arr.splice(index, 1);
};
複製程式碼
進行我們的測試:
console.time('Array');
deleteFromArr(arr, n);
console.timeEnd('Array');
console.time('Set');
set.delete(n);
console.timeEnd('Set');
複製程式碼
Array
: 1.122msSet
: 0.015ms- 這一次,
Set
快了 74.13 倍!
總體來說,我們可以看到在執行時間上,Set
相比 Array
優勢巨大。現在我們來看看 Set
的一些實踐:
用例1: 陣列去重
如果你想要在 Array
中快速去重,你可以將它轉為 Set
。這是目前為止最簡潔的方法。
const duplicateCollection = ['A', 'B', 'B', 'C', 'D', 'B', 'C'];
// 如果你想把 Array 轉成 Set
let uniqueCollection = new Set(duplicateCollection);
console.log(uniqueCollection) // Set(4) {"A", "B", "C", "D"}
// 如果你想讓你的值仍是 `Array`
let uniqueCollection = [...new Set(duplicateCollection)];
console.log(uniqueCollection) // ["A", "B", "C", "D"]
複製程式碼
用例2:谷歌面試題
在我的另一篇文章中,我為谷歌面試官的一個問題討論了一些解決方案。面試是使用 C++
,但是如果是 JS
,Set
會是最終解決方案的關鍵點。
如果你想要更深入瞭解這些解決方案,我推薦閱讀原文,但是這裡,我簡單的介紹一下解決方案。
問
給一個未排序的整數陣列和一個值 sum
,如果陣列中任意兩項相加等於 sum
,則返回 true
,否則返回 false
。
如給定陣列 [3, 5, 1, 4]
和值 9
,我們的方法應該返回 true
,因為 4 + 5 = 9
。
答
這裡解釋思路,不翻譯了,看程式碼就能懂。
const findSum = (arr, val) => {
let searchValues = new Set();
searchValues.add(val - arr[0]);
for (let i = 1, length = arr.length; i < length; i++) {
let searchVal = val - arr[i];
if (searchValues.has(arr[i])) {
return true;
} else {
searchValues.add(searchVal);
}
};
return false;
};
複製程式碼
更簡潔的版本:
const findSum = (arr, sum) => arr.some((set => n => set.has(n) || !set.add(sum - n))(new Set));
複製程式碼
因為 Set.prototype.has()
時間複雜度只有 O(1)
, 使用 Set
儲存資料,結合 Array
的迴圈,我們最終的時間複雜度為 O(N)
。
如果我們依賴 Array.prototype.indexOf()
或 Array.prototype.includes()
,而兩者的時間複雜度都是 O(N)
, 我們最終的時間複雜度會達到 O(N²)
。太慢了!
希望本文對你有所幫助!