list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
list中的接口比较多,然而这些接口往往和其他容器的接口差不多,所以这里就不重复赘述一样的内容了
vector和string,也就是之前谈到的迭代器都是指针实现的,因为他们的物理空间都是连续的,只要++就可以到下一个位置
然而对于list就会比较复杂一点,由于本身原生指针无法完成迭代器的功能,于是尝试用一个类来去封装Node*,然后呢去重载这个类的运算符,使其拥有和指针一样的访问行为,所以在分析的时候我们也可以像指针一样去理解这个迭代器,那么有了这样的方式,我们就可以不关心容器的底层结构到底是数组链表或者时树形结构等等,封装隐藏了底层细节,让我们可以用简单的方式给hi去访问修改容器,这个也是迭代器的真正价值
template<class Conta>
Container:: iterator it=con.begin()
while(it != con.end())
{
*it;
++it;
}
迭代器示意图
之前在vector中存在着迭代器失效,主要原因是因为Insert和Erase所导致的意义失效或者是野指针问题
然而list会有所不同,因为vector造成的原因是因为挪动该数据产生的,然而因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,毕竟在插入的时候不会造成挪动数据
list迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
对于list的sort这需要提到,由于sort的底层使用的是快速排序,快速排序要求容器迭代器必须得是随机迭代器,比如:快排要三数取中优化,链表不支持随机访问,效率就不够了,所以这里干脆不支持用sort
sort(lt.begin(), lt.end()); // 不支持
于是list有自己的sort,而不是在algorithm的库中
list<int> lt;
lt.push_back(10);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
//sort(lt.begin(), lt.end()); 不支持
lt.sort(); // 不建议用
不过还是不建议使用,因为效率太低
这里的迭代器是针对算法的
常见的迭代器一般分为三种:
单向:只支持++(forward_list)
双向:不仅支持++,还支持–(list)
随机:一般物理是连续的,支持++/–/+/-(vector,string)
不同的函数支持的迭代器是不同的
这个接口是之前没有提到的,于是我们了解熟悉一下
unique是可以是连续的数据去重的接口
不过只能对一连串的相同数据去重,不能对所有数据去重
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(2);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(4);
lt.push_back(2);
lt.push_back(2);
PrintContainer(lt);
lt.unique();
PrintContainer(lt);
要真正的去重只能够先排序后去重
it.sort;
it.unique;
不过这很影响效率
注意unique返回值是去重之后的最后一个值,所以要接收以后再输出
reverse接口的提供可以简化我们需要自己去写逆置函数的需求
部分源码,来看一下实现结构
看看初始化函数就知道具体结构了,是一个双向链表
仿造源码中的结构,先简单构造一个类的框架
用struct的原因是,方便访问,这样可以保证是默认共有的
template<class T>
struct _list_node
{
T _val;
_list_node<T>* _next;
_list_node<T>* _prev;
_list_node(const T& val = T())//节点构造函数
:_val(val)
, _prev(nullptr)
, _next(nullptr)
{}
};
template<class T>
class list
{
typedef _list_node<T> node;
public:
list()//list构造函数
{
_head = new node(T());//传一个匿名对象
_head->_next = _head;
_head->_prev = _head;
}
private:
node* _head;
};
void push_back(const T& x)
{
node* newnode = new node(x);
node* tail = _head->_prev;
//head -----tail newnode
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
迭代器有两种实现方式,具体应根据容器底层数据结构实现:
指针可以解引用,迭代器的类中必须重载operator*()
指针可以通过->访问其所指空间成员,迭代器类中必须重载oprator->()
指针可以++向后移动,迭代器类中必须重载operator++()
与operator++(int)
至于operator--()/operator--(int)
释放需要重载,根据具体的结构来抉择,双向链表可以向前 移动,所以需要重载,如果是forward_list就不需要重载–
迭代器需要进行是否相等的比较,因此还需要重载operator==()与operator!=()
下面先是写一个简单的结构,然后一步步补充
template<class T>
struct _list_iterator
{
typedef _list_node<T> node;
typedef _list_iterator<T> self;//自己类型
node* _pnode;//封装一个节点的指针
_list_iterator(node* pnode)
: _pnode(pnode)
{}
};
按照之前的样子,放在class中
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);
}
其中迭代器这里的拷贝构造、operator=、析构我们不写,编译器默认生成可以使用
需要重载实现的*(解引用)
,前置和后置++/--
,以及==
和!=
,还有->
//T&
Ref operator*()
{
return _pnode->_val;
}
//T*
Ptr operator->()
{
return &_pnode->_val;
}
bool operator!=(const self& s) const
{
return _pnode != s._pnode;
}
bool operator==(const self& s) const
{
return _pnode == s._pnode;
}
// ++it -> it.operator++(&it)
self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
// it++ -> it.operator++(&it, 0)
self operator++(int)
{
self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
理解一下
->
这里本来应该是两个->,第一个是it->去调用重载的operator->,返回T*的指针,第二个箭头,T*
去访问对象中的成员,但是两个箭头程序的可读性很差,为了可读性,省略了一个箭头(it->->_year
把it->
替换T*->_year
但是可读性太差优化成it->_year
)list<Date> lt; lt.push_back(Date()); lt.push_back(Date()); lt.push_back(Date()); list<Date>::iterator it = lt.begin(); while (it != lt.end()) { cout << it->_year << " " << it->_month << " " << it->_day << endl; ++it; } cout << endl;
对于const迭代器来说传统的方法是复制一份iterator然后对象加上const,但是这难免导致代码冗余,于是看看源码,大师是选择对模板增加参数,来实现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* pnode)
: _pnode(pnode)
{}
......
};
然后再看list中的
template<class T>
class list
{
typedef _list_node<T> node;
public:
typedef _list_iterator<T, T&,T*> iterator;
//传统想法是拷贝一下iterate,改成const的,但是代码重复是大量的
//优化就是增加模板参数表
typedef _list_iterator<T, const T&, const T*> const_iterator;
......
};
void insert(iterator pos, const T& x)
{
assert(pos._pnode);
node* cur = pos._pnode;
node* prev = cur->_prev;
node* newnode = new node(x);
//prev newnode cur
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
prev->_next = newnode;
}
iterator erase(iterator pos)
{
assert(pos._pnode);
assert(pos != end());
node* prev = pos._pnode->_prev;
node* next = pos._pnode->_next;
delete pos._pnode;
prev->_next = next;
next->_prev = prev;
return iterator(next);
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
bool empty()
{
return begin() == end();
}
size_t size() const
{
size_t sz = 0;
iterator it = begin();
while (it != end())
{
++sz;
++it;
}
return sz;
}
void clear()
{
iterator it = begin();
while (it!=end())
{
//erase(it++);//这个方式只适合这里
it = erase(it);
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
仍需实现深拷贝
list(const list<T>& lt)
{
_head = new node;//传一个匿名对象
_head->_next = _head;
_head->_prev = _head;
for (const auto& e : lt)
{
//this.push_back
push_back(e);
}
}
传统
list<T>& operator=(const list<T>& lt)
{
if (this != <)
{
clear();
for (const auto& e : lt)
{
push_back(e);
}
}
return *this;
现代
list<T>& operator=(list<T> lt)
{
swap(_head, lt._head);
return *this;
}
原理是类模板实例化出了两个代码,其实还是弄了2个类,只是认为不用去增加代码了
list的学习笔记到此暂告一段落,list模拟的源码在如下仓库中,有兴趣可以看一下
https://github.com/Allen9012/cpp/tree/main/STL/list2