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

C/C++ 数据结构(四)链表与STL容器

本篇核心知识:链表头结点设计、STL 容器对比(vector /list/forward_list)、迭代器原理与使用、迭代器失效、仿函数、容器常用算法、C++11 新特性、双向链表手写要求、深浅拷贝与容器类型适配


一、链表头结点详解

概念

头结点是链表额外增设的虚拟节点,不存储有效业务数据,专门用于统一链表操作逻辑,分为带头结点不带头结点两种设计方案。

特性
  1. 不带头结点(首元节点)

    首节点直接存储有效数据,头指针指向第一个数据节点;

    缺陷:在链表头部做插入、删除操作时,必须单独判断边界,代码冗余、易出错。

  2. 带头结点(主流设计)

    额外开辟一块节点内存,无有效数据;

    优势:所有位置的增删操作逻辑完全统一,无需特殊处理头部边界,代码简洁健壮;

    额外开销:多占用一个节点的内存空间,属于空间换代码稳定性。

  3. 适用场景:工程开发、笔试面试均优先使用带头结点链表。

代码示例(两种链表结构对比)

// 1. 不带头结点单链表 struct Node{ int data; Node* next; }; Node* head = nullptr; // 头指针直接指向数据节点 ​ // 2. 带头结点单链表 struct Node{ int data; Node* next; }; Node* head = new Node(); // 头结点,data无有效数据 head->next = nullptr; // 首数据节点为空

拓展

链表排序时,带头结点无需修改头指针指向;不带头结点若交换首节点,必须同步更新头指针,风险更高。


二、STL 核心容器底层与选型对比

概念

C++ STL 容器是基于数据结构封装的通用类模板,不同容器对应不同底层结构,选型核心依据是操作效率、业务场景

主流容器底层、特性、适用场景

1. vector 动态顺序表(动态数组)
概念

底层为连续内存的顺序表,等价于可自动扩容的动态数组。

特性

优点:支持下标随机访问((O(1))),尾部追加 / 删除效率极高;

缺点:中间插入、删除元素效率低,后续元素需要整体前移;

扩容机制:内存不足时自动重新开辟更大空间(主流编译器扩容倍数为 2 倍,部分为 1.5 倍),拷贝原数据并释放旧内存,扩容会产生性能损耗

限制:尽量提前预估容量,减少频繁扩容。

2. list 双向链表
概念

底层为双向循环链表,节点包含前驱prev、后继next两个指针域。

特性

优点:任意位置插入、删除仅修改指针((O(1))),无数据挪动;

缺点:不支持随机访问,必须遍历查找元素;

内存:节点内存离散,存在指针额外开销。

3. forward_list 单向链表(C++11 新增)
概念

底层为单向链表,仅保留后继指针。

特性

内存开销比 list 更小;

仅支持单向遍历,功能精简,适用于极简场景。

容器选型原则(重点)

  1. 频繁查询、尾部增删,极少中间操作 → 优先使用vector

  2. 频繁在任意位置插入 / 删除,查询少 → 优先使用list

  3. 追求极致内存占用、仅单向遍历 → 使用forward_list

代码示例 基础使用

#include <vector> #include <list> using namespace std; ​ int main() { // vector 用法(顺序表) vector<int> vec; vec.push_back(10); // 尾部添加,效率高 vec.pop_back(); // 尾部删除,效率高 ​ // list 用法(双向链表) list<int> lst; lst.push_back(20); lst.push_front(30); // 头部插入,效率极高 return 0; }

拓展

vector 不能直接看作普通数组,它封装了扩容、内存管理逻辑;list 节点由 STL 自动分配释放,无需手动管理堆内存。


三、迭代器(Iterator)

概念

迭代器是 STL 容器的通用遍历工具,本质是对指针的封装,行为和指针高度相似,是连接算法与容器的桥梁。

核心特性

  1. 本质:类模板实现,重载*->++--==!=等运算符;

  2. 通用能力:所有 STL 容器都可通过迭代器遍历,一套写法适配多容器;

  3. 分类:

    普通正向迭代器:begin()(指向首元素)、end()(尾后无效位置,遍历终止标记);

    反向迭代器:rbegin()rend(),从尾部向前遍历;

  4. 区间规则:STL 迭代遵循前闭后开[begin, end)end不指向有效元素。

1. 迭代器遍历(传统写法)

#include <list> using namespace std; ​ int main() { list<int> lst = {1,2,3,4,5}; // 定义迭代器 list<int> it = lst.begin(); // 遍历:it != end 为终止条件 while(it != lst.end()) { cout << *it << " "; // * 解引用,获取元素值 ++it; } return 0; }

2. C++11 范围 for 遍历

概念

底层依旧依赖迭代器,语法更简洁,适合完整遍历容器。

