C语言数据结构(4)——线性表其三(双向链表)

欢迎来到博主的专栏——C语言数据结构
博主ID:代码小豪

文章目录

    • 链表的种类
    • 头结点
    • 循环链表
    • 双向链表
    • 带头双向循环链表
      • 带头双向循环链表的定义与初始化
    • 空链表
      • 尾插法
      • 打印双向链表
      • 头插法
      • 查找指定数据项的节点
      • 在指定位置之后插入节点
      • 指定位置的删除
      • 双向链表的销毁
    • 顺序表与链表的对比

链表的种类

前面介绍了链表的种类之一——单链表,单链表的全称为不带头不循环单向链表

根据链表的性质,我们将链表分为以下几种:

(1)带头节点or不带头节点
(2)单向or双向
(3)循环与不循环

根据这些性质进行排列组合得出的链表共有八种

带头不带头
单向循环带头单向循环链表不带头单向循环链表
双向循环带头双向循环链表不带头双向循环链表
双向不循环带头双向不循环链表不带头双向不循环链表
单向不循环带头单向不循环链表不带头单向不循环链表

头结点

将有头结点的链表称为带头链表,没有头结点的链表称为不带头链表。

前面提到了指向第一个节点的指针被称为头指针,而头指针常常作为链表的函数返回值,如果出现了头指针需要不断修改的情况,可能会导致代码的繁冗。

比如写一个将多个链表进行合并的代码,头指针根据要求会有多种可能性,如果将这些可能性都进行判断的话,难免会让导致代码冗余。

在这里插入图片描述

那我们直接定义一个头结点,这个头结点的作用是当做一个链表的表头,但是不记录有效数据,只纪录指向合并后的链表的第一个节点。
在这里插入图片描述
可以发现,如果不带头结点,那么合并的链表需要区分头指针到底是list1或者其他,而带了头结点,只需要将listhead->next指向合并好的链表就行了。
即将

if(condition1)
return list1;
if(condition2)
return list2;
if(condition3)
return list3;
if(condition4)
return list4;

省略成

return listhead->next;

总结一下头指针的作用

头结点最主要的作用就是统一链表的操作。
(1)比如头指针为空的时候不能使用phead->next,但是头结点不会为空,所以可以使用listhead->next。
(2)有了头结点之后,删除和插入结点的时候不再需要判断头指针的指向问题,将任意位置的插入或删除节点的操作统一起来。(前面写的任意位置的插入节点的函数由于没有头节点,所以插入第一个节点前的位置需要用到头插法,进而导致代码冗余)

循环链表

将链表的最后一个结点的后继置为NULL的链表被称为不循环链表
在这里插入图片描述
如果我们要将链表实现多次遍历的操作时,不循环链表显然不满足要求,因为不循环链表遍历到表尾的时候就会停止遍历,无法进行多次遍历链表。

如果将表尾节点与表头节点连接起来,就能实现遍历表尾之后回到表头,重新遍历一次
在这里插入图片描述
循环链表的实现也非常简单,将表尾的next指向表头即可

ptail->next=phead;

双向链表

单链表的节点存储的只有一个指向后继元素的指针,这就会导致当节点来到下一个节点时,丢失上一个节点的地址,导致无法对当前节点的前驱节点进行操作。
在这里插入图片描述

为了解决这个问题,我们在定义链表的节点类型时可以加入一个指向前驱元素的指针,使得节点的前驱元素也被记录,这样子就能实现回退的操作。
在这里插入图片描述

带头双向循环链表

前篇文章讲了不带头单向不循环链表(单链表),已经了解了其中的三个特性,剩下的三个特性将通过讲解带头双向循环链表带着大家了解。

带头双向循环链表的定义与初始化

先来定义带头双向循环链表(后面简称双向链表)的节点数据类型。

双向链表的节点需要三个数据存储,分别是节点的数据,后继节点的指针,以及前驱节点的指针。

节点类型的定义如下:

typedef int LTData;
typedef struct ListNode
{LTData data;struct ListNode* next;struct ListNode* prev;
}Node;

由于双向链表具有头结点,因此需要对头结点进行创建与初始化。

在这里插入图片描述
前面提到了双向链表需要具备以下结构:
(1)带头
(2)循环
(3)双向
而头结点作为双向链表的一部分,在初始化的的时候也要满足以上要求,所以头结点应该初始化成这样:
在这里插入图片描述
头结点的初始化函数如下:

Node* HeadNodeInit(void)
{Node* head = (Node*)malloc(sizeof(Node));if (!head){perror("Headinit fail");exit(EXIT_FAILURE);}head->data = -1;head->next = head;head->prev = head;return head;
}

