您现在的位置是:首页 >技术交流 >C++ [STL之list的使用]网站首页技术交流

C++ [STL之list的使用]

ARMCSKGT 2024-07-01 11:59:07
简介C++ [STL之list的使用]

list使用

本文已收录至《C++语言和高级数据结构》专栏!
作者:ARMCSKGT

在这里插入图片描述


前言

vector是一片连续的空间,在数据访问上性能较好,但是任意位置插入删除性能较低,头插头删性能亦是如此;此时在这种需要频繁插入的场景下,显然链表是一种更好的选择,STL中实现了带头双选循环链表,本次我们来介绍该如何使用STL中的链表list!
list


正文

本文理论依据来自于官方文档:STL容器list文档
首先在使用list前,需要声明头文件 < list > 且声明命名空间std!
list是通过模板实例的泛型容器,需要指定类型进行实例化

默认成员函数


构造函数类

构造函数类

  • 默认构造
    –构造一个空对象,里面没有任何数据(底层上只有一个头节点)
  • 构造n个值为val的链表对象
    –插入n个值实例类型的val值,构造一个对象
  • 迭代器区间构造
    – 通过其他容器迭代器或指针构造一个list对象
  • 列表初始化(C++11)
    – 对于支持C++11的编译器支持像数组一样使用列表{ }初始化
  • 拷贝构造
    – 拷贝另一个list,从而构造一个一模一样的list对象
#include <iostream>
#include <list>
using namespace std;

void Print(const list<int>& obj) //打印函数
{
	for (const auto& x : obj)
	{
		cout << x << " ";
	}
	cout << endl;
}

int main()
{
	list<int> l1; //默认构造

	list<int> l2(5, 1); //构造5个值为1(int类型)的链表对象
	Print(l2);

	int arr[] = { 5,4,3,2,1 };
	list<int> l3(arr, arr + 5); //指针作为迭代器区间构造
	Print(l3);

	vector<int> v = { 6,7,8,9,10 };
	list<int> l4(v.begin(),v.end()); //容器迭代器区间构造
	Print(l4);

	list<int> l5 = {11,12,13,14,15}; //列表初始化构造(C++11)
	Print(l5);

	list<int> l6(l5); //拷贝构造
	Print(l6);

	return 0;
}

演示



赋值重载

赋值重载与拷贝构造类似,只不过赋值的前提是对象已存在,拷贝构造是初始化对象!

void Print(const list<int>& obj)
{
	for (const auto& x : obj)
	{
		cout << x << " ";
	}
	cout << endl;
}

int main()
{
	vector<int> v = { 5,4,3,2,1 };
	list<int> l1(v.begin(),v.end()); //容器迭代器构造

	list<int> l2(5, 1);
	Print(l2);
	l2 = l1;
	Print(l2);
	return 0;
}

赋值重载



析构函数

析构函数在list对象声明周期结束时调用,释放所有节点(包括头节点)!
析构函数


迭代器


list迭代器比较特殊,知道链表的小伙伴应该都懂链表不是一片连续的空间,所以不能像顺序表那样支持迭代器随机访问!(即it+1,it+2…)

同样的,链表的迭代器也分三种(理论上四种):

  • 正向迭代器
  • 反向迭代器
  • const迭代器(正向和反向)
    迭代器
    const迭代器不支持修改当前指向的值,每一种迭代器不能混用,各自使用各自的迭代器类型!
int main()
{
	vector<int> v = { 5,4,3,2,1 };
	list<int> l1(v.begin(),v.end());
	list<int>::iterator it = l1.begin();
	list<int>::reverse_iterator rit = l1.rbegin();
	while (it != l1.end())
	{
		cout << *it << endl;
		++it;		
	}
	cout << endl;
	while (rit != l1.rend())
	{
		cout << *rit << endl;
		++rit;		
	}
	return 0;
}

迭代器演示
虽然list迭代器不支持随机访问,但是也支持通过++和- -遍历链表,甚至可以反向遍历,可见迭代器的强大之处!

迭代器在链表中的位置:
迭代器在链表中的位置


容量操作类


数据查询类

链表没有提前开空间这样的说法,对于数据只能查询以及对现有数据操作!
数据量查询

  • empty:查询链表是否为空,为空返回真
  • size:返回当前节点的个数
  • max_size:返回当前实例对象可容纳最大节点数(这个函数常用来检查申请空间的合法性即配合resize使用,查看申请的空间是否合法)
