听说std::vector频繁插入有性能问题,那么std::list是std::vector的替代品?非也非也,所谓"寸有所长,尺有所短,物有所必,事有所当", 没有绝对的谁替代谁,作为工具人更正确的做法是根据特定的场景选用特定的工具而已。
老套路,先来两个关于list的权威参考:cppreference
又或者这个也行cplusplus
list的实现原理
list的底层是双向链表结构,因而list可以支持前后双向迭代,双向链表中每个元素存储在独立节点中,这些节点不像vector那样需要连续的内存,在节点中通过指针指向其前一个元素和后一个元素来进行链接。
因为list中的每个节点没有强制的关联性,基本是相互独立的,因此list通常可以在任意位置进行插入、移除元素并获得较好的性能,最差的情况就是在尾部进行插入删除操作,因为这需要迭代一遍。
也正是因为list的每个节点不具备连续关联性,因此它不支持任意位置的访问,而支持任意位置的随机访问,这恰恰是vector的最大优势。
或许世界就是这么难两全,有舍才有得...
list基本使用
对于list的构造比较简单,我们直接上代码:
c
int main() {
// 构造空的list
std::list<int> list_01;
// 构造list中包含2个值为3的元素
std::list<int> list_02(2,3);
// 使用的list_02的区间构造list
std::list<int> list_03(list_02.begin(),list_02.end());
// 是否为空
std::cout << "list_01是否为空:" << list_01.empty() << std::endl;
std::cout << "list_03的size:" << list_03.size() << std::endl;
return 0;
}
对于list的迭代这里就不多说了,无脑使用auto的for即可。
对于list元素的访问我们可以内部函数front
和back
,这两个函数返回一个引用,一次既可以访问到对应值,也可以修改值:
c
int main() {
// 构造list中包含2个值为3的元素
std::list<int> list_01(2,3);
// 修改
list_01.front() = 6;
list_01.back() = 7;
for (auto value:list_01) {
std::cout << "value:" << value << std::endl;
}
return 0;
}
对于list的插入元素,我们可以使用哪些函数呢?最常用的有push_back
、push_front
、insert
这三个,其中push_back
、push_front
意如字面,就是 在尾部和头部插入元素的意思。而insert
则支持在任意位置插入。
c
using namespace std;
int main() {
// 构造list中包含2个值为3的元素
std::list<int> list_01(2,3);
// 尾部插入
list_01.push_back(6);
// 头部插入
list_01.push_front(7);
auto insertPos = list_01.begin();
insertPos++;
// 特定位置插入
list_01.insert(insertPos,5);
for (auto value:list_01) {
std::cout << "value:" << value << std::endl;
}
return 0;
}
上面的示例代码输出如下:
对于list的删除操作比较多,我们直接看代码注释:
c
using namespace std;
int main() {
// 构造list中包含10个值为3的元素
std::list<int> list_01(10,3);
// 尾部删除
list_01.pop_back();
// 头部删除
list_01.pop_front();
// 删除所有为3的元素
// list_01.remove(3);
// 按照条件删除
list_01.remove_if([=](const int value){
// 删除值为2的元素
return value == 2;
});
// 删除特定位置的元素
list_01.erase(list_01.begin());
for (auto value:list_01) {
std::cout << "value:" << value << std::endl;
}
// 清空
list_01.clear();
return 0;
}
需要注意跌是在调用list的pop_front
和pop_back
需要保证list不为空,否则会跑出异常。
list与vector的对比
vector与list都是STL中非常重要的序列容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,笔者把它们的区别整理如下表:
功能 | vector | list |
---|---|---|
底层结构 | 动态顺序表,一段连续空间 | 带头节点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时可能需要增容。 增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1),不存在增容问题 |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点随机动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入删除操作,不关心随机访问 |
关注我,后期不定期更新...