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

数据结构:单链表详解与 C 语言实现

单链表是线性表的链式存储结构,通过指针链接逻辑顺序。详细解析了单链表节点定义、内存申请及核心操作实现,包括头尾插删、查找插入删除等,并对比了不同链表结构的适用场景。重点讲解了二级指针在修改头结点时的必要性,以及边界条件的处理策略,适合希望深入理解底层内存管理的开发者参考。

小熊软糖发布于 2026/3/26更新于 2026/10/1981 浏览
数据结构:单链表详解与 C 语言实现

概念与结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 逻辑结构:线性 物理结构(存储结构):不一定是线性的

可以把链表想象成一列火车,车头是哨兵位(可有可无),车厢就是节点。将某节车厢去掉或加上,不会影响其他车厢,每节车厢都是独立存在的。

在链表里,每节'车厢'是什么样的呢?

结点

与顺序表不同的是,链表里的每节'车厢'都是独立申请下来的空间,我们称之为'结点'。结点的组成主要有两个部分:当前结点要保存的数据和保存下一个结点的地址(指针变量)。

链表中每个结点都是独立申请的(即需要插入数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点位置才能从当前结点找到下一个结点。

链表的性质

  1. 链式机构在逻辑上是连续的,在物理结构上不一定连续。
  2. 结点一般是从堆上申请的。
  3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续。

结合前面学到的结构体知识,我们可以给出每个结点对应的结构体代码。假设当前保存的结点为整型:

struct SListNode {
    int data; // 结点数据
    struct SListNode* next; // 指针变量,用于保存下一个结点的地址
};

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这块内存不仅要保存整型数据,也需要保存下一个结点的地址(当下一个结点为空时保存的地址为空)。当我们想要从第一个结点走到最后一个结点时,只需要在当前结点拿取下一个结点的地址就可以了。

实现单链表

头文件 (SList.h)

这里定义了链表的数据类型、节点结构以及所有操作的函数声明。注意 SLTNode 是 struct SListNode 的别名,方便后续使用。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

// 链表的数据类型
typedef int SLTDataType;

// 链表的结构
typedef struct SListNode {
    SLTDataType data;
    struct SListNode* next; // 指向下一个节点的地址
} SLTNode;

// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
// 尾删
void SLTPopBack(SLTNode** pphead);
// 头删
void SLTPopFront(SLTNode** pphead);
// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
// 在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
// 删除 pos 结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 删除 pos 之后的结点
void SLTEraseAfter(SLTNode* pos);
// 销毁链表
void SListDestroy(SLTNode** pphead);
// 打印链表
void SLTPrint(SLTNode* phead);

源文件 (SList.c)

单链表的打印

遍历链表,输出每个节点的数据,直到遇到空指针。

void SLTPrint(SLTNode* phead) {
    SLTNode* pcur = phead;
    while (pcur != NULL) {
        printf("%d -> ", pcur->data);
        pcur = pcur->next;
    }
    printf("NULL\n");
}
单链表申请新节点内存

这是创建节点的基础函数,负责从堆上分配内存并初始化。

SLTNode* SLTBuyNode(SLTDataType x) {
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL) {
        perror("malloc fail!");
        exit(1);
    }
    newnode->data = x;
    newnode->next = NULL;
    return newnode;
}

这里是申请新节点,并且把对应的数据存入节点中,所以接下来只需要将各个节点连接起来(链表中的指针)。

尾插

在链表尾部添加新节点。如果链表为空,新节点直接成为头结点;否则需要遍历到尾端再连接。

// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
    assert(pphead);
    // 申请新节点
    SLTNode* newnode = SLTBuyNode(x);
    // 链表为空
    if (*pphead == NULL) {
        *pphead = newnode;
    } else {
        SLTNode* ptail = *pphead;
        while (ptail->next != NULL) {
            ptail = ptail->next;
        }
        // 找到了尾结点 ptail,将新节点接在后面
        ptail->next = newnode;
    }
}

传址调用,使用 SLTNode** pphead,不然无法修改 SLTNode* phead 结构体指针。需要做出特殊判断,当链表为空的时候。

头插

在链表头部添加新节点。新节点的 next 指向原头结点,然后更新头指针。

// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
    assert(pphead);
    SLTNode* newnode = SLTBuyNode(x);
    // newnode -> *pphead
    newnode->next = *pphead;
    *pphead = newnode;
}
尾删

删除链表尾部的节点。需要找到倒数第二个节点,将其 next 置为 NULL,并释放原尾节点。注意处理只有一个节点的情况。

// 尾删
void SLTPopBack(SLTNode** pphead) {
    // 链表为空不能删除
    assert(pphead && *pphead);
    // 链表只有一个节点的情况
    if ((*pphead)->next == NULL) {
        free(*pphead);
        *pphead = NULL;
    } else {
        SLTNode* prev = NULL;
        SLTNode* ptail = *pphead;
        while (ptail->next) {
            prev = ptail;
            ptail = ptail->next;
        }
        // prev -> ptail
        prev->next = NULL;
        free(ptail);
        ptail = NULL;
    }
}

处理特殊情况,链表只有一个节点的情况。链表为空不能删除。

头删

删除链表头部的节点。先保存下一个节点地址,释放当前头结点,然后更新头指针。

// 头删
void SLTPopFront(SLTNode** pphead) {
    assert(pphead && *pphead);
    SLTNode* next = (*pphead)->next;
    free(*pphead);
    *pphead = next;
}
查找

