LIST 的相关知识

STL list容器全面解析:使用、模拟实现与vector对比

在C++的STL序列式容器中,list是一款基于双向循环链表实现的容器,与vector形成了鲜明的特性互补,在大量插入、删除操作的场景中有着不可替代的作用。本文将从list的基础使用、迭代器特性、模拟实现,以及与vector的核心对比几个方面,全面解析list容器的核心知识点,帮助大家掌握其使用方法与底层逻辑。

一、list容器的基础介绍与常用接口

list的底层结构为带头结点的双向循环链表,这一结构决定了它在任意位置的插入、删除操作上的效率优势,同时也让它不支持随机访问。list提供了丰富的接口,掌握核心接口的使用是灵活运用list的基础,以下为核心常用接口的详细说明与使用要点。

1.1 list的构造函数

list提供了四种核心构造方式,满足不同的初始化需求,接口说明如下表:

构造函数 接口说明
list (size_type n, const value_type& val = value_type()) 构造包含n个值为val的元素的list
list() 构造空的list
list (const list& x) 拷贝构造函数,复用已有list的元素构造新list
list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list

1.2 list迭代器的使用

可以将list迭代器暂时理解为指向链表节点的"指针",分为正向迭代器反向迭代器,核心接口及特性如下:

  • begin() + end() :begin返回第一个元素的迭代器,end返回最后一个元素下一个位置的迭代器,为正向迭代器;
  • rbegin() + rend():rbegin返回指向end位置的反向迭代器,rend返回指向begin位置的反向迭代器,为反向迭代器。

迭代器核心注意点

  1. 正向迭代器执行++操作,迭代器向后移动;
  2. 反向迭代器执行++操作,迭代器向前移动(反向迭代器的++等价于正向迭代器的--)。

1.3 list的容量判断

提供两个简单的容量相关接口,用于获取容器的基础容量信息:

  • empty():检测list是否为空,为空返回true,否则返回false;
  • size():返回list中有效节点的个数。

1.4 list的元素访问

由于list不支持随机访问(无法通过[]at()访问),仅提供两个接口访问首尾元素,且返回的是元素的引用,支持直接修改:

  • front():返回list第一个节点中值的引用;
  • back():返回list最后一个节点中值的引用。

1.5 list的修改操作

list的修改接口是其核心,充分体现了链表结构插入、删除的效率优势,核心接口如下表:

函数声明 接口说明
push_front(val) 在list首元素前插入值为val的元素
pop_front() 删除list中第一个元素
push_back(val) 在list尾部插入值为val的元素
pop_back() 删除list中最后一个元素
insert(pos, val) 在list的pos迭代器位置插入值为val的元素
erase(pos) 删除list的pos迭代器位置的元素
swap(lst) 交换两个list中的所有元素
clear() 清空list中的所有有效元素

1.6 list的迭代器失效问题

迭代器失效的本质是迭代器指向的节点变为无效(被删除),list的迭代器失效特性由其底层链表结构决定,与vector有显著区别:

  1. 插入操作:不会导致任何迭代器失效,因为链表插入仅改变节点指针指向,不会挪动原有节点;
  2. 删除操作 :仅导致指向被删除节点的迭代器失效,其他迭代器不受任何影响。

典型错误与改正

删除元素时,若直接使用迭代器遍历删除,会因迭代器失效导致程序崩溃,正确的写法有两种:

c++ 复制代码
// 错误写法:erase后it指向的节点被删除,it失效,++it无意义
void TestListIterator1() {
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    auto it = l.begin();
    while (it != l.end()) {
        l.erase(it);
        ++it;
    }
}

// 正确写法1:利用it++的特性,先传参再自增
void TestListIterator() {
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    auto it = l.begin();
    while (it != l.end()) {
        l.erase(it++); 
    }
}

// 正确写法2:接收erase的返回值(erase返回被删除节点下一个位置的迭代器)
void TestListIterator2() {
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    auto it = l.begin();
    while (it != l.end()) {
        it = l.erase(it);
    }
}

二、list容器的模拟实现

要实现list的模拟实现,核心是还原其带头结点的双向循环链表 底层结构,同时实现核心接口与反向迭代器 。反向迭代器的实现是模拟list的重点与难点,其核心思路是基于正向迭代器进行包装

2.1 核心底层结构

首先实现list的节点结构,包含数据域和两个指针域(前驱、后继),再实现list的容器主体,包含头节点指针,完成基础框架搭建。

2.2 反向迭代器的模拟实现

反向迭代器的核心特性是:++等价于正向迭代器的----等价于正向迭代器的++。因此可以将正向迭代器作为反向迭代器的成员变量,对其接口进行封装,核心实现代码如下:

