【C++】:list容器全面解析(超详细)

目录

1.介绍list

[(1)节点结构:list 的 "最小单元"](#(1)节点结构:list 的 “最小单元”)

[(2)list 的整体结构:节点如何串联?](#(2)list 的整体结构:节点如何串联?)

[2. 为什么要设计 list?(核心价值:解决 vector 的致命短板)](#2. 为什么要设计 list?(核心价值:解决 vector 的致命短板))

[(1)vector 的痛点:中间增删要 "搬家"](#(1)vector 的痛点:中间增删要 “搬家”)

[(2)list 的解决方案:增删只需 "改指针"](#(2)list 的解决方案:增删只需 “改指针”)

[(3)list 的其他优势:无扩容、内存利用率高](#(3)list 的其他优势:无扩容、内存利用率高)

[3. 怎么用 list?(核心接口与限制)](#3. 怎么用 list?(核心接口与限制))

[(1)增删操作:高效的双向增删(O (1))](#(1)增删操作:高效的双向增删(O (1)))

[(2)遍历操作:只能用迭代器(不支持 [])](#(2)遍历操作:只能用迭代器(不支持 []))

[(3)其他常用接口(O (1) 或 O (n))](#(3)其他常用接口(O (1) 或 O (n)))

(4)构造函数

[4. 和 vector 比差在哪?(核心差异对比)](#4. 和 vector 比差在哪?(核心差异对比))

[5. 用 list 有什么坑?(边界与局限)](#5. 用 list 有什么坑?(边界与局限))

[(1)坑 1:随机访问效率极低,别用它做 "数组"](#(1)坑 1:随机访问效率极低,别用它做 “数组”)

[(2)坑 2:内存碎片化与指针开销](#(2)坑 2:内存碎片化与指针开销)

[(3)坑 3:迭代器不支持 "算术操作",遍历易出错](#(3)坑 3:迭代器不支持 “算术操作”,遍历易出错)

[(4)坑 4:sort () 效率不如 vector](#(4)坑 4:sort () 效率不如 vector)


前言

在 C++ STL 的序列容器中,vector 因连续内存的高效随机访问 成为 "常客",但当遇到 "频繁中间增删 " 场景时,它的性能短板会暴露 无遗。而**list** ------ 这个基于双向链表的容器,正是为解决这一痛点而生。

1.介绍list

list的底层是双向循环链表,每个元素以 "独立节点 " 形式存在,节点间通过指针关联,内存中不连续存储

list的结构,下图就是带有双向循环链表:

(1)节点结构:list 的 "最小单元"

一个list节点包含三个部分:数据域 (存储元素值)、前驱指针prev,指向前一个节点)、后继指针next,指向后一个节点)。

模拟实现的节点结构代码

cpp 复制代码
template <class T>
struct ListNode {
    T data;          // 存储元素的值
    ListNode* prev;  // 指向"前一个节点"的指针
    ListNode* next;  // 指向"后一个节点"的指针

    // 节点构造函数:初始化数据,前后指针默认空
    ListNode(const T& val) : data(val), prev(nullptr), next(nullptr) {}
};

(2)list 的整体结构:节点如何串联?

list容器通过一个 "头节点 "(_head,不存储实际数据)和 "元素个数 "(_size)管理整个链表。以存储{1, 2, 3,4,5}的**list为例,**其结构如下:

list 整体结构插图

若为双向循环链表 (如 GCC 的 STL 实现),尾节点的next会指向_head_headprev会指向尾节点 ,形成闭环,好处是 "定位尾节点无需遍历,直接_head->prev即可"。

关键区别于 vector

  • **vector**是 "连续一块内存",像一排紧密排列的箱子;
  • **list**是 "零散节点靠指针串起",像一串带绳子的珠子,珠子可散落在不同位置。

2. 为什么要设计 list?(核心价值:解决 vector 的致命短板)

list的存在,不是为了 "替代 vector" ,而是为了弥补 vector 在 "中间增删" 场景下的低效 ------ 这是 vector 的 "致命短板"。

(1)vector 的痛点:中间增删要 "搬家"

vector的内存是连续的,当在中间插入 / 删除元素时,需要移动后续所有元素:

  • 例:在**vector** 的第 100 个元素前插入新元素,需将第 100~ 末尾的所有元素向后移动 1 位(时间复杂度O(n));
  • 若**vector**容量不足,还需扩容(申请新内存→拷贝旧元素→释放旧内存),额外增加开销。

(2)list 的解决方案:增删只需 "改指针"

list 的节点是独立 的,插入 / 删除元素时,无需移动其他节点,只需修改相邻节点的prev和**next** 指针(时间复杂度O(1)):

  • list12之间插入3,只需做 4 步

    1. 新节点3的**prev** 指向1

    2. 新节点3的**next** 指向2

    3. 节点1的**next** 指向3

    4. 节点2的**prev** 指向3

(3)list 的其他优势:无扩容、内存利用率高

  • 无需扩容list 的节点随用随申请 ,用完释放,不会像**vector** 那样 "容量> 大小" 导致内存浪费;

  • 内存碎片化可控 :虽然节点零散,但对于 "频繁增删 " 场景**,总内存开销** 通常低于vector的扩容冗余。

场景例子 :实现 "实时日志系统"------ 需要频繁在日志中间插入紧急记录、删除过期记录。用list处理 10 万条日志的中间增删,耗时仅为**vector**的 1/500(数据量越大,差距越明显)。

3. 怎么用 list?(核心接口与限制)

list的接口设计完全贴合链表特性,重点支持 "双向操作",但不支持**vector**的 "随机访问"。下面分 3 类讲解核心接口,附带代码示例和注意事项。

(1)增删操作:高效的双向增删(O (1))

list 的增删接口是其核心优势,所有接口的时间复杂度均为O(1)(定位插入位置的时间除外)。

接口 功能 代码示例 注意事项
push_front(val) 在头部插入元素 list<int> l; l.push_front(1); 直接修改_head和首节点的指针
push_back(val) 在尾部插入元素 l.push_back(2); 双向循环链表中,直接定位_head->prev
insert(pos, val) 在迭代器pos位置插入元素 auto pos = l.begin(); l.insert(pos, 3); 需先定位pos(O (n)),插入本身 O (1)
erase(pos) 删除迭代器pos指向的元素 l.erase(pos); 删除后pos失效,其他迭代器有效
pop_front() 删除头部元素 l.pop_front(); 需判断链表是否为空
pop_back() 删除尾部元素 l.pop_back(); 同上

完整增删示例代码

cpp 复制代码
#include <list>
#include <iostream>
using namespace std;

int main() {
    list<int> l;
    
    // 1. 头部/尾部插入
    l.push_front(1);  // l: [1]
    l.push_back(2);   // l: [1, 2]
    l.push_front(0);  // l: [0, 1, 2]
    
    // 2. 中间插入:定位到1的位置
    auto pos = l.begin();
    ++pos;  // pos指向1
    l.insert(pos, 5);  // l: [0, 5, 1, 2]
    
    // 3. 删除元素:删除5
    pos = l.begin();
    ++pos;  // pos指向5
    l.erase(pos);  // l: [0, 1, 2]
    
    // 4. 头部/尾部删除
    l.pop_front();  // l: [1, 2]
    l.pop_back();   // l: [1]
    
    // 打印结果:1
    for (auto num : l) {
        cout << num << " ";
    }
    return 0;
}

(2)遍历操作:只能用迭代器(不支持 [])

因**list** 内存不连续,无法通过 "地址偏移" 快速访问元素,所以不支持[]at(),只能用迭代器或范围 for 遍历。

遍历方式 代码示例 适用场景
正向迭代器 for (auto it = l.begin(); it != l.end(); ++it) { ... } 正向遍历所有元素
反向迭代器 for (auto it = l.rbegin(); it != l.rend(); ++it) { ... } 反向遍历(从尾到头)
范围 for(C++11+) for (auto num : l) { ... } 无需修改元素,简洁遍历
const迭代器 for (auto it = l.cbegin(); it != l.cend(); ++it) { ... } 只读遍历,避免修改元素

遍历示例代码

cpp 复制代码
list<int> l = {1, 3, 2};

// 1. 正向迭代器(可读可写)
for (auto it = l.begin(); it != l.end(); ++it) {
    *it *= 2;  // 修改元素:l变为[2, 6, 4]
}

// 2. 反向迭代器(遍历结果:4, 6, 2)
for (auto it = l.rbegin(); it != l.rend(); ++it) {
    cout << *it << " ";
}

// 3. const迭代器(只读,无法修改)
for (auto it = l.cbegin(); it != l.cend(); ++it) {
    // *it = 10;  // 编译报错:const迭代器不可修改
    cout << *it << " ";
}

错误提醒list 的迭代器不支持 "算术操作"(如it += 3),只能**++/** --,若需跳转到第 n 个元素,需循环**++it**n 次:

cpp 复制代码
// 正确:跳转到第3个元素(索引2)
list<int>::iterator it = l.begin();
for (int i = 0; i < 2; ++i) {
    ++it;
}

// 错误:不支持随机访问,编译报错
// it += 2;

(3)其他常用接口(O (1) 或 O (n))

接口 功能 时间复杂度 代码示例
size() 返回元素个数 O(1) cout << l.size();
empty() 判断是否为空 O(1) if (l.empty()) { ... }
clear() 清空所有元素(保留头节点) O(n) l.clear();
swap(list& other) 交换两个 list 的内容 O(1) l1.swap(l2);
front() 获取头部元素(引用) O(1) cout << l.front();
back() 获取尾部元素(引用) O(1) cout << l.back();

注意clear() 会删除所有数据节点,但保留头节点,size() 变为 0,后续仍可正常**push_back**。

(4)构造函数

| 构造函数 | 接口说明 | 代码示例 |
| list (size_type n, const value_type& val = value_type()) | 构造的list中包含n个值为val的元素 | list<int> lt(10,1); |
| list() | 构造空的list | list<int> lt ; |
| list (const list& x) | 拷贝构造函数 | list<int> lt(l); |

list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list list<int>lt(v.begin(),v.end())

4. 和 vector 比差在哪?(核心差异对比)

list和vector 是 STL 中最常用的两个序列容器,90% 的场景需要二选一。两者的差异完全源于 "非连续内存 " vs "连续内存",下表从 8 个维度做详细对比:

对比维度 list(双向链表) vector(动态数组) 优势方
内存分布 节点零散分布,靠指针连接 连续一块内存 vector(缓存友好)
随机访问 不支持(需遍历,O (n)) 支持([]/at (),O (1)) vector
头部增删 O (1)(改指针) O (n)(移动所有元素) list
中间增删 O (1)(改指针,需定位 pos) O (n)(移动后续元素) list
尾部增删 O (1)(改指针) O (1) amortized(扩容时 O (n)) 持平(vector 扩容后更优)
内存利用率 高(无扩容浪费) 低(可能有容量 > 大小的浪费) list
迭代器稳定性 仅删除节点的迭代器失效 扩容 / 中间删除导致多迭代器失效 list
指针 / 内存开销 高(每个节点 2 个指针) 低(仅存储数据) vector

5. 用 list 有什么坑?(边界与局限)

list不是 "万能容器",它的短板同样明显,踩坑往往是因为忽略了这些局限:

(1)坑 1:随机访问效率极低,别用它做 "数组"

若需要频繁通过 "索引" 访问元素 (如第i个元素),**list**的效率会让你崩溃:

  • vector 访问第 1000 个元素:v[999](O(1));
  • **list**访问第 1000 个元素:需从头部迭代 999 次(O (n)),数据量越大,差距越悬殊

错误场景 :用list存储矩阵数据,频繁通过索引访问元素 ------ 程序运行速度会比vector慢 100 倍以上。

(2)坑 2:内存碎片化与指针开销

每个**list** 节点除了存储数据,还要存两个指针(prev 和**next**):

  • 64 位系统中,每个指针占 8 字节,若存储**int**(4 字节),指针开销(16 字节)是数据本身的 4 倍;
  • 节点零散分布会导致 "内存碎片化"------ 系统内存被分割成大量小块,后续申请大块内存时可能失败。

(3)坑 3:迭代器不支持 "算术操作",遍历易出错

用**vector** 的迭代器习惯操作list迭代器:

cpp 复制代码
    list<int> l = {1,2,3,4};
    auto it = l.begin();
    // it += 2;  // 编译报错:list迭代器不支持+=
    // 正确做法:循环++
    for (int i = 0; i < 2; ++i) {
        ++it;
    }
    cout << *it;  // 输出3

(4)坑 4:sort () 效率不如 vector

list有自己的**sort()** 成员函数(因**std::sort** 需要随机访问迭代器),但效率不如**vector** 的**std::sort()**:

  • 两者时间复杂度均为**O(n log n)** ,但**list::sort()**的常数项更大(链表节点跳转需频繁访问指针,缓存命中率低);
  • 测试:对 100 万个int排序,vector 的**std::sort()** 耗时约 10ms,list::sort() 耗时约25ms
相关推荐
IT灰猫2 小时前
C++轻量级配置管理器升级版
开发语言·c++·设计模式·配置管理·ini解析
大白同学4214 小时前
【C++】C++11介绍(Ⅱ)
开发语言·c++
油炸自行车4 小时前
【Qt】编写Qt自定义Ui控件步骤
开发语言·c++·qt·ui·自定义ui控件·qt4 自定义ui控件
呱呱巨基6 小时前
C/C++ 内存管理
c++·笔记·学习
半桔6 小时前
【网络编程】TCP 服务器并发编程:多进程、线程池与守护进程实践
linux·服务器·网络·c++·tcp/ip
橘子136 小时前
C++实战:搜索引擎项目(二)
开发语言·c++·搜索引擎
应用市场7 小时前
Qt C++ 图形绘制完全指南:从基础到进阶实战
开发语言·c++·qt
青草地溪水旁7 小时前
设计模式(C++)详解—单例模式(2)
c++·单例模式
bkspiderx7 小时前
C++时区操作全版本指南(含C++03/C++11-17/C++20)
linux·开发语言·c++·c++20·时区