衡量算法效率的方法:时间复杂度、空间复杂度

衡量算法效率的方法:时间复杂度、空间复杂度

一、好算法的特点

算法是用数学解决问题的方法。一个好算法有以下几个特点:

①正确性:能正确处理各种输入(合法输入、非法输入、边界输入),输出合理的结果。

②可读性:算法描述清晰,方便阅读、理解。

③健壮性:算法应运行一致,对于相同的输入始终输出相同的结果。

④高效性:算法应占用最少的CPU和内存,这一点通过时间复杂度和空间复杂度进行判定。

其中,高效性是优秀算法最突出的特点,也是算法设计的核心。

二、算法效率分析

算法的效率是用算法复杂度来衡量的,有两个方面:

  • 时间效率:算法运行有多快。
  • 空间效率:内存占用有多少。

1. 时间复杂度

算法的运行时间与编程语言、机器配置、机器的状态、数据量的大小都有关系,单纯比较运行时间并不能准备地衡量出算法效率。所以,应该找到一项时间效率的评判标准,只与算法本身有关,而与刚刚提到的那些臭氧层子无关。

由此,提出了时间复杂度的概念,简单来说,就是算法在运行时间层面的复杂程度。什么东西最能直接地体现出算法复不复杂呢?

我们知道,程序=指令+数据,指令就是命令,它是算法的核心。显然指令的数量越多,算法越复杂。我们可以用函数来表示指令的多少。
(1)大T函数T(n)

当只考虑问题规模(要处理的数据量的多少)时,指令执行的次数会随着问题规模的增大而增加,那么指令执行次数相对于问题规模来说,会构成一个函数T(n)。

例如,对于以下数组求和的算法1+2+3+...+n:

cpp 复制代码
int sum = 0;    //指令数为1
for(int i=0; i<n; i++)
    sum += n;   //指令数为n
cout << n;        //指令数为1

显然,总的指令数为T(n)=n+2。

虽然这种计算效率的方法准确度很高,但是太复杂了,对平时的算法效率分析来讲,这样做不仅费时费力,而且完全没有必要。
(2) :大O函数O(n)

大O函数是时间复杂度的一种估算方法。具体来说,它计算的是指令数量的级别,而不是准确的指令数量。就像开车一样,大T函数相当于计算车的速度,而大O函数相当于计算车的档位,显然后者要简单的多。

比如某算法的T(n)=9n^3^+5n+2。对于整个团队T(n)的贡献,最大的主力是最高次数的n^3^。当n越来越大时,其他项基本可以被忽略。

例如:当n=100时,n^3^是n的1万倍,因此可忽略5n的贡献。

当n从100变成1000时,n^3^会增长1000倍,此时9n3前面的系数9也不够看了,可被忽略。

我们常把这个主力单独拎出来衡量时间复杂度,用大O函数表示。前面的T(n)简化为O(T(n))=O(n^3^)。

大O函数的数学定义:存在正常数c和某个规模n0,如果对所有的n≥n0,都有f(n)≤cT(n),则称f(n)为T(n)的大O函数,写成f(n)=O(T(n))。

计算方法:对算法(或代码)的指令次数进行计算组成T(n),只保留最高次项,并去掉最高次项前面的常数。
(3) :常见大O函数

常见大O函数的按时间由少到多排序如下:

代码示例:
O(n) 的例子:输出数组元素。

cpp 复制代码
for(int i=0; i<n; i++)
    cout << a[n] << " ";

O(log n) 的例子:给定n,求p,使得p≤n<2p。

log n是 log ⁡ 2 n \log_2{n} log2n的简写,即以2为底n的对数。

cpp 复制代码
int p = 1;
while(p < n) {
    p *= 2;
}
cout << p;

O(n^2^) 的例子:典型的就是二重循环,如打印二维数组。

cpp 复制代码
for(int i=0; i<n; i++) {
    for(int j=0; j<n; j++)
        cout << a[i][j] << " ";
    cout << endl;
}

