数据结构:单链表(1)

数据结构:单链表(1)

目录

前言 

一.单链表的概念

介绍

二.单链表的结构

介绍

链表的打印

核心逻辑解析

链表的销毁

三、实现单链表

1.单链表的尾插

结点的创建

2.单链表的头插

3.单链表的尾删

4.单链表的头删

代码

  总结


前言 

  最近学校事务较多,我又正巧经历社团换届,所以耽误了几天时间,但好在所有投入都有了温暖的回应,留任成功了(虽然是小社团哈),接下来,我将继续更新博客,与大家分享知识。

本篇文章将讲解单链表的知识,包括:单链表的概念,单链表的结构、实现单链表、链表的分类、单链表算法题知识的相关内容,为5大模块,其中为本章节知识的内容。

一.单链表的概念

介绍

  在之前我们学习了逻辑结构和物理结构都是线性的顺序表,但是我们会发现顺序表有以下3个比较明显的缺陷:中间/头部的插入删除,时间复杂度为O(N)。增容需要申请新空间,拷贝数据,释放旧空间,有不小的消耗。增容一般呈两倍的增长,会有一定的空间浪费。

而链表可以很好的解决该问题:

首先,先介绍一下链表的基础知识:

  链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,即:逻辑顺序通过指针链接 的线性数据结构它由多个节点组成,每个节点包含 数据域(存储具体值)和 指针域(存储下一个节点的地址),通过指针域建立节点间的逻辑关系,形成线性序列(如 A→B→C→NULL)。

例:

  每个节点包含 数据域(存储具体值)和 指针域(存储下一个节点的地址)形象化来看:数据域方面用数字来表示,指针域方面用next,表示:

  图中指针变量list保存的是第⼀个结点的地址,我们称list此时“指向”第⼀个结点,如果我们希望 list“指向”第⼆个结点时,只需要修改plist保存的内容为next即可,链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。

  结构上非连续,非顺序,但它的逻辑结构还是线性的。我们可以把链表想象成火车的一节节车厢链接在一起,只不过不是通过下标来访问节点了,是通过每个节点的地址来访问下一个节点。

  所以,回到上文,链表刚好可以使头部插入与删除的时间复杂度为O(1),不需要增容也不存在空间的浪费。提高使用的效率。

二.单链表的结构

介绍

  简单来说:

单链表,链表是由一个一个节点组成的,它的节点由两个组成部分数据域:保存的数据。指针域:指针,存放的是下一个结点的地址。

根据前面的知识,我们可以得出链表的结构:

#include<stdio.h> typedef int type; typedef struct SListNode { type data; struct SListNode* next; } SListNode; 
当然,数据域的内容并不唯一,你也可以写其他或很多的成员的。当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。 当我们想要从第⼀个结点⾛到最后⼀个结点时,只需要在当前结点拿上下⼀个结点的地址就可以了。

具体使用例子:

#include<stdio.h> #include<stdlib.h> typedef int type; typedef struct SListNode { type data; struct SListNode* next; } SListNode; int main() { SListNode* node1=(SListNode*)malloc(sizeof(SListNode)); SListNode* node2=(SListNode*)malloc(sizeof(SListNode)); SListNode* node3=(SListNode*)malloc(sizeof(SListNode)); SListNode* node4=(SListNode*)malloc(sizeof(SListNode)); node1->data=1; node1->next=node2; node2->data=2; node2->next=node3; node3->data=3; node3->next=node4; node4->data=4; node4->next=NULL; } 

  对于该链表,如果想打印的话,则需要先讲解一下链表的打印,而每一个链表的节点均为malloc所得的结果,需要将申请的空间释放掉:所以接下来也会讲解一下链表的销毁函数。

链表的打印

单链表的打印是遍历链表并输出节点数据的基础操作,函数原形为:

void SLTPrint(SListNode* h);

代码实现为:

void SLTPrint(SListNode* h) { if (h == NULL) { printf("链表为空,无值\n"); return; } SListNode* p = h; while (p) { printf("%d ", p->data); p = p->next; } }

讲解:

核心逻辑解析

1. 空链表判断与处理作用:检查链表是否为空(头指针 h 为 NULL 时,链表无节点)。处理逻辑:若为空链表,打印提示信息 链表为空,无值,并通过 return 终止函数(避免后续无效操作)。为什么要 return
若不终止,会继续执行后续的 p = h 和 while (p) 循环,但此时 p 为 NULL,循环不会执行,虽无语法错误,但逻辑冗余,提前返回更高效

2. 非空链表:遍历打印数据遍历逻辑临时指针 p:用于遍历链表,避免直接修改头指针 h(保护原始链表结构)。循环条件 while (p):当 p 不为 NULL 时,持续访问节点(p 指向 NULL 表示已到链表尾部)。打印数据:通过 p->data 获取当前节点的值,用空格分隔(%d )。

