STL 容器:List

目录

  • [1 list 的概念](#1 list 的概念)
  • [2 list 的常用接口](#2 list 的常用接口)
    • [2.1 list 的构造函数](#2.1 list 的构造函数)
    • [2.2 list 的遍历](#2.2 list 的遍历)
    • [2.3 list 的空间管理](#2.3 list 的空间管理)
      • [2.3.1 size()](#2.3.1 size())
      • [2.3.2 resize()](#2.3.2 resize())
      • [2.3.3 empty()](#2.3.3 empty())
    • [2.4 list 的增删改查](#2.4 list 的增删改查)
      • [2.4.1 front()](#2.4.1 front())
      • [2.4.2 back()](#2.4.2 back())
      • [2.4.4 push_front()](#2.4.4 push_front())
      • [2.4.5 pop_front()](#2.4.5 pop_front())
      • [2.4.6 push_back()](#2.4.6 push_back())
      • [2.4.7 pop_back()](#2.4.7 pop_back())
      • [2.4.8 insert()](#2.4.8 insert())
      • [2.4.9 erase()](#2.4.9 erase())
      • [2.4.10 swap()](#2.4.10 swap())
      • [2.4.11 splice()](#2.4.11 splice())
      • [2.4.12 remove()](#2.4.12 remove())
      • [2.4.13 unique()](#2.4.13 unique())
      • [2.4.14 merge()](#2.4.14 merge())
      • [2.4.15 clear()](#2.4.15 clear())
  • [3 list 的优缺点](#3 list 的优缺点)
  • [4 与 vector 的对比](#4 与 vector 的对比)

1 list 的概念

list 是 STL 容器的一种,list 既可以用来保存内置类型数据 (char,short,int等),又可以用来保存自定义类型数据 (对象),它的底层数据结构是带头结点的双向循环链表

要想使用 list ,就需要包含头文件 list 并展开命名空间 std

cpp 复制代码
#include <list>
using namespace std;

2 list 的常用接口

2.1 list 的构造函数

在构造 list 时,要遵循如下的语法:

cpp 复制代码
list<类型名> 对象名;

list 的构造函数主要有以下几种:

(1)使用空间配置器进行构造,这个方式不太常用,因此在这里不做介绍
(2)使用 n 个 val 进行构造

用这种方式构造时,val 为用户给定的值,如果未给定,则它会使用缺省值(默认值),默认值为当前 list 存储的元素对应的类型的默认值,比如当前的 list 要存储 int 类型的值,那么它的默认值是 0,如果要存储 string,那么它的默认值是空字符串

cpp 复制代码
list<int> list1(5, 1); //n = 5, val = 1


(3)使用其它容器的迭代器来确定区间,用区间内的元素来进行构造

用这种方式构造的时候,要保证 用于构造 list 的容器中所存储的数据的类型要和 list 中将要存储的数据的类型一致 ,也就是说,如果 list 中要存储 int 类型的数据,是不可以用 string 中存储的元素来进行构造的,因为 string 中存储的是字符

cpp 复制代码
string s("hello world");
list<char> list1(s.begin(), s.end());


(4)使用一个已经存在的 list 来进行构造(拷贝构造)

用这种方式构造的时候, 要保证两个 list 存储的数据的类型是一致的

cpp 复制代码
list<int> list1(5, 1);
list<int> list2(list1);


(5)构造一个空的 list

如果在定义 list 时,对象名后不加括号,就会构造出一个空的 list

cpp 复制代码
list<int> v3;

2.2 list 的遍历

由于 list 的底层是 链表所以遍历 不支持下标,只支持迭代器和范围 for

(1)使用迭代器遍历

在使用迭代器遍历时,会使用到以下几个接口:

接口名称 作用 返回值类型
begin() 返回指向第一个元素的迭代器 普通对象返回 iterator,const 对象返回 const_iterator
end() 返回指向最后一个元素的下一个位置的迭代器 普通对象返回 iterator,const 对象返回 const_iterator
rbegin() 返回指向最后一个元素的迭代器 普通对象返回 reverse_iterator,const 对象返回 const_reverse_iterator
rend() 返回指向第一个元素的前一个位置的迭代器 普通对象返回 reverse_iterator,const 对象返回 const_reverse_iterator

list 的迭代器属于 双向迭代器,只能进行 ++,- - 操作, 支持双向遍历

正向遍历:

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5 };
	list<int>::iterator it = list1.begin();
	while (it != list1.end())
	{
		cout << *it << " ";
		it++;
	}
	return 0;
}

反向遍历:

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5 };
	list<int>::reverse_iterator rit = list1.rbegin();
	while (rit != list1.rend())
	{
		cout << *rit << " ";
		rit++;
	}
	return 0;
}

(2)范围 for 与 auto 遍历

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5 };
	for (auto e : list1)
	{
		cout << e << " ";
	}
	return 0;
}

在 auto 后加上 & 就是 引用类型,由 auto& 修饰的变量 e 就是每一个元素的别名, 此时对 e 进行修改会影响结果,但不会影响 list 中元素的值

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5 };
	for (auto& e : list1)
	{
		cout << e + 10 << " ";
	}
	return 0;
}

2.3 list 的空间管理

在对 list 进行空间管理时,经常会使用到以下几个接口:

接口名称 作用 返回值类型
size() 返回 list 中存储的有效元素个数 size_t (无符号整形)
resize() 增加或缩减有效元素个数 void
empty() 判断 list 是否为空 bool

2.3.1 size()

size 的主要作用是 返回当前 list 中有效元素的个数

cpp 复制代码
int main()
{
	vector<int> v1 = { 1,2,3,4,5,6 };
	cout << v1.size() << endl;
	return 0;
}

2.3.2 resize()

resize 的主要作用是 对有效元素的个数进行缩减或增加

它的处理分两种情况:

(1)n > 当前有效元素个数

此时会 在 list 中尾插若干个值为 val 的结点,直到有效元素个数为 n

其中 val 为用户给定的值,若没有给定则使用默认值

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.resize(10);
	return 0;
}


(2)n < 当前有效元素个数

此时会将 超过 n 个有效元素的部分进行删除

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	cout << "size: " << list1.size() << endl;
	list1.resize(3);
	cout << "size: " << list1.size() << endl;
	return 0;
}

减少元素前:

减少元素后:

2.3.3 empty()

empty 的作用是 判断 list 是否为空,是空返回 true,不是空则返回 false

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list<int> list2;
	if (list1.empty())
		cout << "list1 is empty" << endl;
	else
		cout << "list1 is not empty" << endl;

	cout << endl;

	if (list2.empty())
		cout << "list2 is empty" << endl;
	else
		cout << "list2 is not empty" << endl;
	return 0;
}

2.4 list 的增删改查

list 的增删改查接口主要有以下几种:

接口名称 作用 返回值类型
front() 返回 list 的第一个元素 引用
back() 返回 list 的最后一个元素 引用
push_front() 在 list 中头插一个元素 void
pop_front() 在 list 中进行头删 void
push_back() 在 list 中尾插一个元素 void
pop_back() 在 list 中进行尾删 void
insert() 在 list 中插入元素 Iterator/void
erase() 在 list 中删除元素 Iterator
swap() 交换 list 中的所有元素 void
splice() 将一个 list 中的元素转移到另外一个中 void
remove() 清除 list 中符合要求的元素 void
unique() 清除 list 中重复的元素 void
merge() 合并两个有序的 list void
clear() 清空 list void

2.4.1 front()


front 的作用是 访问 list 的第一个元素,返回该元素的引用对于 const 对象,返回的就是 const 引用,就意味着不能更改该对象的值

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	cout << list1.front() << endl;
	return 0;
}

2.4.2 back()


back 的作用是 访问 list 的最后一个元素,返回的也是该元素的引用, 对于 const 对象,返回的就是 const 引用,就意味着不能更改该对象的值

cpp 复制代码
int main()
{
	vector<int> v1 = { 1,2,3,4,5,6 };
	cout << "v1 back:" << v1.back() << endl;
	return 0;
}

2.4.4 push_front()

push_front 的作用是 在 list 中进行头插并更改 size()

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.push_front(8);
	return 0;
}

2.4.5 pop_front()

pop_front() 的作用是 在 list 中进行头删并更新 size()

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.pop_front();
	return 0;
}

2.4.6 push_back()

push_back 的作用是 在 list 中尾插一个元素 val 并更新 size()

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.push_back(7);
	return 0;
}

2.4.7 pop_back()

pop_back 的作用是 删除 list 中的最后一个元素并更新 size()

cpp 复制代码
int main()
{
	vector<int> v1 = { 1,2,3,4,5,6 };
	v1.pop_back();
	return 0;
}

2.4.8 insert()

insert 的作用是 在 list 中指定一个位置进行插入,插入的方式有三种:

(1)在指定位置插入一个值

此时在插入完成后,会返回 指向新元素的迭代器

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.insert(list1.begin(), 10);
	return 0;
}


(2)在指定的位置插入 n 个同样的值

在插入完成后,不进行任何返回

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.insert(list1.begin(), 5, 7);	
	return 0
}


(3)在指定位置插入一个区间内的值

这里的区间范围需要给出 起始迭代器终止迭代器 来确定,区间是左闭右开的,插入完成后,不会返回任何值

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list<int> list2 = { 7,8,9 };
	list1.insert(list1.begin(), list2.begin(), list2.end());//[list2.begin(), list2.end())
	return 0;
}

2.4.9 erase()

erase 主要是 用来删除 list 中指定位置上的元素并更新size(),删除时,有两种方式:

(1)删除指定位置上的一个值

成功删除后,函数会 返回被删除元素的下一个位置的迭代器

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.erase(list1.begin());
	return 0;
}


(2)删除指定区间上的所有值

这里的区间范围需要给出 起始迭代器终止迭代器 来确定,区间是左闭右开 的,删除完成后,会返回最后一个被删除元素的下一个位置的迭代器

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list1.erase(list1.begin(), list1.end());
	return 0;
}

需要注意的是,因为 list 的底层是双向循环带头结点的链表,删除元素就意味着释放元素所在的结点,如果删除后仍然使用被删除元素的迭代器,就会访问到已经被释放的空间,这是十分危险的行为,这也是一种迭代器失效

2.4.10 swap()

swap 的作用主要是 交换两个 list 中的值

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list<int> list2 = { 7,8,9,10,11,12 };
	list1.swap(list2);
	return 0;
}

2.4.11 splice()


splice 主要是 用于将一个 list 中的元素转移至另外一个 list 的指定位置上

转换的方式有三种:

(1)将整个 list 进行转换

在这个转换的过程中,被用于转换的 list 会被清空

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list<int> list2 = { 7,8,9,10,11,12 };
	list1.splice(list1.begin(), list2);
	return 0;
}


(2)指定 list 中的一个元素进行转换,该元素由迭代器 i 指向

在这个转换的过程中,被用于转换的元素会在原来的 list 中被删除

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list<int> list2 = { 7,8,9,10,11,12 };
	list1.splice(list1.begin(), list2);
	return 0;
}


(3)指定 list 中一个区间内的值进行转换

区间需要由两个迭代器给出,分别表示 起始末尾区间是左闭右开的

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list<int> list2 = { 7,8,9,10,11,12 };
	list1.splice(list1.begin(), list2, list2.begin(), list2.end());
	return 0;
}

2.4.12 remove()

remove 的作用是 移除 list 中所有值等于 val 的结点

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,2,4,2,6 };
	list1.remove(2);
	return 0;
}

