时空复杂度分析
时空复杂度分析
数据结构和算法要解决的问题就是让代码运行得更快 , 让存储空间占得更少 , 所以时间和空间复杂度的分析是很重要的 .
那么究竟该如何分析 ?
大 O 复杂度表示法
我们经常会看到 O(1) O(n) O(logn) 这些时空复杂度的表示
其实这就是 大 O 复杂度表示法
, 它并不表示代码执行真正需要的时间 , 而是一种随着数据规模增长的趋势 , 其中 n 就是数据规模的大小 .
比如 n = 100 , 那么 O(n) 时间复杂度需要的时间可以假设为 100 s , 而 O(logn) 时间复杂度需要的时间就是 10 s , 差距一下就体现出来了 .
看下面这段代码
int cal(int n)
{
int sum = 0;
int i = 1;
for (; i <= n; ++i)
{
sum = sum + i;
}
return sum;
}
功能是求 1 - n 的累加和, 假设执行一行代码的时间为一个 run_time
那么 3 4 两行代码执行用时为 2*run_time
而 5 7 两行代码都执行了 n 次, 所以需要的时间为 2n*run_time
于是整个代码运行的时间 T(n) = (2+2*n)*run_time
从这里我们可以看出, 整个代码运行的时间 T(n) 和每行代码运行的次数 n 成正比
设 f(n) = 2+2*n
, 即每行代码运行次数的总和, 因为我们只是估算一个增长趋势, 而常数和系数不会对增长趋势产生什么影响, 所以可以简写为 f(n) = n
则有 T(n) = O(f(n))
, 即表示: 代码执行时间T(n)和代码执行次数f(n)成正比
则上面代码的时间复杂度可以表示为 O(n)
如何快速估算一段代码的时间复杂度?
1, 只关注循环执行次数最多的一段代码
2, 总的复杂度 = 复杂度量级最高的那段代码的复杂度
3, 嵌套代码的复杂度 = 嵌套内外代码复杂度的乘积
举几个例子说明一下:
int cal(int n)
{
int sum_1 = 0;
int p = 1;
for (; p <= 100; ++p)
{
sum_1 = sum_1 + p;
}
int sum_2 = 0;
int q = 1;
for (; q <= n; ++q)
{
sum_2 = sum_2 + q;
}
int sum_3 = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i)
{
j = 1;
for (; j <= n; ++j)
{
sum_3 = sum_3 + i * j;
}
}
return sum_1 + sum_2 + sum_3;
}
这段代码有三个循环
第一个循环固定执行 100 次
第二个循环执行 n 次
第三个双重循环执行 n*n 次
所以时间复杂度为 O(100 + n + n*n)
, 省略常数之后为 O(n + n*n)
但我们一般只关注最高阶, 即时间复杂度为 O(n^2)
再来看看下面这个代码
int f(int n)
{
int sum = 0;
int i = 1;
for (; i < n; ++i)
{
sum = sum + i;
}
return sum;
}
int cal(int n)
{
int ret = 0;
int i = 1;
for (; i < n; ++i)
{
ret = ret + f(i);
}
}
在cal函数中, 第18行代码执行了n次, 但是它每次都调用了f(int)函数, 在f(int)函数中, 第7行代码要执行n次, 所以第7行代码一共执行了n*n次, 所以函数cal的时间复杂度是O(n^2)
其实在实际使用中, 时间复杂度的量级就那么几个, 按从小到大排有
常数级: O(1)
对数级: O(logn)
线性级: O(n)
线性对数级: O(nlogn)
平方级: O(n^2)
立方级: O(n^3) . . . O(n^k)
指数级: O(2^n)
阶乘级: O(n!)
对于这些时间复杂度量级, 可以分为两类:多项式量级和非多项式量级
其中非多项式量级只有: O(2^n)和O(n!)
我们把时间复杂度为非多项式量级的算法问题称为 NP(非确定多项式)问题, 当数据规模n越来越大时, 非多项式算法的执行时间会急剧增长, 因此时间复杂度为非多项式量级的算法是非常低效的!
对于O(1)时间复杂度, 只要执行时间不随n增大而增长, 都可以称为O(1)时间复杂度, 也就是说, 只要代码里没有循环和递归, 即使有成千上万行代码, 时间复杂度也是O(1)
对于O(logn)和O(nlogn), 这是很常见的时间复杂度量级, 同时也是最难分析的
i = 1;
while (i <= n)
{
i = i * 2;
}
例如这个代码, i从1开始, 每次增长2倍, 即 2^0, 2^1, 2^2, 2^3 . . . 2^x = n
x即为第4行代码执行的次数, 可得 x = log2n, 所以这段代码时间复杂度为O(log2n)
我们把代码变一下
i = 1;
while (i <= n)
{
i = i * 3;
}
可以得到时间复杂度为O(log3n)
其实, 无论底数为多少, 我们记做O(logn), 因为对数之间可以相互转换, 即
log3n = log32*log2n
其中 log32是常数, 可以省略.
还有一种表示时间复杂度O(m+n)
int cal(int m, int n)
{
int sum_1 = 0;
int i = 1;
for (; i < m; ++i)
{
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j = 1;
for (; j < n; ++j)
{
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
因为我们无法预估m和n的大小, 所以时间复杂度记为O(m+n)
空间复杂度
相对于时间复杂度, 空间复杂度的分析就容易多了.
空间复杂度全称是渐进空间复杂度, 表示算法的存储空间和数据规模之间的增长关系
常用的空间复杂度量级有O(1), O(n), O(n^2)
void print(int n)
{
int i = 0;
int* a = new int[n];
for (; i < n; ++i)
{
a[i] = i * i;
}
for (i = n - 1; i >= 0; --i)
{
std::cout << a[i] << "\n";
}
}
在这个代码中, 我们申请了一个int型变量存储i, 它的大小是固定的, 不随n改变而改变;还申请了一个大小为n的int型数组, 它的大小随n的改变而改变, 所以这个代码的空间复杂度为O(n).
我们最常见的复杂度量级其实只有五种
O(1), O(logn), O(n), O(nlogn), O(n^2)