2 数据结构 线性表

320 阅读20分钟

考纲要求 💕

(二)线性表:

1.线性表的逻辑结构定义、抽象数据类型定义和各种存储结构的描述方法

2.在线性表的两类存储结构(顺序存储和链式存储)上实现基本操作

3.一元多项式的抽象数据类型定义、表示及加法的实现

主要还是关注分成顺序表和链表 来实现基本操作,可能会有编程题


知识点

简单地说,线性结构是 n 个数据元素的有序(次序)集合,它有下列几个特征:

  • 集合中必存在唯一的一个 "第一个元素";

  • 集合中必存在唯一的一个 "最后的元素";

  • 除最后元素之外,其它数据元素均有唯一的 "后继";

  • 除第一元素之外,其它数据元素均有唯一的 "前驱"。

  • 线性表 - 顺序表 - 链表

▶️ 1. 线性表定义 ✨

如下方逻辑图:


图片.png


参考文章:# (王道408考研数据结构)第二章线性表-第一节:线性表的定义和基本操作

1.1 定义 ✨

它是最常用且最简单的一种数据结构。

图片描述

线性表是一个含有 n≥0 个结点的有限序列,对于其中的结点,有且仅有一个开始结点没有前驱但有一个后继结点,有且仅有一个终端结点没有后继但有一个前驱结点,其它的结点都有且仅有一个前驱和一个后继结点。

若将线性表记为(a1,...,ai-1,ai,ai+1,...,an),则表中 ai-1 领先于 ai,ai 领先于 ai+1,称 ai-1 是 ai 的直接前驱元素,ai+1 是 ai 的直接后继元素。

图片.png

线性表元素的个数 n(n>=0)定义为线性表的长度,当 n=0 时,称为空表。

1.2 基本操作

不管是线性表的哪一种实现方式(例如顺序表和单链表),起码要保证以下操作

  • Initlist(&L):初始化表
  • Destory(&L):销毁
  • ListInsert(&L,i,e):插入
  • ListDelete(&L,i,&e):删除
  • LocateElem(L,e):按值查找
  • Length(L):求表长
  • Printlist(L):输出操作
  • Empty(L):判空

1.3 按存储方式分为顺序表和链表 ✨

根据存储方式不同,线性表可以分为顺序表和链表:

  • 数据元素在内存中集中存储,采用顺序表示结构,简称 “顺序存储”
  • 数据元素在内存中分散存储,采用链式表示结构,简称 “链式存储”

图片描述


接下来,我们将分别学习线性表的顺序存储和链式存储——顺序表和链表,以及在两种存储方式下如何实现基本操作。


▶️ 2. 顺序表 ✨✨

图片.png


2.1 ❗ 顺序表的表示和实现

表示

线性表的顺序表示指的是用物理上的一段连续的地址来存储数据元素,如下图所示。如果第一个元素的在内存上的地址为 a1,每个元素占用的空间是 l,那么第 n 个元素的地址就是 a1 + (n - 1) * l

2-2.1-1

只要确定了第一个元素的地址,那么我们可以对线性表中的任一元素随机存取,由于编程语言中的数组也有随机存取的特点,下面就用数组来描述线性表的顺序存储结构。


下面我们分步讲解


❗ 2.1.1. 结构定义

首先,我们需要定义一个线性表。

有二种方法,一种是静态的长度固定,一种是动态的长度可增

2.1.1.1 (不考虑) 静态分配—长度固定

线性表中存储的元素由 Elemtype 指定,typedef int Elemtype; 指定这里是存储 int 类型的元素。
这样方便一些应用,Elemtype就是存储的线性表结点的元素类型

#define Maxsize 10  //定义最大长度
typedef int ElemType //表元素类型 int

typedef struct    //定义表结构 用结构体 命令为sqList
{
	ElemType data[Maxsize];//定长数组
	int length;//有效数据个数,也就是数组长度
}SqLiSt;



//sqList 就是这个表,其实放前面也行,但是使用时候要加上struct
//参考:[结构体定义 typedef struct 用法详解和用法小结]
// https://blog.csdn.net/mpp_king/article/details/70229150

