什么是list
话不多说,我们直接上list的官方源文档
list也是一个类模版。那么list和前面所学的vector还有string又有什么区别呢?我们进一步来看官方文档
Lists are sequence containers that allow constant time insert and erase operations anywhere within the sequence, and iteration in both directions.
这是截取自官方文档对于list的说明,用通俗一点的话语来描述就是list是一个在任意位置插入删除时间复杂度是O(1)的容器
从源代码来探索list的底层结构
那么list的底层到底是一个什么结构能够在任意位置插入删除达到O(1)的时间复杂度呢?我们结合SGI的的list的源代码来进行分析:
//节选并简化的关键代码
template<class T>
struct _List_node_base
{
_List_node_base<T> * _M_next;
_List_node_base<T> * _M_prev;
T val;
};
list是由一个个的节点组成的,而这里面的每一个节点都是双向的!
我们继续向下查找源代码:
找到并简化源代码
template<class T>
class list
{ typedef _List_node_base<T> Node;
protected:
Node* _head;
}
到这里我们基本就可以确定,list是一个双向带头循环的链表了!这种链表能够做到任意位置插入删除的效率都是O(1).
list的常见构造和接口的使用
同样,我们先来看一看构造函数:
list的构造 | 效果 |
---|---|
list() | 构造一个空的list |
list(size_t n ,const T& val) | 构造n个值是val的节点 |
list(InputIterator first, InputIterator last) | 迭代器区间构造 |
list(const list& x) | 拷贝构造 |
常用的接口:
对于C++来说,由于list在插入删除上面极度优秀的表现,所以说list的使用场景多用于频繁进行任意位置的插入删除,也就是说对于list来讲,增删的成员函数使用的很多,而其他的成员函数相对用的就会偏少
用到的时候,查文档了解即可。
#include<iostream>
#include<list>
using namespace std;
void test()
{
list<int>lt;
//push_back--->尾插
lt.push_back(1);
lt.push_back(2);
for (auto c : lt)
{
cout << c << " ";
}
cout << endl;
//push_front--->头插
lt.push_front(0);
lt.push_front(-1);
for (auto c : lt)
{
cout << c << " ";
}
cout << endl;
//insert--->依旧是只能提供迭代器作为位置
auto it = lt.begin();
lt.insert(++it, -5);
for (auto c : lt)
{
cout << c << " ";
}
cout << endl;
it++;
//erase--->删除对应的迭代器指向的元素
lt.erase(it);
for (auto c : lt)
{
cout << c << " ";
}
cout << endl;
//尾删
lt.pop_back();
//头删
lt.pop_front();
for (auto c : lt)
{
cout << c << " ";
}
cout << endl;
}
int main()
{ test();
return 0;
}
list也提供了sort方法进行对元素进行排序,链表的sort是用归并进行的,不过链表的排序效率非常低,不太推荐使用list结构进行排序
list的迭代器
接下来我们来看一看list的迭代器。和前面的vector不一样,list的迭代器的底层不是一个原生的指针!而是一个更为复杂的自定义类型!二而这个迭代器只有++和–这两种迭代的行为,而没有vector的迭代器可以加减一个常数位置进行访问的功能!
void test2()
{
list<int>lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
//看一看list的迭代器
auto it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//cout<<*(it+3)<<endl; list的迭代器没有随机访问对的功能
}
int main()
{
test2();
return 0;
}
list和list的迭代器的模拟实现
讲了这么多的list的使用,接下来我们来模拟实现一个list来帮助我们更好了解list的底层结构。我们知道,list是一个双向带头循环链表,所以我们需要链表的节点,所以我们需要定义节点的结构
template<typename T>
struct list_node
{
list_node<T>* _prev;
list_node<T>* _next;
T _val;
//因为这里变成了模板类,所以这里全缺省的默认构造函数的缺省值要这么写
list_node(const T& val=T())
:_prev(nullptr)
,_next(nullptr)
,_val(val)
{}
};
有了这么一个节点结构,我们就可以快速上手搭起来一个list类的框架了
//list类的框架
template<class T>
class list
{ public:
typedef _list_node<T> Node;
//提供默认构造函数
list()
: _head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
}
private:
Node _head;
};
对于双向带头循环链表,我们只要实现insert和erase方法,然后其他的接口复用就可以了,这两个方法需要迭代器。所以我们再来实现list的迭代器。
//简单的list迭代器
template<typename T>
struct _List_iterator
{
typedef list_node<T> Node;
typedef _List_iterator<T> Self;
//迭代器本质是封装了节点的指针
Node* _node;
_List_iterator(Node* node)
:_node(node)
{}
//*
T& operator*()
{
return _node->_val;
}
//->
T* operator->()
{
return &_node->_val;
}
//前置++
Self& operator++()
{
_node = _node->_next;
return *this;
}
//前置--
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置++
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
//后置--
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
//重载!=
bool operator!=(const self& it) const
{
return _node != it._node;
}
};
首先这里的迭代器存在一个很严重的问题:如果是一个const对象无法调用这个迭代器!而你可能会想说在创造一个const版本的迭代器类,这样固然是可以,但是这样会使得代码冗余!我们来 看一看官方是怎么处理这个问题的
template<class T,class Ref,class Ptr>
struct __list_iterator
{
///具体实际就不用关注了
};
//list的地方如何处理
template<class T>
class list
{
public:
typedef _list_iterator<T,T&,T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
}
可以看到,STL里面利用了三个模板参数,T, Ref,Ptr。而Ref,Ptr,分别就是引用和指针。所以当我们传递的是非const的迭代器,编译器就会匹配非const的,反之const就会匹配const。这就是大佬设计的独到之处
//完整的模拟实现list的迭代器
template<typename T,typename Ref,typename Ptr>
struct _List_iterator
{
typedef list_node<T> Node;
typedef _List_iterator<T,Ref,Ptr> Self;
Node* _node;
_List_iterator(Node* node)
:_node(node)
{}
//*
Ref operator*()
{
return _node->_val;
}
//->
Ptr operator->()
{
return &_node->_val;
}
//前置++
Self& operator++()
{
_node = _node->_next;
return *this;
}
//前置--
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置++
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
//后置--
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self& it) const
{
return _node != it._node;
}
};
有了迭代器就可以实现insert和erase方法了
//插入pos位置前
iterator insert(iterator pos, const T& val)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(val);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
//删除pos位置
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);
}
接下来,在实现list的begin()和end()成员
iterator begin()
{ //哨兵位的下一个
return iterator(_head->_next);
}
iterator end()
{
//哨兵卫就是end
return iterator(_head);
}
接下来就是复用insert和erase
void push_back(const T& val)
{
insert(end(), val);
}
void push_front(const T& val)
{
insert(begin(), val);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
那么我们在来看看,list的迭代器失效问题
对于insert方法,因为insert方法仅仅只是改变了指针的指向,所以本质pos指向的那个节点的绝对地址并不会随着insert而改变,所以insert不会导致迭代器失效。反而是erase方法反而因为释放了原来的空间导致出现野指针失效 而和vector的处理方式一致,erase方法也是返回指向被删除元素的下一个位置元素的迭代器
//删除pos位置
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);
}
//插入pos位置前,即使返回值改成void也没问题
iterator insert(iterator pos, const T& val)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(val);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
接下来就是list的拷贝构造和赋值元素重载了,我们也是采取现代的写法进行处理。
//构造哨兵位头节点方便交换
void empty_init()
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
}
//迭代器区间构造
template<typename InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
}
//拷贝构造
list(const list<T>& lt)
{
empty_init();
list<T> tmp(lt.begin(), lt.end());
//交换哨兵位头节点
swap(tmp);
}
//赋值运算符重载
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
//添加析构函数
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
_head->_prev = _head;
_head->_next = _head;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
以上就是和list有关的知识的讲解,如果有不足之处希望指出。希望大家可以一起进步。