二、算法
数据结构和算法的关系紧密,不可拆开来。
1、定义
算法:解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
2、特征
算法具有五个基本特性:
- 输入
算法具有零个或多个输入 - 输出
算法至少有一个或多个输出 - 有穷性
指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。 - 确定性
算法的每一步骤都具有确定的含义,不会出现二义性。 - 可行性
算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
3、算法设计的要求
要求:
-
正确性
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
算法的 “正确” 通常在用法上有很大的差别,大体分为以下四个层次:- 算法程序没有语法错误。
- 算法程序对于合法的输入数据能够产生满足要求的输出结果。
- 算法程序对于非法的输入数据能够得出满足规格说明的结果。
- 算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。
证明一个复杂算法在所有层次上都是正确的,代价非常昂贵。所以一般情况下,我们把层次 3 作为一个算法是否正确的标准。
-
可读性
算法设计的另一目的是为了便于阅读、理解和交流。
我们写代码的目的,一方面是为了让计算机执行,但还有一个重要的目的是为了便于他人阅读,让人理解和交流,自己将来也可能阅读,如果可读性不好,时间长了自己都不知道写了些什么。可读性是算法(也包括实现它的代码)好坏很重要的标志。 -
健壮性
当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。 -
时间效率高和存储量低
时间效率指的是算法的执行时间,对于同一个问题,如果有多个算法能够解决,执行时间短的算法效率高,执行时间长的效率低。
存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。
设计算法应该尽量满足时间效率高和存储量低的需求。
4、算法效率的度量方法
-
事后统计方法
这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
缺陷:- 必须依据算法事先编制好程序
- 比较依赖计算机硬件和软件等环境因素
- 算法的测试数据设计困难,并且程序的运行时间往往还与测试数据的规模有很大关系,效率高的算法在小的测试数据面前往往得不到体现。
-
事前分析估算方法
在计算机程序编制前,依据统计方法对算法进行估算
一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:- 算法采用的策略、方法。
- 编译产生的代码质量。
- 问题的输入规模。
- 机器执行指令的速度。
抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模,所谓问题输入规模是指输入量的多少。
测定运行时间最可靠的方法就是计算对运行时间有消耗的基本操作的执行次数。运行时间与这个计数成正比。
不关心编写程序所用的程序设计语言是什么,也不关心这些程序将跑在什么样的计算机中,我们只关心它所实现的算法。最终,在分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤。
5、函数的渐近增长
函数的渐近增长:假设两个算法的输入规模都是n,给定两个函数 f ( n ) f ( n ) f(n) 和 g ( n ) g ( n ) g(n), 如果存在一个整数 N N N,使得对于所有的 n > N n > N n>N, f ( n ) f ( n ) f(n) 总是比 g ( n ) g ( n ) g(n) 大,那么,我们说 f ( n ) f (n) f(n) 的增长渐近快于 g ( n ) g ( n ) g(n)。
- 可以忽略这些加法常数。
- 与最高次项相乘的常数并不重要。
- 最高次项的指数大的,函数随着n 的增长,结果也会变得增长特别快。
判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项 (最高阶项)的阶数。
某个算法,随着 n n n 的增大,它会越来越优于另一算法,或者越来越差于另一算法。这其实就是事前估算方法的理论依据,通过算法时间复杂度来估算算法时间效率。
6、算法时间复杂度
-
定义
进行算法分析时,语句总的执行次数 T ( n ) T(n) T(n) 是关于问题规模 n的函数,进而分析 T ( n ) T ( n ) T(n) 随 n n n 的变化情况并确定 T ( n ) T (n) T(n) 的数量级。算法的时间复杂度,也就是算法的时间量度,记作: T ( n ) = O ( f ( n ) ) T(n)= O(f(n)) T(n)=O(f(n)),它表示随问题规模 n n n 的增大,算法执行时间的增长率和 f ( n ) f(n) f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中 f ( n ) f ( n ) f(n) 是问题规模 n n n 的某个函数。
这样用大写 O ( ) O() O()来体现算法时间复杂度的记法,我们称之为大 O O O 记法。
一般情况下,随着 n n n 的增大, T ( n ) T(n) T(n)增长最慢的算法为最优算法。
显然,由此算法时间复杂度的定义可知,我们的三个求和算法的时间复杂度分别为 O ( n ) , O ( 1 ) , O ( n 2 ) O(n),O(1),O(n^2) O(n),O(1),O(n2) 。 -
推导大 O O O 阶方法
推导大 O O O 阶:- 用常数 1 取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是 1, 则去除与这个项相乘的常数。得到的结果就是大 O O O 阶。
-
常数阶
- 与问题的大小无关 ( n n n 的多少),执行时间恒定的算法,我们称之为具有 O ( 1 ) O(1) O(1) 的时间复杂度,又叫常数阶。
- 一步一步执行的,没有其他结构的,程序的时间复杂度就是 O ( 1 ) O(1) O(1)。
- 如下:
int sum = 0,n = 100; /* 执行一次 */ sum = ( 1+n ) *n/2; /* 执行一次 */ printf( "%dM, sum); /* 执行一次 */
-
线性阶
- 分析算法的复杂度,关键就是要分析循环结构的运行情况。
- 嵌套一层循环,循环内是时间复杂度为 O ( 1 ) O(1) O(1) 的程序步骤序列。那么这个程序的时间复杂度就是 O ( n ) O(n) O(n)。
- 如下:
int i; for( i = 0; i < n; i++) { "时间复杂度为 0(1)的程序步骤序列" }
-
对数阶
- 特殊的循环结构,会出现对数阶。
- 如下:
int count = 1; while (count < n) { count = count * 2; /* 时间复杂度为 0(1)的程序步谈序列 */ }
-
平方阶
- 二层循环嵌套,会出现平方阶。
- 如下:
O
(
n
2
)
O(n^2)
O(n2)
如果外循环的循环次数改为了 m m m, 时间复杂度就变为 O ( m × n ) O(m \times n) O(m×n)。int i,j; for( i - 0; i < n; i++) { for (j = 0; j < n; j++) { /*时间复杂度为0(1)的程序步辣序列 */ } }
- 变式
总的执行次数为: n + ( n − l ) + ( n − 2 ) + . . . + l = n 2 / 2 + n / 2 n + (n-l) + (n-2) + ...+l= n^2/2+ n/2 n+(n−l)+(n−2)+...+l=n2/2+n/2根据推导大 O O O 阶的方法,依旧为 O ( n 2 ) O(n^2) O(n2)。int i,j; for( i - 0; i < n; i++) { for (j = i; j < n; j++) { /*时间复杂度为0(1)的程序步辣序列 */ } }
7、常见的时间复杂度
执行次数函数 | 执行次数函数 | 非正式术语 |
---|---|---|
12 12 12 | O ( 1 ) O(1) O(1) | 常数阶 |
2 n + 3 2n+3 2n+3 | O ( n ) O(n) O(n) | 线性阶 |
3 n 2 + 2 n + 1 3n^2+2n+1 3n2+2n+1 | O ( n 2 ) O(n^2) O(n2) | 平方阶 |
5 l o g 2 n + 20 5log_2n+20 5log2n+20 | O ( l o g n ) O(log n) O(logn) | 对数阶 |
2 n + 3 n l o g 2 n + 19 2n+3nlog_2n+19 2n+3nlog2n+19 | O ( n l o g n ) O(n logn) O(nlogn) | $ nlogn$ 阶 |
6 n 3 + 2 n 2 + 3 n + 4 6n^3+2n^2+3n+4 6n3+2n2+3n+4 | O ( n 3 ) O(n^3) O(n3) | 立方阶 |
2 n 2^n 2n | O ( 2 n ) O(2^n) O(2n) | 指数阶 |
常用的时间复杂度所耗费的时间从小到大依次是:
O
(
1
)
<
O
(
l
o
g
n
)
<
O
(
n
)
<
O
(
n
l
o
g
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(1)<O(logn)<O(n)< O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
8、最坏情况与平均情况
在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。
对算法的分析,一种方法是计算所有情况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。
另一种方法是计算最坏情况下的时间复杂度,这种方法称为最坏时间复杂度。一般在没有特殊说明的情况下,都是指最坏时间复杂度。
9、算法空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作: S ( n ) = O ( f ( n ) ) S(n)= O(f(n)) S(n)=O(f(n)), 其中, n n n 为问题的规模, f ( n ) f(n) f(n)为语句关于 n n n 所占存储空间的函数。