初级数据结构(二)——链表
文中代码源文件已上传:数据结构源码 <-上一篇 初级数据结构(一)——顺序表 | NULL 下一篇-> |
1、链表特征
与顺序表数据连续存放不同,链表中每个数据是分开存放的,而且存放的位置尤其零散,毫无规则可言。对于零散的数据而言,我们当然可以通过某种方式统一存储每一个数据存放的地址,如下图:
但这个 sheet 无论怎么看都是一个数组,而 ptr_data 是个指针,也就是说,以上数据结构仍然是一种顺序表,只不过表中的数据从具体的值改为指针。它仍然没有脱离顺序表的范畴。自然顺序表的优势及劣势它也照单全收。
顺序表的劣势在于,开辟空间并非随需开辟,释放空间也显得不那么灵活。如果顺序表做到每次增加数据便拓展空间,删除数据便回收空间,基于 realloc 可能异地开辟的特点,搬运数据的时间复杂度为 O(N) 。如果顺序表的长度是几千万乃至几亿,每添加或者删除一个数据,其延迟是难以忽略的。因此上篇中的顺序表每次开辟空间均是成批开辟。但这种方式也必然造成空间的浪费。
如果有一种储存方式可以解决上述问题,做到每一个数据的空间都按需开辟,且按需释放,那么在最极端的情况下,它甚至可以节省近一半存储空间。在此思想上,数组的特性完全不符合该要求,首先需要抛弃的便是数组。但上图倘若没了数组,每一个数据节点的位置便无从知晓。于是有了链表的概念。
链表可以将下一个节点的位置储存在上一个节点中,此外还可以将上一个节点的位置储存在下一个节点中。
如上图。链表还需要一个头指针指向第一个节点。上述结构称为单链表。此外链表还有以下常见结构(环链、双向链)。
比如这篇文章最顶端“上一篇”、“下一篇”的导航链接就类似双向链。
2、链表创建
2.1、文件结构
本文以最基本的单链为例,因为其他变形比单链的复杂程度高不了多少,有机会再作补充。仍是先从文件结构开始,分别建立以下三个文件,
lnkTab.h :用于创建结构体类型及声明函数;
lnkFunction.c :用于创建链表各种操作功能的函数;
main.c :仅创建 main 函数,用作测试。
2.2、前期工作
在 lnkTab.h 中先码入以下内容:
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>//自定义数据类型和打印类型,方便后续更改储存数据的类型
#define PRINT_FORMAT "%d"
typedef int DATATYPE;//创建链表节点的结构体类型
typedef struct LinkedListType
{DATATYPE data;struct LinkedListType* next;
}LinkedListType;//---------------------函数声明---------------------
//打印链表
extern void DataPrint(LinkedListType*);//创建节点
extern LinkedListType* NodeCreate(const DATATYPE, const LinkedListType*);//销毁链表
extern void DataDestory(LinkedListType**);
在 lnkFunction.c 中包含 lnkTab.h 并分别创建一个打印链表和销毁链表的函数:
#include "lnkTab.h"//打印链表
void DataPrint(LinkedListType* ptr_headNode)
{//创建节点指针LinkedListType* currentNode = ptr_headNode;//循环打印while (currentNode){//打印printf(PRINT_FORMAT" -> ", currentNode->data);//移动节点指针currentNode = currentNode->next;}printf("NULL\n");
}//创建节点
LinkedListType* NodeCreate(const DATATYPE data, const LinkedListType* next)
{LinkedListType* node = (LinkedListType*)malloc(sizeof(LinkedListType));//加入判断防止空间开辟失败if (node == NULL){perror("Malloc Fail");return NULL;}//节点赋值node->data = data;node->next = next;return node;
}//销毁链表
void DataDestory(LinkedListType** ptr2_headNode)
{//空链表判断if (!ptr2_headNode) return;//创建节点指针LinkedListType* currentNode = *ptr2_headNode;//循环逐个销毁节点while (currentNode){LinkedListType* nextNode = currentNode->next;free(currentNode);currentNode = nextNode;}//头指针置空*ptr2_headNode = NULL;
}
最后在 main.c 中包含 lnkTab.h,并创建一个链表头指针:
#include "lnkTab.h"int main()
{LinkedListType* ptr_headNode = NULL;return 0;
}
至此,前期工作准备完毕。
3、链表操作
3.1、增
同顺序表一样,链表除了指定位置插入数据之外,最好也定义下头部插入数据及尾部插入数据的函数。因此先在 lnkTab.h 中加入以下函数声明:
//指定位置插入数据
extern void DataPush(LinkedListType**, const int, const DATATYPE);
//头部插入数据
extern void DataPushHead(LinkedListType**, const DATATYPE);
//尾部插入数据
extern void DataPushTail(LinkedListType**, const DATATYPE);
之后先创建 DataPush 函数。在此之前把函数的流程图画出,以助于思考。画流程图的过程中能认识到空链表跟非空链表要分开处理,除了头插,其他位置插入的逻辑是相同的:
对照上图,照着在 lnkFunction.c 里写出如下代码:
void DataPush(LinkedListType** ptr2_headNode, const int pos, const DATATYPE data)
{//有效性检查if (!ptr2_headNode) return;LinkedListType* currentNode = *ptr2_headNode;//如果插入位置小于等于0或者没有节点if (pos <= 0 || !currentNode){//创建节点,将头指针的值赋予该节点的指向,并将头指针指向该节点LinkedListType* node = NodeCreate(data, currentNode);*ptr2_headNode = node;return;}//遍历节点至插入位置前一节点for (int i = 0; i < pos - 1; i++){//若遇到最后一个节点则停止遍历,当前节点指针指向最后一个节点if (currentNode->next)currentNode = currentNode->next;elsebreak;}//创建节点,将当前节点的指向值赋予创建的该节点的指向,并将当前节点指向创建的节点LinkedListType* node = NodeCreate(data, currentNode->next);currentNode->next = node;
}
至于头插尾插数据,只不过是上述函数 pos 位置的区别。因此:
//pos = 0 便是头插
void DataPushHead(LinkedListType** ptr2_headNode, const DATATYPE data)
{DataPush(ptr2_headNode, 0, data);
}//由于 DataPush 函数在 pos 大于节点数时自动进行尾插
//因此 pos = INT_MAX 在任意情况下都是尾插
void DataPushTail(LinkedListType** ptr2_headNode, const DATATYPE data)
{DataPush(ptr2_headNode, INT_MAX, data);
}
验证环节。在 main 函数中加入如下代码试运行:
DataPush(&ptr_headNode, 10, 1234);DataPrint(ptr_headNode); // 1234 NULLDataPushTail(&ptr_headNode, 1);DataPrint(ptr_headNode); // 1234 1 NULLDataPushHead(&ptr_headNode, 2);DataPrint(ptr_headNode); // 2 1234 1 NULLDataPushTail(&ptr_headNode, 3);DataPrint(ptr_headNode); // 2 1234 1 3 NULLDataPush(&ptr_headNode, 1, 14542);DataPrint(ptr_headNode); // 2 14542 1234 1 3 NULLDataPushHead(&ptr_headNode, 4);DataPrint(ptr_headNode); // 4 2 14542 1234 1 3 NULLDataPushTail(&ptr_headNode, 114514);DataPrint(ptr_headNode); // 4 2 14542 1234 1 3 114514 NULLDataPush(&ptr_headNode, 10, 1442);DataPrint(ptr_headNode); // 4 2 14542 1234 1 3 114514 1442 NULL
结果与预期无误。至此插入功能便已完成。
3.2、删
第一步当然是在 lnkTab.h 中加入函数声明:
//指定位置删除数据
extern void DataPop(LinkedListType**, const int, const int);
//头部删除数据
extern void DataPopHead(LinkedListType**);
//尾部删除数据
extern void DataPopTail(LinkedListType**);
完毕后,二话不说,先上流程图。这里同样要注意区分空链和其他位置删除。不过删除节点还得将头删及其他位置删除分开判定。
而且这里要注意的是,删除与插入不同,万一 pos 值传错导致小于 0 或者大于链表长度,便不能如上图简单粗暴地执行头删尾删。创建函数的时候多加一个参数来判断是否要在这种情况下头删或者尾删最好不过了。为了直观,还是在 lnkTab.h 头文件中加个枚举类型:
//定义删除节点的暴力模式和非暴力模式
enum Deletion { UNFORCED, FORCED };
然后,在 lnkFunction.c 里码下这些:
void DataPop(LinkedListType** ptr2_headNode, const int pos, const int deletionMode)
{//如果没有节点则直接退出if (!ptr2_headNode || !*ptr2_headNode) return;LinkedListType* currentNode = *ptr2_headNode;//如果插入位置小于等于0则头删,前提是在非暴力模式下if (pos == 0 || (pos < 0 && deletionMode)){*ptr2_headNode = (*ptr2_headNode)->next;free(currentNode);return;}//遍历节点至需要删除的节点前一节点int i;for (i = 1; i <= pos - 1; i++){//若遇到倒数第二个节点则停止遍历,当前节点指针指向倒数第二个节点if (currentNode->next->next)currentNode = currentNode->next;elsebreak;}//模式不暴力的话,pos超出表长度就直接退出了if (i < pos - 1 && !deletionMode) return;//删!LinkedListType* freeNode = currentNode->next;currentNode->next = currentNode->next->next;free(freeNode);
}
然后头删 pos 为 0 ,尾删 pos = INT_MAX 且删除模式为 FORCED 。就没必要再赘述了:
//删除头部节点
void DataPopHead(LinkedListType** ptr2_headNode)
{DataPop(ptr2_headNode, 0, UNFORCED);
}//删除尾部节点
void DataPopTail(LinkedListType** ptr2_headNode)
{DataPop(ptr2_headNode, INT_MAX, FORCED);
}
题外话,这里再提另一种更安全的方式, 指定位置删除的 pos 如果超出链表范围直接报错,然后头删尾删单独写:
//删除头部节点
void DataPopHead(LinkedListType** ptr2_headNode)
{if (!ptr2_headNode) return;LinkedListType* currentNode = *ptr2_headNode;//如果没有节点则直接退出if (currentNode == NULL) return;//将头指针置为第一个节点的指向,并释放第一个节点*ptr2_headNode = currentNode->next;free(currentNode);
}//删除尾部节点
void DataPopTail(LinkedListType** ptr2_headNode)
{if (!ptr2_headNode) return;LinkedListType* currentNode = *ptr2_headNode;//如果没有节点则直接退出if (currentNode == NULL) return;//如果只有一个节点则采用头删else if (currentNode->next == NULL){DataPopHead(ptr2_headNode);return;}//遍历至倒数第二个节点,释放最后一个节点,并将倒数第二个节点指向置空else{while (currentNode->next->next){currentNode = currentNode->next;}free(currentNode->next);currentNode->next = NULL;}
}
回归正题,之后是熟悉的测试阶段。 main 函数中添加:
printf("\n----------DataPopTest----------\n");DataPop(&ptr_headNode, 0, UNFORCED);DataPrint(ptr_headNode); // 2 14542 1234 1 3 114514 1442 NULLDataPop(&ptr_headNode, 2, UNFORCED);DataPrint(ptr_headNode); // 2 14542 1 3 114514 1442 NULLDataPop(&ptr_headNode, 1000, UNFORCED);DataPrint(ptr_headNode); // 2 14542 1 3 114514 1442 NULLDataPop(&ptr_headNode, 1000, FORCED);DataPrint(ptr_headNode); // 2 14542 1 3 114514 NULLDataPop(&ptr_headNode, 0, UNFORCED);DataPrint(ptr_headNode); // 14542 1 3 114514 NULL DataPop(&ptr_headNode, 0, UNFORCED);DataPrint(ptr_headNode); // 1 3 114514 NULL DataPop(&ptr_headNode, 0, UNFORCED);DataPrint(ptr_headNode); // 3 114514 NULL DataPop(&ptr_headNode, 0, UNFORCED);DataPrint(ptr_headNode); // 114514 NULL DataPop(&ptr_headNode, 0, UNFORCED);DataPrint(ptr_headNode); // NULL DataPop(&ptr_headNode, 0, UNFORCED);DataPrint(ptr_headNode); // NULL
完事!
3.3、改和查
这两个功能简直不要太简单。查的逻辑跟打印逻辑一致,至于改,用查的功能返回节点地址,直接改 data 完事。
lnkTab.h :
//查找节点
extern LinkedListType* DataSearch(const LinkedListType*, const DATATYPE);
//修改节点
extern void DataModify(const LinkedListType*, DATATYPE, DATATYPE);
lnkFunction.c:
//查找节点
LinkedListType* DataSearch(const LinkedListType* ptr_headNode, const DATATYPE data)
{LinkedListType* currentNode = ptr_headNode;while (currentNode){if (currentNode->data == data) break;currentNode = currentNode->next;}return currentNode;
}//修改节点
void DataModify(const LinkedListType* ptr_headNode, DATATYPE target, DATATYPE replace)
{LinkedListType* node = DataSearch(ptr_headNode, target);if (!node) return;node->data = replace;
}
main.c 测试:
printf("\n----------DataSearchModifyTest----------\n");for (int i = 20; i >= 10; i--){DataPushHead(&ptr_headNode, i);}DataPrint(ptr_headNode); // 10 12 13 14 15 16 17 18 19 20 NULLDataModify(ptr_headNode, 3, 23457); DataPrint(ptr_headNode); // 10 12 13 14 15 16 17 18 19 20 NULLDataModify(ptr_headNode, 13, 23457);DataPrint(ptr_headNode); // 10 12 23457 14 15 16 17 18 19 20 NULL
搞定!准备下一篇。撒花!
4、作点补充
一开始就提到双向链表和环链。虽然结构复杂了,但是这两种结构比单链简单太多了。可以说学懂了单链,那么其他结构的链表跟玩似的。
以另一个常规的极端(不考虑中途环状),环状双向链举例。在数据操作的代码上,这个结构可比单链节省了寻找尾节点以及记录上一个节点指针的麻烦。
双向链如果一直找上一个( prev )节点,它也只不过是另一种单链,视作 next 结构的倒序。最直观的在于链表倒置:
while(currentNode)
{DATATYPE* nextNode = currentNode->next;currentNode->next = currentNode->prev;currentNode->prev = temp;currentNode = nextNode;
}
一个循环搞定整个链表导致,而如果是单链,需要考虑遍历分别头插之类的,代码量直线上升。至于环链,尾节点的 next 指向头节点。事实上这种结构提供了寻找尾节点的便利( currentNode->prev )。那是更便利了,只不过不支持迭代,总有牺牲嘛。
构建节点的结构体特别自由,链表也就那么几种大体结构,细节没有规定。总之,只要实现节点内记录其他节点的信息便属于链表。奥力给!
相关文章:

