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

数据结构:线性表的链式表示与实现

线性表的链式存储结构,涵盖单链表、双链表及循环链表的定义与特性。内容包含初始化、插入、删除、查找、求表长等核心算法的 C++ 实现,并对比了顺序表与链表在空间、密度、存取效率上的差异,适合数据结构初学者参考。

极光发布于 2026/3/23更新于 2026/6/2137 浏览

线性表的链式表示和实现

链式存储的表示
  • 链式存储结构
    • 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
  • 线性表的链式表示又称为非顺序映像或链式映像。
    • 用一组物理位置任意的存储单元来存放线性表的数据元素。
    • 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。
    • 链表中元素的逻辑次序和物理次序不一定相同。
  • 与链式存储有关的术语
    • 结点:数据元素的存储映像,由数据域和指针域两部分组成 [数据域 | 指针域]。
    • 链表:n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构。
    • 单链表、双链表、循环链表:
      • 结点只有一个指针域的链表,称为单链表或线性链表。
      • 结点有两个指针域的链表,称为双链表。
      • 首尾相接的链表称为循环链表。
    • 头指针、头结点和首元结点:
      • Head -> Info -> a1 -> a2 -> ... -> an -> null
      • 头指针:是指向链表中第一个结点的指针。
      • 首元结点:是指链表中存储第一个数据元素 a1 的结点。
      • 头结点:是在链表的首元结点之前附设的一个结点。
  • 问题讨论
    1. 如何表示空表?
      • 无头结点时,头指针为空时表示空表。
      • 有头结点时,当头结点的指针域为空时表示空表。
    2. 在链表中设置头结点的好处?
      • 便于首元结点的处理:首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理。
      • 便于空表和非空表的统一处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
    3. 头结点的数据域内装的是什么?
      • 头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
  • 链表 (链式存储结构) 的特点
    1. 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
    2. 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
    3. 这种存取元素的方法被称为顺序存取法。
单链表
定义

每个结点只有一个指针域。

struct Lnode {
    ElemType data; // 结点的数据域
    Lnode *next;   // 结点的指针域
};
typedef Lnode *LinkList; // LinkList 为指向结构体 Lnode 的指针类型
算法
【2.1】基础
  • 初始化
  • 算法步骤:生成新结点作头结点,用头指针 L 指向头结点;将头结点的指针域置空。