int main()
{
	list<int> obj(10,5); //初始化10个值为5的节点
	cout << "empty:" << obj.empty() << endl;
	cout << "size:" << obj.size() << endl;
	cout << "max_size:" << obj.max_size() << endl;

	return 0;
}

数据量查询



节点数量修改

list支持resize控制节点个数,resize支持增多节点和减少节点,对于增多的节点设置为val值,val有缺省参数,根据需要可进行设置初始化!
resize

int main()
{
	list<int> obj = { 1,2,3,4,5 };
	cout << "size:" << obj.size() << endl;
	obj.resize(10);
	cout << "size:" << obj.size() << endl;
	obj.resize(3);
	cout << "size:" << obj.size() << endl;
	return 0;
}

resize


数据访问


链表不支持下标访问,提供了额外对头尾节点访问的函数front和back,并且拥有引用和const引用重载函数应对不同的场景!
front和back

int main()
{
	list<int> obj = { 668,778,888,998,1080 };
	cout << "front:" << obj.front() << endl;
	cout << "back:" << obj.back() << endl;
	return 0;
}

front和back

不过list常用的遍历方式还是通过迭代器遍历,而且list因为迭代器所以支持范围for遍历!


增删类


因为链表插入删除效率非常高,所以增删类接口比较多,在这里介绍几种常用的接口!
增删接口



头插头删

头插push_front()头删pop_front() 只需要对头节点next(后继节点)指针操作即可!

int main()
{
	list<int> obj = { 1,2,3,4,5 };
	Print(obj);
	obj.pop_front();
	Print(obj);
	obj.push_front(668);
	Print(obj);

	return 0;
}

push_front和pop_front



尾插尾删

头插push_back()头删pop_back() 也只需要对头节点prev(前驱节点)指针操作即可!

int main()
{
	list<int> obj = { 1,2,3,4,5 };
	Print(obj);
	obj.pop_back();
	Print(obj);
	obj.push_back(668);
	Print(obj);

	return 0;
}

push_back和pop_back



重新分配

assign函数有点类似于 = 赋值重载,区别于赋值的是该函数可以跨容器达到赋值的效果(利用迭代器区间),assign函数会先将list对象清空再重新插入值!

assign函数有两个版本:

  • 分配n个值为val的节点(类似于带参构造函数)
  • 迭代器区间分配
int main()
{
	list<int> obj = { 1,2,3,4,5 };
	Print(obj);
	obj.assign(10, 5); //分配5个值为10的节点
	Print(obj);

	int arr[] = { 668 ,778,888,998,1080 };
	obj.assign(arr,arr+5); //分配arr数组区间
	Print(obj);

	return 0;
}

assign



任意位置插入

insert函数支持在迭代器位置插入节点,有三个版本:
insert

  • 在position迭代器位置下插入值为val的节点(返回新插入节点的迭代器)
  • 在position迭代器位置下插入n个值为val的节点
  • 在position迭代器位置下插入一段区间
int main()
{
	list<int> obj = { 1,2,3,4,5 };
	auto it = ++obj.begin(); //记录2位置迭代器
	Print(obj);

	it = obj.insert(it, 668); //在2位置插入668更新迭代器为668节点
	Print(obj);

	obj.insert(it, 888); //在668位置插入888
	Print(obj);

	int arr[] = { 6,7,8,9,10 };
	obj.insert(obj.begin(), arr, arr + 5); //在头节点1位置插入arr数组区间
	Print(obj);

	return 0;
}

insert演示
虽然insert函数不会导致迭代器失效,但是insert在插入单节点的函数上支持更新迭代器,必要时建议及时更新迭代器!



任意位置删除

erase函数有两种版本,删除函数需要注意迭代器失效问题,因为删除当前迭代器位置的节点后该位置节点内存已经被释放,所以两个版本的删除函数都会返回删除后的下一个节点迭代器
erase

int main()
{
	list<int> l1 = { 1,2,3 };
	list<int> l2 = { 1,2,3,4,5,6,7,8,9,10 };
	Print(l1);
	Print(l2);

	auto it1 = l1.begin();
	auto it2 = l2.begin();
	++it2; ++it2; ++it2;//迭代器挪动到4位置下

	it1 = l1.erase(it1); //删除手节点 - 更新迭代器
	it2 = l2.erase(l2.begin(),it2); //删除1-3区间节点(左闭右开) - 更新迭代器

	Print(l1);
	Print(l2);

	return 0;
}

