目录
1 线性表的定义
线性表(List):零个或多个数据元素的有限序列。
线性表类似于数学中的数列,只不过是有限的,因此可将线性表记为 a 1 , . . . , a i − 1 , a i , a i + 1 , . . . , a n a_1,...,a_{i-1},a_i,a_{i+1},...,a_n a1,...,ai−1,ai,ai+1,...,an,称 a i − 1 a_{i-1} ai−1是 a i a_i ai的直接前驱元素, a i + 1 a_{i+1} ai+1是 a i a_i ai的直接后继元素,于是当 i = 1 , 2 , . . . , n − 1 i=1,2,...,n-1 i=1,2,...,n−1时, a i a_i ai有且仅有一个直接后继,当 i = 2 , 3 , . . . , n i=2,3,...,n i=2,3,...,n时, a i a_i ai有且仅有一个直接前驱。
线性表的元素个数 n ( n ≥ 0 ) n(n\geq0) n(n≥0)定义为线性表的长度,当 n = 0 n=0 n=0时,称为空表。
在非空表中每个数据元素都有一个确定的位置,称 i i i为数据元素 a i a_i ai在线性表中的位序。
2 线性表的抽象数据类型
抽象数据类型(ADT):线性表(List)
数据(Data):
\quad\quad 线性表的数据对象集合为 { a 1 , . . . , a n } \{a_1,...,a_n\} {a1,...,an},每个元素的类型均为DataType。其中,除了第一个元素 a 1 a_1 a1,每一个元素有且仅有一个直接前驱,除了最后一个元素 a n a_n an外,每一个元素有且仅有一个直接后继。数据元素之间的关系是线性关系。
操作(Operation):
\quad\quad Initial(*L):初始化操作,建立一个空的线性表L。
\quad\quad IsEmpty(L):判断线性表是否为空,若为空,返回true,否则返回false。
\quad\quad Clear(*L):将线性表清空。
\quad\quad Get(L,i,*e):将线性表L中的第i个元素值返回给e。
\quad\quad Locate(L,e):在线性表L中查找与给定值e相等的元素,若查找成功,返回该元素在线性表中的序号,否则,返回0。
\quad\quad Insert(*L,i,e):在线性表L中的第i个位置插入新元素e。
\quad\quad Delete(*L,i,*e):删除线性表L中第i个位置的元素,并用e返回其值。
\quad\quad Length(L):返回线性表L的元素个数。
endADT
3 线性表的顺序存储结构
3.1 顺序存储结构定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
3.2 线性表的顺序存储结构代码实现
由于线性表的顺序存储结构是用一段地址连续的存储单元依次存储线性表的数据元素,因此可以用C语言中的一维数组来实现顺序存储结构。
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define SUCCESS 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
/* 线性表的最大存储容量,即数组长度MAXSIZE */
#define MAXSIZE 20
/* ElemType为数据元素类型,这里为int */
typedef int ElemType;
typedef struct
{
/* 数组,用于存储数据元素 */
ElemType data[MAXSIZE];
/* 线性表长度length */
int length;
}SeqList;
/* Status是函数的类型,其值是函数结果状态代码,如SUCCESS等 */
typedef int Status;
/* 初始化顺序线性表 */
Status Initial(SeqList *L)
{
L->length=0;
return SUCCESS;
}
/* 判断线性表是否为空,若为空,返回TRUE,否则返回FALSE */
Status IsEmpty(SeqList L)
{
if(L.length==0)
return TRUE;
else
return FALSE;
}
/* 将线性表清空 */
Status Clear(SeqList *L)
{
L->length=0;
return SUCCESS;
}
/* 将线性表L中的第i个元素值返回给e */
Status Get(SeqList L,int i,ElemType *e)
{
if(L.length==0 || i<1 || i>L.length)
return ERROR;
*e=L.data[i-1];
return SUCCESS;
}
/* 在线性表L中查找与给定值e相等的元素,若查找成功,返回第1个与e相等的元素在线性表中的序号,否则,返回0 */
int Locate(SeqList L,ElemType e)
{
int i;
if(L.length==0)
return 0;
for(i=0;i<L.length;i++)
{
if(L.data[i]==e)
break;
}
if(i>=L.length)
return 0;
return i+1;
}
/* 在线性表L中的第i个位置插入新元素e */
Status Insert(SeqList *L,int i,ElemType e)
{
int k;
/* 若线性表已满,返回ERROR */
if(L->length==MAXSIZE)
return ERROR;
/* 若插入位置不正确,返回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;
/* 插入后线性表长度加1 */
L->length++;
return SUCCESS;
}
/* 删除线性表L中第i个位置的元素,并用e返回其值 */
Status Delete(SeqList *L,int i,ElemType *e)
{
int k;
/* 若线性表为空,返回ERROR */
if(L->length==0)
return ERROR;
/* 若删除位置不正确,返回ERROR */
if(i<1 || i>L->length)
return ERROR;
/* 用e返回线性表L中第i个位置的元素 */
*e=L->data[i-1];
/* 若删除不是最后位置,通过循环将删除位置后继元素前移 */
if(i<L->length)
{
for(k=i;k<L->length;k++)
L->data[k-1]=L->data[k];
}
/* 删除后线性表长度减1 */
L->length--;
return SUCCESS;
}
/* 返回线性表L的元素个数 */
int Length(SeqList L)
{
return L.length;
}
3.3 线性表顺序存储结构的优缺点
优点
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
- 由于顺序存储结构使用一段地址连续的存储单元,因此可以计算出线性表中任意位置的地址,从而可以快速地存取表中任一位置的元素,事实上,其存取时间复杂度为 O ( 1 ) O(1) O(1),这种存储结构也被称为随机存储结构
缺点
- 插入和删除操作需要移动大量元素,其时间复杂度为 O ( n ) O(n) O(n)
- 当线性表长度变化较大时,难以确定存储空间的容量
- 由于使用地址连续的存储单元,因此会造成存储空间的“碎片化”,同时无法有效利用零散的存储空间
4 线性表的链式存储结构(单链表)
4.1 链式存储结构定义
相比于顺序存储结构,链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这些存储单元可以是连续的,也可以是不连续的。因此,在链式存储结构中,除了要存储数据元素信息,还要存储其后继元素的存储地址。
其中,存储数据元素信息的域称为数据域,存储直接后继元素位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素的存储映像,称为结点(Node)。
n n n个结点链结成一个链表,即为线性表的链式存储结构,由于此链表的每个结点中只包含一个指针域,所以又称作单链表。
链表中第一个结点的存储位置叫做头指针,有时,为了方便操作,会在第一个结点前附设一个结点,称为头结点。链表中最后一个结点指针为“空”(一般用NULL表示)。
4.2 线性表的链式存储结构(单链表)代码实现
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define SUCCESS 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
/* 线性表的最大存储容量MAXSIZE */
#define MAXSIZE 20
/* Status是函数的类型,其值是函数结果状态代码,如SUCCESS等 */
typedef int Status;
/* ElemType为数据元素类型,这里为int */
typedef int ElemType;
/* 定义结点Node */
typedef struct Node
{
/* data为数据元素信息,next为直接后继元素位置 */
ElemType data;
struct Node *next;
}Node;
/* 定义链表LinkList */
typedef struct Node *LinkList;
/* 初始化链表 */
Status Initial(LinkList *L)
{
/* 产生头结点,并使L指向此头结点 */
*L=(LinkList)malloc(sizeof(Node));
/* 存储分配失败 */
if(!(*L))
return ERROR;
/* 指针域为空 */
(*L)->next=NULL;
return SUCCESS;
}
/* 判断链表是否为空,若为空,返回TRUE,否则返回FALSE */
Status IsEmpty(LinkList L)
{
if(L->next)
return FALSE;
else
return TRUE;
}
/* 将链表清空 */
Status Clear(LinkList *L)
{
LinkList p,q;
/* 让p指向链表L的第一个结点 */
p=(*L)->next;
/* 遍历释放每一个结点 */
while(p)
{
q=p->next;
free(p);
p=q;
}
/* 头结点指针域为空 */
(*L)->next=NULL;
return SUCCESS;
}
/* 将链表L中的第i个元素值返回给e */
Status Get(LinkList L,int i,ElemType *e)
{
/* 建立一结点p,并让p指向链表L的第一个结点 */
LinkList p=L->next;
int j=1;
/* 当p不为空时,循环遍历直至第i个元素 */
while(p && j<i)
{
p=p->next;
++j;
}
/* 若第i个元素不存在,返回ERROR */
if(!p || j>i)
return ERROR;
/* 获取第i个元素的数据 */
*e=p->data;
return SUCCESS;
}
/* 在链表L中查找与给定值e相等的元素,若查找成功,返回第1个与e相等的元素在链表中的序号,否则,返回0 */
int Locate(LinkList L,ElemType e)
{
int i=0;
/* 建立一结点p,并让p指向链表L的第一个结点 */
LinkList p=L->next;
/* 遍历每一个元素并判断是否与e相等 */
while(p)
{
i++;
if(p->data==e)
return i;
p=p->next;
}
return 0;
}
/* 在链表L中的第i个位置插入新元素e */
Status Insert(LinkList *L,int i,ElemType e)
{
LinkList p,s;
p=*L;
int j=1;
/* 遍历找到第i个元素 */
while(p && j<i)
{
p=p->next;
++j;
}
/* 若第i个元素不存在,返回ERROR */
if(!p || j>i)
return ERROR;
/* 生成新结点 */
s=(LinkList)malloc(sizeof(Node));
s->data=e;
/* 令s的后继为p的后继,同时令p的后继为s */
s->next=p->next;
p->next=s;
return SUCCESS;
}
/* 删除链表L中第i个位置的元素,并用e返回其值 */
Status Delete(LinkList *L,int i,ElemType *e)
{
LinkList p,q;
p=*L;
int j=1;
/* 遍历找到第i个元素,这里第i个元素是p的后继 */
while(p->next && j<i)
{
p=p->next;
++j;
}
/* 若第i个元素不存在,返回ERROR */
if(!(p->next) || j>i)
return ERROR;
/* 令q为p的后继,同时令p的后继为q的后继,即p的后继的后继 */
q=p->next;
p->next=q->next;
/* 获取q,即p的后继中的数据 */
*e=q->data;
/* 释放q */
free(q);
return SUCCESS;
}
/* 返回链表L的元素个数 */
int Length(LinkList L)
{
int i=0;
/* 建立一结点p,并让p指向链表L的第一个结点 */
LinkList p=L->next;
/* 遍历计数 */
while(p)
{
i++;
p=p->next;
}
return i;
}
4.3 线性表链式存储结构(单链表)的优缺点
优点
- 在找到位置的指针后,插入和删除时间复杂度仅为 O ( 1 ) O(1) O(1),对于插入和删除数据频繁的操作具有明显的效率优势
- 由于使用一组任意的存储单元存储线性表的数据元素,因此对于存储空间的利用更加高效和灵活
缺点
- 查找以及获取数据时间复杂度均为 O ( n ) O(n) O(n),不适合进行频繁地查找和获取操作
5 静态链表
5.1 静态链表代码实现
某些早期的高级语言,由于没有类似于C语言中的指针,链表结构无法按照之前的方法实现,于是有人想到用数组来代替指针描述单链表。首先让数组的元素都是由两个数据域组成,data和cur。其中数据域data用来存放数据元素,而cur则用来存放该元素的后继元素在数组中的下标,cur被称作游标。这种用数组描述的链表叫做静态链表,这种描述方法也被称作游标实现法。
#include <string.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define SUCCESS 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
/* 存储空间初始分配量 */
#define MAXSIZE 1000
/* Status是函数的类型,其值是函数结果状态代码,如SUCCESS等 */
typedef int Status;
/* ElemType为数据元素类型,这里为char */
typedef char ElemType;
/* 定义静态链表StaticLinkList */
typedef struct
{
/* data用来存放数据元素 */
ElemType data;
/* 游标(Cursor) ,为0时表示无指向 */
int cur;
} Component,StaticLinkList[MAXSIZE];
/* 初始化静态链表 */
Status Initial(StaticLinkList space)
{
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,指向备用链表(空闲空间)的第一个结点,"0"表示空指针 */
int i;
for(i=0;i<MAXSIZE-1;i++)
space[i].cur=i+1;
/* 目前静态链表为空,最后一个元素的cur为0,若静态链表不为空,则最后一个元素的cur为1,即第一个插入元素的下标 */
space[MAXSIZE-1].cur=0;
return SUCCESS;
}
/* 返回静态链表L的元素个数 */
int ListLength(StaticLinkList L)
{
int j=0;
/* i为第一个插入元素的下标 */
int i=L[MAXSIZE-1].cur;
/* 遍历计数 */
while(i)
{
i=L[i].cur;
j++;
}
return j;
}
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0,类似于malloc函数 */
int Malloc_SSL(StaticLinkList space)
{
/* 数组中第一个元素的cur即空闲空间的第一个结点的位置 */
int i=space[0].cur;
/* 由于下标i的位置将被使用,因此将其后继作为空闲空间的第一个结点 */
if(space[0].cur)
space[0].cur=space[i].cur;
return i;
}
/* 在链表L中的第i个位置插入新元素e */
Status Insert(StaticLinkList L,int i,ElemType e)
{
int j,k,l;
/* 令k是最后一个元素的下标 */
k=MAXSIZE-1;
/* 若插入位置不正确,返回ERROR */
if(i<1 || i>Length(L)+1)
return ERROR;
/* 获得空闲分量的下标 */
j=Malloc_SSL(L);
if(j)
{
/* 将数据赋值给此分量的data */
L[j].data=e;
/* 找到第i-1个元素的位置 */
for(l=1;l<=i-1;l++)
k=L[k].cur;
/* 令新元素的cur为第i-1个元素的cur,同时令第i-1个元素的cur为新元素的下标 */
L[j].cur=L[k].cur;
L[k].cur=j;
return SUCCESS;
}
return ERROR;
}
/* 将下标为k的空闲结点回收到备用链表,类似于free函数 */
void Free_SSL(StaticLinkList space,int k)
{
/* 由于下标为k的结点变为空闲结点,因此令下标为k的元素的cur为第一个元素的cur,同时令第一个元素的cur为k */
space[k].cur=space[0].cur;
space[0].cur=k;
}
/* 删除链表L中第i个位置的元素 */
Status Delete(StaticLinkList L,int i)
{
int j,k;
/* 若删除位置不正确,返回ERROR */
if (i<1 || i>ListLength(L))
return ERROR;
/* 找到第i个元素的位置 */
k=MAXSIZE-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 SUCCESS;
}
要理解上述静态链表的实现代码,关键是要注意到数组的下标并不代表该元素在静态链表中的位置。
5.2 静态链表的优缺点
优点
- 在插入和删除操作时,只需要修改游标,而不需要移动元素,改进了在顺序存储结构中插入和删除操作需要移动大量元素的缺点
缺点
- 没有解决连续存储分配带来的表长难以确定的问题
- 失去了顺序存储结构随机存取的特性
6 循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表解决了单链表不能从一个结点出发,访问到链表的全部结点的问题
7 双向链表
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
双向链表相比于单链表,多了如可以反向遍历查找等操作,同时由于其良好的对称性,使得为某个结点的前后结点的操作变得方便,可以有效提高算法的时间性能,但由于其存储了两个指针变量,相比于单链表要占用更多的空间,相当于用空间换时间。