首页 > 其他分享 >【数据结构】链式家族的成员——循环链表与静态链表

【数据结构】链式家族的成员——循环链表与静态链表

时间:2023-12-30 20:00:53浏览次数:40  
标签:结点 单链 表尾 静态 链表 链式 数据结构 指针

循环链表与静态链表

【数据结构】链式家族的成员——循环链表与静态链表_数据结构

导言

大家好!很高兴又和大家见面啦!!!

经过前面的介绍,相信大家对链式家族的成员——单链表与双链表的相关内容都已经熟练掌握了。前面我们重点介绍了通过C语言来实现单链表与双链表的一些基本操作,希望大家私下能够多多练习一下,帮助自己去吸收消化这些内容。

在今天的篇章中,我们要介绍的是线性表的链式存储另外两个成员——循环链表与静态链表,有了单链表与双链表的基础,相信大家应该能够很容易理解今天的内容。接下来我们就来一起看看吧!

一、循环链表

在前面介绍的单链表和双链表中,我们会发现,不管是单链表的表尾结点还是双链表的头结点和表尾结点,它们在创建好后指向的内容都是空指针,如下图所示:

【数据结构】链式家族的成员——循环链表与静态链表_数据结构_02

正因为这种存储结构,导致我们在处理表头元素、表尾元素与表中元素时会有些许的差异,比如:

  • 在双链表中,我们采用后插法插入元素时,就需要判断该结点的后继结点是否为空指针;
  • 在单链表中,如果我们需要找到结点的前驱结点,我们只能通过从表头元素开始查找;

为了完善单链表与双链表的缺点,我们就可以将单链表与双链表做一个调整,如下所示:

【数据结构】链式家族的成员——循环链表与静态链表_C语言_03

我们将单链表的表尾结点的指针域指向了头结点; 将双链表的表尾指针的后继指针指向了头结点,将双链表的头结点的前驱指针指向了表尾结点;

经过这个调整后我们会发现,此时的单链表和双链表都闭合起来了,这样闭合的链表我们将其称为循环链表。接下来我们就来分别介绍一下这两种循环链表相比于之前的改动;

1.1 循环单链表

循环单链表也就是表尾结点的指针域指向的是单链表的第一个结点,而头指针指向的也是单链表的第一个结点,所以我们可以认为,在循环单链表中,表尾结点的指针域指向的是头指针L。

正因为这个点所以在对循环单链表进行判空操作时我们就有了一个改动:

  • 由原先的判断头结点的指针域指向指向的是不是NULL,改为指向的是不是L;

用C语言来表示则是:

//循环单链表的判空
bool Empty(LinkList L)
{
	assert(L);//当指针L为空指针时报错
	if (L->next == L)
		return true;
	else
		return false;
}

不管是单链表还是循环单链表,它们的插入、删除是一致的,唯一的区别就是,我们在对表尾结点的处理上会有差异:

  • 单链表的表尾结点的指针域判断指向的是NULL;
  • 循环单链表的表尾结点的指针域判断指向的是L;

用C语言来表式则是:

//循环链表的表尾结点判断
bool isTail(LinkList L,LNode* p)
{
	assert(L && p);//当指针L与指针p其中一个为空指针时报错
	if (p->next == L)
		return true;//当结点p的后继指针指向L时表明此时的结点p为表尾结点
	else
		return false;//当它们不相等时表明此时的结点p不是表尾结点
}

我们在对单链表进行遍历时,只能是从头结点开始往后进行遍历,但是在循环链表中,我们可以从任意结点往后遍历,用C语言来表示的话我们则可以写成:

//循环链表的遍历
bool Ergodic(LNode* p)
{
	assert(p);//当p为空指针时报错
	LNode* r = p->next;//进行遍历的指针r
	while (r->next != p)//判断结点r的指针域是否指向结点p
	{
		r = r->next;//往后进行遍历
	}
	return true;//完成遍历返回true
}

在单链表中,我们要想从头结点找到表尾结点的话,我们需要从头开始进行遍历,此时的时间复杂度为O(n);但是在循环链表中,我们如果想通过表尾结点找到头结点的话,此时的时间复杂度则为O(1)。

