list的介绍及使用
list的介绍
list - C++ Reference (cplusplus.com)
list的详细介绍可以看上面的网站,这里只说一下重点地方
1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指其前一个元素和后一个元素。
3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
( 这里如果看的不是很明白,可以去看我之前写过的链表的文章,里面先从单链表介绍,后面就介绍了带头的双向循环链表)
-----
list的使用
构造函数( (constructor)) | 接口说明 |
list() | 构造空的list |
list (size_type n, const value_type& val = value_type()) | 构造的list中包含n个值为val的元素 |
list (const list& x) | 拷贝构造函数 |
list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造list |
经典的几个构造:
无参
开辟n个空间并初始化为val,如果没有val的值,默认为0
用一个list初始化另一个list
用一个迭代器区间初始化另一个list
具体可以看下面
-----
list iterator的使用
函数声明 | 接口说明 |
begin + end | 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器 |
rbegin + rend | 返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的 reverse_iterator,即begin位置 |
这个其实跟vector的差不了多少,暂且可以理解为是一个指针指向list的某一个节点,这里的演示可以看上面的l3,遍历就是用的begin和end,同时范围for(auto)的底层实际上也是调用的迭代器
list capacity
函数声明 | 接口说明 |
empty | 检测list是否为空,是返回true,否则返回false |
size | 返回list中有效节点的个数 |
list element access
函数声明 | 接口说明 |
front | 返回list的第一个节点中值的引用 |
back | 返回list的最后一个节点中值的引用 |
list modifiers
函数声明 | 接口说明 |
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中的有效元素 |
基本的函数操作就是这么多,实际上这些如果之前看明白了vector和string的话,这些即使不演示也能想个大概...
list的迭代器失效
这里说一下,之前的迭代器失效为非是两种情况:
1、迭代器指向错误空间
2、迭代器指向错误数据
但是在list这里迭代器失效只会有一种问题,就是所指向的位置被释放(delete),因为list的底层实际上是一个带头的双向循环链表,所以插入的时候不会引发迭代器失效,只有删除的时候才会,同时失效的只有被删除的那个节点的迭代器,其他迭代器是不会失效的
--------
模拟实现list
首先我们定一个命名空间:
namespace knous
{
}
一开始,这里面什么都没有,这里就快速构造一个list,跟vector一样,这里我们要用模板,因为不确定接收的是什么类似的数据:
template<class T>
struct list_node
{
list_node* next;
list_node* prev;
T _data;
};
template<class T>
class list
{
typedef list_node Node;
public:
private:
Node* _head;
};
首先list里面只有一个Node*的指针,这个Node是被重命名过后的,它是list_node的一个结构体,这个结构体就是我们的节点,里面放着上一个节点和下一个节点的地址,还有数据
因为Node是我们自定义类型,所以这里得写一个默认构造,这个构造也很简单,list里面放着的是我们的头节点,头节点在没有数据的时候它的上一个指针和下一个指针都应该指向自己,里面的数据就用默认的构造即可:
list()
{
_head = new node;
_head->next = _head;
_head->prev = _head;
}
然后来实现与一个尾插:
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;
}
这里用了Node的构造函数,所以这里要写一下Node(list_node)的默认构造:
list_node(const T& val = T())
:_next(nullptr)
, _prev(nullptr)
, _data(val)
{}
这里给了一个缺省值,这个是调用T类型的默认构造,假设是int,那int的默认构造就是0
然后我们既然已经可以插入数据了,那我们是不是就要能够打印出来看看?
如何打印?
我们知道vector的打印可以用下标,可以用迭代器,但是list呢?
vector的底层实际上是一块连续的空间,是一块数组,所以它能直接++、--,list的底层是带头的双向循环链表,里面的成员是某一个节点的指针,我们拿到这个指针所指向的地址,可以直接++吗?
不能,但是我们有没有办法可以让它能直接++呢?
可以,我们重新定义一个类出来,这个类的名字就叫iterator
template<class T>
class __list_iteartor
{
typedef list_node<T> Node;
Node* _node;
};
这个类里面也只有一个参数,就是Node*的指针,它的本质其实跟list一样,都是指向某一个节点的指针,但是我们可以通过对它的重载,来让它具备++、--的功能,所以我们先写一个构造出来
__list_iteartor(Node* Node)
:_node(Node)
{}
它的构造需要一个Node*的指针,然后我们直接赋值上去即可
然后我们写一个begin和end函数:
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
写好了如上迭代器,我们还要解决的一个问题是,迭代器本身是一个指针,我们要通过*解引用来获得这里的数据,但是iterator里面也是一个指针,指针指向的位置是一个节点的地址,我们要的不是地址,而是那个地址的数据,所以这里我们要重载一下*运算符
T operator*()
{
return _node->_data;
}
while的判断条件的!=,所以重载!=运算符
bool operator!=(const self& it)
{
return _node != it._node;
}
打印了这里的数据完了我们还要让it指向下一个位置,所以重载++运算符
因为每次都写__list_iteartor<T>实在过于麻烦,这里重命名为self
self operator++(int)
{
_node = _node->_next;
return _node;
}
因为是后置++,所以在接收里面要加上一个int,这里既然后置++都写了,那就再写一个前置++,前置--和后置--
self operator++()
{
Node* tmp(*this);
_node = _node->_next;
return tmp;
}
self operator--(int)
{
_node = _node->_prev;
return _node;
}
self operator--()
{
Node* tmp(*this);
_node = _node->_prev;
return tmp;
}
至此,看一下效果:
没有问题
既然尾插有了,那我们把剩下几个也写一下:
尾删
void pop_back()
{
Node* tail = _head->_prev;
_head->_prev = tail->_prev;
tail->_prev->_next = _head;
delete tail;
}
头插
void push_front(const T& x)
{
Node* newnode = new Node(x);
Node* next = _head->_next;
_head->_next = newnode;
newnode->_next = next;
next->_prev = newnode;
头删
void pop_front()
{
Node* next = _head->_next;
_head->_next = next->_next;
next->_next->_prev = _head;
delete next;
}
至此,再看一遍前面写的效果:
写到这里基本也就差不多了,最后再写中间插入删除就可以结束了,至于size和capacity,这里大家可以自己尝试写一下
iterator insert(iterator pos, const T& x)
{
Node* newnode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_next = cur;
newnode->_prev = prev;
cur->_prev = newnode;
return iterator(newnode);
}
iterator erase(iterator pos)
{
Node* cur = pos->_node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
注意这里为什么删除为什么要返回的是下一个节点的地址,是为了防止出现迭代器失效的问题,可以参考上面讲的迭代器的部分,因为失效的只有当前被删除的节点,而之后节点的迭代器是没有问题的,所以返回的是下一个节点的迭代器。
写到这里,我们可以考虑优化一下,头插头删 尾插尾删,是不是都可以复用insert和erase?
然后就写出第二版:
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 pop_back()
{
/*Node* tail = _head->_prev;
_head->_prev = tail->_prev;
tail->_prev->_next = _head;
delete tail;*/
erase(--end());
}
void push_front(const T& x)
{
/*Node* newnode = new Node(x);
Node* next = _head->_next;
_head->_next = newnode;
newnode->_next = next;
next->_prev = newnode;*/
insert(begin(), x);
}
void pop_front()
{
/*Node* next = _head->_next;
_head->_next = next->_next;
next->_next->_prev = _head;
delete next;*/
erase(begin());
}
结束了吗?
没有,这里还要考虑一个其他问题,就是解引用的部分,因为解引用会有两种情况:
第一种:我只读,打印
第二种:我不仅要读末尾还要修改
所以这里就会出现一个问题,const的迭代器怎么处理?
为了const迭代器专门写一个类也过于繁琐不堪,所以这里就有一个写法是这样的:
typedef __list_iteartor<T,T&,T*> iterator;
typedef __list_iteartor<T, const T&, const T*> const_iterator;
两个模板,第一个模板是普通类型,传过去的是引用和别名,第二个模板是加了const的,传过去的就是加了const修改的版本
所以最后再将list完善一下功能:
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
然后构造的时候也有用一个区间构造另一个,=号的重载,这里就一起写了:
void empty_Init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_Init();
while (first != last)
{
push_back(*first);
++first;
}
}
因为迭代器区间是一个不肯定类型,所以要用模板,这里在用区间插入的时候最好先初始化一下,所以这里调用了一个空初始化的函数,跟无参构造的时候是一样的
void swap(list<T> it)
{
std::swap(it._head, _head);
}
list(const T& it)
{
empty_Init();
list<T> tmp(it.begin(), it.end());
swap(tmp);
}
list<T>& operator=(list<T> it)
{
swap(it);
return *this;
}
这里用的是现代写法,因为带头链表的交换拷贝构造完了只需要交换头节点,因为形参是实参的临时拷贝,所以这里直接用形参然后交换,然后因为形参出了作用域就销毁,所以它会自动释放
最后放上全部代码
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace knous
{
template<class T>
struct list_node
{
list_node(const T& val = T())
:_next(nullptr)
, _prev(nullptr)
, _data(val)
{}
list_node* _next;
list_node* _prev;
T _data;
};
template<class T, class Ref,class Ptr >
struct __list_iteartor
{
typedef list_node<T> Node;
typedef __list_iteartor<T, Ref, Ptr > self;
__list_iteartor(Node* Node)
:_node(Node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
//return &(operator*());
return &_node->_data;
}
bool operator!=(const self& it)
{
return _node != it._node;
}
self operator++(int)
{
_node = _node->_next;
return _node;
}
self operator++()
{
Node* tmp(*this);
_node = _node->_next;
return tmp;
}
self operator--(int)
{
_node = _node->_prev;
return _node;
}
self operator--()
{
_node = _node->_prev;
return _node;
}
Node* _node;
};
template<class T>
class list
{
typedef list_node<T> Node;
typedef __list_iteartor<T,T&,T*> iterator;
typedef __list_iteartor<T, const T&, const T*> const_iterator;
public:
list()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
void empty_Init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_Init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T> it)
{
std::swap(it._head, _head);
}
list(const T& it)
{
empty_Init();
list<T> tmp(it.begin(), it.end());
swap(tmp);
}
list<T>& operator=(list<T> it)
{
swap(it);
return *this;
}
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
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 pop_back()
{
/*Node* tail = _head->_prev;
_head->_prev = tail->_prev;
tail->_prev->_next = _head;
delete tail;*/
erase(--end());
}
void push_front(const T& x)
{
/*Node* newnode = new Node(x);
Node* next = _head->_next;
_head->_next = newnode;
newnode->_next = next;
next->_prev = newnode;*/
insert(begin(), x);
}
void pop_front()
{
/*Node* next = _head->_next;
_head->_next = next->_next;
next->_next->_prev = _head;
delete next;*/
erase(begin());
}
iterator insert(iterator pos, const T& x)
{
Node* newnode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_next = cur;
newnode->_prev = prev;
cur->_prev = newnode;
return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
private:
Node* _head;
};
}
至此,list大部分功能都实现完了,希望这篇list看完你对list会有更深的理解
Thanks for watching