初级数据结构(二)——链表
文中代码源文件已上传:数据结构源码 <-上一篇 初级数据结构(一)——顺序表 | NULL 下一篇-> 1、链表特征 与顺序表数据连续存放不同,链表中每个数据是分开存放的,而且存放的位置尤其零散&#…...

Kubernetes架构及核心部件
文章目录 1、Kubernetes集群概述1.1、概述1.2、通过声明式API即可 2、Kubernetes 集群架构2.1、Master 组件2.1.1、API Server2.1.2、集群状态存储2.1.3、控制器管理器2.1.4、调度器 2.2、Worker Node 组件2.2.1、kubelet2.2.2、容器运行时环境2.2.3、kube-proxy 2.3、图解架构…...
RAW和YUV的区别
RAW是指未经过任何压缩或处理的原始图像数据。在摄像头中,原始图像数据可以是来自图像传感器的未经处理的像素值。这些原始数据通常以一种Bayer模式的形式存在,其中每个像素仅包含一种颜色信息(红色、绿色或蓝色),需要…...
Linux常见问题-获取日志方法总结(Ubuntu/Debian)
1 日志基本路径和基础查看方法 在 Ubuntu 或 Debian 11 系统中,可以通过不同的日志文件来获取系统日志和内核日志。日志常见路径如下: /var/log/syslog:包含系统的整体日志,包括各种系统事件和服务日志。/var/log/auth.log&…...

