Skip to content

第3章 list 容器详解 ​

list 是 STL 中的双向链表容器,底层由双向循环链表实现,支持在任意位置高效插入、删除元素,但不支持随机访问,是频繁进行中间操作的最优选择。


🎯 本章学习目标 ​

学完本章,你将能够:

  1. 理解 list 的双向链表底层结构
  2. 熟练使用 list 的增删改查等基本操作
  3. 掌握 list 的迭代器特性(双向迭代器、稳定性)
  4. 理解 list 为什么不支持随机访问
  5. 掌握 list 独有的操作(sort、merge、reverse 等)
  6. 区分 list 与 vector、deque 的核心区别
  7. 理解迭代器失效的情况(list 与 vector 的区别)
  8. 根据场景选择合适的序列式容器

🗺️ 本章知识图谱 ​

list 双向链表
├── 核心特性
│   ├── 双向循环链表
│   ├── 任意位置插入删除 O(1)
│   ├── 不支持随机访问
│   └── 迭代器稳定
├── 基本用法
│   ├── 定义与头文件
│   ├── 头尾操作(push/pop front/back)
│   ├── 中间插入删除(insert/erase)
│   ├── 访问头尾(front/back)
│   └── 遍历(迭代器/范围for)
├── 专属操作
│   ├── sort(链表排序)
│   ├── merge(合并有序链表)
│   ├── reverse(反转)
│   ├── remove(按值删除)
│   └── unique(去重)
├── 底层原理
│   ├── 节点结构(prev + data + next)
│   ├── 双向循环
│   └── 迭代器实现
├── 与其他容器对比
│   ├── vs vector
│   └── vs deque
├── 注意事项
│   ├── 不支持 [] 随机访问
│   ├── 迭代器只能 ++ --
│   ├── 空间开销大
│   └── 缓存不友好
└── 适用场景与选型

一、list 核心特性 ​

🍎 生活比喻:list 就像一串珍珠项链——每颗珍珠(节点)通过线(指针)连起来,每颗珍珠都知道前一颗和后一颗在哪。你可以在任意两颗珍珠之间插入新珍珠(中间插入 O(1)),只需要把线重新接一下就行。但你不能直接跳到第 10 颗珍珠(不支持随机访问),必须从头一颗一颗数过去。

  1. 双向链表结构:每个节点保存前驱(prev)和后继(next)指针,支持双向遍历;
  2. 高效插入/删除:在任意位置插入、删除元素效率均为 O(1)(只要有迭代器指向该位置);
  3. 不支持随机访问:不能使用 [] 或 at() 访问元素,只能通过迭代器逐个遍历;
  4. 迭代器稳定性:插入、删除元素不会导致原有迭代器失效(仅被删除节点的迭代器失效);
  5. 自带头尾操作:支持 push_front、pop_front、push_back、pop_back 等高效头尾操作。

🔥 高频考点:list 的任意位置插入删除 O(1)、不支持随机访问、迭代器稳定,这些是选择题的常考点。


二、list 基础用法 ​

使用 list 需包含 <list> 头文件。

cpp
#include <iostream>
#include <list>     // list 头文件
using namespace std;

