【数据结构】C语言实现单链表的基本操作

单链表基本操作的实现

  • 导言
  • 一、查找操作
    • 1.1 按位查找
      • 1.1.1 按位查找的C语言实现
      • 1.1.2 按位查找的时间复杂度
    • 1.2 按值查找
      • 1.2.1 按值查找的C语言实现
      • 1.2.2 按值查找的时间复杂度
  • 二、插入操作
    • 2.1 后插操作
    • 2.2 前插操作
  • 三、删除操作
  • 结语

封面

导言

大家好,很高兴又和大家见面啦!!!
在上一篇中,我们详细介绍了单链表的两种创建方式——头插法与尾插法,相信大家现在对这两种方式都已经掌握了。今天咱们将继续介绍单链表的基本操作——查找、插入与删除。在开始今天的内容之前,我们先通过尾插法创建一个单链表,如下所示:

//定义单链表数据类型
typedef struct LNode{int data;//数据域struct LNode* next;//指针域
}LNode, * LinkList;//结点与单链表数据类型
//初始化单链表
bool InitList(LinkList* L)//二级指针接收头指针的地址
{*L = (LNode*)calloc(1, sizeof(LNode));//为头结点申请空间if (!(*L)){return false;}(*L)->next = NULL;//将头结点定义域初始化为空指针,防止出现野指针return true;
}
//尾插法创建单链表
LinkList List_TailInsert(LinkList* L)
{assert(*L);//通过assert断言确保链表头指针不是空指针LNode* r = *L;//指向新结点的指针LNode* l = *L;//指向尾结点的指针int x = 0;//存储数据域元素的变量while (scanf("%d", &x) == 1)//通过scanf获取数据域存放的数据,这里采用多组输入简化代码{r = (LNode*)calloc(1, sizeof(LNode));//为新结点申请空间assert(r);//通过assert断言确保新结点成功申请了空间r->data = x;//将数据信息存放进新结点的数据域中r->next = l->next;//将尾节点指针域存放的地址信息存放进新结点的指针域中l->next = r;//将尾节点的指针域指向新节点的起始地址l = r;//新结点变成尾结点}return (*L);//返回链表
}
//打印链表
void Print_LinkList(LinkList L)
{printf("\n打印链表数据域的各个元素:>");LNode* p = L;//指向前一个节点的指针LNode* q = L;//指向后一个节点的指针while (q->next)//当q的指针域指向空指针时,表示q此时为表尾结点,不需要继续打印,直接退出循环{q = p->next;//将指针p的指针域存储的下一个节点的地址信息赋值给qprintf("%d ", q->data);//此时指针q指向的需要打印的节点起始地址p = q;//将指针p指向已经打印过的节点}printf("\n");
}
int main()
{LinkList L;//指向单链表的指针L——头指针//初始化单链表if (InitList(&L)){L = List_TailInsert(&L);//创建单链表——尾插法Print_LinkList(L);//打印单链表}else{printf("初始化失败\n");//对初始化失败进行错误提示}return 0;
}

此时咱们就成功创建了一个顺序存放的单链表,如下图所示:
尾插法创建单链表
现在有了这个单链表后,我们就可以对其进行查找、插入与删除等操作了。那这些操作又应该如何实现呢?下面我们就来一一介绍;

一、查找操作

单链表的查找操作同样可以分为按位查找与按值查找,下面我们就来看一下这两种查找方式有什么不同。

1.1 按位查找

单链表是一个非随机存取的存储结构,因此我们想要找到位序i上的结点,只能从表头元素开始依次查找,所以在对单链表进行按位查找时会存在几种情况:

  • 需要查找的位序不合理,此时我们不能进行查找,需要给使用者一定的反馈;
  • 找到了对应位序的结点,此时我们需要将该结点返回给函数;
  • 没有找到对应位序的结点,当我们要找的结点为空指针时,说明已经将链表全部查找完,所以我们需要返回空指针;

对于这些情况,我们在编写查找功能时,就需要将这些可能发生的情况转换为代码,下面我们就来尝试一下;

1.1.1 按位查找的C语言实现

在通过C语言实现按位查找前,我们需要将自己的编写思路梳理一下:

  1. 我们在查找时需要判断该结点的位序与目标位序是否相等:
    • 相等则找到了,就不需要继续查找;
    • 小于目标位序则继续查找;
  2. 我们在查找时还需要判断查找的结点是否为空指针:
    • 不为空指针,表示还未查找完,可以继续查找;
    • 为空指针,表示已经查找完,需不要继续查找;