bool InitList(LinkList &L) {
    L = new Lnode;       // 堆区开辟一个头结点
    L->next = nullptr;   // 空表,也就是说头结点的指针指向为空
    return true;
}
// LinkList &L 等价于 Lnode *&L,是一个指向指针的引用
  • 求链表表长(返回表中元素个数)
    int GetLength(const LinkList &L) {
        Lnode *p;
        p = L->next;
        int cnt = 0;
        while (p) {
            ++cnt;
            p = p->next;
        }
        return cnt;
    }
    // 算法的时间复杂度为 O(n)
    
  • 清空链表(链表变成空链表,还存在)
    bool ClearList(LinkList &L) {
        if (IsEmpty(L)) {
            cerr << "empty List!" << endl;
            return false;
        }
        Lnode *p = L->next; // 保留了头结点
        while (p) {         // 链表还未到达尾端
            Lnode *temp = p->next; // 将头指针指向下一个结点
            delete p;
            p = temp;
        }
        L->next = nullptr; // 头结点指针域为空
        return true;
    }
    
  • 销毁链表(销毁后不存在)
    bool DestroyList(LinkList &L) {
        if (IsEmpty(L)) {
            cerr << "empty List!" << endl;
            return false;
        }
        while (L) { // 链表还未到达尾端
            Lnode *temp = L->next; // 将头指针指向下一个结点
            delete L;
            L = temp;
        }
        return true;
    }
    
  • 判断是否为空
    bool IsEmpty(const LinkList &L) {
        if (L->next) // 非空
            return false;
        else
            return true;
    }
    
  • 【2.2】取值

    取单链表中第 i 个元素内容。

    • 算法思路:从链表的头指针出发,顺着链域 next 逐个结点往下搜索,直至搜索到第 i 个结点为止。因此,链表不是随机存取结构。
    • 算法步骤:
      1. 从第 1 个结点 (L->next) 顺链扫描,用指针 p 指向当前扫描到的结点,p 初值 p=L->next。
      2. j 做计数器,累计当前扫描过的结点数,j 初值为 1。
      3. 当 p 指向扫描到的下一结点时,计数器 j 加 1。
      4. 当 j==i 时,p 所指的结点就是要找的第 i 个结点。
    bool GetElem(const LinkList &L, const int &i, ElemType &e) {
        if (i < 0) {
            cerr << "out of range" << endl;
            return false;
        }
        Lnode *p = L->next;
        for (int j = 1; j <= i; ++j) {
            if (!p) {
                cerr << "out of range" << endl;
                return false; // 如果此时 p 为空,意味着已经到达链表尾端,停止循环
            }
            p = p->next;
        }
        e = p->data;
        return true;
    }
    
    【2.3】查找

    按值查找:获取该数据所在位置 (地址) 或序号。

    • 算法思路:从第一个元素开始依次比对,直到找到相同元素。
    • 算法步骤:
      1. 从第一个结点起,依次和 e 相比较。
      2. 如果找到一个其值与 e 相等的数据元素,则返回其在链表中的地址/序号。
      3. 如果查遍整个链表都没有找到其值和 e 相等的元素,则返回 0 或 "NULL"。
    size_t LocateElem(LinkList &L, ElemType &e) {
        Lnode *p = L->next;
        size_t cnt = 1;
        while (p) {
            if (p->data == e) {
                return cnt;
            }
            ++cnt;
            p = p->next;
        }
        cerr << "not found" << endl;
        return 0;
    }
    // 算法的时间复杂度为 O(n)
    
    【2.4】插入

    在第 i 个结点前插入值为 e 的新结点。

    • 算法步骤:
      1. 首先找到 ai-1 的存储位置 p。
      2. 生成一个数据域为 e 的新结点 s。
      3. 插入新结点:
        • 新结点的指针域指向结点 ai:s->next = p->next;
        • 结点 ai-1 的指针域指向新结点:p->next = s;
      4. 思考:步骤一和步骤二能互换吗?思路可行,但直接换步骤会丢失 ai 的地址。
    bool InsertList(LinkList &L, const int &i, const ElemType &e) {
        Lnode *p = L;
        int j = 0;
        while (p && j < i - 1) {
            p = p->next;
            ++j;
        }
        // 异常判断
        if (!p || i < 0) {
            cerr << "out of range" << endl;
            return false;
        }
        Lnode *newNode = new Lnode;
        newNode->data = e;
        newNode->next = p->next;
        p->next = newNode;
        return true;
    }
    // 算法的时间复杂度为 O(n)
    
    【2.5】删除

    删除第 i 个结点。

    • 算法步骤:
      1. 首先找到 ai-1 的存储位置 p,保存要删除的 a 的值。
      2. 令 p->next 指向 ai+1:p->next = p->next->next;
      3. 释放结点 a 的空间。
    bool EraseList(LinkList &L, const int &i) {
        Lnode *p = L;
        int j = 0;
        while (p->next && j < i - 1) {
            p = p->next;
            ++j;
        }
        // 寻找第 i 个结点,并令 p 指向其前驱
        if (!(p->next) || i < 0) {
            cerr << "out of range" << endl;
            return false;
        }
        Lnode *q = p->next;
        p->next = p->next->next;
        delete q;
        return true;
    }
    // 算法的时间复杂度为 O(n)
    
    【2.6】单链表的建立
    1. 尾插法(元素插入在链表尾部,也叫后插法)
      void CreatListTail(LinkList &L, const size_t n) {
          Lnode *r = L;
          for (int i = 0; i < n; ++i) {
              Lnode *p = new Lnode;
              cin >> p->data;
              p->next = r->next;
              r->next = p;
              r = r->next;
          }
      }
      
    2. 头插法(元素插入在链表头部,也叫前插法)
      void CreatListHead(LinkList &L, const size_t n) {
          for (int i = 0; i < n; ++i) {
              Lnode *p = new Lnode;
              cin >> p->data;
              p->next = L->next;
              L->next = p;
          }
      }
      
    双向链表
    定义
    • 为什么要讨论双向链表:
      • 单链表的结点有指示后继的指针域,找后继结点方便 (O(1))。
      • 无指示前驱的指针域,找前驱结点难 (O(n))。
      • 可用双向链表来克服单链表的这种缺点。
    • 双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针域 prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。
      • 结构:[prior | data | next]
    • 双向链表结构的对称性:
      • 设指针 p 指向某一结点。
      • p->prior->next = p = p->next->prior
      • 在双向链表中有些操作,因仅涉及一个方向的指针,故它们的算法与线性链表的相同。
      • 但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为 O(n)。
    typedef struct DuLnode {
        ElemType data;
        DuLnode *prior, *next;
    } * DuLinkList;
    
    算法
    【3.1】插入

    把 x 插入 a 和 b 中:a 的后继指向 x,b 的前驱指向 x,x 的前驱是 a,x 的后继是 b。

    bool ListInsert_DuL(DuLinkList &L, const int i, const ElemType &e) {
        // 移动指针到 i 处
        DuLnode *p = L->next;
        int j = 1;
        while (p->next && j < i) {
            ++j;
            p = p->next;
        }
        if (j < i || j < 1) // 如果 i 在链表范围内,上面的 while 循环的终止条件就是 j<i
        {
            cerr << "out of range" << endl;
            return false;
        }
        // 在堆区创建要插入的结点
        DuLnode *s = new DuLnode;
        s->data = e;
        // 重新建立链接关系
        s->prior = p->prior;          // 第一步:s 的前趋等于 p 的前趋
        p->prior->next = s;           // 第二步,用 p 的前趋结点的 next 指向插入元素 s
        s->next = p;                  // 第三步:s 的后继指向 p
        p->prior = s;                 // 第四步:p 的前趋改为指向 s
        return true;
    }
    
    【3.2】删除

    删掉第 i 个结点。

    bool ListErase_DuL(DuLinkList &L, const int i) {
        DuLnode *p = L->next;
        int j = 1;
        while (p->next && j < i) // 移动到第 i 个元素的位置
        {
            ++j;
            p = p->next;
        }
        if (j < i || j < 1) {
            cerr << "out of range" << endl;
            return false;
        }
        // 改变链接关系
        p->prior->next = p->next; // p 的前趋结点的 next 等于 p 的后继
        if (p->next)              // 如果删除的不是最后一个元素
        {
            p->next->prior = p->prior;
        }
        delete p; // 释放空间
        return true;
    }
    
    循环链表
    定义
    • 是一种头尾相接的链表。
    • 即:表中最后一个结点的指针域指向头结点,整个链表形成一个环。
    • 优点:从表中任一结点出发均可找到表中其他结点。
    • 注意:由于循环链表中没有 NULL 指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断 p 或 p->next 是否为空,而是判断它们是否等于头指针。
    • 表示方法:
      • 头指针表示:找 a1 的时间复杂度 O(1),找 an 的时间复杂度 O(n)。不方便。
      • 尾指针表示:a1 的储存位置 R->next->next,an 的储存位置 R。时间复杂度 O(1)。表的操作常常是在表的首尾位置上进行。
    • 双向循环链表:和单链的循环表类似,双向链表也可以有循环表。让头结点的前驱指针指向链表的最后一个结点,让最后一个结点的后继指针指向头结点。
    // 单循环链表
    p != L; 
    p->next != L;
    
    // 单链表
    p != nullptr; 
    p->next != nullptr;
    
    实现

    带尾指针循环链表的合并 (将 Tb 合并在 Ta 之后)。

    • 分析:
      • p 存表头结点 p = Ta->next;
      • Tb 表头接到 Ta 表尾 Ta->next=Tb->next->next;
      • 释放 Tb 表头结点 delete Tb->next;
      • 修改指针 Tb->next=p;
    LinkList Connect(LinkList Ta, LinkList Tb) {
        Lnode *p = Ta->next;
        Ta->next = Tb->next->next;
        delete Tb->next;
        Tb->next = p;
        return Tb;
    }
    

    单链表、循环链表和双向链表的时间效率比较

    查找表头结点 (首元结点)查找表尾结点查找结点*P 的前驱结点
    带头结点的单链表 LL->next 时间复杂度 O(1)从 L->next 依次向后遍历时间复杂度 O(n)通过 p->next 无法找到其前驱
    带头结点仅设头指针 L 的循环单链表L->next 时间复杂度 O(1)从 L->next 依次向后遍历时间复杂度 O(n)通过 p->next 可以找到其前驱时间复杂度 O(n)
    带头结点仅设尾指针 R 的循环单链表L->next 时间复杂度 O(1)R 时间复杂度 O(1)通过 p->next 可以找到其前驱时间复杂度 O(n)
    带头结点的双向循环链表 LL->next 时间复杂度 O(1)L->prior 时间复杂度 O(1)p->prior 时间复杂度 O(1)

    顺序表和链表的比较

    • 链式存储结构的优点:
      • 结点空间可以动态申请和释放。
      • 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
    • 链式存储结构的缺点:
      • 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
      • 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
    存储密度

    存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比,即:

    存储密度 = 结点数据本身占用的空间 / 结点占用的空间总量

    例如:|a1|*p|,数据域 8 个字节,指针域 4 个字节,存储密度=8/12=67%。

    • 一般地,存储密度越大,存储空间的利用率就越高。
    • 显然,顺序表的存储密度为 1 (100%),而链表的存储密度小于 1。
    不同情况的比较
    比较项目顺序表链表
    存储空间预先分配,会导致空间闲置或溢出现象动态分配,不会出现存储空间闲置或溢出现象
    存储密度存储密度等于 1,逻辑关系等于存储关系,无额外的存储开销存储密度小于 1,需要借助指针来体现元素间的逻辑关系
    存取元素随机存取,按位置访问元素的时间复杂度为 O(1)顺序存取,按位置访问元素的时间复杂度为 O(n)
    插入、删除平均移动约表中一半元素,时间复杂度为 O(n)不需移动元素,确定插入、删除位置后,时间复杂度为 O(n)
    适用情况1. 表长变化不大,且能事先确定变化的范围
    2. 很少进行插入或删除操作,经常按元素位置序号访问数据元素
    1. 长度变化较大
    2. 频繁进行插入或删除操作
    类比 c++ 模板vectorlist

    目录

    1. 线性表的链式表示和实现
    2. 链式存储的表示
    3. 单链表
    4. 定义
    5. 算法
    6. 【2.1】基础
    7. 【2.2】取值
    8. 【2.3】查找
    9. 【2.4】插入
    10. 【2.5】删除
    11. 【2.6】单链表的建立
    12. 双向链表
    13. 定义
    14. 算法
    15. 【3.1】插入
    16. 【3.2】删除
    17. 循环链表
    18. 定义
    19. 实现
    20. 单链表、循环链表和双向链表的时间效率比较
    21. 顺序表和链表的比较
    22. 存储密度
    23. 不同情况的比较
    • 免费图片AI生成工具免费生成了解详情
    • Magick API 一键接入全球大模型注册送1000万token查看
    • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
    • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
    • 100+免费在线小游戏爽一把
    极客日志微信公众号二维码

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

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

    更多推荐文章

    查看全部
    • OpenClaw 开源 AI 执行引擎:架构、安装与实战详解
    • Python 实现现代游戏窗口无闪烁高性能 DX11/DX12 截图方案
    • C++ 二叉搜索树(BST)核心原理与实现
    • 2024 年前端主流框架技术总结与探索
    • VMware 虚拟机 CentOS 磁盘扩容实战:解决 growpart 报错与 LVM 调整
    • Java LangChain4j 框架入门:环境搭建与 AI 服务集成
    • macOS 配置 OpenClaw 接入 DeepSeek 大模型及飞书
    • RISC-V 处理器实战:Verilog 设计与 FPGA 验证流程
    • Linux 系统学习:深入剖析 Git 原理与进阶使用
    • 深入理解 Agentic AI:概念、架构及与 AIGC 的区别
    • Ubuntu 24.04 GPU 服务器测试系统盘制作指南
    • JavaScript 原生实现图片轮播图
    • 算法刷题:替换所有问号与提莫攻击(模拟)
    • Groovy 与 Java 语言特性差异详解
    • SpringBoot+Vue 手工艺品在线销售系统设计与实现
    • Llama3-8B 实测对比:逻辑推理与编码能力评估
    • C++ 复习核心知识点
    • 大模型基本概念详解:定义、发展、分类与微调技术
    • AstrBot + NapCat 快速部署智能 QQ 机器人及 cpolar 公网访问
    • 基于原生 JavaScript 的 Web 实用工具:成绩统计、完数查找与数组去重

    相关免费在线工具

    • 加密/解密文本

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