线性表及其基本运算
一、 线性表(linear_list)
线性表示n个数据元素的有限序列,记为L=(a1,a2,…,an)
(线性表是最常用且最简单的一种数据结构)
形式化定义:linear_list=(D,R)
其中D={ai | ai ∈D0,i=1,2,…,n,n>=0}(属于相同的数据对象,具有相同的特性)
R={N},N={<ai-1,ai>| ai-1,ai ∈D0,i=2,3,…,n}
N是一个序偶的集合,它表示线性表中数据元素之间的相邻关系ai-1是ai的直接前驱,ai是ai-1的直接后继
二、 基本运算
INITIATE(L) 初始化操作:设定一个空的线性表L(线性表元素个数为0)
LENGTH(L)求长度函数:函数值为线性表L中数据元素的个数
GET(L,i)取元素函数:1<=1<=LENGTH(L)时返回L中第i个数据元素,否则为空元素NULL.(i称为该数据元素在L中的位序)
PRIOR(L,elm) 求前驱函数:elm为L中的一个数据元素,若它的位序大于1,则函数值为elm前驱,否则为NULL
NEXT(L,elm)求后继函数:若elm的位序小于表长,则函数值为elm的后继,否则为NULL
LOCATE(L,x)定位函数:给定值x,若x不在表中,则返回0,否则返回x在表中第一次出现的位序
INSERTE(L,i,b) 前插操作:在第i个元素之前插入新元素b,i的取值范围为1<=i
<=n+1;i=n+1表示在表尾插入,n为表长
DELETE(L,i)删除操作:删除线性表L中的第i个元素,1<=i<=n
EMPTY(L) 判空表函数:若L为空表,则返回布尔值“true”否则返回布尔值“false”
CLEAR(L) 表置空操作:将L置为空表(无返回类型)
例1求两个集合的并,即A=A∪B
分析:设A,B分别由两个线性表LA和LB表示,要求将LB中存在而LA中不存在的DE插入到表LA中。
算法思想:(1)依次从LB中取出一个DE;(GET(LB,i))
(2)判断在LA中是否存在;(LOCATE(LA,x))
(3)若不存在,则插入到LA中。(INSERTE(LA,n+1,b)插在表尾)
形式化算法描述
PROC union(VAR LA:Linear_list;LB:Linear_liist);
{将所有在LB中存在而LA中不存在的DE插入到LA中去}
n=LENGTH(LA);{确定线性表LA的长度}
FOR i:=1 TOLENGTH(LB) DO
[x:=GET(LB,i);{取LB中第i个数据元素}
k:=LOCATE(LA,x);{在LA中进行搜索}
IF k=0THEN[INSERT(LA,n+1,x);{在LA表尾插入}
n:=n+1;{表长加1}] ]
ENDP:{union}
例2归并两个有序的线性表LA和LB为一个新的有序线性表LC
算法思想:
(1) 初始化:置LC为空表,设置变量i,j,初值为1,分别指向LA和LB的第一个DE,k表示LC的长度,初始化 (INITIATE(LC))
(2) 当i<=LENGTH(LA) AND j<=LENGTH(LB)时,判断:若i所指的元素<=j所指的元素,则将i所指的元素插入在LC的k+1前,并且i,k的值分别加1;否则,将j所指的元素插入在LC的k+1前,并且j,k的值分别加1(INSERTE(LC,k+1,b))
(3) 重复(2)直到某个表的元素插入完毕。
(4) 将未插入完的表的余下的元素,依次插入在LC后(INSERTE(LC,k+1,b))
形式化算法描述
PROC merge_list(LA,LB:Linear_liist ; VAR LC:Linear_list);
{ LA,LB中元素依值非递减有序排列,归并得到的LC的元素仍依值非递减有序排列}
INITIATE(LC);i:=1;j:=1;k:=1;{初始化}
WHILE(i<=LENGTH(LA))AND(j<=LENGTH(LB))DO
IFGET(LA,i)<=GET(LB,j)
THEN [INSERT(LC,k+1,GET(LA,i));k=k+1;i=i+1]
ELSE [INSERT(LC,k+1,GET(LB,j));k=k+1;j=j+1];
WHILE i<=LENGTH(LA) DO
[INSERT(LC,k+1, GET(LA,i));k=k+1;i=i+1]
WHILE j<=LENGTH(LB) DO
[INSERT(LC,k+1, GET(LB,j));k=k+1;j=j+1];
ENDP:{merge_list}
算法分析:
主要操作是插入
语句频度:LENGTH(LA)+LENGTH(LB)
算法时间复杂度:O(LENGTH(LA)+LENGTH(LB)),若LA和LB的元素个数同为数量级n,则该算法的时间复杂度为O(n)。
线性表的顺序存储结构
一、顺序存储结构(物理结构的一种)
用一组地址连续的存储单元依次存储线性表的元素,设线性表的每个元素占用k个存储单元,则第i个元素ai的存储位置为:Loc(ai)=Loc(a1)+(i-1)*k 其中,Loc(ai)为线性表的起址。
逻辑存放次序决定物理存放次序,逻辑存放决定物理存放次序,线性表的存储结构和逻辑存储一一对应。
线性表顺序存储结构的定义为:(定义要使用的线性表和存储空间)
CONST maxlen=线性表可能达到的最大长度;
TYPE sqlisttp=RECORD
elem:ARRAY[1…maxlen] OF elemtp 数据元素是相同类型的,允许是任何类型
last:0…maxlen END
线性表的顺序存储结构是一个记录型的结构。
(数据域elem描述了线性表中的DE占用的数组空间,数组的第i个分量为线性表中第i个DE的存储映象;
数据域last指示最后一个DE在数组空间中的位置,也是表长。
二、 插入和删除操作
1. 插入运算INSERT(L,i,b)
插入前:L=(a1,…,ai-1,ai,…,an)
插入后:L=(a1,…,ai-1,b,ai,…,an)
算法思想:
(1) 进行合法性检查,(表头插入)1<=i<=n+1(表尾插入)
(2) 检查线性表是否已满;(L超过内存空间,导致溢出)
(3) 将第n个至第i个元素逐一后移一个单元;(从后往前)
(4) 在第i个位置处插入新元素;
(5) 将表的长度加1。
(1)(2)考虑边界条件,考虑程序健壮性
形式化算法描述:
PROC ins_sqlist(VAR v:sqlisttp; i:integerb:elemtp );
{在顺序存储结构的线性表v中第i个DE之前插入b}
IF(i<1) OR (i>v.last+1)
THEN ERROR(‘i值不合法’)
ELSE IFv.last>=maxlen
THEN ERROR(‘表满溢出’)
ELSE FOR j:=v.last DOWNTO i DO
v.elem[j+1]:=v.elem[j];{右移}
v,elem[i]:=b;
v.last:=v.last+1;
ENDP:{ins_sqlisr}
插入算法时间复杂度分析:
最坏情况是在第1个元素前插入(i=1),此时要后移n个元素,
因此T(N)=O(N)
2. 删除运算DELETE(L,i)
删除前:L=(a1,…,ai-1,ai,ai+1,…,an)
删除后:L=(a1,…,ai-1,ai+1,…,an)
算法思想:
(1) 进行合法性检查,1<=i<=n+1
(2) 判断线性表是否已空,v.last=0;
(3) 将第i+1至第n个元素逐一向前移一个位置;
(4) 将表长的长度减1
形式化算法描述:
PROC del_sqlist(VAR v:sqlisttp; i:integer);
{在顺序存储结构的线性表v中删除第i个DE}
IF(i<1) OR (i>v.last)
THEN ERROR(‘i值错误或者表已空’)
ELSE FOR j:=i+1TO v.last DO
v.elem[j-1]:=v.elem[j];{左移}
v.last:=v.last-1;
ENDP:{del_sqlist}
时间复杂度分析:
最坏情况是删除第一个元素,此时要前移n-1个元素
因此,T(N)=O(N)
三、 线性表顺序存储结构的特点
优点:
(1) 逻辑上相邻的元素,在物理位置也相邻;
(2) 可随机存取表中任一元素;
局限性:
(3) 必须按最大可能的长度预分存储空间,存储空间利用率低,表的容量难以扩充,是一种静态存储结构;
(4) 插入删除时,需移动大量元素,平均移动元素为n/2。
例3顺序结构上的归并有序表算法
PROC merge_sqlist(va, vb: sqlisttp; VAR vc :sqlisttp);
i:=1; j:=1; k:=0;
WHILE ((i<=va.last) AND (j<=vb.last))DO
IF va.elem[i]<=vb.elem[j]
THEN[vc.elem[k+1]:=va.elem[i]; k=k+1; i=i+1];
ELSE[vc.elem[k+1]:=vb.elem[j]; k=k+1; j=j+1];
WHILE i<=va.last DO
[vc.elem[k+1]:=va.elem[i]; k=k+1; i=i+1];
WHILE j<=va.last DO
[vc.elem[k+1]:=va.elem[j]; k=k+1; j=j+1];
vc.last:=k
ENDP: {merge_list}
线性表的链式存储结构
一、 线性链表
1. 链式存储结构
用一组任意的存储单元(不要求地址联系)来存储线性表中的元素,每个元素对应一组存储单元(结点),每个结点包括两个域:存储数据元素信息的数据域和存储直接后继所在位置的指针域。
N个结点通过指针域组成的表,称为线性链表(单链表)
(1)线性表最后一个结点的指针域为“空”(NIL或者∧);
(2)用一个头指针指示链表中第一个结点的存储位置;
(3)链表L=(a1,a2,…,an)逻辑表示
用pascal的指针类型定义单链表
TYPE pointer=↑nodetype;
nodetype=RECORD
data:elemtp;
next:pointer
END;
Linkisttp=pointer;{头指针可以唯一确定一个单链表}
pointer为一指针,指向nodetype记录类型;
nodetype为一记录,它由data和next两项组成;
data为数据元素类型,next为pointer指针;
单链表linkisttp定义为pointer指针。
2. 带头结点的线性链表
在线性链表的第一个元素结点之前附设一个结点(称头结点),它的数据域不存储任何信息,其指针域存储第一个元素结点的存储位置。头指针L指向该头结点。
空表时:L↑.next=NIL (头结点没有后继,空表中包括头结点)
带头结点链表的引入是为了使算法判空和处理一致。
3. 几种基本运算在单链表上的实现
(1) GET(L,i)函数
FUNC get_linklist(la:linkisttp;i:integer):elemtp;
p:=la↑.next; j:=1{移动指针p,计数变量j}
WHLIE(p<>NUL AND j<i)DO {条件1,防止i>表长}
[P:=P↑.next ; j:=j+1;] {条件2控制取第i个,防止i<1}
IF(p<>NULAND j=i){只有一个条件不充分,找到}
THEN RETURN(p↑.data) {正常出循环}
ELSE RETURN(NULL) {异常出循环}
ENDF:{get_linklist}
边界条件:i的合法性检查已经蕴含在WHILE和if条件中;
循环条件分析:
p:=la↑.next; j:=1
WHLIE(p<>NUL AND j<i)DO [P:=P↑.next ; j:=j+1;]
条件1:防止i>表长,条件2:控制取第i个,并防止了i<1。
两个条件有6钟组合:
1.P=NIL AND j<i 空表且i>1或i>表长+1,异常,返回NULL
2.P=NIL AND j=i 空表且i=1或i=表长+1,异常,返回NULL
3.P=NIL AND j>i 空表且i<1,异常出循环,返回NULL
4. p<>NULAND j<i 继续循环
5. p<>NULAND j=i 确定第i个结点,正常出循环(找到的条件)
6. p<>NUL ANDj>i i<1,异常出循环,返回NULL
算法时间复杂度分析:WHILE最多执行i-1次,最坏情况是取第n个结点,需执行n-1次,故:T(N)=O(N)
(2) INSERT(L,i,b) 插入运算
设在单链表结点x和结点y之间插入新结点b,已知p为指向结点x的指针,s为指向新结点b的指针。
插入前: 插入后
插入运算可以由定位和修改指针来完成
定位:得到指针y的前驱的指针p
修改指针:s↑.next:= p↑.next;
p↑.next:=s; (顺序不能变)
PROCins_linklist(la:linkisttp; i:integer; b:elemtp);
{la为带头结点单链表的头指针}
p:=la; j:=0;{置初值p指向头结点}
WHILE(P<>NILAND j<i-1) DO {定位}
[p:= p↑.next; j:=j+1;]
IF(P=NIL ORj>i-1)
THEN ERROR(“ 插入位置不对”)
ELSE[new(s):s↑.data:=b;{插入}
s↑.next:=p↑.next; p↑.next:=s;]
ENDP:{ins_linklist}
与get_linklist函数的区别:
初值保证能在第一个DE之前插入,插入范围为[1,表长+1]
循环定位条件,定位在i的直接前驱i-1
出循环IF条件是get_linklist的求反,定位后,插入
循环条件分析:
第一次循环p<>NIL,j有三种可能
(1) j<i-1 继续循环;
(2) j=i-1此时i=1,在第一个元素前插入;
(3) j>i-1 i<1不合法
第二次循环后p可能为NIL,但j>i-1不可能,出现的各种可能情况:
(1) p<>NIL AND j<i-1 继续循环;
(2) p<>NIL AND j=i-1 已经定位,出循环
(3) p=NIL AND j<=i-1 i不合法(i>=表长+2),出循环
算法复杂度:
关键在定位,最坏情况是INSERT(L,n+1,b)WHILE 执行n次
故T(N)=O(N)
(3)DELETE(L,i)删除运算
删除运算可以由定位和修改指针来完场:
定位:得到指向第i个元素的前驱的指针p;
修改指针:p↑.next:= p↑.next↑.next;
(删除运算和插入运算都定位到i-1个,i的取值范围不同)
PROCdel_linklist(la:linkisttp; i:integer);
{la为带头结点单链表的头指针}
p:=la; j:=0;{置初值p指向头结点}
WHILE(P↑.next <>NIL AND j<i-1) DO {定位}
[p:= p↑.next; j:=j+1;]
IF(P↑.next =NIL OR j>i-1)
THEN ERROR(“ 表空或者位置不对”)
ELSE[q:= p↑.next ;
p↑.next:=p↑.next↑.next;
dispose(q)]
ENDP:{del_linklist}
删除算法讨论:
删除范围为[1,表长],不能删除头结点;
出循环的五种可能情况:
P↑.next =NIL AND j<i-1 空表或i>表长
P↑.next =NIL AND j=i-1 空表且i=1,或i=表长+1
P↑.next =NIL AND j>i-1 空表且i<1
P↑.next <>NIL AND j=i-1 非空表且已定位
P↑.next <>NIL AND j>i-1 非空表且i<1
时间复杂度:
WHILE至多执行i-1次,当i=n时为最坏情形,因此,T(N)=O(N)
(4)建立链表的算法
算法思想:从空链表开始,依次插入各个结点。
PROC crt_linklist(VAR la:linkisttp; a:ARRAY[1…n] OF elemtp);
new(la):la↑.next=NIL;{建立空表,只有一个头结点}
FOR i:=n DOWNTO 1 DO
[new(p):p↑.data=a[i];{等价于INSERT(L,1,a[i])}
p↑.next= la↑.next;la↑.next=p]
ENDP:{crt_linklist}
算法时间复杂度:T(N)=O(N)
思考:为什么从n到1依次插入?若从1到n算法将如何实现?
例:在单链表上实现归并两个有序表,要求产生的结果有序表仍用原有序表的结点空间。
PROCmerge_linklist(la,lb:linkisttp;VAR lc:linkisttp);
{la和lb为参与归并的两个有序表,lc为结果有序表}
pa:=la↑.next;pb:=lb↑.next;lc:=la;pc:=lc;
WHILE(pa<>NIL AND pb<>NIL)DO
IF pa↑.data<= pb↑.data
THEN[pc↑.next:=pa; pc:=pa; pa:= pc↑.next]
ELSE[pc↑.next:=pb; pc:=pb; pb:= pc↑.next];
IF(pa<>NIL)
THEN pc↑.next:=pa
ELSE pc↑.next:=pb;
dispose(lb)
ENDP:{ merge_linklist }
4. 线性表链式存储结构的特点
(1) 逻辑上相邻的元素,其物理位置不一定相邻:元素之间的邻接关系由指针域指示。
(2) 链表是非随机存取的存储结构;对链表的存取必须从头指针开始。
(3) 链表是一种动态存储结构;链表的结点可调用new()申请和dispose()释放。
(4) 插入删除运算非常方便;只需要改相应指针值。
二、 循环链表
1. 循环链表的存储结构
令链表中最后一个结点的指针域指向头结点,使整个链表形成一个环,称这样的链表为循环链表。
循环链表H=(a1,a2,…,an)逻辑表示为:
空表时:H=H↑.next;
2. 循环链表的特点
(1) 从任一结点出发,沿着链可访问全部结点;
(2) 运算与单链表基本一致,仅是循环结束条件为P↑.next=H(H为头指针);
线性表的链式存储结构——双向链表
三、 双向链表
1. 双向链表的存储结构
双向链表中每一个结点有两个指针域;priou和next,priou指向直接前驱;next指向直接后继。
循环链表L=(a1,a2,…,an)逻辑表示为:
2.双向链表的特点
(1)在双向链表中,查找某结点直接前驱PRIOR(L,elem)和直接后继NEXT(L,elem)的运算的时间复杂度均为O(1)。
(2)空表时:L↑.priou=L↑.next=NIL
(3)双向链表也可以首尾相连构成双向循环链表。双向循环链表满足L↑.priou=L↑.next=L。
(4)在双向链表中,除插入、删除操作差别比较大外,其它基本运算均与单链表相同。在P结点之前插入S结点应该做如下修改动作:
插入前:
插入后:
四个指针域修改:
p↑.priou↑.next=s;
s↑.priou= p↑.priou;
p↑.priou=s;
s↑.next=p;
(语句顺序不固定,要防止断链)
线性表的应用——一元多项式的表示
一元多项式按升幂可以表示为Pn(x)=p0+p1x+p2x2+…+pnxn
1. 多项式的几种存储结构
(1) 全部系数顺序存储结构
将多项式的所有幂的系数pi(i=0,1……n)依次存放在数组中。对幂很高,而系数为零的项也很多时,存储空间浪费很大。
例:S(x)=1+3x100 +2x20000 需要20001项
(2) 非零系数顺序存储结构
Pn(x)又可用非零系数项表示为:
Pn(x)=p1xe1+p2xe2+…+pnxen
其中pi为非零系数,ei为相应的指数。可以只将多项式的所有非零系数pi和相应的指数ei存放在数组中。
(3) 非零系数单链表存储结构
线性表的经典应用时表示一元多项式,并很容易实现多项式各种运算,
用单链表表示一元多项式时,每个结点有三个域;
系数域coef,指数域exp,指针域next。
S(x)=1+3x100+2x20000
2. 多项式相加算法的实现
设:(1)多项式采用非零系数单链表结构
(2)多项式A(x)和B(x)相加,“和多项式”C(x)的结点不另外申请存储空间;
(3)p,q分别指向A(x)和B(x)中的某结点。
运算规则:指数相同,系数相加。
若p↑.exp<q↑.exp,则p结点为C(x)的一项,移动p;
若p↑.exp>q↑.exp,则q结点插入在p结点之前,移动q;
若p↑.exp=q↑.exp,则p↑.coef:=p↑.coef +q↑.coef;释放q结点;当和为0时,释放p结点;移动p和q;
PROC add_poly(VARpa:polytp; pb:polytp);
p:=pa↑.next; q:= pb↑.next;
pre:=pa; pc:=pa;{pre指向p的直接前驱,pc为和多项式的头指针}
WHILE p<>NIL AND q<>NIL DO
CASE
p↑.exp<q↑.exp:[pre:=p; p:= p↑.next];
p↑.exp=q↑.exp:[ x:=p↑.coef +q↑.coef;
IF x<>0 THEN [p↑.coef:=x;pre:=p]
ELSE[pre↑.next= p↑.next; dispose(p)];
p:= pre↑.next; u:=q; q:=q↑.next;
dispose(u)];
p↑.exp>q↑.exp:[ u:=q↑.next; q↑.next=p;pre↑.next= q;pre:=q;q:=u]
ENDC:
IF q<>NIL THEN pre↑.next:=q;
dispose(pb)
ENDP:{add_poly}
算法难点分析:
1. pre指针的作用:pre为p的直接前驱,在将q插入在p之前,或者删除“和为0”的项时,以及最后将pb的剩余项挂入“和多项式”时,要用到p的直接前驱。
2. 三处dispose()的含义:
(1)dispose(u)是指数相同时,释放q结点;
(2)dispose(p)是指数相同且和系数为0时,释放p结点;
(3)dispose(pb)是释放多项式B的头结点。
3. 算法结束的三种可能:
(1)A、B最高次幂次相同:p、q都为NIL;
(2)A最高次幂高于B:q=NIL,结束;
(3)B最高次幂高于A:p=NIL,并将B中剩余项挂入A的尾端。
4. 最坏情形为:各指数均不相同,且B中余项仅一项,则算法的时间复杂度为O(m+n),即O(max(m,n)),m,n分别为A、B的最高次幂。当m与n相当时,时间复杂度为O(n)。