3.执行流程图解

开始
  ↓
判断 h 是否为 NULL?
  ├─ 是 → 打印"链表为空,无值" → 结束函数
  └─ 否 → 定义 p = h
       ↓
     while (p != NULL):
       ├─ 打印 p->data
       ├─ p = p->next(移动到下一个节点)
       └─ 重复循环,直到 p 为 NULL
  ↓
结束

链表的销毁

  单链表销毁的核心目标是 释放所有节点占用的堆内存,避免内存泄漏。

 函数原形为:

void SListDestroy(SListNode** h);

参数为二级指针的原因:

  我们知道,函数调用过程中,简单来说,实参的值会被拷贝给形参,形参与实参本质上是两个独立的变量,函数内对形参的修改不会影响实参。

若要通过函数修改外部变量的值,必须传递该变量的 地址。对于指针变量 head,其地址就是 二级指针

代码实现为:

void SListDestroy(SListNode** h) { if (*h == NULL) { printf("链表为空\n"); return; } SListNode* p = *h; while(p) { SListNode* q = p; p = p->next; free(q); } }

讲解  核心知识:

变量 p 和 q 的分工p:遍历指针,从 *h(头节点)开始,逐步移动到 NULL(链表尾)。q:临时指针,用于暂存当前要释放的节点(q = p),避免 p 移动后丢失当前节点地址。循环条件 while (p):当 p 不为 NULL 时,继续遍历(即还有节点未释放)。释放顺序:必须先通过 p = p->next 保存下一个节点地址,再 free(q),否则释放 q 后,p->next 会变为无效地址(断链)。

根据上方的代码:

我们可以打印与销毁了

 SLTPrint(node1); SListDestroy(&node1);

结果:

三、实现单链表

接下来,我将对实现单链表的代码进行实现:

1.单链表的尾插

  我们学习过顺序表的知识了,也清楚,尾插入的逻辑,只不过单链表的尾插,并没有扩容两倍的需要,基本都是随插随申请值。

  我们链表使用是要通过头结点对后面节点进行访问的,你看图,可以明确得知有几个节点,节点的地址、值,但我们写代码时是看不到的,这是链表的“抽象性”与“内存不可见性” 问题——代码中无法直接“看到”链表的物理结构(如节点地址、实际节点数),只能通过头指针间接操作。

函数形式:

void SLTPushBack(SListNode** h, type x);

  由参数可知,在实现单链表的尾插之前,我们要先申请新节点,而后续头插等接口的实现也会用上,所以将其写为函数。

结点的创建

函数形式:

SListNode* SLTBuyNode(type x)

实现代码为:

SListNode* SLTBuyNode(type x) { SListNode* p = (SListNode*)malloc(sizeof(SListNode)); if (p) { p->data = x; p->next = NULL; return p; } else { perror("malloc failed"); return NULL; } }

  该函数用于动态创建单链表节点,分配内存并初始化数据域和指针域。

接下来,我们来实现下链表的尾插入:

void SLTPushBack(SListNode** h, type x) { SListNode* p = SLTBuyNode(x); if (*h == NULL) { *h = p; } else { SListNode* pr = *h; while (pr->next) { pr = pr->next; } pr->next = p; } }
注:参数1SListNode** h(头节点指针的地址)必须传递二级指针:因为若链表为空(*h == NULL),需要修改头指针本身(而非头指针指向的内容),此时一级指针无法实现(值传递特性)。参数2type x

函数中为什么用 *h
因为h 是二级指针,*h 才是头指针本身。直接修改 *h 会改变原链表的头指针地址。

else的逻辑:

pr 从头部出发,通过 pr = pr->next 移动,直到 pr->next == NULL(此时 pr 即为尾节点)。

以下图为例,链表的尾部插入,需要先遍历一遍已有的值,在pr->next == NULL为尾时停止遍历,

尾部之后插入新的值,时间复杂度O(N)。

2.单链表的头插

即为头部插入,与顺序表的整体向后移动一位不同,链表的实现较为简单,效率也高。

函数形式:

void SLTPushFront(SListNode** h, type x)

  在单链表的头部插入新节点,新节点成为新的头节点,原链表(若存在)则链接到新节点之后。

实现

void SLTPushFront(SListNode** h, type x) { SListNode* newnode = SLTBuyNode(x); if (*h == NULL) { *h = newnode; } else { newnode->next = *h; *h = newnode; } }

例:(*h不为空)情况:

创建新节点 newnodedata=0next=NULL)。newnode->next = *h:新节点的 next 指向原头节点,此时 newnode -> 1 -> 2 -> 3-> 4 -> NULL*h = newnode:头指针更新为 newnode,链表变为 0 -> 1 -> 2 -> 3 ->4 -> NULL

属于直接操作,时间复杂度为O(1);