int main() {
    // 1. 定义 list(多种方式)
    list<int> lst1;                  // 空 list
    list<int> lst2(5, 0);            // 5 个元素,都初始化为 0
    list<int> lst3 = {1, 2, 3, 4, 5}; // 初始化列表(C++11)
    
    // 2. 头尾插入(都是 O(1))
    lst1.push_back(10);    // 尾插
    lst1.push_back(20);
    lst1.push_front(5);    // 头插
    lst1.push_front(1);
    // 此时 lst1:1 -> 5 -> 10 -> 20
    
    // 3. 遍历链表(只能用迭代器或范围 for,不能用 [])
    cout << "遍历结果:";
    for (int num : lst1) {
        cout << num << " ";   // 输出:1 5 10 20
    }
    cout << endl;
    
    // 4. 访问头尾元素
    cout << "头部元素:" << lst1.front() << endl;  // 输出:1
    cout << "尾部元素:" << lst1.back() << endl;   // 输出:20
    
    // 5. 指定位置插入(需要先找到迭代器位置,O(1) 插入)
    auto it = lst1.begin();
    it++;                   // 指向第二个元素(5)
    lst1.insert(it, 8);     // 在 5 前面插入 8
    // 此时 lst1:1 -> 8 -> 5 -> 10 -> 20
    
    // 6. 删除元素
    lst1.pop_front();        // 删除头部,lst1:8 -> 5 -> 10 -> 20
    lst1.pop_back();         // 删除尾部,lst1:8 -> 5 -> 10
    lst1.erase(lst1.begin()); // 删除迭代器指向元素,lst1:5 -> 10
    
    // 7. 常用操作
    cout << "链表大小:" << lst1.size() << endl;   // 输出:2
    cout << "是否为空:" << (lst1.empty() ? "是" : "否") << endl;
    lst1.clear(); // 清空链表
    
    return 0;
}

操作汇总表 ​

操作语法说明时间复杂度
头插push_front(元素)在头部插入元素O(1)
尾插push_back(元素)在尾部插入元素O(1)
头删pop_front()删除头部元素O(1)
尾删pop_back()删除尾部元素O(1)
指定位置插入insert(迭代器, 元素)在迭代器位置插入元素O(1)
指定位置删除erase(迭代器)删除迭代器指向的元素O(1)
访问头部front()返回头部元素引用O(1)
访问尾部back()返回尾部元素引用O(1)
清空clear()删除所有元素O(n)
判空empty()是否为空O(1)
获取大小size()返回元素个数O(1)

💡 小技巧:list 的 insert 和 erase 都是 O(1),前提是你已经有了指向该位置的迭代器。如果需要先查找位置,查找本身是 O(n) 的。


三、list 的专属操作 ​

因为 list 是链表结构,它有一些独有的成员函数(算法库中的同名函数对 list 效率低,所以 list 自己实现了):

1. sort(排序) ​

cpp
list<int> lst = {3, 1, 4, 1, 5, 9, 2, 6};
lst.sort();  // 升序排序
// 结果:1 1 2 3 4 5 6 9

⚠️ 易错点:list 不能用 sort(lst.begin(), lst.end())(算法库的 sort),因为算法库的 sort 需要随机访问迭代器,而 list 只有双向迭代器。所以 list 自己提供了 sort 成员函数。

2. reverse(反转) ​

cpp
list<int> lst = {1, 2, 3, 4, 5};
lst.reverse();  // 反转链表
// 结果:5 4 3 2 1

3. remove(按值删除) ​

cpp
list<int> lst = {1, 2, 3, 2, 4, 2, 5};
lst.remove(2);  // 删除所有值为 2 的元素
// 结果:1 3 4 5

4. unique(去重) ​

cpp
list<int> lst = {1, 1, 2, 2, 3, 3, 3};
lst.unique();  // 删除相邻的重复元素(只保留一个)
// 结果:1 2 3

⚠️ 易错点:unique 只能删除相邻的重复元素。如果要完全去重,需要先 sort 再 unique。

5. merge(合并有序链表) ​

cpp
list<int> lst1 = {1, 3, 5};
list<int> lst2 = {2, 4, 6};
lst1.merge(lst2);  // 合并两个有序链表,合并后 lst2 为空
// lst1 结果:1 2 3 4 5 6
// lst2 结果:空

🔥 高频考点:list 有自己的 sort、reverse、merge 等成员函数,不能用算法库的版本,这是选择题和判断题的常考点。


四、底层原理(进阶) ​

1. 节点结构 ​

list 的每个节点包含三部分:

struct Node {
    T data;       // 数据
    Node* prev;   // 前驱指针
    Node* next;   // 后继指针
};

2. 双向循环链表 ​

list 通常实现为双向循环链表,即:

  • 头节点的 prev 指向尾节点
  • 尾节点的 next 指向头节点
  • 这样头尾操作都很方便

3. 迭代器 ​

list 的迭代器是双向迭代器,只支持 ++ 和 -- 操作,不支持 +n、-n(那是随机访问迭代器才有的)。

cpp
list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
it++;    // ✅ 正确,指向下一个
it--;    // ✅ 正确,指向上一个
// it += 2; // ❌ 错误!list 迭代器不支持跳跃

五、迭代器稳定性 ​

这是 list 的一个重要特性,也是和 vector 的核心区别之一。

vector 的迭代器失效 ​

vector 在插入元素时,如果发生扩容,所有迭代器都会失效。即使不扩容,插入点之后的迭代器也会失效。

list 的迭代器稳定 ​

list 在插入、删除元素时,只有被删除的那个节点的迭代器会失效,其他所有迭代器都仍然有效。

🍎 生活比喻:就像项链上的珍珠——摘掉一颗珍珠,其他珍珠之间的连接不变,你仍然可以从任意一颗珍珠开始遍历。而 vector 像公交车——如果中间加了座位,后面的座位都往后挪了,你之前记的座位号就不对了。

🔥 高频考点:list 迭代器的稳定性(插入删除不影响其他迭代器)是选择题和判断题的常考点。


六、list 与其他容器对比 ​

特性vectordequelist
内存结构一整块连续分段连续不连续(链表)
随机访问✅ O(1) 最快✅ O(1) 稍慢❌ O(n)
尾插尾删✅ O(1) 均摊✅ O(1)✅ O(1)
头插头删❌ O(n)✅ O(1)✅ O(1)
中间插入删除❌ O(n)❌ O(n)✅ O(1)
迭代器类型随机访问随机访问双向
迭代器稳定性差(扩容全失效)一般好(仅删除节点失效)
空间开销小中大(指针开销)
缓存友好性最好一般最差

七、常见误区与注意事项 ​

⚠️ 易错点 1:用 [] 访问元素。list 不支持 [] 和 at(),不能随机访问。只能用迭代器遍历或用 front/back 访问头尾。

⚠️ 易错点 2:迭代器可以 +n。list 的迭代器是双向迭代器,只支持 ++ 和 --,不支持 +n、-n、+= 等跳跃操作。要移动 n 步,只能循环 n 次 ++。

⚠️ 易错点 3:用算法库的 sort 排序 list。算法库的 sort 需要随机访问迭代器,list 不满足。list 有自己的 sort 成员函数。

⚠️ 易错点 4:list 的 size() 是 O(1) 还是 O(n)。在 C++11 及以后,list 的 size() 是 O(1) 的(有一个计数器成员)。但在一些老版本编译器中可能是 O(n),需要遍历计数。

⚠️ 易错点 5:list 空间效率高。实际上 list 的空间效率很低,因为每个节点都要存两个指针(prev 和 next),对于 int 这种小类型,指针的开销甚至比数据本身还大。

⚠️ 易错点 6:list 插入删除一定比 vector 快。不一定!list 插入虽然是 O(1),但找到插入位置需要 O(n) 遍历,而且链表节点的分配释放也有开销。对于小数据量,vector 可能更快(因为缓存友好)。


八、适用场景总结 ​

  1. 频繁在任意位置插入/删除:这是 list 最大的优势,只要有迭代器,插入删除都是 O(1);
  2. 需要迭代器稳定:如果插入删除后其他迭代器必须保持有效,list 是最佳选择;
  3. 频繁头尾操作:list 的头尾操作也是 O(1),和 deque 一样;
  4. 大数据量 + 频繁中间操作:数据量很大且经常在中间增删时,list 的优势才明显。

不适合的场景:

  • 需要随机访问 → 用 vector 或 deque
  • 数据量小、操作简单 → 用 vector(缓存友好,更快)
  • 空间敏感 → 用 vector(list 指针开销大)

