如何理解算法复杂度

1,120 阅读7分钟

算法

算法的定义是这样的:解题方案的准确而完善的描述,是一系列解决问题的清晰指令,就是解决一个问题的完整性描述。

如何衡量一个算法的好坏,可以通过空间复杂度和时间复杂度两个方面来进行衡量。

  1. 空间复杂度 评估执行程序所需的存储空间。可以估算出程序对计算机内存的使用程度。

  2. 时间复杂度 评估执行程序所需的时间。可以估算出程序对处理器的使用程度。

设计算法时,时间复杂度要比空间复杂度更容易出问题,所以一般情况下我们只对时间复杂度进行研究。

时间复杂度

时间频度

如果一个算法所花费的时间与算法中代码语句执行次数成正比,那么那个算法执行语句越多,它的花费时间也就越多。我们把一个算法中的语句执行次数称为时间频度。通常用T(n)表示。n用来表示问题的规模。

一般情况下,算法中基本操作重复执行的次数是n的某个函数,用T(n)表示,f(n)用来描述T(n) 函数中增长最快的部分,。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。

该表示方法被成为大O表示法。

大O表示法

时间复杂度常用大O符号——O(f(n))表述,不包括这个函数的低阶项和首项系数。

推导大O阶有一下三种规则:

  1. 用常数1取代运行时间中的所有加法常数
  2. 只保留最高阶项
  3. 去除最高阶的常数

大O阶的推导方法

大O表示法O(f(n))中的f(n)的值可以为1、n、logn、n^2 等,所以我们将O(1)、O(n)、O(logn)、O( n^2 )分别称为常数阶、线性阶、对数阶和平方阶。下面我们来看看推导大O阶的方法:

常数阶

例:段代码的大O是多少?

    let sum = 0, n = 100;

第一条就说明了所有加法常数给他个O(1)即可

线性阶

一般含有非嵌套循环涉及线性阶,线性阶就是随着问题规模n的扩大,对应计算次数呈直线增长。

    let i , n = 100, sum = 0;
    for( i=0; i < n; i++ )
    {
        sum = sum + i;
    }

上面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码需要执行n次。

平方阶

    let i, j, n = 100;
    for( i=0; i < n; i++ )
    {
        for( j=0; j < n; j++ )
        {
            console.log('hi')
        }
    }

n等于100,也就是说外层循环每执行一次,内层循环就执行100次,那总共程序想要从这两个循环出来,需要执行100*100次,也就是n的平方。所以这段代码的时间复杂度为O(n^2)。

总结:如果有三个这样的嵌套循环就是n^3。所以总结得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。

对数阶

    let i = 1, n = 100;
    while( i < n )
    {
        i = i * 2;
    }

由于每次i*2之后,就距离n更近一步,假设有x个2相乘后大于或等于n,则会退出循环。 于是由2^x = n得到x = log(2)n,所以这个循环的时间复杂度为O(logn)

举例:冒泡排序

冒泡排序

假设冒泡排序存在三种情况,1.数组全部正序排列;2.数组全部反向排列;3.数组混乱排序;

第一种情况下,数组只需遍历一遍就可以完成排序,假设数组长度为n,需要遍历n-1次,此时:

    T(n) = n-1 = T(O(n));

