【算法】算法的时间复杂度和空间复杂度


目录结构:

contents structure [-]
  1. 时间复杂度的定义
  2. 推导大O阶
  3. 最优、平均、最差时间复杂度
  4. 常见算法的时间复杂度图标

算法的时间复杂度就是估计一个算法所需的时间,算法的空间复杂度就是估计一个算法所需的内存。算法可以以空间换取时间,也可以以时间换空间。比如,需要求出当前年份是否为闰年,可以写个算法进行计算;也可以预先就把近100年的闰年情况加载到内存中,如果是闰年则为1否则为0,这就是以空间换取时间的栗子。通常情况下“复杂度”都是指的是“时间复杂度”,因此在这篇文章中,笔者也重点介绍时间复杂度。

1.时间复杂度的定义

时间复杂度:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记做T(n)=O(f(n)),它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称为算法的渐进时间复杂度,简称为时间复杂度。可以将算法的时间复杂度理解循环次数。

利用大写O()来体现算法时间复杂度的计法,称为大O计法。

最优算法:一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优。

2.推导大O阶

推导的一般步骤:

  • 用常数1取代运算中的所有加法常数
  • 在修改后的运行次数中,只保留最高项
  • 如果最高项阶存在且不为1,则去除与这个项相乘的参数

接下来举几个栗子:

#include 

int main()
{
    printf("Hello, World! \n");
    printf("Hello, World! \n");
    printf("Hello, World! \n");
    printf("Hello, World! \n");
    printf("Hello, World! \n");
    printf("Hello, World! \n");
    
    return 0;
}

在上面的main中,printf一共打印为6次,这里绝不要理解为算法时间复杂度就是O(6),根据推导大O的阶的推导步骤,可以得出算法的时间复杂度为O(1)。

    int n=100,sum=0;          //1
    for(int i=0;i//n
        sum+=(i+1);
    }
    printf("%d",sum);         //1

这个算法求得1到100的和,总的执行次数为n+2(注意:这里只是大致分析,该代码转化为汇编语言的命令行数绝不止这几行),根据推导规则就是O(n)。

接下来介绍一下高斯算法:

    int n=100;
    int sum=0;
    sum=(1+n)*n/2;
    printf("%d",sum);

可以看出,高斯算法的效率非常高,无论求的数之和有多大,算法的时间复杂度都为O(1)。

接来是一个对数阶的栗子:

    int i=1,n=100;          //1
    while(i//x
        i=i*2;
    }
    printf("%d",i);         //1

从上面这行代码中,不能直观的观察出while的循环次数,可以假设循环x次,每一次i都乘于2,所以满足这个条件2^x=n;就可以让程序退出循环。推导可以推出x=logn,所以大O阶为:O(logn)。

 3.最优、平均、最差时间复杂度

时间复杂度就是循环次数,那么最优时间复杂度就是考虑循环次数最少的复杂度,最差时间复杂度就是循环次数最多的复杂度,平均时间复杂度就是取中间值。最差时间复杂度是讨论一个算法必需要衡量的一个复杂度。通常情况下,出现最优、平均、最差时间复杂度是因为排序的次数和需要排序的元素大小有关。上面的几个栗子中,就只有一个算法复杂度。比如插入排序,快排等算法的循环次数就和元素的大小、排列顺序有关,他们就具有最优、平均、最差时间复杂度。

上面的概念是不是感觉有点空洞,下面笔者举了个案例来帮助理解。

def findElement(arr, ele):
    length = len(arr)
    for index in range(length):
        if arr[index] == ele:
            return index
    return -1;

arr = [1,2,3,4]
target = 10
res = findElement(arr ,target)
print(res)

上面的Python脚本,定义了一个 findElement 函数,这个函数会查找指定某个数是否在一个数组中, 如果有这个元素,那么就返回其下表,如果没有就返回 -1 。

首先我们来看时间复杂度吧,相信经过上面的学习,这里对时间复杂度的分析应该就驾轻就熟了,最优的情况就是:每次要找的元素都在数组的第一个位置,所以最优的时间复杂度就是O(1), 而最差的情况就是要找的元素总是在数组最末尾的位置,而 findElement 的查早算法是从第一个元素开始依次遍历所有的元素,所以查早目标元素也就是需要遍历完数组所有的元素,因此最差时间复杂度就是O(n)。

而空间复杂度就比较简单了, findElement 额外分配了一个length变量 和 index 变量,都是integer类型,所使用的空间和 arr的大小 几乎无关联,因此空间复杂度就是O(1)。O(1) 代表了所使用的空间量是一个常数。

4. 常见算法的复杂度图表

下面是常用算法的时间复杂度(最优,最坏,平均)

上面的图表来自 Big-O Algorithm Complexity Chear Sheet (Know Why Complexities!) 。