结构体定义 typedef struct 用法详解和用法小结

但是长度不能变,所以不考虑使用

2.1.1.2 ❗ (好)动态分配

首先讲解下 分配空间的函数

图片.png

然后使用它来分配

typedef int ElemType //表元素类型 int

typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; 
  • Maxsize记录顺序表最大容量,也就是当前情况下开辟了多少空间;
  • length是实际数据长度

参考:# (王道408考研数据结构)第二章线性表-第二节1:顺序表的定义


❗ 2.1.2 初始化和销毁

上面已经定义好了结构,这里使用动态分配的方式:

#include <stdio.h>

#define init_size 4 //定义初始长度
typedef int ElemType //表元素类型 int```
#define MaxSize 10 //定义最大长度


typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; 

main函数内实例化一个SeqList L以供测试:

int main()
{
  SeqList L; //一个顺序表
}

❗ 2.1.2.1 初始化

使用 InitList 来初始化,其中

  • 初始化长度用 #define init_size 4 //定义初始长度
  • 由于指数初始化,顺表内部没有任何有效元素,所以length暂时为0
//基本操作--初始化一个顺序表 L
void InitList(SqList *L){
	//用malloc函数申请一片连续的存储空间
	L.data=(int*)malloc(init_size*sizeof(ElemType));
	L.length=0;
	L.MaxSize=init_size;
}

差不多是上面这样

main函数中调用:

int main()
{
  SeqList L; //一个顺序表

InitList(&L)
 //&L:获得L的存储地址,将此存储地址传给被调函数中的指针变量*L,
 //从而获取到被调函数改动后的L(所以可以理解为"回传")

}

增加长度

如果想要增加长度咋办呢?我们可以通过二种方法:

    1. 改变结构上的初始长度 init_size
    1. 增加一个增加长度函数,malloc扩容空间
//增加数组len长度 
void IncreaseSize(SeqList *L,int len) 
//此函数对L作出了改动,需要"回传"给主调函数,所以使用指针类型*L 因为是C语言,c++用&

{
	int* p=L.data; //int *指针型
	L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));
        
   for (int i = 0; i<L.length; i++) //把以前的给复制到现在的空间中,顺序不变
   {
   	 L.data[i]=p[i];
   }
   
   L.MaxSize=L.MaxSize+len;
   free(p);
   
}

main函数中调用:

int main()
{
  SeqList L; //一个顺序表

InitList(&L)
 //&L:获得L的存储地址,将此存储地址传给被调函数中的指针变量*L,
 //从而获取到被调函数改动后的L(所以可以理解为"回传")

IncreaseSize(&L,5);
}

❗ 2.1.2.2 销毁

跟初始化差不多,但是相反操作

void DestoryList(SeqList *L)
{
    free(L.data);
    L=NULL;
    L.MaxSize=L.lengt=0;
}


总算完成了上面的一个顺序表的基本,下面来试试其他操作,里面的数据操作


❗ 2.1.3 插入

  • 插入元素

这里我们给出一个插入元素的示例:

图片描述

其代码实现如下:

//顺序表的基本操作--插入
void Listinsert(SqList *L,int i,int e)
{
	if (i<1||i>L.length+1) //判断i的范围是否有效
	return false;
   if(L.length>=MaxSize) //当前空间已满,不能·插入
   	return false;
   
	for (int j = L.length; j >= i; j++)//将i后面的元素向后移
	   L.data[j]=L.data[j-1];

	   L.data[i-1]=e;//在位置i处放入e
	   L.length++;
    return true;
}

✨时间复杂度

知道这个操作需要的时间复杂度

因此,顺序表插入操作时间复杂度为:

  • 最好情况:新元素插入到表尾,不需要移动元素,为O(1)
  • 最坏情况:新元素插入到表头,需要n那个元素,为O(n)
  • 平均情况:假设新元素插入到任何一个位置的概率相同,即i= 1,2,3,... , length(n+1)概念都是P=1/n+1.
    i= 1,循环n次;
    i=2时,循环n-1次;
    i=3,循环n-2次......
    i=n+1时,循环0次
    平均循环次数= np +(n-1)p + (n-2)p +...+ 1.p = n(n+1)/2
    →平均时间复杂度=o(n)

