👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
目录
一、list的基本概念
-
功能:将数据进行链式存储(物理存储单元上非连续的存储结构)。
-
STL
中的链表是一个 带头双向循环链表。这样的设计使得在链表中插入、删除节点的操作更加高效;时间复杂度都是O(1)
-
list
的数据域同样可以存储不同数据类型,因此它同样是一个类模板。
二、list的构造
【函数原型】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> l{ 1,2,3 };
// 1. 默认构造。构造空对象
list<int> l1;
for (auto e : l1)
{
cout << e << ' ';
}
cout << endl;
// 2. 拷贝构造
list<int> l2(l);
for (auto e : l2)
{
cout << e << ' ';
}
cout << endl;
// 3. n个val的元素构造
list<int> l3(3, 666); // 3个666
for (auto e : l3)
{
cout << e << ' ';
}
cout << endl;
// 4. 迭代区间的元素构造
list<int> l4(l.begin(), l.end());
for (auto e : l4)
{
cout << e << ' ';
}
cout << endl;
return 0;
}
【输出结果】
三、迭代器 begin + end
begin
:返回第一个元素的迭代器end
:返回带头节点
四、容量操作 size + empty
五、list的遍历
list
本质是链表,不是用连续性空间存储数据的。因此,list
是不支持下标访问[]
。
六、list的获取元素操作
6.1 front
功能:返回
list
的第一个节点中值的引用。
6.2 back
功能:返回
list
的最后一个节点中值的引用。
七、list的对容器修改操作
7.1 push_front
功能:头插
7.2 pop_front
功能:头删
7.3 push_back
功能:尾插
7.4 pop_back
功能:尾删
7.5 insert + 随机访问问题
从vector
开始insert
都是使用迭代器来访问的
假设已有数据:1 2 3 4
,现要在2
后插入100
。
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> l5;
l5.push_back(1);
l5.push_back(2);
l5.push_back(3);
l5.push_back(4);
l5.insert(l5.begin() + 2, 100);
for (auto x : l5)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
以上代码报错了。原因是:list
本质是链表,不一定连续性空间存储数据的,迭代器也是不支持随机访问(下标访问operator[]
),只能支持++
和--
操作(支持双向遍历)
【正确写法】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> l5;
l5.push_back(1);
l5.push_back(2);
l5.push_back(3);
l5.push_back(4);
auto it = l5.begin();
for (int i = 0; i < 2; i++)
{
it++;
}
l5.insert(it, 100);
for (auto x : l5)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
- 可
++
的底层就是+1
,那么为什么+1
不行,而++
可以?
这都归功于类的封装。在对迭代器封装的时候,重新的定义了这些符号的意义,也就是符号的重载。这才使得我们能就像使用指针一样去使用迭代器。
下面是list
的源代码(部分)
self& operator++()
{
node = (link_type)((*node).next);
return *this;
}
self operator++(int)
{
self tmp = *this;
++*this;
return tmp;
}
self& operator--()
{
node = (link_type)((*node).prev);
return *this;
}
self operator--(int)
{
self tmp = *this;
--*this;
return tmp;
}
7.6 erase + 迭代器失效问题
功能:删除某个位置的元素
【代码示例】
目的:删除所有元素
#include <iostream>
#include <list>
using namespace std;
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(a, a + sizeof(a) / sizeof(a[0]));
cout << "删除前:";
for (auto x : l)
{
cout << x << ' ';
}
cout << endl;
auto it = l.begin();
while (it != l.end())
{
l.erase(it);
++it;
}
cout << "删除后:";
for (auto x : l)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
程序崩了!这和vector
的情况类似,erase()
函数执行后,it
所指向的节点已被删除,因此it
无效。
解决方法:在下一次使用it
时,必须先给其赋值。
【正确代码】
#include <iostream>
#include <list>
using namespace std;
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(a, a + sizeof(a) / sizeof(a[0]));
cout << "删除前:";
for (auto x : l)
{
cout << x << ' ';
}
cout << endl;
auto it = l.begin();
while (it != l.end())
{
// l.erase(it); 错误
it = l.erase(it);
}
cout << "删除后:";
for (auto x : l)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
【总结】
list
在insert
操作时不存在迭代器失效问题。因为链表不涉及扩容- 而
erase
操作会有迭代器失效问题
7.7 swap
功能:交换两个
list
中的元素
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> l1;
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
list<int> l2;
l2.push_back(2);
l2.push_back(2);
l2.push_back(2);
l2.push_back(2);
cout << "交换前" << endl;
cout << "l1:";
for (auto x : l1)
{
cout << x << ' ';
}
cout << endl;
cout << "l2:";
for (auto x : l2)
{
cout << x << ' ';
}
cout << endl;
l1.swap(l2);
cout << "交换后" << endl;
cout << "l1:";
for (auto x : l1)
{
cout << x << ' ';
}
cout << endl;
cout << "l2:";
for (auto x : l2)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
7.8 clear
功能:清空
lis
t中所有的有效元素
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> l1;
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
l1.push_back(1);
l1.clear();
if (l1.empty())
{
cout << "已清空" << endl;
}
return 0;
}
【输出结果】
八、其他操作(常见)
主要讲解画方括号的,剩下的自行了解即可 ~
8.1 reverse
功能:逆置链表元素
【代码示例】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<char> lc1;
lc1.push_back('a');
lc1.push_back('b');
lc1.push_back('c');
lc1.push_back('d');
// list的逆置接口
lc1.reverse();
for (auto x : lc1)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
其实list
设计这个接口没有必要,因为算法库(algorithm
)也设计了reverse
算法
【代码示例】
#include <iostream>
#include <list>
#include <algorithm> // 使用算法库需要包含头文件
using namespace std;
int main()
{
list<char> lc1;
lc1.push_back('a');
lc1.push_back('b');
lc1.push_back('c');
lc1.push_back('d');
// 算法库逆置
reverse(lc1.begin(), lc1.end());
for (auto x : lc1)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
8.2 sort
- 功能:排序
list
。- 注意:
list
底层的sort
是归并算法。
【代码示例】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> ll;
ll.push_back(5);
ll.push_back(4);
ll.push_back(1);
ll.push_back(2);
ll.push_back(6);
ll.push_back(3);
ll.sort();
for (auto x : ll)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
同样的,算法库里面也设计了一个sort
,但注意:算法库里面的sort
对于list
是用不了的。
事实上,这是因为迭代器从功能上进行了分类:单向迭代器(只能++
)、双向迭代器(可以++
/--
)、随机迭代器(可以++
/--
,还可以-
和+
)
那我们怎么知道一个容器是什么类型的迭代器呢?很简单,查文档就行:点击跳转
这里我为大家总结了一些常见容器的迭代器:
我们再来看看,由于算法库中的sort
是随机迭代器(如下所示)
而list
是双向迭代器(如下所示)
因此,由于list
适合双向迭代器,所以用不了库里的sort
(RadomAccessIterator
)
当然了,如果见到InputIterator
,那么说明所有迭代器都可以用。
因此,list
接口中实现sort
还是有点意义的。我只是说“有点”。
-
在排序中,
vector
的排序速度要比list
快。这是因为vector
是一个连续存储的容器,它的元素在内存中是相邻的,可以利用局部性原理进行高效的排序算法,如快速排序。 -
相比之下,
list
是一个链表结构,其元素在内存中是分散存储的,无法直接利用局部性原理,因此排序操作的性能通常较慢。 -
在某些特定情况下,
list
可能更适合进行插入和删除操作,因为它对于这些操作的开销较小。因此,在选择容器时,应该根据具体的需求来决定使用哪种容器。
8.3 remove
功能:删除指定数据
【代码示例】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lit;
lit.push_back(1);
lit.push_back(2);
lit.push_back(3);
lit.push_back(4);
for (auto x : lit)
{
cout << x << ' ';
}
cout << endl;
// 删除4
lit.remove(4);
for (auto x : lit)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
8.4 unique
功能:去重。但是要注意首先得先进行排序,才能进行去重。否则效率极低
【代码示例】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lit;
lit.push_back(3);
lit.push_back(4);
lit.push_back(1);
lit.push_back(2);
lit.push_back(4);
for (auto x : lit)
{
cout << x << ' ';
}
cout << endl;
// 去重
lit.sort();
lit.unique();
for (auto x : lit)
{
cout << x << ' ';
}
cout << endl;
return 0;
}
【输出结果】
九、 list与vector的对比
vecto | list | |
---|---|---|
底层结构 | 动态顺序表(数组) | 带头结点的双向循环链表 |
随机访问 | 支持随机访问operator[] ,访问某个元素效率O(1) | 不支持随机访问 |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N) ,插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效问题 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效;删除也会造成迭代器失效 | 插入元素不会导致迭代器失效,删除元素会导致迭代器失效 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |