链表(2)——带头双向循环链表
🍁一、链表的分类
🌕1.单向或者双向
🌕2.带头或者不带头(有无哨兵)
🌕3.循环或者不循环
🌕4.无头单向非循环链表(常用)
🌕5.带头双向循环链表(常用)
🌕注意:
1. 无头单向非循环链表: 结构简单 ,一般不会单独用来存数据。实际中更多是作为 其他数据结 构的子结构 ,如哈希桶、图的邻接表等等。另外这种结构在 笔试面试 中出现很多。2. 带头双向循环链表: 结构最复杂 ,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
🍁二、双向链表的定义:
我们知道单链表的结点有一个数据域用于存放数据,一个指针域用于指向下一个结点。而 双向链表即是在此基础上每个结点多了一个指针域用于指向前一个结点;
🍁三、带头双向循环链表的定义
带头双向循环链表:即在双向链表的基础上,尾结点的next域指向头结点,使之体现出一个循环的结构。
🍁四、带头双向循环链表操作实现(多文件)
🌕1.定义:
只需在单链表定义的基础上多一个指针域prve,用于指向前驱;
typedef int SLDataType;typedef struct ListNode {struct ListNode* prev;//指向前驱struct ListNode* next;//指向后继SLDataType data;//数据域 }ListNode;
🌕2.获得新结点
因为后续经常用到此函数,所以首先介绍。
操作很简单,用malloc函数生成即可
//获得新结点 ListNode* BuyLTNode(SLDataType x) {//用malloc函数动态生成即可ListNode* node = (ListNode*)malloc(sizeof(ListNode));if (node == NULL){//检查malloc错误原因perror("malloc");exit(-1);}//处理新结点的成员node->data = x;node->next = NULL;node->prev = NULL;return node; }
🌕3.初始化
①:原本初始化需要改变头结点phead,所以需要结构体二级指针,但其他操作都不需要二级指针,所以为了排面,我们可以用返回值来代替使用二级指针;
②:初始化只需要获得一个新结点作为一个头结点,然后头结点的两个指针域互相指向代表此时为空表;
//初始化 ListNode* Init() {//获得头结点,头结点数据域可以存点有意义的数据,也可以随便存,因为用不着ListNode* phead = BuyLTNode(0);//初始化头结点的两个指针域指向头结点本身表示为空表phead->next = phead;phead->prev = phead;return phead; }
🌕4.尾插法
该种类链表虽然结构复杂,但操作却非常简单,比如尾插法就有几点优势于单链表;
2.1:优势
①:单链表尾插需要考虑元素是否为空,当链表中没有元素时会改变头指针(头结点),所以需要使用结构体二级指针;但带头双向循环链表因为带有头,所以不管有无元素,在尾插时只需改变结构体指针域,即改变结构体,所以都只需要使用结构体指针;
②:单链表尾插时需要找到尾结点,但带头双向循环链表不需要,因为多了一个prev指针域,头结点的prev域就是尾结点;可以参考上述图片;
2.2:尾插法大致分为“四步骤”:
首先创建一个临时指针tail指向头结点的prev域,即指向尾结点便于操作
①:将tail的next域指向新结点;
②:将新结点的prev域指向tail结点(尾结点);
③:将新结点的next域指向头结点;
④:将头结点的prev域指向新结点。
2.3:源代码
//尾插 //因为带有头,所以操作只需要改变结构体,所以只需要结构体指针 //具体操作看注释 void LTPushBack(ListNode* phead, SLDataType x) {//因为是带头的,所以phead至少是个头指针,所以phead不可能为空,所以需要用assert检查一下assert(phead);//找到尾结点tailListNode* tail = phead->prev;//获取新结点newnodeListNode* newnode = BuyLTNode(x);//四步骤tail->next = newnode;newnode->prev = tail;phead->prev = newnode;newnode->next = phead; }
🌕5.打印数据
此链表打印数据与单链表有一个区别,就是结束条件不同;因为带头双向循环链表的尾结点的next域不指向NULL,而是指向头结点,所以结束条件为“tail==head”;
//打印 void LTprint(ListNode* phead) {//创建一个临时指针便于遍历操作ListNode* node = phead->next;//为了体现此链表结构而打印printf("phead<->");//打印数据,当临时指针node等于头结点时结束while (node != phead){printf("%d<->", node->data);node = node->next;}//为了体现此链表结构而打印printf("phead\n"); }
🌕6.尾删法
6.1:相对于单链表,该链表也有几个优点:
①:尾删不用找尾结点以及倒数第二个结点,用prev域就可以找到;
②:当表中只有一个元素时,单链表需要改变结构体指针,所以需要单独分类;而此链表因为有带头结点和prev域,所以用正常尾删方法即可;
6.2:尾删步骤:
①:判断单链表是否为空(条件:phead->next=phead时即为空);
②:创建一个临时指针tail1用于保存尾结点,方便后续释放尾结点;
③:创建一个临时指针tail2用于保存尾结点的prev域(尾结点的前一个结点),方便进行尾删操作;
④:tail2的next域指向头结点:tail2->next=phead;
⑤:头结点的prev域指向tail2结点:phead->prev=tail2;
⑥:释放尾结点tail1。
6.3:源代码:
//尾删 void LTPopBack(ListNode* phead) {assert(phead);//检查是否为空if (phead->next == phead){printf("此链表为空,尾删失败!\n");return;}//临时指针保存结点ListNode* tail1 = phead->prev;ListNode* tail2 = phead->prev->prev;//断开与尾结点的链接tail2->next = phead;phead->prev = tail2;//释放尾结点free(tail1); }
🌕7.头插法
同上,因为prev的存在,所以不用考虑初始表是否为空表的情况;
7.1:四步骤:
①:新结点的next域指向head的next域(即指向插入前的首结点);
②:head的next域的prev域指向新结点(即插入前的首结点的prev域指向新结点);
③:新结点的prev域指向头结点head;
④:头结点head的next域指向新结点。
7.2:源代码
//头插 void LTPushFront(ListNode* phead, SLDataType x) {assert(phead);//新结点ListNode* newnode = BuyLTNode(x);//四步骤newnode->next = phead->next;phead->next->prev = newnode;newnode->prev = phead;phead->next = newnode; }
🌕8.头删法
头删法也很简单,只需考虑个个指针的链接即可;
8.1:步骤
①:创建临时指针first指向首结点,便于后续释放首结点;
②:创建临时指针second指向第二个结点,便于进行删除操作;
③:改变指针链接:
second->prev = phead;
phead->next = second;
④:释放首结点;
8.2:源代码
//头删 void LTPopFront(ListNode* phead) {assert(phead);if (phead->next == phead){printf("链表为空,头删失败!\n");return;}//临时指针first指向首结点,便于后续释放首结点//临时指针second指向第二个结点,便于进行删除操作ListNode* first = phead->next;ListNode* second = first->next;//删除second->prev = phead;phead->next = second;//释放首结点free(first); }
🌕9.在pos位置之前插入结点
其实很简单,只需要搞得指针域的链接顺序,防止指针丢失即可
9.1:源代码如下:
//在pos位置之前插入结点 ListNode* LTInsrt(ListNode* pos, SLDataType x) {assert(pos);//新结点ListNode* newnode = BuyLTNode(x);//插入pos->prev->next = newnode;newnode->prev = pos->prev;newnode->next = pos;pos->prev = newnode; }
9.2:有了这个算法后我们可以改进头插与尾插:
①:当pos==phead->next时,即为头插算法:
//头插 void LTPushFront(ListNode* phead, SLDataType x) {assert(phead);//改进LTInsrt(phead->next, x); }
②:当pos等于phead时,即为尾插算法:
//尾插 void LTPushBack(ListNode* phead, SLDataType x) {//因为是带头的,所以phead至少是个头指针,所以phead不可能为空,所以需要用assert检查一下assert(phead);//改进LTInsrt(phead, x); }
🌕10.删除pos位置的结点
10.1:步骤:
①:创建临时指针first保存pos前一个结点;
②:创建临时指针second保存pos后一个结点;
③:改变指针链接,删除pos结点:
first->next = second;
second->prev = first;
④:释放pos结点。
10.2:源代码
//删除pos位置的结点 void LTErase(ListNode* pos) {assert(pos);//临时指针ListNode* first = pos->prev;ListNode* second = pos->next;//删除first->next = second;second->prev = first;//释放posfree(pos); }
10.3:有了这个算法,我们可以改进头删与尾删
①:当pos==phead->next时,即为头删算法:
//头删 void LTPopFront(ListNode* phead) {assert(phead);//改进LTErase(phead->next); }
②:当pos==phead->prev时,即为尾删算法:
//尾删 void LTPopBack(ListNode* phead) {assert(phead);//改进LTErase(phead->prev); }
🍁五、测试源代码
main.c
#include"List.h"void STTest1() {ListNode* plist = NULL;plist = Init();//初始化//尾插LTPushBack(plist, 1);LTPushBack(plist, 2);LTPushBack(plist, 3);LTPushBack(plist, 4);LTPushBack(plist, 5);//打印LTprint(plist); }void STTest2() {ListNode* plist = NULL;plist = Init();//初始化//尾插LTPushBack(plist, 1);//打印LTprint(plist);//尾删LTPopBack(plist);//打印LTprint(plist); }void STTest3() {ListNode* plist = NULL;plist = Init();//初始化//头插LTPushFront(plist, 1);LTPushFront(plist, 2);//打印LTprint(plist);//头删LTPopFront(plist);//打印LTprint(plist);//头删LTPopFront(plist);//打印LTprint(plist); }int main() {//STTest1();//STTest2();STTest3();return 0; }
List.c
#include"List.h"//获得新结点 ListNode* BuyLTNode(SLDataType x) {//用malloc函数动态生成即可ListNode* node = (ListNode*)malloc(sizeof(ListNode));if (node == NULL){//检查malloc错误原因perror("malloc");exit(-1);}//处理新结点的成员node->data = x;node->next = NULL;node->prev = NULL;return node; }//初始化 ListNode* Init() {//获得头结点,头结点数据域可以存点有意义的数据,也可以随便存,因为用不着ListNode* phead = BuyLTNode(0);//初始化头结点的两个指针域指向头结点本身表示为空表phead->next = phead;phead->prev = phead;return phead; }//打印 void LTprint(ListNode* phead) {assert(phead);//创建一个临时指针便于遍历操作ListNode* node = phead->next;//为了体现此链表结构而打印printf("phead<->");//打印数据,当临时指针node等于头结点时结束while (node != phead){printf("%d<->", node->data);node = node->next;}//为了体现此链表结构而打印printf("phead\n"); }//尾插 //因为带有头,所以操作只需要改变结构体,所以只需要结构体指针 //具体操作看注释 void LTPushBack(ListNode* phead, SLDataType x) {//因为是带头的,所以phead至少是个头指针,所以phead不可能为空,所以需要用assert检查一下assert(phead);找到尾结点tail//ListNode* tail = phead->prev;获取新结点newnode//ListNode* newnode = BuyLTNode(x);四步骤//tail->next = newnode;//newnode->prev = tail;//phead->prev = newnode;//newnode->next = phead;//改进LTInsrt(phead, x); }//尾删 void LTPopBack(ListNode* phead) {assert(phead);检查是否为空//if (phead->next == phead)//{// printf("此链表为空,尾删失败!\n");// return;//}临时指针保存结点//ListNode* tail1 = phead->prev;//ListNode* tail2 = phead->prev->prev;断开与尾结点的链接//tail2->next = phead;//phead->prev = tail2;释放尾结点//free(tail1);//改进LTErase(phead->prev); }//头插 void LTPushFront(ListNode* phead, SLDataType x) {assert(phead);//新结点//ListNode* newnode = BuyLTNode(x);四步骤//newnode->next = phead->next;//phead->next->prev = newnode;//newnode->prev = phead;//phead->next = newnode;//改进LTInsrt(phead->next, x); }//头删 void LTPopFront(ListNode* phead) {assert(phead);/*if (phead->next == phead){printf("链表为空,头删失败!\n");return;}*/临时指针first指向首结点,便于后续释放首结点临时指针second指向第二个结点,便于进行删除操作//ListNode* first = phead->next;//ListNode* second = first->next;删除//second->prev = phead;//phead->next = second;释放首结点//free(first);//改进LTErase(phead->next); }//在pos位置之前插入结点 void LTInsrt(ListNode* pos, SLDataType x) {assert(pos);//新结点ListNode* newnode = BuyLTNode(x);//插入pos->prev->next = newnode;newnode->prev = pos->prev;newnode->next = pos;pos->prev = newnode; }//删除pos位置的结点 void LTErase(ListNode* pos) {assert(pos);//临时指针ListNode* first = pos->prev;ListNode* second = pos->next;//删除first->next = second;second->prev = first;//释放posfree(pos); }
List.h
#pragma once #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #include<assert.h>typedef int SLDataType;typedef struct ListNode {struct ListNode* prev;//指向前驱struct ListNode* next;//指向后继SLDataType data;//数据域 }ListNode;//获得一个新结点 ListNode* BuyLTNode(SLDataType x);//初始化 ListNode* Init();//打印 void LTprint(ListNode* phead);//尾插 void LTPushBack(ListNode* phead,SLDataType x);//尾删 void LTPopBack(ListNode* phead);//头插 void LTPushFront(ListNode* phead, SLDataType x);//头删 void LTPopFront(ListNode* phead);//在pos位置之前插入结点 void LTInsrt(ListNode* pos, SLDataType x);//删除pos位置的结点 void LTErase(ListNode* pos);
本次知识到此结束,希望对你有所帮助!
相关文章:

链表(2)——带头双向循环链表
🍁一、链表的分类 🌕1.单向或者双向 🌕2.带头或者不带头(有无哨兵) 🌕3.循环或者不循环 🌕4.无头单向非循环链表(常用) 🌕5.带头双向循环链表(常用…...

C语言 函数指针
函数指针是C语言中的一种特殊类型,它允许你像操作变量一样操作函数。函数指针的主要用途是存储并后续调用一组函数。 在C语言中,函数指针的定义通常如下所示: 返回类型 (*指针变量名)(参数类型) 例如,如果你有一个返回整数并接受…...

F. Vasilije Loves Number Theory
Problem - F - Codeforces 思路:分析一下题意,对于第一种操作来说,每次乘以x,那么nn*x,然后问是否存在一个a使得gcd(n,a)1并且n*a的约数个数等于n,有最大公约数等于1我们能够知道其实这两个数是互质的&…...

electron打包后主进程下载文件崩溃
electronvue3写了一个小项目,实现了一个文件下载功能 存在的问题 打包后,应用下载文件崩溃代码 // 渲染进程window.electron.ipcRenderer.invoke(save-file, {path: r.filePath,fileurl: previewUrl,}).then(response > {console.log(response ----…...

Spring实例化源码解析之Custom Events下集(九)
上集从官网的角度讲解了基本的使用和源码的内容,没有深入的进行分析,本章将从源码的角度分析ApplicationEvent、ApplicationListener、ApplicationEventMulticaster这三者之间的关系。 initApplicationEventMulticaster 上一章后续部分给出了源码的含义…...

python numpy库关键函数说明
python numpy库函数说明 np.argwhere()np.dtype()np.shape()np.zeros() np.argwhere() 输入参数是一个基本的逻辑表达式,输出检索结果的索引值。 >>> x np.arange(6).reshape(2,3) >>> x array([[0, 1, 2],[3, 4, 5]]) >>> np.argwhe…...

【Linux C】Linux如何执行一个程序(程序存储空间、系统调用、内核调用)
文章目录 一、程序存储空间1.1 C语言程序存储空间1.2 用户空间和内核空间1.3 用户模式和内核模式 二、内核调用-系统调用-C语言库函数2.1 系统调用和内核调用2.2 C语言库函数 三、Linux如何执行一个程序 一、程序存储空间 本节说的空间主要是指内存空间,即程序如何分…...

IP协议总结
一、定义。 IP全称为Internet Protocol,是TCP/IP协议族中的一员,负责实现数据在网络上的传输。它是一种无连接、不可靠的数据报协议。 IP协议常用于Internet网络和局域网中,它通过将数据包进行分组并进行逐跳转发来实现数据在网络中的传输。…...

微信支付v2
文档: https://pay.weixin.qq.com/wiki/doc/api/index.html 微信小程序:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter11_1 需要一个微信认证后的小程序,,还需要一个,在微信商户平台,&…...

tcpdump(二)命令行参数讲解(一)
一 tcpdump实战详解 1、我们做抓包,一般都需要指定条件,保证对系统的CPU、内存、磁盘资源不会产生过大的响应备注: 遇到过tcpdump持续抓包导致系统挂了2、条件:1) tcpdump的 基础命令选项参数2) 真正的 过滤条件 抓包工具tcpdump用法说明 ① 参数学…...

