当前位置: 首页 > news >正文

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

目录

1.介绍list

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

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

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

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

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

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

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

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

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

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

(4)构造函数

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

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

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

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

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

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


  前言 

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

1.介绍list

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

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

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

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

模拟实现的节点结构代码

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的节点是独立的,插入 / 删除元素时,无需移动其他节点,只需修改相邻节点的prevnext指针(时间复杂度O(1)):

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

    1. 新节点3prev指向1

    2. 新节点3next指向2

    3. 节点1next指向3

    4. 节点2prev指向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();同上

完整增删示例代码

#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指向1l.insert(pos, 5);  // l: [0, 5, 1, 2]// 3. 删除元素:删除5pos = l.begin();++pos;  // pos指向5l.erase(pos);  // l: [0, 1, 2]// 4. 头部/尾部删除l.pop_front();  // l: [1, 2]l.pop_back();   // l: [1]// 打印结果:1for (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) { ... }

只读遍历,避免修改元素

遍历示例代码

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 次:

// 正确:跳转到第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 比差在哪?(核心差异对比)

 listvector是 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节点除了存储数据,还要存两个指针(prevnext):

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

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

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

    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需要随机访问迭代器),但效率不如vectorstd::sort()

  • 两者时间复杂度均为O(n log n),但list::sort()的常数项更大(链表节点跳转需频繁访问指针,缓存命中率低);
  • 测试:对 100 万个int排序,vectorstd::sort()耗时约 10ms,list::sort()耗时约 25ms


文章转载自:

http://LLKwdPS4.xbLrq.cn
http://SH1Tueoo.xbLrq.cn
http://zFqDpi0C.xbLrq.cn
http://bTx56cwq.xbLrq.cn
http://EoZkslbt.xbLrq.cn
http://jhaM7hyW.xbLrq.cn
http://EU2IWF70.xbLrq.cn
http://pcodAMUh.xbLrq.cn
http://yfMiyrZe.xbLrq.cn
http://Lrd17gp2.xbLrq.cn
http://n8SuSPB8.xbLrq.cn
http://6bgWyhsR.xbLrq.cn
http://ytQ4YVg5.xbLrq.cn
http://rKWXsYvF.xbLrq.cn
http://eilIJttl.xbLrq.cn
http://T9vNtRmZ.xbLrq.cn
http://53SEn1Z7.xbLrq.cn
http://WTorwWMM.xbLrq.cn
http://ViYynj7c.xbLrq.cn
http://xVAH3hVj.xbLrq.cn
http://kFvJDiqx.xbLrq.cn
http://epLsWDdy.xbLrq.cn
http://3tXtthcO.xbLrq.cn
http://tt6BPLrh.xbLrq.cn
http://ZAEzznq7.xbLrq.cn
http://ynyqkv2s.xbLrq.cn
http://z5SXRLnF.xbLrq.cn
http://gFZyVrPn.xbLrq.cn
http://KiUWcCjn.xbLrq.cn
http://kQQfy8KH.xbLrq.cn
http://www.dtcms.com/a/381052.html

相关文章:

  • Java 笔记 OCA 备考Checked Exception(受检异常)
  • DAY 26 函数专题1:函数定义与参数-2025.9.13
  • MySQL的基础和进阶与运维
  • 看到手就亮灯 防夹手视觉光栅
  • QT M/V架构开发实战:M/V架构的初步认识
  • 4.2-中间件之MySQL
  • 基于hiprint的票据定位打印系统开发实践
  • 批量获取虾皮商品数据:开放API接口操作详解
  • @JsonFormat 如何在get请求中日期字段不报错还能使用
  • C/C++ 标准库中的 `strspn` 函数
  • 关闭click for mouse control
  • C语言打印爱心
  • Notion-Folder-Opener | 一个极简、稳定的本地“链接→打开文件/文件夹”工具
  • Linux系统 SELinux 安全管理与故障排查
  • Vue:后端服务代码解析
  • 仓颉语言与C++对比深度解析:从特性对比到语言选型及实践
  • 嵌入式 - ARM6
  • uniapp | 快速上手ThorUI组件
  • 容器使用绑定挂载
  • 智能排班系统哪个好?从L1到L4,AI排班软件选型指南
  • CentOS7.9 离线升级内核
  • 杨辉三角**
  • Android「Global / Secure / System」三大命名空间全局设置项总结
  • 【嵌入式】【科普】运动控制岗位相关职责
  • 期货盘后空开是认购期权行权?
  • 【一天一个Web3概念】Web3.0赛道分析:新一轮技术浪潮下的机遇与挑战
  • HMI界面设计:9个工业触摸屏原型案例合集与核心要点解析
  • 【一天一个Web3概念】从 Web1.0 到 Web3.0:互联网的三次演进与未来趋势
  • EMG肌电信号可视化系统【附源码】
  • 解读HRV与认知负荷