2.1 线性表的定义和特点
线性表(Linear List)是具有相同特性的数据元素的一个有限序列
(
a
1
,
a
2
,
⋯
,
a
i
−
1
,
a
i
,
a
i
+
1
,
⋯
,
⋯
,
a
n
)
(a_1,a_2,\cdots,a_{i-1},a_i,a_{i+1},\cdots,\cdots,a_n)
(a1,a2,⋯,ai−1,ai,ai+1,⋯,⋯,an)
例1 分析26个英文字母组成的英文表
(
A
,
B
,
C
,
D
,
⋯
,
Z
)
(A,B,C,D,\cdots,Z)
(A,B,C,D,⋯,Z)
例2 分析学生情况登记表
例3 十二星座(白羊座、金牛座、双子座、巨蟹座、狮子座、处女座、天秤座、天蝎座、射手座、摩羯座、水瓶座、双鱼座)
同一线性表中的元素必定具有相同特性,数据元素间的关系是线性关系
从以上例子可看出线性表的逻辑特征是:
- 非空线性表有且仅有一个开始结点 a 1 a_1 a1,它没有直接前趋,而仅有一个直接后继 a 2 a_2 a2
- 有且仅有一个终端结点 a n a_n an,它没有直接后继,而仅有一个直接前趋 a n − 1 a_{n-1} an−1
- 其余的内部结点 a i ( 2 ≤ i ≤ n − 1 ) a_i(2\le i\le n-1) ai(2≤i≤n−1)都有且仅有一个直接前趋 a i − 1 a_{i-1} ai−1和一个直接后继 a i + 1 a_{i+1} ai+1
线性表是一种典型的线性结构
2.2 案例引入
【案例2.1】一元多项式的运算:实现两个多项式加、减、乘运算
P
n
(
x
)
=
p
0
+
p
1
x
+
p
2
x
2
+
⋯
+
p
n
x
n
P_n(x)=p_0+p_1x+p_2x^2+\cdots+p_nx^n
Pn(x)=p0+p1x+p2x2+⋯+pnxn
线性表
P
=
(
p
0
,
p
1
,
⋯
+
p
n
)
P=(p_0,p_1,\cdots+p_n)
P=(p0,p1,⋯+pn)(每一项的指数i隐含在其系数
p
i
p_i
pi的序号中)
例如:
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)=10+5x−4x2+3x3+2x4
两个多项式相加
【案例2.2】稀疏多项式的运算
稀疏多项式
S
(
x
)
=
1
+
3
x
10000
+
2
x
20000
S(x)=1+3x^{10000}+2x^{20000}
S(x)=1+3x10000+2x20000按如上方法将会造成存储空间的很大浪费
- 创建一个新数组C(数组C多大合适?)
- 分别从头遍历比较A和B的每一项
- 指数相同,对应系数相加,若其和不为零,则在C中增加一个新项
- 指数不相同,则将指数较小的项复制到C中
- 一个多项式已遍历完毕时,将另一个剩余项依次复制到C中
顺序存储结构存在问题
- 存储空间分配不灵活
- 运算的空间复杂度高
采用链式存储结构:
【案例2.3】图书信息管理系统
需要的功能:
(1)查找(2)插入
(3)删除(4)修改
(5)排序(6)计数
图书表抽象为线性表,表中每本图书抽象为线性表中数据元素
总结:
- 线性表中数据元素的类型可以为简单类型,也可以为复杂类型
- 许多实际应用问题所涉的基本操作有很大相似性,不应为每个具体应用单独编写一个程序
- 抽象出共性的逻辑结构和基本操作(ADT),然后实现其存储结构和基本操作
2.3 线性表的类型定义
(本节的类型指抽象数据类型)
抽象数据类型线性表的定义如下:
基本操作:
-
InitList(&L)
(Initialization List)
操作结果:构造一个空的线性表L -
DestroyList(&L)
初始条件:线性表L已经存在
操作结果:销毁线性表L -
ClearList(&L)
初始条件:线性表L已经存在
操作结果:将线性表L重置为空表 -
ListEmpty(L)
:判断是否为空表
初始条件:线性表L已经存在
操作结果:若线性表L为空表,返回TRUE;否则返回FALSE -
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已经存在,i<=i<=ListLength(L)+1
(可以插在线性表最后)
操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加1
-
ListDelete(&L,i,&e)
:删除线性表中元素
初始条件:线性表L已经存在, i ≤ i ≤ L i s t L e n g t h ( L ) i\le i\le ListLength(L) i≤i≤ListLength(L)
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
-
ListTraverse(&L,visited())
:遍历线性表
初始条件:线性表L已经存在
操作结果:依次对线性表中每个元素调用visited()
,一旦visited()
失败,则操作失败
以上所提及的运算是逻辑结构上定义的运算,至于实现细节只有待确定了存储结构之后才考虑
接下来两节学习线性表的存储及在存储结构上各操作的实现
2.4 线性表的顺序表示和实现
线性表有两种基本的存储结构:顺序存储结构和链式存储结构
2.4.1 线性表的顺序存储表示
线性表的顺序表示又称为顺序存储结构或顺序映像
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
线性表的第1个数据元素 a 1 a_1 a1的存储位置,称作线性表的起始位置或基地址
例如:线性表(1,2,3,4,5,6)的存储结构
线性表顺序存储结构占用一片连续的存储空间,知道某个元素的存储位置就可以计算其他元素的存储位置。
假设线性表的每个元素需占用
l
l
l个存储单元,则第i+1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系:
L
O
C
(
a
i
+
1
)
=
L
O
C
(
a
i
)
+
l
LOC(a_{i+1})=LOC(a_i)+l
LOC(ai+1)=LOC(ai)+l由此,所有数据元素的存储位置均可由第一个数据元素的存储位置得到:
L
O
C
(
a
i
)
=
L
O
C
(
a
1
)
+
(
i
−
1
)
×
l
LOC(a_i)=LOC(a_1)+(i-1)\times l
LOC(ai)=LOC(a1)+(i−1)×l
称
L
O
C
(
a
1
)
LOC(a_1)
LOC(a1)为基地址
每个元素的位置可以直接由计算得到,运算数量级是 O ( 1 ) O(1) O(1),跟规模 n n n没有关系。
顺序表的特点:
以物理位置相邻表示逻辑关系,任意元素均可随机存取(优点)
#define LIST_INIT_SIZE 100 // 线性表存储空间的初始分配量
typedef struct{
ElemType elem[LIST_INIT_SIZE];
int length; //当前长度
}SqList;
多项式的顺序存储结构类型定义:
图书表的顺序存储结构类型定义:
2.4.2 顺序表基本操作的实现
顺序表示意图
#define MAXSIZE 100
typedef struct{
ElemType *elem;
int length;
}SqList;//定义顺序表类型
SqList L; // 定义变量L,L是SqList类型的,L是顺序表
补充:操作算法中用到的预定义常量和类型
// 函数结果状态代码
#define TRUE 1
#define FLASE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
// Status是函数的类型,其值是函数结果状态代码
typedef int Status;
typedef char ElemType;
【算法2.1】线性表L的初始化(参数用引用)
#define MAXSIZE 100
typedef struct {
ElemType* elem;// 指示动态分配数组的指针
int length;// 顺序表的当前长度
}SqList;
Status InitList_Sq(SqList& L) {//构造一个空的顺序表L
L.elem = new ElemType[MAXSIZE];//为顺序表分配空间
if (!L.elem) exit(OVERFLOW);//存储分配失败
L.length = 0;//空表长度为0
return OK;
}
销毁线性表L
void DestroyList(SqList& L) {
if (L.elem) delete L.elem;//释放存储空间
}
清空线性表L
本身这片空间还在,但是没有元素,所以长度为0
void ClearList(SqList& L) {
L.length = 0; // 将线性表的长度置为0
}
求线性表的长度
int GetLength(SqList L) {
return(L.length);
}
判断线性表L是否为空
int IsEmpty(SqList L) {
if (L.length == 0)
return 1;
else
return 0;
}
【算法2.2】顺序表的取值(根据位置i获取相应位置数据元素的内容)
int GetElem(SqList L, int i, ElemType& e) {
//判断i值是否合理,若不合理返回ERROR
if (i<1 || i>L.length) return ERROR;
e = L.elem[i - 1];//第i-1的单元存储着第i个数据
return OK;
}
该算法复杂度为 O ( 1 ) O(1) O(1),体现了顺序表随机存取的好处
顺序表上的查找操作
按值查找
例如:在图书表中,按照给定书号进行查找,确定是否存在该图书
如果存在:输出是第几个元素;如果不存在,输出0
【算法2.3】顺序表的查找
- 在线性表L中查找与指定值e相同的数据元素的位置
- 从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号;未找到,返回0(这里我们选择从前往后找)
// 用for语句实现
int LocateElem(SqList L, ElemType e) {
//在线性表L中查找值为e的数据元素,返回其序号(是第几个元素)
for (int i = 0; i < L.length; i++)
if (L.elem[i] == e)
return i+1;//查找成功,返回序号
return 0;//查找失败,返回0
}
// 用while语句实现
int LocateElem(SqList L, ElemType e){
//在线性表L中查找值为e的数据元素,返回其序号(是第几个元素)
int i = 0;
while(i < L.length && L.elem[i] != e) i++;
if(i < L.length) return i+1; // 查找成功,返回序号
return 0; // 查找失败,返回0
}
顺序表的查找算法分析:
因为查找算法的基本操作为:L.elem[i]==e
将记录的关键字与给定值进行比较
平均查找长度ASL(Average Search Length)
- 为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度。
- 对含有n个记录的表,查找成功时: A S L = ∑ i = 1 n P i C i ASL=\sum_{i=1}^nP_iC_i ASL=∑i=1nPiCi,其中 P i P_i Pi是第i个记录被查找的概率, C i C_i Ci是找到第i个记录需比较的次数
- 顺序查找的平均查找长度: A S L = P 1 + 2 P 2 + ⋯ + ( n − 1 ) P n − 1 + n P n ASL=P_1+2P_2+\cdots+(n-1)P_{n-1}+nP_n ASL=P1+2P2+⋯+(n−1)Pn−1+nPn,假设每个记录查找概率相等: P i = 1 / n P_i=1/n Pi=1/n,则 A S L = n + 1 2 ASL=\frac{n+1}{2} ASL=2n+1,故该算法的平均时间复杂度为 O ( n ) O(n) O(n)
【算法2.4】顺序表的插入
线性表的插入运算是指在表的第i(
1
≤
i
≤
n
+
1
1\le i\le n+1
1≤i≤n+1)个位置上,插入一个新结点e,使长度为n的线性表
(
a
1
,
,
⋯
,
a
i
−
1
,
a
i
,
⋯
,
a
n
)
(a_1,,\cdots,a_{i-1},a_i,\cdots,a_n)
(a1,,⋯,ai−1,ai,⋯,an)变成长度为n+1的线性表
(
a
1
,
,
⋯
,
a
i
−
1
,
e
,
a
i
,
⋯
,
a
n
)
(a_1,,\cdots,a_{i-1},e,a_i,\cdots,a_n)
(a1,,⋯,ai−1,e,ai,⋯,an)
算法思想:
- 判断插入位置i是否合法
- 判断顺序表的存储空间是否已满,若已满返回ERROR
- 将第n至第i位的元素依次向后移动一个位置,空出第i个位置
- 将要插入的新元素e放入第i个位置
- 表长加1,插入成功返回OK
Status ListInsert_Sq(SqList& L, int i, ElemType e) {
//这里的i是元素位序而非数组下标
if (i<1 || i>L.length + 1)return ERROR;// i值不合法
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++;
return OK;
}
顺序表的插入算法分析:
算法时间主要耗费在移动元素的操作上
- 若插入在尾结点之后,根本无需移动
- 若插入在首结点之前,则表中元素全部后移(特别慢)
- 若要考虑在各种位置插入(共n+1种可能)的平均移动次数 E i n s = 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) = 1 n + 1 ( n + ⋯ + 1 ⋯ 0 ) = 1 n + 1 n ( n + 1 ) 2 = n 2 E_{ins}=\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1)=\frac{1}{n+1}(n+\cdots+1\cdots0)=\frac{1}{n+1}\frac{n(n+1)}{2}=\frac{n}{2} Eins=n+11i=1∑n+1(n−i+1)=n+11(n+⋯+1⋯0)=n+112n(n+1)=2n其中 n n n是线性表中元素的个数,
- 该算法的平均时间复杂度为为 O ( n ) O(n) O(n)
【算法2.5】顺序表的删除
线性表的删除运算是指将表的第i(
1
≤
i
≤
n
1\le i\le n
1≤i≤n)个结点删除
使长度为n的线性表
(
a
1
,
,
⋯
,
a
i
−
1
,
a
i
,
a
i
+
1
,
⋯
,
a
n
)
(a_1,,\cdots,a_{i-1},a_i,a_{i+1},\cdots,a_n)
(a1,,⋯,ai−1,ai,ai+1,⋯,an)
变为长度为n-1的线性表
(
a
1
,
,
⋯
,
a
i
−
1
,
a
i
+
1
,
⋯
,
a
n
)
(a_1,,\cdots,a_{i-1},a_{i+1},\cdots,a_n)
(a1,,⋯,ai−1,ai+1,⋯,an)
算法思想:
- 判断删除位置i是否合法(合法值为 1 ≤ i ≤ n 1\le i\le n 1≤i≤n)
- (可选)将欲删除的元素保留在e中
- 将第i+1至第n位的元素依次向前移动一个位置
- 表长减1,删除成功返回OK
Status ListDelete_Sq(SqList& L, int i) {
//这里的i是元素位序而非数组下标
if (i<1 || i>L.length)//i值不合法
return ERROR;
for (j = i; j <= L.length - 1; j++)
//被删除元素之后的元素前移,将后一个位置元素赋值给前一个位置
L.elem[j - 1] = L.elem[j];
L.length--;//表长减1
return OK;
}
- 若删除尾结点,无需移动
- 若删除首结点,则表中n-1个元素全部前移(特别慢)
- 若要考虑在各种位置删除(共n种可能)的平均移动次数 E i n s = 1 n ∑ i = 1 n ( n − i ) = 1 n n ( n − 1 ) 2 = n − 1 2 E_{ins}=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{1}{n}\frac{n(n-1)}{2}=\frac{n-1}{2} Eins=n1i=1∑n(n−i)=n12n(n−1)=2n−1
- 顺序表删除算法的平均时间复杂度为 O ( n ) O(n) O(n)
顺序表(线性表的顺序存储结构)的特点
- 线性表的逻辑结构与存储结构一致
- 在访问线性表时,可以快速计算出任何一个数据元素的存储地址。可以粗略认为访问每个元素所花时间相等。这种存取元素的方法被称为随机存取法
顺序表的操作算法分析
-
时间复杂度
- 查找、插入、删除算法的平均时间复杂为 O ( n ) O(n) O(n)
-
空间复杂度
- 顺序表操作算法的空间复杂度 S ( n ) = O ( 1 ) S(n)=O(1) S(n)=O(1)(没有占用辅助空间)
-
优点:
- 存储密度大(结点本身所占存储量/结点结构所占存储量)
- 可以随机存取表中任一元素
-
缺点
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 属于静态存储形式,数据元素的个数不能自由扩充
2.5 线性表的链式表示和实现
2.5.1 线性表的链式存储表示
- 用一组物理位置任意的存储单元来存放线性表的数据元素
- 这组存储单元可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的
- 链表中元素的逻辑次序和物理次序不一定相同
单链表由头指针唯一确定,因此单链表可以用头指针的名字来命名
例:26个英文小写字母表的链式存储结构
各结点由两个域组成:
数据域:存储元素数值数据
指针域:存储直接后继结点的存储位置
与链式存储有关的术语
-
结点:数据元素的存储映像,由数据域和指针域两部分组成
-
链表:n个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构。
-
单链表、双链表、循环链表:
-
结点只有一个指针域的链表,称为单链表或线性链表
-
结点有两个指针域的链表,称为双链表
-
首尾相接的链表称为循环链表
-
-
头指针、头结点和首元结点
头指针:指向链表中第一个结点的指针
首元结点:指链表中存储第一个数据元素 a 1 a_1 a1的结点
头结点:在链表的首元结点之前附设的一个结点
链表的存储结构示意图有以下两种形式:
1. 不带头结点
2. 带头结点
【讨论1】如何表示空表?
-
无头结点时,头指针为空时表示空表
-
有头结点时,当头结点的指针域为空时表示空表
【讨论2】在链表中设置头结点的好处
- 便于首元结点的处理
首元结点的地址保存在头结点的指针域中,在链表的第一个位置上的操作和其他位置一致,须进行特殊处理 - 便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
【讨论3】头结点的数据域内装什么?
头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此节点不含数据元素,不计入链表长度值。
链表(链式存储结构)的特点(顺序存取)
- 节点在存储器中的位置任意,逻辑上相邻的数据元素在物理上不一定相邻
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
2.5.2 单链表的定义和表示
带头结点的单链表
单链表由表头唯一确定,可以用头指针的名字来命名。
若头指针名是L,则把链表称为表L
- 单链表的存储结构
//typedef为一种数据类型定义一个新名字
typedef struct LNode { // 声明结点的类型和指向结点的指针类型
ElemType data; //结点的数据域
struct LNode* next;//结点的指针域,嵌套定义
}Lnode,*LinkList;//Lnode和*LinkList都是该结构的别名
定义链表L:可以用LNode* L
或LinkList L
(通常用后者)
由于LinkList
已经是指针型变量了,所以L
不用加*号
定义结点指针p:LNode* p
或LinkList p
(通常用前者)
例如:存储学生学号、姓名、成绩的单链表结点类型定义如下:
typedef struct student {
char num[8];// 数据域
char name[8];// 数据域
int score;// 数据域
struct student* next;//指针域
}LNode,*LinkList;
为了统一链表的操作,通常这样定义:
typedef struct {//将数据域打包为ElemType
char num[8];//数据域
char name[8];//数据域
int score;//数据域
}ElemType;
typedef struct student {
ElemType data;//数据域
struct student* next;//指针域
}LNode,*LinkList;
2.5.3 单链表上基本操作的实现
【算法2.6】单链表的初始化(带头结点的单链表)
即构造一个如图空表
算法步骤:
- 生成新结点作为头结点,用头指针L指向头结点
- 将头结点的指针域置空。
status InitList_L(LinkList &L){
L=new LNode; // 或L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
return OK;
}
【补充算法1】判断链表是否为空:
空表:链表中无元素,称为空链表(头指针和头结点仍然存在)
算法思路:判断头结点指针域是否为空
int ListEmpty(LinkList L){ // 若L为空表则返回1,否则返回0
if(L->next) // 非空
return 0;
else
return 1;
}
【补充算法2】单链表的销毁:链表销毁后不存在(过河拆桥)
算法思路:从头指针开始,依次释放所有结点
- 先让
p=L
- 再让L指向下一个结点
L=L->next;
(next
域存放的是下一个结点的地址) free(p)
,重复上述操作(循环条件:L!=NULL
或L
),直至L==NULL
(结束条件)
status DestroyList_L(LinkList &L){//销毁单链表L
LNode* p; //或LinkList p;
while(L){
p=L; // p和L指向当前结点
L=L->next; // L指向下一结点
free(p); // 或delete p; 释放当前结点
}
return OK;
}
【补充算法3】清空链表
链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)
算法思路:从首元结点开始,依次释放所有结点,并将头结点指针域设置为空
- 从首元结点开始:
p=L->next;
,使p指针指向首元结点; - 在释放某个结点前,要先引入指针q(因为这里要保留L)指向该结点指向的下一个结点:
q=p->next;free(p);
- 反复执行
p=q;q=p->next;
直至p==NULL
- 设置
L->next=NULL
status ClearList(LinkList &L){//将L重置为空表
LNode *p,*q;//或LinkList p,q;
p=L->next;
while(p){//没到表尾
q=p->next;
free(p);
p=q;
}
L->next=NULL;//头结点指针域为空
}
【补充算法4】求单链表的表长
算法思路:从首元结点开始,依次计数所有结点
int ListLength_L(LinkList L){ // 返回L中数据元素个数
LNode *p;
p=L->next;//p指向第一个结点
int i=0;
while(p){//遍历单链表,统计结点数 判断当前结点是否为空,不空则加1
i++;
p=p->next;
}
return i;
}
阶段总结:
【算法2.7】取值——取单链表中第i个元素的内容
算法思路:
从链表的头指针出发,顺着链域next
逐个结点往下搜索,直至搜索到第i个结点位置。因此,链表不是随机存取结构。
算法步骤:
- 从第1个结点(
L->next
)顺链扫描,用指针p指向当前扫描到的结点,p初值p=L->next
- j做计数器,累计当前扫描过的结点数,j初值为1
- 当p指向扫描到的下一结点时,计数器j加1
- 当
j==i
时,p所指的结点就是要找的第i个结点
status GetElem_L(LinkList L,int i, ElemType &e){
//获取线性表L中的某个数据元素内容,通过变量e返回
//L为带头结点的单链表的头指针
LNode *p=L->next; // p指向首元结点
int j=1;//初始化
// 向后扫描,直到p指向第i个元素或p为空
while(p && j<i){
p=p->next;
j++;
}
//!p表明i>L.length,j>i表明i<0
if(!p || j>i) return ERROR;//第i个元素不存在
e=p->data; // 取第i个元素
return OK;
}
【算法2.8】查找
- 按值查找:根据指定数据获取该数据所在的位置(该数据的地址)
算法步骤:
- 从第一个结点起,依次和e相比较
- 如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或“地址”
- 如果查遍整个链表都没有找到其值和e相等的元素,则返回0或"NULL"
//在线性表L中查找值为e的数据元素
//找到,则返回L中值为e的数据元素的地址,查找失败返回NULL
LNode* LocateElem_L(LinkList L,Elemtype e){
LNode* p=L->next;
while(p && p->data!=e)
p=p->next;
return p;// 找不到就返回NULL
}
- 按值查找:根据指定数据获取该数据所在的位置序号(是第几个数据元素)
//在线性表L中查找值为e的数据元素的位置序号
//返回L中值为e的数据元素的位置序号,查找失败返回0
int LocateElem_L(LinkList L,Elemtype e){
LNode *p=L->next;
int j=1;
while(p && p->data!=e){
p=p->next;
j++;
}
if(p) return j;
else return 0;
}
【算法2.9】插入——在第i个结点前插入值为e的新结点(
1
≤
i
≤
n
+
1
1\le i\le n+1
1≤i≤n+1)
算法步骤:
- 首先找到 a i − 1 a_{i-1} ai−1的存储位置p
- 生成一个数据域为e的新结点s
- 插入新结点:
- 新结点的指针域指向结点
a
i
a_i
ai:
s->next=p->next;
- 结点
a
i
−
1
a_{i-1}
ai−1的指针域指向新结点:
p->next=s;
步骤3.1和3.2不能互换,否则会丢失 a i a_i ai地址
- 新结点的指针域指向结点
a
i
a_i
ai:
//在L中第i个元素之前插入数据元素e
status ListInsert_L(LinkList &L,int i,ElemType e){
//从头结点出发
LNode *p=L;
int j=0;
while(p && j<i-1){//寻找第i-1个结点,p指向i-1结点
p=p->next;
j++;
}
if(!p||j>i-1) return ERROR;//!p表明i>L.length,j>i表明i<0,插入位置非法
// 以上操作等价于GetElem(L,i-1)
s=new LNode;//生成新结点s
s->data=e;//将结点的数据域置为e
s->next=p->next;//将结点s插入L中
p->next=s;
return OK;
}
【算法2.10】删除——删除第i个结点(
1
≤
i
≤
n
1\le i\le n
1≤i≤n)
算法步骤:
-
首先找到 a i − 1 a_{i-1} ai−1的存储位置p,保存要删除的 a i a_i ai的值(可选)
-
令
p->next
指向 a i + 1 a_{i+1} ai+1
-
释放结点 a i a_i ai的空间
// 将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L, int i, ElemType &e) {
// 从头指针开始
LNode* p = L;
int j = 0;
//寻找第i个结点,并令p指向其前驱
while (p->next && j < i - 1) {// p->next是保证
p = p->next;
j++;
}
if (!(p->next) || j > i - 1) {//删除位置不合理,只能删除第1-n个
return ERROR;
}
// 以上操作等价于GetElem(L,i-1)
LNode* q = p->next;//临时保存被删结点的地址以备释放
p->next = q->next;//改变删除结点前驱结点的指针域
e = q->data;//保存删除结点的数据域
delete q;//释放删除结点的空间
return OK;
}
注:删除是需要p
指向要删除结点的前一个结点,如果p->next
为空代表指向了最后一个结点。但是不能删除最后一个结点的后面一个结点,要删除最后一个结点就需要p
指向倒数第二个结点,所以判断条件是p->next
- 单链表的查找、插入、删除算法时间效率分析
- 查找:
LNode* LocateElem_L(LinkList L,Elemtype e)
- 因为线性链表只能顺序存取,查找的时间复杂度为 O ( n ) O(n) O(n)
- 插入和删除:
- 不需要移动元素,只要修改指针,一般时间复杂度为 O ( 1 ) O(1) O(1)
- 但如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O ( n ) O(n) O(n)
【算法2.11】建立单链表:头插法——元素插入在链表头部,也叫前插法
算法步骤:
- 从一个空表开始,重复读入数据
- 生成新结点,将读入数据存放到新结点的数据域中
- 从最后一个结点开始,依次将各结点插入到链表的前端
void CreateList_H(LinkList &L, int n) {//n指结点数
L = new LNode;//创建头结点 L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;//建立一个带头结点的单链表,将头结点置空
for (int i = n; i > 0; i--) {
LNode* p = new LNode;//生成新结点 LNode* p = (LNode*)malloc(sizeof(LNode));
cin >> p->data//输入元素值 scanf(&p->data);
p->next = L->next;//插入到表头
L->next = p;
}
}
头插法的时间复杂度为 O ( n ) O(n) O(n)
【算法2.12】建立单链表:尾插法——元素插入在链表尾部,也叫后插法
算法步骤:
- 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点
- 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
//正位序输入n个元素的值,建立带表头结点的单链表L
void CreateList_R(LinkList &L, int n){// n为结点个数
L = new LNode;// L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;// 头结点置空
LNode* r = L;// 尾指针r指向头结点
for (int i = 0; i < n; i++) {
LNode* p = new LNode;//生成新结点 LNode* p = (LNode*)malloc(sizeof(LNode));
cin >> p->data;//读入元素值 scanf(&p->data);
p->next = NULL;
r->next = p;//插入到表尾
r = p;//r指向新的尾结点
}
}
尾插法的时间复杂度为 O ( n ) O(n) O(n)
2.5.4 循环链表
循环链表是一种头尾相接的链表(表中最后一个结点的指针域指向头结点,整个链表形成一个环)
空表的头结点的指针域存的是自身的地址
优点:从表中任一结点出发均可找到表中其他结点
由于循环链表中没有空指针,故涉及遍历操作时,其终止条件是判断它们是否等于头指针
注意:表的操作常常是在标的首尾位置上进行。
所以在使用循环链表时,更多使用带尾指针的循环链表
带尾指针循环链表的合并(将Tb合并在Ta之后)
操作步骤:
- p存表头结点:
p=Ta->next;
- Tb表头连接Ta表尾:
Ta->next=Tb->next->next;
- 释放Tb表头结点:
free(Tb->next);
- 修改指针:
Tb->next=p;
LinkList Connect(LinkList Ta, LinkList Tb) {
//假设Ta、Tb都是非空的单循环链表
LNode* p = Ta->next; // p存表头结点
Ta->next = Tb->next->next; // Tb表头连接Ta表尾
delete Tb->next; // 释放Tb表头结点
Tb->next = p; // 修改指针
return Tb;
}
时间复杂度是 O ( 1 ) O(1) O(1)
2.5.5 双向链表
为什么要讨论双向链表:
可以用双向链表克服单链表的这种缺点。
在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,称为双向链表
双向链表的结构定义如下:
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior,*next;
}DuLNode,*DuLinkList;
其中,头结点的prior
域为空,尾结点的next
域为空
双向循环链表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点
尾结点的next
域指向头结点,头结点的prior
域指向尾结点
双向链表结构的对称性(设指针p指向某一结点)
p->prior->next = p = p->next->prior
后继的前驱是前驱的后继
双向链表中的操作(如:ListLength, GetElem
等),因仅设计一个方向的指针,故算法与线性链表的相同。
但在插入、删除时,则需要同时修改两个方向上的指针,两者的操作的时间复杂度均为
O
(
n
)
O(n)
O(n)
【算法2.13】双向链表的插入
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
Status ListInsert_DuL(DuLinkList &L, int i, ElemType e){
//在带头结点的双向循环链表L中第i个位置之前插入元素e
if(!(p=GetElemP_DuL(L,i))) retunr ERROR;
LNode* s=new 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;
Status ListDelete_DuL(DuLinkList &L, int i, ElemType &e){
//删除带头结点的双向循环链表L的第i个元素,并用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;
}
插入和删除操作的时间复杂度都是
O
(
1
)
O(1)
O(1),但是之前要查找,查找的时间复杂度是
O
(
n
)
O(n)
O(n)
所以整个算法的时间复杂度是
O
(
n
)
O(n)
O(n)
单链表、循环链表和双向链表的时间效率比较
查找表头结点(首元结点) | 查找表尾结点 | 查找结点*p的前驱结点 | |
---|---|---|---|
带头结点的单链表L | L->next 时间复杂度 O ( 1 ) O(1) O(1) | 从L->next 依次向后遍历 时间复杂度 O ( n ) O(n) O(n) | 通过p->next 无法找到其前驱 |
带头结点仅设头指针L的循环单链表 | L->next 时间复杂度 O ( 1 ) O(1) O(1) | 从L->next 依次向后遍历 时间复杂度 O ( n ) O(n) O(n) | 通过p->next 可以找到其前驱 时间复杂度 O ( n ) O(n) O(n) |
带头结点仅设尾指针R的循环单链表 | R->next 时间复杂度 O ( 1 ) O(1) O(1) | R 时间复杂度 O ( 1 ) O(1) O(1) | 通过p->next 可以找到其前驱 时间复杂度 O ( n ) O(n) O(n) |
带头结点的双向循环链表L | L->next 时间复杂度 O ( 1 ) O(1) O(1) | L->prior 时间复杂度 O ( 1 ) O(1) O(1) | p->prior 时间复杂度 O ( 1 ) O(1) O(1) |
2.6 顺序表和链表的比较
链式存储结构的优点:
- 结点空间可以动态申请和释放;
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
链式存储结构的缺点:
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大
- 非随机存取结构,对任一结点的操作都要从头指针依指针链查找到该结点,增加了算法的复杂度
存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比:
存储密度
=
结点数据本身占用的空间
结点占用的空间总量
存储密度=\frac{结点数据本身占用的空间}{结点占用的空间总量}
存储密度=结点占用的空间总量结点数据本身占用的空间
例如:
一般地,存储密度越大,存储空间的利用率就越高。
显然,顺序表的存储密度为100%,而链表的存储密度小于1
2.7.2 有序表的合并
问题描述:已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列。
算法步骤
- 创建一个空表Lc
- 依次从La或Lb中选取元素值较小的结点插入到Lc表的最后,直至其中一个表变空为止
- 继续将La或Lb其中一个标的剩余结点插入在Lc表的最后
【算法2.16】有序表合并——用顺序表实现
void MergeList_Sq(SqList La,SqList Lb,SqList &Lc){
ElemType* pa=La.elem;//指针pa和pb的初值分别指向两个标的第一个元素
ElemType* pb=Lb.elem;
Lc.length=La.length+Lb.length;//新表长度为待合并量表长度之和
Lc.elem=new ElemType[Lc.length];//为合并后的新表分配一个数组空间
pc=Lc.elem;//指针pc指向新表的第一个元素
pa_last=La.elem+La.length-1;//指针pa_last指向La表的最后一个元素
pb_last=Lb.elem+Lb.length-1;//指针pb_last指向Lb表的最后一个元素
while(pa<=pa_last && pb<=pb_last){//两个表都非空
if(*pa<=*pb)
*pc++ = *pa++;//依次“摘取”两表中值较小的结点 先*pa=*pc后两个指针++
else
*pc++ = *pb++;
}
while(pa<=pa_last)
*pc++ = *pa++;//Lb表已到达表尾,将La中剩余元素加入Lc
while(pb<=pb_last)
*pc++ = *pb++;//La表已到达表尾,将Lb中剩余元素加入Lc
}
算法时间复杂度是两表长度和:
O
(
L
i
s
t
L
e
n
g
t
h
(
L
a
)
+
L
i
s
t
L
e
n
g
t
h
(
L
b
)
)
O(ListLength(La)+ListLength(Lb))
O(ListLength(La)+ListLength(Lb))
算法空间复杂度也是两表长度和:
O
(
L
i
s
t
L
e
n
g
t
h
(
L
a
)
+
L
i
s
t
L
e
n
g
t
h
(
L
b
)
)
O(ListLength(La)+ListLength(Lb))
O(ListLength(La)+ListLength(Lb))
【算法2.17】有序表合并——用链表实现
用La的头结点作为Lc的头结点
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
LNode* pa=La->next;
LNode* pb=Lb->next;
LNode* pc=Lc=La;//用La的头结点作为Lc的头结点
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;//插入剩余段
delete Lb; // 释放Lb的头结点
}
}
算法的时间复杂度是
O
(
L
i
s
t
L
e
n
g
t
h
(
L
a
)
+
L
i
s
t
L
e
n
g
t
h
(
L
b
)
)
O(ListLength(La)+ListLength(Lb))
O(ListLength(La)+ListLength(Lb))
算法的空间复杂度是
O
(
1
)
O(1)
O(1)
2.8 案例分析与实现
【案例2.1】一元多项式的运算:实现两个多项式加、减、乘运算
实现两个多项式相加运算:
【案例2.2】稀疏多项式的运算
线性表A=((7,0),(3,1),(9,8),(5,17))
线性表B=((8,1),(22,7),(-9,8))
- 创建一个新数组C
- 分别从头遍历比较A和B的每一项
- 指数相同,对应系数相加,若其和不为0,则在C中增加一个新项
- 指数不相同,则将指数较小的项复制到C中
- 一个多项式已遍历完毕时,将另一个剩余项依次复制到C中即可
数组C最大可能是m+n项,最小可能是0项,大小不确定
并且需要创建新数组C,空间复杂度较高
采用链式存储结构可以解决这一问题
typedef struct PNode{
float coef; // 系数
int expn; // 指数
struct PNode *next; // 指针域
}PNode, *Polynomial;
多项式创建——算法步骤:
- 创建一个只有头结点的空链表
- 根据多项式的项的个数n,循环n次执行以下操作:
- 生成一个新结点
*s
- 输出多项式当前项的系数和指数赋给新结点
*s
的数据域 - 设置一前驱指针
pre
,用于指向待找到的第一个大于输入项指数的结点的前驱,pre
初值指向头结点 - 指针
q
初始化,指向首元结点 - 循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输入项指数的结点
*q
- 将输入项结点
*s
插入到结点*q
之前
- 生成一个新结点
void CreatePolyn(Polynomial &p, int n){//输入n项的系数和指数,建立表示多项式的有序链表P
PNode* s = NULL;
PNode* pre = NULL;
PNode* q = NULL;
p = new PNode;
p->next=NULL;//先建立一个带头结点的单链表
for(int i=1; i<=n; i++){//依次输入n个非零项
s = new PNode;//生成新结点
cin >> s->coef >> s->expn;//输入系数和指数
pre = p;//pre用于保存q的前驱,初值为头结点
q = p->next;//q初始化,指向首元结点
while(q && q->expn < s->expn){//找到第一个大于输入项指数的项*q
pre = q;
q = q->next;
}
s->next = q;//将输入项s插入到q和其前驱结点pre之间
pre->next = s;
}
}
用的头插法,首先第一步p肯定是空的,跳过循环直接到赋值部分,新节点就是尾结点指针域赋值为空。之后每次pq都是头结点和首元结点和输入的数据对比然后循环到合适位置插入
多项式相加——算法步骤:
- 指针
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】图书信息管理系统
struct Book{
char id[20]; //ISBN
char name[20]; //书名
int price; // 定价
}
typedef struct{//顺序表
Book* elem;
int length;
}SqList;
typedef struct{//链表
Book data;
struct LNode *next;
}LNode, *LinkList;