初级数据结构(二)——链表

 文中代码源文件已上传:数据结构源码

<-上一篇 初级数据结构(一)——顺序表        |        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

        搞定!准备下一篇。撒花! 

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

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

相关文章

Django回顾 - 6 Ajax

【1】Ajax 定义&#xff1a; 异步Javscript和XML 作用&#xff1a; Javascript语言与服务器(django)进行异步交互&#xff0c;传输的数据为XML&#xff08;当然&#xff0c;传输的数据不只是XML,现在更多使用json数据&#xff09; 同步交互和异步交互&#xff1a; 1、同步交互&…

如何解决syntaxerror: more than 255 arguments 报错

如何解决syntaxerror: more than 255 arguments 报错 问题背景解释解决方案 问题背景 今天拼接特征的时候&#xff0c;突然代码报错syntaxerror: more than 255 arguments &#xff0c;看了一下感觉这个报错非常有意思&#xff0c;估计平时也是没机会碰到&#xff0c;和大家分…

用Mnesia为cache增加分布式支持

一&#xff1a;分布式缓存 1.选取通信策略 在设计分布式程序时&#xff0c;可供选择的通信方式主要有两种&#xff1a;异步通信和同步通信。采用异步通信时&#xff0c;发送方无须等待任何确认或应答。而在采用同步通信时&#xff0c;发送方会处于挂起状态&#xff0c;直至收…

Stable Diffusion AI绘画系列【17】:绘本童话风格场景

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

【南京站-EI会议征稿中】第三届网络安全、人工智能与数字经济国际学术会议(CSAIDE 2024)

第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&#xff09; 2024 3rd International Conference on Cyber Security, Artificial Intelligence and Digital Economy 第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&…

制作蓝牙小车

制作控制蓝牙小车app 想制作一个蓝牙小车&#xff0c;通过手机app程序操控小车运行&#xff0c;制作分三个部分&#xff08;app制作&#xff0c;蓝牙小车硬件制作&#xff0c;小车程序制作&#xff09;&#xff0c;先完成第一个部分app制作&#xff0c;本次app是通过androidstu…

MongoDB知识总结

这里写自定义目录标题 MongoDB基本介绍MongoDB基本操作数据库相关集合相关增删改查 MongoDB基本介绍 简单介绍 MongoDB是一个基于分布式文件存储的数据库。由C语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。 MongoDB是一个介于关系数据库和非关系数据库之间的产…

【Hive】——数据仓库

1.1 数仓概念 数据仓库&#xff08;data warehouse&#xff09;&#xff1a;是一个用于存储&#xff0c;分析&#xff0c;报告的数据系统 目的&#xff1a;是构建面向分析的集成化数据环境&#xff0c;分析结果为企业提供决策支持 特点&#xff1a; 数据仓库本身不产生任何数据…

Spring Boot学习随笔-SpringBoot的引言,回顾传统SSM开发

学习视频&#xff1a;【编程不良人】2021年SpringBoot最新最全教程 第一章、传统SSM开发回顾以及问题 Spring SpringMVC Mybatis SSM 实现一个简单功能 员工添加、查询… SSM项目简单实现 项目 需求分析 —>概要设计 —>&#xff08;库表设计&#xff09; —> 详细…

从零开始的c语言日记day40——字符函数和字符串函数——内存函数

常用函数介绍 求字符串长度 strlen 长度不受限制的字符串函数 Strcpy Strcat strcmp 长度受限制的字符串函数介绍 strncpy strncat strncmp 字符串查找 Strstro strtok 错误信息报告 strerror 字符操作 内存操作函数 memcpy memmove memset Memcmp 使用Asser…

点击el-tree小三角后去除点击后的高亮背景样式,el-tree样式修改

<div class"videoTree" v-loading"loadingTree" element-loading-text"加载中..." element-loading-spinner"el-icon-loading" element-loading-background"rgba(0, 0, 0, 0.8)" > <el-tree :default-expand-all&q…

