文章目录
初识list
list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且支持前后双向迭代。其底层是双向链表,因此其不支持下标随机访问,但是在已知位置进行插入删除操作非常高效。
对于list接口的使用这里就不做描述了,需要时可以自行查看官网文档库,list的各接口使用与vector的使用类似。下面将对list的模拟实现以及list的一些使用细节进行探究。
模拟实现list
list成员
前面说了list是一个双向链表结构,因此我们得先创建出节点类型,因为其为双向则节点类型里要有指向下一个节点的指针也要有指向上一个节点的指针。对于list而言,其为类模板因此节点类也必须为类模板
//定义节点类
template<class T>
struct List_Node {
List_Node* _next;
List_Node* _prev;
T _data;
List_Node(const T& x)
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
list内部成员包括一个哨兵位节点,也就是根据这个头节点去连接插入后的节点。同时也可以增加一个size变量记录list的长度
template<class T>
class mylist {
//重命名节点类
typedef List_Node<T> Node;
private:
//列表哨兵位节点
Node* _head;
//记录链表长度
size_t _size = 0;
};
构造与析构
默认构造
因为无论是哪一种构造都需要先把哨兵位节点创建出,所以可以将创建哨兵位封装成一个函数。
//创建哨兵位节点
void Init() {
_head = new Node(T());
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
对于默认构造而言只需要将哨兵位节点创建出即可
mylist(){
Init();
}
迭代器区间构造
因为list并不是一段连续的空间所以其迭代器不能用原生指针实现,这个到下面再讲。在STL中所有的容器的迭代器的访问方式都是一样的,我们只需要按照统一的方式使用即可。迭代器区间构造我们只需要将这块区间的每一个节点的值依次插入到链表中即可
//迭代器区间构造
template<class InputIterator>
mylist(InputIterator first, InputIterator last) {
Init();
while (first != last) {
push_back(*first);
first++;
}
}
拷贝构造
一旦涉及到拷贝构造就得注意深拷贝和浅拷贝的问题,因为新建出来的是一个新的链表所以不能直接让两个相等,这样就会指向同一块空间。可以利用迭代器区间构造新建一个临时的链表出来,然后再让其与我们的链表进行交换即可。
这里我们要有一个属于list内部的交换函数,只需要将哨兵位节点进行交换那么两个链表就可以交换过来了。
void swap(mylist<T>& l) {
//交换哨兵位节点
std::swap(_head, l._head);
std::swap(_size, l._size);
}
mylist(const mylist<T>& l) {
Init();
mylist<T> tmp(l.begin(), l.end());
swap(tmp);
}
=号赋值
相比而言直接赋值会更加常用,重载=号时需要注意的是,传入的形参对象一定不能引用传参,因为我们只需要赋值给目标对象并不会改变原对象的值,所以要用传值传参(形参的改变不会影响实参)
mylist<T>& operator=(mylist<T> l) {
swap(l);
return *this;
}
析构
因为list是一个链表,所以在销毁时需要把所有的节点都释放。后面会讲到一个clear接口,可以将所有的节点全部释放。释放完所有节点后还要把哨兵位节点也释放。
~mylist() {
clear();
delete _head;
_head = nullptr;
_size = 0;
}
迭代器
相较于vector和string的的迭代器,list的迭代器不能够直接用原生指针去实现,因为链表不是一段连续的空间。因此为了能使list的迭代器有着同样的使用方式,就需要创建一个属于迭代器的类模板,并通过重载运算符实现对应的操作。
定义迭代器类模板
之前我们定义的类模板都是只有一个参数的,但是在实现list迭代器需要用到三个参数。不同的参数就会编译器就会生成不同类型的类。根据之前的学习我们知道,迭代器都是会有一个普通版本和一个const版本,但是相较于前面的不同,list的迭代器是需要我们去重载各个运算符的。例如现在我们重载了*这个运算符,那么如果我们直接在迭代器前面加上const就会出现问题
const mylist::iterator it = l.begin();
//const T* it = l.begin();
像这样的代码const修饰的就会是指针的本身,也就是说这样修饰后it就不再可以改变自身也就是不能进行 ++ --的操作,那和我们的操作方式就对不上了。因此为了解决这个问题就可以有两种方法,第一种我们可以重新定义一个const迭代器的类,但是这样的话代码的重复率太高,因为我们只需要改变重载*的方法而已。所以这个时候就可以用到类的第二个参数,让编译器根据我们想要的不同类型生成不同的对象。
那么第三个参数有什么用呢,我们知道list是一个类模板所以可以list里面的数据类型是我们的自定义类型,那么假如现在list里面存放的是我们的自定义类型A,并且A里面有两个int类型的成员变量a, b,那么我们可以编程一下代码分别获取A里面的两个变量
(&(*lt))->a; (&(*lt))->b;
//迭代器解引用得到自定义类型,再去自定义类型的地址利用->运算符可以指定类里面的变量
所以为了这个操作可以实现,我们还需要重载->运算符,同样的也会有对应的const版本,所以当我们将第三个参数定义为T*后,那进行->操作时就不再需要写的这么麻烦了,就可以直接使用了。
下面来看看整体实现的代码
//定义列表迭代器类
template<class T, class Ref, class Ptr>
struct __list_iterator {
typedef List_Node<T> node;//重命名节点类
typedef __list_iterator<T, Ref, Ptr> self;//重命名迭代器类
//迭代器成员
node* _pnode;
__list_iterator(node* p)
:_pnode(p)
{}
Ref operator*(){
//解引用得到节点的数据
return _pnode->_data;
}
Ptr operator->() {
return &_pnode->_data;
}
//后置
//让节点变成指向的前一个或后一个节点
self operator++(int){
self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
self operator--(int) {
self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
//前置
//让节点变成指向的前一个或后一个节点
self& operator++() {
_pnode = _pnode->_next;
return *this;
}
self& operator--() {
_pnode = _pnode->_prev;
return *this;
}
bool operator!=(const self& node) const{
return _pnode != node._pnode;
}
bool operator==(const self& node) const{
return _pnode == node._pnode
}
};
这样定义之后我们只需要改变第二个参数Ref就可以让编译器生成不同的对象了
//重命名迭代器类
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
iterator begin(){
return iterator(_head->_next);
}
iterator end(){
return iterator(_head);
}
const_iterator begin() const {
return const_iterator(_head->_next);
}
const_iterator end() const {
return const_iterator(_head);
}
插入删除
insert
在指定位置进行插入,那这个就比较简单了,list的insert不会出现迭代器失效的情况可以放心使用,可以给一个返回值返回新插入的节点的迭代器
iterator insert(iterator pos, const T& x) {
//新建一个节点
Node* newnode = new Node(x);
Node* cur = pos._pnode;
Node* prev = cur->_prev;
//将节点插入
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
//链表长度加加
_size++;
return iterator(newnode);
}
erase
删除指定的节点。这个就要考虑迭代器失效了,因为删除该节点后迭代器不发生变化就会指向已释放的空间就是一个野指针。所以可以给一个返回值返回删除节点的下一个节点
iterator erase(iterator pos) {
assert(pos != end());
//链接删除的节点的前后节点
Node* prev = pos._pnode->_prev;
Node* cur = pos._pnode->_next;
prev->_next = cur;
cur->_prev = prev;
//释放删除节点
delete pos._pnode;
pos._pnode = nullptr;
//链表长度减减
_size--;
return iterator(cur);
}
clear
删除所有节点,那这个就比较简单了,从首迭代器一次往后释放即可。每一次erase后都更新为下一个节点。
void clear() {
iterator it = begin();
while (it != end())
it = erase(it);
}
模拟实现全代码
其他的简单接口就不详细说了,大家一看就懂
//定义节点类
template<class T>
struct List_Node {
List_Node* _next;
List_Node* _prev;
T _data;
List_Node(const T& x)
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
/*
const T* p1; 保护指向的对象不能被修改,指针本身可以修改
T* const o2; 指针本身不能修改,指针指向的对象可以修改
如果直接在迭代器之前加const 则迭代器本身就不能够修改,也就不能++ --
*/
//定义列表迭代器类
template<class T, class Ref, class Ptr>
struct __list_iterator {
typedef List_Node<T> node;//重命名节点类
typedef __list_iterator<T, Ref, Ptr> self;//重命名迭代器类
//迭代器成员
node* _pnode;
__list_iterator(node* p)
:_pnode(p)
{}
Ref operator*(){
//解引用得到节点的数据
return _pnode->_data;
}
Ptr operator->() {
return &_pnode->_data;
}
//后置
//让节点变成指向的前一个或后一个节点
self operator++(int){
self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
self operator--(int) {
self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
//前置
//让节点变成指向的前一个或后一个节点
self& operator++() {
_pnode = _pnode->_next;
return *this;
}
self& operator--() {
_pnode = _pnode->_prev;
return *this;
}
bool operator!=(const self& node) const{
return _pnode != node._pnode;
}
bool operator==(const self& node) const{
return _pnode == node._pnode
}
};
//定义列表
template<class T>
class mylist {
//重命名节点类
typedef List_Node<T> Node;
public:
//重命名迭代器类
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//创建哨兵位节点
void Init() {
_head = new Node(T());
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
mylist(){
Init();
}
//迭代器区间构造
template<class InputIterator>
mylist(InputIterator first, InputIterator last) {
Init();
while (first != last) {
push_back(*first);
first++;
}
}
mylist(const mylist<T>& l) {
Init();
mylist<T> tmp(l.begin(), l.end());
swap(tmp);
}
mylist<T>& operator=(mylist<T> l) {
swap(l);
return *this;
}
~mylist() {
clear();
delete _head;
_head = nullptr;
_size = 0;
}
void swap(mylist<T>& l) {
std::swap(_head, l._head);
std::swap(_size, l._size);
}
void clear() {
iterator it = begin();
while (it != end())
it = erase(it);
}
iterator begin(){
return iterator(_head->_next);
}
iterator end(){
return iterator(_head);
}
const_iterator begin() const {
return const_iterator(_head->_next);
}
const_iterator end() const {
return const_iterator(_head);
}
size_t size() {
return _size;
}
bool empty() {
return _size == 0;
}
//尾插
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;*/
insert(end(), x);
}
//头插
void push_Front(const T& x) {
insert(begin(), x);
}
//尾删
void Pop_Back() {
erase(--end());
}
//头删
void Pop_Front() {
erase(begin());
}
iterator insert(iterator pos, const T& x) {
Node* newnode = new Node(x);
Node* cur = pos._pnode;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
_size++;
return iterator(newnode);
}
iterator erase(iterator pos) {
assert(pos != end());
Node* prev = pos._pnode->_prev;
Node* cur = pos._pnode->_next;
prev->_next = cur;
cur->_prev = prev;
delete pos._pnode;
pos._pnode = nullptr;
_size--;
return iterator(cur);
}
private:
//列表哨兵位节点
Node* _head;
//记录链表长度
size_t _size = 0;
};
list和vector的区别
vector的优缺点
- 支持下标随机访问
- 空间利用率,缓存利用率高
- 任意位置插入删除效率低
- 可能导致空间浪费
list的优缺点
- 不支持随机访问,访问某个元素效率低
- 任意位置插入删除效率高
- 空间利用率,缓存利用率低
各有优缺,根据使用场景正确选择。