第2章 线性表
本章主要介绍下列内容:
1.线性表的定义和基本操作
2.线性表的顺序存储结构
3.线性表的链式存储结构
4.循环链表、线性表的应用举例
课时分配:
1、2两个学时,3两个学时, 4两个学时,上机两个学时
重点、难点:
线性表的存储结构、链式存储结构、循环链表
第一节 线性表的定义和基本操作
1.线性表的定义
线性表是由n(n≥0)个类型相同的数据元素组成的有限序列。通常表示成下列形式:
L=( a1, a2,...,ai-1,ai,ai+1,...,an)
其中:L为线性表名称,习惯用大写书写;ai为组成该线性表的数据元素,习惯用小写书写;
线性表中数据元素的个数被称为线性表的长度,当n=0时,线性表为空,又称为空线性表。
举例:
La=(34,89,765,12,90,-34,22)
数据元素类型为int。
Ls=("Hello","World", "China", "Welcome") 数据元素类型为string。
Lb=(book1,book2,...,book100) 数据元素类型为下列所示的结构类型:
struct bookinfo{
int No; //图书编号
char *name; //图书名称
char *auther; //作者名称
...;
}
2.线性表的基本操作(略介绍)
(1)初始化线性表L InitList(L)
(2)销毁线性表L DestoryList(L)
(3)清空线性表L ClearList(L)
(4)求线性表L的长度 ListLength(L)
(5)判断线性表L是否为空 IsEmpty(L)
(6)获取线性表L中的某个数据元素内容 GetElem(L,i,e)
(7)检索值为e的数据元素 LocateELem(L,e)
(8)返回线性表L中e的直接前驱元素 PriorElem(L,e)
(9)返回线性表L中e的直接后继元素 NextElem(L,e)
(10)在线性表L中插入一个数据元素 ListInsert(L,i,e)
(11)删除线性表L中第i个数据元素 ListDelete(L,i,e)
第二节 线性表的顺序存储结构
1.线性表的顺序存储结构
线性表的顺序存储结构是指用一组连续的存储单元依次存储线性表中的每个数据元素。如下图所示:
其中,L为每个数据元素所占据的存储单元数目。
相邻两个数据元素的存储位置计算公式
LOC(ai+1)=LOC(ai)+L
线性表中任意一个数据元素的存储位置的计算公式为:
LOC(ai+1)=LOC(a1)+(i-1)*L
2.顺序存储结构的特点
(1)利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构(物理结构)一致;
(2)在访问线性表时,可以利用上述给出的数学公式,快速地计算出任何一个数据元素的存储地址。因此,我们可以粗略地认为,访问每个数据元素所花费的时间相等。这种存取元素的方法被称为随机存取法,使用这种存取方法的存储结构被称为随机存储结构。
在C语言中,实现线性表的顺序存储结构的类型定义
#define LIST_MAX_LENGTH 100 //线性表的最大长度
typedef struct {Elemtype *elem; //指向存放线性表中数据元素的基地址
int length; //线性表的当前长度
}SEQLIST;
数组形式定义:(教材是用这种形式)
typedef struct {
Elemtype elem[LIST_MAX_LENGTH]; //指向存放线性表中数据元素
int length; //线性表的当前长度
}SEQLIST;
注:为帮助学生考研,上课讲解用指针形式。
3.典型操作的算法实现
(1)初始化线性表L
int InitList(SEQLIST *L)
{
L->elem=(Elemtype*)malloc(LIST_MAX_LENGTH *sizeof(Elemtype)); //分配空间
if(L->elem==NULL) return ERROR; //若分配空间不成功,返回ERROR
L->length=0; //将当前线性表长度置0
return OK; //成功返回OK
}
(2)销毁线性表L
void DestroyList(SEQLIST *L)
{
if (L->elem) free(L->elem); //释放线性表占据的所有存储空间
}
(3)清空线性表L
void ClearList(SEQLIST *L)
{
L->length=0; //将线性表的长度置为0
}
(4)求线性表L的长度
int GetLength(SEQLIST L)
{
return (L.length);
}
(5)判断线性表L是否为空
int IsEmpty(SEQLIST L)
{
if (L.length==0) return TRUE; else return FALSE;
}
(6)获取线性表L中的某个数据元素的内容
int GetElem(SEQLIST L,int i,Elemtype *e)
{
if (i<1||i>L.length) return ERROR; //判断i值是否合理,若不合理,返回ERROR
*e=L.elem[i-1]; //数组中第i-1的单元存储着线性表中第i个数据元素的内容
return OK;
}
(7)在线性表L中检索值为e的数据元素
int LocateELem(SEQLIST L,Elemtype e)
{
for (i=0;i< L.length;i++)
if (L.elem[i]==e) return i+1;
return 0;
}
(8)在线性表L中第i个数据元素之前插入数据元素e
int ListInsert(SEQLIST *L,int i,Elemtype e)
{
if (L->length==LIST_MAX_LENGTH) return ERROR; //检查是否有剩余空间
if (i<1||i>L->length+1) return ERROR; //检查i值是否合理
for (j=L->length-1;j>=i-1;i++) //将线性表第i个元素之后的所有元素向后移动
L.->elem[j+1]=L->elem[j];
L->elem[i-1]=e; //将新元素的内容放入线性表的第i个位置,
L->length++;
return OK;
}
(9)将线性表L中第i个数据元素删除
int ListDelete(SEQLIST *L,int i,Elemtype *e)
{
if (IsEmpty(L)) return ERROR; //检测线性表是否为空
if (i<1||i>L->length) return ERROR; //检查i值是否合理
*e=L->elem[i-1]; //将欲删除的数据元素内容保留在e所指示的存储单元中
for (j=i;j<=L->length-1;j++) //将线性表第i+1个元素之后的所有元素向前移动
L->elem[j-1]=L->elem[j];
L->length--;
return OK;
}
}
第三节 线性表的链式存储结构
1.线性表顺序存储结构的特点
它是一种简单、方便的存储方式。它要求线性表的数据元素依次存放在连续的存储单元中,从而利用数据元素的存储顺序表示相应的逻辑顺序,这种存储方式属于静态存储形式。
暴露的问题:
(1)在做插入或删除元素的操作时,会产生大量的数据元素移动;
(2)对于长度变化较大的线性表,要一次性地分配足够的存储空间,但这些空间常常又得不到充分的利用;
(3)线性表的容量难以扩充。
2.线性表的链式存储结构
线性表的链式存储结构是指用一组任意的存储单元(可以连续,也可以不连续)存储线性表中的数据元素。为了反映数据元素之间的逻辑关系,对于每个数据元素不仅要表示它的具体内容,还要附加一个表示它的直接后继元素存储位置的信息。假设有一个线性表(a,b,c,d),可用下图所示的形式存储:
3.链式存储结构的特点
(1)线性表中的数据元素在存储单元中的存放顺序与逻辑顺序不一定一致;
(2)在对线性表操作时,只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,这样就会造成寻找第一个结点和寻找最后一个结点所花费的时间不等,具有这种特点的存取方式被称为顺序存取方式。
在C语言中,实现线性表的链式存储结构的类型定义
typedef strcut linklist{ //结点类型
Elemtype elem;
struct linklist *next;
}LINKLIST;
typedef struct{ //链表类型
LINKLIST *head;
}LINK_LIST;
4.典型操作的算法实现
(1)初始化链表L
int InitList(LINK_LIST *L)
{
L->head=(*LINKLIST)malloc(sizeof(LINKLIST)); //为头结点分配存储单元
if (L->head) {L->head->next=NULL; return OK;}
else return ERROR ;
}
(2)销毁链表L
void DestoryList(LINK_LIST *L)
{
LINKLIST *p;
while (L->head){ //依次删除链表中的所有结点
p=L->head; L->head=L->head->next;
free(p);
}
}
(3)清空链表L
void ClearList(LINK_LIST *L)
{
LINKLIST *p;
while (L->head->next){
p=L->head->next; //p指向链表中头结点后面的第一个结点
L->head->next=p->next; //删除p结点
free(p); //释放p结点占据的存储空间
}
}
(4)求链表L的长度
int ListLength(LINK_LIST L)
{
LINKLIST *p;
int len;
for(p=L.head, len=0;p->next==NULL; p=p->next,len++);
return(len);
}
(5)判链表L空否。
int IsEmpty(LINK_LIST L)
{
if(L.head->next==NULL) return TRUE;
else return FALSE;
}
(6)通过e返回链表L中第i个数据元素的内容
void GetElem(LINK_LIST L,int i,Elemtype *e)
{
LINKLIST *p;
int j;
if (i<1||i>ListLength(L)) exit ERROR; //检测i值的合理性
for (p=L.head,j=0; j!=i;p=p->next,j++); //找到第i个结点
*e=p->elem; //将第i个结点的内容赋给e指针所指向的存储单元中
}
(7)在链表L中检索值为e的数据元素
LINKLIST *LocateELem(LINK_LIST L,Elemtype e)
{
LINKLIST *p;
for (p=L.head->next;p&&p->elem!=e;p=p->next); //寻找满足条件的结点
return(p);
}
(8)返回链表L中结点e的直接前驱结点
LINKLIST *PriorElem(LINK_LIST L,LINKLIST* e)
{
LINKLIST *p;
if (L.head->next==e) return NULL; //检测第一个结点
for (p=L.head;p->next&&p->next!=e;p=p->next);
if (p->next==e) return p;
esle return NULL;
}
(9)返回链表L中结点e的直接后继结点
LINKLIST *NextElem(LINK_LIST L,LINKLIST* e)
{
LINKLIST *p;
for(p=L.head->next;p&&p!=e;p=p->next);
if(p) p=p->next;
return p;
}
(10)在链表L中第i个数据元素之前插入数据元素e
int ListInsert(LINK_LIST *L,int i,Elemtype e)
{
LINKLIST *p,*s;
int j;
if (i<1||i>ListLength(L)+1) return ERROR;
s=(LINKLIST*)malloc(sizeof(LINKLIST));
if(s==NULL) return ERROR;
s->elem=e;
for(p=L->head,j=0;p&&j<i-1;p=p->next;j++); //寻找第i-1个结点
s->next=p->next; p->next=s; //将s结点插入
return OK;}
(11)将链表L中第i个数据元素删除,并将其内容保存在e中。
int ListDelete(LINK_LIST *L,int i,Elemtype *e)
{
LINKLIST *p*s;
int j;
if(i<1||i>ListLength(L)) return ERROR; //检查i值的合理性
for(p=L->head, j=0;j<i-1;p=p->next,j++); //寻找第i-1个结点
s=p->next; //用s指向将要删除的结点
*e=s->elem;
p->next=s->next; //删除s指针所指向的结点
free(s);
return OK;
}
5.循环链表
若将链表中最后一个结点的next域指向第一个结点,如下图
:
实现循环链表的类型定义与单链表完全相同,它的所有操作也都与单链表类似。只是判断链表结束的条件有所不同。下面我们就列举两个循环链表操作的算法示例。
(1)初始化链表CL
int InitList(LINK_LIST *CL)
{
CL->head=(*LINKLIST)malloc(sizeof(LINKLIST));
if (CL->head) {CL->head->next=CL->head; return OK;} //让next域指向它自身
else return ERROR ;
}
(2)在循环链表CL中检索值为e的数据元素
LINKLIST *LocateELem(LINK_LIST CL,Elemtype e)
{
LINKLIST *p;
for(p=CL.head->next;(p!=CL.head)&&(p->elem!=e);p=p->next);
if(p!=CL.head) return p;
else return NULL ;
}
6.双向循环链表(选讲)
在循环链表中,访问结点的特点访问后继结点,只需要向后走一步,而访问前驱结点,就需要转一圈。
结论:循环链表并不适用于经常访问前驱结点的情况。
解决方法:在需要频繁地同时访问前驱和后继结点的时候,使用双向链表。所谓双向链表。
双向链表就是每个结点有两个指针域。一个指向后继结点,另一个指向前驱结点。
用C语言实现双向循环链表的类型定义
typedef struct du_linklist
{Elemtype elem;struct
du_linklist *prior,*next;
}DU_LINKLIST;
typedef struct{ //双向链表类型
DU_LINKLIST *head;
}DU_LINK_LIST;
(1)初始化双向循环链表DL
int InitDuList(DU_LINK_LIST *DL)
{
DL->head=(DU_LINKLIST*)malloc(sizeof(DU_LINKLIST)); //为头结点分配存储单元
if (DL->head==NULL) return ERROR;
DL->head->next=DL->head; //让头结点的next域指向自身
DL->head->prior=DL->head; //让头结点的prior域指向自身
return OK;
}
(2)在双向循环链表DL中,第i个数据元素之前插入数据元素e
在一个结点之前插入一个新结点的过程。
s->elem=e;
for (p=L->head,j=0;p&&j<i;p=p->next;j++); //寻找第i 个结点
s->next=p; s->prior=p->prior; //将新结点插入
p->prior->next=s; p->prior=s;
return OK;
}
(3)创建双向循环链表DL。
void Create_Du_Link_List(DU_LINK_LIST *DL)
{
if(InitDulist(DL)==ERROR) exit ERROR;
scanf("%d",&data);
for(int i=1;data;i++){
DuListInsert(DL,i,data);
scanf("%d",&data);
}
}
第四节 线性表的应用举例
约瑟夫(Joseph)问题:编号为1,2,···,n的n个人按顺时针方向围坐在一张圆桌旁,每个人手中持有一个密码(正整数)。首先输入一个正整数作为报数上限值m,然后,从第一个人开始按顺时针方向自1开始顺序报数,报到m的人离开桌旁,并将他手中的密码作为新的m值,从顺时针方向的下一个就坐在桌旁的人人开始重新从1报数,如此下去,直至所有人全部离开桌旁为止。
假设有7个人,编号从1到7,他们手中的密码分别是3,1,7,2,4,8,4,最初的m=2,通过报数,这7个人离开桌旁的顺序应该是:2,3,5,4,7,6,1。
(1)数据结构的分析
这个问题的主角是n个人,每个人需要描述的信息有:编号、密码和是否在桌旁的状态。假设有7个人,他们的信息可以表示成下面的形式。
(2)算法描述
让n个人围坐在一张圆桌旁;
for (i=1;i<=n;i++){
从1开始报数,报到m停止;
报到m的人离开桌子;
}
(3)最终的算法实现
#define LIST_MAX_LENGTH 7
#define n LIST_MAX_LENGTH
typedef int Elemtype; //将Elemtype定义为int类型
void Joseph(int code[],int n)
{//通过一维数组code带入n个人手中的密码,n是开始就坐在桌旁的人数
SEQLIST people;
int temp,m; //m是报数的上限值
scanf("%d",&m); //输入最初的m
if(InitList(&people)==ERROR) exit ERROR;
for(i=1;i<=n;i++)
if(ListInsert(&people,i,code[i-1])==ERROR) exit ERROR;
position=0; //记录当前报数人的编号
for(i=1;i<=n;i++)
{
count=0; //记录当前所报的数目
do{ //报数
position=(position+1)%n;
GetElem(people,position,&temp);
if(temp>0) count++;
}while (count!=m);
printf("%d",position); //输出当前离开桌旁人的编号
GetElem(people,position,&m);
people.elem[position-1]=-people.elem[position-1]; //将密码变为负值
}
}
(4)链式存储结构
使用一个不带头结点的循环单链表结构。结点结构为:
用C语言定义
typedef struct{ //循环链表中每个结点的数据域部分的类型
int No; //编号
int code; //密码
}INFO;
typedef INFO Elemtype;
算法
void Joseph(int code[],int n)
{
LINK_LIST people;
INKLIST *position,*pre; //position指向当前报数的结点
if(InitList(&people)==ERROR) exit ERROR; //初始化链表people
for(i=1;i<=n;i++) //以n个人的信息为数据域内容向链表插入n个结点
if(ListInsert(&people,i,code[i-1])==ERROR) exit ERROR;
position=people.head; //让position指向最后一个结点,以便报数从第一个开始
while(position->next!=people.head)
position= NextElem(people,position);
scanf("%d",&m); //输入最初的m
for(i=1;i<n;i++){
count=0; //报数处理
do{
position=NextElem(people,position);
count++;
}while (count!=m);
printf("%d",position->elem.No); //离开桌子处理
m=position->elem.code;
pre=PriorElem(people,position);
pre->next=position->next;
free(position);
position= pre;
}
printf("%d",position->elem.No); //处理最后一个人
free(position);
}