用 JavaScript 學習算法複雜度

做者:Joshua Hall

翻譯:瘋狂的技術宅javascript

原文:https://alligator.io/js/big-o...前端

未經容許嚴禁轉載java

在本文中,咱們將探討 「二次方」 和 「n log(n)」 等術語在算法中的含義。程序員

在後面的例子中,我將引用這兩個數組,一個包含 5 個元素,另外一個包含 50 個元素。我還會用到 JavaScript 中方便的 performance API 來衡量執行時間的差別。面試

const smArr = [5, 3, 2, 35, 2];

const bigArr = [5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2, 5, 3, 2, 35, 2];

什麼是 Big O 符號?

Big O 表示法是用來表示隨着數據集的增長,計算任務難度整體增加的一種方式。儘管還有其餘表示法,但一般 big O 表示法是最經常使用的,由於它着眼於最壞的狀況,更容易量化和考慮。最壞的狀況意味着完成任務須要最多的操做次數;若是你在一秒鐘內就能恢復打亂魔方,那麼你只擰了一圈的話,不能說本身是作得最好的。算法

當你進一步瞭解算法時,就會發現這很是有用,由於在理解這種關係的同時去編寫代碼,就能知道時間都花在了什麼地方。segmentfault

當你瞭解更多有關 Big O 表示法的信息時,可能會看到下圖中不一樣的變化。咱們但願將複雜度保持在儘量低的水平,最好避免超過 O(n)。api

image.png

O(1)

這是理想的狀況,不管有多少個項目,不論是一個仍是一百萬個,完成的時間量都將保持不變。執行單個操做的大多數操做都是 O(1)。把數據寫到數組、在特定索引處獲取項目、添加子元素等都將會花費相同的時間量,這與數組的長度無關。數組

const a1 = performance.now();
smArr.push(27);
const a2 = performance.now();
console.log(`Time: ${a2 - a1}`); // Less than 1 Millisecond


const b1 = performance.now();
bigArr.push(27);
const b2 = performance.now();
console.log(`Time: ${b2 - b1}`); // Less than 1 Millisecond

O(n)

在默認狀況下,全部的循環都是線性增加的,由於數據的大小和完成的時間之間存在一對一的關係。因此若是你有 1,000 個數組項,將會花費的 1,000 倍時間。瀏覽器

const a1 = performance.now();
smArr.forEach(item => console.log(item));
const a2 = performance.now();
console.log(`Time: ${a2 - a1}`); // 3 Milliseconds

const b1 = performance.now();
bigArr.forEach(item => console.log(item));
const b2 = performance.now();
console.log(`Time: ${b2 - b1}`); // 13 Milliseconds

O(n^2)

指數增加是一個陷阱,咱們都掉進去過。你是否須要爲數組中的每一個項目找到匹配對?將循環放入循環中是一種很好的方式,能夠把 1000 個項目的數組變成一百萬個操做搜索,這將會使你的瀏覽器失去響應。與使用雙重嵌套循環進行一百萬次操做相比,最好在兩個單獨的循環中進行 2,000 次操做。

const a1 = performance.now();
smArr.forEach(() => {
    arr2.forEach(item => console.log(item));
});
const a2 = performance.now();
console.log(`Time: ${a2 - a1}`); // 8 Milliseconds


const b1 = performance.now();
bigArr.forEach(() => {
    arr2.forEach(item => console.log(item));
});
const b2 = performance.now();
console.log(`Time: ${b2 - b1}`); // 307 Milliseconds

O(log n)

我認爲關於對數增加最好的比喻,是想象在字典中查找像 「notation」 之類的單詞。你不會在一個詞條一個詞條的去進行搜索,而是先找到 「N」 這一部分,而後是 「OPQ」 這一頁,而後按字母順序搜索列表直到找到匹配項。

經過這種「分而治之」的方法,找到某些內容的時間仍然會因字典的大小而改變,但遠不及 O(n) 。由於它會在不查看大部分數據的狀況下逐步搜索更具體的部分,因此搜索一千個項目可能須要少於 10 個操做,而一百萬個項目可能須要少於 20 個操做,這使你的效率最大化。

在這個例子中,咱們能夠作一個簡單的 快速排序

const sort = arr => {
  if (arr.length < 2) return arr;

  let pivot = arr[0];
  let left = [];
  let right = [];

  for (let i = 1, total = arr.length; i < total; i++) {
    if (arr[i] < pivot) left.push(arr[i]);
    else right.push(arr[i]);
  };
  return [
    ...sort(left),
    pivot,
    ...sort(right)
  ];
};
sort(smArr); // 0 Milliseconds
sort(bigArr); // 1 Millisecond

O(n!)

最糟糕的一種可能性是析因增加。最經典的例子就是旅行的推銷員問題。若是你要在不少距離不一樣的城市之間旅行,如何找到在全部城市之間返回起點的最短路線?暴力方法將是檢查每一個城市之間全部可能的路線距離,這是一個階乘而且很快就會失控。

因爲這個問題很快會變得很是複雜,所以咱們將經過簡短的遞歸函數演示這種複雜性。這個函數會將一個數字去乘以函數本身,而後將數字減去1。階乘中的每一個數字都會這樣計算,直到爲 0,而且每一個遞歸層都會把其乘積添加到原始數字中。

階乘只是從 1 開始直至該數字的乘積。那麼 6!1x2x3x4x5x6 = 720

const factorial = n => {
  let num = n;

  if (n === 0) return 1
  for (let i = 0; i < n; i++) {
    num = n * factorial(n - 1);
  };

  return num;
};
factorial(1); // 2 Milliseconds
factorial(5); // 3 Milliseconds
factorial(10); // 85 Milliseconds
factorial(12); //  11,942 Milliseconds

我本來打算顯示 factorial(15),可是 12 以上的值都太多,而且使頁面崩潰了,這也證實了爲何須要避免這種狀況。

結束語

咱們須要編寫高性能的代碼彷佛是一個不爭得事實,可是我敢確定,幾乎每一個開發人員都建立過至少兩重甚至三重嵌套循環,由於「它確實有效」。Big O 表示法在表達和考慮複雜性方面是很是必要的,這是咱們從未有過的方式。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索