【机器视觉技术栈】03 - 镜头
镜头 定焦镜头变焦镜头远心镜头 FA镜头与远心镜头的区别? 焦距越小畸变程度越大,精度要求不高的场景可以使用焦距大的FA镜头做尺寸测量,但焦距越大带来的问题就是整个机械设备越大。精度高的场景使用远心镜头进行尺寸测量。 光学基础知识…...

判断一个Series序列的值是否为单调递减Series.is_monotonic_decreasing
【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 判断一个Series序列中 各值是否单调递减 s.is_monotonic_decreasing [太阳]选择题 以下代码的输出结果中正确的是? import pandas as pd s1 pd.Series([3,2,1]) s2 pd.Series([3,2,4]) pri…...

CSPNet: A New Backbone that can Enhance Learning Capability of CNN(2019)
文章目录 -Abstract1 Introduction2 Related workformer work 3 Method3.1 Cross Stage Partial Network3.2 Exact Fusion Model 4 Experiments5 Conclusion 原文链接 源代码 - 梯度信息重用(有别于冗余的梯度信息)可以减少计算量和内存占用提高效率&am…...

本科毕业论文查重的依据
大家好,今天来聊聊本科毕业论文查重的依据,希望能给大家提供一点参考。 以下是针对论文重复率高的情况,提供一些修改建议和技巧: 本科毕业论文查重依据:维护学术诚信的基石 摘要: 本科毕业论文是衡量学生学…...