由这个点,我们如果想对头结点或者表尾结点进行一些操作的话,我们则可以设置表尾指针r,这样我们就可以通过表尾指针来找到头指针,用C语言表示则是r->next即为头指针,这样我们要对表尾结点或者头结点进行插入或者删除元素的时间复杂度都是O(1);

注:通过设置表尾指针对头结点或者表尾结点完成插入或删除操作后,需要对指针r指向的内容进行修改。

1.2 循环双链表

循环双链表也就是表尾结点的后继指针指向了链表的第一个结点,而链表第一个结点的前驱指针指向了表尾结点。因此如果我们要对循环双链表进行判空操作时,我们只需要判断第一个结点的后继指针与前驱指针是否相等并且都等于头指针。

用C语言表示则是:

//循环双链表的判空操作
bool Empty(DLinkList L)
{
	assert(L);//当L为空指针时报错
	if (L->prior == L->next && L->prior == L)//判断前驱指针与后继指针是否都等于头指针
		return true;
	else
		return false;
}

这里一定要注意如果仅仅判断头结点的前驱指针与后继指针相等的话,是不能确定是否为空表的,如下所示:

【数据结构】链式家族的成员——循环链表与静态链表_C语言_04

当双链表中有一个元素时,此时这个元素所在的结点既是表头结点又是表尾结点,因此在这种情况下循环双链表的头结点的前驱指针与后继指针都是指向这个结点的,所以在对循环双链表进行判空时一定要判断是否等于头指针;

循环双链表的其它变化与循环单链表类似,这里我就不再重复说明了,大家可以好好消化一下;

二、静态链表

静态链表我们可以理解为时顺序表与单链表的一个结合体。

静态链表是通过数组来描述线性表的链式存储结构,链表中的结点结构与单链表一致,都是由数据域与指针与构成;

但是不同的是,静态链表中的结点的指针域存储的是结点的相对地址,也就是在数组中的下标,这里我们将它称为游标,如下所示:

【数据结构】链式家族的成员——循环链表与静态链表_静态链表_05

由图可知,静态链表在内存中也是需要先申请一块连续的空间,对应的数组下标表示的是链表中的各个元素在物理位置上的关系,而游标表示的是链表中各个元素在逻辑上的关系。

在静态链表中,下标为0的元素被作为静态链表的头结点,它的数据域中可以不用存放信息,它的游标存放的是链表首元素的数组下标;

虽然静态链表是申请的一块连续的空间,但是表中的各个元素与单链表相同,不需要满足物理位置上相邻,只需要满足逻辑上相邻即可;

因此对于静态链表而言,它也是不能进行随机存取的,要访问各个元素的话只能通过从头结点开始往后访问;

2.1 静态链表的创建

我们要创建一个静态链表的话,我们就可以像创建一个静态顺序表一样,如下所示:

//静态链表的创建格式
#define MaxSize 10//静态链表的最大表长
typedef struct SLinkList {
	ElemType data;//数据域
	int next;//指针域——游标
}SLinkList[MaxSize];
//静态链表的类型为结构体数组类型
//SLinkList——重命名后的类型名
//MaxSize——链表的最大表长,不可修改
//SLinkList<==>struct SLinkList [MaxSize]
int main()
{
	SLinkList a;//定义一个静态链表a
	struct SLinkList b[MaxSize];//定义一个静态链表b
	//两种定义方式都是可以的
	return 0;
}

因为静态链表是通过数组实现的一个单链表,因此数组内的元素类型都是结构体类型,所以静态链表的实质是一个结构体数组。

这里对typedef的使用,实质上就是对数组类型的重命名的使用,有兴趣的朋友可以回看一下【C语言总集篇】指针篇中的typedef的使用,这里我有介绍通过typedef对函数指针类型进行重命名,这里的对数组类型进行重命名也是同理,如下所示:

【数据结构】链式家族的成员——循环链表与静态链表_静态链表_06

我们在声明静态链表的数据类型时实质上是在声明一个结构体类型的数组,这里的静态链表类型定义等价于先定义一个结构体,再将该结构体对应的数组类型通过typedef重命名,如下所示:

//静态链表的创建
#define MaxSize 10//静态链表的最大表长
typedef struct SLinkList {
	ElemType data;//数据域
	int next;//指针域——游标
};//声明结构体类型
typedef struct SLinkList SLinkList[MaxSize];
//struct SLinkList[MaxSize]——数组类型
//通过typedef将数组类型重命名为SLinkList

