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

news2024/11/16 21:01:38

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;
else
          something 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;
else
         fileld = 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"
    int
    dll_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;
         else
              newnode->bwd = NULL;
         if( next != NULL )
              next->bwd = newnode;
         else
              rootp->bwd = newnode;
         return 1;
}

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

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

相关文章

运维备忘录』之 TAR 命令详解

运维人员不仅要熟悉操作系统、服务器、网络等只是&#xff0c;甚至对于开发相关的也要有所了解。很多运维工作者可能一时半会记不住那么多命令、代码、方法、原理或者用法等等。这里我将结合自身工作&#xff0c;持续给大家更新运维工作所需要接触到的知识点&#xff0c;希望大…

go语言每日一练——链表篇(六)

传送门 牛客面试必刷101题—— 判断链表中是否有环 牛客面试必刷101题—— 链表中环的入口结点 题目及解析 题目一 代码 package mainimport . "nc_tools"/** type ListNode struct{* Val int* Next *ListNode* }*//**** param head ListNode类* return bool…

java日志框架总结(五、logback日志框架)

一、logback概述 Logback是由log4j创始人设计的又一个开源日志组件。 Logback当前分成三个模块&#xff1a; 1、logback-core, 2、logback- classic 3、logback-access。 1&#xff09;logback-core是其它两个模块的基础模块。 2&#xff09;logback-…

MySQL之建表操作

华子目录 表操作创建表数据类型文本类型数值类型日期/时间类型Bit数据类型常见数据类型 MySQL存储引擎创建表的三个操作创建表时指定存储引擎&#xff0c;字符集&#xff0c;校对规则&#xff0c;行格式 查看表显示数据库中所有表显示数据库中表的信息&#xff08;表结构&#…

flutter监听app进入前后台状态的实现

在开发app的过程中&#xff0c;我们经常需要根据app的前后台的状态&#xff0c;做一些事情&#xff0c;那么我们在flutter中是如何实现这一监听的&#xff1f; flutter给我们提供了WidgetsBindingObserver来进行一些状态的判断&#xff0c;但是判断前后台的状态只是该API种其中…

红队打靶练习:PHOTOGRAPHER: 1

目录 信息收集 1、arp 2、nmap 3、nikto 目录扫描 1、gobuster 2、dirsearch WEB 信息收集 enum4linux smbclient 8000端口 CMS利用 信息收集 文件上传漏洞利用 提权 信息收集 get user.txt get flag 信息收集 1、arp ┌──(root㉿ru)-[~/kali] └─# a…

C#中实现串口通讯(使用SerialPort类)

仅作自己学习使用 1 准备部份 需要两个调试软件commix和Virtual Serial Port Driver&#xff0c;分别用于监视串口和创造虚拟串口。 第一个软件是这样的&#xff1a; 资源在这里&#xff1a;免费下载&#xff1a;Commix 也可以前往官网下载&#xff1a;Bwsensing— Attitude…

苹果macbook电脑删除数据恢复该怎么做?Mac电脑误删文件的恢复方法

苹果电脑删除数据恢复该怎么做&#xff1f;Mac电脑误删文件的恢复方法 如何在Mac上恢复误删除的文件&#xff1f;在日常使用Mac电脑时&#xff0c;无论是工作还是娱乐&#xff0c;我们都会创建和处理大量的文件。然而&#xff0c;有时候可能会不小心删除一些重要的文件&#x…

【GPT】一个高效使用excel获得结果的案例

问:请介绍通过规划求解&#xff0c;求出以最低成本购买固定数量礼品的方法。 ChatGPT: 通过规划求解方法&#xff0c;可以确定以最低成本购买固定数量礼品的方法。以下是使用规划求解进行最低成本购买礼品的一般步骤。 1.定义目标: 明确目标是以最低成本购买固定数量的礼品。…

哪个牌子的游泳耳机质量好又耐用?性价比高的游泳耳机品牌排行榜

如今&#xff0c;越来越多的人开始注重运动健身&#xff0c;并在运动时习惯享受音乐的陪伴。市场上的运动耳机种类繁多&#xff0c;以蓝牙耳机为主流。然而&#xff0c;在一些特定的运动项目&#xff0c;尤其是游泳&#xff0c;将手机放在附近并不方便。因此&#xff0c;如果在…

SpringMVC原理(设计原理+启动原理+工作原理)

文章目录 前言正文一、设计原理1.1 servlet生命周期简述1.2 设计原理小结 二、启动原理2.1 AbstractHandlerMethodMapping 初始化 --RequestMapping注解解析2.2 DispatcherServlet 的初始化2.3 DispatcherServlet#initHandlerMappings(...) 初始化示例说明 三、工作原理 前言 …

2023年12月CCF-GESP编程能力等级认证C++编程一级真题解析

一、单选题(共15题,共30分) 第1题 以下C++不可以作为变量的名称的是( )。 A:CCF GESP B:ccfGESP C:CCFgesp D:CCF_GESP 答案:A 第2题 C++表达式 10 - 3 * (2 + 1) % 10 的值是( )。 A:0 B:1 C:2 D:3 答案:B 第3题 假设现在是上午十点,求出N小时(正整数…

「 CISSP学习笔记 」08. 安全运营

该知识领域涉及如下考点&#xff0c;具体内容分布于如下各个子章节&#xff1a; 理解并遵守调查执行记录和监控活动执行配置管理 (CM)&#xff08;例如&#xff0c;预配、基线、自动化&#xff09;应用基本的安全操作概念应用资源保护执行事故管理执行和维护检测和预防措施实施…

基于微信小程序校园浴室预约系统设计与实现(php+mysql后台)

博主介绍&#xff1a;黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者&#xff0c;CSDN博客专家&#xff0c;在线教育专家&#xff0c;CSDN钻石讲师&#xff1b;专注大学生毕业设计教育和辅导。 所有项目都配有从入门到精通的基础知识视频课程&#xff…

分析伦敦银报价总失败?你试试这样

做伦敦银交易的投资者要先对伦敦银报价进行分析&#xff0c;但是有些投资者反映自己分析伦敦银报价总是失败&#xff0c;抓不住市场价格的变化趋势&#xff0c;为什么会这样呢&#xff1f;我们可以从以下这两个方面来考虑。 转变分析工具。为什么分析伦敦银报价总失败&#xff…

重生奇迹mu仙踪林npc

工匠尤达 NPC工匠尤达位于仙踪林的坐标为(87, 134)&#xff0c;他可以给玩家制作装备和强化装备。 精灵安吉拉 NPC精灵安吉拉位于仙踪林的坐标为(29, 196)&#xff0c;她可以给玩家提供补血、补魔服务&#xff0c;同时也能提供提高属性点数的服务。 仓库使者塞维特 NPC仓库…

相机图像质量研究(6)常见问题总结:光学结构对成像的影响--对焦距离

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

JetpackCompose中的Dialog、AlertDialog

跟View体系一样&#xff0c;Compose中也用Dialog做提示框的。既然有这个API&#xff0c;那我们还是得卷起来熟悉下使用流程及方法。 Dialog 其构造函数如下&#xff1a; Composable fun Dialog(onDismissRequest: () -> Unit,properties: DialogProperties DialogProper…

机器学习复习(5)——激活函数

目录 激活函数分类 区别与优缺点 饱和激活函数 非饱和激活函数 综合考虑 Sigmoid激活函数 Tanh激活函数 ReLU激活函数 Leaky Relu激活函数 Swish激活函数 激活函数分类 激活函数可以分为两大类 &#xff1a; 饱和激活函数&#xff1a;sigmoid、tanh非饱和激活函数:…

Blender教程(基础)-顶点的移动、滑移-16

一、顶点的移动与缩放 ShiftA新建柱体、切换到编辑模式 点模式下&#xff0c;选择一个顶点、选择移动&#xff08;GZ&#xff09;&#xff0c;发现顶点严Z轴移动&#xff0c;如下图所示 GY 按数字键盘7切换视图&#xff0c;选择这个面的所有顶点 按S把面缩放大 Ctrl…