数据结构和算法,本身是让计算机,即快又省。
执行效率是一个非常重要的考量指标,需要从时间维度和空间维度来评估数据结构和算法性能。
分别用时间复杂度和空间复杂度来描述性能问题,二者统称为复杂度。
定义:时间复杂度的全称是 渐进时间复杂度, 表示算法的执行时间与数据规模之间的增长关系。
大O复杂度分析法
例子1:
1 int cal(int n) {
2 int sum = 0;
3 int i = 1;
4 for (; i <= n; ++i) {
5 sum = sum + i;
6 }
7 return sum;
8 }
分析: 从 CPU 的角度来看,这段代码的每一行都执行着类似的操作: 读数据,运算,写数据 。尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以可以假设每行代码执行的时间都一样,为 unit_time。在这个假设的基础之上,这段代码的总执行时间是多少呢?
解答:
其中,循环这块,会执行N次,那就是n个unit_time,其他的都是1个unit_time。第二行和第三行还有第七行,都是 1个unit_time
第四行,第五行,因为一直循环,是2n*unit_time
例子2:双重循环
int cal(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum = sum + i * j;
}
}
}
其实我们最关注的的应该是双重循环这块的时间单位,其他的可以忽略,这块就是:n*unit_time * n*unit_time。
两个例子得出的规律:
所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。
其中,T(n) 我们已经讲过了,它表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
这就是 大 O 时间复杂度表示法。
大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势, 所以,也叫作 渐进时间复杂度, (asymptotic time complexity),简称: 时间复杂度。
当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n 2 )。这里的2代表的是n的平方。
1. 只关注循环执行次数最多的一段代码
我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了, 这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
如上,这个例子,就可以排除其他的代码的时间复杂度分析,只关注循环,即复杂度等于:O(n)
2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
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的那个第一个循环可以忽略,第二个循环是o(n),第三个循环是 o(n2)。
为什么第一个循环100的可以忽略,不计算时间复杂度呢?
即便这段代码循环 10000 次、100000 次,只要是一个已知的数,跟 n 无关,照样也是常量级的执行时间。当 n 无限大的时候,就可以忽略。尽管对代码的执行时间会有很大影响,但是回到时间复杂度的概念来说,它表示的是一个算法执行效率与数据规模增长的变化趋势,所以不管常量的执行时间多大,我们都可以忽略掉。因为它本身对增长趋势并没有影响。
总的时间复杂度就等于:o(n2)【2是平方】
3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
int cal(int n) {
int ret = 0;
int i = 1;
for (; i < n; ++i) {
ret = ret + f(i);
}
}
int f(int n) {
int sum = 0;
int i = 1;
for (; i < n; ++i) {
sum = sum + i;
}
return sum;
}
cal的时间复杂度是:o(n), f函数时间复杂度是:o(n),相乘就是:o(n2)
1. O(1) 复杂度
一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)
2. O(logn)、O(nlogn)
i=1;
while (i <= n) {
i = i * 2;
}
从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:
所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2x =n 求解 x, x=log2n 时间复杂度 = o(log2n),其实,在时间复杂度真正使用的时候,我们都会忽略对数x,那么就是:o(logn)。
3. O(m+n)、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,两个数据规模,但是无法评估m和n的大小,此时的复杂度,应该是两个的乘积:o(m*n)。
时间复杂度随着数据规模增长的趋势图:
1.说明:文章的内容通过阅读《极客时间-数据结构与算法专栏》综合整理而来的读书笔记。
2.各个数据结构与算法复杂度速查表:https://liam.page/2016/06/20/big-O-cheat-sheet/