在这里插入图片描述
迭代器失效情况:
迭代器失效
对于单节点删除,如果不更新迭代器,肯定会造成野指针访问,但对于区间删除,因为是左避右开,所以删不到闭区间的迭代器,所以不更新迭代器影响不大,但是如果删除的区间是全部,则还是需要更新迭代器!
这里可以发现it1如果更新迭代器则指向下一个节点迭代器!


其他操作


清空函数

clear可以将链表清空,也就是逐个释放节点,只留下头节点(区别于析构函数,clear不会释放对象)!

int main()
{
	list<int> obj = { 1,2,3 };
	cout << "size:" << obj.size() << endl;
	obj.clear();
	cout << "size:" << obj.size() << endl;
	return 0;
}

clear



交换函数

list有属于自己的交换函数,同时库中的函数也支持对list进行交换!

int main()
{
	list<int> l1 = { 1,2,3 };
	list<int> l2 = { 4,5,6 };
	cout << "l1:"; Print(l1);
	cout << "l2:"; Print(l2);
	cout << endl;

	l1.swap(l2); //list对象swap函数交换
	cout << "l1:"; Print(l1);
	cout << "l2:"; Print(l2);
	cout << endl;

	std::swap(l1, l2); //库函数交换
	cout << "l1:"; Print(l1);
	cout << "l2:"; Print(l2);
	cout << endl;

	return 0;
}

在这里插入图片描述
list自带的swap和库中的swap都支持容器的交换!



拼接

splice支持将一个list对象中的一个节点或者一段节点从原list对象上截取下来链接在调用对象上!
splice有三个版本:

  • 一个list截取插入到另一个list中的position迭代器位置
  • 将一个list的一个迭代器节点截取插入另一个list中的position迭代器位置
  • 将一个list的迭代器区间截取插入到另一个list中的position迭代器位置
    splice
int main()
{
	list<int> l1 = { 1,2,3 };
	list<int> l2 = { 4,5,6 };

	l1.splice(l1.begin(), l2); //将l2插入l1中
	cout << "l1:"; Print(l1);
	cout << "l2:"; Print(l2);
	cout << endl;

	list<int> l3 = { 1,2,3 };
	list<int> l4 = { 7,8,9 };
	l3.splice(l3.begin(),l4,l4.begin()); //将l4中的头节点插入l3中
	cout << "l3:"; Print(l3);
	cout << "l4:"; Print(l4);
	cout << endl;

	list<int> l5 = { 1,2,3 };
	list<int> l6 = { 448,558,668,778,888 };
	l5.splice(l5.begin(), l6, ++l6.begin(), l6.end()); //将l5的558-888插入l6中
	cout << "l5:"; Print(l5);
	cout << "l6:"; Print(l6);
	cout << endl;

	return 0;
}

splice演示
splice函数每次执行都需要另一个list对象的引用,哪怕只截取一个节点,说明在函数内部需要对节点进行剥离和缝合!

splice函数原理:
出自SLT源码剖析



移除指定值元素

remove类似于find+erase,find先找到这个值返回其迭代器供erase删除,该函数会根据给定的val值找到此值并删除,如果该值不存在,则什么也不做!
remove

int main()
{
	list<int> l1 = { 1,2,3 };
	l1.remove(2); //删除2
	l1.remove(5); //删除非list中的元素则执行无效
	Print(l1);

	return 0;
}

remove示例



排序

list自带排序算法,但效率非常低!

一般如果要对list排序,可以先将数据拷贝到vector,然后使用库函数sort快速排序后再拷贝会list,虽然有两次拷贝,但是排序性能仍然超过直接对list排序!

#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
	list<int> l1;
	list<int> l2;
	for (int i = 1; i < 10000000; ++i) //一千万降序数
	{
		l1.push_back(i);
		l2.push_back(i);
	}
	//排升序	
	time_t t = clock(); //记录开始时间
	l1.sort(); //list排序
	cout << "list sort:" << clock() - t << endl;

	t = clock();
	vector<int> v(l2.begin(),l2.end());
	std::sort(v.begin(), v.end()); //库函数排序
	l2.assign(v.begin(), v.end()); //将排完序的元素分配回list
	cout << "lib sort:" << clock() - t << endl;

	return 0;
}

