单链表的插入和删除

2023-10-27

前言

在上一篇文章(单链表的定义)中我们已经了解了单链表的含义和简单的实现。那么在这篇文章中,我们将要来讲解单链表的插入和删除操作。

按位序插入(带头结点)

我们在上篇文章中已经讲解过,如果想要在表L中的第i个位置上插入指定元素e,我们需要找到第i-1的结点,在其后插入新的结点。大致步骤如下:

  1. 使用malloc()函数申请一个新的结点
  2. 往新的结点存入新的元素e
  3. 将i-1位的元素指针指向新结点
  4. 将新结点的指针指向原先第i个元素

当然当我们想在第一位插入新结点时,对于带头结点的链表的好处就是,在头部插入数据和其他任意一个位置插入数据的操作是一样的,非常方便。

代码实现

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

bool ListInsert(LinkList &L,int i,ElemType e){
    if(i<i)
        return false;
    LNode *p;			// 指针p指向当前扫描到的结点
    int j = 0;			// 当前p指针指向的是第几个结点
    p = L;				// L指向头结点,头结点时第0个结点(不存放数据)
	while(p!=NULL && j<i-1){
        p = p->next;
        j++;
    }
    if(p=NULL)			// i值不合法
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;		// 将结点s连接到p之后
    
    return true;		// 插入成功
}

代码分析(在第一个位置插入结点)

若 i = 1

我们直接看ListInsert()函数的内部

  1. 在进入时首先会判断,i的合法性(i小于1是不合法的)

    • 位序是从1开始,不可能会比开头的还要小。如果小的话那就不属于链表中的元素。
  2. 声明一个p指针,声明一个j变量用来表示p指针指向第几个结点

  3. 紧接着将p指针指向头结点L

  4. 这时候由于j = 0;i = 0不满足循环条件,所以直接不进行循环。

  5. 判断i值是否合法

  6. 创建一个新的结点,并且将结点的指针返回给*s

  7. s->data:将e赋值给s的数据域(将e装入s)

  8. s->next=p->next:将s的指针指向p指针指向的位置。

    • 由于p指针在上面已经指向头结点了,所以p指针的next就跟头节点一样指向a1

    image-20220707214308607

  9. p->next=s:将p指针的指针指向s

    • 由于在第一个位置插入结点,所以链表中的第一个结点不再是a1,而是s了,所以需要将p的指针指向s。

注意:s->next=p->nextp->next=s的代码顺序不能颠倒。如果颠倒则会出现一下状况:

  1. p先将指针指向s
  2. s再将指针指向p指向的(也就是s)

就会形成一种“我指我自己”的尴尬局面。

image-20220707214815631

代码分析(在第三个位置插入结点)

若 i = 3

我们直接从不同的地方说起:

  1. 这时候则会进入while循环

    • while循环的作用,其实就是将p指针指向下一个结点,直至指向想要插入的位置-1——i - 1为止。

    • 未执行while循环

      image-20220707215116959

    • 执行1次while循环

      image-20220707215238548

    • 以此类推,直至到想要的位置-1(这里是到第二个位置)

  2. 紧接着下述的结点如何插入,与在第一个位置插入结点的操作相同,所以不多赘述

代码分析(在第六个位置插入结点)

由于该链表中只有4个结点(头结点不算),所以while循环最多只能循环5次——也就是当p指针指向a4结点的next——NULL时就会推出循环。

image-20220707215735557

并且在if判断的时候由于P==NULL所以会直接退出函数,并且返回false。

时间复杂度

最好情况:在第一个位置插入新结点,时间复杂度为O(1)

最坏情况:在最后一个位置+1插入新结点,时间复杂度为O(n)

平均情况:假设在每一个位置插入新结点是等可能事件,时间复杂度为O(n)

按位序插入(不带头结点)

其实不带头结点的插入原理和带头结点是一样的,都是先找到i-1位置上的结点,再进行插入。但是不带头结点的单链表在第一个位置插入元素时需要特殊处理。

代码实现

bool ListInsert(LinkList &L, int i, ELemType e){
    if(i<1)
        return false;
    if(i==1){		// 在第一个位置插入结点
        LNode *s = (LNode *)malloc(sizeof(LNode));
        s->data = e;
        s->next = L;
        L = s;
        return true;
    }
    LNode *p;		// 指针p指向当前扫描到的结点
    int j = 1;		// 当前p指针指向的时第几个结点
    p = L;			// p指针指向第一个结点(注意:不是头结点)
    while(p!=NULL && j<i-1){	// 循环找到第i-1个结点
        p = p->next;
        j++;
    }
    if(p==NULL)		// 检测i合法性(是否超出链表长度)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;
    
    return true;	// 插入成功
}

