肯尼斯·里科《C和指针》第12章 使用结构和指针(2)双链表

12.3 双链表

单链表的替代方案就是双链表。在一个双链表中,每个节点都包含两个指针——指向前一个节点的指针和指向后一个节点的指针。这可以使我们以任何方向遍历双链表,甚至可以随意在双链表中访问。下面的图展示了一个双链表。

下面是节点类型的声明:

typedf struct  NODE   {struct  NODE   *fwd;struct  NODE   *bwd;int              value;
} Node;

现在,存在两个根指针:一个指向链表的第一个节点,另一个指向最后一个节点。这两个指针允许从链表的任何一端开始遍历链表。

我们可能想把两个根指针分开声明为两个变量。但这样一来,我们必须把两个指针都传递给插入函数。为根指针声明一个完整的节点更为方便,只是它的值字段绝不会被使用。在我们的例子中,这个技巧只是浪费了一个整型值的内存空间。对于值字段非常大的链表,分开声明两个指针可能更好一些。另外,也可以在根节点的值字段中保存其他一些关于链表的信息,例如链表当前包含的节点数量。

根节点的fwd字段指向链表的第1个节点,根节点的bwd字段指向链表的最后1个节点。如果链表为空,这两个字段都为NULL。链表第1个节点的bwd字段和最后1个节点的rwd字段都为NULL。在一个有序的链表中,各个节点将根据value字段的值以升序排列。

12.3.1 在双链表中插入

这一次,我们要编写一个函数,把一个值插入到一个有序的双链表中。dll_insert函数接受2个参数:一个指向根节点的指针和一个整型值。

我们先前所编写的单链表插入函数把重复的值也添加到链表中。在有些应用程序中,不插入重复的值可能更为合适。dll_insert函数只有当待插入的值原先不存在于链表中时才将其插入。

让我们用一种更为规范的方法来编写这个函数。当把一个节点插入到一个链表时,可能出现4种情况:

1.新值可能必须插入到链表的中间位置;

2.新值可能必须插入到链表的起始位置;

3.新值可能必须插入到链表的结束位置;

4.新值可能必须既插入到链表的起始位置,又插入到链表的结束位置(即原链表为空)。

在每种情况下,有4个指针必须进行修改。

在情况1和情况2中,新节点的fwd字段必须设置为指向链表的下一个节点,链表下一个节点的bwd字段必须设置为指向这个新节点。在情况3和情况4中,新节点的fwd字段必须设置为NULL,根节点的bwd字段必须设置为指向新节点。

在情况1和情况3中,新节点的bwd字段必须设置为指向链表的前一个节点,而链表前一个节点的fwd字段必须设置为指向新节点。在情况2和情况4中,新节点的bwd字段必须设置为NULL,根节点的fwd字段必须设置为指向新节点。

/*
** 把一个值插入到一个双链表,rootp是一个指向根节点的指针,
** value是待插入的新值。
** 返回值:如果欲插值原先已存在于链表中,函数返回0;
** 如果内存不足导致无法插入,函数返回-1;如果插入成功,函数返回1。
*/
#include <stdlib.h>
#include <stdio.h>
#include "doubly_linked_list_node.h"
int
dll_insert( Node *rootp, int value )
{Node  *this;Node  *next;Node  *newnode;/*** 查看value是否已经存在于链表中,如果是就返回。** 否则,为新值创建一个新节点("newnode"将指向它)。** "this"将指向应该在新节点之前的那个节点,** "next"将指向应该在新节点之后的那个节点。*/for( this = rootp; (next = this->fwd) != NULL; this = next ){if( next->value == value )return 0;if( next->value > value )break;
}
newnode = (Node *)malloc( sizeof( Node ) );
if( newnode == NULL )return -1;
newnode->value = value;
/*
** 把新值添加到链表中。
*/
if( next != NULL ){
/*
** 情况1或2: 并非位于链表尾部。
*/if( this != rootp ){      /* 情况1: 并非位于链表起始位置。 */newnode->fwd = next;this->fwd = newnode;newnode->bwd = this;next->bwd = newnode;}else {                          /* 情况2: 位于链表起始位置。 */newnode->fwd = next;rootp->fwd = newnode;newnode->bwd = NULL;next->bwd = newnode;}}else {/*** 情况3或4: 位于链表的尾部。*/if( this != rootp ){    /* 情况3: 并非位于链表的起始位置。 */newnode->fwd = NULL;this->fwd = newnode;newnode->bwd = this;rootp->bwd = newnode;}else {                         /* 情况4: 位于链表的起始位置。 */newnode->fwd = NULL;rootp->fwd = newnode;newnode->bwd = NULL;rootp->bwd = newnode;}}return 1;
}

一开始,函数使this指向根节点。next指针始终指向this之后的那个节点。它的思路是这两个指针同步前进,直到新节点应该插入到这两者之间。for循环检查next所指节点的值,判断是否到达需要插入的位置。

如果在链表中找到新值,函数就简单地返回;否则,当到达链表尾部或找到适当的插入位置时循环终止。在任何一种情况下,新节点都应该插入到this所指的节点后面。注意,在决定新值是否应该实际插入到链表之前,并不为它分配内存。如果事先分配内存,但发现新值原先已经存在于链表中,就有可能发生内存泄漏。

4种情况是分开实现的。让我们通过把12插入到链表中来观察情况1。下面这张图显示了for循环终止之后几个变量的状态。

然后,函数为新节点分配内存,下面几条语句执行之后,

newnode->fwd = next;
this->fwd = newnode;

链表的样子如下所示。

然后,执行下列语句:

newnode->bwd = this;
next->bwd = newnode;

这就完成了把新值插入到链表的过程。

批注:书中给的图错了吧,,,如果执行newnode->bwd = this和next->bwd=newnode,应该不是上面的这张图吧。。。

简化插入函数

细心的程序员会注意到,在函数中各个嵌套的if语句群存在大量的相似之处,而优秀的程序员会对程序中出现这么多的重复代码感到厌烦。所以,我们现在将使用两个技巧消除这些重复的代码。第一个技巧是语句提炼(statement factoring),如下面的例子所示:

if( x == 3) {i = 1;something;j = 2;
}
else {i = 1;something different;j = 2;
}

注意,不管表达式x==3的值是真还是假,语句i=1和j=2都将执行。在if之前执行i=1不会影响x==3的测试结果,所以这两条语句都可以被提炼出来,这样就产生了更为简单但同样完整的语句:

i = 1;
if( x == 3 )something;
elsesomething different;
j = 2;

把上面程序最内层嵌套的if语句进行提炼,就产生了下面程序的代码段。请将这段代码和前面的函数进行比较,确认它们是等价的。

/*
** 把新节点添加到链表中。
*/
if( next != NULL ){/*** 情况1或情况2: 并非位于链表的尾部。*/newnode->fwd = next;if( this != rootp ){     /* 情况1: 并非位于链表起始位置。 */this->fwd = newnode;newnode->bwd = this;}else {                         /* 情况2: 位于链表起始位置。 */rootp->fwd = newnode;newnode->bwd = NULL;}next->bwd = newnode;
}
else {/*** 情况3或情况4: 位于链表尾部。*/newnode->fwd = NULL;if( this != rootp ){  /* 情况3: 并不位于链表起始位置。 */this->fwd = newnode;newnode->bwd = this;}else {                       /* 情况4: 位于链表起始位置。 */rootp->fwd = newnode;newnode->bwd = NULL;}rootp->bwd = newnode;}

第二个简化技巧很容易用下面这个例子进行说明:

if( pointer !=NULL )field = pointer;
elsefileld = NULL;

这段代码的意图是设置一个和pointer相等的变量,如果pointer未指向任何内容,这个变量就设置为NULL。但是,请看下面这条语句:

field = pointer;

如果pointer的值不是NULL,field就像前面一样获得它的值的一份副本。但是,如果pointer的值是NULL,那么field将从pointer获得一份NULL的副本,这和把它赋值为常量NULL的效果是一样的。这条语句所执行的任务和前面那条if语句相同,但它明显简单多了。

在上面程序中运用这个技巧的关键是找出那些虽然看上去不一样但实际上执行相同任务的语句,然后对它们进行改写,写成同一种形式。我们可以把情况3和情况4的第一条语句改写为:

newnode->fwd = next;

由于if语句刚刚判断出next==NULL,这个改动使if语句两边的第一条语句相等,因此可以把它提炼出来。请做好这个修改,然后对剩余的代码进行研究。

我们还可以对代码作进一步的完善。第一条if语句的else子句的第一条语句可以改写为:

