list 容器详解:基本介绍与常见使用

一、list的介绍和使用

1. list 的介绍

list 是 C++ STL 中的一种容器,底层采用环状双向链表 结构存储数据。与 vector 不同,list 并不支持随机访问,但其在任意位置的插入和删除操作都非常高效。

主要特点:

  • 底层结构:环状双向链表

  • 元素存储:节点独立,非连续内存

  • 迭代器类型:双向迭代器

  • 支持正向和反向遍历

  • 适用于频繁插入/删除、不关心随机访问的场景

cpp 复制代码
#include <list>
#include <algorithm>

list<int> mylist;
auto it = find(mylist.begin(), mylist.end(), 3);

2. list 的使用(常用接口)

2.1 list 的构造

构造函数 说明
list() 构造一个空的 list
list(size_type n, const value_type& val = value_type()) 构造一个包含 n 个值为 val 的元素的 list
list(const list& x) 拷贝构造函数
list(InputIterator first, InputIterator last) [first, last) 区间中的元素构造 list
cpp 复制代码
list<int> l1;                 // 空list
list<int> l2(5, 10);          // 5个10
list<int> l3(l2);             // 拷贝构造
list<int> l4(l2.begin(), l2.end()); // 迭代器区间构造

2.2 list 迭代器的使用

迭代器可理解为指向链表节点的指针,支持移动和访问。

函数声明 说明
begin() / end() 返回第一个元素的正向迭代器 / 返回最后一个元素下一个位置的正向迭代器
rbegin() / rend() 返回最后一个元素的反向迭代器 / 返回第一个元素前一个位置的反向迭代器

注意

  • begin() / end():正向迭代器,++ 操作指向下一个节点

  • rbegin() / rend():反向迭代器,++ 操作指向前一个节点

cpp 复制代码
list<int> lst = {1, 2, 3, 4, 5};

// 正向遍历
for(auto it = lst.begin(); it != lst.end(); ++it) {
    cout << *it << " ";
}

// 反向遍历
for(auto it = lst.rbegin(); it != lst.rend(); ++it) {
    cout << *it << " ";
}

2.3 list 容量相关接口

函数声明 说明
empty() 判断 list 是否为空
size() 返回 list 中有效元素的个数
cpp 复制代码
list<int> lst;
cout << lst.empty() << endl; // 1
lst.push_back(1);
cout << lst.size() << endl;  // 1

2.4 list 元素访问

函数声明 说明
front() 返回第一个元素的引用
back() 返回最后一个元素的引用

⚠️ 注意 :使用 front()back() 前需确保 list 非空,否则行为未定义。

cpp 复制代码
list<int> lst = {10, 20, 30, 40, 50};

cout << "第一个元素: " << lst.front() << endl;  // 10
cout << "最后一个元素: " << lst.back() << endl; // 50

// 修改第一个元素
lst.front() = 100;
cout << "修改后第一个元素: " << lst.front() << endl; // 100

2.5 list 插入操作

函数声明 说明
push_front(const value_type& val) 在链表头部插入元素
pop_front() 删除头部元素
push_back(const value_type& val) 在链表尾部插入元素
pop_back() 删除尾部元素
insert(iterator pos, const value_type& val) 在指定位置前插入元素
insert(iterator pos, size_type n, const value_type& val) 在指定位置前插入 n 个相同元素
insert(iterator pos, InputIterator first, InputIterator last) 在指定位置前插入迭代器区间内的元素
cpp 复制代码
list<int> lst = {1, 2, 3};

// 头尾插入删除
lst.push_front(0);     // {0, 1, 2, 3}
lst.push_back(4);      // {0, 1, 2, 3, 4}
lst.pop_front();       // {1, 2, 3, 4}
lst.pop_back();        // {1, 2, 3}

// insert 操作
auto it = lst.begin();
++it;                  // 指向第二个元素(值为2)
lst.insert(it, 99);    // {1, 99, 2, 3}

lst.insert(lst.begin(), 2, 88);  // {88, 88, 1, 99, 2, 3}

list<int> other = {100, 200};
lst.insert(lst.end(), other.begin(), other.end()); // {88, 88, 1, 99, 2, 3, 100, 200}

2.6 list 删除操作

函数声明 说明
erase(iterator pos) 删除指定位置的元素,返回下一个位置的迭代器
erase(iterator first, iterator last) 删除 [first, last) 区间内的元素,返回最后一个被删除元素的下一个位置
clear() 清空所有元素
remove(const value_type& val) 删除所有值为 val 的元素
remove_if(UnaryPredicate pred) 删除所有满足条件的元素(谓词返回 true)
cpp 复制代码
list<int> lst = {1, 2, 3, 2, 4, 2, 5};

// 按位置删除
auto it = lst.begin();
++it;                  // 指向第二个元素(值为2)
lst.erase(it);         // 删除该元素 → {1, 3, 2, 4, 2, 5}

// 区间删除
auto first = lst.begin();
auto last = lst.begin();
++last; ++last;        // first 指向1,last指向2(第二个2)
lst.erase(first, last); // 删除1和3 → {2, 4, 2, 5}

