目录标题
- list介绍
- list定义
- list遍历
- list数据插入
- push_back
- push_front
- insert
- list删除
- pop_back
- pop_front
- erase
- list排序
- list去重
- list合并
- list转移
- list其他函数
- empty
- size
- front
- back
- assign
- swap
- resize
- clear
- list排序效率问题
list介绍
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
- 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率 更好。
- 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素).
list定义
首先得知道的一点就是list是一个类模板,在使用list来创建对象的时候我们必须得进行显示实例化:
那么这里的模板提供了两个类型,第一个类型是存储数据的类型,第二个类型是关于内存池的,既然这里提供了默认类型,那么我们这里就不需要多管。我们再来看看这个类型的构造函数:
这就是四种不同的list构造函数,大家可以发现一点就是这里跟vector一样都是由内存池来申请空间的,所以这里就会多一个参数
const allocator_type& alloc = allocator_type()
但是好在这里提供了缺省值不需要我们初学者来进行传参,但是如果未来我们的水平非常高了嫌弃库里面的内存池写的不好的话,我们可以自己写一个内存池并作为参数传递过去,那这里我们都是初学者就不要太管这个了,然后这个函数里面的size_type就是无符号整型的意思。
value_type的意思就是第一个模板参数的类型。
第一个形式:
explicit list (const allocator_type& alloc = allocator_type());
表示的意思是,无参构造也就是说通过这个构造函数创建出来的list对象没有任何内容,比如说下面的代码:
void test1()
{
list<int> l1;
list<int>::iterator it = l1.begin();
while (it!=l1.end())
{
cout << *it << " ";
++it;
}
}
将这段代码运行一下就可以发现这里打印不出来任何的内容:
第二种形式:
explicit list (size_type n, const value_type& val = value_type(),
const allocator_type& alloc = allocator_type());
用n个相同的数据来进行初始化,这里的第二个参数你可以传也可以不传不传的话这里就会调用该类型的默认构造函数来进行赋值,比如说下面的代码:
list<int> l1(10, 3);
list<int> l2(10);
cout << "l1的内容为:";
for (auto l : l1)
{
cout << l;
}
cout << endl;
cout << "l2的内容为:" ;
for (auto l : l2)
{
cout << l;
}
代码的运行结果如下:
第三种形式:
template <class InputIterator>
list (InputIterator first, InputIterator last,
const allocator_type& alloc = allocator_type());
使用迭代器区间来进行初始化,比如说下面的代码:
list<int>l1(5, 2);
list<int>::iterator it1 = l1.begin();
list<int>l2(++it1,--l1.end());
cout << "l1的内容为:";
for (auto l : l1)
{
cout << l;
}
cout << endl;
cout << "l2的内容为:" ;
for (auto l : l2)
{
cout << l;
}
这里大家要注意的一点就是:list的迭代器不能像前面的string和vector的迭代器一样,加一个常数来指向对应的位置,只能通过前置和后置++和–来改变迭代器的位置,那么上面的代码运行的结果就如下:
第四种形式:
list (const list& x);
使用另外一个形式相同的list对象来初始化该对象,那么这里的使用方法就如下:
list<int>l1(5, 2);
list<int>l2(l1);
cout << "l1的内容为:";
for (auto l : l1)
{
cout << l;
}
cout << endl;
cout << "l2的内容为:";
for (auto l : l2)
{
cout << l;
}
该代码运行结果如下:
list遍历
与前面的string和vector不一样的地方在于,我们这里的list不存在用[ ]遍历和修改数据的方式,因为string和vector都是在一块连续的空间存放的数据,而list不一样,它是在不同的地方存放数据,这些数据通过指针来进行相互的关联,那这里我们就可以通过数组来理解这里为什么不能用方括号,首先我们知道数组名是首元素的地址,而且数组中的元素在一块连续的区间,每个元素的地址之间相差为4:
int arr[10]={1,2,3,4,5,6,7,8,9,10};
这里的arr就是一个地址,该地址指向的是这个数组中的第一个元素也就是1,当我们使用这种形式来访问数据时:
int i=0;
cout<<arr[1]<<endl;
编译器会将这里的arr[1]转换成指针解引用的形式:*(arr + i)
arr是首元素的地址该地址的类型是int*类型,当这里的i等于0时这里的地址就不会发生改变,从而得到下标为0的元素,当我们将这里i的值加一时,由于数据是int类型所以它会将这里的地址加4从使这里指针指向下一个元素,当我们再解引用的时候就可以得到第二个元素,所以问题就来了,list的元素并不在一块连续的空间,当我们使用[ ]来获取元素时,通过对里面的值加1减1能获取对应的元素吗?那很明显是不行的,所以对于list的元素遍历我们可以采用迭代器遍历,比如说下面的代码:
void test2()
{
list<int> l1(10, 4);
list<int>::iterator it1 = l1.begin();
while (it1 != l1.end())
{
cout << *it1 << " ";
++it1;
}
}
这个代码的运行结果如下:
既然范围for的底层是迭代器实现的话,那么范围for也可以实现list的遍历:
void test2()
{
list<int> l1(10, 4);
list<int>::iterator it1 = l1.begin();
while (it1 != l1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
for (auto l : l1)
{
cout << l << " ";
}
}
代码的运行结果如下:
那么以上就是list遍历的内容。
list数据插入
push_back
这个函数的功能就是在list对象的尾部插入一个数据,可以通过下面的代码看看该函数的功能:
void test3()
{
list<int> l1(5, 4);
l1.push_back(2);
for (auto l : l1)
{
cout << l << " ";
}
}
该代码的运行结果如下:
push_front
由于list的数据不在一块连续的空间,所以当我们在对象的头部插入内容的时候就不会造成数据的挪动,所以该类型就提供了push_front函数:
该函数的使用形式如下:
void test3()
{
list<int> l1(5, 4);
l1.push_back(2);
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
l1.push_front(1);
for (auto l : l1)
{
cout << l << " ";
}
}
代码的运行结果如下:
insert
上面两个函数只能实现在对象的头部和尾部插入数据,并且一次只能插入一个数据,那么这里的insert函数就可以实现在任意位置插入一个数据,或者一次性插入n个相同的数据,或者一段来自于其他对象的数据,insert的使用得用到迭代器,而list的迭代器无法通过加减一个整数来指向指定的位置,所以要想很好的使用这里的迭代器我们这里就得使用库中的find函数:
该函数的返回类型是迭代器类型,所以我们可以使用该函数的返回值来初始化迭代器,进而更好的使用insert函数,那么这里的使用代码就如下:
void test4()
{
list<int> l1(5,4);
l1.push_back(3);
list<int> ::iterator it1 = find(l1.begin(), l1.end(), 3);
it1=l1.insert(it1, 2);//指定位置插入一个数据,并更新迭代器的位置
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "对象中的内容为:";
l1.insert(it1,2, 1);//指定位置插入n个相同的数据
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
it1 = find(l1.begin(), l1.end(), 3);//再更新it1的值使其再指向原来的3
list<int> l2(3, 7);
cout << "对象中的内容为:";
l1. insert(it1, l2.begin(), l2.end());//指定位置插入一段数据
for (auto l : l1)
{
cout << l << " ";
}
}
那么这段代码的运行结果就如下:
list删除
pop_back
这个函数的功能就是删除尾部的数据:
该代码的使用如下:
void test5()
{
list<int> l1(5, 4);
l1.push_back(3);
l1.push_front(5);
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "删除尾部数据"<<endl;
l1.pop_back();
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
}
代码的运行结果为:
pop_front
因为这里是链表,对头部删除数据不会挪动数据,所以就有了pop_front函数该函数的介绍如下:
代码的使用如下:
void test5()
{
list<int> l1(5, 4);
l1.push_back(3);
l1.push_front(5);
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "删除尾部数据"<<endl;
l1.pop_back();
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "删除头部数据" << endl;
l1.pop_front();
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
}
代码的运行结果如下:
erase
erase函数能够实现任意位置的删除,我们来看看该函数的参数:
这个函数重载了两个不同的形式,第一个形式表示的意思是删除指定位置上的一个元素,第二个形式的意思就是删除对象中的一段数据,那么这里我们可以通过下面的代码来了解这个函数的使用:
void test6()
{
list<int> l1(2, 4);
l1.push_back(3);
l1.push_back(2);
l1.push_back(1);
l1.push_back(0);
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "删除指定位置的一个元素" << endl;
list<int>::iterator it1 = find(l1.begin(), l1.end(),3);
l1.erase(it1);
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "删除一段数据" << endl ;
cout << "对象中的内容为:";
l1.erase(++l1.begin(), --l1.end());
for (auto l : l1)
{
cout << l << " ";
}
}
代码的运行结果如下:
那么这里大家要注意一个问题就是:当我们使用迭代器删除对象中的一个元素的时,该迭代器是会失效的,原因很简单迭代器指向了一个数据,当我们把这个数据删除之后改迭代器指向的那个空间就被操作系统回收了,这个时候的迭代器就相当于指针中的野指针,这里大家要注意一下。
list排序
c++本身就提供了一个sort函数用来对数据进行排序:
那为什么我们这里的list还得自己提供一个sort函数呢?
那要想解决这个问题我们就得来提提迭代器分类的问题,c++将迭代器分为了三类:单向迭代器,双向迭代器,随机迭代器。单向迭代器只能够执行++的功能使其迭代器指向下一个元素,单向链表中的迭代器就是单向迭代器;双向迭代器不仅能够执行++功能,而且还能够执行- -功能这种迭代器既可以通过++指向下一个元素,还可以通过使用 - -使其指向上一个元素,那么我们这里的list双向链表就是这种迭代器;随机迭代器在双向迭代器之上还可以通过 + 或者 - 来达到一下指向后n个或者前n个元素,那么vector和string中的迭代器就是随机迭代器,那我们这里再来看看系统中sort函数的参数类型是:RandomAccessIterator
将其翻译一下就是随机迭代器,而我们list中的迭代器是双向迭代器,如果我们使用双向迭代器来调用sort函数的话,看看会发生什么样的情况,测试的代码如下:
void test7()
{
list<int> l1(2, 4);
l1.push_back(3);
l1.push_back(2);
l1.push_back(1);
l1.push_back(0);
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "调用系统中的sort函数" << endl;
sort(l1.begin(), l1.end());
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
}
我们将代码运行一下就可以看到这里报错:
我们将这里的swap修改一下,改成这样: l1.sort();
这样的话我们调用的就是list库中的swap函数,我们再运行一下上面的代码就可以发现正常运行了:
那这是为什么呢?原因很简单,系统的sort函数在实现的过程中会将两个迭代器进行相减,然后用相减得到的结果结合快排从而实现数据的排序,我们这里传过去的迭代器是双向迭代器不支持两个迭代器相减,所以在使用的时候就会报错,这也是为什么list库要单独提供一个sort函数的原因,list中的sort函数采用的是归并排序而不是快速排序。
list去重
这个函数的功能是去除对象中重复的数据,比如说对象还有三个整型1和两个整型2,那么使用这个函数之后这个对象当中就只会有1个整型1和1个整型2,但是使用这个函数得有个前提,就是对象中的数据必须是有序的才行,比如说我们下面的代码:
void test8()
{
list<int> l1(3, 2);
l1.push_back(1);
l1.push_back(2);
l1.push_back(6);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
l1.push_back(1);
l1.push_back(2);
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "将对象的数据进行去重之后对象的内容为:";
l1.unique();
for (auto l : l1)
{
cout << l << " ";
}
}
我们这里是个无序的数据,并且内部含有重复的数据但是我们将上面的代码运行一下就可以发现这里的去重函数并没有发挥作用:
虽然去除了一些内部重复的数据但是在该对象中依然含有重复的数据,那么这就可以证明一点当数据是无序的时候这里的去重函数会失效,我们在去重函数之前使用一下sort函数,将对象的数据变成有序的,再运行一下看看结果会是如何,那么下面是代码:
void test8()
{
list<int> l1(3, 2);
l1.push_back(1);
l1.push_back(2);
l1.push_back(6);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
l1.push_back(1);
l1.push_back(2);
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "将对象的内容变成有序的:" << endl;
l1.sort();
cout << "对象中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "将对象的数据进行去重" << endl;
cout << "对象中的内容为:";
l1.unique();
for (auto l : l1)
{
cout << l << " ";
}
}
代码的运行结果如下:
那么这里的结果就非常的明显了,使用这个函数之前我们必须得将对象的数据进行排序,这样才能发挥它的作用,那么这里大家肯定会有个疑问就是为为什么不直接在unique函数中直接帮我们排序呢?这样我们就不用自己调用函数了啊,那么为什么没有这么做的原因也非常的简单,因为如果我们对象的数据本来就是有序的话,那调用这个函数再进行一次排序的话不就会造成浪费了吗?所以在unique函数里面是不会对我们传过来的对象进行排序的,得我们使用者自己排序,那么者就是该函数的介绍。
list合并
将两个相同数据类型的list对象合并成一个list对象就得用到下面这个函数:
该函数有个特性就是当你给的两个对象的数据是有序的话,那么我们使用这个函数合并之后的结果依然也是有序的,我们可以看看下面的代码:
void test9()
{
list<int> l1(3, 2);
list<int> l2;
l2.push_back(1);
l2.push_back(2);
l2.push_back(3);
l2.push_back(4);
l2.push_back(5);
l1.merge(l2);
cout << "对象l1中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "对象l2中的内容为:";
for (auto l : l2)
{
cout << l << " ";
}
}
这段代码的运行结果如下:
通过这个运行结果大家可以看到这个函数的使用特性就是哪个对象调用的这个函数,那么就会将另外一个对象的内容合并到这个对象里面去,并且另外一个对象的内容会被清空,那么这就是该函数的使用规则。
list转移
将一个list转移到另外一个list对象的话就可以用到下面这个函数:
这就是该函数的介绍,我们可以将一个对象的内容转移到另外一个对象的指定position位置,这就是第一种形式对应的指定方式:
void test10()
{
list<int> l1(3, 2);
list<int> l2;
l2.push_back(1);
l2.push_back(2);
l2.push_back(3);
l2.push_back(4);
l2.push_back(5);
cout << "对象l1中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
l1.splice(++l1.begin(), l2);
cout << "对象l1中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "对象l2中的内容为:";
for (auto l : l2)
{
cout << l << " ";
}
}
代码的运行结果如下:
当我们以这种形式使用这个函数之后对象l2中的内容就完全没有了,全部都转移到l1的指定位置当然我们还可以将l2的部分内容转移到l1里面,那这里就得用到第二和第三种形式,第二种形式就是将位置i的元素进行转移,第三种是将first到end之间的内容进行转移,我们来看看下面的代码,这是第二种形式对应的代码:
void test11()
{
list<int> l1(3, 2);
list<int> l2;
l2.push_back(1);
l2.push_back(2);
l2.push_back(3);
l2.push_back(4);
l2.push_back(5);
cout << "对象l1中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
l1.splice(++l1.begin(),l2, ++l2.begin());
cout << "对象l1中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "对象l2中的内容为:";
for (auto l : l2)
{
cout << l << " ";
}
}
将这段代码运行一下就可以看到这里只转移了一个元素:
下面是第三种形式的代码:
void test12()
{
list<int> l1(3, 2);
list<int> l2;
l2.push_back(1);
l2.push_back(2);
l2.push_back(3);
l2.push_back(4);
l2.push_back(5);
cout << "对象l1中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
l1.splice(++l1.begin(), l2, ++l2.begin(),--l2.end());
cout << "对象l1中的内容为:";
for (auto l : l1)
{
cout << l << " ";
}
cout << endl;
cout << "对象l2中的内容为:";
for (auto l : l2)
{
cout << l << " ";
}
}
代码的运行结果如下:
我们可以看到这里的l2除了第一个元素和最后一个元素其他元素都转移到了l1的第二个元素上了,那么这就是该函数的使用方法。
list其他函数
empty
用来返回该对象的内容是否为空。
size
返回list对象的长度
front
返回list对象的第一个元素
back
返回list对象的最后一个元素
assign
将list对象的空间进行清空,然后用新的内容来进行填充。
swap
交换两个list对象的内容。
当然list中也提供了两个不同参数的swap,以防止使用者写错从而调用了效率较低的库中的swap
resize
修改对象中的长度,如果修改的长度超过原来的长度则将会用参数中的内容来进行填充
clear
将对象中的内容全部清空。
list排序效率问题
即便list中提供了sort函数,但是在实际的使用情况中我们使用该函数的地方依旧很少因为这个函数的效率太低了,比如说下面的代码,我们将list的sort函数与vector的sort函数来进行一下对比,首先先生成100w个随机数,并将这些随机数尾插到两个对象里面:
void test14()
{
srand((unsigned int)time(0));
const int N = 1000000;
vector<int> v;
list<int> it1;
for (int i = 0; i < N; i++)
{
auto e = rand();
v.push_back(e);
it1.push_back(e);
}
}
然后我们在使用clock函数来进行计时并打印其最后的结果:
void test14()
{
srand((unsigned int)time(0));
const int N = 1000000;
vector<int> v;
list<int> it1;
for (int i = 0; i < N; i++)
{
auto e = rand();
v.push_back(e);
it1.push_back(e);
}
int begin1 = clock();
sort(v.begin(),v.end());
int end1 = clock();
int begin2 = clock();
it1.sort();
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
我们在release环境下跑一下这段代码就可以发现这两个排序的效率差别挺大的:
所以当我们想对list数据进行排序的话,我们采用的方法一般都是先将,list的数据拷贝到vector中,再对vector进行排序,最后再将排序后的结果拷贝的list里面从而实现list的数据拷贝,那么下面的代码就是上面的排序的改进性:
void test13()
{
srand((unsigned int)time(0));
const int N = 1000000;
vector<int> v;
v.reserve(N);
list<int> lt1;
list<int> lt2;
for (int i = 0; i < N; ++i)
{
auto e = rand();
//v.push_back(e);
lt1.push_back(e);
lt2.push_back(e);
}
// 拷贝到vector排序,排完以后再拷贝回来
for (auto e : lt1)
{
v.push_back(e);
}
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
size_t i = 0;
for (auto& e : lt1)
{
e = v[i++];
}
int begin2 = clock();
// sort(lt.begin(), lt.end());
lt2.sort();
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
我们将这段代码运行一下发现就,尽管我们这样折腾但是它的效率依然比list单独排序的效率要高:
那么这就是list排序的效率问题,大家理解就行。