list<int> lst = {1,2,3,4,5}; // 只读遍历 for(auto val : lst) cout << val << " "; ​ // 可修改遍历(加引用) for(auto& val : lst) val += 1;

3. 迭代器失效

概念

容器操作后,原有迭代器指向的内存失效,继续使用会引发野指针、程序崩溃。

不同容器失效规则
  1. vector(顺序表)

    中间插入 / 删除、触发扩容 → 所有迭代器全部失效

    原因:元素整体挪动 / 内存重新分配,原地址作废。

  2. list(双向链表)

    仅被删除节点对应的迭代器失效;

    其余迭代器保持有效;

    原因:仅修改指针指向,其他节点内存不变。

解决方案

执行增删操作后,重新获取迭代器;使用list::erase时接收返回值(返回下一个有效迭代器)。

list<int> lst = {1,2,3}; auto it = lst.begin(); it = lst.erase(it); // 接收返回值,避免迭代器失效

拓展

手写容器(链表 / 顺序表)时,需要自行封装迭代器类,核心就是重载指针相关运算符。


四、模板类与容器类型适配

概念

STL 容器均为类模板,模板参数指定容器存储的数据类型;模板对存储类型有隐性要求。

特性
  1. 模板语法:template<typename T>T为通用类型占位符;

  2. 类型约束:

    存入容器的类型,必须支持拷贝构造(容器添加元素时会拷贝对象);

    自定义类若包含指针成员,必须实现深拷贝,否则出现内存错误;

  3. 推荐用法:存储自定义类型时,优先使用指针,规避浅拷贝问题。

代码示例 模板与类型约束

// 普通类型:天然支持拷贝 list<int> lst1; ​ // 自定义结构体(需保证拷贝合法) struct Person { string name; int age; }; list<Person> lst2; ​ // 存储指针:无需考虑深浅拷贝 list<Person*> lst3;

五、仿函数(函数对象)

概念

仿函数是重载了()括号运算符的类 / 结构体,本质是对象,使用语法和普通函数一致,STL 算法中广泛用于自定义比较、判断逻辑。

特性
  1. 核心实现:类内重载operator()

  2. 分类:

    一元仿函数:接收 1 个参数(用于条件判断,如remove_if);

    二元仿函数:接收 2 个参数(用于大小比较,如sort排序);

  3. 用途:容器排序、条件删除时,自定义规则(默认比较无法满足复杂业务)。

代码示例

示例 1 二元仿函数(自定义排序规则)
#include <list> #include <iostream> using namespace std; ​ // 自定义英雄结构体 struct Hero { string name; int level; // 等级 int hp; // 血量 }; ​ // 二元仿函数:先按等级降序,等级相同按血量降序 struct CompareHero { bool operator()(const Hero& h1, const Hero& h2) { if(h1.level != h2.level) return h1.level > h2.level; return h1.hp > h2.hp; } }; ​ int main() { list<Hero> heroList; // 填充数据 heroList.push_back({"战士", 10, 1000}); heroList.push_back({"法师", 8, 800}); // 传入仿函数对象,自定义排序 heroList.sort(CompareHero()); return 0; }
示例 2 一元仿函数(条件删除)
// 一元仿函数:判断血量小于500 struct DelLowHp { bool operator()(const Hero& h) { return h.hp < 500; } }; // 删除符合条件的元素 heroList.remove_if(DelLowHp());

拓展

默认排序会调用类型原生比较运算符;自定义类型若无重载</>,必须使用仿函数指定规则。


六、链表常用算法与容器对应方法

1. list 合并操作

特性

list::merge():要求两个链表已有相同排序规则,合并后依旧有序,仅修改指针,不拷贝数据;

普通拼接:不要求有序,直接串联两个链表。

2. list 去重unique()

特性

默认判断==相等元素;自定义类型需重载相等运算符,或搭配仿函数。

3. 排序底层说明

list底层排序算法为归并排序,专为链表结构优化;

顺序表常用快速排序,二者底层实现不同。


七、C++11 容器新特性补充

1. forward_list 单向链表

C++11 新增精简版链表,仅单向遍历,内存开销更小,功能少于 list。

2. emplace 系列函数

概念

emplace_back/emplace_front直接在容器内存原地构造对象,替代push_back

特性

push_back:先创建对象,再拷贝到容器(调用拷贝构造);

emplace:直接调用构造函数原地生成,省去拷贝开销,效率更高

list<Hero> lst; lst.emplace("射手",9,900); // 原地构造,无拷贝

八、手写双向链表(综合实战要求)

整体设计要求

  1. 采用带头、带尾结点双向链表,统一边界操作;

  2. 节点结构:数据域 + 前驱指针 prev + 后继指针 next

  3. 封装为类模板,适配任意数据类型;

  4. 内部封装迭代器(类内嵌套实现);

  5. 实现基础接口:增、删、改、查、遍历、排序。

