jjzjj

数据结构与算法之《带头双向循环链表》详解

Ggggggtm 2023-04-15 原文

文章目录

一、带头双向循环链表概念及结构

1、1 带头双向循环链表的概念

1、2  带头双向循环链表的结构

二、带头双向循环链表的思路及代码实现详解

2、1 带头双向循环链表实现思路

2、2 带头双向循环链表的模块细节及代码实现

2、2、1 结构体的声明与定义

2、2、2 初始化结构体

2、2、3 打印链表数据

2、2、4 开辟节点

2、2、5 销毁链表

2、2、6 判断链表是否为空

2、2、7 头插

2、2、8 尾插

2、2、9 头删

2、2、10 尾删

2、2、11 查找结点

2、2、12 在pos位置前插入

2、2、13 删除pos位置节点

三、带头双向循环链表代码整合

QList.h

QList.c

test.c


标题:《链表》之带头双向循环链表

作者:@Ggggggtm

寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景

    我们知道,链表实际上结构有很多种。我们大致可分为带头或者不带头单向或者双向循环或者非循环三种大类情况。而每种大类都可与其他类别组合形成新的一种结构,纵沟会有八种情况。  每种情况也是大同小异。其中常用的只有两种:无头单向非循环链表带头双向循环链表。想要具体学习了解无头单向非循环链表可参考这篇文章:数据结构与算法之《单链表》详解。本篇文章会详细讲述带头双向循环链表。其实我们把最为常用的两篇文章掌握后,其他的链表结构也就很容易写出来了。

一、带头双向循环链表概念及结构

1、1 带头双向循环链表的概念

  我们学完无头单向非循环链表后,第一次见到带头双向循环链表感觉会很难,结构也很复杂。其实并不是这样的。带头双向循环链表甚至写起来和构思都十分简单。我们接着往下看就可以知道了。

  带头双向循环链表到底是个什么呢?如同正常链表一样,是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。无非结构上会有所差异,那我们来看一下结构带头双向循环链表的结构把。

1、2  带头双向循环链表的结构

  带头双向循环链表顾名思义,链表会有一个头节点(哨兵位),但是这个头节点不存储有效数据。同时,每个节点会有两个指针,一个指向后面的节点,另一个指向前面一个节点。循环的意思是尾结点指向头节点,头节点指向尾节点。如下图所示:  带头双向循环链表结构看上去很复杂,实现起来却很容易,我们接着往下看带头双向循环链表的实现。

二、带头双向循环链表的思路及代码实现详解

2、1 带头双向循环链表实现思路

  我们学习完无头单向非循环链表后,对带头双向循环链表理解上就会很简单。两者的实现思路大同小异。具体到每个模块细节的实现也是几乎相同的。我们先来看单链表整体思路分为多种不同的模块有哪几个:

  1. 定义一个结构体,该结构体包含一个可以存放数据的变量、一个能够指向下一个节点的指针和一个能够指向前面一个节点的指针。
  2. 初始化结构体。
  3. 打印链表数据。
  4. 开辟节点。
  5. 销毁链表。
  6. 判断链表是否为空。
  7. 头插。
  8. 尾插。
  9. 头删。
  10. 尾删。
  11. 查找节点。
  12. 任意位置插入。
  13. 任意位置删除。

   以上就是整个带头双向循环链表的不同模块,那我们接下来看一下不同模块思路及代码实现的细节吧。

2、2 带头双向循环链表的模块细节及代码实现

2、2、1 结构体的声明与定义

  上面我们提到,定义结构体时,该结构体包含一个可以存放数据的变量、一个能够指向下一个节点的指针和一个能够指向前面一个节点的指针。那么代码的实现就很简单了。

typedef int LNDataType;

typedef struct DListNode
{
	struct DListNode* prev;
	struct DListNode* next;
	LNDataType data;
}LTNode;

2、2、2 初始化结构体

  初始化结构体的方式有很多种,在这里我们是先定义一个结构体指针为空,在对其进行初始化,这个时候我们要改变结构体指针的话,我们就需要传二级指针。我们在初始化时,此时链表中只有一个头节点,所以我们要头结点的next和prev都指向自己。我们看代码的实现。

