当前位置: 首页 > 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://lti8DgyW.hwhnx.cn
http://WgRdZs3b.hwhnx.cn
http://Y8OVm2k5.hwhnx.cn
http://l1zpliq2.hwhnx.cn
http://xVNcbY2e.hwhnx.cn
http://txMhQTwz.hwhnx.cn
http://saGLCiGM.hwhnx.cn
http://M9jWyaYe.hwhnx.cn
http://l7AM3tTL.hwhnx.cn
http://GoSfrX5S.hwhnx.cn
http://XEn36C7c.hwhnx.cn
http://Zy9OEc7O.hwhnx.cn
http://q6hg4EZD.hwhnx.cn
http://sjOjBFh4.hwhnx.cn
http://j8I56XHU.hwhnx.cn
http://RlihbOok.hwhnx.cn
http://fzut8fmU.hwhnx.cn
http://377RIxjV.hwhnx.cn
http://uhsx3v6w.hwhnx.cn
http://V4lPN31s.hwhnx.cn
http://v5WAkekH.hwhnx.cn
http://ea7DA2A1.hwhnx.cn
http://1QwJMCf1.hwhnx.cn
http://Jn5hWSz2.hwhnx.cn
http://XoEaOGjc.hwhnx.cn
http://9SVtkPSi.hwhnx.cn
http://tsp51kAC.hwhnx.cn
http://OlkpvC4H.hwhnx.cn
http://Mgw5SV1Y.hwhnx.cn
http://qRdFWObr.hwhnx.cn
http://www.dtcms.com/a/380349.html

相关文章:

  • 渲染农场多少钱一小时
  • IDEA试用过期,无法登录,重置方法
  • IP验证学习之case编写
  • 通过Dockerfile构建Docker镜像并训练模型
  • 操作系统内核架构深度解析:从微内核到宏内核的设计哲学与性能权衡
  • IIS运行账户设置记录
  • 服务管理 systemctl
  • HTTP与HTTPS
  • devextreme-vue表格设置可复制粘贴
  • Go 语言 PDF 生成库综合比较与实践指南
  • 图技术重塑金融未来:悦数图数据库如何驱动行业创新与风控变革
  • 金融数据---ETF日线行情数据
  • Vue 整体框架全面解析
  • 鸿蒙 NEXT应用国际化:时区与夏令时处理
  • 海外代理IP平台哪家好?高纯净度稳定住宅代理IP平台推荐
  • 锂电池行业生产中 AI 应用场景与价值分析
  • MySQL 命令行导入 SQL 文件
  • 3DMAX自动材质开关插件AutoMaterial安装和使用方法
  • Ubuntu C编程 (make工具和Makefile的引用)
  • 9.12AI简报丨腾讯投资AI游戏平台,B站开源AniSora V3
  • 家庭健康智能终端:解锁智能家居时代的健康管理
  • 机器视觉检测如何使用360 度全景成像镜头进行AI 瑕疵检测
  • # Windows驱动程序开发入门:从原理到实践
  • 在Webpack中集成Vite的开发服务器时,可能会遇到哪些兼容性问题?如何解决?
  • DCA1000 AWR1843 环境安装
  • 零公网IP 跨设备协同OctoPrint+cpolar3D打印远程管理新方法
  • 【Spring】原理解析:Spring Boot 自动配置的核心机制与实战剖析
  • Linux挂在目录空间问题--随手
  • Linux:线程控制详解
  • 花漾TK功能重要更新:界面重大更新、新增店铺数字面板(Dashboard)等(20250820)