list是C++标准库中STL的一部分,list基于链表结构的类,相信你在阅读了本文后一定能熟练掌握使用list。
list是基于双向链表结构
的一种容器,能够在任意位置进行插入与删除,以及双向迭代,因为其内存分布不是连续的,所以并不支持像vector的随机访问。
其特征如下:
因为list的接口较多,所以只介绍常见的重要接口
list的构造函数接口都是STL的通用接口。
构造函数 | 接口说明 |
---|---|
list(size_type n, const value_type& val = value_type() | 构造n个值为val的元素 |
list() | 构造空的list |
list(const list& x) | 拷贝构造函数 |
list(InputIterator first, InputIterator last)) | 利用迭代器[first,last)的元素构造list |
使用演示:
list<int> lt1(); //默认构造函数
list<int> lt2(10, 1); //创建了10个值为1的元素
list<int> lt3(lt2.begin(), lt2.end()); //利用迭代器进行遍历
迭代器是STL的重要功能,提供了一种通用的遍历方式,本文将重点介绍迭代器的实现。
成员函数 | 功能说明 |
---|---|
begin+end | 返回第一个元素的迭代器和最后一个元素下一个位置的迭代器 |
rbegin+rend | 返回第一个元素的反向迭代器(end+1)+返回最后一个元素下一个位置的反向迭代器(begin-1) |
使用演示:
vector<int> nums {1,2,3,4,5,6};
list<int> lt(nums.rbegin(),nums.rend()); //用反向迭代器构造获得逆序
list<int>::reverse_iterator it = lt.rbegin();
while(it != lt.rend())
{
cout << *it << " "l
++lt;
}
在这里我们可以把迭代器当成指针,迭代器失效就是指针指向的空间是未知或者已经释放掉的。在之前讨论vector的文章有讲解过,因为vector是连续的内存空间,每次扩容都会导致迭代器失效,而list却有些许不同。当我们使用list进行插入时,并不会使迭代器失效,只有在结点时才会导致迭代器失效,并且只有指向被删除结点的迭代器才会失效。
可以通过以下代码观察到该现象:
int arr[] = {1,2,3,4,5,6,7,8,9};
list<int> lt(arr, arr+sizeof(arr)/sizeof(arr[0]));
list<int>::iterator it = lt.begin();
while(it != lt.end())
{
if(*it % 2 == 0)
lt.erase(it);
++it;
}
报错信息: [1] 2734 segmentation fault (core dumped)
成员函数 | 功能说明 |
---|---|
push_front | 头插 |
pop_front | 头删 |
push_back | 尾插 |
pop_back | 尾删 |
insert | 在任意结点位置插入 |
erase | 在任意结点位置删除 |
swap | **交换两个list的元素 |
clear | 清理list的有效元素 |
使用演示:
int main() {
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
//这里也能把指针当作迭代器
list<int> lt(array, array+sizeof(array)/sizeof(array[0]));
lt.pop_back();
lt.push_front(0);
lt.insert(lt.end(),10);
lt.erase(lt.begin());
for(auto it : lt)
cout << it << " ";
cout << endl;
return 0;
}
因为list是模板类,所以建议将类的声明与定义都写在头文件中,否则编译将会报错,或者你也可以在.cpp文件中使用export关键字来定义成员函数。
template <class T>
struct list_node //链表结构
{
T _data; //数据
list_node<T>* _next; //下一个结点
list_node<T>* _perv; //上一个结点
list_node(const T& data = T()) //默认构造函数
:_data(data)
,_next(nullptr)
,_perv(nullptr) {}
};
template <class T, class Ref, class Ptr> //三个参数分别为插入的数据类型,它的引用,与指针
class ListIterator //自定义迭代器
{
typedef list_node<T> Node;
typedef ListIterator<T, Ref, Ptr> self;
Node* _node;
// ------ 成员数据 --------
};
template<class T>
class list
{
public:
typedef ListIterator<T, T&, T*> iterator; //迭代器
typedef ListIterator<T, const T&, const T*> const_iterator; //const 迭代器
typedef list_node<T> Node; // 链表结构
// ----------- 成员函数 -------------
private:
Node* _node;
};
你看到上面代码中的迭代器声明可能会有些不解,为什么不是用普通的指针而是用自定义类型,而且还是有这么多参数的自定义类型。我们已经知道,list的内存空间是不是连续的,普通的++操作已经不能够访问到下一个结点,必须要求我们重载这些操作符,至于为什么这么多参数,那都是为了做出const的迭代器的同时复用代码。
试着想一想,如果只用一个模板参数T做迭代器的模板类,那么const迭代器要怎么来呢?const iterator吗?
template <class T>
class ListIterator
{
typedef list_node<T> Node;
Node* _node;
T& operator*() { return *_it->_data; }
T* operator->() { return &_it->_data; }
// ------- 成员函数 -------
};
当然不是,const iterator意味着我们不能修改类的成员变量,也就是不能做到++和–等操作。
多定义一个专门的const iterator类也能解决问题,也就是把* ->
这两个操作符重载的返回指改为const
但这样代码的复用度太高,于是这也多参数的写法便诞生了,只要把后面两个参数改为引用和指针便
完美解决了问题。
// T对应数据类型,Ref对应数据引用,Ptr对应数据的指针
template <class T, class Ref, class Ptr>
class ListIterator
{
typedef list_node<T> Node;
typedef ListIterator<T, Ref, Ptr> self;
Node* _node;
Ref operator*() { return _node->_data;}
Ptr operator->() { return &_node->}
}
template<class T>
class list //
{
public:
typedef ListIterator<T, T&, T*> iterator; //迭代器
typedef ListIterator<T, const T&, const T*> const_iterator; //const 迭代器
typedef list_node<T> Node; // 链表结构
iterator begin() { return _node->_next;} //这里隐式转化成了iterator类型
iterator end() { return _node;}
const_iterator begin() const { return _node->_next;}
const_iterator end() const { return _node->_prev; }
private:
Node* _node
}
因为我们的list是靠另一个结构体list_node()实现的,不太方便使用初始化列表来进行初始化,所以这里我是用了一个私有函数init()来进行辅助初始化。
template <class T>
void list<T>::init()
{
_node = new Node;
_node->_perv = _node; //这里使用了头结点来方便后续的增删查改操作
_node->_next = _node;
}
首先是默认构造函数,直接调用init函数便可。
list() { init(); }
拷贝构造函数等
list(const list<T>& l)
{
init();
for(auto it : l) //依次推入新结点
push_back(it);
}
list(int n, const T& value)
{
init();
for(int i = 0; i < n; ++i)
push_back(value);
}
list的增删查改操作和普通的双向带头链表的增删查改没有多少区别。
void push_back(const T& val)
{
Node* cur = new Node(val);
cur->_next = _node;
cur->_perv = _node->_perv;
_node->_perv->_next = cur;
_node->_perv = cur;
// insert(end(), val);
}
void pop_back()
{
assert(size() > 0);
Node* pev = _node->_perv->_perv;
delete _node->_perv;
_node->_perv = pev;
pev->_next = _node;
}
template <class T>
typename list<T>::iterator list<T>::erase(iterator pos)
{
Node* cur = pos._node;
Node* perv = pos._perv;
cur->_next->_perv = perv;
perv->_next = cur->_next;
delete cur;
return perv->_next;
}
list是双向链表的容器,在插入数据方面比顺序表的速度要快不少,因为它不用挪动数据,但在排序方面的效率就比顺序表要低不少,而且在迭代器的处理上要比vector麻烦上不少。如果需要完整代码,可以到我的github上去寻找。
博客主页:主页
我的专栏:C++
我的github:github