c++ 复制代码
template<class Iterator>
class ReverseListIterator
{
public:
    // 明确Ref和Ptr是Iterator的内嵌类型
    typedef typename Iterator::Ref Ref;
    typedef typename Iterator::Ptr Ptr;
    typedef ReverseListIterator<Iterator> Self;

    // 构造函数:接收正向迭代器
    ReverseListIterator(Iterator it): _it(it){}

    // 重载*:反向迭代器解引用指向正向迭代器前一个节点
    Ref operator*(){
        Iterator temp(_it);
        --temp;
        return *temp;
    }

    // 重载->:返回解引用结果的地址
    Ptr operator->(){ 
        return &(operator*());
    }

    // 重载++:反向迭代器++,正向迭代器--
    Self& operator++(){
        --_it;
        return *this;
    }

    Self operator++(int){
        Self temp(*this);
        --_it;
        return temp;
    }

    // 重载--:反向迭代器--,正向迭代器++
    Self& operator--(){
        ++_it;
        return *this;
    }

    Self operator--(int){
        Self temp(*this);
        ++_it;
        return temp;
    }

    // 重载比较运算符
    bool operator!=(const Self& l)const {
        return _it != l._it;
    }
    bool operator==(const Self& l)const {
        return _it == l._it;
    }

private:
    Iterator _it; // 封装正向迭代器
};

关键要点 :使用typename明确Iterator::RefIterator::Ptr是内嵌类型,避免编译器将其解析为静态成员变量。

三、list与vector的核心对比

vector和list是STL中最常用的两个序列式容器,二者底层结构不同,导致特性、效率、应用场景天差地别。以下为二者的核心对比,清晰呈现各自的优劣:

对比维度 vector list
底层结构 动态顺序表,一段连续的内存空间 带头结点的双向循环链表
随机访问 支持,访问任意元素效率O(1) 不支持,访问任意元素效率O(N)
插入/删除 任意位置操作效率低,需搬移元素,时间复杂度O(N);插入可能触发增容(开辟新空间+拷贝元素+释放旧空间),效率更低 任意位置操作效率高,无需搬移元素,时间复杂度O(1)
空间利用率 连续空间,不易产生内存碎片,空间利用率高,缓存利用率高 节点动态开辟,小节点易产生内存碎片,空间利用率低,缓存利用率低
迭代器本质 原生态指针(指向连续空间的地址) 对节点指针的封装,非原生态指针
迭代器失效 插入:可能因增容导致所有迭代器失效;删除:仅当前迭代器失效,需重新赋值 插入:无迭代器失效;删除:仅指向被删节点的迭代器失效
核心应用场景 需高效存储、频繁随机访问,对插入/删除效率要求低的场景 需大量插入、删除操作,对随机访问无要求的场景

四、总结

  1. list的底层是带头结点的双向循环链表 ,这一结构决定了它的核心优势是任意位置插入/删除效率高(O(1)) ,劣势是不支持随机访问
  2. list的迭代器失效仅发生在删除操作时,且仅失效指向被删节点的迭代器,插入操作无迭代器失效问题,使用时需注意迭代器的正确更新;
  3. 反向迭代器的实现可基于正向迭代器封装,通过重载运算符实现反向的移动与访问,是list模拟实现的核心考点;
  4. list与vector的选择需根据业务场景判断:频繁随机访问选vector,大量插入/删除选list,二者形成特性互补,覆盖绝大多数序列式容器的使用场景。

掌握list的核心特性与接口使用,结合其与vector的对比,能让我们在实际开发中精准选择合适的容器,提升程序的运行效率。

相关推荐
Old Uncle Tom4 小时前
OpenClaw 记忆系统 -- 记忆预加载
java·数据结构·算法·agent
会编程的土豆4 小时前
洛谷题单入门1 顺序结构
数据结构·算法·golang
JasmineX-17 小时前
数据结构(笔记)——双向链表
c语言·数据结构·笔记·链表
嘻嘻哈哈樱桃10 小时前
牛客经典101题题解集--动态规划
java·数据结构·python·算法·职场和发展·动态规划
电科一班林耿超10 小时前
第 16 课:动态规划专题(二)—— 子序列与子数组问题:面试最高频的 DP 题型
数据结构·算法·动态规划
hnjzsyjyj10 小时前
洛谷 B3622:枚举子集(递归实现指数型枚举)← DFS
数据结构·dfs
qiqsevenqiqiqiqi12 小时前
MT2048三连 暴力→数学推导→O (n) 优化
数据结构·c++·算法
码之气三段.12 小时前
十五届山东ccpc省赛补题(update)
数据结构·c++·算法
保持清醒54014 小时前
二叉链表实现
数据结构
paeamecium14 小时前
【PAT甲级真题】- Recover the Smallest Number (30)
数据结构·算法·pat考试·pat