节点基础结构代码

template<typename T> struct Node { T data; Node<T>* prev; // 前驱 Node<T>* next; // 后继 Node(const T& val) : data(val), prev(nullptr), next(nullptr) {} }; // 双向链表模板类 template<typename T> class DoubleList { private: Node<T>* head; // 头结点 Node<T>* tail; // 尾结点 public: DoubleList(); ~DoubleList(); // 基础接口声明 void push_back(const T& val); void pop_back(); void clear(); // 迭代器、排序等接口自行拓展 };

核心注意点

  1. 析构函数必须遍历释放所有节点,防止内存泄漏;

  2. 双向链表增删时,必须同步修改prevnext两个指针,防止断链;

  3. 迭代器嵌套在类内部,设置为public对外访问。


九、知识点总结 & 重点梳理

  1. 容器选型核心

    • 随机访问、尾部操作 →vector;任意位置频繁增删 →list

  2. 迭代器

    • 本质:封装的指针,前闭后开区间;

    • 失效规则:vector 扩容 / 中间操作全失效,list 仅删除节点迭代器失效。

  3. 仿函数

    重载()运算符,用于自定义排序、条件筛选,是 STL 算法核心组件。

  4. 模板约束

    容器存储类型必须支持拷贝,含指针成员优先深拷贝或存储指针。

  5. C++11 优化点

    emplace原地构造、范围 for、forward_list单向链表,均为性能 / 语法优化。

  6. 手写重点

    带头双向链表 + 自定义迭代器,是数据结构实操高频大题。

http://www.jsqmd.com/news/1009253/

相关文章:

  • Nordic芯片量产烧录怎么选?从nRF Connect到离线编程器,四种方法优缺点全解析
  • 告别充电焦虑:一文看懂CCS、CHAdeMO和国标GB/T的充电枪与协议区别(2024版)
  • 2026年租丰田12座中巴怎么选?深圳、成都两大市场品牌横向实测与案例解析 - 优质品牌商家
  • VLM视觉语言模型生产部署2026:图文交错推理的工程挑战
  • 构建强大的RAG应用:从零到一的问答系统开发指南
  • Hive Catalog vs Hadoop Catalog:在Iceberg集成中如何选择与配置?附完整SQL示例
  • 水面黄花蔺分割数据集labelme格式1003张1类别
  • 2026年阿里云Hermes Agent/OpenClaw配置Token Plan集成详细指南
  • 别再只盯着3DR了:聊聊SiK Radio的开源生态与选购避坑指南(含mRo、Holybro型号对比)
  • TFT Overlay:云顶之弈玩家的三大痛点解决方案与实战指南
  • AList项目易主后,我的私人云存储方案还安全吗?聊聊替代方案与数据安全实践
  • 教学辅助系统毕业设计源码
  • 2026年新消息:探访山东沼气池复合土工膜源头厂家山东建通工程科技有限公司 - 品牌鉴赏官2026
  • 别再纠结了!从零到一,手把手教你根据项目场景选MySQL还是PostgreSQL
  • 紧束缚模型中的缺陷态弛豫动力学研究
  • 2026排插品牌哪个好?安全与性能维度解析 - 品牌排行榜
  • 从Kinect到iPhone:聊聊TOF、结构光这些‘黑科技’是怎么一步步走进我们生活的
  • 2026年腾讯云Hermes Agent/OpenClaw配置Token Plan安装全步骤
  • 告别手动搜索!用GAMP_GOOD和Net_diff一站式搞定GNSS数据下载(附详细配置对比)
  • 给MOS管栅极串0欧电阻?实测IX4427驱动芯片在不同工作电压下的表现与选型建议
  • 从实验室到产线:手把手解析立式外延炉的工作原理与核心部件(附主流厂家盘点)
  • 别再只看电流电压了!给硬件新手的MOSFET选型避坑指南(附实战参数表)
  • 别再只盯着UR了:聊聊协作机器人末端执行器的选型与集成避坑指南
  • Rusted PackFile Manager:全面战争MOD开发工作流的革命性重构
  • 教师薪酬管理系统毕业设计
  • 手把手解析:从MIPI D-PHY/C-PHY到A-PHY,车载摄像头接口协议到底怎么选?
  • 3个关键步骤:安全解除原神60帧限制的完整方案
  • SouthUAV虚拟仿真竞赛备赛:如何优化从空三到模型重建的电脑配置与参数?
  • 深入对比:在ZYNQ Linux下用GPIO模拟MDIO,与硬件MDIO控制器相比到底差在哪?
  • S7-1200的PID三兄弟(Compact/3Step/Temp)到底怎么选?一张表帮你搞定选型与快速上手