list学习总结

list 学习总结

std::list 是 C++ 标准库中最常用的链表容器,本文从零到熟练梳理它的所有核心用法。

1. list 是什么

  • 存储不连续内存的元素序列,节点之间通过指针相连。
  • 底层是双向链表:每个节点有两个指针,分别指向前驱和后继。
  • 任意位置插入、删除都是 O(1)(只需要改指针,不需要移动元素)。
  • 不支持随机访问:不能按下标 O(1) 访问,只能从头部或尾部逐个遍历。
  • 属于"序列容器",头文件 <list>。
#include <list>

list<int> l; // 空 list,int 类型

对比记忆:vector 是"连续内存的动态数组",访问快、尾部增删快;list 是"分散内存的链表",任意位置增删快但访问慢;map 是"按 key 组织成树"的关联容器。

2. 定义与初始化

#include <list>
#include <string>
using namespace std;

// 1. 空 list
list<int> l1;

// 2. 指定大小
list<int> l2(5);          // 5 个元素,都是 0
list<int> l3(5, 42);      // 5 个元素,都是 42

// 3. 初始化列表
list<int> l4{1, 2, 3, 4, 5};

// 4. 从其他容器构造
vector<int> v{5, 4, 3, 2, 1};
list<int> l5(v.begin(), v.end()); // {5, 4, 3, 2, 1}

// 5. 拷贝构造
list<int> l6(l4);

// 6. 移动构造(C++11)
list<int> l7(std::move(l4)); // l4 被掏空

3. 核心操作一览

操作 写法 说明 复杂度
头部插入 l.push_front(x) 在头部添加元素 O(1)
头部插入 l.emplace_front(args...) C++11,原地构造 O(1)
尾部插入 l.push_back(x) 在末尾添加元素 O(1)
尾部插入 l.emplace_back(args...) C++11,原地构造 O(1)
中间插入 l.insert(it, x) 在迭代器 it 前插入 O(1)
头部删除 l.pop_front() 删除第一个元素 O(1)
尾部删除 l.pop_back() 删除最后一个元素 O(1)
中间删除 l.erase(it) 删除迭代器 it 处元素,返回下一个迭代器 O(1)
范围删除 l.erase(first, last) 删除 [first, last) 范围内元素 O(n)
按值删除 l.remove(x) 删除所有值等于 x 的元素 O(n)
条件删除 l.remove_if(pred) 删除所有满足谓词的元素 O(n)
去重 l.unique() 删除相邻重复元素 O(n)
排序 l.sort() 成员函数排序 O(n log n)
反转 l.reverse() 反转整个 list O(n)
拼接 l.splice(pos, other) 把 other 的节点搬进 l(不拷贝) O(1)
合并 l.merge(other) 合并两个有序 list O(n)
首元素 l.front() 返回第一个元素的引用 O(1)
尾元素 l.back() 返回最后一个元素的引用 O(1)
大小 l.size() 元素个数 O(1)
清空 l.clear() 删除所有元素 O(n)
判空 l.empty() size == 0 返回 true O(1)
交换 l.swap(other) 交换两个 list 的内容 O(1)

注意:list 没有 operator[]、at()、data(),不支持随机访问!

4. 增删查改实战

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

int main() {
    list<int> l;

    // ===== 插入 =====
    l.push_back(30);
    l.push_front(10);      // {10, 30}
    l.push_back(40);       // {10, 30, 40}
    l.emplace_back(50);    // {10, 30, 40, 50}

    auto it = l.begin();
    advance(it, 2);        // 找到位置(list 迭代器只能 ++/--)
    l.insert(it, 20);      // 在 30 前插入 20 -> {10, 20, 30, 40, 50}

    // ===== 访问 =====
    cout << l.front() << endl; // 10
    cout << l.back() << endl;  // 50

    // ===== 查找(list 没有内置 find,用 std::find) =====
    auto pos = find(l.begin(), l.end(), 30);
    if (pos != l.end()) {
        cout << "Found: " << *pos << endl; // Found: 30
    }

    // ===== 遍历 =====
    // 1. 迭代器遍历(不支持下标)
    for (auto it = l.begin(); it != l.end(); ++it) {
        cout << *it << " "; // 10 20 30 40 50
    }
    cout << endl;

    // 2. 范围 for(C++11)
    for (auto& x : l) {
        cout << x << " ";
    }
    cout << endl;

    // ===== 删除 =====
    l.pop_front();         // 删除头部 {20, 30, 40, 50}
    l.pop_back();          // 删除尾部 {20, 30, 40}
    l.remove(30);          // 按值删除 {20, 40}

    l.push_back(4);
    l.push_back(6);
    l.remove_if([](int x) { return x % 2 == 0; }); // 删除所有偶数
    // l: { }(20, 40, 4, 6 全被删除)

    l.clear();             // 清空
    cout << l.size() << endl; // 0

    return 0;
}

