經過 JavaScript 學習算法複雜度

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

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

1const smArr = [5, 3, 2, 35, 2];
2
3const 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 表示法是最經常使用的,由於它着眼於最壞的狀況,更容易量化和考慮。最壞的狀況意味着完成任務須要最多的操做次數;若是你在一秒鐘內就能恢復打亂魔方,那麼你只擰了一圈的話,不能說本身是作得最好的。數組

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

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

O 表示法複雜度

O(1)

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

1const a1 = performance.now();
 2smArr.push(27);
 3const a2 = performance.now();
 4console.log(`Time: ${a2 - a1}`); // Less than 1 Millisecond
 5
 6
 7const b1 = performance.now();
 8bigArr.push(27);
 9const b2 = performance.now();
10console.log(`Time: ${b2 - b1}`); // Less than 1 Millisecond
複製代碼

O(n)

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

1const a1 = performance.now();
2smArr.forEach(item => console.log(item));
3const a2 = performance.now();
4console.log(`Time: ${a2 - a1}`); // 3 Milliseconds
5
6const b1 = performance.now();
7bigArr.forEach(item => console.log(item));
8const b2 = performance.now();
9console.log(`Time: ${b2 - b1}`); // 13 Milliseconds
複製代碼

O(n^2)

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

1const a1 = performance.now();
 2smArr.forEach(() => {
 3    arr2.forEach(item => console.log(item));
 4});
 5const a2 = performance.now();
 6console.log(`Time: ${a2 - a1}`); // 8 Milliseconds
 7
 8
 9const b1 = performance.now();
10bigArr.forEach(() => {
11    arr2.forEach(item => console.log(item));
12});
13const b2 = performance.now();
14console.log(`Time: ${b2 - b1}`); // 307 Milliseconds
複製代碼

O(log n)

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

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

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

1const sort = arr => {
 2  if (arr.length < 2) return arr;
 3
 4  let pivot = arr[0];
 5  let left = [];
 6  let right = [];
 7
 8  for (let i = 1, total = arr.length; i < total; i++) {
 9    if (arr[i] < pivot) left.push(arr[i]);
10    else right.push(arr[i]);
11  };
12  return [
13    ...sort(left),
14    pivot,
15    ...sort(right)
16  ];
17};
18sort(smArr); // 0 Milliseconds
19sort(bigArr); // 1 Millisecond
複製代碼

O(n!)

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

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

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

1const factorial = n => {
 2  let num = n;
 3
 4  if (n === 0) return 1
 5  for (let i = 0; i < n; i++) {
 6    num = n * factorial(n - 1);
 7  };
 8
 9  return num;
10};
11factorial(1); // 2 Milliseconds
12factorial(5); // 3 Milliseconds
13factorial(10); // 85 Milliseconds
14factorial(12); //  11,942 Milliseconds
複製代碼

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

結束語

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

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


歡迎關注前端公衆號:前端先鋒,免費領取 Vue、React 性能優化教程。

相關文章
相關標籤/搜索