基于青岛大学王卓老师《数据结构与算法基础》课程笔记
- 勤于思考
- 多做练习
- 多上机
- 善于寻求帮助
- 不怕困难,不放弃!!!
1.1 数据结构的研究内容
数据结构是一门研究非数值计算的程序设计中计算机的操作对象以及它们之间的关系和操作的学科。
1.2 数据结构的基本概念
1.2.1 基本概念和术语
-
数据(Data)
- 能输入计算机且能被计算机处理的各种符号的集合
- 数值型的数据:整数、实数等
- 非数值型的数据:文字、图像、图形、声音等
-
数据元素(Data element)
- 数据的基本单位,通常作为一个整体进行考虑和处理
- 简称为元素,或称为记录、结点或顶点
- 一个数据元素可由若干个数据项(Data Item)组成
-
数据项(Data Item)
-
构成数据元素的不可分割的最小单位
-
数据、数据元素、数据项三者之间的关系:数据>数据元素>数据项
-
-
数据对象(Data Object)
- 是性质相同的数据元素的集合,是数据的一个子集
- 例如:整数数据对象是集合 N = { 0 , ± 1 , . . . } N=\{0,±1,...\} N={0,±1,...}
- 数据元素与数据对象
- 数据元素——组成数据的基本单位
- 与数据的关系:是集合的个体
- 数据对象——性质相同的数据元素的集合
- 与数据的关系:集合的子集
- 数据元素——组成数据的基本单位
- 是性质相同的数据元素的集合,是数据的一个子集
-
数据结构(Data Structure)
-
数据元素相互之间的关系称为结构
-
是指相互之间存在一种或多种特定关系的数据元素集合
-
或者说,数据结构式带结构的数据元素的集合
-
数据结构包括以下三方面的内容:
- 数据元素之间的逻辑关系,称为逻辑结构
- 数据元素及其关系在计算机内存中的表示(称为映像),称为数据的物理结构或数据的存储结构
- 数据的运算和实现,对数据元素可施加的操作以及这些操作在相应的存储结构上的实现。
-
1.2.2 数据结构三要素
- 逻辑结构
- 描述数据元素之间的逻辑关系
- 与数据的存储无关,独立于计算机
- 是从具体问题抽象出来的数学模型
- 物理结构(存储结构)
- 数据元素及其关系在计算机存储器中的结构(存储方式)
- 数据结构在计算机中的表示
- 逻辑结构与存储结构的关系:
- 存储结构式逻辑关系的映像与元素本身的映像
- 逻辑结构是数据结构的抽象,存储结构是数据结构的实现
- 两者综合起来建立了数据元素之间的结构关系
逻辑结构的种类
-
划分方法一
- 线性结构
有且只有一个开始和一个终端节点,并且所有结点都最多只有一个直接前驱和一个直接后继
例如:线性表、栈、队列、串 - 非线性结构
一个结点可能有多个直接前驱和直接后继
例如:树、图
- 线性结构
-
划分方法二——四类基本逻辑结构
- 集合结构:结构中的数据元素之间除了同属于一个集合的关系外,无任何其他关系。
- 线性结构:结构中的数据元素之间存在着一对一的线性关系。
- 树形结构:结构中的数据元素之间存在着一对多的层次关系。
- 图状结构或网状结构:结构中的数据元素之间存在着多对多的任意关系。
存储结构的种类
顺序存储结构:
- 用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示。
- C语言中数组来实现顺序存储结构
- 例:(bat, cat, eat,…)
链式存储结构:
- 用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系用指针来表示。
- C语言中用指针来实现链式存储结构
- 例:(bat, cat,eat,…, mat)
索引存储结构:
- 在存储结点信息的同时,还建立附加的索引表
- 索引表中的每一项称为一个索引项
- 索引项的一般形式是:(关键字,地址)
- 关键字是能唯一标识一个结点的那些数据项
- 若每个结点在索引表中都有一个索引项,则该索引表称之为稠密索引(Dense Index)。若一组结点在索引表中只对应一个索引项,则该索引表称之为稀疏索引(Sparse Index)。
散列存储结构:
- 根据结点的关键字直接计算出该结点的存储地址。
1.2.3 数据类型和抽象数据类型
C语言中
- 提供int, char, double等基本数据类型
- 数组、结构、共用体、枚举等构造数据类型
- 指针、空(void)类型
- 还可用typedef自定义数据类型
一些最基本数据结构可以用数据类型来实现,如数组、字符串等
另一些常用的数据结构,如栈、队列、树、图等,不能直接用数据类型来表示
数据类型(Data Type)
- 定义:数据类型是一组性质相同的值的集合以及定义于这个值集合上的一组操作的总称。
抽象数据类型(Abstract Data Type, ADT)
- 是指一个数学模型以及定义在此数学模型上的一组操作。
抽象数据类型的形式定义
- 抽象数据类型可用 ( D , S , P ) (D,S,P) (D,S,P)三元组表示
- 其中:D是数据对象,S是D上的关系集,P是对D的基本操作集
一个抽象数据类型的定义格式如下:
- 数据对象、数据关系的定义用伪代码描述
- 基本操作的定义格式为:
- 基本操作名(参数表)
- 初始条件:<初始条件描述>
- 操作结构:<操作结果描述>
基本操作定义格式说明:
- 参数表:赋值参数只为操作提供输入值。引用参数以
&
打头,除可提供输入值外,还将返回操作结果。 - 初始条件:描述操作执行之前数据结构和参数应满足的条件,若不满足,则操作失败,并返回相应出错信息,若初始条件为空,则省略之
- 操作结果:说明操作正常完成之后,数据结构的变化状况和应返回的结果。
抽象数据类型(ADT)定义举例:Circle的定义
抽象数据类型(ADT)定义举例:复数的定义
Assign(&Z, v1, v2)
操作结果:构造复数Z,其实部和虚部分别被赋以参数v1、v2值
Destroy(&Z)
操作结果:复数Z被销毁
GetReal(Z, &realPart)
初始条件:复数已存在
操作结果:用realPart返回复数Z的实部值
GetImag(Z, &ImagPart)
初始条件:复数已存在
操作结果:用ImagPart返回复数Z的虚部值
Add(Z1, Z2, &sum)
初始条件:Z1,Z2是复数
操作结果:sum返回两个复数Z1,Z2的和
1.3 抽象数据类型的表示和实现
ADT“复数”的实现
typedef struct{
float realPart; // 实部
float imagPart; // 虚部
}Complex
void assign(Complex *A, float real, float imag); // 赋值
void add(Complex *C, Complex A, Complex B); // A+B
void minus(Complex *C, Complex A, Complex B); // A-B
void multiply(Complex *C, Complex A, Complex B); // A*B
void divide(Complex *C, Complex A, Complex B); // A/B
void assign(Complex *A, float real, float imag){
A->realPart = real; // 实部赋值
A->imagPart = imag; // 虚部赋值
}
void add(Complex *C, Complex A, Complex B){
C->realPart = A.realPart + B.realPart; // 实部相加
C->imagPart = A.imagPart + B.imagPart; // 虚部相加
}
类C语言
- 用已有数据类型定义描述ADT的存储结构
- 用函数定义描述ADT的操作
#define TRUE 1
#define FLASE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status是函数的类型,其值是函数结果状态代码
typedef int Status;
1.4 算法和算法分析
算法(algorithm)是对特定问题求解方法和步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作,简而言之, 算法就是解决问题的方法和步骤。
算法的描述
- 自然语言
- 流程图:传统流程图、NS流程图
- 伪代码:类C语言
- 程序代码:C、JAVA
算法与程序
- 算法是解决问题的一种方法或一个过程,考虑如何将输入转换成输出,一个问题可以有多种算法。
- 程序是用某种程序设计语言对算法的具体实现。
算法特性:一个算法必须具备以下五个重要特性
- 有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成
- 确定性:算法中每一条指令必须有确切地涵义,没有二义性
- 可行性:算法是可执行的,算法描述的操作可以通过已经实现的基本操作执行有限次来实现。
- 输入:一个算法有0个或多个输入
- 输出:一个算法有一个或多个输出
算法设计的要求:
- 正确性(correctness)
- 程序不含语法错误
- 程序对于几组输入数据能够得出满足要求的结果
- 对于精心选择的、典型、苛刻且带有刁难性的机组输入数据能够得出满足要求的结果
- 对于一切合法的输入数据都能得出满足要求的结果
- 通常以第三层意义上的正确性作为衡量一个算法是否合格的标准
- 可读性(readability)
- 易于人的理解
- 晦涩难读的算法易于隐藏错误而难以调试
- 健壮/鲁棒性(robustness)
- 输入非法数据时,算法恰当地做出反应或进行相应处理,而非产生莫名其妙的输出结果
- 处理出错的方法,不应中断程序执行,而应返回一个表示错误或错误性质的值
- 高效性(efficiency)
- 要求花费尽量少的时间和尽量低的存储需求
一个好的算法首先要具备正确性、健壮性、可读性,在几方面都满足的条件下,主要考虑算法的效率
算法效率以下两个方面来考虑:
- 时间效率:算法所耗费的时间;
- 空间效率:指的是算法执行过程中所耗费的存储空间
二者有时候是矛盾的
算法时间效率的度量
- 依据该算法编制的程序在计算机上执行所消耗的时间来度量
- 两种度量方法
- 事后统计
- 将算法实现,测算其时间和空间开销
- 缺点:花费较多时间和精力;所得结果依赖于计算机的软硬件等环境因素,掩盖算法本身的优劣
- 事前分析
- 对算法所消耗资源的一种估算方法
- 一个算法的运行时间可以等于计算机执行一种简单操作(如赋值、比较、移动等)所需的时间与算法中进行的简单操作次数乘积(算法运行时间=一个简单操作所需时间 × \times ×简单操作次数)
- 算法运行时间= ∑ \sum ∑每条语句的执行次数(语句频度) × \times ×该语句执行一次所需的时间
- 每条语句执行一次所需的时间随机器而异,可假设均为单位时间,因此直接比较语句频度就好了
- 事后统计
例1:两个 n × n n\times n n×n矩阵相乘的算法可描述为
for(i=1;i<=n;i++){ // n+1次
for(j=1;j<=n;j++){ // n(n+1)次
c[i][j] = 0; // n*n次
for(k=0;k<n;k++){ // n*n*(n+1)次
c[i][j] = c[i][j] + a[i][k] * b[k][j]; // n*n*n次
}
}
}
该算法的时间消耗 T ( n ) = 2 n 3 + 3 n 2 + 2 n + 1 T(n)=2n^3+3n^2+2n+1 T(n)=2n3+3n2+2n+1,这是一个关于n的函数
为了便于比较不同算法的时间效率,仅比较它们的数量级
若有某个辅助函数
f
(
n
)
f(n)
f(n),使得当
n
n
n趋近于无穷大时,
T
(
n
)
/
f
(
n
)
T(n)/f(n)
T(n)/f(n)的极限值为不等于0的常数,则称
f
(
n
)
f(n)
f(n)是
T
(
n
)
T(n)
T(n)的同数量级函数,记作
T
(
n
)
=
O
(
f
(
n
)
)
T(n)=O(f(n))
T(n)=O(f(n)),称
O
(
f
(
n
)
)
O(f(n))
O(f(n))为算法的渐进时间复杂度(O是数量级的符号),简称时间复杂度
对于求解矩阵相乘问题,算法耗费时间
T
(
n
)
=
2
n
3
+
3
n
2
+
2
n
+
1
T(n)=2n^3+3n^2+2n+1
T(n)=2n3+3n2+2n+1
当
n
→
∞
n\to\infty
n→∞时,
T
(
n
)
/
n
3
→
2
T(n)/n^3\to2
T(n)/n3→2,表示
n
n
n充分大时,
T
(
n
)
T(n)
T(n)与
n
3
n^3
n3是同阶或同数量级,引入O符号,则
T
(
n
)
T(n)
T(n)可记作:
T
(
n
)
=
O
(
n
3
)
T(n)=O(n^3)
T(n)=O(n3),这就是求解矩阵相乘问题的算法时间复杂度。
一般情况下,不必计算所有操作的执行次数,只考虑算法中基本操作执行的次数,它是问题规模 n n n的某个函数,用 T ( n ) T(n) T(n)表示。
算法时间复杂度
算法中基本语句重复执行的次数是问题规模n的某个函数
f
(
n
)
f(n)
f(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)的增长率相同,称渐进时间复杂度
基本语句重复执行的次数:
- 算法中重复执行次数和算法的执行时间成正比的语句
- 对算法运行时间的贡献最大
- 执行次数最多
问题规模n:
- 排序:n为记录数
- 矩阵:n为矩阵的阶数
- 多项式:n为多项式的项数
- 集合:n为元素个数
- 树:n为树的结点个数
- 图:n为图的顶点数或边数
一般情况下,只用考虑算法中基本操作执行的次数
定理1.1
若 f ( n ) = a m n m + a m − 1 n m − 1 + ⋯ + a 1 n + a 0 f(n)=a_mn^m+a_{m-1}n^{m-1}+\cdots+a_1n+a_0 f(n)=amnm+am−1nm−1+⋯+a1n+a0是m次多项式,则 T ( n ) = O ( n m ) T(n)=O(n^m) T(n)=O(nm)
基本方法:
- 长出语句频度最大的那条语句作为基本语句
- 计算基本语句的频度得到问题规模 n n n的某个函数 f ( n ) f(n) f(n)
- 取其数量级用符号"O"表示
例2:
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
for(k=1;k<=j;k++)
x=x+1;
∑
i
=
1
n
∑
j
=
1
i
∑
k
=
1
j
1
=
∑
i
=
1
n
∑
j
=
1
i
j
=
∑
i
=
1
n
i
(
i
+
1
)
2
=
1
2
{
∑
i
=
1
n
i
2
+
∑
i
=
1
n
i
}
=
n
(
n
+
1
)
(
n
+
2
)
6
\sum_{i=1}^{n} \sum_{j=1}^{i}\sum_{k=1}^{j}1= \sum_{i=1}^{n} \sum_{j=1}^{i}j=\sum_{i=1}^{n} \frac{i(i+1)}{2} =\frac{1}{2}\left \{ \sum_{i=1}^{n}i^2+\sum_{i=1}^{n}i \right \} =\frac{n(n+1)(n+2)}{6}
i=1∑nj=1∑ik=1∑j1=i=1∑nj=1∑ij=i=1∑n2i(i+1)=21{i=1∑ni2+i=1∑ni}=6n(n+1)(n+2)
所以
T
(
n
)
=
O
(
n
3
)
T(n)=O(n^3)
T(n)=O(n3)
例3:分析以下程序段的时间复杂度
i=1;
while(i<=n)
i=i*2;
若循环执行1次:
i
=
1
∗
2
=
2
i=1*2=2
i=1∗2=2
若循环执行2次:
i
=
2
∗
2
=
2
2
i=2*2=2^2
i=2∗2=22
…
若循环执行
x
x
x次:
i
=
2
x
i=2^x
i=2x
由循环条件
i
≤
n
i\le n
i≤n,得
x
≤
log
2
n
x\le\log_2n
x≤log2n
取最大值
f
(
n
)
=
log
2
n
f(n)=\log_2n
f(n)=log2n
所以该程序段的时间复杂度
T
(
n
)
=
O
(
log
2
n
)
T(n)=O(\log_2n)
T(n)=O(log2n)
最坏时间复杂度:指在最坏情况下,算法的时间复杂度
平均时间复杂度:指在所有可能输入实例在等概率出现的情况下,算法的期望运行时间
最好时间复杂度:指在最好情况下,算法的时间复杂度
一般总是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长
对于复杂的算法,可以将它分成几个容易估算的部分,然后利用大O加法法则和乘法法则,计算算法的时间复杂度:
- 加法规则
T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( max ( f ( n ) , g ( n ) ) ) T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O(\max(f(n),g(n))) T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n))) - 乘法规则
T ( n ) = T 1 ( n ) × T 2 ( n ) = O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) T(n)=T_1(n)\times T_2(n)=O(f(n))\times O(g(n))=O(f(n)\times g(n)) T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))
算法时间效率的比较
当n取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊
时间复杂度
T
(
n
)
T(n)
T(n)按数量级递增顺序为:
渐进空间复杂度
- 空间复杂度:算法所需存储空间的度量
- 记作: S ( n ) = O ( f ( n ) ) S(n)=O(f(n)) S(n)=O(f(n)),其中n为问题的规模(或大小)
- 算法要占据的空间
- 算法本身要占据的空间,输入/输出,指令,常数,变量
- 算法要使用的辅助空间
例:将一维数组a中的n个数逆序存放到原数组中。
算法1:
S
(
n
)
=
O
(
1
)
S(n)=O(1)
S(n)=O(1)——原地工作
for(i=0;i<n/2;i++){
t=a[i];
a[i]=a[n-i-1];
a[n-i-1]=t;
}
算法2
S
(
n
)
=
O
(
n
)
S(n)=O(n)
S(n)=O(n)
for(i=0;i<n;i++)
b[i]=a[n-i-1];
for(i=0;i<n;i++)
a[i]=b[i];
设计好算法的过程