惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
A
Arctic Wolf
S
Securelist
O
OpenAI News
T
Threatpost
Forbes - Security
Forbes - Security
N
News and Events Feed by Topic
S
Secure Thoughts
H
Heimdal Security Blog
S
Security Affairs
P
Privacy International News Feed
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
S
Security @ Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
T
Tailwind CSS Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hacker News: Ask HN
Hacker News: Ask HN
T
Troy Hunt's Blog
S
SegmentFault 最新的问题
腾讯CDC
V
Visual Studio Blog
Last Week in AI
Last Week in AI
H
Hacker News: Front Page
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Project Zero
Project Zero
WordPress大学
WordPress大学
NISL@THU
NISL@THU
博客园 - 【当耐特】
博客园 - Franky
Webroot Blog
Webroot Blog
博客园_首页
T
Tenable Blog
雷峰网
雷峰网
Google Online Security Blog
Google Online Security Blog
阮一峰的网络日志
阮一峰的网络日志
V2EX - 技术
V2EX - 技术
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
Lohrmann on Cybersecurity
The Hacker News
The Hacker News

qlAD 的技术笔记

2026 年 6 月 7 日 -- 我有社交恐惧症,恐惧外面到处雷电风雨声(大学生活记录) 手把手带你实现哈希表(C/C++ 数据结构) 手把手带你实现图结构(C/C++ 数据结构) 手把手带你实现二叉树(C/C++ 数据结构) 手把手带你实现栈和队列(C/C++ 数据结构) 手把手带你实现动态数组(C/C++ 数据结构) 2026 年 1 月 2 日 -- 我不怕冷,因为我内心炽热 【2025 年度总结】(大学生活记录) 2025 年 12 月 2 日 -- 省电模式:大一 100 天耗电日志(大学生活记录) 蓝桥杯备赛第一期 —— 枚举与模拟 2025 年 11 月 3 日 -- 用爱发电、大爱无私(大学生活记录) 使用循环嵌套输出规则图形找规律讲解(嵌套循环-图形输出) 2025 年 10 月 1 日 -- 二字开头第一年的生日记录及感悟随笔(大学生活记录) Python 初级 04 -- 目前为止 Python 中你们做题可能会遇到的问题以及七七八八 2025 年 9 月 24 日 -- 开学后的第一课及军训汇演(大学生活记录) Python 初级 03 -- 程序设计的三种结构之选择结构和循环结构 Python 初级 02 -- 基础的数据类型和输入输出 2025 年 9 月 17 日 -- 大连 2025 国际大体联足球世界杯(大学生活记录) Python 初级 01 -- 初识编程、理解计算机语言及程序运行方式 适合大一新生学习编程的前一课 -- 编程先导(编程的本质) 2025 年 9 月 12 日 -- 三天军训感受以及一些七七八八(大学生活记录) 2025 年 9 月 9 日 -- 体检、军训前的准备(大学生活记录) 适合大一新生的计算机基础知识 -- 个人认为足够版 2025 年 9 月 7 日 -- 既来之、则安之(大学生活记录) C++ 程序的内存布局 —— 代码区、全局/静态区、栈区和堆区 联想小新 pro 16 2025 开箱记录:跳过联网、office 激活、更换 win 11 专业版 R 语言中的数学函数 计算机软件著作权申请过程记录 从聚类到回归:用 Python 解析鸢尾花数据集的完整数据科学流程 条件概率、全概率与贝叶斯公式 —— 一篇文章带你死磕概率公式 高中化学物质反应规则表 —— 方程式之间也有规律可循 集合元素性质、关系、子集公式、基本运算 —— 一篇文章带你全方面死磕集合 如何为你的项目选择合适的许可证 -- 全方位指南 解释 3:1 和 9:3:3:1 的详细来源 —— 建立减数分裂与孟德尔遗传学定律之间的联系 重新设置磁盘分区以及 Btrfs 子卷(subvolume)布局的无损方案 零基础实战:用 Docker 启动 dockermailserver 绕过 25 端口封锁搭建个人邮箱服务器 基于 GitHub Actions + OSS 的自托管 APT 仓库全面指南 如何在安装 Debian 时配置 Btrfs 子卷 | 详细分步指南 一个合格的个人网站站长应该知道哪些事? CSS 基础入门教程 —— 选择器、样式声明 HTML 基础入门 —— 基本结构和语法 给博客添加一个文章数据统计的页面 Git 学习笔记(命令备忘表) Anki 牌组选项详细解释与设置 手动编译带有 KernelSU 的 OnePlus 6 内核 安卓类原生系统补全计划 —— PixelExperience(一加 OnePlus) 如何使用 use-sound 为 React 应用添加声音效果 标准化项目的 GitHub Flow 工作流 如何搭建设计自己的博客网站? 使用安知鱼主题同款的 Twikoo 评论组件 配置 GitHub Actions 实现自动化提交百度收录 第00期 | 环境搭建 & 递归 (一) | 基本数列递归 逐字符讲解 C 语言 HelloWorld 程序 2023 年回顾 🤟 Anki:记忆神器,助你高效学习 Gentoo 学习笔记 美化你的 PixelExperience OS Markdown 语法指南 搭建 OneDrive 目录索引 为什么用 65 表示大写字母 A
手把手带你实现单/双链表(C/C++ 数据结构)
qlAD · 2026-02-08 · via qlAD 的技术笔记

