您现在的位置是:首页 >技术杂谈 >【STL】list的使用网站首页技术杂谈

【STL】list的使用

LinAlpaca 2024-06-23 12:01:02
简介【STL】list的使用

系列文章

学习C++途中自然绕不过STL,在这个系列文章之中

我们讲了string的使用string的模拟实现,以及vector的使用vector的模拟实现

感兴趣的可以翻翻看。


目录

系列文章

前言

默认成员函数

构造函数

拷贝构造

赋值重载

迭代器

容量查询

数据访问

数据修改

assign

头插头删尾插尾删

指定位置插入删除

迭代器失效

交换

其他操作

splice

remove

合并

去重

逆置

排序


前言

☘️list 是 STL 中又一重要容器,而 list 其实就是对链表的封装。一提到链表,相信大家第一反应就是单链表,而单链表的缺点又是显而易见的:尾插过于麻烦、只能单向访问等等。但实际上list中封装的链表并非普通的单链表,而是之前讲过的带头双向循环链表

☘️其能够优化除随机访问之外,单链表的大部分缺点,而且再次经过封装因此使用起来十分地方便。

☘️这次主要讲讲 list 该如何使用,更多实现的细节等到模拟实现的时候再讲。

默认成员函数

☘️使用list前记得要带头文件<list>,否则就无法使用list了。 

构造函数

☘️跟以前学习的容器一样,list 的构造函数还是老三样。

  • 创建空链表
  • 创建n个值为val的节点的链表
  • 以一个迭代器区间进行构造

 ☘️根据不同的写法初始化后的结果也有所不同。

int main()
{
	list<int> l1;                 //空表
	list<int> l2(5, 3);           //33333
	int arr[] = { 1,2,3,4,5,6 };
	list<int> l3(arr, arr + 6);   //123456
	return 0;
}

拷贝构造

☘️参数同样为 list 的自然就是拷贝构造了,该拷贝为深拷贝,节点内部的值都相同,但节点的地址不同。

☘️以一个已经初始化过的 list 来初始化一个新的 list,最后打印出来的结果都应是相同的。

int main()
{
	list<int> l1(5, 3);
	list<int> l2(l1); 
	//list<int> l2 = l1;   //也可以这样写
    for (auto i : l2)
	{
		cout << i << " ";
	}
	cout << endl;
	return 0;
}

 

赋值重载

☘️赋值重载则是用于两个已经存在的 list 之间进行赋值的操作,若其中一个还未初始化,那么实际上调用的是拷贝构造。

☘️通过这串代码,我们便可以清晰地看到赋值前后 list 的变化。

int main()
{
	list<int> l1(5, 3);
	list<int> 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;
}

 

迭代器

☘️虽然我们平时只是讲迭代器,但是根据其能力的不同,迭代器也被分作三类。

  • 单向迭代器:只能++   例如 unorder_map 的迭代器
  • 双向迭代器:可以++还能--    例如 list 的迭代器
  • 随机迭代器:++、--、+、-   例如 vector 的迭代器

☘️之所以是双向迭代器,得益于 list 的底层结构是一个带头双向循环链表,使得每一个节点都能访问自己的上下两个节点

☘️通过调用 begin 这个函数,我们能拿到指向 list 第一个有效数据的迭代器

☘️再调用 end 就能拿到标志 list 结尾的迭代器,便有了边界,就可以完成对 list 的遍历了。

      

 ☘️先构建一个 list,之后拿到 begin 的迭代器,直到 end 前打印当前节点的值。注意: 这个区间是左闭右开的。

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	list<int> l(arr, arr + 9);
	list<int>::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<int> l(arr, arr + 9);
	list<int>::reverse_iterator it = l.rbegin();  //类型名改变
	while (it != l.rend())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	return 0;
}

容量查询

☘️由于链表并没有扩容的概念,因此并没有 reserve 这个函数,而 resize 则是作为修改的函数,也不经常使用。

☘️最常用的莫过于上面这两个函数了,empty 用于判断链表是否为空,而 size 用于查看当前有效节点的数量

  

☘️我们可以写一段代码简单使用一下。

int main()
{
	list<int> l1(3, 6);
	list<int> 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<int> l(arr, arr + 9);
	cout << "front is: " << l.front() << endl;
	cout << "back is: " << l.back() << endl;
	return 0;
}

 

数据修改

assign

☘️就像我们前面构造函数中的那样,可以讲内容转换为 n 个 val 或是一个迭代器区间。

☘️但是,以前的内容会被清除

int main()
{
	list<int> l(2, 3);    // 33
	l.assign(5, 6);       // 66666
	for (auto it : l) cout << it << " ";
	cout << endl;
	vector<int> 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_frontpop_back 可以做到头删和尾删。

☘️只要记住 front 是前 back 是后push 代表插入 pop 代表删除,之后怎么排列组合都不会记混了。

int main()
{
	vector<int> v = { 2,3,5,4,2 };
	list<int> 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函数能够满足我们的使用需求,使用前记得带上头文件<algorithm>。

☘️这样我们就能任意地插入删除了。

int main()
{
	vector<int> v = { 2,3,5,4,2 };
	list<int> l(v.begin(), v.end());
	list<int>::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<int> v = { 2,3,5,4,6 };
	list<int> l(v.begin(), v.end());
	list<int>::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<int> v1 = { 2,3,5,4,6 };
	vector<int> v2 = { 1,2,3,4,5 };
	list<int> l1(v1.begin(), v1.end());
	list<int> 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;
}

其他操作

☘️除了上面那些基础了操作,库中还有一些其他函数,挑了几个讲讲。

splice

☘️这个函数可以帮助我们将一个链表的节点转移到另外一个链表中。

☘️函数结束后,l2 就变成了空链表,而其节点被转移到 l1 之中。 

int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	int arr2[] = { 10,20,30 };
	list<int> l1(arr, arr + 6);
	list<int> l2(arr2, arr2 + 3);
	for (auto i : l1) cout << i << " ";  //转移前
	cout << endl;
	for (auto i : l2) cout << i << " ";
	cout << endl;
	cout << endl;
	list<int>::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

☘️remove 本质上就是 find + erase,可以方便我们的实际的使用。

int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	list<int> 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<int> l1(arr, arr + 6);
	list<int> 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<int> v = { 1,1,1,2,3,3,4,4,5 };
	list<int> 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<int> v = { 1,2,3,4,5,6 };
	list<int> 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<int> v;
	list<int> 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<int> l1;
	list<int> l2;

	for (int i = 0; i < n; i++)
	{
		int val = rand() % 100;
		l1.push_back(val);
		l2.push_back(val);
	}

	int begin1 = clock();
	vector<int> 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 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。