【数据结构】双向链表(真正的零基础)

链表是一种物理存储单元上非连续非顺序的存储结构。数据元素的逻辑顺序是通过指针的链接来实现的!在上篇我们学习了单向链表,而单向链表虽然空间利用率高,插入和删除也只需改变指针就可以达到!但是我们在每次查找、删除、访问.....时,都受到单向链表的一个特性干扰:单向不可逆,只能以遍历的形式去使用,这样使得需要从头到尾找到特定的节点再进行操作!今天我们来学习另外一种链表结构:双向链表。既然是零基础,我会从小白的角度进行讲解!放心食用!

目录

                                                            一.链表的分类

                                                   二.带头双向循环链表

                                                                               二.(1)链表节点设置

                                                                      二.(2)链表新增节点

                                                             二.(3)链表初始化 

                                                    二.(4)链表扩容节点

                                                    二.(5)链表打印

                                                    二.(6)在目标节点前面插入

                                                    二.(7)在目标节点后面插入

                                                    二.(8)链表头删

                                                    二.(9)链表尾删

                                                    二.(10)链表头插

                                                    二.(11)链表尾插

                                                    二.(12)删除中间目标节点

                                                    二.(13)释放双链表

               三.总结

               四.代码合并


一.链表的分类

这里就不介绍哈是链表了,如果对此感到疑惑,可以看我上一篇投稿,咱直接进入分类!

链表主要可以分为以下3大类:

单向链表:

单向链表是链表中最简单的一种,它由一系列节点组成,每个节点包含两个部分:指针域与数据域。指针域用来指向下一个节点的地址,数据域用来存储数据。以下是抽象草图,方便巧记:

双向链表:

 双向链表与单向链表不同,它的每个节点有两个指针(nextprev)跟一个数据域,两个指针分别指向前一个节点和后一个节点。同样,如图巧记:

循环链表:
 循环链表是一种特殊的单链表,它的最后一个节点的指针指向头节点,形成一个“环”,如图:

在进行下面的学习之前,针对链表的各种分类,我们需要理清几个名词:

头指针:

头指针本质上是结构体类型的指针,指向一个节点,这个节点一般是 第一个节点 或者 头节点

头结点:

简单理解为在 第一个节点 前面还有一个节点,这个节点就称为头节点。头节点一般不存储数据

带头:

头指针指向头节点就称为带头节点,如图:

不带头: 

头指针指向第一个节点就称为不带头节点,如图:

针对这几种分类:带头或者不带头、单向或者双向、循环或者非循环 

我们总共可以组合八种链表,我们主要学习 无头双向非循环链表     带头双向循环链表    这两种!

八种链表结构中其中这两种最具代表性,另外六种便自然理解了!上一篇我们已经学完了第一种,今天我们来学习第二种!

注意:双链表一般需要改变四个指针的指向,因此建议大家画图理解!这样就可以轻松操作了!

二.带头双向循环链表

理解了上面几个名词后,我们就能轻易理解哈是带头双向循环链表了!

带头:无非就是第一个节点前还有一个头节点嘛!

双向:节点里面有两个指针,两个指针分别指向一前一后的节点

循环:尾节点的后指针指向头节点,头节点的前指针指向尾节点,达到封闭,形成循环!

二.(1)链表节点设置

不论怎样,我们都需要先创建一个结构体作为接下来各种功能的节点基础, 这个结构体里面有两个指针,一个数据域。这里为了缩短结构体类型,我们用 typedef 重定义以下,需要注意的是typedef重定义的这个结构体里面,结构体指针必须写完整,因为此处还属于声明阶段,后续才可以正常缩写。代码如下:

typedef struct Pointdef
{struct Pointdef* next;//指向下一个节点struct Pointdef* prev;//指向上一个节点int data;//数据域
}Pointdef;
二.(2)链表新增节点

因为我们不管初始化还是各种增添功能,都需要节点,所以我们先完成开辟节点的函数,让这个函数返回指向这个开辟好的节点指针,然后才可以进行初始化操作,代码如下:

//开辟新节点
Pointdef* Newspoint(int x)
{Pointdef* news = (Pointdef*)malloc(sizeof(Pointdef));if (news == NULL){perror("malloc");return NULL;}//初始化新节点news->next = NULL;news->prev = NULL;news->data = x;return news;
}

我们刚开辟好的节点还没有连接,只是一个结构体大小,因此它的prevnext指针都指向NULLx就是初始化节点的数据域,如下图理解:

二.(3)链表初始化 

上面我们先调用 新增节点 的函数,在外面开辟一个节点作为头节点(如果有问题,可以先看下面的合并代码!),接下来对这个头节点进行初始化,将它作为头节点,此时头节点的nextprev指针都应该指向自己(因为是循环链表,此时只有一个节点)。代码如下:

//链表初始化
void Initialize(Pointdef* head)
{head->next = head;head->prev = head;
}

二.(4)链表扩容节点

此时我们已经初始化了链表,下面我们给它新增几个节点,调用新增函数,改变节点指针指向,代码如下:

//链表扩容节点
Pointdef* tail = head;
for (int i = 2; i <= 5; i++)
{Pointdef* news = Newspoint(i);//连接tail->next = news;news->prev = tail;news->next = head;head->prev = news;tail = tail->next;
}

二.(5)链表打印

与单向链表访问区别不大,只需要注意是这里的链表是循环的,需要控制打印的条件不能是当前指针不能指向头节点,否则就是死循环了,代码如下: 

//链表打印
void Printf(Pointdef* head)
{Pointdef* tail = head->next;while (tail != head){printf("%d <= ", tail->data);tail = tail->next;}printf("NULL\n");
}

二.(6)在目标节点前面插入

我们已经写好了一条双链表,那么我们只需要找到目标节点然后连接四个指针就行了,代码如下:

这里我们假设目标节点是 tail ,新节点 news  新节点前面的节点是 prev

//在目标节点前面插入
void FrontInsertion(Pointdef* tail ,int x)
{assert(tail);Pointdef* prev = tail->prev;Pointdef* news = Newspoint(x);prev->next = news;news->prev = prev;news->next = tail;tail->prev = news;}

二.(7)在目标节点后面插入

将找到的目标节点作为参数,然后在函数中改变它的前后四个指针就行,如图:

//在目标节点后面插入
void OutInsertion(Pointdef* tail, int x)
{assert(tail);//目标节点的下一个节点不能是空,否则就是尾插了assert(tail->next);Pointdef* news = Newspoint(x);Pointdef* next = tail->next;tail->next = news;news->prev = tail;news->next = next;next->prev = news;
}


 二.(8)链表头删

头删咱直接将头节点作为参数就行了,再记录第二个节点方便释放,然后用四个指针连接头节点和第二个节点,最后记得释放第一个节点的空间,如图:

//链表头删
void Outomit(Pointdef* head,int x)
{assert(head);assert(head->next);Pointdef* tail = head->next->next;Pointdef* next = head->next;head->next = tail;tail->prev = head;free(next);next = NULL;
}

二.(9)链表尾删

这个咱就不多说了!就是先记录倒数第二个节点,将它作为尾节点就行了,同样记得释放空间,直接上代码:

//链表尾删
void Tailomit(Pointdef* head, int x)
{assert(head);Pointdef* prev = head->prev->prev;Pointdef* tail = head->prev;prev->next = head;head->prev = prev;free(tail);tail = NULL;
}

二.(10)链表头插

但凡插入节点的都具有很大的相似之处,无非就是将这个节点插入某处,通过记录附近节点的位置,再把新增节点的左右节点与它进行连接,看代码:

//链表头插
void Headinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->next;Pointdef* news = Newspoint(x);head->next = news;news->prev = head;news->next = tail;tail->prev = news;
}

二.(11)链表尾插
//链表尾插
void Tailinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->prev;Pointdef* news = Newspoint(x);tail->next = news;news->prev = tail;head->prev = news;news->next = head;
}

二.(12)删除中间目标节点

删除节点我们肯定要先找到目标节点,我们在链表中可以通过以下两种方式寻找:指针域  数据域,在上面的前后插入中,我们选择的指针查找方式,下面我们进行数据查找方式来删除:

//链表中间节点删除
void Omit(Pointdef* tail)
{Pointdef* pc = tail->prev;Pointdef* pt = tail->next;pc->next = pt;pt->prev = pc;free(tail);tail = NULL;
}

二.(13)释放双链表

咋从第一个节点释放到尾就OK了!注意:链表的空间不是连续的,因此需要一个个销毁!

//双链表销毁
void Undermine(Pointdef* head)
{assert(head);Pointdef* cur = head->next;Pointdef* tail = cur->next;while (cur != head){tail = cur->next;free(cur);cur = tail;}free(head);head=NULL;printf("释放完毕\n");
}
三.总结

针对以上实现的双链表,其实针对整个链表,都有以下特点:

1:首先我们要有设计好开辟节点的函数,然后知道如何连接这些节点

2:头插、尾插、中间插其实都有很大的共性,只是目标地点稍微差异,这点大家可以画图理解!

3:删除某个节点需要及时释放该空间,避免空间泄漏。需要注意是先连接,再释放

4:针对为何单向链表很多涉及二级指针,而双向链表大多是一级指针?

因为单向链表有很多地方需要改变头指针(单向链表将第一个节点作为头节点)的指向,而双向链表是固定了哨兵节点,因此其它操作其实是针对结构体里面的指针操作的。该知识的原理是:对形参的改变不影响实参,如需改变形参,需要使用二级指针

5:双向链表为哈要从第一个节点开始释放?

双向链表的头节点是链表结构的一部分,不是数据节点,应该先释放数据节点,再释放链表结构

四.代码合并

流程文件(可以使用打印来进行功能的测试!)

#define _CRT_SECURE_NO_WARNINGS 1
#include"text.h"int main()
{int x = 4;//链表新增节点Pointdef* head=Newspoint(1);//链表初始化void Initialize(head);//链表扩容节点Pointdef* tail = head;for (int i = 2; i <= 5; i++){Pointdef* news = Newspoint(i);//连接tail->next = news;news->prev = tail;news->next = head;head->prev = news;tail = tail->next;}//链表打印Printf_t(head);//在目标节点前面插入                      tail = (head->next)->next;x = 6;FrontInsertion(tail, x);//在目标节点后面插入                OutInsertion(tail, x);//                                  测试点Printf_t(head);//链表头删x = 7;Outomit(head,x);//链表尾删x = 8;Tailomit(head, x);//                                  测试点Printf_t(head);//链表头插x = 1;Headinsert(head, x);//链表尾插x = 9;Tailinsert(head, x);//                                  测试点Printf_t(head);//链表中间节点删除x = 4;tail = head -> next;while (tail->data != x){tail = tail->next;}Omit(tail);//                                   测试点Printf_t(head);//双链表销毁Undermine(head);return 0;
}

头文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>typedef struct Pointdef
{struct Pointdef* next;//指向下一个节点struct Pointdef* prev;//指向上一个节点int data;//数据域
}Pointdef;//链表新增节点
Pointdef* Newspoint(int x);
//链表初始化
void Initialize(Pointdef* head);
//链表打印
void Printf_t(Pointdef* head);
//在目标节点前面插入
void FrontInsertion(Pointdef* tail, int x);
//在目标节点后面插入
void OutInsertion(Pointdef* tail, int x);
//链表头删
void Outomit(Pointdef* head ,int x);
//链表尾删
void Tailomit(Pointdef* head, int x);
//链表头插
void Headinsert(Pointdef* head, int x);
//链表尾插
void Tailinsert(Pointdef* head, int x);
//链表中间节点删除
void Omit(Pointdef* tail);
//双链表销毁
void Undermine(Pointdef* head);

函数执行文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"text.h"//链表新增节点
Pointdef* Newspoint(int x)
{Pointdef* news = (Pointdef*)malloc(sizeof(Pointdef));if (news == NULL){perror("malloc");return NULL;}//初始化新节点news->next = NULL;news->prev = NULL;news->data = x;return news;
}//链表初始化
void Initialize(Pointdef* head)
{head->next = head;head->prev = head;
}//链表打印
void Printf_t(Pointdef* head)
{Pointdef* tail = head->next;while (tail != head){printf("%d <= ", tail->data);tail = tail->next;}printf("NULL\n");
}//在目标节点前面插入
void FrontInsertion(Pointdef* tail ,int x)
{assert(tail);Pointdef* prev = tail->prev;Pointdef* news = Newspoint(x);prev->next = news;news->prev = prev;news->next = tail;tail->prev = news;}//在目标节点后面插入
void OutInsertion(Pointdef* tail, int x)
{assert(tail);//目标节点的下一个节点不能是空,否则就是尾插了assert(tail->next);Pointdef* news = Newspoint(x);Pointdef* next = tail->next;tail->next = news;news->prev = tail;news->next = next;next->prev = news;
}//链表头删
void Outomit(Pointdef* head,int x)
{assert(head);assert(head->next);Pointdef* tail = head->next->next;Pointdef* next = head->next;head->next = tail;tail->prev = head;free(next);next = NULL;
}//链表尾删
void Tailomit(Pointdef* head, int x)
{assert(head);Pointdef* prev = head->prev->prev;Pointdef* tail = head->prev;prev->next = head;head->prev = prev;free(tail);tail = NULL;
}//链表头插
void Headinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->next;Pointdef* news = Newspoint(x);head->next = news;news->prev = head;news->next = tail;tail->prev = news;
}//链表尾插
void Tailinsert(Pointdef* head, int x)
{assert(head);Pointdef* tail = head->prev;Pointdef* news = Newspoint(x);tail->next = news;news->prev = tail;head->prev = news;news->next = head;
}//链表中间节点删除
void Omit(Pointdef* tail)
{Pointdef* pc = tail->prev;Pointdef* pt = tail->next;pc->next = pt;pt->prev = pc;free(tail);tail = NULL;
}//双链表销毁
void Undermine(Pointdef* head)
{assert(head);Pointdef* cur = head->next;Pointdef* tail = cur->next;while (cur != head){tail = cur->next;free(cur);cur = tail;}free(head);head = NULL;printf("释放完毕\n");
}

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

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

相关文章

网络编程-day5-sqlite3数据库

思维导图 服务器 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> #include <semaphore.h>…

Spring AI 介绍

文章来源&#xff1a;AI 概念 (AI Concepts) _ Spring AI1.0.0-SNAPSHOT中文文档(官方文档中文翻译)|Spring 教程 —— CADN开发者文档中心 本节介绍 Spring AI 使用的核心概念。我们建议仔细阅读它&#xff0c;以了解 Spring AI 是如何实现的。 模型 AI 模型是旨在处理和生成…

【Elasticsearch】监控与管理:集群监控指标

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…

黑马Redis详细笔记(实战篇---短信登录)

