前端学算法之算法复杂度

前面的话

  本文将详细介绍算法复杂度

 

大O表示法

  大O表示法是描述算法的性能和复杂程度。 分析算法时,时常遇到以下几类函数

符号             名称
O(1)            常数的
O(log(n))        对数的
O((log(n))c)    对数多项式的
O(n)            线性的
O(n2)            二次的
O(nc)            多项式的
O(cn)            指数的

  如何衡量算法的效率?通常是用资源,例如CPU(时间)占用、内存占用、硬盘占用和网络占用。当讨论大O表示法时,一般考虑的是CPU(时间)占用

  下面用一些例子来理解大O表示法的规则

【O(1)】

function increment(num){ 
  return ++num;
}

  假设运行increment(1)函数,执行时间等于X。如果再用不同的参数(例如2)运行一次increment函数,执行时间依然是X。和参数无关,increment函数的性能都一样。因此,我们说上述函数的复杂度是O(1)(常数)

【O(n)】

  现在以顺序搜索算法为例:

function sequentialSearch(array, item){ 
  for (var i=0; i<array.length; i++){
    if (item === array[i]){ //{1} 
      return i;
    }
  }
  return -1;
}

  如果将含10个元素的数组([1, ..., 10])传递给该函数,假如搜索1这个元素,那么,第一次判断时就能找到想要搜索的元素。在这里我们假设每执行一次行{1} ,开销是1。

  现在,假如要搜索元素11。行{1}会执行10次(遍历数组中所有的值,并且找不到要搜索的元素,因而结果返回 -1)。如果行{1}的开销是1,那么它执行10次的开销就是10,10倍于第一种假设

  现在,假如该数组有1000个元素([1, ..., 1000])。搜索1001的结果是行{1}执行了1000次(然后返回-1)

  sequentialSearch函数执行的总开销取决于数组元素的个数(数组大小),而且也和搜索的值有关。如果是查找数组中存在的值,行{1}会执行几次呢?如果查找的是数组中不存在的值,那么行{1}就会执行和数组大小一样多次,这就是通常所说的最坏情况

  最坏情况下,如果数组大小是10,开销就是10;如果数组大小是1000,开销就是1000。可以得出sequentialSearch函数的时间复杂度是O(n),n是(输入)数组的大小

  回到之前的例子,修改一下算法的实现,使之计算开销:

function sequentialSearch(array, item){
 var cost = 0;
 for (var i=0; i<array.length; i++){
  cost++;
  if (item === array[i]){ //{1}
    return i;
  }
 }
 console.log('cost for sequentialSearch with input size ' +  array.length + ' is ' + cost);
 return -1;
} 

  用不同大小的输入数组执行以上算法,可以看到不同的输出

O(n2)】

  用冒泡排序做O(n2)的例子:

function swap(array, index1, index2){
 var aux = array[index1];
 array[index1] = array[index2];
 array[index2] = aux;
}
function bubbleSort(array){
 var length = array.length;
 for (var i=0; i<length; i++){ //{1}
  for (var j=0; j<length-1; j++ ){ //{2}
    if (array[j] > array[j+1]){
      swap(array, j, j+1);
    }
  }
 }
} 

  假设行{1}和行{2}的开销分别是1。修改算法的实现使之计算开销:

function bubbleSort(array){
 var length = array.length;
 var cost = 0;
 for (var i=0; i<length; i++){ //{1}
  cost++;
  for (var j=0; j<length-1; j++ ){ //{2}
    cost++;
    if (array[j] > array[j+1]){
      swap(array, j, j+1);
    }
  }
 }
 console.log('cost for bubbleSort with input size ' + length + ' is ' + cost);
} 

  如果用大小为10的数组执行bubbleSort,开销是100(102)。如果用大小为100的数组执 行bubbleSort,开销就是10 000(1002)。需要注意,我们每次增加输入的大小,执行都会越来越久

  时间复杂度O(n)的代码只有一层循环,而O(n2)的代码有双层嵌套循环。如 果算法有三层遍历数组的嵌套循环,它的时间复杂度很可能就是O(n3)

 

时间复杂度

  下图比较了前述各个大O符号表示的时间复杂度:

arithmetic21

  下表是常用数据结构的时间复杂度

arithmetic22

  下表是图的时间复杂度: 

arithmetic23

  下表是排序算法的时间复杂度: 

arithmetic24

  下表是搜索算法的时间复杂度: 

arithmetic25

 

NP

  一般来说,如果一个算法的复杂度为O(nk),其中k是常数,我们就认为这个算法是高效的,这就是多项式算法

  对于给定的问题,如果存在多项式算法,则计为P(polynomial,多项式)

  还有一类NP(nondeterministicpolynomial,非确定性多项式)算法。如果一个问题可以在多项式时间内验证解是否正确,则计为NP。如果一个问题存在多项式算法,自然可以在多项式时间内验证其解。因此,所有的P都是NP。然而,P=NP是否成立,仍然不得而知。NP问题中最难的是NP完全问题,它满足以下两个条件:(1)是NP问题,也就是说,可以在多项式时间内验证解,但还没有找到多项式算法;(2)所有的NP问题都能在多项式时间内归约为它。为了理解问题的归约,考虑两个决策问题L和M。假设算法A可以解决问题L,算法B可以验证输入y是否为M的解。目标是找到一个把L转化为M的方法,使得算法B可以用于构造算法A

  还有一类问题,只需满足NP完全问题的第二个条件,称为NP困难问题。因此,NP完全问题也是NP困难问题的子集

  下面是满足P < > NP时,P、NP、NP完全和NP困难问题的欧拉图: 

arithmetic26

 

  非NP完全的NP困难问题的例子有停机问题和布尔可满足性问题(SAT)。 NP完全问题的例子有子集和问题、旅行商问题、顶点覆盖问题等等

  我们提到的有些问题是不可解的。然而,仍然有办法在符合要求的时间内找到一个近似解。启发式算法就是其中之一。启发式算法得到的未必是最优解,但足够解决问题了。启发式算法的例子有局部搜索、遗传算法、启发式导航、机器学习等

 

posted @ 2018-01-05 18:44 小火柴的蓝色理想 阅读(...) 评论(...) 编辑 收藏