参考课程:数据结构与算法基础(青岛大学-王卓),笔记中截图都来自王老师的课件
参考书目:《数据结构(C语言版)》严蔚敏 吴伟民 著
第一章 绪论
Niklaus Wirth教授提出:程序=算法+数据结构
算法:处理问题的策略;数据结构:问题的数学模型 ;程序设计:为计算机处理问题编制一组指令集
软件=程序+文档(软件工程的观点)
数据结构是一门研究非数值计算
的程序设计中计算机的操作对象以及他们之间的关系
和操作
的学科。
基本概念
1.数据:在计算机领域中指所有能被计算机识别、存储、处理、传输的符号的集合,是计算机程序加工处理的对象。(包括数值型数据:整数、实数等;非数值型数据:文字、图像、图形、声音等)
2.数据元素:数据中的一个“个体”,是数据的基本单位
,又可称为元素、节点、顶点或记录。在计算机程序中通常作为一个整体进行考虑和处理。
3.数据项:构成数据元素不可分割的具有独立含义的最小标识单位。(字段,属性)例如学号、姓名。
数据>数据元素>数据项
4.数据对象:性质相同的数据元素的集合,是数据的一个子集。
5.数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。这些数据元素不是孤立存在的,而是有着某种关系,这种关系称为结构。或者说,数据结构是带结构的数据元素的集合。
数据结构包括以下三个方面的内容:
- 数据元素之间的逻辑关系,也称为
逻辑结构
- 数据元素及其关系在计算机内存中的表示(又称为映像),成为数据的物理结构或数据的
存储结构
- 数据的运算和实现,即对数据元素可以施加的操作以及这些操作在相应的存储结构上的实现
存储结构是逻辑关系的映像与元素本身的映像;逻辑结构是数据结构的抽象,存储结构是数据结构的实现。
逻辑结构的种类:
- (1)集合结构:结构中的数据元素之间除了同属一个集合的关系外,无任何其他关系
- (2)线性结构:结构中的数据元素之间存在着一对一的线性关系
- (3)树形结构:结构中的数据元素之间存在着一对多的层次关系
- (4)图状结构或网状结构:结构中的数据元素之间存在着多对多的任意关系
存储结构的种类:
- (1)顺序存储结构:逻辑上相邻,存储上也相邻。用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的
存储位置
来表示(C语言中的数组) - (2)链接存储结构:附加指针,表示关系。用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系用
指针
来表示(C语言中的链表) - (3)索引存储结构:在存储结点信息的同时,还建立附加的索引表
- (4)散列存储结构:根据结点的关键字直接计算出该结点的存储位置
顺序存储与链接存储的比较:
顺序存储需要一块连续的存储空间,存储单元仅存储了数据元素,存储密度最大。逻辑关系是隐式的,逻辑相邻通过元素存储单元相邻表示。
链接存储不需要一块连续的存储空间,存储单元存储数据元素和后继元素的存储地址,显示存储逻辑关系。
数据类型和抽象数据类型
数据类型:是一组性质相同的值
的集合以及定于与这个值集合上的一组操作
的总称。数据类型=值的集合+值集合上的一组操作。
抽象数据类型(abstract data type 简称ADT):是指一个数学模型以及定义在此数学模型上的一组操作。
抽象数据类型的形式定义:使用三元组表示:(D,S,P)其中D是数据对象,S是D上的关系集,P是对D的基本操作集。
一个抽象数据类型的定义格式如下:
ADT 抽象数据类型名{
数据对象:〈数据对象的定义〉
数据关系:〈数据关系的定义〉
基本操作:〈基本操作的定义〉
} ADT 抽象数据类型名
其中基本操作的定义格式为:
基本操作名(参数表)
初始条件:〈初始条件描述〉
操作结果:〈操作结果描述〉
基本操作由两种参数:赋值参数和引用参数:
赋值参数只为操作提供输入值;引用参数以&打头,除可提供输入值外,还将返回操作结果。
初始条件描述操作执行之前的数据结构和参数应满足的条件,若不满足,则操作失败,并返回相应出错信息。初始条件可以为空,并可省略。操作结果说明在操作正常完成之后,数据结构的变化状况和应返回的结果。
例,抽象数据类型复数的定义:
ADT Complex
{
数据对象:
D={e1,e2|e1,e2均为实数}
数据关系:
R1={<e1,e2>|e1是复数的实数部分,e2 是复数的虚数部分。}
基本操作:InitComplex(&Z,v1,v2)
操作结果:构造复数Z,其实部和虚部分别赋以参数v1和v2的值。
DestroyComplex(&Z)
初始条件:复数Z已存在。
操作结果:复数Z被销毁。GetReal(Z,&realpart)
初始条件:复数Z已存在。
操作结果:用realpart返回复数Z的实部值。GetImag(Z,&Imagpart)
初始条件:复数Z已存在。
操作结果:用Imagpart返回复数Z的虚部值。Add(z1,z2,&sum)
初始条件: z1,z2是复数。
操作结果:用sum返回两个复数z1,z2的和值。} ADT Complex
抽象数据类型的表示与实现
用C语言真正实现抽象数据类型的定义(抽象数据类型“复数”的实现)
#include <stdio.h>
typedef struct {
float realpart;
float imagpart;
}Complex;
void assign(Complex* A, float real, float imag); /*赋值操作*/
void add(Complex* C, Complex *A, Complex *B); /*加法*/
void minus(Complex* C, Complex *A, Complex *B); /*减法*/
void multiply(Complex* C, Complex *A, Complex *B); /*乘法*/
void divide(Complex* C, Complex *A, Complex *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;
}
void minus(Complex* C, Complex* A, Complex* B) {
C->realpart = A->realpart - B->realpart;
C->imagpart = A->imagpart - B->imagpart;
}
void 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);
}
void divide(Complex* C, Complex* A, Complex* B) {
float denominator;
denominator = B->realpart * B->realpart + B->imagpart * B->imagpart;
C->realpart = ((A->realpart * B->realpart) + (A->imagpart * B->imagpart)) / denominator;
C->imagpart = ((A->imagpart * B->realpart) - (A->realpart * B->imagpart)) / denominator;
}
int main() {
Complex M;
Complex N;
Complex y;
assign(&M, 2, 3);
assign(&N, 3, 7);
minus(&y, &M, &N);
if (y.imagpart > 0) {
printf("y = %f+%fi\n", y.realpart, y.imagpart);
}
else if (y.imagpart < 0) {
printf("y = %f%fi\n", y.realpart, y.imagpart);
}
else {
printf("y = %f\n", y.realpart);
}
}
算法与算法分析
算法(algorithm):是对特定问题求解步骤的一种描述,它是指令的有限序列
,其中每一条指令表示一个或多个操作。算法需要具有下面5个特性:
- 有穷性:步骤,指令有限
- 确定性:算法中的每条指令都必须是清楚的,指令无二义性
- 可行性:每一步或者每个指令是可行的,经过有限次计算能够完成
- 有输入:具有0个或多个由外界提供的量(输入)
- 有输出:产生1个或多个结果
算发的描述形式:
- 自然语言(简单,但不够严谨)
- 图示:流程图、N-S图(直观,但不能运行。)
- 伪语言(介于高级语言和自然语言之间,描述简单,忽略语法,不能编译、执行)。
- 高级程序设计语言:C/C++、Java、python等等。(能编译、执行)。
算法与程序的区别:算法可以转换为程序,但是程序不一定是算法。例如操作系统是程序不是算法。
算法设计的定性评价:正确性、可读性、健壮性(鲁棒性)、效率与低存储量需求
算法设计的定量评价
- 时间效率:算法运行时所耗费的系统时间,称为时间复杂度
- 空间效率:算法运行时所耗费的存储空间,称为空间复杂度
算法效率的度量:
事前分析方法:算法运行时间 = 一个简单操作所需的时间*简单操作次数。
例如下面是两个N*N矩阵的相乘的算法:
int i,j,k;
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 = 1; k <= n; k++) { //执行n*n*(n+1)次
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趋向无穷大时,时间复杂度
T(n)的数量级,亦称为算法的渐近时间复杂度
,一般记为T(n)= O(f(n))。f(n)是问题规模n的某个函数。上面n*n的矩阵算法中时间复杂度为:
T
(
n
)
=
O
(
n
3
)
T(n)=O(n^3)
T(n)=O(n3)
算法时间复杂度的定义:
算法中基本语句重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记为: T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
时间复杂度是由嵌套最深层语句的频度决定的。可以使用数学公式中级数求和的方法进行计算。如下图所示:
下面是一个易错的题目(关键是要找出来执行次数n与i的关系):
若循环执行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次: i = 2 n − 1 ∗ 2 = 2 x i=2^{n-1}*2=2^x i=2n−1∗2=2x
如果语句②执行了x次,因为 i<=n,所以 2 x < = n 2^x<=n 2x<=n即, x < = l o g 2 n x<=log_{2}{n} x<=log2n,取最大值 f ( n ) < = l o g 2 n f(n)<=log_{2}{n} f(n)<=log2n,(不区分对数以谁为底)所以时间复杂度为 T ( n ) = O ( l o g 2 n ) T(n)=O(log_{2}{n}) T(n)=O(log2n)
值得注意的是:有的情况下,算法中基本操作重复执行的次数还随问题的输入数据集不同而不同,如下图情况:
算法空间复杂度:算法所需要的存储空间的度量,记作 S ( n ) = O ( f ( n ) ) S(n)=O(f(n)) S(n)=O(f(n))其中n为问题的规模。例如要把一维数组a中的n个数逆序存放到原数组中:
算法1中只用到了一个变量t,算法1的空间复杂度是 S ( n ) = O ( 1 ) S(n)=O(1) S(n)=O(1),而算法2的空间复杂度 S ( n ) = O ( n ) S(n)=O(n) S(n)=O(n)。
总结:抽象数据类型=数据的逻辑结构(不同的存储结构)+抽象运算(运算功能的描述)