学习C++途中自然绕不过STL,在这个系列文章之中
我们讲了string的使用和string的模拟实现,以及vector的使用、vector的模拟实现。
感兴趣的可以翻翻看。
目录
系列文章
前言
默认成员函数
构造函数
拷贝构造
赋值重载
迭代器
容量查询
数据访问
数据修改
assign
头插头删尾插尾删
指定位置插入删除
迭代器失效
交换
其他操作
splice
remove
合并
去重
逆置
排序
☘️list 是 STL 中又一重要容器,而 list 其实就是对链表的封装。一提到链表,相信大家第一反应就是单链表,而单链表的缺点又是显而易见的:尾插过于麻烦、只能单向访问等等。但实际上list中封装的链表并非普通的单链表,而是之前讲过的带头双向循环链表。
☘️其能够优化除随机访问之外,单链表的大部分缺点,而且再次经过封装因此使用起来十分地方便。
☘️这次主要讲讲 list 该如何使用,更多实现的细节等到模拟实现的时候再讲。
☘️使用list前记得要带头文件,否则就无法使用list了。
☘️跟以前学习的容器一样,list 的构造函数还是老三样。
☘️根据不同的写法初始化后的结果也有所不同。
int main()
{
list l1; //空表
list l2(5, 3); //33333
int arr[] = { 1,2,3,4,5,6 };
list l3(arr, arr + 6); //123456
return 0;
}
☘️参数同样为 list 的自然就是拷贝构造了,该拷贝为深拷贝,节点内部的值都相同,但节点的地址不同。
☘️以一个已经初始化过的 list 来初始化一个新的 list,最后打印出来的结果都应是相同的。
int main()
{
list l1(5, 3);
list l2(l1);
//list l2 = l1; //也可以这样写
for (auto i : l2)
{
cout << i << " ";
}
cout << endl;
return 0;
}
☘️赋值重载则是用于两个已经存在的 list 之间进行赋值的操作,若其中一个还未初始化,那么实际上调用的是拷贝构造。
☘️通过这串代码,我们便可以清晰地看到赋值前后 list 的变化。
int main()
{
list l1(5, 3);
list l2(3, 2);
cout << "原来的l1" << ": ";
for (auto i : l1) cout << i << " ";
cout << endl;
l1 = l2;
cout << "现在的l1" << ": ";
for (auto i : l1) cout << i << " ";
cout << endl;
return 0;
}
☘️虽然我们平时只是讲迭代器,但是根据其能力的不同,迭代器也被分作三类。
☘️之所以是双向迭代器,得益于 list 的底层结构是一个带头双向循环链表,使得每一个节点都能访问自己的上下两个节点。
☘️通过调用 begin 这个函数,我们能拿到指向 list 第一个有效数据的迭代器。
☘️再调用 end 就能拿到标志 list 结尾的迭代器,便有了边界,就可以完成对 list 的遍历了。
☘️先构建一个 list,之后拿到 begin 的迭代器,直到 end 前打印当前节点的值。注意: 这个区间是左闭右开的。
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
list l(arr, arr + 9);
list::iterator it = l.begin();
//auto it = l.begin(); //还可以这样写
while (it != l.end()) //[begin,end)
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
☘️反向迭代器的使用也是类似,只不过调用的函数换成了 rbegin 和 rend 而已。
☘️值得注意的一点是,反向迭代器的迭代使用的也是 ++。但迭代区间一样是 [rbegin,rend)。
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
list l(arr, arr + 9);
list::reverse_iterator it = l.rbegin(); //类型名改变
while (it != l.rend())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
☘️由于链表并没有扩容的概念,因此并没有 reserve 这个函数,而 resize 则是作为修改的函数,也不经常使用。
☘️最常用的莫过于上面这两个函数了,empty 用于判断链表是否为空,而 size 用于查看当前有效节点的数量。
☘️我们可以写一段代码简单使用一下。
int main()
{
list l1(3, 6);
list l2;
if (!l1.empty()) //l1不为空,打印大小为3
cout << "l1 size: " << l1.size() << endl;
if (!l2.empty()) //l2为空,不打印
cout << "l2 size: " << l2.size() << endl;
return 0;
}
☘️在 list 中,我们可以使用 front 和 back 访问链表的首尾元素。
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
list l(arr, arr + 9);
cout << "front is: " << l.front() << endl;
cout << "back is: " << l.back() << endl;
return 0;
}
☘️就像我们前面构造函数中的那样,可以讲内容转换为 n 个 val 或是一个迭代器区间。
☘️但是,以前的内容会被清除。
int main()
{
list l(2, 3); // 33
l.assign(5, 6); // 66666
for (auto it : l) cout << it << " ";
cout << endl;
vector v = { 2,3,5,4,2 };
l.assign(v.begin(), v.end()); // 23542
for (auto it : l) cout << it << " ";
cout << endl;
return 0;
}
☘️通过 push_front 和 push_back 可以做到头插和尾插,而 pop_front 和 pop_back 可以做到头删和尾删。
☘️只要记住 front 是前 back 是后,push 代表插入 pop 代表删除,之后怎么排列组合都不会记混了。
int main()
{
vector v = { 2,3,5,4,2 };
list l(v.begin(), v.end());
for (auto it : l) cout << it << " "; //原链表
cout << endl;
l.push_front(10);
l.push_back(20);
for (auto it : l) cout << it << " "; //插入后的链表
cout << endl;
l.pop_front();
l.pop_back();
for (auto it : l) cout << it << " "; //删除后的链表
cout << endl;
return 0;
}
☘️我们使用 insert 和 erase 可以做到指定位置插入和删除,但是我们发现 list 中并没有 find 这个函数。若是每次从 begin 获取迭代器还要++,那未免过于麻烦了。
☘️没关系,算法库里有一个find函数能够满足我们的使用需求,使用前记得带上头文件
☘️这样我们就能任意地插入删除了。
int main()
{
vector v = { 2,3,5,4,2 };
list l(v.begin(), v.end());
list::iterator pos = find(l.begin(), l.end(), 3);
l.insert(pos, 20); //在3前插入20
for (auto it : l) cout << it << " ";
cout << endl;
pos = find(l.begin(), l.end(), 5);
l.erase(pos); //删除5
for (auto it : l) cout << it << " ";
cout << endl;
return 0;
}
☘️当我们使用 erase 进行删除后,此时指向删除位置的迭代器就失效了,再次使用就会令程序崩溃。
☘️因此若要多次删除,则需要在使用后利用 erase 的返回值更新迭代器,这样使用才不会出现错误。
int main()
{
vector v = { 2,3,5,4,6 };
list l(v.begin(), v.end());
list::iterator pos = find(l.begin(), l.end(), 3);
for (int i = 0; i < 3; i++)
{
pos = l.erase(pos); //利用erase的返回值更新迭代器
}
for (auto it : l) cout << it << " ";
cout << endl;
return 0;
}
☘️跟 vector 和 string 里面的一样都是直接交换内部的指向,而不需要拷贝节点。
int main()
{
vector v1 = { 2,3,5,4,6 };
vector v2 = { 1,2,3,4,5 };
list l1(v1.begin(), v1.end());
list l2(v2.begin(), v2.end());
for (auto it : l1) cout << it << " "; //交换前
cout << endl;
for (auto it : l2) cout << it << " ";
cout << endl;
cout << endl;
l1.swap(l2);
for (auto it : l1) cout << it << " "; //交换后
cout << endl;
for (auto it : l2) cout << it << " ";
cout << endl;
return 0;
}
☘️除了上面那些基础了操作,库中还有一些其他函数,挑了几个讲讲。
☘️这个函数可以帮助我们将一个链表的节点转移到另外一个链表中。
☘️函数结束后,l2 就变成了空链表,而其节点被转移到 l1 之中。
int main()
{
int arr[] = { 1,2,3,4,5,6 };
int arr2[] = { 10,20,30 };
list l1(arr, arr + 6);
list l2(arr2, arr2 + 3);
for (auto i : l1) cout << i << " "; //转移前
cout << endl;
for (auto i : l2) cout << i << " ";
cout << endl;
cout << endl;
list::iterator it = l1.begin();
++it;
l1.splice(it, l2);
for (auto i : l1) cout << i << " "; //转移后
cout << endl;
for (auto i : l2) cout << i << " ";
cout << endl;
return 0;
}
☘️remove 本质上就是 find + erase,可以方便我们的实际的使用。
int main()
{
int arr[] = { 1,2,3,4,5,6 };
list l1(arr, arr + 6);
for (auto i : l1) cout << i << " "; //删除前
cout << endl;
l1.remove(5);
for (auto i : l1) cout << i << " "; //删除后
cout << endl;
return 0;
}
☘️使用 merge,我们可以合并两个有序的链表,实际上的操作就类似于归并排序。
int main()
{
int arr[] = { 1,2,3,4,5,6 };
int arr2[] = { 10,20,30 };
list l1(arr, arr + 6);
list l2(arr2, arr2 + 3);
for (auto i : l1) cout << i << " "; //合并前
cout << endl;
for (auto i : l2) cout << i << " ";
cout << endl;
cout << endl;
l1.merge(l2);
for (auto i : l1) cout << i << " "; //合并后
cout << endl;
for (auto i : l2) cout << i << " ";
cout << endl;
return 0;
}
☘️使用这个函数可以去除掉链表中的重复元素,但前提是这个链表必须是有序的。
int main()
{
vector v = { 1,1,1,2,3,3,4,4,5 };
list l1(v.begin(),v.end());
for (auto i : l1) cout << i << " "; //去重前
cout << endl;
cout << endl;
l1.unique();
for (auto i : l1) cout << i << " "; //去重后
cout << endl;
return 0;
}
☘️可以通过这个函数可以将链表逆置。
int main()
{
vector v = { 1,2,3,4,5,6 };
list l1(v.begin(), v.end());
for (auto i : l1) cout << i << " "; //逆置前
cout << endl;
cout << endl;
l1.reverse();
for (auto i : l1) cout << i << " "; //逆置后
cout << endl;
return 0;
}
☘️list 并不能使用算法库里面的 sort,因为它的迭代器并不是随机访问的迭代器,所以库中重载了一个 sort。
☘️但其实,这个函数的效率并不高,我们可以写个代码来测试一下。
int main()
{
srand((size_t)time(NULL));
int n = 1000000;
vector v;
list l;
for (int i = 0; i < n; i++)
{
int val = rand() % 100;
v.push_back(val);
l.push_back(val);
}
int begin1 = clock(); //vector开始排序
sort(v.begin(), v.end());
int end1 = clock(); //vector结束排序
int begin2 = clock(); //list开始排序
l.sort();
int end2 = clock(); //list结束排序
cout << "vector: " << end1 - begin1 << endl;
cout << "list: " << end2 - begin2 << endl;
return 0;
}
☘️当 n 为1000000时,差距就非常的明显了。
☘️之后我们将一个 list 拷贝进 vector 后再进行排序,看看最后结果如何。
int main()
{
srand((size_t)time(NULL));
int n = 1000000;
list l1;
list l2;
for (int i = 0; i < n; i++)
{
int val = rand() % 100;
l1.push_back(val);
l2.push_back(val);
}
int begin1 = clock();
vector v;
for (auto it : l1) v.push_back(it); //把list拷贝进vector
sort(v.begin(), v.end()); //再次排序
int end1 = clock();
int begin2 = clock();
l2.sort();
int end2 = clock();
cout << "l1: " << end1 - begin1 << endl;
cout << "l2: " << end2 - begin2 << endl;
return 0;
}
☘️可以看出,差距还是很大的,因此未来的使用中,除非数值较小为了图个方便可以直接使用,否则还不如拷贝到 vector 里再进行排序。
☘️好了,今天 list 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。