目录
一、STL中的list简介
二、构造函数
2.1 默认构造函数
2.2 填充构造(用n个相同的值构造)
2.3 迭代器构造
2.4 拷贝构造和赋值运算符重载
三、迭代器
3.1 正向迭代器
3.2 反向迭代器
四、容量相关
4.1 获取list中有效数据的个数
4.2 判断list是否为空 —— empty
五、元素访问
5.1 获取第一个有效节点的数据
5.2 获取最后一个有效节点的数据
六、修改相关
6.1 头插、尾插 —— push_front、push_back
6.2 尾插、尾删 —— pop_front、pop_back
6.3 在任意位置插入
6.4 在任意位置删除 —— erase
6.5 交换两个list对象
6.6 清空list中的有效节点
七、链表操作
7.1 将一个list上的数据转移到另一个list —— splice
7.2 删除某个特定数据
7.3 给数据排序 —— sort
7.4 去除重复数据 —— unique
7.5 反转数据 —— reverse
八、list与vector的对比
一、STL中的list简介
1. list是可以在常数时间(O(1))内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2. list与forward_list非常相似,最主要的不同在于forward_list是单链表,只能向前迭代,这让forward_list更简单高效。
3. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
4. 与其他的序列式容器(array,vector等)相比,list通常在任意位置进行插入、移除元素的执行效率更好。
5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,必须从已知的位置(比如头部或者尾部)迭代到目标位置,而迭代到目标位置需要线性的时间(O(n))开销;除此之外list还需要一些额外的空间,以保存每个节点的相关联系。
【文档描述】
二、构造函数
2.1 默认构造函数
【代码演示】
#include <iostream>
#include <list>
#include <string>
using namespace std;
int main()
{
//默认拷贝拷贝构造 list<T> lt;(T为list存储的数据类型)
list<string> ls;
return 0;
}
【输出结果】
注:list不像string和vector,list没有容量capacity这个属性。
2.2 填充构造(用n个相同的值构造)
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
//填充构造 list (size_type n, const value_type& val = value_type())
list<int> li(5, 20);
//如果不指定val的值就会初始化为相应类型的默认值
//int为0 double为0.0 char为'\0'……
list<double> ld(3);
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
for (const auto& k : ld)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
2.3 迭代器构造
【代码演示】
#include <iostream>
#include <list>
#include <string>
using namespace std;
int main()
{
//迭代器构造 list (InputIterator first, InputIterator last)
//迭代器区间为左闭右开 -》 [first, last)
//用数组构造
int array[] = { 1,2,3,4,5,6 };
list<int> li(array, array + sizeof(array) / sizeof(array[0]));
//这个其实是先用数组构造了一个list<int> temp
//然后用拷贝构造将temp拷贝给了li_another -》 list<int> li_another(temp)
list<int> li_another{ 1,2,3,4,5 };
//可以用别的容器的迭代器来构造list
string s("Hello list!");
list<char> lc(s.begin(), s.end());
//可以用list的迭代器来构造list
list<char> lc_copy(++lc.begin(), --lc.end());
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
for (const auto& k : li_another)
{
cout << k << " ";
}
cout << endl;
for (const auto& k : lc)
{
cout << k;
}
cout << endl;
for (const auto& k : lc_copy)
{
cout << k;
}
cout << endl;
return 0;
}
【输出结果】
2.4 拷贝构造和赋值运算符重载
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
//拷贝构造 list (const list& x)
//赋值运算符重载 list& operator=(const list& x)
list<int> li{ 1,2,3,4,5 };
//拷贝构造
list<int> li_copy(li);
//赋值运算符重载
list<int> li_another = li;
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
for (const auto& k : li_copy)
{
cout << k << " ";
}
cout << endl;
for (const auto& k : li_another)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
三、迭代器
【示意图】
3.1 正向迭代器
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,4,5 };
cout << *li.begin() << endl;
cout << *(--li.end()) << endl;
return 0;
}
list的迭代器不能直接使用加法或减法(如+1或-1)来实现迭代器位置的更换,因为list的各个节点并不在一块连续的空间,如果直接通过加减法来实现迭代器位置的更换会造成非法访问。所以我们需要使用重载过后的前置或后置的加加、减减操作符来实现迭代器位置的变更。
【输出结果】
3.2 反向迭代器
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,4,5,6,7 };
list<int>::reverse_iterator rbit = li.rbegin();
while (rbit != li.rend())
{
//从begin到end 从rbegin到rend都是++
cout << *rbit << " ";
++rbit;
}
cout << endl;
return 0;
}
【输出结果】
四、容量相关
4.1 获取list中有效数据的个数
【代码演示】
#include <iostream>
#include <list>
#include <string>
using namespace std;
int main()
{
list<string> ls{ "Hello", "lisr", "and", "string", "!" };
cout << ls.size() << endl;
return 0;
}
【输出结果】
4.2 判断list是否为空 —— empty
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li;
cout << li.empty() << endl;
//尾插一个1
li.push_back(1);
cout << li.empty() << endl;
return 0;
}
【输出结果】
五、元素访问
5.1 获取第一个有效节点的数据
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,4,5,6 };
cout << li.front() << endl;
return 0;
}
【输出结果】
5.2 获取最后一个有效节点的数据
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<string> ls{ "Hello", "list!" };
cout << ls.back() << endl;
return 0;
}
【输出结果】
六、修改相关
6.1 头插、尾插 —— push_front、push_back
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 2,3,4,5 };
//头插1
li.push_front(1);
//尾插6
li.push_back(6);
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
6.2 尾插、尾删 —— pop_front、pop_back
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,4,5,6 };
//头删
li.pop_front();
//尾删
li.pop_back();
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
6.3 在任意位置插入
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,5,6,7 };
list<int>::iterator pos = li.begin();
//list的迭代器不支持直接加减一个值。
//比如我们要在5的前面插入一个4,就必须让pos++3次,而不能pos + 3
//循环的次数可以看成目标位置下标的数值,但不是真正的下标
//因为list节点并不分布在一块连续的空间
for (int i = 0; i < 3; ++i)
{
++pos;
}
li.insert(pos, 4);
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
list的插入操作不像vector的插入操作,在插入数据中如果进行了扩容,vector会发生迭代器失效,而list不会。这是因为vector的扩容是开辟一块更大的空间,将原空间数据拷贝到新空间后再释放原空间,而迭代器pos任然指向旧空间,这样就会对非法空间进行访问。但是list的扩容只是开辟一块空间来存储新的数据,其他数据的位置和关系并不会发生变化,所以并不会发生迭代器失效。
6.4 在任意位置删除 —— erase
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,4,5,6,7,8 };
list<int>::iterator pos = li.begin();
//删除list中的所有偶数
while (pos != li.end())
{
//避免发生迭代器失效
if (*pos % 2 == 0)
{
pos = li.erase(pos);
}
else
{
++pos;
}
}
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
虽然list在插入过程中不会发生迭代器失效,但是在删除过程中任然会发生迭代器失效。为了避免这种情况我们就需要利用erase的返回值
erase的返回值是一个指向被删除数据的下一个数据的迭代器,我们在执行删除操作之后让迭代器pos等于erase的返回值,如果没有执行就正常++,这样就能避免迭代器失效问题了。
6.5 交换两个list对象
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li1{ 1, 2, 3, 4 ,5 };
list<int> li2{ 6, 7, 8 ,9, 10 };
li1.swap(li2);
cout << "li1的数据为:";
for (const auto& k : li1)
{
cout << k << " ";
}
cout << endl;
cout << "li2的数据为:";
for (const auto& k : li2)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
6.6 清空list中的有效节点
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,4,5 };
cout << "clear前的数据为:";
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
li.clear();
cout << "clear后的数据为:";
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
七、链表操作
7.1 将一个list上的数据转移到另一个list —— splice
【函数原型】
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> des1{ 1,2,3,4,5 };
list<int> des2(des1);
list<int> des3 = des2;
list<int> sour1{ 6,7,8,9,10 };
list<int> sour2(sour1);
list<int> sour3 = sour2;
//转移一整个链表
des1.splice(des1.end(), sour1);
//转移一个元素
des2.splice(des2.end(), sour2, sour2.begin());
//转移一个区间 -》左闭右开
des3.splice(des3.end(), sour3, ++sour3.begin(), --sour3.end());
cout << "des1的数据为:";
for (const auto& k : des1)
{
cout << k << " ";
}
cout << endl;
cout << "sour1的数据为:";
for (const auto& k : sour1)
{
cout << k << " ";
}
cout << endl;
cout << "des2的数据为:";
for (const auto& k : des2)
{
cout << k << " ";
}
cout << endl;
cout << "sour2的数据为:";
for (const auto& k : sour2)
{
cout << k << " ";
}
cout << endl;
cout << "des3的数据为:";
for (const auto& k : des3)
{
cout << k << " ";
}
cout << endl;
cout << "sour3的数据为:";
for (const auto& k : sour3)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
7.2 删除某个特定数据
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,1,1,5,8,5,4,3 };
cout << "remove前的数据为:";
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
li.remove(1);
cout << "remove后的数据为:";
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
7.3 给数据排序 —— sort
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 32,5,86,12,6,2 };
//list排序通过归并排序实现
li.sort();
for (auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
注:list用不了algorithm算法库中的sort,因为algorithm的sort的参数要求是随机迭代器RandomAccessIterator,而list的迭代器是双向迭代器bidirectional iterator。
除了随机迭代器RandomAccessIterator和双向迭代器bidirectional iterator外还有一种迭代器类型是InputIterator,这个迭代器类型就是只要是迭代器就行。
不过话虽如此,但并不建议对list进行排序,因为list的数据在空间上是分散开的,并不适合排序,list排序的效率远远不及vector这种在连续空间上储存数据的容器,因此并不建议对list进行排序。
7.4 去除重复数据 —— unique
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,4,6,4,6,7,8,9,1,3,5 };
//在去重之前建议先对list进行排序 不然效率实在是太低了 不过排序的效率也很低就是了
li.sort();
li.unique();
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
7.5 反转数据 —— reverse
【代码演示】
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> li{ 1,2,3,4,5,6 };
li.reverse();
for (const auto& k : li)
{
cout << k << " ";
}
cout << endl;
return 0;
}
【输出结果】
八、list与vector的对比
vector | list | |
底 层 结 构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随 机 访 问 | 支持随机访问,访问某个元素时间复杂度为O(1) | 不支持随机访问,访问某个元素时间复杂度为O(N) |
插 入 和 删 除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容;增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1) |
空 间 利 用 率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低, 缓存利用率低 |
迭 代 器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭 代 器 失 效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效;删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效, 删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使 用 场 景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |