💓博主CSDN主页::Am心若依旧💓
⏩专栏分类c++从入门到精通⏪
🚚代码仓库:青酒余成🚚
🌹关注我🫵带你学习更多c++
🔝🔝
1.前言
本章重点
本章重点讲解list的接口函数的熟悉,并且讲解list迭代器失效的特性和迭代器的功能分类以及算法库函数中谁能用谁不能用,最后比较一下list和vector的优势和劣势
2.list的介绍
对上诉大致理解如下:
1.list是一种可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2.list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立结点当中,在结点中通过指针指向其前一个元素和后一个元素。
3.list与forward_list非常相似,最主要的不同在于forward_list是单链表,只能进行单方向迭代。
与其他容器相比,list通常在任意位置进行插入、删除元素的执行效率更高。
4.list和forward_list最大的缺陷是不支持在任意位置的随机访问,其次,list还需要一些额外的空间,以保存每个结点之间的关联信息(对于存储的类型较小元素来说这可能是一个重要的因素)。
更加具体的学习可以参考如下链接:list - C++ Reference (cplusplus.com)
3.list的使用
构造函数
在这里主要介绍四种构造函数
1.是无参构造,一眼就能看懂。
2.构造n个值为val的双向链表。
3.迭代器区间构造。
4.用一个初始值构造,也称之为拷贝构造函数。
上述四个构造的方法都是以int为类型,也可以使用其他的类型。具体视情况而
cpp
list<int> l1;//无参构造
list<int> l2(10,5);//用10个5初始化链表
vector<int> vv{1,2,3,4,5,6};
list<int> l3(vv.begin(),vv.end());//用迭代器区间初始化
//拷贝构造函数
list<char> l4('a');//用一个字符来初始化
迭代器
list底层实现迭代器的不是指针,但是可以把他当成指针来理解。list中的迭代器在底层重新封装了*,->,++,--,!=,==这几个函数。这样我们就能够像vector那样的把迭代器当成指针来使用了
这四个函数在前面讲解string和vector的时候都讲过,就不过多的叙述了。
但是一定要搞清楚这几个函数分别指向什么位置,下面用一张图帮助大家理解
begin()和rend()指向的位置相同
end()和rbegin()指向的位置相同
迭代器来遍历链表
cpp
vector<int> vv{1,3,5,7,9,11,13,15};
list<int> l(vv.begin(),vv.end());
auto it = l.begin();
while(it!=l.end())
{
cout << *it<< " ";
it++;
}
值得注意的是:这里用的是!=而没有使用等于,而在vector中使用的是<。这是为什么呢?
这是因为在list中元素都是不连续的,如果用<的话非常有可能是错误的。而在vector中由于空间都是连续的,地址前面的一定是比后面的小的。
同时还要注意的是:这里没有使用[]来进行读取元素的值,而是使用了解引用,那是因为list不能支持随机访问,只支持解引用来读取元素。
与容量相关的函数
这四个都是常见的函数了。resize在list中使用的不多,就不拿出来介绍了,有想法的可以参考上面提供的网站自己阅读。
增删查改相关函数
这几个函数一看就知道什么意思了,就不多叙述
insert插入重点讲解
使用insert函数时可以使用在某个位置插入某个值,也可以在某个位置插入n个值,也可以在某个位置前面插入一段区间大小的值
cpp
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
using namespace std;
int main()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
list<int>::iterator pos = find(lt.begin(), lt.end(), 2);
lt.insert(pos, 9); //在2的位置插入9
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; //1 9 2 3
pos = find(lt.begin(), lt.end(), 3);
lt.insert(pos, 2, 8); //在3的位置插入2个8
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; //1 9 2 8 8 3
vector<int> v(2, 7);
pos = find(lt.begin(), lt.end(), 1);
lt.insert(pos, v.begin(), v.end()); //在1的位置插入2个7
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; //7 7 1 9 2 8 8 3
return 0;
}
注: find函数是头文件"algorithm"当中的一个函数,该函数在指定迭代器区间寻找指定值的位置,并返回该位置的迭代器
erase函数
list当中的erase函数支持两种删除方式:
- 删除指定迭代器位置的元素。
- 删除指定迭代器区间(左闭右开)的所有元素。
cpp
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
using namespace std;
int main()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator pos = find(lt.begin(), lt.end(), 2);
lt.erase(pos); //删除2
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; //1 3 4 5
pos = find(lt.begin(), lt.end(), 4);
lt.erase(pos, lt.end()); //删除4及其之后的元素
for (auto e : lt)
{
cout << e << " ";
}
cout << endl; //1 3
return 0;
}
clear函数
清空双向链表中除了头结点以外的所有节点
cpp
vector<int> vv{1,5,10,15,20,100,120};
list<int> ll(vv.begin(),vv.end());
auto pos = find(ll.begin(),ll.end(),20);
ll.insert(pos,250);
ll.erase(ll.begin()+2);
ll.clear();
clear之后就会把所有的值全部清空。(除了头结点之外,头结点可以把他理解为哨兵卫,里面的值无效)。
4.关于迭代器的问题
由于list的底层是双向带头循环链表,所以插入操作时,pos指向的节点的位置始终都是一个位置,它们只改变链接关系,并不连续,所以插入操作,并不会导致list迭代器失效。
list迭代器失效只在erase删除时才会发生
erase删除的位置在空间上是唯一的,这个位置的数据被删除后,只是改变了数据的链接关系,然而此位置已经不在原先的链表中了,再次使用会出错!
例如你删除了最后一个元素位置的值,你再对最后一个元素的迭代器位置进行解引用,那么就会报错。这就是迭代器失效的问题
因此在设计删除erase函数的时候,每当删除一个位置的时候,他都会给你返回一个有效元素的位置。
举个例子,你要把所有的偶数全部删除。按照如下的方法写就不会出现问题了。
cpp
list<int> ll{1,2,3,4,5,6,7,8};
auto it=ll.begin();
while(it!=ll.end())
{
if((*it)%2==0)
{
it=erase(it);
}
else
{
it++;
}
}
5.各种迭代器的介绍
迭代器从功能可以大体分为三种:
- 正向迭代器: forward_iterator
- 双向迭代器: bidirectional_iterator
- 随机迭代器: random_iterator
他们分别支持++,++、--,++和--以及+和-
常见的容器以及它们的迭代器类型:
结论:
-
若函数模板给的随机迭代器
则只能传有随机迭代器的容器 -
若函数模板给的双向迭代器
则可以传有随机或者双向迭代器的容器 -
若函数模板给的单向迭代器
则三种迭代器都可以传进来!
可以看出迭代器的优先级是:单向,双向,随机。分别依次向前兼容
6.vector与list的对比
好啦,list的相关接口就介绍到这里了,本篇文章主要介绍的是博主认为比较重要的接口,其他接口可以去下面网站自行阅读。
list::insert - C++ Reference (cplusplus.com)
下期预告:list的模拟实现