1.基本概念
1.1 背景
1.2 术语
1.1.1 数据(Data)
分为数值型数据和非数值型数据
1.1.2 数据元素(Data Element)
数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理,也简称为元素,或称为记录、结点或顶点
1.1.3 数据项(Data Item)
构成数据元素的不可分割的最小单位
1.1.4 数据对象(Data Object)
是性质相同的数据元素的集合,是数据的一个子集
1.1.5 数据结构(Data Structure)
数据元素不是孤立存在的,它们之间存在着某种关系,数据元素相互之间的关系称为结构(Structure)
是指相互之间存在一种或多种特定关系的数据元素集合;或者说,数据结构是带结构的数据元素的集合
数据结构包括以下三个方面的内容:
-
数据元素之间的逻辑关系,也称为逻辑结构
-
数据元素及其关系在计算机内存中的表示(又称为映像),称为数据的物理结构或数据的存储结构
-
数据的运算和实现,即对数据元素可以施加的操作以及这些操作在相应的存储结构上的实现
1.1.5.1 数据结构的两个层次
1.1.5.1.1 逻辑结构
-
描述数据元素之间的逻辑关系
-
与数据的存储无关,独立于计算机
-
是从具体问题抽象出来的数学模型
1.1.5.1.1.1 逻辑结构的种类
划分方法一
- 线性结构
有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前趋和一个直接后继
例如:线性表、栈、队列、串
- 非线性结构
一个结点可能有多个直接前取和直接后继
例如:树、图
划分方式二—四类基本逻辑结构
-
集合结构:结构中的数据元素之间除了同属于一个集合的关系外,无任何其它关系
-
线性结构:结构中的数据元素之间存在着一对一的线性关系
-
树形结构:结构中的数据元素之间存在着一对多的层次关系
1.1.5.1.2 物理结构(存储结构)
-
数据元素及其关系在计算机存储器中的结构(存储方式)
-
是数据结构在计算机中的表示
四种基本的存储结构
- 顺序存储结构
用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示
例如:C语言中用数组来实现顺序存储结构
- 链式存储结构
用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系用指针来表示
例如:C语言中用指针来实现链式存储结构
- 索引存储结构
在存储结点信息的同时,还建立附加的索引表
索引表中的每一项称为一个索引项
索引项的一般形式是:(关键字,地址)
关键字是能唯一标识一个结点的那些数据项
若每个结点在索引表中都有一个索引项,则该索引表称为稠密索引(Dense Index)。若一组结点在索引表中只对应一个索引项,则该索引表称之为稀疏索引(Sparse Index)
- 散列存储结构
根据结点的关键字直接计算出该结点的存储地址
1.1.5.1.3 逻辑结构与存储结构的关系
-
存储结构是逻辑关系的映像与元素本身的映像
-
逻辑结构是数据结构的抽象,存储结构是数据结构的实现
1.1.6 数据类型和抽象数据类型
在使用高级程序设计语言编写程序时,必须对程序中出现的每个变量、常量或表达式明确说明它们所属的数据类型
高级语言中的数据类型明显地或隐含地规定了在程序执行期间变量的所有可能的取值范围,以及在这些数值范围上所允许进行的操作
数据类型的作用:
-
约束变量或常量的取值范围
-
约束变量或常量的操作
数据类型(Data Type)
定义:数据类型是一组性质相同的值的集合以及定义于这个值集合上的一组操作的总称
数据类型 = 值的集合 + 值集合上的一组操作
抽象数据类型(Abstract Data Type, ADT)
定义:是指一个数学模型以及定义在此数学模型上的一组操作
-
由用户定义,从问题抽象出数据模型(逻辑结构)
-
还包括定义在数学模型上的一组抽象运算(相关操作)
-
不考虑计算机内的具体存储结构与运算的具体实现算法
抽象数据类型的形式定义:
抽象数据类型可用(D, S, P)三元组表示。
其中:D是数据对象;S是D上的关系集;P是对D的基本操作集
一个抽象数据类型的定义格式如下:
ADT 抽象数据类型名{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作:<基本操作的定义>
} ADT 抽象数据类型名
其中:
-
数据对象、数据关系的定义用伪代码描述
-
基本操作的定义格式为:
-
基本操作名(参数表)
-
初始条件:<初始条件描述>
-
操作结果:<操作结果描述>
-
基本操作定义格式说明:
参数表:赋值参数只为操作提供输入值。引用参数以&打头,除可提供输入值外,还将返回操作结果
初始条件:描述操作执行之前数据结构和参数应满足的条件,若不满足,则操作失败,并返回相应出错信息。若初始条件为空,则省略之
操作结果:说明操作正常完成之后,数据结构的变化状况和应返回的结果。
e.g. Circle
ADT Circle{
数据对象:D = {r,x,y|r,x,y均为实数}
数据关系:S = {<r,x,y>|r是半径,<x,y>是圆心坐标}
基本操作:
Circle(&C,r,x,y)
操作结果:构造一个圆
double Area(C)
初始条件:圆已存在
操作结果:计算面积
double Circumference(C)
初始条件:圆已存在
操作结果:计算周长
......
} ADT Circle
e.g. 复数
ADT Complex{
数据对象:D = {r1, r2|r1,r2都是实数}
数据关系:S = {<r1, r2>|r1是实部,r2是虚部}
基本操作:
assign(&C,v1,v2)
初始条件:空的复数C已存在
操作结果:构造复数C,r1,r2分别被赋以参数v1,v2的值
destory(&C)
初始条件:复数C已存在
操作结果:复数C被销毁
GetReal(C, &realPart)
初始条件:复数C已存在
操作结果:用realPart返回复数C的实部值
GetImag(C, &ImagPart)
初始条件:复数已存在
操作结果:用ImagPart返回复数C的虚部值
Add(c1, c2, &sum)
初始条件:c1, c2是复数
操作结果:sum返回两个复数c1, c2的和
......
} ADT Complex
1.3 总结
2.抽象数据类型的表示与实现
抽象数据类型可以通过固有的数据类型(如整型、实型、字符型等)来表示和实现
- 即利用处理器中已存在的数据类型来说明新的结构,用已经实现的操作来组合新的操作
例如:抽象数据类型“复数”的实现
typedef struct{
float realpart; // 实部
float imagpart; // 虚部
}Complex // 定义复数抽象类型
Complex assign(Complex* A, float real, float imag); // 赋值
Complex add(Complex* c, Complex A, Complex B); // A + B
Complex minus(Complex* c, Complex A, Complex B); // A - B
Complex multiply(Complex* c, Complex A, Complex B); // A * B
Complex divide(Complex* c, Complex A, Complex B); // A / B
Complex assign(Complex* A, float real, float imag){
A->realpart = real; // 实部赋值
A->imagpart = imag; // 虚部赋值
return A;
}
Complex add(Complex* c, Complex A, Complex B){
c->realpart = A.realpart + B.realpart; // 实部相加
c->imagpart = A.imagpart + B.imagpart; // 虚部相加
return c;
}
Complex minus(Complex* c, Complex A, Complex B){
c->realpart = A.realpart - B.realpart; // 实部相减
c->imagpart = A.imagpart - B.imagpart; // 虚部相减
return c;
}
Complex multiply(Complex* c, Complex A, Complex B){
c->realpart = A.realpart * B.realpart - A.imagpart * B.imagpart;
c->imagpart = A.imagpart * B.realpart + A.realpart * B.imagpart;
return c;
}
Complex divide(Complex* c, Complex A, Complex B){
c->realpart = (A.realpart * B.realpart + A.imagpart * B.imagpart) / (B.realpart * B.realpart + B.imagpart * B.imagpart);
c->imagpart = (A.imagpart * B.realpart - A.realpart * B.imagpart) / (B.realpart * B.realpart + B.imagpart * B.imagpart);
return c;
}
3.算法和算法分析
算法的定义:对特定问题求解方法和步骤的一种描述,它是指令的有限序列。其中每个指令表示一个或多个操作。简言之,算法就是解决问题的方法和步骤
算法的描述:
-
自然语言
-
流程图:传统流程图、NS流程图
-
伪代码/类语言
-
程序代码
算法与程序
-
算法是解决问题的一种方法或一个过程,考虑如何将输入转换成输出,一个问题可以有多种算法
-
程序是用某种程序设计语言对算法的具体实现
程序 = 数据结构 +算法
数据结构通过算法实现操作
算法根据数据结构设计程序
算法特性
一个算法必须具备以下五个重要特性:
-
有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成
-
确定性:算法中的每一条指令必须有确切的含义,没有二义性,在任何条件下,只有唯一的一条执行路径,即对于相同的输入只能得到相同的输出
-
可行性:算法是可执行的,算法描述的操作可以通过已经实现的基本操作执行有限次来实现
-
输入:一个算法有零个或多个输入
-
输出:一个算法有一个或多个输出
算法设计的要求
- 正确性(Correctness)
- 可读性(Readability)
- 健壮性(Robustness)
- 高效性(Efficiency)
一个好的算法首先要具备正确性,然后是健壮性,可读性,在几个方面都满足的情况下,主要考虑算法的效率,通过算法的效率高低来评判不同算法的优劣程度
算法效率以下两个方面来考虑:
-
时间效率:指的是算法所耗费的时间;
-
空间效率:指的是算法执行过程中所耗费的存储空间
注意:时间效率和空间效率往往不可兼得
算法时间效率的度量
算法时间效率可以用依据该算法编制的程序在计算机上执行所消耗的时间来度量
两种度量方法:
-
事后统计
-
将算法实现,测算其时间和空间开销
-
缺点:编写程序实现算法将花费较多的时间和精力;所得实验结果依赖于计算机的软硬件等环境因素,掩盖算法本身的优劣
-
-
事前分析
-
对算法所消耗资源的一种估算方法
-
一个算法的运行时间是指一个算法在计算机上运行所耗费的时间大致可以等于计算机执行一种简单的操作(如赋值、比较、移动等)所需的时间与算法中进行的简单操作次数乘积
算法运行时间 = 一个简单操作所需时间 x 简单操作次数
-
也即算法中每条语句的执行时间之和
算法运行时间 = ∑
∑ 每条语句的执行次数 x 该语句执行一次所需的时间 -
-
注意:每条语句的执行次数又称为语句频度
每条语句执行依次所需的时间,一般是随机器而异的。取决于机器的指令性能、速度以及编译的代码质量。是由机器本身软硬件环境决定的,它与算法无关
所以我们可以假设执行每条语句所需的时间均为单位时间。此时对算法的运行时间的讨论就可转化为讨论该算法中所有语句的执行次数,即频度之和了
这就可以独立于不同机器的软硬件环境来分析算法的时间性能了
O:Order 数量级
一般情况下,不必计算所有操作的执行次数,而只考虑算法中基本操作执行的次数,它是问题规模n个某个函数,用T(n)表示
基本语句:
-
算法中重复执行次数和算法的执行时间成正比的语句
-
对算法运行时间的贡献最大
-
执行次数最多
问题规模n:n越大算法的执行时间越长
-
排序:n为记录数
-
矩阵:n为矩阵的阶数
-
多项式:n为多项式的项数
-
集合:n为元素个数
-
树:n为树的结点个数
-
图:n为图的顶点数或边数
定理1.1
若f(n) = amnm
忽略所有低次幂项和最高次幂系数,体现出增长率的含义
分析算法时间复杂度的基本方法
-
找出语句频度最大的那条语句作为基本语句
-
计算基本语句的频度得到问题规模n的某个函数f(n)
-
取其数量级用符号"O"表示
注意:时间复杂度是由嵌套最深层语句的频度决定的
练习:
算法时间复杂度
请注意:有的情况下,算法中基本操作重复执行的次数还随问题的输入数据集不同而不同
[例] 顺序查找,在数组中查找值等于e的元素,返回其所在位置
for(i = 0; i < n; i++)
if(a[i] == e) return i + 1;
return 0;
最好情况:1次;最坏情况:n;平均时间复杂度为:O(n)
-
最坏时间复杂度:指在最坏情况下,算法的时间复杂度
-
平均时间复杂度:指在所有可能输入实例在等概率出现的情况下,算法的期望运行时间
-
最好时间复杂度:指在最好情况下,算法的时间复杂度
对于复杂的算法,可以将它分成几个容易估算的部分,然后利用大O加法法则和乘法法则,计算算法的时间复杂度
-
加法法则:T(n) = T1
T1 (n) + T2T2 (n) = O(f(n)) + O(g(n)) = O(max(f(n),g(n))) -
乘法法则:T(n) = T1
T1 (n) × T2(n) = O(f(n)) × O(g(n)) = O(f(n)×g(n))
算法时间效率的比较
- 当n取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊
复杂度:O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < ······ < O(nk) < O(2n) < O(n!)
渐进空间复杂度
空间复杂度:算法所需存储空间的度量,记作:S(n) = O(f(n)),其中n为问题的规模(或大小)
算法要占据的空间
-
算法本身要占据的空间,输入/输出,指令,常量,变量等
-
算法要使用的辅助空间
例如:将一维数组a中的n个数逆序存放到原数组中
// 算法1
for(int i = 0; i < n / 2; i++){
// t为辅助空间
t = a[i];
a[i] = a[n-i-1];
a[n-i-1] = t;
// S(n) = O(1) 原地工作
}
// 算法2
for(int i = 0; i < n; i++){
b[i] = a[n-i-1];
}
for(int i = 0; i < n; i++){
a[i] = b[i];
}
// S(n) = O(n)