数据结构篇三:双向循环链表

news2024/11/13 10:25:21

文章目录

  • 前言
  • 双向链表的结构
  • 功能的解析及实现
    • 1. 双向链表的创建
    • 2. 创建头节点(初始化)
    • 3. 创建新结点
    • 4. 尾插
    • 5. 尾删
    • 6. 头插
    • 7. 头删
    • 8. 查找
    • 9. 在pos位置前插入
    • 10. 删除pos位置的结点
    • 11. 销毁
  • 代码实现
    • 1.ListNode.h
    • 2. ListNode.c
    • 3. test.c
  • 总结

前言

  前面我们学习了单链表的实现,我们发现它在进行从后往前查找的时候有很大的不便,为了解决这个问题,双向链表油然而生。它可以很好的解决单链表无法从后往前查找的困难。

双向链表的结构

在这里插入图片描述

  如图所示,它是有两个指针域,一个指向后一个结点,一个指向前一个结点。它存储了前一个结点的地址与后一个结点的地址,所以可以很方便的进行向前遍历或者向后遍历。同时它还是一个循环链表,可以通过第一个结点直接找到最后一个结点。

功能的解析及实现

1. 双向链表的创建

  就像前文所说,它包含了两个指针域和一个数据域,用来存放它前一个结点的地址和后一个结点的地址以及本身的数据。

typedef struct LTNode
{
	LTDataType data;
	struct LTNode* prev;
	struct LTNode* next;
}LTNode;

2. 创建头节点(初始化)

  此次双向链表的结构我是采用了带头结点的结构,好处就是头节点是malloc出来的,是在堆区上存放,不会因为出了函数就被销毁,也意味着后续的各种操作我们只需要传递一级指针就会有实现单链表时传递二级指针的效果。

LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		return NULL;
	}
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

3. 创建新结点

  每次插入新的数据都需要开辟新的结点,因此把它单独拿出来放到一个函数中实现。

LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		return NULL;
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}

4. 尾插

  因为是循环链表,我们可以通过第一个头节点直接找到尾结点,而在连接的时候,需要将新结点分别连接到头节点的prev以及尾结点的next,同时自身的prev存放尾结点的地址,next存放头节点的地址。如图:
在这里插入图片描述

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyListNode(x);
	
	newnode->data = x;
	newnode->next = phead;
	phead->prev = newnode;
	newnode->prev = tail;
	tail->next = newnode;
}

5. 尾删

  在创建头节点时,我们是将头节点的prev与next都指向了它自身,因此我们可以通过头节点的next指向的是不是自身来判断是否为存放了数据。(头节点自身不存放数据)。与尾插类似,如图:
在这里插入图片描述

void ListPopBack(LTNode* phead)
{
	assert(phead);
	if (phead->next == phead)//判断链表是否存放了数据
	{
		return;
	}
	LTNode* tail = phead->prev;
	LTNode* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;
	free(tail);
	tail = NULL;
}

6. 头插

  与尾插类似,只不过这个放到了最前面。在尾插是我们是有tail与phead来与新结点连接,头插也一样。先保存当前的第一个结点地址,然后再将新结点连接到头节点与原第一个结点的中间即可。

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* next = phead->next;//保存当前的第一个结点地址
	LTNode* newnode = BuyListNode(x);

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

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

7. 头删

  我们只需要保存第一个结点与第二节结点的地址,然后在将第二个与头节点连接,再释放掉第一个结点即可。同时还需要判断链表是否为空(即有没有元素存放其中)。

void ListPopFront(LTNode* phead)
{
	//assert(phead->next != phead);  //暴力解决

	//温和解决
	if (phead->next == phead)
	{
		return;
	}
	LTNode* prev = phead->next;
	LTNode* next = prev->next;

	phead->next = next;
	next->prev = phead;
	free(prev);
	prev = NULL;
}

8. 查找

  依次遍历链表即可,从phead开始,一直到再次遇到phead结束(循环链表)。

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	printf("该元素不存在\n");
	return NULL;
}

9. 在pos位置前插入

  与头插相似,这里只需要用prev保存pos位置的前一个结点地址,然后将新结点与prev与pos相连接即可。

