时间复杂度和空间复杂度
Posted 桐原亮司.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了时间复杂度和空间复杂度相关的知识,希望对你有一定的参考价值。
算法复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢
空间复杂度主要衡量一个算法运行所需要的额外空间
时间复杂度
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
换一种易于理解的说法:算法时间复杂度以算法中基本操作重复执行的次数(简称为频度)作为算法的时间度量。简单来说就是算法中的基本操作的执行次数
大O的渐进表示法
通常用大写O()来体现算法时间复杂度,我们称之为大O的渐进表示法
推导大O阶的方法:
1.用常数1取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。
得到的结果就是大O阶。
下面介绍一下常见的时间复杂度。
常数阶
int a = 10, sum = 0; //执行1次
sum = a + 1; //执行1次
printf("sum = %d", sum);//执行1次
这个算法的执行次数函数是f(n)=3。根据我们推导大O阶的方法,第一步就是把常数项3改为1。在保留最高阶项时发现并没有最高阶项,所以这个算法的时间复杂度为O(1)。
如果这个算法中的语句sun = a + 1有10句,即:
int a = 10, sum = 0; //执行1次
sum = a + 1; //执行第1次
sum = a + 1; //执行第2次
sum = a + 1; //执行第3次
sum = a + 1; //执行第4次
sum = a + 1; //执行第5次
sum = a + 1; //执行第6次
sum = a + 1; //执行第7次
sum = a + 1; //执行第8次
sum = a + 1; //执行第9次
sum = a + 1; //执行第10次
printf("sum = %d", sum);//执行1次
那么这个算法的时间复杂度又该是什么呢?
事实上无论n为多少,上面两段代码仅仅只是3次和12次执行的差异。这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。
不管这个常数是多少,都记作O(1),而不能是O(3)、O(12)等其他任何数字。
对数阶
下面这段代码时间复杂度又是多少呢?
int count = 1;
int n = 50;
while (count < n)
{
count = count * 2;
}
首先两个变量,循环条件 c o u n t < n count<n count<n,每循环一次count的值就会变成原来的二倍,就距离n更近了一步。也就是说,有多少个2相乘后大于n,就会退出循环。设有x个2相乘后跳出循环,由 2 x = n 2^x=n 2x=n得到 x = log 2 n x=\\log_{2}^{n} x=log2n。所以这个算法的时间复杂度为O( log 2 n \\log_{2}^{n} log2n)。称为对数阶。
线性阶
int sum = 0;
for (int i = 0; i < n; i++)
{
sum += 10;
}
分析算法的复杂度,关键就是要分析循环结构的运行情况
这段代码中的循环语句会被执行n次,随着n的增加或减少,执行次数就会发生变化,就是说循环内语句的执行次数会随着n的变化而变化,所以说这段代码的时间复杂度是O(n)。
nlogn阶
for (int i = 0; i < n; i++)
{
int k = 1;
while (k < n)
{
k = k * 2;
}
}
内部while循环我们已经清楚它的时间复杂度为O( log 2 n \\log_{2}^{n} log2n),for循环将此时间复杂度的代码循环n遍,那么它的时间复杂度就是O( n l o g 2 n nlog_{2}^{n} nlog2n)。
平方阶
int sum = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
sum += 10;
}
}
这段代码是一个循环嵌套,内循环刚才分析过是O(n),外层循环每循环一次,内层循环循环n次。所以说对于外层的循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为O( n 2 n^2 n2)。
int sum = 0;
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
sum += 10;
}
}
如果外循环的循环次数变为m,时间复杂度就变为O( m × n m\\times n m×n)。
int sum = 0;
for (int i = 0; i < n; i++)
{
for (int j = i; j < n; j++)
{ //j = i
sum += 10;
}
}
此时内层循环的
j
=
i
j = i
j=i,当
i
=
0
i = 0
i=0时,内层循环执行n次,当
i
=
1
i = 1
i=1时,内层循环执行
n
−
1
n - 1
n−1次,
⋯
⋯
\\cdots\\cdots
⋯⋯当
i
=
n
−
1
i = n - 1
i=n−1时,内层循环执行了一次。
所以总的执行次数为
:
:
:
n
+
(
n
−
1
)
+
(
n
−
2
)
+
⋯
+
1
n + (n - 1) + (n - 2) + \\cdots +1
n+(n−1)+(n−2)+⋯+1
可以看出这是个等差数列,根据等差数列公式可以得出
:
:
:
n
+
(
n
−
1
)
+
(
n
−
2
)
+
⋯
+
1
=
n
(
n
+
1
)
2
=
n
2
2
+
n
2
n + (n - 1) + (n - 2) + \\cdots +1 = \\frac{n(n+1)}{2} = \\frac{n^2}{2} + \\frac{n}{2}
n+(n−1)+(n−2)+⋯+1=2n(n+1)=2n2+2n
这时候就用推导大O阶的方法
:
:
:
第一条,没有加法常数所以不考虑;
第二条,只保留最高阶项,所以保留
n
2
2
\\frac{n^2}{2}
2n2;
第三条,去除这个项相乘的常数,也就是除以
1
2
\\frac{1}{2}
21
最终得出这段代码的时间复杂度为O(
n
2
n^2
n2)。
立方阶
int sum = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
for (int k = 0; k < n; k++)
{
sum++;
}
}
}
最里层for循环循环语句时间复杂度为O(1),循环n次,时间复杂度为O(n),一共有三层循环,所以时间复杂度为O(
n
3
n^3
n3),称为立方阶。
不一定两层循环时间复杂度就是O(
n
2
n^2
n2),三层循环就是O(
n
3
n^3
n3),这里只是两个简单的例子。
指数阶
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
递归实现斐波那契数列的时间复杂度就是指数阶。
假设要计算Fib(5),只需要计算Fib(4)和Fib(3)的和。只需要计算一次即
2
0
2^0
20,但是并不知道Fib(4)和Fib(3)的值,所以还要计算一次Fib(4)和Fib(3),计算两次即
2
1
2^1
21,依次递归下去。
时间复杂度:
2
0
2^0
20 +
2
1
2^1
21 +
2
2
2^2
22 +
⋯
\\cdots
⋯
⋯
\\cdots
⋯ +
2
(
N
−
2
)
2^{(N-2)}
2(N−2) =
2
(
N
−
1
)
−
1
2^{(N-1)}-1
2(N−1)−1。
但是我们可以清楚的看到这并不是一棵满二叉树,即后面部分计算次数并不是
2
3
2^3
23,应该在原式的基础上再减去一个值:
2
(
N
−
1
)
−
1
−
2^{(N-1)}-1-
2(N−1)−1− ____,把这个值减去之后看这个表达式可以发现增长最快的一项就是前面的
2
(
N
−
1
)
2^{(N-1)}
2(N−1),所以时间复杂度就是O(
2
n
2^n
2n)。
除了以上几种时间复杂度外,还有O(n!),O(
n
n
n^n
nn),这两种时间复杂度太大了,除非n是很小的值,否则将会是噩梦般的运行时间。
空间复杂度
算法的空间复杂度通过计算算法所需
以上是关于时间复杂度和空间复杂度的主要内容,如果未能解决你的问题,请参考以下文章