cover

一、链表

1、单链表

链表由一系列节点组成,每个节点包含数据和指向下一个节点的指针

请欣赏我的灵魂手绘图:

火车

就如你所看见的一样,链表就像一列火车(list),每节车厢(node)装着货物(data)而且带着一个钩子(pointer)准备连接下一节车厢(next)。

并且一列火车的第一节车厢有点特殊,它这样设计是为了方便人们找到火车头(head)。火车的最后一节车厢也有点特殊,它的钩子没有连接(nullptr),表示火车的结束。

2、双链表

在实际开发中,我们经常需要在链表中进行前向和后向的遍历,这时候单链表就显得有些力不从心了。

而双链表(Doubly Linked List)就是为了解决这个问题而诞生的。

火车

同样可以把双链表想象成一列火车(list),每节车厢(node)装着货物(data)而且带着两个钩子(pointer),一个指向下一节车厢(next),另一个指向上一节车厢(prev)。这样我们就可以方便地在链表中进行前向和后向的遍历了。

并且头结点的前一个钩子(prev)和尾结点的下一个钩子(next)都没有连接(nullptr),表示链表的开始和结束。

一些名词解释:

术语解释
节点(Node)链表中的基本单位,包含数据和指向其他节点的指针。
指针(Pointer)用于连接链表中节点的变量,指向下一个节点或前一个节点。
头结点(Head)链表的第一个节点,通常用来标识链表的开始。
尾结点(Tail)链表的最后一个节点,通常用来标识链表的结束。
前驱指针(Prev Pointer)在双链表中,指向前一个节点的指针。
后继指针(Next Pointer)在单链表和双链表中,指向下一个节点的指针。
前驱节点(Prev Node)在双链表中,当前节点的前一个节点。
后继节点(Next Node)在单链表和双链表中,当前节点的下一个节点。

3、链表的优缺点

相比于数组来说,数组是连续的内存空间,访问元素非常快(O(1)),但是在插入和删除元素时需要移动大量的元素,效率较低(O(n))。

而链表是非连续的内存空间,访问元素需要遍历链表(O(n));插入/删除元素时,若已找到目标位置,仅需修改指针(O(1)),但找位置的过程仍需 O(n),因此整体插入/删除复杂度为 O(n)。

二、单链表的增删改查

1、单链表类

使用类来定义一个单链表节点:

template<typename T>
class ListNode {
public:
    T data_;
    ListNode *next_;

    explicit ListNode(T x) : data_(x), next_(nullptr) {
    }
};

再使用工具函数来将一个数组转换成一个单链表:

template<typename T>
ListNode<T> *arrayToList(const std::vector<T> &arr) {
    if (arr.empty()) {
        return nullptr;
    }

    // 创建链表头结点
    ListNode<T> *head = new ListNode<T>(arr[0]);

    // 依次创建后续节点并连接
    ListNode<T> *current = head;
    for (int i = 1; i < arr.size(); i++) {
        // 让当前节点的后继指针指向新创建的节点
        current->next_ = new ListNode<T>(arr[i]);

        // 将当前节点移动到新创建的节点
        current = current->next_;
    }

    // 返回链表头结点
    return head;
}

