目录
一、list的介绍和基本使用的方法
1.1 list的介绍
同样的给大家上图。
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- . list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
- 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
- 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;(简单来说就是不支持随机访问)。
1.2 list的基本使用方法
1.2.1 构造方法
常用的构造方法主要是以下4种:
- list (size_type n, const value_type& val = value_type())
构造的list中包含n个值为val的元素 list<int> st(10,1);
- list()
构造空的list list<int> st;
- list (const list& x)
拷贝构造函数 list<int> l1(10,1); list<int> l2(l1);
- list (InputIterator first, InputIterator last)
用[first, last)区间中的元素构造list,即使用迭代器进行构造 list<int> l1(10,1); list<int> l2(l1.begin(),l1.end());
1.2.2 迭代器
对于list的迭代器,我们可以暂且认为其是指向list中某个节点的指针,等到模拟实现的时候再给大家详细介绍。这里大概给大家画一下begin()和end()的指向。
1.2.3 容量相关的接口
- empty()
检测list是否为空,是返回true,否则返回false - size()
返回list中有效节点的个数
1.2.4 增删查改的相关接口
push_front()
在list首元素前插入值为val的元素 pop_front()
删除list中第一个元素 - push_back()
在list尾部插入值为val的元素 - pop_back()
删除list中最后一个元素 - insert()
在list position 位置中插入值为val的元素 - erase()
删除list position位置的元素 - swap()
交换两个list中的元素 - clear()
清空list中的有效元素
1.3 关于list迭代器失效的问题
迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
二、模拟实现list
2.1 节点类
这里采取的是带头双向列表,如果你不是很了解的话可以参考一下http://t.csdnimg.cn/uSKci
我的这篇文章。
话不多说,上代码。
template<class T>
struct ListNode
{
ListNode(const T& val = T())
:_pPre(nullptr)
,_pNext(nullptr)
,_val(val)
{}
ListNode<T>* _pPre;
ListNode<T>* _pNext;
T _val;
};
由于不知道模板参数,所以我们的默认初始化不能定死,而是采用了一个匿名对象,原本内置类型是没有类似于 T() 这种构造方式的,但是为了方便我们类的定义,祖师爷们特意进行了添加。
2.2 迭代器类
当大家看到这个标题的时候就可能会疑惑了,为什么要重新定义一个迭代器类呢,为什么不像vector一样,直接对节点重命名不久好了吗?这确实是一个值得我们探讨的问题,要回答这个问题,我们还是要回到迭代器的本质,我在上节就有提到过:
迭代器提供了一种统一的访问数据结构(如容器、数组、集合等)元素的方式,使得我们能够以统一的接口遍历和操作数据结构中的元素,而不必关心底层数据结构的实现细节。
如果要满足这个需求,简单的重命名可不好使呀,首先就是范围for你无法支持,vector可以是因为其在内存中是连续的,其++和--都不需要进行重载,而list就不行,因为list所存储数据的内存是不连续的,你要是用老一套的方法肯定是不行的,所以我们才需要将list的迭代器单独封装起来,这恰恰就体现了C++中面向对象的思想。
解决这个问题之后,我们又迎来了新的问题,const 迭代器我们该怎么实现呢?这时你肯定会说直加个const不就行了吗,答案是显而易见的,肯定不行,不然我就不会问了。那么为什么呢,那是因为这个迭代器是我们封装实现的,我们在list类中直接加const的话,相当于是对我们做的迭代器实例化出来的对象加了个const,那就是对我们所封装的节点加了个const,但节点是指针啊,要清楚,并不是指针不能动(不然范围for又死了),而是所指向的数据域不能改变,那我们应该怎么办呢?这里就需要好好的使用我们的模板了,看我的解决方案。
template<class T, class Ref, class Ptr>
class ListIterator
{
typedef ListNode<T>* PNode;
typedef ListIterator<T, Ref, Ptr> Self;
public:
ListIterator(PNode pNode = nullptr)
:_pNode(pNode)
{}
ListIterator(const Self& l)
:_pNode(l._pNode)
{}
Ref operator*()
{
return _pNode->_val;
}
Ptr operator->()
{
return &_pNode->_val;
}
Self& operator++()
{
_pNode = _pNode->_pNext;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_pNode = _pNode->_pNext;
return tmp;
}
Self& operator--()
{
_pNode = _pNode->_pPre;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_pNode = _pNode->pPre;
return tmp;
}
bool operator!=(const Self& l)
{
return _pNode != l._pNode;
}
bool operator==(const Self& l)
{
return _pNode == l._pNode;
}
PNode _pNode;
};
可以看的我的模板有三个参数template<class T, class Ref, class Ptr> 那这玩意有什么用呢,这后面两个参数主要作用于该类有返回节点数据域的成员函数,const对象和非const对象最大的区别就是一个内容无法被修改一个不受限制,而我们通过对节点的封装,使得外界通过迭代器访问我们数据域时,就一定会访问我们返回节点数据域的重载函数,而这样恰恰给我们模板提供了机会,我们可以根据模板传参去重载我们的返回值,这样也就实现了我们const迭代器的建立。但是这不是最妙的地方,因为要实现这个要求我们也可以复制粘贴改一下条件多写一个类就可以了,可当我们使用了模板之后,你会发现这部分工作就交给编译器去做了,实现了我们的代码复用,这就是泛型编程的好处。
2.3 主类list类
2.3.1 成员变量的建立
这里我们主要的成员变量就是我们的哨兵位节点和我们有效节点size,为了方便初始化,我还加了一个专门初始化头节点的成员函数。
private:
void CreateHead()
{
_pHead = new Node();
_pHead->_pNext = _pHead;
_pHead->_pPre = _pHead;
_size = 0;
}
PNode _pHead;
size_t _size;
2.3.2 迭代器的建立和相关接口的实现
有关迭代器的主要内容已经在刚刚讲的差不多了,这里我就不在过多的讲解了,直接上代码。
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return _pHead->_pNext;
}
iterator end()
{
return _pHead;
}
const_iterator begin() const
{
return _pHead->_pNext;
}
const_iterator end() const
{
return _pHead;
}
2.3.3 增删查改以及容量接口的实现
这里接口的实现和我们双向链表以及vector的实现方式很像,我这里就不啰嗦了,有问题可以参考http://t.csdnimg.cn/uSKci和http://t.csdnimg.cn/N3bhY或者私信问我也是可以的哦。
1.size()
size_t size()const
{
return _size;
}
2.empty()
bool empty()const
{
return _size == 0;
}
3.insert()
iterator insert(iterator pos, const T& val)
{
PNode tmp = new Node(val);
PNode cur = pos._pNode;
PNode pre = pos._pNode->_pPre;
tmp->_pNext = cur;
tmp->_pPre = pre;
pre->_pNext = tmp;
cur->_pPre = tmp;
_size++;
return tmp;
}
4.push_back()
void push_back(const T& val)
{
insert(end(), val);
}
5.pop_back()
void pop_back()
{
erase(--end());
}
6.erase()
iterator erase(iterator pos)
{
PNode Next = pos._pNode->_pNext;
PNode pre = pos._pNode->_pPre;
pre->_pNext = Next;
Next->_pPre = pre;
_size--;
return Next;
}
7.clear()
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
8.swap()
void swap(list<T>& l)
{
std::swap(_pHead, l._pHead);
std::swap(_size, l._size);
}
2.3.4 构造函数和析构函数
废话不多说,直接开造!
list()
{
CreateHead();
}
list(int n, const T& value = T())
{
CreateHead();
while (n--)
{
push_back(value);
}
}
template <class Iterator>
list(Iterator first, Iterator last)
{
CreateHead();
while (first != last)
{
push_back(*first);
first++;
}
}
list(const list<T>& l)
{
CreateHead();
for (auto ch : l)
{
push_back(ch);
}
}
list<T>& operator=(list<T> l)
{
CreateHead();
swap(l);
return *this;
}
~list()
{
clear();
delete _pHead;
_pHead = nullptr;
_size = 0;
}