绪论
为甚要学习数据结构?
首先来了解什么是数据:
- 数据是表征客观事物的可记录可识别的符号集合。
- 数据是信息处理的核心基础。
早期计算机主要是进行数值计算。
现在的计算机主要进行 非数值计算。
这里的非数值,包括处理字符、表格、图像等等
它们都是具有一定结构的数据。
数据的内容存在联系。
因此,要想设计出高效的算法,必须要分清楚数据的的内在联系,合理的组织数据。
而数据结构主要研究的问题就是:如何合理的组织数据?如何有效的处理数据?
所以,学习数据结构是十分有必要的。数据结构和算法是计算机科学的基石。
数据结构研究的内容
计算机进行数值计算的研究过程
- 从具体问题中抽象出数学模型
- 分析问题并提出操作对象
- 找出操作对象之间的关系
- 用数学语言描述这种关系,并建立数学方程
- 建立求解该数学模型的算法
- 编写程序,测试、调试程序,
计算机如何进行非数值运算
非数值运算无法用数学方程建立数学模型,而数据结构则能解决这个问题。
学生管理系统类
学生的信息,包括学生的学号、姓名、性别、籍贯、专业等。
每个学生的基本情况按照不同的顺序号,依次存在学生信息表中。
每个学生的基本信息记录按顺序号排列,形成了学生信息记录的线性序列,呈一种线性关系。
学号 | 姓名 | 性别 | 籍贯 | 专业 |
---|---|---|---|---|
1812050001 | 张三 | 男 | 河南省南阳市 | 计算机科学与技术 |
1812050002 | 李四 | 男 | 河南省开封市 | 信息安全 |
诸如此类的线性表结构还有图书馆的数目管理系统,库房管理系统。
这类问题,计算机处理的对象是各种表,元素之间存在简单一对一的线性关系。
因此这类问题的数学模型就是各种线性表,
可对线性表进行查找、插入、和删除操作。
这种数学模型成为“线性”数据结构。
人机对弈问题
人机对弈的数学模型就是如何用树结构来表示棋盘和棋子等。
算法是博弈的规则和策略
诸如此类的结构还有计算机的文件系统,一个单位的组织机构。
这类问题中,计算机处理的对象是树结构,元素之间是一对多的层次关系。
可对对象进行查找、插入、和删除操作。
这类数学模型称为“树”的数据结构。
最短路径问题
最短路径问题的数学模型是图结构,算法是求解两点之间的最短路径。
诸如此类的还有网络工程图和网络通信图,
这类问题中,元素之间是多对对的网状关系
可对对象进行查找、插入、和删除操作。
这类数学模型称为图的数据结构。
由以上三个实例,自然就可以看出,非数值计算问题的数学模型不再是数学方程,而是,线性表、树、图的数据结构.
简单的讲,数据结构是一门研究非数值计算程序设计中的操作对象以及这些对象之间的关系和操作的学科。数据结构是计算机专业的核心课程,分为基础、结构和技术三大块。
基本概念和术语
数据
数据(Data)是客观事物的符号表示,是所有能输入到计算机中并能被计算机处理的符号的总称。
数据是描述客观事物的数值、字符以及一切能输入到计算机且 能被处理的符号集合。
如:进行数学计算是用到的整数与实数,
文本编辑中用到的字符串,
多媒体程序处理的图形、图像、声音及动画等(通过特殊编码定义后的数据)。
数据元素
**数据元素(Data Element)**是数据的基本单位,在计算机在通常作为一个整体进行考虑和处理。 在某些情况下,数据元素也称为元素、记录。
数据元素是组成数据的基本单位,是数据集合的个体,用学籍表里的一条学生记录理解,虽然学生信息中有姓名、班级多个属性,通常作为一个整体考虑和处理,是一个数据元。
数据项
数据项(Data Item)是组成数据元素的、有独立含义的、不可分割的最小单位。
如学生信息表中的学号、姓名、性别等都是数据项。
数据对象
数据对象(Data Object)是性质相同的数据元素的集合,是数据的一个子集。
例如:
学生信息表也是一个数据对象,
由此可看,不论数据元素的集合是无限集(整数集),还是有限集(如字母字符集),
还是由多个数据项组成的复合数据元素(学生表)的集合,只要集合内元素性质均相同,都可称为一个数据对象。
数据构成
以上四种概念的关系
数据>数据对象>数据元素>数据项
- 数据子集 - - - 数据对象
- 数据个体 - - - 数据元素
数据结构
数据结构(Data Structure)是相互之间存在一种或多种特定关系的数据元素的集合。也就是带有**“结构”的数据元素的集合。“结构”**就是数据元素之间的关系即数据的组织形式。
如学生信息表的表结构,图书馆中的图书按照索引分类。
数据类型
数据类型(Data Type) 是高级程序设计语言中的一个基本概念,是一组性质相同的值集合以及定义其上的一组操作的集合。也可以说成是高级语言中已经实现的数据结构。
如C语言
- 基本数据类型: char int float double void
- 构造数据类型:数组、结构体、共用体
明显或隐含地规定了在程序执行期间变量或表达式所有可能取值的范围,以及在这个值上允许的操作。
如C语言中,整型类型的取值范围可为:-32767~+32768;
运算符集合为:+、-、*、/、%。
抽象数据类型
数据的抽象抽象就是抽取实际问题的本质。
如在计算机中使用二进制来表示数据,而在汇编语言中则可以给出各种数据的十进制表示,它们是二进制的抽象。
在高级语言中,则给出了更高一级的抽象,出现了数据类型,如整形,实型,字符型等;
可以进一步利用这些类型构造出线性表、栈、队列、树、图等复杂的抽象数据类型。
抽象数据类型(Abstract Data Type,ADT)
一个数学模型以及定义在该模型上的一组操作。
- 由用户定义,从问题抽象出数据模型(逻辑结构)
- 还包括定义在数据模型上的一组抽象运算(相关操作)
- 不考虑计算机内的具体存储结构与运算的具体实现算法
ADT定义了一个数据对象,数据对象中各个元素之间的结构关系,以及一组处理数据的操作。
ADT的特点是数据抽象与信息隐蔽
ADT包括定义和实现两个方面,其中定义是独立于实现的。
ADT两个重要特征
数据抽象
- 通过数据抽象,将一个数据对象的规格说明与其实现分离,对外提供简洁、清晰的接口。比如:手机。
数据封装
- 通过数据封装,将一个数据对象的内部结构和实现细节对外屏蔽
如书库管理
用户通过服务界面实现查书、借书和还书,用户只使用功能,具体处理过程对用户是封装的,用户也不必关心具体书在哪个书架上的具体实现问题。
抽象数据类型的表示
可以用三元组进行表示
ADT = (D,R,P)
- D:数据对象
- R :D上的关系集
- P: D上的操作集
ADT常见的定义格式
ADT抽象数据类型名{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作 :<基本操作的定义>
} ADT抽象数据类型名
基本操作的定义格式
基本操作名(参数表)
初始条件:〈初始条件描述〉
操作结果:〈操作结果描述〉
- 赋值参数:只为操作提供输入值
- 引用参数:以&开头,除提供输入值外,还将返回操作结果
- 初始条件:描述了操作执行前数据结构和参数应满足的条件,若不满足,操作失败,并返回相应出错信息。若初始条件为空,则省略之。
- 操作结果:说明操作正常完成之后,数据结构的变化情况和应返回的结果。
ADT定义实例
ADT Complex {
数据对象:D={e1,e2|e1,e2均为实数}
数据关系:R={<e1,e2>| e1是复数的实部,e2 是复数的虚部 }
基本操作:
AssignComplex(&Z,v1,v2)
操作结果:构造复数Z,其实部和虚部分别赋以参数v1和v2的值。
DestroyComplex(&Z)
操作结果: 复数Z被销毁。
GetReal(Z,&real)
初始条件:复数已存在。
操作结果: 用real返回复数Z的实部值。
GetImag(Z,&imag)
初始条件:复数已存在。
操作结果: 用imag返回复数Z的虚部值。
Add(z1,z2,&sum)
初始条件:z1,z2是复数。
操作结果: 用sum返回两个复数z1,z2的和值
} ADT Complex
ADT Circle {
数据对象:D={r,x,y| r,x,y 均为实数}
数据关系:R={< r,x,y >| r是半径,<x,y>是圆心座标 }
基本操作:
Circle(&C,r,x,y)
操作结果:构造一个圆。
double Area(C)
初始条件:圆已存在。
操作结果:计算面积。
double Circumference (C)
初始条件:圆已存在。
操作结果: 计算周长。
……
} ADT Circle
类比接口的定义
接口:里面内容都是抽象的,怎么实现取决于具体应用
抽象数据类型的实现
ADT实现的含义
- 将ADT转换成程序设计语言的说明语句,加上对应于该ADT中的每个操作的函数。
- 用适当的数据结构来表示ADT中的数学模型,并用一组函数来实现该模型上的各种操作。
抽象数据类型如何实现
抽象数据类型可以通过固有的数据类型(如整型、实型、字符型等)来表示和实现。即利用处理器中已存在的数据类型来说明新的结构,用已经实现的操作来组合新的操作。
数据结构的内容
逻辑结构
定义
数据的逻辑结构是指数据元素之间的逻辑关系的描述。
形式化描述
数据结构是一个二元组Data_Structure=(D,R)
其中D指数据元素的有限集,R是D上关系的有限集。
四类基本的逻辑结构
集合结构
数据元素之间除了“属于同一集合外”的关系外,别无其他关系。
例如:确定一名学生是否为班级成员,只需将班级看作为一个集合结构。
线性结构
数据元素之间存在一对一的关系。
如将学生入学信息按照入学报道的时间顺序进行排列,将组成一个线性结构。
树形结构
数据元素之间存在一对多的关系。
如,在班级的管理体系中,班长管理多个组长,每位组长管理多名组员,从而形成树状结构。
图结构或网状结构
数据元素之间存在多对多的关系。
例如,多为同学之间的朋友关系,任何两位同学都可以是朋友,从而构成图结构或网状结构。
线性结构与否
线性结构
- 线性表(典型的线性结构,如学生基本信息表)
- 栈和队列(具有特殊限制的线性表,数据操作只能在表的一端或两端进行)
- 字符串(特殊的线性表,特殊性表现在他的数据元素仅有一个字符组成)
- 数组(是线性表的推广,他的数据元素是一个线性表)
- 广义表(线性表的推广,他的数据元素是一个线性表,但不同构,即或者是单元素,或者是线性表)
非线性结构
- 树(具有多个分支的层次结构)
- 二叉树(具有两个分支的层次结构)
- 有向图(一种图结构,边是顶点的有序对)
- 无向图(一种图结构,边是顶点的无序对)
逻辑结构图示
存储结构
定义
数据对象在计算机中的存储表示称为存储结构。
存储结构又称物理结构。
把数据对象存储到计算机时,既要存储各数据元素的数据,又要存储各数据元素之间的关系。
是逻辑结构在计算机中的存储映像,是逻辑结构在计算机中的实现。包含数据元素的表示和关系的表示。
形式化描述
D要存入计算机中,建立一从D的数据元素到存储空间M单元的映像S,D–>M,即对每一个d,d属于D,都有唯一的z属于M使S(D)=Z,同时这个映像必须明显或隐含的显示关系R。
逻辑结构与存储结构的关系
逻辑结构是逻辑关系的映像和元素本身的映像。
逻辑结构是数据结构的抽象,存储结构是数据结构的实现。
两者综合起来建立了数据元素之间的结构关系。
数据元素之间的关系在计算机中的表示方法。
-
顺序映像(顺序存储结构)
-
非顺序结构(非顺序存储结构)
顺序存储结构
顺序存储结构是借助元素在存储器的相对位置来表示数据元素之间的元素关系。
通常借助程序设计语言中的数组来描述。
Lo(元素i)=L0+(i-1)*m
链式存储结构
顺序存储结构要求各数据元素依次存放在一片连续的存储空间,而链式存储结构则没有这样要求。它无需占用一整块的存储空间。它在每个节点后面附加指针字段,用于存放后续的数据元素的存储地址。通常借助程序设计语言的指针来描述。
运算集合
讨论数据的目的是为了在计算机中实现操作,因此在结构上运算集合是很重要的一部分。数据结构就是研究一类数据的表示及其相关的运算操作。
数据的运算
在数据的逻辑结构上定义操作算法,而在存储结构上进行实现。
最常用的5种运算:
- 插入、删除、修改、查找、排序
逻辑结构和存储结构相同, 但运算不同, 则数据结构不同。例如, 栈与队列
数据结构的精确定义
按照一定的逻辑关系组织起的数据;
按照一定的映像关系 ,存放在计算机中;
并在其上定义一个运算集合。
(1)数据的逻辑结构是数据的机外表示,数据的存储结构是数据的机内表示。
(2) 一种数据的逻辑结构可以用多种存储结构来存储。
(3) 任何一个算法的设计取决于选定的逻辑结构,而算法的实现依赖于采用的存储结构。
(4) 采用不同的存储结构,其数据处理的效率往往是不同的。
研究数据结构的方法
数据结构与算法之间存在着联系,而且在有些类型的数据结构中,我们总是要涉及到在这类数据结构上施加的运算。我们需要对这些运算进行研究,才能够理解这类数据结构的定义和作用。
所以,算法是研究数据结构的主要途径。我们在研究数据结构的过程,由于算法联系着数据在计算中的组织方式,为了描述和实现某种操作,常常需要设计算法。
算法
定义
A finite set of rules which gives a sequence of operation for solving a specific type of problem.
翻译过来就是:算法是规则的有限集合,是为解决特定问题而规定的一系列操作。换句话就是算法是对特定问题求解步骤的一种描述,是指令的有限序列。
特性
一个算法必须满足五种特性:
-
有穷性
一个算法必须在执行有穷的步骤之后结束,且每一步都要在有穷的时间内完成。
也就是有限步骤之内结束,不能形成死循环 -
确定性
算法中的每一条指令必须有确切的含义,在任何条件下,只有唯一的一条执行路径,且无二义性。即对于相同的输入只能得到相同的输出。
这样每种情况下所执行的操作,在算法中都有明确的规定。无论是阅读者还是执行者,都能明确其含义和作用。 -
可行性
原则上可以精确的运行,算法描述的操作可以通过已经实现的基本操作运算执行有限次来实现。 -
输入
一个算法有零个或多个输入。
若用函数来描述算法时,往往借助形参,在函数被调用时,从主调函数来获取输入值。 -
输出
一个算法有一个或多个输出,无输出的算法是没有意义的。当用函数来描绘算法时,输出往往是函数的返回值或引用类型的形参。
算法设计的标准
-
正确性
在合理的数据输入下,能够在有限的运行时间下,得到正确的结果;
正确性有四个层次:- 所设计的程序没有语法错误
- 程序能够对几组输入的数据都能够得到满足的结果;
- 所设计的程序对于精心选择的典型,苛刻而带有刁难性的几组输入数据能够得到满足要求的结果。
- 程序对一切合法的输入数据都能够产生满足要求的结果。
-
可读性
一个好的算法首先应该便于人们理解和相互交流。其次才是机器的可执行。
可读性好的算法有助于加强我们对算法的理解,难懂的算法容易隐藏错误并且难以调试和修改。 -
健壮性
即对非法数据的抵抗性。
它主要强调,如果输入非法数据,算法应能加以识别并作出处理,而不是产生误动作输出莫名奇妙的结果或者陷入瘫痪。 -
高效率和低存储量
算法的效率通常是指算法的执行时间。
对于一个具体问题的解决通常可以通过多种算法,对于执行时间短的算法其效率就高。所谓的存储量要求,是指算法在执行过程中所需要的最大的存储空间。
前者用时间复杂度来衡量,后者用空间复杂度来衡量。两者都与问题的规模有关。
算法描述的工具
算法+数据结构=程序
算法、语言、程序的关系
算法:算法是规则的有限集合,是为解决特定问题而规定的一系列操作。
**描述算法的工具:**可以用自然语言,框图或高级程序设计语言来描述。
**程序:**程序是算法在计算机中的实现。
类描述法的语言选择
类语言
类语言是接近与高级语言而又不是严格的高级语言。
具有高级语言的一般语句设施,但是撇去了高级语言中的细节,而是把注意力集中在算法处理步骤本身的描述上。
算法性能评价的标准
性能评价
算法描述机器性能主要表现在时间代价(T和空间代价(S上。
算法的效率与**问题规模(N)**有关。算法效率是问题规模的函数。
算法效率的度量
算法效率可以用依据该算法编制的程序在计算机上执行所消耗的时间来度量。
两种度量方法:
-
事后统计
- 将算法实现,测算其时间和空间开销。
缺点:- 编写程序实现算法将花费较多的时间和精力;
- 所得实验结果依赖于计算机的软硬件等环境因素
- 将算法实现,测算其时间和空间开销。
-
事前分析
对算法所消耗资源的一种估算方法。
一个高级语言程序在计算机上运行所消耗的时间取决于:- 算法选用的策略
- 问题的规模
- 编写程序的语言
- 编译程序产生的机器代码质量
- 机器执行指令的速度
同一个算法用不同的语言、不同的编译程序、在不同的计算机上运行,效率均不同。因此,使用绝对时间单位衡量算法效率是不合适的。
关于算法的执行时间
由于算法的实际执行时间和机器硬件和系统软件等多种环境因素有关,所以算法的执行时间是针对算法中的语句执行次数做出估计,从中得到算法执行时间的本质信息。
定义:
一个算法的执行时间大致上等于其所有语句执行时间的总和。
对于语句的执行时间是指该条语句的执行次数和执行一次所需时间的乘积。
语句频度:
语句频度是指该语句在一个算法中重复执行的次数。
例如求两个n阶矩阵乘积的算法
for(int i=1;i<=n;i++)..............................n+1
{
for(int j=1;j<=n;j++ )........................n*(n+1)
{
c[i][j]=0;.................................n^2
for(k=1;k<=n;k++)..........................n^2(n+1)
{
c[i][j]=c[i][j]+a[i][k]* b[k][j];.......n^3
}
}
}
注:循环执行n次,还要加上一次跳出循环的判断,故为n+1。
总执行次数为$ T_{(n)}=2n3+3n2+n+1$;
问题规模(N)
反映问题大小的本质数目,对于不同的问题其含义不同。
如:
- 对矩阵是阶数
- 对多项式运算是多项式的项数
- 对图是顶点的个数
- 对集合运算是集合中的元素的个数
算法的时间复杂度
即算法的时间量度,以语句频度为量度。记作
最坏时间复杂度:讨论算法在最坏情况下的时间复杂度,即分析最坏情况下估计出算法执行时间的上界。
渐进符号(O)的定义
- 如果存在两个正常数c和,对于所有的n ≥ ,有
- 则记作 ;
- f(n)是参照物,通常是一些常用的标准函数。
常用的时间复杂度的频率基数
- O(1)…常数型
- O(n)…线性型
- O()…平方型
- O()…立方型
- O()…指数型
- O()…对数型
- O(n)…二维型
n | $ n^3$ | $ 2^ n$ | |||
---|---|---|---|---|---|
0 | 1 | 0 | 1 | 1 | 2 |
1 | 2 | 2 | 4 | 8 | 4 |
2 | 4 | 8 | 16 | 64 | 16 |
3 | 8 | 24 | 64 | 512 | 256 |
4 | 16 | 64 | 256 | 5096 | 65536 |
5 | 32 | 160 | 1024 | 32728 | 2147483648 |
一般来说,前三种可以实现,后三种理论上可实现,但实际上只有把N限制在很小的范围才有意义,当N较大的时候不可能实现。
定理:
若是一个m次多项式,则 。
-
在计算算法时间复杂度时,可以忽略所有低次幂和最高次幂的系数。
- , c是正整数
对定理的一个证明:
分析算法时间复杂度的基本方法
- 找出语句频度最大的那条语句作为基本语句
- 计算基本语句的频度得到问题规模n的某个函数
- 取其数量级用符号表示
分析时间复杂度的例子
for (int i=1; i<=n; i++)
x=x+1;
时间复杂度为。
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
x=x+1;
时间复杂度为。
void exam ( float x[ ][ ], int m, int n ) {
float sum [ ];.......................1
for ( int i = 0; i < m; i++ ) { .....m+1
sum[i] = 0.0;....................m
for ( int j = 0; j < n; j++ ) ...m*(n+1)
sum[i] += x[i][j];...........m*n--------------最深层的语句
}
for ( i = 0; i < m; i++ )............m+1
cout << i << “ : ” <<sum [i] << endl;...m
}
时间复杂度为。
i=1; ①
while(i<=n)
i=i*2; ②
对于循环语句;
,可得。
故时间复杂度。
for (int i=2;i<=n;++i)
for (int j=2;j<=i-1;++j)
{
++x;
a[i][j]=x;......................分析最深层的语句
}
当i=2时,频度为0
当i=3时,频度为1
…
当i=n时,频度为n-2
故
故时间复杂度。
for( i=1; i<=n; i++)
for (j=1; j<=i; j++)
for (k=1; k<=j; k++)
x=x+1;........................f(n)
i=1…1
i=2…1+2
i=3…1+2+3
…
i=n…1+2+…+n
所以时间复杂度为。
算法的空间复杂度
算法开始运行直至结束过程中所需要的最大存储资源开销的一种度量。
表示随着问题规模 n 的增大,算法运行所需存储量的增长率与 $f(n) $的增长率相同。
算法占据的存储空间
- 输入数据所占空间;
- 程序本身所占空间;
- 辅助变量所占空间。
一维数组的空间复杂度是O(n)
矩阵的空间复杂度为
若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。
原地工作:若额外空间相对于输入数据量来说是常数,则称此算法为原地工作。
实例
将一维数组a中的n个数逆序存放到原数组中。
【算法1】
for(i=0;i<n/2;i++)
{ t=a[i];
a[i]=a[n-i-1];
a[n-i-1]=t;
}
【算法2】
for(i=0;i<n;i++)
b[i]=a[n-i-1];
for(i=0;i<n;i++)
a[i]=b[i];