list容器概述
- list的容器的实现是使用双向链表的形式的数据结构实现的。(也有的编译器使用双向循环链表)
- 链表是一种数据结构,这种结构与数组的结构不同,链表的每个节点都存放有特定个数的指针(双向链表:两个(一个指向前面的元素,另一个指向后面的元素),普通的链表:一个(指向后面的元素))。
- 链表中的数据也不是连续的,也就是说它们的物理内存中不是挨着的,这样链表就不用局限于空间不够的情况,只要内存中有位置,它可以在任意位置创建节点,存放数据,只要使用上一个或者下一个节点的指针存放这个内存的地址就行。
- 由于这样的结构,链表就不能像数组一样,随机存取数据了(简单来说,使用[]或者at()访问数据)。
数组是由于内存连续,所以我们可以根据首地址+偏移值就很快找到相应位置了。
链表其存储的数据是不连续的,我们想要找到下一个数据存储的位置需要根据上一个节点中记录的位置来找到它。 --> 所以我们找一个数据,需要从最开始一个一个往后找。
- 鉴于链表这种存储数据的结构,list容器在添加和删除数据的时候很快,但是在存取数据的时候,由于需要从头开始遍历寻找,所以很慢。 所以,当我们的场景是需要多次插入删除,但是很少存取元素的时候,建议使用list容器。
- 而且使用list容器插入和删除数据的时候,不会影响到别的数据,只需要将对应的指针进行修改就行。
- 所以,注意在list容器中开辟内存时,是需要多少开辟多少,什么时候需要再开辟,是不会像vector一样提前开辟,list提前开辟相当于一种浪费。
要使用list容器需要导入头文件#include<list>
1. list容器的默认构造
代码
/*list<int> l1;list<float> l2;list<Student> l3;list<int*> l4;list<Student*>l5;
*/
2. list容器的有参构造
代码
/*int arr[] = { 1,2,3,4,5 };vector<int> v1{ 10,11,12,,13,14,15,16 };// 第一种: 开始l1容器具有10个元素,并且元素值为类型默认值(int的默认值为0)list<int> l1(10);// 第二种: 开始l2容器具有10个元素,并且元素值为5list<int> l2(10, 5);// 第三种: 开始l3容器中具有初始化列表中的元素list<int> l3{ 1,2,3,4,5 };// 第四种: 开始l4容器中具有l3的全部元素list<int> l4(l3.begin(), l3.end());// 第五种: 拷贝构造函数list<int> l5(l3);// 第六种: 开始l6容器中具有数组第二个---第四个元素list<int> l6(arr + 1, arr + 5);// 第七种: 开始l7中具有v1容器中的全部元素list<int> l7(v1.begin(), v1.end());
*/
相关知识点:
- list<类型> l1(n); // l1开始就有n个元素,并且值为类型默认值
- list<类型> l1(n,elem); // l1开始就有n个元素,并且值为elem
- list<类型> l1(l2); // 拷贝构造函数
- list<类型> l1({data...}); // 开始l1就拥有初始化列表中的元素
- list<类型> l1(beg,end); // 在左闭右开的迭代器范围内的数据放入l1。(可以为其它容器的迭代器)
- list<类型> l1(ptr1,ptr2); // 在左开右闭的指针区间内的数据放入l1。(一般指定数组中数据的范围)
3. list容器尾部添加和删除元素(和deque的使用方法类似)
代码
/*list<int> l1;// 添加元素l1.push_back(1);l1.push_front(2);l1.emplace_back(3);l1.emplace_front(4);// 删除元素l1.pop_back();l1.pop_front();
*/
4. list容器的元素个数
代码
list<int> l1{10,11,12,13,14,15};cout << "l1容器的元素个数: " << l1.size() << endl; // 元素个数为6
5.在指定位置插入元素
代码:使用insert()函数
list<int> l1;list<int> l2{ 1,2,3,4,5 };vector<int> v1{10,11,12,13,14,15};// 一. 使用insert// 方式一:l1.insert(l1.begin(), 5);// 方式二:l1.insert(l1.begin(), 5, 3);// 方式三:l1.insert(l1.begin(), l2.begin(), l2.end()); // 使用相同容器l1.insert(l1.begin(), v1.begin(), v1.end()); // 使用不同容器// 方式四:l1.insert(l1.begin(), { 5,6,7,8,9 });// 二. 使用emplacel1.emplace(l1.begin(), 5);
相关知识点:
- 和deque的用法是类似的。
- insert在插入多个元素和单个元素时候,会返回一个直系那个插入位置的迭代器。(不同的编译器实现可能不一样)
- 使用insert()函数,将一个list(l1)中的元素添加到另外一个list(l2)容器中,不会影响l1中的数据。
代码: 使用splice()函数
/*splice 的多种重载形式1. l1.splice(l1.begin(),l2, l2.begin(),++l2.begin()); // 将l2中[beg,end)范围内的元素 添加到l1的开始位置2. l1.splice(l1.begin(),l2); // 将l2的全部数据添加到l1的开始位置3. l1.splice(l1.begin(), l2, ++l2.begin()); // 将l2中第二个元素放到l1的开始位置4. l1.splice(l1.begin(), list<int>{1,2,3}); // 将匿名对象中的元素放到l1的开始位置5. l1.splice(l1.begin(),{1,2,3}); // 同上*/int main(void) {list<int> l1{ 10,20,30,40,50};list<int> l2{ 1,2,3,4,5 };l1.splice(l1.begin(), l2,l2.begin()); // 将l2的第一个元素放到l1的开始位置cout << "l1中的元素:" << endl;for (list<int>::iterator it = l1.begin(); it != l1.end(); it++) {cout << *it << " "; }cout << endl;cout << "l2中的元素:" << endl;for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {cout << *it << " ";}cout << endl;system("pause");return 0;
}
结果:
相关知识点
- splice(list1.loc,list2); // 将list2中的元素拼接到list1的loc位置。
- splice(list1.loc,list2,list2.loc); // 将list2中loc位置的元素拼接到list1的loc位置
- splice(list1.loc,list2,list2.beg,list2.end); // 将list2[beg,end)范围内的元素拼接到list1的loc位置
- splice(list1.loc,{data...}); // 将data中的数据拼接到list1的loc位置。
注意事项
splice函数添加数据和insert函数不同,splice函数会影响第二个list中的元素。
仔细观察上面的输出,会发现我们将l2中的第二个数据添加到l1中,l2中的这个数据就没有了。
这是为什么呢?
这其实和list的内部实现使用链表是有关系的,注意我们在介绍splice的时候,使用的是拼接而不是添加。
图解splice的过程:
list的底层是使用链表实现的,链表的内存不连续,使用指针维系先后关系。
此时我们使用splice函数将list2中的2拼接到list1的开头。
如图,我们会发现在list1中添加list2中的第二个元素,并不是像insert一样,在list1中添加一个和list2对应元素相同的数据。而是改变list1和list2的指针指向。
将list2中本来第一个元素指向的下一个元素是2,因为我们使用splace将2拼接到list1中了,所以list2中的第一个元素的下一个元素,就是2对应的元素的下一个元素,也就是3。
与list2一样,list1就是将list2中解放出来的元素中指针,指向list1中当前的第一个元素。
所以,使用splice()函数的时候,是会影响list2中的元素的,因为它是直接改变对应元素的指针的指向,将list2中的元素链接到list1中,这样这个元素就变成list1中的元素了。(list2中这个元素就没有了)
代码: 使用merge()函数
int main(void) {list<int> l1{ 1,2,30,40,50};list<int> l2{ 1,2,3,4,5 };/*list2.merge(list1); 将list1中的元素,根据其在list2中的大小顺序拼接到list2中list2.merge(list1,函数对象); 与上面类似,但是我们可以参数2指定拼接规则*/l1.merge(l2);cout << "l1中的元素:" << endl;for (list<int>::iterator it = l1.begin(); it != l1.end(); it++) {cout << *it << " "; }cout << endl;cout << "l2中的元素:" << endl;for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {cout << *it << " ";}cout << endl;system("pause");return 0;
}
结果:
相关知识点
- list2.merge(list1); // 将list1中的元素根据其在list2中的大小关系拼接到list2中。(默认是从小到大)
例: list1: 1,2,3 list2: 1,5,6 list2.merge(list1)之后, list1: 空, list2: 1,1,2,3,5,6。
- list2.merge(list1,函数对象); //作用和上面类似,只不过是拼接规则我们来指定
注意事项:
- 使用merge()进行拼接时,两个容器中的元素必须都符合拼接规则。否则程序中断。
比如: 我们要求从小到大拼接, 那么list1和list2中的元素也必须是从小到大的。
- 如果我们只传入一个参数,那么就默认从小到大拼接。 因为内部会调用less函数对象类(就是c++内部实现的一个函数对象类,(要求传入两个参数n1,n2,n1<n2返回true),在此处表示从小到大排的)
- 我们也可以传入使用list2.merge(list1,greater<类型>())(传入一个greater类的匿名对象,因为less和greater都是类模板,所以使用是要传入类型参数), 来实现从大到小拼接,但是这样,list1和list2中的元素也必须是从大到小的。
- greater和less一样,都是内部实现的函数对象,只不过greater和less相反,当n1>n2时返回true。(在此处表示从大到小拼接)
- 当然我们可以在第二个参数传入我们自定义的函数对象来指定拼接规则。
- 使用merge()进行拼接和splice()一样,都会影响list1中的元素。
6. list容器元素的访问(注意有变化)
注意
前面说到,list的内部是以链表的形式实现的,所以它与vector和deque不同,它是不能使用下标来访问数据的(也就是[]和at())。
代码
list<int> l2{ 1,2,3,4,5 };// 使用front()获取第一个元素,并可以将其修改
l2.front() = 10;
cout << l2.front() << endl;// 使用back()获取最后一个元素,并可以将其修改
l2.back() = 20;
cout << l2.back() << endl;// 还有就是使用迭代器,我们后面说
相关知识点
- 除注意事项外,其它的和deque是类似的。
7. list容器迭代器的使用(注意有变化)
注意
迭代器我们可以看做指针,它是用来遍历容器用的。
对于list容器,其迭代器除++,--之外不能进行任何的运算操作(比如: it+1, it+5, it1+it2 , it2-it1等)。而且也不能进行比较(>,<,=等)
原因就是因为,list的底层结构是用链表来实现的,它的内存不是连续的,你对指针或者迭代器+1等,其也是根据指针或者迭代器所在位置,偏移相应的字节来使其指向下一个位置,但是链表的内存不连续,指不定存哪呢,你怎么知道偏移多少,所以不允许。
那为什么++,--可以,此处的++,--你可以理解成其指定了迭代器的方向,++表示迭代器向后,--表示向前。其内部进行了操作,是可以找到下一个元素的。
只使用++,--也能体现出链表在访问数据时,需要一个一个找。
至于为什么不能比较,因为其指向的内存可能相差很多,你用它们比较没有意义。(vector和deque的可以比较,因为其内存是连续的)
代码
list<int> l2{ 1,2,3,4,5,6,7,8,9,10};list<int>::iterator it1 = l2.begin();
it1++;
it1++;list<int>::iterator it2 = l2.end();// 遍历数据
while (it1 != it2) {cout << *it1 << " "; // 3 4 5 6 7 8 9 10it1++;
}
相关知识点:
- 除去注意中的,其它的都和deque类似。
- 对于rbegin,cbegin,crbegin等在vector中已经演示。
8. list容器删除元素(有新增)
代码: erase()和clear()
list<int> l2{ 1,2,3,4,5,6,7,8,9,10};// 一.erase的两种用法
// 删除指定位置的元素
l2.erase(l2.begin());// 删除规定范围内的元素
list<int>::iterator it = l2.begin();
it++;
it++;
l2.erase(it, l2.end());// 二.clear的用法: 清空元素
l2.clear();for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {cout << *it << " ";
}
cout << endl;
相关知识点:
- 对于erase和clear函数的使用,和deque是类似的。
- 在erase和循环结合删除指定元素的时候,应该注意迭代器的更新的问题。-- 使用erase函数的返回值(在vector中已经演示过了)
代码:使用remove()函数
list<int> l2{ 1,1,2,3,55,1,1,1,6,5};l2.remove(1);for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {cout << *it << " "; // 输出2,3,55,6,5}cout << endl;
相关知识点:
- remove(nub); // 用于删除list容器中所有与nub值相同的元素。
代码: remove_if()函数
class Compare1 {
public:bool operator()(int n1) {return n1 >= 5; // 将大于等于5的元素都删除掉}
};int main(void) {list<int> l2{ 6,6,6,4,5,4,3,2,7,9,10};l2.remove_if(Compare1());system("pause");return 0;
}
结果:
相关知识点:
- remove_if()函数,用于删除满足条件的元素。
- 这是一个函数模板, 它接收一个类对象用来指定删除元素的条件。编译器会根据传入实参的类型,来实例化类参数
使用一个类对象来指定某种规则(此处为删除元素的规则),我们使用函数对象来指定。
Compare1是一个函数对象类,其指定了删除元素的规则(将list容器中>=5的元素删除掉,当传入元素>=5,()重载函数返回true,那么就删除此元素)。
我们在remove_if()中传入一个临时对象,函数内部会使用函数对象来调用()重载方法,来判断一个元素是否应该被删除。
代码: unique()函数
int main(void) {list<int> l2{ 6,6,6,4,5,4,3,2,2,7,9,10};l2.unique();for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {cout << *it << " "; }cout << endl;system("pause");return 0;
}
结果:
代码: 使用unique(参数)函数
class uniqueClass {
public:bool operator()(int a, int b) {return a!=b;}
};int main(void) {list<int> l2{ 6,6,6,4,5,4,3,2,2,7,9,10};l2.unique(uniqueClass());for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {cout << *it << " "; }cout << endl;system("pause");return 0;
}
结果:
相关知识点:
- unique()函数,如果不写参数,这个函数用于将存储在列表中的数据,如果相邻的元素有相同的,那么只保留其中的一个元素,将其它的相同的都删除掉。(注意: 必须是相邻的元素,不相邻的不会)。
- unique(参数)函数,传入函数对象,用来指定unique的删除规则。上面代码中,我们传入了自定义的规则,如果相邻的元素不相同,则删除元素。 -- 可以根据自己的需求定义删除规则。
unique删除过程: (图解)
删除相邻的相同元素:
最开始,会比较前两个数据,如果后面的等于前面的,那就将后面的元素删除。
第一个元素与第二个元素相同,则删除第二个元素,然后第一个元素再和第三个元素进行比较,判断是否相同。
如果第一个元素和第三个元素不相同,那么就跳过当前数据,让第三个元素和第四个元素进行相比。以此类推,知道判断结束。
删除相邻的不同元素:(也就是我们代码中自定义的删除方式,也可以定义别的)
判断第一个元素和第二个元素是否不同,如果不同就删除后面的元素,如果相同,就使用第二个元素和第三个元素进行比较。
第二个元素和第三个元素比相同,那么删除第三个元素,第二个元素再和第四个元素比较,以此类推,知道判断完所有的元素。 (所以第二个代码的结果为666)
9. list容器的排序函数
代码: sort()
int main(void) {list<int> l1{ 5,4,3,2,1};/*sort(); // 默认将list中的元素按照从小到大排序sort(函数对象); // 将元素按照函数对象指定的规则排序*/l1.sort();//l1.sort(greater<int>());cout << "l1中的元素:" << endl;for (list<int>::iterator it = l1.begin(); it != l1.end(); it++) {cout << *it << " "; }cout << endl;system("pause");return 0;
}
结果:
相关知识点
- sort(); // 将list中的元素默认按照从小到大的顺序排列。(其实就是默认使用less函数对象)
- sort(函数对象); // 将list中的元素按照函数规则指定的规则进行排列。
我们使用greater给出实例: sort(greater<int>());
10. list容器数据翻转
代码: reverse()函数
int main(void) {list<int> l1{ 5,4,3,2,1,6};/*reverse()将lsit容器中的元素进行翻转*/l1.reverse();cout << "l1中的元素:" << endl;for (list<int>::iterator it = l1.begin(); it != l1.end(); it++) {cout << *it << " "; }cout << endl;system("pause");return 0;
}
结果:
相关知识点:
- reverse(); // 将list容器中的元素进行翻转。
10.list的其它函数
代码: swap()函数,交换两容器的数据
int main(void) {list<int> l1{ 5,4,3,2,1,6};list<int> l2{ 10,11,12 };l1.swap(l2);cout << "l1中的元素:" << endl;for (list<int>::iterator it = l1.begin(); it != l1.end(); it++) {cout << *it << " "; }cout << endl;cout << "l2中的元素:" << endl;for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {cout << *it << " ";}cout << endl;system("pause");return 0;
}
结果:
代码: max_size()(和前面容器类似)
int main(void) {list<int> l1{ 5,4,3,2,1,6};cout << l1.max_size() << endl; // 768614336404564650system("pause");return 0;
}
代码: resize()函数
和deque和vector类似,所以就不在展示了。