list
- 1.list概述
- 2.list的数据结构
- 3.list的迭代器(operator)
- 4.list的接口设计
- push_back()
- insert()
- erase()
- 其余接口
1.list概述
好说,相信能点进这篇文章的大部分人应该都了解和学习过list
——链表
,
相较于vector
——顺序表
(可随机访问),list
的好处就在于它不需要开辟一些可能会浪费的空间
,在它每次插入和删除元素的同时,对于堆栈上的空间就只会相对应的配置和释放一个元素内存。
这也同时给它赋予另外一个好处:
插入push
, 删除pop
的运算时间都只用常数时间O(N)
关于这两个最常用容器的选择,可以去参考网上文章,这里就不做赘述。
2.list的数据结构
list
是链表,我们可以先理解为是一条绳子,那么绳子就应该将东西穿起来才能最后构成链表这一数据结构。
所以node
的结构和list
应该是不一样的,我们需要分开来进行设计
我们来先看看开发者团队的源代码
template <class T>
struct __list_node {
typedef void* void_pointer;
void_pointer next;
void_pointer prev;
T data;
};
看到next和prev就应该知道了,这就是一个双向链表
我们也来自己搭建一个节点node
的结构类:
2.1节点模仿代码
template <class T>
struct _list_node//节点
{
T _val;
_list_node<T> *_next;
_list_node<T> *_prev;
_list_node(const T &val) //要加引用,不然string vector 就会进行拷贝构造
: _val(val), _prev(nullptr), _next(nullptr)
{
}
};
这里我们和源码不同的是,我们在这里要加入一个构造函数进行初始化。
原因是:我在这构建的双向链表,是带哨兵位
的双向链表,所以一定要先初始化。
深受一些大佬的代码影响,本人也很喜欢并推荐大家使用
_下划线
来加在成员变量前面,便于之后于成员函数的区分。(尤其是初始化列表中)
2.2再来看看list
的结构
开发者源代码:
template <class T,class Alloc = alloc>
class list
{
protected:
typedef __list_node<T> list_node;
public:
typedef list_node* link_type;
protected:
link_type node;
...
}
这里的list是一个结构非常完美的环形双向链表,所以只要找到了end
节点,就可以来回遍历整个链表。
3.list的迭代器(operator)
看过我上一篇文章的同学应该知道,在设计vector
的时候,迭代器的构造其实就是原生指针
的使用。
但这里不大一样,我们要了解,vector
这样的顺序表,在堆栈内存空间上是连续的,所以利用原生指针
可以很好的对vector
进行操作(*,++,–等)。
但list
不同,因为其节点不保证在储存空间中连续存在,所以不能像vector
那样,用原生指针作为迭代器。
设计的思路是,list
的迭代器必须要有能力指向list
的内存,然后还要具备能力,能够进行正确的++,--,*,&....
那么我们设计一个_list_iterator
这样的类来对原生指针进行封装,并在此类里面设计一系列功能(前移,后移,运算符重载等)。
3.1开发者源代码:
template<class T, class Ref, class Ptr>
struct __list_iterator {
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
typedef __list_iterator<T, Ref, Ptr> self;
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T>* link_type;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
link_type node;//节点指针
__list_iterator(link_type x) : node(x) {}
__list_iterator() {}
__list_iterator(const iterator& x) : node(x.node) {}
bool operator==(const self& x) const { return node == x.node; }
bool operator!=(const self& x) const { return node != x.node; }
reference operator*() const { return (*node).data; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
self& operator++() { //往后一个节点(前置++
node = (link_type)((*node).next);
return *this;
}
self operator++(int) { //往后一个节点(后置++
self tmp = *this;
++*this;
return tmp;
}
self& operator--() { //往前一个节点(前置--
node = (link_type)((*node).prev);
return *this;
}
self operator--(int) { //往前一个节点(后置--
self tmp = *this;
--*this;
return tmp;
}
};
但在这,我在第一次见源代码的时候其实就很困惑,为啥会有三个模版参数呢?!
T
很好理解,但是Ref
和Ptr
呢?
先模仿这个框架先搭建一个阉割版迭代器类:
template<class T>
struct _list_iterartor//用struct相当于成员全是public
{
typedef _list_node<T> node;
typedef _list_iterator<T> self;
node* _pnode;
...
T& operator*()
{
return _pnode->_val;
}
...
}
利用上面写的迭代器,那么我们的list
类中,正确使用迭代器就成了我们的问题
template<class T>
class list
{
typedef _list_node<T> node;
public:
typedef _list_iterator<T> iterator;
...
}
就拿比较通俗易懂的例子来说,如果我创建了一个printList
这样的函数来访问list
中的成员,利用刚刚创建的迭代器,我们是可以对类中的成员进行修改的!而这样我们的封装就失去了意义,因为有时候我们是不想用户能够修改我的成员变量。
void printList(const list<int>& lt)
{
list<int>::iterator it = lt.begin();
while(it != lt.end())
{
*it += 1;//这串代码不会报错,且顺利修改成员变量
cout << *it << " ";
++it;
}
cout << endl;
}
所以在这里肯定是需要一个,只可读不可写的迭代器,也就是我们待会写的const_iterator
typedef _list_iterator<T> const_iterator
单单这样修改肯定还是没用的,因为就算你将刚刚printList
函数中的iterator
修改成const_iterator
也没有用,因为在printList
中,模版参数给的是const
类型的list
类,返回给it
同样也不起作用,因为it
本身就属于非const
类型,只是用了一个带有const
的类型名字罢了。
有两种解决办法:
- 1.设计两个类来封装(const和非const)
- 2.设计一个带有三个模版参数的类
先来说说传统的第一种解决思路吧:
这个好想,就是把刚刚的_list_iterator
这一个类拷贝一份,设计一份新的_list_const_iterator
来控制const类型的迭代器。
template<class T>
struct _list_iterartor//非const
{
typedef _list_node<T> node;
typedef _list_iterator<T> self;
node* _pnode;
...
T& operator*()
{
return _pnode->_val;
}
...
}
template<class T>//const类型
struct _list_const_iterator
{
typedef _list_node<T> node;
typedef _list_iterator<T> self;
node* _pnode;
...
const T& operator*()
{
return _pnode->_val;
}
...
}
这样就可以很好控制了
但是这样的代码有点冗余了,所以介绍一下第二种方法,也就是开发者团队的设计的巧妙方法。
template <class T, class Ref, class Ptr>
struct _list_iterator
{
typedef _list_node<T> node;
typedef _list_iterator<T, Ref, Ptr> self;
node* _pnode;
Ref iterator*()//reference
{
return _pnode->_val;
}
Ptr iterator->()//pointer
{
return &_pnode->_val;
}
...
}
template <class T>
class list
{
typedef _list_node<T> node;
public:
typedef _list_iterator<T, T &, T *> iterator;
typedef _list_iterator<T,const T &, const T *> const_iterator;
...
private:
node* _head;
}
这样的话,你传的参数不管是const
也好还是非const
也罢,都可传回到Ref
和Ptr
进行相对应的操作。
其实实际上是让编译器去帮我们将类模版实例化出了两个类,本质和方法一没有太大区别
接下来放出自己代码中关于迭代器的接口和重载函数
template <class T, class Ref, class Ptr> //Ref - T&
struct _list_iterator//封装_list_node的普通指针,作用其实类似
{
typedef _list_node<T> node;
typedef _list_iterator<T, Ref, Ptr> self;
node *_pnode; //对链表指针进行封装
//拷贝函数,operator=,析构我们不写,编译器默认生成就可以用
//构造函数
_list_iterator(node *node)
: _pnode(node)
{
}
Ref operator*() //读和写
{
return _pnode->_val;
}
Ptr operator->()
{
return &_pnode->_val;
}
bool operator!=(const self &s)
{
return _pnode != s._pnode;
}
bool operator==(const self &s)
{
return _pnode == s._pnode;
}
bool operator!=(const self &s) const
{
return _pnode != s._pnode;
}
bool operator==(const self &s) const
{
return _pnode == s._pnode;
}
self &operator++()
{
_pnode = _pnode->_next;
return *this;
}
self &operator--()
{
_pnode = _pnode->_prev;
return *this;
}
self operator++(int)
{
self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
self operator--(int)
{
self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
};
4.list的接口设计
4.1 push_back —— 尾插
作为构造链表的重要接口之一,其实如果学过数据结构的大伙们都知道,push_back
其实挺好写的,将结构牢记于心就可以慢慢搭建出这个结构来。
void push_back(const T &x) //传引用
{
node *newnode = new node(x); //调用构造函数初始化
node *_tail = _head->_prev;
_tail->_next = newnode;
newnode->_prev = _tail;
newnode->_next = _head;
_head->_prev = newnode;
}
4.2 insert —— 插入
插入也好说,先配置并构造一个节点,然后在pos后面插入元素x
void insert(iterator pos, const T &x) //插入数据
{
assert(pos._pnode);
node *cur = pos._pnode;
node *prev = cur->_prev;
node *newnode = new node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
}
这里不同于
vector
插入insert
并不会使得迭代器会失效,因为链表的数据本身在内存中就是随机散布开的。
4.3 erase —— 删除
erase
这里的设计比较巧妙,他会将删除节点位置处的下一个迭代器返回回来
iterator erase(iterator pos)
{
node *prev = pos._pnode->_prev;
node *next = pos._pnode->_next;
delete pos._pnode;
prev->_next = next;
next->_prev = prev;
return iterator(next);
}
4.4 其余接口 —— pop_front pop_back push_front clear ~list
其余这些,我们就疯狂复用刚刚实现的接口就行了
void pop_front()
{
erase(begin());
}
void push_front(const T& x)
{
insert(begin(),x);
}
void pop_back()
{
erase(end());
end();//别忘了调整水位
}
void clear()
{
iterator it = begin();
while(it != end())
{
it = erase(it);
}
}
~list()
{
clear();
delete _head;//在list类中的成员变量
_head = nullptr;
}
这样,基本的功能就都实现了~