这个内容我们就先介绍到这里,接下来我们来看一下静态链表的初始化;

2.2 静态链表的初始化

有看过【函数栈帧的创建与销毁】的朋友应该就会知道,我们在内存中申请空间时,申请的空间中会有一些初始的数据,这些初始数据如果我们将它们打印出来的会,会是一些随机的数据,因此为了避免我们创建的静态链表中存在这些随机值,所以我们要对其进行初始化。

由于游标存储的是各个元素的数组下标,数组的下标是从0开始依次递增,我们可以通过将表尾结点的游标设置为-1,来表示这个结点为表尾结点,同样的,我们在对其进行初始化时,可以将其设为-2,来表示此时的空间未被使用,如下所示:

//静态链表的初始化格式
bool InitSLinkList(SLinkList a)
{
	assert(a);//当a为空指针时报错
	for (int i = 0; i < MaxSize; i++)
	{
		(a + i)->data = 0;//初始化数据域
		(a + i)->next = -2;//初始化游标
	}
	return true;
}

下面我们来测试一下初始化这个功能,这里我们还是以整型数据元素为例子,代码如下所示:

//静态链表的创建
#define MaxSize 10//静态链表的最大表长
typedef struct SLinkList {
	int data;//数据域
	int next;//指针域——游标
}SLinkList[MaxSize];
//静态链表的类型为结构体数组类型
//SLinkList——重命名后的类型名
//MaxSize——链表的最大表长,不可修改
//SLinkList<==>struct SLinkList [MaxSize]
//静态链表的初始化
bool InitSLinkList(SLinkList a)
{
	assert(a);//当a为空指针时报错
	for (int i = 0; i < MaxSize; i++)
	{
		(a + i)->data = 0;//初始化数据域
		(a + i)->next = -2;//初始化游标
	}
	return true;
}
//打印静态链表
void Print_SLinkList(SLinkList a)
{
	printf("\n打印静态链表的各个元素的数据:>");
	for (int i = 0; i < MaxSize; i++)
		printf("%2d ", (a + i)->data);
	printf("\n打印静态链表的各个元素的游标:>");
	for (int i = 0; i < MaxSize; i++)
		printf("%2d ", (a + i)->next);
	printf("\n");
}
int main()
{
	SLinkList a;//定义一个静态链表a
	struct SLinkList b[MaxSize];//定义一个静态链表b
	//两种定义方式都是可以的
	if (InitSLinkList(a))//a为数组名,因此我们在传参时只需要传入数组名就可以了
	{
		Print_SLinkList(a);
	}
	return 0;
}

我们来看一下测试结果:

【数据结构】链式家族的成员——循环链表与静态链表_循环链表_07

当我们要对其进行插入或删除元素时,需要从头结点开始通过修改对应结点的游标来进行插入或者删除操作,这里我就不进行演示了,有兴趣的朋友可以自己下去试着编写一下对应的代码;

2.3 小结

对于静态链表,我们需要掌握以下内容:

  • 静态链表时通过数组实现的一个单链表;
  • 在静态链表中,下标为0的首元素作为静态链表的头结点,数据域中不需要存放任何内容;
  • 与静态顺序表一致,静态链表的大小是不可改变的;
  • 与单链表一致,静态链表不支持随机存取,只能从头结点开始往后查找;
  • 静态链表中的指针域存储的是下一个元素的数组下标;
  • 我们通过游标-1来表示链表的表尾结点;
  • 为了避免静态链表中未使用的空间的游标存储的是随机值,我们需要对其初始化为-2;
  • 静态链表的插入与删除操作与单链表的插入删除操作相同,只需要修改指针,不需要移动元素;
  • 静态链表适用于一些不支持指针的高级语言(如:Basic);
  • 静态链表还适用于数据元素数量固定不变的场景(如:操作系统中的文件分配表FAT);

结语

今天的内容到这里就全部结束了,有了顺序表、单链表与双链表这些知识点的基础,对于循环链表与静态链表的理解上就会相对容易一点,希望大家能够通过今天的内容强化对链表相关知识点的理解与使用。

在下一篇内容中,我们将对顺序表与链表的相关知识点做个回顾、对比与总结,大家记得关注哦!!!

最后感谢大家的翻阅,咱们下一篇再见!!!