使用这个函数将会生成一个头结点,并将该头节点返回。函数的调用方式如下:

Node* plist = HeadNodeInit();

空链表

不带头的链表如果为空,就将头指针置为NULL,或者将头指针为NULL的链表视为空链表。

而带头的链表如果为空链表,就说明链表当中只有一个头结点,将只有头结点的链表视为空链表。

比如上一章的单链表,如果要创造一个空链表,只需要声明一个头指针,并将头指针置为空即可

phead=NULL;

而若是创建带头的空链表,则需要创建并初始化头结点。

plist=HeadNodeInit();

尾插法

将头结点传入函数,通过头结点来对链表进行操作

void ListDataPushBack(Node*headnode,SLData n);

先来讲讲尾插的原理,假设当前链表中有多个节点
在这里插入图片描述
如果在表尾插入新的节点,首先要找到表尾,由上图可见,由于链表是循环的,只需要访问头结点的前驱节点即可找到尾结点。

ptail=headnode->prev;

接着便是将新的节点插入表尾。方法如下:

(1)将新的节点的前驱节点设为表尾
(2)将新节点的后继节点设为头结点
(3)将表尾的后继设为新节点
(4)将头结点的前驱设为新节点

完成后新的节点将会插入至链表的末尾部分
在这里插入图片描述
避免代码冗余,将生成新节点的代码封装成函数,后续生成新节点的操作,都会通过调用这个函数实现