// 按值删除
lst.remove(2);         // 删除所有值为2的元素 → {4, 5}

// 条件删除(删除所有偶数)
lst.push_back(6);      // {4, 5, 6}
lst.remove_if([](int n) { return n % 2 == 0; }); // 删除4和6 → {5}

// 清空所有元素
lst.clear();           // {}

2.7 其他常用操作

函数声明 说明
resize(size_type n, value_type val = value_type()) 调整链表大小为 n,超出部分删除,不足部分用 val 填充
assign(size_type n, const value_type& val) 用 n 个 val 替换当前内容
assign(InputIterator first, InputIterator last) 用迭代器区间内的元素替换当前内容
swap(list& x) 交换两个 list 的内容
reverse() 反转链表元素顺序
unique() 删除链表中相邻的重复元素(仅保留一个)
sort() 对链表元素进行排序(默认升序)
merge(list& x) 合并两个已排序的链表,结果放入当前链表,x 变为空
cpp 复制代码
// resize
list<int> lst = {1, 2, 3};
lst.resize(5, 100);    // {1, 2, 3, 100, 100}
lst.resize(2);         // {1, 2}

// assign
lst.assign(4, 7);      // {7, 7, 7, 7}

list<int> other = {10, 20, 30};
lst.assign(other.begin(), other.end()); // {10, 20, 30}

// swap
list<int> a = {1, 2, 3};
list<int> b = {4, 5, 6};
a.swap(b);             // a = {4,5,6}, b = {1,2,3}

// reverse
list<int> nums = {1, 2, 3, 4, 5};
nums.reverse();        // {5, 4, 3, 2, 1}

// unique(仅删除相邻重复)
list<int> dup = {1, 1, 2, 2, 2, 3, 1, 1};
dup.unique();          // {1, 2, 3, 1}  ------ 注意最后两个1不相邻,未被删除

// sort
list<int> unsorted = {5, 2, 8, 1, 9};
unsorted.sort();       // {1, 2, 5, 8, 9}
unsorted.sort(greater<int>()); // 降序 {9, 8, 5, 2, 1}

// merge(需两个list都已排序)
list<int> l1 = {1, 3, 5};
list<int> l2 = {2, 4, 6};
l1.merge(l2);          // l1 = {1,2,3,4,5,6}, l2 = {}

二、list迭代器失效问题

1. list的底层实现特点

std::list底层是双向链表,每个节点独立分配内存,节点间通过指针连接。这种结构决定了:

  • 插入/删除操作只影响相邻节点的指针,不会导致其他节点内存移动

  • 迭代器本质上是指向链表节点的指针

2. 迭代器失效场景分析

2.1 插入操作:不失效 ✅

cpp 复制代码
#include <list>
#include <iostream>

int main() {
    std::list<int> lst = {1, 2, 3};
    auto it = lst.begin();  // 指向元素1
    
    lst.insert(it, 100);    // 在it之前插入100
    
    std::cout << *it << std::endl;  // 依然输出1,it未失效
    // 输出:1
}

结论list的插入操作不会导致任何已有迭代器失效。

2.2 删除操作:被删元素的迭代器失效 ❌

cpp 复制代码
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();  // it指向1
++it;                   // it指向2

lst.erase(it);          // 删除元素2

// std::cout << *it << std::endl;  // 未定义行为!it已失效

关键点:只有被删除元素对应的迭代器失效,其他迭代器(包括指向前后元素的迭代器)依然有效。

2.3 正确删除的两种方式

方式一:利用erase返回值
cpp 复制代码
std::list<int> lst = {1, 2, 3, 4, 5};
for (auto it = lst.begin(); it != lst.end(); ) {
    if (*it % 2 == 0) {
        it = lst.erase(it);  // erase返回下一个元素的有效迭代器
    } else {
        ++it;
    }
}
// lst = {1, 3, 5}
方式二:使用remove/remove_if(更简洁)
cpp 复制代码
std::list<int> lst = {1, 2, 3, 4, 5};
lst.remove_if([](int n) { return n % 2 == 0; });
// lst = {1, 3, 5}

2.4 clear()操作:所有迭代器失效 ❌

cpp 复制代码
std::list<int> lst = {1, 2, 3};
auto it = lst.begin();
lst.clear();        // 清空所有元素
// it 已失效,不能再使用

2.5 splice操作:迭代器保持有效 ✅

splice(转移节点)是list特有的操作,转移的节点本身不会导致迭代器失效:

cpp 复制代码
std::list<int> lst1 = {1, 2, 3};
std::list<int> lst2 = {4, 5, 6};

auto it = lst1.begin();  // 指向1
auto it2 = lst2.begin(); // 指向4

lst1.splice(it, lst2, it2);  // 将lst2的it2节点转移到lst1中it之前

std::cout << *it2 << std::endl;  // 输出4,it2依然有效(指向被转移的节点)
// 但注意:it2现在属于lst1了