目录 一.短信登录 1.1 导入项目 1.2 Session 实现短信登录 1.3 集群的 Session 共享问题 1.4 基于 Redis 实现共享 Session 登录 一.短信登录 1.1 导入项目 数据库准备 -- 创建用户表 CREATE TABLE user (id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID,phone …

大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡

大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡 背景 前端开发接口请求&#xff0c;调试&#xff0c;联调&#xff0c;接入数据&#xff0c;前端必不可少工具&#xff0c;postman是一个非常好…

开源身份和访问管理方案之keycloak(一)快速入门

文章目录 什么是IAM什么是keycloakKeycloak 的功能 核心概念client管理 OpenID Connect 客户端 Client Scoperealm roleAssigning role mappings分配角色映射Using default roles使用默认角色Role scope mappings角色范围映射 UsersGroupssessionsEventsKeycloak Policy创建策略…

java项目之直销模式下家具工厂自建网站源码(ssm+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的直销模式下家具工厂自建网站源码。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 直销模式下家具…

C基础(十)动态内存分配和结构体

动态内存分配 堆区与栈区空间&#xff1a;申请堆区空间需手动操作&#xff0c;使用完要手动释放&#xff1b;栈区空间由系统自动分配和释放。相关函数与概念 malloc&#xff1a;从堆区申请指定字节数的空间&#xff0c;返回首地址&#xff0c;需搭配头文件#include <stdlib.…

图7.1-7.6《分析模式》第7章使用会计模型-原图和UML图对比

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 图7.1 TT示例的包。 账户包持有抽象的会计类型&#xff0c;电话服务包为这个特定领域扩展这些类型。 图7.2 TT的账户模型。 7.3 电话服务的结构模型。 图7.4 创建新电话服务的事件图。…

大模型基本原理(四)——如何武装ChatGPT

传统的LLM存在几个短板&#xff1a;编造事实、计算不准确、数据过时等&#xff0c;为了应对这几个问题&#xff0c;可以借助一些外部工具或数据把AI武装起来。 实现这一思路的框架包括RAG、PAL、ReAct。 1、RAG&#xff08;检索增强生成&#xff09; LLM生成的内容会受到训练…

Qt监控设备离线检测/实时监测设备上下线/显示不同的状态图标/海康大华宇视华为监控系统

一、前言说明 监控系统中一般有很多设备&#xff0c;有些用户希望知道每个设备是否已经上线&#xff0c;最好有不同的状态图标提示&#xff0c;海康的做法是对设备节点的图标和颜色变暗处理&#xff0c;离线的话就变暗&#xff0c;有可能是加了透明度&#xff0c;而大华的处理…

数据结构-栈和队列的应用

目录 前言一、栈的应用二、队列的应用&#xff08;农夫过河问题&#xff09;2.1 问题描述2.2 算法选择2.3 算法精化2.4 算法实现2.5 问题结果 总结 前言 本篇文章使用两个例子说明栈和队列的应用&#xff0c; 对于迷宫问题&#xff0c;使用栈实现深度优先策略解决迷宫问题&…

Acwing-基础算法课笔记之基础算法(差分)

Acwing-基础算法课笔记之基础算法&#xff08;差分&#xff09; 一、一维差分1、差分的概念2、差分思想 二、二维差分操作流程 一、一维差分 1、差分的概念 对于一个给定的序列a&#xff0c;它的差分序列b定义为&#xff1a; b [ 1 ] a [ 1 ] b[1]a[1] b[1]a[1]&#xff0c…

SkyWalking 10.1.0 实战:从零构建全链路监控,解锁微服务性能优化新境界

文章目录 前言一、集成SkyWalking二、SkyWalking使用三、SkyWalking性能剖析四、SkyWalking 告警推送4.1 配置告警规则4.2 配置告警通知地址4.3 下发告警信息4.4 测试告警4.5 慢SQL查询 总结 前言 在传统监控系统中&#xff0c;我们通过进程监控和日志分析来发现系统问题&…

【动态规划】风扫枯杨,满地堆黄叶 - 9. 完全背包问题

本篇博客给大家带来的是完全背包问题之动态规划解法技巧. &#x1f40e;文章专栏: 动态规划 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅&#x1f680; 要开心要快乐顺…

MyBatis的工作流程是怎样的?

大家好&#xff0c;我是锋哥。今天分享关于【MyBatis的工作流程是怎样的&#xff1f;】面试题。希望对大家有帮助&#xff1b; MyBatis的工作流程是怎样的&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 MyBatis 的工作流程可以分为几个主要的步骤&…

python-leetcode 25.环形链表

题目&#xff1a; 给定一个链表的头节点head,判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪next指针再次到达&#xff0c;则链表中存在环。为了表示给定链表中的环&#xff0c;评测系统内部使用整数pos来表示链表尾连接到链表中的位置&#xff08;…

瑞芯微开发板/主板Android调试串口配置为普通串口方法 深圳触觉智能科技分享

本文介绍瑞芯微开发板/主板Android调试串口配置为普通串口方法&#xff0c;不同板型找到对应文件修改&#xff0c;修改的方法相通。触觉智能RK3562开发板演示&#xff0c;搭载4核A53处理器&#xff0c;主频高达2.0GHz&#xff1b;内置独立1Tops算力NPU&#xff0c;可应用于物联…

Datawhale 组队学习 Ollama教程 task1

一、Ollama 简介 比喻&#xff1a;Ollama 就像是一个“魔法箱子”&#xff0c;里面装满了各种大型语言模型&#xff08;LLM&#xff09;。你不需要懂复杂的魔法咒语&#xff08;配置&#xff09;&#xff0c;只需要轻轻一按&#xff08;一条命令&#xff09;&#xff0c;就能让…

vulnhub 靶场 —— NullByte

免责声明 本博客文章仅供教育和研究目的使用。本文中提到的所有信息和技术均基于公开来源和合法获取的知识。本文不鼓励或支持任何非法活动&#xff0c;包括但不限于未经授权访问计算机系统、网络或数据。 作者对于读者使用本文中的信息所导致的任何直接或间接后果不承担任何…