Node* CreateNewNode(LTData n)
{Node* newnode = (Node*)malloc(sizeof(Node));if (newnode == NULL) {perror("newnode malloc fail");exit(EXIT_FAILURE);}newnode->data = n;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

尾插法的函数实现如下:

void ListDataPushBack(Node* headnode,LTData n)
{Node* newnode = CreateNewNode(n);Node* ptail = headnode->prev;newnode->next = headnode;newnode->prev = ptail;ptail->next = newnode;headnode->prev = newnode;
}

打印双向链表

为了便于调式,可以封装一个打印双向链表的函数。

打印的方法如下:
通过遍历链表,将链表中除了头结点以外的所有节点的数据打印至屏幕。由于双向链表具有循环的性质,因此从第一个节点开始遍历至头结点为止。

void printlist(Node* headnode)
{Node* pcur = headnode->next;while (pcur!= headnode){printf("%d->", pcur->data);pcur = pcur->next;}printf("headnode\n");
}

可以用打印的方式测试尾插法是否符合要求,测试方法如下:

	Node* plist = HeadNodeInit();ListDataPushBack(plist, 1);ListDataPushBack(plist, 2);ListDataPushBack(plist, 3);ListDataPushBack(plist, 4);printlist(plist);

打印结果如下,测试结果为尾插法符合要求。
在这里插入图片描述

头插法

将节点插入头结点之后,第一个节点之前的方法称为头插法。

头插法的方法如下:

(1)将新节点的后继节点设置为第一个节点
(2)将新节点的前驱节点设置为头结点
(3)将第一个节点的前驱节点设置为新节点
(4)将头结点的后继节点设置为新节点

注意步骤(3)和(4)的顺序不能调换。

在这里插入图片描述
代码如下:`

void ListDataPushFront(Node* headnode, LTData n)
{assert(headnode);Node* newnode = CreateNewNode(n);newnode->next = headnode->next;newnode->prev = headnode;headnode->next->prev = newnode;headnode->next = newnode;
}

查找指定数据项的节点

将整个链表遍历一遍寻找数据项,如果某节点的数据项符合查找的要求,就返回该节点位置。
函数原型如下:

Node* FindListNode(Node* headnode, LTData n)

(1)headnode是待查找的双向链表的头结点
(2)n是指定的数据项

遍历的方式如下:

(1)从头结点的下一个节点开始
(2)依次遍历所有节点,对比数据项
(3)回到头结点后结束遍历

Node* FindListNode(Node* headnode, LTData n)
{assert(headnode);//头结点不为空assert(headnode->next != headnode);//待查找的链表不为空链表Node* pcur = headnode->next;while (pcur != headnode)//循环结束条件为指针回到头结点{if (pcur->data == n)//匹配数据项return pcur;//完成匹配pcur = pcur->next;}return NULL;//未完成匹配
}

在指定位置之后插入节点

前面讲解了如何在查找指定的位置,这里就利用前面查找位置的函数来讲解如何在指定位置之后插入位置。

首先通过前面写的查找数据项的函数确定插入位置。
插入的方法如下:

(1)将新节点的前驱节点设为位置节点
(2)将新节点的后继节点设为位置节点的后一个节点
(3)令位置节点的后一个节点的前驱设为新节点
(4)令位置节点的后继节点设为新节点

注意步骤3和步骤4的位置不能变换
在这里插入图片描述
函数原型如下:

void ListNodeInsert(Node* headnode, LTData n, Node* pos);

(1)headnode——待插入链表的头结点
(2)n——节点的数据项
(3)pos——插入的节点位置

函数实现如下:

void ListNodeInsert(Node* headnode, LTData n, Node* pos)
{assert(pos);assert(headnode);Node*newnode=CreateNewNode(n);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}

指定位置的删除

删除指定位置的方法如下:

(1)将待删除的节点的后继节点的前驱设为待删除节点的前驱节点
(2)将待删除的节点的前驱节点的后继设为待删除节点的后继节点
(3)释放待删除节点
在这里插入图片描述

函数的原型如下:

void ListNodeDelete(Node* headnode, Node* pos);

(1)headnode是待删除节点的链表的头结点
(2)pos是待删除节点的位置

函数的实现如下:

void ListNodeDelete(Node* headnode, Node* pos)
{assert(headnode);assert(pos);assert(pos != headnode);//禁止删除头结点pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}

双向链表的销毁

将双向链表销毁的方法如下:

从头结点开始往后遍历(也可以往前遍历),将每个遍历的节点进行销毁,最后回到头结点时结束遍历,别忘了头结点也要进行销毁。

函数的代码如下:

void ListDestory(Node* headnode)
{Node* pcur = headnode->next;Node* ptmp = pcur;while (pcur != headnode){ptmp = pcur;pcur = pcur->next;free(ptmp);}free(headnode);
}

顺序表与链表的对比

既然顺序表与链表都是线性表,那么这两种线性表的优劣是什么呢

顺序表线性表
物理结构顺序存储非线性存储
访问元素时间复杂度O(1)时间复杂度O(n)
插入/删除元素O(N)O(1)
空间利用需要扩容(利用程度低)不需要扩容(利用程度高)
优势查找元素和访问速度快频繁插入删除的速度快

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

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

相关文章

C语言第十二弹--扫雷

✨个人主页: 熬夜学编程的小林 💗系列专栏: 【C语言详解】 【数据结构详解】 扫雷 1、扫雷游戏分析和设计 1.1、扫雷游戏的功能说明 1.2 游戏的分析和设计 1.2.1、数据结构的分析 1.2.2、文件结构设计 2、扫雷游戏的结构分析 2.1、用…

vmware虚拟机centos8共享文件夹挂载

1.设置虚拟机共享文件夹 2. 上述设置完毕之后,重启进入虚拟机,查看出现的共享文件夹名称 vmware-hgfsclient 3.查看是否有挂载目录,挂在目录默认为 /mnt/hgfs。没有时可以使用以下命令创建 mkdir /mnt/hgfs 4. 手动挂载目录--只能实现一次 注…

Arduino Uno R3通过ESP-01S连接网络

一、材料准备 Arduino Uno R3开发板 1 USB串口通信数据线(Uno开发板使用) 1 ESP8266-01S Wi-Fi模块 1 ESP8266固件烧录下载器(烧录固件使用) 1 WiFi无线收发转接板(适用于ESP-01S、ESP-01) 杜邦线…

java设计模式:工厂模式

1:在平常的开发工作中,我们可能会用到不同的设计模式,合理的使用设计模式,可以提高开发效率,提高代码质量,提高系统的可拓展性,今天来简单聊聊工厂模式。 2:工厂模式是一种创建对象的…

如何查看某网站的谷歌流量的组成情况

在独立站跨境贸易当中,很多时候我们都会重复一个动作,那就是查看对手网站或者某一网站的流量,以此来分析和总结如何优化自己的站点,借鉴对手优秀的地方来补足自己的缺点,或者某些时候会模仿甚至抄袭竞品网站。那么如何…

C++ 类与对象(上)

目录 本节目标 1.面向过程和面向对象初步认识 2.类的引入 3.类的定义 4.类的访问限定符及封装 4.1 访问限定符 4.2 封装 5. 类的作用域 6. 类的实例化 7.类对象模型 7.1 如何计算类对象的大小 7.2 类对象的存储方式猜测 7.3 结构体内存对齐规则 8.this指针 8.1 thi…

数据结构(一)------顺序表

文章目录 前言一、什么是顺序表二、实现顺序表1.静态顺序表2.动态顺序表总结 前言 制作不易!三连支持一下呗!!! 从今天起我们将会进入数据结构的学习! 我们先来了解 什么是数据结构 数据结构是计算机存储、组织数…

LeetCode(2)

目录 概念解释 栈 队列 树 树的概念 结点的分类 有序树 无序树 森林 二叉树 满二叉树 完全二叉树 二叉排序树 平衡二叉树 1.用栈实现队列 解法:双栈 2.字符串解码 解法:栈 3.二叉树的中序遍历 解法一:递归 解法二&#xff…

云微呼探索人工智能机器人对话:过去、现在和未来

随着科技的迅速发展,人工智能(AI)机器人已经成为我们日常生活中的重要一部分。从简单的语音助手到能够进行复杂对话的智能机器人,AI技术正在改变着我们与机器之间的互动方式。本文将探讨人工智能机器人对话的历史、现状以及未来发…

[SwiftUI]系统弹窗和自定义弹窗

一、系统弹窗 在 SwiftUI 中,.alert 是一个修饰符,用于在某些条件下显示一个警告对话框。Alert 可以配置标题、消息和一系列的按钮。每个按钮可以是默认样式、取消样式,或者是破坏性的样式,它们分别对应不同的用户操作。 1.Aler…

前端qrcode生成二维码详解

文章目录 前言1、浏览器支持2、优点3、缺点4、相关方法5、安装及使用示例 前言 qrcode 是一个基于JavaScript的二维码生成库,主要是通过获取 DOM 的标签,再通过 HTML5 Canvas 绘制而成,不依赖任何库。 官方文档:https://www.npm…

Kafka-服务端-GroupCoordinator

在每一个Broker上都会实例化一个GroupCoordinator对象,Kafka按照Consumer Group的名称将其分配给对应的GroupCoordinator进行管理; 每个GroupCoordinator只负责管理Consumer Group的一个子集,而非集群中全部的Consumer Group。 请注意与Kaf…

Java项目:基于SSM框架实现的企业员工岗前培训管理系统(ssm+B/S架构+源码+数据库+毕业论文)

一、项目简介 本项目是一套ssm821基于ssm框架实现的企业员工岗前培训管理系统,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含:项目源码、数据库脚本等,该项目附带全部源码可作为毕设使用。 项目都经过严格…

NetExec:一款功能强大的自动化网络安全评估与漏洞测试工具

关于NetExec NetExec是一款功能强大的自动化网络安全评估与漏洞测试工具,该工具可以帮助广大研究人员以自动化的形式测试大型网络的安全,并通过利用网络服务漏洞来评估目标网络的安全态势。 支持的协议 1、SMB协议 2、LDAP协议 3、WinRM协议 4、MSSQL协…

go语言函数进阶

1.变量作用域 全局变量 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。 package mainimport "fmt"//定义全局变量num var num int64 10func testGlobalVar() {fmt.Printf("num%d\n", num) /…

vue-head 插件设置浏览器顶部 favicon 图标 - 动态管理 html 文档头部标签内容

目录 需求实现11. 安装插件2. 项目内 main.js 引入3. vue页面使用 实现2其他 需求 vue项目中浏览器页面顶部图标可配置 实现1 使用 vue-head 插件实现 vue-head 插件可实现 html 文档中 head 标签中的内容动态配置(npm 官网 vue-head 插件) 1. 安装插件 …

promethues基础概念

promethues是一个开源的系统监控以及报警系统,整个zabbix的功能,系统,网络,设备 promethues可以兼容网络和设置被,容器监控,告警系统,因为他和k8s是一个项目基金开发的产品,天生匹配…

代码随想录算法训练营第34天 | 1005.K次取反后最大化的数组和 134.加油站 135.分发糖果

K次取反后最大化的数组和 贪心局部最优:将绝对值大的负数变为正数,当前和变为最大;全局最优:整体获得最大和。 如果负数都变成正数之后,k > 0,仍然需要继续翻转,贪心局部最优:将最…

14.STM32F4 LCD屏幕概念及源码下载(LCD之一)

一、LCD液晶显示屏介绍 1、常见的显示设备 在目前市面上,常见的显示设备种类有:LED、显示数码管、点阵LED显示屏、LCD液晶显示屏,这几种设备的特点是: (1)LED LED灯是最简单的显示设备,它只有两…

天拓四方:物联网网关在机械制造企业的应用

随着物联网技术的不断发展,越来越多的机械制造企业开始探索如何利用物联网技术提升生产效率、降低运营成本。物联网网关作为物联网架构中的关键设备,能够实现设备间的数据交互与远程控制,为机械制造企业带来了巨大的商业价值。它能够实现设备…