线性表
线性表:具有相同类型的n个数据元素的有限序列
一、线性表的定义
- 用数学语言定义:若将线性表记为(a1,a2,…,ai-1,ai,ai+1,…,an),ai称为表项。则表中ai-1领先于ai,ai领先于ai+1,称ai-1为ai的直接前驱元素,ai+1为ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当i=2,3,…,n时,ai有且仅有一个直接前驱。
- 线性元素个数n(n>=0)定义为线性表的长度,当n=0时,称为空表
- i称为数据元素ai在线性表中的位序
- 在较复杂的线性表中,一个数据元素可以由若干个数据项组成
- 数据元素的类型必须相同
二、线性表的抽象数据类型
线性表的抽象数据类型定义:
ADT 线性表(List)
DATA
线性表的数据对象集合为{a1,a2,......,an},每个元素的类型均为DataType.其中,除第一个元素a1外,每个元素有且只有一个直接前驱元素,除了最后一个元素an外,每个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系
Operation
Initlist(*L);//初始化操作,建立一个空的线性表L
ListEmpty(L);//若线性表为空,返回true,否则返回false
ClearList(*L);//将线性表清空
GetElem(L,i,*e);//将线性表中的第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
三、线性表的顺序存储结构
(1)顺序存储定义
线性表的顺序存储结构:指用同一段地址连续的存储单元依次存储线性表的数据元素(数组实现)
(2)顺序存储方式
线性表的顺序存储的结构代码
#define MAXSIZE 20//存储空间初始分配量
tepedef int ElemType;//ElemType类型根据实际情况而定,这里假设为int
typedef struct
{
ElemType data[MAXSIZE];//数组存储数据元素,最大值为MAXSIZE
int length;//线性表当前长度
}SqList;
描述顺序存储结构需要的三个属性
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置
- 线性表的最大存储容量:数组长度MaxSize
- 线性表的当前长度:length
(3)数据长度与线性表长度区别
数据长度:指存放线性表的存储空间的长度,存储分配后这个量一般是不变的
线性表的长度:指线性表中数据元素的个数,随着线性表的插入与删除操作的进行,这个量是变化的
(4)地址计算方法
- 线性表的起始位置是1,线性表中的第i个元素是要存储在数组下标为i-1的位置。
- 地址:存储器中的每个存储单元都有自己的编号,这个编号就是地址
- 每个数据元素,无论是什么类型,都需要占据一定的存储空间,假设占用了c个存储单元
用LOC表示获得存储位置的函数
//线性表中第i+1个数据元素的存储位置
LOC(ai+1)=LOC(ai)+c;
//线性表中第i个数据元素的存储位置
LOC(ai)=LOC(a1)+(i-1)*c;
- 对每一个线性表位置的存入或取出数据,对于计算机来说都是相等的时间,就是一个常数,它的存取时间性能为O(1).通常把具有这一特点的存储结构称为随机存取结构
(5)顺序存储结构的插入与删除
a.获得元素操作
对于线性表的顺序存储结构,如果要实现GetElem操作,即将线性表L中的第i个位置元素值返回,只有i的数值在数值的下标范围内,即把数组中的第i-1下标的值返回
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef FALSE 0
typedef int Status;
//Status是函数的类型,其值是函数结果状态代码,如OK等
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(SqList L,int i,ElemList *e)
{
if(L.length==0||i<1||i>L.length)
{
return ERROR;
}
else
{
*e=L.data(i-1);
return OK;
}
}
b.插入操作
实现代码
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if(L->length==MAXSIZE)//顺序线性表已经满
{
return ERROR;
}
if(i<1||i>L->length+1)//当i不在范围内、
{
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;
}
c.删除操作
实现代码
//初始条件:顺序线性表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;
}
插入或删除时,其时间复杂度均为O(n)
五、线性表的链式存储结构
(1)线性表链式存储结构定义
-
线性表的链式存储结构特点:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着这些数据元素可以存在内存未被占用的任意位置。现在的链式存储结构,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
-
为了表示每个数据元素ai与其直接后继元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息外,还需要一个指示其直接后继的信息(即直接后继的存储信息)。把存储数据元素信息的域称为数据域。 把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点。
-
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链表存储结构,因此此链表的每一个结点中只包含一个指针域,所以叫做单链表。
-
头指针:链表中第一个结点的存储位置
-
规定线性链表的最后一个结点指针为“空”(用NULL或^表示)
-
头结点:为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点。头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
-
头指针
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空,头指针是链表的必要元素
-
头结点
- 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义
- 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了
- 头结点不一定是链表的必须要素
(2)线性表链式存储结构代码描述
结构定义:
//线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;//定义LinkList
结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
假设p是指向线性表的第i个元素的指针,则该结点ai的数据域可以用p->data表示,p->data的值是一个数据元素,结点ai的指针域可以用p->next表示,p->next的值是一个指针。p->next指向第i+1个元素,即指向ai+1的指针。如果p->data=ai,那么p->next->data=ai+1。
(3)单链表的读取
实现代码
//初始条件:顺序链表L已存在,1<=i<=LisyLength(L)
//操作结构:用e返回L中第i个数据元素的值
Statue 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;
}
时间复杂度为O(n)
(4)单链表的插入与删除
a.单链表的插入
代码实现
//初始条件:顺序链表L已存在,1<=i<=LisyLength(L)
//操作结构:在L中第i个结点位置之前插入新的数据元素e,L的长度加1
Statue 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));//生成新结点
s->data=e;
s->next=p->next;//将p的后继结点赋值给s的后继
p->next=s;//将s赋值给p的后继
return OK;
}
b.单链表的删除
代码实现
//初始条件:顺序链表L已存在,1<=i<=LisyLength(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(q);//让系统回收此结点,释放内存
return OK;
}
(5)单链表的整表创建
单链表整表创建的算法思路
- 声明一指针p和计数器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表
- 循环:
- 生产一新结点赋值给p
- 随机生产一数字赋值给p的数据域p->data
- 将p插入到头结点与前一新结点之间
头插法代码实现
//随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)
//让新结点一直在第一的位置
void CreateLisrHead(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;//随机生成100以内的数字
p->next=(*L)->next;
(*L)->next=p;//插入到表头
}
}
尾插法代码实现
//随机产生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;//表示当前链表结束
}
(6)单链表的整表删除
算法思路
- 声明一个结点p和q
- 将第一个结点赋值给p
- 循环
- 将下一结点赋值给q
- 释放p
- 将q赋值给p
代码实现
//初始条件:顺序链表已经存在
//操作结果:将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;
}
六、静态链表
静态链表:用数组描述的链表。让数组的元素都是由两个数据域组成,data和cur。即数组的每一个下标都对应一个data和cur。数据域data用来存放数据元素,即要处理的数据,cur相当于单链表中的next指针,存放该元素后面的后继在数组中的下标,把cur叫做游标。这种描述方法称为游标实现法。
(1)静态链表存储结构代码描述
//线性表的静态链表存储结构
#define MAXSIZE 100//假设链表的最大长度是1000
typedef struct
{
ElemType data;
int cur;//游标cursor,为0时表示无指向
}component,StaticLinkList[MAXSIZE];
对数组第一个和最后一个元素作为特殊元素处理,不存储数据。通常将未被使用的数组称为备用链表。数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于链表头结点作用,当整个链表为空时,则为0的平方。
图片
此时图示相当于初始化的数组状态,代码实现如下:
//将一维数组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;
}
图片
此时,甲这里就存有下一元素乙的游标2,乙则存有下一元素丁的下标3,庚是最后一个有值元素,所以它的cur设置为0,而最后一个元素cur则因甲是第一有值元素而存有它的下标为1.而第一个元素则因空闲空间的第一个元素下标为7,所以它的cur存有7。
(2)静态链表的插入操作
为辨明数组中哪些分量未被使用,可以将所以未被使用的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
//若备用空间链表非空,则返回分配的结点下标,否则返回0
int Malloc_SLL(StaticLinkList space)
{
int i=space[0].cur;//当前数组第一个元素的cur存的值,就是要返回的第一个备用空间的下标
if(space[0].cur)
{
space[0].cur=space[i].cur;//由于要拿出一个分量来使用,所以将其下一个分量用来做备用
}
return i;
}
代码实现
//在L中第i个元素之前插入新的数据元素e
Status ListInsert(StaticLinkList 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_SSL(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赋值给新元素的cur
L[k].cur=j;//把新元素的下标赋值给第i个元素之前元素的cur
return OK;
}
return ERROR;
}
(3)静态链表的删除操作
代码实现
//删除在L中第i个数据元素e
Satus 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;
L[k].cur=L[j].cur;
Free_SSL(L,j);
return OK;
}
Free_SSL(L,j)代码意思
void Free_SSL(StaticLinkList space,int k)
{
space[k].cur=space[0].cur;//把第一个元素cur赋值给要删除的分量cur,把要删除的分量下标赋值给第一个元素的cur
space[0].cur=k;//把要删除的分量下标赋值给第一个元素的cur
}
ListLength代码意思
//初始条件:静态链表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)合并两个循环链表
p=rearA->next;//保存A的头结点
rearA->next=rearB->next->next;//将本是指向B的第一个结点(不是头结点)赋值给rearA->next
q=rearB->next;
raerB->next=p;//将原A表的头结点赋值给rearB->next
free(q);//释放q
八、双向链表
双向链表:是在单链表的每个结点中,在设置一个指向其前驱结点的指针域。双向链表的每个结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
(1)线性表的双向链表存储结构
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;//直接前驱指针
struct DulNode *next;/直接后继指针
struct DuLNode *next;//直接后继指针
}DuLNode,*DuLinkList;