欢迎光临
我们一直在努力

C++ list双向带头循环链表完全指南

1. 什么是 list?

std::list 是 STL 提供的双向链表容器,每个元素存放在一个独立的节点中,节点之间通过指针链接。结构特点:

  • 内存不连续,不支持随机访问(没有 [] 和 at())。

  • 在任意位置插入或删除元素都非常高效,时间复杂度 O(1)(前提是你已经拥有指向该位置的迭代器)。

  • 不需要像 vector 那样扩容或移动元素,插入后其他元素的地址不会改变。

  • 自带一些高效的成员函数:sort、merge、reverse、splice(拼接)、remove、unique 等。

一句话概括:当程序频繁在序列中间插入或删除元素,且不需要随机访问时,优先考虑 list。


2. 引入头文件与基本声明

使用 list 前,必须包含头文件:

#include <list>

声明一个 list 对象:

std::list<int> lst; // 存放 int 的空链表
std::list<std::string> words; // 存放字符串
std::list<double> prices; // 存放价格

同样,我们用 std:: 前缀,保持代码清晰。


3. 初始化方法

list 的初始化与 vector 基本一致:

list<int> l1; // 空链表
list<int> l2(5); // 5 个元素,默认值 0
list<int> l3(5, 10); // 5 个元素,都是 10
list<int> l4 = {1, 2, 3, 4}; // 列表初始化
list<int> l5(l4); // 拷贝构造
list<int> l6(l4.begin(), l4.end());// 用迭代器范围构造

注意:list 没有类似 vector 的 capacity() 和 reserve(),因为它根本不需要预分配连续内存。


4. 常用操作详解

list 比 vector 多了很多首部操作,而且有自己专属的算法成员。

4.1 添加元素

list<int> lst;

// 尾部添加
lst.push_back(30);
lst.emplace_back(40); // 原地构造,更高效

// 首部添加
lst.push_front(20);
lst.emplace_front(10);

// 任意位置插入
auto it = lst.begin();
++it; // 移动到第二个位置
lst.insert(it, 25); // 在迭代器前插入 25
lst.insert(it, 2, 7); // 插入两个 7
lst.insert(it, {1,2,3}); // 插入初始化列表(C++11)

因为 list 是双向链表,在头部操作非常快,这是 vector 做不到的(vector 头插要移动所有元素)。

4.2 访问元素

list 不提供随机访问,只能访问头尾:

int first = lst.front(); // 第一个元素
int last = lst.back(); // 最后一个元素

如果要访问中间元素,必须使用迭代器逐一遍历。下面这样是错误的:

// int x = lst[2]; // 编译错误!list 没有 operator[]

4.3 删除元素

lst.pop_back(); // 删除尾部元素
lst.pop_front(); // 删除头部元素

// 删除指定迭代器所指元素
auto it = lst.begin();
++it;
lst.erase(it); // 返回下一个有效迭代器

// 删除所有值为 val 的元素
lst.remove(10); // 删除所有等于 10 的元素

// 按条件删除(C++11 的 lambda 很方便)
lst.remove_if([](int x) { return x % 2 == 0; }); // 删除所有偶数

lst.clear(); // 清空链表

注意 remove 是 list 的成员函数,它会真正删除节点并调整链接,与头文件 <algorithm> 中的泛型 std::remove 不同(后者只是移动元素,不改变容器大小)。

4.4 其他特色操作

list 还有一些独门绝技,特别适合某些算法:

// 反转链表
lst.reverse();

// 排序(list 自带的 sort,效率优于 std::sort)
lst.sort(); // 默认升序
lst.sort(std::greater<int>()); // 降序

// 合并两个已排序链表(合并后,另一个链表为空)
list<int> other = {2,4,6};
lst.merge(other); // 要求两个链表都已排序

// 移除连续重复元素(通常配合 sort 使用)
lst.unique(); // 只保留连续相同元素中的第一个

// 拼接(splice)—— 将另一个链表的元素整个移动到指定位置
list<int> extra = {100, 200};
auto pos = lst.begin();
++pos;
lst.splice(pos, extra); // extra 中所有元素移到 pos 前,extra 变空

splice 是 list 的一大亮点:它将另一个链表的节点“偷”过来,只修改指针,不复制元素,效率极高。实现复杂的数据结构(如 LRU 缓存)时非常有用。


5. 遍历 list

因为没有下标,我们主要使用范围 for 循环或迭代器。

范围 for(推荐)

for (int x : lst) {
cout << x << " ";
}

需要修改元素时用引用:

cpp

for (int& x : lst) {
x *= 2;
}

迭代器

for (auto it = lst.begin(); it != lst.end(); ++it) {
cout << *it << " ";
}

list 的迭代器是双向迭代器,可以 ++ 和 –,但不能 it += 3 这样随机移动。如果需要跳过多个元素,只能循环递增。

6. list 的内存管理特点

  • 非连续内存:每个元素是独立的节点,节点中包含数据以及指向前驱和后继的两个指针。因此 list 占用的总内存比 vector 多(多出指针开销)。

  • 没有 capacity 和 reserve:添加元素时就分配一个新节点,删除时立即释放节点内存。

  • 迭代器稳定性:在 list 上插入或删除元素不会导致其他元素的迭代器失效(当然,被删除的那个元素自己的迭代器会失效)。这一点比 vector 安全得多,vector 扩容会让所有迭代器失效。

  • 不能通过下标访问:访问第 n 个元素需要从头(或尾)遍历 n 步,复杂度 O(n)。因此不要用 list 做频繁的随机访问操作。

7. 常见误区与注意事项

  • 试图用 [] 访问元素
    list 没有 operator[],编译错误。请用迭代器或范围 for。

  • 对 list 使用全局 std::sort

    std::sort(lst.begin(), lst.end()); // 错误!list 迭代器不是随机访问迭代器

    必须用成员函数 lst.sort()。

  • 删除元素时迭代器失效
    类似 vector,erase 会返回下一个有效迭代器,正确写法:

    for (auto it = lst.begin(); it != lst.end(); ) {
    if (需要删除 *it) {
    it = lst.erase(it);
    } else {
    ++it;
    }
    }

    如果写成 lst.erase(it); ++it;,it 已经失效,程序会崩溃。

  • remove 与全局 std::remove 的混淆
    使用 lst.remove(value) 是 O(n) 删除所有匹配节点,真正改变大小;而 std::remove 配合 erase 是另一种惯用法,但不适用于 list(而且更繁琐)。建议在 list 上就用成员 remove。

  • 误以为 list 总是比 vector 快
    链表的插入删除是 O(1),但找到插入位置可能需要 O(n) 的遍历。而且链表的内存分散,缓存局部性差,遍历速度远不如 vector。实际选择时要基于场景。

  • unique 只移除连续的重复
    如果希望移除所有重复元素,需要先排序

  • 8. 小结

    • list 是双向链表,头尾操作和任意位置插入删除极快。

    • 不支持随机访问,没有 [],遍历速度不如 vector。

    • 自带 sort、merge、reverse、splice、remove、unique 等高效成员函数。

    • 插入删除不导致其他迭代器失效,这是它的重要安全优势。

    • 选择合适的容器:需要随机访问用 vector,频繁中间插入删除用 list。

    可以这样理解:vector 是你的瑞士军刀,list 是你的手术刀——各有专长。

    赞(0)
    未经允许不得转载:171主机测评 » C++ list双向带头循环链表完全指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址