在本文中,咱們將探討 「二次方」 和 「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 表示法的信息時,可能會看到下圖中不一樣的變化。咱們但願將複雜度保持在儘量低的水平,最好避免超過 O(n)。性能優化
這是理想的狀況,不管有多少個項目,不論是一個仍是一百萬個,完成的時間量都將保持不變。執行單個操做的大多數操做都是 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
複製代碼
在默認狀況下,全部的循環都是線性增加的,由於數據的大小和完成的時間之間存在一對一的關係。因此若是你有 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
複製代碼
指數增加是一個陷阱,咱們都掉進去過。你是否須要爲數組中的每一個項目找到匹配對?將循環放入循環中是一種很好的方式,能夠把 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
複製代碼
我認爲關於對數增加最好的比喻,是想象在字典中查找像 「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
複製代碼
最糟糕的一種可能性是析因增加。最經典的例子就是旅行的推銷員問題。若是你要在不少距離不一樣的城市之間旅行,如何找到在全部城市之間返回起點的最短路線?暴力方法將是檢查每一個城市之間全部可能的路線距離,這是一個階乘而且很快就會失控。
因爲這個問題很快會變得很是複雜,所以咱們將經過簡短的遞歸函數演示這種複雜性。這個函數會將一個數字去乘以函數本身,而後將數字減去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/