2、查找和修改

我要查找链表中的某个元素,首先需要从头结点开始,依次访问每个节点,直到找到目标元素或者遍历完整个链表。

// 创建一个单链表
std::vector<int> arr = {1, 2, 3, 4, 5};
ListNode<int> *head = arrayToList(arr);

// 定义目标元素和新值
int target = 3;
int newValue = 10;

// 查找修改
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    if (p->data_ == target) {
        p->data_ = newValue;
        break;
    }
}

// 遍历打印
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 1 2 10 4 5
}

可能由于网站样式的原因,你看不清代码中的 ->(->),它表示访问指针所指向的对象的成员。!=(!=)不等于。

单链表的查找和修改操作的时间复杂度是 O(n),因为在最坏的情况下需要遍历整个链表。

3、在链表的头部插入和删除

在链表的头部插入一个新节点:

// 创建一个单链表
std::vector<int> arr = {1, 2, 3, 4, 5};
ListNode<int> *head = arrayToList(arr);

// 在链表头部插入一个新节点
int newValue = 0;
ListNode<int> *newNode = new ListNode<int>(newValue);
newNode->next_ = head;  // 将新节点的后继指针指向当前的头结点
head = newNode;         // 更新头结点为新节点

// 遍历打印
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 0 1 2 3 4 5
}

在链表的头部删除一个节点:

// 创建一个单链表
std::vector<int> arr = {1, 2, 3, 4, 5};
ListNode<int> *head = arrayToList(arr);

// 在链表头部删除一个节点
ListNode<int> *temp = head;  // 暂存当前头结点
head = head->next_;          // 更新头结点为下一个节点
delete temp;                 // 释放原头结点的内存

// 遍历打印
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 2 3 4 5
}

在链表的头部插入和删除节点的时间复杂度是 O(1),因为只需要修改几个指针即可完成操作。

4、在链表的尾部插入和删除

在链表的尾部插入一个新节点:

// 创建一个单链表
std::vector<int> arr = {1, 2, 3, 4, 5};
ListNode<int> *head = arrayToList(arr);

// 在链表尾部插入一个新节点
int newValue = 6;
ListNode<int> *newNode = new ListNode<int>(newValue);

for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    if (p->next_ == nullptr) {  // 找到尾结点
        p->next_ = newNode;     // 将尾结点的后继指针指向新节点
        break;
    }
}

// 遍历打印
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 1 2 3 4 5 6
}

在链表的尾部删除一个节点:

// 创建一个单链表
std::vector<int> arr = {1, 2, 3, 4, 5};
ListNode<int> *head = arrayToList(arr);

// 在链表尾部删除一个节点
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    if (p->next_->next_ == nullptr) {  // 找到倒数第二个节点
        p->next_ = nullptr;  // 将倒数第二个节点的后继指针置空
        break;
    }
}

// 遍历打印
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 1 2 3 4
}

在链表的尾部插入和删除节点的时间复杂度是 O(n),因为在最坏的情况下需要遍历整个链表找到尾结点。

5、在链表的中间插入和删除

在中间操作需要先找到目标位置的前一个节点,然后进行插入或删除操作。

在链表的中间插入一个新节点:

// 创建一个单链表
std::vector<int> arr = {1, 2, 3, 4, 5};
ListNode<int> *head = arrayToList(arr);

int newValue = 10;
ListNode<int> *newNode = new ListNode<int>(newValue);

// 在第 2 个节点后面插入一个新节点
// 那么目标位置是第 3 个节点,所以我们需要前驱节点(第 2 个节点)
ListNode<int> *p = head;
for (int i = 0; i < 1; i++) {
    p = p->next_;   // 此时 p 指向第 2 个节点
}

newNode->next_ = p->next_;  // 将新节点的后继指针指向前驱节点的下一个节点
p->next_ = newNode;         // 将前驱节点的后继指针指向新节点

// 遍历打印
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 1 2 10 3 4 5
}

在链表的中间删除一个节点:

