前言
list(双向链表)类属于STL标准模版库容器模块,它的迭代器是双向迭代器,也比较好理解,它申请空间不是一次申请一块空间而是每一个节点各自独立的空间,它不再能够支持随机访问和[],如果想要和string类容器或者vector容器一样使用[]进行访问数据,需要重新定义迭代器,重载运算符
单向迭代器:只支持单方向移动也就是++,如单链表
双向迭代器:它比单向多了一个功能向后移动,支持++和--,但不能够随机访问数据,如双向链表,树
随机迭代器:除了包含以上两个迭代器都有的功能以外还可以通过索引访问就像数组一样,如stirng类和vector类
注意:list类含有哨兵位,所以第一个元素都是哨兵位的下一个,末尾元素是哨兵位的上一个,后面我将不在特意强调
我比较喜欢依靠注释讲解,如有解释不到位的地方,望包含
list类各项接口详细文档传送门:list - C++ Reference
list类迭代器相关:
begin //返回链表的第一个元素的迭代器
end //返回链表结尾元素的迭代器
容量相关:
empty //判空
size //返回有效元素个数
访问数据:
front //访问并返回第一元素的数据
back //访问并返回末尾元素的数据
迭代器类相关:
//我们需要自己进行iterator(迭代器)封装,因为要屏蔽底层细节使其使用起来和vector类一致
operator* //访问元素里的数据
operator== //对比两个迭代器内的指针是否相等
operator!= //对比两个迭代器内的指针是否不相等
operator++ //跳到当前节点的下一个节点
operator-- //跳到当前节点的上一个节点
operator-> //返回一个类型的指针
对元素操作相关的:
push_front //在哨兵位节点的下一个节点之前插入新元素(头插)
pop_front //删除哨兵位节点的下一个节点(头删)
push_back //在末尾元素的后面插入新元素(尾插)
pop_back //删除末尾元素(尾插)
insert //在迭代器pos位置上插入新数据(需要通过Find(查找)找到pos节点返回其迭代器)
erase //删除某个节点或者迭代器区间
swap //交换两个对象的数据(直接将哨兵位互换即可)
operator= //将一个对象赋值给另一个对象
resize //将链表元素缩小或者扩大到N个
clear //将链表初始化
目录
list类的成员变量,迭代器类的成员变量:
list类中与迭代器相关的函数及容量相关函数:
迭代器类相关:
list类中的构造函数,析构函数,拷贝构造:
构造函数:
析构函数:
拷贝构造:
对元素操作相关的函数:
元素插入:
元素删除:
operator=(赋值),swap(交换),clear(初始化),resize(增大个数或者缩小):
list类的成员变量,迭代器类的成员变量:
首先需要定义一个节点类,定义这个类就当成在C语言中的结构体就行,只不过struct在C++中升级了,它也可以创建类,所以需要构造函数
注意:struct和class两个都可以创建类,但struct默认里面的成员们是公共(public)的,class默认是私有(private)
template <class T> //节点 struct list_node { T _val; //数据 list_node<T>* _next; //下一个节点的指针 list_node<T>* _prev; //上一个节点的指针 list_node(const T&val=T()) //构造 :_val(val) ,_next(nullptr) ,_prev(nullptr) {} };
现在节点定义好了,接下来就是list类的成员变量
list类的成员变量是一个指向节点的指针,我这里指向哨兵位;还有一个我自加的count方便查看节点个数
template <class T > class list { public: //对节点重命名 using Node = list_node<T>; //这个是C++11新加的重命名方式 //typedef list_node<T> Node; //typedef当然还是可以的 private: Node* _root=nullptr; //指向哨兵位 size_t count=0; //记录节点个数 };
后面的成员函数们会经常使用到迭代器遍历,但list类与vector类不同,它是由一个个节点链接起来的所以直接++是肯定到不了下一个节点的,现在我们需要在迭代器类中重载“++”运算符来帮助迭代器到达下一个节点
现在我们来实现一个迭代器,迭代器的底层是一个节点指针,它会接收传进来的节点指针指向的地址
template <class T> struct list_iterator { using Node = list_node<T>; //对节点重命名 using self = list_iterator<T>; //对自己重命名不然作为返回值写老长 Node* _node; //指向当前节点的成员变量 list_iterator(Node*cur) //指向节点的指针都可以传过来变成一个迭代器 :_node(cur){} };
list类中与迭代器相关的函数及容量相关函数:
现在迭代器类已经创建好了,可以实现begin()和end()函数了,他们的作用分别是创建一个有效元素起始位置的迭代器,末尾元素的迭代器。容量部分函数比较少且简单直接看代码实现的即可。
template <class T > class list { public: using Node = list_node<T>; using iterator = list_iterator<T>; iterator begin() //第一个有效元素的迭代器 { return _root->_next; //返回时隐式类型转成迭代器 } iterator end() //末尾元素的迭代器 { return _root; //道理同上 } bool Empty() //直接利用count判空即可 { return count == 0; } void Size() //返回有效元素个数即可 { return count; } private: ... };
迭代器类相关:
现在对迭代器的各项运算符进行重载,list的各项初始化,插入,删除,修改,查找都会用到这些函数
<1>operator*()
重载运算符“*”(解引用),解引用是为了找到数据,所以直接将数据返回即可
template <class T> struct list_iterator { /... T& operator*() { return _node->_val; //成员变量就是当前节点指针直接索引即可 } };
<2>operator++和operator--
这两位就非常熟悉了吧,在vector类中指针中的地址加上当前类型(自定义类型,int,char...)的大小就可以到达下一个元素的起始地址,但list类是依靠next指针到达下一个节点,所以直接把list到达下一个节点的逻辑写进重载的运算符即可
template <class T> struct list_iterator { /... using self = list_iterator<T>; self& operator++() //前置++ { _node = _node->_next; //把_next中的地址给到当前指针 return *this; //返回*this } self operator++(int) //后置++ { self temp (*this); //构造一个原来的迭代器(临时对象) _node = _node->_next; return temp; //返回临时对象 } self& operator--() //前置-- { _node = _node->_prev; //把_prev中的地址给当前指针 return *this; //返回*this } self operator--(int) //后置-- { self temp (*this); //构造一个原来的迭代器(临时对象) _node = _node->_prev; return temp; //返回临时对象 } };
<3>operator==和operator!=
重载==和!=里面的对比逻辑不能对比_val值,如果有多个相同的值就会有漏洞(比如遍历查找返回),正确的做法应该是对比两者的指针,这样就可以保证在有多个同样值的情况下正确判断
注意:对比的两个参数都是迭代器
template <class T> struct list_iterator { /... bool operator==(const self& l1) { return _node == l1._node; //对比节点指针 } bool operator!=(const self& l1) { return _node != l1._node; } };
<4>operator->
重载->主要是为了应对自定义类型。自定义类型中大概率会有多个数据,又想要快速索引数据,重载->是一个利器。它会返回一个T类型指针
注意:重载->在调用时会有隐藏的效果,使它在调用时看起来就和指针一样
下面实现一个简单的用法,我也会把它实际的样子写出来:
template <class T> //这个是list的迭代器里的 struct list_iterator { /... T* operator->() { return &_node->_val; //咱就一个数据直接返回数据的指针即可 } }; //operator->()使用试例 struct AA { int a1 = 1; int a2 = 2; }; int main() { list<AA> l1; l1.Push_back(AA()); l1.Push_back(AA()); l1.Push_back(AA()); l1.Push_back(AA()); auto it = l1.begin(); cout << it->a1<<":"<<it->a2<<endl; //实际上长这样 //cout << it.operator->()->a1<<":"<<it.operator->()->a2<<endl; }
上面是调用试例结果
因为我实现的list类里没有多个数据就不太需要使用->,实现后调用看起来很怪,非常怪,
也不要想着返回对象里的节点指针,这个iterator(迭代器)类是为了服务list类,所以他的T类型和list类中的T一致,所以在只有一个数据的情况下->基本没用
cout<<*(it.operator->()) //因为返回的数据的指针直接解引用拿数据即可
list类中的构造函数,析构函数,拷贝构造:
构造函数:它有四种类型的构造,同时因为哨兵位的存在需要额外写一个哨兵位初始化
1.无参构造
2.插入N个节点,每个节点用T类型初始化
3.迭代器区间构造
4.数组或者一串元素构造
构造函数:
无参构造只需要初始化哨兵位即可
void Empty_init() { _root = new Node; //申请空间 _root->_next = _root; //哨兵位的两个指针都指向直接 _root->_prev = _root; count = 0; //计数为0 } list() { Empty_init(); //无参构造 }
插入N个节点,每个节点用T类型初始化构造:这里需要对N进行一次判断,假设N为0就单独对哨兵位初始化即可。它和vector类一样需要一个int类型作为第一个参数的函数重载,来避免用插入N个数据,节点用int类型初始化的错误匹配到迭代器初始化
注意:在没有实现int类型作为第一个参数的函数重载时,模板会推导两次,两个类型为int,按需实例化是挑做符合要求的所以会跑到迭代器模板初始化去,实现了就推导一次,就得出了两个int所以不再会跑到模板去
list(size_t n, const T& val = T()) { if (n == 0) //判断n是否为0 { Empty_init(); } else { Empty_init(); //不能忘了哨兵位初始化 while (n--) //连续插入 { Push_back(val); //调用尾插函数 } } } list(int n, const T& val = T()) //这个函数重载主要是为了避免错误匹配 { if (n == 0) { Empty_init(); } else { Empty_init(); while (n--) { Push_back(val); } } }
迭代器区间构造:这里需要使用模板,因为要匹配每个类型
template <class InputIterator> list(InputIterator first, InputIterator last) { Empty_init(); //哨兵位初始化 while (first != last) //迭代器区间遍历 { Push_back(*first); //获取数据插入即可 first++; //不要忘了迭代器++ } }
数组或者一串元素构造:实现这个构造需要使用到initializer_list类,它的作用主要是讲一串元素或者数组转变成对象,然后通过迭代器遍历插入
注意:initializer_list类初始化,一组数据或者数组进行传参时会强转成该类对象,它能自动识别类型,并含有迭代器
list(initializer_list<T> li) { Empty_init(); //哨兵位初始化 for (auto& ch : li) //迭代器遍历 { Push_back(ch); //插入 } }
析构函数:
析构函数就不使用迭代器遍历删除了(不是说不可以),因为迭代器删除会导致迭代器失效容易出问题,所以这里我用传统的循环释放
~list() { Node* cur = _root; //从哨兵位开始 while (cur!=_root) //不能回到哨兵位 { Node* next = cur->_next; //储存下一个节点 delete cur; cur = next; } _root = nullptr; //哨兵位指针给空 }
拷贝构造:
拷贝构造没什么难度,初始化哨兵位后只需要将属于该类型的对象引用传入,然后遍历插入即可
list(const list<T>& l1) { Empty_init(); //初始化哨兵位 for (auto ch : l1) //遍历对象插入 { Push_back(ch); } }
对元素操作相关的函数:
元素插入:
push_front(头插)和push_back(尾插):
尾插要修改的节点如下图,头插就是插入的位置不同,逻辑还是一致的
void Push_back(const T& val) //尾插 { /*Node* newnode = new Node(val); Node* ptail = _root->_prev; //头节点的prev就是尾 newnode->_next = _root; //新节点的_next指向哨兵位 newnode->_prev = ptail; //新节点的_prev指向记录的尾 ptail->_next = newnode; //原来的尾_next连接新节点 _root->_prev = newnode; //哨兵位的_prev连接新节点 count++;*/ //计数++ //这里复用了Insert函数 Insert(end(), val); } void Push_front(const T& val) //头插 { /*Node* newnode = new Node(val); //这里的连接方式和上面基本一致 Node* pnext = _root->_next; newnode->_next = pnext; newnode->_prev = _root; pnext->_prev = newnode; _root->_next = newnode; count++;*/ //复用了Insert函数 Insert(begin(), val); }
Insert(插入):在pos位置上插入一个元素很轻松,迭代器区间插入则需要在此基础上实现,
因为要复用插入单个元素
注意:迭代器区间遍历使用模板是为了也可以使用其他相同T类型的对象插入,比如vector<int>类型的对象list<int>的迭代器区间插入就支持遍历插入,因为都是int
void Insert(iterator pos, const T& val) //单个元素pos位置插入 { Node* cur = pos._node; //获取迭代器中存放的节点指针 Node* newprev = cur->_prev; //记录插入节点的前一个节点 Node* newnode = new Node(val); //获取新节点 newnode->_next = cur; //新节点链接前后节点 newnode->_prev = newprev; newprev->_next = newnode; //前后节点的部分指针修改 cur->_prev = newnode; count++; //计数++ } //迭代器区间插入 template<class Inputiterator> void Insert(iterator pos, Inputiterator first, Inputiterator last) { while (first != last) //直接迭代器区间遍历 { T newnode = *first; //获取迭代器里的val Insert(pos, newnode); //pos位置插入 first++; } }
元素删除:
尾删需要修改的节点如下图,头删只是删除的节点位置不同,逻辑一致
上面有头插尾插,这里自然有pop_front(头删)和pop_back(尾删)
void Pop_back() //这两个函数都可以复用Erase { /*Node* cur = _root->_prev; //找到尾节点 Node* Next = cur->_next; //记录尾节点的前后节点 Node* Prev = cur->_prev; delete cur; //这里是直接指向节点的指针所以释放即可 Next->_prev = Prev; //连接前后节点 Prev->_next = Next; count--;*/ //计数-- //复用Erase Erase(end()); } void Pop_front() { /*Node* cur = _root->_next; //这个整体逻辑和上面一致,只换了删除的节点罢了 Node* Next = cur->_next; Node* Prev = cur->_prev; delete cur; Next->_prev = Prev; Prev->_next = Next; count--;*/ Erase(begin()); }
Erase(删除):删除就需要注意一下了,我们通过cur指针获取迭代器里的节点指针,在删除时应该释放迭代器里的指针,删除cur确实能够释放节点,但是!迭代器里的指针依旧指向该空间,那不就是一个野指针了吗?尽管前后节点已经链接起来了,这个问题不会造成结果影响,但不能放过每一个可能出现的错误
下图为错误演示:可以看到我访问那释放后的空间不会报错,获取的值(newval)是随机值
下面是正确做法:
void Erase(iterator pos) { Node* cur = pos._node; //获取迭代器中的节点 Node* Next = cur->_next; //记录前后节点 Node* Prev = cur->_prev; delete pos._node; //释放迭代器中的节点指针,cur在生命周期结束后自动释放 Next->_prev = Prev; //链接节点 Prev->_next = Next; count--; //记数-- } //迭代器区间删除 void Erase(iterator first, iterator last) { Node* cur = first._node; //获取迭代器节点 Node* final = last._node; while (cur!=final) //循环判断 { Node* curnext = cur->_next; //储存下一个节点 Erase(cur); //删除当前节点 cur = curnext; //将下一个节点赋值给cur } }
operator=(赋值),swap(交换),clear(初始化),resize(增大个数或者缩小):
赋值需要借助swap(交换)来辅助完成,如果不借助swap的话,也可以将主链表初始化后遍历赋值链表插入,但那手搓也太麻烦了
void clear() //初始化 { Node* cur = _root->_next; //指向哨兵位的下一个节点 while (cur != _root) //循环删除 { Node* next = cur->_next; delete cur; cur = next; } count = 0; //计数归零 } void swap(list<T>& l1) //交换成员变量 { std::swap(_root, l1._root); std::swap(count, l1.count); } list<T>& operator=(list<T> l1) //这里传参触发拷贝构造 { swap(l1); //交换临时对象和主对象的成员变量 return *this; //返回*this指针 }
resize:这个不太需要多少技巧,是增大到N个就尾插,如果缩小到N个就遍历到N个后将它的_next指针修改和哨兵位的_prev指针修改即可
void resize(size_t n, const T& val=T()) { if (count>n) //判断个数大小 { Node* Re = _root; while (n--) { Re = Re->_next; } Re->_next = _root; _root->_prev = Re; count = n; } else { while (count < n) //这里不需要修改count,尾插的时候他已经自加了 { Push_back(val); } } }
元素修改:
这个需要自己实现一个find(查找),返回迭代器然后直接解引用修改即可。实际上在上面的操作中就已经完成了,剩下的就是需要遍历查找返回即可
iterator Find(size_t val) { auto it = begin(); //遍历查找返回 while (it != end()) { if (*it == val) { return it; } it++; } return nullptr; }
本篇文章的主要内容就到这里了,希望能够对你产生帮助,感谢阅读
下面是所有实现代码和测试用例
template <class T>
struct list_node
{
T _val;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T&val=T())
:_val(val)
,_next(nullptr)
,_prev(nullptr)
{}
};
template <class T,class Ref,class Ptr>
struct list_iterator
{
using Node = list_node<T>;
using self = list_iterator<T,Ref,Ptr>;
Node* _node;
list_iterator(Node*cur)
:_node(cur){}
Ptr operator->()
{
return &_node->_val;
}
Ref operator*()
{
return _node->_val;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self temp (*this);
_node = _node->_next;
return temp;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)
{
self temp(*this);
_node = _node->_prev;
return temp;
}
bool operator==(const self& l1)
{
return _node == l1._node;
}
bool operator!=(const self& l1)
{
return _node != l1._node;
}
};
template <class T >
class list
{
public:
using Node = list_node<T>;
using iterator = list_iterator<T,T&,T*>;
using const_iterator = list_iterator<T, const T&, const T*>;
iterator begin()
{
return _root->_next;
}
iterator end()
{
return _root;
}
const_iterator begin()const
{
return _root->_next;
}
const_iterator end()const
{
return _root;
}
bool Empty()
{
return count == 0;
}
void Size()
{
return count;
}
void Empty_init()
{
_root = new Node;
_root->_next = _root;
_root->_prev = _root;
count = 0;
}
list()
{
Empty_init();
}
list(size_t n, const T& val = T())
{
if (n == 0)
{
Empty_init();
}
else
{
Empty_init();
while (n--)
{
Push_back(val);
}
}
}
list(int n, const T& val = T())
{
if (n == 0)
{
Empty_init();
}
else
{
Empty_init();
while (n--)
{
Push_back(val);
}
}
}
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
if (_root == nullptr)
{
Empty_init();
}
while (first != last)
{
Push_back(*first);
first++;
}
}
list(initializer_list<T> li)
{
Empty_init();
for (auto& ch : li)
{
Push_back(ch);
}
}
~list()
{
Node* cur = _root;
while (cur!=_root)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_root = nullptr;
count = 0;
}
list(const list<T>& l1)
{
Empty_init();
for (auto ch : l1)
{
Push_back(ch);
}
}
T& Front()
{
return (*begin());
}
T& Back()
{
return *(--end());
}
iterator Find(size_t val)
{
auto it = begin();
while (it != end())
{
if (*it == val)
{
return it;
}
it++;
}
return nullptr;
}
void Push_back(const T& val)
{
/*Node* newnode = new Node(val);
Node* ptail = _root->_prev;
newnode->_next = _root;
newnode->_prev = ptail;
ptail->_next = newnode;
_root->_prev = newnode;
count++;*/
Insert(end(), val);
}
void Push_front(const T& val)
{
/*Node* newnode = new Node(val);
Node* pnext = _root->_next;
newnode->_next = pnext;
newnode->_prev = _root;
pnext->_prev = newnode;
_root->_next = newnode;
count++;*/
Insert(begin(), val);
}
void Insert(iterator pos, const T& val)
{
Node* cur = pos._node;
Node* newprev = cur->_prev;
Node* newnode = new Node(val);
newnode->_next = cur;
newnode->_prev = newprev;
newprev->_next = newnode;
cur->_prev = newnode;
count++;
}
template<class Inputiterator>
void Insert(iterator pos, Inputiterator first, Inputiterator last)
{
while (first != last)
{
T newnode = *first;
Insert(pos, newnode);
first++;
}
}
void Pop_back()
{
/*Node* cur = _root->_prev;
Node* Next = cur->_next;
Node* Prev = cur->_prev;
delete cur;
Next->_prev = Prev;
Prev->_next = Next;
count--;*/
Erase(end());
}
void Pop_front()
{
/*Node* cur = _root->_next;
Node* Next = cur->_next;
Node* Prev = cur->_prev;
delete cur;
Next->_prev = Prev;
Prev->_next = Next;
count--;*/
Erase(begin());
}
void Erase(iterator pos)
{
Node* cur = pos._node;
Node* Next = cur->_next;
Node* Prev = cur->_prev;
delete pos._node;
Next->_prev = Prev;
Prev->_next = Next;
count--;
}
void Erase(iterator first, iterator last)
{
Node* cur = first._node;
Node* final = last._node;
while (cur!=final)
{
Node* curnext = cur->_next;
Erase(cur);
cur = curnext;
}
}
void clear()
{
Node* cur = _root->_next;
while (cur != _root)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
count = 0;
}
void swap(list<T>& l1)
{
std::swap(_root, l1._root);
std::swap(count, l1.count);
}
list<T>& operator=(list<T> l1)
{
swap(l1);
return *this;
}
void resize(size_t n, const T& val=T())
{
if (count>n)
{
Node* Re = _root;
while (n--)
{
Re = Re->_next;
}
Re->_next = _root;
_root->_prev = Re;
count = n;
}
else
{
while (count < n)
{
Push_back(val);
}
}
}
private:
Node* _root=nullptr;
size_t count=0;
};
template <class T>
void print_container(const T& node)
{
for (auto ch : node)
{
cout << ch << " ";
}
}
struct AA
{
int a1 = 1;
int a2 = 2;
};
//构造测试
void test()
{
list<int> l1;
print_container(l1);
cout << endl;
list<int> l2(5, 3);
print_container(l2);
cout << endl;
vector<int> v1({1, 2, 3, 4, 5, 6, 7, 8});
list<int> l3(v1.begin()+1, v1.end()-2);
print_container(l3);
cout << endl;
list<int> l4({ 8,8,8,8,8,8,8 });
print_container(l4);
cout << endl;
list<int> l5(l3);
print_container(l5);
}
//插入测试
void test1()
{
vector<int> v1({ 1, 2, 3, 4 });
list<int> l1;
for (auto ch : v1)
{
l1.Push_back(ch);
}
print_container(l1);
cout << endl;
list<int> l2;
for (auto ch : v1)
{
l2.Push_front(ch);
}
print_container(l2);
cout << endl;
l1.Insert(l1.begin(), 8);
print_container(l1);
cout << endl;
l1.Insert(++l1.begin(), v1.begin(), v1.end());
print_container(l1);
cout << endl;
}
//删除和修改测试
void test2()
{
list<int> l1({1,2,3,4,5,6,7,8});
l1.Erase(++l1.begin());
print_container(l1);
cout << endl;
l1.Erase(++l1.begin(), --l1.end());
print_container(l1);
cout << endl;
for (auto& ch : l1)
{
ch *= 10;
}
print_container(l1);
cout << endl;
list<int> l2({ 1,2,3,4,5,6,7,8 });
auto it = l2.Find(4);
*it = 9;
print_container(l2);
cout << endl;
}