数据结构-双向链表

目录

1.带头双向循环链表:

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

双向链表初始化:

双向链表打印:

开辟节点函数:

双向链表头插:

双向链表尾插:

双向链表头删:

双向链表尾删:

 双向链表查找:

双向链表在pos之前插入:

双向链表在pos位置删除:

 双向链表的销毁:

3.完整代码:

test.c

List.h

List.c

4.测试:​编辑

5.顺序表和链表的区别


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.如果数据不在缓存中,就叫做缓存不命中,这时候需要先把数据加载到缓存中,然后再访问数据

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

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

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

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

相关文章

HMM与LTP词性标注之马尔科夫模型(HMM原理剖析)

文章目录 问题描述viterbi算法联合概率与条件概率维特比算法实例 问题描述 viterbi算法 联合概率与条件概率 维特比算法实例

界面控件DevExpress WPF PDF Viewer,更快实现应用的PDF文档浏览

DevExpress WPF PDF Viewer控件可以轻松地直接在Windows应用程序中显示PDF文档&#xff0c;而无需在最终用户的机器上安装外部PDF查看器。 P.S&#xff1a;DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress…

【寒武纪(3)】媒体处理系统的系统控制、视频输入和后处理子系统

系统控制 文章目录 系统控制1、配置视频缓存池Video Pool2、配置硬件IP为在线工作&#xff08;不通过DDR数据交互&#xff09;/ 离线工作&#xff08;写入DDR&#xff09;模式3、硬IP可以使用 非Video Block &#xff08;VB&#xff09;内存4、配置是否启动内存传递的压缩 视频…

【Hadoop】YARN容量调度器详解

&#x1f984; 个人主页——&#x1f390;开着拖拉机回家_Linux,Java基础学习,大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341; &#x1fa81;&#x1f341;&am…

Redis实战 | 使用Redis 的有序集合(Sorted Set)实现排行榜功能,和Spring Boot集成

专栏集锦&#xff0c;大佬们可以收藏以备不时之需 Spring Cloud实战专栏&#xff1a;https://blog.csdn.net/superdangbo/category_9270827.html Python 实战专栏&#xff1a;https://blog.csdn.net/superdangbo/category_9271194.html Logback 详解专栏&#xff1a;https:/…

[架构之路-244]:目标系统 - 设计方法 - 软件工程 - 软件开发方法:结构化、面向对象、面向服务、面向组件的开发方法

目录 前言&#xff1a; 一、概述: 软件聚合的程度由简单到复杂 二、主要开发方法详见 2.1 结构化的开发方法 2.2 面对对象的开发方法 2.3 面向服务的开发方法 2.4 面向组件的开发方法 三、不同开发方法比较 3.1 结构化开发方法 3.2 面向对象(OOP)开发方法 3.3 面向服…

【计算机网络】网络层IP协议

文章目录 1. IP协议介绍2. IP报头3. IP的分片和组装4. IP地址网段划分特殊的IP地址子网、局域网、网段的区别IP地址的数量限制 5. 公网IP和私有IP6. NAT技术7. 路由Route 1. IP协议介绍 IP协议&#xff08;Internet Protocol&#xff09;是一种最常用的网络层协议&#xff0c;…

CSS 边框、轮廓线

一、CSS边框&#xff1a; CSS边框属性允许指定一个元素边框的样式和颜色。 1&#xff09;、边框样式&#xff1a;border-style属性用来定义边框的样式&#xff0c;border-style值&#xff1a; 2&#xff09;、边框宽度&#xff1a;border-width属性用于指定边框宽度。指定变宽…

2023-11-07 C语言链接库编译命令

点击 <C 语言编程核心突破> 快速C语言入门 C语言链接库编译命令 前言一、引入库文件, 包括头文件和lib库二、简单示例总结 前言 要解决问题: 一般没有给新手的链接库编译命令学习资料, 然而, 不解决这个问题, 调用库就能折腾到劝退, 我近日回答一个问题, 很简单, 调用…

【 Docker: 数据卷挂载】

背景 Docker只提供了容器运行的必备依赖&#xff0c;但是一些编辑等操作的依赖是不支持的&#xff0c;如vi操作容器内部文件、将静态资源拷贝到容器内来等。 docker pull nginx docker run -d -p 81:80 --namemynginx -v D:/docker/nginx/www:/usr/share/nginx/www -v D:/dock…

柯桥英语培训,商务英语学习,常用口语

欢迎各位小伙伴来到 ——“每个单词我都认识&#xff0c;但我又不认识整个短语”的时候啦&#xff01; “dog”是“狗” “breakfast”是早餐 那“a dogs breakfast”是“狗的早餐”&#xff1f; 狗听了都摇头。 a dogs breakfast是一句英文俚语&#xff0c;指的是无序、混…

LeetCode | 面试题 02.02. 返回倒数第 k 个节点

LeetCode | 面试题 02.02. 返回倒数第 k 个节点 OJ链接 思路&#xff1a;定义两个快慢指针&#xff0c;让快指针先提前走k个节点&#xff0c;然后再让慢结点和快结点一起走&#xff0c;当快指针 NULL时&#xff0c;慢指针就是倒数第k个节点 代码如下&#xff1a; int kthT…

计算机毕业设计java+springboot+vue的旅游攻略平台

项目介绍 本系统结合计算机系统的结构、概念、模型、原理、方法&#xff0c;在计算机各种优势的情况下&#xff0c;采用JAVA语言&#xff0c;结合SpringBoot框架与Vue框架以及MYSQL数据库设计并实现的。员工管理系统主要包括个人中心、用户管理、攻略管理、审核信息管理、积分…

STM32Cube +VSCode开发环境搭建

STM32Cube VSCode开发环境搭建 0.前言一、各种方式对比1.STM32CubeMX CLion2.STM32CubeIDE VSCode STM32 VSCode Extension3.VSCode EIDE插件 二、STM32CubeIDE VSCode STM32 VSCode Extension环境搭建1.需要安装的软件2.相关配置3.编译测试 三、总结 0.前言 工欲善其事&…

最新Next14 路由处理器 Route Handlers

四、使用Next路由处理程序 Next.js Route Handlers I. Next中路由处理程序是什么 ​ 路由处理程序是在用户访问站点路由时执行的功能。它们负责处理对定义的URL或路由的传入HTTP请求&#xff0c;以生成所需的数据。从本质上讲&#xff0c;当用户访问Next.js应用程序中的特定页…

chatglm3-6b部署及微调

chatglm3-6b部署及微调 modelscope: https://modelscope.cn/models/ZhipuAI/chatglm3-6b/filesgithub: https://github.com/THUDM/ChatGLM3镜像: ubuntu20.04-cuda11.8.0-py38-torch2.0.1-tf2.13.0-1.9.4v100 16G现存 单卡 安装 软件依赖 pip install --upgrade pippip ins…

使用IDEA让文本对比不在变的困难

文章目录 前言操作1、IDEA与电脑磁盘任意文件的比较2、项目内部的文件比较3、剪切板比较4、IDEA本地历史比较5、IDEA版本历史对比 前言 在日常实际开发当中我们常常会对一些代码或内容进行比对查看是否有差异&#xff0c;这个时候不需要借用第三方比对插件&#xff0c;在IDEA中…

git笔记

git常见命令 git init :初始化本地仓库&#xff0c;会生成一个.git文件&#xff0c;该文件用于管理和追踪该本地仓库&#xff0c;只有在git仓库下的文件才能被管理! git config user. name "用户名” git config user. email " 邮箱” git config -1 :列出当前git仓库…

老李测评:网络电视盒子哪个好?双十一必看电视盒子推荐

大家好&#xff0c;我是测评人老李&#xff0c;双十一大促期间我们都在买买买&#xff0c;本期老李要分享的数码产品推荐是电视盒子&#xff0c;为了推荐更客观&#xff0c;老李购入了各平台热销的十几款电视盒子&#xff0c;通过两周的对比后&#xff0c;整理了这份电视盒子推…

class类默认导出,header字段在请求中的位置

这是封装好的&#xff0c;没封装的如下 如果没有用uni.post那么就是如下的结构 let header {Content-Type: application/x-www-form-urlencoded,tenant: MDAwMA, } request({url:/sal/formula/validFormula,method:post,data:{},header })