// 创建一个单链表
std::vector<int> arr = {1, 2, 3, 4, 5};
ListNode<int> *head = arrayToList(arr);

// 删除第 3 个节点
// 那么目标位置是第 3 个节点,所以我们需要前驱节点(第 2 个节点)
ListNode<int> *p = head;
for (int i = 0; i < 1; i++) {
    p = p->next_;   // 此时 p 指向第 2 个节点
}

p->next_ = p->next_->next_;  // 将前驱节点的后继指针指向目标节点的下一个节点

// 遍历打印
for (ListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 1 2 4 5
}

在链表的中间插入和删除节点的时间复杂度是 O(n),因为在最坏的情况下需要遍历链表找到目标位置。

三、双链表的增删改查

1、双链表类

使用类来定义一个双链表节点:

template<typename T>
class DoublyListNode {
public:
    T data_;
    DoublyListNode *prev_;
    DoublyListNode *next_;

    explicit DoublyListNode(T x) : data_(x), prev_(nullptr), next_(nullptr) {
    }
};

再使用工具函数来将一个数组转换成一个双链表:

template<typename T>
DoublyListNode<T> *arrayToDoublyList(const std::vector<T> &arr) {
    if (arr.empty()) {
        return nullptr;
    }

    // 创建链表头结点
    DoublyListNode<T> *head = new DoublyListNode<T>(arr[0]);
    DoublyListNode<T> *current = head;

    // 依次建立链接
    for (int i = 1; i < arr.size(); i++) {
        // 将当前节点的后继指针指向新创建的节点
        current->next_ = new DoublyListNode<T>(arr[i]);

        // 将新节点的前驱指针指向当前节点
        current->next_->prev_ = current;

        // 将当前节点移动到新节点
        current = current->next_;
    }

    // 返回链表头结点
    return head;
}

2、查找和修改

双链表的查找和修改操作与单链表类似,只不过在双链表中我们可以方便地进行前向和后向的遍历。

// 创建一个双链表
std::vector<int> arr = {1, 2, 3, 4, 5};
DoublyListNode<int> *head = arrayToDoublyList(arr);
DoublyListNode<int> *tail = nullptr;

// 先找到尾结点
if (head == nullptr) {
    tail = nullptr;
} else {
    DoublyListNode<int> *p = head;
    while (p->next_ != nullptr) {
        p = p->next_;
    }
    tail = p;
}

// 定义目标元素和新值
int target = 3;
int newValue = 10;

// 从前向后查找修改
for (DoublyListNode<int> *p = head; p != nullptr; p = p->next_) {
    if (p->data_ == target) {
        p->data_ = newValue;
        break;
    }
}

// 遍历打印
for (DoublyListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 1 2 10 4 5
}

std::cout << std::endl;

// 从后向前查找修改
for (DoublyListNode<int> *p = tail; p != nullptr; p = p->prev_) {
    if (p->data_ == target) {
        p->data_ = newValue;
        break;
    }
}

// 遍历打印
for (DoublyListNode<int> *p = head; p != nullptr; p = p->next_) {
    std::cout << p->data_ << " ";   // 1 2 10 4 5
}

双链表的查找和修改操作的时间复杂度也是 O(n),因为在最坏的情况下需要遍历整个链表。

3、在双链表的头部插入和删除

在双链表的头部插入一个新节点:

// 创建一个双链表
std::vector<int> arr = {1, 2, 3, 4, 5};
DoublyListNode<int> *head = arrayToDoublyList(arr);

// 在链表头部插入一个新节点
int newValue = 0;
DoublyListNode<int> *newNode = new DoublyListNode<int>(newValue);
newNode->next_ = head;  // 将新节点的后继指针指向当前的头结点
head->prev_ = newNode;  // 将当前头结点的前驱指针指向新节点
head = newNode;         // 更新头结点为新节点

// 0 1 2 3 4 5

在双链表的头部删除一个节点:

// 创建一个双链表
std::vector<int> arr = {1, 2, 3, 4, 5};
DoublyListNode<int> *head = arrayToDoublyList(arr);

