一、课程目标
- 算法的目标
- 算法复杂度
- 大O函数
- 常见大O函数
二、目标详解
1、算法的目标
算法是对问题的解决方案,但一个问题会有很多种算法,通常一个好的算法需要具备以下目标:
- 正确性:对合法输入、非法输入、边界输入都能正确处理,输出合理的结果。
- 可读性:算法应该描述清晰,方便阅读、理解和交流。
- 健壮性:算法应运行一致,对于相同的输入始终输出相同的结果。
- 高效性:算法应占用最少的cpu和内存得到满足的结果,这通过时间复杂度和空间复杂度进行判定。
2、算法复杂度
算法复杂度用来衡量算法的高效性,简单的说就是:
- 算法运行的有多快(时间效率)
- 内存占用的有多少(空间效率)
然而,运行时间和语言、机器、机器的状态、数据量的大小都有关系,不好横向比较,为此通常使用一个时间复杂度(相对度量)的概念来衡量算法有多快。
我们假设其它状态不变,仅当问题规模(数据大小)增长时,指令执行的次数也在增长,那么指令执行次数相对于问题规模来说,会构成一个函数T(n)。
例如-对于以下数组求和的算法1+2+3+…+n:
int sum = 0; //指令数为1
for(int i=0; i<n; i++)
sum += n; //指令数为 n
cout << n; //指令数为1
显然,总的指令数为T(n) = n + 2
3、大O函数
假设一个算法的T(n) = 4n^3 - 2n + 5
当n越来越大时,对于T(n)增长的贡献来说,最高阶的n^3会占据主导地位,其它项可被忽略。
例如:
- n=100时,n^3是n的的1万倍,因此可忽略掉n的贡献。
- 当n从100变成1000时,n^3会增长1000倍,此时4n^3前面的4也可倍忽略。
我们一般用大O函数来表示最主要的贡献部分:O(T(n)) = O(n^3),也即算法的时间复杂度。
数学定义
:当存在正常数c和某个规模n0,如果对所有的n>=n0,都有f(n) <= c T(n),则称f(n)为T(n)的大O函数,写成:f(n) = O(T(n))。
4、常见大O函数
函数 | 名称 | 例子 |
---|---|---|
O(1) | 常数阶 | 交换算法 |
O(logn) | 对数阶 | 二分查找算法 |
O(n) | 线性阶 | 求和算法 |
O(nlogn) | 线性对数阶 | 快速排序算法 |
O(n^2) | 平方阶 | 冒泡排序算法 |
O(n^c) | 多项式阶(c>1) | 多重循环的算法 |
O(c^n) | 指数阶 | 汉诺塔问题 |
O(n!) | 阶乘阶 | 旅行商问题 |
三、扩展理解-复杂度计算与例子
1、计算方法
对算法(或代码)的指令次数进行计算组成T(n),只保留最高阶项,然后去掉最高阶项前面的常数。
例如以下代码的T(n) = 3, 时间复杂度为O(1):
int a=20;
int b = a*3 + 4;
cout << b;
2、O(n)的例子
输出数组元素:
for(int i=0; i<n; i++)
cout << a[n] << " ";
3、 O(logn)的例子
给定n,求2的指数p,使得p <= n < 2p
int p = 1;
while(p < n) {
p *= 2;
}
cout << p;
4、O(n^2)的例子
打印二维数组:
for(int i=0; i<n; i++) {
for(int j=0; j<n; j++)
cout << a[i][j] << " ";
cout << endl;
}
5、O(nlogn)的例子
for(int i=0; i<n; i++)
for(int j=0; j<n; j *= 2)
...
6、O(2^n) 的例子
汉诺塔问题-代码略。
递归表达式:
- 将n-1个盘子从A经过C移动到B
- 将第n个盘子从A移动到C
- 将n-1个盘子从B经过A移动到C
显然T(n) = 2T(n-1) + 1 = 2(2T(n-2) + 1) + 1 = ….,最高阶项为2^n,即O(2^n)。
7、O(n!)的例子
旅行商问题:从一个城市出发,经过所有城市后返回出发地,求最短的路径。
如果用朴素算法,第一个城市有n种选择,第二个有n-1种选择,依次类推,复杂度为O(n!)。
8、空间复杂度
空间复杂度指算法运行过程种临时所占用的内存大小,例如变量、数组等的开销,规则与时间复杂度一样。
在noi竞赛里,对于内存限制一般为128M(或256M),一般都足够使用。