一、list 的介绍及使用
cplusplus.com/reference/list/list/?kw=list
- list 是可以在常数时间 O(1) 内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list 的底层是带头双向循环链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- list 与 forward_list 非常相似:最主要的不同在于 forward_list 是单链表,只能朝前迭代,不支持尾插、尾删,对比双向链表的唯一优势就是每个节点少存一个指针。但是forward_list 在实际中用的很少,单链表尾插和尾删效率太低,尤其是尾删,不实用。
- 与其他的序列式容器相比(array,vector,deque),list 在容器内任意位置进行插入、删除、移动元素方面的执行效率通常表现更好。
- 与其他序列式容器相比,list 和 forward_list 最大的缺陷是不支持任意位置的随机访问,比如:要访问 list 的第 6 个元素,必须从已知的位置(比如头部 / 尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大 list 来说这可能是一个重要的因素)。
1、list 的使用
list 中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为 list 中一些常见的重要接口。
(1)list 的构造
// list的构造 void TestL1() { list<int> l1; // 构造空的l1 list<int> l2(4, 100); // l2中放4个值为100的元素 list<int> l3(l2.begin(), l2.end()); // 用l2的[begin(), end())左闭右开的区间构造l3 list<int> l4(l3); // 用l3拷贝构造l4 // 以数组为迭代器区间构造l5 int array[] = {16, 2, 77, 29}; list<int> l5(array, array + sizeof(array) / sizeof(int)); // 列表格式初始化C++11 list<int> l6{1, 2, 3, 4, 5}; }
(2)迭代器的使用
可以暂时将迭代器理解成一个指针,该指针指向 list 中的某个节点。
// list迭代器的使用 void PrintList(const list<int>& l) { // 注意这里调用的是list的 begin() const,返回list的const_iterator对象 for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it) { cout << *it << " "; // *it = 10; // 不能改变该值 -- 否则编译不通过 } cout << endl; }
void TestL2() { int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; list<int> l(array, array + sizeof(array) / sizeof(array[0])); // 使用正向迭代器正向list中的元素 // list<int>::iterator it = l.begin(); // C++98中语法 auto it = l.begin(); // C++11之后推荐写法 while (it != l.end()) { cout << *it << " "; ++it; } cout << endl; // 使用反向迭代器逆向打印list中的元素 // list<int>::reverse_iterator rit = l.rbegin(); auto rit = l.rbegin(); while (rit != l.rend()) { cout << *rit << " "; ++rit; } cout << endl; // 以数组为迭代器区间构造l5 list<int> l(array, array + sizeof(array) / sizeof(int)); // 用迭代器方式打印元素 list<int>::iterator it = l.begin(); while (it != l.end()) { cout << *it << " "; ++it; } cout << endl; // C++11范围for的方式遍历 for (auto& e : l) { cout << e << " "; } cout << endl; }
遍历链表只能用迭代器和范围for。
注意 :
- begin 与 end 为正向迭代器,对迭代器执行 ++ 操作,迭代器向后移动。
- rbegin(end) 与 rend(begin) 为反向迭代器,对迭代器执行 ++ 操作,迭代器向前移动。
(3)容量操作
cplusplus.com/reference/list/list/empty/
cplusplus.com/reference/list/list/size/
(4)访问操作
list 不支持 [] 运算符,因为 list 容器物理地址不是连续的。
cplusplus.com/reference/list/list/front/
cplusplus.com/reference/list/list/back/
(5)修改操作
注意:list 可以在任意位置进行 O(1) 的插入和删除操作。
// list插入和删除 // push_back/pop_back/push_front/pop_front void TestL3() { int array[] = {1, 2, 3}; list<int> L(array, array + sizeof(array) / sizeof(array[0])); L.push_back(4); // 在list的尾部插入4 L.push_front(0); // 在list的头部插入0 PrintList(L); L.pop_back(); // 删除list尾部节点 L.pop_front(); // 删除list头部节点 PrintList(L); }
// insert /erase void TestL4() { int array1[] = {1, 2, 3}; list<int> L(array1, array1 + sizeof(array1) / sizeof(array1[0])); auto pos = ++L.begin(); // 获取链表中第二个节点 cout << *pos << endl; L.insert(pos, 4); // 在pos前插入值为4的元素 PrintList(L); L.insert(pos, 5, 5); // 在pos前插入5个值为5的元素 PrintList(L); vector<int> v{7, 8, 9}; L.insert(pos, v.begin(), v.end()); // 在pos前插入[v.begin(), v.end)区间中的元素 PrintList(L); L.erase(pos); // 删除pos位置上的元素 PrintList(L); L.erase(L.begin(), L.end()); // 删除list中[begin, end)区间中的元素,即删除list中的所有元素 PrintList(L); }
为什么 C++98 建议使用各自容器里的 swap,而不建议使用算法里的 swap?
可以看到算法里 swap 的 C++98 的实现,无论是 string、vector、list 使用它都会涉及深拷贝问题,而且这里的深拷贝代价极大,需要深拷贝 3 次 —— 当 l1 和 l2 交换,这里会把 l1 拷贝构造一份 c,然后把 l2 赋值于 l1,c 赋值于 l2,完成交换。
而如果是容器里的 swap,需要交换 l1 和 l2,只需要头指针交换即可。假设是 vector,只要把 l1 和 l2 对应的 _start、_finish、_endofstorage 交换即可。相比算法里的 C++98 里的 swap,这里可以认为没有任何代价。
// resize/swap/clear void TestL5() { // 用数组来构造list int array1[] = {1, 2, 3}; list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0])); PrintList(l1); list<int> l2; l1.swap(l2); // 交换l1和l2中的元素 PrintList(l1); PrintList(l2); l2.clear(); // 将l2中的元素清空 cout << l2.size() << endl; }
🔺list 的迭代器失效
先将迭代器暂时理解成类似于指针, 迭代器失效即迭代器所指向的节点无效,即该节点被删除了 。
因为 list 的底层结构为带头结点的双向循环链表,因此在 list 中进行 插入 时是 不会 导致 list 的迭代器失效的,只有在 删除 时才 会 失效,并且 失效的只是指向被删除节点的迭代器 ,其他迭代器不会受到影响。
void TestListIterator() { int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; list<int> l(array, array+sizeof(array)/sizeof(array[0])); auto it = l.begin(); while (it != l.end()) { // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值 l.erase(it); ++it; } } // 改正 void TestListIterator() { int array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; list<int> l(array, array+sizeof(array)/sizeof(array[0])); auto it = l.begin(); while (it != l.end()) { l.erase(it++); // it = l.erase(it); } }
【补充】
a. 容器迭代器的分类
容器迭代器的分类:
- 使用功能的角度可分为:正向 / 反向 + const。
- 容器底层结构的角度可分为:单向迭代器、双向迭代器、随机访问迭代器。
单链表 forward_list、哈希表 unordered_set/map 迭代器就是单向(Unidirectional Iterator),特征是能 ++,不能 --;
双向链表 list、红黑树 set/map 迭代器就是双向(Bidirectional Iterator),特征是能 ++ / --;
string、vector、deque 迭代器就是随机迭代器(Random Access Iterator),特征是能可以++ / -- / + / -,一般随机迭代器底层都是一个连续的空间。
迭代器的本质是:
在不暴露容器底层实现细节的情况下,提供了一种统一的方式来访问 / 修改容器中存储的数据。
容器、迭代器、算法间的关系:
算法不能直接访问和修改容器中存储的数据,而迭代器就是容器和算法之间的胶合剂。
stl_iterator 源码中对于迭代器的分类:
b. 迭代器的实现方式分析
迭代器有两种实现方式,具体应根据容器底层数据结构的实现来确定。
因为每个特定的容器,都有特定的内部内存结构,也就意味着遍历 / 迭代过程的细节是不一样的。
方式一:前提条件:原生指针指向的空间物理地址是连续的,比如 string、vector。
- 所以让原生指针 ++ 可以直接走到下一个位置,-- 可以直接回到上一个位置,* 解引用可以直接得到这个位置的数据。比如 vector:
- 此时的原生指针就是一个天然的迭代器(但并不是原生指针厉害,而是因为它指向的空间物理地址是连续的)。
方式二:原生指针指向的空间物理地址不连续,比如:list、map / set。
- 所以让原生指针 ++ 不能直接走到下一个位置,-- 不能直接回到上一个位置,* 解引用不能直接得到这个位置的数据,只能得到一个结构体(结构体中包含数据和指针域)。比如 list:
- 此时原生指针不是一个天然的迭代器,所以我们需要用一个类封装原生指针,重载相关运算符,控制对象使用这些运算符的行为,让这个类的对象,用起来像指针一样。
- 为了让迭代器的使用形式与指针完全相同,因此在自定义的类中必须实现以下方法:
- 指针可以解引用,则迭代器的类中必须重载 operator*()。
- 指针可以通过 -> 访问其所指空间成员,则迭代器类中必须重载 oprator->()。
- 指针可以 ++ 向后移动,则迭代器类中必须重载 operator++() 与 operator++(int) ,至于 operator–() / operator–(int) 是否需要重载,要根据容器具体的底层结构来抉择,比如 list 双向链表可以向前移动,所以需要重载 operator--() 和 operator--(int),如果是 forward_list 就不需要重载 --。
- 迭代器需要进行是否相等的比较,因此还需要重载 operator==() 与 operator!=()。
(6)容器操作(了解)
a. sort
cplusplus.com/reference/list/list/sort/
- 等价元素的结果顺序是稳定的:即等价元素保留它们在调用之前的相对顺序。
- 整个操作不涉及任何元素对象的构造、销毁或复制。元素在容器内移动。
sort() 对 list 容器中的元素进行排序,效率很低,链表排序也没有太大的作用和价值。
函数默认是排升序(<),如果想要排降序(>),需要传仿函数。
void test() { int arr[] = { 10, 2, 5 }; list<int> lt(arr, arr + sizeof(arr) / sizeof(int)); // > 排降序 // 写法一: greater<int> g; // 类模板,在<functional>头文件中 lt.sort(g); // 写法二: lt.sort(greater<int>()); // 匿名对象 // lt: 10 5 2 }
b. unique
cplusplus.com/reference/list/list/unique/
去重,必须要先排序,才能去完重复值。
void test() { int arr[] = { 2, 4, 3, 2, 4 }; list<int> lt(arr, arr + sizeof(arr) / sizeof(int)); lt.sort(); // 去重前,必须要先排序 lt.unique(); // 去除重复值 // lt: 2 3 4 }
c. remove
cplusplus.com/reference/list/list/remove/
从容器中移除所有值等于 val 的元素。
void test() { int arr[] = { 1, 2, 3, 3, 4 }; list<int> lt(arr, arr + sizeof(arr) / sizeof(int)); lt.remove(3); // lt: 1 2 4 }
d. reverse
cplusplus.com/reference/list/list/reverse/
反转容器中元素的顺序。
void test() { int arr[] = { 1, 2, 3, 4, 5 }; list<int> lt(arr, arr + sizeof(arr) / sizeof(int)); lt.reverse(); // rlt: 5 4 3 2 1 }
e. merge
cplusplus.com/reference/list/list/merge/
合并排序序列(合并前两个 list 容器都要排好序)。
【C++】List -- 详解(下)https://developer.aliyun.com/article/1514688?spm=a2c6h.13148508.setting.24.4b904f0ejdbHoA