- list作为一个容器组件,有着其重要的价值,其实在底层这是一个带头双向循环的链表数据结构,可以在任意位置进行插入和删除的序列式容器,并且该容器可以前后进行遍历。
- 那么同样作为序列式容器,list和vector相比又有哪些优缺点呢?
- 优点:对于在任意位置进行插入、删除操作,list的效率相对于vector是高很多的(这是由于vector在进行这些操作时需要移动数据进行覆盖)
- 缺点:list不支持任意位置的随机访问,这是由于list内部数据的存储方式并不是连续的,随机访问所需的代价较高,并且对缓存的利用率较低
因此,同样作为序列式容器,vector和list是优势互补,不可替代的,并且list在实际中的使用频率也很高,所以,为了更好的使用list,我们需要进行模拟实现。
在进行模拟实现之前,我们首先需要直到STL的list的各种使用接口以及其功能,然后才能高效的使用和模拟,下面是stl_list的使用文档及其说明:
list使用文档
首先,我们直到对于带头双向循环链表来说,每个数据是存在一个节点中的,这个节点还要包括指向前一个节点和后一个节点的指针,所以我们需要用一个结构体来封装这些信息。
template<typename T>
struct list_node
{
list_node* next;
list_node* prev;
T val;
};
在将要模拟实现的类中,我们想要找到所有的数据,只需要保存头节点即可,并且为了更简单的获得类的成员数量,可以在加一个size_t的_size
成员标式数量。
template<typename T>
class list
{
private:
//这里为了方便,仿照源代码使用了typedef
//并且使用了c++11的缺省参数语法
typedef list_node<T> link_node;
link_node* _head = nullptr;
size_t _size = 0;
}
带头双向循环链表的构造函数要求创建一个头节点,并且让其头节点的next
和prev
都指向自身。
list()
:_head(new link_node)
{
_head->next = _head;
_head->prev = _head;
_head->val = T();
}
我们知道,迭代器实现了stl容器和算法的分离,对于vector来说,迭代器使用原生的指针进行typedef来即可,但这是由于vector内部数据在物理空间上是连续的,但是对于list来说其数据在物理空间上并不是连续的,所以对于list来说,他的迭代器就不能是指针,而是一个类,通过重载运算符的方式,做到其使用方法能够和指针相同(对迭代器++使得指针指向next,–使得指针指向prev)下面是实现:
template<class T>
struct __list_iterator
{
typedef list_node<T> Node;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{}
T& operator*()
{
return _node->_val;
}
T* operator->()
{
return &_node->_val;
}
__list_iterator<T>& operator++()
{
_node = _node->_next;
return *this;
}
__list_iterator<T> operator++(int)
{
__list_iterator<T> tmp(*this);
_node = _node->_next;
return tmp;
}
bool operator!=(const __list_iterator<T>& it)
{
return _node != it._node;
}
bool operator==(const __list_iterator<T>& it)
{
return _node == it._node;
}
};
这里,有人可能会不知道operator->()函数有什么用,那么可以看下面的代码:
lzz::list<pair<int, int>> lt;
lt.push_back({ 1,1 });
lt.push_back({ 2,2 });
lt.push_back({ 3,3 });
lt.push_back({ 4,4 });
lt.push_back({ 5,5 });
auto it = lt.begin();
while (it != lt.end())
{
//这里其实应该是it->->first(it.operator->()->first)
//c++为了代码观赏性,对其进行了优化
cout << it->first << " " << it->second << endl;
it++;
}
对于结构体,为了使得使用方式和指针一样,所以重载了该运算符。并且由于list的数据结构进行随机访问是极为低效的**o(n)**的复杂度,所以list的迭代器并没有支持+功能。
那么又该如何设计const_iterator呢?通过分析我们会发现,对于const_iterator类,只有operator*()
和operator->()
函数需要修改返回值,其他则不需要变,如果重新写一个类命名为const_iterator,就过于冗余了,那么有什么更好的解决方法呢?
这里参考了库里的巧妙的实现方式,通过巧用模板完美的解决了这个问题,看下面代码:
//这里传输三个引用是为了让const引用和const指针能够不用重复设计
template<typename T, typename Ref, typename Ptr>
//为了便于在list里面使用迭代器,用struct而不是类
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> self;
/*迭代器成员*/
Node* _node;
//构造函数
__list_iterator(Node* x = nullptr)
:_node(x)
{}
//析构函数
//不需要析构函数,根据需求,iterator消失并不代表数据消失
Ref operator*() { return _node->val; }
self& operator++()
{
_node = _node->next;
//返回的都是iterator
return *this;
}
//这里比较直接用const防止权限放大问题
bool operator!=(const __list_iterator& it) { return _node != it._node; }
Ptr operator->() { return &(_node->val); }
self operator++(int)
{
self tmp(*this);
_node = _node->next;
return tmp;
}
bool operator==(const self& it) { return _node == it._node; }
self& operator--()
{
_node = _node->prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->prev;
return tmp;
}
};
上面代码多传入了两个模板参数,分别是Ref和Ptr,并且分别用其替代了operator*()和operator->()的返回值,然后通过控制模板参数就可以控制返回值的类型了!
在类中可以使用typedef来进行使用,如下:
class list
{
typedef __list_iterator<T, T&, T*> iterator;
//错误的设计,类比vector
//typedef __list_iterator const_iterator;
//如果使用const T类型,就表示了内部指针不能够进行改变,那如何修改他指向下一个或上一个指针?
typedef __list_iterator<T, const T&, const T*> const_iterator;
}
需要啥传啥,就能够进行更好的控制了!
**begin()*直接返回头节点的下一个指针(第一个位置指向的指针),利用隐式类型转换自动构造迭代器
**end()*直接返回头节点,利用隐式类型转换构造迭代器
iterator begin() { return _head->next; }
iterator end() { return _head; }
const_iterator begin() const { return const_iterator(_head->next); }
const_iterator end() const { return const_iterator(_head); }
对于链表来说,insert和erase操作都是在修改数据之间的链接关系罢了,大家可以画图理解,但是要注意链接前后关系
iterator insert(const iterator& pos, const T& val)
{
link_node* tmp = new link_node;
tmp->val = val;
tmp->next = pos._node;
tmp->prev = pos._node->prev;
pos._node->prev->next = tmp;
pos._node->prev = tmp;
//记得维护_size
++_size;
return tmp;
}
iterator erase(iterator pos)
{
//不能删去头节点
assert(pos != end());
link_node* tmp = pos._node;
tmp->prev->next = tmp->next;
tmp->next->prev = tmp->prev;
//别忘了释放空间
++pos;
delete tmp;
--_size;
return pos;
}
对于这部分,可以直接复用insert和erase即可,这也是先实现insert和erase的原因
void push_back(const T& val) { insert(end(), val); }
void pop_back()
{
assert(!empty());
erase(--end());
}
void push_front(const T& val) { insert(begin(), val); }
void pop_front()
{
assert(!empty());
erase(begin());
}
同样由于list随机访问极为低效,所以该容器并不支持
operator[]()
函数功能
T& front() { return _head->next->val; }
const T& front() const { return _head->next->val; }
T& back() { return _head->prev->val; }
const T& back() const { return _head->prev->val; }
size_t size() { return _size; }
在通过遍历一个个delete
节点时,要记得先保存下一个节点,否则析构之后再使用当前节点就出现了野指针问题。
void clear()
{
//不删除头节点
link_node* del = _head->next;
while (del != _head)
{
link_node* ne = del->next;
delete del;
del = ne;
}
_size = 0;
}
析构与clear()的主要区别就是析构需要释放头节点,而clear()函数不需要清空,所以析构可以先调用clear()函数,然后再释放头节点即可。
~list()
{
clear();
delete _head;
_head = nullptr;
}
#pragma once
#include
namespace lzz
{
template<typename T>
struct list_node
{
list_node* next;
list_node* prev;
T val;
};
//这里传输三个引用是为了让const引用和const指针能够不用重复设计
template<typename T, typename Ref, typename Ptr>
//为了便于在list里面使用迭代器,用struct而不是类
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> self;
/*迭代器成员*/
Node* _node;
//构造函数
__list_iterator(Node* x = nullptr)
:_node(x)
{}
//析构函数
//不需要析构函数,根据需求,iterator消失并不代表数据消失
Ref operator*() { return _node->val; }
self& operator++()
{
_node = _node->next;
//返回的都是iterator
return *this;
}
//这里比较直接用const防止权限放大问题
bool operator!=(const __list_iterator& it) { return _node != it._node; }
Ptr operator->() { return &(_node->val); }
self operator++(int)
{
self tmp(*this);
_node = _node->next;
return tmp;
}
bool operator==(const self& it) { return _node == it._node; }
self& operator--()
{
_node = _node->prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->prev;
return tmp;
}
};
template<typename T>
class list
{
public:
typedef __list_iterator<T, T&, T*> iterator;
//错误的设计,类比vector
//typedef __list_iterator const_iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
typedef Reverse_iterator<iterator, T&, T*> reverse_iterator;
typedef Reverse_iterator <const_iterator, const T&, const T*> constReverseIterator;
private:
typedef list_node<T> link_node;
link_node* _head = nullptr;
size_t _size = 0;
public:
//void empty_init()
//带头双向循环链表初始化
list()
:_head(new link_node)
{
_head->next = _head;
_head->prev = _head;
_head->val = T();
}
//这里用int是为了防止和之后的迭代器区间初始化产生冲突
list(int n, const T& val = T())
:list()
{
for (; _size < n;)
push_back(val);
}
//深拷贝
list(const list<T>& lt)
:list()
{
for (auto& e : lt)
push_back(e);
}
template<typename InputIterator>
list(InputIterator first, InputIterator last)
:list()
{
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
//经典写法
/*void push_back(const T& val)
{
link_node* tmp = new link_node;
tmp->val = val;
tmp->next = _head;
_head->prev->next = tmp;
tmp->prev = _head->prev;
_head->prev = tmp;
}*/
bool empty() { return !_size; }
//复用写法
void push_back(const T& val) { insert(end(), val); }
void pop_back()
{
assert(!empty());
erase(--end());
}
void push_front(const T& val) { insert(begin(), val); }
void pop_front()
{
assert(!empty());
erase(begin());
}
iterator begin() { return _head->next; }
iterator end() { return _head; }
const_iterator begin() const { return const_iterator(_head->next); }
const_iterator end() const { return const_iterator(_head); }
iterator insert(const iterator& pos, const T& val)
{
link_node* tmp = new link_node;
tmp->val = val;
tmp->next = pos._node;
tmp->prev = pos._node->prev;
pos._node->prev->next = tmp;
pos._node->prev = tmp;
++_size;
return tmp;
}
iterator erase(iterator pos)
{
//不能删去头节点
assert(pos != end());
link_node* tmp = pos._node;
tmp->prev->next = tmp->next;
tmp->next->prev = tmp->prev;
//别忘了释放空间
++pos;
delete tmp;
--_size;
return pos;
}
size_t size() { return _size; }
void clear()
{
//不删除头节点
link_node* del = _head->next;
while (del != _head)
{
link_node* ne = del->next;
delete del;
del = ne;
}
_size = 0;
}
T& front() { return _head->next->val; }
const T& front() const { return _head->next->val; }
T& back() { return _head->prev->val; }
const T& back() const { return _head->prev->val; }
~list()
{
clear();
delete _head;
_head = nullptr;
}
};
}
对于list底层和vector,string的底层模拟来说,最大的难点就时list的迭代器需要自己写一个类封装起来,而另两个由于数据存储空间连续直接使用原生指针即可,但是能使用指针的情况也就只有这两种,stl的其他容器的迭代器基本都是用类封装的,但这种设计思路大体时相同的,这也是stl设计的强大之处了!大家在以后学习其他stl容器时,会越来越体会到这种容器的巧妙的!
以上就是本篇博客模拟实现list的全部内容,如果还有不懂的或者作者写的有问题的地方,欢迎在评论区提出!