❗ 2.1.4 删除

  • 删除元素

这个我们给出一个删除元素的示例:

图片描述

其代码实现如下:

//顺序表删除元素 返回bool型,e是删除的元素值,通过引用指针*e传回去(c语言)
bool ListDelete(SqList *L,int i,int *e){
	if(i<1||i>L.length) //判断i的范围是否有效
		return false; 
                
	e=L.data[i-1]; //将被删除的元素赋值给e
	for (int j = i; j < L.length; j++) //将i后面的前移1
	{
		L.data[j-1]=L.data[j];
	}
	L.length--;
	return true;
	
}

下面咱们继续搞,查找元素


❗ 2.1.5 查找

这里有二种查找方法,一种是遍历查找,也就是常规的顺序查找。还有一种就是二分查找,查找中间,比较大小。


2.1.5.1 顺序查找

LocateElem(L,e): 按值查找。在表L中查找具有给定关键字值的元素.返回下标

//在顺序表中查找一个元素等于e的元素,返回其位置
int LocateElem(Sqlist *L,int e){
    for(int i=0;i<L.length;i++)
        if(L.data[i]==e)
            return i+1;//位置
    return 0;
}
✨ 时间复杂度

图片.png


❗ 2.1.5.2 二分法查找

需要注意的是使用二分查找法必须先进行排序

int  SeqListFindvalue_Bind(SeqList* L, int pos)//二分查找法(前提要排好序)
{
	int low = 0;
	int high = L->length - 1;
	while (low < high) 
	{
		int mid = (low + high) / 2;
		if (pos < L->data[mid]) //pos值小于中间值,在mid左边
			high = mid - 1;
		else if (pos>  L->data[mid])  //pos值大于中间值,在mid右边
			low = mid + 1;
		else                        //直到等于,找到他
			return mid;
	}
}
✨ 时间复杂度

二分查找就是查找中间,比较大小,然后把low,high ==mid 这样就可以在划分中间,分配大小 image-20220202230439697
视频:youtu.be/JuDAqNyTG4g
类似这样youtube很多教程youtu.be/RH3tZldhjJ0

二分法的关键思想是 假设该数组的长度是N那么二分后是N/2,再二分后是N/4……直到二分到1结束(当然这是属于最坏的情况了,即每次找到的那个中点数都不是我们要找的),那么二分的次数就是基本语句执行的次数,于是我们可以设次数为xN*(1/2)^x=1;则x=logn,底数是2


这样也完成了插入删除和查找等

下面最后给他打印出来:遍历


2.1.6 打印

void SeqListPrint(SeqList* L)
{
	for (int i = 0; i < L->Length; i++)
	{
		printf("%d ", L->data[i]);
	}
	printf("\n");
}

这个章节顺序表算是讲解清楚了,下面看链表


▶️ 3. 链表 ✨✨

图片.png

图片.png

图片.png


✨ 为什么要设计链表(与顺序表相比)

线性的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的

图片.png

这里顺序表是连续的空间,插入修改不方便,时间复杂度高,单链表采用指针指向的方式,插入修改不需要移动大量空间,而只需要修改指针,但是单链表必须有从头结点开始查找你要的指针,所以可能失去随机存取的优点


✨ 链表的种类 单\循环\双向

通常链式存储结构会有一个个结点组成,结点中包含两个域一个是数据域,一个是指针域数据域中存储数据,指针域中存储下一个后继元素的地址,如下图所示,这一个个结点组成链表,也称线性链表或单链表。

2-2.2-1

    1. 单链表的逻辑结构如下图所示:

2-2.2-2

除了单链表之外还有循环链表和双向链表,循环链表的特点是最后一个结点的指针指向头结点,形成一个环,双向链表的特点是结点中多了一个指向前驱元素的指针,这两种链表的逻辑结构如下面两张图所示

循环链表

2-2.2-3