代码分析(在第一个位置插入结点)

若 i = 1

  1. 判断I的合法性,i是否是小于1(起始值)
  2. 判断是否要在第一个位置插入节点
    • 使用malloc()函数创建一个新的结点
    • 将e的值存入新结点中
    • 将新结点的指针指向原先链表中的第一个结点(头指针L在不带头结点的链表中代表的就是第一个结点)
    • 由于在第一个位置插入了新的结点,所以应该将头指针L(指向第一个位置结点的指针)指向s
    • 插入成功,返回true

从上述我们可以看出,如果是不带头结点的单链表的话,在第一个位置插入元素,需要改变头指针L指向新插入的结点。而在带头结点的单链表中,头指针始终都是指向头结点的,只需要改变头结点的指针指向新插入的元素,其实和在任意位置插入一个新的结点的操作是相同的,并不用专门写一段代码来对在第一个位置插入元素进行特殊处理。

从这里我们就可以看出带头结点的链表的代码便捷性。

代码分析(i>1)

不带头结点的链表,在除了第一个位置外的其他位置插入新结点,跟带头结点的链表是一样的,维度有一个不同就是在j的赋值上:

  • 带头结点:是因为p指针刚开始指向的是头结点(头结点在链表中不算一个真正的结点,所以我们将其记为0),故将j赋值为0
  • 不带头结点:由于p指针直接就是指向链表中的第一个结点,故直接将J赋值为1

指定结点的后插操作

后插的操作和按位序操作的区别就是:前者是给你一个结点的地址,你直接在该结点后插入一个结点,而后者是给你要插入结点的位序,你在该位序插入结点。

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

bool InsertNextNode(LNode *p,ElemType e){
    if(P==NULL)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s==NULL)				// 内存分配失败
        return false;
    s->data = e;			// 将e存入新结点
    s->next = p->next;
    p->next = s;			// 将结点s连接到p之后
    
    return true;
}

由于我们已经知道了i-1的地址,所以不需要再进行遍历去找到i-1的结点的位置。所以可以直接创建一个新的结点,进行存入数据和指针重指的操作(插入新指针的操作同上,不再赘述)

注意:if(s==NULL)是用来判断内存分配是否成功,若失败则退出并返回false

由于链表中插入结点的代码是一样的,那么再按位序插入的代码中,我们可以将插入结点的代码删去,直接改为调用InsertNextNode()函数

  • 按位序插入和后插操作就是多了遍历和判断,后插操作只有插入结点的操作,所以我们可以在按位序插入的函数中直接调用后插操作,减少代码的冗余性

指定结点的前插操作

在单链表中我们知道,给定一个结点(p)后,我们可以知道这个结点后面的所有元素,但是无法知道前面的元素。如果给定一个地址,需要在该节点的前方插入结点的话,我们就需要头指针。通过头指针找到给定地址的结点(p)的前驱,在前驱后插入结点即可。

  • 如何找到前驱:前驱的指针应该是指向给定地址的结点(p),当指针的地址和给定地址的结点的地址相等时,则代表当前结点为前驱

但是这样子的操作,时间复杂度为O(n),那么还有没有另一种方法呢?

有的!

我们换一个思路,我们虽然不能将结点直接在前驱后插入,但是我们可以将数据对换。

  1. 在给定地址的结点(p)后面插入一个结点

    image-20220707224455638

  2. 这时候重点来了,我们将结点p的数据值和新插入的结点的值对调

虽然我们无法找到p结点的前驱结点,但是在逻辑上,我们实现了前插操作,并且时间复杂度为O(1)

代码实现

bool InsertPriorNode(LNode *p,ElemType e){
    if(p==NULL)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s==NULL)		// 内存分配失败
        return false;
    s->next = p->next;
    p->next = s;		// 新结点s连接到p之后
    s->data = p->data;	// 将p中元素赋值到s中
    p->data = e;		// p中元素覆盖为e
    
    return true;
}

因为我们进行的前插操作,也是要先将新结点插在p结点后,最后调换数据域,所以为了减少代码的冗余性,又由于我们知道插入新结点的操作已经可以由InsertNextNode()实现,那么我们调用该函数先创建一个新的结点,然后再将新的结点传入前插操作的函数。

image-20220707232235472

bool InsertPriorNode(LNode *p,LNode *s){
    if(p==NULL || s==NULL)
        return false;
    s->next = p->next;
    p->next = s			// 将结点s连接到p之后
    ElemType temp = p->data	// 交换数据域部分
    p->data = s->data;
    s->data = temp;
    
    return true;
}