有了思路,我们就可以开始编写代码了,如下所示:

//按位查找
LNode* GetElem(LinkList L, int i)
{if (i < 1)return NULL;//当位序<1时,此时的位序不合理,返回空指针LNode* p = L->next;//寻找目标结点的指针,从表头结点开始查找int j = 1;//寻找的结点位序while (p && j < i)//当位序j与i相等时表示找到了对应的结点,退出循环;//当p为空指针时,表示查找完成,没有找到对应的结点,退出循环{p = p->next;//指针p指向下一个结点j++;//查找下一个位序}return p;//查找结束后返回指针p
}

这个代码能否实现咱们的按位查找功能呢?我们测试一下:
按位查找
可以看到我们很好的通过C语言实现了单链表的按位查找。

1.1.2 按位查找的时间复杂度

我们在进行按位查找时,查找的过程会有三种情况:

  • 最好情况,要查找的结点为表头结点,此时按位查找的时间复杂度为O(1);
  • 最坏情况,要查找的结点为表尾结点,此时按位查找的时间复杂度为O(n);
  • 平均情况,要查找的每个结点的概率是相同的,因此,我需要查找的次数与元素对应的位序是成正比的,所以此时按位查找的时间复杂度为O(n);

1.2 按值查找

单链表的按值查找与按位查找的逻辑相同,都是从表头结点开始查找,只不过在查找的内容上会有区别,按位查找查找的是位序,而按值查找查找的是数据域内存储的元素。在查找的过程中也会有以下几种情况:

  • 找到了对应的值,此时我们需要将该值所在的结点返回给函数;
  • 没有找到对应的值,此时我们需要给函数返回一个空指针;

对于按值查找而言,此时我们是不需要对值的合理性进行判断的,因此我们只需要完成从表头开始查找的工作就行。

1.2.1 按值查找的C语言实现

为了更好的实现按值查找,我来梳理一下编写思路:

  1. 我们在查找的过程中需要判断查找结点的数据域与目标值是否相等:
    • 不相等,表示还未找到对应的结点,需要继续查找;
    • 相等,表示已经找到了对应的结点,可以结束查找;
  2. 我们在查找的过程中还需要判断查找的结点是否为空指针:
    • 结点为空指针,表示已经查找完所有结点,此时不需要继续查找;
    • 结点不为空指针,表示还未查找完,需要继续查找;;

有了具体的思路,我们就可以开始编写代码了:

//按值查找
LNode* LocateElem(LinkList L, int e)
{LNode* p = L->next;//寻找目标结点的指针,从表头结点开始寻找while (p && p->data != e)//当结点数据域存放的值与目标值相等时,表示找到了对应结点,退出循环//当p为空指针时表示查找完全部结点,没有找到对应结点,退出循环{p = p->next;//指针p指向下一个节点}return p;//完成查找后返回指针p
}

下面我们来测试一下此时能否完成按值查找的功能:
按值查找
从测试结果中我们可以看到,此时很好的完成了按值查找的功能。

1.2.2 按值查找的时间复杂度

我们在进行按值查找时,查找的过程会有三种情况:

  • 最好情况,要查找的结点为表头结点,此时按位查找的时间复杂度为O(1);
  • 最坏情况,要查找的结点为表尾结点,此时按位查找的时间复杂度为O(n);
  • 平均情况,要查找的每个结点的概率是相同的,因此,我需要查找的次数与元素对应的位序是成正比的,所以此时按值查找的时间复杂度为O(n);

二、插入操作

因为单链表的各个元素是离散的分布在内存中的,因此我们想要插入新的结点时,就不需要像顺序表那样移动大量的元素,但是,我们想要插入新结点时需要先找到插入位序的前一个结点,才能将新的结点插入到单链表中,如下图所示:
插入操作
由于单链表的特性是只能从前往后查找,因此要想实现单链表的插入操作只能够借助前一个结点。

2.1 后插操作

通过上图这种方式实现的插入操作我们将其称之为后插操作。

不难发现,在带头结点的单链表中,不管是头插法创建的单链表,还是后插法创建的单链表,它们插入新结点的逻辑都是通过后插操作实现的,也就是说对于后插法的插入过程实际上就是我们前面提到的过程:

