
⭐️博主: 此生决int-@CSDN博客
** 速胜派就是最大的投降派!!!**
** 🔥热门专栏🔥**
文章目录
- 深入理解C++------list链表
- 一、list的使用
-
- 1,list介绍:
- [2、list 不支持 `operator\[\]`](#2、list 不支持
operator[]) - [3、list 的访问和遍历方式](#3、list 的访问和遍历方式)
- 4,list相关接口⭐️
- [5、使用 list 接口时需要注意的地方(坑!)](#5、使用 list 接口时需要注意的地方(坑!))
-
-
- [① 不支持随机访问](#① 不支持随机访问)
- [② insert()、erase() 的位置必须是迭代器](#② insert()、erase() 的位置必须是迭代器)
- [③ insert()、erase() 不会导致其它迭代器失效(被删除节点除外)](#③ insert()、erase() 不会导致其它迭代器失效(被删除节点除外))
- [④ erase() 会返回下一个位置,](#④ erase() 会返回下一个位置,)
- [⑤ sort() 不能使用 std::sort()](#⑤ sort() 不能使用 std::sort())
- [⑥ reverse() 也尽量使用成员函数](#⑥ reverse() 也尽量使用成员函数)
- [⑦ merge() 合并前必须保证两个链表都有序](#⑦ merge() 合并前必须保证两个链表都有序)
- [⑧ unique() 只能删除连续重复元素](#⑧ unique() 只能删除连续重复元素)
- [⑨ splice() 是剪切,不是复制](#⑨ splice() 是剪切,不是复制)
- [⑩ list 最大优势不是遍历,而是频繁插入删除操作](#⑩ list 最大优势不是遍历,而是频繁插入删除操作)
-
- 5,迭代器的类型:迭代器的简单介绍⭐️⭐️
- [二、list 模拟实现------知识点 & 易错点总结⭐️⭐️⭐️](#二、list 模拟实现——知识点 & 易错点总结⭐️⭐️⭐️)
-
- [一、list 底层结构](#一、list 底层结构)
- 二、关于模板
- 三、接口实现
-
- [1、list_node ------链表的节点类,](#1、list_node ——链表的节点类,)
- [2、iterator ------迭代器类⭐️⭐️⭐️](#2、iterator ——迭代器类⭐️⭐️⭐️)
- [3、empty_init() ⭐️](#3、empty_init() ⭐️)
- 4、默认构造
- [5、begin() / end()](#5、begin() / end())
- [6、insert() ⭐️⭐️⭐️](#6、insert() ⭐️⭐️⭐️)
- 7、push_back()
- 8、push_front()
- [9、erase() ⭐️⭐️⭐️](#9、erase() ⭐️⭐️⭐️)
- 10、pop_back()
- 11、pop_front()
- [12、clear() ⭐️⭐️⭐️](#12、clear() ⭐️⭐️⭐️)
- 13、析构函数
- [14、拷贝构造 ⭐️⭐️](#14、拷贝构造 ⭐️⭐️)
- [15、initializer_list 构造](#15、initializer_list 构造)
- 16、swap()
- [17、operator= ⭐️⭐️](#17、operator= ⭐️⭐️)
- 18、size()
- 19、empty()
- 四,主播在模拟实现时犯的那些错!
- 五,补充知识:
-
- [**1、iterator 为什么设计成三个模板参数?⭐️⭐️⭐️**](#1、iterator 为什么设计成三个模板参数?⭐️⭐️⭐️)
- [**10、operator->() 为什么返回指针?⭐️⭐️⭐️**](#10、operator->() 为什么返回指针?⭐️⭐️⭐️)
- 三、总结与回顾
-
- [一、list 的底层结构](#一、list 的底层结构)
- [二、list 与 vector 最大区别](#二、list 与 vector 最大区别)
- [三、list 最重要的接口](#三、list 最重要的接口)
- 下期预告
- 结语
上期回顾
上一篇我们主要学习了 vector ,重点掌握了 vector的相关接口,以及模拟实现了vector,正式开始了STL的学习。如果还有印象模糊的地方,建议先回顾上一篇内容,再继续阅读本文。
深入理解C++------list链表
精简目录:
一、list的使用
1、list介绍
2、list 不支持 operator\[\]
3、list 的访问和遍历方式
4、list相关接口 ⭐
5、使用 list 接口时需要注意的地方(坑!)
6、迭代器的类型:迭代器的简单介绍 ⭐⭐
二、list 模拟实现------知识点 & 易错点总结 ⭐⭐⭐
1、list 底层结构
2、关于模板
3、接口实现
4、主播在模拟实现时犯的那些错!
5、补充知识
三、总结与回顾
四,下期预告
stack 和 queue
五,结语
一、list的使用
1,list介绍:
1、C++里面list 的底层结构
list底层采用 带头双向循环链表 实现。
2、list 的构造方式
list 的构造方式与前面学习过的 vector、string 基本一致,都是 STL 容器统一的设计风格,因此学习起来并不陌生。
| 构造方式 | 说明 | 示例 |
|---|---|---|
| 默认构造 | 创建一个空链表 | list<int> lt; |
| 指定元素个数 | 创建 n 个元素,每个元素为默认值 | list<int> lt(10); |
| 指定元素个数和值 | 创建 n 个值为 val 的元素 | list<int> lt(10, 1); |
| 区间构造 | 用一段迭代器区间构造 | list<int> lt(v.begin(), v.end()); |
| 拷贝构造 | 用已有 list 创建新对象 | list<int> lt2(lt1); |
| 初始化列表构造(C++11) | 使用 {} 初始化 |
list<int> lt{1,2,3,4}; |
| 移动构造(C++11)了解 | 转移资源,提高效率 | list<int> lt2(std::move(lt1)); |
| 例如: |
cpp
list<int> lt; // 空链表:{}
list<int> lt(10); // 创建10个元素,每个元素默认初始化为0:{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
list<int> lt(10, 1); // 创建10个元素,每个元素初始化为1:{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
list<int> lt(v.begin(), v.end()); // 用区间[v.begin(), v.end())中的元素初始化链表:内容与v完全相同
list<int> lt2(lt1); // 拷贝构造:lt2的内容与lt1完全相同
list<int> lt{1, 2, 3, 4}; // 列表初始化(C++11):{1, 2, 3, 4}
2、list 不支持 operator[]
vector 由于采用连续存储,可以通过地址计算实现随机访问,因此支持:operator[],时间复杂度为 O(1)。
而 list 的节点分散在内存各处,访问第 i 个元素必须沿着链表逐个遍历,因此时间复杂度为 O(N)。
所以,list没有提供operator\[\]
3、list 的访问和遍历方式
由于不能随机访问,list 主要通过:
front()、back()- 迭代器(Iterator)
- 范围
for来访问元素。
最常见的遍历方式:
cpp
for (auto e : lt)
{
cout << e << " ";
}
或者:
cpp
for (auto it = lt.begin(); it != lt.end(); ++it)
{
cout << *it << " ";
}
4,list相关接口⭐️
大多数与vector类似:
| 接口 | 作用 | 时间复杂度(典型) |
|---|---|---|
push_back() |
尾插 | O(1) |
push_front() |
头插 | O(1) |
pop_back() |
尾删 | O(1) |
pop_front() |
头删 | O(1) |
front() |
获取首元素 | O(1) |
back() |
获取尾元素 | O(1) |
begin() / end() |
获取迭代器 | O(1) |
insert() |
指定位置前插入 | O(1)(已知位置) |
erase() |
删除指定位置 | O(1)(已知位置) |
clear() |
清空链表 | O(N) |
size() |
元素个数 | O(1)(现代 STL 实现) |
empty() |
是否为空 | O(1) |
reverse() |
链表逆置 | O(N) |
sort() |
链表排序 | O(N log N) |
| 链表特有的: | ||
merge() |
合并两个有序链表 | O(N+M) |
unique() |
删除连续重复元素 | O(N) |
remove() |
删除指定值 | O(N) |
splice() |
(剪切)节点转移并且不拷贝数据 | O(1)(单节点)或与转移区间长度相关 |
cpp
//==================== splice ====================
list<int> lt1{1, 2, 3, 4};
list<int> lt2{10, 20, 30};
// 将lt2中的所有节点转移到lt1末尾(不会拷贝数据,只修改节点链接)
lt1.splice(lt1.end(), lt2);
// lt1:1 2 3 4 10 20 30
// lt2:空
//==================== remove ====================
list<int> lt3{1, 2, 3, 2, 4, 2, 5};
// 删除所有值为2的节点
lt3.remove(2);
// lt3:1 3 4 5
//==================== unique ====================
list<int> lt4{1, 1, 2, 2, 2, 3, 4, 4, 5};
// 删除连续重复元素(必须是相邻的重复元素)
lt4.unique();
// lt4:1 2 3 4 5
//==================== merge ====================
list<int> lt5{1, 3, 5, 7};
list<int> lt6{2, 4, 6, 8};
// 合并两个有序链表
// 合并后lt5仍然有序,lt6变为空
lt5.merge(lt6);
// lt5:1 2 3 4 5 6 7 8
// lt6:空
5、使用 list 接口时需要注意的地方(坑!)
① 不支持随机访问
list 底层是双向链表,因此不能像 vector 一样随机访问元素。
下面的代码都是错误的:
cpp
list<int> lt{1,2,3,4};
cout << lt[0] << endl; // ❌ 不支持[]
cout << lt.at(0) << endl; // ❌ 没有at()
auto it = lt.begin() + 2; // ❌ 迭代器不能+2
如果想访问第 3 个元素,只能:
cpp
auto it = lt.begin();
advance(it, 2); // 向后移动2次
cout << *it << endl;
原因:
链表节点在内存中不是连续存储的,无法通过地址直接定位到第 n 个元素。
② insert()、erase() 的位置必须是迭代器
不像 vector 可以使用下标,list 的插入删除都是基于迭代器。
cpp
list<int> lt{1,2,3,4};
auto it = lt.begin();
++it;
lt.insert(it, 100); // 在2前面插入100
lt.erase(it); // 删除2
③ insert()、erase() 不会导致其它迭代器失效(被删除节点除外)
这是 list 最大的优势。
例如:
cpp
list<int> lt{1,2,3,4};
auto it = lt.begin(); // 指向1
lt.push_back(5);
lt.push_front(0);
cout << *it << endl; // 仍然是1
对于链表来说:
- 插入节点
- 删除其它节点
都不会影响已有节点的位置。
只有被删除节点对应的迭代器会失效。
例如:
cpp
auto it = lt.begin();
lt.erase(it);
// it已经失效
// *it; // ❌ 未定义行为
对比与vector
vector可能失效,因为可能扩容!
cpp
vector<int> v{1, 2, 3, 4};
auto it = v.begin(); // 指向1
// 假设这里触发扩容
v.push_back(5);
// it可能已经失效(未定义行为)
// cout << *it << endl; // ❌ 不安全
注意: 如果 push_back() 没有触发扩容,那么已有元素不会移动,原来的迭代器仍然有效。因此 vector 的迭代器是否失效,取决于是否发生了扩容。
erase后也是失效,并且后面的所有迭代器也全都失效了!
cpp
vector<int> v{1, 2, 3, 4, 5};
auto it = v.begin() + 1; // 指向2
v.erase(it);
// it失效
从删除位置开始,到 end() 之间的所有迭代器全部失效。
一句话总结!
list:节点不移动,所以迭代器很稳定;只有删除的那个节点对应的迭代器失效。
vector:元素可能移动(扩容或搬移),所以迭代器容易失效。
④ erase() 会返回下一个位置,
cpp
list<int> lt{1, 2, 3, 4, 5};
// 找到值为3的位置
auto it = lt.begin();
++it;
++it; // it指向3
// 删除3,并返回下一个节点(即4)的迭代器
it = lt.erase(it);
cout << *it << endl; // 输出:4
// 此时链表内容:1 2 4 5
//如果要用erase来删除整个链表的内容:
auto it = lt.begin(); // 从头开始遍历删除
while (it != lt.end())
{
// 删除当前节点,并返回下一个节点的位置
it = lt.erase(it);
}
⑤ sort() 不能使用 std::sort()
cpp
sort(lt.begin(), lt.end()); // ❌
这是错误的。因为:std::sort() 需要随机访问迭代器 。而 list 只有双向迭代器。
应该调用成员函数:
cpp
lt.sort(); // ✔
⑥ reverse() 也尽量使用成员函数
cpp
lt.reverse();
成员函数专门针对链表实现,效率更高。
⑦ merge() 合并前必须保证两个链表都有序
例如:
cpp
list<int> lt1{1,3,5};
list<int> lt2{2,4,6};
lt1.merge(lt2);
结果:
text
1 2 3 4 5 6
如果链表无序:
cpp
list<int> lt1{3,1,5};
list<int> lt2{6,2,4};
lt1.merge(lt2); // ❌ 结果不可预期(不会自动排序)
正确做法应是先排序再合并!
cpp
lt1.sort();
lt2.sort();
lt1.merge(lt2);
⑧ unique() 只能删除连续重复元素
如果想删除所有重复元素,一般先排序:
cpp
lt.sort();
lt.unique();
⑨ splice() 是剪切,不是复制
例如:
cpp
list<int> lt1{1,2,3};
list<int> lt2{4,5,6};
lt1.splice(lt1.end(), lt2);
结果:
text
lt1:1 2 3 4 5 6
lt2:空
splice = 剪切(节点转移),节点直接移动过去,没有任何数据拷贝,因此效率非常高。
⑩ list 最大优势不是遍历,而是频繁插入删除操作
5,迭代器的类型:迭代器的简单介绍⭐️⭐️
1、迭代器的简单认识
迭代器(Iterator) 可以理解为 STL 为所有容器提供的一种统一访问元素的方式 。

| 迭代器类型 | 支持操作 | 典型容器/对象 |
|---|---|---|
| 输入迭代器(Input Iterator) | ++、*(只读) |
istream_iterator |
| 输出迭代器(Output Iterator) | ++、*(只写) |
ostream_iterator |
| 前向迭代器(Forward Iterator) | ++ |
forward_list、unordered_map、unordered_set |
| 双向迭代器(Bidirectional Iterator) | ++、-- |
list、set、map |
| 随机访问迭代器(Random Access Iterator) | ++、--、+、-、[] |
vector、string、deque |
对于 STL 容器的学习,我们主要关注后三种:前向迭代器、双向迭代器和随机访问迭代器。输入迭代器和输出迭代器主要用于流(输入输出)操作,后续学习算法时再接触即可。
二、list 模拟实现------知识点 & 易错点总结⭐️⭐️⭐️
具体实现见博主的 Gitee 仓库,博主的 Gitee 链接:财哥
一、list 底层结构
list底层采用带头双向循环链表实现。- 每个节点都包含三部分:
_data:存储数据。_next:指向后继节点。_prev:指向前驱节点。
- 链表中始终存在一个哨兵节点(头节点)
二、关于模板
template<class T>决定链表存储的数据类型。- 为了同时支持普通迭代器和 const 迭代器,迭代器设计成三个模板参数:
cpp
template<class T,class Ref,class Ptr>
其中:
Ref控制operator*()返回类型。Ptr控制operator->()返回类型。
最终利用 typedef 得到两种迭代器:
cpp
typedef list_iterator<T,T&,T*> iterator;
typedef list_iterator<T,const T&,const T*> const_iterator;
这样就无需再写一个 const_iterator 类。
三、接口实现
1、list_node ------链表的节点类,
每个节点维护:
- 数据
- 前驱指针
- 后继指针
cpp
template<class T>
struct list_node
{
T _data;//数据
list_node<T>* _next;//后继指针
list_node<T>* _prev;//前驱指针
};
2、iterator ------迭代器类⭐️⭐️⭐️
list 的迭代器本质就是对节点指针进行封装。
至少需要实现:
operator*operator->operator++operator--- 前置/后置 ++ --
- == 与 !=
operator*
cpp
Ref operator*()
{
return _node->_data;
}
operator->()
一定要明确他的作用:
cpp
it->_a1
实际上会被编译器翻译成
cpp
it.operator->()->_a1
所以必须返回
cpp
&_node->_data;
而不是返回对象本身。
cpp
Ptr operator->()
{
return &_node->_data;
}
前置++、--
cpp
Self& operator++()//前置++
{
_node = _node->_next;
//return _node;注意返回类型
return *this;
}
后置++、--
注意:
后置 ++ 返回修改前的值。返回值不能加引用
cpp
Self operator++(int)
{
Self tmp(_node);
_node = _node->_next;
return tmp;
}
注意返回值不能是引用!!!。
3、empty_init() ⭐️
由于多个构造函数都会初始化哨兵节点,因此将重复代码提取出来。
cpp
void emptry_init()
{
Node* head = new Node();
_head = head;
//哨兵节点没有很好的初始化
_head->_next = _head;
_head->_prev = _head;
//size也没有初始化
_size = 0;
}
注意:
_next、_prev都必须指向自己。- 别忘了初始化
_size。
4、默认构造
默认构造直接调用初始化函数即可。
cpp
list()
{
emptry_init();
}
5、begin() / end()
由于采用带头双向循环链表:
- begin 指向第一个有效节点
- end 指向哨兵节点
cpp
iterator begin()
{
return _head->_next;
}
iterator end()
{
return _head;
}
同时需要提供 const 版本。
6、insert() ⭐️⭐️⭐️
插入操作是整个 list 最核心的接口。
代码核心:
cpp
void insert(iterator pos, const T& x)
{
//Node newnode(x);不能定义在栈上
Node* newnode = new Node(x);
newnode->_next = pos._node;
newnode->_prev = pos._node->_prev;
//pos._node->_prev->_next = newnode;
pos._node->_prev->_next = newnode;
pos._node->_prev = newnode;
//别忘了维护size
_size++;
}
最后可以优化为:
cpp
//insert返回的是新插入元素的位置
template<class T>
typename list<T>::iterator list<T>::insert(iterator pos, const T& x)// typename list<T>::iterator list类里面的iterator类型,不是变量 list<T>::insert(iterator pos, const T& x)
{
//Node newnode(x);不能定义在栈上
Node* newnode = new Node(x);
newnode->_next = pos._node;
newnode->_prev = pos._node->_prev;
//pos._node->_prev->_next = newnode;
pos._node->_prev->_next = newnode;
pos._node->_prev = newnode;
//别忘了维护size
_size++;
return newnode;
}
7、push_back()
尾插实际上就是:
cpp
insert(end(), x);
8、push_front()
头插实际上就是:
cpp
insert(begin(), x);
9、erase() ⭐️⭐️⭐️
代码核心:
cpp
void erase(iterator pos)
{
//不能删除end
assert(pos != end());
pos._node->_prev->_next = pos._node->_next;
pos._node->_next->_prev = pos._node->_prev;
//delete (*pos);不对!!!
// delete *(pos._node);也不对!!
// delete 的参数必须是一个指针,但是释放的是这个指针所指向的对象。
delete pos._node;
//别忘了维护size
size--;
}
注意:
- 不能删除
end()。 delete的对象是指针,而不是对象。- 返回下一个位置的迭代器,方便连续删除。
10、pop_back()
尾删直接调用:
cpp
erase(--end());
11、pop_front()
头删直接调用:
cpp
erase(begin());
12、clear() ⭐️⭐️⭐️
不能简单移动头指针。必须真正释放所有节点。
由于erase 后,it 已经失效。所以,必须先保存下一个位置。
cpp
void clear()
{
//这个不能简单的移动指针,而要全部删除
//for (auto it = begin(); it != end(); ++it)
//{
// //erase(it);经典问题,迭代器失效
// /*iterator tmp = it;
// erase(tmp);*///这样也是不对的,还是失效
//}
// erase 后 tmp 会失效,因此必须提前保存下一个位置
auto it = begin();
while (it != end())
{
auto tmp = it;
++it;//必须先让他走到下一个位置
erase(tmp);
}
//最简单的写法:
/*while (!empty())
{
pop_back();
}*/
}
13、析构函数
cpp
~list()
{
clear();
delete _head;
}
14、拷贝构造 ⭐️⭐️
先初始化空链表。再依次尾插。
cpp
list(const list& l)
{
emptry_init();
for(auto it=l.begin();it!=l.end();++it)
{
push_back(*it);
}
}
注意:
- 不要直接复制
_head。 - 必须深拷贝所有节点。
15、initializer_list 构造
支持:
cpp
list<int> lt={1,2,3,4};
实现思路:
cpp
for(const auto& e:il)
{
push_back(e);
}
坑:
initializer_list是只读序列。- 不支持
[]。 - 一般使用范围 for 遍历。
16、swap()
交换两个 list:
cpp
void swap(list& l)
{
std::swap(_head, l._head);
//忘记交换size
std::swap(_size, l._size);
}
17、operator= ⭐️⭐️
采用 Copy-Swap 写法:
cpp
list& operator=(list l)
{
swap(l);
return *this;
}
18、size()
cpp
return _size;
19、empty()
cpp
return _size==0;
四,主播在模拟实现时犯的那些错!
1,初始化列表写法错误(今天出现 2 次)
初始化列表:要用括号初始化!!!
cpp
list()
:_head(nullptr)
,_size(0)
{}
2,begin() / end() 返回错误
注意我们实现的是:带头双向循环链表:,有哨兵节点!
3,节点不要申请到栈上
一般都要用new 来申请空间
4,delete 理解错误
new的时候是T*什么什么,所以,delete后面也是跟指针 ,就是和malloc类似
5,忘记维护 _size
6,assert判断写反
7,重复维护 _size
erase已经维护了size
8,注意:erase后迭代器失效!但是可以接收继续使用
9,initializer_list 构造函数
支持:begin,end,size;
建议:以后每写一个接口,都建议自己问一句:这个接口会不会改变链表的结构?如果会,需要同步维护哪些成员 ?还有,不要忘记写返回值!!!
五,补充知识:
1、iterator 为什么设计成三个模板参数?⭐️⭐️⭐️
小知识:如果两份代码逻辑完全相同,仅仅类型不同,就应该考虑使用模板来消除重复代码。
为了能够同时支持普通迭代器和 const 迭代器,如果只使用一个模板参数 T,我们通常需要分别实现两个迭代器类
cpp
template<class T>
struct list_iterator
{
T& operator*();
T* operator->();
......
};
template<class T>
struct list_iterator
{
const T& operator*();
const T* operator->();
......
};
我们可以发现:普通迭代器 和 const 迭代器 的区别其实只有两个地方:
operator*()返回值不同operator->()返回值不同
除了这两个返回值,其余代码(++、--、==、!= 等)完全一样。
如果分别写两个类:那么 90% 的代码都会重复。
因此 STL 采用三个模板参数:
cpp
template<class T,class Ref,class Ptr>
其中:
cpp
Ref决定operator*()
Ptr决定operator->()
于是只需要写一份代码:
cpp
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
那他什么时候会实例化?
当你在后面用到的时候,比如:
cpp
list_iterator<T, T&, T*> begin()
{
return _head->_next;
}
list_iterator<T, const T&, const T*> begin() const
{
return _head->_next;
}
那当编译的时候,他就可以分别实例化出类似于
cpp
struct list_iterator<int, int&,int*>
{
int& operator*();
int* operator->();
......
};
struct list_iterator<int, const int&, const int*>
{
const int& operator*();
const int* operator->();
......
};
也就是说,这里是用来两套模版,
第一层 T 用来决定容器存储的数据类型(如 int、double、string)。
第二层 T、Ref、Ptr 用来决定迭代器访问元素时返回的类型,是const修饰还是普通的,从而复用普通迭代器和 const 迭代器的大部分代码。
我说白了,这就是把两段只有类型不同的代码把类型换成一个通用的名字和并成一份代码,然后,后面根据不同的模版参数实例化出不同类型的代码!!
最后再利用 typedef(或 using)为这两个具体类型分别起一个名字:
cpp
typedef list_iterator<T,T&,T*> iterator;
typedef list_iterator<T,const T&,const T*> const_iterator;
总结:
三个模板参数并不是为了支持不同的数据类型,而是为了复用普通迭代器和 const 迭代器的大部分实现,只让返回值不同。
10、operator->() 为什么返回指针?⭐️⭐️⭐️
我们首先要明白,在迭代器里面,我们想要达到什么样的效果
cpp
struct AA
{
int _a1;
};
list<AA> lt;
auto it = lt.begin();
it->_a1 = 10;
想要就是迭代器it->_a1 可以访问到list里面存的自定义类型里面的数据!
那么,我们要访问到AA里面的数据肯定是要这样子的
cpp
AA*->_a1
因此,operator->() 必须返回一个指针。
正确实现:
cpp
Ptr operator->()
{
return &_node->_data;//&这里是取地址符号哦!
}
千万不要写成:
cpp
return _node->_data;
总结:
写
operator->()之前,一定先想清楚它是怎样被调用的。
it->_a1实际上等价于:
cppit.operator->()->_a1;所以返回值必须是 指针,而不是对象,也不是引用。
三、总结与回顾
一、list 的底层结构
✅ list 底层采用带头双向循环链表实现。
每个节点包含:
_data:存储数据_next:后继节点_prev:前驱节点
整个链表始终存在一个哨兵节点(head)。
二、list 与 vector 最大区别
| vector | list |
|---|---|
| 连续存储 | 链式存储 |
支持 [] |
不支持 [] |
| 随机访问 O(1) | 随机访问 O(N) |
| 尾插效率高 | 任意位置插删效率高 |
| 扩容可能导致迭代器失效 | 节点不移动,迭代器更稳定 |
一句话:
vector 擅长访问;list 擅长插删。
三、list 最重要的接口
大部分与vector一样,list新增的
cpp
merge()//合并两个有序链表
remove()//删除指定值
unique()//删除连续重复元素
splice()//剪切
- 不支持\[\]、at()、begin()+n 因为:list 只有双向迭代器,不支持随机访问。
- 插入、删除必须使用迭代器
erase()返回的是下一个位置的迭代器sort()要调用成员函数。merge()前必须保证两个链表有序unique()只能删除连续重复元素如果想去重,一般先排序:splice()是剪切节点,不是复制数据。因此效率非常高- operator->() 必须返回指针,而不是对象。
下期预告
stack和queue
结语
** 本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**
** 也欢迎订阅我的深入理解 C++系列专栏、快速复习系列专栏 、算法系列专栏。**
** 愿每一次敲下键盘,都比昨天更进一步!** ** 愿每一行代码落下,都让未来多一种可能!**