算法复杂度分析

什么是算法?

算法(algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操做;此外,一个算法一般来讲具备如下五个特性:java

  • 输入:一个算法应以待解决的问题的信息做为输入。
  • 输出:输入对应指令集处理后获得的信息。
  • 有穷性:算法执行的指令个数是有限的,每一个指令又是在有限时间内完成的,所以整个算法也是在有限时间内能够结束的。
  • 可行性:算法是可行的,即算法中的每一条指令都是能够实现的,均能在有限的时间内完成。
  • 肯定性:算法对于特定的合法输入,其对应的输出是惟一的。即当算法从一个特定输入开始,屡次执行同一指令集结果老是相同的。

算法效率的度量

过后统计法

这种方法有两个缺陷:一是必须先运行一句算法编制的程序;二是所得时间的统计量依赖于计算机的硬件、软件、等环境因素,有时容易掩盖算法自己的优点。故通常采用事前估算的方法。算法

事前分析估算法

一个算法是由控制结构(顺序、分支和循环3种)和原操做(指固有数据类型的操做)构成的,则算法时间取决于二者的综合效果。为了便于比较同一个问题的不一样算法,一般的作法是,从算法中选取一种对于所研究的问题(或算法类型)来讲是基本操做的原操做,以该基本操做的重复执行的次数做为算法的时间量度。

一个用高级程序语言编写的程序在计算机上运行式所消耗的时间取决于下列因素:数组

  1. 算法采用的策略、方法
  2. 问题的规模
  3. 书写程序的语言
  4. 编译程序所产生的机器代码的质量
  5. 机器执行指令的速度

时间复杂度分析

渐进时间复杂度(asymptotic time complexity):若存在函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同量级函数。记做T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称 时间复杂度。渐进时间复杂度用大写O来表示,因此也被称为 大O表示法

T(n)=O(f(n))这个公式中,T(n)表示代码的执行时间;n表示数据规模的大小;f(n)表示每行代码执行的次数总和。由于这是一个公式,因此用f(n)来表示,公式中的O表示渐进于无穷的一种行为。大O时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增加的变化趋势。微信

如何推导出时间复杂度呢?有以下几个原则:

  1. 若是运行时间是常数量级,用常数1表示。
  2. 只保留时间函数中的最高阶项
  3. 若是最高阶项存在,则省去最高阶项前面的系数。

或者换种说法:在”公式中的低阶、常量、系数三部分并不左右增加趋势,因此均可以忽略“基础上,数据结构

  1. 只关注循环执行次数最多的一段代码
  2. 加法法则:总复杂度等于量级最大的那段代码的复杂度

    若是T1(n)=O(f(n)),T2(n)=O(g(n));函数

    那么T(n)=T1(n)+T2(n)=max(O(f(n)),O(g(n)))=O(max(f(n),g(n)))性能

  3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

    若是 T1(n)=O(f(n)),T2(n)=O(g(n));测试

    那么 T(n)=T1(n)T2(n)=O(f(n))O(g(n))=O(f(n)g(n));code

几种常见时间复杂度

按数量级递增:
常量阶 O(1)
对数阶 O(logn)
线性阶 O(n)
线性对数阶 O(nlogn)
平方阶 O(n^2)、立方阶 O(n^3)、...k次方阶 O(n^k)
指数阶 O(2^n)
阶乘阶 O(n!)

对于上述罗列的复杂度量级,能够分为:多项式量级非多项式量级。其中,非多项式量级只有两个:指数阶 O(2n)和阶乘阶 O(n!)。排序

咱们把时间复杂度为非多项式量级的算法问题叫做NP(Non-Deterministic Polynomial,非肯定多项式)问题。当数据规模 n 愈来愈大时,非多项式量级算法的执行时间会急剧增长,求解问题的执行时间会无限增加。因此,非多项式时间复杂度的算法实际上是很是低效的算法。

下面结合简单的代码分析几种常见的多项式时间复杂度:

O(1)

public void fun(){//没有入参变量
    int sum = 0;//执行次数:1
    int p = 1;//执行次数:1
    for(; p <= 100; ++p){//执行次数为:100
        sum = sum + p;//执行次数为:100
    }
}

这段代码的时间复杂度为O(1)。这段代码执行了100次,是一个常量的执行时间,跟n的规模无关。即只要代码的执行时间不随n的增加而增加,代码的时间复杂度都记做为O(1)。

O(n)

public void fun(){
    int n = 100;//执行次数:1
    int sum = 0;//执行次数:1
    for(int j = 1; j <= n; ++j){//执行次数:n
        sum += j;//执行次数:n
    }
}

即T(n)=1+1+n+n=2n+2,根据前面说到的算法分析原则,当n趋向于无穷大时,能够忽略低阶项和最高项系数,则时间复杂度为O(n)。

O(logn)、O(nlogn)

int i = 1;
while (i <= n) {
    i = i * 2;
}

实际上,咱们只需找出循环执行次数最多的代码,求出该代码被执行了多少次,就能知道整段代码的时间复杂度。从代码中能够看出,变量i的值从1开始,每循环一次乘以2,实际上i的取值是一个等比数列。经过2x=n,求出x=log2n,因此这段代码的时间复杂度为O(log2n)。

int i = 1;
while (i <= n) {
    i = i * 3;
}

根据上述思路,得出这段代码的时间复杂度为O(log3n))。