双向链表

2-2.2-4


问题很多,我们一个一个来分析,先分析单链表


3.1 ❗ 单链表

❗ 3.1.1. 结构定义

首先,我们需要定义一个链表。

ElemType data; 定义了数据域(存储数据),struct LNode *next; 定义了指针域(存储数据元素之间的关系)。

定义如下:

typedef int ElemType;

typedef struct LNode{
    ElemType data;
    struct LNode *next;  
}LNode, *LinkList;  //*LinkList 等价于 struct LNode *LNode ,主要是给人看的

其中,*LinkListLNode 类型的指针。当我们定义 LNode *p 时,可以使用 LinkList p 进行替换。


❗ 3.1.2 初始化

有二种形式,就是带头不带头结点

图片.png


测试用例的main函数如下:

void test()
{
    LinkList L;
     InitList(L);
}

3.1.2.1 不带头结点

对于不带头结点的单链表来说,整个单链表只有一个指针标识,等于初始化只有个空表,只有一个头指针。

图片.png

//初始化一个空的单链表
bool InitList(LinkList& L)//注意一定传入引用,不然会发生值传递(不修改main中L),里面的指针在进行赋值
//相当于 LinkList& L = LNode * &L 
{
	L=NULL;//表明此时是一个空表
	return true;
 }

❗ 3.1.2.2 带头结点

步骤如下:

  1. 生成新结点作头结点,用头指针 L 指向头结点
  2. 头结点的指针域置空(确保后面没结点)

就是需要先申请一个LNode的空间(EleType和LNode* next)一个结点包括一个数据和next结点
确保里面的L->next后面还没有数据 (初始化时候)

//初始化一个带头结点的单链表
bool InitList(LinkList &L){
	L=(LNode *)malloc(sizeof(LNode));//分配一个头结点
	if(L=NULL)    //内存不足,分配失败
		return false;
                
	L->next=NULL;//头节点后面暂时没有节点
	return true;
}

这里没有用LinkList代替是因为:

"LinkList”等价于“LNode*" 前者强调这是链表,后者强调这是结点 合适的地方使用合适的名字,代码可读性更高


❗ 3.1.3 销毁链表

销毁链表的算法步骤:

  1. 创建一个新的空表
  2. 释放原链表的空间
  3. 将空表赋值给原来的链表
void DestroyList(LinkList &L){
    LinkList temp; //创建新的空表
    while (L!=NULL){ //不等于空
        temp = L->next;
        free L;
        L = temp; //一个一个结点的消去
    }
}

main函数中这样写:

void test()
{
    LinkList L;
     InitList(L);
     DestroyList(L)
}

❗ 3.1.4 插入

单链表插入的演示(在第 3 的位置插入元素 90):

图片描述

还是分状态的,带头还不带头结点的,先分析带头的

插入(带头结点)

图片.png

一步一步说起:


  1. 新建一个结点p指向第一个结点
