时间复杂度与空间复杂度
引言
一个算法的优劣主要取决于执行时间和所占用的存储空间,即时间复杂度T(n)和空间复杂度S(n)。
一、O渐进表示法(重点)
- 1.定义:在一个算法语句总的执行次数总是关于问题规模N的某个函数,我们把它记为f(N),N称为问题的规模。语句总的执行次数记为T(N),当N不断变化是,T(N)也在变化,算法执行次数的增长速率和f(N)的增长速率相同。则有T(N)=O(f(N)),称O(f(N))为时间复杂度O的渐进表示法。
2.计算方法: - ①先找次数
- ②O( 次数)
- ③用常数1取代运行时间中的所有常数
- ④保留最高阶
- ⑤丢掉常数C
二、时间复杂度(必考点)
1.定义
时间复杂度即通常所说的算法执行所需要耗费的时间,时间越短,算法越好。它反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好地反映出算法的优劣与否。但是,一个算法的执行时间往往无法精确估计。通常需要在实际的计算机运行才知道具体的执行时间。但是也可以大致进行估计,得到算法的时间复杂度。算法的执行时间往往和算法代码中语句执行的数量有关。由于一段代码中,每条语句的执行都需要时间,因此,可以这么认为,代码执行次数越多,程序耗费的时间越长,效率越差。因此,我们需要多写一些短小精悍的代码来提高代码的执行效率,这就是能区别一个程序员功底如何的所在之处。
2.常见的时间复杂度有
常数阶O(1),
对数阶O(log2 n),
线性阶O(n),
线性对数阶O(n log2 n),
平方阶O(n^2),
立方阶O(n^3)
k次方阶O(n^K),
指数阶O(2^n)。
随着n的不断增大,时间复杂度不断增大,算法花费时间越多。
3.计算方法
- ①找出算法中的基本语句
算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。 - ②计算基本语句的执行次数的数量级
只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。 - ③ 用大Ο记号表示算法的时间性能
将基本语句执行次数的数量级放入大Ο记号中。如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加
实例1
// 计算Func1的时间复杂度?
void Func1(int N) {
int count = 0;
for (int k = 0; k < 2 * N ; ++ k) {
++count; }
int M = 10;
while (M--) {
++count; }
printf("%d\n", count);
}
实例2
// 计算Func2的时间复杂度?
void Func2(int N, int M) {
int count = 0;
for (int k = 0; k < M; ++ k) {
++count; }
for (int k = 0; k < N ; ++ k) {
++count; }
printf("%d\n", count);
}
实例三
// 计算Func3的时间复杂度?
void Func3(int N) {
int count = 0;
for (int k = 0; k < 100; ++ k) {
++count; }
printf("%d\n", count);
}
实例四
// 计算斐波那契递归Fibonacci的时间复杂度?
long long Fibonacci(size_t N) {
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
- 分析:
1.实例1基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)
2. 实例2基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)
3. 实例3基本操作执行了10次,通过推导大O阶方法,时间复杂度为 O(1)
4. 实例4通过计算分析发现基本操作递归了2^N次,时间复杂度为 O(2^N)。(建议画图递归栈帧的二叉树理解)
4.最坏时间复杂度和平均时间复杂度
-
(1)最坏情况下的时间复杂度称最坏时间复杂度 一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。
这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。 -
在最坏情况下的时间复杂度为T(n)=0(n²),它表示对于任何输入实例,该算法的运行时间不可能大于0(n²)。
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。指数阶0(2n),显然,时间复杂度为指数阶0(2n)的算法效率极低,当n值稍大时就无法应用。
对于时间复杂度的分析,一般是这两种方法:
(2)最坏时间复杂度
最坏情况运行时间 通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间
对于追问为什么是最坏时间复杂度的好奇宝宝: 如果最差情况下的复杂度符合我们的要求,我们就可以保证所有的情况下都不会有问题。
也许你觉得平均情况下的复杂度更吸引你(见下),但是:第一,难计算第二,有很多算法的平均情况和最差情况的复杂度是一样的.
第三,而且输入数据的分布函数很可能是你没法知道。
(2)平均时间复杂度
平均时间复杂度也是从概率的角度看,更能反映大多数情况下算法的表现。当然,实际中不可能将所有可能的输入都运行一遍,因此平均情况通常指的是一种数学期望值,而计算数学期望值则需要对输入的分布情况进行假设。平均运行时间很难通过分析得到,一般都是通过运行一定数量的实验数据后估算出来的。
三、空间复杂度(重点)
1.定义
- ①空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,通常来说,只要算法不涉及到动态分配的空间,以及递归、栈所需的空间,空间复杂度通常为0(1);
- ②一个算法在计算机上占用的内存包括:程序代码所占用的空间,输入输出数据所占用的空间,辅助变量所占用的空间这三个方面,程序代码所占用的空间取决于算法本身的长短,输入输出数据所占用的空间取决于要解决的问题,是通过参数表调用函数传递而来,只有辅助变量是算法运行过程中临时占用的存储空间,与空间复杂度相关;
- ③对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。
2.计算方法
- ①忽略常数,用O(1)表示。
- ②递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间。
- ③对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程。
实例1
// 计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N) {
return N < 2 ? N : Factorial(N-1)*N; }
实例2
// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n) {
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
分析
- 实例1递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N).
- 2. 实例2使用了常数个额外空间,所以空间复杂度为 O(1)。
一些常见排序算法的复杂度与稳定性: