当前位置: 首页 > news >正文

数据结构-双向链表

1.带头双向循环链表:

前面我们已经知道了链表的结构有8种,我们主要学习下面两种:

前面我们已经学习了无头单向非循环链表,今天我们来学习带头双向循环链表:

带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了 

 带头双向循环链表不需要二级指针,因为我们刚开始就为其开辟了一个节点,叫做哨兵位头节点,它是结构体中的指针,用结构体指针就能改变它,而要改变结构体外面的指针才会用二级指针。

双向循环链表顾名思义,除了哨兵位头节点以外,每个节点里面应该有两个指针,下面我们定义一个结构体:

typedef int ListDatatype;
typedef struct ListNode
{struct ListNode* prev;struct ListNode* next;ListDatatype data;
}LTNode;

prev指向前一个节点,next指向后一个节点。

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

双向链表初始化:

LTNode* InitList()
{LTNode* phead = BuyList(-1);phead->next = phead;phead->prev = phead;return phead;
}

 双向循环链表开始时头和尾都指向自己:

BuyList函数的功能是创建节点,我们在初始化时用它创建哨兵位头节点,因为后面还有多次使用,所以把它封装为函数。

双向链表打印:

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

开辟节点函数:

LTNode* BuyList(ListDatatype x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->prev = NULL;newnode->next = NULL;newnode->data = x;return newnode;
}

双向链表头插:

头插实际是在哨兵位头节点phead的后面插入,保存住head->next的位置,然后把newnode前后分别和head、head->next链接起来就行。

代码如下: 

void ListPushFront(LTNode* phead, ListDatatype x)
{assert(phead);LTNode* newnode = BuyList(x);LTNode* next = phead->next;phead->next = newnode;next->prev = newnode;newnode->next = next;newnode->prev = phead;
}

这段代码神奇的地方在于,即使链表为空,它也能头插,并且不需要判断链表是否为空

因为就算链表为空,我们有哨兵位头节点存在,就不用担心空指针的问题。 

双向链表尾插:

双向链表相对于单链表的优势就是不用找尾,因为它的phead->prev就是尾,尾插同头插差不多, 把newnode的前后分别和链表的尾和头链接起来即可。

代码如下:

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

和头插一样,尾插也不用判断链表为空的情况。

双向链表头删:

头删指的是删除哨兵位头节点后面一个节点,只要将头节点与要删除的节点后面的节点相连接,然后free掉要删除的节点即可。

 代码如下:

void ListPopFront(LTNode* phead)
{assert(phead);assert(!ListEmpty(phead));LTNode* cur = phead;LTNode* first = cur->next;LTNode* second = first->next;second->prev = phead;phead->next = second;free(first);
}

 删除时要注意不能删除哨兵位头节点,所以要断言一下,链表为空就不能再删了,我们也可以封装一个判断链表是否为空的函数ListEmpty():

bool ListEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}

bool类型的返回值是true或者false。 

双向链表尾删:

尾删要保存尾节点的前一个节点,然后把前一个节点和头节点链接起来,free尾节点即可。

代码如下:

void ListPopBack(LTNode* phead)
{assert(phead);assert(!ListEmpty(phead));LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;tailPrev->next = phead;phead->prev = tailPrev;free(tail);
}

 注意:同头删一样,尾删也要判断是否为空链表。

 双向链表查找:

双向循环链表的查找和单链表的查找不同,遍历时从head的下一个节点开始,到head的上一个节点(即尾节点)结束,所以判断条件有所不同,注意区分。找到时,返回该节点位置。

代码如下:

LTNode* ListSearch(LTNode*phead,ListDatatype x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

双向链表在pos之前插入:

保存pos的前一个节点,把newnode的前后分别与pos的前一个节点和pos链接起来即可。

要测试该功能,可以配合查找函数,先找到pos。

代码如下:

void ListInsert(LTNode* pos, ListDatatype x)
{assert(pos);LTNode* newnode = BuyList(x);LTNode* posPrev = pos->prev;newnode->next = pos;newnode->prev = posPrev;posPrev->next = newnode;pos->prev = newnode;
}

这段代码可以直接在头插和尾插中复用,也就是说,我们要实现头插、尾插和任意位置插入,只用这一个函数就可以解决。

头插:

ListInsert(phead->next, x);

 尾插:

ListInsert(phead, x);

双向链表在pos位置删除:

配合查找函数先找到pos位置,然后删除就行。

代码如下:

void ListErase(LTNode* pos)
{assert(pos);LTNode* posPrev = pos->prev;LTNode* posNext = pos->next;posPrev->next = posNext;posNext->prev = posPrev;free(pos);
}

这段代码也可以同时实现头删、尾删和任意位置删除:

头删:
 

ListErase(phead->next);

 尾删:

ListErase(phead->prev);

 双向链表的销毁:

销毁也是从哨兵位的下一个节点开始,注意每次都要保存要销毁节点的后面一个节点的位置,防止找不到后面的节点,最终要把哨兵位也销毁掉。

代码如下:

void ListDestory(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);
}

 以上就是双向链表的全部功能实现,下面给出完整代码:

3.完整代码:

test.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//测试
ListTest1()
{LTNode* plist = InitList();Print(plist);//头插ListPushFront(plist, 1);ListPushFront(plist, 2);ListPushFront(plist, 3);ListPushFront(plist, 4);Print(plist);//尾插ListPushBack(plist, 5);ListPushBack(plist, 6);ListPushBack(plist, 7);ListPushBack(plist, 8);Print(plist);//头删ListPopFront(plist);ListPopFront(plist);Print(plist);//尾删ListPopBack(plist);ListPopBack(plist);Print(plist);//在pos位置之前插入LTNode* pos = ListSearch(plist, 1);if (pos != NULL)ListInsert(pos, 666);Print(plist);//在pos位置删除pos = ListSearch(plist, 6);if(pos!=NULL)ListErase(pos);Print(plist);
}
int main()
{ListTest1();return 0;
}

List.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int ListDatatype;
typedef struct ListNode
{struct ListNode* prev;struct ListNode* next;ListDatatype data;
}LTNode;
//双向链表初始化
LTNode* InitList();
//双向链表打印
void Print(LTNode* phead);
//双向链表头插
void ListPushFront(LTNode* phead, ListDatatype x);
//双向链表尾插
void ListPushBack(LTNode* phead, ListDatatype x);
//双向链表头删
void ListPopFront(LTNode* phead);
//双向链表尾删
void ListPopBack(LTNode* phead);
//双向链表查找
LTNode* ListSearch(LTNode*phead,ListDatatype x);
//在pos位置之前插入
void ListInsert(LTNode*pos, ListDatatype x);
//在pos位置删除
void ListErase(LTNode* pos);
//销毁链表
void ListDestory(LTNode* phead);

List.c

#define  _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//开辟节点函数
LTNode* BuyList(ListDatatype x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->prev = NULL;newnode->next = NULL;newnode->data = x;return newnode;
}
//双向链表初始化
LTNode* InitList()
{LTNode* phead = BuyList(-1);phead->next = phead;phead->prev = phead;return phead;
}
//打印函数
void Print(LTNode* phead)
{LTNode*cur = phead->next;printf("guard<->");while (cur != phead){printf("%d<->", cur->data);cur = cur->next;}printf("\n");
}
//双向链表头插
void ListPushFront(LTNode* phead, ListDatatype x)
{assert(phead);LTNode* newnode = BuyList(x);LTNode* next = phead->next;phead->next = newnode;next->prev = newnode;newnode->next = next;newnode->prev = phead;/*ListInsert(phead->next, x);*/
}
//双向链表尾插
void ListPushBack(LTNode* phead, ListDatatype x)
{assert(phead);LTNode* newnode = BuyList(x);LTNode* tail = phead->prev;tail->next = newnode;phead->prev = newnode;newnode->prev = tail;newnode->next = phead;/*ListInsert(phead, x);*/
}
//判断空链表函数
bool ListEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}
//双向链表头删
void ListPopFront(LTNode* phead)
{assert(phead);assert(!ListEmpty(phead));LTNode* cur = phead;LTNode* first = cur->next;LTNode* second = first->next;second->prev = phead;phead->next = second;free(first);/*ListErase(phead->next);*/
}
//双向链表尾删
void ListPopBack(LTNode* phead)
{assert(phead);assert(!ListEmpty(phead));LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;tailPrev->next = phead;phead->prev = tailPrev;free(tail);/*ListErase(phead->prev);*/
}
//双向链表查找
LTNode* ListSearch(LTNode*phead,ListDatatype x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
//双向链表在pos之前插入
void ListInsert(LTNode* pos, ListDatatype x)
{assert(pos);LTNode* newnode = BuyList(x);LTNode* posPrev = pos->prev;newnode->next = pos;newnode->prev = posPrev;posPrev->next = newnode;pos->prev = newnode;
}
//双向链表在pos位置删除
void ListErase(LTNode* pos)
{assert(pos);LTNode* posPrev = pos->prev;LTNode* posNext = pos->next;posPrev->next = posNext;posNext->prev = posPrev;free(pos);
}
//双向链表销毁
void ListDestory(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);
}