标签:结点,单链,表尾,静态,链表,链式,数据结构,指针
From: https://blog.51cto.com/u_16231477/9041806

相关文章

  • 代码随想录day04 两两交换链表中的节点 删除链表的倒数第N个节点 链表相交 环形链表
    两两交换链表中的节点题目:这题画一下链表会比较清晰写写画画指针位置很快就可以写出来一开始以为一个tmp就够用了写着写着发现需要多一个代码:删除链表的倒数第N个节点:没什么思路只好先看看视频思路视频思路很简单也很清晰只需要两个指针一快一慢两指针的间......
  • 数据结构应用之桶排序
    问:有10G的订单数据,希望订单金额(假设都是正整数)进行排序,但我们内存有限,只有几百MB,如何进行排序?答:因内存有限,需要排序的数据量巨大,所以,此时需要外部排序,外部排序采用的是一种分治思想,外部排序最常用的是多路归并排序,即将大数据切成多份一次可以载入内存的小数据,对小数据进行内......
  • 代码随想录day03 链表删除 链表类的实现 反转链表
    首先是链表的删除操作热身题这里使用了一个新的头指针这样在删除头指针的时候就不需要进行额外的判断然后是链表类的实现需要一点背诵加上深刻理解有时候理解了但是写起来还是会有些指针边界的小问题应该多写写多记一下就会好了还有就是手写链表要常复习吧学习新......
  • 【数据结构】C语言实现双链表的基本操作
    双链表导言大家好,很高兴又和大家见面啦!!!经过前面几个篇章的内容分享,相信大家对顺序表和单链表的基本操作都已经熟练掌握了。今天咱们将继续分享线性表的链式存储的第二种形式——双链表。在今天的内容中,咱们将介绍双链表的创建以及一些基本操作,接下来跟我一起来看看吧!一、单链表与双......
  • 数据结构实验代码分享 - 4
    迷宫与栈问题(图的应用)【问题描述】以一个m*n的长方阵表示迷宫,0和1分别表示迷宫中的通路和障碍。设计一个程序,对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。输入:行列迷宫,0表示无障碍,1表示有障碍输出:一条Path或“NOPATH” 注:参考了《数据结......
  • 数据结构实验代码分享 - 3
    哈夫曼编码/译码系统(树应用)[问题描述]任意给定一个仅由26个大写英文字母组成的字符序列,根据哈夫曼编码算法,求得每个字符的哈夫曼编码。要求:1)输入一个由26个英文字母组成的字符串,请给出经过哈夫曼编码后的编码序列及其编码程度。(编码)2)采用上一问题的哈夫曼编码,给定一串编......
  • python 删除链表的倒数第 N 个结点 多种解法
    解法一:双指针法这种解法使用两个指针,一个快指针和一个慢指针,它们之间的距离为N。首先,让快指针先移动N步。然后,同时移动快指针和慢指针,直到快指针到达链表末尾。此时,慢指针指向的节点就是要删除的节点的前一个节点。最后,将慢指针的下一个节点指向下下个节点,完成删除操作。defre......
  • 数据结构之<散列表>的介绍
    简介散列表也叫做哈希表,是根据键值对(key,value)进行存储的一种数据结构。散列表利用哈希函数将给定的键映射到一个特定的位置(通常是数组索引),这个位置通常被称为哈希值或哈希地址。这里可以举个微信好友列表的例子说明,存放好友首字母的表对应的就是散列表。1.哈希函数哈希函数是......
  • 数据结构&&集合总结
    总结数据结构数据结构:保存数据的一种方式常见的数据结构通过数组来保存,基于数组的数据结构(动态数组,长度可变的数组)基于数组的结构的优缺点​ 1.通过下标查询元素,效率高​ 2.通过下标修改元素,效率高​ **查改快**​ 在需要扩容的时候:添加慢,删除慢,插入元素慢......
  • 【数据结构】C语言实现单链表的基本操作
    单链表基本操作的实现导言大家好,很高兴又和大家见面啦!!!在上一篇中,我们详细介绍了单链表的两种创建方式——头插法与尾插法,相信大家现在对这两种方式都已经掌握了。今天咱们将继续介绍单链表的基本操作——查找、插入与删除。在开始今天的内容之前,我们先通过尾插法创建一个单链表,如......