【C++ 】STL详解(五)—玩转 list,你真的懂了吗?
坚持用 清晰易懂的图解 + 代码语言,让每个知识点变得简单!
🚀呆头个人主页详情
🌱 呆头个人Gitee代码仓库
📌 呆头详细专栏系列
座右铭: “不患无位,患所以立。”
【C++ 】STL详解(五)—玩转 list,你真的懂了吗?
- list文档参考----------请点击
- 摘要
- 目录
- 一、`list`的介绍
- 二、`list`的使用
-
- 1.构造函数
- 2.特殊成员函数
-
- 析构函数`~list`
- 赋值运算符重载`operator=`
- 3.迭代器的使用
-
- `begin` 和 `end`
- `rbegin` 和 `rend`
- 4.容器相关函数
-
- `size`和`empty`
- 5.修改容器内容相关函数
-
- `push_front()` 和 `emplace_front()`
- `emplace_back()`- `push_back()`
- `insert()` 和 `emplace()`
- `pop_front()`和 `pop_back()`
- `erase()`和 `clear()`
- `resize`
- `assign`
- `swap`
- 6.list的相关操作函数
-
- `sort()`
- 2. **`remove()`**
- 3. **`unique()`**
- 4. **`merge()`**
- 5. **`reserve()`**
list文档参考----------请点击
摘要
🚀 欢迎来到《C++修炼之路》!
这里是C++程序员的成长乐园,带你领略从面向对象到现代C++的精彩世界。我们将>用简洁的代码和生动的案例,助你掌握C++核心精髓。
🔍 专栏亮点:
- 现代C++特性解析(C++11/14/17)
- STL源码剖析与实战应用
- 内存管理与性能优化技巧
💡 收获预期:
✔️ 写出更健壮的C++代码
✔️ 深入理解面向对象设计
✔️ 掌握模板编程基础📌 编程箴言:
“好的C++代码就像好酒,需要时间沉淀。”
(正文开始👇)---- 本文学习list的使用
目录
一、list
的介绍
list
是C++标准库中一个序列式容器,它采用双向链表结构,使得在常数时间内能够在任意位置进行插入和删除操作。这一特性使得list
在需要频繁修改数据的场景中,表现得尤为高效。与vector
和deque
这类基于数组的容器相比,list
能够更轻松地应对复杂的数据操作,尤其是插入和删除数据。list
的底层结构是双向链表,每个元素(节点)都存储在独立的内存空间中,并且通过两个指针相互关联:一个指向前一个节点,另一个指向后一个节点。这种结构使得list
能够灵活地在任意位置插入或删除元素,而不需要像vector
那样频繁地移动其他元素。list
与forward_list
有些相似,二者的主要区别在于链表的类型:list
是双向链表,而forward_list
是单向链表。这意味着forward_list
仅能进行单方向的迭代(从前到后),而list
支持双向迭代,可以从前向后和从后向前遍历。这一差异使得list
在某些需要双向遍历的场景中,显得更加灵活和高效。
优点:
- 高效的插入和删除操作:在
list
中,插入和删除元素的时间复杂度是 O(1),无论是在容器的头部、尾部,还是在中间位置,这使得它在频繁进行数据修改时,具有明显的性能优势。- 双向迭代:
list
支持双向迭代,可以从前到后,也可以从后到前,这对于某些需要逆向遍历的操作非常方便。
缺点:
- 缺乏随机访问:与
vector
或deque
不同,list
不支持通过下标来访问元素,这意味着不能像数组那样直接访问任意位置的元素。这是它的一大局限,尤其是在需要快速随机访问元素的场景下。- 额外的内存开销:
list
需要为每个元素存储前后指针,这会导致比其他容器(如vector
)消耗更多的内存。对于存储小型数据的容器来说,这一额外的空间开销可能是一个不可忽视的问题。
二、list
的使用
1.构造函数
让我们如上图的构造进行解释:explicit list(const allocator_type& alloc = allocator_type());----默认构造函数
这个构造函数创建一个空的 list,即没有任何元素。它接受一个 allocator_type 类型的参数,这个参数用于自定义内存分配策略,默认为标准分配器 allocator_type()。(免了频繁的向堆上申请空间,提高效率)
explicit list(size_type n, const value_type& val = value_type(),
const allocator_type& alloc = allocator_type());----填充构造函数该构造函数会创建一个含有 n 个元素的 list,并且所有元素的初始值都为 val(默认为类型 value_type 的默认值)。同样地,您可以选择使用自定义的分配器来管理内存。
template
list(InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());----范围构造函数此构造函数接受两个迭代器,first 和 last,并根据这两个迭代器所指向的范围来初始化 list 中的元素。这种构造函数通常用于通过已有容器或数组来初始化 list。
list(const list& x);----拷贝构造函数
该构造函数用于创建一个与已有 list 完全相同的新 list,也就是说,所有的元素和分配器都会被复制到新的容器中。
#include#includeusing namespace std;void test01(){list<int> l1; //默认构造list<int> l2(6, 6); //填充构造list<int> l3(l2.begin(), l2.end()); //迭代器范围构造int arr[] = { 5,2,0,1,3,1,4 };//数组初始化范围构造list<int> l4(arr, arr + 7);list<int> l5(l2);//拷贝构造}int main(){test01();return 0;}
运行结果如下:
2.特殊成员函数
析构函数~list
- 析构函数 是类中的一个特殊成员函数,它在对象生命周期结束时自动调用。其主要作用是释放对象所占用的资源。
- 对于
std::list
来说,析构函数会在list
对象销毁时自动释放它所管理的内存,包括容器中的每个元素(即每个节点)所占的内存。
赋值运算符重载operator=
赋值运算符重载(operator=) 允许我们定义如何将一个对象赋值给另一个对象。它是在 C++ 中非常常见的操作,尤其是在处理对象之间的赋值时。对于 std::list 等容器类来说,赋值运算符重载的目标是确保容器内部的元素被正确地复制。
3.迭代器的使用
begin()
,end()
,rbegin()
,rend()
用于获取std::list
容器的开头或结尾位置的迭代器。要接收迭代器时,通常使用iterator
类型,它是std::list
类内部定义的类型。由于iterator
是在类内部通过typedef
定义的,它属于类的成员类型,因此在使用时需要使用::
来指定类的域,明确指定是哪个类的迭代器。值得注意的是,迭代器区间是左闭右开区间的,即区间包含起始迭代器指向的元素,但不包含结束迭代器指向的元素。
由于
std::list
支持迭代器,范围for
循环的原理是被编译器转换为迭代器循环,因此它也可以用于list
中。与直接使用迭代器相比,范围for
的功能稍显有限:它只能实现正向遍历,而迭代器则支持正向和反向遍历(通过rbegin()
和rend()
)。
begin
和 end
int arr[] = { 5,2,0,1,3,1,4};list<int> l1(arr, arr + 7);list<int>::iterator it = l1.begin();while (it != l1.end()){cout << *it << \' \';it++;}cout << endl;
rbegin
和 rend
int arr[] = { 5,2,0,1,3,1,4};list<int> l1(arr, arr + 7);list<int>::reverse_iterator rit = l1.rbegin();while (rit != l1.rend()){cout << *rit << \' \';rit++;}cout << endl;
4.容器相关函数
size
和empty
size()获取当前容器有效数据个数,然后empty通过bool判断容器是否为空
list<int> l1;list<int> l2(6, 6);cout << \"l1 size: \" << l1.size() << endl;cout << \"l2 size: \" << l2.size() << endl;cout << \"l1 : \" << l1.empty() << endl;cout << \"l2 : \" << l2.empty() << endl;
5.修改容器内容相关函数
push_front()
和 emplace_front()
push_front()
:将一个元素添加到容器的前端。这个元素是通过拷贝或移动构造的。emplace_front()
:直接在容器前端原地构造元素,避免了拷贝或移动操作,效率较高,尤其是对于复杂类型。
int arr[] = { 5,2,0,1,3,1,4};int arr1[] = { 1,3,1,4,5,2,0 };list<int> l1(arr, arr + 7);list<int> l2(arr1, arr1 + 7);l1.emplace_front(6);l2.push_front(100);
emplace_back()
- push_back()
push_back()
:将一个元素添加到容器的末端,通过拷贝或移动。emplace_back()
:直接在容器的末端构造元素,避免拷贝或移动,效率更高。
int arr[] = { 5,2,0,1,3,1,4};int arr1[] = { 1,3,1,4,5,2,0 };list<int> l1(arr, arr + 7);list<int> l2(arr1, arr1 + 7);l1.emplace_back(6);l2.push_back(100);
insert()
和 emplace()
insert()
- 迭代器不失效:
在
std::list
中,insert()
操作不会导致迭代器失效,因为插入只是修改了节点间的链接关系。
- 插入单个元素:
insert()
可以在指定位置之前插入一个元素,返回指向新插入元素的迭代器。
- 插入多个相同元素:
可以在指定位置之前插入多个相同的元素,返回指向第一个新插入元素的迭代器。
- 插入区间:
可以在指定位置之前插入一个迭代器区间中的所有元素,返回指向第一个新插入元素的迭代器。
- 位置迭代器的要求:
若要在特定位置(非开头或结尾)插入,必须提前将迭代器移动到该位置。
emplace()
:与insert()
相似,但在指定位置 直接构造 元素,避免了拷贝操作。
int arr[] = { 5,2,0,1,3,1,4};int arr1[] = { 1,3,1,4,5,2,0 };list<int> l1(arr, arr + 7);list<int> l2(arr1, arr1 + 7);list<int> l3(arr, arr + sizeof(arr) / sizeof(arr[0])); //初始化数组范围构造list<int>::iterator pos = l3.begin(); //开始位置 5int flag = 1;for (int i = 0; i < flag; i++){pos++; //pos位置变成2}l3.insert(pos, 0);//在2的位置插入0,其他数据后移for (auto e : l3){cout << e << \' \';}cout << endl;l3.insert(pos, 5, 6); //50201314 pos的值为2,插入5个6 for (auto e : l3){cout << e << \' \';}cout << endl;list<int> lt4(6, 5);l3.insert(pos, lt4.begin(), lt4.end());//把l4的数据6个5从pos位置插入for (auto e : l3){cout << e << \' \';}cout << endl;
pop_front()
和 pop_back()
pop_front()
:删除容器前端的元素。pop_back()
:删除容器末端的元素。
- 这两者的差别在于删除的位置不同,前者删除的是第一个元素,后者删除的是最后一个元素。
list<int> l1;l1.push_back(0);l1.push_back(1);l1.push_back(2);l1.push_back(3);for (auto e : l1){cout << e << \" \";}cout << endl; //0 1 2 3lt.pop_back();lt.pop_back();for (auto e : l1){cout << e << \" \";}cout << endl;//0 1
erase()
和 clear()
erase()
- 迭代器失效:
在
std::list
中,erase()
会删除指定迭代器指向的节点,并改变节点之间的链接关系。因此,删除的节点的迭代器会失效,再次使用会导致错误。
- 返回有效迭代器:
erase()
会返回一个有效的迭代器,指向被删除节点的下一个节点。如果继续操作容器,应该使用erase()
返回的迭代器,而不是已失效的迭代器。
- 删除单个元素:
erase()
可以删除指定迭代器位置的单个元素,操作完成后返回下一个位置的迭代器。
- 删除区间元素:
erase()
还可以删除一个迭代器区间内的所有元素,操作完成后返回删除区域后一个位置的迭代器。
- 避免迭代器失效:
删除元素后,继续操作时要使用
erase()
返回的新迭代器,而不是旧的、已经失效的迭代器。
clear()
:删除容器中的所有元素,使容器变为空。
int arr[] = { 5,2,0,1,3,1,4};list<int> l1(arr, arr + sizeof(arr) / sizeof(arr[0]));for (auto e : l1){cout << e << \' \';}cout << endl;l1.erase(--l1.end());//end返回的是最后一个数据的下一个位置的迭代器//所以删除最后一个数据所以需要--end使迭代器指向最后一个位置的数据for (auto e : l1){cout << e << \' \';}cout << endl;//5 2 0 1 3 1l1.clear(); //清空容器for (auto e : l1){cout << e << \" \";}cout << endl; //(无数据)cout << l1.size() << endl; //0
resize
- resize是用于改变list有效数据的大小的,如果n小于当前大小,那么会将多余的部分删除销毁
- 如果n大于当前大小,那么会尾插val,如果用户没有显示给出val,那么会使用缺省值,即默认构造生成的值。
list<int> l1(5, 3);for (auto e : l1){cout << e << \" \";}cout << endl; //3 3 3 3 3l1.resize(7, 6); //将size扩大为7,扩大的值为6for (auto e : l1){cout << e << \" \";}cout << endl; //3 3 3 3 3 6 6l1.resize(2); //将size缩小为2for (auto e : l1){cout << e << \" \";}cout << endl; //3 3}
assign
- 清空原数据:
assign()
会清空容器中的所有原数据,再进行数据替换。- 使用迭代器区间替代数据:可以通过传入一个迭代器区间来替代容器中的数据,替换后容器大小调整。
- 使用相同值替代数据:也可以通过指定值和重复次数来填充容器,容器中将包含指定数量的相同元素。
- 迭代器区间要求:传入的区间是前闭后开的(即包括起始位置,不包括结束位置)。
int arr[] = { 5,2,0,1,3,1,4 };list<int> l1(arr, arr + 7);list<int> l2;l2.assign(l1.begin(), l1.end());for (auto e : l2){cout << e << \' \';}cout << endl;//5201314
swap
交换两个容器的数据
list<int> l1(4, 2);list<int> l2(4, 6);lt1.swap(l2); //交换两个容器的内容for (auto e : l1){cout << e << \" \";}cout << endl; //6 6 6 6for (auto e : l2){cout << e << \" \";}cout << endl; //2 2 2 2}
6.list的相关操作函数
sort()
sort()
函数用于对std::list
中的元素进行排序。排序的默认顺序是从小到大,也可以传入一个自定义比较函数来进行自定义排序。注意事项*:
sort()
会修改list
内部元素的顺序,原有的顺序将丢失。- 它使用的是双向链表的内部结构进行排序,因此时间复杂度是
O(n log n)
,其中n
是元素的个数。- 默认排序是基于
<
运算符进行的,若需要自定义排序,必须提供一个比较函数或函数对象。
list<int> lt;lt.push_back(4);lt.push_back(7);lt.push_back(5);lt.push_back(9);lt.push_back(6);lt.push_back(0);lt.push_back(3);for (auto e : lt){cout << e << \" \";}cout << endl; //4 7 5 9 6 0 3lt.sort(); //默认将容器内数据排为升序for (auto e : lt){cout << e << \" \";}cout << endl; //0 3 4 5 6 7 9return 0;
2. remove()
remove()
函数用于移除std::list
中所有等于指定值的元素。注意事项:
remove()
不会改变列表的顺序,只会删除匹配的元素。- 删除的元素是通过值来匹配的,如果你需要根据条件删除元素,可以考虑使用
remove_if()
。- 该函数是原地修改容器的,操作后迭代器指向的元素会失效。
list<int> lt;lt.push_back(1);lt.push_back(4);lt.push_back(3);lt.push_back(3);lt.push_back(2);lt.push_back(2);lt.push_back(3);for (auto e : lt){cout << e << \" \";}cout << endl; //1 4 3 3 2 2 3lt.remove(3); //删除容器当中值为3的元素for (auto e : lt){cout << e << \" \";}cout << endl; //1 4 2 2
3. unique()
unique()
函数用于删除连续的重复元素。它会删除相邻的重复元素,只保留一个副本。注意事项:
unique()
只会删除相邻的重复元素,因此需要先对list
进行排序,以确保相同的元素是相邻的。- 如果希望自定义判断条件,可以传入一个二元函数来判断元素是否相等。
list<int> lt;lt.push_back(1);lt.push_back(4);lt.push_back(3);lt.push_back(3);lt.push_back(2);lt.push_back(2);lt.push_back(3);for (auto e : lt){cout << e << \" \";}cout << endl; //1 4 3 3 2 2 3lt.sort(); //将容器当中的元素排为升序lt.unique(); //删除容器当中连续的重复元素for (auto e : lt){cout << e << \" \";}cout << endl; //1 2 3 4
4. merge()
merge()
函数用于将两个已排序的std::list
合并为一个新的排序好的std::list
。合并时,元素会按照排序顺序被放入目标列表中。注意事项:
merge()
要求两个list
都是已排序的,否则行为不可预测。- 合并后,
list2
将为空,所有的元素都会移动到list1
中。- 如果需要自定义排序顺序,可以传入一个比较函数。
list<int> lt1;lt1.push_back(3);lt1.push_back(8);lt1.push_back(1);list<int> lt2;lt2.push_back(6);lt2.push_back(2);lt2.push_back(9);lt2.push_back(5);lt1.sort(); //将容器lt1排为升序lt2.sort(); //将容器lt2排为升序lt1.merge(lt2); //将lt2合并到lt1当中for (auto e : lt1){cout << e << \" \";}cout << endl; //1 2 3 5 6 8 9
5. reserve()
reserve()
函数用于为std::list
容器预分配空间。然而,std::list
并不直接支持reserve()
,因为它是基于链表实现的,链表的内存分配是动态的。因此,它没有类似于std::vector
中的reserve()
方法。注意事项:
- 对于链表容器
std::list
,你无法预先分配空间,reserve()
仅适用于基于连续内存的容器,如std::vector
。std::list
不支持reserve()
,因此无法提前分配空间。容器会根据需要动态分配空间。
📢 如果你也喜欢这种“不呆头”的技术风格:
👁️ 【关注】 看一个非典型程序员如何用野路子解决正经问题
👍 【点赞】 给“不写八股文”的技术分享一点鼓励
🔖 【收藏】 把这些“奇怪但有用”的代码技巧打包带走
💬 【评论】 来聊聊——你遇到过最“呆头”的 Bug 是啥?
🗳️ 【投票】 您的投票是支持我前行的动力
技术没有标准答案,让我们一起用最有趣的方式,写出最靠谱的代码! 🎮💻