鸿蒙4.0开发笔记之ArkTS语法基础之应用生命周期与页面中组件的生命周期(十六)

文章目录 一、应用生命周期二、生命周期函数定义三、生命周期五函数练习 一、应用生命周期 1、定义 应用生命周期就是代表了一个HarmonyOS应用中所有页面从创建、开启到销毁等过程的全生命周期。查看路径如下&#xff1a; Project/entry/src/main/ets/entryability/EntryAbili…

17、XSS——session攻击

文章目录 一、session攻击简介二、主要攻击方式2.1 预测2.2 会话劫持2.3 会话固定 一、session攻击简介 session对于web应用是最重要&#xff0c;也是最复杂的。对于web应用程序来说&#xff0c;加强安全性的首要原则就是&#xff1a;不要信任来自客户端的数据&#xff0c;一定…

Spring Boot与Mybatis基础配置(手动写增删改查)

一、 配置 1.新建项目 1.项目基础配置 解释&#xff1a;记得把这个改成start.aliyun.com要不没有java8也就是jdk1.8 2.项目依赖配置 2.配置maven 配置前&#xff1a; 配置后&#xff1a; 3.创建子项目并配置父子项目pom.xml 配置父pom.xml 声明当前项目不是要打成jar包的…

NFC和蓝牙在物联网中有什么意义?如何选择?

#NFC物联网# #蓝牙物联网# 在物联网中&#xff0c;NFC和蓝牙有什么意义&#xff1f; NFC在物联网中代表近场通信技术。它是一种短距离、高频的无线通信技术&#xff0c;可以在近距离内实现设备间的数据传输和识别。NFC技术主要用于移动支付、电子票务、门禁、移动身份识别、防…

利用阿里云 DDoS、WAF、CDN 和云防火墙为在线业务赋能

在这篇博客中&#xff0c;我们将详细讨论使用阿里云 CDN 和安全产品保护您的在线业务所需的步骤。 方案描述 创新技术的快速发展为世界各地的在线业务带来了新的机遇。今天的人们不仅习惯了&#xff0c;而且依靠互联网来开展他们的日常生活&#xff0c;包括购物、玩游戏、看电…

【python VS vba】(7) python与numpy (建设ing)

目录 1 numpy 的基本介绍 2 numpy里的两种新数据类型&#xff1a;ndarray 和 matrix 2.1 numpy特殊的数据类型 2.1.1 python的数据类型 2.1.2 首先 python原生的list 和 tuple 2.1.3 numpy的数据类型 2.2 np.matrix() 或者 np.mat() 2.2.1 首先&#xff0c;两种写法相…

基于PicGo实现Typora图片自动上传GitHub

文章目录 一. 引言二. 原理三. 配置3.1 GitHub 设置3.2 下载配置 PicGo3.3 配置 Typora3.4 使用 一. 引言 Typora是一款非常好的笔记软件&#xff0c;但是有一个比较不好的地方&#xff1a;默认图片是存放在本地缓存中。这就会导致文件夹一旦被误删或电脑系统重装而忘记备份文件…

18、XSS——cookie安全

文章目录 1、cookie重要字段2、子域cookie机制3、路径cookie机制4、HttpOnly Cookie机制5、Secure Cookie机制6、本地cookie与内存cookie7、本地存储方式 一般来说&#xff0c;同域内浏览器中发出的任何一个请求都会带上cookie&#xff0c;无论请求什么资源&#xff0c;请求时&…

西南科技大学C++程序设计实验六( 继承与派生一)

一、实验目的 1. 理解不同继承属性对派生类访问基类成员的区别 2. 掌握单继承程序编写 二、实验任务 1、调试下列程序,并在对程序进行修改后再调试,指出调试中的出错原因(该题中A为基类,B为派生类,B以public方式继承A) 重点:理解不同继承方式数据的访问权限,派生类…