初阶数据结构:链表(二)

目录

 一、前言

二、带头双向循环链表

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

(1)什么是带头?

  (2)什么是双向呢?

(3)那什么是循环呢?

2.带头双向循环链表的实现

(1)节点结构

(2)创建链表的头节点,也即哨兵节点

(3)创建其他节点和链表打印

(4)链表尾插和尾删功能的实现

(5)链表的头插和头删

(6)链表的查找

(7)双链表在pos位置前后插入和删除pos位置功能


一、前言

          在上一篇博客中,我们实现的是单链表。我们知道链表有8种结构,由单向和双向、有头和无头、循环和非循环组合而成。单链表就是无头单向非循环链表。它是一种结构简单的链表,通常不会用来单独作为存储数据用,实际中更多的是作为其它数据结构的子结构存在,如哈希桶、图的邻接等等。单链表虽然在头插、头删方面很方便,但在尾插和尾删又比不过顺序表。那么有没有一种链表,在头插头删和尾插尾删上都很方便呢?

         当然有,那就是链表中的“王者”------带头双向循环链表。它的结构非常复杂,但效率极高。让我们来看看带头双向循环链表的结构吧!

二、带头双向循环链表

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

(1)什么是带头?

指链表中没有存储数据的节点时,头指针仍然指向一个节点,这个节点不存储数据,只起到站位的作用,其后才是链表的实际数据节点,因此它也被成为哨兵节点。

它的作用是什么呢?在单链表中,我们在改变头指针的链接对象时,需要使用二级指针。有了哨兵节点,我们就不需要二级指针。在执行插入、删除等操作时,也不需要对链表是否为空或是否为最后一个节点进行特殊判断,从而使代码更加简洁和统一,也让我们不至于被绕晕。

  (2)什么是双向呢?

看图就可以明白。

节点结构体指针域中有两个指针。一个指向上一个节点,一个指向下一个节点。如此就可以由一个链表中的一个节点位置得到所有节点的位置。

(3)那什么是循环呢?

循环链表则是将尾节点的后继指针指向头结点,而头结点的前驱指针指向尾节点,从而形成一个闭环。这样的设计使得从链表的任何一个节点开始都可以很方便地遍历整个链表,无论是向前还是向后。也即:

循环链表的哨兵节点的头指针指向尾节点,尾节点的next指针指向哨兵节点。

不要看带头双向循环链表的结构很复杂,就认为它的实现也很难,正因为结构如此,它的实现也避开了需多难题。相比于单链表的实现,它反而简单。

2.带头双向循环链表的实现

(1)节点结构

为了实现双向,那么作为节点的结构体就需要有两个指针和存储数据的位置。代码如下:

typedef int type;
typedef struct ListNde
{struct ListNde* next;//指向下一个节点struct ListNde* head;//指向上一个节点type data;//存储数据
}ListNode;

(2)创建链表的头节点,也即哨兵节点

先看代码:

ListNode* ListCreate()
{//动态申请一个结构体空间ListNode* head = (ListNode*)malloc(sizeof(ListNode));if (head == NULL){perror("ListCreate::malloc");return NULL;}//使节点不存储数据head->data = NULL;//因为作为头节点存在,因此在没有其他节点时,需要让//前指针和后指针都指向自己head->head = head;head->next = head;//如果不返回,那就需要传二级指针来使头指针和哨兵节点链接return head;
}

为什么哨兵节点的前指针和后指针都要指向自己。看图:

(3)创建其他节点和链表打印

创建其他节点和之前单链表创建节点一样,代码如下:

ListNode* BuyListNode()
{ListNode* ptr = (ListNode*)malloc(sizeof(ListNode));if (ptr == NULL){perror("BuyListNode::malloc");return NULL;}ptr->head = NULL;ptr->next = NULL;return ptr;
}

链表打印和单链表打印大至一样,循环打印即可,但与单链表打印不同的是,它需要有一个循环终止条件,单纯的不为空可不行,带头双向循环链表可没有为空。而且因为哨兵节点没有存储数据,因此要避免打印哨兵节点。代码如下:

void ListPrint(ListNode* plist)
{//不打印哨兵节点,哨兵节点不存储任何数据ListNode* ptr = plist->next;//以这个代表哨兵节点printf("<head>");//循环打印while (ptr){printf("<%d>", ptr->data);//因为带头双向循环链表尾节点和哨兵节点是相链接的//所以需要一个条件来作为循环的终止条件if (ptr->next == plist){//ptr指向尾节点时,ptr->next指向哨兵节点,退出循环break;}ptr = ptr->next;}
}