10_8C++
X-Mind #include <iostream>using namespace std; class Rect { private:int width;int heigjt; public:void init(int w,int h){width w;heigjt h;}void set_w(int w){width w;}void set_h(int h){heigjt h;}void show(){cout << "矩形的周长" <…...

JVM篇---第七篇
系列文章目录 文章目录 系列文章目录一、Minor GC与Full GC分别在什么时候发生?二、你知道哪些JVM性能调优参数?(简单版回答)三、对象一定分配在堆中吗?有没有了解逃逸分析技术?一、Minor GC与Full GC分别在什么时候发生? 新生代内存不够用时候发生MGC也叫YGC,JVM内存…...

更新Xcode 版本后运行项目出现错误 Unable to boot the Simulator 解决方法
错误截图 出现 Unable to boot the Simulator 错误原因很多,以下方法不一定都适用,我是通过以下方法解决的 打开命令终端输入以下命令,可能需要你输入开机密码 sudo rm -rf ~/Library/Developer/CoreSimulator/Caches...

winform窗体控件太多显示不过来,怎么实现滚动条
winform窗体控件太多显示不过来,怎么实现滚动条 Winform Panel实现滚动条 一、创建panel 在界面上拖拽一个父级Panel1,然后在Panel1里面拖拽一个子级Panel2 设置父级Panel1的AutoScroll属性为True 属性设置好后,当子级高度或者宽度大于父…...