2.4.13 unique()


unique 的作用是 对 list 进行去重,将相等的多余的值进行移除前提是去重的 list 需要有序

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,2,2,2,6 };
	list1.unique();
	return 0;
}

如果去重的 list 无序,那么就不能完全去重

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,2,4,2,6 };
	list1.unique();
	return 0;
}

2.4.14 merge()


merge 的作用是将一个 list 合并到另外一个 list 中

合并时会将元素按序摆放,并且合并后,被合并的 list 会被清空

合并有两种方式:

(1)直接进行合并

直接进行合并时,默认是按照升序进行合并,此时要求两个 list 都为升序

cpp 复制代码
int main()
{
	list<int> list1 = { 1,2,3,4,5,6 };
	list<int> list2 = { 2,3,4,5,6 };
	list1.merge(list2);
	return 0;
}

(2)合并时,按照给定的比较方式进行合并

比较方式为 x > y ,那么就是 降序合并要求两个 list 都为降序

比较方式为 x < y ,那么就是 升序合并要求两个 list 都为升序

cpp 复制代码
bool comparison(int x, int y)
{
	return x > y;
}

int main()
{
	list<int> list1 = { 6,5,4,3,2,1 };
	list<int> list2 = { 6,5,4,3,2 };
	list1.merge(list2, comparison);
	return 0;
}

