list的官方文档
前言
list
是数据结构中的链表,在C++的STL中,有list的模板,STL中的list的结构是带头双向循环链表,当然STL中还有一个forward_list
的链表,这个链表是一个带头的单链表。
关于本章的代码,你可以点击这里进行获取
为了更好的理解list,我们来对其进行模拟实现。
对于链表的节点我们都很熟悉了,节点中包含两个域,一个指针域一个数据域,为了让list
能够通用,我们选择使用模板。
节点的结构如下:
template<class T>
//struct也能定义类,默认类的访问限定符是 public
struct list_node
{
//这个指针指向前一个节点
list_node<T>* _prev;
//这个指针指向后一个节点
list_node<T>* _next;
//这个是数据域中的元素
T _data;
//对节点使用匿名对象进行初始化
list_node(const T& data = T())
:_prev(nullptr)
,_next(nullptr)
,_data(data)
{}
};
现在我们已经有了节点了,我们还要有迭代器,如果没有迭代器我们就不能很好的访问每一个节点。
对于迭代器我们要让它指向我们想要的节点,这才能便于我们的访问,于是很明显我们迭代器的成员变量就要是一个节点的指针!同时为了让list
能够通用,我们选择使用模板来定义迭代器。
迭代器的结构如下:
//这里后面的两个参数,在实际应用时通常是T& , T* 或者是 const T& , const T*
//根据加与不加const 可以分别实例化出:普通正向迭代器与正向const迭代器
template<class T, class Ref, class Ptr>
struct __list_iterator
{
//将节点的类型进行typedef方便使用
typedef list_node<T> node;
//将类自己进行typedef方便使用
typedef __list_iterator<T,Ref,Ptr> self;
//成员变量 是一个指向节点的指针
node* _pnode;
//构造函数 用一个节点的地址对迭代器进行初始化,
__list_iterator(node* pnode)
:_pnode(pnode)
{}
};
由于list是带头双向循环链表,我们只需要一个指向头节点的指针便能够管理所有的节点了。
template<class T>
class list
{
public:
//将节点的类型进行typedef方便使用
typedef list_node<T> node;
//将迭代器进行typedef方便使用
typedef __list_iterator<T, T&, T*> iterator;
//将const迭代器进行typedef方便使用
typedef __list_iterator<T, const T&, const T*> const_iterator;
//默认构造函数
list()
{
empty_init();
}
//初始化函数
void empty_init()
{
//申请一个头节点,将节点的地址给_head
_head = new node;
//让哨兵位节点的 前指针指向自己
_head->_prev = _head;
//让哨兵位节点的 后指针指向自己
_head->_next = _head;
}
private:
//指向哨兵位节点的指针
node* _head;
};
到此为止我们一共建立了三个类,下面我们模拟实现链表的各种接口时,我们还要继续丰富迭代器类的接口与list类的接口
由于链表的许多操作都要用到迭代器,但是迭代器的一些其他接口我们还没有实现,在这里我们来实现迭代器的所有接口。
对于原生指向节点的指针来说*
运算符能让我们拿到节点,但还无法拿到节点数据域中的数据,但是对于迭代器来说*
运算符就要拿到容器中存储的数据,所以我们还要对迭代器的*
运算符进行重载。
// *运算符重载
Ref operator*()
{
//迭代器中的那个指针不能是nullptr
assert(_pnode);
//返回节点中的数据域中的数据
return _pnode->_data;
}
++
运算符分为两种:一种是前置++
一种是后置++
,这两个函数构成函数重载,后置++
的参数部分会多一个int
类型。(--
运算符同理)
对于原生指向节点的指针来说:++
指针是让指针移动到下一个紧挨着的同类型的指针位置,但是对于迭代器来说:++
是让迭代器指向下一个节点的位置,这两者并不匹配,所以我们要对++
运算符进行函数重载。
//前置++运算符
self& operator++()
{
_pnode = _pnode->_next;
return (*this);
}
//后置++运算符
self operator++(int)
{
//先保存++之前的结果
self tmp(*this);
_pnode = _pnode->_next;
//返回++之前的值
return tmp;
}
//前置--运算符
self& operator--()
{
_pnode = _pnode->_prev;
return (*this);
}
//后置--运算符
self operator--(int)
{
self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
虽然在前面我们已经实现了迭代器*
的运算符重载,已经可以访问数据域中的数据了。但是当我们的list
里面存储的是自定义类型的数据,而我们想要访问自定义类型中的成员变量时迭代器*
的运算符就不能够帮到我们了。
例如:
struct Date
{
int _year;
int _month;
int _day;
}
//it是迭代器,指向了存储了Date类型的节点
//假设:在没有->操作符时,我们想要修改_year的值,
(*it)._year = 2023;
//如果有了-> 操作符,我们就能这样操作,更加符合我们的使用习惯
it->_year = 2023;
于是我们来实现:->运算符的重载,我们先来看代码:
// ->运算符重载
Ptr operator->()
{
return &(_pnode->_data);
}
看到这里你可能会觉得很奇怪,觉得这段代码是错误的,下面我们就来详细讲解这里的问题和注意事项。
_pnode
是迭代器的成员变量,是一个节点的指针,它使用的->
是C++的内置类型的操作符,这段代码(_pnode->date)
是拿到的是节点中存储的数据,这段代码&(_pnode->date)
是拿到的是节点中存储的数据的地址,返回之后我们好像并没有得到自定义类型中的数据,好像还差一次->
操作,比如这样:
it->->_year = 2023;
//it-> 等价于 (&(_pnode->date))
//(&(_pnode->date))->year = 2023;
实际上按上面的运算符重载函数写法确实是少了一次->
,但是C++为了代码的简洁性在这里进行了特殊处理,我们写->的运算符重载时只需要返回list
里面自定义类型的地址就行了,在外面实际应用时,编译器在编译时会为我们自动加上一次->
。
我们在使用迭代器进行遍历数据的时候,经常要使用关系运算符 !=
==
来判断条件是否达到,在这里我们对关系运算符 !=
==
进行函数重载。
判断两个迭代器是否相等的办法就是两个迭代器是不是指向同一个位置!
// !=运算符重载
bool operator!=(const self& s)
{
return _pnode != s._pnode;
}
// ==运算符重载
bool operator==(const self& s)
{
return _pnode == s._pnode;
}
在实现完迭代器之后,我们就要实现list的其他接口了。
虽然在list
的类外我们已经实现了迭代器的各种接口,但是list
类内我们还没有提供使用迭代器的接口的函数,这个函数就是我们常用的begin()
与end()
函数!下面我们来一起实现一下。
//正向迭代器
iterator begin()
{
//_head指向的是哨兵位的头节点,_head的下一个才是第一个节点!
//这里使用的是一个指针构造的匿名对象做返回值,编译器会对此进行优化,能够增加效率
return iterator(_head->_next);
}
iterator end()
{
//由于是双向循环链表,所以最后一个节点的下一个位置就是哨兵位节点
return iterator(_head);
}
//const迭代器的思路与普通迭代器类似
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
list
链表的插入很简单,我们需要先申请一个新节点存储我们想要插入的数据,然后将新节点的_prev
指针指向前一个节点,同时新节点的_next
指针指向当前节点。同时再对当前节点与前一个节点中相应的指针进行更新,就完成了指针的链接。
void insert(iterator pos, const T& x)
{
//先申请一个节点,存储我们要插入的数据
node* new_node = new node(x);
node* prev = pos._pnode->_prev;
//链接过程
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = pos._pnode;
pos._pnode->_prev = new_node;
}
插入函数写完以后,我们的头插尾插函数也就相当于写完了
头插函数
void push_front(const T& x)
{
//在begin()位置进行插入就是头插!
insert(begin(), x);
}
尾插函数
//尾插函数
void push_back(const T& x)
{
//在end()位置进行插入,就是尾插
insert(end(), x);
}
链表的删除没有顺序表那么复杂,但是我们应该注意:应该先将前后节点的连接关系给建立好,然后再删除节点!
iterator erase(iterator pos)
{
assert(pos != end());
//链接过程
node* prev = pos._pnode->_prev;
node* next = pos._pnode->_next;
prev->_next = next;
next->_prev = prev;
//删除节点
delete pos._pnode;
//返回指向原节点的下一个节点的迭代器,外部接收后可以防止迭代器失效!
return iterator(next);
}
同理删除函数写完以后,我们的头删尾删函数也就相当于写完了!
头删函数
void pop_front()
{
erase(begin());
}
尾删函数
void pop_back()
{
//由于end()是最后一个节点的下一个位置,所以这里要对end()进行一次自减运算
erase(--end());
}
清除函数的作用就是删除除了哨兵位节点以外所有节点,现在我们有了迭代器我们访问每个节点都变得非常容易,删除相应的节点也变的非常容易,我们只需要遍历一遍链表逐一进行删除就行了。
void clear()
{
list<T>::iterator it = begin();
while (it != end())
{
//erase函数删除相应节点以后会返回下一个节点的迭代器
it = erase(it);
}
}
对于链表的交换我们只需要交换list
的成员变量中指向哨兵位节点的指针(即_head
指针)就可以完成整个链表的交换了!
//swap函数
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
}
此函数的作用就是用一个迭代器的区间来构造一个链表,要实现这个函数我们只需要用迭代器进行遍历,然后将遍历到的数据一个一个尾插就能构成一个新的链表了,同时为了能够支持更多的迭代器能够去构造链表,我们可以将该函数变成一个函数模板。
//迭代器区间构造,传入的迭代器应该至少是一个二元迭代器,能支持向前和向后遍历,这时链表的最低要求。
template<class Biditerator>
list(Biditerator first, Biditerator last)
{
//调用初始化函数
empty_init();
//遍历迭代器同时将数据形成一个新节点插入链表中
while (first != last)
{
push_back(*first);
++first;
}
}
有了迭代器区间构造和交换函数我们就可以写现代写法的拷贝构造了!
现代写法的拷贝构造就是用迭代器区间构造一个完整的链表,然后交换给拷贝对象。
//拷贝构造
list(const list<T>& lt)
{
//初始化
empty_init();
//用迭代器区间构造创建一个新的list对象
list<T> tmp(lt.begin(), lt.end());
//将this指针指向的对象与这个新的tmp对象进行交换,拷贝就变相完成了
swap(tmp);
}
有了拷贝构造和交换函数,我们还是可以采用现代版本的赋值重载,原理与上面的拷贝构造同理。
//赋值运算符重载
//注意这里的传参方式是传值传参
list<T>& operator=(list<T> lt)
{
//将this指针指向的对象与这个lt对象进行交换,赋值就变相完成了
swap(lt);
return (*this);
}
最后就是析构函数了,由于我们已经实现过了clear
函数,所以我们可以先调用clear
函数删除所有有效节点,然后再删除哨兵位的节点就行了!
~list()
{
clear();
delete _head;
_head = nullptr;
}