实际上,不管是以 2 为底、以 3 为底,仍是以 10 为底,咱们能够把全部对数阶的时间复杂度都 记为 O(logn)。

咱们知道,对数之间是能够互相转换的,log n 就等于 log 2 log n,因此 O(log n) = O(C log n),其中 C=log 2 是一个常量。基于咱们前面的一个理论:在采用大 O 标记复杂度的时 候,能够忽略系数,即 O(Cf(n)) = O(f(n))。因此,O(log n) 就等于 O(log n)。所以,在对数阶 时间复杂度的表示方法里,咱们忽略对数的“底”,统一表示为 O(logn)。

若是一段代码的时间复杂度是 O(logn),咱们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。并且,O(nlogn) 也是一种很是常见的算法时间复杂度。好比,归并排序、快速排序的时间复杂度都是 O(nlogn)。

O(n2)

public void fun(){
    int n = 100;//执行次数:1
    int sum = 0;//执行次数:1
    for(int i = 1; i <= n; ++i){//执行次数:n
        for(int j = 1; j <= n; ++j){//执行次数n*n
            sum += j;//执行次数n*n
        }
    }
}

根据乘法规则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。故时间复杂度为O(n2)。

O(m+n)

public int fun(int m, int n){
    int sum1 = 0;
    int i = 1;
    for(; i < m; ++i){
        sum1 = sum1 + i;
    }
    
    int sum2 = 0;
    int j = 1;
    for(; j < n; ++j){
        sum2 = sum2 + j;
    }
    return sum1 + sum2;
}

从代码中能够看出,m 和 n 是表示两个数据规模。咱们没法事先评估 m 和 n 谁的量级大,因此咱们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。因此,上面代码的时间复杂度就是 O(m+n)。

空间复杂度分析

空间复杂度(Space Complexity)是对一个算法在运行过程当中临时占用存储空间大小的量度。

算法的空间复杂度经过计算算法所需的存储空间实现,算法的空间复杂度的计算公式记做:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。

程序执行时所需存储空间包括如下两部分。  
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。

一个算法的空间复杂度只考虑在运行过程当中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。

public void fun(int n){
    int[] a = new int[n];
    for(int i = 1; i < n; ++i){
        a[i] = i * i;
    }
}

从代码中能够看出,申请了一个空间存储变量i,可是它是常量阶的,跟数据规模n无关。第二行代码为申请了一个大小为n的int类型数组,除此以外,没有占用更多的空间,因此整段代码的空间复杂度就是O(n)。

最好、最坏、平均状况时间复杂度

最好状况时间复杂度:代码在最理想状况下执行的时间复杂度。

最坏状况时间复杂度:代码在最坏状况下执行的时间复杂度。

平均状况时间复杂度:用代码在全部状况下执行的次数的加权平均值表示,简称平均时间复杂度。

public int find(int[] array, int n. int x){
    int index = -1;
    for(int i = 0; i < n; ++i){
        if(array[i] == x){
            index = i;
            break;
        }
    }
    return index;
}