5. 迭代器的特点(重点)

  • list 的迭代器是双向迭代器:只支持 ++ / --,不支持 it + n(vector 才可以)。
  • 想跳 n 步必须循环 ++ n 次,或用 std::advance / std::next:
list<int> l{1, 2, 3, 4, 5};
auto it = l.begin();

// advance(it, 2); // it 指向 3
// auto it2 = next(it, 2); // 3 往后两个 -> 5

// 危险:list<int>::iterator 不能 it + 2!
// auto it3 = it + 2; // 编译错误

迭代器稳定性(list 最大的优势):

  • 插入元素不会使任何已有迭代器失效。
  • 删除元素只会使指向被删除元素的迭代器失效。
  • vector 的 insert/erase 会让后面的迭代器全部失效;list 完全不会。
list<int> l{1, 2, 3, 4, 5};
auto it3 = find(l.begin(), l.end(), 3);

l.insert(it3, 100);   // 插入后 it3 仍然有效,还指向 3
l.push_back(6);       // it3 依然有效
l.erase(it3);         // 现在只有 it3 失效,其它(包括 100)不受影响

6. 排序:成员函数 sort()(重点)

list 不能使用 std::sort(),因为 std::sort 需要随机访问迭代器(it + n)。list 只能用成员函数 sort():

list<int> l{3, 1, 4, 1, 5, 9};

l.sort(); // 升序:{1, 1, 3, 4, 5, 9}

// 降序(自定义比较器)
l.sort(greater<int>()); // {9, 5, 4, 3, 1, 1}

// 自定义比较器:按绝对值
l.sort([](int a, int b) { return abs(a) < abs(b); });

为什么 std::sort 不能用?

sort(l.begin(), l.end()); // 编译错误!list 的迭代器不支持 it + n

std::sort 内部依赖 first + n、last - n 这样的随机访问操作,list 的双向迭代器不支持。所以标准库专门为 list 提供了成员 sort()。

merge 合并两个有序 list:

list<int> a{1, 3, 5};
list<int> b{2, 4, 6};
a.merge(b);      // 两个都升序才能 merge,结果 {1, 2, 3, 4, 5, 6}
// b 被清空(节点被搬走)

7. splice:高效的节点搬移(list 独有的核心能力)

splice 把一个 list 的节点搬到另一个 list,不拷贝元素,纯指针操作 O(1):

list<int> a{1, 2, 3, 4, 5};
list<int> b{7, 8, 9};

// 把 b 的全部节点搬到 a 的开头
a.splice(a.begin(), b); // a: {7, 8, 9, 1, 2, 3, 4, 5}, b 为空

// 把 a 的一个元素搬回 b 的头部
auto it = find(a.begin(), a.end(), 3);
b.splice(b.begin(), a, it); // b: {3}, a: {7, 8, 9, 1, 2, 4, 5}

// 把 a 的一个区间搬到 b 的尾部
auto lo = find(a.begin(), a.end(), 1);
auto hi = next(lo, 3); // 范围 [1, 2, 4)
b.splice(b.end(), a, lo, hi); // b: {3, 1, 2, 4}

vector 做不到这件事:vector 的"拼接"是拷贝元素 O(n),而且会让迭代器失效。

8. unique 去重

注意:unique() 只能删除相邻的重复元素! 使用前必须先 sort():

list<int> l{1, 1, 2, 2, 3, 1, 2};

l.unique(); // 只去相邻重复:{1, 2, 3, 1, 2} —— 最后的 1, 2 还留着!
// 因为没有先排序

l.sort();   // {1, 1, 1, 2, 2, 2}
l.unique(); // {1, 2} ✓ 真正的去重

unique 也支持自定义谓词,删除"满足条件"的相邻元素:

list<string> l{"a", "AB", "b", "CD", "c"};
l.unique([](const string& x, const string& y) {
    return tolower(x[0]) == tolower(y[0]); // 忽略大小写去重
});

9. list 与 vector、deque、forward_list 的对比

