list,简单来讲,我们可以将其看做“一个带头双向循环链表”。它的每个数据块都是通过指针互相连接起来的。这里“带头”的意思是指它带有一个哨兵位头节点,哨兵位指向下一个数据块和尾部数据块的位置,不保存数据;“双向循环”是指这里面的数据是以双指针首尾相连的,其中尾结点与头节点相连。
在list中,就不再支持“[]”方式的随机访问了,需要访问时就必须要使用“迭代器”。并且其具体使用方式和vector基本一样,这里就不做过多演示,而是简单介绍一下list中的部分接口的作用
在list中,有正向和反向两种迭代器。上图是正向迭代器中的beign()和end()。可以看到,这里进行了函数重载,提供了const类型和非const类型的迭代器。
要获取头节点和尾结点的数据也很简单,直接调用front()和back()函数即可。同样的,为了支持const和非const类的传入,也是分别提供了两个函数接口。
如果要在头部插入,使用push_front();如果要在头部删除,则使用pop_front()
如果要在尾部插入,使用push_back();如果要在尾部删除,则使用pop_back()
要在任意位置插入数据,使用insert()函数即可。一般来讲,我们最常用的是第一个insert()函数,第二个在pos位置插入n个val和在pos位置插入另一个类的迭代器区间数据都并不常用。
在vector中,我们知道vector因为可以看做是以数据的形式存在的,所以如果在除了尾部的尾部插入数据需要进行数据挪动,效率比较低,不建议使用。但是在list中,因为它的数据可以看做是链表链接起来的,所以在任意位置插入数据都可以看做是O(1)的时间复杂度。当然,O(1)的前提是你已经知道了要插入的数据的位置。因此当需要频繁在中间位置插入或删除数据时,推荐使用list
要在任意位置删除数据,使用erase()即可。第一个接口为删除pos位置的数据,第二个接口为删除一个迭代器区间的数据
数据交换的逻辑很简单,只需要交换两个list的头节点即可。
resize()函数可以用来插入节点,当然,该函数是从尾部开始插入的,会插入n个val。同时,resize()还可以用来删除数据,按照尾删的方式,将你的节点个数删除到只剩下n个
如果我们想删除保存了某个数据的节点,就可以使用remove()函数。该函数会在list中查找val值,如果找到了存有val的节点,就删除该节点。如果没有找到,就什么都不做
unique()函数可以供我们将list中的数据去重。但是要注意,该函数只能在list有序的情况下才能正确运行。也就是说,如果我们要用该函数,就必要要保证对应的list中的数据是有序的。
splice()函数可以将一个节点上的数据转移到另一个list上。这里的转移并不是指拷贝,转移走的数据在原list中会被删除
list的函数实现上和vector相比,其实并没有什么难度。但是其某些函数实现上,却和vector上有着很大的不同
因此,在这里就不再逐一讲解各个函数的逻辑实现,而是挑选其中比较有价值的几个实现具体讲解。
以下是模拟实现部分list的代码:
#pragma once
#include
#include
#include
using namespace std;
namespace MyList
{
template
struct ListNode//list中存储指向上下节点的指针和数据块中的数据
{
ListNode* _next;
ListNode* _prev;
T _data;
ListNode(const T& x)//构造函数
: _next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
template
struct list_iterator//迭代器,每个迭代器都是一个类ListNode的指针
{
typedef ListNode node;
typedef list_iterator Self;
node* _pnode;
list_iterator(node* _pn)//构造函数
: _pnode(_pn)
{}
Ref operator*()//运算符*重载
{
return _pnode->_data;
}
Self& operator++()//运算符++重载
{
_pnode = _pnode->_next;
return *this;
}
Self& operator--()//运算符--重载
{
_pnode = _pnode->_prev;
return *this;
}
Ptr operator->()//运算符->重载
{
return &_pnode->_data;
}
bool operator!=(const Self& it) const//迭代器是否相等
{
return _pnode != it._pnode;
}
bool operator==(const Self& it) const
{
return _pnode == it._pnode;
}
};
template
class list
{
typedef ListNode node;//结构体重命名为node,方便使用
public:
typedef list_iterator iterator;//迭代器类重命名
typedef list_iterator const_iterator;//const迭代器重命名
void empty_initialize()//空初始化(创建哨兵位)
{
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
//默认成员函数
list()//构造函数
{
empty_initialize();
}
template
list(inputiterator first, inputiterator last)//迭代器区间构造
{
empty_initialize();
size_t size = 0;
while (first != last)
{
push_back(first._pnode->_data);
++first;
++size;
}
_size = size;
}
list( list& lt)//构造函数
{
empty_initialize();
list tmp(lt.begin(), lt.end());
swap(tmp);
_size = tmp._size;
}
~list()//析构函数
{
clear();
delete[] _head;
_head->_prev = _head->_next = nullptr;
_head = nullptr;
}
list& operator=(list lt)//运算符=重载
{
swap(lt);
return *this;
}
//迭代器
iterator begin()//普通迭代器
{
return iterator(_head->_next);
}
iterator end()//普通迭代器
{
return iterator(_head);
}
const_iterator begin() const//const迭代器
{
return const_iterator(_head->_next);
}
const_iterator end() const//const迭代器
{
return const_iterator(_head);
}
//Capacity(容量相关):
size_t size() const//链表中的数据个数
{
return _size;
}
bool empty() const//链表是否为空
{
return _size == 0;
}
//(数据操作相关)Modifiers:
iterator insert(iterator pos, const T& val)//在任意pos位置插入val
{
node* newnode = new node(val);
node* tail = pos._pnode->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = pos._pnode;
pos._pnode->_prev = newnode;
++_size;
return iterator(newnode);
}
iterator erase(iterator pos)//删除pos位置的值
{
assert(pos != end());
node* tail = pos._pnode->_prev;
node* cur = pos._pnode->_next;
tail->_next = cur;
cur->_prev = tail;
--_size;
delete pos._pnode;
return iterator(cur);
}
void push_back(const T& x)//尾插
{
insert(end(), x);
}
void pop_back()//尾删
{
erase(--end());
}
void push_front(const T& x)//头插
{
insert(begin(), x);
}
void pop_front()//头删
{
erase(begin());
}
void clear()//清除list除哨兵位的所有节点
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
void swap(list& lt)//哨兵位交换
{
std::swap(_head, lt._head);
}
private:
node* _head;//头节点(哨兵位)
size_t _size;//节点个数
};
}
仔细观察上面的代码,我们可以发现,这里的迭代器iterator不再是像vector和string中那样,采用原生指针typedef的方式实现迭代器。而是用类模板重新封装。不知道大家听过这么一句话没,“迭代器只是一个类似指针,和指针有着类似功能的东西”。看了上面的代码大家应该就能明白,在stl的某些组件中,是可以用原生指针来当做迭代器的。但是在某些组件中却不行。原因很简单,可以使用原生指针当做迭代器,仅仅只是一种巧合。
以vector为例,vector我们可以将其看做 一个类似数组的东西,它所存储的数据都是在一段连续空间之上。因此,通过指针++,--等形式,我们就可以遍历整个空间。但是list却不同,list可以看成“带头双向循环链表”,它的每个数据块都是独立的,通过存储在其数据块内的指针找到上下数据块的位置。从存储结构上看,并不连续。
在list上是无法通过原生指针满足迭代器需求的。因此,我们就需要在重新写一个类域,用于实现迭代器。
在这里,我们的写了一个是struct ListNode类,用于存储节点和数据。
因此,在实现迭代器的struct list_iterator类中,我们需要一个指向ListNode
为了简化书写,这里进行了typedef。有了这个类后,要实现对应的功能,如解引用、++、--等功能,就很简单了。我相信这对绝大部分人来说都不是难事。这里就不过多讲解了。
但是,看了上面的代码,我相信很多人都会有一个疑惑,为什么这里的类模板中参数有三个?这其实就和iter要实现的功能有关了。我们知道,在使用iterator时,我们不仅会使用非const修饰的迭代器,也可能会使用const修饰的迭代器。由const修饰的迭代器的功能主要是用于防止修改对应节点中的数据,而不是阻止迭代器访问。
我们查看库中的list提供的迭代器接口,也可以发现它是提供了const和非const两种形式的:
但是我们知道,const迭代器和非const迭代器其实只有一个差异,就是const迭代器可以修改数据,但是非const迭代器不能修改数据。为了实现这一功能,如果我们不写上第二个类模板参数,就需要将这个类模板重新拷贝一份,再将里面的*重载的返回值加上const修饰。这个方法虽然可以解决问题,但是却存在大量的重复代码,导致代码冗余。是一种非常不推荐的方式。因此,我们采用第二种解决方案,就是在原有的类模板中再加上一个参数。
我们知道,类模板会根据传入参数的不同初始化不同的类。利用这一特性,我们在类模板中加入一个“Ref”参数,并将Ref参数设置为运算符*重载的返回值。
然后再在我们的list类中,重命名一个“const_iterator”迭代器,该迭代器的类型为“list_iterator
通过这两个重命名,我们就确定了两种参数传入的模式。第一个传入的参数相同,但第二个就会有是否有const修饰的差异。由此,我们构造出const迭代器和非const迭代器:
编译器会根据传入的参数的不同,自动匹配对应的迭代器。
迭代器中的第三个参数,则与运算符->重载有关。“->”的重载比较奇怪,与运算符“*”重载返回解引用不同,运算符“->”重载返回的是要访问的数据的地址:
原因也很简单,我们知道,要访问类的数据,我们有两种方式,其中“.”是用类名访问,“->”则是用指针访问。因此在有指针的情况下,我们是可以用“指针->变量名”来访问变量的。但是,如果我们此时没有指针但又想用“->”来访问呢?要知道,例如在list中,都是用迭代器访问的,而迭代器实际上是一个类,而非指针。
在上图中,我们给list中传入了一个Pos结构体,为了便于访问,我们写了一个iterator it来进行迭代器访问。在访问的过程中,我们需要使用“(*it)._row”来进行访问。虽然这种访问方式可行,但是实际使用起来却是比较麻烦的。
通过重载“->”运算符的方式,我们就可以用以下方式访问:
在这里,“it->”实际上就是一个函数。但是就算是一个函数,大家也可能很奇怪,因为当其为函数时,“it->”实际看起来应该是“it.operator->() ”,它在上图的代码中返回的是一个“Pos*”指针,此时“it->_row”就可以看成“Pos*_row”。很明显,这样是无法访问数据的。这就说明“it->_row”的使用方式是错误的,正确的写法应该是“it->->_row”,这样才会被视为“Pos*->_row”。但是,如果我们用这种写法,就会导致写出的代码可读性较差,且会比较麻烦。因此,系统为了提高代码的可读性,就做了特殊处理,使我们重载了“->”后,在使用该运算符时可以省略一个“->”。因此,虽然写出来是“it->_row”,但是在系统中其实是被视为“it->->_row”的。
如果仅仅只是上述原因,其实并不需要在类模板中多加一个模板参数,其根本原因是,这里的“->”也可能传入const对象,以禁止对对应地址上的数据进行修改。由此,基于满足const和非const对象的需求,提供第三个类模板参数,以根据传入的第三个参数的类型进行不同的实例化: