C++第十讲:list
list 是 STL 中第二个最常用的序列式容器 ,底层是带头结点的双向循环链表,完美解决了 vector 插入删除效率低的痛点。它和 vector 是互补关系:vector 擅长随机访问,list 擅长任意位置插入删除。
一、list 简介
1. 什么是 list
list 是 C++ 标准库提供的双向循环链表容器,每个元素存储在独立的节点中,节点通过指针连接。
-
底层结构:带头结点的双向循环链表
-
特点:
-
✅ 任意位置插入删除效率 O (1)(不需要搬移元素)
-
❌ 不支持随机访问(不能用
[],访问元素需要遍历,O (N)) -
没有容量概念,节点动态申请释放
-
2. 为什么需要 list
vector 的致命缺陷:任意位置插入删除效率极低(需要搬移后面所有元素,O (N)),且扩容会导致大量拷贝。
list 正好弥补了这个缺陷:插入删除只需要修改相邻节点的指针,效率 O (1)。
二、list 常用接口
使用 list 需要包含头文件:#include <list>,所有接口都在std命名空间中。
1. 构造函数
和 vector 完全一致:
| 构造函数 | 功能说明 | 示例 |
|---|---|---|
list<T>() |
无参构造,空 list | list<int> l1; |
list<T>(size_t n, const T& val = T()) |
构造 n 个 val | list<int> l2(5, 10); |
list<T>(const list<T>& l) |
拷贝构造 | list<int> l3(l2); |
list<T>(InputIterator first, InputIterator last) |
迭代器区间构造 | list<int> l4(l2.begin(), l2.end()); |
2. 迭代器
list 的迭代器不是原生指针,而是对节点指针的封装。
| 迭代器 | 功能说明 |
|---|---|
begin()/end() |
正向迭代器:begin 指向第一个数据节点,end 指向头节点 |
rbegin()/rend() |
反向迭代器:rbegin 指向最后一个数据节点,rend 指向头节点 |
注意
list 的迭代器是双向迭代器 ,只支持++和--操作,不支持+n、-n(因为内存不连续):
cpp
list<int> l = {1,2,3,4,5};
auto it = l.begin();
++it; // 正确,指向2
--it; // 正确,指向1
it += 2; // 错误!双向迭代器不支持+=
3. 容量操作
list 没有容量概念,不需要reserve和capacity:
| 函数 | 功能说明 |
|---|---|
size_t size() const |
返回有效节点个数 |
bool empty() const |
判断是否为空 |
void clear() |
清空所有节点 |
4. 元素访问
list 不支持随机访问,只能访问头尾元素:
| 函数 | 功能说明 |
|---|---|
T& front() |
返回第一个元素的引用 |
T& back() |
返回最后一个元素的引用 |
5. 增删改查操作(list 的优势)
| 函数 | 功能说明 | 时间复杂度 |
|---|---|---|
void push_front(const T& val) |
头插 | O(1) |
void pop_front() |
头删 | O(1) |
void push_back(const T& val) |
尾插 | O(1) |
void pop_back() |
尾删 | O(1) |
iterator insert(iterator pos, const T& val) |
在 pos 位置插入 val | O(1) |
iterator erase(iterator pos) |
删除 pos 位置的元素 | O(1) |
void swap(list<T>& l) |
交换两个 list | O (1)(只交换头节点指针) |
代码示例
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> l;
// 尾插
l.push_back(1);
l.push_back(2);
l.push_back(3);
// 头插
l.push_front(0);
// 遍历
for (auto e : l) {
cout << e << " "; // 0 1 2 3
}
cout << endl;
// 插入:在2前面插入10
auto it = l.begin();
while (it != l.end()) {
if (*it == 2) {
l.insert(it, 10);
break;
}
++it;
}
// 删除:删除1
it = l.begin();
while (it != l.end()) {
if (*it == 1) {
it = l.erase(it); // erase返回下一个节点的迭代器
} else {
++it;
}
}
for (auto e : l) {
cout << e << " "; // 0 10 2 3
}
cout << endl;
return 0;
}
三、核心难点:list 迭代器失效
1. 迭代器失效的原因
list 的迭代器指向某个节点,迭代器失效意味着该节点被删除,变成野指针。
2. 两种操作对迭代器的影响
✅ 插入操作:不会导致任何迭代器失效
插入节点只需要修改相邻节点的指针,原来的节点都还在,所以所有迭代器都有效。
❌ 删除操作:只会导致被删除节点的迭代器失效
删除某个节点后,只有指向该节点的迭代器失效,其他迭代器仍然有效。
3. 正确删除元素的写法
和 vector 类似,erase会返回被删除节点的下一个节点的迭代器:
cpp
// 正确:删除所有偶数
int main() {
list<int> l = {1,2,3,4,5,6};
auto it = l.begin();
while (it != l.end()) {
if (*it % 2 == 0) {
it = l.erase(it); // 用返回值更新it
} else {
++it;
}
}
return 0;
}
错误写法
cpp
// 错误:erase后it失效,++it会崩溃
int main() {
list<int> l = {1,2,3,4};
auto it = l.begin();
while (it != l.end()) {
if (*it % 2 == 0) {
l.erase(it); // it失效
}
++it; // 访问失效的迭代器
}
return 0;
}
四、list 底层原理与模拟实现
1. 底层结构
list 的底层是带头结点的双向循环链表,包含两部分:
-
节点结构:每个节点包含数据、前驱指针、后继指针
-
list 类:只包含一个指向头节点的指针
节点结构
cpp
template<class T>
struct ListNode {
T _data; // 数据
ListNode* _prev; // 前驱指针
ListNode* _next; // 后继指针
ListNode(const T& data = T())
: _data(data)
, _prev(nullptr)
, _next(nullptr)
{}
};
list 类结构
cpp
template<class T>
class list {
public:
// 迭代器类型(后面实现)
typedef ListIterator<T> iterator;
typedef ListIterator<const T> const_iterator;
private:
ListNode<T>* _head; // 头节点指针
};
2. 核心函数模拟实现
2.1 构造与析构
cpp
// 创建头节点
void CreateHead() {
_head = new ListNode<T>();
_head->_prev = _head;
_head->_next = _head;
}
// 无参构造
list() {
CreateHead();
}
// 构造n个val
list(size_t n, const T& val = T()) {
CreateHead();
for (size_t i=0; i<n; ++i) {
push_back(val);
}
}
// 拷贝构造(深拷贝)
list(const list<T>& l) {
CreateHead();
for (const auto& e : l) {
push_back(e);
}
}
// 赋值运算符重载(现代版)
list<T>& operator=(list<T> l) {
swap(l);
return *this;
}
// 析构函数
~list() {
clear();
delete _head;
_head = nullptr;
}
// 清空所有节点
void clear() {
iterator it = begin();
while (it != end()) {
it = erase(it);
}
}
// 交换两个list
void swap(list<T>& l) {
std::swap(_head, l._head);
}
2.2 尾插与尾删
cpp
// 尾插
void push_back(const T& val) {
ListNode<T>* newNode = new ListNode<T>(val);
ListNode<T>* tail = _head->_prev;
// 链接节点
tail->_next = newNode;
newNode->_prev = tail;
newNode->_next = _head;
_head->_prev = newNode;
}
// 尾删
void pop_back() {
ListNode<T>* tail = _head->_prev;
ListNode<T>* prevTail = tail->_prev;
// 链接节点
prevTail->_next = _head;
_head->_prev = prevTail;
delete tail;
}
2.3 插入与删除
cpp
// 在pos位置插入val
iterator insert(iterator pos, const T& val) {
ListNode<T>* cur = pos._node;
ListNode<T>* prev = cur->_prev;
ListNode<T>* newNode = new ListNode<T>(val);
// 链接节点
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode); // 返回新节点的迭代器
}
// 删除pos位置的节点
iterator erase(iterator pos) {
ListNode<T>* cur = pos._node;
ListNode<T>* prev = cur->_prev;
ListNode<T>* next = cur->_next;
// 链接节点
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next); // 返回下一个节点的迭代器
}
3. list 迭代器的实现(面试重点)
list 的迭代器不是原生指针 ,而是一个封装了节点指针的类,需要重载*、->、++、--等运算符。
迭代器类定义
cpp
template<class T, class Ref, class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node; // 指向节点的指针
ListIterator(Node* node)
: _node(node)
{}
// 解引用:返回节点数据的引用
Ref operator*() {
return _node->_data;
}
// 箭头:返回节点数据的指针
Ptr operator->() {
return &_node->_data;
}
// 前置++
Self& operator++() {
_node = _node->_next;
return *this;
}
// 后置++
Self operator++(int) {
Self temp(*this);
_node = _node->_next;
return temp;
}
// 前置--
Self& operator--() {
_node = _node->_prev;
return *this;
}
// 后置--
Self operator--(int) {
Self temp(*this);
_node = _node->_prev;
return temp;
}
// 比较
bool operator!=(const Self& s) const {
return _node != s._node;
}
bool operator==(const Self& s) const {
return _node == s._node;
}
};
在 list 类中定义迭代器
cpp
template<class T>
class list {
public:
// 普通迭代器和const迭代器
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
// begin和end
iterator begin() {
return iterator(_head->_next);
}
iterator end() {
return iterator(_head);
}
const_iterator begin() const {
return const_iterator(_head->_next);
}
const_iterator end() const {
return const_iterator(_head);
}
// ... 其他成员函数
};
4. 反向迭代器的实现
反向迭代器的本质是对正向迭代器的封装:
-
反向迭代器的
++= 正向迭代器的-- -
反向迭代器的
--= 正向迭代器的++
cpp
template<class Iterator, class Ref, class Ptr>
struct ReverseIterator {
Iterator _it; // 正向迭代器
ReverseIterator(Iterator it)
: _it(it)
{}
Ref operator*() {
Iterator temp = _it;
--temp;
return *temp;
}
Ptr operator->() {
return &(operator*());
}
// 反向++ = 正向--
ReverseIterator& operator++() {
--_it;
return *this;
}
// 反向-- = 正向++
ReverseIterator& operator--() {
++_it;
return *this;
}
bool operator!=(const ReverseIterator& s) const {
return _it != s._it;
}
};
五、list vs vector(面试必背)
| 对比项 | vector | list |
|---|---|---|
| 底层结构 | 动态顺序表,连续内存 | 带头结点的双向循环链表,非连续 |
| 随机访问 | ✅ 支持,O (1) | ❌ 不支持,O (N) |
| 插入删除效率 | 尾插 O (1),其他位置 O (N)(需要搬移元素) | 任意位置 O (1)(只修改指针) |
| 空间利用率 | 高,连续内存,缓存利用率高 | 低,每个节点需要额外存储两个指针,容易产生内存碎片 |
| 迭代器类型 | 随机访问迭代器(原生指针) | 双向迭代器(封装节点指针) |
| 迭代器失效 | 扩容和 erase 会导致所有迭代器失效 | 只有 erase 会导致被删除节点的迭代器失效 |
| 适用场景 | 频繁随机访问,少量插入删除 | 频繁任意位置插入删除,不需要随机访问 |
关键结论
-
优先使用 vector,除非有大量任意位置的插入删除操作
-
vector 和 list 是互补关系,不是替代关系
六、经典 OJ 实战
1. 反转链表
cpp
// 迭代法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* cur = head;
while (cur) {
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
};
2. 合并两个有序链表
cpp
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while (list1 && list2) {
if (list1->val < list2->val) {
cur->next = list1;
list1 = list1->next;
} else {
cur->next = list2;
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2;
return dummy->next;
}
};
七、本章核心总结
-
list 底层是带头结点的双向循环链表,任意位置插入删除 O (1),不支持随机访问
-
迭代器是封装节点指针的类,重载了 *、->、++、-- 等运算符
-
迭代器失效:只有删除时会导致被删除节点的迭代器失效,插入不会失效
-
反向迭代器是对正向迭代器的封装,++ 对应正向 --
-
和 vector 对比:vector 擅长随机访问,list 擅长插入删除,根据场景选择