l i s t list list 是 C C C++ 标准模板库( S T L STL STL)中的一个序列容器( S e q u e n c e C o n t a i n e r Sequence\ Container Sequence Container),它允许在容器的任意位置快速插入和删除元素,是一个能够存放任意类型的双向带头循环链表。与数组或向量( v e c t o r vector vector)不同, l i s t list list 不需要在创建时指定大小,并且在任何位置添加或删除元素的效率为 O ( 1 ) O(1) O(1),而不需要重新分配内存。
文章目录
前言 —— 迭代器
1. 迭代器的分类
迭代器根据各个容器底层结构(决定可以使用哪些算法)不同,虽然所实现的功能相同,但是性质不同:迭代器只能够进行 ++ 操作,像
++begin()/begin()++
的就是单向迭代器;迭代器只能进行 ++ / / / - - 操作,像++begin()/--begin()
的是双向迭代器;而迭代器能够随机访问(底层是连续空间)操作的,像++begin()/--begin()/begin()+i/end()-i
,是随机迭代器。
从 C C C++ 官方文档中,我们看到 l i s t list list 的迭代器是一个双向的迭代器( b i d i r e c t i o n a l i t e r a t o r bidirectional\ iterator bidirectional iterator):
其他的像 v e c t o r vector vector 是一个随机访问迭代器( r a n d o m a c c e s s i t e r a t o r random\ access\ iterator random access iterator):
u n o r d e r e d _ m a p unordered\_map unordered_map 是一个单向迭代器( f o r w a r d _ i t e r a t o r forward\_iterator forward_iterator):
2. 不同性质迭代器的区别
容器的底层结构决定了迭代器的性质,也决定了这些容器能使用哪些算法。
比如说排序算法 s o r t sort sort:
可以看出,看似 s o r t sort sort 是一个函数模板支持所有类型,实际上其只支持随机访问迭代器的容器使用。
再比如逆置算法 r e v e r s e reverse reverse:
可以看出,由于其使用到了 - - 迭代器的操作,因此 r e v e r s e reverse reverse 只支持双向迭代器的容器使用。
有些算法像 f i n d find find,还会出现 I n p u t I t e r a t o r InputIterator InputIterator 的概念:
这其实就代表着 f i n d find find 函数支持所有类型的迭代器。
实际上,单向迭代器、双向迭代器和随机访问迭代器属于继承关系,即包含关系:
总结:
也就是说,单向( F o r w a r d Forward Forward)迭代能支持的算法,双向( B i d i r e c t i o n a l Bidirectional Bidirectional)迭代器也支持;双向( B i d i r e c t i o n a l Bidirectional Bidirectional)迭代器支持的算法,随机访问( R a n d o m A c c e s s Random\ Access Random Access)迭代器也支持;如果是支持 I n p u t I t e r a t o r InputIterator InputIterator 的算法,那么所有迭代器(容器)都支持。
一、list 的介绍
相较于 v e c t o r vector vector 的 连续线性空间, l i s t list list 就显得复杂很多,它的好处是:每次插入或删除一个元素,就配置或释放一个元素空间。而且,对于任何位置的元素插入或删除都是常数时间( O ( 1 ) O(1) O(1))。
因此, l i s t list list 对于空间的运用有绝对的精准,一点也不会浪费。
1. 结点(node)
每一个设计过 l i s t list list 的人都知道, l i s t list list 本身和 l i s t list list 的结点是不同的结构,需要分开设计。以下是 S T L STL STL l i s t list list 结点( n o d e node node)结构:
template<class T>
struct __list_node
{
typedef void* void_pointer;
void_pointer prev; // 型别为 void*。其实可设为 __list_node<T>*
void_pointer next;
T data;
};
显然这是一个双向链表。
2. 迭代器
l i s t list list 不再能够像 v e c t o r vector vector 一样以普通指针作为迭代器,因为其结点不保证在存储空间中连续存在。 l i s t list list 迭代器必须有能力指向 l i s t list list 的结点,并有能力进行正确的递增、递减、取值、成员存取等操作。
由于 S T L STL STL l i s t list list 是一个双向链表( d o u b l e l i n k e d − l i s t double\ linked-list double linked−list),迭代器必须具备前移、后移的能力,所以 l i s t list list 提供的是 B i d i r e c t i o n a l i t e r a t o r s Bidirectional\ iterators Bidirectional iterators。
以下是 l i s t list list 迭代器的设计:
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, Ref, Ptr> self;
typedef bidrectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T>* link_type;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
link_type node; // 迭代器内部当然要有一个普通指针,指向 list 的结点
// constructor
__list_iterator(link_type x) : node(x) {}
__list_iterator() {}
__list_iterator(const iterator& x) : node(x.node) {}
bool operator == (const self& x) const { return node == x.node; }
bool operator != (const self& x) const ( return node != x.node; )
// 以下对迭代器取值(dereference),取的是结点的数据值
reference operator* () const { return (*node).data; }
// 以下是迭代器成员存取(member access)运算子的标准做法
pointer operator -> const { return &(operator * ()); }
// 对迭代器累加 1,就是前进一个结点
self& operator ++ ()
{
node = (link_type)((*node).next);
return *this;
}
self operator ++ (int)
{
self tmp = *this;
++*this;
return tmp;
}
// 对迭代器递减 1,就是后退一个结点
self& operator -- ()
{
node = (link_type)((*node).prev);
return *this;
}
self operator -- (int)
{
self tmp = *this;
--*this;
return tmp;
}
};
3. 数据结构
S G I SGI SGI l i s t list list 不仅是一个双向链表,而且还是一个环状双向链表。所以它只需要一个指针,便可完整表现整个链表:
template<class T, class Alloc = alloc> // 缺省使用 alloc 为配置器
class list
{
protected:
typedef __list_node<T> list_node;
public:
typedef list_node* link_type;
protected:
link_type node; // 只要一个指针,便可表示整个环状双向链表
...
};
如果让指针 n o d e node node 指向刻意置于尾端的一个空白节点, n o d e node node 便能符合 S T L STL STL 对于 “ 前闭后开 ” 区间的要求,成为 l a s t last last 迭代器,如下图所示。这么一来,以下几个函数便都可以轻易完成:
iterator begin() { return (link_type)((*node).next); }
iterator end() { return node; }
bool empty() const { return node->next == node; }
size_type size() const
{
size_type result = 0;
distance(begin(), end(), result); // 全局函数
return result;
}
// 取头结点的内容(元素值)
reference front() { return *begin(); }
// 取尾结点的内容(元素值)
reference back() { return *(--end()); }
关于更多 l i s t list list 的介绍,还是要多查官方文档: l i s t list list的文档介绍。
二、list 的使用(常用接口)
下面只列举了 l i s t list list 最常用的几个接口,更多详细信息可以自行查官方文档: l i s t list list。
l i s t list list 也采用了模板: t e m p l a t e < c l a s s T , c l a s s A l l o c = a l l o c a t o r < T > > c l a s s l i s t ; template < class\ T,\ class\ Alloc = allocator<T> > class\ list; template<class T, class Alloc=allocator<T>>class list;
1. 常见构造
构造函数( c o n s t r u c t o r constructor constructor) | 接口说明 |
---|---|
l i s t ( s i z e _ t y p e n , c o n s t v a l u e _ t y p e & v a l = v a l u e _ t y p e ( ) ) list(size\_type\ n,\ const\ value\_type\&\ val = value\_type()) list(size_type n, const value_type& val=value_type()) | 构造的 l i s t list list 中包含 n n n 个值为 v a l val val 的元素 |
l i s t ( ) list() list() | 构造空的 l i s t list list |
l i s t ( c o n s t l i s t & x ) list(const\ list\&\ x) list(const list& x) | 拷贝构造函数 |
l i s t ( I n p u t I t e r a t o r f i r s t , I n p u t I t e r a t o r l a s t ) list(InputIterator\ first, InputIterator\ last) list(InputIterator first,InputIterator last) | 用 [ f i r s t , l a s t ) [first, last) [first,last) 区间中的元素构造 l i s t list list |
2. iterator 的使用
此处,可以暂时将迭代器理解成一个指针,该指针指向 l i s t list list 中的某个结点。
函数声明 | 接口说明 |
---|---|
b e g i n begin begin + + + e n d end end | 返回第一个元素的迭代器 + + + 返回最后一个元素下一个位置的迭代器 |
r b e g i n rbegin rbegin + + + r e n d rend rend | 返回第一个元素的反向迭代器,即 e n d end end 位置,返回最后一个元素下一个位置的反向迭代器,即 b e g i n begin begin 位置 |
【注意】
-
b e g i n begin begin 与 e n d end end 为正向迭代器,对迭代器执行 ++ 操作,迭代器向后移动。
-
r b e g i n ( e n d ) rbegin(end) rbegin(end) 与 r e n d ( b e g i n ) rend(begin) rend(begin) 为反向迭代器,对迭代器执行 ++ 操作,迭代器向前移动。
3. capacity
函数声明 | 接口说明 |
---|---|
e m p t y empty empty | 检测 l i s t list list 是否为空,是返回 t r u e true true,否则返回 f a l s e false false |
s i z e size size | 返回 l i s t list list 中有效节点的个数 |
4. element access
函数声明 | 接口说明 |
---|---|
f r o n t front front | 返回 l i s t list list 的第一个节点中值的引用 |
b a c k back back | 返回 l i s t list list 的最后一个节点中值的引用 |
5. modifiers
函数声明 | 接口说明 |
---|---|
p u s h _ f r o n t push\_front push_front | 在 l i s t list list 首元素前插入值为 v a l val val 的元素 |
p o p _ f r o n t pop\_front pop_front | 删除 l i s t list list 中第一个元素 |
p u s h _ b a c k push\_back push_back | 在 l i s t list list 尾部插入值为 v a l val val 的元素 |
p o p _ b a c k pop\_back pop_back | 删除 l i s t list list 中最后一个元素 |
i n s e r t insert insert | 在 l i s t p o s i t i o n list\ position list position 位置中插入值为 v a l val val 的元素 |
e r a s e erase erase | 删除 l i s t p o s i t i o n list\ position list position 位置的元素 |
s w a p swap swap | 交换两个 l i s t list list 中的元素 |
c l e a r clear clear | 清空 l i s t list list 中的有效元素 |
6. operations
函数声明 | 接口说明 |
---|---|
s p l i c e splice splice | 将元素从 x x x( l i s t list list) 转移到容器中,并将它们插入到 p o s pos pos 位置(剪切) |
r e m o v e remove remove | 删除 l i s t list list 中值为 v a l val val 的元素 |
u n i q u e unique unique | 删除 l i s t list list 中重复的元素(先排序再去重) |
m e r g e merge merge | 合并有序的多个 l i s t list list |
s o r t sort sort | 对容器中的元素进行排序 |
r e v e r s e reverse reverse | 反转元素的顺序 |
三、list 的迭代器失效问题
之前把迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为 l i s t list list 的底层结构为带头结点的双向循环链表,因此在 l i s t list list 中进行插入时是不会导致 l i s t list list 的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
1. 插入时不失效(不挪动数据):
void test1()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
auto it = l.begin();
l.insert(it, 10);
*it = 99;
for (const auto& i : l) cout << i << " "; cout << endl;
}
2. 删除时失效:
void test2()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
//删除所有偶数
auto it = l.begin();
while(it != l.end())
{
if (*it % 2 == 0)
{
l.erase(it);
}
++it;
}
for (const auto& i : l) cout << i << " "; cout << endl;
}
3. 解决方案 —— —— —— 接收 e r a s e erase erase 返回值,返回的是下一个结点的迭代器:
void test3()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
//删除所有偶数
auto it = l.begin();
while(it != l.end())
{
if (*it % 2 == 0)
{
//需要更新一下it
it = l.erase(it);
}
else
{
++it;
}
}
for (const auto& i : l) cout << i << " "; cout << endl;
}
四、list 与 vector 的对比
v e c t o r vector vector 与 l i s t list list 都是 S T L STL STL 中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
v e c t o r vector vector | l i s t list list | |
---|---|---|
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率 O ( 1 ) O(1) O(1) | 不支持随机访问,访问某个元素效率 O ( N ) O(N) O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素,实现复杂度为 O ( N ) O(N) O(N),插入时有可能需要扩容:开辟新空间,拷贝元素,释放就空间,导致效率更低 | 任意位置插入删除效率高,不需要搬移元素,时间复杂度为 O ( 1 ) O(1) O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层结点动态开辟,小结点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(结点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有迭代器重新赋值,因为插入元素有可能会导致重新扩容,导致原来的迭代器失效;删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效;删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关系插入删除效率 | 大量插入和删除操作,不关心随机访问 |
五、list 的模拟实现
1. 模拟实现 list
根据之前的经验,由于 l i s t list list 也属于 C C C++ S T L STL STL 容器(模板类),因此, l i s t list list 声明和定义都要写到一个文件中。这里不同的是: l i s t list list 的迭代器要自己封装实现(不能用原生态指针了),而且每个结点也是一个独立的结构要单独封装,因此,类外还要定义两个结构体:一个用来封装结点,另一个用来封装迭代器。
因此,总共分为两个文件: l i s t . h list.h list.h 用来存放 l i s t list list 的定义和实现; t e s t . c p p test.cpp test.cpp 用来测试。
注意:这里两个文件中的 l i s t list list 都是自己定义的,为了和 s t d : : l i s t std::list std::list 作区分,我们要单独创建一个命名空间,这里我用的是 n a m e s p a c e y b c namespace\ ybc namespace ybc 。
- l i s t . h list.h list.h
#pragma once
#include<iostream>
#include<list>
#include<assert.h>
using namespace std;
namespace ybc
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _prev;
list_node<T>* _next;
list_node(const T& data = T()) //缺省参数要给匿名对象
:_data(data)
, _prev(nullptr)
, _next(nullptr)
{}
};
template<class T, class Ref, class 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->_data;
}
Ptr operator -> ()
{
return &_node->_data;
}
self& operator ++ ()
{
_node = _node->_next;
return *this;
}
self operator ++ (int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self& operator -- ()
{
_node = _node->_prev;
return *this;
}
self operator -- (int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator == (const self& it) const
{
return _node == it._node;
}
bool operator != (const self& it) const
{
return _node != it._node;
}
};
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
/*iterator it(_head->_next);
return it;*/
/*return iterator(_head->_next); //返回匿名对象*/
return _head->_next; //类型转换
}
iterator end()
{
/*iterator it(_head);
return it;*/
/*return iterator(_head); //返回匿名对象*/
return _head; //类型转换
}
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
void empty_init()
{
_head = new node;
_head->_prev = _head;
_head->_next = _head;
_size = 0;
}
list()
{
empty_init();
}
list(const list<T>& l)
{
empty_init();
for (auto& i : l)
{
push_back(i);
}
}
list<T>& operator = (list<T> l)
{
swap(l);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
void swap(list<T>& l)
{
std::swap(_head, l._head);
std::swap(_size, l._size);
}
iterator insert(iterator pos, const T& x)
{
node* cur = pos._node->_prev;
node* newnode = new node(x);
cur->_next->_prev = newnode;
newnode->_prev = cur;
newnode->_next = cur->_next;
cur->_next = newnode;
++_size;
return newnode;
}
void push_front(const T& x)
{
insert(begin(), x);
}
void push_back(const T& x)
{
insert(end(), x);
}
iterator erase(iterator pos)
{
assert(pos != end());
node* prev = pos._node->_prev;
node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
--_size;
return next;
}
void pop_front()
{
erase(begin());
}
void pop_back()
{
erase(--end());
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
T& front()
{
return _head->_next->_data;
}
const T& front() const
{
return _head->_next->_data;
}
T& back()
{
return _head->_prev->_data;
}
const T& back() const
{
return _head->_prev->_data;
}
private:
node* _head;
size_t _size = 0;
};
}
- t e s t . c p p test.cpp test.cpp
#include"list.h"
namespace ybc
{
template<class Container>
void print(const Container& l)
{
for (const auto& i : l)
{
cout << i << " ";
}
cout << endl;
}
void test1()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
l.push_back(6);
l.push_front(0);
l.push_front(6);
l.push_front(5);
l.push_front(4);
l.push_front(3);
l.push_front(2);
l.push_front(1);
print(l);
}
void test2()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
l.push_back(6);
print(l);
l.pop_back();
print(l);
l.pop_front();
print(l);
}
struct AA
{
int a1 = 1;
int a2 = 2;
};
void test3()
{
list<AA> l;
l.push_back(AA());
l.push_back(AA());
l.push_back(AA());
l.push_back(AA());
l.push_back(AA());
l.push_back(AA());
for (auto it = l.begin(); it != l.end(); ++it)
{
cout << it->a1 << " " << it->a2 << endl;
//cout << it->->a1 << " " << it->->a2 << endl;
//特殊处理:本来应该是两个->才合理,为了可读性,省略了一个->
//cout << it.operator->()->a1 << " " << it.operator->()->a2 << endl;
}
cout << endl;
}
void test4()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
auto iti = l.begin();
l.insert(iti, 10);
*iti *= 99;
print(l);
//删除所有偶数
auto it = l.begin();
while (it != l.end())
{
if (*it % 2 == 0)
{
l.erase(it);
}
++it;
}
print(l);
}
void test5()
{
list<int> l1;
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
l1.push_back(5);
cout << "l1:"; print(l1);
list<int> l2 = l1;
cout << "l2:"; print(l2);
list<int> l3;
l3.push_back(10);
l3.push_back(20);
l3.push_back(30);
l3.push_back(40);
l3.push_back(50);
l3.push_back(60);
cout << "l3:"; print(l3);
l1 = l3;
cout << "l1:"; print(l3);
}
void test6()
{
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
l.push_back(6);
l.push_back(7);
print(l);
cout << l.front() << endl;
cout << l.back() << endl;
}
}
int main()
{
//ybc::test1();
//ybc::test2();
//ybc::test3();
//ybc::test4();
//ybc::test5();
ybc::test6();
return 0;
}
2. list 的反向迭代器(拓展)
通过前面例子知道,反向迭代器的 ++ 就是正向迭代器的 - -,反向迭代器的 - - 就是正向迭代器的 ++,因此反向迭代器的实现可以借助正向迭代器,即:反向迭代器内部可以包含一个正向迭代器,对正向迭代器的接口进行包装即可。
template<class Iterator>
class ReverseListIterator
{
// 注意:此处typename的作用是明确告诉编译器,Ref是Iterator类中的类型,而不是静态成员变量
// 否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量
// 因为静态成员变量也是按照 类名::静态成员变量名的方式访问的
public:
typedef typename Iterator::Ref Ref;
typedef typename Iterator::Ptr Ptr;
typedef ReverseListIterator<Iterator> Self;
public:
// 构造
ReverseListIterator(Iterator it): _it(it){}
// 具有指针类似行为
Ref operator*(){
Iterator temp(_it);
--temp;
return *temp;
}
Ptr operator->(){ return &(operator*());}
// 迭代器支持移动
Self& operator++(){
--_it;
return *this;
}
Self operator++(int){
Self temp(*this);
--_it;
return temp;
}
Self& operator--(){
++_it;
return *this;
}
Self operator--(int)
{
Self temp(*this);
++_it;
return temp;
}
// 迭代器支持比较
bool operator!=(const Self& l)const{ return _it != l._it;}
bool operator==(const Self& l)const{ return _it != l._it;}
Iterator _it;
};
总结
l i s t list list 是 S T L STL STL 封装好的一个双向带头循环链表的模板类。相较于 v e c t o r vector vector 的连续线性空间, l i s t list list 就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释放一个元素空间,且效率为 O ( 1 ) O(1) O(1)。因此, l i s t list list 对于空间的运用有绝对的精准,一点也不浪费。 v e c t o r vector vector 和 l i s t list list 作为两个最常被使用的容器,在上文一个表格中做了对比,两个容器各有优劣,根据各自的应用场景使用最合适的容器,就能够发挥出最好的效果。