如何利用Axure制作移动端产品原型
Axure是一款专业的快速原型设计工具,作为专业的原型设计工具,Axure 能够快速、高效地创建原型,同时支持多人协作设计和版本控制管理。它已经得到了许多大公司的采用,如IBM、微软、思科、eBay等,这些公司都利用Axure 进…...
Java中时间之间的转换
Java中常见的时间类有:Date、Calendar、SimpleDateFormat等。下面对不同时间类之间的转换进行介绍。 1、Date和Calendar之间的转换 Date和Calendar都可以表示时间,但是它们的使用方式不同。Date是一个表示特定时间点的类,而Calendar则是一个…...

【win32_005】调试信息打印到控制台----2种简单方法
方法1:使用win32 api函数 PCTSTR str1 TEXT("123456789");AllocConsole();HANDLE HConsole GetStdHandle(STD_OUTPUT_HANDLE);WriteConsole(HConsole, str1, 9, NULL, NULL);https://learn.microsoft.com/zh-cn/windows/console/writeconsole 方…...

PPT添加备注
0 Preface/Foreward 1 添加备注方法 添加备注方法:在page的最下端,有一个空白文本框,该文本框用来添加备注。...

Ubuntu20.04使用cephadm部署ceph集群
文章目录 Requirements环境安装Cephadm部署Ceph单机集群引导(bootstrap)建立新集群 管理OSD列出可用的OSD设备部署OSD删除OSD 管理主机列出主机信息添加主机到集群从集群中删除主机 部署Ceph集群 Cephadm通过在单个主机上创建一个Ceph单机集群࿰…...

