容器list的本质就是我们之前模拟实现过的链表中的双向带头循环链表,因此在使用的时候和之前我们实现的链表的功能是类似的。但是作为容器,list相较于我们之前实现的链表增加了迭代器的使用,同时这里的迭代器和之前模拟实现string和vector的迭代器也有所不同。因为string和vector的迭代器都可以用原生的指针来模拟实现,但是list中的每一个结点都是分散的,同时每个结点还要保存上一个结点以及下一个结点的信息,所以用原生的指针来实现不是理想,具体的实现方式会在后面具体提到。
一、构造函数
list作为容器也有多种构造函数,这里我们只对构造一个空的list进行模拟实现,其中只进行使用介绍。
链表中的结点不仅需要存储数据,同时还要记录前后结点的信息,因此我们需要用一个结构体来作为链表的结点。因为我们的list能够存储各种类型的数据,所以这里存储的数据类型要用模版来代替。
template<class T>
struct list_Node
{
T _data;
list_Node* _next;
list_Node* _prev;
//T的类型是自定义类型时,T()会调用T类型的构造函数
//当T是内置类型是,T()会将T初始化为默认值
list_Node(const T& data = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{
}
};
由于list的本质上是一个双向带头的循环链表是,所以我们只需要记录list的头结点和就能找到这一整个链表了,同时我们还需要记录一下list内部数据的个数方便后续的操作。因为list能够存储的数据类型是不确定的,我们的结点是一个模版类,所以我们的list也要是一个模版类。它的基本结构如下:
template<class T>
class list
{
//在后面的操作中我们会经常用到结点的类型,因此我们直接把它typedef成Node方便操作
typedef list_Node<T> Node;
Node* _head;
size_t _size;
}
因为list的本质是一个双向带头的循环链表,所以当构造出一个链表时,它应该要一个用来标记的头结点,不论链表中的是否有数据,或者有多少个数据,我们链表都要满足它的基本结构的特点,所以这个用来标记的头结点也把它指向前后结点的指针都指向自己,这样才满足了条件。
list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
//构造一个包含n个值为val的list
list (size_type n, const value_type& val =value_type())
//用任意迭代器把[first,last)的数据存入一个list中
list (InputIterator first, InputIterator last)
//list的拷贝构造函数,可以用一个list来构造另外一个list
list (const list& x)
二、list常见的插入和删除
和我们实现的list类似,容器list也分别有三种插入和删除的方式,push_back在链表的尾部插入一个数据,push_front在链表的头部插入一个数据,insert在指定位置前插入一个数据;pop_back删除链表尾部的数据,pop_front删除链表头部的数据,erase删除链表指定位置的数据。
这里先模拟实现在 头部和尾部操作数据的函数,在特定位置操作的函数放在实现了迭代器以后再进行模拟实现。
void push_back(const T& x)
{
//循环链表的首尾相连 头结点的前一个结点就是链表的最后一个数据
//先保存下插入前尾部数据的信息 方便后续操作
Node* cur = _head->_prev;
Node* newnode = new Node(x);
//把数据先接到头结点之前的位置 也就是最后一个数据的位置
_head->_prev = newnode;
newnode->_next = _head;
//再把插入前的最后一个数据和新的尾部数据连接起来
cur->_next= newnode;
newnode->_prev = cur;
++_size;
}
void push_front(const T& x)
{
//保存插入前的头部数据
Node* cur = _head->next;
Node* newnode = new Node(x);
//先把新的数据挂在头结点的后面
_head->_next = newnode;
newnode->_prev = _head;
//再把原先的头部数据接到新的头部数据的后面
cur->_prev = newnode;
newnode->_next = cur;
++_size;
}
pop_back()
{
//保存尾部数据的信息
Node* cur = _head->_prev;
//将尾部数据前一个结点中指向下一个结点的指针指向头结点
//再把头结点指向前一个结点的指针指向尾部数据的前一个结点
//改变这两个指针的指向就可以删除尾部的数据
_head->prev = cur->prev;
cue->prev->next = _head;
//再释放掉被删除结点申请的空间
delete cur;
--_size;
}
pop_front()
{
//保存原先的头部数据的信息
Node* cur =_head->_next;
//把头结点指向下一个结点的指针指向头部数据的下一个节点
//再把头部数据下一个结点指向前一个结点的指针指向头结点
//这样原先的头部数据就被踢出链表了
_head->_next =cur->_next;
cur->next->prev =_head;
//释放被删除结点申请的空间
delete cur;
--_size;
}