相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓
目录
✨说在前面
🍋知识点一:什么是list?
•🌰1.list的定义
•🌰2.list的基本特性
•🌰3.常用接口介绍
🍋知识点二:list常用接口
•🌰1.默认成员函数
🔥构造函数(⭐)
🔥析构函数
•🌰2.list对象的访问和遍历操作
🔥front、back
🔥迭代器(⭐)
•🌰3.list对象的容量操作
🔥empty
🔥size
•🌰4.list对象的修改操作
🔥push_back、pop_back
🔥push_front、pop_front
🔥insert(⭐)
🔥reverse
🔥merge
🔥sort(⭐)
🔥unique
🔥remove、remove_if
🔥splice
•🌰5.list的模拟实现
• ✨SumUp结语
✨说在前面
亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇文章我给大家介绍了一下vector的定义、常用接口以及模拟实现。如果大家没有掌握好相关的知识,上一篇篇文章讲解地很详细,可以再回去看看,复习一下,再进入今天的内容。
我们今天简单给大家讲解一下STL中的一员——list。ilst对应C语言中的链表,也是STL标准库中的一大容器。如果大家准备好了,那就接着往下看吧~
👇👇👇
💘💘💘知识连线时刻(直接点击即可)【C++】_string类字符串万字详细解析
【C++】_vector定义、_vector常用方法解析
🎉🎉🎉复习回顾🎉🎉🎉
博主主页传送门:愿天垂怜的博客
🍋知识点一:什么是list?
•🌰1.list的定义
在C++中,list是一个双向链表容器,它允许在序列的任何位置进行快速的插入和删除操作。与vector(基于数组的连续存储容器)不同,list的元素在内存中不是连续存储的,而是通过指针(或引用)相互连接。这种结构使得list在进行元素插入和删除时不需要移动其他元素,从而提高了这些操作的效率。
我们来看看文档中list的定义:list的文档介绍
那么list的底层是如何实现的呢?
list的底层实现是一个双向带头循环链表。每个节点(node)包含三个部分:
- 数据部分:存储元素的值。
- 指向前一个节点的指针:允许从后向前遍历链表。
- 指向下一个节点的指针:允许从前向后遍历链表。
这种结构使得list支持高效的插入和删除操作,因为你可以直接修改指针来添加或删除节点,而不需要移动其他元素。然而,由于元素在内存中的非连续存储,list的随机访问(如通过索引直接访问元素)效率较低,因为需要从头或尾开始遍历链表直到找到目标元素。
•🌰2.list的基本特性
🔥动态大小
list的大小可以动态地增长和缩小。你可以随时向其中添加或删除元素,而不需要担心容器的容量限制。
🔥非连续存储
与vector不同,list的元素在内存中不是连续存储的。每个元素都是一个节点,节点之间通过指针(或引用)相互连接。这种非连续存储的特性使得list在进行插入和删除操作时不需要移动其他元素。
🔥双向遍历
list提供了双向遍历的能力。每个节点都包含指向前一个节点和下一个节点的指针(或引用),这使得你可以轻松地从头遍历到尾,或者从尾遍历到头。
🔥高效的插入和删除
由于list的非连续存储和双向遍历特性,它在序列的任何位置插入或删除元素都是高效的。这些操作通常只需要修改几个指针,而不需要移动其他元素。
🔥不支持随机访问
尽管list提供了灵活的插入和删除操作,但它不支持通过索引直接访问元素。这是因为元素在内存中的位置不连续,无法直接通过索引计算出元素的地址。相反,你需要从头或尾开始遍历链表,直到找到目标元素。
🔥迭代器失效
在list中进行插入或删除操作时,只有指向被插入或删除元素本身的迭代器会失效。其他迭代器(包括指向其他元素的迭代器)仍然保持有效。这与vector不同,后者在插入或删除元素时可能会使所有指向该元素之后元素的迭代器失效。
🔥内存分配
由于list的每个节点都是单独分配的,因此它可能会比vector消耗更多的内存(因为每个节点都需要额外的空间来存储指针)。然而,这种内存分配方式也使得list在处理大量小对象时更加灵活和高效。
🔥排序和搜索
尽管list不支持随机访问,但它仍然提供了排序(如std::sort)和搜索(如std::find)算法。这些算法通过遍历链表来工作,因此它们的效率可能低于在随机访问容器(如vector)上执行的相同算法。
•🌰3.常用接口介绍
🔥list对象的常见构造
构造函数( (constructor)) | 接口说明 |
list (size_type n, const value_type& val =
value_type())
|
构造的
list
中包含
n
个值为
val
的
元素
|
list() | 构造空的list |
list (const list& x) | 拷贝构造函数 |
list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造list |
🔥list iterator的使用
函数声明 | 接口说明 |
begin
+
end
| 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器 |
rbegin
+
rend
|
返回第一个元素的
reverse_iterator,
即
end
位置
,
返回最后一个元素下一个位
置的
reverse_iterator,
即
begin
位置
|
注意:
1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
2. rbegin(end)于rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
🔥list capacity
函数声明 | 接口说明 |
empty | 检测list是否为空,是返回true,否则返回false |
size | 返回list中有效节点的个数 |
🔥list element access
函数声明 | 接口说明 |
front | 返回list的第一个节点中值的引用 |
back | 返回list的最后一个节点中值的引用 |
🔥list modifiers
函数声明 | 接口说明 |
push_front | 在list首元素前插入值为val的元素 |
pop_front | 删除list中第一个元素 |
push_back | 在list尾部插入值为val的元素 |
pop_back | 删除list中最后一个元素 |
insert | 在list position 位置中插入值为val的元素 |
erase | 删除list position位置的元素 |
swap | 交换两个list中的元素 |
clear | 清空list中的有效元素 |
list中还有一些操作,需要用到时大家可参阅list的文档说明。
🍋知识点二:list常用接口
•🌰1.默认成员函数
🔥构造函数(⭐)
接口如下,前面也有,大家再仔细看看:
在文档中我们可以查看它们的具体用法:
我们大家也需要学会查看英文文档,有不懂的就去查,锻炼我们查看英文文章的能力。
代码示例如下:
#include <iostream>
using namespace std;
#include <list>
int main()
{
//test_list1();
list<int> first;
list<int> second(4, 100);
list<int> third(second.begin(), second.end());
list<int> fourth(third);
int myints[] = { 16,2,77,29 };
list<int> fifth(myints, myints + sizeof(myints) / sizeof(int));
return 0;
}
有效list数据如下:
100 100 100 100
100 100 100 100
100 100 100 100
16 2 77 29
以上这些就是常规操作。那如果我们要遍历它呢?由于list不支持通过索引直接访问元素,所以有两种方式:
1. 利用迭代器
void print_list(const list<int>& lt)
{
//迭代器
list<int>::const_iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
2. 利用范围for
void print_list(const list<int>& lt)
{
//范围for
for (auto& e : lt)
{
cout << e << " ";
}
cout << endl;
}
🔥析构函数
析构函数直接用编译器默认生成、调用的就可以了,非常简单~
•🌰2.list对象的访问和遍历操作
🔥front、back
【front】和【back】是两个非常常用的成员函数,它们分别用于访问list容器中的第一个元素和最后一个元素。
front
【front】用于访问list中的第一个元素。调用【front】成员函数时,它返回对列表中第一个元素的引用。如果list为空(即没有任何元素),那么调用【front】会导致未定义行为(通常是程序崩溃),因此在使用之前应该检查list是否为空。
【front】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 1, 2, 3, 4, 5 };
if (!myList.empty())
{
cout << "The first element is: " << myList.front() << endl;
}
else
{
cout << "The list is empty." << endl;
}
return 0;
}
back
与【front】类似,【back】用于访问list中的最后一个元素。调用【back】成员函数时,它返回对列表中最后一个元素的引用。同样地,如果list为空,则调用篇【back】会导致未定义行为。
【back】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 1, 2, 3, 4, 5 };
if (!myList.empty())
{
cout << "The last element is: " << myList.back() << endl;
}
else
{
cout << "The list is empty." << endl;
}
return 0;
}
🔥迭代器(⭐)
C++中的迭代器(Iterator)是一种允许你访问容器中元素的对象,而无需暴露容器的内部结构。迭代器提供了一种统一的方法来遍历容器中的所有元素,无论容器的具体类型如何(如数组、向量vector列表list等)。通过使用迭代器,你可以读取、写入或删除容器中的元素,而无需关心容器的具体实现细节。
🔥list iterator的使用
函数声明 | 接口说明 |
begin
+
end
| 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器 |
rbegin
+
rend
|
返回第一个元素的
reverse_iterator,
即
end
位置
,
返回最后一个元素下一个位
置的
reverse_iterator,
即
begin
位置
|
注意:
1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
2. rbegin(end)于rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
3. 在list中,迭代器并不是原生指针,不能通过+或-来访问对应的节点元素。
//不能直接加
it = lt.begin();
lt.erase(it + 3);
在这里,我们可以对迭代器进行更加深层次的总结:
按照功能划分
按照迭代器的功能,我们可以将迭代器的类型分为iterator、reverse_iterator、const_iterator和const_reverse_iterator,我们再之前的string和vector中都了解过他们的功能。
按照性质划分
按照迭代器的性质,我们可以将迭代器分为单向迭代器、双向迭代器和随机迭代器。
1、单向迭代器
单向迭代器可以向前遍历容器中的元素,但只能++它们来访问下一个元素。你不能通过单向迭代器回退(即,你不能使用--运算符)。单向迭代器至少满足输入迭代器的所有要求,并添加了能够递增自身的能力。
单向迭代器的有:forward_list、unordered map/set...
2、双向迭代器
双向迭代器比单向迭代器更强大,因为它们可以向前遍历容器(递增),还可以向后遍历(递减)。这意味着你可以使用++和--运算符来遍历容器中的元素。
双向迭代器的有:list、map、set...
3、随机迭代器
随机访问迭代器是最强大的迭代器类型,它们支持上述所有操作,并增加了随机访问容器元素的能力。这意味着你可以使用迭代器算术来访问或比较容器中的元素。
随机迭代器的有:vector、string、deque...
也就是说,迭代器的性质是由对应数据结构的底层结构所决定的。
•🌰3.list对象的容量操作
list对象的容量操作比较简单,我们介绍一下三个:
或如下:
函数声明 | 接口说明 |
empty | 检测list是否为空,是返回true,否则返回false |
size | 返回list中有效节点的个数 |
🔥empty
【empty】是list容器的一个成员函数,用于检查该容器是否为空。
【empty】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList;
if (myList.empty())
{
cout << "List is empty." << endl;
}
else
{
cout << "List is not empty." << endl;
}
myList.push_back(10);
myList.push_back(20);
if (myList.empty())
{
cout << "List is empty." << endl;
}
else {
cout << "List is not empty." << endl;
}
return 0;
}
运行结果:
List is empty.
List is not empty.
🔥size
【size】是一个非常重要的成员函数,它用于返回容器中元素的数量。这是一个只读操作,其时间复杂度为常数时间O(1),因为list内部通常维护了一个表示元素数量的计数器。
【size】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 1, 2, 3, 4, 5 };
cout << "The size of myList is: " << myList.size() << endl;
myList.push_back(6);
cout << "After adding an element, the size of myList is: "
<< myList.size() << endl;
return 0;
}
输出将会是:
The size of myList is: 5
After adding an element, the size of myList is: 6
•🌰4.list对象的修改操作
🔥push_back、pop_back
push_back
【push_back】用于在list的末尾添加一个元素。这个操作的时间复杂度是常数时间O(1),因为它只是简单地修改链表末尾的节点来指向新添加的节点,并更新链表的尾部指针。
【push_back】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList;
myList.push_back(10);
myList.push_back(20);
for (int n : myList)
{
cout << n << " ";
}
cout << endl;
return 0;
}
注意:在list的成员函数中,有一个叫作【emplace_back】的成员函数。它的功能类似于【push_back】,但是它们有本质上的区别。
在我们现在的阶段,暂时理解一下的区别即可:
struct A
{
public:
A(int a1 = 1, int a2 = 2)
:_a1(a1)
,_a2(a2)
{
cout << "A(int a1 = 1, int a2 = 2)" << endl;
}
int _a1;
int _a2;
A(const A& aa)
:_a1(aa._a1)
,_a2(aa._a2)
{
cout << "A(const A& aa)" << endl;
}
};
void test_list2()
{
list<int> lt1;
lt1.push_back(1);
lt1.emplace_back(2);
lt1.emplace_back(3);
lt1.emplace_back(4);
lt1.emplace_back(5);
print_list(lt1);
list<A> lt2;
//有名对象
A aa1(1, 2);
lt2.push_back(aa1);
//匿名对象
lt2.push_back(A(1, 2));
//隐式类型转换
lt2.push_back({ 1, 2 });
//而emplace back支持直接传构造A对象的参数
lt2.emplace_back((1, 2));
}
pop_back
【pop_back】用于移除list的最后一个元素。与【push_back】一样,这个操作的时间复杂度也是常数时间O(1)。它简单地删除链表末尾的节点,并更新链表的尾部指针。
【pop_back】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList;
myList.push_back(10);
myList.push_back(20);
if (!myList.empty())
{
myList.pop_back();
}
for (int n : myList)
{
cout << n << ' ';
}
return 0;
}
🔥push_front、pop_front
push_front
【push_front】用于在list的开头添加一个元素。这个操作的时间复杂度是常数时间O(1),因为它只是简单地创建一个新节点,将其指向当前列表的第一个节点,并更新列表的头部指针以指向这个新节点。
【push_front】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList;
myList.push_front(10);
myList.push_front(20);
for (int n : myList)
{
cout << n << " ";
}
return 0;
}
pop_front
【pop_front】用于移除list的第一个元素。与【push_front】一样,这个操作的时间复杂度也是常数时间O(1)。它简单地删除列表的第一个节点,并更新列表的头部指针以指向下一个节点。
【pop_front】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList;
myList.push_front(10);
myList.push_front(20);
if (!myList.empty())
{
myList.pop_front();
}
for (int n : myList)
{
std::cout << n << " ";
}
return 0;
}
🔥insert(⭐)
🔥reverse
【reverse】没有参数,也不返回任何值。它的作用是就地(in-place)反转list中元素的顺序。这意味着原始容器被直接修改,而不是创建一个新的反转后的容器。
【reverse】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 1, 2, 3, 4, 5 };
cout << "原始list: ";
for (int num : myList)
{
cout << num << " ";
}
cout << endl;
myList.reverse();
cout << "反转后的list: ";
for (int num : myList)
{
cout << num << " ";
}
cout << endl;
return 0;
}
输出将会是:
原始list: 1 2 3 4 5
反转后的list: 5 4 3 2 1
🔥merge
【merge】用于合并两个已排序的list容器。与通用【merge】算法(定义在<algorithm>头文件中,用于合并两个已排序的范围)不同,list的【merge】成员函数是专门为链表设计的,并且它直接在原链表上操作,而不需要额外的存储空间来存储合并后的结果。
【merge】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> list1 = { 1, 3, 5 };
list<int> list2 = { 2, 4, 6 };
//合并list1和list2,list2的内容将被移动到list1中,且list2将变为空
list1.merge(list2);
for (int num : list1)
{
std::cout << num << " ";
}
std::cout << std::endl; //输出: 1 2 3 4 5 6
// list2现在是空的
if (list2.empty())
{
std::cout << "list2 is empty after merge." << std::endl;
}
return 0;
}
注意:
1. 在调用【merge】之前,两个list必须已经是有序的。如果它们不是有序的,则合并后的结果将不是有序的。
2. 【merge】函数会修改调用它的list对象,并清空另一个list对象(other)。
🔥sort(⭐)
list是一个双向链表,它不支持随机访问迭代器,因此不能直接使用通用【sort】算法进行排序。但是list提供了自己的【sort】成员函数,该函数使用链表特有的排序算法(如归并排序或某种形式的插入排序),以就地(in-place)方式对链表中的元素进行排序。
【sort】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 4, 1, 3, 5, 2 };
myList.sort();
for (int num : myList)
{
cout << num << " ";
}
cout << endl;
return 0;
}
与通用【sort】对比
1. 适用容器
通用【sort】算法适用于支持随机访问迭代器的容器(如vector、deque),而list的【sort】成员函数仅适用于list。
2. 性能
对于list,使用其【sort】成员函数通常比尝试使用通用【sort】算法更高效,因为list的【sort】成员函数利用了链表的结构特性。然而,对于支持随机访问的容器,通用【sort】算法通常能提供更快的排序速度。
3. 稳定性
两者都提供稳定的排序,即相等元素的相对顺序在排序后保持不变。
4. 用法
通用【sort】算法需要指定排序范围的开始和结束迭代器,而list的【sort】成员函数则不需要,因为它直接作用于整个链表。此外,通用【sort】算法允许指定自定义的比较函数或函数对象,而list的【sort】成员函数也支持这一点,但通常是通过成员函数模板的重载来实现的。
测试算法库中【sort】和list中【sort】的性能:
//测试算法库中sort和list中sort的性能
void test_op1()
{
srand((unsigned int)time(nullptr));
const int N = 1000000;
list<int> lt;
vector<int> v;
for (size_t i = 0; i < N; i++)
{
auto e = rand() + i;
lt.push_back(e);
v.push_back(e);
}
//排序
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt.sort();
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("list1 sort:%d\n", end2- begin2);
}
在Release版本下的其一运行结果为:
vector sort:61
list sort:128
再看另外一个代码:
void test_op2()
{
srand(time(0));
const int N = 1000000;
list<int> lt1;
list<int> lt2;
for (int i = 0; i < N; i++)
{
auto e = rand() + i;
lt1.push_back(e);
lt2.push_back(e);
}
int begin1 = clock();
//拷贝vector
vector<int> v(lt2.begin(), lt2.end());
//排序
sort(v.begin(), v.end());
//拷贝回lt2
lt2.assign(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt1.sort();
int end2 = clock();
printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
其中lt2先拷贝到vector上用库里的【sort】进行排序,然后lt1依然调用自身的【sort】,我们来看其中一个运行结果:
list copy vector sort copy list sort:82
list sort:191
显然在数据量比较大的时候,list尽量不要用它自己的【sort】进行排序。
🔥unique
【unique】用于移除容器中连续重复的元素,只保留每个元素组中的第一个元素。注意,这里的“重复”是指相邻元素的相等性,而不是在整个容器范围内的唯一性。【unique】函数通过比较相邻元素来工作,如果两个相邻元素相等,则删除第二个元素。
【unique】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 1, 2, 2, 3, 4, 4, 4, 5 };
myList.unique();
for (int num : myList)
{
cout << num << " ";
}
cout << endl; //输出: 1 2 3 4 5
return 0;
}
remove
🔥remove、remove_if
【remove】接受一个值作为参数,并移除容器中所有等于该值的元素。它使用元素的==运算符来比较元素与给定值。
【remove】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 1, 2, 3, 2, 4, 2, 5 };
myList.remove(2);
for (int num : myList)
{
cout << num << " ";
}
cout << endl;//输出: 1 3 4 5
return 0;
}
remove_if
如果你希望删除的不是某个特定值的元素,而是满足某个条件的元素,此时就可以使用【remove_if】。【remove_if】接受一个谓词(即一个返回布尔值的函数或函数对象)作为参数,并移除容器中所有使该谓词返回true的元素。
【remove_if】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> myList = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
//这里使用了lambda表达式作为谓词
myList.remove_if([](int i) { return i <= 5; });
for (int num : myList)
{
cout << num << " ";
}
cout << endl;//输出: 6 7 8 9
return 0;
}
🔥splice
【splice】用于合并两个list容器中的部分或全部元素,同时保持元素的相对顺序不变。与list的其他成员函数相比,【splice】函数的一个显著优点是它可以在不复制或移动元素的情况下重新排列元素,这就更加高效了,特别是对于大型容器或包含复杂对象的容器。
【splice】使用示例:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> list1 = { 1, 2, 3, 4, 5 };
list<int> list2 = { 6, 7, 8, 9, 10 };
//将list2的所有元素移动到list1的开头
auto it = list1.begin();
list1.splice(it, list2);
//输出处理后的list1
for (int num : list1)
{
cout << num << " ";
}
cout << endl;//输出: 6 7 8 9 10 1 2 3 4 5
return 0;
}
【splice】不仅可以将一个链表的节点转移到另一个链表,也可以调整当前链表的顺序。比如我们想将[1,2,3,4,5,6]中的某个元素后面的节点移动到最前面,我们可以:
#include <iostream>
using namespace std;
#include <list>
int main()
{
list<int> lt = { 1, 2, 3, 4, 5, 6};
int x = 0;
cin >> x;
list<int>::iterator it = find(lt.begin(), lt.end(), x);
if (it != lt.end())
{
lt.splice(lt.begin(), lt, it, lt.end());
}
return 0;
}
list2现在是空的,因为没有元素被复制或移动到list1,而是被重新链接到了list1上。
•🌰5.list的模拟实现
list这块的源代码可以参考:list的源码
源代码看明白是有些难度的,我们可以结合这一篇文章,再参考一下我写的模拟实现:list模拟实现
• ✨SumUp结语
到这里本篇文章的内容就结束了,本节介绍了C++中list的相关知识。这里的内容虽然很熟悉了,毕竟我们有了string和vector的基础,但是有一定的难度。希望大家能够认真学习,打好基础,迎接接下来的挑战,期待大家继续捧场~💖💖💖