sort
使用库函数sort需要声明头文件algorithm和std命名空间,此结果是在release模式下运行!
库函数对容器操作一般都是使用迭代器!



逆置

list对象自带reverse逆置函数,库中也有reverse函数,作用是将当前序列反过来!
list自带的reverse直接调用即可,库中的reverse则是通过容器迭代器作为参数进行逆置!

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;

int main()
{
	list<int> obj = { 1,2,3,4,5,6,7,8,9,10 };
	Print(obj);
	obj.reverse(); //list自带逆置
	Print(obj);
	std::reverse(obj.begin(), obj.end()); //库函数逆置
	Print(obj);

	return 0;
}

reverse
同样的,使用库函数reverse需要声明头文件algorithm和命名空间std!



去掉重复元素

unique函数支持对元素,但是在去重之前需要进行排序,因为其底层原理是两个相邻的重复元素会删除一个,而需要将所有相同的元素相邻就需要sort排序!

同样的,如果数据量小我们可以直接使用list的排序和去重,如果数据量较大建议拷贝到vector排序然后拷贝会list再去重!

int main()
{
	list<int> obj = { 1,2,5,3,7,1,6,5,9,8,7,2 };
	Print(obj);

	obj.unique(); //不排序直接去重没有任何效果
	Print(obj);

	obj.sort(); //排序再去重才会有效果
	obj.unique();
	Print(obj);

	return 0;
}

unique
算法库algorithm中也有unique,同样的适用于所有容器,因为使用迭代器操作!



合并

merge函数支持将一个list合并(移动)到另一个list中(而并非复制),与splice函数类似,只不过merge是将list中的全部节点按升序或降序的方式链入被插入对象中,该函数有两种形式:
merge

  • 第一种是合并一个list,并按升序插入其中
  • 第二种支持控制升序和降序,我们可以选择将list按升序插入其中,也可以按降序插入其中(使用greater和less控制)
int main()
{
	list<int> l1 = {6,10};
	list<int> l2 = { 1,2,5 };
	Print(l1);

	l1.merge(l2); //将l2按默认规则链入l1中
	Print(l1);

	list<int> l3 = {3,4,9,8,7};
	l1.merge(l3, less<int>()); //将l3按升序规则链入l1中
	Print(l1);

	return 0;
}

merge



关于查找

  • list没有提供查找函数,因为对于链表的查找只能用迭代遍历的方式进行,所以用库中的find足以!
  • 库中的find也是基于迭代器的遍历查找,也适用于所有支持迭代器的容器,find如果查找到了就会返回该元素的迭代器,如果没找到则返回传入的开区间(end())迭代器,如果有多个重复的值则返回正向序列上第一个符号条件的值!
//find的使用
find(迭代器开区间(起始区间),迭代器闭区间(结束区间),查找元素);

find底层原理:

template<class InputIterator, class T>
InputIterator find (InputIterator first, InputIterator last, const T& val)
{
 	while (first!=last) {
    if (*first==val) return first;
    	++first;
	}
 return last;
}

使用展示:

int main()
{
	list<int> obj = { 1,2,5,3,7,1,6,5,9,8,7,2 };
	list<int>::iterator it = std::find(obj.begin(), obj.end(), 2); //找到了就返回迭代器
	cout << *it << endl;
	it = std::find(obj.begin(), obj.end(), 10);
	cout << (it == obj.end()) << endl; //没找到返回开区间
	return 0;
}

find
find属于库函数,需要algorithm算法头文件和是他的命名空间!


最后

list的使用介绍到这里就结束了,相信学会了list在需要频繁增删数据的场景下我们那个轻松应对,对于vector和list两者各有优劣,应对不同的场景我们需要合理应用甚至结合使用;list的底层实现非常值得我们去研究,C++下一节将为大家介绍list模拟实现,揭开迭代器的背后秘密!

本次 <C++ list的使用> 就先介绍到这里啦,希望能够尽可能帮助到大家。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!

C-PLUS-PLUS

?其他文章阅读推荐?
C++ <STL之vector模拟实现> -CSDN博客
C++ <STL之vector的使用> -CSDN博客
C++ <STL之string的使用> -CSDN博客
C++ <STL之string模拟实现> -CSDN博客
?欢迎读者多多浏览多多支持!?

​​

​​


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