3.单链表的尾删

  删除单链表的最后一个节点,并释放其内存,需处理链表为空、一个节点、多个节点的不同场景。

函数:

void SLTPopBack(SListNode** h)
void SLTPopBack(SListNode** h) { if (*h == NULL) { return; } if ((*h)->next == NULL) { free(*h); *h = NULL; } else { SListNode* p = *h; SListNode* pr = *h; while (p->next) { pr = p; p = p->next; } free(p->next); pr->next = NULL; } }
场景处理流程
链表为空直接返回,无操作。
单节点链表释放头节点,*h = NULL(头指针置空)。
多节点链表遍历找到尾节点 p 和前节点 pr,释放 ppr->next = NULL
首先是断言,链表不可以为空特别判断只有一个节点的情况,如果只有一个节点的话直接释放掉就好了找尾节点的同时找到尾节点的前一个节点,每次尾节点向前走之前,先让pr指向其原来的位置最后直接让pr->next=NULL,释放掉p就好了。

4.单链表的头删

  单链表的头删是指删除链表的第一个节点,核心目标是 释放头节点内存 + 更新头指针,需处理空链表、单节点链表等边界情况。

void SLTPopFront(SListNode** h)
void SLTPopFront(SListNode** h) { if (*h == NULL) { return; } SListNode* p = (*h)->next; free(*h); *h = p; }

讲解:



这里主要就是先定义一个中间变量记录头的下一个节点,再直接free掉头节点。最后让中间变量成为新的头节点就可以了。

接下来我将列出练习的代码,和示例:

代码

  代码我分三个文件写的分别是 1.h 1.cpp main.cpp

1.h

#include<stdio.h> #include<stdlib.h> typedef int type; typedef struct SListNode { type data; struct SListNode* next; }SListNode; void SLTPrint(SListNode* h); void SListDestroy(SListNode** h); void SLTPushBack(SListNode** h, type x); void SLTPushFront(SListNode** h, type x); void SLTPopBack(SListNode** h); void SLTPopFront(SListNode** h); SListNode* SLTBuyNode(type x);

1.cpp

#include"1.h" void SLTPrint(SListNode* h) { if (h == NULL) { printf("链表为空,无值\n"); return; } SListNode* p = h; while (p) { printf("%d ", p->data); p = p->next; } printf("\n"); } void SListDestroy(SListNode** h) { if (*h == NULL) { printf("链表为空\n"); return; } SListNode* p = *h; while(p) { SListNode* q = p; p = p->next; free(q); } } SListNode* SLTBuyNode(type x) { SListNode* p = (SListNode*)malloc(sizeof(SListNode)); if (p) { p->data = x; p->next = NULL; return p; } else { perror("malloc failed"); return NULL; } } void SLTPushBack(SListNode** h, type x) { SListNode* p = SLTBuyNode(x); if (*h == NULL) { *h = p; } else { SListNode* pr = *h; while (pr->next) { pr = pr->next; } pr->next = p; } } void SLTPushFront(SListNode** h, type x) { SListNode* newnode = SLTBuyNode(x); if (*h == NULL) { *h = newnode; } else { newnode->next = *h; *h = newnode; } } void SLTPopBack(SListNode** h) { if (*h == NULL) { return; } if ((*h)->next == NULL) { free(*h); *h = NULL; } else { SListNode* p = *h; SListNode* pr = *h; while (p->next) { pr = p; p = p->next; } free(p); pr->next = NULL; } } void SLTPopFront(SListNode** h) { if (*h == NULL) { return; } SListNode* p = (*h)->next; free(*h); *h = p; } 

main.cpp

#include"1.h" void test() { SListNode* node1 = (SListNode*)malloc(sizeof(SListNode)); SListNode* node2 = (SListNode*)malloc(sizeof(SListNode)); SListNode* node3 = (SListNode*)malloc(sizeof(SListNode)); SListNode* node4 = (SListNode*)malloc(sizeof(SListNode)); node1->data = 1; node1->next = node2; node2->data = 2; node2->next = node3; node3->data = 3; node3->next = node4; node4->data = 4; node4->next = NULL; SLTPrint(node1); SListDestroy(&node1); } void test2() { SListNode* h=NULL; SLTPushBack(&h, 10); SLTPushBack(&h, 20); SLTPrint(h); SLTPushFront(&h, 30); SLTPushFront(&h, 40); SLTPrint(h); SLTPopBack(&h); SLTPrint(h); SLTPopFront(&h); SLTPrint(h); SListDestroy(&h); } int main() { test2(); } 

结果为:

  总结

  以上就是今天要讲的内容,本篇文章涉及的知识点为:单链表的概念,单链表的结构、实现单链表等知识的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,接下来的内容我会很快更新。

Read more

5个维度带你掌握python-okx库:从API整合痛点到量化交易落地

