C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比

C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 《C++从入门到入土》 ✨ 数据即知识,压缩即智能

文章目录

  • [C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比](#C++ STL 详解:list 的使用、迭代器失效、模拟实现与 vector 对比)
    • 前言
    • [一、list 是什么?](#一、list 是什么?)
    • [二、list 的底层结构:带头结点的双向循环链表](#二、list 的底层结构:带头结点的双向循环链表)
      • [1. 双向链表](#1. 双向链表)
      • [2. 循环链表](#2. 循环链表)
      • [3. 带头结点](#3. 带头结点)
    • [三、list 的构造函数](#三、list 的构造函数)
      • [1. 构造空 list](#1. 构造空 list)
      • [2. 构造 n 个相同元素](#2. 构造 n 个相同元素)
      • [3. 拷贝构造](#3. 拷贝构造)
      • [4. 区间构造](#4. 区间构造)
    • [四、list 的遍历方式](#四、list 的遍历方式)
      • [1. 迭代器遍历](#1. 迭代器遍历)
      • [2. 范围 for 遍历](#2. 范围 for 遍历)
      • [3. 反向迭代器遍历](#3. 反向迭代器遍历)
    • [五、begin、end、rbegin、rend 的关系](#五、begin、end、rbegin、rend 的关系)
    • [六、list 的容量接口](#六、list 的容量接口)
      • [1. empty:判断是否为空](#1. empty:判断是否为空)
      • [2. size:返回有效节点个数](#2. size:返回有效节点个数)
    • [七、list 的元素访问接口](#七、list 的元素访问接口)
      • [1. front:访问第一个元素](#1. front:访问第一个元素)
      • [2. back:访问最后一个元素](#2. back:访问最后一个元素)
    • [八、list 的增删接口](#八、list 的增删接口)
      • [1. push_front:头插](#1. push_front:头插)
      • [2. pop_front:头删](#2. pop_front:头删)
      • [3. push_back:尾插](#3. push_back:尾插)
      • [4. pop_back:尾删](#4. pop_back:尾删)
      • [5. insert:在指定位置前插入](#5. insert:在指定位置前插入)
      • [6. erase:删除指定位置元素](#6. erase:删除指定位置元素)
      • [7. clear:清空所有有效元素](#7. clear:清空所有有效元素)
      • [8. swap:交换两个 list](#8. swap:交换两个 list)
    • [九、list 的迭代器失效问题](#九、list 的迭代器失效问题)
    • [十、erase 后为什么不能继续使用原迭代器?](#十、erase 后为什么不能继续使用原迭代器?)
    • [十一、erase 删除时的正确写法](#十一、erase 删除时的正确写法)
    • [十二、删除 list 中所有偶数](#十二、删除 list 中所有偶数)
    • [十三、list 为什么不支持随机访问?](#十三、list 为什么不支持随机访问?)
    • [十四、list 插入和删除为什么效率高?](#十四、list 插入和删除为什么效率高?)
    • [十五、list 的模拟实现思路](#十五、list 的模拟实现思路)
      • [1. 节点结构](#1. 节点结构)
      • [2. list 的基本成员](#2. list 的基本成员)
      • [3. push_back 的实现思路](#3. push_back 的实现思路)
      • [4. insert 的实现思路](#4. insert 的实现思路)
      • [5. erase 的实现思路](#5. erase 的实现思路)
    • [十六、list 迭代器的模拟实现思路](#十六、list 迭代器的模拟实现思路)
    • 十七、反向迭代器的理解
    • [十八、list 和 vector 的对比](#十八、list 和 vector 的对比)
      • [1. 底层结构](#1. 底层结构)
      • [2. 随机访问](#2. 随机访问)
      • [3. 插入和删除](#3. 插入和删除)
      • [4. 空间利用率和缓存命中](#4. 空间利用率和缓存命中)
      • [5. 迭代器失效](#5. 迭代器失效)
    • [十九、什么时候用 list,什么时候用 vector?](#十九、什么时候用 list,什么时候用 vector?)
      • [适合用 vector 的场景](#适合用 vector 的场景)
      • [适合用 list 的场景](#适合用 list 的场景)
    • [二十、list 常见面试问题](#二十、list 常见面试问题)
      • [1. list 的底层是什么?](#1. list 的底层是什么?)
      • [2. list 支持随机访问吗?](#2. list 支持随机访问吗?)
      • [3. list 的 begin 和 end 分别指向哪里?](#3. list 的 begin 和 end 分别指向哪里?)
      • [4. list 插入会导致迭代器失效吗?](#4. list 插入会导致迭代器失效吗?)
      • [5. list 删除会导致哪些迭代器失效?](#5. list 删除会导致哪些迭代器失效?)
      • [6. erase 后为什么要接收返回值?](#6. erase 后为什么要接收返回值?)
      • [7. list 和 vector 怎么选?](#7. list 和 vector 怎么选?)
    • 总结

前言

在 C++ STL 中,vectorlist 都是非常重要的序列式容器。

如果说 vector 更像是"可以自动扩容的数组",那么 list 更像是"一串用指针连接起来的节点"。

很多同学刚学 list 的时候,会觉得它没有 vector 好用:不能下标访问,遍历起来还要用迭代器,看起来没有那么直观。

list 的优势也很明显:

  • 适合频繁插入和删除
  • 插入时不会整体搬移元素
  • 删除某个节点时,通常只影响被删除节点对应的迭代器
  • 底层结构非常适合帮助我们理解链表和 STL 迭代器

所以学习 list 不只是为了会用一个容器,更重要的是通过它理解 STL 容器背后的设计思想。

这篇文章会从使用入手,再逐步讲到底层结构、迭代器失效、模拟实现和 vector 对比。


一、list 是什么?

list 是 C++ STL 中的一个序列式容器,底层通常是双向链表。

使用前需要包含头文件:

cpp 复制代码
#include <list>

基本使用:

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;

int main()
{
    list<int> lt;

    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);

    for (auto e : lt)
    {
        cout << e << " ";
    }

    return 0;
}

输出:

cpp 复制代码
1 2 3

从表面上看,listvector 都能保存一组数据。

但它们的底层完全不同。

vector 底层是一段连续空间:

text 复制代码
[1][2][3][4][5]

list 底层是一个个节点通过指针连接起来:

text 复制代码
[prev | data | next] <-> [prev | data | next] <-> [prev | data | next]

也正因为底层不同,它们的使用场景也不同。


二、list 的底层结构:带头结点的双向循环链表

为了理解 list,最好先理解它的底层结构。

一个典型的 list 可以抽象成:

这里有几个关键词:

1. 双向链表

每个节点不只知道下一个节点,还知道上一个节点。

text 复制代码
prev <- 当前节点 -> next

所以 list 的迭代器既可以向后走,也可以向前走。

cpp 复制代码
++it; // 向后移动
--it; // 向前移动

这和单链表不同。单链表只能从前往后走,不能直接往回走。

2. 循环链表

最后一个节点的 next 指向头结点,头结点的 prev 指向最后一个节点。

这样整个链表就形成一个环。

3. 带头结点

头结点一般不存储有效数据,它主要用于统一边界情况。

比如:

  • 空链表
  • 第一个节点前插入
  • 最后一个节点后删除
  • begin 和 end 的组织

都可以通过头结点简化处理。

可以简单理解:

text 复制代码
head 是哨兵节点
真实数据从 head->next 开始
end() 通常对应 head 节点的位置

三、list 的构造函数

list 有几种常见构造方式。

1. 构造空 list

cpp 复制代码
list<int> lt;

此时链表中没有有效元素。

2. 构造 n 个相同元素

cpp 复制代码
list<int> lt(5, 10);

表示构造 5 个元素,每个元素都是 10。

内容是:

text 复制代码
10 10 10 10 10

3. 拷贝构造

cpp 复制代码
list<int> lt1{1, 2, 3};
list<int> lt2(lt1);

lt2 会拷贝 lt1 中的所有元素。

4. 区间构造

cpp 复制代码
vector<int> v{1, 2, 3, 4};

list<int> lt(v.begin(), v.end());

这表示用 [first, last) 区间中的元素构造 list

STL 中很多区间都是左闭右开:

text 复制代码
[first, last)

也就是包含 first 指向的位置,不包含 last 指向的位置。


四、list 的遍历方式

list 不支持下标访问。

也就是说,不能这样写:

cpp 复制代码
list<int> lt{1, 2, 3};

cout << lt[0] << endl; // 错误

因为 list 底层不是连续空间,不能像数组一样通过地址偏移直接找到某个元素。

遍历 list 通常有三种方式。


1. 迭代器遍历

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

list<int>::iterator it = lt.begin();

while (it != lt.end())
{
    cout << *it << " ";
    ++it;
}

这里可以把迭代器暂时理解成"指向某个节点的对象"。

cpp 复制代码
*it

表示取出当前节点中的值。

cpp 复制代码
++it

表示移动到下一个节点。


2. 范围 for 遍历

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

for (auto e : lt)
{
    cout << e << " ";
}

如果只是读取元素,范围 for 更简洁。

如果要修改元素,需要使用引用:

cpp 复制代码
for (auto& e : lt)
{
    e *= 2;
}

否则 e 只是元素的拷贝,修改 e 不会影响原链表。


3. 反向迭代器遍历

list 支持反向迭代器:

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

auto rit = lt.rbegin();

while (rit != lt.rend())
{
    cout << *rit << " ";
    ++rit;
}

输出:

cpp 复制代码
4 3 2 1

注意,反向迭代器的 ++ 是向前移动。

这里需要和正向迭代器区别开来:


五、begin、end、rbegin、rend 的关系

对于 list<int> lt{1, 2, 3, 4}; 来说,可以大致理解成:

text 复制代码
head <-> 1 <-> 2 <-> 3 <-> 4 <-> head

其中:

text 复制代码
begin()  指向第一个有效节点,也就是 1
end()    指向最后一个有效节点的下一个位置,通常可以理解为头结点 head

所以:

cpp 复制代码
auto it = lt.begin();
cout << *it << endl; // 1

但是:

cpp 复制代码
auto it = lt.end();
cout << *it << endl; // 错误,end() 不能解引用

end() 是边界位置,不是有效元素。

反向迭代器中:

text 复制代码
rbegin() 从最后一个有效元素开始
rend()   到第一个有效元素之前结束

所以:

cpp 复制代码
auto rit = lt.rbegin();
cout << *rit << endl; // 4

六、list 的容量接口

list 的容量接口比较简单,常见的是:

cpp 复制代码
empty()
size()

1. empty:判断是否为空

cpp 复制代码
list<int> lt;

cout << lt.empty() << endl; // true

如果链表中没有有效元素,返回 true

2. size:返回有效节点个数

cpp 复制代码
list<int> lt{1, 2, 3};

cout << lt.size() << endl; // 3

size() 返回当前 list 中有效元素的个数。

注意,list 没有 capacity()

这是因为 list 不是连续空间,不存在类似 vector 那样"底层容量"的概念。

vector 有:

cpp 复制代码
size()
capacity()
reserve()
resize()

list 只有节点数量,不需要提前扩容。


七、list 的元素访问接口

list 不支持下标访问,但可以访问首尾元素。

1. front:访问第一个元素

cpp 复制代码
list<int> lt{1, 2, 3};

cout << lt.front() << endl; // 1

front() 返回第一个有效节点中值的引用。

所以也可以修改:

cpp 复制代码
lt.front() = 100;

2. back:访问最后一个元素

cpp 复制代码
list<int> lt{1, 2, 3};

cout << lt.back() << endl; // 3

同样,back() 返回的是引用:

cpp 复制代码
lt.back() = 300;

此时链表变成:

text 复制代码
1 2 300

需要注意:如果 list 为空,不要调用 front()back(),否则行为是不安全的。

更稳妥的写法是:

cpp 复制代码
if (!lt.empty())
{
    cout << lt.front() << endl;
}

八、list 的增删接口

list 最核心的优势就在插入和删除。

常见接口有:

cpp 复制代码
push_front()
pop_front()
push_back()
pop_back()
insert()
erase()
clear()
swap()

1. push_front:头插

cpp 复制代码
list<int> lt{2, 3, 4};

lt.push_front(1);

结果:

text 复制代码
1 2 3 4

push_front 会在第一个元素前插入新元素。


2. pop_front:头删

cpp 复制代码
list<int> lt{1, 2, 3};

lt.pop_front();

结果:

text 复制代码
2 3

3. push_back:尾插

cpp 复制代码
list<int> lt;

lt.push_back(1);
lt.push_back(2);
lt.push_back(3);

结果:

text 复制代码
1 2 3

4. pop_back:尾删

cpp 复制代码
list<int> lt{1, 2, 3};

lt.pop_back();

结果:

text 复制代码
1 2

5. insert:在指定位置前插入

cpp 复制代码
list<int> lt{1, 2, 4};

auto it = lt.begin();
++it;
++it; // it 指向 4

lt.insert(it, 3);

结果:

text 复制代码
1 2 3 4

注意:

cpp 复制代码
lt.insert(pos, val);

是在 pos 位置之前插入。


6. erase:删除指定位置元素

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

auto it = lt.begin();
++it; // it 指向 2

lt.erase(it);

结果:

text 复制代码
1 3 4

erase 会删除迭代器指向的节点。

删除后,被删除节点对应的迭代器会失效。


7. clear:清空所有有效元素

cpp 复制代码
list<int> lt{1, 2, 3};

lt.clear();

cout << lt.size() << endl; // 0

clear() 会清空所有有效节点,但链表对象本身还在。


8. swap:交换两个 list

cpp 复制代码
list<int> lt1{1, 2, 3};
list<int> lt2{10, 20};

lt1.swap(lt2);

交换后:

text 复制代码
lt1: 10 20
lt2: 1 2 3

九、list 的迭代器失效问题

迭代器失效是 STL 容器中非常重要的一类问题。

可以先简单理解:

迭代器失效,就是迭代器原来指向的节点或空间已经不能继续安全使用了。

对于 vector 来说,插入元素可能触发扩容,扩容会导致底层空间变化,所以很多迭代器都会失效。

list 不一样。

list 是一个个独立节点连接起来的链表。

插入新节点时,只需要调整前后节点的指针关系,不需要整体搬移已有节点。

所以:

text 复制代码
list 插入元素通常不会导致已有迭代器失效
list 删除元素时,只有指向被删除节点的迭代器失效
其他迭代器不受影响

这一点是 list 相比 vector 的重要优势。


十、erase 后为什么不能继续使用原迭代器?

看下面这段代码:

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

auto it = lt.begin();

while (it != lt.end())
{
    lt.erase(it);
    ++it;
}

这段代码是错误的。

问题出在:

cpp 复制代码
lt.erase(it);

执行完之后,it 指向的节点已经被删除了。

此时 it 已经失效。

后面再写:

cpp 复制代码
++it;

就是对一个失效迭代器进行操作,可能导致程序崩溃或者出现不可预期结果。


十一、erase 删除时的正确写法

更稳妥的写法是使用 erase 的返回值。

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

auto it = lt.begin();

while (it != lt.end())
{
    it = lt.erase(it);
}

为什么这样写?

因为 erase 删除当前节点后,会返回被删除节点后面那个节点的迭代器。

比如:

text 复制代码
1 <-> 2 <-> 3 <-> 4
     ↑
     it 指向 2

执行:

cpp 复制代码
it = lt.erase(it);

删除 2 后:

text 复制代码
1 <-> 3 <-> 4
     ↑
     it 指向 3

这样循环就可以继续安全进行。

也可以看到,有些代码会写成:

cpp 复制代码
lt.erase(it++);

这个写法的含义是:先把旧的 it 传给 erase,同时让 it 移动到下一个位置。

不过初学阶段,我更建议写成:

cpp 复制代码
it = lt.erase(it);

它的意图最清楚,也最不容易误解。


十二、删除 list 中所有偶数

cpp 复制代码
#include <iostream>
#include <list>
using namespace std;

int main()
{
    list<int> lt{1, 2, 3, 4, 5, 6};

    auto it = lt.begin();

    while (it != lt.end())
    {
        if (*it % 2 == 0)
        {
            it = lt.erase(it);
        }
        else
        {
            ++it;
        }
    }

    for (auto e : lt)
    {
        cout << e << " ";
    }

    return 0;
}

输出:

text 复制代码
1 3 5

这里的关键是:

cpp 复制代码
it = lt.erase(it);

如果当前元素要删除,就让 it 接收删除后的下一个位置。

如果当前元素不删除,才手动执行:

cpp 复制代码
++it;

十三、list 为什么不支持随机访问?

vector 可以这样访问:

cpp 复制代码
vector<int> v{1, 2, 3};

cout << v[1] << endl;

因为 vector 底层是连续空间。

假设第一个元素地址是 _start,那么第 i 个元素可以直接通过地址偏移找到:

text 复制代码
_start + i

所以访问 v[i] 是 O(1)。

list 不一样。

list 的节点在内存中不一定连续。

它们可能是这样分布的:

text 复制代码
node1 在地址 A
node2 在地址 C
node3 在地址 B
node4 在地址 F

虽然逻辑上是:

text 复制代码
node1 <-> node2 <-> node3 <-> node4

但物理内存中并不连续。

所以要访问第 4 个元素,只能从头开始一个节点一个节点走过去。

text 复制代码
1 -> 2 -> 3 -> 4

因此访问某个位置的复杂度是 O(N)。

这就是为什么 list 类中没有重载 operator[]


十四、list 插入和删除为什么效率高?

假设现在有链表:

text 复制代码
1 <-> 2 <-> 4

现在要在 2 和 4 之间插入 3。

只需要创建一个新节点,然后修改前后几个指针:

text 复制代码
1 <-> 2 <-> 3 <-> 4

不需要像 vector 那样把后面的元素整体往后搬。

对于 list 来说,只要已经拿到了插入位置的迭代器,插入和删除节点本身通常就是 O(1)。

但这句话有一个前提:

已经找到了位置。

如果你要先从头遍历找到某个位置,这个查找过程仍然是 O(N)。

所以不能简单说:

text 复制代码
list 所有操作都是 O(1)

更准确地说:

text 复制代码
list 在已知位置处插入和删除节点效率高
list 查找某个位置或某个值仍然需要遍历

十五、list 的模拟实现思路

如果要自己模拟实现一个简化版 list,一般需要先定义节点。

1. 节点结构

cpp 复制代码
template<class T>
struct ListNode
{
    ListNode<T>* _prev;
    ListNode<T>* _next;
    T _data;

    ListNode(const T& data = T())
        : _prev(nullptr)
        , _next(nullptr)
        , _data(data)
    {}
};

每个节点包含三部分:

text 复制代码
_prev:指向前一个节点
_next:指向后一个节点
_data:存储数据

2. list 的基本成员

cpp 复制代码
template<class T>
class list
{
    typedef ListNode<T> Node;

public:
    list()
    {
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }

private:
    Node* _head;
};

这里 _head 是头结点。

空链表时:

text 复制代码
_head->_next == _head
_head->_prev == _head

这就是带头双向循环链表的典型结构。


3. push_back 的实现思路

尾插可以复用 insert

尾插本质上是在 end() 位置之前插入。

因为 end() 对应头结点,所以:

cpp 复制代码
push_back(x)

等价于:

cpp 复制代码
insert(end(), x)

简化理解:

cpp 复制代码
void push_back(const T& x)
{
    insert(end(), x);
}

4. insert 的实现思路

假设要在 pos 位置前插入新节点。

原来的连接关系:

text 复制代码
prev <-> pos

插入后:

text 复制代码
prev <-> newnode <-> pos

核心逻辑就是修改四条指针:

cpp 复制代码
newnode->_next = pos;
newnode->_prev = prev;

prev->_next = newnode;
pos->_prev = newnode;

这也是链表插入的本质。


5. erase 的实现思路

删除某个节点时,假设要删除 pos

text 复制代码
prev <-> pos <-> next

删除后:

text 复制代码
prev <-> next

核心逻辑:

cpp 复制代码
prev->_next = next;
next->_prev = prev;
delete pos;

同时,最好返回 next 位置的迭代器,方便外部继续遍历:

cpp 复制代码
iterator erase(iterator pos)
{
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    Node* next = cur->_next;

    prev->_next = next;
    next->_prev = prev;

    delete cur;

    return iterator(next);
}

这也解释了为什么标准库里的 erase 返回下一个位置。


十六、list 迭代器的模拟实现思路

list 的迭代器不能简单地用原生指针 T*

因为 list 的节点不是连续空间。

vector 的迭代器可以接近原生指针,因为它底层连续。

list 的迭代器需要封装节点指针。

大致结构可以这样写:

cpp 复制代码
template<class T>
struct ListIterator
{
    typedef ListNode<T> Node;
    typedef ListIterator<T> Self;

    Node* _node;

    ListIterator(Node* node)
        : _node(node)
    {}

    T& operator*()
    {
        return _node->_data;
    }

    Self& operator++()
    {
        _node = _node->_next;
        return *this;
    }

    Self& operator--()
    {
        _node = _node->_prev;
        return *this;
    }

    bool operator!=(const Self& it) const
    {
        return _node != it._node;
    }

    bool operator==(const Self& it) const
    {
        return _node == it._node;
    }
};

这里的关键是:

cpp 复制代码
operator++()

不是地址加 1,而是走到当前节点的下一个节点:

cpp 复制代码
_node = _node->_next;

这就是 list 迭代器和 vector 迭代器最大的不同之一。


十七、反向迭代器的理解

反向迭代器看起来特殊,其实可以基于正向迭代器进行封装。

核心思想是:

text 复制代码
反向迭代器的 ++ 等价于正向迭代器的 --
反向迭代器的 -- 等价于正向迭代器的 ++

比如正向遍历:

text 复制代码
1 -> 2 -> 3 -> 4

反向遍历:

text 复制代码
4 -> 3 -> 2 -> 1

所以反向迭代器内部可以保存一个正向迭代器 _it

当反向迭代器执行:

cpp 复制代码
++rit;

内部其实可以让正向迭代器:

cpp 复制代码
--_it;

这就是"适配器"的思想:

不重新造一套完整逻辑,而是包装已有能力,让它表现出新的行为。


十八、list 和 vector 的对比

listvector 都是序列式容器,但它们的底层结构不同,所以适用场景也不同。

1. 底层结构

vector

text 复制代码
动态顺序表
底层是一段连续空间

list

text 复制代码
带头结点的双向循环链表
节点之间通过指针连接

2. 随机访问

vector 支持随机访问:

cpp 复制代码
v[i]

时间复杂度是 O(1)。

list 不支持随机访问,访问某个位置需要从头或尾遍历,时间复杂度是 O(N)。


3. 插入和删除

vector 在中间插入和删除时,通常需要搬移元素。

例如:

text 复制代码
[1][2][4][5]

在 2 和 4 之间插入 3:

text 复制代码
[1][2][3][4][5]

后面的 4 和 5 都要往后移动。

如果空间不够,还可能触发扩容,代价更高。

list 在已知位置插入和删除时,只需要修改指针,不需要搬移其他节点。

所以在大量中间插入和删除的场景中,list 更合适。


4. 空间利用率和缓存命中

vector 底层连续,空间利用率较高,缓存友好。

CPU 读取连续内存时通常更高效,所以遍历 vector 往往很快。

list 的每个节点通常单独申请空间。

每个节点除了数据,还要存储前后指针。

text 复制代码
prev + data + next

因此额外空间开销更大,而且节点分散在内存中,缓存利用率通常不如 vector

这也是一个很重要的事实:

list 插入删除理论上很强,但并不代表所有场景都比 vector 快。

在实际开发中,如果数据量不是特别大,或者主要是遍历和随机访问,vector 往往表现更好。因此在实际工程中也有链表已死的说法。


5. 迭代器失效

vector

text 复制代码
插入可能导致扩容,扩容后旧迭代器全部失效
删除元素后,被删位置及其后的迭代器可能失效

list

text 复制代码
插入不会导致其他迭代器失效
删除只会导致被删除节点对应的迭代器失效
其他迭代器通常不受影响

十九、什么时候用 list,什么时候用 vector?

可以简单这样判断。

适合用 vector 的场景

如果你需要:

text 复制代码
高效随机访问
频繁通过下标访问元素
主要在尾部插入数据
希望遍历速度快
希望内存连续、缓存友好

优先考虑 vector

例如:

cpp 复制代码
vector<int> scores;

适合保存成绩、数组、动态表、图像像素数据、算法题中的一维数组等。


适合用 list 的场景

如果你需要:

text 复制代码
频繁在中间插入和删除
已经能快速拿到插入或删除位置的迭代器
不关心随机访问
希望插入删除时尽量不影响其他迭代器

可以考虑 list

例如:

cpp 复制代码
list<int> tasks;

适合某些任务队列、需要稳定迭代器的结构、频繁节点调整的场景。

不过要注意:

如果你每次插入前都要从头遍历查找位置,那么查找本身仍然是 O(N)。

这时候未必一定比 vector 更快。


二十、list 常见面试问题

1. list 的底层是什么?

通常是带头结点的双向循环链表。

2. list 支持随机访问吗?

不支持。

因为节点不连续,不能通过地址偏移直接访问第 n 个元素。

3. list 的 begin 和 end 分别指向哪里?

begin() 指向第一个有效节点。

end() 指向最后一个有效节点的下一个位置,通常可以理解为头结点位置。

end() 不能解引用。

4. list 插入会导致迭代器失效吗?

通常不会导致已有迭代器失效。

因为插入只是新增节点并调整指针,不需要搬移已有节点。

5. list 删除会导致哪些迭代器失效?

删除会导致指向被删除节点的迭代器失效。

其他节点的迭代器通常不受影响。

6. erase 后为什么要接收返回值?

因为当前节点被删除后,原迭代器失效。

erase 返回下一个有效位置,所以遍历删除时应该写:

cpp 复制代码
it = lt.erase(it);

7. list 和 vector 怎么选?

需要随机访问、连续存储、主要尾插:优先 vector

需要频繁中间插入删除,并且不关心随机访问:可以考虑 list


总结

list 是 STL 中非常典型的链式结构容器。

它不像 vector 那样支持随机访问,也没有连续空间带来的缓存优势,但它在插入删除方面有自己的特点。

不过在真实开发中,不要只看时间复杂度,也要考虑缓存命中、空间开销、代码可读性和数据规模。

相关推荐
Tim_101 小时前
【C++】019、内存泄漏
java·开发语言
梅梅绵绵冰1 小时前
算法题-矩阵
数据结构·算法·矩阵
牢姐与蒯1 小时前
c++之异常
开发语言·c++·c++11
此生决int1 小时前
深入理解C++系列(02)——类和对象(上)
开发语言·c++
red_redemption2 小时前
自由学习记录(208)
学习
一起努力啊~2 小时前
DataWhale组队学习笔记--llm-algo-leetcode(五)
笔记·学习·llm
zjun10012 小时前
C++:单例模式
c++·单例模式
是上好佳佳佳呀3 小时前
【机器学习|DAY03】K近邻算法(KNN)笔记
笔记·机器学习·近邻算法
-银雾鸢尾-6 小时前
C#中的拓展方法
开发语言·c#