目录
1、list的介绍
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
- 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
- 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要因素
2、list的使用
2.1、list的构造
构造函数 接口说明 1、list() 构造空的list 2、list (size_type n, const value_type& val = value_type()) 构造的list中包含n个值为val的元素 3、list (const list& x) 拷贝构造函数 4、list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list
- 1、list()
list<int> lt1;
- 2、list (size_type n, const value_type& val = value_type())
list<int> lt2(5, 3);//构造5个值为3的元素
- 3、list (const list& x)
list<int> lt3(lt2);//用lt2拷贝构造lt3
- 4、list (InputIterator first, InputIterator last)
//1、用l2的[begin(), end())左闭右开的区间构造lt4 list<int> lt4(lt2.begin(), lt2.end()); //2、以数组为迭代器区间构造lt5 int array[] = { 1,2,3,4 }; list<int> lt5(array, array + sizeof(array) / sizeof(int));
2.2、迭代器的使用
函数声明 接口说明 1、begin 返回第一个元素的迭代器 2、end 返回最后一个元素下一个位置的迭代器 3、rbegin 返回第一个元素的reverse_iterator,即end位置 4、rend 返回最后一个元素下一个位置的reverse_iterator,即begin位置
begin和end
begin是返回第一个元素的迭代器,end是返回最后一个元素下一个位置的迭代器。可以通过迭代器进行元素访问:
void test() { list<int> lt(5, 3); list<int>::iterator it = lt.begin(); while (it != lt.end()) //不能用it < lt.end() { cout << *it << " "; //3 3 3 3 3 it++; } }
- 注意:begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
rbegin和rend
和先前学到的string类似,rbegin的第一个元素为尾部end位置,rend返回的是begin的位置。
void test() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } list<int>::reverse_iterator rit = lt.rbegin(); //或者用auto自动识别类型:auto rit = lt.rbegin(); while (rit != lt.rend()) { cout << *rit << " "; //4 3 2 1 rit++; } }
- 注意:rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
范围for
范围for的底层就是迭代器实现的,利用其也可进行元素访问:
void test() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } for (auto e : lt) { cout << e << " ";//1 2 3 4 } }
2.3、list的元素获取
函数声明 接口声明 1、front 返回list第一个节点中值的引用 2、back 返回list最后一个节点中值的引用
front和back
front返回第一个元素,back返回最后一个元素
void test() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } cout << lt.front() << endl;//1 cout << lt.back() << endl; //4 }
2.4、list的大小
函数声明 接口说明 1、empty 检测list是否为空,是返回true,不是返回false 2、size 返回list中有效节点的个数
empty
empty判断list是否为空
void test() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } cout << lt.empty() << endl;//0 }
size
size用来获取list中有些元素个数
void test5() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } cout << lt.size() << endl;//4 }
2.5、list修改的相关函数
函数声明 接口声明 1、push_front 在list首元素前插入值为val的元素 2、pop_front 删除list中第一个元素 3、push_back 在list尾部插入值为val的元素 4、pop_back 删除list中最后一个元素 5、insert 在list position 位置中插入值为val的元素 6、erase 删除list position位置的元素 7、swap 交换两个list中的元素 8、clear 清空list中的有效数据
push_front和pop_front
push_front即头插,pop_front即头删。
void test() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } lt.push_front(0);//头插0 cout << lt.front() << endl;//0 lt.pop_front();//头删 cout << lt.front() << endl;//1 }
push_back和pop_back
push_back即尾插,pop_back即尾删。
void test6() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } lt.push_back(9);//尾插9 cout << lt.back() << endl;//9 lt.pop_back();//尾删 cout << lt.back() << endl;//4 }
简述emplace_back接口
首先,从功能上讲emplace_back和push_back是差不多的,都属于尾插,区别在于emplace_back支持可变参数模板(以后会讲到),push_back只能插入实例化的类型,emplace_back的传入更加多元化
如下push_back能完成的操作我emplace_back也同样能完成:
void test_list() { list<int> lt1; lt1.push_back(1); lt1.push_back(2); /*lt1.push_back(3); lt1.push_back(4);*/ lt1.emplace_back(3); lt1.emplace_back(4); for (auto e : lt1) cout << e << " ";//1 2 3 4 cout << endl; }当要插入的元素是内置类型,那么push_back和emplace_back插入的方式差不多,但如果是自定义类型,则会有差异,如下我有一个A类
struct A { A(int a1 = 1, int a2 = 2) :_a1(a1) ,_a2(a2) { } int _a1; int _a2; };若想使用push_back进行插入,有如下三种方式:
- 定义一个有名对象,并传入
- 利用匿名对象进行插入
- 走隐式类型转换
对于emplace_back,则有如下三种方式,和push_back会有不同之处:
- 定义一个有名对象,并传入
- 利用匿名对象进行插入
- 利用可变参数,直接一个一个传入
emplace_back不支持推导类型,所以不能用隐式类型转换的方式进行插入,但当是多参数时,可以一个一个传,这就利用到了可变参数,此方式相比其它几种方法更加高效
void test_list() { list<A> lt2; A aa1(2, 2); //A aa2 = { 2, 2 ); lt2.push_back(aa1); lt2.push_back(A(2, 2)); lt2.push_back({ 2, 2 }); lt2.emplace_back(aa1); lt2.emplace_back(A(2, 2)); lt2.emplace_back(2, 2);//更高效 }对于emplace系列的其他用法等到后面再进行详谈,这里简要了解
insert
list中的insert支持下列三种插入方式:
- 在指定位置插入一个值为val的元素
- 在指定位置插入n个值为val的元素
- 在指定位置插入一段左闭右开的迭代器区间
void test() { list<int> lt; for (int i = 1; i <= 4; i++) { lt.push_back(i);//1 2 3 4 } list<int>::iterator pos = find(lt.begin(), lt.end(), 3); //1、在3的位置插入值7 lt.insert(pos, 7); for (auto e : lt) cout << e << " ";//1 2 7 3 4 cout << endl; //2、在3的位置插入5个-1 pos = find(lt.begin(), lt.end(), 3); lt.insert(pos, 5, -1); for (auto e : lt) cout << e << " ";//1 2 7 -1 -1 -1 -1 -1 3 4 cout << endl; //3、在7的位置插入迭代器区间 pos = find(lt.begin(), lt.end(), 7); list<int> lt2(3, 0); lt.insert(pos, lt2.begin(), lt2.end()); for (auto e : lt) cout << e << " ";//1 2 0 0 0 7 -1 -1 -1 -1 -1 3 4 }
erase
erase支持下列两种删除方式:
- 删除在指定迭代器位置的元素
- 删除指定迭代器区间的元素
void test() { list<int> lt; for (int i = 1; i <= 7; i++) { lt.push_back(i);//1 2 3 4 5 6 7 } list<int>::iterator pos = find(lt.begin(), lt.end(), 2); //1、删除2位置的元素 lt.erase(pos); for (auto e : lt) cout << e << " ";//1 3 4 5 6 7 cout << endl; //2、删除值为4后的所有元素 pos = find(lt.begin(), lt.end(), 4); lt.erase(pos, lt.end()); for (auto e : lt) cout << e << " ";//1 3 }
swap
swap用于交换两个list的内容。
void test() { list<int> lt1(5, -1); list<int> lt2(3, 7); lt2.swap(lt1); for (auto e : lt1) cout << e << " "; //7 7 7 cout << endl; for (auto d : lt2) cout << d << " "; //-1 -1 -1 -1 -1 }
clear
clear用来清空list中的有效数据。
void test() { list<int> lt(5, -1); for (auto e : lt) cout << e << " ";//-1 -1 -1 -1 -1 cout << endl; lt.clear(); for (auto e : lt) cout << e << " ";//空 }
2.6、list的操作函数
函数声明 接口说明 1、sort 对list进行排序 2、unique 删除list中的重复元素 3、splice 将元素从 x 传输到容器中,将它们插入到适当的位置
sort
list中的sort函数默认排升序。
void test() { list<int> lt; for (int i = 3; i >= -3; i--) { lt.push_back(i);//3 2 1 0 -1 -2 -3 } lt.sort(); for (auto e : lt) cout << e << " ";//-3 -2 -1 0 1 2 3 }
unique
unique是删除list中的重复元素,但它只能去重挨着的元素:
void test_list() { list<int> lt1 = { 10, 2, 30, 30, 5, 2, 2, 4, 2, 30 }; for (auto e : lt1) cout << e << " "; //10 2 30 30 5 2 2 4 2 30 cout << endl; lt1.unique(); for (auto e : lt1) cout << e << " "; //10 2 30 5 2 4 2 30 cout << endl; lt1.sort(); lt1.unique(); for (auto e : lt1) cout << e << " "; //2 4 5 10 30 cout << endl; }
- 注意:要想对list进行真正的去重,必须先排序(sort)。
splice
splice用于将元素从 x 传输到容器中,将它们插入到适当的位置,如下的使用方式:
void test_list() { list<int> lt1 = { 1, 2, 3, 4, 5, 6 }; for (auto e : lt1) cout << e << " "; //1 2 3 4 5 6 cout << endl; //把1移动到尾部 lt1.splice(lt1.end(), lt1, lt1.begin()); for (auto e : lt1) cout << e << " "; //2 3 4 5 6 1 cout << endl; }
3、迭代器
对于上文提到的list中的sort,明明算法库里已经提供了sort函数,可list还是提供了sort排序函数,这是因为算法库里的sort函数不能适用list,具体原因要从迭代器开始说起,算法库里sort的迭代器要支持-,但list中的迭代器支持++/--,却不支持-,现在我们对迭代器展开讲讲:
- 首先迭代器的底层本质就是指针,或者是对指针的封装,它的行为就像指针一样。
- 对于vector的迭代器支持++和--,同样也支持-,因为-的成本低,如果是链表就不支持-了,因为成本高,因为链表间的节点间是相互独立的,节点之间没有固定的联系,导致链表的-效率很低
对于迭代器,我们可以从功能和性质两个角度进行分类:
算法作用到容器上是通过迭代器的,而算法对迭代器是有要求的,并且不同算法的名字已经暗示了你要使用哪种迭代器,如下三种算法(find & reverse & sort),而迭代器之间又是一种继承的关系:
- 对于sort算法(随机),是肯定不能传链表(list)的迭代器,因为只能传递随机迭代器,如vector、string、deque
- 对于reverse算法(双向),我需要传递双向迭代器,不能传递单向迭代器(因为不支持++/--),但是可以支持传递随机迭代器(因为支持了++/--),也可以说随机迭代器是一个特殊的双向迭代器。这即利用了C++的继承思想(父子关系),子类是一个特殊的父类,父类的功能子类都支持
- 对于find算法(单向),同时随机和双向迭代器又是一个特殊的单向迭代器,单向迭代器的功能我双向迭代器和随机迭代器都有,因此对于find是各类迭代器都通吃的
综上,由于库里的sort算法支持的是随机迭代器,所以对于list就需要自己单独实现一个sort了,并且内部用的是归并算法进行排序,不同于库里的快排
void test_list() { list<int> lt1 = { 10, 2, 30, 5, 2, 4, 2 }; for (auto e : lt1) cout << e << " "; //10 2 30 5 2 4 2 cout << endl; lt1.sort(); for (auto e : lt1) cout << e << " "; //2 2 2 4 5 10 30 cout << endl; }list里的sort默认也是升序,如果要排降序,则需要用到仿函数,这里进行简单演示,详情见后续
void test_list() { list<int> lt1 = { 10, 2, 30, 5, 2, 4, 2 }; for (auto e : lt1) cout << e << " "; //10 2 30 5 2 4 2 //降序 greater<int> gt; lt1.sort(gt); //lt1.sort(greater<int>());//匿名对象 for (auto e : lt1) cout << e << " "; //30 10 5 4 2 2 2 cout << endl; }
4、list与vector对比
vector list 底层结构 动态顺序表,一段连续空间 带头结点的双向循环链表 随即访问 支持随机访问,访问某个元素效率O(1) 不支持随机访问,访问某个元素效率O(N) 插入和删除 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) 空间利用率 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 迭代器 原生态指针 对原生态指针(节点指针)进行封装 迭代器失效 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 使用场景 需要高效存储,支持随机访问,不关心插入删除效率 大量插入和删除操作,不关心随机访问
本文详细介绍了C++中list容器的使用,包括构造、迭代器的使用、元素获取、大小检查、元素修改及操作函数。强调了list在任意位置插入和删除的高效性,并对比了list与vector在性能和应用场景上的差异。



389