this->fwd = newnode;

这是因为if语句已经判断出this==rootp。现在,这条改写后的语句以及它的同类也可以被提炼出来。

下面的程序是实现了所有修改的完整版本。它所执行的任务和最初的函数相同,但体积要小得多。局部指针被声明为寄存器变量,进一步改善了代码的体积和执行速度。

/*
** 把一个新值插入到一个双链表中。rootp是一个指向根节点的指针,
** value是需要插入的新值** 返回值:如果链表原先已经存在这个值,函数返回0。** 如果为新值分配内存失败,函数返回-1。** 如果新值成功地插入到链表中,函数返回1。*/#include <stdlib.h>#include <stdio.h>#include "doubly_liked_list_node.h"intdll_insert( register Node *rootp, int value ){register Node  *this;register Node  *next;register Node  *newnode;/*** 查看value是否已经存在于链表中,如果是就返回。** 否则,为新值创建一个新节点("newnode"将指向它)。** "this"将指向应该在新节点之前的那个节点,** "next"将指向应该在新节点之后的那个节点。*/for( this = rootp; (next = this->fwd) != NULL; this = next ){if( next->value == value )return 0;if( next->value > value )break;}newnode = (Node *)malloc( sizeof( Node ) );if( newnode == NULL )return -1;newnode->value = value;/*** 把新节点添加到链表中。*/newnode->fwd = next;this->fwd = newnode;if( this != rootp )newnode->bwd = this;elsenewnode->bwd = NULL;if( next != NULL )next->bwd = newnode;elserootp->bwd = newnode;return 1;
}

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

算法刷题:移动零

移动零 .题目链接详解curdesc算法原理 答案 . 题目链接 移动零 详解 题目要求我们要把数组中所有的零都移动到数组的末尾,且要求其余数字顺序不改变.这道题,我们使用到的是双指针算法: 利用两个指针,将数组分为三个部分, 三个区间分别为 [0,desc][desc1,cur-1][cur,n-1] 在…

算法学习——LeetCode力扣二叉树篇1

算法学习——LeetCode力扣二叉树篇1 144. 二叉树的前序遍历 144. 二叉树的前序遍历 - 力扣&#xff08;LeetCode&#xff09; 描述 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&a…

电子电器架构 —— 区域控制器是未来架构的正解吗?

