第二章 线性表
2.1 线性表的逻辑结构
2.1.1线性表的定义
线性表是一种线性结构。线性结构的特点是数据元素之间是一种线性关系,数据元素“一个接一个的排列”。在一个线性表中数据元素的类型是相同的,或者说线性表是由同一类型的数据元素构成的线性结构。
线性表是具有相同类型的n(n>=0)个数据元素的有限序列,通常记为:
(a1,a2,…,ai-1,ai,ai+1,…an)
其中n为表长,n=0时称为空表。
线性表中相邻元素之间存在着顺序关系。将ai-1称为ai的直接前趋,ai+1称为ai的直接后继。而a1是表中第一个元素,它没有前趋,an是最后一个元素无后继。
需要说明的是:
ai为序号为i的数据元素(i=1,2,…,n),通常我们将它的数据类型抽象为datatype,datatype根据具体问题而定。
2.1.2线性表的基本操作
(1)线性表初始化:Int_List(L)
初始条件:表L不存在
操作结果:构造一个空的线性表
(2)求线性表的长度:Length_List(L)
初始条件:表L存在
操作结果:返回线性表中的所含元素的个数
(3)取表元:Get_List(L,i)
初始条件:表L存在且1<=i<=Length_List(L)
操作结果:返回线性表L中的第i个元素的值或地址
(4)按值查找:Locate_List(L,x),x是给定的一个数据元素。
初始条件:线性表L存在
操作结果:返回在L中首次出现的值为x的那个元素的序号或地址,称为查找成功;否则,在L中未找到值为x的数据元素,返回一特殊值表示查找失败。
(5)插入操作:Insert_List(L,i,x)
初始条件:线性表L存在,插入位置正确(1<=i<=n+1,n为插入前的表长)。
操作结果:在线性表L的第i个位置上插入一个值为x的新元素,这样使原序号为i,i+1,…,n的数据元素的序号变为I+1,i+2,…,n+1,插入后表长=原表长+1。
(6)删除操作:Delete_List(L,i)
初始条件:线性表L存在,1<=i<=n
操作结果:在线性表L中删除序号为i的数据元素,删除后使序号为i+1,i+2,…,n的元素变为序号为i,i+1,…,n-1,新表长=原表长-1。
需要说明的是:
某数据结构上的基本运算,不是它的全部运算,而是一些常用的基本的运算,而每一个基本运算在实现时也可能根据不同的存储结构派生出一系列相关的运算来。
在上面各操作中定义的线性表L仅仅是一个抽象在逻辑结构层次的线性表,尚未涉及到它的存储结构。
2.2 线性表的顺序存储及运算实现
2.2.1线性表的顺序存储
线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素,用这种存储形式存储的线性表称其为顺序表。
设a1的存储地址为Loc(a1),每个数据元素占d个存储地址,则第i个数据元素的地址为:
Loc(ai)=Loc(a1)+(i-1)*d 1<=i<=n
连续的存储空间借助一维数组来实现。
结构性上考虑,通常将data和last封装成一个结构作为顺序表的类型:
typedef struct
{
datatype data[MAXSIZE];
int last;
}SeqList;
SeqList L;
//定义一个顺序表类型的变量
SeqList *L;//定义一个顺序表类型的指针变量
L.Last 为最后一个元素的下标
①线性表中数据元素顺序存储的基址为:
L.data 或者 L->data
②线性表的表长表示为:
L.last+1 或者 L->last+1
③线性表中数据元素的存储表示为:
L.data[0]~L.data[last] 或者
L->data[0]~L->data[L->Last]
2.2.2顺序表上基本运算的实现
1.顺序表的初始化
顺序表的初始化即构造一个空表,对表是一个加工型的运算。因此,将L设为指针参数,首先动态分配存储空间,然后,将表中last指针置为-1,表示表中没有数据元素。
顺序表的初始化算法
SeqList *init_SeqList(){
SeqList *L;
L=new SeqList;//申请顺序表的存储空间
if(L){
L->last=-1;
return L;//返回顺序表的存储地址
}
else
return -1;
}
2.插入运算
线性表的插入是指在表的第i个位置上插入一个值为x的新元素:
int Insert_SeqList(SeqList *L,int i,DataType x){
int j;
if(L->last==MAXSIZE-1){
cout<<"装满"<<end;
retrurn -1;
}//表空间已满,不能插入,返回错误代码-1
if(i<1||i>L->last){
cout<<"位置错"<<end;
return 0;
}//检查插入位置的正确性
for(j=L->last;j>=i-1;j--)
L->data[j+1]=L->data[j];//节点移动
L->data[i-1]=x;
L->last++;
return 1;
}
顺序表上的插入运算,时间主要消耗在了数据的移动商。在第i个位置上插入x,从ai到an都要向下移动一个位置,共需要移动n-(i-1)个元素,而i的取值范围为1<=i<=n+1,即有n+1个位置可以插入。
设在第i个位置上作插入的概率为Pi,则平均移动数据元素的次数
考虑等概率情况,即Pi=1/ (n+1) ,则:
结论:在顺序表上做插入操作需移动表中一半的数据元素。显然时间复杂度为O(n)。
⒊.删除运算
线性表的删除运算是指将表中第 i 个元素从线性表中去掉。
int Delete_SeqList(SeqList *L,int i){
int j;
if(i<1||i>L->last+1){
cout<<"不存在第i个元素"<<end;
return 0;
}
for(j=i;j<=L->last;j++)
L->data[j-1]=L->data[j];//数据元素向前移动
L->last--;
return 1;
}
与插入运算相同,其时间主要消耗在了移动表中元素上。删除第i个元素时,其后面的元素 ai+1~an 都要向上移动一个位置,共移动了 n-i 个元素,所以平均移动数据元素的次数:
在等概率情况下,pi =1/ n,则:
结论:顺序表上作删除运算时大约需要移动表中一半的元素,显然该算法的时间复杂度为O(n)。
⒋按值查找
线性表中的按值查找是指在线性表中查找与给定值x相等的数据元素。
int Location_SeqList(SeqList *L,DataType x){
int i;
i=0;
while(i<=L->last&&L->data[i]!=x)//顺序检查数据元素值
i++;
if(i>L->last)//到最后元素,没有找到
return -1;//查找不成功,返回错误代码-1
else return i;//查找成功,返回的是存储位置
}
本算法的主要运算是比较。显然比较的次数与x在表中的位置有关,也与表长有关。
结论:平均比较次数为(n+1)/2,时间性能为O(n)。
2.2.3顺序表应用举例
例2.1 将顺序表 (a1,a2,… ,an) 重新排列为以 a1 为界的两部分:a1 前面的值均比 a1 小,a1 后面的值都比 a1 大。
算法 基本思思想:
从第二个元素开始到最后一个元素,逐一向后扫描:
⑴当前数据元素 aI 比 a1 大时,表明它已经在 a1 的后面,不必改变它与 a1 之间的位置,继续比较下一个。
⑵当前结点若比 a1 小,说明它应该在 a1 的前面,此时将它上面的元素都依次向下移动一个位置,然后将它置入最上方。
void part(SeqList *L){
int i,j;
DataType x,y;
x=L->data[0];//将基准数据元素置入x中
for(i=1;i<=L->last;i++)
if(L->data[i]<x)
{
y=L->data[i];
for(j=i-1;j>=0;j--)//前面所有元素向后移动
L->data[j+1]=L->data[j];
L->data[0]=y;
}
}
例2.2 有顺序表A和B,其元素均按从小到大的升序排列,编写一个算法将它们合并成一个顺序表C,要求C的元素也是从小到大的升序排列。
算法思路:
依次扫描通过A和B的元素,比较当前的元素的值,将较小值的元素赋给C,如此直到一个线性表扫描完毕。
然后,将未完的那个顺序表中余下部分赋给C即可。
C的容量要能够容纳A、B两个线性表相加的长度。
void merge(SeqList A,SeqList B,SeqList *C){
int i,j,k;//i,j,k分别为顺序表A、B、C当前元素指针
i=0;
j=0;//i,j分别指向顺序表A、B当前待处理元素
k=0;//k指向顺序表C待插入元素位置
while(i<=A.last&&j<=B.last)//依次扫描比较顺序表A、B中的数据元素
{
if(A.data[i]<B.data[j])
{
C->data[k]=A->data[i];
k++;
i++;
}
else C->data[k++]=B->data[j++];
while(i<=A->last)//将A中剩余元素复制到表C
C->data[k++]=A->data[i++];
while(j<=B->last)//将B中剩余元素复制到表C
C->data[k++]=B->data[j++];
C->last=k-1;
}
}
算法的时间性能是O(m+n),其中m是A的表长,n是B的表长。
例2.3 比较两个线性表的大小。两个线性表的比较依据下列方法:设A、B是两个线性表,均用向量表示,表长分别为m和n。 A’和B’分别为 A 和 B 中除去最大共同前缀后的子表。
例如A=(x,y,y,z,x,z), B=(x,y,y,z,y,x,x,z),两表最大共同前缀为 (x,y,y,z) 。则A’=(x,z),B’=(y,x,x,z),若A’= B’= 空表,则A=B;若A’=空表且B’≠空表,或两者均不空且A’首元素小于B’首元素,则A<B;否则,A>B。
算法思路:
①找出A、B的最大共同前缀;
②求出A’和B’;
③按规则比较进行比较A’和B’ 。
int compare(int A[],int B[],int m, int n){
int i,j,AS[],BS[],ms,ns;
i=0;
while(i<=m&&i<=n&&A[i]==B[i])
i++;//找到最大共同前缀
ms=ns=0;
for(j=i;j<m;j++)
{
AS[j-i]=A[j];
ms++;
}
for(j=i;j<n;j++)
{
BS[j-i]=B[j];
ns++;
}
if(ms==ns&&ms==0) return 0;
else if(ms==0&&ns>0||ms>0&&ns>0&&A[i]<B[i]) return -1;
else return 1;
}
算法的时间性能是O( m+n )。
2.3 线性表的链式存储和运算实现
线性表的链式存储,即用一段不连续的存储空间存储数据。
2.3.1单链表
单链表作为线性表的一种存储结构,对每个数据元素ai,除了存放数据元素的自身的信息 ai 之外,还需要和ai一起存放其后继 ai+1所在的存贮单元的地址,这两部分信息组成一个“结点”。
在实际应用中对每个结点的实际地址并不关心,关心更多的是结点之间的逻辑结构。所以,通常的单链表用下图的形式表示。
结点定义如下:
typedef struct node
{ datatype data;
struct node *next;
} LNode,*LinkList;
单链表结点结构
存放数据元素信息的称为数据域,存放其后继地址的称为指针域。
LinkList H;//定义头指针变量
单链表的形态
2.3.2单链表上基本运算的实现
⒈建立单链表:
① 在不带头结点的链表的尾部插入结点建立单链表
建立单链表的算法一
#define Flag;//Flag为根据实际情况设定的结束数据输入的标志数据
LinkList Create_LinkList1(){
LinkList L;
LNode *s,*r;
int x; //设数据元素的类型为int
L=r=NULL;
cin>>x;
while(x!=FLag)//Flag表示输入结束
{
s=new LNode;
s->data=x;
if(L==NULL)
L=s;//第一个结点的处理
else r->next=s;//其他结点的处理
r=s;//r指向新的尾结点
cin>>x;
}
if(r!=NULL) r->next=NULL;//对于非空表,最后结点的指针域置为空
return L;
}
②在带头结点的链表的尾部插入结点建立单链表
#define Flag;
LinkList Creat_LinkList1(){
LinkList L;
LNode *s,*r;
int x; //设数据元素的类型为int
L=r=NULL;
cin>>x;
while(x!=Flag)//Flag表示输入结束
{
s=new LNode;
s->data=x;
r->next=s;//插入新结点
r=s;
cin>>x;
}
if(r!=NULL) r->next=NULL;//对于非空表,最后结点的指针域置为空
return L;
}
③在带头结点的链表的头部插入结点建立单链表
#define Flag;
LinkList Creat_LinkList1(){
LinkList L;
LNode *s;
int x;
L=NULL;
cin>>x;
while(x!=Flag)
{
s=new LNode;
s->data=x;
s->next=L;
L->next=s;//若是不带头结点,此句改为L=s
}
}
2.求表长:
①设L是带头结点的单链表(线性表的长度不包括头结点)。
带头结点的单链表求表长算法
int Length_LinkList1(LinkList L){
LNode *p;
int i;
p=L;//p指向头结点
i=0;
while(p->next)
{
p=p->next;
i++;
}//p指向第i个结点
return i;
}
②设L是不带头结点的单链表。
int Length_LinkList2(LinkList L){
LNode *p;
int i;
p=L;
i=0;
while(p)
{
i++;
p=p->next;
}//p指向第i+1个结点
rerturn i;
}
3.查找操作:
①按值查找,即定位 Locate_LinkList(L,x)
算法思路:从链表的第一个元素结点起,判断当前结点值是否等于x,若是,返回该结点的指针,否则继续后一个,表结束为止。找不到时返回空指针。
LNode *Locate_LinkList(LinkList L,DataType x){
//在单链表L中查找值为x的结点,找到后返回其指针,否则返回空
LNode *p;
p=L->next;//p指向第1个数据元素结点
while(p!=NULL&&p->data!=x)
p=p->next;
return p;
}
②按序号查找 Get_Linklist(L,i)
算法思路:从链表的第一个元素结点起,判断当前结点是否是第i个,若是,则返回该结点的指针,否则继续后一个,表结束为止。没有第i个结点时返回空指针。
LNode *Get_LinkList(LnkList L,Int i){
LNode *p;
int j;
p=L->next;//p指向第1个数据元素结点
j=1;
while(p!=NULL&&j<i)
{
p=p->next;
j++;
}//p指向第j个数据元素结点
if(j==i) return p;
else return NULL;
}
4.插入操作:
⑴后插结点:设p指向单链表中某结点,s指向待插入的值为x的新结点,将s插入到p的后面。操作如下:
①s->next=p->next;
②p->next=s;
注意:两个指针的操作顺序不能交换。
⑵前插结点:设p指向链表中某结点,s指向待插入的值为x的新结点,将s插入到p的前面。与后插不同的是:首先要找到p的前驱q,然后再完成在q之后插入s,设单链表头指针为L,操作如下:
q=L;
while (q->next!=p)
q=q->next;
s->next=q->next;
q->next=s;
插入运算 Insert_LinkList(L,i,x)
算法思路:
①找到第i-1个结点;若存在继续2,否则结束。
②申请、填装新结点。
③将新结点插入,结束。
int Insert_LinkList(LinkList L,int i,DataType x){
//在单链表L的第i个位置上插入值为x的元素
LNode *p,*s;
p=Get_LinkList(L,i-1);//查找第i-1个结点
if(p==NULL)
{
cout<<"参数错"<<endl;
return 0;
}//第i-1个不存在,不能进行插入操作
else
{
s=(LNode*)malloc(sizeof(LNode));//申请、填装结点
s->data=x;
s->next=p->next;//新节点插入在第i-1个结点的后面
p->next=s;
return 1;
}
}
5.删除操作:
设p指向单链表中某结点,删除*p。
要实现对结点*p的删除,首先要找到 *p的前驱结点 *q,然后完成指针的操作即可。指针的操作由下列语句实现:
q->next=p->next;
free§;
显然,找*p前驱的时间复杂性为O(n)。
若要删除*p的后继结点(假设存在),则可以直接完成。
s=p->next;
p->next=s->next;
free(s);
该操作的时间复杂性为O(1)。
删除运算:Del_LinkList(L,i)
算法思路:
①找到第i-1个结点;若存在继续2,否则结束。
②若存在第i个结点则继续3,否则结束。
③删除第i个结点,结束。
int Del_LinkList(LinkList,int i){
//删除单链表L上的第i个节点
LinkList p,s;
p=Get_LinkList(L,i-1);//查找第i个结点
if(p==NULL)
{
cout<<"第i-1个结点不存在"<<endl;
return -1;
}
else if(p->next=NULL)
{
cout<<"第i个结点不存在"<<endl;
return 0;
}
else
{
s=p->next;//s指向第i个结点
p->next=s->next;//从链表中删除
free(S);//释放*s
return 1;
}
}
上述算法的时间复杂度为O(n).
2.3.3循环链表
对于单链表而言,最后一个结点的指针域是空指针。如果将该链表头指针置入该指针域,则使得链表头尾结点相连,就构成了单循环链表。
需要说明的是:
①单循环链表上的操作基本上与单链表相同,只是将原来判断指针是否为NULL变为是否是头指针而已;
②对于单循环链表则可以从表中任意结点开始遍历整个链表;
③考虑到对链表常做的操作是在表尾、表头进行,此时可以用一个指向尾结点的指针R来标识整个链表。
例如,对两个单循环链表H1 、H2的连接操作,是将H2的第一个数据结点接到H1的尾结点,如用头指针标识,则需要找到第一个链表的尾结点,其时间复杂性为O(n),而链表若用尾指针R、R2来标识,则时间性能为O(1)。
操作如下:
P= R–>next; /保存第一个表的头结点指针/
R->next=R2->next->next; /头尾连接/
free(R2->next); /释放第二个表的头结点/
R2->next=P; /组成循环链表/
2.3.4双向链表
问题:单链表的结点中只有一个指向其后继结点的指针域next,找后继的时间性能是O(1),找前驱的时间性能是O(n);如何提高找前驱的性能?
办法:可以付出空间的代价使得找前驱的时间性达到O(1),即每个结点再加一个指向前驱的指针域。用这种结点组成的链表称为双向链表。
双向链表结点的定义该如何调整?
typedef struct dlnode
{ datatype data;
struct dlnode *prior,*next;
}DLNode,*DLinkList;
需要说明的是:
①双向链表通常也是用头指针标识,也可以带头结点和做成循环结构。
②通过某结点的指针p即可以直接得到它的后继结点的指针p->next,也可以直接得到它的前驱结点的的指针p->prior。 ③ p->prior->next == p== p->next->prior
在双向链表中插入一个结点:
设p指向双向链表中某结点,s指向待插入的值为x的新结点,将*s插入到 *p的前面。
操作如下:
①s->prior=p->prior;
②p->prior->next=s;
③s->next=p;
④p->prior=s;
思考:上面指针操作的顺序可以改变吗?
解答:指针操作的顺序不唯一,但也不是任意的。操作①必须要放到操作④的前面完成,否则*p的前驱结点的指针就丢掉了。
在双向链表中删除指定结点:
设p指向双向链表中某结点,删除*p。操作如下:
①p->prior->next=p->next;
②p->next->prior=p->prior;
③free§;
2.3.5静态链表
暂省
2.3.6单链表应用举例
暂省
2.4 顺序表和链表的比较
本章介绍了线性表的逻辑结构及它的两种存储结构:顺序表和链表。
两种线性存储结构的特点
顺序存储有三个优点:
(1) 方法简单,各种高级语言中都有数组,容易实现。
(2) 不用为表示结点间的逻辑关系而增加额外的存储开销。
(3) 顺序表具有按元素序号随机访问的特点。
顺序存储也有两个缺点:
⑴ 在顺序表中做插入删除操作时,平均移动大约表中一半的元素,因此对n较大的顺序表效率低。
⑵ 需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
链表的优缺点恰好与顺序表相反。
怎么选择存储结构?
在实际中怎样选取存储结构呢?通常有以下几点考虑:
⒈ 基于存储的考虑
对线性表的长度或存储规模难以估计时,不宜采用顺序表;链表不用事先估计存储规模,但链表的存储密度较低。
⒉ 基于运算的考虑
如果经常做的运算是按序号访问数据元素,顺序表优于链表; 在顺序表中做插入、删除操作时平均移动表中一半的元素,在链表中作插入、删除操作,虽然也要找插入位置,但操作主要是比较操作,从这个角度考虑后者优于前者。
⒊ 基于环境的考虑
顺序表容易实现,任何高级语言中都有数组类型,链表的操作是基于指针的,相对来讲前者简单些,也是用户考虑的一个因素。