// 在链表头部删除一个节点
DoublyListNode<int> *temp = head;  // 暂存当前头结点
head = head->next_;          // 更新头结点为下一个节点
head->prev_ = nullptr;     // 将新的头结点的前驱指针置空
temp->next_ = nullptr;   // 将原头结点的后继指针置空

// 2 3 4 5

在双链表的头部插入和删除节点的时间复杂度是 O(1),因为只需要修改几个指针即可完成操作。

4、在双链表的尾部插入和删除

在双链表的尾部插入一个新节点:

// 创建一个双链表
std::vector<int> arr = {1, 2, 3, 4, 5};
DoublyListNode<int> *head = arrayToDoublyList(arr);
DoublyListNode<int> *tail = nullptr;

// 先找到尾结点
if (head == nullptr) {
    tail = nullptr;
} else {
    DoublyListNode<int> *p = head;
    while (p->next_ != nullptr) {
        p = p->next_;
    }
    tail = p;
}

int newValue = 6;
DoublyListNode<int> *newNode = new DoublyListNode<int>(newValue);
tail->next_ = newNode;  // 将当前尾结点的后继指针指向新节点
newNode->prev_ = tail;  // 将新节点的前驱指针指向当前尾结点
tail = newNode;         // 更新尾结点为新节点
// 1 2 3 4 5 6

在双链表的尾部删除一个节点:

// 创建一个双链表
std::vector<int> arr = {1, 2, 3, 4, 5};
DoublyListNode<int> *head = arrayToDoublyList(arr);
DoublyListNode<int> *tail = nullptr;

// 先找到尾结点
if (head == nullptr) {
    tail = nullptr;
} else {
    DoublyListNode<int> *p = head;
    while (p->next_ != nullptr) {
        p = p->next_;
    }
    tail = p;
}

// 在链表尾部删除一个节点
tail->prev_->next_ = nullptr;  // 将倒数第二个节点的后继指针置空
tail->prev_ = nullptr;         // 将尾结点的前驱指针置空
// 1 2 3 4

如果我们已经在双链表中维护一个尾结点的指针,那么时间复杂度也是 O(1)。当前的实现中需要先找到尾结点,所以时间复杂度是 O(n)。

5、在双链表的中间插入和删除

在双链表的中间插入和删除节点的操作与单链表类似,只不过在双链表中我们需要同时修改前驱节点和后继节点的指针。

在双链表的中间插入一个新节点:

// 创建一个双链表
std::vector<int> arr = {1, 2, 3, 4, 5};
DoublyListNode<int> *head = arrayToDoublyList(arr);

int newValue = 10;
DoublyListNode<int> *newNode = new DoublyListNode<int>(newValue);

// 在第 2 个节点后面插入一个新节点
// 那么目标位置是第 3 个节点,所以我们需要前驱节点(第 2 个节点)
DoublyListNode<int> *p = head;
for (int i = 0; i < 1; i++) {
    p = p->next_;   // 此时 p 指向第 2 个节点
}

newNode->next_ = p->next_;  // 将新节点的后继指针指向前驱节点的下一个节点
newNode->prev_ = p;         // 将新节点的前驱指针指向前驱节点
p->next_->prev_ = newNode;  // 将前驱节点的下一个节点的前驱指针指向新节点
p->next_ = newNode;         // 将前驱节点的后继指针指向新节点
// 1 2 10 3 4 5

在双链表的中间删除一个节点:

// 创建一个双链表
std::vector<int> arr = {1, 2, 3, 4, 5};
DoublyListNode<int> *head = arrayToDoublyList(arr);

// 删除第 3 个节点
// 那么目标位置是第 3 个节点,所以我们需要前驱节点(第 2 个节点)
DoublyListNode<int> *p = head;
for (int i = 0; i < 1; i++) {
    p = p->next_;   // 此时 p 指向第 2 个节点
}

DoublyListNode<int> *temp = p->next_;  // 暂存目标节点
p->next_ = temp->next_;  // 将前驱节点的后继指针指向目标节点的下一个节点
temp->next_->prev_ = p;  // 将目标节点的下一个节点的前驱指针指向前驱节点
temp->prev_ = nullptr;   // 将目标节点的前驱指针置空
temp->next_ = nullptr;   // 将目标节点的后继指针置空
// 1 2 4 5

