【譯】使用Set使你的程式碼執行更快

zhCN_超發表於2019-04-18

How to make your code faster using JavaScript Sets

我堅信很多開發者依舊與這些基本的全域性物件打交道:numbers,strings,objects,arrays 和 booleans。

大部分業務場景,以上這些已經夠用了。但是,如果你想讓你的程式碼執行的儘可能快、可擴充套件性儘可能的好,那麼這些基本型別並不夠優秀。

在這篇文章,我們將要討論如何利用 JS 的 Set 物件讓你的程式碼執行的更快——尤其是在它所處理的資料量大的時候。ArraySet 在處理資料時,兩則有太多的相似。但是使用 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: 你無法使用 ArrayindexOf() 或者 includes() 去定位 NaN 值,但是 Set 可以並且能夠儲存這個值
  • 去重: Set 物件只儲存獨一無二的值,如果你想避免儲存重複值,這是比 Array 更好的選擇,因為使用 Array,你需要使用額外的程式碼去處理這種情況。

Note: 更多 Set 內建方法,請查閱 MDN Web Docs

什麼是時間複雜度?

使用 Array 去查詢是一個為 O(N) 的線性時間複雜度。換句話說,隨著資料量的提高,執行時間隨著增加。

相比而言,使用 Set 去查詢,不管是刪除還是插入的時間複雜度都僅僅是 O(1)——這意味著,執行時間不隨著數量的提高而增加。

Note: 想要了解更多關於時間複雜度的內容,請查閱我的文章 Understanding Big O Notation

那麼 Set 究竟有多快呢?

雖然執行時間受使用的作業系統、資料的大小和其它的一些變數的影響,我希望我的測試結果能讓你對 Set 的速度有個直觀的感受。

準備測試

在開始執行之前,我們簡單的將 ArraySet 填充 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.173ms
  • Set: 0.023ms
  • Set 快了 7.54 倍

測試2: 新增

新增一個值,變數為 n

console.time('Array'); 
arr.push(n);
console.timeEnd('Array');
console.time('Set'); 
set.add(n);
console.timeEnd('Set');
複製程式碼
  • Array: 0.018ms
  • Set: 0.003ms
  • Set 快了 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.122ms
  • Set: 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++,但是如果是 JSSet 會是最終解決方案的關鍵點。

如果你想要更深入瞭解這些解決方案,我推薦閱讀原文,但是這裡,我簡單的介紹一下解決方案。

給一個未排序的整數陣列和一個值 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²)。太慢了!

希望本文對你有所幫助!

相關文章