(4)链表尾插和尾删功能的实现

先看图:

我们想要使新节点和链表链接最好带顺序的去进行指针的互换,不然容易漏掉,或者换错。我们先让尾节点的后指针指向新节点,新节点的前指针指向尾节点,新节点的后指针指向哨兵节点,哨兵节点的前指针指向新节点,这样按顺序来,既不容易漏掉,也具有逻辑美,更容易理解。代码如下

void ListPushBack(ListNode* plist)
{//断言判断plist不为空,因为有哨兵节点存在,那么plist//传过来的必定不为空assert(plist);ListNode* ptr = plist;//由ptr->head得到尾节点ListNode* tail = ptr->head;//申请一个新的节点ListNode* NewNode = BuyListNode();printf("请输入要尾插的数字\n");scanf("%d", &NewNode->data);//尾插:尾节点的next指向新节点tail->next = NewNode;//新节点的前指针指向尾节点NewNode->head = tail;//新节点的后指针指向哨兵节点NewNode->next = ptr;//哨兵节点的前指针指向新节点ptr->head = NewNode;
}

链表的尾删也很简单,只需按上述步骤逆着来然后释放要删除的空间就行了。代码如下:

void ListPopBack(ListNode* plist)
{assert(plist);ListNode* ptr = plist;ListNode* tail = ptr->head;ListNode* tail1 = tail->head;tail1->next = ptr;ptr->head = tail1;free(tail);printf("删除成功\n");
}

(5)链表的头插和头删

链表的头插和头删和链表的尾插尾删差不多,只不过这里的头插和头删是在哨兵节点的下一个节点,不是让你删或者插在哨兵节点后面。并且,在所有带头链表的删除功能里一定不能删除哨兵节点。那会出现野指针的,程序运行也会不安全。代码如下:

// 双向链表头插
void ListPushFront(ListNode* plist)
{assert(plist);ListNode* ptr = plist;ListNode* newnode = BuyListNode();ListNode* ptrnext = ptr->next;ptr->next = newnode;newnode->head = ptr;newnode->next = ptrnext;ptrnext->head = newnode;printf("请输入头插数字\n");scanf("%d", &newnode->data);printf("插入成功\n");
}
// 双向链表头删
void ListPopFront(ListNode* plist)
{assert(plist);ListNode* ptr = plist;ListNode* ptrnext2= ptr->next->next;free(ptr->next);ptr->next = ptrnext2;ptrnext2->head = ptr;printf("删除成功\n");
}

(6)链表的查找