5个维度带你掌握python-okx库:从API整合痛点到量化交易落地 【免费下载链接】python-okx 项目地址: https://gitcode.com/GitHub_Trending/py/python-okx 作为一名有2年经验的加密货币量化开发者,我曾为整合OKX交易所API付出过惨痛代价。3000行冗余代码、15个零散接口调用、3次生产环境连接中断——这些数字背后是无数个调试到凌晨的夜晚。直到遇见python-okx库,我才发现:原来连接交易所API可以像高速公路通行般顺畅。本文将从架构设计、场景落地、性能优化等五个维度,带你彻底掌握这款工具的实战价值,让你的量化交易系统开发效率提升一个数量级。 直击行业痛点:加密货币API开发的三大陷阱 在量化交易领域,API整合向来是开发者的"噩梦起点"。我曾见过三个典型陷阱,每个都可能让项目功亏一篑。 第一个陷阱是接口碎片化。OKX V5 API包含18个业务模块、超过120个接口方法,手动封装不仅耗时,还容易遗漏关键功能。某团队曾因缺少WebSocket断线重连机制,导致行情数据中断3小时,直接造成约2万美元损失。

By Ne0inhk
【2025 最新】 Python 安装教程 以及 Pycharm 安装教程(超详细图文指南,附常见问题解决)

【2025 最新】 Python 安装教程 以及 Pycharm 安装教程(超详细图文指南,附常见问题解决)

前言         Python 作为目前最热门的编程语言之一,在数据分析、人工智能、Web 开发等领域应用广泛。而 PyCharm 作为 JetBrains 推出的 Python 集成开发环境(IDE),以其强大的功能和友好的界面成为开发者的首选工具。         本文针对 2025 年最新版 Python(3.13.x)和 PyCharm(202x.x.x),提供Windows 10或11和macOS Sonoma双系统安装教程,从官网下载到环境配置一步到位,同时整理了安装过程中最常见的 10 类问题及解决方案,确保新手也能顺利完成环境搭建。 一、Python 安装教程(2025 最新版) 1. 下载 Python 安装包 步骤 1:访问 Python 官网

By Ne0inhk
# 新手进阶Python:办公自动化之**全工具整合桌面应用(一键EXE分发)**

# 新手进阶Python:办公自动化之**全工具整合桌面应用(一键EXE分发)**

大家好!我是ZEEKLOG的Python新手博主~ 前面我们已经完成了办公看板、AI智能助手、邮件自动机器人、数据清洗报表生成四大核心办公自动化工具,彻底解决了日常办公的重复劳动。 但很多新手同学反馈了一个核心痛点: * 脚本都是命令行运行,新手改参数不方便 * 发给同事用,对方电脑没装Python,根本跑不起来 * 功能分散在不同脚本里,切换起来麻烦 * 没有可视化界面,操作不直观,容易出错 今天就用极简代码,把我们前面所有的办公自动化工具,整合成一个带图形界面的桌面应用,最后打包成EXE文件,无需安装Python,双击就能用,发给同事也能直接跑! 一、本次你能学到什么 1. 用 ttkbootstrap 快速开发美观的Python桌面界面(新手友好,比PyQt5门槛低) 2. 把之前的邮件机器人、数据处理工具完整整合到界面中 3. 实现配置保存、日志实时展示、文件选择等实用功能 4. 用 pyinstaller 一键打包成EXE文件,无Python环境也能运行 5. 学会模块化开发,后续可以无限扩展新功能 二、前期准备(

By Ne0inhk
从割裂到融合:MATLAB与Python混合编程实战指南

从割裂到融合:MATLAB与Python混合编程实战指南

从割裂到融合:MATLAB与Python混合编程实战指南 摘要:在科学计算领域,MATLAB和Python就像两把各有所长的“神兵利器”——MATLAB凭借矩阵运算的“独门绝技”称霸工程仿真,Python则依靠开源生态的“人海战术”横扫AI与数据科学。但在实际研发中,单一语言往往难以覆盖全流程需求:用MATLAB做完工程仿真,想对接Python的机器学习模型;用Python训练好AI模型,又需要MATLAB做工程验证。 这种场景下,MATLAB与Python的混合编程不再是“锦上添花”,而是提升研发效率的“刚需”。本文将手把手教你打通两大语言的壁垒,从技术原理到代码实战,全方位解析跨语言协作的最优路径。 一、核心技术路径对比 在动手编码前,先理清MATLAB与Python互调的核心方案,不同场景适配不同技术: 技术方案适用场景性能部署复杂度核心优势MATLAB Engine APIPython调用MATLAB函数(开发阶段)高低(需装MATLAB)调用最直接,支持全量MATLAB功能MATLAB Compiler SDKMATLAB代码打包部署(生产环境)中中(需运行时

By Ne0inhk