九、🔖 本章小结 ​

list 是一种双向链表容器,最大的优势是任意位置插入删除 O(1) 和迭代器稳定,但代价是不支持随机访问、空间开销大、缓存不友好。它和 vector 正好互补——vector 擅长随机访问和尾插,list 擅长中间插入删除。在实际使用中,需要根据具体场景选择最合适的容器。


十、📝 实战练习 ​

练习题 1(选择题) ​

题目: list 的底层数据结构是?

A. 连续内存的动态数组 B. 双向链表 C. 红黑树 D. 哈希表

🔑 点击查看答案与解析

答案: B

**解析:**list 底层是双向链表,每个节点有前驱和后继指针。A 是 vector 的底层结构,C 是 set/map 的底层结构,D 是 unordered_set/unordered_map 的底层结构。


练习题 2(判断题) ​

题目: list 支持使用 [] 进行随机访问( )

🔑 点击查看答案与解析

答案: 错误

**解析:**list 是双向链表,不支持随机访问,没有 [] 和 at() 操作符。只能通过迭代器逐个遍历,或用 front()/back() 访问头尾元素。


练习题 3(选择题) ​

题目: 在 list 中,已知迭代器指向某个位置,在该位置插入一个元素的时间复杂度是?

A. O(1) B. O(log n) C. O(n) D. O(n^2)

🔑 点击查看答案与解析

答案: A

**解析:**list 是双向链表,只要有了指向插入位置的迭代器,插入操作只需要修改相邻节点的指针,时间复杂度是 O(1)。这是 list 最大的优势。

注意:如果需要先查找插入位置,查找本身是 O(n) 的。


练习题 4(选择题) ​

题目: 以下关于 list 迭代器的说法,正确的是?

A. list 的迭代器是随机访问迭代器 B. list 插入元素后,所有迭代器都会失效 C. list 删除元素后,只有被删除节点的迭代器失效 D. list 的迭代器支持 += n 操作

🔑 点击查看答案与解析

答案: C

解析:

  • A 错误:list 的迭代器是双向迭代器,不是随机访问迭代器
  • B 错误:list 插入元素不会导致任何迭代器失效
  • C 正确:list 删除元素只有被删除节点的迭代器失效,其他都有效
  • D 错误:只有随机访问迭代器才支持 += n,list 只支持 ++ 和 --

练习题 5(程序阅读题) ​

题目: 阅读以下代码,写出输出结果:

cpp
#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> lst = {1, 2, 3, 4, 5};
    lst.push_front(0);
    lst.push_back(6);
    lst.pop_front();
    lst.pop_back();
    
    auto it = lst.begin();
    it++;
    lst.insert(it, 10);
    
    for (int x : lst) {
        cout << x << " ";
    }
    cout << endl;
    return 0;
}
🔑 点击查看答案与解析

答案: 1 10 2 3 4 5

解析:

  • 初始:1 2 3 4 5
  • push_front(0) → 0 1 2 3 4 5
  • push_back(6) → 0 1 2 3 4 5 6
  • pop_front() → 1 2 3 4 5 6
  • pop_back() → 1 2 3 4 5
  • it = begin() 指向 1,it++ 后指向 2
  • insert(it, 10) 在 it 前面(即 2 前面)插入 10 → 1 10 2 3 4 5

最终输出:1 10 2 3 4 5


练习题 6(选择题) ​

题目: 以下哪个操作,list 有自己的成员函数版本,而不能直接用 algorithm 库的版本?

A. find B. sort C. count D. for_each

🔑 点击查看答案与解析

答案: B

**解析:**algorithm 库的 sort 需要随机访问迭代器(用了快排等需要随机访问的算法),而 list 只有双向迭代器,所以不能用 sort(lst.begin(), lst.end())。list 自己提供了 sort 成员函数(用归并排序实现)。

