前言
紧跟上篇文章的vector,本文介绍list,感冒了状态不好,很多细节根本没有写上来,实际使用时还会与遇到很多问题。
还可通过这篇文章来了解list的简单概念。
STL【list的仿写】
list 的介绍
- list是序列容器,允许在序列中的任何位置执行固定O(1)时间的插入和删除操作,并在两个方向上进行迭代。
- list容器使用双链表实现;双链表将每个元素存储在不同的存储(内存)位置。每个节点通过next,prev指针链接成顺序表。
- list 与其他基本标准序列容器(array、vector和deque)相比,list 通常在容器内的任何位置插入、提取和移动元素(已经获得迭代器的情况下时间渐进复杂度O(1)) 。
- list 与其他序列容器(vector,array, deque)相比,list和forward _list(单链表实现)的主要缺点是它们不能通过位置直接访问元素;例如,要访问列表中的第六个元素,必须从已知位置〈如开始或结束)迭代到该位置,需要线性时间开销。
- 存储密度低,list要使用一些额外的内存空间(next,prev)来保持与每个元素相关联(前后序的线性)的链接信息,从而导致存储小元素类型(int, short, char)的列表的存储密度低。
图示
空的list,只有一个头结点。
非空list:
list的使用
1.构造函数
int main(void)
{
// 默认构造
list<int> ilist1;
// 构造10个值为20的结点
list<int> ilist2(10, 20);
// 构造十个默认结点
list<int> ilist3(10);
// 利用别的list的已知范围来初始化
list<int> ilist4(ilist2.begin(), ilist2.end());
// 利用一个list来初始化另一个
list<int> ilist5(ilist4);
// 初始化列表方案
list<int> ilist6({ 12,23,34 });
list<int> ilist7 = { 12,23,34 };
list<int> ilist8{ 12,23,34 };
// 利用已存在的list来直接赋值
list<int> ilist9 = ilist6;
return 0;
}
2.元素访问
通过迭代器访问,或通过基于范围的for循环;
不能使用下标访问和at()访问。
int main(void) {
list<int> ilist1{ 12,23,34,45,56 };
list<int>::iterator it = ilist1.begin();
cout << *it << endl;
for (auto& x : ilist1)
{
cout << x << endl;
}
return 0;
}
迭代器失效问题
在list中,迭代器失效和vector中并不同,vector由于插入时可能出现重新申请空间的问题,所以会导致迭代器指向的原始空间被析构,导致迭代器失效。
而list在插入元素时不会出现迭代器失效,原因:
list插入时是按照结点申请的,只需要把新节点链入list中,而原始数据并未出现重新申请空间的情况。
示例,迭代器it指向list的开始位置,头插后依然指向
int main(void) {
list<int> ilist1{ 12,23,34,45,56 };
list<int>::iterator it = ilist1.begin();
cout << *it << endl;
ilist1.push_front(10);
cout << *it << endl;
return 0;
}
但删除时会出现失效问题,这是显而易见的:
int main(void) {
list<int> ilist1{ 12,23,34,45,56 };
list<int>::iterator it = ilist1.begin();
cout << *it << endl;
ilist1.pop_front();
cout << *it << endl;
return 0;
}
删除元素时如何防止迭代器失效呢?
可以使用erase()函数,将迭代器传入进去,删除当前对象,饼返回迭代器的后继位置。但如果一直删除,删完所有结点之后也会失效。
int main(void) {
list<int> ilist1{ 12,23,34,45,56 };
list<int>::iterator it = ilist1.begin();
cout << *it << endl;
it = ilist1.erase(it);
cout << *it << endl;
return 0;
}
如图:初始状态-> 删除后
vector和list的区别
-
底层实现不同
vector:连续存储的容器,是一个动态数组,在堆区上分配空间。
list:动态双向链表,靠各个结点地址连接起来,在堆区分配空间。 -
空间利用率
vector:连续空间,不易造成内存碎片。空间利用率高
list:结点不连续,易造成内存碎片,小元素使结点密度低。空间利用率低。 -
查询元素
vector:由于是连续空间,所以可以通过 iterator, operator[],find()来查询,时间复杂度为O(n),还可通过二分查询binary_serach(),时间复杂度O(log2n)。
list:空间不连续,只能通过iterator, find()来查询,时间复杂度O(n)。 -
插入和删除
vector:
在末尾插入:1. 容量足够,push_back()时间复杂度为O(1).
2.容量不足时,push_back()时间复杂度为O(n); 因为需要重新申请空间,再把原有数据拷贝过去。
在中间插入:1.容量够时,需要插入后需要将后面数据后移。2.容量不够时,重新申请空间,再拷贝之前数据。
insert(it, val):O(n);
删除:尾删(pop_back()),O(1);其他地方O(n);
erase(it): O(n);
list:
插入:需要申请内存,时间复杂度O(1),push_back(),push_front(),insert(it, val);
删除:需要释放内存,时间复杂度O(1),pop_back(), pop_front()
erase(it):O(n); -
迭代器
vector:
随即迭代器,迭代器检查越界。支持 ++, – , +, += , > , < , == , !=
list:
双向迭代器,迭代器检查越界。支持 ++, --, ==, !=。 -
迭代器失效
vector:
插入和删除都会导致迭代器失效
list:
插入元素不会导致迭代器失效;删除元素使当前迭代器失效,不影响其他迭代器。
总结
在平时使用时,不能谈论某个容器的优缺点,因为每种容器都有它适合的场景,只能说哪种容器在哪种场合更实用。
- vector底层实现是数组; list是双向链表。
- vector支持随机访问,list不支持。
- vector是顺序内存,list不是。
- vector在中间节点进行插入删除会导致内存拷贝,list不会。
- vector一次性分配好内存,不够时才进行2倍扩容(或1.5倍);list每次插入新节点都会进行内存申请。
- vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。
什么时候该用vector? 什么时候该用list?
- 如果需要高效的随机存取,而不在乎插入和删除的效率(很少使用插入和删除操作)。选用vector。
- 如果需要大量的插入和删除的操作,随机存取很少使用。选用list。