❀保持低旋律节奏->个人主页
文章目录
一、list介绍
list容器使用双链表来实现,每个元素指针通过前驱和后续相关联。元素可以存储在不连续的不同位置。
- 核心优势
1.任意位置的插入 / 删除操作是常数时间(O (1)):只要有对应位置的迭代器,在列表的任何位置插入或删除元素都很高效
2.支持双向迭代:可以向前或向后遍历元素。 - 主要劣势
1.不支持随机访问:无法像数组、vector 那样通过下标 [] 直接访问元素(例如访问第 6 个元素,必须从已知位置遍历,时间复杂度为 O (n))。
2.内存开销大:每个元素需要额外存储前驱和后继指针,对于小元素、大规模列表,内存消耗会比较明显 - 与其他容器相比
1.对比 vector/array/deque:在 "任意位置的插入 / 删除" 场景下表现更好,适合排序等频繁操作元素位置的算法。
2.对比 forward_list:forward_list 是单向链表,更小巧高效,但仅支持单向遍历;std::list 是双向链表,支持双向遍历。
双链表介绍

哨兵节点的介绍
在链表数据结构中,哨兵节点(Sentinel Node) 是一个不存储实际业务数据的虚拟节点,主要作用是统一链表操作的逻辑,消除边界条件的特殊处理,让代码更简洁、更不易出错。

场景 | 无哨兵实现 | 有哨兵实现 |
---|---|---|
链表为空时 | 需要单独判断 head == nullptr | 无需判断(哨兵的 next 自然是 nullptr) |
删除头节点时 | 需要单独处理(头节点无前驱) | 无需特殊处理(头节点的前驱是哨兵) |
代码复杂度 | 多分支判断,逻辑分散 | 逻辑统一,无额外边界判断 |
出错概率 | 高(容易遗漏边界情况) | 低(所有节点处理逻辑一致) |
有无哨兵节点的对比------底层!
cpp
//存在哨兵的代码
#include <list>
#include<iostream>
using namespace std;
template<class T>
class ListNode
{
public:
//using T* = ListNode*; //T是一个具体的类ListNode是一个结构体
//using node = ListNode<T>; //ListNode<T> 是一个结构体 node 也是一个结构体
T val;
ListNode<T>* prev;
ListNode<T>* next;
ListNode(T val) : val(val), prev(nullptr), next(nullptr) {}
};
template<class T>
ListNode<T>* del(ListNode<T>*head,T target)
{
//using node = ListNode<T>;
//边界一:链表为空
if (head == nullptr)return nullptr;
//边界二:删除头节点
if (head->val == target)
{
ListNode<T>* newHead = head->next;
delete head;
return newHead;
}
//情况三:处理非头节点
ListNode<T>* prev = head;
while (prev->next != nullptr)
{
if (prev->next->val == target)
{
ListNode<T>* toDelete = prev->next;
// 同步更新后继节点的prev指针(双向链表关键)
if (toDelete->next != nullptr) {
toDelete->next->prev = prev;
}
prev->next = toDelete->next;
delete toDelete;
break;
}
prev = prev->next;
}
return head;
}
int main() {
// 创建链表:1 <-> 2 <-> 3
ListNode<int>* head = new ListNode<int>(1);
head->next = new ListNode<int>(2);
head->next->prev = head; // 2的prev指向1
head->next->next = new ListNode<int>(3);
head->next->next->prev = head->next; // 3的prev指向2
// 删除值为2的节点
head = del(head, 2);
// 验证结果:1 <-> 3(正向和反向遍历)
cout << "正向遍历:";
ListNode<int>* cur = head;
while (cur != nullptr) {
cout << cur->val << " ";
cur = cur->next;
}
// 输出:1 3
cout << "\n反向遍历:";
cur = head->next; // 此时cur指向3
while (cur != nullptr) {
cout << cur->val << " ";
cur = cur->prev; // 反向遍历(依赖prev指针)
}
// 输出:3 1
return 0;
}
cpp
//无哨兵的代码
#include <list>
#include<iostream>
using namespace std;
template<class T>
class ListNode
{
public:
T val;
ListNode<T>* prev;
ListNode<T>* next;
ListNode(T val) : val(val), prev(nullptr), next(nullptr) {}
};
template<class T>
ListNode<T>* del(ListNode<T>* head, T target)
{
// 创建哨兵节点(值无意义,仅作虚拟头节点)
ListNode<T>* sentinel = new ListNode<T>(0);
sentinel->next = head; // 哨兵指向原头节点
if (head != nullptr) {
head->prev = sentinel; // 原头节点的prev指向哨兵(双向链表维护)
}
// 从哨兵开始遍历(统一所有节点的前驱逻辑)
ListNode<T>* prev = sentinel;
while (prev->next != nullptr)
{
if (prev->next->val == target)
{
ListNode<T>* toDelete = prev->next;
// 同步更新后继节点的prev指针
if (toDelete->next != nullptr) {
toDelete->next->prev = prev;
}
prev->next = toDelete->next;
delete toDelete;
break;
}
prev = prev->next;
}
// 恢复新头节点,释放哨兵
ListNode<T>* newHead = sentinel->next;
if (newHead != nullptr) {
newHead->prev = nullptr; // 新头节点prev置空(脱离哨兵)
}
delete sentinel; // 哨兵完成使命,释放内存
return newHead; // 返回新头节点
}
int main() {
// 创建链表:1 <-> 2 <-> 3
ListNode<int>* head = new ListNode<int>(1);
head->next = new ListNode<int>(2);
head->next->prev = head; // 2的prev指向1
head->next->next = new ListNode<int>(3);
head->next->next->prev = head->next; // 3的prev指向2
// 删除值为2的节点
head = del(head, 2);
// 验证结果:1 <-> 3(正向和反向遍历)
cout << "正向遍历:";
ListNode<int>* cur = head;
while (cur != nullptr) {
cout << cur->val << " ";
cur = cur->next;
}
// 输出:1 3
cout << "\n反向遍历:";
cur = head->next; // 此时cur指向3
while (cur != nullptr) {
cout << cur->val << " ";
cur = cur->prev; // 反向遍历(依赖prev指针)
}
// 输出:3 1
return 0;
}
二、list使用
list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已
达到可扩展的能力。以下为list中一些常见的重要接口。
2.1list构造
常见的构造 list构造
构造函数( (constructor)) | 接口说明 |
---|---|
list (size_type n, const value_type& val =value_type()) | 造的list中包含n个值为val的元素 |
list() | 构建空list |
list(const list&x) | 拷贝构造 |
list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造 |
2.2list iterator的使用
此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。