激光打标机在智能手表上的应用:科技与时尚的完美结合
随着科技的飞速发展,智能手表已经成为我们日常生活中不可或缺的智能设备。而在智能手表制造中,激光打标机扮演着至关重要的角色。本文将详细介绍激光打标机在智能手表制造中的应用,以及其带来的优势和影响。 一、激光打标机在智能手表制…...

ROS-ROS通信机制-参数服务器
文章目录 一、基础理论知识二、C实现三、Python实现 一、基础理论知识 参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点…...
在github中通过action自动化部署 hugo academic theme,实现上传md文件更新博客内容
在github中通过action自动化部署 hugo academic theme 一、GitHub Action自动化部署Hugo博客方法 主要参考:【Hugo网站搭建】GitHub Action自动化部署Hugo博客 次要参考:使用 Github Action 自动部署 Hugo 博客 二、部署过程中遇到的问题和解决办法 …...
深入理解asyncio:异步编程的基础用法
引言: 随着计算机硬件的不断发展,对于异步编程的需求也越来越强烈。Python中的asyncio模块为开发者提供了一种强大而灵活的异步编程方式。本文将介绍asyncio的基础用法,包括async/await/run语句的使用、多个协程的并发执行、以及在协程中进行…...
Android 消息分发机制解读
前言 想必大家都知道Android系统有自己的一套消息分发机制,,从App启动那一刻起,App就创建了主线程的消息分发实例:Looper.sMainLooper,并开始无限循环,也就是App的心脏,一直跳动,负责协调分配来…...
【ML】LSTM应用——预测股票(基于 tensorflow2)
LSTM 应用预测股票数据 所用数据集:https://www.kaggle.com/datasets/yuanheqiuye/bank-stock 基于:tensorFlow 2.x 数据处理 import numpy as np import pandas as pd from matplotlib import pyplot as plt from sklearn.model_selection import tr…...

汇编语言程序设计实验报告
一、实验一 1、实验内容 (1)用Debug命令查看寄存器和内存中的内容 (2)上机过程及程序调试 2、实验目的 (1)要求掌握使用Debug命令查看寄存器和内存的方法; (2)通过…...

C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...

佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...

免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...

【网络安全】开源系统getshell漏洞挖掘
审计过程: 在入口文件admin/index.php中: 用户可以通过m,c,a等参数控制加载的文件和方法,在app/system/entrance.php中存在重点代码: 当M_TYPE system并且M_MODULE include时,会设置常量PATH_OWN_FILE为PATH_APP.M_T…...

ZYNQ学习记录FPGA(一)ZYNQ简介
一、知识准备 1.一些术语,缩写和概念: 1)ZYNQ全称:ZYNQ7000 All Pgrammable SoC 2)SoC:system on chips(片上系统),对比集成电路的SoB(system on board) 3)ARM:处理器…...

pgsql:还原数据库后出现重复序列导致“more than one owned sequence found“报错问题的解决
问题: pgsql数据库通过备份数据库文件进行还原时,如果表中有自增序列,还原后可能会出现重复的序列,此时若向表中插入新行时会出现“more than one owned sequence found”的报错提示。 点击菜单“其它”-》“序列”,…...
Python的__call__ 方法
在 Python 中,__call__ 是一个特殊的魔术方法(magic method),它允许一个类的实例像函数一样被调用。当你在一个对象后面加上 () 并执行时(例如 obj()),Python 会自动调用该对象的 __call__ 方法…...