绪论
基本概念和术语
数据结构的两个层次
1.逻辑结构
·描述数据元素之间的逻辑关系
·与数据的存储无关,独立于计算机
·是从具体问题抽象出来的数学模型
2.物理结构(存储结构)
·数据元素及其关系在计算机存储器中的结构(存储方式)
·是数据结构在计算机中的表示
逻辑结构与存储结构的关系
·存储结构是逻辑关系的映像与元素本身的映像
·逻辑结构是数据结构的抽象,存储结构是数据结构的实现
·两者综合起来建立了数据元素之间的结构关系
逻辑结构的种类
划分方法一
(1)线性结构
有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前驱和一个直接后继
例如:线性表、栈、队列、串
(2)非线性结构
一个结点可能有多个直接前驱和直接后继
例如:树、图
划分方法二
(1)集合结构:
结构中的数据元素之间除了同属于一个集合 的关系外,无任何其他关系
(2)线性结构:
结构中的数据元素之间存在着一对一 的线性关系
(3)树形结构:
结构中的数据元素之间存在着一对多 的层次关系
(4)图状结构或网状结构
结构中的数据元素之间存在着多对多 的任意关系
存储结构的种类
顺序存储结构:
用一组连续 的存储单元依次 存储数据元素,数据元素之间的逻辑关系由元素的存储位置 来表示
C语言中用数组来实现顺序存储机构
链式存储结构:
用一组任意 的存储单元存储数据元素,数据元素之间的逻辑关系用指针 来表示
C语言中用指针来实现链式存储结构
索引存储结构:
在存储结点信息的同时,还建立附加的索引表
索引表中的每一项称为一个索引项
索引项的一般形式 是:(关键字,地址)
关键字是能唯一标识 一个结点的哪些数据项
若每个结点在索引表中都有一个索引项,则该索引表称之为稠密索引 (Dense Index)。若一组结点在索引表中只对应一个索引项,则该索引表称之为稀疏索引 (Sparse Index).
散列存储结构:
根据结点的关键字直接计算出该结点的存储地址
数据类型和抽象数据类型
在使用高级程序设计语言编写程序时,必须对程序中出现的每个变量,常量或表达式,明确说明它们所属的数据类型
例如,C语言中:
提供int、char、float、double、等基本数据类型
数组、结构、共用体、枚举等构造数据类型
还有指针,空(void)类型
用户也可以用typedef自己定义数据类型
一些最基本数据结构可以用数据类型来实现,如数组、字符串等
而另一些常用的数据结构,如栈、队列、树、图等,不能直接用数据类型来表示
抽象数据类型
是指一个数学模型以及定义在此数学模型上的一组操作
1.由用户定义,从问题抽象出数据模型 (逻辑结构)
2.还包括定义在数据模型上的一组抽象运算 (相关操作)
3.不考虑计算机内的具体存储与运算的具体实现算法
抽象数据类型的形式定义
抽象数据类型可用(D,S,P)三元组表示。
其中:
D是数据对象
S是D上的关系集合
P是对D的基本操作集
一个抽象数据类型的定义格式:
ADT 抽象数据类型名{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作:<基本操作的定义>
}ADT 抽象数据类型名
其中:数据对象、数据关系的定义用伪代码描述
基本操作的定义格式为:
- 基本操作名(参数表)
- 初始条件:<初始条件描述>
- 操作结果:<操作结果描述>
基本操作 定义格式说明:
参数表:赋值参数只为操作提供输入值
引用参数以&打头,除可提供输入值外,还将返回操作结果
初始条件:描述操作执行之前数据结构和参数应满足的条件,若不满足,则操作失败,并返回相应出错信息。若初始条件为空,则省略之
操作结果:说明操作正常完成之后,数据结构的变化状况和应返回的结果
抽象数据类型定义举例:Circle的定义
ADT 抽象数据类型名{
Data
数据对象的定义
数据元素之间逻辑关系的定义
Operation
操作1
初始条件
操作结果描述
操作2
…
操作n
…
}
ADT Circle{
数据对象: D = {r,x,y|r,x,y均为实数}
数据关系: R = {<r,x,y>|r是半径,<x,y>是圆心坐标}
基本操作:
Circle(&C,r,x,y)
操作结构:构造一个圆
double Area©
初始条件:圆已存在
操作结构:计算面积
double Circumstance©
初始条件:圆已存在
操作结果:计算周长
}
抽象数据类型的表示和实现
ADT 抽象数据类型名{
Data
数据对象的定义
数据元素之间逻辑关系的定义
Operation
操作1
初始条件
操作结果描述
操作2
…
操作n
…
}
C语言实现抽象数据类型
用已有的数据类型定义描述它的存储结构
用函数定义描述它的操作
就可以在程序中使用
例如:抽象数据类型“复数”的实现
1 2 3 4 typedef struct { float realpart; float imagpart; }Complex
1 2 3 4 void assign (Complex *A,float real,float imag) { A->realpart = real; A->imagepart = imag; }
1 2 3 4 void add (Complex *c,Complex A,Complex B) { c->realpart = A.realpart + B.realpart; c->imagpart = A.imagpart + B.imagpart; }
注意:Complex是我们定义的一个结构体类型
带*:指针变量,它是指向Complex类型的指针
不带*:Complex类型的普通变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdlib.h> typedef struct { float realpart; float imagpart; }Complex; 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; } int main (void ) { Complex z1,z2,z3,z4,z; float RealPart,ImagePart; assign(&z1,8.0 ,6.0 ); assign(&z2,4.0 ,3.0 ); add(&z3,z1,z2); multiply(z1,z2,z4); if (divide(&z,z4,z3)){ GetReal(&z,RealPart); GetImag(&z,ImagePart); } return 0 ; }
以上代码由类C语言实现,仍有不全之处,读者自行补充
算法和算法分析
算法特性:一个算法必须具备以下五个重要特性
有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在又穷时间内完成
确定性:算法中的每一条指令必须有确切的含义,没有二义性,在任何条件下,只有唯一的一条执行路径,即对于相同的输入只能得到相同的输出。
可行性:算法是可执行的,算法描述的操作可以通过已经实现的基本操作执行有限次来实现
输入:一个算法有零个或多个输入
输出:一个算法有一个或多个输出
算法设计的要求
算法效率以下两个方面来考虑:
1.时间效率: 指的是算法所消耗的时间
2.空间效率: 指的是算法执行的过程中所消耗的存储空间
时间效率和空间效率有时候是矛盾的
算法时间效率的度量
算法时间效率可以用一句该算法编制的程序在计算机上执行所消耗的时间 来度量
两种度量方法
事后统计:
将算法实现后,测算其时间和空间开销
事前统计:
对算法所消耗资源的一种估算方法
事前分析方法
一个算法的运行时间是指一个算法在计算机上运行所耗费的时间大致可以等于计算机执行一种简单的操作(如赋值、比较、移动等)所需的时间 与算法中进行的简单操作次数乘积
算法运行时间 = 一个简单操作所需的时间 × 简单操作次数
也即算法中每条语句的执行时间之和
算法运行时间 = ∑每条语句的执行次数 × 该语句执行一次所需的时间
每条语句执行一次所需的时间,一般是由机器而异的。取决于机器的指令性能、速度以及编译的代码质量。是由机器本身软硬件环境决定的,它与算法无关。
所以,我们可假设执行每条语句所需的时间均为单位时间。此时对算法的运行时间的讨论就可转化为讨论该算法中所有语句的执行次数,即频度之和了。
例如:两个n×n矩阵相乘的算法可描述为:
1 2 3 4 5 6 7 8 for (i = 1 ;i <= n; i++){ for (j = 1 ; j <= n; j++){ c[i][j] = 0 ; for (k = 0 ; k < n; k++){ c[i][j] = c[i][j] + a[i][k]*b[k][j]; } } }
为了便于比较不同算法的时间效率,我们仅比较它们的数量级
例如:两个不同的算法,时间消耗分别是:
T1(n) = 10n² 与 T2(n) = 5n³
若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数 ,则成f(n)是T(n)的同数量级函数。记作T(n) = O(f(n)),称 O(f(n))为算法的渐进时间复杂度 (O是数量级的符号),简称时间复杂度
算法时间复杂度定义
算法中基本语句重复执行的次数 是问题规模n 的某个函数f(n),算法的时间量度记作:T(n) = O(f(n))
基本语句:
算法中重复执行次数和算法的执行时间成正比的语句
对算法运行时间的贡献最大
执行次数最多
n越大算法的执行时间越长
排序:n为记录数
矩阵:n为矩阵的阶数
多项式:n为多项式的项数
集合:n为元素个数
树:n为树的结点个数
图:n为图的定点数或边数
定理1.1
若f(n) = am n^m + am-1 n^m-1 + … + a1 n + a0是m次多项式
则T(n) = O(n^m)
忽略所有低次幂项和最高次幂系数,体现出增长率的含义
分析算法时间复杂度的基本方法
1.找出语句频度最大 的那条语句作为基本语句
2计算基本语句 的频度得到问题规模n的某个函数f(n)
3.取其数量级用符号"O"表示
例1:
1 2 3 4 5 6 x = 0 ; y =0 ; for (int k = 0 ;k < n; k++) x ++; for (int i = 0 ;i < n;i++) for (int j = 0 ;j < n;j++) y ++;
时间复杂度是由嵌套最深语句的频度决定的
例2:
1 2 3 4 for (i = 1 ji <= 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 语句频度=\sum_{i=1}^n\sum_{j=1}^i\sum_{k=1}^j1=\sum_{i=1}^n\sum_{j=1}^ij=\sum_{i=1}^n\frac{i(i+1)}{2}
语 句 频 度 = i = 1 ∑ n j = 1 ∑ i k = 1 ∑ j 1 = i = 1 ∑ n j = 1 ∑ i j = i = 1 ∑ n 2 i ( i + 1 )
例3:
1 i = 1 ;①while (i <= n) ② i = i * 2 ;
关键是要找出来执行次数x与n的关系,并形成n的函数
若 循 环 执 行 1 次 : i = 1 ∗ 2 = 2 , 若 循 环 执 行 2 次 : i = 2 ∗ 2 = 2 2 , 若 循 环 执 行 3 次 : i = 2 ∗ 2 ∗ 2 = 2 3 , 若 循 环 执 行 x 次 : i = 2 x 设 语 句 ② 执 行 次 数 为 x 次 , 由 循 环 条 件 i ≤ n ∴ 2 x ≤ n ∴ x ≤ l o g 2 n 2 f ( n ) ≤ n 即 f ( n ) ≤ l o g 2 n 取 最 大 值 f ( n ) = l o g 2 n 所 以 该 程 序 段 的 时 间 复 杂 度 T ( n ) = O ( l o g 2 n ) 若循环执行1次:i = 1*2=2,\\
若循环执行2次:i = 2*2=2^{2},\\
若循环执行3次:i = 2*2*2=2^{3},\\
若循环执行x次:i = 2^{x}\\
设语句②执行次数为x次,由循环条件\\
i \leq n\\
∴ 2^{x} \leq n\\
∴ x \leq {log_2{n}}\\
2^{f(n)} \leq n\\
即 f(n) \leq {log_2{n}}\\
取最大值 f(n) = {log_2{n}}\\
所以该程序段的时间复杂度T(n) = O({log_2{n}})
若 循 环 执 行 1 次 : i = 1 ∗ 2 = 2 , 若 循 环 执 行 2 次 : i = 2 ∗ 2 = 2 2 , 若 循 环 执 行 3 次 : i = 2 ∗ 2 ∗ 2 = 2 3 , 若 循 环 执 行 x 次 : i = 2 x 设 语 句 ② 执 行 次 数 为 x 次 , 由 循 环 条 件 i ≤ n ∴ 2 x ≤ n ∴ x ≤ l o g 2 n 2 f ( n ) ≤ n 即 f ( n ) ≤ l o g 2 n 取 最 大 值 f ( n ) = l o g 2 n 所 以 该 程 序 段 的 时 间 复 杂 度 T ( n ) = O ( l o g 2 n )
请注意:有的情况下,算法中基本操作重复执行的次数还随问题的输入数据集 不同而不同
例:顺序查找,在数组a[i]中查找值等于e的元素,返回其所在位置
1 2 3 for (i = 0 ;i < n;i ++) if (a[i] == e) return i+1 ; return 0 ;
最好情况:1次
最坏情况:n
平均时间复杂度:O(n)
最坏时间复杂度: 指在最坏情况下,算法的时间复杂度
平均时间复杂度: 指在所有可能输入实例在等概率出现的情况下,算法的期望运行时间
最好时间复杂度: 指在最好情况下,算法的时间复杂度
一般总是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长
对于复杂的算法,可以将它分成几个容易估算的部分,然后利用大O加法法则和乘法法则,计算算法的时间复杂度:
a)加法规则
T(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(max(f(n),g(n)))
b)乘法规则
T(n) = T1(n) × T2(n) = O(f(n)) × O(g(n)) = O(f(n)×g(n))
算法时间效率的比较
当n取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊
时间复杂度T(n)按数量级递增顺序为:
常 数 阶 O ( 1 ) 对 数 阶 O ( l o g 2 n ) 线 性 阶 O ( n ) 线 性 对 数 阶 O ( n l o g 2 n ) 平 方 阶 O ( n 2 ) . . . . . . . . . 立 方 阶 O ( n k ) 指 数 阶 O ( 2 n ) 常数阶 O(1)\\
对数阶 O({log_2{n}})\\
线性阶 O(n)\\
线性对数阶 O(n {log_2{n}})\\
平方阶 O(n^2)\\
.........\\
立方阶 O(n^k)\\
指数阶 O(2^n)
常 数 阶 O ( 1 ) 对 数 阶 O ( l o g 2 n ) 线 性 阶 O ( n ) 线 性 对 数 阶 O ( n l o g 2 n ) 平 方 阶 O ( n 2 ) . . . . . . . . . 立 方 阶 O ( n k ) 指 数 阶 O ( 2 n )
渐进空间复杂度
记作 S(n) = O(f(n))
其中n为问题的规模
算法本身要占据的空间,输入/输出,指令,常数,变量等
算法要是用的辅助空间
例:将一维数组a中的n个数逆序存放到原数组中
【算法1】
1 for ( i = 0 ; i < n/2 ; i++){ t = a[i]; a[i] = a[n-i-1 ]; a[n-i-1 ] = t;}
S(n) = O(1)
【算法2】
1 2 3 4 for (i = 0 ;i < n;i ++) b[i] = a[n-i-1 ] for (i = 0 ; i < n;i ++) a[i] = b[i]
S(n) = O(n)
线性表
线性表的定义和特点
线性表是具有相同特性的数据元素的一个有限序列
线性表(Linear List):
由n(n>=0)个数据元素(结点)a1,a2,…an组成的 有限序列
其中数据元素的个数 n定义为表的长度
当n=0时称为空表
将非空的线性表(n>0)记作:(a1,a2,an)
这里的数据元素ai(1<=i<=n)只是一个抽象的符号,其具体含义在不同的情况下可以不同
线性表的例子
例1:分析26个英文字母组成的英文表
(A,B,C,D,…,Z)
数据元素都是字母;元素关系是线性
例2:分析学生情况登记表
学号 姓名 性别 年龄 班级
498623 nww 男 26 9
线性表的逻辑特征
从以上例子可看出线性表的逻辑特征是:
在非空的线性表,有且仅有一个开始结点a1,它没有直接前驱,而有且仅有一个直接后继a2;
有且仅有一个终端节点an,它没有直接后继,而有且仅有一个直接前驱an-1;
其他的内部节点==结点ai(2<=i<=n-1)都有且仅有一个直接前驱ai-1和一个直接后继ai+1
线性表是一种典型的线性结构
案例引入
案例【2.1】一元多项式的运算:实现两个多项式加、减、乘运算
Pn(x) = p0 + p1x + p2x^2 + … + pnx^n
线性表P = (p0,p1,p2…pn)
(每一项的指数i隐含在其系数pi的序号中)
例如P(x) = 10 +5x - 4x^2 + 3x^3 + 2x^4 用数组来表示
指数(下标i)
0
1
2
3
4
系数p[i]
10
5
-4
3
2
稀疏多项式
S(x) = 1 + 3x^10000 + 2x^20000
会造成存储空间的很大浪费,怎么办?
案例【2.2】稀疏多项式的运算
多项式非零项的数组表示
(a) A(x) = 7 + 3x + 9x^8 + 5x^17
下标i
0
1
2
3
系数a[i]
7
3
9
5
指数
0
1
8
17
(b) B(x) = 8x + 22x^7 - 9x^8
下标i
0
1
2
系数b[i]
8
22
-9
指数
1
7
8
Pn(x) = p1x^e1 + p2x^e2 + … + pmx^em
线性表 P = ((p1,e1),(p2,e2),…(pm,em))
线性表 A = ((7,0),(3,1),(9,8,),(5,17))
线性表 B = ((8,1),(22,7),(-9,8))
顺序存储结构存在问题
链式存储结构
线性表的类型定义
基本操作:
InitList(&L)
DestoryList(&L)
ClearList(&L)
初始条件:线性表L已经存在
操作结果:将线性表L重置为空表
ListEmpty(L)
初始条件:线性表L已经存在
操作结果:若线性表L为空表,则返回TRUE;否则返回FALSE4
ListLength(L)
初始条件:线性表L已经存在
操作结果:返回线性表L中的数据元素个数
GetElem(L,i,&e)
初始条件:线性表L已经存在,1<=i<=ListLength(L)
操作结果:用e返回线性表L中第i个数据元素的值
LocateElem(L,e,compare())
初始条件:线性表L已经存在,compare()是数据元素判定函数
操作结果:返回L中第1个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值为0
PriorElem(L.cur_e,&pre_e)
初始条件:线性表L已经存在
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败;pre_e无意义
NextElem(L,cur_e,&next_e)
初始条件:线性表L已经存在
操作结果:若cur_e是L的数据元素,且不是第最后个,则用next_e返回它的后继,否则操作失败,next_e无意义
ListInsert(&L,i,e)
初始条件:线性表L已经存在,1<=i<=ListLengtgh(L) + 1
操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加一
ListDelete(&L,i,&e)
初始条件:线性表L已经存在,1<=i<=ListDelete(&L,i,&e)
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一
ListTraverse(&L,visited())
初始条件:线性表L已经存在
操作结果:依次对线性表中的每个元素调用visited()
线性表的顺序表示和实现
用一维数组表示顺序表
用一变量表示顺序表的长度属性
1 2 3 4 5 6 #define LIST_INIT_SIZE 100 typedef int Element;typedef struct { ElemType elem[LIST_INIT_SIZE]; int length; }SqList;
多项式的顺序存储结构类型定义
Pn(x) = p1x^e1 + p2x^e2 + … +pmx^em
1 2 3 4 5 6 7 8 9 #define MAXSIZE 1000 typedef struct { float p; int e; }Polynomial; typedef struct { Polynomial *elem; int length; }SqList;
图书表的顺序存储结构类型定义
1 2 3 4 5 6 7 8 9 10 #define MAXSIZE 10000 typedef struct { char no[20 ]; char name[50 ]; float price; }Book; typedef struct { Book *elem; int length; }SqList;
补充:元素类型说明
顺序表类型定义:
1 2 3 4 5 typedef struct { ElemType data[]; int length; }SqList;
补充:数组静态分配
1 2 3 4 typedef struct { ElemType data[MaxSize]; int length; }SqList;
补充:数组动态分配
1 2 3 4 5 6 7 typedef struct { ElemType *data; int length; }SqList; SqList L; L.data = (ElemType*)malloc (sizeof (ElemType)*MaxSize);
补充:C语言的内存动态分配
malloc(m)函数,开辟m字节长度地址空间,并返回这段空间的首地址
sizeof(x)运算,计算变量x的长度
free§函数,释放指针p所指变量的存储空间,即彻底删除一个变量
需要加载头文件:<stdlib.h >
顺序表基本操作实现
线性表的基本操作
InitList(&L) //初始化操作,建立一个空的线性表L
Destory(&L) //销毁已存在的线性表L
ClearList(&L) //将线性表清空
**ListInsert(&L,i,e) ** //在线性表L中的第i个位置插入新元素e
ListDelete(&L,i,&e) //删除线性表L中的第i个位置元素,用e返回
IsEmpty(L) //若线性表为空,则返回True,否则False
ListLength(L) //返回线性表L的元素个数
LocateElem(L,e) //L中查找与给定值e相等的元素,若成功返回该元素在表中的序号,否则返回0
GetElem(L,i,&e) //将线性表L的第i个位置元素返回给e
补充:操作算法中用到的预定义常量和类型
1 2 3 4 5 6 7 8 9 10 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status;typedef char ElemType;
顺序表基本操作的实现
【算法2.1】线性表L的初始化(参数用引用)
1 2 3 4 5 6 Status InitList_Sq (SqList &L) { L.elem = (ElemType*)malloc (sizeof (ElemType)*MaxSize); if (!L.elem) exit (OVERFLOW); L.length = 0 ; return OK; }
【算法2.2】销毁线性表
1 2 3 void Destory (SqList &L) { if (L.elem) free (L.elem); }
【算法2.3】清空线性表
1 void ClearList (SqList &L) { L.length = 0 ;
【算法2.4】求线性表的长度
1 int GetLength (SqList L) { return L.length;}
【算法2.5】判断线性表是否为空
1 int IsEmpty (SqList L) { if (L.length == 0 ) return 1 ; else return 0 ;}
【算法2.6】顺序表的取值(根据位置i获取相应位置数据元素的内容)
1 int GetElem (SqList L,int i,ElemType &e) { if (i<1 ||i>L.length) return ERROR;
顺序表上的查找操作
按值查找:
例如:在图书表中,按照给定书号进行查找,确定是否存在该图书
如果存在:输出是第几个元素
如果不存在:输出0
【算法2.7】顺序表的查找
在线性表L中查找与指定值e相同的数据元素的位置
从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号,未找到,返回0
1 int LocateElem (SqList L,ElemType e) {
1 2 3 4 5 6 7 int LocateElem (SqList L,ElemType e) { i = 0 ; while (i<L.length&&L.elem[i]!=e) i++; if (i<L.length) return i+1 ; return 0 ; }
顺序表的查找算法分析:
因为查找算法的基本操作为:将记录的关键字同给定值进行比较
基本操作:L.elem[i] == e
平均查找长度ASL(Average Search Length):
为确定记录在表中位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度
A S L = ∑ i = 1 n P i C i ASL = \sum_{i = 1}^nP_iC_i
A S L = i = 1 ∑ n P i C i
【算法2.8】顺序表的插入
算法思想:
判断插入位置i是否合法
判断顺序表的存储空间是否已满,若已满返回ERROR
将第n至第i位的元素依次向后移动一个位置,空出第i个位置
将要插入的新元素e放入第i个位置
1 2 3 4 5 6 7 8 9 Status ListInsert_Sq (SqList &L,int i,ElemType e) { ① if (i<1 ||i>L.length+1 ) return ERROR; ② if (L.length == MAXSIZE) return ERROR; ③ for (j = L.length-1 ;j>=i-1 ;j--){ L.elem[j+1 ] = L.elem[j]; } ④ L.elem[i-1 ] = e; ⑤ L.length ++; }
【算法2.9】顺序表的删除
算法思想:
判断删除位置i是否合法(合法值为1<=i<=n)
将欲删除的元素保留在e中
将第i+1至第n位的元素依次向前移动一个位置
表长减一,删除成功返回OK
1 2 3 4 5 6 7 8 Status ListDelete_Sq (SqList &L,int i) { ① if (i<1 ||i>L.length) return ERROR; ② for (j = i;j<=L.length-1 ;j++){ L.elem[j-1 ] = L.elem[j]; } ④ L.length--; return OK; }
小结
顺序表(线性表的顺序存储结构)的特点
(1)利用数据元素的存储位置表示线性表中的相邻数据元素之间的前后关系,即线性表得逻辑结构与存储结构一致
(2)在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
线性表的基本操作
InitList(&L) //初始化操作,建立一个空的线性表L
Destory(&L) //销毁已存在的线性表L
ClearList(&L) //将线性表清空
**ListInsert(&L,i,e) ** //在线性表L中的第i个位置插入新元素e
ListDelete(&L,i,&e) //删除线性表L中的第i个位置元素,用e返回
IsEmpty(L) //若线性表为空,则返回True,否则False
ListLength(L) //返回线性表L的元素个数
LocateElem(L,e) //L中查找与给定值e相等的元素,若成功返回该元素在表中的序号,否则返回0
GetElem(L,i,&e) //将线性表L的第i个位置元素返回给e
优点
存储密度大(结点本身所占存储量/结点结构所占存储量)
可以随机存取表中任一元素
缺点
在插入、删除某一元素时,需要移动大量元素
浪费存储空间
属于静态存储形式,数据元素的个数不能自由扩充
线性表的链式表示和实现
链式存储结构
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
线性表的链式表示又称为非顺序映像 或链式映像
用一组物理位置任意的存储单元 来存放线性表的数据元素
这组存储单元既可以是连续的 ,也可以是不连续的 ,甚至是零散分布在内存中任意位置上的
链表中元素的逻辑次序和物理次序不一定相同
例
线性表(赵,钱,孙,李,周,吴,郑,王)
顺序表
存储地址
存储状态
0031
赵
0033
钱
0035
孙
0037
李
0039
周
0041
吴
0043
郑
0045
王
链表
存储地址
数据域
指针域
0001
李
0043
0007
钱
0013
0013
孙
0001
0019
王
NULL
0025
吴
0037
0031
赵
0007
0037
郑
0019
0043
周
0025
与链式存储有关的术语
1.结点 :数据元素的存储映像。由数据域和指针域两部分组成
2.链表 :n个结点由指针链 组成一个链表
它是线性表的链式存储映像,称为线性表的链式存储结构
3.单链表、双链表、循环链表:
结点只有一个指针域的链表,称为单链表 或线性链表
结点有两个指针域的链表,称为双链表
首尾相接的链表称为循环链表
4.头指针、头结点和首元结点:
头指针: 是指向链表中第一个结点的指针
首元结点: 是指向链表中存储第一个数据元素a1的结点
头结点: 是在链表的首元结点之前附设的一个结点
如何表示空表?
无头结点时,头指针为空 时表示空表
有头结点时,当头结点的指针域为空 时表示空表
在链表中设置头结点有什么好处?
1 便于首元结点 的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无需进行特殊处理
2 便于空表和非空表 的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
头结点的数据域 内装的是什么?
头结点的数据域 可以为空,也可存放表长度 等附加信息,但此结点不能计入链表长度值
链表的特点
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费时间不等。这种存取元素的方法被称为顺序存取法
单链表是由表头唯一确定,因此单链表可以用头指针 的名字来命名,若头指针名是L,则把链表称为表L
单链表的存储结构:
1 2 3 4 typedef struct Lnode { ElemType data; struct Lnode *next ; }Lnode,*LinkList;
**定义一个链表L:**LinkList L;
**定义结点指针p:**LNode *p;LinkList p;
例如:存储学生学号、姓名、成绩的单链表结点类型定义如下:
1 2 3 4 5 6 typedef struct student { char num[8 ]; char name[8 ]; int score; struct student *next ; }Lnode,*LinkList;
单链表基本操作的实现
单链表的初始化
即构造一个空表
【算法步骤】
生成新结点作头结点,用头指针L指向头结点
将头结点的指针域置空
【算法描述】
1 2 3 4 typedef struct Lnode { ElemType data; struct Lnode *next ; }LNode,*LinkList;
1 2 3 4 Status InitList L (LinkList &L) { L = (LinkList)malloc (sizeof (LNode)); L->next = NULL ; }
【补充算法1】判断链表是否为空
空表 :链表中无元素,称为空链表(头指针和头结点仍然在)
【算法思路】判断头结点指针域是否为空
1 2 3 4 5 6 int ListEmpty (LinkList L) { if (L->next) return 0 ; else return 1 ; }
【补充算法2】单链表的销毁:链表销毁后不存在
【算法思路】从头指针开始,依次释放所有结点
1 2 3 4 5 6 7 8 9 Status DestoryList L (LinkList &L) { Lnode *p; while (L){ p = L; L = L->next; free (p); } return OK; }
【补充算法3】清空链表
链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)
【算法思路】依次释放所有结点,并将头结点指针域设置为空
1 2 3 4 5 6 7 8 9 10 11 Status ClearList (LinkList &L) { Lnode *p,*q; p = L->next; while (p){ q = p->next; free (p); p = q; } L->next = NULL ; return OK; }
【补充算法4】求单链表的表长
【算法思路】从首元结点开始,依次计数所有结点
1 2 3 4 5 6 7 8 9 10 int ListLength_L (LinkList L) { LinkList p; p = L->next; i = 0 ; while (p){ i++; p = p->next; } return i; }
【算法2.7】取值——取单链表中第i个元素的内容
【算法思路】从链表的头指针出发,顺着链域next逐个结点往下搜索,直到搜索到底i个结点为止。因此,链表不是随机存取结构
1.从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值p=L->next
2.j做计数器,累计当前扫描过得结点数,j初值为1
3.当p指向扫描到的下一结点时,计数器j加1
4.当j==i时,p所指的结点就是要找的第i个结点
1 2 3 4 5 6 7 8 9 Status GetElem_L (LinkList L,int i,ElemType &e) { p = L->next; j =1 ; while (p&&j<i){ p = p->next; ++j; } if (!p||j>i) return ERROR; e = p->data; return OK; }
【算法2.8】按值查找——根据指定数据获取该数据所在的位置(地址)
【算法思路】
1.从第一个结点起,依次和相比较
2.如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址;
3.如果查遍整个链表都没找到其值和e相等的元素,则返回0或“NULL“
1 2 3 4 5 6 7 8 9 Lnode *LocateElem_L (LinkList L,ElemType e) { p = L->next; while (p&&p->data!=e){ p = p->next; } return p; }
【算法2.8变化】按值查找——根据指定数据获取该数据位置序号
1 2 3 4 5 6 7 8 9 10 11 int LocateElem_L (LinkList L,ElemType e) { p = L->next; j = 1 ; while (p&&p->data!=e){ p = p->next; j ++; } if (p) return j; else return 0 ; }
【算法2.9】插入——在第i个结点前插入值为e的新结点
【算法思路】
1. 首 先 找 到 a i − 1 的 存 储 位 置 p 2. 生 成 一 个 数 据 域 为 e 的 新 结 点 s 3. 插 入 新 结 点 : ① 新 结 点 的 指 针 域 指 向 结 点 a i ② 结 点 a i − 1 的 指 针 域 指 向 新 结 点 s − > n e x t = p − > n e x t p − > n e x t = s 1.首先找到a_{i-1}的存储位置p\\
2.生成一个数据域为e的新结点s\\
3.插入新结点:\\①新结点的指针域指向结点a_i\\
②结点a_{i-1}的指针域指向新结点\\
s->next = p->next\\
p->next = s
1 . 首 先 找 到 a i − 1 的 存 储 位 置 p 2 . 生 成 一 个 数 据 域 为 e 的 新 结 点 s 3 . 插 入 新 结 点 : ① 新 结 点 的 指 针 域 指 向 结 点 a i ② 结 点 a i − 1 的 指 针 域 指 向 新 结 点 s − > n e x t = p − > n e x t p − > n e x t = s
1 2 3 4 5 6 7 8 9 10 11 12 13 Status ListInsert (LinkList &L.int i,ElemType e) { p = L;j = 0 ; while (p&&j<i-1 ){ p = p->next; ++j; } if (!p||j>i-1 ) return ERROR; s = (LinkList)malloc (sizeof (LNode)); s->data = e; s->next = p->next; p->next = s; return OK; }
【算法2.10】删除——删除第i个结点
【算法思路】
1. 首 先 找 到 a i − 1 的 存 储 位 置 p , 保 存 要 删 除 的 a i 的 值 2. 令 p − > n e x t 指 向 a i + 1 p − > n e x t = p − > n e x t − > n e x t 3. 释 放 结 点 a i 的 空 间 1.首先找到a_{i-1}的存储位置p,保存要删除的a_i的值\\
2.令p->next指向a_{i+1}\\
p->next = p->next->next\\
3.释放结点a_i的空间
1 . 首 先 找 到 a i − 1 的 存 储 位 置 p , 保 存 要 删 除 的 a i 的 值 2 . 令 p − > n e x t 指 向 a i + 1 p − > n e x t = p − > n e x t − > n e x t 3 . 释 放 结 点 a i 的 空 间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Status ListDelete_L (LinkList &L,int i,ElemType &e) { p = L; j = 0 ; while (p->next&&j<i-1 ){ p = p->next; ++ j; } if (!(p->next)||j>i-1 ) return ERROR; q = q->next; p->next = q->next; e = q->data; free (q); return Ok; }
单链表的查找、插入、删除算法时间效率分析
1.查找:
因线性表只能顺序存取,即查找时要从头指针找起,查找的时间复杂度为O(n)
2.插入和删除:
因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度O(1)
但是,如果要在单链表中进行前插或者删除操作,由于要从头查找前驱结点,所耗时间复杂度为O(n)
【算法2.11】建立单链表:头插法——元素插入在链表头部,也叫前插法
【算法思路】
1.从一个空表开始,重复读入数据
2.生成新结点,将读入数据存放到新结点的数据域中
3.从最后一个结点开始,依次将各结点插入到链表的前端
1 2 3 4 5 6 7 8 9 10 void CreatList_H (LinkList &L,int n) { L = (LNode*)malloc (sizeof (LNode)); L->next = NULL ; for (i = n;i > 0 ;i --){ p = (LNode*)malloc (sizeof (LNode)); scanf (&p->data); p->next = L->next; L->next = p; } }
【算法2.12】建立单链表:尾插法——元素插入在链表尾部,也叫后插法
【算法思路】
1.从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点
2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点
1 2 3 4 5 6 7 8 9 10 11 12 13 void CreatList_R (LinkList &L,int n) { L = (LinkList)malloc (sizeof (LNode)); L->next = NULL ; r = L; for (int i = 0 ; i < n; i ++){ p = (LinkList)malloc (sizeof (LNode)); scanf (&p->data); p->next = NULL ; r->next = p; r = p; } }
循环链表
是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)
优点:从表中任一结点出发均可找到表中其他结点
注意:
由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件 就不再像非循环链表 那样判断p或p->next是否为空 ,而是判断它们是否等于头指针
循环条件:
单链表
1 2 p != NULL p->next != NULL
单循环链表
注意:表的操作常常是在表的首尾位置上进行
头指针表示单循环列表
找 a 1 的 时 间 复 杂 度 : O ( 1 ) 找 a n 的 时 间 复 杂 度 : O ( n ) 找a_1的时间复杂度:O(1)\\
找a_n的时间复杂度:O(n)
找 a 1 的 时 间 复 杂 度 : O ( 1 ) 找 a n 的 时 间 复 杂 度 : O ( n )
不方便
尾指针表示单循环链表
a 1 的 存 储 位 置 是 : R − > n e x t − > n e x t a n 的 存 储 位 置 是 : R 时 间 复 杂 度 都 是 O ( 1 ) a_1的存储位置是: R->next->next\\
a_n的存储位置是: R\\
时间复杂度都是O(1)
a 1 的 存 储 位 置 是 : R − > n e x t − > n e x t a n 的 存 储 位 置 是 : R 时 间 复 杂 度 都 是 O ( 1 )
带尾指针的循环链表的合并(将Tb合并在Ta之后)
分析有哪些操作?
p存表头结点
Tb表头连接到Ta表尾
释放Tb表头结点
修改指针
1 2 3 4 p = Ta->next Ta->next = Tb->next->next free (Tb->next)Tb->next = p
1 2 3 4 5 6 7 8 LinkList Connect (LinkList Ta,LinkList Tb) { p = Ta->next; Ta->next = Tb->next->next; free (Tb->next); Tb->next = p; return Tb; }
双向链表
为什么要讨论双向链表:
单链表:
即:查找某结点的后继结点的执行时间为O(1)
即:查找某结点的前驱结点的执行时间为O(n)
在单链表的每个结点里再增加一个指向其直接前驱的指针域prior 。这样链表中就形成了有两个方向不同的链,故称为双向链表
双向链表的结构可以定义如下:
1 2 3 4 typedef struct DuLNode { Elemtype data; struct *prior ,*next ; }DuLNode,*DuLinkList;
双向循环链表
和单链的循环表类似,双向链表也可以有循环链表
让头结点的前驱指针指向链表的最后一个结点
让最后一个结点的后继指针指向头结点
双向链表的对称性 (设指针p指向某一个结点):
p->prior->next = p = p->next->prior
在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时在,则需同时修改两个方向上的指针,两者操作的时间复杂度均为O(n)
【算法2.13】双向链表的插入
s->prior = p->prior
p->prior->next = s
s->next = p
p->prior = s
1 2 3 4 5 6 7 8 9 10 11 void ListInsert_DuL (DuLinkList &L,int i,ElemType e) { if (!(p=GetElemP_DuL(L,i))) return ERRPR; s = (DuLinkList)malloc (sizeof (DuLNode)); s->data = e; s->prior = p->prior; p->prior->next = s; s->next = p; p->prior = s; return OK; }
【算法2.14】双向链表的删除
p->prior->next = p->next
p->next->prior = p->prior
1 2 3 4 5 6 7 8 9 void ListDelete_DuL (DuLink &L,int i,ElemType e) { if (!(p = GetElemP_DuL(L,i))) return ERROR; e = p->data; p->prior->next = p->next; p->next->prior = p->prior; free (p); return OK; }
单链表、循环链表、和双向链表时间效率比较
查找表头结点(首元结点)
查找表尾结点
查找结点*p的前驱结点
带头结点的单链表L
L->next 时间复杂度O(1)
从L->next 依次向后遍历时间复杂度O(n)
通过p->next无法找到其前驱
带头结点仅设头指针 的循环 单链表
L->next 时间复杂度O(1)
从L->next 依次向后遍历时间复杂度O(n)
通过p->next可以找到前驱 时间复杂度O(n)
带头结点仅设尾指针R 的循环 单链表
R->next 时间复杂度O(1)
R 时间复杂度O(1)
通过p->next可以找到前驱 时间复杂度O(n)
带头结点的双向循环 链表L
L->next 时间复杂度O(1)
L-prior 时间复杂度O(1)
p->prior 时间复杂度O(1)
顺序表和链表的比较
链式存储结构优点:
结点空间可以动态申请和释放
数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
链式存储结构缺点:
存储密度 小没每个结点的指针域需要额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大
链式存储结构是非随机存取 结构。对任一节点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度
存储密度
是指结点数本身所占的存储量和整个结点结构所占存储量之比,即
存储密度 = 结点数据本身占用的空间/结点占用的空间总量
线性表的应用
线性表的合并:
问题描述:
假设利用两个线性表La和Lb分别表示两个集合A和B,现要求一个新的集合A=A∪B
La = (7,5,3,11)Lb = (2,6,3)=》 La = (7,5,3,11,2,6)
【算法思路】
依次取出Lb中的每个元素,执行以下操作:
1.在La中查找该元素
2.如果找不到,则将其插入La的最后
1 2 3 4 5 6 7 8 void union (List &La,List &Lb) { La_len = ListLength(La); Lb_len = ListLength(Lb); for ( i = 1 ; i < Lb_len; i ++ ){ GetElem(Lb,i,e); if (!LocateElem(La,e)) ListInsert(&La,++La_len,e); } }
有序表的合并:
问题描述:
已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素扔按值非递减有序排列
La = (1,7,8) Lb = (2,4,6,8,10,11) => Lc = (1,2,4,6,7,8,8,10,11)
【算法思路】
1.创建一个空表
2.依次从La中或Lb中"摘取"元素值娇小的结点插入到Lc表的最后,直至其中一个变空表为止
3.继续将La或Lb其中一个表的剩余结点插入在Lc表的最后
顺序表实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void MergeList_Sq (SqList LA,SqList LB,SqList &LC) { pa = LA.elem; pb = LB.elem; LC.length = LA.length + LB.length; LC.elem = (int *)malloc (sizeof (LC.length)); pc = LC.elem; pa_last = LA.elem + LA.length - 1 ; pb_last = LB.elem + LB.length - 1 ; while (pa<=pa_last&&pb<=pb_last){ if (*pa<=*pb) *pc ++ = *pa++; else *pc ++ = *pb ++; } while (pa<=pa_last) *pc++ = *pa++; while (pb<=pb_last) *pc++ = *pb++; }
链表实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void MergeList_L (LinkList &La,LinkList &Lb,LinkList &Lc) { pa = La->next; pb = Lb->next; pc = Lc = La; while (pa&&pb){ if (pa->data<=pb->data){ pc->next = pa; pc = pa; pa = pa->next; }else { pc->next = pb; pc = pb; pb = pb->next; } } pc->next = pa?pa:pb; free (Lb); }
案例分析与实现
【案例2.1】一元多项式的运算:实现两个多项式加、减、乘运算
P n ( x ) = p 0 + p 1 x + p 2 x 2 + . . . + p n x n 线 性 表 P = ( p 0 , p 1 , p 2 , . . . , p n ) 每 一 项 的 指 数 i 隐 含 在 其 系 数 p i 的 序 号 中 P_n(x) = p_0+p_1x+p_2x^2+...+p_nx^n\\
线性表P = (p_0,p_1,p_2,...,p_n)\\
每一项的指数i隐含在其系数p_i的序号中
P n ( x ) = p 0 + p 1 x + p 2 x 2 + . . . + p n x n 线 性 表 P = ( p 0 , p 1 , p 2 , . . . , p n ) 每 一 项 的 指 数 i 隐 含 在 其 系 数 p i 的 序 号 中
例如:
P ( x ) = 10 + 5 x − 4 x 2 + 3 x 3 + 2 x 4 P(x) = 10 + 5x - 4x^2 + 3x^3 + 2x^4
P ( x ) = 1 0 + 5 x − 4 x 2 + 3 x 3 + 2 x 4
指数(下标i)
0
1
2
3
4
系数p[i]
10
5
-4
3
2
例如 实现两个多项式相加运算
P a ( x ) = 10 + 5 x − 4 x 2 + 3 x 3 + 2 x 4 P b ( x ) = − 3 + 8 x + 4 x 2 − 5 x 4 + 7 x 5 − 2 x 6 Pa(x) = 10 + 5x - 4x^2 + 3x^3 + 2x^4\\
Pb(x) = -3 + 8x + 4x^2 - 5x^4 + 7x^5 - 2x^6
P a ( x ) = 1 0 + 5 x − 4 x 2 + 3 x 3 + 2 x 4 P b ( x ) = − 3 + 8 x + 4 x 2 − 5 x 4 + 7 x 5 − 2 x 6
系数Pa[i] 10 5 -4 3 2
系数Pb[i] -3 8 4 0 -5 7 -2
系数Pc[i] 7 13 0 3 -3 7 -2
【案例2.2】系数多项式的运算
多项式非零项的数组表示
( a ) A ( x ) = 7 + 3 x + 9 x 8 + 5 x 17 (a) A(x) = 7 + 3x + 9x^8 + 5x^{17}
( a ) A ( x ) = 7 + 3 x + 9 x 8 + 5 x 1 7
下标i
0
1
2
3
系数a[i]
7
3
9
5
指数
0
1
8
17
( b ) B ( x ) = 8 x + 22 x 7 − 9 x 8 (b) B(x) = 8x + 22x^7 - 9x^8
( b ) B ( x ) = 8 x + 2 2 x 7 − 9 x 8
下标i
0
1
2
系数b[i]
8
22
-9
指数
1
7
8
P n ( x ) = p 1 x e 1 + p 2 x e 2 + . . . + p m x e m P_n(x) = p_1x^{e_1} + p_2x^{e_2} + ... + p_mx^{e_m}
P n ( x ) = p 1 x e 1 + p 2 x e 2 + . . . + p m x e m
线 性 表 P = ( ( p 1 , e 1 ) , ( p 2 , e 2 ) , . . . , ( p m , e m ) ) 线性表 P = ((p_1,e_1),(p_2,e_2),...,(p_m,e_m))
线 性 表 P = ( ( p 1 , e 1 ) , ( p 2 , e 2 ) , . . . , ( p m , e m ) )
线性表A = ((7,0),(3,1),(9,8),(5,17))
线性表B = ((8,1),(22,7),(-9,8))
链式存储结构结点定义
1 2 3 4 5 typedef struct PNode { float coef; int expn; struct PNode *next ; }PNode,*Polynomial;
【算法步骤】多项式创建
①0创建一个只有头结点的空链表
②根据多项式的项的个数n,循环n次执行以下操作:
生成一个新结点*s
输入多项式当前项的系数和指数赋给新结点*s的数据域
设置一前驱指针pre,用于指向待找到的第一大于输入项指数的结点的前驱,pre初始值指向头结点
指针q初始化,指向首元结点
循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输入项指数的结点*q
将输入项结点 *s 插入到结点 *q之前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void CreatPolyn (Polynomial &P,int n) { P = new PNode; P->next = NULL ; for (i = 1 ;i <=n ;i++){ s = new PNode; cin >>s->coef>>s->expn; pre = P; q = P->next; while ( q && q->expn < s->expn ){ pre = q; q = q->next; } s->next = q; pre->next = s; } }
【算法思路】多项式相加
①指针p1和p2初始化,分别指向Pa和Pb的首元结点
②p3指向和多项式的当前结点,初值为Pa的头结点
③当指针p1和p2均未到达表尾时,则循环比较p1的p2所指结点对应的指数值
(p1->expn与p2->expn),有下列3种情况
当p1->expn==p2->expn时,则将两个结点中的系数相加
若和不为零 ,则修改p1所指结点的系数值,同时删除p2所指结点
若和为零 ,则删除p1和p2所指结点
当 p1->expn < p2->expn时,则应摘取p1所指结点插入到"和多项式"链表中去
当p1->expn > p2->expn时,则应摘取p2所指结点插入到"和多项式"链表中去
④将非空多项式的剩余段插入到p3所指结点之后
⑤释放Pb的头结点
【案例2.3】图书信息管理系统
1 2 3 4 5 struct Book { char id[20 ]; char name[50 ]; int price; };
1 2 3 4 typedef struct { Book *elem; int length; }SqList;
1 2 3 4 typedef struct LNode { Book data; struct LNode *next ; }LNode,*LinkList;
栈和队列
栈和队列的定义和特点
栈和队列是两种常用的、重要的数据结构
栈和队列是限定插入和删除只能在表的"端点"进行的线性表
线性表
Insert(L,i,x) 1<=i<=n+1
Delete(L,i) 1<=i<=n
栈 先进后出
Insert(S,n+1,x)
Delete(S,n)
队列 先进先出
Insert(Q,n+1,x)
Delete(Q,1)
栈的应用
队列的常见应用
栈的定义和特点
栈(Stack)是一个特殊的线性表,是限定仅在一端(通常是表尾)进行插入和删除操作的线性表
又称为后进先出 (Last In First Out)的线性表,简称LIFO 结构
栈的相关概念
栈 是仅在表尾进行插入、删除操作的线性表
表尾(即an端)称为栈顶 Top;表头(即a1端)称为栈底 Base
例 如 : 栈 S = ( a 1 , a 2 , a 3 , . . . , a n ) a 1 称 为 栈 底 元 素 a n 称 为 栈 顶 元 素 例如:栈 S = (a_1,a_2,a_3,...,a_n)\\
a_1称为栈底元素 \\ a_n称为栈顶元素
例 如 : 栈 S = ( a 1 , a 2 , a 3 , . . . , a n ) a 1 称 为 栈 底 元 素 a n 称 为 栈 顶 元 素
插入元素到栈顶 (即表尾)的操作,称为入栈
从栈顶 (即表尾)删除最后一个元素的操作,称为入栈
“入” = 压入 = PUSH(x)
“出” = 弹出 = POP(x)
【思考】假设有3个元素a,b,c,入栈顺序是a,b,c,则它们的出栈顺序有几种可能?
c b a , a b c, a c b, b a c, b c a
插入的时机不一样 出栈顺序不一样
队列的定义和特点
队列(queue)是一种先进先出 (First In First Out ----FIFO)的线性表。在表的一端插入(表尾),在另一端(表头)删除
案例引入
【案例3.1】进制转换
十进制整数N向其他进制数d(二、八、十六)的转换是计算机实现计算的基本问题
转换法则:除以d倒取余
该转换法则对应于一个简单算法原理
n = (n div d)*d + n mod d
其中:div为整除运算,mod为求余运算
例 把十进制数159转换成八进制数
159/8 = 19 余7
19/8 = 2 余3
2/8 = 0 余2
所以是237 后进先出 用栈
【案例3.2】括号匹配的检验
假设表达式中允许包含两种括号:圆括号和方括号
其嵌套的顺序随意,即:
( [ ] ( ) ) 或 [ ( [ ] [ ])]为正确格式
[ ( ] ) 为错误格式
( [ ( ) 或 ( ( ) ] )为错误格式
【算法思路】
可以利用一个栈结构保存每个出现的左括号,当遇到右括号时,从栈中弹出左括号,检验匹配情况
在检验过程中,若遇到以下几种情况之一,就可以得出括号不匹配的结论
当遇到某一个右括号时,栈已空,说明到目前为止,右括号多余左括号
从栈中弹出的左括号与当前检验的右括号类型不同,说明出现了括号交叉 情况
算术表达式输入完毕,但栈中还有没有匹配的左括号,说明左括号多于右括号
【案例3.3】表达式求值
表达式求值是程序设计语言编译中一个最基本的问题,它的实现也需要运用栈
这里介绍算法是由运算符优先级确定运算顺序的对表达式求值算法
算符优先算法
为了实现表达式求值。需要设置两个栈:
一个是算符栈OPTR,用于寄存运算符
另一个是称为操作数符OPND,用于寄存运算数和运算结果
求值的处理过程是自左至右扫描表达式的每一个字符
当扫描到的是运算符,则将其压入栈OPND
当扫描到的是运算符时
若这个运算符比OPTR栈顶运算符的优先级高,则入栈OPTR,继续向后处理
若这个运算符比OPTR栈顶运算符优先级低,则从OPND栈中弹出两个运算符,从栈OPTR中弹出栈顶运算符进行运算,并将运算结果入栈OPND
继续处理当前字符,直到遇见结束符为止
【案例3.4】舞伴问题
栈的表示和操作的实现
顺序栈的表示和实现
存储方式:同一般线性表的顺序存储结构完全相同,利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。栈底一般在低地址端
附设top指针,指示栈顶元素在顺序栈中的位置
另设base指针,指示栈底元素在顺序栈中的位置
但是,为了方便操作,通常top指示真正的栈顶元素之上 的下标地址
空栈:base==top是栈空标志
栈满:top-base==stacksize
栈满时的处理方法:
1.报错,返回操作系统
2.分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈
使用数组作为顺序栈存储方式的特点:
简单方便 但易溢出 (数组大小固定)
上溢(overflow):栈已经满,又要压入元素
下溢(underflow):栈已经空,还要弹出元素
**注:**上溢是一种错误,使问题的处理无法进行,而下溢一般认为是一种结束条件,即问题处理结束
顺序栈的表示
1 2 3 4 5 6 #define MAXSIZE 100 typedef struct { SElemType *base; SElemType *top; int stacksize; }SqStack;
顺序栈相关算法
【算法3.1】顺序栈的初始化
1 2 3 4 5 6 7 Status InitStack (SqStack &S) { S.base = (SElemType)malloc (MAXSIZE*sizeof (SElemType)); if (!S.base) exit (OVERFLOW); S.top = S.base; S.tacksize = MAXSIZE; return OK; }
【算法补充】顺序栈判断栈是否为空
1 2 3 4 5 6 7 8 Status StackEmpty (SqStack S) { if (S.top == S.base){ return TRUE; }else { return FALSE; } }
【算法补充】求顺序栈的长度
1 2 3 int StackLength (SqStack S) { return S.top-S.base; }
【算法补充】清空顺序栈
1 2 3 4 Status ClearStack (SqStack &S) { if (S.base) S.top == S.base; return OK; }
【算法补充】销毁顺序栈
1 2 3 4 5 6 7 8 Status DestoryStack (SqStack &S) { if (S.base){ free (S.base); S.stacksize = 0 ; S.base = S.top = NULL ; } return OK; }
【算法3.2】顺序栈的入栈
1.判断是否栈满,若满则出错(上溢)
2.元素e压入栈顶
3.栈顶指针加1
1 2 3 4 5 6 Status Push (SqStack &S,SElemType e) { if (S.top-S.base == S.stacksize) return ERRPR; *S.top = e; S.top++; }
【算法3.3】顺序栈的出栈
判断是否栈空,若空则出错(下溢)
栈顶指针减1
获取栈顶元素e
1 2 3 4 5 6 7 Status Pop (SqStack &S,SElemType &e) { if (S.top == S.base) return ERROR; e = *--S.top; return OK; }
链栈的表示和实现
链栈的表示
链栈是运算受限 的单链表,只能在链表头部 进行操作
1 2 3 4 typedef struct StackNode { SElemType data; struct StackNode *next ; }StackNode,*Linkstack;
注意:链表中指针的方向是反过来的
链表的头指针就是栈顶
不需要头结点
基本不存在栈满的情况
空栈相当于头指针指向空
插入和删除仅在栈顶处执行
【算法3.5】链栈的初始化
1 2 3 4 5 void InitStack (LinkStack &S) { S = NULL ; return OK; }
【补充算法】判断链栈是否为空
1 2 3 4 Status StackEmpty (LinkStack S) { if (S == NULL ) return TRUE; else return FALSE; }
【算法3.6】链栈的入栈
1 2 3 4 5 6 7 Status Push (LinkStack &S,SElemType e) { p = (LinkStack)malloc (sizeof (StackNode)); p->data = e; p->next = S; S = p; return OK; }
【算法3.7】链栈的出栈
1 2 3 4 5 6 7 8 Status Pop (LinkStack &S,SElemType &e) { if (S==NULL ) return ERROR; e = S->data; p = S; S = S->next; free (p); return OK; }
【算法3.8】取栈顶元素
1 2 3 4 SElemType GetTop (LinkStack S) { if (S!==NULL ) return S->data; }
栈与递归
递归的定义
若一个对象部分地包含它自己 ,或用它自己给自己定义 ,则称这个对象是递归的
若一个过程直接地或间接地调用自己 ,则称这个过程是递归的过程
分治法求解递归算法的一般形式:
1 2 3 4 void p (参数表) { if (递归结束条件) 可直接求解步骤; else p(较小的参数); }
函数调用过程
调用前,系统完成
将实参,返回地址 等传递给被调函数
为被调函数的局部变量 分配存储区
将控制转移到被调函数的入口
调用后,系统完成
保存被调函数的计算结果
释放被调函数的数据区
依照被调用函数保存的返回地址 将控制转移到调用函数
队列的表示和操作的实现
队列的顺序表示——用一位维数组base[MAXSIZE]
1 2 3 4 5 6 #define MAXSIZE 100 typedef struct { QElemType *base; int front; int rear; }SqQueue;
入队存在的问题
设数组大小为MAXSIZE
rear =MAISIZE时,发生溢出
若 front = 0 rear = MAXSIZE时,再入队,真溢出
若 front ≠ 0 rear = MAXSIZE时,再入队,假溢出
解决假上溢的办法
将队中元素依次向队头方向移动
缺点:浪费时间。每移动一次,队中元素都要移动
将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为maxsize时,若向量的开始端空着,又可以从头使用空着的空间。当front为maxsize时,也是一样
引入循环队列
base[0]
接在base[MAXSIZE-1]之后,若rear+1==M,则令rear=0
实现方法:利用模(mod,C语言中:%)运算
插入元素:Q.base[Q.rear] = x
Q.rear = (Q.rear+1)%MAXSIZE
删除元素:x = Q.base[s.front]
Q.front = (Q.front+1)%MAXSIZE
队空:front == rear
队满:front == rear
解决方案:
另外设一个标志以区别队空、队满
另设一个变量,记录元素个数
少用一个元素空间
【算法3.11】队列的初始化
1 2 3 4 5 6 Status InitQueue (SqQueue &Q) { Q.base = (QElemType*)malloc (MAXSIZE*sizeof (QElemType)); if (!Q.base) exit (OVERFLOW); Q.front = Q.base = 0 ; return OK; }
【算法3.12】求队列的长度
1 2 3 int QueueLength (SqQueue Q) { return (Q.rear-Q.front+MAXSIZE)%MAXSIZE; }
【算法3.13】循环队列入队
1 2 3 4 5 Status ENQueue (SqQueue &) { if ((Q.rear+1 )%MAXSIZE==Q.front) return ERROR; Q.base[Q.rear] = e; Q.rear = (Q.rear+1 )%MAXSIZE; }
【算法3.14】循环队列出队
1 2 3 4 5 Status DeQueue (SqQueue &Q,QElemType &e) { if (Q.front == Q.rear) return ERROR; e = Q.base[Q.base]; Q.front = (Q.front+1 )%MAXSIZE; }
【算法3.15】取队头元素
1 2 3 4 SElemType GetHead (SqQueue Q) { if (Q.front!=Q.rear) return Q.base[Q.base]; }
链队
若用户无法估计所用队列的长度,则宜采用链队列
链队列的类型定义
1 2 3 4 5 #define MAXSIZE 100 typedef struct Qnode { QElemType data; struct Qnode *next ; }QNode,*QuenePtr;
1 2 3 4 typedef struct { QuenePtr front; QuenePtr rear; }LinkQueue;
链队列运算指针变化状况
【算法3.16】链队列的初始化
1 2 3 4 5 6 Status InitQueue (LinkQueue &Q) { Q.front = Q.rear = (QueuePtr)malloc (sizeof (QNode)); if (!Q.front) exit (OVERFLOW); Q.front->NULL ; return OK; }
【补充算法】销毁链队列
1 2 3 4 5 6 7 8 Status DestroyQueue (LinkQueue &Q) { while (Q.front){ p = Q.front->next; free (Q.next); Q.front = p; } return OK; }
【算法3.17】将元素e入队
1 2 3 4 5 6 7 Status EnQueue (LinkQueue &Q,QElemType e) { p = (QueuePtr)malloc (sizeof (QNode)); p->data = e; p->next = NULL ; Q.rear = p; return OK; }
【算法3.18】链队列出队
1 2 3 4 5 6 7 8 9 10 11 Status DeQueue (LinkQueue &Q,QElemType &e) { if (Q.front == Q.rear) return ERROR; p = Q.front->next; e = p->data; Q.front->next = p->next; if (Q.rear == p){ Q.rear = Q.front; } free (p); return OK; }
【算法3.19】求链队列的队头元素
1 2 3 4 5 Status GetHead (LinkQueue Q,QElemType &e) { if (Q.front == Q.rear) return ERROR; e = Q.front->next->data; return OK; }
串、数组和广义表
## 串
串的定义
零个或多个任意字符组成的有限序列
子串:串中任意个连续字符组成的 子序列 (含空串)称为该串的子串
主串:包含子串的串相应的称为 主串
字符位置:字符 在序列中的序号 为该字符在串中的位置
子串位置:****子串中第一个字符 在主串中的位置
**空格串:**由一个或多个空格组成的串 与空串不同
串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相等时,这两个串才是 相等 的
案例引入
【案例4.1】病毒感染监测
研究者将人的DNA和病毒DNA均表示成由一些字母组成的字符串序列,然后检测某种病毒DNA序列是否在患者的DNA序列中出现过,如果出现过,则此人感染了该病毒,否则没有感染
例如:假设被病毒的DN序列为baa,患者1的DNA序列为aaabbba,则感染,患者2的DNA序列为babbba,则未感染
注意,人的DNA序列是线性的,而病毒的DNA序列是环状的
串的类型定义、存储结构及运算
1 2 3 4 5 #define MAXLEN 255 typedef struct { char ch[MAXLEN+1 ]; int length; }SString;
1 2 3 4 5 6 7 8 9 #define CHUNKSIZE 80 typedef struct Chunk { char ch[CHUNKSIZE]; struct Chunk *next ; }Chunk; typedef struct { Chunk *head,*tail; int curlen; }LString;
串的模式匹配算法
算法目的:
确定主串 中所含子串 (模式串)第一次出现的位置(定位)
算法应用:
搜索引擎 拼写检查 语言翻译 数据压缩
算法种类:
BF算法(Brute-Force,又称古典的、经典的、朴素的、穷举的)
KMP算法(特点:速度快)
Brute-Force简称为BF算法 ,亦称简单匹配算法。采用穷举法的思想
S: a a a a b c d 主串:正文串
T: a b c 子串:模式
算法思路是从S的每一个字符开始依次与T的字符进行匹配
BF算法设计思想:
将主串的第pos个字符和模式串的第一个字符比较
若相等,继续逐个比较后续字符
若不等,从主串的下一字符起,重新与模式串的第一个字符比较
直到主串的一个连续子串字符序列与模式串相等。返回值为S中与T匹配的子序列第一个字符的的序号,即匹配成功。否则,匹配失败,返回0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int Index_BF (SString S,SString T) { int i = 1 , j = 1 ; while (i<=S.length&&j<=T.length){ if (s.ch[i] == t.ch[i]){ ++i; ++j; }else { i = i-j+2 ; j = 1 ; } } if (j>T.length) return i-T.length; else return 0 ; }
KMP算法
KMP算法设计思想
利用已经部分匹配 的结果而加快模式串的滑动速度且主串S的指针i不必回溯
可提速到O(n+m)
j
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
模式串
a
b
c
a
a
b
b
c
a
b
c
a
a
b
d
a
b
next[j]
0
1
1
1
2
2
3
1
1
2
3
4
5
6
7
1
2
1 2 3 4 5 6 7 8 9 10 11 12 13 int Index_KMP (SString S,SString T,int pos) { i = pos,j = 1 ; while (i<S.length&&j<T.length){ if (j == 0 ||S.ch[i] = S.ch[j]){ i++; j++; }else { j = next[j]; } } if (j>T.length) return i-T.length; else return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void get_next (SString T,int &next[]) { i = 1 ; next[1 ] = 0 ; j = 0 ; while (i<T.length){ if (j == 0 ||T.ch[i] == T.ch[j]){ i++; j++; next[i] = j; }else { j = next[j]; } } }
数组
声明格式: 数据类型 变量名称 [行数] [列数]
n维数组:若n-1维数组中的元素又是一个一维数组结构,则称作n维数组
一维数组:
例如,有数组定义:int a[5]
每个字节占用4字节,假设a[0]存储在2000单元,a[3]地址是多少
数组的顺序存储
存储单元是一维结构,而数组是个多维结构,则用一维连续存储单元存放数组的数据元素就有个次序约定问题
C JAVA PASCAL BASIC
特殊矩阵的压缩存储
什么是压缩存储?
若多个数据元素的值都相同 ,则只分配一个元素值的存储空间,且零元素不占存储空间
什么样的矩阵能够压缩?
一些特殊矩阵,如:对称矩阵,三角矩阵,对角矩阵,稀疏矩阵等
什么叫稀疏矩阵?
矩阵中非零元素的个数很少(一般小于5%)
1.对称矩阵
【特点】在n×n的矩阵a中,满足如下性质:
a i j = a j i ( 1 < = i , j < = n ) a_{ij} = a_{ji}(1<=i,j<=n)
a i j = a j i ( 1 < = i , j < = n )
【存储方式】只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间
下三角:k = i(i-1)/2 + j
2.三角矩阵
【特点】对角线以下(或者以上)的数据元素(不包括对角线)全部为常数c
【存储方法】重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素空间
下三角矩阵
k = i×(i-1)/2 + j i>= j
n(n+1)/2 + 1 i<j
上三角矩阵
k = i(2n-i+1)/2+j-1 i<=j
n(n+1)/2 + 1 i>j
3.对角矩阵
【特点】在n×n的方阵中,所有非零元素都集中在以主对角线为中心的带状区域中,区域外的值全为0,则成为对角矩阵 。常见的有三对角矩阵、五对角矩阵、七对角矩阵等
稀疏矩阵存储
设 在 m × n 的 矩 阵 中 有 t 个 非 零 元 素 令 δ = t / ( m × n ) 当 δ ≤ 0.05 时 称 为 稀 疏 矩 阵 设在m×n的矩阵中有t个非零元素\\
令 {\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,} \delta = t/(m×n)\\
{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}{\,}当{\,}{\,}{\,}{\,}{\,}{\,}\delta \leq0.05时称为稀疏矩阵
设 在 m × n 的 矩 阵 中 有 t 个 非 零 元 素 令 δ = t / ( m × n ) 当 δ ≤ 0 . 0 5 时 称 为 稀 疏 矩 阵
三元组顺序表:
用行数、列数表示非零元素所在位置
十字链表:
在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了(row,col,value)以外,还要有两个域:
right:用于链接同一行中的下一个非零元素
down:用于链接同一列中的下一个非零元素