在这段代码中,若是要查找的变量x出如今第一个元素,那就不须要继续遍历剩下的n-1个数据了,那么时间复杂度就是O(1),为最好状况时间复杂度。但若是数组中不存在变量x,那咱们就须要把整个数组遍历一遍,时间复杂度就成O(n),为最坏状况时间复杂度。故在不一样状况下代码的时间复杂度是不同的。

最好状况时间复杂度和最坏状况时间复杂度都是在极端状况下的代码时间复杂度,发生的几率不大。为了更好地表示平均状况下的复杂度,咱们须要引入一个新的概念:平均时间复杂度

在这段代码中,要查找的变量x在数组中的位置有n+1中状况:在数组的0~n-1位置中和不在数组中。假设在数组中和不在数组中的几率都为1/2;变量x出如今0~n-1这n个位置的几率为1/n,根据几率乘法规则,要查找的数据出如今0~n-1中任意位置的几率为1/(2n)。故平均时间复杂度的计算为:(查找须要遍历的元素个数乘以相应的权术)

$$ 1*1/(2n)+2*1/(2n)+3*1/(2n)+···+n*1/(2n)+n*1/2=(3n+1)/4 $$

这个值为加权平均值,也叫指望值。因此平均时间复杂度的全称应该叫加权平均时间复杂度或者指望时间复杂度。故这段代码的平均时间复杂度为O(n)。

加权平均值即将各数值乘以相应的权数,而后加总求和获得整体值,再除以总的单位数。

在大多数状况下,咱们并不须要区分最好、最坏、平均状况时间复杂度三种状况。咱们使用一个复杂度就能够知足需求了。只有同一块代码在不同的状况下,时间复杂度有量级的差距,咱们才会使用这三种复杂度表示法来区分。

均摊时间复杂度

对一个数据结构进行一组连续操做中,大部分状况下时间复杂度都很低,只有个别状况下时间复杂度比较高,并且这些操做之间存在先后连贯的时序关系,这个时候,咱们就能够将这一组操做放在一起分析,看是否能将较高时间复杂度那次操做的耗时,平摊到其余那些时间复杂度比较低的操做上。并且,在可以应用均摊时间复杂度分析的场合,通常均摊时间复杂度就等于最好状况时间复杂度。

请看下面这段代码:

int[] array = new int[10];//声明一个大小为10的数组array
int length = 10;
int i = 0;
//往数组里添加一个元素
public void addElement(int element){
    if(i > = len){
        int[] new_array = new int[len*2];
        for(int j = 0; j < len; ++j){
            //复制数据到new_array数组中
            new_array[j] = array[j];
        }
        array = new_array;
        len = len * 2;
    }
    //将element插入到下标为i的位置
    array[i] = element;
    ++i;
}

在这段代码中,当i<len时,不走for循环,因此时间复杂度为O(1);

当i>=len时,即当i=n时,for循环进行数组的复制,只有这一次的时间复杂度为O(n)。

由此可知:

  • 该算法的最好状况时间复杂度为O(1);
  • 最坏状况时间复杂度为O(n);
  • 平均状况时间复杂度为:

    $$ 1*1/(n+1)+1*1/(n+1)+···+1*1/(n+1)+n*1/(n+1)=2n/(n+1) $$

    故平均时间复杂度为O(1)。

  • 均摊复杂度:前n个操做复杂度都是O(1),第n+1次操做的复杂度是O (n),因此把最后一次的复杂度分摊到前n次上,那么均摊下来每次操做的复杂度为O(1)。

总结:渐进式时间,空间复杂度分析只是一个理论模型,只能提供给粗略的估计分析,一个低阶的时间复杂度程序有极大的可能性会优于一个高阶的时间复杂度程序,针对不同的宿主环境,不同的数据集,不同的数据量的大小,在实际应用上面可能真正的性能会不同。针对不同的实际状况, 进而进行必定的性能基准测试是颇有必要的,进而选择适合特定应用场景下的最优算法。

文章同步在微信公众号,习惯微信上看文章的能够关注微信公众号:加二减壹
相关文章
相关标签/搜索