🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

一、list 是什么?先搞懂底层结构

二. list核心接口使用:必学的几个高频操作

  1、构造函数:初始化一个list

  2、迭代器:遍历 list 的 “指针”

  2.1 迭代器知识补充

总结

  3、容量与元素访问:判断空、查大小、取首尾

  4、元素修改:插入,删除,清空

  4、常用算法与操作:find、sort、unique、reverse、merge

三、list迭代器失效:只在删除时发生

四、list vs vector:该选哪个?

结束语


一、list 是什么?先搞懂底层结构

      list 的本质是双向循环链表
,且带有一个"哨兵位头结点"(不存储有效数据),结构如下:

  • 双向:每个字节包含前驱指针 (prev) 和后继指针 (next) ,支持向前,向后遍历;
  • 循环:尾节点的 next 指向头结点,头结点的 prev 指向尾结点,形成闭环;
  • 哨兵位头结点:避免插入/删除时判断"是否为空"”是否为头结点“的麻烦,简化代码逻辑。

      这种结构决定了 list 的核心特性:任意位置插入/删除效率高(O(1)),相较于前面所学的 vector 而言,vector 插入删除都需要挪动后续的数据,list 的插入删除效率就高很多了,但是 list 不支持随机访问(访问元素需要遍历,O(N))。

参考文档:list - C++ Reference

二. list核心接口使用:必学的几个高频操作

      list 的接口丰富,但重点掌握"构造,迭代器,容量,元素访问,修改’五大类即可满足日常开发需求,以下结合代码示例讲解。

  1、构造函数:初始化一个list

      list 提供4种常用构造方式,覆盖"空容器,n个相同元素,拷贝,区间初始化"场景:

构造函数接口说明代码示例
list()构造空 list

list<int> l1 (空链表,仅含头结点)

list(size_type n, const T& val = T())

构造含 n 个 val 的 list

list<int> l2(5, 3); (元素3,3,3,3,3)

list(const list& x)

拷贝构造

list<int> l3(l2); ( l3 是 l2 的副本)

list(InputIterator first, InputIterator last)

用 [first, last) 区间构造

int arr[] = {1,2,3};

list<int> l4(arr, arr+3); (元素:1,2,3)

代码演示:

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

void test_list1()
{
	//构造
	list<int> l1; //无参构造
	list<int> l2(5, 1);
	list<int> l3(l2); //拷贝构造
	//遍历打印(list不能再像vector用[]下标访问来打印,只能用迭代器)
	list<int>::iterator it = l2.begin();
	while (it != l2.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	for (auto e : l3)
	{
		cout << e << " ";
	}
	cout << endl;

}

int main()
{
	test_list1();
	return 0;
}

  2、迭代器:遍历 list 的 “指针”

      list 的迭代器我们就需要好好讲解一下了,因为不同于前面我们所学的 string 和 vector,list 的迭代器本质是 “结点指针的封装”,支持正向和反向遍历,核心接口如下(当然也可以使用范围for):

函数声明接口说明特点

begin() /end()

正向迭代器:begin() 指向 list 第一个有效元素,end() 指向头结点(尾后位置)

对迭代器执行 ++ 操作,迭代器向后移动,用于正向遍历所有有效元素

rbegin() /rend()

反向迭代器:rbegin() 指向 list 最后一个有效元素,rend() 指向头结点(反向尾后位置)

对迭代器执行 ++ 操作,迭代器向前移动,用于反向遍历所有有效元素

  2.1 迭代器知识补充

      在前面学习 string 和 vector 时我们讲了迭代器可以分为:iterator、reverse_iterator、const_iterator 和 const_reverse_iterator,reverse_iterator就是反向迭代器,const_iterator就是对应只读不写的对象,但其实这是按照功能的角度来进行的划分。
      按照性质迭代器也有分类:

  • 单向(forward_iterator):forward_list/unordered_map...
  • 双向(bidirectional_iterator):list/map/set...
  • 随机(random_access_iterator):vector/string/deque...

      所以按照性质来分类我们的 list 的迭代器就是属于双向迭代器,而前面所学的 string 和 vector 就是属于随机迭代器。那为什么要这样来分类呢?
      原因就在于这些容器底层结构的不同,从而决定了这些容器能使用哪些算法

      之前讲解算法的时候我们只是讲解了用法却没有注意观察这些算法的迭代器是什么样子的,我们以之前用到的 sort 为例:

      我们现在就会发现之前所用到的 sort 的迭代器就是随机迭代器(random_access_iterator),这其实就告诉了我们算法并不是所有容器都能使用的。
      以 sort 为例,我们之前讲过 sort 的底层其实就是快排,能实现快排的前提就是这个容器在物理上必须是连续的,而我们知道 list 是链表,链表在物理上不一定是连续的,所以 list 其实就不能使用 sort 来进行排序。

void test_list2()
{
	//迭代器
	list<int> l1;
	l1.push_back(1);
	l1.push_back(2);
	l1.push_back(3);
	l1.push_back(4);
	sort(l1.begin(), l1.end());
}


int main()
{
	test_list1();
	return 0;
}

      报错的原因就在于这个地方,因为 sort 涉及 - ,而 list 对应的双向迭代器是不支持 - 这个逻辑的:

      解释原因也能简单,在数据结构讲解 list 我们就说了,list 并不是在连续的空间里面而是随机空间存放数据然后用指针来指向,而两个指针相减得到的值应该表示为长度,对于 list 而言就没有意义了。

      那我们再以 之前讲过的逆置(reverse)为例:

      我们就会发现算法 reverse 所要求的迭代器就是双向迭代器,但是这就有一个问题了:之前我们讲 string 和 vector 的时候就使用过这个算法,可是上面我们提到了这两个容器的迭代器是随机迭代器,那为什么我们使用这个算法的时候没有出现问题呢?

      原因就在于这些迭代器是一种包含的关系

      是包含的关系也很好解释:对于单向迭代器(forward_iterator)而言能实现的逻辑就只有 ++,而双向迭代器(bidirectional_iterator)能实现的逻辑除了 ++ 还有 --,也就是说能用单向迭代器的算法双向迭代器照样也能用,但是反过来就不行了,就是因为用双向迭代器的算法可能会出现 -- 这种逻辑,而单向迭代器是不能实现的。而随机迭代器除了能实现 ++/-- 还能实现 +/- 的逻辑,所以这就是一种包含的关系。

      但除开这三种迭代器其实还有一种迭代器就是 input_iterator 和 output_iterator(一般不涉及),通过上面的箭头指向我们也就能大致猜到使用 input_iterator 迭代器的算法对于后面的所有迭代器都能实现

      而且通过观察 find 的函数模板我们也会发现只有 ++ 的逻辑,这也印证了包含的关系。

总结

      对迭代器知识的补充讲了这么多,首先是因为我们已经讲到了 list,并且发现 list 的迭代器和我们之前所学的有所不同,所有要对迭代器的分类进行讲解;其次是为了告诉大家以后在使用算法的时候就需要去观察一下自己的容器是否能使用这个算法

  3、容量与元素访问:判断空、查大小、取首尾

      list 不支持随机访问(没有 operator[] 和 at() ),仅提供“判断空、获取大小、取首尾元素”的接口:

函数声明接口说明代码示例(基于 lt = {1,2,3,4})

empty()

检测 list 是否为空,空则返回true,非空返回false

lt.empty(); (返回 false,因 lt 含4个 empty() 有效元素)

size()

返回 list 中有效元素的个数,单位为无符号整数(size_type)

lt.size(); (返回 4,对应 lt 中的元素 1、2、3、4)

front()

返回 list 第一个有效元素的引用,支持读写操作(可直接修改元素值)

lt.front() = 10; (修改后 lt 变为 {10, 2, 3, 4} )

back()

返回 list 最后一个有效元素的引用,支持读写操作(可直接修改元素值)

lt.back() = 40; (修改后 lt 变为 {10, 2, 3, 40} )

  4、元素修改:插入,删除,清空

      list 的核心优势在"修改操作",任意位置插入/删除仅需调整指针,效率极高,常用接口如下:

函数声明接口说明
push_front(const T& val)在 list 头部(第一个个有效元素之前)插入值为 val 的元素
pop_front()删除 list 的第一个有效元素
push_back(const T& val)在 list 尾部(最后一个有效元素之后)插入值为 val  的元素
pop_back()删除 list 的最后一个有效元素
insert(iterator pos, const T& val)在迭代器 pos 指向的位置之前插入值为 val  的元素,返回指向新插入元素的迭代器
erase(iterator pos)删除迭代器 pos 指向的元素,返回指向被删除元素下一个元素的迭代器
clear()清空 list 中所有有效元素(仅保留头结点),清空后 size() 为 0

代码演示:

void test_list3()
{
	//元素修改:插入,删除,清空
	list<int> l1({ 1, 2, 3 });
	//头插push_front
	l1.push_front(0);
	//尾插push_back
	l1.push_back(4);
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;

	//头删pop_front
	l1.pop_front();
	//尾删pop_back
	l1.pop_back();
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;

	//插入insert
	list<int>::iterator it1 = l1.begin();
	//l1.insert(it + 2, 10); //error
	//因为list的迭代器是双向迭代器,而双向迭代器是不支持 +/- 的操作,只支持 ++/--
	//所以不能是 it + 2 这种操作
	size_t k;
	cin >> k;
	while (k--)
	{
		it1++;
	}
	l1.insert(it1, 10);
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;

	//删除erase(和insert一样,删除中间某个位置数据只能通过循环++获取)
	list<int>::iterator it2 = l1.begin();
	size_t t;
	cin >> t;
	while (t--)
	{
		it2++;
	}
	l1.erase(it2);
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	test_list3();
	return 0;
}

  4、常用算法与操作:find、sort、unique、reverse、merge

函数类型函数声明 / 调用方式接口说明注意事项
算法函数(需包含 <algorithm>)find(iterator first, iterator last, const T& val)在[first, last) 区间查找值为 val 的元素,返回首个匹配元素的迭代器;若未找到,返回 last1. 属于 STL 通用算法,非 list 成员函数
2. 时间复杂度 O(N),需遍历查找
成员函数sort()对 list 元素进行升序排序(默认按 < 比较)1. list 不支持随机访问,不能用 STL 通用 sort 算法,需用自身成员函数
2. 底层通常为归并排序,时间复杂度 O(N log N)
成员函数unique()移除连续重复的元素(只保留第一个),返回指向最后一个有效元素后位置的迭代器1. 仅移除“连续重复”元素,需先 sort() 使相同元素相邻才能完全去重
2. 时间复杂度 O(N)
成员函数reverse()反转 list 中所有元素的顺序仅调整节点指针方向,时间复杂度 O(N),效率极高
成员函数merge(list& x);将两个链表合并在一起并重新排序两个链表本身是要求有序的

代码演示:

      find(iterator first, iterator last, const T& val)

void test_list4()
{
	//常用算法
	//find
	list<int> l1({ 1, 2, 3, 4, 5 });
	size_t k;
	cin >> k;
	auto pos = find(l1.begin(), l1.end(), k);
	if (pos != l1.end())
	{
		l1.erase(pos);
	}
	else
	{
		cout << "没有找到" << endl;
	}
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	test_list4();
	return 0;
}

      sort()

void test_list4()
{
	//常用算法
	//sort
	list<int> l2({ 5, 3, 1, 2, 4 });
	l2.sort();
	for (auto e : l2)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	test_list4();
	return 0;
}

      unique()

void test_list4()
{
	//常用算法
	//unique()
	list<int> l3({ 5, 1, 4, 3, 5, 1, 2, 2, 4, 1 });
	l3.sort();
	for (auto e : l3)
	{
		cout << e << " ";
	}
	cout << endl;
	l3.unique();
	for (auto e : l3)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	test_list4();
	return 0;
}

      reverse()

void test_list4()
{
	//常用算法
	//reverse
    list<int> l4({ 1, 2, 3, 4, 5 });
    l4.reverse();
    for (auto e : l4)
    {
    	cout << e << " ";
    }
    cout << endl;
}

int main()
{
	test_list4();
	return 0;
}

      merge(list& x);

void test_list4()
{
	//常用算法
	//merge
    list<int> l5({ 5, 3, 1, 2, 4 });
    list<int> l6({ 7, 9, 6, 10, 8 });
    l5.sort();
    l6.sort();
    l5.merge(l6); //此时是把l6的数据合并到l5,l6变为空链表
    for (auto e : l5)
    {
    	cout << e << " ";
    }
    cout << endl;
    for (auto e : l6)
    {
    	cout << e << " ";
    }
    cout << endl;
}

int main()
{
	test_list4();
	return 0;
}

三、list迭代器失效:只在删除时发生

      前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效迭代器所指向的节点的无效,即该节点被删除了。因为 list 的底层结构为带头结点的双向循环链表,因此在 list 中进行插入时是不会导致 list 的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
迭代器失效是使用 list 时的核心注意点,但其失效规则比 vector 简单得多:

  • 插入时: list 插入仅需调整指针指向的位置,不会移动现有的节点,因此所有迭代器都不会导致失效
  • 删除时: 仅指向 “被删除节点的迭代器失效,其他迭代器(指向未删除节点)不受影响。
void test_list4()
{
	list<int> l1({ 1, 2, 3, 4, 5 });
	size_t k;
	cin >> k;
	auto pos = find(l1.begin(), l1.end(), k);
	if (pos != l1.end())
	{
		//l1.erase(pos);//pos失效了,pos 就无法再次访问了,不要这样写
		pos = l1.erase(pos); //给 pos 重新赋值,此时 pos 指向的是删除数据的下一个数据
	}
	else
	{
		cout << "没有找到" << endl;
	}
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << *pos << endl;
}

int main()
{
	test_list4();
	return 0;
}

四、list vs vector:该选哪个?

      list 和 vector 是 STL中最常用的两个序列容器,但适用场景完全不同,核心差异如下表:

差异vectorlist
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素效率O(1)不支持随机访问,访问某个元素效率O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针(节点指针)进行封装
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,导致原来迭代器失效,删除时,当前迭代器也需要重新赋值否则会失效插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

选择建议:

  • 若需 “快速查改元素”(如数组下标访问)、数据量固定或尾插为主 → 选 vector
  • 若需 “频繁在中间插入 / 删除”(如链表排序、队列实现)、无需随机访问 → 选 list

结束语

      到此,list 的常见相关接口的使用以及注意事项就讲解完了,在使用上 list 相比前面学习的 string 和 vector 是比较简单的,但 list 难在模拟实现,后面我会对 list 的模拟实现进行详细的讲解。望这篇文章对大家学习C++能有所帮助!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

更多推荐