1.begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
2.rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
2.3list capacity
2.4 ❀list element access
函数声明 | 接口使用 |
---|---|
front | 返回list的第一个节点中值的引用 |
back | 返回list最后一个节点中值的引用 |
2.5list modifiers
迭代器失效
前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
类型 | insert | erase |
---|---|---|
string | 1.若插入后未触发扩容:插入位置及之后的迭代器全部失效(元素后移,位置改变)。2.若插入后触发扩容:所有迭代器、指针、引用全部失效(内存地址整体改变)。 | 删除位置及之后的迭代器全部失效(元素前移,位置改变)。未删除的元素(删除位置之前)的迭代器仍然有效。 |
vector | 与string 同理 | 与string同理 |
list | 所有迭代器都不会失效(插入的新元素不会改变原有元素的地址和指针关系)。 | 只有被删除元素的迭代器失效,其他所有迭代器(包括前驱和后继)仍然有效。 |
三、list和vector对比
vector | list | |
---|---|---|
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问 ,访问某个元素效率O(N) |
插入和删除 | 任意位置插入和删除效率低 ,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 |
任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低 ,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
四、对list的一些补充
list::sort
list库里面存在一个单独的sortlist::sort
algorithm 里面也有sortalgorithm::sort
- 那么两者有什么区别?
algorithm里面的sort使用的是快速排序,我们都知道快速排序在排序算法里面已经是最快的了
而list里面的sort使用的是归并排序
在进行大量数据排序比较时,使用list::sort就会花费很大的代价。因此不建议使用list::sort
list 底层迭代器的实现