【前端学算法】:时间复杂度和空间复杂度详解

744 阅读5分钟

前言

最近亲眼见证了算法的魅力。

在一场集成测试中,产品通过excel导入800条数据,用时8s之久,体验很不友好。

之所以用时这么久,除了校验逻辑比较多之外,更多的是3个sheet联动校验造成多层嵌套循环,形成了笛卡尔积

后来后端重新找了3个sheet的对应关系,避免了笛卡尔积的形成,优化之后,同样导入800条数据,时间缩短至2-3秒。

也许有人会说,你这上面举的是后端的例子,跟前端有什么关系。

其实不然,随着大前端的发展,前端不只局限于界面与交互,越来越多的计算也交于前端处理。

我本人在进行excel导出功能开发时,就被要求前端进行求和运算、数据拆分、数据排序等,而后端只负责返回原始数据。

所以,我开始了js数据结构与算法的学习,以期能够适应日趋复杂的前端计算。

开始

在正式学习js数据结构与算法之前,我们需要了解两个非常重要的概念: 时间复杂度空间复杂度

他们是学好算法的基石,也是衡量我们代码好坏的两个非常重要的标准。

我们平时表示算法复杂度主要就是用 O(),读作大欧表示法。它的主要计算原则如下:

  • 如果只是常数直接估算为1,即 O(1),不是说只执行了1次,而是对常量级时间复杂度的一种表示法。

  • 对于 O(3n+4) 里常数4对于总执行次数的几乎没有影响,直接忽略不计,系数3影响也不大,因为3n和n都是一个量级的,所以作为系数的常数3也估算为1或者可以理解为去掉系数,所以 O(3n+4) 的时间复杂度为 O(n)

  • 如果是多项式,只需要保留n的最高次项,O( 666n³ + 666n² + n ),这个复杂度里面的最高次项是n的3次方。因为随着n的增大,后面的项的增长远远不及n的最高次项大,所以低于这个次项的直接忽略不计,常数也忽略不计,简化后的时间复杂度 O(n³)

时间复杂度

时间复杂度对应代码的运行时间

常见的时间复杂度如下:

  • 常数型复杂度:O(1)
  • 对数型复杂度:O(logn)
  • 线性型复杂度:O(n)
  • 线性对数型复杂度:O(nlogn)
  • k次型复杂度:O(nᵏ)
  • 指数型复杂度:O(kⁿ)
  • 阶乘型复杂度:O(n!)

它们的排序如下:

O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)

下面我们具体介绍一下:

O(1)

  • 算法里没有循环和递归,只有一些赋值和运算等简单操作
  • 算法消耗不随变量增长而增长,性能最佳
  • 无论代码执行多少行,即使有几千几万行,时间复杂度都为O(1)
function fun {
  const n = 1;
  const m = n *  10000;
  ...
};

O(logn)

  • 循环的次数呈现对数级别的增长
  • 性能较好

function fun() { 
  let i = 1; 
  const n = 100;
  while (i < n) { 
    i *= 2; 
   } 
}

O(n)

  • 只有一层循环
  • 算法消耗随n的增长而增长,性能一般
  • 无论n值有多大,即使是Inifinity,时间复杂度都为O(n)
function fun() { 
  for (let i = 0; i < n; i++) { 
    console.log(i); 
  };
};

O(nlogn)

  • 常用于一个对时间复杂度为O(logn)的代码执行一个n次循环
  • 算法消耗随n的增长而增长,性能较差
function fun() { 
  for (let i = 0; i < n; i++) { 
    while (i < n) { 
     i *= 10;
    }; 
   };
 };

O(n²)

  • 最常见的算法时间复杂度,可用于快速开发业务逻辑
  • 常见于2次循环,或者3次循环,以及k次循环
  • 算法消耗随n的增长而增长,性能糟糕
  • 实际开发过程中,不建议使用K值过大的循环,否则代码将非常难以维护
  function fun() { 
    for (let i = 0; i < n; i++) { 
      for (let j = 0; j < n; j++) {
      };
     };
   };

O(2ⁿ)

  • 常见于2次递归、3次递归以及k次递归的情况
  • 算法消耗随n的增长而增长,性能糟糕
  • 实际开发过程中,k为1时,一次递归的时间复杂度为O(1)。因为O(1^n)无论n为多少都为O(1)。
function fun(n) { 
  if {n <= 2}{
    return 1
  }  
  return fun(n - 1) + fun(n - 2)
 };

O(n!)

  • 极其不常见
  • 算法消耗随n的增长而增长,性能糟糕
  function fun(n) { 
    let count = 0;
    for(let i=0; i<n; i++) { 
    count++;
      fun(n-1); 
    };
  };

空间复杂度

空间复杂度对应代码的占用内存

常用的空间复杂度有:O(1)、 O(n)O(n²),下面我们具体介绍一下:

O(1)

无论代码多少行,只要它不会因为算法的执行而导致额外的空间增长,那么它的空间复杂度就是 O(1),例如:

function fun {
  const n = 1;
  const m = n *  10000;
  ...
};

 O(n)

如果n的数值越大,算法需要分配存储的空间就越多,那么它的空间复杂度就是 O(n)。例如:

function fun {
  let arr = [];
  for( let i = 1; i < n; i++ ) { 
    arr[i] = i;
   };
};

O(n²)

O(n²) 这种空间复杂度一般出现在比如二维数组,或是矩阵的情况下,例如:

const arr = [ 
  [1,2,3,], 
  [1,2,3,], 
  [1,2,3,],
];