在双链表的中间插入和删除节点的时间复杂度也是 O(n),因为在最坏的情况下需要遍历链表找到目标位置。

是不是很好玩?是不是很有成就感?是不是很想自己动手实现一下?

四、单/双链表的实现

其实上面我们已经实现了单链表和双链表的基本操作了,不过我们还没有把它们封装成一个类来使用。

接下来会使用类的封装并且使用虚拟头尾结点来实现一个更完整的单链表和双链表。

1、什么是虚拟头尾结点?

虚拟头尾结点(Dummy Head/Tail Node)是链表中的一个特殊节点,它不存储任何有效数据,只是为了简化链表的操作而存在。

在上文中增删改查一共分成三类:

  • 在链表的头部操作
  • 在链表的尾部操作
  • 在链表的中间操作

而如果使用虚拟节点,则只剩下了在链表的中间操作,因为头部和尾部的操作都可以看成虚拟节点的中间操作。

2、双链表完整代码

#include <iostream>
#include <string>
#include <stdexcept>

template<typename T>
class MyLinkedList {
private:
    struct ListNode {
        T data_;
        ListNode *prev_;
        ListNode *next_;

        // 无参构造(虚拟节点专用)
        ListNode() : prev_(nullptr), next_(nullptr) {}
        // 有参构造(有效节点专用)
        explicit ListNode(T x) : data_(x), prev_(nullptr), next_(nullptr) {}
    };

    ListNode *head_; // 虚拟头结点
    ListNode *tail_; // 虚拟尾结点
    int size_; // 链表的实际大小(不包括虚拟节点)

public:
    // 构造函数:初始化虚拟头尾节点
    MyLinkedList() : size_(0) {
        head_ = new ListNode(); // 创建虚拟头结点
        tail_ = new ListNode(); // 创建虚拟尾结点
        head_->next_ = tail_; // 虚拟头结点的后继指针指向虚拟尾结点
        tail_->prev_ = head_; // 虚拟尾结点的前驱指针指向虚拟头结点
    }

    // 析构函数:释放所有节点
    ~MyLinkedList() {
        ListNode *current = head_;
        while (current != nullptr) {
            ListNode *next = current->next_;
            delete current;
            current = next;
        }
        head_ = nullptr;
        tail_ = nullptr;
        size_ = 0;
    }

    // 增加
    void add(int index, T value) {
        checkPositionIndex(index);
        ListNode *prevNode = index == 0 ? head_ : getNode(index - 1); // 获取前驱节点
        ListNode *nextNode = prevNode->next_; // 获取后继节点
        ListNode *newNode = new ListNode(value); // 创建新节点

        // 双向链接 prevNode <-> newNode <-> nextNode
        prevNode->next_ = newNode; // 将前驱节点的后继指针指向新节点
        newNode->prev_ = prevNode; // 将新节点的前驱指针指向前驱节点
        newNode->next_ = nextNode; // 将新节点的后继指针指向后继节点
        nextNode->prev_ = newNode; // 将后继节点的前驱指针指向新节点

        size_++;
    }

    void addFirst(T value) {
        add(0, value);
    }

    void addLast(T value) {
        add(size_, value);
    }

    // 删除
    T remove(int index) {
        checkElementIndex(index);

        // prevNode <-> targetNode <-> nextNode
        ListNode *targetNode = getNode(index); // 直接获取目标节点
        ListNode *prevNode = targetNode->prev_; // 直接从目标节点取前驱
        ListNode *nextNode = targetNode->next_; // 直接从目标节点取后继

        // 断开链接 prevNode <-> nextNode
        prevNode->next_ = nextNode; // 将前驱节点的后继指针指向后继节点
        nextNode->prev_ = prevNode; // 将后继节点的前驱指针指向前驱节点

        T removedData = targetNode->data_;
        delete targetNode;
        size_--;

        return removedData;
    }

    T removeFirst() {
        return remove(0);
    }

    T removeLast() {
        return remove(size_ - 1);
    }

    // 修改
    T set(int index, T value) {
        checkElementIndex(index);
        ListNode *targetNode = getNode(index);
        T oldValue = targetNode->data_;
        targetNode->data_ = value;
        return oldValue;
    }

