线性表
零个或多个数据元素的有限序列
非空表中每个数据元素都有一个确定的位置
在较复杂的线性表中,一个数据元素可以由若干个数据项组成
线性表的抽象数据类型定义如下:
ADT //线性表
Data
//线性表的数据对象集合,每个元素的类型均为DataType,其中除第一个元素外,每一个元素有且只有一个前驱元素,除了最后一个元素外,每一个元素有且只有一个直接后继元素。数据元素之间是一对一的关系。
Operation
InitList(*L); //初始化操作,建立一个空的线性表
ListEmpty(L); //若线性表为空,返回true,否则返回false
ClearList(*L); //将线性表清空
GetElem(L,i,*e); //将线性表L中的第i个元素值返回给e
LocateElem(L,e); //在线性表中查找与给定e相等的元素,如果查找成功,返回该元素在表中序号标识成功,否则返回0标识失败
ListInsert(*L,i,e); //在线性表中的第i个位置插入新元素e
ListDelete(*L,i,*e); //删除线性表L中第i个位置元素,并用e返回其值
ListLength(L); //返回线性表L的元素个数
endADT
/*将所有的在线性表Lb中但不在La中的数据元素插入到La中
*/
void union(List *La,List *Lb)
{
int La_len,Lb_len,i;
ElemType e;
La_len = ListLength(La);
Lb_len = ListLength(Lb);
for(i=1;i<Lb_len;i++)
{
GetElem(Lb,i.e); //取Lb中第i个数据元素赋给e
if(!LocateElem(La,e,equal)) //如果定位不到La中相同的元素
{
ListInsert(La,++La_len,e);
}
}
}
线性表的顺序存储结构
顺序存储定义:
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素
可以用C语言中的一维数组来实现顺序存储结构
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length;
}SqList;
描述顺序存储结构的三个属性:
(1)存储空间的起始位置:数组data
(2)线性表的最大存储容量:数组长度MaxSize
(3)线性表的当前长度:length
数组长度是存放线性表的存储空间的长度,分配后这个量一般是不变的:
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的
任意时刻,线性表的长度应该<=数组的长度
存储器中的每个存储单元都有自己的编号,这个编号称为地址。
每个数据元素占c个存储单元,LOC:存储位置
LOC(a+1)=LOC(a)+c
所以对于第i个数据元素的存储位置可以由a1推算出
LOC(Ai)=LOC(A1)+(i-1)*c
存取时间性能为O(1),通常把具有这一特点的存储结构称为随机存取结构
顺序存储结构的插入和删除
-
获得元素操作
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int status;
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; }
-
插入操作
插入算法思路:
1.如果插入位置不合理,抛出异常
2.如果线性表长度>=数组长度,则抛出异常或动态增加容量
3.从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置
4.将要插入元素填入位置i
5.表长度加1
status ListInsert(SqList *L,int i,ElemType 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; }
-
删除操作
删除算法思路:
如果删除位置不合理,抛出异常
取出删除元素
从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置表长度-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; }
线性表顺序存储结构的优缺点
-
优点:
无须为表示表中元素之间的逻辑关系而增加额外的存储空间
可以快速地存取表中任一位置的元素
-
**缺点: **
插入和删除需要移动大量的元素
当线性表长度变化较大时,难以确定存储空间的容量
造成存储空间的碎片
线性表的链式存储结构
为了表示每个元素及其直接后继元素之间的逻辑关系。对数据元素来说,除了要存储其本身的信息之外,还需要存储一个指示其直接后继元素的信息
将存储数据元素信息的域称为数据域,把存储直接后继元素位置的域成为指针域。指针域中存储的信息称为指针或者链。这两部分信息组成数据元素ai的存储映像,称为节点(Node)
链表中第一个结点的存储位置叫头指针
规定:线性链表的最后一个结点指针为空“NULL”或“^”
为了方便,会在单链表的第一个结点前附设一个结点,称为头结点,头结点的数据域可以不存储信息,也可以存储例如线性表的长度等附件信息,头结点的指针域存储指向第一个结点的指针
定义线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; //定义LinkList
- 单链表的读取
- 获得链表第i个数据的算法思路:
- 声明一个结点p指向链表的第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针像后移动,不断指向下一结点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,返回结点p的数据
- 获得链表第i个数据的算法思路:
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList 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) //第i个结点不存在
{
return ERROR;
}
*e = p->data; //取第i个元素的数据
return OK;
}
核心思想:工作指针后移
-
单链表的插入
-
思路
声明一结点p指向链表第一个结点,初始化j从1开始
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
若到链表末尾p为空,则说明第i个元素不存在
否则查找成功,在系统中生成一个空结点s
将数据元素e赋值给s->data
单链表的插入标准语句s->next=p->next; p->next=s;
返回成功
Status ListInsert(LinkList *L,int i,ElemType e) { int j; LinkList p,s; p = *L; j = 1; while(p && j <1) //寻找第i个结点 { p=p->next; ++j; } if(!p || j>1) { return ERROR; //第i个结点不存在 } s = (LinkList)malloc(sizeof(Node)); //生成新结点 s->data=e; s->next=p->next; //将p的后继节点赋值给s的后继 p-next=s; //将s赋值给p的后继 return OK; }
-
-
单链表第i个数据删除结点的算法思路
-
声明一结点p指向链表第一个结点,初始化j从1开始
-
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
-
若到链表末尾p为空,则说明第i个元素不存在
-
否则查找成功,将欲删除的结点p->next赋值给q
-
单链表的标准删除语句:p->next=q->next
-
将q结点中的数据赋值给e,作为返回
-
释放q结点
-
返回成功
status ListDelete(Linklist *L,int i,ElemType *e) { int j; LinkList p,q; p=*L; //将L给p,之后遍历的时候只用p就可以,防止修改原指针 j=1; while(p->next && j<i) { p=p->next; ++j; } if(!(p->next) || j>i) { return ERROR; } q=p->next; /*将q的后继赋值给p的后继 */ p->next=q->next; *e=q->data; //将q结点中的数据给e free(q); //回收结点,释放内存 return OK; }
显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显
-
-
单链表的整表创建
- 创建单链表的过程就是一个动态生成链表的过程
- 算法思路
- 声明一结点p和计数器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表
- 循环
- *生成一新结点赋值给p
- *随机生成一数字赋值给p的数据域p–>data
- *将p插入到头结点与前一个新结点之间
/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/ void CreateListHead(LinkList *L,int n) { LinkList p; int i; srand(time(NULL)); //初始化随机数种子 *L = (LinkList)malloc(sizeof(Node)); L->next =NULL; for(i=0;i<n;i++) { p=(Node)malloc(sizeof(Node)); p->data=rand()%100+1; p->next=L->next; L->next=p; } }
/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/ void CreateListTail(LinkList *L,int n) { LinkList p,r; int i; srand(time(NULL)); //初始化随机数种子 *L = (LinkList)malloc(sizeof(Node)); // L为整个线性表 r=*L; //r为指向尾部的结点 for(i=0;i<n;i++) { p=(Node)malloc(sizeof(Node)); p->data=rand()%100+1; r->next=p; r=p; } r->next=NULL; }
-
单链表的整表删除
实现思路
1.声明一结点p和q
2.将第一个结点赋值给p
3.循环
*将下一结点赋值q
*释放p
*将q赋值给p
status ClearList(LinkList *L) { LinkList p,q; p=(*L)->next; while(p) { q=p->next; free(p); p=q; } (*L)->next = NULL; return OK; }
单链表结构与顺序存储结构优缺点
用数组描述的链表叫做静态链表(游标实现法)
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur; //游标,为0时表示无指向
}Component,StaticLinkList[MAXSIZE];
静态链表的结构
数组的每一个元素由数据域data,和游标cur(相当于单链表的next指针)**
*数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标
*数组的最后一个元素的cur则存放第一个有数值的元素的下标
- 初始化静态链表
status InitList(StaticLinkList space)
{
int i;
for(i=0;i<MAXSIZE-1;i++)
{
space[i].cur=i+1;
}
space[MAXSIZE-1].cur=0;
return OK;
}
-
静态链表的插入
-
实现操作
-
获取空置元素位置函数
int Malloc_SLL(StaticLinkList space) { int i=space[0].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=MAXSIZE-1; //K是最后一个元素的下标 if(i < 1 || i>ListLength(L) +1) { return ERROR; } 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 OK; } return ERROR; }
-
静态链表的删除操作
status ListDelete(StaticLinkList L,int i) { int j,k; if(i<1 || i>ListLength(L)) { return ERROR; } k=MAXSIZE-1; for(j=1;j<i-1;j++) { k=L[k].cur; } //找到第i个元素之前的元素 j=L[k].cur; L[k].cur=L[j].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 }
int ListLength(StaticLinkList L) { int j=0; int i=L[MAXSIZE-1].cur; while(i) { i=L[i].cur; j++; } return j; }
-
-
静态链表的优缺点
循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表
双向链表
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域
存储结构
typedef struct DulNode
{
ElemType data;
struct DulNode *prior;
struct DulNode *next;
}DulNode *DuLinkList;
参考:《大话数据结构》.程杰著