🎉博客主页:小智_x0___0x_
🎉欢迎关注:👍点赞🙌收藏✍️留言
🎉系列专栏:C++初阶
🎉代码仓库:小智的代码仓库
一、list介绍
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
- 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
- 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
二、list的使用
list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。下面为大家介绍list中一些常见的重要接口。
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 |
void ListTest1()
{
//构造n个val的list
list<int> lt1(10, 1);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
//构造一个空的list
list<int> lt2;
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
//拷贝构造一个list
list<int> lt3(lt1);
for (auto e : lt3)
{
cout << e << " ";
}
cout << endl;
//使用迭代器区间构造一个list
list<int> lt4(lt1.begin(), lt1.end());
for (auto e : lt4)
{
cout << e << " ";
}
cout << endl;
}
运行结果>
迭代器
这里我们可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。
函数声明 | 接口说明 |
---|---|
begin +end |
返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器 |
rbegin +rend |
返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的reverse_iterator,即begin位置 |
- begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动.
- rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动.
empty和size
函数声明- | - 接口说明 |
---|---|
empty |
检测list是否为空,是返回true,否则返回false |
size |
返回list中有效节点的个数 |
front和back
函数声明- | - 接口说明 |
---|---|
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中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
void TestListIterator1()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array + sizeof(array) / sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
l.erase(it);
++it;
}
}
运行报错>
改正过的代码:
void TestListIterator()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array + sizeof(array) / sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
l.erase(it++); // it = l.erase(it);
}
}
这样运行就不会出问题了。
三、list的模拟实现
list的结构模型
成员变量
//自定义新的命名空间
namespace csdn {
//定义模板参数
template<class T>
//定义结构体
struct list_node
{
list_node<T>* _next;//指向下一个结构体的指针变量
list_node<T>* _prev;//指向上一个结构体的指针变量
T _val;//定义val值
//构造函数
list_node(const T& val = T())
:_next(nullptr)
, _prev(nullptr)
, _val(val)
{}
};
//定义模板参数
template<class T>
class list//定义list类
{
typedef list_node<T> Node;//重定义为Node
private:
Node* _head;//定义头节点
size_t _size;//定义_size保存元素个数
};
}
初始化函数
void empty_init()
{
//new一个新的节点
_head = new Node;
_head->_prev = _head;//初始化指针变量
_head->_next = _head;//初始化指针变量
_size = 0;//初始化_size
}
这里定义这个函数的意义是为了降低代码的冗余度,申请节点是比较常用的,这里封装成函数也方便我们直接调用。
构造函数
list()
{
//直接调用初始化函数
empty_init();
}
拷贝构造
list(const list<T>& lt)
{
//调用初始化函数
empty_init();
//使用范围for对数据进行逐个尾插
for (auto& e : lt)
{
push_back(e);
}
}
clear()
void clear()
{
//通过迭代器对每一个数据进行删除
iterator it = begin();
while (it != end())
{
it = erase(it);//调用erase对该节点进行删除释放
}
_size = 0;//将_size置为0
}
析构函数
~list()
{
//调用clear清空数据
clear();
delete _head;//释放头节点空间
_head = nullptr;//将头节点置为空
}
迭代器
STL中迭代器的设计思想:
STL中list的迭代器设计思想是将list中的节点作为迭代器,而不是像vector那样使用指针作为迭代器。这是因为list是一个双向链表,每个节点都包含指向前一个节点和后一个节点的指针,因此可以很方便地实现双向迭代器。
在STL中,list的迭代器包括正向迭代器和反向迭代器。正向迭代器可以从头部开始遍历到尾部,而反向迭代器则可以从尾部开始遍历到头部。通过这种设计,可以方便地实现list的各种操作,如插入、删除、排序等。
此外,在STL中,list的迭代器还支持一些常用的操作符重载,如*、->、++、–等,使得使用起来更加方便。
根据STL
中的设计思想我们需要新定义一个结构体重新封装我们的链表节点然后再重载各种操作符,来达到我们实现迭代器的目的。
封装的结构体:
// typedef __list_iterator<T, T&, T*> iterator;
// typedef __list_iterator<T, const T&, const T*> const_iterator;
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node; // 定义类型别名 Node,代表指向 list_node 的指针类型
typedef __list_iterator<T, Ref, Ptr> self; // 定义类型别名 self,代表当前迭代器类型
Node* _node; // 指向 list_node 的指针,表示当前迭代器指向的节点
__list_iterator(Node* node)
:_node(node) // 构造函数,接受一个指向 list_node 的指针,并将其赋值给 _node
{}
Ref operator*()
{
return _node->_val; // 重载 * 运算符,返回当前节点的值(通过指针访问节点的值)
}
Ptr operator->()
{
return &_node->_val; // 重载 -> 运算符,返回指向当前节点值的指针
}
self& operator++()
{
_node = _node->_next; // 重载前置 ++ 运算符,使迭代器指向下一个节点,并返回自身引用
return *this;
}
self operator++(int)
{
self tmp(*this); // 重载后置 ++ 运算符,先用当前迭代器创建一个临时副本
_node = _node->_next; // 然后将当前迭代器指向下一个节点
return tmp; // 最后返回保存原始位置的临时副本
}
self& operator--()
{
_node = _node->_prev; // 重载前置 -- 运算符,使迭代器指向前一个节点,并返回自身引用
return *this;
}
self operator--(int)
{
self tmp(*this); // 重载后置 -- 运算符,先用当前迭代器创建一个临时副本
_node = _node->_prev; // 然后将当前迭代器指向前一个节点
return tmp; // 最后返回保存原始位置的临时副本
}
bool operator!=(const self& it) const
{
return _node != it._node; // 重载 != 运算符,判断两个迭代器是否不相等
}
bool operator==(const self& it) const
{
return _node == it._node; // 重载 == 运算符,判断两个迭代器是否相等
}
};
类内部:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return _head->_next;//这里会发生隐式类型转换自动转换为iterator类型
//return iterator(_head->_next);
}
iterator end()
{
return _head;
//return iterator(_head);
}
const_iterator begin() const
{
return _head->_next;
//return const_iterator(_head->_next);
}
const_iterator end() const
{
return _head;
//return const_iterator(_head);
}
swap()
void swap(list<T>& lt)
{
std::swap(_head, lt._head);//调用std库里面的swap函数来交换_head
std::swap(_size, lt._size);//调用std库里面的swap函数来交换_size
}
重载=赋值运算符
list<T>& operator=(list<T> lt)
{
//调用swap函数交换*this和lt
swap(lt);
return *this;
}
lt
的作用域在这个函数内,出了函数就会自动析构,我们把*this
和lt
的内容进行了交换,完了出了作用域lt
也会帮我们析构掉交换之后lt
的空间。
insert()
// 在迭代器 pos 指向的节点之前插入一个值为 x 的节点
iterator insert(iterator pos, const T& x)
{
// 获取 pos 指向的节点
Node* cur = pos._node;
// 获取 pos 前面的节点
Node* prev = cur->_prev;
// 创建一个新的节点,值为 x
Node* newnode = new Node(x);
// 将 prev 的 next 指针指向新节点
prev->_next = newnode;
// 将新节点的 next 指针指向 cur
newnode->_next = cur;
// 将 cur 的 prev 指针指向新节点
cur->_prev = newnode;
// 将新节点的 prev 指针指向 prev
newnode->_prev = prev;
// 链表大小加 1
++_size;
// 返回新节点的迭代器
return newnode;
}
erase()
// 删除迭代器 pos 指向的节点
iterator erase(iterator pos)
{
// 断言 pos 不等于 end()
assert(pos != end());
// 获取 pos 指向的节点
Node* cur = pos._node;
// 获取 cur 前面的节点
Node* prev = cur->_prev;
// 获取 cur 后面的节点
Node* next = cur->_next;
// 将 prev 的 next 指针指向 next
prev->_next = next;
// 将 next 的 prev 指针指向 prev
next->_prev = prev;
// 删除 cur 指向的节点
delete cur;
// 链表大小减 1
--_size;
// 返回下一个节点的迭代器
return next;
}
size()
size_t size()
{
return _size;//直接返回_size即可
}
头插/删、尾插/删
// 在链表尾部添加一个元素
void push_back(const T& x)
{
insert(end(), x); // 调用 insert 函数,在迭代器 end() 处插入元素 x
}
// 在链表头部添加一个元素
void push_front(const T& x)
{
insert(begin(), x); // 调用 insert 函数,在迭代器 begin() 处插入元素 x
}
// 移除链表尾部的元素
void pop_back()
{
erase(--end()); // 调用 erase 函数,移除迭代器 end() 前一个位置的元素
}
// 移除链表头部的元素
void pop_front()
{
erase(begin()); // 调用 erase 函数,移除迭代器 begin() 处的元素
}
这里的代码是在一个链表容器中实现了四个成员函数,它们分别是 push_back
、push_front
、pop_back
和 pop_front
。这些函数对链表进行尾部添加、头部添加、尾部移除和头部移除的操作。
-
push_back(const T& x)
:在链表的尾部添加一个元素 x,使用insert
函数,在迭代器end()
处插入元素 x。 -
push_front(const T& x)
:在链表的头部添加一个元素 x,使用insert
函数,在迭代器begin()
处插入元素 x。 -
pop_back()
:移除链表的尾部元素,使用erase
函数,移除迭代器end()
前一个位置的元素。 -
pop_front()
:移除链表的头部元素,使用erase
函数,移除迭代器begin()
处的元素。
这些函数利用了之前定义的链表迭代器,通过合适的迭代器位置,实现了对链表容器的尾部添加、头部添加、尾部移除和头部移除的功能。
总结
当谈到STL(标准模板库)中的 list
容器时,它是一个双向链表实现的容器。list
提供了许多有用的方法,使其成为处理插入和删除元素效率高的容器。下面是对 list
容器的一些总结:
-
双向链表结构:
list
是一个双向链表容器,每个节点包含一个值以及指向前一个节点和后一个节点的指针。这种结构使得在任何位置插入和删除元素的代价都是常数时间 O(1)。 -
不支持随机访问:
由于list
是一个链表,不像vector
那样支持随机访问,即不能通过索引直接访问元素。要访问list
的元素,只能通过迭代器进行顺序访问。 -
插入和删除高效:
在list
中插入和删除元素的效率非常高。由于链表的特性,插入和删除操作只涉及相邻节点指针的重新连接,而不需要像动态数组那样移动大量元素。 -
不连续内存存储:
list
的元素在内存中不是连续存储的,而是通过指针链接在一起的。这意味着list
对内存的利用较低,但在元素插入和删除时效率很高。 -
不支持快速随机访问:
由于不支持常数时间的随机访问,使用list
迭代器进行元素遍历和访问时的时间复杂度是 O(n),其中 n 是链表中元素的数量。 -
不支持数组操作:
与vector
不同,list
不能通过[]
运算符访问元素。如果需要通过索引访问元素,list
不是最佳选择。 -
没有预留空间:
list
不支持预留内存空间,因为其元素并非连续存储。因此,与vector
不同,list
不提供capacity()
方法。 -
迭代器稳定性:
list
在插入或删除元素后,除了当前元素的迭代器,其他迭代器仍然保持有效。这是由于链表的性质,与动态数组vector
不同。 -
使用时的注意事项:
-
list
适用于需要频繁插入和删除元素的场景,但不需要随机访问。 - 如果需要频繁随机访问元素或预留大量内存空间,更适合使用
vector
。 -
list
不支持与排序相关的随机访问算法,例如std::sort()
,因为它没有随机访问能力。
-
总的来说,list
是一个高效的双向链表容器,对于频繁插入和删除元素的场景非常有用。但是,如果需要随机访问元素或需要预留大量内存空间,应该选择其他支持这些操作的容器,如 vector
。选择合适的容器取决于特定的使用情况和性能要求。文章来源:https://www.toymoban.com/news/detail-597779.html
🍀小结🍀
今天我们学习了"list"容器从使用到模拟实现
相信大家看完有一定的收获。种一棵树的最好时间是十年前,其次是现在!
把握好当下,合理利用时间努力奋斗,相信大家一定会实现自己的目标!加油!创作不易,辛苦各位小伙伴们动动小手,三连一波💕💕~~~
,本文中也有不足之处,欢迎各位随时私信点评指正!
本节课的代码已上传gitee仓库文章来源地址https://www.toymoban.com/news/detail-597779.html
到了这里,关于【STL】“list“容器从使用到模拟实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!