上述的新代码中*p是要在哪个结点前插入,*s是要插入的新结点。

按位序删除(带头结点)

代码实现

bool ListDelete(LinkList &L,int i,ElemType &e){
    if(i<1)
        return false;
    LNode *p;
    int j = 0;
    p = L;
    while(p!=NULL && j<i-1){
        p = p->next;
        j++;
    }
    if(p==NULL)
        return false;
    if(p->next==NULL)
        return false;
    LNode *q = p->next;
    e = q->data;
    p->next = q->next;
    free(q);
    
    return true;
}

由于,在if(p==NULL)的代码前面的部分,和按位序插入的操作一样,就是找到要删除的结点的前驱结点,所以在这里不再赘述。

重点我们看到LNode *q = p->next;及之后的代码。

  1. 创建一个q指针,指向p的后继结点
    • p指针在经过上述的操作后,已经指向要删除的结点的前驱结点,也就是将q指针指向要删除的结点
  2. 使用e变量将要删除的值带回
    • 注意:这里e是引用型变量
  3. 将p的指针指向,q的后继
  4. 使用free()释放q指针

时间复杂度

最坏、平均时间复杂度:O(n)

最好时间复杂度:O(1)

指定结点的删除

经过了上面前插操作,我们可以知道指定结点的删除也有两种方法:

p结点为要删除的结点,q指针为指向要删除的结点的指针

  1. 方法一:传入头之间,遍历找到前驱结点
  2. 方法二:将q指针,指向p结点的后继结点。将p结点的后继结点的数据域复制给p结点,并且将p结点的next指针指向p结点的后继节点的next,再使用free(q)释放结点

代码实现

bool DeleteNode(LNode *p){
    if(p==NULL)
        return false;
    LNode *q = p->next;				// 令q指向*p的后继结点
    p->data = p->next->data;		// 和后继结点交换数据域
    p->next = q->next;				// 将*q结点从链中“断开”
    free(q);						// 释放后继结点的存储空间
}

上述这种方法的时间复杂度为:O(1)

但是如果当我们要删除的结点是最后一个时,就会出现问题。如果真的遇到这种情况,那么就只能使用头指针的方式遍历找到前继结点再删除,所以上述的代码其实是有一些bug的,但是可以这么写大概会扣个1分。

单链表的局限性

从上述可以看出单链表的局限性:无法逆向检索,有时候不太方便。

结束语

已同步更新至个人博客:https://www.hibugs.net/index.php/linklistinsdel

本人菜鸟一枚,仅分享学习笔记和经验。若有错误欢迎指出!共同学习、共同进步

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

单链表的插入和删除 的相关文章

  • 函数模板与类模板的具体化

    这两天在学习 C primer 这本书时 发现有关函数与类模板的相关内容多且繁琐 而且容易混淆 因此决定写一篇博客 将它们的概念与之间的区别梳理一下 一 函数模板 在 C primer 一书中 函数模板的具体化包括了三个部分 显式具体化 隐
  • CVPR 2021|一个绝妙的想法:在类别不平衡的数据上施展半监督学习

    点击上方 视学算法 选择加 星标 或 置顶 重磅干货 第一时间送达 作者丨kid丶 知乎 已授权 来源丨https zhuanlan zhihu com p 360067653 编辑丨极市平台 CReST A Class Rebalanci
  • elasticsearch 设置seed hosts

    es集群中配置的seed hosts 通过seed hosts provider提供 provider的数据来源有集群配置文件和第三方插件提供 集群配置文件又有两种方式 一种是直接在elasticsearch yml配置文件中通过disco

