定义
零个或者多个数据元素的有限序列。是数据结构中最常用最简单的一种结构。元素是有顺序的,并且是有限个,若有多个元素,第一个元素无前驱,最后一个无后继,其他每个元素都有且只有一个前驱和后继。
线性表的抽象数据类型
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,…an},每个元素的类型均为DataType。其中除了第一个元素外,每个元素有且只有一个直接前驱元素,除了最后一个元素外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是**一对一的关系。**对于不同的应用,线性表的基本操作有不同的,以下是最基本的操作:
Operation
- InitList(*L) 初始化,建立一个空的线性表L
- ListEmpty(L) 若线性表为空,返回true,否则false
- ClearList(*L) 将线性表清空
- GetElem(L,i,*e) 将线性表L中的第i个为止元素值返回给e
- LocateElem(*L,i,e) 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号,否则返回0
- ListInsert(*L,i, *e) 在线性表L中的第i个位置插入新元素e
- ListDelete(*L,i , *e) 删除线性表L中的第i个位置元素,并用e返回其值
- ListLength(L) 返回线性表L的元素个数
endADT
线性表的顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。在内存中,通过占位的形式,把一定的内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中。用C语言的一维数组来表示:
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length;
}SqList;
顺序存储结构的三个属性:
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
- 纤细你个表的最大存储容量:数组长度为MaxSize。
- 线性表的当前长度:length。
数组长度是存放线性表的存储空间的长度,存储分配后通常是不变的。
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作,这个量是变化的。但线性表的长度是不大于数组长度的。
插入与删除
- 获取元素
int GetElem(SqList L, int i, ElemType *e)
{
if (L.length == 0 || i < 1 || i > L.length)
// 1<=i<=L.length
return 0;
*e = L.data[i - 1];
return 1; //获取成功
}
- 插入操作
在线性表的第i个位置插入元素e,思路是:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,抛出异常或者动态增加容量;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们后移一个位置;
- 将新元素填入位置i处;
- 表长加1。
//假设线性表L存在
int ListInsert(SqList *L, int i, ElemType e)
{
int k;
if(L->length == MAXSIZE)
return 0;
if(i<1 || i>L->length+1)
return 0;
if(i<=L->length)//i位置是否在表尾
{
for(k = L->length-1;k>=i-1;k--)
{
L->data[k+1] = L->data[k];//i位置及以后的元素后移一个位置
}
}
L->data[i-1] = e; //插入新元素
L->length++;
return 1;
}
- 删除操作
删除线性表的第i个位置的元素,思路:
- 删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将他们向前移动一个位置;
- 表长减1。
int ListDelete(SqList *L, int i, ElemType *e)
{
int k;
if(L->length == 0)
return 0;
if(i<1 || i>L->length)
return 0;
*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 1;
}
在插入和删除元素时候,最坏情况就是删除或者插入第一个位置,后续都要移动,时间复杂度为O(n)。
通过位置i,可以直接计算出它在线性表的位置,所以对于储存和读取,时间复杂度是O(1)。
通过对比可知,线性表适合元素个数变化不太大,更多的是存取数据的应用。
优缺点
- 优点:无须为表示表中元素之间的逻辑关系而增加额外的存储空间;可以快速地存取表中任何一个位置的元素。
- 缺点:插入和删除操作需要移动大量元素;当线性表长度变化较大时,难以确定存储空间的容量;造成存储空间的“碎片”。
线性表的链式存储结构
n个结点链接成一个链表,即为线性表的链式存储结构,每个结点中包含数据域和指针域,指指针域存储直接后继的存储位置。链表的每个结点中只包含一个指针域成为单链表。
链表中的第一个结点的存储位置叫做头指针,整个链表的存取就必须从头指针开始进行了,最后一个结点没有后继,指针为空。
有时为了更方便对链表进行操作,会在单链表的第一个结点前附设一个结点,为头结点。头结点的数据域可以不存储任何信息,也可以存储如表长等附加信息。
头指针和头结点
- 头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
- 头指针具有标识作用,所以常用头指针冠以链表的名字。
- 无论链表是否为空,头指针不为空,头指针是链表的必要元素。
- 头结点:
- 头结点是为了操作的统一和方便设立的,放在第一个元素的结点之前,其数据域一般无意义(或者存放链表的长度)。
- 有了头结点,对在第一个元素结点前插入结点和删除第一个结点,其操作与其它结点的操作就统一了。
- 头结点不一定是链表的必须要素。
代码描述
//线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
假设p是只想线性表第i个元素的指针,则该结点的数据域表示为p->data,其值是一个数据元素,结点的指针域用p->next来表示,其值是一个指针。
单链表的读取
思路:从开头开始遍历,直到第i个元素为止,最坏情况时间复杂度为O(n)。
- 声明一个结点p只想链表的第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,返回结点p的数据。
//线性表L存在
//获取第i个数据元素的值
int GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p;
p = L->next;//p指向L的第一个结点
j = 1;
while(p && j<i)
{
p = p->next;//p指向下一个结点
++j;
}
if (!p || j>i)
{
return 0;//第i个元素不存在
}
*e = p->data;
return 1;
}
单链表的插入与删除
插入
单链表第i个数据插入结点的算法思路:
- 声明一个结点p指向链表第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成空,在系统中生成一个空结点s;
- 将数据元素e赋值给s->data;
- 单链表的插入标准语句s->next = p ->next; p->next = s;
- 返回成功;
//线性表存在,在L中第i个位置之前插入新的元素e
int ListInsert(LinkList *L,int i, ElemType e)
{
int j;
LinKList p,s;
p = *L;
j = 1;
while(p && j<i)
{
p = p->next;
++j;
}
if(!P || j>i)
{
return 0;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return 1;
}
删除
核心语句:p->next = p->next->next;
用q代替p-next: q = p->next; p->next = q->next; free(q);
单链表的整表创建
对于一个链表来说,它所占空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。所以单链表创建过程就是一个动态生成链表的过程,从空表起,依次建立各个元素结点,并逐个插入链表。
算法如下:
//随机生成n个元素的值,建立带表头结点的单链线性表L(头插法)
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; //建立一个带头结点的单链表
for(i = 0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node));//新结点
p->data = rand()%100+1; //给新结点随机赋值
p->next = (*L)->next;
(*L)->next = p; //插入到表头
}
}
以上为头插法,尾插法即是将新元素依次插入到链表末尾,最后一个元素指针域设为NULL。
单链表的正标删除
算法:
- 声明一个结点p和q
- 将第一个结点赋值给p
- 循环:
- 将下一个结点赋值给q
- 释放p
- 将q赋值给p
代码:
int ClearList(LinkList *L)
{
LinkList p,q;
p = (*L)->next;
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return 1;
}
单链表结构与顺序存储结构的优缺点
- 存储分配方式:
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
- 时间性能:
- 查找:顺序存储结构O(1),单链表O(n)
- 插入删除:顺序存储结构需要平均移动表长一半的元素为O(n), 单链表在算出某位置的指针后,插入删除时间仅为O(1)。
- 空间性能:
- 顺序存储结构需要预先分配存储空间,分大了,浪费,小了容易发生上溢。
- 单链表不需要分配存储空间,只要有就可以分配。
因此,若频繁查找,可以采用顺序存储结构,若频繁删除和插入,可以采用单链表结构。
当元素个数未知或者变化较大时,可以采用单链表结构。
静态链表
让数组的元素都由两个数据域组成,data和cur,也就是说每个下标都对应一个data和一个cur。data存放数据元素,cur存放后继在数组中的下标。这种用数组描述的链表叫做静态链表。
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur;//若为0则是无指向
}Component,StaticLinkList[MAXSIZE];
通常数组的最后一个和第一个元素不存数据,把未被使用的数组元素成为备用链表,下标为0的元素的cur存放备用链表的第一个结点的下标,最后一个元素的cur存放第一个有数值的元素的下标
int InitList(StaticLinkList space)
{
int i;
for(i = 0; i < MAXSIZE-1; i++)
{
space[i].cur = i+1;//space[0].cur为头指针
}
space[MAXSIZE-1].cur = 0;//静态链表为空,最后一个元素的cur为0
return 1;
}
静态链表的插入操作
数组不存在像静态链表的结点申请和释放问题,所以需要自己实现两个函数。将所有未被使用过的及已经被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表取得第一个结点作为待插入新结点。
//若备用空间链表非空,则返回分配的结点下标,否则返回0
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur;//当前数组第一个元素的cur值,也就是第一个备用空闲的下标。
if(space[0].cur)
{
space[0].cur = space[i].cur;//拿走第一个结点,则下一个结点就是
//备用空闲的第一个结点
}
return i;
}
插入操作:
int ListInsert(StaticLinkList L; int i; ElemType e)
{
int j,k,l;
k = MAX_SIZE-1;
if(i<1 || i>ListLength(L) + 1)
{
return 0;
}
j = Malloc_SSL(L);//空闲的下标
if (j)
{
L[j].data = e;
for(l = 1; 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 1;
}
return 0;
}
静态链表的删除操作
同样,删除元素时,需要释放结点,需要自己实现。
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur;//把第一个元素cur值赋给要删除的分量
space[0].cur = k;//把要删除的分量下标赋值给第一个元素的cur
}
//删除L中第i个数据元素e
int ListDelete(StaticLinkList L, int i)
{
int j,k;
if(i<1||i>ListLength(L))
{
return 0;
}
k = MAX_SIZE-1;
for(j=1;j<=i-1;j++)
{
k = L[k].cur;
}
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L,j);
return 1;
}
优缺点
- 优点
- 在插入和删除操作时,只需要修改游标,不需要移动元素,改进了顺序结构中插入和删除操作需要移动大量元素的缺点。
- 缺点
- 没有解决连续存储分配带来的表长难以确定的问题
- 失去了顺序存储结构随机存取的特性
循环链表
将单链表中的终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表成为单循环链表,简称循环链表。
双向链表
双向链表是在单链表的每个结点中,在设置一个指向其前驱结点的指针域。
//线性表的双向链表存储结构
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DulNode,*DuLinkList;
插入s结点
s->prior = p;
s->next = p->next;
p->next->prior = s;
p->next = s;
删除p结点
p->prior->next = p->next;
p->next->prior = p->prior;
总结
本章各个概念关系图如下:
注:以上内容参考《大话数据结构》,图片由Visio 2016绘画后截图而成。