2.1线性表的类型定义
线性表 是具有相同特性的数据元素的一个有限序列
- (,,……,,,,……,),其中是线性起点(起始节点),作为数据元素,是的直接前趋,是的直接后驱,是线性终点(终端节点),下标是元素的序号,n为元素总个数即表长。n=0时为空表。
- 在非空的线性表中,有且仅有一个开始节点,它没有直接前趋,仅有一个直接后继
- 有且仅有一个终端节点,它没有直接后继,仅有一个直接前趋
- 类型定义:
ADT List{
数据对象:D = {ai | ai属于Elemset,(i=1,2,……,n,n>=0)}
数据关系:R = {<ai-1,ai> | ai-1,ai属于D,(i=2,3,……,n)}
基本操作:
InitList(&L); DestroyList(&L);
ListInsert(&L,i,e); ListDelete(&L,i,&e);
……
}ADT List
InitList(&L)
- 操作结果:构造一个空的线性表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已经存在,1<=i<=ListLength(L)+1
- 操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加1
ListDelete(&L,i,&e)
- 初始条件:线性表L已经存在且非空,1<=i<=ListLength(L)
- 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
ListTraverse(L,visited())
- 初始条件:线性表L已经存在
- 操作结果:依次对线性表中每个元素调用visited()
2.2线性表的顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像
<1> 顺序存储:逻辑上相邻,物理上也相邻;依次存储,地址连续,中间没有空出存储单元
<2> 线性表第1个数据元素的存储位置,称作线性表的起始位置或基地址
<3> 假设线性表的每个元素需占L个存储单元,则第i+1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系:LOC()=LOC()+l
<4> 所有数据元素的存储位置均可由第一个数据元素的存储位置得到:LOC()=LOC()+(i-1)l
<5> 随机存取
一维数组表示顺序表,用一变量表示顺序表的长度属性:
#define MAXSIZE 100 //最大长度
typedef struct{
ElemType *elem; //指向数据元素的基地址
int length; //线性表的当前长度
}SqList;
//ElemType不能直接用
//malloc(m)函数,开辟m字节长度的地址空间,并返回空间首地址
//sizeof(x)函数,计算变量x的长度
//free(p)函数,释放指针p所指变量的存储空间,即彻底删除一个变量
//需要加载头文件:<stdlib.h>
传地址方式——数组名做参数
- 传递的是数组的首地址
- 对形参数组所做的任何改变都将反映到实参数组中
——> SqList L; //定义变量L,L是SqList这种类型的,L是个顺序表
线性表的初始化:
销毁线性表:
清空线性表:
求线性表的长度:
判断线性表是否为空:
1.顺序表的查找操作
- 按值查找:如果存在,输出是第几个元素;如果不存在,输出0
- 顺序查找:从表的一端开始,逐个进行记录的关键字和给定值的比较
- 平均查找长度ASL:为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值(平均值)叫做查找算法的平均查找长度
- ASL= =(n+1)/2 //是第i个纪录被查找的概览,是找到第i个记录需要比较的次数,假设每个记录的查找概率相等:=1/n
- 时间复杂度为O(n)
- 空间复杂度为O(1)
2.顺序表的插入操作
- 在表的第i(1<=i<=n+1)个位置上,插入一个新结点e,使长度为n的线性表(,……,,,……,)变成长度为n+1的线性表(,……,,e,,……,)
- 判断插入位置是否合法
- 判断顺序表的存储空间是否已满,若已满返回ERROR(不是length)
- 平均移动次数:n/2,n是线性表中元素个数
- 时间复杂度为O(n)
- 空间复杂度为O(1)
3.顺序表的删除算法
- 线性表的删除运算是指将表的第i(1<=i<=n)个结点删除,使长度为n的线性表(,……,,,……,)变成长度为n-1的线性表(,……,,,……,)
- 平均移动次数:(n-1)/2
- 时间复杂度为O(n)
- 空间复杂度为O(1)
顺序表优缺点:
<1> 优点
- 存储密度大(结点本身所占存储量/结点结构所占存储量)
- 可以随机存取表中任一元素
<2> 缺点
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 属于静态存储形式,数据元素的个数不能自由扩充
2.3线性表的链式表示和实现
线性表的链式表示又称为非顺序映像或链式映像
<1> 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。这组存储单元既可以是连续的,也可以不连续,甚至零散分布在内存中的任意位置上
<2> 单链表是由头指针唯一确定,因此单链表可以用头指针的名字来命名
<3> 各节点由两个域组成:
- 数据域:存储元素数值数据
- 指针域:存储直接后继结点的存储位置
<4> 链表:n个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构
<5> 顺序存取法:访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花的时间不等
单链表 结点只有一个指针域的链表,也称线性链表
双链表 结点有两个指针域的链表
循环链表 首尾相接的链表
头指针 是指向链表中第一个结点的指针
首元结点 链表中存储第一个数据元素的结点
头节点 在链表的首元结点之前附设的一个结点
空表的表示
1.无头结点时,头指针为空时表示空表
2.有头结点时,当头结点的指针域为空时表示空表
设头结点的好处
1.便于首元结点的处理:首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无需进行特殊处理
2.便于空表和非空表的统一处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理统一
头结点的数据域内装的是什么
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值
单链表
带头结点的单链表:单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名,若头指针名是L,则把链表称为表L
单链表的存储结构:
定义链表L:LinkList L;
定义结点指针p:LNode *p; <——> LinkList p;
1.单链表的初始化
- 生成新结点作头结点,用头指针L指向头结点
- 将头结点的指针域置空
补充算法1:判断链表是否为空(头结点和头指针仍然在),只要判断头结点指针域是否为空
补充算法2:单链表的销毁,销毁后不存在。从头指针开始,依次释放所有结点
Status DestroyList_L(LinkList &L){ //销毁单链表L
Lnode *p; // 或LinkList p
while(L){
p=L;
L=L->next;
delete p;
}
return OK;
}
补充算法3:清空链表。链表仍存在,但无元素,成空链表(头结点和头指针仍然在)。依次释放所有结点,并将头结点指针域设置为空
补充算法4:求单链表表长。从首元结点开始,依次计数所有结点
int ListLength_L(LinkList L){ //返回L中数据元素的个数
LinkList p;
p=L->next; //p指向第一个结点
i=0;
while(p){ //遍历单链表,统计结点数
i++;
p=p->next;
}
return i;
}
2.取值
- 从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止
- 1.从第一个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值p=L->next
- 2.j做计数器,累计当前扫描过的结点数,j初值为1
- 3.当p指向扫描到的下一结点时,计数器j加1
- 4.j==1时,p所指的结点就是要找的第i个结点
3.查找
按值查找——
根据指定数据获取该数据所在的位置(地址)
- 1.从第一个结点起,依次和e相比较
- 2.如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址
- 3.如果查遍整个链表都没有找到其值和e相等的元素,则返回0或“NULL”
根据指定数据获取该数据位置序号
- 时间复杂度为O(n)
4.插入
- 在第i个结点前插入值为e的新结点
- 1.首先找到的存储位置p
- 2.生成一个数据域为e的新结点s
- 3.插入新结点:<1> 新结点的指针域指向结点 <2> 结点的指针域指向新结点
- 时间复杂度为O(1) //因为不会随着n变大而增加遍历次数(不需要移动元素,只要修改指针)
5.删除
- 1.首先找到的存储位置p,保存要删除的的值
-
2.令p->next指向
-
3.释放结点的空间
- 时间复杂度为O(1) //因为不会随着n变大而增加遍历次数(不需要移动元素,只要修改指针)
上述算法为简化,缺少q和i 的定义
6.建立单链表
头插法——元素插入在链表头部,也叫前插法(每次接在头结点后)
- 1.从一个空表开始,重复读入数据
- 2.生成新结点,将读入数据存放到新结点的数据域中
- 3.从最后一个结点开始,依次将各结点插入到链表的前端(而不是直接在已经接好的结点后面接)
- 时间复杂度为O(n)
尾插法——元素插入在链表尾部,也叫后插法
- 1.从一个空表L开始,将新结点逐个插入到链表尾部,尾指针r指向链表的尾结点
- 2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点
- 时间复杂度为O(n)、
7.循环链表
——是一种头尾相接的链表(即表中最后一个结点的指针域指向头结点,整个链表形成一个环)
- 优点:从表中任一结点出发均可1找到表中其他结点
- 注意:由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针
- 头指针表示单循环链表:找的时间复杂度O(1),找的时间复杂度O(n)——不方便
- 尾指针表示单循环链表:的存储位置是R->next->next,的存储位置是R,两者的时间复杂度均为O(1)
带尾指针循环链表的合并
- p存表头结点 //p=Ta->next
- Tb表头连接到Ta表尾 //Ta->next=Tb->next->next
- 释放Tb表头结点 //delete Tb->next
- 修改指针 //Tb->next=p
算法时间复杂度为O(1)
8.双向链表
——在单链表的每个结点里再增加一个指向其直接前趋的指针域prior,这样链表中就形成了有两个不同方向的链
双向循环链表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点
对称性(设指针p指向某一结点):p->prior->next=p=p->next->prior
在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作时间复杂度均为O(n)
双向链表的插入
- s->prior=p->prior
- p->prior->next=s
- s->next=p
- p->prior=s
双向链表的删除
- p->prior->next=p->next
- p->next->prior=p->prior
知道删除结点是哪一个,时间复杂度为O(1);如果需要查找删除位置在什么地方,时间复杂度为O(n)
2.4顺序表和链表的比较
链式存储结构的优点:
- 结点空间可以动态申请和释放
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
缺点:
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。(存储密度 = 结点数据本身占用的空间 / 结点占用的空间总量,一般存储密度越大,存储空间利用率越高,顺序表的存储密度为1,链表小于1)
- 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针沿指针链查找到该结点,这增加了算法复杂度
2.5线性表的应用
线性表的合并
问题描述:假设利用两个线性表La和Lb分别表示两个集合A和B,现要求一个新的集合A=
如:La = (7,5,3,11) Lb = (2,6,3) ——>Lc = (7,5,3,11,2,6)
算法步骤:
- 依次取出Lb中的每个元素,执行以下操作
- 在La中查找该元素
- 如果找不到,则将其插入La的最后
算法时间复杂度:O(ListLength(La)*ListLength(Lb))
有序表的合并
问题描述:已知线性表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)
算法步骤:
- 创建一个空表Lc
- 依次从La或Lb中“摘取”元素值较小的结点插入到Lc表的最后,直至其中一个表变空为止
- 继续将La或Lb其中一个表的剩余结点插在Lc表的最后
顺序表实现
- 时间复杂度为O(ListLength(La)+ListLength(Lb))
- 空间复杂度为O(ListLength(La)+ListLength(Lb))
用链表实现
- 时间复杂度为O(ListLength(La)+ListLength(Lb))
- 空间复杂度为O(1) ,不需要额外空间
2.6案例分析与实现
【案例一】一元多项式的运算:实现两个多项式加、减、乘运算
例如:
【案例二】稀疏多项式的运算
顺序存储结构
1.创建一个新数组c
2.分别从头遍历比较a和b的每一项
- 指数相同,对应系数相加,若其和不为0,则在c中增加一个新项
- 指数不相同,则将指数较小的项复制到c中
3.一个多项式已遍历完毕时,将另一个剩余项依次复制到c中即可
存在的问题:存储空间分配不灵活;运算的空间复杂度高
链式存储结构
例如:
算法步骤:(建立链表)
(多项式相加)
【案例三】图书信息管理系统