LTNode* plist = NULL;
LTInit(&plist);

void LTInit(LTNode** phead)
{
	(*phead) = BuyLTNode(-1);
	(*phead)->next = *phead;
	(*phead)->prev = *phead;
}

2、2、3 打印链表数据

  注意,带头双向循环链表可不是随便的遍历打印哦。带头双向循环链表是一个循环的结构,一但不加操作遍历,就会死循环。我们可以从头节点的下一个位置开始遍历,结束条件就是回到头节点。这样就解决问题了,我们结合代码理解一下:

void LTPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	printf("<-head->");
	while (cur != phead)
	{
		printf("%d<-->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

2、2、4 开辟节点

  这里为什么要把开辟节点单独里一个函数呢?在我们插入数据之前,不管是头插还是尾插,还是任意插入,我们都需要新开辟一个节点。然后把要插入的数据存储到新节点当中,然后进行连接即可。所以这里我们把开辟节点单独分为一个模块来解释。我们来看一下代码的是实现。  

LTNode* BuyLTNode(LNDataType x)
{
	LTNode* tmp = (LTNode*)malloc(sizeof(LTNode));
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	tmp->data = x;
	tmp->next = NULL;
	tmp->prev = NULL;

	return tmp;
}

2、2、5 销毁链表

  因为链表是我们动态开辟出来的,所以我们要释放掉,避免空间泄露。销毁链表时,我们不只是释放头节点的空间,每个节点的空间都要释放。  

void LTDestory(LTNode** pphead)
{
	LTNode* cur = (*pphead)->next;
	while (cur != *pphead)
	{
		LTNode* tmp = cur->next;
		free(cur);
		cur = tmp;
	}
	free(*pphead);
	*pphead = NULL;
}

2、2、6 判断链表是否为空

  判断链表为空的情况在我们后面对链表删除节点的时候需要的,所以这里我们单独列出一个函数,判断也较为简单,我们直接看代码。 

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

2、2、7 头插

  在带头双向循环链表中,头插我们直接插入就行,并没什么特殊的情况。我们只需要把结构修改到位就行。我们看代码实现。

void LTPushFront(LTNode* phead, LNDataType x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x);

	newnode->next = phead->next;
	phead->next->prev = newnode;

	phead->next = newnode;
	newnode->prev = phead;
}

2、2、8 尾插

  尾插同样也很简单。我们可以直接通过头节点phead的prev找到尾节点,然后直接连接截取就行。也无特殊情况。而在无头单向非循环链表中,我们不仅要遍历找尾节点,还要判断特使为空的情况。带头双向循环链表的头节点和双向循环的结构给我们带来了很大的便利。我们直接看代码的实现。

void LTPushBack(LTNode* phead, LNDataType x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x);
	LTNode* tail=phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

2、2、9 头删

  在删除之前,我们应该首先判断链表中是否包含有效的节点(不包含头节点)。然后正常删除链接即可。我们看代码的实现。

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tmp = phead->next;

	phead->next = tmp->next;
	tmp->next->prev = phead;
}

2、2、10 尾删

  尾删的情况与头删的情况相似。同样,  在删除之前,我们应该首先判断链表中是否包含有效的节点(不包含头节点)。然后正常删除链接即可。我们看代码:

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tmp = phead->next;

	phead->next = tmp->next;
	tmp->next->prev = phead;

	free(tmp);
}

2、2、11 查找结点

  这里查找节点是为我们后面的任意插入和删除做铺垫,我们是需要在查找出的位置进行删除和插入操作。我们遍历查找的方法与上述打印查找的方法是一样的,避免陷入死循环。

LTNode* LTNodeFind(LTNode* phead, LNDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}

	return NULL;  // 找不到
}

2、2、12 在pos位置前插入

  在pos位置前插入和头插基本相同。我们在这里不过多解释,直接看代码。

void LTInsert(LTNode* pos, LNDataType x)
{
	assert(pos);

	LTNode* newnode = BuyLTNode(x);
	LTNode* prev = pos->prev;

	prev->next = newnode;
	newnode->prev = prev;

	newnode->next = pos;
	pos->prev = newnode;
}