遍历链表寻找值为 x 的节点。不需要传二级指针,因为只读不写头指针。

// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
    SLTNode* pcur = phead;
    while (pcur) {
        if (pcur->data == x) {
            return pcur;
        }
        pcur = pcur->next;
    }
    // 未找到
    return NULL;
}
在指定位置之前插入数据

在 pos 节点之前插入新节点。如果 pos 是头结点,则退化为头插操作;否则需要找到 pos 的前驱节点。

// 在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
    assert(pphead && pos);
    SLTNode* newnode = SLTBuyNode(x);
    // pos 指向头结点
    if (pos == *pphead) {
        SLTPushFront(pphead, x);
    } else {
        // 找 pos 的前一个节点
        SLTNode* prev = *pphead;
        while (prev->next != pos) {
            prev = prev->next;
        }
        // prev -> newnode -> pos
        prev->next = newnode;
        newnode->next = pos;
    }
}

pos 不能为空指针。特殊情况,pos 指向头结点。

在指定位置之后插入数据

在 pos 节点之后插入新节点。逻辑相对简单,只需调整指针指向。

// 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
    assert(pos);
    SLTNode* newnode = SLTBuyNode(x);
    // pos -> newnode -> pos->next
    newnode->next = pos->next;
    pos->next = newnode;
}
删除 pos 结点

删除指定的 pos 节点。如果 pos 是头结点,执行头删;否则找到前驱节点,断开连接并释放内存。

// 删除 pos 结点
void SLTErase(SLTNode** pphead, SLTNode* pos) {
    assert(pphead && pos);
    // pos 刚好就是头结点——头删
    if (pos == *pphead) {
        SLTPopFront(pphead);
    } else {
        SLTNode* prev = *pphead;
        while (prev->next != pos) {
            prev = prev->next;
        }
        // prev -> pos->next
        prev->next = pos->next;
        free(pos);
        pos = NULL;
    }
}

pos 不能为空指针。特殊情况,pos 刚好就是头结点—头删。

删除 pos 之后的结点

删除 pos 节点的下一个节点。需要确保 pos 和 pos->next 都不为空。

// 删除 pos 之后的结点
void SLTEraseAfter(SLTNode* pos) {
    assert(pos && pos->next);
    // pos -> del -> del->next
    SLTNode* del = pos->next;
    pos->next = del->next;
    free(del);
    del = NULL;
}
销毁链表

遍历整个链表,逐个释放节点内存,最后将头指针置空。

// 销毁链表
void SListDestroy(SLTNode** pphead) {
    assert(pphead);
    SLTNode* pcur = *pphead;
    while (pcur) {
        SLTNode* next = pcur->next;
        free(pcur);
        pcur = next;
    }
    *pphead = NULL;
}

链表的分类

链表的结构非常多样,以下情况组合起来就有 8 种(2 x 2 x 2)链表结构:有无头结点、单向/双向、循环/非循环。

虽然有这么多的链表结构,但是我们实际中最常用还是两种结构:

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

在实际开发中,理解单链表是基础,掌握其内存模型和指针操作对于深入理解 C/C++ 至关重要。

目录

  1. 概念与结构
  2. 结点
  3. 链表的性质
  4. 实现单链表
  5. 头文件 (SList.h)
  6. 源文件 (SList.c)
  7. 单链表的打印
  8. 单链表申请新节点内存
  9. 尾插
  10. 头插
  11. 尾删
  12. 头删
  13. 查找
  14. 在指定位置之前插入数据
  15. 在指定位置之后插入数据
  16. 删除 pos 结点
  17. 删除 pos 之后的结点
  18. 销毁链表
  19. 链表的分类

更多推荐文章

查看全部
  • OpenClaw 多节点访问 Gateway 的配置方法
  • 基于Xilinx FPGA的RISC-V五级流水线CPU设计与实现
  • 学术论文降低 AI 检测率的实操方法与工具推荐
  • JVM 运行时数据区域详解与内存结构分析
  • Qwen2.5-7B 生产级部署:vLLM + Docker + OpenResty 高并发架构
  • 大数据思维怎么用?7 个经典案例解析
  • 数据库迁移 TCO 全景账本:MySQL 替代中的隐性成本与工程化工具链
  • 基于腾讯云轻量应用服务器部署 OpenClaw 并接入 QQ 与飞书机器人
  • NVIDIA DGX Spark 部署 Stable Diffusion 3.5 与 ComfyUI
  • Windows 安装 Python 后 CMD 命令行无法识别命令
  • 命令行大模型交互工具 MCPHost 使用指南
  • Windows 11 使用 llama.cpp 运行 Qwen3.5 量化模型测试
  • ERNIE-4.5-VL-28B-A3B-Paddle 与 DeepSeek-R1 多维度测评分析
  • 二叉树深度优先搜索算法及经典例题解析
  • 大模型在智能客服领域的应用现状与最佳实现路径
  • 千笔 AI 论文辅助写作工具功能介绍
  • AIGC 实战:优化图文生成 20 秒与 30 秒视频的成本差异
  • 2025 年 AIGC 六大发展趋势与落地应用
  • Chromium 144 编译指南:macOS 安装 Xcode
  • AIGC 时代大模型幻觉问题深度治理:技术体系、工程实践与未来演进

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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