3. 与其他容器的对比

操作 vector list deque
头部插入 所有迭代器失效 不失效 可能失效
中间插入 所有迭代器失效 不失效 可能失效
尾部插入 仅end失效(扩容时全部失效) 不失效 不失效
删除元素 被删元素及之后迭代器失效 仅被删元素失效 被删元素及之后迭代器失效

4. 常见陷阱与最佳实践

陷阱1:循环中错误删除

cpp 复制代码
// ❌ 错误写法
for (auto it = lst.begin(); it != lst.end(); ++it) {
    if (*it == target) {
        lst.erase(it);  // it失效后还执行++it,未定义行为
    }
}

// ✅ 正确写法
for (auto it = lst.begin(); it != lst.end(); ) {
    if (*it == target) {
        it = lst.erase(it);
    } else {
        ++it;
    }
}

陷阱2:保存迭代器后执行删除

cpp 复制代码
std::list<int> lst = {1, 2, 3};
auto it = std::next(lst.begin());  // 指向2
lst.erase(lst.begin());            // 删除1,it依然有效 ✅
lst.erase(it);                     // 删除2,有效

// 但注意:如果删除的就是it指向的元素
auto it2 = lst.begin();
lst.erase(it2);  // it2失效
// std::cout << *it2;  // 错误!

陷阱3:list的迭代器与vector不同

cpp 复制代码
// vector的insert会导致迭代器失效
std::vector<int> vec = {1, 2, 3};
auto vit = vec.begin();
vec.insert(vit, 100);
// std::cout << *vit;  // ❌ 未定义行为,vit可能失效

// list的insert安全得多
std::list<int> lst = {1, 2, 3};
auto lit = lst.begin();
lst.insert(lit, 100);
std::cout << *lit;  // ✅ 输出1,安全

三、list与vector对比

在 C++ STL 中,vectorlist 是最常用的两种序列式容器。虽然它们都可以存储元素,但底层实现差异巨大,直接决定了它们的性能表现与适用场景。

1. 底层结构

特性 vector list
底层结构 动态顺序表,一段连续内存空间 带头结点的双向循环链表
内存分配 一次性分配连续内存,容量不足时重新分配并拷贝 每次插入动态分配节点内存

2. 访问方式

  • vector :支持随机访问(下标 []),时间复杂度 O(1),适合频繁读取任意位置元素。

  • list :不支持随机访问,只能通过迭代器顺序遍历,访问中间元素时间复杂度 O(N)

3. 插入与删除

  • vector

    • 非尾部插入/删除需要移动大量元素,时间复杂度 O(N)

    • 尾部插入可能触发扩容(重新分配、拷贝、释放旧空间),开销较大

  • list

    • 任意位置插入/删除只需调整指针,时间复杂度 O(1)

    • 无扩容概念,不会引起整体元素拷贝

4. 空间与缓存效率

  • vector

    • 空间连续,缓存友好,遍历效率高

    • 内存碎片少,空间利用率高

  • list

    • 节点分散,缓存不友好,遍历时频繁跳转

    • 小节点易造成内存碎片,额外存储前后指针(通常 8~16 字节/节点)

5. 迭代器

特性 vector list
迭代器类型 原生指针 对节点指针的封装
插入时失效 可能全部失效(扩容导致) 不会失效
删除时失效 删除位置及之后的迭代器失效 仅当前迭代器失效

⚠️ 迭代器失效是实际开发中极易踩的坑,尤其是在遍历过程中做删除或插入操作,必须谨慎处理。

6. 适用场景

场景 推荐容器 理由
需要频繁随机访问、尾部增删 vector O(1) 随机访问,尾部增删高效
大量中间插入/删除,很少访问 list O(1) 插入/删除,无扩容成本
小数据量、频繁遍历 vector 缓存命中率高
需要稳定迭代器(插入不失效) list 迭代器稳定性强
相关推荐
Book思议-2 小时前
【数据结构】字符串模式匹配:暴力算法与 KMP 算法实现与解析
数据结构·算法·kmp算法·bf算法
顶点多余2 小时前
线程互斥+线程同步+生产消费模型
java·linux·开发语言·c++
Albert Edison2 小时前
【ProtoBuf 语法详解】更新消息|保留字段|未知字段
开发语言·c++·protobuf
mifengxing2 小时前
力扣HOT100——(1)两数之和
java·数据结构·算法·leetcode·hot100
無限進步D2 小时前
算竞常用STL cpp
开发语言·c++·算法·竞赛
罗湖老棍子2 小时前
【 例 1】区间和(信息学奥赛一本通- P1547)(基础线段树和单点修改区间查询树状数组模版)
数据结构·算法·线段树·树状数组·单点修改 区间查询
Book思议-3 小时前
【数据结构】栈与队列核心对比
数据结构·栈与队列对比
南境十里·墨染春水3 小时前
C++ 笔记 深赋值 浅赋值(面向对象)
开发语言·jvm·c++·笔记
旺仔.2913 小时前
常用算法 详解
数据结构·算法