Chapter 3 线性表
- 是什么:零个或多个数据元素的有限序列。(序列表明其有顺序,n=0成为空表)
- 在复杂线性表中,一个数据元素可由多个数据项组成。
线性表的抽象数据类型
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,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中
序号表示成功;否则,返回0表示失败。
ListInsert(*L,i,e): 在线性表L中的第i个位置插入新元素e。
ListDelete(*L,i,*e): 删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L): 返回线性表L的元素个数。
e.g.
/*实现:两个线性表集合A和B的并集操作,
La表示集合A,Lb表示集合B,即,将所有在线性表Lb中但不在La中的数据元素插入到La中 */
/*方法:循环集合B中的每个元素,判断当前元素是否在A中,若不存在,插入到A中即可*/
void unionL(List *La,List Lb)
{
int La_len,Lb_len;
ElemType e; /*声明与La和Lb相同的数据元素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)) /*La中不存在和e相同的数据元素*/
ListInsert(La, ++La_len,e); /*插入*/
}
}
线性表的顺序存储结构
- 是什么: 用一段地址连续的存储单元依次存储线性表的数据元素。
- 如何存:用一维数组。找到第一个位置(存储空间的起始位置),估计线性表最大存储容量,数组的长度就是这个最大存储容量。
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList;
- 线性表的长度是线性表中数据元素的个数,随插入删除变化,任意时刻小于等于数组长度。
- 地址计算方法:LOC(ai)=LOC(a1)+(i-1)c 。由公式可计算线性表中任意位置地址,存取元素时间性能为O(1),具有这一特点的结构叫随机存取结构。
- 获得元素操作
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
/*实现:i在数组下标范围内,就把数组第i-1小标的值返回*/
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;
}
- 插入操作
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
/*实现:从最后一个元素向前遍历到第i个,分别后移1位,再把元素插入i处,表长加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;
}
- 删除操作
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/*操作结果:删除L中的第i个数据元素,并用e返回其值,L的长度减1*/
/*实现:从删除元素位置开始遍历到最后一个元素位置,分别将它们都前移一个位置,表长减1*/
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
if (L->length==0) /*线性表为空*/
return ERROR;
if(i<1 || i>L->length+1) /*删除位置不正确*/
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(1),插入删除操作,时间复杂度为O(n)。
- 线性表顺序存储结构的优点:无须为表示表中元素之间的逻辑关系而增加额外的存储空间;可以快速地存取表中任一位置的元素。
- 线性表顺序存储结构的缺点:插入和删除操作需要移动大量元素;当线性表长度变化较大时,难以确定存储空间的容量;载程存储空间的“碎片”。
线性表的链式存储结构
- 是什么:每个结点包含数据域(存储自己的信息)和指针域(指向后继的存储位置),n个结点链结成一个链表。
- 怎么存:单链表用结构体指针。
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /*定义LinkList*/
- 头指针:链表指向第一个结点的指针。若有头结点就是指向头结点的指针。常用头指针冠以链表的名字。无论链表是否为空,头指针均不为空。
- 头结点:单链表的第一个结点前附设一个结点,为了操作的统一和方便设立的。数据域一般无意义或者放链表长度。
- 单链表的读取操作:
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
/*实现:j<i是就让p指针后移,不断指向下一个结点。若到p为空说明不存在,否则查找成功,返回p的数据*/
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;
}
//最坏情况时间复杂度为O(n)
//由于单链表结构没有定义表长,所以不知要循环多少次,因此不能用for控制循环。
//其主要核心思想是“工作指针后移”。
- 单链表的插入操作
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
/*实现:遍历到i后用标准插入语句*/
Status ListInsert (LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p && j<i) /*寻找第i个结点*/
{
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;
}
- 单链表的删除操作
其实直接就一步 p->next=p->next->next;
用q来取代p->next,就是 q=p->next; p->next=q->next;
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
/*实现:遍历到i后用标准删除语句*/
Status ListDelete (LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p && j<i) /*寻找第i个结点*/
{
p = p->next;
++j
}
if(!p || j>i)
return ERROR; /*第i个元素不存在*/
q = p->next;
p->next = q->next;
*e=q->data;
free(q);
return OK;
}
- 单链表的整表创建操作:
/*操作结果:随机产生n个元素的值,建立带头结点的单链线性表L*/
/*实现:(头插法)建立头结点,循环n次在头结点与前一新结点间用标准语句插入*/
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; /*随机生成100以内的数字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
/*操作结果:随机产生n个元素的值,建立带头结点的单链线性表L*/
/*实现:(尾插法)循环n次每次在表尾插入新结点*/
void CreateListHead (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; /*表示当前链表结束*/
}
//r随着循环不断地变化,而L则是随着循环增长为一个多结点的链表
- 单链表的整表删除
/*初始条件:顺序线性表L已存在,操作结果:将L重置为空表*/
/*实现:(尾插法)循环n次每次在表尾插入新结点*/
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;
}
- 单链表结构与顺序结构对比:
①存储分配方式:顺序结构用连续存储单元,链式结构是一组任意存储单元。
②时间性能:顺序结构查找为O(1),插入删除为O(n);单链表查找为O(n),在找出某位置的指针后插入删除仅为O(1)。 - 频繁查找少并插入删除,用顺序结构;频繁插入删除用单链表。元素个数变化大或者根本不知道大小,用单链表。
其他链表结构
静态链表
- 是什么:用数组代替指针,描述单链表。
- 怎么搞:两个数据域,data存放数据元素,cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。通常对数组第一个和最后一个元素特殊处理。把未被使用的数组元素称为备用链表。数组第一个元素的cur用来存放备用链表第一个节点的下标。数组最后一个元素的cur用来存放第一个插入元素的下标,相当于单链表中的头结点作用。
/*线性表的静态链表存储结构*/
/*通常把数组建立得大一些,以便插入时不至于溢出*/
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur; /*游标(Cursor),为0时表示无指向*/
}Component,StaticLinkList[MAXSIZE];
- 静态链表的初始化:
/*将一维数组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函数*/
/*为了辨明数组中哪些分量未被使用,可将所有未被使用以及被删除的分量用游标链成一个备用的链表,
每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。*/
/*若备用空间链表非空,则返回分配的节点下表,否则返回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_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; /*相当于s->next=p->next;*/
L[k].cur = j; /*相当于p->next=s; */
retunr OK;
}
return ERROR;
}
- 静态链表的删除操作
/*因为是数组,自己实现free函数*/
/*将下标为k的空闲结点回收到备用链表*/
void Free_SSL (StaticLinkList space, int k)
{ /*把空闲点加入到备用链表,其实是相当于插入*/
space[k].cur = space[0].cur; /*s->next=p->next;*/
space[0].cur = k; /*p->next=s*/
}
/* 删除在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 = l; j<= i-1; j++)
k=L[k].cur; /*k变为第i-1个,要删除第i个*/
j = L[k].cur; /*相当于q=p->next*/
L[k].cur = L[j].cur; /*相当于p->next=q->next*/
Free_SSL(L, j);
return OK;
}
- 静态链表的长度
/*初始条件:静态链表L已存在。操作结果:返回L中数据元素的个数*/
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while (i)
{
i=L[i].cur;
j++;
}
return j;
}
- 静态链表的优缺点
①有点:插入删除只需要修改游标 ②缺点:没有解决连续存储分配带来的表长难以确定的问题,失去了顺序存储结构随机存取的特性。
循环链表
- 是什么:单链表的尾指针指向头结点,形成环。
- 干什么:解决从一个结点出发,可以访问到链表的全部结点。
- 与单链表的差异在循环的判断,原来是判断p->next是否为空,现在是p->next是否为头结点。
- 有头指针时,访问第一个结点为O(1),想访问最后一个结点也为O(1),就改用尾指针rear,此时头指针为rear->next,rear->next->next; 就为头结点,时间为O(1)。
- 有尾指针合并链表非常简单:
p=rearA->next;
rearA->next=rearB->next->next;
q=rearB->next;
rearB->next=p;
free(q);
双向链表
- 是什么:单链表的每个节点中,再设一个指向前驱的指针。
- 存储结构
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DulNode, *DuLinkList;
- p->next->prior = p = p->prior->next
- 单链表可以有循环链表,双向链表也可以有循环链表。
双向链表的循环带头结点的空链表如图
非空的循环的带头结点的双向链表如图 - 双向链表可反向遍历查找数据,但在插入和删除时,需要更改两个指针变量。注意顺序。
插入:
s->prior = p;
s->next = p->next;
p->next->prior =s;
p->next = s;
删除:
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
总结
- 首先定义线性表,给出基本操作。
- 线性表两大结构。由顺序结构插入删除不方便引出链式存储结构,又介绍了链式存储结构的不同形式:单链表、循环链表、双向链表。还有不用指针的静态链表。