2、2、13 删除pos位置节点

  删除pos位置节点与尾删也是大同小异,直接看代码:

void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;

	prev->next = pos->next;
	pos->prev = prev;

	free(pos);
}

三、带头双向循环链表代码整合

  由于代码量相对来说有一点多,所以我们就将函数的声明的定义分开,这样有利于提高代码的可读性,同时会保持一个良好的思路,且方便编写代码。

  我们将函数的声明放在单独的一个QList.h的头文件,函数的实现放在一个单独的QList.c源文件,函数的主方法及调用放在另一个单独的test.c源文件。

QList.h

#define _CRT_SECURE_NO_WARNINGS 1

#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>

typedef int LNDataType;

typedef struct DListNode
{
	struct DListNode* prev;
	struct DListNode* next;
	LNDataType data;
}LTNode;

LTNode* BuyLTNode(LNDataType x);
void LTInit(LTNode** phead);
void LTPrint(LTNode* phead);
void LTDestory(LTNode** phead);
bool LTEmpty(LTNode* phead);

void LTPushBack(LTNode* phead, LNDataType x);
void LTPopBack(LTNode* phead);

void LTPushFront(LTNode* phead, LNDataType x);
void LTPopFront(LTNode* phead);

// 在pos前一个位置插入
LTNode* LTNodeFind(LTNode* pos, LNDataType x);
void LTInsert(LTNode* pos, LNDataType x);

// 删除pos节点
void LTErase(LTNode* pos);

QList.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"Qlist.h"

LTNode* BuyLTNode(LNDataType x)
{
	LTNode* tmp = (LTNode*)malloc(sizeof(LTNode));
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	tmp->data = x;
	tmp->next = NULL;
	tmp->prev = NULL;

	return tmp;
}

void LTInit(LTNode** phead)
{
	(*phead) = BuyLTNode(-1);
	(*phead)->next = *phead;
	(*phead)->prev = *phead;
}

void LTPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;
	printf("<-head->");
	while (cur != phead)
	{
		printf("%d<-->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

void LTDestory(LTNode** pphead)
{
	LTNode* cur = (*pphead)->next;
	while (cur != *pphead)
	{
		LTNode* tmp = cur->next;
		free(cur);
		cur = tmp;
	}
	free(*pphead);
	*pphead = NULL;
}

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

void LTPushBack(LTNode* phead, LNDataType x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x);
	LTNode* tail=phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;

	tailprev->next = phead;
	phead->prev = tailprev;

	free(tail);
	tail = NULL;
}

void LTPushFront(LTNode* phead, LNDataType x)
{
	assert(phead);

	LTNode* newnode = BuyLTNode(x);

	newnode->next = phead->next;
	phead->next->prev = newnode;

	phead->next = newnode;
	newnode->prev = phead;
}
 
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));

	LTNode* tmp = phead->next;

	phead->next = tmp->next;
	tmp->next->prev = phead;

	free(tmp);
}

// pos位置插入
LTNode* LTNodeFind(LTNode* phead, LNDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}

	return NULL;
}
void LTInsert(LTNode* pos, LNDataType x)
{
	assert(pos);

	LTNode* newnode = BuyLTNode(x);
	LTNode* prev = pos->prev;

	prev->next = newnode;
	newnode->prev = prev;

	newnode->next = pos;
	pos->prev = newnode;
}
void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;

	prev->next = pos->next;
	pos->prev = prev;

	free(pos);
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "Qlist.h"

void TestNode()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);


	LTPushFront(plist, 1);
	LTPrint(plist);
	LTPushFront(plist, 2);
	LTPrint(plist);
	LTPushFront(plist, 3);
	LTPrint(plist);
	LTPushFront(plist, 4);
	LTPrint(plist);
	LTPushFront(plist, 5);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);

	LTDestory(&plist);
}
int main()
{
	TestNode();
	return 0;
}

  我们对带头双向循环链表的讲解就到这里,希望以上内容会对你有所帮助,感谢阅读ovo~

有关数据结构与算法之《带头双向循环链表》详解的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 树顶语法无限循环 - 2

    我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He

  3. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  5. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  6. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  7. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  8. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  9. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  10. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

随机推荐