目录
需要注意的是:当结点只有一个的时候,头指针和尾指针均需指向自己。(而且尾结点是_head的_prev所指向的结点,并不是_next)
前言
STL中list容器就是带头结点的双向带头循环链表,也是之前使用c语言实现过的。
对比顺序表的优点是:
1.头插头删效率高。
2.增容效率高,存一个增容一个。
3.空间利用率高。
缺点是:
1.不支持随机访问,访问元素的时间复杂度是O(N)。
2.要占用额外的物理空间进行存储元素之间的关系(prev与next指针)
一、list的框架
list是双线链表,链表就需要使用指针去完成这个结构。
list只需要一个成员,那就是头结点。
这个头节点需要引出其他的结点,每个结点是由头指针、尾指针以及数据组成的,据此我们也可以得到结点的框架。(应为struct类型,因为要能够被list类访问到)
代码如下:
#pragma once
#include <iostream>
using namespace std;
namespace qyy
{
template<class T>
struct ListNode
{
// 初始化结点
ListNode(cosnt T& data = T())
:_data(data)
,_prev(nullptr)
,_next(nullptr)
{}
// 构成结点的成员
T _data;
ListNode<T>* _prev;
ListNode<T>* _next;
};
template<class T>
class list
{
public:
typedef ListNode<T> list_node;
list()
:_head(new list_node())
{
_head->_prev = _head;
_head->_next = _head;
}
private:
ListNode<T>* _head;
};
}
需要注意的是:当结点只有一个的时候,头指针和尾指针均需指向自己。(而且尾结点是_head的_prev所指向的结点,并不是_next)
代码如下:
list()
:_head(new list_node())
{
_head->_prev = _head;
_head->_next = _head;
}
图示:
二、push_back函数
尾插就如同之前实现的双向带头循环链表一样:
找到为尾结点,创建一个要插入的结点,将新节点与旧链链接起来即可。
代码如下:
void push_back(const T& val)
{
// 创造新结点
list_node* newNode = new list_node(val);
// 找到尾结点
list_node* tail = _head->_prev;
// 链接
tail->_next = newNode;
newNode->_prev = tail;
newNode->_next = _head;
_head->_prev = newNode;
}
图示:
三、迭代器
使用迭代器遍历整个list进行验证这个迭代器是否生效。
list的迭代器是最重要的一个部分,这决定了这个链表能否实现随机访问。
这个迭代器是一个双向迭代器(Bidirection Iterator),需要支持++、--、但不能支持+、-。
但是list的迭代器比较特殊:特殊在它不是原生指针,因为链表不是连续存储的物理空间,对于原生指针来说“++”到不了真正意义上的下一个位置。
所以,list的迭代器需要对原生指针进行封装,使得“++”能够到达下一个结点(可得迭代器的成员变量是指针)。
同样地,这个迭代器也需要被list类进行访问,所以实现为struct是最好的结构。
1.迭代器框架
由以上的分析可知,迭代器需要由原生结点指针进行构造。
代码如下:
// 迭代器结构
template<class T>
struct Iterator
{
typedef ListNode<T> list_node;
// 成员:结点指针
list_node* _node;
// 使用结点指针构造迭代器
Iterator(list_node* node)
:_node(node)
{}
}
2.迭代器++
与c语言实现访问链表写一个结点相同,使当前的结点指向_next即可。
需要注意的是:
前置++不需要进行多余操作,但是后置++需要返回++之前的值,所以需要保存将++之前的值保存下来。
代码如下:
// ++it
Iterator<T> operator++()
{
_node = _node->_next;
return *this;
}
// it++
Iterator<T> operator++(int)
{
// 保存没++的迭代器用于return
Iterator<T>* tmp = this;
_node = _node->_next;
return *tmp;
}
3.*迭代器
对迭代器进行解引用得到的是结点对应的data,返回类型应为T。
代码如下:
// *it
T& operator*()
{
return _node->_data;
}
4. it1 != it2
判断相等不相等主要是看结点是不是一个结点,为bool类型。
代码如下:
// it1 != it2
bool operator!=(Iterator<T> it)
{
return _node != it._node;
}
5.迭代器的begin()与end()
需要注意的是迭代器的起始位置与结束位置是对于list来说的,所以应该是list的成员函数。
返回类型为迭代器,但是_head与_head->next只是一个结点,需要用这两个结点去构造两个迭代器。
结构见图:
代码如下:
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
6.重载->
“->”主要是针对于T是自定义类型的模板参数,需要使用箭头进行解引用。
返回结点数据的地址即可。
代码如下:
// 当T是自定义类型时,需要使用“->”进行解引用
// it->
Ptr operator->()
{
return &(_node->_data);
}
7.测试
四、再谈迭代器
在(三)中实现了一个迭代器,但是它是不全面的,const对象无法调用,即使重载为const成员函数也不对。
因为这个迭代器仍不是const的迭代器,在成员函数后面加const只是不能修改this中的成员变量,但是iterator仍然不是const iterator。
解决方法:
增加模板参数列表,使得迭代器在返回的时候直接返回成const的迭代器,从根本上解决了这个问题。
升级如下:
// 迭代器结构
// 传进来的是const,那么Ref与Ptr就是const的,反之亦然
template<class T, class Ref, class Ptr>
struct Iterator
{
typedef ListNode<T> list_node;
typedef Iterator<T, Ref, Ptr> self;
// 成员:结点指针
list_node* _node;
// 使用结点指针构造迭代器
Iterator(list_node* node)
:_node(node)
{}
// ++it
self operator++()
{
_node = _node->_next;
return *this;
}
// --it
self operator--()
{
_node = _node->_prev;
return *this;
}
// it--
self operator--(int)
{
self* tmp = this;
_node = _node->_prev;
return *tmp;
}
// it++
self operator++(int)
{
// 保存没++的迭代器用于return
self* tmp = this;
_node = _node->_next;
return *tmp;
}
// *it
Ref operator*()
{
return _node->_data;
}
// 当T是自定义类型时,需要使用“->”进行解引用
// it->
Ptr operator->()
{
return &(_node->_data);
}
// it1 != it2
bool operator!=(const self& it) const
{
return _node != it._node;
}
};
实现的对于常对象与非常对象均适用的print_list函数:
void print_list() const
{
const_iterator it = begin();
while (it != end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
测试:
五、insert()函数
与双链表实现的插入相同:
创建要插入的结点,找到要插入地方的前一个,进行链接即可。
图解:
代码如下:
void insert(iterator pos, const T& val)
{
// 创建新结点
list_node* newNode = new list_node(val);
// 找出前一个和当前结点
list_node* prev = pos._node->_prev;
list_node* cur = pos._node;
// 链接
prev->_next = newNode;
newNode->_next = cur;
cur->_prev = newNode;
newNode->_prev = prev;
}
测试:
六、erase()函数
与双链表实现的删除相同:
找到要删除结点的前一个结点与后一个结点,跨过欲删除结点进行链接,完成链接后释放欲删除结点即可。
需要注意的是不能够删除头结点。
图解:
代码如下:
void erase(iterator pos)
{
// 不能删除头结点
assert(pos != end());
// 找到前一个与后一个结点
list_node* prev = pos._node->_prev;
list_node* next = pos._node->_next;
// 链接
prev->_next = next;
next->_prev = prev;
// 释放
delete pos._node;
}
图解:
需要注意的是:
insert()与erase()均是由返回值的,可以更新迭代器进行持续地插入删除。
官方库:
insert():
erase():
更新后代码如下:
iterator insert(iterator pos, const T& val)
{
// 创建新结点
list_node* newNode = new list_node(val);
// 找出前一个和当前结点
list_node* prev = pos._node->_prev;
list_node* cur = pos._node;
// 链接
prev->_next = newNode;
newNode->_next = cur;
cur->_prev = newNode;
newNode->_prev = prev;
return iterator(newNode);
}
iterator erase(iterator pos)
{
// 不能删除头结点
assert(pos != end());
// 找到前一个与后一个结点
list_node* prev = pos._node->_prev;
list_node* next = pos._node->_next;
// 链接
prev->_next = next;
next->_prev = prev;
// 释放
delete pos._node;
return iterator(next);
}
测试:
insert():
erase():
七、反向迭代器
1.框架
反向迭代器与正向迭代器的本质都一样,都是为了访问list的结点且“++”或者“--”能变换指向的结点。不同之处在于反向迭代器的“++”方向与正向的“--”等效,反之亦然。
反向迭代器是基于正向迭代器之上进行的操作,可以代码复用。(所以得出反向迭代器是需要传一个正向迭代器进来进行构造的)
和正向迭代器一样,可以使用多个模板参数对于const对象进行设计。
代码如下:
#pragma once
#include <iostream>
namespace qyy
{
template<class Iterator, class Ref, class Ptr>
class reverse_iterator
{
public:
// 定义反向迭代器的类型,方便改,也能够简化
typedef reverse_iterator<Iterator, Ref, Ptr> self;
// 使用正向迭代器进行构造
reverse_iterator(const Iterator& it)
:_rit(it)
{}
private:
Iterator _rit;
};
}
为了能够使每个容器都能使用,反向迭代器的rebgin()与rend()是与正向的begin()和end()对称存在的。
图解:
结合这幅图我们可以知道,如果还像正向迭代器那样去访问结点的data,那么会有一个 _head->next位置的结点是访问不到的,所以我们需要使用一个临时正向迭代器tmp进行提前访问。
当rit到达rend时结束,此时tmp如果要走,已经到了_head。
_head->_next是什么时候访问的呢?
当rit到 _head->_next->_next就已经访问过了。
图示:
QQ录屏20230302125602
2.++反向迭代器 & 反向迭代器++
对于反向迭代器来书,++相当于对正向迭代器的--,所以可以代码复用。
代码如下:
// ++rit <==> --it
self operator++()
{
--_rit;
return *this;
}
后置++只需要保存++之前的值然后返回即可。
代码如下:
// rit++ <==> it--
self operator++(int)
{
self* tmp = this;
_rit--;
return *tmp;
}
3.重载*
需要解引用前一个结点的值。
代码如下:
Ref operator*()
{
Iterator tmp = _rit;
return *--tmp;
}
4.重载->
复用正向迭代器的解引用操作,对其进行取地址可达成目标。
代码如下:
// 复用正向迭代器的取地址操作
Ptr operator->()
{
return &operator*();
}
5.重载!=
复用正向迭代器的!=重载即可。
代码如下:
// rit1 != rit2
bool operator!=(const self& rit)
{
return _rit != rit._rit;
}
6.反向迭代器的rbegin()与rend()
为了能够使每个容器都能使用,反向迭代器的rebgin()与rend()是与正向的begin()和end()对称存在的。
同正向的begin()与end()一样,这个也是list的成员函数。
图解:
代码如下:
typedef reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
typedef reverse_iterator<iterator, T&, T*> reverse_iterator;
reverse_iterator rbegin()
{
return iterator(_head);
}
reverse_iterator rend()
{
return iterator(_head->_next);
}
const_reverse_iterator rbegin() const
{
return const_iterator(_head);
}
const_reverse_iterator rend() const
{
return const_iterator(_head->_next);
}
7.测试
八、其他的复用代码
1.push_front
2.push_back
3.pop_front
4.pop_back
//头插
iterator push_front(const T& val)
{
insert(begin(), val);
return begin();
}
//头删
iterator pop_front()
{
erase(begin());
return begin();
}
//尾插
iterator pop_front(const T& val)
{
insert(end(), val);
return begin();
}
//尾删
iterator pop_back()
{
// end()是头结点
erase(--end());
return begin();
}
九、默认成员函数
6个默认成员函数有:初始化(default构造)和清理(析构),拷贝(cop构造)和赋值(=重载),取地址重载(&重载(常对象与非常对象))。
在list中,我们使用结点对list进行了default构造(也就是初始化)。
1.copy构造函数
需要完成深拷贝,如果让两个指针均指向同一个结点,那么在析构时候会使得一块空间释放两回,造成内存错误。(可以复用尾插)
代码如下:
// copy构造
// 必须完成深拷贝,否则内存错误
list(const list<T>& lt)
:_head(new list_node())
{
_head->_next = _head;
_head->_prev = _head;
list<T>::const_iterator it = lt.begin();
while (it != lt.end())
{
push_back(*it);
++it;
}
}
3.析构函数
可以先实现一个clear()函数对于结点进行数据的清理与结点的回收。
3.1clear()
可以复用erase()函数进行数据清理。
代码如下:
/ 清理结点,复用erase
void clear()
{
iterator it = begin();
while (it != end())
{
iterator tmp = it++;
erase(tmp);
}
}
3.2~list()
可以复用clear()函数进行空间回收,需要注意的是clear()函数不会对头结点进行清理,所以剩下的只用做一个销毁头结点。
代码如下:
// 析构
~list()
{
clear();
// 最后会剩下头结点
delete _head;
_head = nullptr;
}
测试: