一、list概述
list是双向链表,相较于vector的连续性空间,list就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释放一个元素空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list永远是常数时间。
二、list的迭代器
list不能再像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。list迭代器必须有能力指向list的节点,并且有能力进行正确的递增、递减、取值、成员存取等操作。
由于SGI list是一个双向链表,迭代器必须具备前移、后移的能力,所以list提供的是Bidirectional Iterator。如下图所示:
以下是list迭代器的设计:
template<class T, class Ref, class Ptr>
struct __list_iterator {
/* 遵守STL迭代器规范,需要定义这5个类型 */
typedef bidirectional_iterator_tag iterator_category; //双向迭代器
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef ptrdiff_t difference_type;
typedef __list_node<T>* link_type; //链表的节点
link_type node; //节点指针
};
list的迭代器含有一个节点指针,指向list维护的链表中的某个节点。
list迭代器的前置++操作,就是前进一个节点:
self& operator++() {
node = (link_type)((*node).next); //迭代器的node指针指向链表下一个元素
return *this;
}
list迭代器的后置++操作:
self operator++(int){
self tmp = *this;
++*this;
return tmp;
对于list迭代器的前置–和后置–,也是同样的原理。
再看一看取值操作:
reference operator*() const { return (*node).data; } //返回所指节点中的数据值
迭代器的成员存取操作:
pointer operator-> const { return &(operator*()); } //迭代器的成员存取运算子的标准做法
list有一个重要性质:插入操作(insert)和接合操作(splice)都不会造成原有的list迭代器失效。这在vector是不成立的。因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作(erase)也只有“指向被删除元素”的那个迭代器失效,其他迭代器不受影响。
三、list的数据结构
SGI list不仅是一个双向链表,而且还是一个环状双向链表。所以它只需要一个指针,便可以完整表现整个链表:
template <class T, class Alloc = alloc>
class list {
typedef __list_node<T> list_node; //节点
typedef list_node* link_type; //链接节点
typedef simple_alloc<list_node, Alloc> list_node_allocator; //空间配置器
typedef __list_iterator<T, T&, T*> iterator; //迭代器
protected:
link_type node; //成员
};
list内部的成员只有link_type node,其类型为list_node*,表示链表中的一个节点。下面来看一看list_node的定义:
template <class T>
struct __list_node {
typedef void* void_pointer;
void_pointer next; //指向下一个元素的指针
void_pointer prev; //指向上一个元素的指针
T data; //数据
};//如下图所示:
如果让指针node指向可以置于尾端的一个空白节点,node便符合STL对于“前闭后开”区间的要求,成为last迭代器。如下图所示:
虚线表示指向上一个元素,实线表示指向下一个元素。
四、list的构造与析构
默认构造函数
void empty_initialize() {
node = get_node(); //分配内存
node->next = node;
node->prev = node;
}
list() { empty_initialize(); }
node是list中的链表头节点,当默认构造的时候,list里面没有元素,所以初始化一个链表头,首先分配内存,然后将首尾指针指向自己。
拷贝构造函数
template <class InputIterator>
void range_initialize(InputIterator first, InputIterator last) {
empty_initialize(); //初始化链表头
insert(begin(), first, last); //插入所有元素
}
list(const list<T, Alloc>& x) {
range_initialize(x.begin(), x.end());
}
list的拷贝构造会调用range_initialize,range_initialize函数会先初始化list的链表头,然后再通过insert来复制所有的节点,下面来看一看insert:
template <class T, class Alloc>
void list<T, Alloc>::insert(iterator position, const T* first, const T* last) {
for ( ; first != last; ++first)
insert(position, *first);
}
list的insert函数会从头到尾,调用全局函数insert来插入,其定义如下:
iterator insert(iterator position, const T& x) {
link_type tmp = create_node(x); //分配内存
/* 双向链表插入元素的过程 */
tmp->next = position.node;
tmp->prev = position.node->prev;
(link_type(position.node->prev))->next = tmp;
position.node->prev = tmp;
return tmp;
}
insert首先会分配一个节点,然后插入双向链表中。如下图所示:
析构函数
~list() {
clear();
put_node(node);
}
clear()的作用是清除头结点外的所有结点,其定义如下:
template <class T, class Alloc>
void list<T, Alloc>::clear()
{
link_type cur = (link_type) node->next; //得到第一个元素
while (cur != node) { //遍历链表
link_type tmp = cur;
cur = (link_type) cur->next; //从链表删除节点
destroy_node(tmp); //释放节点
}
node->next = node;
node->prev = node;
}
clear函数会遍历整个链表,将每个元素从链表中删除,然后释放它,destroy_node的定义如下:
void destroy_node(link_type p) {
destroy(&p->data); //析构对象
put_node(p); //释放节点
}
destroy_node首先通过destroy析构对象,然后通过put_node释放内存,put_node定义如下:
/* 通过空间配置器释放内存 */
void put_node(link_type p) { list_node_allocator::deallocate(p); }
五、插入和删除元素
插入元素
可以通过insert和push_front还有push_back插入元素,insert表示在指定位置插入节点,上面已经讨论过了,这里不讨论了,下面来看一看push_front和push_back。
push_front()表示在链表头插入节点,其定义如下:
void push_front(const T& x) { insert(begin(), x); }
push_back()表示在链表尾插入节点,其定义如下:
void push_back(const T& x) { insert(end(), x); }
注:所谓“插入”,是指“插入在…之前”。
删除元素
可以通过erase、pop_front、pop_back删除节点。erase删除指定节点,pop_front删除链表第一个节点,pop_back删除链表最后一个节点。
erase()的定义如下:
iterator erase(iterator position) {
/* 双向链表删除节点的过程 */
link_type next_node = link_type(position.node->next);
link_type prev_node = link_type(position.node->prev);
prev_node->next = next_node;
next_node->prev = prev_node;
destroy_node(position.node); //释放节点
return iterator(next_node);
}
首先将指定节点从双向链表中删除,然后调用destroy_node析构还有释放节点。
pop_front()定义如下:
void pop_front() { erase(begin()); }
pop_back()定义如下:
void pop_back() {
iterator tmp = end();
erase(--tmp);
}
end()指向的是最后一个节点的下一个位置,所以需要将迭代器减一(即往后移动一个位置)才能得到最后一个节点。
六、其他操作
transfer()
将某连续范围的元素迁移到某个特定位置之前。技术上讲很简单,节点直接的指针移动而已。这个操作为其他复杂操作如splice,sort,merge等奠定了良好的基础。
void transfer(iterator position, iterator first, iterator last) {
if (position != last) {
(*(link_type((*last.node).prev))).next = position.node; //(1)
(*(link_type((*first.node).prev))).next = last.node; //(2)
(*(link_type((*position.node).prev))).next = first.node;//(3)
link_type tmp = link_type((*position.node).prev); //(4)
(*position.node).prev = (*last.node).prev; //(5)
(*last.node).prev = (*first.node).prev; //(6)
(*first.node).prev = tmp; //(7)
}
} //如下图所示
七、总结
●初始化操作
list<Type> a; //定义一个Type类型的链表a
list<Type> a(10); //定义一个Type类型的链表a,并设置初始大小为10
list<Type> a(10, 1); //定义一个Type类型的链表a,并设置初始大小为10,且初始值都为1
list<Type> a(b); //定义并用链表b初始化链表a_
除此之外,还可以直接使用数组来初始化向量:
int array[] = {1, 2, 3, 4, 5};
list<int> a(array, array+5); //将数组n的前5个元素作为链表a的初值
● 与迭代器相关
begin(); //链表的第一个元素
end(); //指向最后一个元素的下一个位置
rbegin(); //反向迭代器指针,指向最后一个元素
rend(); //反向迭代器指针,指向第一个元素的前一个元素
●与容量相关
empty(); //判断链表为空
size(); //容器大小
resize(); //重置有效元素个数
●与元素访问相关
front(); //访问链表中第一个元素
back(); //访问链表中最后一个元素
●添加元素
push_front(const T& x); //头部添加元素
push_back(const T& x); //尾部添加元素
insert(iteratpr it, const T& x); //任意位置插入一个元素
insert(iterator it, int n, const T& x); //任意位置插入n个相同元素
insert(iterator it, iterator first, iterator last); //插入另一个向量的[first,last]间的数据
●删除元素
pop_front(); //头部删除元素
pop_back(); //尾部删除元素
erase(iterator it); //任意位置删除一个元素
erase(iterator first, iterator last); //删除[first, last]之间的元素
clear(); //清空所有元素
●其他操作
assign(int nSize, const T& x); //类似于初始化时用数组进行赋值
swap(list&); //交换两个同类型容器的元素
merge(); //合并两个列表的元素(默认升序排列)
splice(iterator it, list&); //在任意位置拼接入另一个list
unique(); //删除容器中相邻的重复元素
可以看到,list 与 vector、deque 的用法基本一致,除了以下几处不同:
● list 为双向迭代器,故不支持it+=i;
● list 不支持下标访问和at方法访问。