第一章 绪论
1.1 什么是数据结构
数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及它们之间的关系和操作等的学科。
1.1.1 数据结构的定义
数据(data):描述客观事物的数和字符的集合。
数据元素(data element):作为数据的基本单位。(数据元素=数据项+数据对象+数据结构)
数据项(data item):具有独立含义的数据最小单元,也称字段或域。
数据对象(data object):性质相同的数据元素的集合,它是数据的一个子集。
数据结构(data structure):所有数据元素以及数据元素之间的关系,可以看作是相互之间存在着某种特定关系的数据元素的集合。
三者之间的关系:数据对象>数据元素>数据项
【例】班级通讯录>个人记录>姓名、年龄……
数据结构的三要素:
- 逻辑结构:由数据元素之间的逻辑关系构成。
- 存储结构: 数据元素及其关系在计算机存储器中的存储表示,也称为数据的物理结构。
- 运算:施加在改数据上的操作。
数据结构研究的内容
逻辑结构:线性结构(线性表、栈、队列)、非线性结构(树形结构、图形结构)
存储结构:顺序结构、链式结构、索引结构、散列结构
数据运算:检索、排列、插入、删除、修改等
1.1.2 逻辑结构
指数据元素之间的逻辑关系。即从逻辑关系上描述数据,他与数据的存储无关,是独立于计算机的。
1.逻辑结构的表示
-
图表表示
-
二元组表示
2.逻辑结构的类型
-
集合:数据元素之间除了“同属于一个集合”的关系以外别无其他关系。
-
线性结构:数据元素之间存在一对一的关系。
-
树形结构:数据元素之间存在一对多的关系。
-
图形结构:数据元素之间存在多对多的关系。
1.1.3 存储结构
存储结构也称物理结构,是数据的逻辑结构在计算机存储器内的表示(或映像),它依赖于计算机。
1.顺序存储结构
采用一组连续的存储单元存放所有的数据元素,借助元素在存储器中的相对位置来表示数据元素间的逻辑关系。
2.链式存储结构
每个逻辑元素用一个内存结点存储,每个结点单独分配,借助元素存储地址的指针表示数据元素间的逻辑关系
3.索引存储结构
在存储元素信息的同时还建立一个附加的索引表
4.散列存储结构
根据元素的关键字通过哈希(或散列)函数直接计算出一个值,并将这个值作为该元素的存储地址。
1.1.4 数据运算
数据运算是指对数据实施的操作。在数据的逻辑结构上定义的操作算法,在数据的存储结构上实现。
最常用的数据运算:查找、插入、修改、删除、排序
1.1.5 数据类型和抽象数据类型
抽象数据类型和伪码是学习数据结构的工具
1.数据类型
一组性质相同的值的集合和定义在此集合上的一组操作的总称,是某种程序设计语言中已实现的数据结构。
【例】C语言中的整型、浮点数、字符型、双精度型
2.抽象数据类型
用户定义,从问题的数学模型中抽象出来的逻辑数据结构和运算,不考虑具体的存储结构和运算的具体实现算法。由基本的数据类型组成,并包括一组相关的操作。
数据类型与抽象数据类型的区别:抽象数据类型与数据类型实质上是一个概念,但其特征是使用与实现分离,实行封装和信息隐藏。
抽象数据类型的基本描述格式:
ADT抽象数据类型名
{ 数据对象:数据对象的声明
数据关系:数据关系的声明
基本运算:基本运算的声明
}ADT抽象数据类型名
1.2 算法及其描述
1.2.1 什么是算法
算法是对特定问题求解步骤的一种描述,它是指令的有限序列。
算法的5个特性:
- 有穷性:一个算法无论在什么情况下都应在执行有穷步后结束,每一步都在有穷时间内完成。
- 确定性:算法的每一步都应确切的、无歧义的定义。对于每一种情况,需要执行的动作应严格的、清晰的规定,不能有二义性。
- 可行性:算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现。
- 有输入:一个算法必须有0个或多个输入。用函数描述算法时,输入往往是通过形参表示的,在被调用时,从主函数获得输入值。
- 有输出:一个算法应有一个或多个输出,输出的量是算法计算的结果。无输出的算法没有任何意义,输出一般多用返回值或引用类型的形参表示。
【例】考虑下面两段描述:
(1) 描述一
void exam1()
{
int n=2;
while(n%2==0)
n=n+2;
printf("%d\n",n);
}
(2) 描述二
void exam2()
{
int x,y;
y=0;
x=5/y;
printf("%d,%d\n",x,y);
}
这两段描述均不能满足算法的特征,试问它们违反了哪些特征?
【解】
(1) 算法是一个死循环,违反了算法的有穷性特性。
(2) 算法包含除零错误,违反了算法的可行性特征。
算法和程序的不同:
- 算法必须满足有穷性,而程序不一定满足有穷性。(操作系统不是一个算法)
- 算法侧重于对解决问题的方法描述,即要做什么。程序是指使用某种计算机语言对一个算法的在计算机上的具体实现,即具体要怎么做。
- 一个算法若用程序设计语言来描述,则它就是一个程序。
1.2.2 算法设计目标
算法设计应满足以下几个目标:
- 正确性:能够正确地执行预先规定的功能和性能要求。
- 可使用性:要求算法能够狠方便地使用。(用户友好性)
- 可读性:程序可读性好,易于人对算法的理解。要求算法必须是清晰的、简单的和结构化的。
- 健壮性:要求算法具有很好的容错性,即提供异常处理,能够对不合理的数据进行检查,不经常出现异常中断或死机现象。
- 高效率与低存储量需求:低时间复杂度,低空间复杂度
1.2.3 算法描述
【例】设计一个算法,求一元二次方程 a x 2 + b x + c = 0 ax^2 + bx + c = 0 ax2+bx+c=0 的根。
int solution(double a,double b,double c,double &x1,double &x2)
{
double d;
d=b*b-4*a*c;
if(d>0)
{
x1=(-b+sqrt(d))/(2*a);
x1=(-b-sqrt(d))/(2*a);
return 2; //两个实根
}
else if(d==0)
{
x1=(-b)/(2*a);
retun 1; //一个实根
}
else
return 0; //不存在实根,返回0
}
1.3 算法分析
1.3.1 算法分析概述
算法分析就是分析算法占用计算机资源的多少。占用CPU时间的多少称为时间性能分析,占用内存空间的多少称为空间性能分析。
算法分析的目的:分析算法的时空性能以便改进算法。
1.3.2 算法时间性能分析
1.两种算法时间性能分析方法
事后统计法:编写算法对应程序,统计其执行时间。
事前估计法:仅考虑算法本身的效率高低,算法的“运行工作量”的大小只依赖与问题的规模(通常用整数 n n n 表示),算法的执行时间是问题规模函数。
2.算法时间复杂度分析
- 计算算法的频度 T ( n ) T(n) T(n)
一个算法是由控制结构(顺序、分支、循环)和原操作(固有数据类型的操作)构成,算法的执行时间取决于控制结构和原操作的综合效果。
算法时间分析就是求出算法所有原操作的执行次数(也称频度),它是问题规模 n n n 的函数,用 T ( n ) T(n) T(n) 表示。
【例】求两个 n n n 阶方阵 A 、 B A、B A、B 相加 C = A + B C = A + B C=A+B 的算法如下,计算其执行时间 T ( n ) T(n) T(n)。
#define MAX 20 //定义最大的方阶
void matrixadd(int n,int A[MAX][MAX],int B[MAX][MAX],int C[MAX][MAX])
{
int i,j;
for(i=0;i<n;i++) //语句1
for(j=0;j<n;j++) //语句2
C[i][j]=A[i][j]+B[i][j]; //语句3
}
【解】
语句1: i i i 从 0 到 n n n ⇒ 频度 = n + 1 =n+1 =n+1
语句2:语句2作为语句1循环体内的语句,执行 n n n 次;语句2本身需要执行 n + 1 n+1 n+1 次 ⇒ 频度 = n ( n + 1 ) =n(n+1) =n(n+1)
语句3:同理 ⇒ 频度 = n 2 =n^2 =n2
该算法中所有语句的频度之和(执行时间)为: T ( n ) = n + 1 + n ( n + 1 ) + n 2 = 2 n 2 + 2 n + 1 T(n)=n+1+n(n+1)+n^2=2n^2+2n+1 T(n)=n+1+n(n+1)+n2=2n2+2n+1
- T(n) 用"O"表示
算法分析不是绝对时间的比较,在求出 T(n) 后,通常进一步采用时间复杂度来表示。算法时间复杂度用 T ( n ) T(n) T(n) 的数量级来表示,记做 T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))。
【例】 T ( n ) = 2 n 2 + 2 n + 1 = O ( n 2 ) T(n)=2n^2+2n+1=O(n^2) T(n)=2n2+2n+1=O(n2) 该算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
时间复杂度 T ( n ) T(n) T(n) 按数量级递增顺序为:
常数阶 | 对数阶 | 线性阶 | 线性对数阶 | 平方阶 | 立方阶 | …… | k次方阶 | 指数阶 | 阶乘 |
---|---|---|---|---|---|---|---|---|---|
O ( 1 ) O(1) O(1) | O ( l o g 2 n ) O(log_2n) O(log2n) | O ( n ) O(n) O(n) | O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n 3 ) O(n^3) O(n3) | O ( n k ) O(n^k) O(nk) | O ( 2 n ) O(2^n) O(2n) | O ( n ! ) O(n!) O(n!) |
- 简化的算法时间复杂度分析
仅仅考虑算法中的基本操作,所谓基本操作是指算法中最深层循环类的原操作。
【例】基本操作是两重循环中最深层次的语句3,分析它的频度,即 T ( n ) = n 2 = O ( n 2 ) T(n)=n^2=O(n^2) T(n)=n2=O(n2)
- 时间复杂度的求和、求积定理
假设 T 1 ( n ) T_1(n) T1(n) 和 T 2 ( n ) T_2(n) T2(n) 是程序 P 1 、 P 2 P_1、P_2 P1、P2 的执行时间,并且 T 1 ( n ) = O ( f ( n ) ) T_1(n)=O(f(n)) T1(n)=O(f(n)) , T 2 ( n ) = O ( g ( n ) ) T_2(n)=O(g(n)) T2(n)=O(g(n))
求和定理:若先执行 P 1 P_1 P1 ,再执行 P 2 P_2 P2 ,总执行时间是 T 1 ( n ) + T 2 ( n ) = O ( M A X ( f ( n ) , g ( n ) ) ) T_1(n)+T_2(n)=O(MAX(f(n),g(n))) T1(n)+T2(n)=O(MAX(f(n),g(n))) 。【例】多个并列循环
求积定理: T 1 ( n ) × T 2 ( n ) = O ( f ( n ) × g ( n ) ) T_1(n)×T_2(n)=O(f(n)×g(n)) T1(n)×T2(n)=O(f(n)×g(n)) 。【例】多层嵌套循环
3.算法的最好、最坏和平均时间复杂度
【例】以下算法用于求含 n n n 个整数元素的序列中前 i ( 1 ≤ i ≤ n ) i(1\leq i \leq n) i(1≤i≤n) 个元素的最大值,分析该算法的最好、最坏和平均时间复杂度。
int fun(int a[],int n,int i)
{
int j,max=a[0];
for(j=0;i<=i-1;j++)
if(a[j]>max)
max=a[j];
return(max);
}
【解】该算法中的整数序列用数组
a
a
a 表示,前
i
i
i 个元素为
a
[
0..
i
−
1
]
a[0..i-1]
a[0..i−1] 。
i
i
i 的取值范围为
1
1
1 ~
n
n
n (共
n
n
n 种情况),当求前
i
i
i 个元素的最大值时需要比较
(
i
−
1
)
−
1
+
1
=
i
−
1
(i-1)-1+1=i-1
(i−1)−1+1=i−1 次。在等概率情况(每种情况的概率为
1
/
n
1/n
1/n )下:
T
(
n
)
=
∑
i
=
1
n
1
n
(
i
−
1
)
=
1
n
∑
i
=
1
n
(
i
−
1
)
=
n
−
1
2
=
O
(
n
)
T(n)=\sum_{i=1}^n\frac{1}{n}(i-1)=\frac{1}{n}\sum_{i=1}^n(i-1)=\frac{n-1}{2}=O(n)
T(n)=i=1∑nn1(i−1)=n1i=1∑n(i−1)=2n−1=O(n)
平均时间复杂度:
O
(
n
)
O(n)
O(n)
最好的情况是 i = 1 i=1 i=1 时,最好时间复杂度: O ( 1 ) O(1) O(1)
最坏的情况是 i = n i=n i=n 时,最坏时间复杂度: O ( n ) O(n) O(n)
4.递归算法时间复杂度分析
【例】有以下算法,求调用算法的语句为 f u n ( a , n , 0 ) fun(a,n,0) fun(a,n,0) ,求其时间复杂度。
void fun(int a[],int n,int k) //数组a共有n个元素,执行时间为T1(n,k)
{
int i;
if(k==n-1)
{
for(i=0;i<n;i++)
printf("%d\n",a[i]); //该语句执行次数为n
}
else
{
for(i=k;i<n;i++)
a[i]=a[i]+i*i; //该语句执行次数为n-k
fun(a,n,k+1); //执行时间为T1(n,k+1)
}
}
【解】设
f
u
n
(
a
,
n
,
k
)
fun(a,n,k)
fun(a,n,k) 的执行时间为
T
1
(
n
,
k
)
T1(n,k)
T1(n,k) ,
f
u
n
(
a
,
n
,
0
)
fun(a,n,0)
fun(a,n,0) 的执行时间为
T
(
n
)
T(n)
T(n) ,显然有
T
(
n
)
=
T
1
(
n
,
0
)
T(n)=T_1(n,0)
T(n)=T1(n,0) ,由
f
u
n
(
)
fun()
fun() 算法得到以下执行时间的递推式。
T
1
(
n
,
k
)
=
{
n
当
k
=
n
−
1
时
(
n
−
k
)
+
T
1
(
n
,
k
+
1
)
其他情况
T_1(n,k)=\begin{cases} n &\text {当 $k=n-1$ 时} \\(n-k)+T_1(n,k+1) &\text{其他情况} \end{cases}
T1(n,k)={n(n−k)+T1(n,k+1)当 k=n−1 时其他情况
则:
T
(
n
)
=
T
1
(
n
,
0
)
=
n
+
T
1
(
n
,
1
)
=
n
+
(
n
−
1
)
+
T
1
(
n
,
2
)
.
.
.
=
n
+
(
n
−
1
)
+
.
.
.
+
2
+
T
1
(
n
,
n
−
1
)
=
(
n
+
2
)
(
n
−
1
)
2
+
n
=
n
2
2
+
3
n
2
−
1
=
O
(
n
2
)
\begin{aligned} T(n) & =T_1(n,0)=n+T_1(n,1) \\ & = n+(n-1)+T_1(n,2) \\ & ...\\ & = n+(n-1)+...+2+T_1(n,n-1)\\ & = \frac{(n+2)(n-1)}{2}+n=\frac{n^2}{2}+\frac{3n}{2}-1\\ & = O(n^2) \end{aligned}
T(n)=T1(n,0)=n+T1(n,1)=n+(n−1)+T1(n,2)...=n+(n−1)+...+2+T1(n,n−1)=2(n+2)(n−1)+n=2n2+23n−1=O(n2)
所以调用
f
u
n
(
a
,
n
,
0
)
fun(a,n,0)
fun(a,n,0) 的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2) 。
1.3.3 算法空间性能分析
算法的存储量包括:
- 输入数据所占空间
- 程序本身所占空间
- 辅助变量多占空间
若输入的数据所占空间值取决于问题本身,与算法无关,则只需要分析除输入和程序之外的额外空间,即只考察临时变量所占空间。
1.算法空间复杂度分析
算法空间复杂度是对一个算法在运行过程中临时占用的存储空间大小的量度。
记作: S ( n ) = O ( g ( n ) ) S(n)=O(g(n)) S(n)=O(g(n))
若所需临时空间相对于问题规模来说是常数,则称此算法为原地工作算法或就地工作算法,即 O ( 1 ) O(1) O(1)。
2.递归算法空间复杂度分析
【例】分析调用语句 f u n ( a , n , 0 ) fun(a,n,0) fun(a,n,0) 的空间复杂度。
【解】设
f
u
n
(
a
,
n
,
k
)
fun(a,n,k)
fun(a,n,k) 占用的临时空间为
S
1
(
n
,
k
)
S_1(n,k)
S1(n,k) ,
f
u
n
(
a
,
n
,
0
)
fun(a,n,0)
fun(a,n,0) 占用的临时空间为
S
(
n
)
S(n)
S(n) ,显然有
S
(
n
)
=
S
1
(
n
,
0
)
S(n)=S_1(n,0)
S(n)=S1(n,0) ,由
f
u
n
(
)
fun()
fun() 算法得到以下占用临时空间的递推式。
S
1
(
n
,
k
)
=
{
1
当
k
=
n
−
1
时(此时仅仅定义了一个临时变量
i
)
1
+
S
1
(
n
,
k
+
1
)
其他情况
S_1(n,k)=\begin{cases} 1 &\text {当 $k=n-1$ 时(此时仅仅定义了一个临时变量 $i$)} \\1+S_1(n,k+1) &\text{其他情况} \end{cases}
S1(n,k)={11+S1(n,k+1)当 k=n−1 时(此时仅仅定义了一个临时变量 i)其他情况
则:
S
(
n
)
=
S
1
(
n
,
0
)
=
1
+
T
S
1
(
n
,
1
)
=
1
+
1
+
S
1
(
n
,
2
)
.
.
.
=
1
+
1
+
.
.
.
+
1
+
S
1
(
n
,
n
−
1
)
=
1
+
1
+
.
.
.
+
1
(
n
个
1
)
=
O
(
n
)
\begin{aligned} S(n) & =S_1(n,0)=1+TS1(n,1) \\ & = 1+1+S_1(n,2) \\ & ...\\ & = 1+1+...+1+S_1(n,n-1)\\ & = 1+1+...+1 (n个1)\\ & = O(n) \end{aligned}
S(n)=S1(n,0)=1+TS1(n,1)=1+1+S1(n,2)...=1+1+...+1+S1(n,n−1)=1+1+...+1(n个1)=O(n)
所以调用
f
u
n
(
a
,
n
,
0
)
fun(a,n,0)
fun(a,n,0) 的空间复杂度为
O
(
n
)
O(n)
O(n) 。
1.4 数据结构+算法=程序
略
本章小结
- 理解数据结构的定义,数据结构包含的逻辑结构、存储结构和运算三方面的相互关系。
- 掌握各种逻辑结构(线性结构、树形结构和图形结构)的特点。
- 了解各种存储结构(顺序存储结构、链式存储结构、索引和散列)之间的差别。
- 了解数据类型和抽象数据类型的概念和区别。
- 掌握算法定义及特性。
- 掌握使用C/C++语言描述算法的方法。
- 重点掌握算法的时间复杂度和空间复杂度分析方法。
- 掌握从数据结构的角度求解问题的基本过程。
练习题1
- 简述数据和数据元素的关系和区别。
- 采用二元组表示的数据逻辑结构 S = < D , R > S=<D,R> S=<D,R> ,其中 $D={a,b,i}