跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C算法

数据结构:单链表基础与实现

单链表是一种物理存储非连续但逻辑顺序线性的数据结构,通过指针连接节点。涵盖单链表概念、结构定义、打印与销毁函数实现,以及尾插、头插、尾删、头删等核心操作。重点解析二级指针在修改头指针时的必要性,内存分配与释放流程,确保无内存泄漏。包含完整 C 语言代码示例及逻辑图解说明。

RefactorPro发布于 2026/3/20更新于 2026/8/1650 浏览
数据结构:单链表基础与实现

数据结构:单链表

一、单链表的概念

介绍

在之前我们学习了逻辑结构和物理结构都是线性的顺序表,但是我们会发现顺序表有以下 3 个比较明显的缺陷:

  1. 中间/头部的插入删除,时间复杂度为 O(N)。
  2. 增容需要申请新空间,拷贝数据,释放旧空间,有不小的消耗。
  3. 增容一般呈两倍的增长,会有一定的空间浪费。

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

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

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

  type;
 
    type data;
    
} SListNode;

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

注:参数 1:SListNode** h(头节点指针的地址)必须传递 二级指针:因为若链表为空(*h == NULL),需要修改头指针本身(而非头指针指向的内容),此时一级指针无法实现(值传递特性)。参数 2:type 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 不为空)情况:

创建新节点 newnode(data=0,next=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);
        pr->next = NULL;
    }
}
场景处理流程
链表为空直接返回,无操作。
单节点链表释放头节点,*h = NULL(头指针置空)。
多节点链表遍历找到尾节点 p 和前节点 pr,释放 p,pr->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();
}

结果为:

文章配图

即

目录

  1. 数据结构:单链表
  2. 一、单链表的概念
  3. 介绍
  4. 二、单链表的结构
  5. 介绍
  6. 链表的打印
  7. 链表的销毁
  8. 三、实现单链表
  9. 1. 单链表的尾插
  10. 结点的创建
  11. 2. 单链表的头插
  12. 3. 单链表的尾删
  13. 4. 单链表的头删
  14. 代码
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 智能家居芯片市场规模与技术迭代趋势分析
  • VLM Unlearning 技术路线论文阅读总结与梳理
  • DeepCreamPy 二次元图片 AI 去码工具使用指南
  • 7 款最佳开源 LLM WebUI 工具推荐
  • Visual Studio GitHub Copilot 隐私设置及代码数据共享控制
  • Whisper Large v3 多语言语音识别 Web 服务部署指南
  • Capacitor 跨平台 Web 原生应用开发及鸿蒙适配指南
  • Go Web 开发必备理论知识
  • K-RagRec:基于知识图谱检索增强的大语言模型推荐框架
  • 深度学习 YOLOv11 空域安全无人机检测识别系统
  • 树莓派 5 结合 Whisper 与 EdgeTTS 构建全离线语音助手
  • C++主流日志库深度剖析:从原理到选型
  • 6 款主流 AI 写作工具实测:网文创作效率对比
  • Clawdbot 结合 Qwen3-32B 在 HR 与 IT 运维场景的落地实践
  • 大模型 LLM 微调技术论文精选汇总
  • 大模型提示工程进阶:思维链与思维树详解
  • Z-Image-Turbo 生成写实图像技术指南
  • Quartus Prime FPGA 开发入门:从安装到工程落地
  • 基于 FPGA 的 PWM 信号生成与高精度控制设计
  • Claude Code Rules 配置指南:基础与进阶实践

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online