第二种情况,数组全部反向排列,那么就需要一次次遍历数组直到顺序正确,第一次排序需要执行n-1步,第二次排序可以排除排序正确的数,只需要执行(n-1)-1)次,以此类推,直到最后执行1次完成排序,该过程次数为:

    T(n)=n(n-1)/2 = T(O(n^2);

第三种情况,数组混乱排序,可能不需要执行到最后即可完成排序,假设到第i次即可完成排序,该过程执行次数:

    T(n)=n(n-i)/2 = T(O(n^2);

综上,冒泡排序的平均时间复杂度为

    O(n^2)

规则解析

推导大O阶有一下三种规则:

  1. 用常数1取代运行时间中的所有加法常数
  2. 只保留最高阶项
  3. 去除最高阶的常数

当n增大到一定程度时,f(n)中最高阶的部分占据了主导地位,低阶变量和常数对结果对影响几乎可以忽略不计。

工具地址:函数生成器

低阶变量影响

低阶变量的影响

图中的两个函数分别为

    y=3x^2
    y=3x^2+2x

随着X的变大,两个函数曲线几乎重合,常数10000的影响微乎其微,可忽略不计。

常量的影响

图中的两个函数分别为

    y=2x^2
    y=2x^2+10000

随着X的变大,两个函数曲线几乎重合,常数10000的影响微乎其微,可忽略不计。

常数影响

同理,高阶变量前的常量也可忽略不计

高阶常量忽略不计

综合

综上,低阶变量和常量对最终结果影响不大

综合影响

常见时间复杂度的比较

复杂度比较

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

大O阶的应用

数组去重

方法1:

    var arr = [1,2,2,4,3,4,1,3,2,7,5,6,1]
    var newArr = new Set(arr); //n
T(n)=n=T(O(n));

该算法时间复杂度为

O(n);

方法2:

    function fn(arr){
       let newArr = [] // 1
       arr.sort((a,b)=>{
           return a-b
       }) // O(n)=n^2
       
       arr.forEach((val,index)=>{ // n
           if(val != arr[index+1]){ // 1
                newArr.push(val) // 1
           }
       })
       return newArr //1
    }
T(n) =T(O(n^2)) +n+2=T(O(n^2));

该算法时间复杂度为

O(n^2);

方法3:

    for(var i=0;i<arr.length;i++){ //n 
        for(var j=i+1;j<arr.length;j++){ // n*n
             if(arr[i]==arr[j]){ // n*n
                  arr.splice(j,1) // n*n*n
             }
        }
    } 
T(n)=n^3+2n^2+n=T(O(n^3));

该算法时间复杂度为

O(n^3);

算法对比

O(n)表示了算法的复杂程度,但是并不代表复杂程度越大的算法,消耗时间越长,具体需要根据n的值来判断。以上面的3种数组去重算法为例,分析n不同的情况下,不同算法的消耗时间。

    for(var i = 0,arr =[];i<n;i++){
        arr[arr.length]=parseInt(Math.random()*n);
    }    
    function fn1(arr){
       let newArr = [] // 1
       arr.sort((a,b)=>{
           return a-b
       }) // O(n)=n^2
    
       arr.forEach((val,index)=>{ // n
           if(val != arr[index+1]){ // 1
                newArr.push(val) // 1
           }
       })
       return newArr //1
    }
    function fn2(arr){
        for(var i=0;i<arr.length;i++){ //n 
            for(var j=i+1;j<arr.length;j++){ // n*n
                 if(arr[i]==arr[j]){ // n*n
                      arr.splice(j,1) // n*n*n
                 }
            }
        }
    }
    var newArr = new Set(arr)
    console.time("newArr");
    new Set(arr);
    console.timeEnd("newArr");
    console.time("fn1");
    fn1(arr);
    console.timeEnd("fn1");
    console.time("fn2");
    fn2(arr);
    console.timeEnd("fn2");

n=500

500结果1

500结果2

500结果3

n=1000

1000结果1

1000结果2

1000结果3

n=2000

2000结果1

2000结果2

2000结果3

n=5000

5000结果1

5000结果2

5000结果3

n=10000

10000结果1

10000结果2

10000结果3

综合以上的对比结果可以得知,当n<1000时,算法3的时间小于算法2的时间,尽管算法2的时间复杂度小于算法3。

当一个算法当T(n)可以获知时,可以通过对比T(n)来决定使用哪种算法。

综合上文大O阶表示法的推导规则,当n的数值绝对大时,低阶表达式和常数的影响微乎其微,而上述的应用也验证了这一观点,当n超过1000时,O(n^3)的算法耗时大大超过O(n^2)的算法。

因此,当算法应用的情况较为复杂时,利用时间复杂度——O(n)来判断是行之有效的方法。时间复杂度可以是评价一个算法的相对条件,但不是绝对条件。