4.测试:

5.顺序表和链表的区别

不同点 顺序表链表
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问支持O(1) 不支持:O(N)
任意位置插入或者删除元
可能需要搬移元素,效率低O(N)只需修改指针指向
插入 动态顺序表,空间不够时需要扩
没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

总结一下:

 

下面我们再来补充一些内容:

 

这里有个问题,在计算机中使用顺序表效率高还是使用链表效率高呢?

答案是:顺序表。

因为在计算机中,由于运行速度不匹配的问题,CPU不会直接和主存交换数据,而是先把数据从主存中取出来放到高速缓存中,然后再进行访问数据,而访问数据会出现两种情况:

1.如果数据在缓存中,就叫做缓存命中,可以直接访问。

2.如果数据不在缓存中,就叫做缓存不命中,这时候需要先把数据加载到缓存中,然后再访问数据

当缓存不命中时,计算机会把数据加载到缓存中,而加载时会将这个数据后面的数据也一起加载进去(局部性原理),如果是顺序表,因为它的内存空间是连续的,后面的数据会直接命中,这样它的缓存命中率就高;如果是链表,它一旦命中不了,也会加载一段数据,但是这些数据不一定会用,这就造成了浪费,还会导致数据污染,这样它的缓存命中率就低了。

 

这就是今天关于双向链表的全部内容了,未完待续。。。

相关文章:

数据结构-双向链表

1.带头双向循环链表&#xff1a; 前面我们已经知道了链表的结构有8种&#xff0c;我们主要学习下面两种&#xff1a; 前面我们已经学习了无头单向非循环链表&#xff0c;今天我们来学习带头双向循环链表&#xff1a; 带头双向循环链表&#xff1a;结构最复杂&#xff0c;一般用…...

CV计算机视觉每日开源代码Paper with code速览-2023.11.6

精华置顶 墙裂推荐&#xff01;小白如何1个月系统学习CV核心知识&#xff1a;链接 点击CV计算机视觉&#xff0c;关注更多CV干货 论文已打包&#xff0c;点击进入—>下载界面 点击加入—>CV计算机视觉交流群 1.【点云3D目标检测】&#xff08;NeurIPS2023&#xff09;…...

GB28181学习(十五)——流传输方式

前言 基于GB/T28181-2022版本&#xff0c;实时流的传输方式包括3种&#xff1a; UDPTCP被动TCP主动 UDP 流程 注意&#xff1a; m字段指定传输方式为RTP/AVP&#xff1b; 抓包 SIP服务器发送INVITE请求&#xff1b; INVITE sip:xxx192.168.0.111:5060 SIP/2.0 Via: SIP…...

【Linux】:初识git || centos下安装git || 创建本地仓库 || 配置本地仓库 || 认识工作区/暂存区(索引)以及版本库

&#x1f4ee;1.初识git Git 原理与使用 课程⽬标 • 技术⽬标:掌握Git企业级应⽤&#xff0c;深刻理解Git操作过程与操作原理&#xff0c;理解⼯作区&#xff0c;暂存区&#xff0c;版本库的含义 • 技术⽬标:掌握Git版本管理&#xff0c;⾃由进⾏版本回退、撤销、修改等Git操…...

Vue 3 中,watch 和 watchEffect 的区别

结论先行&#xff1a; watch 和 watchEffect 都是监听器&#xff0c;都是用来监听响应式数据的变化并执行相应操作。区别是&#xff1a; watch&#xff1a;需要指明要监听的数据&#xff0c;而且在回调函数中可以获取到属性变化的前后值&#xff1b; 适用于需要精确控制监视…...

鲜花展示服务预约小程序的效果如何

鲜花产品的市场需求度非常高&#xff0c;互联网深入各个行业&#xff0c;很多鲜花商家都会通过线上建立平台实现产品销售、获客引流、转化复购、生意增长等&#xff0c;当然除了搭建鲜花商城小程序外&#xff0c;对鲜花供应商及门店还有展示预约方面的需求。 通过【雨科】平台可…...

Linux下多个盘符乱的问题处理

参考文档&#xff1a; linux下man fstab命令查看帮助&#xff0c;有一段说明&#xff0c;可以使用UUID&#xff0c;或者LABEL 来绑定盘。这里使用UUID来绑定 Instead of giving the device explicitly, one may indicate the filesystem that is to be mounted by its UUID …...

uniapp小程序使用web-view组件页面分享后,点击没有home小房子解决办法

uniapp小程序使用web-view组件页面分享后&#xff0c;点击没有home小房子解决办法 小程序 &#xff1a;IOS 测试正常&#xff0c; 安卓 不显示home 微信小程序使用的是全局自定义导航&#xff0c;通过首页 banner 跳转到一个 web-view 页面&#xff0c;展示官网。 web-view 页…...

SLAM_语义SLAM相关论文

目录 1. 综述 2. 相关文章 Probabilistic Data Association for Semantic SLAM VSO:Visual Semantic Odometry 语义信息分割运动物体...

【技巧】并发读取Mysql数据保证读取到的数据不重复

【技巧】并发读取Mysql数据保证读取到的数据不重复 使用场景: 并发场景下, 保证不获取到重复的数据 思路: 先通过 MYSQL锁 去占位打标识,然后再去取数据 相当于几个人抢蛋糕, A先把蛋糕打上记号 蛋糕是A的, 然后再慢慢吃 表结构 表 t_userid name val used_flag 是否使用…...

Lavarel异步队列的使用

系统为window 启动队列&#xff1a; php artisan queue:listen设置队列类 .env文件需设置&#xff1a;QUEUE_CONNECTIONredis <?phpnamespace App\Jobs;use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Disp…...

JVM知识分享(PPT在资源里)

一、前言 1.自动内存管理 有句经典的话是这样说&#xff0c;Java与C之间有一堵由内存动态分配和垃圾收集技术所围成的高墙&#xff0c;墙外面的人想进去&#xff0c;墙里面的人却想出来。对于Java程序员来说&#xff0c;在虚拟机自动内存管理机制的帮助下&#xff0c;不再需要…...

整合Salesforce Org需要避免的3大风险

管理多个Salesforce实例是成长型企业可能遇到的场景&#xff0c;每个Salesforce实例都包含可能需要整合的关键业务数据和流程。 除了整合&#xff0c;组织可能会在不同的发展阶段采用Salesforce(例如CRM、服务、运营)。整合的最终结果是多个Salesforce实例被统一&#xff0c;并…...

viple进阶3:打印不同形状的三角形

&#xff08;1&#xff09;题目&#xff1a;打印实心的三角形&#xff08;正三角&#xff09; 第一步&#xff1a;观察图形。首行是1颗星&#xff0c;其余的每一行都比上一行多1颗星&#xff1b;其次&#xff0c;每一行的星号数和行数值相等&#xff0c;第一行有1颗星&#xff…...

pytest+yaml实现接口自动化框架

前言 httprunner 用 yaml 文件实现接口自动化框架很好用&#xff0c;最近在看 pytest 框架&#xff0c;于是参考 httprunner的用例格式&#xff0c;写了一个差不多的 pytest 版的简易框架 项目结构设计 项目结构完全符合 pytest 的项目结构&#xff0c;pytest 是查找 test_.…...

编译器使用优化后出现的busfault

遇到的问题&#xff1a; 未开优化是正常执行&#xff0c;打开优化&#xff0c;无法运行&#xff0c;定位到异常语句 //ADC_REG 是ADC结果寄存器地址 uint32 adc *(uint32 *)ADC_REG; uint32 temp adc&0xffff;未优化汇编代码 //uint32 adc *(uint32*)ADC_REG; MOVW R…...

rebase current onto selected作用

rebase current onto selected作用 "rebase current onto selected"是一个版本控制工具中的命令&#xff0c;通常用于将当前分支的修改合并到已选定的分支中&#xff0c;以保持代码库的整洁性和可维护性。 具体来说&#xff0c;这个命令会将当前分支的提交历史记录…...

深度学习入门

全连接批量归一化 目的是&#xff1a;只有一个学习率&#xff0c; 通过归一化&#xff0c;让所有的 x i x_i xi​具有一样的分布&#xff0c;则对每个参数 w i w_i wi​梯度的作用是相当的实现是&#xff1a;实际上是在全连接中增加了两个节点 γ \gamma γ, β \beta β 卷积…...

嵌入式图像处理机器视觉库YMCV使用

YMCV入门 一个可以免操作系统的机器视觉库&#xff0c;由c语言编写可以跑在单片机上。项目地址https://gitee.com/yao_mi/ymcv 使用的时候&#xff0c;可以参考他们的教程和demo&#xff0c;建议先看教程&#xff0c;上面有架构说明。 一个典型的应用就是渲染器&#xff0c;需…...

vscode设置pycharm中的项目路径和debug方法

真大佬在这 真大佬在这 必须给大佬star 命令行运行&#xff1a; export PYTHONPATH:pwd:/home/bennie/bennie/bennie_project/AI_Lab python main.py 当关闭此命令行时&#xff0c;临时路径会清除&#xff0c;可以将上述export的整条语句&#xff0c;加入~/.bashrc中 该命令中…...

10-27 maven概念

maven maven的概念模型: 项目对象模型(POM: Project object Model)&#xff0c;一组标准集合: pom.xml 依赖管理系统(Dependency Management System) 项目生命周期(Project Lifecycle) 项目对象模型&#xff1a; 把项目当成一个对象&#xff0c;描述这个项目&#xff0c;使用p…...

SQL审计是什么意思?目的是什么?有什么好处?

很多刚入行的运维小伙伴对于SQL审计不是很了解&#xff0c;不知道其是什么意思&#xff1f;使用SQL审计的目的是什么&#xff1f;使用SQL审计的好处有哪些&#xff1f;这里我们大家就来一起聊聊&#xff0c;仅供参考哈&#xff01; SQL审计是什么意思&#xff1f; 【回答】&…...

CSS 网页布局

网页布局有很多种方式&#xff0c;一般分为以下几个部分&#xff1a;头部区域、菜单导航区域、内容区域、底部区域&#xff1a; 1&#xff09;、头部区域位于整个网页的顶部&#xff0c;一般用于设置网页的标题或者网页的logo。 <style> body { margin: 0; } /* 头部样…...

智慧燃气管网监测系统功能简要介绍

关键词&#xff1a;智慧燃气、智慧燃气管网、智慧燃气管网监测、智慧燃气管网监测系统、智慧燃气解决方案 燃气作为城市主要燃料&#xff0c;遍布整个城区&#xff0c;其安全运行直接关系到居民的生命安全&#xff0c;不可不重视。 智慧燃气中的GIS和SCADA系统&#xff0c;可…...

深度解析:如何开发一对一交友App的关键要素和流程

在数字化时代&#xff0c;人们越来越倾向于使用交友App来寻找自己的伴侣或交流朋友。而一对一交友App的开发成为了创业者们追逐的热门领域。本文将深入探讨一对一交友App开发的关键要素和流程&#xff0c;帮助您在竞争激烈的市场中脱颖而出。 关键要素&#xff1a;打造独特的用…...

ClickHouse 学习之从高级到监控以及备份(二)

第 一 部分 高级篇 第 1 章 Explain 查看执行计划 在 clickhouse 20.6 版本之前要查看 SQL 语句的执行计划需要设置日志级别为 trace 才能可以看到&#xff0c;并且只能真正执行 sql&#xff0c;在执行日志里面查看。在 20.6 版本引入了原生的执行计划的语法。在 20.6.3 版本成…...

「随笔」IT行业哪个方向比较好就业

一、IT行业就业的PEST分析 在当前的全球经济环境下&#xff0c;IT行业的发展迅速&#xff0c;就业前景广阔。以下从政治、经济、社会和科技四个维度对IT行业就业进行PEST分析。 1.1 政治&#xff08;Political&#xff09; 政府政策&#xff1a;近年来&#xff0c;各国政府都…...

Halcon WPF 开发学习笔记(0):开篇介绍

文章目录 文章专栏Halcon是什么&#xff1f;安装教学视频链接简单来说 Halcon快速开发环境确认新建项目 文章专栏 Halcon开发 Halcon是什么&#xff1f; 史上最全VisionPro和Halcon 的详细对比 Halcon简述 Halcon基础大全&#xff08;基础算子、高阶算子、数组、分割、字符检测…...

SLAM中求导相关的公式总结

李代数与李群的关系 R ˙ R T \dot{R}R^{T} R˙RT 是一个反对称矩阵&#xff0c;所以这个矩阵可以用一个13向量进行反对称来表示 R ˙ R T Φ ^ \dot{R}R^{T}Φ^{\hat{}} R˙RTΦ^ &#xff0c; 根据十四讲 4.8 的推导&#xff0c;最后则有 R ( t ) ˙ Φ ^ ⋅ R ( t ) \d…...

在微信小程序中怎么做投票活动

在当今社交媒体时代&#xff0c;微信小程序已经成为一种广泛使用的互动营销工具。通过各种活动&#xff0c;企业可以吸引用户的关注&#xff0c;提升品牌影响力。其中&#xff0c;投票活动是一种特别受欢迎的形式。本文将为你详细介绍如何在微信小程序中创建投票活动。 一、微信…...