算法复杂度之大O表示法

181 阅读4分钟

大O表示法

它用于描述算法的性能和复杂程度。大O表示法将算法按照消耗的时间进行分类,依据随输入增大所需要的空间/内存。 分析算法时,时常遇到以下几类函数。

符号名称
O(1)常数的
O(log(n))对数的
O(log(n)c)对数多项的
O(n)线性的
O(n²)二次的
O(n^c)多项的
O(nⁿ)指数的

理解大O表示法

如何衡量算法的效率?通常是用资源,例如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, value, equalsFn = defaultEquals) {
    for(let i = 0; i < array.length; 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是输入数组的大小。

  • O(n²) 用冒泡排序做O(n²)的例子
function bubbleSort(array, compareFn = defaultCompare) {
    const { length } = array;
    for (let i = 0; i < length; i++) { // {1}
        for(let j = 0; j < length - 1; j++) { // {2}
            if(compareFn(array[j], array[j + 1]) === Compare.BIGGER_THAN) {
                swap(array, j, j + 1);
            }
        }
    }
    return array;
}

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

function bubbleSort(array, compareFn = defaultCompare) {
    const { length } = array;
    let cost = 0;
    for (let i = 0; i < length; i++) { // {1}
        cost++;
        for(let j = 0; j < length - 1; j++) { // {2}
            cost++;
            if(compareFn(array[j], array[j + 1]) === Compare.BIGGER_THAN) {
                swap(array, j, j + 1);
            }
        }
    }
    console.log(`cost for bubbleSort with input size ${length} is ${cost}`);
    return array;
}

如果用大小为10的数组执行bubbleSort,开销是100(10²)。如果用大小为100的数组执行bubbleSort,开销就是10000(100²)。需要注意,我们每次增加输入的大小,执行都会越来越久。
时间复杂度O(n)的代码只有一层循环,而O(n²)的代码有双层嵌套循环。如果算法有三层迭代数组的嵌套循环,它的时间复杂度很可能就是O(n³)。

时间复杂度比较

我们可以创建一个表格来表示不同的时间复杂度。

输入大小(n)O(1)O(log(n))O(n)O(nlog(n))O(n²)O(2ⁿ)
101110101001024
2011.32026.024001048576
5011.695084.942500非常大
1001210020010000非常大
50012.695001349.48250000非常大
100013100030001000000非常大
100001410000400001000000000非常大