2.4.15 clear()

clear 的作用是 清空 list 中的元素,将 size() 置为 0

这个操作相当于释放了 list 中的结点,只留下了虚拟头结点

cpp 复制代码
int main()
{
	list<int> list1 = { 6,5,4,3,2,1 };
	list1.clear();
	return 0;
}

3 list 的优缺点

由于 list 的底层数据结构是带头结点的双向循环链表,所以它的优缺点就是该结构的优缺点:

优点:

  • 插入删除十分方便,时间复杂度低
  • 需要插入新的值时,直接创建新结点再链接即可,不需要进行扩容

缺点:

  • 不能随机访问,要查找值时,需要遍历一整个结构,效率低
  • 结点分散,容易造成内存碎片,空间利用率低

4 与 vector 的对比

对比 vector list
底层数据结构 动态顺序表 带头结点的双向循环链表
访问效率 支持使用下标随机访问,效率高 不支持使用下标随机访问,效率低
插入删除效率 尾插时可能原空间已满,需要开辟新空间,效率低,在头部和中间插入时,都需要将后方元素向后移动,时间复杂度是 O(N),尾删时,效率较高,不需要移动元素,在其他地方删除时,要将后方元素前移,时间复杂度是O(N) 插入删除只需要改动结点的指针即可,效率高,并且不需要进行扩容,每次插入只需要开一个结点的空间即可
空间利用率 空间连续,不会产生内存碎片,空间利用率高 由于内部是一个个不连续的结点,所以容易产生内存碎片,空间利用率低
迭代器 随机迭代器,支持进行 ++, - -,+,- 操作 双向迭代器,只支持 ++, - - 操作
迭代器失效时机 插入删除均会失效 只有删除时会失效
使用场景 插入删除频率低,访问频率高时使用 插入删除频率高时使用
相关推荐
小朩3 小时前
数据结构C语言
数据结构·c#·1024程序员节
大叔_爱编程3 小时前
基于随机森林算法的Boss直聘数据分析及可视化-hadoop+django+spider
hadoop·django·1024程序员节·spider·随机森林算法·boss直聘
GIS数据转换器3 小时前
城市基础设施安全运行监管平台
大数据·运维·人工智能·物联网·安全·无人机·1024程序员节
Y_Chime4 小时前
[特殊字符] YOLO 多模型连续训练时报错与 NaN 问题全解析(附完整解决方案)
1024程序员节
uxiang_blog4 小时前
C++进阶:继承
开发语言·c++
ʜᴇɴʀʏ4 小时前
Watch and Learn: Semi-Supervised Learning of Object Detectors from Videos
1024程序员节
檀越剑指大厂4 小时前
金仓多模数据库:电子证照系统国产化替代MongoDB的优选方案
1024程序员节
赵杰伦cpp4 小时前
数据结构——二叉搜索树深度解析
开发语言·数据结构·c++·算法
汤姆yu4 小时前
基于python机器学习的农产品价格数据分析与预测的可视化系统
1024程序员节·农产品价格预测·农产品分析