//在第i个位置插入元素e(带头结点)
bool  ListInsert(LinkList &L,int i,int e){
	if(i<1)
		return false;

	LNode *p;//指针p指向当前扫描到的结点
	int j=0; //当前p指向的是第几个结点
	p=L;  //L指向头节点,头节点是第0个节点(不存数据)
	

这时候初始是这样的,p为链表结点指针,指向头节点,未存数据

图片.png


  1. 遍历链表,查找成功,使用s->next=p->next;p->next=s 插入p
	while (p!=NULL&&j<i-1)   //循环找到i-1个结点
	{
		p=p->next;
		j++;
	}
        
        if(p->next==NULL || j>i) //找不到,如果 找到最后一个,或者指向的j超过i
		return false;
                
	LNode *s=(LNode *)malloc(sizeof(LNode)); //新建一个结点空间s
	s->data=e;
	s->next=p->next;
	p->next=s; //将结点s连到p之后
	return true; //插入成功

图片.png

千万注意,这个过程不能交换,如果先p->next=s,再s->next=p->next,那么执行时相当于就是s->next=s

这个代码扫描一圈 时间复杂度为O(n)


下面分析不带头结点的


插入(不带头结点)

也没啥,就是新建p结点,第一次指向的应该变为第一个结点了

图片.png

所以只用修改新建p,查找那时候的j,就可以了

int j=1; //当前p指向的是第几个结点 初始为第一个

但是要对i=1时候进行讨论

  • i=1时候: 图片.png
//在第i个位置插入元素e(不带头结点)
bool  ListInsert(LinkList &L,int i,int e){
	if(i<1)
		return false;
	if(i==1){//插入第1个节点位置与其他的操作不同
		LNode *s=(LNode *)malloc(sizeof(LNode));
		s->data=e;
		s->next=L; //指向第一个
		L=s; //头指针指向s 新节点
		return true;
	}
        
	//i不是1的情况如下:
	LNode *p;//指针p指向当前扫描到的结点
	int j=1; //当前p指向的是第几个结点 初始为第一个
	p=L;  //p指向第1个节点(不是头节点)
		while (p!=NULL&&j<i-1)   //循环找到i-1个结点
	{
		p=p->next;
		j++;
	}
	 if(p->next==NULL || j>i) //找不到,如果 找到最后一个,或者指向的j超过i
		return false;
                
	LNode *s=(LNode *)malloc(sizeof(LNode)); //新建一个结点空间
	s->data=e;
	s->next=p->next;
	p->next=s; //将结点s连到p之后
	return true; //插入成功
}

❗ 3.1.5 删除

单链表删除元素的演示(删除第 5 个位置的元素):

图片描述

跟插入差不多,遍历查找,找到后,最重要的还是那个删除语句:p->next=q->next;

//删除第i位结点,返回它元素值
bool ListDelete(LinkList L,int i,int& e)
{
	if(i<1)
		return false;
	LNode *p; //指针p指向当前扫描到的结点
	int j=0;  //当前p指向的是第几个结点
	p=L;      //L指向头节点,头节点是第0个结点(不存数据)
	while (p!=NULL&&j<i-1)   //循环找到i-1个结点
	{
		p=p->next;
		j++;
	}
	if(p->next==NULL || j>i) //找不到,如果 找到最后一个,或者指向的j超过i
		return false; 
                
	LNode *q=p->next;//q指向被删除结点i
	e=q->data;//e返回被删除结点元素的值、
	p->next=q->next;  //将*q从链表中断开
	free(q);
	return true;  //删除成功
}

图片.png

然后p->next=q->next; //将*q从链表中断开

直接跳过要删除的q(即i结点),但是此时的q指向i结点的,得出e元素值

最后free q,等于删除i结点


❗ 3.1.6 单链表的查找

3.1.6.1 按位查找

图片.png

LNode* GetElem(LinkList L,int i)
{
	if(i<0)
		return NULL;
                
	LNode* p;//用于扫描
	int j=0;//当前p指向的是第几个结点
	p=L;//首先指向头结点
	while(p!=NULL && j<i)
	{
		p=p->next;
		j++;
	}
	return p;
}


3.1.6.2 按值查找

图片.png

LNode* LocateElem(LinkList L,DataType e)
{
	LNode* p=L->next; //指向第一个结点
	while(p!=NULL && p->data!=e)
	{
		p=p->next;
	}
	return p;
}

❗❗ 3.1.7 单链表创建(头插\尾插)

这里有二个重要的方法:头插法尾插法

跟上面不一样的是,他是通过建立单链表来实现的,其实跟插入差不多,下面稍微讲解下


❗ 3.1.7.1 尾插法

如名字,是把新来的数据插到尾部

假设一组数组,用这些数据以尾插建立单链表

int arr[]={2,12,35,7,344,563,456,234,2346,7}
void CreateListTail(LinkList& L,,int* arr,int n,DataType e)
//arr用于接收数组
//n表示数组长度
{
	LNode* p,r;//使用一个r始终指向链表最后一个结点
	int i;
	
	L=(LNode*)malloc(sizeof(LNode)); //新建一个结点空间(头结点)
	r=L;//此时头结点就是最后一个结点
	for(int i=0;i<n;i++)
	{
		p=(LNode*)malloc(sizeof(LNode));
		p->data=arr[i];
		r->next=p;
		r=p;//r始终指向尾节点
	}
	r->next=NULL;//最后指向空,代表链表结束
	
}

图片.png


❗ 3.1.7.2 头插法

就是从最前面一个一个加进去

假设一组数组,用这些数据以头插法建立单链表

int arr[]={2,12,35,7,344,563,456,234,2346,7}
void CreatListHead(LinkList& L,int* arr,int n,DataType e)
//arr用于接受数组
//n表示数组长度
{
	LNode* p;
	int i;
	L=(LNode*)malloc(sizeof(LNode));
	L->next=NULL; //头结点后面是空
	for(int i=0;i<n;i++)
	{
		p=(LNode*)mallco(sizoef(LNode));
		p->data=arr[i];
		p->next=L->next;//新插入的结点一定要使其处于第一个结点位置
		L->next=p; //这两句跟插入一样,相当于把p插入L后面
                
	}
}

图片.png


✨✨ 链表的逆置问题

这里要注意下会涉及到链表的逆置问题

图片.png

解决方法:

逆置算法:

LNode* ListReverse(LNode* head)
{
	LNode* cur=head;//用于扫描原链表
	LNode* newhead=NULL;//反转链表后的头

	while(cur)//cur一直扫描
	{
		head=cur;
		cur=cur->next;
		head->next=newhead;
		newhead=head;
	}
	return newhead;
}

图片.png

这样一直扫描到cur==NULL时候,newhead等于最后原先的最后一个结点,逆置


3.2 ❗ 双链表


图片.png


3.2.1 定义

双链表:双链表在单链表的基础上再增加一个指针域,用于指向它的前驱结点

相当于这样:

图片.png

❗ 3.2.2 结构定义

在单链表的基础上加一个指针,指向前驱结点的

typedef int ElemType;

typedef struct DNode
{
	DataType data;
	struct DNode* prior;//指向前驱结点
	struct DNode* next;//指向后继结点
}DNode, *LinkList;

因此对于链表中的某一个结点p,其前驱结点就是p->prior,其后继结点就是p->next,也会有下面的等式成立

p->next->prior=p=p->prior->next

更细一点的图是这样的:

图片.png

下面来实现它的操作


❗ 3.2.3 初始化

测试函数实例化一个双链表后,将其传入

void test()
{
	LinkList L; 
	InitList(L);
}

初始化代码如下:

bool InitDLinkList(LinkList &L)
{
 	L=(DNode*)malloc(sizeof(DNode));//分配一个头结点
 	if(L==NULL) 
 		return false;
                           
 	L->prior=NULL;
 	L->next=NULL;
	return true;/* 只有个头结点*/
}

这是带头结点的,如果不要头结点就是

bool InitDLinkList(LinkList &L)
{
      L=NULL;//表明此时是一个空表,只有一个L头指针
	return true;
}

下面再来讲下 它的一些操作,比如插入,查找,等


❗ 3.2.4 插入

图片.png

这里要注意,我们需要将 p结点的next指向s结点, s的prior指向p结点,p->next结点的prior结点指向e结点, s结点的next结点指向p->next结点

图片.png

特别注意:p->next=NULL时要特殊处理, 这时候相当于p是头\尾结点

bool InsertNextNode(LinkList &L,int i,int e) //i是插入位置,e是元素值
{
	
   if(i<1)  return false;

	LNode *p;//指针p指向当前扫描到的结点
	int j=0; //当前p指向的是第几个结点
	p=L;  //L指向头节点,头节点是第0个节点(不存数据)
        
       
        while (p!=NULL&&j<i)   //循环找到i个结点
	{
		p=p->next;
		j++;
	}
       
                
	LNode *s=(LNode *)malloc(sizeof(LNode)); //新建一个结点空间s
	s->data=e;
        
        if(p->next=NULL) 
        {
         s->prior=p;
        s->next=p->next;
         p->next=s;
        }
        
        s->prior=p;
        s->next=p->next;
        p->next->prior=s;
        p->next=s;
    
}
  • 这里考虑p->next=NULL的情况:

特别注意:p->next=NULL时要特殊处理, 这时候相当于p是头\尾结点 这个不能执行 p->next->prior=s;


❗ 3.2.5 删除

假设要删除结点p

  • ①:p->prior->next=p->next;
  • ②:p->next->prior=p->prior
图片.png

特别注意当被删除结点p为最后一个结点 ,这时候p->prior->next=p->next; 就行了,不执行p->next->prior=p->prior

//删除第i位结点,返回它元素值
bool ListDelete(LinkList &L,int i,int& e)
{
	if(i<1)
		return false;
	LNode *p; //指针p指向当前扫描到的结点
	int j=0;  //当前p指向的是第几个结点
	p=L;      //L指向头节点,头节点是第0个结点(不存数据)
	while (p!=NULL&&j<i)   //循环找到i个结点
	{
		p=p->next;
		j++;
	}

        e=p->data;//e返回被删除结点元素的值
	
        if(p->next=NULL) //如果p是最后一个结点
        {p->prior->next=p->next;}
        
        p->prior->next=p->next;
        p->next->prior=p->prior;
        
	free(p);
	return true;  //删除成功
}

算是把双向链表简单介绍了一下,其他的操作比如查找跟单链差不多


3.3 ❗ 循环链表


图片.png

3.3.1 定义(循环单\双链表)

循环链表:规定好头尾结点的指向形成成环状

  • 循环单链表:其尾节点的next指针由原本的空改为指向头结点
  • 循环双链表:其尾节点的next指针由原本的空改为指向头结点,同时头结点的prior指针指向尾节点

❗ 3.3.2 循环单链表

  • 循环单链表:其尾节点的next指针由原本的空改为指向头结点

图片.png

  • 对于单链表,如果p是尾节点,一定有p->next=NULL;

  • 对于循环单链表,如果p是尾节点,一定有p->next=Head


3.3.2.1 初始化

注意循环单链表的初始化

图片.png

bool InitList(LinkList &Head)
{
	Head=(LNode*)malloc(sizeof(LNode));
	if(L==NULL)
		return false;
	Head->next=Head;//注意
	return true;
}

因此Head->next=Head成立代表这是一个空表


❗ 3.3.3 循环双链表

  • 循环双链表:其尾节点的next指针由原本的空改为指向头结点,同时头结点的prior指针指向尾节点

图片.png

  • 对于双链表,如果p是尾节点,一定有p->next=NULL;
  • 对于循环双链表,如果p是尾节点,一定有p->next=Head;且有Head->prior=p

3.3.3.1 初始化

注意循环双链表的初始化

图片.png

bool InitDlinkList(LinkList &Head)
{
	Head=(DNode*)malloc(sizeof(DNode));
	if(Head=NULL)
		return false;
	Head->prior=Head;//注意
	Head->next=Head;//注意
	return true;
}

对于循环双链表,如果p是尾节点,一定有p->next=Head;且有Head->prior=p


最后这算是简单介绍了下循环链表,上面各种类型的链表还是要了解清楚的


对了还有个静态链表需要介绍下


3.4 静态链表


图片.png

3.4.1 静态链表定义

静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。

图片.png


3.4.2 结构定义

他的结构描述如下:

#define Maxsize 50 //静态链表的最大长度
typedef struct {  //静态链表结构类型的定义
ElemType data;    //存储数据元素
int next;         //下一个元素的数组下标
}SLinkList[MaxSize];

这是一个结构体数组,有数组下标next,数据元素值

静态链表以next==-1作为其结束的标志

静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便,但在一些不支持指针的高级语言(如Basic)中,这是一种非常巧妙的设计方法。

他也就是用数组的next作为下标,来模拟动态链表,只需要修改下标,就能指向不同元素


❗ 3.5 顺序表与链表的比较


图片.png


具体可见这篇文章:

# 第二章线性表-第三节5:顺序表和链表的比较

运用具体环境,需求选择顺序表还是链表


🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝🌝

祝大家学习快乐,总结.....


参考: