大话数据结构-线性表③
一、开场白
数据结构中最简单的一种数据结构,类似我们军训时的按照特定的次序进行排队。
二、线性表的定义
定义:零个或多个数据元素的有限序列。
-
在线性表中,除了第一元素无前驱元素,和最后一个元素无后继元素,其它每个元素有且只有一个前驱和后继。
举星座线性表例:
-
线性表元素的个数n(n>=0)定义为线性表的长度,当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的元素个数
endADT
线性表的基本操作与其他数据结构的基本操作基本相似。其它复杂的操作一般都是由以上基本操作组合来实现的
四、线性表的顺序存储结构
定义:线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
- 顺序存储方式,可以用一维数组来实现顺序存储结构。
- 线性表的顺序存储的结构代码
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
ElemType data[MAXSIZE]; /* 数组存储数据元素,最大值为MAXSIZE */
int length; /* 线性表当前长度 */
}SqList;
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
- 线性表的最大存储容量:数组长度MaxSize。
- 线性表的当前长度:length。
- 在任何时刻,线性表的长度应该小于等于数组的长度。
- 地址计算方法,存储器中的每个存储单元都有自己的编号,这个编号称为地址。
第i位的地址 = 首地址 + (i-1) * 存储单元;
五、顺序存储结构的插入与删除
- 获得元素操作
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/* Status是函数的类型,其值是函数结构状态代码,如OK等 */
/* 初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)*/
/* 操作结果:用e返回L中第i个数据元素的值 */
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;
}
注意这里返回值类型Status是一个整型,返回OK代表1,ERROR代表0。
2. 插入操作
算法思路如下:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置i处;
- 表长加1;
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的元素e,L的长度加1*/
Starus 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++; /*线性表长度加1*/
return OK;
}
注意:C 语言数组中的数组是从0开第一个下标,于是线性表的第i个元素存储在数组的下标为i-1的位置😀😀😀
3. 删除操作
算法思路如下:
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长减1;
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqLite *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],删除的位置在前面,移动的元素多。
4. 线性表顺序存储结构的优缺点
优点:
①无须为表示表中元素之间的逻辑关系而增加额外的存储空间;
②可以快速地存取表中任一位置的元素;
缺点:
①插入和删除操作需要移动大量的数据;
②当线性表长度变化较大时,难以确定存储空间的容量;
③造成存储空间的“碎片”;
六、线性表的链式存储结构
-
定义
为了表示每个数据元素与其直接后继数据元素之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。n个结点链接成一个链表,即为线性表的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表
链表中第一个结点的存储位置叫做头指针
在单链表的第一个结点前附设一个结点,称为头结点。
-
头指针与头结点的异同
头指针 | 头结点 |
---|---|
①头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针 | ①头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可以存放链表的长度) |
②头指针具有标识作用,所以常用头指针冠以链表的名字 | ②有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了 |
③无论链表是否为空,头指针均不为空。头指针是链表的必要元素 | ③头结点不一定是链表的必要元素 |
- 线性表链式存储结构代码描述
结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
/*线性表的单链表存储结构*/
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的数据。
实现代码如下:
/*初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
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],而顺序存储结构读取的时间复杂度为O[1]。
八、单链表的插入与删除
1.单链表的插入
单链表第i个元素插入结点算法思路如下:
- 声明一指针p指向链表头结点,初始化j从1开始;
- 当j < i 时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累计1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,在系统中生成一个空结点s;
- 将数据元素e赋值给 s->data;
- 单链表的插入标准语句 s->next = p->next; p->next = s;
- 返回成功;
实现代码如下:
/*初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)*/
/*操作结果:在L中第i个结点位置之前插入新的元素e,L的长度加1*/
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j; /**/
LinkList p,s; /*声明p,s指针*/
p = *L; /*让p指向链表L的头指针,而读取时指向链表L的第一个结点*/
j = 1; /*j为计数器*/
while(P && j<i) /*寻找第i-1个结点,而读取时寻找第i个结点*/
{
p = p->next; /*让p指向下一个结点*/
j++;
}
if(!p || j>i) /*寻找不到第i-1个结点*/
return ERROR;
s = (LinkList)malloc(sizee(Node)); /*开辟堆空间,生成新结点*/
s->data = e; /*把插入的数据e保存到结点s的数据域*/
s->next = p->next; /*将p的后继结点赋值给s的后继,即先接新结点s的尾巴*/
p->next = s; /*将s赋值给p的后继,然后接新结点s的头*/
return OK;
}
图片演示:
malloc标准函数,它的作用是为新结点开辟堆空间,可使用free函数释放其内存空间
2. 单链表的删除
单链表第i个元素删除结点算法思路如下:
- 声明一指针p指向链表头指针,初始化从j从1开始;
- 当j < i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句p->next=q->next(本来p->next = p->next->next一步搞定);
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回成功;
实现代码如下:
/*初始条件:顺序线性表已存在,1<=i<=ListLength(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); /*释放q的内存,让系统回收此结点*/
returun OK;
}
free标准函数,它的作用是释放堆空间,让系统回收一个Node结点
单链表的结点(大量)插入和删除的时间复杂度都是O[1],对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
九、单链表的整表创建
单链表整表创建算法思路如下:
- 声明一指针p和计数器变量i;
- 初始化一空链表L;
- 让L的头结点的指针指向NULL,即创建一个带头结点的单链表;
- 循环:①生成一新结点赋值给p;②随机生成一数字赋值给p的数据域p->data;③将p插入到头结点与前一新结点之间。
(头插法)实现代码如下:
/*随机产生n个元素的值,建立带头结点的单链线性表L(头插法)*/
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; /*插入到表头*/
}
}
rand()函数可以用来产生随机数,但是这不是真真意义上的随机数,是一个伪随机数。一般srand的参数用time(NULL)和time(0),因为系统的时间一直在变,所以rand()获得的数,也就一直在变,相当于是随机数了
(尾插法)实现代码如下:
/*随机产生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; /*表示当前链表结束*/
}
注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表。
图片演示:
r->next = p;
r = p; r移动到尾结点
十、单链表的整表删除
单链表整表销毁算法思路如下:
- 声明一结点p和q;
- 将第一个结点赋值给p;
- 循环:①将下一个结点赋值给q;②释放p;③将q赋值给p。
销毁单链表实现代码如下:
/*初始条件:顺序线性表L已存在,*/
/*操作结果:将线性表L重置为空表*/
Status ClearList(LinkList *L)
{
LinkList p,q;
p = *L->next; /*p指向第一个结点*/
while(p) /*遍历到表尾*/
{
q = p->next; /*q指向p下一个结点*/
free(p); /*释放p*/
p = q; /*重新把q赋值给p*/
}
(*L)->next = NULL; /*头结点的指针域为空*/
return OK;
}
q的作用是记录p的下一个结点,记录好p的位置然后把p的内存销毁掉,在向下一个重复相同的动作。
十一、单链表结构与顺序存储结构优缺点
1.若线性表需要频繁的查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
2.当线性表中的元素个数变化较大或者根本不知道多大时,最好采用单链表结构,这样可以不需要考虑存储空间的大小问题。
十二、静态链表
使用数组描述的链表叫做静态链表。
/*线性表的静态链表存储结构*/
#define MAXSIZE 1000 /*假设链表的最长长度是1000*/
typedef struct
{
ElemType data;
int cur; /*游标(Cursor),为0时表示无指向*/
}Component,StaticLinkList(MAXSIZE);
把没使用的数组元素称为备用链表,下标为0的第一个元素的cur存放备用链表的第一个结点的下标,数组最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点的作用。
静态链表初始化实现代码如下:
/*将一维数组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].cur = 0; /*目前静态链表为空,最后一个元素的cur为0*/
return OK;
1.静态链表的插入操作
静态链表获取一个空闲位置实现代码如下:
/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur;/*当前数组第一个元素的cur存的值*/
/*就是要返回的第一个备用空闲的下标*/
if(space[0].cur)
space[0].cur = space[i].cur;/*由于要拿出一个分量来使用了,所有我们*/
/*就得把它的下一个分量用来做备用*/
return i;
}
该代码的作用返回备用链表的第一个下标值,更新数组的第一个元素的cur值。
在静态链表第i个元素插入新的元素e,实现代码如下:
/*在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个位置插入“丙”,
2. 静态链表的删除操作
在静态链表删除第i个元素,并返回删除元素的值,实现代码如下:
Status ListDelete(StaticLinkList L,int i)
{
int j,k;
if(i<1 || i>ListLength(L))
return ERROR; /*删除i的位置不在静态链表范围内*/
k = MAX_SIZE - 1; /*注意k首先是最后一个元素的下标*/
for(j=1;j<=i-1;j++) /*找到第i个元素的前一个位置(i-1)的下标*/
k = L[k].cur; /*k保存第(i-1)的游标,即i个位置的下标*/
j = L[k].cur; /*j保存着第i个元素的游标,第(i+1)个元素的下标*/
L[k].cur = L[j].cur; /*把i的游标赋值给i-1*/
Free_SSL(L,j); /*将下标为k的空闲结点回收到备用链表*/
return OK;
}
/*将下标为k的空闲结点回收到备用链表*/
void Free_SSL(StaticLinkList space,int k)
{
space[k].cur = space[0].cur;/*把第一个元素cur值赋值给要删除的分量cur*/
space[0].cur = k; /*把要删除的分量下标赋值给第一个元素的cur*/
}
/*初始条件:静态链表L已存在,操作结果:返回L中数据元素个数*/
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur; /*保存的静态链表的第一个元素的下标*/
while(i)
{
i=L[i].cur; /*如果游标为0,说明下一位数据为空;否则往后遍历*/
j++;
}
return j; /*返回静态链表L的长度*/
}
3. 静态链表优缺点
优点:
在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了顺序存储结构中的插入和删除操作需要移动大量元素的缺点;
缺点:
①没有解决连续存储分配带来的表长确定的问题;
②失去了顺序存储结构随机存取的特性。
**静态链表是为了给没有指针的高级语言设计的一种实现单链表能力的方法,应理解其思想,以备不时之需。**😂
十三、循坏链表
将单链表中终端节点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表
带头结点的空循坏链表:
非空的循环链表:
合拼两个循环链表A和B,实现代码:
p=rearA->next; /*保存A表的头结点,即 ① */
rearA->next=rearB->next->next;/*将本是指向B的第一个结点,赋值给rearA->next,即 ②*/
q=rearB->next;
rearB->next=p; /*将原A表的头结点赋值给rearB->next,即 ③*/
free(q);
十四、双向链表
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; /*直接前驱指针*/
struct DuLNode *next; /*直接后继指针*/
}DulNode,*DuLinkList;
双向链表的循环带头结点的空链表:
非空的循环的带头结点的双向链表:
假设存储元素e的结点为s,要实现s插入到p结点的后面,实现代码:
s->prior = p; /*把p赋值给s的前驱,如图中的 ① */
s->next = p->next; /*把p->next赋值给s的后继,如图 ② */
p->next->prior = s; /*把s赋值给p->next的前驱, 如图 ③*/
p->next = s; /*把s赋值给p的后继,如图中 ④ */
删除结点双向链表的结点p,实现代码:
p->prior->next = p->next; /*把p->next赋值给p->prior的后继,如图中 ①*/
p->next->prior = p->priot; /*把p->prior赋值给p->next的后驱,如图中 ②*/
free(p); /*释放结点p*/
在单链表的基础上添加了prior指针,增大内存,提高算法的效率。
十五、总结回顾
先是线性表的定义,零个或多个具有相同类型的数据元素的有限序列,然后讲顺序存储结构,其和删除操作不方便,引入了链式结构;然后在单链表的基础上衍出循环链表、双向链表;另外还讲了不使用指针如何处理链表结构的静态链表。
注意点:
①数组的下标是从0开始的,在实现代码极易出错;
②在实现算法前,先使用图表把算法画出来,在加深理解的基础上,还不易出错;
③顺序表的顺序和链式实现方法各有优缺点,在实际应用中,根据需求使用;
线性表的这两种结构对后面其它结构起着至关重要的作用。