list vector deque forward_list
底层 双向链表 动态数组 分段数组 单向链表
内存连续 否 是 否(分段) 否
随机访问 不支持 O(1) O(1) 不支持
头部增删 O(1) O(n) O(1) O(1)
尾部增删 O(1) O(1) 均摊 O(1) 无 push_back
中间增删 O(1)(已有迭代器) O(n) O(n) O(1)(已有迭代器)
迭代器类型 双向 随机访问 随机访问 单向
迭代器稳定性 插入不影响,删除只影响被删 插入/删除可能全部失效 头部尾部插入不影响 同 list
额外能力 splice/merge/remove reserve/[]/at/data push_front splice_after/remove

选择建议:

  • 默认选择 vector:大多数场景 vector 最快(缓存友好)。
  • 中间频繁增删且不需要随机访问 → list,但注意内存开销和低速遍历。
  • 只需要单向遍历、最小化内存 → forward_list(C++11)。
  • 频繁头部增删 → deque。

10. 常见坑汇总

  1. 没有 operator[]:list 不支持随机访问,不能 l[0]。
  2. 不能直接用 std::sort:必须用成员函数 l.sort()。
  3. 迭代器不支持 it + n:需要 std::advance / std::next,或用 std::distance 求距离。
  4. unique 只去相邻重复:去重前必须先 sort。
  5. remove 是 O(n):它必须遍历整个 list 找到所有匹配的元素。
  6. 内存开销大:每个元素额外存 2 个指针(前驱+后继),加上节点分配开销。
  7. 遍历慢:节点内存不连续,每次访问都可能缓存未命中。大 list 的遍历比 vector 慢一个数量级。
  8. splice 会搬走源 list 的节点:用完后源 list 对应的元素就没了。
  9. merge 要求两个 list 有序:用 undefined behavior 如果没排序。
  10. advance 跳过整个 list:中间插入需要 O(n) 找到位置(虽然插入本身 O(1))。
  11. list 排序慢:虽然 sort() 是 O(n log n),但实际是链表归并排序,常数大,比 vector 排序慢很多。

11. 实战:LRU 缓存模型(list 最经典的场景)

LRU(最近最少使用)缓存需要"移动已有节点到头部",这正是 list 的 splice 擅长的:

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

// 简化版 LRU:只演示 list 的核心用法
int main() {
    list<int> history;      // 访问历史,最近访问的在前
    unordered_map<int, list<int>::iterator> pos; // 元素 -> list 中位置

    auto access = [&](int x) {
        if (pos.count(x)) {
            // 已存在:把节点搬到头部(O(1) 指针操作)
            history.splice(history.begin(), history, pos[x]);
        } else {
            // 不存在:插入头部
            history.push_front(x);
            pos[x] = history.begin();
        }
    };

    access(1); // history: {1}
    access(2); // history: {2, 1}
    access(3); // history: {3, 2, 1}
    access(2); // history: {2, 3, 1} —— 2 被移到头部
    access(1); // history: {1, 2, 3}

    for (auto& x : history) cout << x << " "; // 1 2 3
    cout << endl;

    return 0;
}

如果换成 vector 实现同样的功能:每次把元素移到头部都要 O(n) 移动内存,LRU 命中就退化成 O(n)。

12. 实战场景

给定一个需求:需要维护一组数据,频繁在任意位置增删元素,不要求随机访问。

方法一:使用std::vector建模,但中间插入/删除 O(n),频繁操作性能差。
方法二:使用std::list建模,只要有迭代器,任意位置插入/删除都是 O(1),迭代器不会因其它操作失效。
方法三:使用std::deque建模,头尾 O(1),但中间操作仍然是 O(n)。

总结:
1、频繁中间增删且不需要随机访问,选择 list
2、需要拼接(splice)、归并(merge)等操作,只有链表能 O(1) 完成
3、LRU 缓存、"移动已有节点到头部"这类操作必须用 list
4、只是尾部增删 + 随机访问,选 vector(缓存友好,更快)
5、默认不要选 list,它在大多数场景都比 vector 慢(内存开销 + 缓存不命中)
6、list 的迭代器是双向迭代器,只支持 ++/--;vector 的迭代器是随机访问的,支持 it+n
7、list 的迭代器稳定性是它最大的优势:插入不影响已有迭代器,删除只影响被删的那个
另外 list 节点分散在堆上,遍历时需要跳内存访问,速度远慢于 vector 的连续存储

posted @ 2026-09-08 23:50  LemHou  阅读(7)  评论(0)    收藏  举报