find、count、for_each 只需要单向或双向迭代器,list 可以直接使用 algorithm 库的版本。


练习题 7(编程题) ​

题目: 输入 n 个整数,存入 list 中,然后删除所有值为 x 的元素,最后输出剩余元素。

**输入格式:**第一行两个整数 n 和 x,第二行 n 个整数 **输出格式:**一行,删除后的元素,用空格隔开

样例输入:

7 2
1 2 3 2 4 2 5

样例输出:

1 3 4 5
🔑 点击查看答案与解析

参考代码(方法一:用 list 的 remove 成员函数):

cpp
#include <iostream>
#include <list>
using namespace std;

int main() {
    int n, x;
    cin >> n >> x;
    list<int> lst;
    
    for (int i = 0; i < n; i++) {
        int num;
        cin >> num;
        lst.push_back(num);
    }
    
    lst.remove(x);  // 删除所有值为 x 的元素
    
    bool first = true;
    for (int num : lst) {
        if (!first) cout << " ";
        cout << num;
        first = false;
    }
    cout << endl;
    
    return 0;
}

参考代码(方法二:手动遍历删除,演示 erase 的用法):

cpp
#include <iostream>
#include <list>
using namespace std;

int main() {
    int n, x;
    cin >> n >> x;
    list<int> lst;
    
    for (int i = 0; i < n; i++) {
        int num;
        cin >> num;
        lst.push_back(num);
    }
    
    // 注意:erase 会使当前迭代器失效,所以要接收返回值
    for (auto it = lst.begin(); it != lst.end(); ) {
        if (*it == x) {
            it = lst.erase(it);  // erase 返回下一个元素的迭代器
        } else {
            it++;
        }
    }
    
    bool first = true;
    for (int num : lst) {
        if (!first) cout << " ";
        cout << num;
        first = false;
    }
    cout << endl;
    
    return 0;
}

解析:

  • 方法一:直接用 list 的 remove 成员函数,最简单,一行搞定。
  • 方法二:手动遍历删除,注意 erase 之后迭代器会失效,要用 erase 的返回值(指向下一个元素)。这是 list 遍历删除的标准写法,vector 也是类似的。

方法一是 list 专属的便捷操作,方法二更通用,也更考验对迭代器的理解。


十一、📊 考点总结表 ​

知识点重要程度出题形式易错点
底层是双向链表⭐⭐⭐⭐⭐选择题、判断题与 vector 混淆
不支持随机访问⭐⭐⭐⭐⭐选择题、判断题以为可以用 []
任意位置插入删除 O(1)⭐⭐⭐⭐⭐选择题忘了前提是有迭代器
迭代器稳定性⭐⭐⭐⭐选择题、判断题与 vector 混淆
双向迭代器(只有 ++ --)⭐⭐⭐选择题以为支持 += n
有自己的 sort 成员函数⭐⭐⭐⭐选择题、判断题用 algorithm 的 sort
空间开销大(指针)⭐⭐⭐选择题以为 list 更省空间
remove 按值删除⭐⭐⭐选择题、编程题不知道有这个函数
与 vector/deque 对比⭐⭐⭐⭐⭐选择题、简答题三者区别记不清

⭐ 必背速记卡 ​

┌──────────────────────────────────────┐
│  list 必背速记卡                      │
├──────────────────────────────────────┤
│  1. 底层是双向循环链表                 │
│  2. 任意位置插入删除都是 O(1)         │
│  3. 不支持随机访问,不能用 []         │
│  4. 迭代器是双向的,只能 ++ --        │
│  5. 插入不失效迭代器,删除仅失效当前   │
│  6. 有自己的 sort、reverse、merge    │
│  7. remove 按值删,unique 去相邻重复  │
│  8. 空间开销大(每个节点两个指针)     │
│  9. 缓存不友好,小数据可能比 vector 慢│
│  10. 适合:频繁中间插入删除的场景     │
└──────────────────────────────────────┘

百炼成钢,融会贯通