【内容简介】本文整理数据结构(C语言版)相关内容的复习笔记,供各位朋友借鉴学习。本章内容更偏于记忆和理解,请读者们耐心阅读。
数据结构教程 · 绪论(下)
本节学习目标
- 了解算法和算法设计的要求
- 熟悉算法的时间复杂度和空间复杂度的概念和基本计算方式
1.3 算法与算法设计的要求
算法(algorithm)是对特定问题求解步骤的一种描述。它由若干条指令组成,每一条指令代表一个或多个操作。此外,算法有如下 5 个重要特性:
- 有穷性:一个算法必须总是(对任何合法的输入值)在执行有穷步之后结束,且每一步都可在有穷时间内完成。
- 确定性:算法中的每一条指令都必须有确切的含义,读者理解时不会产生二义性。并且在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得到相同的输出。
- 可行性:一个算法是可行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的。
- 输入:一个算法有零个或多个的输入。
- 输出:一个算法有一个或多个的输出。
通常而言,一个好的算法应当达到以下几个目标:正确性、可读性、健壮性、效率高且低存储量需求。其中,我们对健壮性作一个简单阐释,即当输入数据非法时,算法也能适当地做出反应或进行处理,而不会莫名其妙的输出结果。并且处理出错的方法应是返回一个表示错误或错误性质的值,而不是打印错误信息或异常,并中止程序的执行,以便在更高的抽象层次上进行处理。
1.4 算法的时间复杂度
一般而言,度量一个程序(算法)执行时间有两种方法:
1、事后统计的方法 (程序运行完统计时间):这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。
2、事前估算的方法 (程序运行之前算时间) :也就是说,通过分析某个算法的时间复杂度来判断哪个算法更优。
由此,我们可以看出分析一个算法的时间复杂度的意义。下面我们要学习如何分析一个算法的时间复杂度:
1.4.1 时间复杂度的概念
首先分析算法的时间复杂度,使用大O表示法,其核心思想是:所有代码的执行时间与代码的执行次数成正比,可以使用以下公式来表达:
T(n) = O( f(n) )
对该公式的解释如下:
- T(n) 表示算法中语句的执行次数
- n 表示数据规模的大小,数据规模 n 不同,代码的执行时间 T(n) 也会随之而改变
- f(n) 表示算法的基本操作(执行次数最多的那条语句)重复执行的次数
在进行算法分析时,代码的总执行时间 T(n) 是关于问题规模 n 的函数,进而分析 T(n) 随 n 的变化情况并确定 T(n) 的数量级。算法的时间复杂度,记作:T(n)= O(f(n))。它表示随问题规模 n 的增大,算法执行时间的增长率和 f(n) 的增长率相同,大O时间复杂度实际上并不具体表示代码的真正执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以称作算法的渐近时间复杂度,简称为时间复杂度。
1.4.2 分析时间复杂度的过程
首先,我们在计算时间复杂度时需要接受一些必须遵守的原则:
(1)对于一些简单的输入输出语句或赋值语句,近似认为需要O(1)时间。(注意:常数项忽略不计)
(2)对于顺序结构,需要依次执行一系列语句所用的时间可采用大O下“求和法则”(加法原则:总复杂度等于量级最大的代码复杂度,即忽略低阶项,高阶项系数以及常数项)。因此,我们只关注循环次数最多的代码。
(3)对于选择结构,如 if 语句,它的主要时间耗费是在执行 then 字句或 else 字句所用的时间,需注意的是检验条件也需要 O(1) 时间。
(4)对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大O下“乘法法则”(乘法原则:嵌套代码的复杂度等于嵌套内外代码的复杂度乘积)。
(5)对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则计算整个算法的时间复杂度。
根据以上原则,我们可以归纳出计算时间复杂度的基本步骤:
第一步:找出算法中的基本语句(执行次数最多的那条语句),通常是最内层循环的循环体。
第二步:计算基本语句的执行次数的数量级。
第三步:将基本语句执行次数的数量级放入大Ο记号中。
1.4.3 一些时间复杂度的例子分析
一、常数阶 O(1)
int a=3;
int b=4;
int sum=a+b;
总结:只要代码的执行时间不随 n 的增大而增大,这样的代码时间复杂度都为 O(1),一般情况下,只要代码中不存在循环语句、递归语句,即使代码成千上万,都是常数阶。
二、线性阶 O(n)
int sum=0;
for(i=0;i<n;i++){
sum++;
}
三、平方阶 O(n^2)
最常见的平方阶算法即为两个循环嵌套的情况:
int i;
for(i = 0 ; i < n ; i++){
for(j = 0 ; j < n ; j++){
System.out.println("hello");
}
}
以下这段代码的时间复杂度同样为平方阶:
int i;
for(i = 0 ; i < n ; i++){
for(j = i ; j < n ; j++){
System.out.println("hello");
}
}
这段代码的内循环 j 是从 i 开始的,而不是从 0 开始,因此它的总执行时间为:n+(n-1)+(n-2)+...+1 =n^2/2+n/2。忽略低阶项,去掉最高阶系数,得出它的时间复杂度为 O(n^2)。
四、对数阶 O(logn)
int i = 1;
while(i <= n){
i *= 2;
}
这段代码表示:当 x 个 2 相乘大于 n 时,退出循环,即 2^x=n,x=log2(n) ,代码执行次数 x 为log2(n)。
五、线性对数阶 O(nlogn)
for(int i = 0 ; i < n ; i++){
fun (i);
}
void fun(int n){
while(i<n){
i*=2;
}
}
外层循环调用的 fun() 函数时间复杂度为 O(logn),利用乘法原则,代码的时间复杂度为 O(nlogn)
看完上面五个常见的时间复杂度,我们来总结一下:比较一些常见的时间复杂度,有
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < { O(2^n) < O(n!) < O(n^n) }
注意:其中 O(2^n) O(n!) O(n^n) 这三项都是非多项式时间复杂度。当 n 越来越大时,它们的时间复杂度会急剧增长。因此,算法时间复杂度为以上三者时,算法效率奇低。
1.5 算法的空间复杂度
学完时间复杂度之后,我们再来看一下空间复杂度。它的定义为一个算法在运行过程中临时占用存储空间大小的量度,记做
S(n) = O( f(n) )
其中,n 为问题的规模,f(n) 为语句关于 n 所占存储空间的函数。
常见空间复杂度: O(1)、O(n)
一般来说,对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。