WebSocket连接异常 Error parsing HTTP request header Connection reset by peer
问题描述 在使用spring的方式集成websocket时,在配置WebSocketConfigurer后 Configuration EnableWebSocket public class WebSocketConfiguration implements WebSocketConfigurer {ResourceServletWebSocketServerHandler servletWebSocketServerHandler;Overri…...

Spring中shutdown hook作用
在Spring框架中,Shutdown Hook(关闭钩子)是一种机制,用于在应用程序关闭时执行一些清理操作Spring会向JVM注册一个shutdown hook,在接收到关闭通知的时候,进行bean的销毁,容器的销毁处理等操作在…...

关于IvorySQL和OpenGauss包SPEC处理的一些思考
包的SPEC区可以定义下面三种类型(本篇只讨论SPEC区的情况) 变量类型(nested table等)(注意这是包内定义的类型,与SQL创建的不通)游标 这三种类型在PG原生中,是找不到相似的功能的&…...

我用PYQT5做的第一个实用的上位机项目(六)
将之前的画面和代码用复制粘贴的方法复制四份,就完成了整个主画面和主程序的基本构建。 下面的工作是关于PLC和通信。 上位机项目,其与PLC通信的模式很多都是这样的:在没有操作和设置的平常显示界面,按照预定周期从PLC读取当前页…...

【高级语言程序设计】python函数式编程(一)
基础知识 Python函数式编程的主要内容包括以下几个方面: (1)函数作为一等公民:在函数式编程中,函数被视为一等公民,可以像其他数据类型一样被传递、赋值以及作为返回值。 (2)不可变数据:函数式编程鼓励使用不可变数据…...

使用python查找指定文件夹下所有xml文件中带有指定字符的xml文件
文件夹目录如下(需要递归删除文件夹下的.DS_Store文件): labels文件夹下面是xml文件: import os import os.pathpath "name/labels" files os.listdir(path) # 得到文件夹下所有文件名称 s []for xmlFile in files:…...

flutter实现透明appbar(一)
前言 在项目中如何实现透明的appbar,方式一: 使用stack和positioned定位功能把appbar定位到页面的最上面, 实现 实现 Widget build(BuildContext context) {return Scaffold(body: Stack(children: [_homePage(), _appBar()],),);}_appbar…...

(四)正点原子STM32MP135移植——u-boot移植
一、概述 u-boot概述就不概述了,u-boot、kernel、dtb三件套,dddd 经过国庆艰苦奋战,已经成功把所有功能移植好了 二、编译官方代码 进入u-boot的目录 2.1 解压源码、打补丁 /* 解压源码 */ tar xf u-boot-stm32mp-v2022.10-stm32mp-r1-r0.…...

[计算机入门] 应用软件(办公类)
3.19 应用软件(办公类) 3.19.1 Microsoft office办公软件套件 Microsoft Office 是一套广泛使用的办公软件套件,由Microsoft公司开发和发布。它包含了多个应用程序,用于处理各种办公任务。以下是Office常见的几个应用程序: Microsoft Word…...

基于安卓android微信小程序音乐播放器
运行环境 小程序前端框架:uniapp 小程序运行软件:微信开发者 后端技术:javaSsm(SpringSpringMVCMyBatis)vue.js 后端开发环境:idea/eclipse 数据库:mysql 项目介绍 音乐播放器小程序的设计主要是对系统所要实现的功能进行详细考虑,确定所要…...

Java的指针、引用与C++的指针、引用的对比
笔者前两天在参加菜鸟面试的时候被面试官问到了这个问题,由于只在本科程序设计课上学过C,已经好久没有开发实际项目,所以对C相关的指针以及引用的记忆较为模糊,在此进行一定的知识汇总与梳理。 我们以面试中出现的问题为例来进行整…...

串级/级联控制知识点整理
串级控制系统是改善控制质量的有效方法之一,在过程控制中得到了广泛的应用。所谓串级控制,就是采用两个控制器串联工作,外环控制器的输出作为内环控制器的设定值,由内环控制器的输出去操纵控制阀,从而对外环被控量具有…...

数据产品读书笔记——认识数据产品经理
🌻大家可能听说的更多是产品经理这个角色,对数据产品经理可能或多或少了解一些,但又不能准确的描述数据产品经理的主要职能和与其他产品的不同,因此通过读一些书来对数据产品经理有一个准确且全面的认知。 目录 1. 数据的产品分类…...

从 0 到 1 ,手把手教你编写《消息队列》项目(Java实现) —— 创建虚拟机
文章目录 一、虚拟机二、关于消息的API发布消息直接交换机 DIRECT 转发规则扇出交换机 FANOUT 转发规则主题交换机 TOPIC 转发规则匹配规则Router类 订阅消息消费者队列如何给订阅的消费者发送消息自动发送消息至订阅者 应答消息 三、代码编写 一、虚拟机 接下来要创建虚拟机,…...

GIT版本控制--前言
欢迎来到《GIT版本控制》专栏!在当今软件开发和协作的世界中,版本控制是不可或缺的工具之一。无论您是一名初学者,一位经验丰富的开发者,还是一个项目团队的成员,都有可能会受益于对GIT的深入了解。 GIT是一个强大的分…...

由于 MAC 地址的问题,导致网络不通的原因和分析
由于 MAC 地址的问题,导致网络不通的原因和分析 将现象及原因分析发给大家,供大家参考,以后有类似问题时有个解决问题的参考开发板网络不通,也抓不到包,折腾了好久,将电脑和开发板用网线直连,结…...