目录
1、复杂度
1.1 概述
如何衡量一个算法的好坏呢?——算法的复杂度。
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
1.2 时间复杂度
如:
找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。如下:
void Function(int N) { int count = 0; for (int i = 0; i < N ; ++ i) { for (int j = 0; j < N ; ++ j) { ++count; } } for (int k = 0; k < 2 * N ; ++ k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }
Function 执行的基本操作次数数学表达式:F(N)=N^2+2*N+10,因此,
(1)当 N=10 F(N)=130
(2)当N=100 F(N)=10210
(3)当N=1000 F(N)=1002010
但是在实际中计算时间复杂度时,其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大 O 的渐进表示法。那么什么是大 O 渐近法呢?如下:
1.3 大 O 的渐进表示法
大 O 符号(Big O notation): 是用于描述函数渐进行为的数学符号。
推导大 O 阶方法:
(1)用常数 1 取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大 O 阶。
使用大 O 的渐进表示法以后,上例子的 Function 的时间复杂度为: O(N^2)(1)N=10 F(N)=100(2)N=100 F(N)=10000(3)N=1000 F(N)=1000000
通过上面我们会发现大 O 的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。另外有些算法的时间复杂度存在最好、平均和最坏情况:
(1)最坏情况:任意输入规模的最大运行次数(上界)。(2)平均情况:任意输入规模的期望运行次数。(3)最好情况:任意输入规模的最小运行次数(下界) 。例如: 在一个长度为 N 数组中搜索一个数据 x。(1)最好情况:1次找到。(2)最坏情况:N次找到。(3)平均情况:N/2次找到。那么在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为:O(N)
常见时间复杂度计算举例如下:
(1)
void Function1(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); }
对于 Function1,函数大概的基本执行次数为 M+N ,因为该函数有两个未知数 M 和 N ,所以两项都不能被忽略,通过推导大 O 阶方法,时间复杂度为: O(M+N)
(2)
void Function2(int N) { int count = 0; for (int k = 0; k < 100; ++k) { ++count; } printf("%d\n", count); }
对于 Function2,函数大概的基本执行次数为 100 ,是一个常数,通过推导大 O 阶方法,时间复杂度为: O(1)
1.4 空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时(额外)占用存储空间大小的量度 。空间复杂度不是程序占用了多少字节空间,而是变量(空间)的个数。即使想要计算程序占用了多大空间也没有意义。
空间复杂度计算规则和时间复杂度一样,都是使用大 O 渐进表示法。
空间复杂度计算举例如下:
(1)
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; } }
计算空间复杂度是计算变量(空间)的个数。因为所创建的变量(空间)数为常数,所以空间复杂度为: O(1)
分析如下图:
(2)
long long* Fibonacci(size_t n) { if (n == 0) return NULL; long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n; ++i) { fibArray[i] = fibArray[i - 1] + fibArray[i - 2]; } return fibArray; }
看到函数 Fibonacci 中的 malloc,动态开辟了 N 个空间,所以空间复杂度为:O(N)
(3)
long long Fac(size_t N) { if (N == 0) return 1; return Fac(N - 1) * N; }
函数 Fac 递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为: O(N)
注意:以上的例子中,对空间复杂度的计算,并没有把形参所占的空间给计算进去。为什么呢?原因是:函数的形参是完成算法的条件,并不是额外开辟的一个临时空间。