//插入操作
New_LNode->next = Ahead_LNode->next;//将前一个位序的结点的指针域指向的内容存放入新结点的指针域中
Ahead_LNode->next = New_LNode;//将新结点的位置信息存放入前一个结点的指针域中

只不过在进行后插前我们需要先通过按位查找找到前一个结点的位序,然后再进行插入操作,因此后插操作的完整流程应该是:

//插入过程
Ahead_LNode = GetElem(L, i - 1);//通过调用按位查找函数来找到位序为i-1的节点
New_LNode->next = Ahead_LNode->next;//将前一个位序的结点的指针域指向的内容存放入新结点的指针域中
Ahead_LNode->next = New_LNode;//将新结点的位置信息存放入前一个结点的指针域中

将后插操作封装为一个函数的话,我们可以编写如下代码:

//后插操作
bool InsertNextNode(LinkList L,int i, ElemType e)
{LNode* p = GetElem(L, i - 1);//通过按位查找找到前驱结点pif (!p)return false;//如果前驱结点为空指针,则返回falseLNode* s = (LNode*)calloc(1, sizeof(LNode));//为新结点申请空间assert(s);//如果空间申请失败,则报错s->data = e;//将要插入的元素存放入新结点的数据域中s->next = p->next;//将新结点的指针域指向前驱结点的指针域指向的空间p->next = s;//将新结点的地址存放入前驱结点的指针域中return true;//完成插入操作则返回true
}

因此这里调用了按位查找的函数,因此对于后插操作来说,此时的时间复杂度为O(n);

下面我们思考一个问题,我们能不能在一个结点的前面进行插入操作呢?

2.2 前插操作

在单链表中,如果我们想实现在结点的前面插入一个新结点,这是不现实的,根据单链表的特性来看,我们只能够通过后插的方式来插入新结点,这样才能保证各个结点能够通过指针域的指向相互联系起来,但是我们可以换一种角度来模拟实现前插操作,如下所示:

前插操作
从图中可以看到,我们在执行前插操作的步骤是:

  1. 通过后插操作对位序i的结点后插入一个新结点,只不过插入的新结点的数据域未存放元素;
  2. 之后再将位序i结点的数据域存放的元素放入新结点中;
  3. 最后再将新的元素放入位序i的结点的数据域中;

看似我们现在完成的是完成了前插操作,实质上完成的依旧是一次后插操作。下面我们通过C语言来描述前插操作:

//前插操作
bool InsertPriorNode( LNode* p, ElemType e)//需要指向前插操作的指针p与需要插入的数据e
{if (!p)return false;//如果需要指向前插操作的指针p为空指针,则无法执行前插操作LNode* s = (LNode*)calloc(1, sizeof(LNode));//插入的新结点assert(s);//如果新结点申请空间失败,则报错s->data = p->data;//将p结点的数据域中的数据放置到新结点中s->next = p->next;//将p结点的指针域中的数据放置到新结点中p->next = s;//将新结点的位置信息放置到p结点的指针域中p->data = e;//将需要插入的数据信息放入到p结点的数据域中return true;//完成插入操作后返回true
}

前插操作因为不需要进行搜索结点p的前驱结点,因此在已知结点p的情况下,前插操作的时间复杂度为O(1);

通过前插操作与后插操作的对比我们可以看到,在已知需要执行插入操作的节点p时,前插操作通过进行数据的移动这个操作就规避了需要查找前驱结点的步骤,大幅度提高了算法的效率。

三、删除操作

在单链表中,如果我们需要删除一个元素,那我们需要执行的逻辑应该是:

  1. 找到需要删除元素的前驱结点;
  2. 修改前驱结点的指针域指向的对象;
  3. 释放需要删除元素结点的内存空间;

通过删除操作的逻辑,不难想象,因为需要通过遍历整个链表来寻找需要删除的结点的前驱结点,因此删除操作的时间复杂度为O(n)。将这个逻辑转换成C语言,则如下所示:

//删除操作
bool ListDelete(LinkList* L, int i, ElemType* e)
{if (i < 1)return false;//当位序不合法时,返回falseLNode* p = GetElem(*L, i - 1);//通过按位查找找到前驱结点assert(p);//当未找到前驱结点时,将会报错if (!(p->next))return false;//如果前驱结点p为表尾结点,则无法删除位序为i的结点,因此返回falseLNode* q = p->next;//当位序i的结点合法且存在时,将该结点的信息存放进指针q中*e = q->data;//将需要删除的元素存放进变量e中p->next = q->next;//修改前驱结点p的指针域指向的对象free(q);//释放删除结点的内存空间return true;//完成删除后返回true
}

下面我们来测试一下删除操作与插入操作,将单链表中位序为3的结点删除后在位序4处插入新的结点,如下所示:
插入与删除操作
可以看到,此时咱们已经实现了单链表的插入与删除操作。

结语

咱们今天的内容到这里就全部结束了,今天咱们详细介绍了单链表的查找、插入、删除操作的实现,希望这篇内容能够帮助大家更好的理解这些基本操作。

在下一篇内容中我们会继续介绍链表的第二种形式——双链表,大家记得关注哦!最后感谢各位的翻阅,咱们下一篇见!!!

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

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

相关文章

C++ 之LeetCode刷题记录(三)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 开始cpp刷题之旅&#xff0c;多学多练&#xff0c;尽力而为。 先易后难&#xff0c;先刷简单的。 13、罗马数字转整数 罗马数字包含以下七种字符: I&#xff0c…

ClickHouse基础知识(二):ClickHouse 安装教程

1. 准备工作 1.1 确定防火墙处于关闭状态 1.2 CentOS 取消打开文件数限制 &#xff08;1&#xff09;在 hadoop101 的 /etc/security/limits.conf 文件的末尾加入以下内容 sudo vim /etc/security/limits.conf&#xff08;2&#xff09;在 hadoop101 的/etc/security/limits.…

Rocky9 1.28安装kubernetes

1.环境准备 二进制安装比较复杂&#xff0c;但是也比较稳定&#xff0c;适用于线上环境使用。   本笔记参考自&#xff1a;https://github.com/cby-chen/Kubernetes &#xff0c;针对文中内容&#xff0c;有部分镜像无法拉取等&#xff0c;还有一部分有点小问题&#xff0c;…

SQL小技巧5:数据去重的N种方法,总有一种你想不到!

在平时工作中&#xff0c;使用SQL语句进行数据去重的场景非常多。 今天主要分享几种数据去重的SQL写法。 假如有一张student表&#xff0c;结构如下&#xff1a; create table student( id int, name varchar(50), age int, address varchar(100)); 表中的数据…

【解决方案】智能语音模块,东胜物联远场语音解决方案让控制更简单,应用于智能家居等场景

现在的天气真是冷得不想多动一下&#xff0c;又想打开取暖器&#xff1f;有了它&#xff0c;用声音就能遥控&#xff0c;今天我们就来聊聊智能语音模块。 技术概述 远场语音技术&#xff0c;采用了麦克风阵列、信号处理技术以及先进的语音识别引擎&#xff0c;使得设备能够在距…

C++ DAY2作业

1.课堂struct练习&#xff0c;用class&#xff1b; #include <iostream>using namespace std;class Stu { private:int age;char sex;int high; public:double score;void set_values(int a,char b,int c,double d);int get_age();char get_sex();int get_high(); }; vo…

Java开发框架和中间件面试题(8)

目录 82.Mybatis一级缓存&#xff0c;二级缓存&#xff1f; 83.Mybatis如何防止SQL注入&#xff1f; 84.mybatis中resultType和resultMap有什么区别&#xff1f; 85.如何在SpringBoot中禁用Actuator断点安全性&#xff1f; 86.什么是SpringBoot&#xff1f;SpringBoot有哪些…

go 使用 - sync.Metux

[TOC]&#xff08;sync.metux 使用&#xff09; 简介 简述使用metux使用的方法&#xff0c; 使用的注意点&#xff0c; 以及使用情况使用方法 提供的方法 Lock() 方法用于获取锁 Unlock() 方法用于释放锁 TryLock()方法尝试获取锁 对共享资源进行加锁&#xff0c; 例 &#…

数据库(Database)基础知识

什么是数据库 数据库是按照数据结构来组织、存储和管理数据的仓库&#xff0c;用户可以通过数据库管理系统对存储的数据进行增删改查操作。 数据库实际上是一个文件集合&#xff0c;本质就是一个文件系统&#xff0c;以文件的方式&#xff0c;将数据保存在电脑上。 什么是数据…

阿里云 ACK 云上大规模 Kubernetes 集群高可靠性保障实战

作者&#xff1a;贤维 马建波 古九 五花 刘佳旭 引言 2023 年 7 月&#xff0c;阿里云容器服务 ACK 成为首批通过中国信通院“云服务稳定运行能力-容器集群稳定性”评估的产品&#xff0c; 并荣获“先进级”认证。随着 ACK 在生产环境中的采用率越来越高&#xff0c;稳定性保…

