目录
list介绍
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向
其前一个元素和后一个元素。- list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高
效。- 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率
更好。- 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list
的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间
开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这
可能是一个重要的因素)
list的底层是带头双向循环链表,在学习数据结构时也使用C语言对带头双向循环链表进行过模拟实现——
双向链表的模拟实现;得益于其优秀的结构设计,在数据的插入,删除方面有着极高的效率。
但在STL中的list更让人叫绝的是其迭代器是设计。
- 迭代器的主要作用就是让算法能够不用关心底层数据结构,对外提供一个统一的访问方式,即使不了解容器的底层是什么,也能通过迭代器对容器进行访问。
- 迭代器其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* ,而今天学习的list的迭代器则是一个封装过的类。
list结构介绍
list得益于其结构,实现增删等功能比前面实现的vector,string更加简单。list更值得我们学习的是其迭代器的的实现。而且迭代期也是我们访问list的主要手段,所以优先实现迭代器。
list的实现需要有三个类:节点类,迭代器类,list类。
节点类的实现
节点作为链表组成的基本单位,其成员有:
- 存储数据的_data
- 连接前一节点的前驱指针_prev
- 连接后一节点的后继指针_next
节点类只需要实现默认构造函数即可。负责将成员初始化。
template<class T>
struct list_node
{
T _data;//数据
list_node<T>* _prev;//前驱指针
list_node<T>* _next;//后继指针
list_node(const T& x = T())
:_data(x)
,_prev(nullptr)
,_next(nullptr)
{
}
};
迭代器的实现
先实现非const版的迭代器;
迭代器的意义就是:
让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。
而list的底层已不再是连续的空间,而是分散的节点,无法再使用[]加下标访问,也不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作。究其原因就是vector,string的底层空间是连续的,其迭代器就是对应的指针,天然就支持++,–这样的操作。
list的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种operator运算符操作进行重载,使其符合迭代器的行为。如:当你使用list当中的迭代器进行自增操作时,实际上执行了p = p->next语句,只是你不知道而已,我们通过类的封装屏蔽了其细节,符合迭代器方法的使用。
总结:
用一个类封装迭代器去模拟指针的行为,该类的成员函数为运算符重载函数(模拟指针行为)。
template<class T>
struct __list_iterator//用一个类封装迭代器去模拟指针的行为,该类的成员函数为运算符重载函数(模拟指针行为)
{
typedef list_node<T> Node;
typedef __list_iterator<T> self;
Node* _node;//节点
__list_iterator(Node* node)
:_node(node)
{
}
self& operator++();
self& operator--();
self operator++(int);
self operator--(int);
bool operator==(const self& it);//it为迭代器,_node为节点
bool operator!=(const self& it);
T& operator*();
T* operator->();
};
该类中只有一个成员,那就是节点。类名太长了,使用typedef对 __list_const_iterator 重命名为self
构造函数
对于封装的迭代器类,我们只需要实现一个构造函数即可,只需要将获取的链表的节点用来构造迭代器中的节点即可。
__list_const_iterator(Node* node)
:_node(node)
{
}
对于我们模拟实现的迭代器,需要明确我们的目的:我们实现的迭代器是为了帮我们去遍历,访问我们的链表。这也是为什么我们不需要实现拷贝构造和赋值重载函数进行深拷贝的原因,我们使用编译器默认生成的浅拷贝的拷贝构造和赋值重载函数就能达到我们的目的,使用深拷贝反而达不到我们但目的。
++运算符重载
对于前置++,使用原则是:先++,再使用;所以直接将当前节点往后走一位,再返回当前节点即可。引用返回效率更好。
self& operator++()
{
_node = _node->_next;
return *this;
}
后置++使用原则为:先使用,再++,所以需要返回++之前的值,所以先用一个临时对象保存该值,再让节点往后走,最后返回临时对象。不能引用返回,tmp为临时对象,函数结束就销毁,不能使用引用返回。
self operator++(int)
{
Node* tmp(_node);
_node = _node->_next;
return tmp;
}
–运算符重载
前置–:先–,再使用:让节点往前走一位,再返回。
self& operator--()
{
_node = _node->_prev;
return *this;
}
后置–:先使用,再–;逻辑与后置++一致。
self operator--(int)
{
Node* tmp(_node);//
_node = _node->_prev;
return tmp;
}
==运算符重载
重载节点与迭代期是否相等。分清_node为节点,而it为迭代期,类型不同;对比的是节点_node和迭代器的成员:节点_node。
bool operator==(const self& it)//it为迭代器,_node为节点
{
return _node == it._node;
}
!=运算符重载
原理与上述==相同,只不过现在是!=。
bool operator!=(const self& it)
{
return _node != it._node;
}
*运算符重载
使用解引用操作符时,是想得到该位置的数据内容。因此,我们直接返回当前结点指针所存储数据即可,但是这里使用引用返回,因为解引用是支持对数据进行修改的。
T& operator*()
{
return _node->_data;//返回引用
}
->运算符重载
在某些场景下,我们需要使用->来访问数据如指针,或对象类型为自定义类型时。
->操作符会先对指针进行解引用,然后访问其指向对象的成员。所以我们只需要返回数据的地址即