ListNode* ListFind(ListNode* plist)
{assert(plist);int a;printf("请输入要查找的数字\n");scanf("%d", &a);ListNode* ptr = plist->next;while (ptr){//查找到直接返回if (ptr->data == a){printf("查找成功\n");return ptr;}//一定要有这个条件,防止查找不到陷入死循环if (ptr->next == plist){printf("查找失败,未找到\n");return NULL;}ptr = ptr->next;}
}

(7)双链表在pos位置前后插入和删除pos位置功能

这些和头插头删没有多大区别,且双链表在pos位置前插入比单链表简单。代码如下:

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos)
{assert(pos);ListNode* front = pos->head;ListNode* ptr = pos;ListNode* newnode = BuyListNode();front->next = newnode;newnode->head = front;newnode->next = ptr;ptr->head = newnode;printf("请输入要在pos前插入的数\n");scanf("%d", &newnode->data);printf("插入成功\n");
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{assert(pos);ListNode* front = pos->head;ListNode* posnext = pos->next;free(pos);front->next = posnext;posnext->head = front;printf("删除成功\n");
}
// 双向链表在pos位置之后插入
void ListInsertAfter(ListNode* pos)
{assert(pos);ListNode* posnext = pos->next;ListNode* newnode = BuyListNode();ListNode* ptr = pos;ptr->next = newnode;newnode->head = ptr;newnode->next = posnext;posnext->head = newnode;printf("请输入要插入的数字\n");scanf("%d", &newnode->data);
}

这样一个带头双向循环链表也就完成了。单链表和带头双向循环链表虽然是两个极端,但当我们可以自主实现后,其他的链表结构我们也可以信手拈来。

全部代码如下:

listnode.h:

#pragma once
#pragma warning(disable : 4996)
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int type;
typedef struct ListNde
{struct ListNde* next;//指向下一个节点struct ListNde* head;//指向上一个节点type data;//存储数据
}ListNode;
// 创建链表的头节点,也即哨兵节点.
ListNode* ListCreate();
//创建其他节点
ListNode* BuyListNode();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
// 双向链表在pos位置之后插入
void ListInsertAfter(ListNode* pos);

Flistnode.c:

#include"listnode.h"
ListNode* ListCreate()
{//动态申请一个结构体空间ListNode* head = (ListNode*)malloc(sizeof(ListNode));if (head == NULL){perror("ListCreate::malloc");return NULL;}//使节点不存储数据head->data = NULL;//因为作为头节点存在,因此在没有其他节点时,需要让//前指针和后指针都指向自己head->head = head;head->next = head;//如果不返回,那就需要传二级指针来使头指针和哨兵节点链接return head;
}ListNode* BuyListNode()
{ListNode* ptr = (ListNode*)malloc(sizeof(ListNode));if (ptr == NULL){perror("BuyListNode::malloc");return NULL;}ptr->head = NULL;ptr->next = NULL;return ptr;
}
void ListDestory(ListNode* plist)
{//assert(plist);ListNode* ptr = plist->next;while (ptr){ListNode* ptrnext = ptr->next;free(ptr);if (ptrnext == plist){break;}ptr = ptrnext;}free(plist);printf("销毁成功\n");
}
void ListPrint(ListNode* plist)
{//不打印哨兵节点,哨兵节点不存储任何数据ListNode* ptr = plist->next;//以这个代表哨兵节点printf("<head>");//循环打印while (ptr){printf("<%d>", ptr->data);//因为带头双向循环链表尾节点和哨兵节点是相链接的//所以需要一个条件来作为循环的终止条件if (ptr->next == plist){//ptr指向尾节点时,ptr->next指向哨兵节点,退出循环break;}ptr = ptr->next;}
}void ListPushBack(ListNode* plist)
{//断言判断plist不为空,因为有哨兵节点存在,那么plist//传过来的必定不为空assert(plist);ListNode* ptr = plist;//由ptr->head得到尾节点ListNode* tail = ptr->head;//申请一个新的节点ListNode* NewNode = BuyListNode();printf("请输入要尾插的数字\n");scanf("%d", &NewNode->data);//尾插:尾节点的next指向新节点tail->next = NewNode;//新节点的前指针指向尾节点NewNode->head = tail;//新节点的后指针指向哨兵节点NewNode->next = ptr;//哨兵节点的前指针指向新节点ptr->head = NewNode;
}void ListPopBack(ListNode* plist)
{assert(plist);ListNode* ptr = plist;ListNode* tail = ptr->head;ListNode* tail1 = tail->head;tail1->next = ptr;ptr->head = tail1;free(tail);printf("删除成功\n");
}
// 双向链表头插
void ListPushFront(ListNode* plist)
{assert(plist);ListNode* ptr = plist;ListNode* newnode = BuyListNode();ListNode* ptrnext = ptr->next;ptr->next = newnode;newnode->head = ptr;newnode->next = ptrnext;ptrnext->head = newnode;printf("请输入头插数字\n");scanf("%d", &newnode->data);printf("插入成功\n");
}
// 双向链表头删
void ListPopFront(ListNode* plist)
{assert(plist);ListNode* ptr = plist;ListNode* ptrnext2= ptr->next->next;free(ptr->next);ptr->next = ptrnext2;ptrnext2->head = ptr;printf("删除成功\n");
}ListNode* ListFind(ListNode* plist)
{assert(plist);int a;printf("请输入要查找的数字\n");scanf("%d", &a);ListNode* ptr = plist->next;while (ptr){//查找到直接返回if (ptr->data == a){printf("查找成功\n");return ptr;}//一定要有这个条件,防止查找不到陷入死循环if (ptr->next == plist){printf("查找失败,未找到\n");return NULL;}ptr = ptr->next;}
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos)
{assert(pos);ListNode* front = pos->head;ListNode* ptr = pos;ListNode* newnode = BuyListNode();front->next = newnode;newnode->head = front;newnode->next = ptr;ptr->head = newnode;printf("请输入要在pos前插入的数\n");scanf("%d", &newnode->data);printf("插入成功\n");
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{assert(pos);ListNode* front = pos->head;ListNode* posnext = pos->next;free(pos);front->next = posnext;posnext->head = front;printf("删除成功\n");
}
// 双向链表在pos位置之后插入
void ListInsertAfter(ListNode* pos)
{assert(pos);ListNode* posnext = pos->next;ListNode* newnode = BuyListNode();ListNode* ptr = pos;ptr->next = newnode;newnode->head = ptr;newnode->next = posnext;posnext->head = newnode;printf("请输入要插入的数字\n");scanf("%d", &newnode->data);
}

listnode.c:

#include"listnode.h"
ListNode* pplist = NULL;
int main()
{int a;pplist = ListCreate();ListNode* pos = NULL;do{printf("请输入数字");scanf("%d", &a);switch (a){case 1:// 双向链表打印ListPrint(pplist);break;case 2:// 双向链表尾插ListPushBack(pplist);break;case 3:// 双向链表的头插ListPushFront(pplist);break;case 4:// 双向链表的尾删ListPopBack(pplist);break;case 5:// 双向链表头删ListPopFront(pplist);break;case 6:// 双向链表查找pos = ListFind(pplist);break;case 7:// 双向链表在pos位置之后插入xListInsertAfter(pos);break;case 8:// 在pos的前面插入ListInsert(pos);break;case 9:// 删除pos位置ListErase(pos);break;case 0://链表的销毁ListDestory(pplist);printf("退出\n");break;default:printf("输入数字错误,请重新输入\n");break;}} while (a);return 0;
}

至此,链表就完了,大家可以去力扣或者牛客上找一些链表的题做一做来巩固一下。提个建议,如果在链表中,或者说在整个数据结构中,画图永远是你最好的伙伴。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

项目开发实践——基于SpringBoot+Vue3实现的在线考试系统(九)(完结篇)

文章目录 一、成绩查询模块实现1、学生成绩查询功能实现1.1 页面设计1.2 前端页面实现1.3 后端功能实现2、成绩分段查询功能实现2.1 页面设计2.2 前端页面实现2.3 后端功能实现二、试卷练习模块实现三、我的分数模块实现1、 页面设计2、 前端页面实现3、 后端功能实现四、交流区…

环境搭建--vscode

vscode官网下载合适版本 安装vscode插件 安装 MinGW 配置环境变量 把安装目录D&#xff1a;\mingw64 配置在用户的环境变量path里即可 选择用户环境变量path 点确定保存后开启cmd输入g&#xff0c;如提示no input files 则说明Mingw64 安装成功&#xff0c;如果提示g 不是内…

爱的魔力转圈圈,基于carsim与simulink模拟仰望u8原地调头

仰望U8原地转向的示意图如下&#xff0c;不动方向盘的情况下&#xff0c;车可以自己转圈圈&#xff1a; 原理也很简单&#xff0c;仰望u8是四轮驱动&#xff0c;四个轮子都单独由四个轮边电机驱动。主要我们将左右的车轮轮速控制成左右两边轮速相同&#xff0c;但是方向相反&am…

1.1第1章DC/DC变换器的动态建模-1.1状态平均的概念--电力电子系统建模及控制 (徐德鸿)--读书笔记

电力电子系统一般由电力电子变换器&#xff08;滤波电路和开关&#xff09;、PWM 调制器、驱动电路、反馈控制单元构成&#xff0c;如图1-1所示。由控制理论的知识&#xff0c;电力电子系统的静态和动态性能的好坏与反馈控制设计密切相关。要进行反馈控制设计&#xff0c;首先要…

6. 使用springboot做一个音乐播放器软件项目【1.0版项目完结】附带源码~

#万物OOP 注意&#xff1a; 本项目只实现播放音乐和后台管理系统。 不分享任何音乐歌曲资源。 上一篇文章我们 做了音乐播放器后台的功能。参考地址&#xff1a; https://jsonll.blog.csdn.net/article/details/145214363 这个项目已经好几天也没更新了&#xff0c;因为临近放…

WGCLOUD使用介绍 - 如何监控ActiveMQ和RabbitMQ

根据WGCLOUD官网的信息&#xff0c;目前没有针对ActiveMQ和RabbitMQ这两个组件专门做适配 不过可以使用WGCLOUD已经具备的通用监测模块&#xff1a;进程监测、端口监测或者日志监测、接口监测 来对这两个组件进行监控

豆包MarsCode:字符串字符类型排序问题

问题描述 思路分析 我们需要对字符串中的字母、数字、问号按照规则进行排序&#xff0c;具体要求是&#xff1a; 问号的位置不变。数字的位置不变&#xff0c;但数字之间要按照从大到小排序。字母的位置不变&#xff0c;但字母之间要按照字典序排序。 解决此问题的思路分为以…

[STM32 标准库]定时器输出PWM配置流程 PWM模式解析

前言&#xff1a; 本文内容基本来自江协&#xff0c;整理起来方便日后开发使用。MCU&#xff1a;STM32F103C8T6。 一、配置流程 1、开启GPIO&#xff0c;TIM的时钟 /*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟RCC_APB2PeriphClockC…

无人机红外热成像:应急消防的“透视眼”

无人机红外热成像&#xff1a;应急消防的“透视眼” 亲爱的小伙伴们&#xff0c;每年一到夏天&#xff0c;应急消防的战士们就像上紧了发条的闹钟&#xff0c;时刻准备应对各种灾害。炎热天气让火灾隐患“蹭蹭”往上涨&#xff0c;南北各地还有防洪救灾、台风、泥石流等灾害轮…

【Redis】常见面试题

什么是Redis&#xff1f; Redis 和 Memcached 有什么区别&#xff1f; 为什么用 Redis 作为 MySQL 的缓存&#xff1f; 主要是因为Redis具备高性能和高并发两种特性。 高性能&#xff1a;MySQL中数据是从磁盘读取的&#xff0c;而Redis是直接操作内存&#xff0c;速度相当快…

LLM - 大模型 ScallingLaws 的指导模型设计与实验环境(PLM) 教程(4)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/145323420 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 Scalin…

书生大模型实战营2

L0——入门岛 Python基础 Conda虚拟环境 虚拟环境是Python开发中不可或缺的一部分&#xff0c;它允许你在不同的项目中使用不同版本的库&#xff0c;避免依赖冲突。Conda是一个强大的包管理器和环境管理器。 创建新环境 首先&#xff0c;确保你已经安装了Anaconda或Minico…

力扣111二叉树的最小深度(DFS)

Problem: 111. 二叉树的最小深度 文章目录 题目描述思路复杂度Code 题目描述 思路 1.欲望求出最短的路径&#xff0c;先可以记录一个变量minDepth&#xff0c;同时记录每次当前节点所在的层数currentDepth 2.在递的过程中&#xff0c;每次递一层&#xff0c;也即使当前又往下走…

Scrapy如何设置iP,并实现IP重用, IP代理池重用

前置知识 1/3乐观锁 2/3 Scrapy流程(非全部) 3/3 关于付费代理 我用的"快代理", 1000个ip, 每个ip1min的有效期, 你用的时候, 把你的链接, 用户名填上去就行 设置代理IP &#x1f512; & 帮助文档: ①meta ②meta#proxy$ 语法: ①proxy的设置: Request对象中…

渗透测试-WAF是什么以及原理解释 waf功能详解

目录 waf功能介绍 waf出现的地点: 什么是waf 功能: 常见的系统攻击分为两类 一是利用Web服务器的漏洞进行攻击 二是利用网页自身的安全漏洞进行攻击 WAF主要功能&#xff1a; waf的特点1 waf主要功能2 网马木马主动防御及查杀 流量监控 网站漏洞防御功能 危险组件…

KF-GINS源码阅读

原始 Markdown文档、Visio流程图、XMind思维导图见&#xff1a;https://github.com/LiZhengXiao99/Navigation-Learning 文章目录 一、KF-GINS 简介1、程序概述2、相关资料3、文件结构4、第三方库 二、编译、调试三、类型定义1、核心类&#xff1a;GIEngine2、文件读写类型3、配…

基础项目实战——3D赛车(c++)

目录 前言一、渲染引擎二、关闭事件三、梯形绘制四、轨道绘制五、边缘绘制六、草坪绘制七、前后移动八、左右移动​九、曲线轨道​十、课山坡轨道​十一、循环轨道​十二、背景展示​十三、引入速度​十四、物品绘制​十五、课数字路障​十六、分数展示​十七、重新生成​十八、…

探索与创新:DeepSeek R1与Ollama在深度研究中的应用

在当今信息爆炸的时代&#xff0c;获取和处理信息的能力变得至关重要。特别是在学术和研究领域&#xff0c;如何有效地进行深度研究是一个亟待解决的问题。最近&#xff0c;一个名为DeepSeek R1的模型结合Ollama平台提供了一种创新的解决方案。本文将分析并解构这一新兴的研究工…

【Linux】gdb——Linux调试器

gdb使用背景 程序的发布方式有两种&#xff0c;debug模式和release模式 Linux gcc/g出来的二进制程序&#xff0c;默认是release模式 要使用gdb调试&#xff0c;必须在源代码生成二进制程序的时候, 加上 -g 选项 gdb使用方法 首先进入gdb gdb test_glist显示代码 断点 b 行…

【单链表算法实战】解锁数据结构核心谜题——环形链表

题目如下&#xff1a; 解题过程如下&#xff1a; 环形链表&#xff1a;尾结点的next指针不为空&#xff0c;而是指向链表中的任一结点。 思路&#xff1a;快慢指针&#xff0c;慢指针每次走一步&#xff0c;快指针每次走两步。快慢指针在环中追逐相遇&#xff0c;那么这个链表…