void ListInsert(LTNode* pos, LTDataType x)
{
	LTNode* prevPos = pos->prev;
	LTNode* newnode = BuyListNode(x);

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

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

10. 删除pos位置的结点

  保存pos的前一个结点地址与后一个结点地址,然后将彼此相连接,然后free掉pos结点就完成了。

void ListErase(LTNode* pos)
{
	LTNode* nextPos = pos->next;
	LTNode* prevPos = pos->prev;

	nextPos->prev = prevPos;
	prevPos->next = nextPos;
	free(pos);
	pos = NULL;
}

11. 销毁

  动态开辟的结点在最后结束时都需要进行释放,防止出现内存泄漏。用cur保存当前准备要释放的结点,用next保存cur的下一个结点,释放完cur后,再将cur指向next,进行循环。

void ListDestroy(LTNode* phead)
{
	LTNode* cur = phead;
	LTNode* next = cur->next;

	while (cur)
	{
		free(cur);
		cur = NULL;

		if (cur != NULL)
		{
			cur = next;
			next = next->next;
		}
	}
}

代码实现

1.ListNode.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>

typedef int LTDataType;

typedef struct LTNode
{
	LTDataType data;
	struct LTNode* prev;
	struct LTNode* next;
}LTNode;

// 创建返回链表的头结点.
LTNode* ListInit();

// 双向链表销毁
void ListDestroy(LTNode* phead);

// 双向链表打印
void ListPrint(LTNode* phead);

// 双向链表尾插
void ListPushBack(LTNode* phead, LTDataType x);

// 双向链表尾删
void ListPopBack(LTNode* phead);

// 双向链表头插
void ListPushFront(LTNode* phead, LTDataType x);

// 双向链表头删
void ListPopFront(LTNode* phead);

// 双向链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);

// 双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x);

// 双向链表删除pos位置的节点
void ListErase(LTNode* pos);

2. ListNode.c

#include"ListNode.h"

LTNode* BuyListNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		return NULL;
	}
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;
	return newnode;
}
LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		return NULL;
	}
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyListNode(x);
	
	newnode->data = x;
	newnode->next = phead;
	phead->prev = newnode;
	newnode->prev = tail;
	tail->next = newnode;
}

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

void ListPopBack(LTNode* phead)
{
	assert(phead);
	if (phead->next == phead)
	{
		return;
	}
	LTNode* tail = phead->prev;
	LTNode* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;
	free(tail);
	tail = NULL;
}

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* next = phead->next;
	LTNode* newnode = BuyListNode(x);

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

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

void ListPopFront(LTNode* phead)
{
	//assert(phead->next != phead);  //暴力解决

	//温和解决
	if (phead->next == phead)
	{
		return;
	}
	LTNode* prev = phead->next;
	LTNode* next = prev->next;

	phead->next = next;
	next->prev = phead;
	free(prev);
	prev = NULL;
}

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	printf("该元素不存在\n");
	return NULL;
}

void ListInsert(LTNode* pos, LTDataType x)
{
	LTNode* prevPos = pos->prev;
	LTNode* newnode = BuyListNode(x);

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

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

void ListErase(LTNode* pos)
{
	LTNode* nextPos = pos->next;
	LTNode* prevPos = pos->prev;

	nextPos->prev = prevPos;
	prevPos->next = nextPos;
	free(pos);
	pos = NULL;
}

void ListDestroy(LTNode* phead)
{
	LTNode* cur = phead;
	LTNode* next = cur->next;

	while (cur)
	{
		free(cur);
		cur = NULL;

		if (cur != NULL)
		{
			cur = next;
			next = next->next;
		}
	}
}

3. test.c

#include"ListNode.h"

void test()
{
	LTNode* phead = ListInit();
	if (phead == NULL)
	{
		return;
	}
	ListPushBack(phead, 1);//测试:尾插
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPrint(phead);

  	ListPopBack(phead);//测试:尾删
	ListPopBack(phead);
	ListPopBack(phead);
	ListPrint(phead);

	ListPopBack(phead);//测试:如果链表为空继续删除会不会报错
	ListPopBack(phead);

	ListPushBack(phead, 1);//尾插一个数据来对比头插
	ListPushFront(phead, 1);//测试:头插
	ListPushFront(phead, 2);
	ListPushFront(phead, 3);
	ListPushFront(phead, 4);
	ListPrint(phead);

	ListPopFront(phead);//测试:头删
	ListPopFront(phead);
	ListPopFront(phead);
	ListPrint(phead);

	ListPopFront(phead);//测试:如果链表删除完毕,继续删除会不会报错
	ListPopFront(phead);
	ListPopFront(phead);
	ListPrint(phead);

	ListPushBack(phead, 1);//插入新元素进行后续测试
	ListPushBack(phead, 2);
	ListPushBack(phead, 3);
	ListPushBack(phead, 4);
	ListPrint(phead);
	ListFind(phead, 5);

	LTNode* pos = ListFind(phead, 2);//测试:在2前面插入数字5
	ListInsert(pos, 5);
	ListPrint(phead);

	pos = ListFind(phead, 2);//测试:删除结点2
	ListErase(pos);
	ListPrint(phead);

	ListDestroy(phead);//测试:销毁链表
}

int main()
{
	test();
	return 0;
}

总结

  总体而言难度不大,并且双向链表解决了单链表的很多问题,值得好好学习一下。并且在这里总结一下数据结构中形参能对实参产生影响的三种方式:二级指针,头节点(在堆区),返回值。
  双向循环链表就先告一段落了,如果发现文章哪里有问题可以在评论区提出来或者私信我嗷。接下来我会继续学习栈与队列,开启新的篇章,那么本期就到此结束,让我们下期再见!!觉得不错可以点个赞以示鼓励喔!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/481096.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

03-WAF绕过-漏洞利用之注入上传跨站等绕过

WAF绕过-漏洞利用之注入上传跨站等绕过 思维导图 一、sql注入绕过 使用sqlmap注入测试绕过 1.绕过cc流量 通过sqlmap对网站进行测试的时候&#xff0c;如果对方有cc流量防护&#xff0c;需要给sqlmap设置一个代理进行注入。 防cc拦截&#xff1a;修改user-agent头代理&…

ADB调试删除手机内置应用

前言 最近手机升级到了鸿蒙3系统&#xff0c;但是内置了两个输入法&#xff0c;我想删掉小艺输入法&#xff0c;于是就有了这篇记录。   本文在B站上ADB调试卸载应用的教程的基础上&#xff0c;去掉了内网穿透相关操作步骤。 前期准备 手机&#xff08;荣耀10青春版&#x…

3.4 只读存储器

学习目标&#xff1a; 学习只读存储器&#xff08;ROM&#xff09;的目标可以包括以下内容&#xff1a; 了解ROM的基本概念、分类以及适用场景。掌握ROM的电路原理、逻辑结构和读取方式。熟悉ROM的编程方式和编程工具。理解ROM与EPROM、EEPROM和闪存的区别和联系。了解ROM在计…

IPsec中IKE与ISAKMP过程分析(快速模式-消息1)

IPsec中IKE与ISAKMP过程分析&#xff08;主模式-消息1&#xff09;_搞搞搞高傲的博客-CSDN博客 IPsec中IKE与ISAKMP过程分析&#xff08;主模式-消息2&#xff09;_搞搞搞高傲的博客-CSDN博客 IPsec中IKE与ISAKMP过程分析&#xff08;主模式-消息3&#xff09;_搞搞搞高傲的博客…

[架构之路-181]-《软考-系统分析师》-19- 系统可靠性分析与设计 - 概览

前言&#xff1a; 可靠性工程是研究产品生命周期中故障的发生、发展规律&#xff0c;达到预防故障&#xff0c;消灭故 障&#xff0c;提高产品可用性的工程技术。 信息系统的可靠性是指系统在满足一定条件的应用环境中能够正常工作的能力&#xff0c;可以按一般工程系统的可靠性…

图像生成论文阅读:GLIDE算法笔记

标题&#xff1a;GLIDE: Towards Photorealistic Image Generation and Editing with Text-Guided Diffusion Models 会议&#xff1a;ICML2022 论文地址&#xff1a;https://proceedings.mlr.press/v162/nichol22a.html 官方代码&#xff1a;https://github.com/openai/glide-…

【算法】回文数

目录 一.回文数 二.求回文数&#xff08;10000以内&#xff09; 代码&#xff1a; 翻译&#xff1a; 调试&#xff1a; 三.判断回文数 代码&#xff1a; 调试&#xff1a; 一.回文数 "回文数"是一种数字。 如&#xff1a;12321, 这个数字正读是12321,倒读也是…

C++的类

文章目录 class定义类声明和定义不分离成员函数声明与定义的分离 类的访问限定符类的实例化类对象的大小this指针 引入&#xff1a;什么是类呢&#xff1f; 在C语言阶段,结构体成员只能是它的属性,这个结构体就相当于张三,小时候它只被赋予了名字,性别,家庭住址等属性,但是他没…

docker Mysql部署主从集群

目录 1 docker安装 2 docker mysql 安装配置 远程连接 2.0 配置 2.0.1 文件夹 配置 2.0.2 主库文件配置 my.cnf -> 主库 id 和 开启二进制日志 2.0.3 从库文件配置 -> 从库 id 2.1 mysql 主 -> 第一个端口号和从库不一样 2.1.1 docker run 主库 2.1.2 查看主…

Postman创建项目 对接口发起请求处理

查看本文之前 您需要理解了解 Postman 的几个简单工作区 如果还没有掌握 可以先查看我的文章 简单认识 Postman界面操作 那么 掌握之后 我们就可以正式来开启我们的接口测试 我们先选择 Collections 我们点上面这个加号 多拉一个项目出来 然后 我们选我们刚加号点出来的项目…

用LangChain构建大语言模型应用

用LangChain构建大语言模型应用 自 ChatGPT 发布以来&#xff0c;大型语言模型 (LLM) 广受欢迎。尽管您可能没有足够的资金和计算资源从头开始训练自己的大语言模型&#xff0c;但您仍然可以使用预训练的大语言模型来构建一些很酷的东西&#xff0c;例如&#xff1a; 可以根据…

01-权限提升-网站权限后台漏洞第三方获取

权限提升-网站权限后台漏洞第三方获取 本节课内容主要是权限提升的思路&#xff0c;不涉及技术 当前知识点在渗透流程中的点 前期-中期-后期对应知识关系 当前知识点在权限提升的重点 知识点顺序&#xff0c;理解思路&#xff0c;分类介绍等 当前知识点权限提升权限介绍 …

Java8

Java8 &#xff08;一&#xff09;、双列集合&#xff08;二&#xff09;、Map集合常用api&#xff08;三&#xff09;、Map集合的遍历方式&#xff08;四&#xff09;、HashMap&#xff08;五&#xff09;、LinkedHashMap&#xff08;六&#xff09;、TreeMap&#xff08;七&a…

Steve:AI创建视频和动画的在线工具

【产品介绍】 steve.ai是一款利用人工智能技术创建视频和动画的在线工具&#xff0c;可以让任何人在几分钟内把文字转换成吸引人的视频。核心功能是根据用户输入的文本&#xff0c;自动选择合适的素材、音乐、字幕和动效&#xff0c;生成高质量的视频。还提供了多种模板、风格和…

Photoshop如何使用滤镜之实例演示?

文章目录 0.引言1.将普通照片制作成油画效果2.使用液化滤镜修出完美身材3.用镜头光晕滤镜制作唯美的逆光人像4.用Camera Raw滤镜对偏色风景照进行调色 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对PS进行了学习&#xff0c;本文通过《Photoshop2021入门教程》及其…

Servlet 笔记

1. HTTP 协议 1.1 HTTP协议简介 超文本传输协议&#xff08;英文&#xff1a;HyperText Transfer Protocol&#xff0c;缩写&#xff1a;HTTP&#xff09;是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。 HTTP的发展是由蒂姆伯纳斯-…

etcd原理剖析一

为什么Kubernetes使用etcd&#xff1f; 首先我们来看服务高可用以及数据一致性。单副本存在单点故障&#xff0c;而多副本又引入数据一致性问题。 为了解决数据一致性问题&#xff0c;需要引入一个共识算法。例如Raft等。etcd选择了Raft&#xff0c;它将复杂的一致性问题分解…

Maven 笔记

1. Maven 的简介 1.1 简介 Maven 这个词可以翻译为"专家","内行"。作为Apache 组织中的一个开源项目&#xff0c;主要服务于基于java平台的项目构建&#xff0c;依赖管理和项目信息管理。 无论是小型的开源类库项目&#xff0c;还是大型的企业级应用&am…

Spring 5 笔记 - 入门与IOC

1. Spring 入门简介 Spring&#xff1a;轻量级、开源的JavaEE框架&#xff0c; 解决企业应用的复杂性。包括IOC和AOP两个核心部分。 IOC&#xff1a; 控制反转&#xff0c;把创建对象和对象之间的调用的过程都交给Spring 进行管理&#xff0c;使耦合度降低。 AOP&#xff1a…

Winform从入门到精通(38)—StatusStrip(史上最全)更新中

一、属性 1、Name 获取StatusStrip控件对象 2、AllowDrop 允许用户拖拽数据到控件上 3、AllowItemReorder 当用于按下alt键时,是否允许对项进行排列,如下图: 4、AllowMerge 5、Anchor 6、AutoSize 7、BackColor 设置StatusStrip的背景色 8、BackgroundImage 设置背…