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. 手写重点

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

相关推荐
不吃土豆的马铃薯1 小时前
C++ 正则表达式入门详解
linux·服务器·网络·数据库·c++·正则表达式
满怀冰雪1 小时前
第15篇-链表基础-反转链表-合并链表与快慢指针
java·算法·链表
玖玥拾2 小时前
C/C++ 数据结构(一)基础概念、线性表链表
c语言·数据结构·c++·链表
星恒随风2 小时前
C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念
开发语言·c++·笔记·学习
芋只因2 小时前
力扣100题解(Java版)
数据结构
郝学胜-神的一滴2 小时前
Qt 高级开发 031:QListWidget图标布局实战
开发语言·c++·qt·程序人生·软件构建·用户界面
QiLinkOS2 小时前
极客精神与商业思维的融合实践(3)
c语言·c++·人工智能·算法·开源协议
牛油果子哥q2 小时前
队列(Queue)深度精讲,先进先出原理、顺序/链式/循环队列、STL queue底层、栈队列互模拟与面试考点全解
开发语言·c++·面试
暖阳华笺2 小时前
【数据结构与算法】哈希专题
数据结构·c++·算法·leetcode·哈希算法