    // 查找
    T get(int index) const {
        checkElementIndex(index);
        ListNode *targetNode = getNode(index);
        return targetNode->data_;
    }

    T getFirst() const {
        return get(0);
    }

    T getLast() const {
        return get(size_ - 1);
    }

    // 其他
    int getSize() const {
        return size_;
    }

    bool isEmpty() const {
        return size_ == 0;
    }

    void printList() const {
        if (isEmpty()) {
            std::cout << "Empty list" << std::endl;
            return;
        }
        ListNode *current = head_->next_;
        while (current != tail_) {
            std::cout << current->data_ << " ";
            current = current->next_;
        }
        std::cout << std::endl;
    }

private:
    // 获取指定索引的有效元素节点(0 ~ size_-1)
    ListNode *getNode(int index) const {
        checkElementIndex(index);

        ListNode *current = nullptr;
        if (index < size_ / 2) {
            // 前半部分:从头遍历
            current = head_->next_;
            for (int i = 0; i < index; i++) {
                current = current->next_;
            }
        } else {
            // 后半部分:从尾遍历
            current = tail_->prev_;
            for (int i = size_ - 1; i > index; i--) {
                current = current->prev_;
            }
        }
        return current;
    }

    // 检查索引是否为有效元素索引(0 ~ size_-1)
    bool isElementIndex(int index) const {
        return index >= 0 && index < size_;
    }

    // 检查索引是否为有效插入位置(0 ~ size_)
    bool isPositionIndex(int index) const {
        return index >= 0 && index <= size_;
    }

    void checkElementIndex(int index) const {
        if (!isElementIndex(index)) {
            throw std::out_of_range(
                "Invalid element index: " + std::to_string(index) + ", valid range: [0, " + std::to_string(size_ - 1) +
                "]"
            );
        }
    }

    void checkPositionIndex(int index) const {
        if (!isPositionIndex(index)) {
            throw std::out_of_range(
                "Invalid position index: " + std::to_string(index) + ", valid range: [0, " + std::to_string(size_) + "]"
            );
        }
    }
};

// 使用示例
int main() {
    MyLinkedList<int> list;
    list.addLast(1);
    list.addLast(2);
    list.addLast(3);
    list.addFirst(0); // 0 1 2 3
    std::cout << "Size: " << list.getSize() << std::endl; // 4
    std::cout << "First element: " << list.getFirst() << std::endl; // 0
    std::cout << "Last element: " << list.getLast() << std::endl; // 3
    std::cout << "List content: ";
    list.printList(); // 0 1 2 3

    list.set(1, 10); // 修改索引 1 为 10 → 0 10 2 3
    std::cout << "After set index 1 to 10: ";
    list.printList(); // 0 10 2 3
    std::cout << "Element at index 1: " << list.get(1) << std::endl; // 10

    list.removeFirst(); // 删除第一个元素 → 10 2 3
    std::cout << "After remove first: ";
    list.printList(); // 10 2 3
    std::cout << "First element: " << list.getFirst() << std::endl; // 10

    list.removeLast(); // 删除最后一个元素 → 10 2
    std::cout << "After remove last: ";
    list.printList(); // 10 2
    std::cout << "Last element: " << list.getLast() << std::endl; // 2

    std::cout << "Final list: ";
    for (int i = 0; i < list.getSize(); i++) {
        std::cout << list.get(i) << " "; // 10 2
    }
    std::cout << std::endl;

    // 测试边界:删除所有元素后再操作
    list.removeFirst();
    list.removeFirst();
    std::cout << "After remove all elements, size: " << list.getSize() << std::endl; // 0
    list.printList(); // Empty list
    return 0;
}

五、链表 List

在 C++ 中,STL 提供了一个非常强大的链表容器——std::list,它是一个双向链表。

1、简单实例

创建和初始化 std::list 非常简单:

#include <iostream>
#include <list>

int main() {
    std::list<int> lst1;                  // 空的list
    std::list<int> lst2(5);               // 包含5个默认初始化元素的list
    std::list<int> lst3(5, 10);           // 包含5个元素,每个元素为10
    std::list<int> lst4 = {1, 2, 3, 4};   // 使用初始化列表

    return 0;
}