电子电器架构 —— 区域控制器是未来架构的正解吗? 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师(Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶…

C++ //练习 5.5 写一段自己的程序,使用if else语句实现把数字成绩转换成字母成绩的要求。

C Primer&#xff08;第5版&#xff09; 练习 5.5 练习 5.5 写一段自己的程序&#xff0c;使用if else语句实现把数字成绩转换成字母成绩的要求。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /***************************…

视觉开发板—K210自学笔记(四)

在点灯之后&#xff0c;我们就需要饿补一下相关的编程基础知识&#xff0c;了解基本语法&#xff0c;加深底蕴才能编写出更好的程序来。由于 MaixPy 是基于 MicroPython 之上进行开发构建的&#xff0c;提供给用户最终的接口是 Micropython &#xff0c;所以在使用 MaixPy 开发…

C语言-----自定义类型-----结构体枚举联合

结构体和数组一样&#xff0c;都是一群数据的集合&#xff0c;不同的是数组当中的数据是相同的类型&#xff0c;但是结构体中的数据类型可以不相同&#xff0c;结构体里的成员叫做成员变量 结构体类型是C语言里面的一种自定义类型&#xff0c;我们前面已经了解到过int,char,fl…

选择影视行业创业的原因,影视从业者创业成功的秘密

一、教程描述 本套教程是面向影视从业者的创业教程&#xff0c;主讲人将把自己的创业经验、行业观察、成长心得分享给大家。如果你正在创业&#xff0c;这门课可以让你飞速成长、弯道超车。主讲人积累的行业经验&#xff0c;会让你比大多数同行站的更高&#xff0c;看的更宽。…

KEIL-MDK的时间戳之time.h 结合gd32f1的RTC应用

KEIL-MDK的时间戳之time.h 的应用 1 时间戳介绍 现在物联网产品的在进行通讯的时候&#xff0c;需要加入时间戳的这个信息参数&#xff0c;方便服务器和产品之间交换时间信息。 时间戳是计算机系统中用来表示日期和时间的一种方式&#xff0c;通常是一个数字或者一串字符&am…

【DDD】学习笔记-统一语言与领域分析模型

无论你采用什么样的软件开发过程&#xff0c;对于一个复杂的软件系统&#xff0c;都必然需要通过分析阶段对问题域展开分析&#xff0c;如此才能有的放矢地针对该软件系统的需求寻找设计上的解决方案。在领域驱动设计中&#xff0c;分析阶段完全围绕着“领域”为中心展开&#…

从信息隐藏到功能隐藏

本文主要记录复旦大学张新鹏教授于2022年12月在第三届CSIG中国媒体取证与安全大会上的汇报

微信小程序 民宿预订租赁系统uniApp

通过山青水磨APP办理租房相关业务&#xff0c;线上解决预定、退订的业务&#xff0c;旅客在使用时更加灵活&#xff0c;实现了快速找房&#xff0c;在线沟通、便捷租赁等操作&#xff0c;除此以外&#xff0c;还能帮助旅客获取周边资讯、当地特色活动服务&#xff0c;提升旅客的…

1-3 mininet中使用python API直接拓扑定义以及启动方式对比

作为SDN网络中搭建拓扑非常重要的仿真平台&#xff0c;我们可以使用mininet默认的库内拓扑文件&#xff0c;也可以使用python语言进行自定义拓扑。使用python进行拓扑定义时&#xff0c;不同的定义方式将导致其启动的方式由所不同。 一、采用最原始的命令启动方式&#xff1a; …

Python 视频转场特效处理笔记

本文参考Python-OpenCV 实现美图秀秀视频剪辑效果【特效】_opencv 多张图片 视频 特效-CSDN博客 最近研究了点python处理视频相关的东西&#xff0c;本文展示特效包括&#xff0c;竖向开幕/横向开幕&#xff0c;渐隐/渐显&#xff0c;推近/拉远&#xff0c;方形开幕&#xff0…

yolo层数连接

head [-1,6]连接的是第六层 [-1,4连接的是第四层

在虚拟机上完成Centos安装

Linux学习和使用 前言如何安装Centos初始化操作 使用VMware备份操作系统快照克隆 内容总结参考链接 本人介绍:2023年全国大学生数学建模竞赛国家二等奖,2022年蓝桥杯省二等奖,这里是一个和你一起不断努力,不断前进的程序猿一枚 前言 简单介绍一下本片文章将会讲到的内容:本章节…

大模型训练所需的硬件配置

1. 引入 训练一个大模型&#xff0c;到底需要投入多少块GPU&#xff0c;需要多少数据&#xff0c;训练多长时间能达到一个不错的效果&#xff1f; 本文引用靠谱的数据&#xff0c;来回答这些问题。 2. 全流程训练 大模型的训练&#xff0c;简单来说&#xff0c;分为Pretrain…

Peter算法小课堂—单调队列

祝大家新年快乐&#xff01; 今天这一次有点简单。 单调队列有两个要点&#xff0c;一个是单调&#xff0c;另一个就是我们的队列。 听到队列&#xff0c;我相信大家一定会想到它的好朋友BFS吧。但是……今天……可……没……那么……简单哦。 西佳佳偶像天团1 题目描述 …

第74讲Breadcrumb 面包屑实现

Breadcrumb 面包屑实现 为了实现二级路由&#xff0c;我们搞成搞个子路由&#xff0c;对于二级菜单 const routes [{path: /,name: 首页,component: () > import(../views/layout),redirect:/home,children:[{path: /home,name: 首页,component: () > import(../views…

vtkActor 设置特定图层 显示及置顶显示

问题&#xff0c;有时我们需要显示某个 Actor 在相机最前面&#xff0c;可以遮盖后面的物体;显示在顶层有点不准确&#xff1b;因为这个还相机位置也有关系&#xff1b; 这里讲三种情况&#xff1a; 1. 设置 Mapper 顶层&#xff0c;尝试了一下&#xff0c;可以用于某些场景&…

对话模型Demo解读(使用代码解读原理)

文章目录 前言一、数据加工二、模型搭建三、模型训练1、构建模型2、优化器与损失函数定义3、模型训练 四、模型推理五、所有Demo源码 前言 对话模型是一种人工智能技术&#xff0c;旨在使计算机能够像人类一样进行对话和交流。这种模型通常基于深度学习和自然语言处理技术&…