数据结构之——单循环链表和双向循环链表
一、单循环链表的奥秘
单循环链表是一种特殊的链表结构,它在数据结构领域中具有重要的地位。其独特的循环特性使得它在某些特定的应用场景中表现出强大的优势。
(一)结构与初始化
单循环链表的结构由节点组成,每个节点包含数据域和指向下一个节点的指针域。与普通单链表不同的是,单循环链表的最后一个节点的指针指向头节点,从而形成一个循环。在初始化单循环链表时,通常先创建一个头节点,头节点的数据域可以存储一些特定信息,比如链表长度等。头节点的指针域指向自身,代表此时链表为空。例如在 C 语言中,可以这样初始化单循环链表:
typedef int ElemType;typedef struct Node
{ElemType data;struct Node *next;
} Node;typedef struct Node *LinkList;// 初始化单循环链表
void InitList(LinkList &L)
{// 分配内存空间用于创建头节点L = (LinkList)malloc(sizeof(Node));if (!L) {// 如果内存分配失败,退出程序并返回错误码 OVERFLOWexit(OVERFLOW);}// 将头节点的 next 指针指向自身,形成单循环链表L->next = L;// 头节点的数据域可以用于存储链表的长度等信息,这里初始化为 0L->data = 0;
}
(二)插入与删除操作
1.头插法:头插法是将新节点插入到单循环链表的头部。首先创建新节点,将新节点的指针指向头节点的下一个节点,然后将头节点的指针指向新节点,完成插入操作。例如:
// 在单循环链表头部插入节点
void headInsert(LinkList &L, int data)
{// 分配新节点内存空间Node *newNode = (Node *)malloc(sizeof(Node));// 设置新节点的数据域为传入的数据newNode->data = data;// 让新节点的 next 指针指向当前链表头部的下一个节点newNode->next = L->next;// 更新链表头部的 next 指针,使其指向新节点L->next = newNode;
}
2.尾插法:尾插法是将新节点插入到单循环链表的尾部。首先找到链表的尾节点,然后将新节点插入到尾节点之后,使其成为新的尾节点。例如:
// 在单循环链表尾部插入节点
void tailInsert(LinkList &L, int data)
{// 分配新节点内存空间Node *newNode = (Node *)malloc(sizeof(Node));newNode->data = data;newNode->next = L;// 创建一个临时指针用于遍历链表找到尾节点Node *p = L;// 遍历链表直到找到尾节点(即当前节点的下一个节点是头节点)while (p->next!= L) {p = p->next;}// 将尾节点的 next 指针指向新节点p->next = newNode;
}
3.删除操作:删除操作可以分为删除头节点和删除其他节点两种情况。删除头节点时,只需将头节点的指针指向头节点的下一个节点即可。删除其他节点时,需要找到要删除节点的前一个节点,然后将其指针指向要删除节点的下一个节点,完成删除操作。例如:
// 删除指定值的节点
void deleteNode(LinkList &L, int data)
{// 创建两个指针 p 和 q,用于遍历链表Node *p = L;Node *q = L->next;// 遍历链表,直到找到要删除的节点或遍历到链表尾部(q == L)while (q!= L && q->data!= data) {p = q;q = q->next;}// 如果遍历到链表尾部仍未找到要删除的节点if (q == L) {printf("节点不存在!\n");} else {// 调整指针,将待删除节点从链表中移除p->next = q->next;// 释放待删除节点的内存空间free(q);}
}
(三)总结与应用
单循环链表的优势在于可以方便地进行循环遍历,无需担心链表的末尾。在一些需要循环处理数据的场景中,如约瑟夫问题、魔术师发牌问题等,单循环链表表现出了强大的应用价值。然而,单循环链表也存在一些问题,比如在插入和删除操作时,需要特别注意链表的循环特性,否则容易出现错误。解决这些问题的方法是在编写代码时,仔细考虑各种情况,确保代码的正确性。总之,单循环链表是一种非常有用的数据结构,掌握它的特点和操作方法,对于提高编程能力和解决实际问题具有重要意义。
二、双向循环链表的魅力
双向循环链表作为一种复杂而强大的数据结构,具有独特的魅力和价值。
(一)结构与初始化
双向循环链表的节点结构包含数据域、指向前驱节点的指针域和指向后继节点的指针域。这使得在链表中可以方便地双向遍历,提高了操作的灵活性。在 C 语言中,初始化双向循环链表通常先创建一个头节点,头节点的数据域一般不存储实际数据,其前驱和后继指针都指向自身,形成一个循环。例如:
#include <stdio.h>
#include <stdlib.h>typedef int ElemType;// 定义双向链表节点结构体
typedef struct DNode
{ElemType data;struct DNode *prev; // 指向前驱节点的指针struct DNode *next; // 指向后继节点的指针
} DNode;// 定义指向双向链表节点的指针类型别名
typedef struct DNode *DLinkList;// 初始化双向循环链表
void initDList(DLinkList &DL)
{// 分配内存空间用于创建头节点DL = (DLinkList)malloc(sizeof(DNode));if (!DL) {// 如果内存分配失败,退出程序并返回错误码 OVERFLOWexit(OVERFLOW);}// 将头节点的 prev 和 next 指针都指向自身,形成双向循环链表DL->prev = DL;DL->next = DL;// 头节点的数据域可以用于存储链表的长度等信息,这里初始化为 0DL->data = 0;
}
(二)插入操作
1.头部插入:头部插入操作先创建新节点,然后调整新节点的前驱和后继指针,使其指向头节点和原来的头节点的下一个节点,最后调整头节点和原来头节点的下一个节点的指针,使其指向新节点。例如:
// 在双向循环链表头部插入节点
void headInsertDList(DLinkList &DL, int data)
{// 分配新节点内存空间DNode *newNode = (DNode *)malloc(sizeof(DNode));newNode->data = data;// 设置新节点的前驱指针指向头节点newNode->prev = DL;// 设置新节点的后继指针指向原来头节点的下一个节点newNode->next = DL->next;// 原来头节点下一个节点的前驱指针更新为新节点DL->next->prev = newNode;// 头节点的后继指针更新为新节点DL->next = newNode;
}
2.尾部插入:尾部插入操作先找到链表的尾节点,然后创建新节点,调整新节点的前驱和后继指针,使其指向尾节点和头节点,最后调整尾节点和头节点的指针,使其指向新节点。例如:
// 在双向循环链表尾部插入节点
void tailInsertDList(DLinkList &DL, int data)
{// 分配新节点内存空间DNode *newNode = (DNode *)malloc(sizeof(DNode));newNode->data = data;// 设置新节点的前驱指针指向原尾节点(即头节点的前驱)newNode->prev = DL->prev;// 设置新节点的后继指针指向头节点newNode->next = DL;// 更新原尾节点的后继指针为新节点DL->prev->next = newNode;// 更新头节点的前驱指针为新节点DL->prev = newNode;
}
3.指定位置插入:指定位置插入操作先找到要插入位置的前一个节点,然后创建新节点,调整新节点的前驱和后继指针,使其指向该节点和该节点的下一个节点,最后调整该节点和该节点的下一个节点的指针,使其指向新节点。例如:
// 在双向循环链表指定位置插入节点
void insertAtPositionDList(DLinkList &DL, int pos, int data)
{// 创建一个指针 p,初始指向头节点DNode *p = DL;// 用于计数当前位置的变量int i = 0;// 遍历链表找到要插入的位置while (p->next!= DL && i < pos - 1) {p = p->next;i++;}// 如果遍历完没有找到指定位置,输出插入位置无效的提示信息并返回if (i!= pos - 1) {printf("插入位置无效!\n");return;}// 分配新节点的内存空间DNode *newNode = (DNode *)malloc(sizeof(DNode));newNode->data = data;// 设置新节点的前驱指针指向当前位置的节点 pnewNode->prev = p;// 设置新节点的后继指针指向当前位置节点 p 的下一个节点newNode->next = p->next;// 将当前位置节点 p 的下一个节点的前驱指针更新为新节点p->next->prev = newNode;// 将当前位置节点 p 的后继指针更新为新节点p->next = newNode;
}
(三)删除操作
1.删除头部节点:删除头部节点操作先保存头节点的下一个节点,然后调整头节点和头节点的下一个节点的下一个节点的指针,使其指向对方,最后释放头节点的下一个节点。例如:
// 删除双向循环链表的头部节点
void deleteHeadDList(DLinkList &DL)
{// 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回if (DL->next == DL) {printf("链表为空,无法删除头部节点!\n");return;}// 保存要删除的头节点DNode *temp = DL->next;// 更新头节点的 next 指针指向原来头节点的下一个节点DL->next = temp->next;// 更新新的头节点的前驱指针为原来的头节点的前驱(现在还是尾节点)temp->next->prev = DL;// 释放被删除的头节点的内存空间free(temp);
}
2.删除尾部节点:删除尾部节点操作先找到链表的尾节点的前一个节点,然后调整该节点和头节点的指针,使其指向对方,最后释放尾节点。例如:
// 删除双向循环链表的尾部节点
void deleteTailDList(DLinkList &DL)
{// 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回if (DL->next == DL) {printf("链表为空,无法删除尾部节点!\n");return;}// 找到尾节点的前驱节点DNode *p = DL->prev->prev;// 保存要删除的尾节点DNode *temp = DL->prev;// 更新尾节点前驱节点的 next 指针指向头节点p->next = DL;// 更新头节点的 prev 指针指向新的尾节点(原来尾节点的前驱)DL->prev = p;// 释放被删除的尾节点的内存空间free(temp);
}
3.删除指定位置节点:删除指定位置节点操作先找到要删除位置的前一个节点,然后保存要删除的节点,调整该节点和该节点的下一个节点的指针,使其指向对方,最后释放要删除的节点。例如:
// 删除双向循环链表指定位置的节点
void deleteAtPositionDList(DLinkList &DL, int pos)
{// 创建一个指针 p,初始指向头节点DNode *p = DL;// 用于计数当前位置的变量int i = 0;// 遍历链表找到要删除的位置的前一个节点while (p->next!= DL && i < pos - 1) {p = p->next;i++;}// 如果遍历完没有找到指定位置或者链表为空,输出删除位置无效的提示信息并返回if (i!= pos - 1 || p->next == DL) {printf("删除位置无效!\n");return;}// 保存要删除的节点DNode *temp = p->next;// 更新当前节点的 next 指针指向要删除节点的下一个节点p->next = temp->next;// 更新要删除节点的下一个节点的 prev 指针指向当前节点temp->next->prev = p;// 释放被删除的节点的内存空间free(temp);
}
(四)遍历与查找
1.遍历:遍历双向循环链表可以从任意一个节点开始,向前或向后遍历。例如从头节点开始向后遍历:
// 遍历双向循环链表
void traverseDList(DLinkList DL)
{// 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回if (DL->next == DL) {printf("链表为空!\n");return;}// 创建一个指针 p,初始指向头节点的下一个节点DNode *p = DL->next;// 遍历链表并输出每个节点的数据while (p!= DL) {printf("%d ", p->data);p = p->next;}printf("\n");
}
2.查找:查找指定元素可以从任意一个节点开始,向前或向后遍历,比较节点的数据域和要查找的元素是否相等。例如从头节点开始向后查找:
// 在双向循环链表中查找指定元素
DNode *findInDList(DLinkList DL, int data)
{// 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回 NULLif (DL->next == DL) {printf("链表为空!\n");return NULL;}// 创建一个指针 p,初始指向头节点的下一个节点DNode *p = DL->next;// 遍历链表查找指定元素while (p!= DL && p->data!= data) {p = p->next;}// 如果遍历完没有找到指定元素,输出未找到的提示信息并返回 NULLif (p == DL) {printf("未找到指定元素!\n");return NULL;}// 返回找到的节点指针return p;
}
(五)总结与展望
双向循环链表具有双向遍历、高效插入和删除等特点,在很多应用场景中都有重要的价值。例如在操作系统的内存管理中,可以使用双向循环链表来管理空闲内存块;在数据库系统中,可以用双向循环链表来实现事务的回滚和重做。未来,随着计算机技术的不断发展,双向循环链表可能会在更多的领域得到应用,并且可能会与其他数据结构结合,产生更强大的数据管理和处理能力。同时,对于双向循环链表的优化和改进也将是一个持续的研究方向,比如提高插入和删除操作的效率、减少内存占用等。总之,双向循环链表作为一种重要的数据结构,将在计算机科学的发展中继续发挥重要作用。
相关文章:
数据结构之——单循环链表和双向循环链表
一、单循环链表的奥秘 单循环链表是一种特殊的链表结构,它在数据结构领域中具有重要的地位。其独特的循环特性使得它在某些特定的应用场景中表现出强大的优势。 (一)结构与初始化 单循环链表的结构由节点组成,每个节点包含数据域…...
Git Stash: 管理临时更改的利器
Git 是一个非常强大的版本控制系统,它不仅帮助我们管理代码的版本,还提供了许多实用的功能来优化我们的工作流程。今天,我们要介绍的是 Git 中的一个非常实用的功能——git stash。 什么是 Git Stash? 在开发过程中,…...
ELK--收集日志demo
ELK--收集日志demo 安装ELK日志收集配置启动容器springboot配置测试 之前项目多实例部署的时候,由于请求被负载到任意节点,所以查看日志是开多个终端窗口。后来做了简单处理,将同一项目的多实例日志存入同一个文件,由于存在文件锁…...
Redis的主要特点及运用场景
Redis的主要特点及运用场景 Redis(Remote Dictionary Server)是一个开源的高性能键值对(key-value)数据库。它支持多种类型的数据结构,如字符串(strings)、散列(hashes&…...
与我免费ai书童拆解《坚持》创作历程
插科打诨的海侃胡闹,调侃舒展《坚持》诗创的灵魂盛宴之旅。 (笔记模板由python脚本于2024年09月30日 19:11:42创建,本篇笔记适合喜欢python和诗歌的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网:https://www.python.org/ Free&#x…...
昇思MindSpore进阶教程--下沉模式
大家好,我是刘明,明志科技创始人,华为昇思MindSpore布道师。 技术上主攻前端开发、鸿蒙开发和AI算法研究。 努力为大家带来持续的技术分享,如果你也喜欢我的文章,就点个关注吧 正文开始 昇腾芯片集成了AICORE和AICPU等…...
Hive SQL业务场景:连续5天涨幅超过5%股票
一、需求描述 现有一张股票价格表 dwd_stock_trade_dtl 有3个字段分别是: 股票代码(stock_code), 日期(trade_date), 收盘价格(closing_price) 。 请找出满足连续5天以上(含)每天上涨超过5%的股票,并给出连续满足…...
Java 如何从图片上提取文字
生活中我们可能会遇到想从图片上直接复制上边的文字,该如何获取呢,接下来看看如何使用Java程序实现从图片中读取文字。 实现过程 1、引入Tess4J 依赖 <!--Tess4J 依赖--> <dependency><groupId>net.sourceforge.tess4j</groupId…...
C#进阶-读写Excel常用框架及其使用方式
目录 一、MiniExcel开源框架(推荐) 1、写/导出 方式一 方式二 多表创建 更改配置 特性使用 CSV尾行新增行 CSV、XLSX互转 2、读/导入 简单示例 二、NPOI开源框架 一、MiniExcel开源框架(推荐) 添加NuGet包MiniExcel…...
Python爬虫lxml模块安装导入和xpath基本语法
lxml模块是Python的一个解析库,主要用于解析HTML和XML文件。 一、安装导入 使用包管理器安装,在cmd下或编辑器下的控制台,运行: pip install lxml 导入: from lxml import etree 二、xpath基础知识 XPath&#…...
python魔法(python高级magic方法进阶)
python特殊方法(magic方法也叫魔术方法) 魔法方法是python的内置函数,一般以双下划线开头和结尾, 构造和初始化 每个人都知道一个最基本的魔术方法, init 。 通过此方法我们可以定义一个对象的初始操作。 然而,当我调用 x S…...
【论文笔记】Flamingo: a Visual Language Model for Few-Shot Learning
🍎个人主页:小嗷犬的个人主页 🍊个人网站:小嗷犬的技术小站 🥭个人信条:为天地立心,为生民立命,为往圣继绝学,为万世开太平。 基本信息 标题: Flamingo: a Visual Langu…...
问:JAVA阻塞队列实现类及最佳实践?
在多线程编程中,阻塞队列作为一种关键的数据结构,为线程间安全、高效的数据交换提供了重要支持。Java的java.util.concurrent包中提供了多种阻塞队列的实现,每种实现都有其独特的特点和适用场景。 一、阻塞队列实现类 以下是Java中Blocking…...
Springboot3 + MyBatis-Plus + MySql + Vue + ProTable + TS 实现后台管理商品分类(最新教程附源码)
Springboot3 MyBatis-Plus MySql Uniapp 商品加入购物车功能实现(针对上一篇sku) 1、效果展示2、数据库设计3、后端源码3.1 application.yml 方便 AliOssUtil.java 读取3.2 model 层3.2.1 BaseEntity3.2.1 GoodsType3.2.3 GoodsTypeSonVo3.3 Controll…...
消费电子制造企业如何使用SAP系统提升运营效率与竞争力
在当今这个日新月异的消费电子市场中,企业面临着快速变化的需求、激烈的竞争以及不断攀升的成本压力。为了在这场竞赛中脱颖而出,消费电子制造企业纷纷寻求数字化转型的突破点,其中,SAP系统作为业界领先的企业资源规划(ERP)解决方…...
算法记录——树
二叉树 3.1二叉树的最大深度 思路:二叉树的最大深度 根节点的最大高度。因此本题可以转换为求二叉树的最大高度。 而求高度的时候应该采用后序遍历。遍历顺序为:左右中。每次遍历的节点按后序遍历顺序,先收集左右孩子的最大高度,…...
单片机在控制和自动化任务中的应用场景广泛
单片机在控制和自动化任务中的应用场景广泛,以下是一些具体示例: 1. 家电控制 洗衣机:单片机用于控制洗衣周期、温度和水位。微波炉:控制加热时间、功率和用户界面。 2. 工业自动化 生产线监控:单片机用于控制传送…...
UEFI EDK2框架学习(三)——protocol
一、Protocol协议 搜索支持特定Protocol的设备,获取其Handle gBS->LocateHandleBuffer 将内存中的Driver绑定到给定的ControllerHandle gBS->OpenProtocol 二、代码实现 Protocol.c #include <Uefi.h> #include <Library/UefiLib.h> #includ…...
PostgreSQL的字段存储类型了解
PostgreSQL的字段存储类型了解 在 PostgreSQL 中,每个字段(列)都有其存储类型,这些存储类型决定了数据库如何存储和处理该字段的数据。了解和适当地利用这些存储类型,可以提高数据库的性能和存储效率。 主要的存储类…...
CTFshow 命令执行 web29~web36(正则匹配绕过)
目录 web29 方法一:include伪协议包含文件读取 方法二:写入文件 方法三:通识符 web30 方法一:filter伪协议文件包含读取 方法二:命令执行函数绕过 方法三:写入文件 web31 方法一:filter伪…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...
【网络安全】开源系统getshell漏洞挖掘
审计过程: 在入口文件admin/index.php中: 用户可以通过m,c,a等参数控制加载的文件和方法,在app/system/entrance.php中存在重点代码: 当M_TYPE system并且M_MODULE include时,会设置常量PATH_OWN_FILE为PATH_APP.M_T…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...
Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...
STM32标准库-ADC数模转换器
文章目录 一、ADC1.1简介1. 2逐次逼近型ADC1.3ADC框图1.4ADC基本结构1.4.1 信号 “上车点”:输入模块(GPIO、温度、V_REFINT)1.4.2 信号 “调度站”:多路开关1.4.3 信号 “加工厂”:ADC 转换器(规则组 注入…...
js 设置3秒后执行
如何在JavaScript中延迟3秒执行操作 在JavaScript中,要设置一个操作在指定延迟后(例如3秒)执行,可以使用 setTimeout 函数。setTimeout 是JavaScript的核心计时器方法,它接受两个参数: 要执行的函数&…...
统计学(第8版)——统计抽样学习笔记(考试用)
一、统计抽样的核心内容与问题 研究内容 从总体中科学抽取样本的方法利用样本数据推断总体特征(均值、比率、总量)控制抽样误差与非抽样误差 解决的核心问题 在成本约束下,用少量样本准确推断总体特征量化估计结果的可靠性(置…...