简单示例:

#include <iostream>
#include <list>

int main() {
    // 创建一个整数类型的列表
    std::list<int> numbers;

    // 向列表中添加元素
    numbers.push_back(10);
    numbers.push_back(20);
    numbers.push_back(30);

    // 访问并打印列表的第一个元素
    std::cout << "First element: " << numbers.front() << std::endl;

    // 访问并打印列表的最后一个元素
    std::cout << "Last element: " << numbers.back() << std::endl;

    // 遍历列表并打印所有元素
    std::cout << "List elements: ";
    for (std::list<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 删除列表中的最后一个元素
    numbers.pop_back();

    // 再次遍历列表并打印所有元素
    std::cout << "List elements after removing the last element: ";
    for (std::list<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

2、常用成员函数

list<int> lst1 = {1, 2, 3, 4, 5};
list<int> lst2 = {6, 7, 8, 9, 10};
成员函数说明效果
lst1.push_back(6)在列表末尾添加一个元素。lst1 变为 {1, 2, 3, 4, 5, 6}
lst1.push_front(0)在列表开头添加一个元素。lst1 变为 {0, 1, 2, 3, 4, 5, 6}
lst1.pop_back()删除列表末尾的元素。lst1 变为 {0, 1, 2, 3, 4, 5}
lst1.pop_front()删除列表开头的元素。lst1 变为 {1, 2, 3, 4, 5}
lst1.front()返回列表的第一个元素。返回 1
lst1.back()返回列表的最后一个元素。返回 5
lst1.size()返回列表中元素的数量。返回 5
lst1.empty()检查列表是否为空。返回 false
lst1.clear()删除列表中的所有元素。lst1 变为 {}
lst1.insert(lst1.begin(), 1)在指定位置插入一个元素。lst1(清空后)变为 {1}
lst2.erase(lst2.begin())删除指定位置的元素。lst2 变为 {7, 8, 9, 10}
lst2.remove(7)删除列表中所有与指定值相等的元素。lst2 变为 {8, 9, 10}
lst2.begin()返回指向列表第一个元素的迭代器。返回指向 8 的迭代器
lst2.end()返回指向列表末尾的迭代器。返回指向列表末尾(最后一个元素的下一个位置)的迭代器
lst2.sort()对列表中的元素进行排序。lst2 变为 {8, 9, 10}
lst2.reverse()反转列表中的元素顺序。lst2 变为{10, 9, 8}
lst1.merge(lst2)将另一个列表合并到当前列表中lst1 变为 {1, 10, 9, 8},lst2 变为 {}

完整代码:

#include <iostream>
#include <list>

int main() {
    std::list<int> lst1 = {1, 2, 3, 4, 5};
    std::list<int> lst2 = {6, 7, 8, 9, 10};

    lst1.push_back(6); // lst1 变为 `{1, 2, 3, 4, 5, 6}`
    lst1.push_front(0); // lst1 变为 `{0, 1, 2, 3, 4, 5, 6}`
    lst1.pop_back(); // lst1 变为 `{0, 1, 2, 3, 4, 5}`
    lst1.pop_front(); // lst1 变为 `{1, 2, 3, 4, 5}`
    std::cout << lst1.front() << std::endl; // 1
    std::cout << lst1.back() << std::endl; // 5
    std::cout << lst1.size() << std::endl; // 5
    std::cout << lst1.empty() << std::endl; // 0(false)
    lst1.clear(); // lst1 变为 `{}`
    lst1.insert(lst1.begin(), 1); // lst1 变为 `{1}`
    lst2.erase(lst2.begin()); // lst2 变为 `{7, 8, 9, 10}`
    lst2.remove(7); // lst2 变为 `{8, 9, 10}`
    lst2.sort(); // lst2 变为 `{8, 9, 10}`
    lst2.reverse(); // lst2 变为 `{10, 9, 8}`
    lst1.merge(lst2); // lst1 变为` {1, 10, 9, 8}`,lst2 变为 `{}`

    for (int num: lst1) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    for (int num: lst2) {
        std::cout << num << " ";
    }

    return 0;
}