随机推荐

  • Cocos Creator Android 平台 Facebook 原生登录

    在做海外项目中 经常需要接入Facebook SDK 现将CocosCreator Android 平台 Facebook 登录的接入流程记录下来 以备有需要的朋友做参考 一 准备工作 1 首先在facebook 开发者平台 注册账号 创建
  • MAC系统 WORD 如何调整自动序号的间隔距离

    在MAC big Sur系统中 安装OFFICE 后 遇到WORD排版时 自动序号的间隔距离太远 研究一段时间发现可以用以下方式解决 1 问题界面 二 解决步骤 选中文字后 点击右键 选择 段落 点击 制表符 点击 全部清除 点击 确定 最
  • 最长公共上升子序列(LCIS)

    目录 一 前言 二 最长公共上升子序列 1 问题描述 2 基本思路 1 状态表示 2 状态计算 三 题例 1 上链接 2 基本思路 3 代码 1 python未优化版 2 python优化版 一 前言 对于学计算机的同学来说 学习算法是一件
  • 【DockerCE】使用docker配置和运行HertzBeat

    HertzBeat是一款免Agent的监控平台 拥有强大自定义监控能力 可以对应用服务 中间件 数据库 操作系统 云原生等进行监控 配置监控告警阈值 以及告警通知 邮件 微信 钉钉 飞书 关于这个软件的介绍 我这里就不做过多的介绍了 感兴趣
  • (二)代码好坏判定

    好坏只是笼统的判定 好代码 易扩展 易读 简单 易维护 判断代码的角度 灵活性 flexibility 可扩展性 extensibility 可维护性 maintainability 可读性 readability 可理解性 underst
  • Linux多进程编程

    fork系统调用 include
  • scrapy爬虫的搭建过程(实战篇)

    scrapy爬虫的搭建过程 实战篇 1 爬虫功能 以 http bbs fengniao com forum forum 125 1 lastpost html 为起始页 爬取前十页的信息 包括文章的标题 链接地址和图片地址 保存到mong
  • 超详细!基于Proteus的简易测频计实现(数字电路课程设计)

    本文阐述基于Proteus 7 8的简易测频计电路的实现 附具体电路的工程文件下载 工程文件下载链接 设计要求 闸门时间1S 10S可选 读数保持时间10秒 可选 四位数字显示 范围000 1 9999 Hz 能够自动进行下一次测量 设计方
  • 关于null的typeof和instanceof

    问题 alert typeof null object alert null instanceof Object false 答案 这是由Javascript规范规定的 Null和Object都是javascript中的数据类型 Null数
  • DC靶机系列:DC-3

    一 信息收集 查询本机ip及目标靶机ip 本机ip 192 168 56 104 利用nmap查询同网段存活的ip 或者使用arp scan l 靶机ip为 192 168 56 112 下一步收集靶机开放的端口信息 收集靶机开放端口 输入
  • Springboot解决跨域问题的配置

    由于自己是主后端开发 前端自己很少去配置 所以自己留一个配置SpringBoot配置跨域问题的代码在这里 注意一点 如果是在生产环境 应该根据实际需求设置allowedOrigins来限制允许访问的域名 而不是使用通配符 import or
  • nvidia 显卡硬件文档手册

    https github com NVIDIA open gpu doc
  • vue项目控制按钮是否显示

    import Vue from vue permission 用于控制是否显示按键 控制权限的指令 Vue directive has bind function el binding if Vue
  • 批量将markdown内本地图片转换为网络图片

    批量将markdown内本地图片转换为网络图片 在线地址 http 106 52 170 128 8003 需求 大部分支持markdown格式的网站 都不支持将markdown和其内置的图片同时上传到服务器 因此增大了小朋友们写文档的负担
  • 软件开发中几个常用功能的实现

    软件开发中几个常用功能的实现 出处 vchelp net责任编辑 leelee 04 8 12 10 01 作者 戚高 在进行软件开发过程中间 有很多小功能的实现 虽然这些东西你可以不用 但是如果应用仂将会是你的程序更具有专业性 一 设置程
  • Unity 3D 动画系统(Mecanim)

    Unity 3D 动画系统 Mecanim Mecanim 动画系统是 Unity 公司推出的全新动画系统 具有重定向 可融合等诸多新特性 可以帮助程序设计人员通过和美工人员的配合快速设计出角色动画 其主界面如下图所示 Unity 公司计划
  • 小写的bool和大写BOOL

    bool是标准C 中的布尔量 占一个字节大小内存 只有false或者true 具有跨平台特性 BOOL是MFC定义的宏 typedef int BOOL define FALSE 0 define TRUE 1 其实是个int类型 占四个字
  • 学习笔记1.STM32HAL库之点灯

    学习笔记1 STM32HAL库之点灯 前段时间学习了51单片机的相关知识 接下来进行32的学习 这里我使用的是野火的stm32f103v6核心板 进入正题 1 首先打开cubemx 进行相关配置 选择SYS 在debug中选择烧录方式 Se
  • codeforces 950 #469 div2 D A Leapfrog in the Array

    Problem codeforces com contest 950 problem D Reference Codeforces Round 469 Div 2 D A Leapfrog in the Array 思维 Meaning 开
  • 单链表的插入和删除

    前言 在上一篇文章 单链表的定义 中我们已经了解了单链表的含义和简单的实现 那么在这篇文章中 我们将要来讲解单链表的插入和删除操作 按位序插入 带头结点 我们在上篇文章中已经讲解过 如果想要在表L中的第i个位置上插入指定元素e 我们需要找到