什么是线性表?从他的名字我们很容易想到的可能是类似于ASCII表一样的,方方正正有行有有列的表格结构,并非如此,书上有一条启示说,线性表是零个或多个数据元素的有限序列,诶?是不是和数组有些像,那他又与数组有什么区别呢?
所谓线性表,其实关键在于线性,线性即一一对应的一种关系,线性表的每一个元素(除首元素与尾元素)都会对应存在一个前驱与后继,首元素是先驱者,没有人领导他,而尾元素作为最后一个后继者,后方自然也再无后记。可以简单理解为一个队伍,中间的人前后都会有一个人,而队首前无人,队尾后无人。
生活中哪些是线性表?
比如我们常的说十二生肖。
鼠-牛-虎-兔-龙-蛇-马-羊-猴-鸡-狗-猪
通常以鼠开头,以猪结尾,其内的其余生肖都对应有前驱与后继。
而以家庭为例,每个人都会有父母以及子女,但这就不是一种线性表,因为一个人对应了不止一个。
对于一个队列来说,插入和删除是必不可少的操作,我们在排队时经常会遇到人因为各种情况插队(很讨厌),其后面的人都需要依次向后退一位,而如果有人因为有事离开了队伍,后面的人会依次向前一位。现在用抽象数据类型来定义一下线性表吧。
如下:
ADT 线性表(list)
Data
线性表的数据对象类型集合为(a1,a1,...,an),每个元素的类型均为DataType。其中,除第一个元素a1外,
每个元素有且只有一个直接前驱元素,出最后一个元素an外,每个元素有且只有一个直接后继元素。
数据元素之间都是一对一的关系。
Operation
InitList(*L): 初始化操作,建立一个空的线性表L。
ListEmpty(l): 若线性表为空,返回true,否则返回false。
CLearList(*L): 将线性表清空。
GetElem(L,i,*e): 将线性表L中的第i个元素值返回给e。
LocateELem(L,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的位置
表示成功,否则,返回0表示失败。
ListInsert(*L,i,e): 在线性表L中的第i个位置插入新元素e。
ListDelete(*L,i,*e): 删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L): 返回线性表L的元素个数。
endADT
线性表有两种物理存储结构,一是顺序存储结构,二是链式存储结构。
首先先来了解一下顺序存储结构及操作。
线性表的顺序存储结构指的是用一段地址连续的存储单元依次存储线性表的数据元素
即将线性表(a1,a2,…,an)存储在如下结构中
a1 | a2 | … | an |
---|
在c语言中,通常用一维数组实现顺序存储结构,即把第一个元素存到数组下标为0的位置,表示从这开始,接着把相邻元素依次存储在数组中的相邻位置。
结构代码如下:
#define MAXSIZE 20 //存储空间初始分配量
typedef int ElemType; //ElemType类型视情况而定这里设为int
typedef struct
{
ElemType data[MAXSIZE]; //数组存储数据元素
int length; //线性表当前长度
}SqList;
这里,我们发现顺序结构需要的三个属性:
那么数据长度和线性表长度有什么区别呢?
数据长度即数组长度,而数组大小在分配内存后一般是不会改变的。而线性表的长度会随着线性表中的元素插入和删除试试变化,且我们因注意保持让线性表的长度小于数组长度。
获得元素操作
对于线性表的顺序存储结构来说,要实现GetElem操作,将L中的第i个位置元素返回,是非常简单的,只要i在数组下标范围内,将数组的i-1个元素返回即可。
代码如下:
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status
//Status是函数类型,返回函数结果状态代码,如OK
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(SqList L,int i,Elemtype *e)
{
if(L.length==0 || i<1 || i>l.Length)
return ERROR;
*e=L.data[i-1];
return OK;
}
插入操作
根据上面排队插队提到的,插入算法的思路为:
代码如下:
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if(L->length==MAXSIZE)
return ERROR;
if(i<1 || i>L->length)
return ERROR;
if(i<=L->length)
{
for(k=l->length-1;k>=i-1;k--)
L->data[k+1]=L->[k];
}
L->data[i-1]=e;
L->length++;
return OK;
}
删除操作
删除算法思路:
代码如下:
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
if(L->length==MAXSIZE)
return ERROR;
if(i<1 || i>L->length)
return ERROR;
*e=L->data[i-1];
if(ilength)
{
for(k=i;klength;k++)
L->data[k-1]=L->data[k];
}
L->length--;
return OK;
}
顺序线性表的优缺点:
优点:
·不用为元素之间的逻辑额外分配存储空间;
·可以快速地存取表中任意位置的元素
缺点:
·插入和删除需要移动大量元素
·当线性表长度变化较大时,难以确定存储空间的容量
·造成存储空间的“碎片”
为了解决顺序线性表中存在的问题,这里引入线性表的另一种另一种物理存储结构——链式存储结构。
线性表链式存储结构定义
用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以不连续,这就意味着,这些数据元素可以存在未被占用的任意位置,对于顺序结构来说,数据元素只需要存储数据元素信息即可,而对于链式结构来说,每个数据元素不仅要存储元素信息,还需要存储它后继元素的位置信息。
因此,为了表示每个数据元素ai与其后继数据元素ai+1的逻辑关系,对于每个数据元素ai来说,出来存储本身信息,还需要存储一个指示其后继元素的信息。把存储数据元素信息的域称为数据域,存储后继元素位置的域称为指针域。
… | ai | 0500 | … | ai+1 | 0200 | … |
---|
ai和ai+1为数据域,0500,0200为指针域且ai+1的地址为0500。
对于线性表来说,总得有头有尾,链表也不例外。我们把链表的第一个节点的存储位置叫做头指针,使得整个链表的存取从头指针开始,之后的每一个结点就是上一个结点的指针指向的位置,而最后一个结点的无后继元素,因此,它指向空(通常用NULL表示或“^”符号表示)。
有时为了更方便的操作,会在单链表的第一结点前设一个头结点,它的数据域可以不存储任何信息,也可以存储线性表长度等附加信息,头结点的指针域会指向第一个结点的位置。
头结点与头指针的区别
头结点:
头指针:
单链表的读取:在线性表中,元素的存储位置很容易读取,但在链表中,第i个元素的位置不确定,必须从头开始找,因此要实现单链表的读取要相对麻烦一些。
基本思路:
代码如下:
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p;
p = L->next;
j = 1;
while(p&&jnext;
++j;
}
if(!p || j>i)
return ERROR;
*e=p->data;
return OK;
}
就是从头开始找知道第i个元素。
单链表的插入与删除:
单链表的插入:
假设存储元素e的结点为s,实现将s插入p与p->next间,只需要将结点s的指针域指向p->next,之后让p再指向s,如此便可将p到p->next的逻辑变成p到s再到p->next,即那么思考一下,能否先让p指向s,再让s指向p->next呢?
首先,答案肯定时不可以,那么为什么不行,在没有其余结点的情况下,要想访问到p->next,只能通过p来找到它,而如果先让p指向s就会导致原来指针域中指向p->next的信息被覆盖,从而导致p->next不会再被s找到,而若p->next后面还有很多信息,这些信息也会随它一起丢失,这样的结果时非常可怕的,因此一定要先让s先指向p->next。
基本思路:
代码如下:
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p=*L;
j=1;
while(p && jnext;
++j;
}
if(!p || j > i)
return ERROR;
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
malloc函数的作用就是生成一个新的结点大小与Node一样。
单链表的删除:
相比于插入,删除就比较容易操作了,如 p - p->next - p->next->next这样一个结构,如果我们想要把p->next删除,只需要将结点p指向结点p->next->next就可以了,但是我们要先用一个结点q将要删除的p->next赋值给q然后再让p->next=q->next即可。
基本思路:
代码如下:
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p=*L;
j=1;
while(p->next && jnext;
j++;
}
if(!(p->next) || j>i)
return ERROR;
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
return OK;
}
单链表的整表创建
基本思路:
代码如下:
//头插法
void CreatLIstHead(LinkList *L,int n)
{
LinkList p;
int i;
srand(time(0));//这里用随机数存在链表的数据域
*L=(LinkList)malloc(sizeof(Node));
(*L)->next=NULL;
for(i=0;inext=(*L)->next;
(*L)->next=p;
}
}
//尾插法
void CreatLIstHead(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));//这里用随机数存在链表的数据域
*L=(LinkList)malloc(sizeof(Node));
r=*L;
for(i=0;idata=rand()%100+1;
r->next=p;
r=p;
}
r->next=NULL;
}
单链表的整表删除:
基本思路:
代码如下:
Status ClearList(LinkList *L)
{
LinkList p,q;
p=(*L)->next;
while(p)
{
q=p->next;
free(p);
p=q;
}
(*L)->next=NULL;
return OK;
}
单链表与顺序存储结构优缺点:
存储分配方式:
·顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
·单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能:
·查找:顺序存储结构O(1),单链表O(n)
·插入与删除:顺序存储结构需要平均移动表长一般的元素O(n);而单链表找到后插入与删除仅为O(1)
空间性能:
·顺序存储结构需要预分配存储空间,分大浪费,分小易溢出
·单链表不需要预分配空间,不存在上述问题
若线性表频繁查找,很少进行插入和删除操作时,宜用顺序存储结构。频繁使用插入删除时,宜用单链表。
静态链表:
有些语言不具有指针变量,那么如何才能实现单链表的操作呢?这就不得不说到静态链表。那么,静态链表如何实现呢?
首先让数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标对应一个data和cur。数据域data,用来存放数据元素,而游标cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,这种用数组描述的链表还有一种起名叫做,游标实现法。
代码如下:
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur;
}Component,StaticLinkList[MAXSIZE];
数组的第一个和最后一个元素作为特殊元素处理,不存数据。通常称未被使用的数组元素为备用链表。而第一个元素,即下标为0的意思cur就存放在备用链表的第一个结点的下标,相当于单链表中的头结点作用,链表为空时即为0的平方。
代码如下:
//space[0].cur为头指针,“0”表示空指针
Status InitList(StaticLinkList space)
{
int i;
for(i=0;i
静态链表的插入:
动态链表中结点的申请与释放分别借用malloc()和free()实现。在静态链表中,操作的是数组,不存在结点的申请和释放问题,所以需要自己实现这两个函数才能做到插入和删除。
为了辨明数组中哪些分量未被使用,解决的办法就是将未被使用过的及已被删除的分量用游标链成一个备用链表,每当插入时,便可以从备用链表上取第一个结点作为待插入的新结点。
代码如下:
int Malloc_SLL(StaticLinkList space)
{
int i=space[0].cur;//数组第一个元素的cur存的值就是要返回的第一个备用空闲的下标
if(space[0].cur)
space[0].cur=space[i].cur;//取出一个分量使用,它的下一个分量作为备用
return i;
}
Status ListInsert(StaticLinkList L,int i,ElemType e)
{
int j,k,l;
k=MAX_SIZE-1;//k首先为最后一个元素下标
if(iListLength(L)+l)
return ERROR;
j=Malloc_SSL(L);//获得空闲分量下标
if(j)
{
L[j].data=e;
for(l=l;l<=i-1;l++)//找到i之前的位置
k=L[k].cur;
L[j].cur=L[k].cur;//把i之前的cur赋值给新元素的cur
L[k].cur=j;//把新元素的下标赋值给i之前元素的cur
return OK;
}
return ERROR;
}
静态链表的删除:
//把要删除的元素cur回收到备用链表
void Free_SSL(StaticLingList space,int k)
{
space[k].cur=space[0].cur;//把第一个元素cur值赋值给要删除的分量cur
space[0].cur=k;//把要删除的分量下标赋值给第一个元素的cur
}
Status ListDelete(StaticLinkList L,int i)
{
int j,k;
if(iListLength(L))
return ERROR;
k=MAX_SIZE-1;
for(j=l;j<=i-1;++j)
k=l[k].cur;
L[k].cur=L[j].cur;
Free_SSL(L,j);
return OK;
}
静态链表的优缺点:
总的来说,静态链表时为了给有些没有指针的高级语言实现单链表能力的方法。
循环链表