leaflet学习笔记-地图图层控制(二)

图层介绍 Leaflet的地图图层控件可控制两类图层&#xff1a;一类是底图图层&#xff08;Base Layers&#xff09;&#xff0c;一次只能选择一个图层作为地图的背景图层&#xff0c;即底图图层&#xff0c;在地图图层控件中用单选按钮控制&#xff1b;另一类是覆盖图层&#xff…

大数据与人工智能|信息技术产业架构、行业发展与前沿技术(第2节)

内容链接&#xff1a;信息技术产业架构、行业发展与前沿技术&#xff08;大数据与人工智能系列课程 第2节&#xff09; 声明&#xff1a;学习使用&#xff0c;侵权必删&#xff01; 主要内容&#xff1a;1. 从算盘到量子计算机&#xff0c;介绍了半导体行业的发展历程和技术原…

吓一跳!哈佛大学最受欢迎的课程,我不敢相信我的眼睛!

文件销毁、硬盘销毁、数据销毁以及物料销毁是四个相互关联且在企业或组织运营中至关重要的环节。这四个方面都涉及信息安全和保密管理的核心内容&#xff0c;关乎企业的商业秘密、客户的个人信息以及各种敏感数据的保护。随着信息化和数字化的快速发展&#xff0c;如何安全、有…

thinkcmf 文件包含 x1.6.0-x2.2.3 已亲自复现

thinkcmf 文件包含 x1.6.0-x2.2.3 CVE-2019-16278 已亲自复现 漏洞名称漏洞描述影响版本 漏洞复现环境搭建漏洞利用 修复建议总结 漏洞名称 漏洞描述 ThinkCMF是一款基于PHPMYSQL开发的中文内容管理框架&#xff0c;底层采用ThinkPHP3.2.3构建。ThinkCMF提出灵活的应用机制&a…

微信小程序登录(生成token,token校验)——后端

写在前面&#xff1a;如果想自己开发微信小程序&#xff0c;需要先到微信小程序官方平台注册账号&#xff0c;地址为&#xff1a;https://mp.weixin.qq.com/wxopen/waregister?actionstep1. 登录流程 其中&#xff0c;开发者服务器就是我们的后端服务器&#xff0c;微信接口服…

传感器基础:传感器使用与编程使用(三)

目录 常用传感器讲解九--雨滴传感器具体讲解电路连接代码实现 常用传感器讲解十--光传感器根据亮度安排灯具体讲解电路连接代码实现 常用传感器讲解七--light cup&#xff08;KY-008&#xff09;具体讲解电路连接代码实现 常用传感器讲解十二--倾斜开关传感器&#xff08;KY-02…

Java版企业电子招标采购系统源码——鸿鹄电子招投标系统的技术特点

在数字化时代&#xff0c;采购管理也正经历着前所未有的变革。全过程数字化采购管理成为了企业追求高效、透明和规范的关键。该系统通过Spring Cloud、Spring Boot2、Mybatis等先进技术&#xff0c;打造了从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通过…

旧衣回收小程序搭建,稳占回收市场

近几年我国大众的消费水平不断提升&#xff0c;闲置物品也相应增加了不少&#xff0c;尤其是闲置衣服&#xff0c;为了减少资源浪费&#xff0c;旧衣服回收回收行业受到了大众的关注。 目前我国旧衣服回收行业的市场规模达到了300多亿元&#xff0c;旧衣回收行业的商业价值非常…

Linux 查看系统类型和版本(内核版本 | 发行版本)

Linux 查看系统类型和版本 首先普及下linux系统的版本内容1. 查看linux系统内核版本2. 查看linux系统发行版本 首先普及下linux系统的版本内容 内核版本和发行版本区别 内核版本就是指 Linux 中最基层的代码&#xff0c;版本号如 Linux version 3.10.0-327.22.2.el7.x86_64发行…

JavaSE50题:26. (数组练习题)使奇数位于偶数之前

概述 调整数组顺序使得奇数位于偶数之前&#xff0c;调整之后&#xff0c;不关心大小顺序。 如数组&#xff1a;{1,2,3,4,5,6} 调整后可能是&#xff1a;{1&#xff0c;5&#xff0c;3&#xff0c;4&#xff0c;2&#xff0c;6} 方法 定义 left 和 right&#xff0c;二者分别…