O(nlog n) 的例子:最典型的例子是快速排序,但这个例子比较复杂,这里只搞一个简明阉割版示意一下。

cpp 复制代码
for(int i=0; i<n; i++) //第一重循环,复杂度O(n)
    for(int j=0; j<n; j *= 2) //第二重循环,复杂度O(log n)
        ...

O(2^n^) 的例子:汉诺塔问题。

递归表达式如下:

1)将n-1个盘子从A经过C移动到B。

2)将第n个盘子从A移动到C。

3)将n-1个盘子从B经过A移动到C。

显然T(n)=2T(n-1)+1=2(2T(n-2)+1)+1=...,最高阶项为2^n^,即O(2^n^)。
O(n!) 的例子:旅行商问题,即从一个城市出发,经过所有城市后返回出发地,求最短的路径。

第一个城市有n种选择,第二个有n-1种选择,以此类推,复杂度为O(n!)。

2. 空间复杂度

空间复杂度是对一个算法在运行过程中占用内存空间大小的度量。

一个算法在计算机存储器上所占用的存储空间包括:

①代码本身占用空间。

②输入输出数据占用空间。

③过程占用空间。即程运行过程中临时占用的存储空间。

第②项取决于问题要求,不能改变;第①项取决于代码长度,影响也不大。

算法的输入输出数据所占用的存储空间是由要解决的问题决定的,它不随算法的不同而改变。

算法在运行过程中临时占用的存储空间因算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是"就地"进行的,是节省存储空间的算法;有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,如归并排序。

分析一个算法所占用的存储空间要从各方面综合考虑。如对于递归算法来说,一般都比较简短,算法本身所占用的存储空间较少,但运行时需要一个附加栈,从而占用较多的临时工作单元;若写成非递归算法,一般可能比较长,算法本身占用的存储空间较多,但运行时可能需要较少的存储单元。
(1)S(n)

一个算法的空间复杂度只考虑在运行过程中为变量分配的存储空间的大小,记为S(n),它包括:

1)函数参数表中形参变量分配的存储空间。

2)函数体中定义的局部变量分配的存储空间。

若一个算法为递归算法,其空间复杂度为递归所使用的栈空间的大小,它等于一次调用所分配的临时存储空间的大小乘以被调用的次数。
(2)大O函数

与时间复杂度类似,算法的空间复杂度一般也以数量级的形式(O(n))给出。

一般而言:

1)当一个算法的空间复杂度S(n)为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1)。(如冒泡排序。)

2)当一个算法的空间复杂度S(n)与以2为底的n的对数成正比时,可表示为O(log n)。

3)当一个算法的空间复杂度S(n)与n成线性比例关系时,可表示为O(n)。(如归并排序。)

特殊的:

1)若形参为数组,则只需要为它分配一个用于存储由实参传送来的一个地址指针的空间,即一个机器字长空间。

2)若形参为引用方式,则也只需要为其分配用于存储一个地址的空间,即用它来存储对应实参变量的地址,以便由系统自动引用实参变量。

在竞赛里,内存限制一般为128MB(或256MB),一般都足够用。

相关推荐
kk无敌怕21 分钟前
分布式主键ID生成方式-snowflake雪花算法
分布式·算法
Fishel-1 小时前
线性回归api再介绍
算法·回归·线性回归
勤劳的进取家1 小时前
协方差矩阵
线性代数·算法·机器学习·矩阵
野生派蒙2 小时前
排序算法:冒泡排序
java·数据结构·算法·排序算法
晨辉软件2 小时前
晨辉面试抽签和评分管理系统之三:考生批量抽签
算法·面试
2401_843785234 小时前
C语言 扫雷程序设计
c语言·算法
qystca4 小时前
炸弹 (boom.c)
算法
pen-ai4 小时前
【LeetCode】4. 去重的效率提升
算法·leetcode·哈希算法
Tisfy4 小时前
LeetCode 3019.按键变更的次数:遍历(转小写)
算法·leetcode·字符串·题解·遍历
无限码力4 小时前
目标值子矩阵的数量
c++·算法·矩阵