数据结构:单链表(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

DeepFace深度学习库+OpenCV实现——情绪分析器

DeepFace深度学习库+OpenCV实现——情绪分析器

目录 应用场景 实现组件 1. 硬件组件 2. 软件库与依赖 3. 功能模块 代码详解(实现思路) 导入必要的库 打开摄像头并初始化变量 主循环 FPS计算 情绪分析及结果展示 显示FPS和图像 退出条件 编辑 完整代码 效果展示 自然的 开心的 伤心的 恐惧的 惊讶的  效果展示 自然的 开心的 伤心的 恐惧的 惊讶的   应用场景         应用场景比较广泛,尤其是在需要了解和分析人类情感反应的场合。: 1. 心理健康评估:在心理健康领域,可以通过长期监控和分析一个人的情绪变化来辅助医生进行诊断或治疗效果评估。 2. 用户体验研究:在产品设计、广告制作或网站开发过程中,通过观察用户在使用过程中的情绪反应,来优化产品的用户体验。 3. 互动娱乐:在游戏或虚拟现实应用中,根据玩家的情绪状态动态调整游戏难度或故事情节,以增加沉浸感和互动性。

By Ne0inhk
最全java面试题及答案(208道)

最全java面试题及答案(208道)

本文分为十九个模块,分别是:「Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM」 ,如下图所示: 共包含 208 道面试题,本文的宗旨是为读者朋友们整理一份详实而又权威的面试清单,下面一起进入主题吧。 Java 基础 1. JDK 和 JRE 有什么区别? * JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java

By Ne0inhk
用 DeepSeek 打造你的超强代码助手

用 DeepSeek 打造你的超强代码助手

DeepSeek Engineer 是啥? 简单来说,DeepSeek Engineer 是一个基于命令行的智能助手。它能帮你完成这些事: * 快速读文件内容:比如你有个配置文件,直接用命令把它加载进助手,后续所有操作都可以基于这个文件。 * 自动改文件:它不仅能提建议,还可以直接生成差异表(diff),甚至自动应用修改。 * 智能代码生成:比如你让它生成代码片段,它会按照指定格式和规则直接返回。 更重要的是,这一切都是通过 DeepSeek 的强大 API 来实现的。想象一下,你有个贴身助手,不仅能听懂你的代码需求,还能直接动手帮你写! 核心功能拆解 我们先来看 DeepSeek Engineer 的几个核心能力,让你更好地理解它的强大之处。 1. 自动配置 DeepSeek 客户端 启动这个工具时,你只需要准备一个 .env 文件,里面写上你的 API Key,比如: DEEPSEEK_API_

By Ne0inhk
解锁DeepSeek潜能:Docker+Ollama打造本地大模型部署新范式

解锁DeepSeek潜能:Docker+Ollama打造本地大模型部署新范式

🐇明明跟你说过:个人主页 🏅个人专栏:《深度探秘:AI界的007》 🏅 🔖行路有良友,便是天堂🔖 目录 一、引言 1、什么是Docker 2、什么是Ollama 二、准备工作 1、操作系统 2、镜像准备 三、安装 1、安装Docker 2、启动Ollama 3、拉取Deepseek大模型 4、启动Deepseek  一、引言 1、什么是Docker Docker:就像一个“打包好的App” 想象一下,你写了一个很棒的程序,在自己的电脑上运行得很好。但当你把它发给别人,可能会遇到各种问题: * “这个软件需要 Python 3.8,但我只有 Python 3.6!

By Ne0inhk