1.定义
线性表(List):零个或多个数据元素的有限序列。
首先它是一个序列。也就是说,元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
2.线性表的抽象数据类型
线性表作为一种数据结构我们在掌握了它的定义之后,接下来需要知道对这样的一种数据结构,我们可以提供哪些操作来将它应用于编程中,来实现各种各样的需求。
1.InitList(*L):初始化操作,建立一个空的线性表L。
2.ListEmpty(L):若线性表为空,返回true,否则返回false。
3.ClearList(*L):将线性表清空。
4.GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。
5.LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号表示成功;否则,返回0表示失败。
6.ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e。
7.ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值。
8.ListLength(L):返回线性表L中的元素个数。
每一种操作的具体实现代码就不一一赘述了,根据具体情况具体分析。
3.线性表的存储结构
线性表的存储结构分为两种,一种是顺序存储,另一种是链式存储。顺序存储就是,线性表内存储的元素在内存中的地址是连续的。链式存储就是,线性表内存储的元素在内存中的地址是不连续的。
(1)顺序存储结构
线性表的顺序存储结构,指的是用一段内存连续的存储单元一次存储线性表的数据元素。
因为线性表的每个数据元素的类型都相同,所以可以用C语言(其它语言也相同)中的一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为0的位置,接着把线性表相邻的元素存储在数组中相邻的位置。
下面是线性表顺序存储结构的实现代码
#define MAXSIZE 20 //存储空间初始分配量
typedef int ElemType; //ElemType类型根据实际情况而定,这里假设为int
typedef struct{
Elemtype data[MAXSIZE]; //数组存储数据元素,最大值为MAXSIZE
int length; //线性表当前长度
}SqList;
获得元素操作
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 20
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType;
typedef int Status;
Status GetElem(SqList L, int i,int *e) //将线性表L中的第i个位置元素值返回给e
{
if(i > L.length || i < 1 || L.length == 0)
return ERROR;
*e = L.data[i-1];
return OK;
}
插入操作
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status ListInsert(SqList *L, int i, ElemType e) //在线性表L中的第i个位置插入新元素e
{
int k;
if(L->length == MAXSIZE)
return ERROR;
if(i<1 || i>L->length+1)
return ERROR;
if(i<=L->length){
for(k=L->length-1; k>=i-1; k--){
L->data[k+1] = L->data[k];
}
L->data[i-1] = e;
L->length++;
return OK;
}
}
删除操作
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
Status ListDelete(SqList *L, int i, ElemType *e)
{
int k;
if(L->length == 0) //线性表为空
return ERROR;
if(i<1 || i>L->length) //删除位置不正确
return ERROR;
*e = L->data[i-1];
if(i<L->length){ //如果删除不是最后位置
for(k=i; k<L->length; k++) //将删除位置元素前移
L->data[k-1] = L->data[k];
}
L->length--;
return OK;
}
下面我们来总结一下顺序存储结构的优缺点
优点:①无需为表示表中元素之间的逻辑关系而增加额外的存储空间
②可以快速地存取表中任一位置的元素
缺点:①插入和删除操作需要移动大量元素
②当线性表长度变化较大时,难以确定存储空间的容量
③造成存储空间的“碎片”
(2)链式存储结构
之前我们讲的线性表的顺序存储结构是有缺点的,最大的缺点就是插入和删除时需要移动大量元素。为什么会出现这个问题呢?仔细分析后,发现原因就在于相邻两元素的存储位置也具有邻居关系,它们的编号是1,2,3,…,n,它们在内存中的位置也是挨着的,中间没有空隙,当然无法快速介入,而删除后,当中就会留出空隙,自然需要弥补。
为了解决这个问题,使线性表能更方便的实现插入和删除操作,我们引入了链式存储结构的概念。
在链式存储结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
因此,为了表示每个数据元素ai与其直接后继元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个只是其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域成为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,a3,……,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
区分几个概念:
头指针:我们把链表中第一个结点的存储位置叫做头指针。
头结点:为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。
下面是线性表链式存储结构的实现代码
//线性表的链式存储结构
typedef struct Node{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
读取操作
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; //声明一指针p
p = L->next; //让p指向链表L的第一个结点
j = 1; //j为计数器
while(p && j < i){ //p不为空且计数器j还没有等于i时,循环继续
p = p->next; //让p指向下一个结点
++j;
}
if(!p || j > i)
return ERROR; //第i个结点不存在
*e = p->data; //取第i个结点的数据
return OK;
}
插入操作
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while(p && j < i){ //寻找第i-1个结点
p = p->next;
++j;
}
if(!p || j > i)
return ERROR; //第i个结点不存在
s = (LinkList)malloc(sizeof(Node)); //生成新结点(C标准函数)
s->data = e;
s->next = p->next; //将p的后继结点赋值给s的后继
p->next = s; //将s赋值给p的后继
return OK;
}
删除操作
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:删除L的第i个结点,并用e返回其值,L的长度减1
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while(p->next && j < i){ //遍历寻找第i-1个结点
p = p->next;
++j;
}
if(!(p->next) || j > i)
return ERROR; //第i个结点不存在
q = p->next;
p->next = q->next; //将q的后继赋值给p的后继
*e = q->data; //将q结点中的数据给e
free(p); //让系统回收此结点,释放内存
return OK;
}
单链表的整表创建
1.头插法:采用插队的方法,始终让新结点在第一的位置,这种算法称为头插法。
//随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)
void CreatListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); //初始化随机数种子
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; //先建立一个带头结点的单链表
for(int i=0; i<n; i++){
p = (LinkList)malloc(sizeof(Node)); //生成新结点
p->data = rand()%100 + 1; //随机生成100以内的数字
p->next = (*L)->next; //头插法单链表创建的关键两行代码
(*L)->next = p; //插入到表头
}
}
2.尾插法:把每次新结点都插在终端结点的后面,这种算法称为尾插法
//随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)
void CreateListTail(LinkList *L, int n)
{
LinkList p, r;
int i;
srand(time(0)); //初始化随机数种子
*L = (LinkList)malloc(sizeof(Node)); //为整个线性表
r = *L; //r为指向尾部的结点
for(i=0; i<n; i++){
p = (Node *)malloc(sizeof(Node)); //生成新结点
p->data = rand()%100 + 1; //随机生成100以内的数字
r->next = p; //将表尾终端结点的指针指向新结点
r = p; //将当前的新结点定义为表尾终端结点
}
r->next = NULL; //表示当前链表结束
}
单链表的整表删除
//初始条件:顺序线性表L已存在,操作结果:将L重置为空表
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next; //p指向第一个结点
while(p){ //没到表尾
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; //头结点指针域为空
return OK;
}
4.静态链表
对于一些如Basic、Fortran等早期的编程高级语言,并没有指针的概念,链表结构按照以前的讲法,它就没法实现了。怎么办呢?
有人就想出来用数组代替指针,用来描述单链表。
首先我们让数组的元素都是有两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个data和一个cur。数据域data,用来存放数据元素,也就是通常我们要处理的数据;而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把cur叫做游标。
我们把这种用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法。
下面是静态链表的实现代码
#define MAXSIZE 1000 //假设链表的最大长度时1000
typedef struct{
ElemType data;
int cur; //游标(Cursor),为0时表示无指向
}Component, StaticLinkList[MAXSIZE];
另外我们对数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用。
//将一维数组space中各分量链成一备用链表
//space[0].cur为头指针,"0"表示空指针
Status initList(StaticLinkList space)
{
int i;
for(i=0; i<MAXSIZE-1; i++)
space[i].cur = i + 1;
space[MAXSIZE-1].cur = 0; //目前静态链表为空,最后一个元素的cur为0
return OK;
}
插入操作
静态链表中要解决的时:如何用静态模拟动态结构的存储空间的分配,需要时申请,无用时释放。
我们前面说过,在动态链表中,结点的申请和释放分别借用malloc()和free()两个函数来实现。在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以我们需要自己实现这两个函数,才可以做插入和删除的操作。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
//若备用空间链表为非空,则返回分配的结点下标,否则返回0
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(StatusLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAX_SIZE - 1; //注意k首先是最后一个元素的下标
if(i<1 || i>ListLength(L)+1)
return ERROR;
j = Malloc_SLL(L); //获得空闲分量的下标
if(j){
L[j].data = e; //将数据赋值给此分量的data
for(l=1; l<=i-1; l++){ //找到第i个元素之前的位置
k = L[k].cur;
}
L[j].cur = L[k].cur; //把第i个元素之前的cur赋值给新元素
L[k].cur = j; //这两行代码是插队操作实现的关键
return OK;
}
return ERROR;
}
删除操作
和前面一样,删除元素时,原来是需要释放结点的函数free()。现在我们也得自己实现它:
//删除在L中第i个数据元素e
Status ListDelete(StaticLinkList L, int i){
int j, k;
if(i<1 || i>ListLength(L))
return ERROR;
k = MAX_SIZE - 1;
for(j=1; j<=i-1; j++)
k = L[k].cur;
j = L[k].cur;
Free_SSL(L, j);
return OK;
}
void Free_SSL(StaticLinkList space, int k){
space[k].cur = space[0].cur; //把第一个元素cur值赋给要删除的分量cur
space[0].cur = k; //把要删除的分量下标赋值给第一个元素的cur
}
返回静态链表中元素个数
//初始条件:静态链表L已存在
//操作结果:返回L中数据元素个数
int ListLength(StaticLinkList L){
int j = 0;
int i = L[MAXSIZE-1].cur;
while(i){
i = L.[i].cur;
j++;
}
return j;
}
需要注意的是:
1.第一个节点不存放数据data,并且游标cur指向第一个不存放数据的元素的下标。
2.最后一个节点不存放数据data,并且游标cur指向第一个存放数据的元素的下标。
3.最后一个元素的游标必须为0
优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
缺点:①没有解决连续存储分配带来的表长难以确定的问题
②失去了顺序存储结构随机存取的特性
5.循环链表
将单链表中终端结点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linkedlist)。
循环链表解决了一个很麻烦的问题,如何从当中一个结点出发,访问到链表的全部结点。
其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。
在单链表中,我们有了头结点时,我们可以用O[1]的时间访问第一个结点,但对于要访问到最后一个结点,却需要O[n]的时间,因为我们需要将单链表全部扫描一遍。
如果我们改造一下循环链表,不用头指针,而是用指向终端结点的尾指针来表示循环链表,此时查找开始结点和终端结点都很方便了,便可以用O[1]的时间由链表指针访问到最后一个结点。
此时终端结点用尾指针rear指示,rear->next->next就是开始结点。
6.双向链表
双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以再双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
下面是双向链表的实现代码
typedef struct DulNode{
ElemType data;
struct DulNode *prior; //直接前驱指针
struct DulNode *next; //直接后继指针
}DulNode, *DuLinkList;
插入操作
s->prior = p; //把p赋值给s的前驱
s->next = p->next; //把p -> next 赋值给s的后继
p->next->prior = s; //把s赋值给p -> next 的前驱
p->next = s; //把s赋值给p的后继
删除操作
p->prior->next = p->next; //把p->next赋值给p->prior的后继
p->next->prior = p->prior; //把p->prior赋值给p->next的前驱
free(p);