list是什么?
std::list
是 C++ 标准库中的一个带哨兵位双向循环链表容器。与vector
连续内存存储的容器不同,list
使用节点(node)来存储每个元素,这些节点通过指针相互链接形成链表结构。每个节点包含一个数据元素以及两个指针,分别指向前一个和后一个节点,因此可以高效地在链表的任意位置进行插入和删除操作。如果需要了解更加详细list底层和成员函数用法可参考:cplusplus.com/reference/list/list/
该容器具备链表本质的特点:每个节点的动态空间都是独立的,不一定连续!
list结构
为更直观的观察list的结构可以结合图形理解:
结合上图可以看出每个节点装着:_prev,_data,_next。而在库中是用结构体封装起来的,大致如下:
template<typename T>
struct ListNode {
ListNode* _prev; // 指向前一个节点的指针
ListNode* _next; // 指向下一个节点的指针
T _data; // 存储的数据
ListNode(const T& data = T()) : _prev(nullptr), _next(nullptr), _data(data) {}
};
图形表示一个节点的结构:
注:哨兵位头节点,也是这个结构只是里面_data不装数据(无效数据)
list由1个或者多个节点链接的链表:
list使用
如果学习过string和vector的小伙伴,那么对list的成员函数应该会感到熟悉,我们先来看啊可能一个见到的list使用实例:
#include <iostream>
#include <list> //使用list需要包含的头文件
using namespace std;
int main() {
// 创建一个空的list容器,类型为int
list<int> myList;
// 使用push_back向list的末尾添加元素(尾插)
myList.push_back(10);
myList.push_back(20);
myList.push_back(30);
// 使用push_front向list的头部添加元素(头插)
myList.push_front(0);
// 使用insert在指定位置插入元素
list<int>::iterator it = myList.begin();
++it; // 指向第二个元素
myList.insert(it, 5); // 在第二个位置插入5
// 范围for 打印list中的所有元素
cout << "myList: ";
for (int value : myList)
{
cout << value << " ";
}
cout << endl;
// 使用erase删除指定位置的元素
it = myList.begin();
++it; // 指向第二个元素
myList.erase(it); // 删除第二个元素
// 再次打印list
cout << "myList after erase(it): ";
for (int value : myList)
{
cout << value << " ";
}
cout << endl;
// 使用pop_back删除末尾的元素(尾删)
myList.pop_back();
// 使用pop_front删除头部的元素(头删)
myList.pop_front();
// 打印list
cout << "myList after popping : ";
for (int value : myList)
{
cout << value << " ";
}
cout << endl;
// 使用size获取list的大小并打印
cout << "List size: " << myList.size() << endl;
// 使用clear清空list中的所有元素
myList.clear();
cout << " clear(); size()= " << myList.size() << endl;
return 0;
}
虽然 list的底层数据结构和vector相比有着翻天覆地的区别,但是在成员函数的使用上是非常类似的,这也归功于C++的前辈们刻意把容器的风格设计的很类似。
现在结合图形来逐步分析这段代码:
1.创建myList
在初始化一个空list时,已经生成了头节点(哨兵位)此时哨兵位的_prev和_next都存自己(_head)的地址
2.尾插
三次push_back(),将 10
, 20
, 30
分别插入到 myList
的尾部。
// 使用push_back向list的末尾添加元素(尾插)
myList.push_back(10);
myList.push_back(20);
myList.push_back(30);
我们只需要调用push_back() 就可以,push_back函数内部实现会让尾插元素的_prev存前一个节点的地址,_next存后一个地址。
4.头插
使用 push_front()
将 0
插入到 myList
的头部。
// 使用push_front向list的头部添加元素(头插)
myList.push_front(0);
图形表示:
从图中可以直观地看到_data=10的节点 原本_prev指向_head,现在新的节点插入,指向改变了,_head节点的_next也改变,指向新的节点。
改变之后第一个节点变成新插入的_data=0的节点。
但这些都是push_front()
函数的实现,我们只需要调用push_front(0);就可以做到头插。
5.位置插入
首先,定义了一个 list<int>::iterator
类型的迭代器 it
,并将其指向 myList
的第一个元素。接着,使用 ++it
将迭代器移动到第二个元素的位置。最后,使用 insert(it, 5)
在迭代器 it
指向的位置之前插入 5
,即插入到链表的第二个位置。
// 使用insert在指定位置插入元素
list<int>::iterator it = myList.begin();
++it; // 指向第二个元素
myList.insert(it, 5); // 在第二个位置插入5
先看看list的迭代器:
看懂了迭代器的begin()和end()的位置,在这里的插入和头插尾插也是同样的操作:
6.遍历打印
使用范围 for
循环遍历 myList
中的每一个元素,并打印出来,输出结果为 myList: 0 5 10 20 30
。这里迭代器从使用的角度来看和vector是一样的,但是由于list不是顺序表的原因,list的迭代器底层的实现和vector是不一样的!!因为list不是连续的空间所以list的迭代器不支持 begin()+5 类似的操作,会导致非法访问的问题!!!(具体迭代器细节在下一张详细介绍)
// 范围for 打印list中的所有元素
cout << "myList: ";
for (int value : myList)
{
cout << value << " ";
}
cout << endl;
遍历打印,和vector中的用法一样。
7.删除操作
再次将迭代器 it
指向 myList
的第一个元素,然后移动到第二个元素的位置。使用 erase(it)
删除 it
指向的第二个元素,即删除 5
。
删除的函数代码使用 :
// 使用erase删除指定位置的元素
it = myList.begin();
++it; // 指向第二个元素
myList.erase(it); // 删除第二个元素
图形表示:
删除函数调用很简单。在底层的也不复杂,看图形可直观的看到,把前面的节点(_data=0)的_next指向后一个节点,后一个节点(_data=10)的_prev指向前一个节点,再释放要删除位置节点就好。
8.尾删和头删操作
使用 pop_back()
删除链表末尾的元素 30
,然后使用 pop_front()
删除链表头部的元素 0
。
// 使用pop_back删除末尾的元素(尾删)
myList.pop_back();
// 使用pop_front删除头部的元素(头删)
myList.pop_front();
图形表示:(标画太丑了见谅)
删除的操作和7.是一样的,只不过 pop_back()删除最后一个节点,pop_front()删除第一个。
注意:这里的pop_front()删除第一个节点是指有效数据的第一个,在所有对list的操作中哨兵位都是不动的,只有析构的时候才会释放。
9.
清空链表
使用 clear()
函数清空 myList
中的所有元素,然后再次使用 size()
函数获取链表的大小,并打印出来,输出结果为 clear(); size()= 0
。
// 使用clear清空list中的所有元素
myList.clear();
cout << " clear(); size()= " << myList.size() << endl;
因为size()和之前vector的用法一样,所以这里只简述,需要注意的是size()获取的依然是有效数据的大小 ,换言之就是不算哨兵位!!
清除函数也一样把除哨兵位之外的所有有效数据清除。
list 迭代器
list 迭代器分为:(同vector)
iterator
:普通迭代器,用于读取和修改元素。const_iterator
:常量迭代器,只能读取元素,不能修改。reverse_iterator
:反向迭代器,从链表末尾向前遍历。const_reverse_iterator
:常量反向迭代器,只能读取元素,不能修改。
这里只作普通迭代器的介绍,其他的可自己翻阅开头网站,理解普通迭代器,其他3种看看就懂。
list
中的普通迭代器(iterator
)是一个双向迭代器,允许我们顺序访问链表中的元素。
双向迭代器不能通过加减整数值的方式直接跳转到链表中的特定位置。例如,在 vector
中,迭代器可以通过 it + n
直接访问容器中的第 n
个元素,而在 list
中是不可能的。
为什么说它是双向迭代器,不是随机访问迭代器呢?
看上图,图中拿到第一个节点的地址,有了第1个节点的地址通过_next和_prev只能访问前一个和后一个元素,而不能直接跳到链表中的任意位置。
再看vector:
可通过下标随机访问元素,因为vector是存储数据的是连续的地址。
list的iterator 支持什么操作?怎么使用呢?
其实也非常简单。直接看代码吧
#include <iostream>
#include <list> //使用list需要包含的头文件
using namespace std;
int main() {
// 初始化 list 容器
list<int> myList = { 10, 20, 30 };
// 获取指向第一个元素的迭代器
list<int>::iterator it = myList.begin();
// 遍历链表并打印每个元素
cout << "1 myList : ";
for (list<int>::iterator it = myList.begin(); it != myList.end(); ++it) //支持前后置++ ,--
{
cout << *it << " "; //支持解引用取值
}
cout << endl;
// 双向移动,移动到链表的最后一个元素
it = myList.end(); //之前说过end()是指向哨兵位,哨兵位的前一个就是最后一个元素了
--it; // 移动到最后一个元素
cout << "Last : " << *it << endl;
// 访问并修改第一个元素
it = myList.begin();
*it = 25; // 修改当前元素的值为 25
cout << "*it = " << *it << endl;
// 再次遍历链表并打印修改后的元素
cout << "2 myList: ";
for (it = myList.begin(); it != myList.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
return 0;
}
输出结果:
1 myList : 10 20 30
Last : 30
*it = 25
2 myList: 25 20 30
从使用角度来看list的迭代器是非常简单的,这都是C++各位前辈大神们的功劳阿。
下面是list的iterator的底层实现(不同编译器不同 仅参考,感兴趣可以看看)
template <typename T>
struct ListNode {
T data;
ListNode* prev;
ListNode* next;
};
template <typename T>
class ListIterator {
private:
ListNode<T>* node; // 指向链表节点的指针
public:
// 重载解引用操作符,返回当前节点的数据
T& operator*() {
return node->data;
}
// 前置++运算符,移动到下一个节点
ListIterator& operator++() {
node = node->next;
return *this;
}
// 后置++运算符
ListIterator operator++(int) {
ListIterator temp = *this;
node = node->next;
return temp;
}
// 前置--运算符,移动到前一个节点
ListIterator& operator--() {
node = node->prev;
return *this;
}
// 判断两个迭代器是否相等
bool operator==(const ListIterator& other) const {
return node == other.node;
}
// 判断两个迭代器是否不相等
bool operator!=(const ListIterator& other) const {
return node != other.node;
}
};
list和vector使用差异
1.list不支持随机访问
vector
:底层是一个动态数组,所有元素在内存中是连续存储的。访问任何一个元素都可以在常数时间内完成(O(1)),因为可以通过索引直接访问。
list
:底层是一个双向链表,每个元素存储在独立的节点中,节点通过指针相连。元素在内存中不必连续存储,插入和删除操作在链表的任意位置都可以在常数时间内完成(O(1)),但访问元素时需要线性时间(O(n)),因为必须从头或尾开始遍历。
所以:
list
:不支持随机访问,只能通过迭代器遍历访问元素。vector
:支持随机访问,可以使用索引[]
或at()
函数直接访问任意位置的元素。
2.list不需要容量管理
list
:不提供容量管理相关的函数,因为链表不需要连续的内存空间。vector
:提供容量管理函数,如capacity()
和reserve()
,可以预先分配内存以减少后续插入时的重新分配次数。
3.list不能用std::sort()
list
:因为链表不支持随机访问,不能直接使用std::sort()
,但list
提供了自己的成员函数sort()
,可以原地对链表进行排序。vector
:标准库提供std::sort()
函数来排序vector
中的元素。
std::list
不能直接使用 std::sort()
函数,如果尝试对 std::list
容器使用 std::sort()
,编译器将会报错。
原因:
std::sort()
函数要求随机访问迭代器:std::sort()
是专门为具有随机访问迭代器的容器设计的,如vector
、deque
等。这些容器的迭代器支持使用算术运算符(如+
、-
)进行直接的偏移访问,从而实现高效的排序操作。
list
的迭代器是双向迭代器:std::list
使用双向链表作为底层结构,其迭代器只能向前或向后遍历,无法通过算术运算符直接跳转到任意位置,因此不满足std::sort()
的要求。
所以如果list需要排序:使用list
自带的 sort()
成员函数:list
提供了自己的 sort()
成员函数,可以对链表中的元素进行排序。
用法:
#include <iostream>
#include <list>
int main() {
std::list<int> myList = {30, 10, 20, 50, 40};
// 使用list自带的sort()成员函数对链表排序
myList.sort();
// 打印排序后的list
std::cout << "myList.sort(): ";
for (int value : myList) {
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
输出结果:
myList.sort(): 10 20 30 40 50
但是实际上不建议用list直接排序(数据量少可以),因为list的sort是归并排序,效率比标准库的sort低,甚至在一些数据量大(如果:数据量10000000以上)情况下拷贝到vector排序完再拷贝回去都比用list成员函数直接排块
总结
适用vector场景:
- 频繁的随机访问
- 频繁在容器的尾部添加或删除元素,
vector
会表现得非常高效 - 相对较少的中间插入和删除,
vector
在中间插入和删除数据效率较低(因为需要挪动后面全部数据) - 内存连续性
适用list场景:
- 频繁的中间插入和删除,list支持在 O(1) 时间内在链表的任意位置插入或删除元素 (不需要挪动后面数据)
- 迭代器的稳定性,插入或删除元素不会使现有的迭代器失效(除非删除了迭代器指向的元素)
- 不需要随机访问,list不支持随机访问,可以遍历链表但效率低
- 避免内存重新分配,每个元素都是独立分配的节点,因此在插入新元素时不需要像
vector
那样重新分配和移动内存。(如果vector
异地扩容,需要拷贝数据)