前言
衡量一个算法是不是一个好算法,一般是看这个算法的时间复杂度和空间复杂度,因此算法学习笔记的首篇会从算法的这两个度量指标开始,然后结合欧拉计划(Project Euler 欧拉计划中文网)里的题目来进一步举例说明。这里使用的语言是C++。
一、时间复杂度
1.概念
时间复杂度(Time Complexity),决定了算法运行时间的长短。一个算法执行所耗费的时间,无法从理论上直接算出,只能通过运行测试才能知道,而且不同性能的计算机得出的结果也会不同,因此该指标不是量化值。
我们只需评估出哪个算法花费时间多,哪个算法花费时间少即可。一个算法花费的时间与要解决的问题复杂程度(语句执行的次数)成正相关。
常见的时间复杂度有O(1)、O(
log
2
n
\log_2 n
log2n)、O(n)、O(n
log
2
n
\log_2 n
log2n)、O(
n
2
n^2
n2)等。
注:由于计算机使用二进制的记数系统,对数常常以2为底(即
log
2
n
\log_2 n
log2n,有时写作lgn),由对数的换底公式,
log
a
n
\log_a n
logan和
log
b
n
\log_b n
logbn只有一个常数因子不同,这个因子在大O记法中被丢弃。因此记作O(
log
n
\log n
logn),而不论对数的底是多少,是对数时间算法的标准记法。
2.实例1
欧拉计划第1题——3或5的倍数。
方法一是暴力解法,直接两个for循环搞定。该方法的时间复杂度为O(n)。
#include <iostream>
using namespace std;
int main() {
int ans = 0;
int sum = 0;
for (int i = 3; i < 1000; i++) {
if ((i % 3 == 0) || (i % 5 == 0)) {
ans += i;
}
}
cout << ans << endl;
}
方法二是根据等差数列公式,直接计算。该方法的时间复杂度为O(4),将常数约掉,时间复杂度为O(1)。
#include <iostream>
using namespace std;
int main(void) {
int ans = 0;
int t3 = (3 + 999) * 333 / 2;
int t5 = (5 + 995) * 199 / 2;
int t15 = (15 + 990) * 66 / 2;
cout << ans << endl;
}
3.实例2
欧拉计划第4题——最大回文乘积。
#include <iostream>
using namespace std;
int fun(int x) {
int raw = x, t = 0;
while(x) {
t = t * 10 + x % 10;
x /= 10;
}
return raw == t;
}
int main(void) {
int ans = 0;
for (int i = 100; i < 1000; i++) {
for (int j = i; j < 1000; j++) {
if (fun(i * j)) {
ans = max(ans, i * j);
}
}
}
cout << ans << endl;
}
main函数中的两个for循环的运行次数为
1
0
n
10^n
10n *
1
0
n
10^n
10n ,在这道题目中是计算三位数的回文数,所以n = 3,如果是计算四位数,则n = 4。
fun函数的参数是两个三位数相乘,两个n位数相乘一般结果为2n位,fun函数中的while循环要计算每一位,因此运行次数为2n。
整个函数的时间复杂度为O(
1
0
n
10^n
10n *
1
0
n
10^n
10n * 2n),简化后时间复杂度为O(
1
0
n
10^n
10n * n)。
4.实例3
举一个时间复杂度为O(n log n \log n logn)的例子。
#include <iostream>
using namespace std;
int main(void) {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j *= 2) {
cout << i * j << endl;
}
}
}
在第二个for循环中,当 j = 0时,j * 2 = 0;
当 j = 1时,j * 2 = 1 * 2 =
2
1
2^1
21;
当 j = 2时,j * 2 = 1 * 2 * 2=
2
2
2^2
22;
当 j = 3时,j * 2 = 1 * 2 * 2 * 2=
2
3
2^3
23;
当 j = m时,j * 2 = 1 * 2 * 2 * 2 * … * 2=
2
m
2^m
2m;
循环继续的条件为
2
m
2^m
2m < n,则 m <
log
2
n
\log_2 n
log2n 。
因此该for循环的运行次数最大为 1 +
log
2
n
\log_2 n
log2n,时间复杂度为O(1+
log
2
n
\log_2 n
log2n),简化为O(
log
n
\log n
logn)。
因此整个程序的空间复杂度为O(n
log
n
\log n
logn)。
二、空间复杂度
1.概念
空间复杂度(Space Complexity),决定了计算时所需的资源(空间资源)。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。这里提到的占用的存储空间用来存储三部分:1.算法本身;2.算法的输入输出数据;3.算法运行过程中临时数据或开辟的空间。
对于第一部分,存储算法本身所占用的存储空间与算法书写的长短成正比。要压缩这方面的存储空间,就要编写短小精悍的算法。
对于第二部分,算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。
对于第三部分,算法在运行过程中临时占用的存储空间因算法而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,称这种算法是“就地”(in-place)进行的,是节约存储的算法。这一部分是设计算法时主要关注的部分。
常见的空间复杂度有O(1)、O(
n
n
n)(一维数组)、O(
n
2
n^2
n2)(二维数组)。
2.实例
欧拉计划第2题——偶斐波那契数。
方法一是暴力解法,开辟一个大数组,将每次计算得到的值依次存入数组中,用于计算后续的值。该方法的空间复杂度为O(n)。
#include <iostream>
using namespace std;
int num[4000005];
int main(void) {
num[1] = 1;
num[2] = 2;
int ans = 2;
for (int i = 3; 1; i++) {
num[i] = num[i - 1] + num[i - 2];
if (num[i] > 4000000) {
break;
}
if (num[i] % 2 == 0)
ans += num[i];
}
cout << ans << endl;
}
方法二,根据定义,斐波那契数列从第3项开始,每一项都等于前两项之和。因此在计算数列中第i个数 n i n_i ni时,真正用的数其实就只有 n i − 1 n_{i-1} ni−1和 n i − 2 n_{i-2} ni−2这两个数。这里也利用了“就地”思想。该方法的空间复杂度为O(2),常数约掉,即为O(1)。
#include <iostream>
using namespace std;
int main(void) {
int a = 1, b = 2, ans = 0;
while (b < 4000000) {
if (b % 2 == 0) {
ans += b;
}
b += a; // b向后移动一位
a = b - a; // a向后移动一位
}
cout << ans << endl;
}