经过 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 性能优化教程。

相关文章
相关标签/搜索