序列式容器( List)
前言
在SGI STL中,list容器是一个循环的双向链表,它的内存空间效率较前文介绍的vector容器高。相对于 vector 的连续线性空间,list 就显得复杂许多,他的好处是每次插入和删除一个元素,就配置和释放一个元素空间。因此,list 对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list 永远是常数时间。
与vector容器不同的是,list容器在进行插入操作或拼接操作时,迭代器并不会失效;且不能以普通指针作为迭代器,因为普通指针的+或-操作只能指向连续空间的后移地址或前移个地址,不能保证指向list的下一个节点,迭代器必须是双向迭代器,因为list容器具备有前移和后移的能力。
list 和 vector 是两个最常被使用的容器 , 什么时机最适合使用哪种容器 , 必须视元素的多寡,元素的构造 复杂度 ( 有无 non-trivial copy constructor, non-tirivial copy assigiunen operator) 、元素存取行为的特性而定。
list节点和list数据结构
在list容器中,list本身和list节点是分开设计的,list节点结构是存储数据和指向相邻节点的指针;如下源码所示:
template<class T>
struct _list_node
{
typedef _list_node* void_pointer;
void_pointer prev; //指向直接前驱节点
void_pointer next; //指向直接后继节点
T data; //节点存储的数据
};
list本身的数据结构是只有一个指向链表节点的指针,因为list容器是循环双向链表,则足够遍历整个链表;如下源码所示:
//以下是双向链表list类的定义,分配器_Alloc默认为第二级配置器
template <class T, class Alloc = alloc>
class list
{
protected:
typedef _list_node<T> list_node;
public:
typedef list_node* link_type;
protected:
link_type node; //list是一个环状双向链表,一个指针即可表示整个环状双向链表,指向尾端的空白节点
...
};
下面给出list节点和list本身的数据结构图:
list容器的迭代器
list容器的内存空间存储不一定是连续的,则不能用普通指针做为迭代器;list的迭代器要能够指向list的节点,并且可以进行正确的递增、递减、取值和成员存取等操作。所以迭代器要有前移、后退的能力,所以list的迭代器为双向迭代器。
这也是导致list容器的排序成员函数sort()不能使用STL算法中的排序函数,因为STL中的排序算法接受的迭代器是随机访问迭代器;
list容器在进行插入和拼接操作时迭代器不会失效;以下是源码对迭代器的定义:
#include "listNode.h"
#include "iterator.h"
namespace EasySTL
{
//迭代器本身不是指针,因为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; //指向List节点的迭代器
typedef bidirectional_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; //普通指针指向节点
//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;}
//迭代器的成员存取运算的标准运算
pointer operator->() const {return &(operator*());};
//迭代器向前移动一个位置++
self& operator++()
{
node=(link_type)((*node).next); //node=node->next;
return *this;
}
self operator++(int)
{
self tmp=*this;
++*this;
return tmp;
}
//迭代器后退一个节点
self& operator--()
{
node=node->prev;
return *this;
}
self operator--(int)
{
self tmp=*this;
--*this;
return tmp;
}
self operator+(int dis)
{
self tmp=*this;
while(dis-->0)
tmp=tmp.node->next;
return tmp;
}
self operator-(int dis)
{
self tmp=*this;
while(dis-->0)
tmp=tmp.node->prev;
return tmp;
}
};
}
list容器的构造与析构
list缺省使用alloc二级空间配置器,每次配置、释放、构造和销毁一个空间。初始化时,会生成一个节点其next和prev指针都指向自己。
namespace EasySTL
{
template <class T, class Alloc = alloc>
class list
{
public:
//配置一个节点并传回
link_type get_node()
{
return list_node_allocator::allocate();
}
//释放一个节点
void put_node(link_type x)
{
list_node_allocator::deallocate(x);
}
//产生一个节点,并且带有元素值
link_type create_node(const T& x)
{
link_type newListNode = get_node();
construct(&newListNode->data, x); //所指的数据,创建
return newListNode;
}
//销毁一个元素点 析构并释放
void destroy_node(link_type d)
{
destroy(&d->data);
put_node(d);
}
//构造函数
list() { empty_initialize();} //产生空的链表
~list()
{
clear();
erase(end());
}
protected:
link_type node;//list是一个环状双向链表,一个指针即可表示整个环状双向链表,指向尾端的空白节点
void empty_initialize()
{
node = get_node();
node->next = node;
node->prev = node;
}
...
};
}
list容器的简单成员函数
list容器的成员函数为我们使用该容器提供了很大的帮助,所以这里对其进行讲解,首先先给出源码的剖析,然后在对其中一些重点的成员函数进行图文讲解;先讲解链表操作,插入,删除等:
//尾部插入节点
void push_back(const T& x)
{ //在list尾部空元素处添加数据元素
insert(end(), x);
}
//头部插入节点
void push_front(const T& x)
{
insert(begin(), x);
}
//删除头部节点
void pop_front()
{
erase(begin());
}
//删除尾部节点
void pop_back()
{
erase(--end());
}
//清除所有list节点
void clear()
{
iterator s = begin();
while(s != end())
{
s = erase(s);
}
//空list的状态
node->next=node;
node->prev=node;
}
//删除所有值为x的节点
void remove(const T& x)
{
iterator s = begin();
while(s != end())
{
if(*s == x)
s = erase(s);
else
s++;
}
}
//移除连续并相同的元素
void unique()
{
iterator first = begin();
iterator last = end();
if(first == end) return; //空链表
iterator next = first;
while(++next != last)
{
if (*first == *next)
erase(next);
else
first = next;
next = first;
}
}
iterator erase(iterator position)
{
link_type next_node = position.node->next;
link_type prev_node = position.node->prev;
prev_node->next = next_node;
destroy_node(position.node);
return iterator(next_node);
}
iterator insert(iterator position, const T& x)
{
link_type tmp = create_node(x);
tmp->next = position.node;
tmp->prev = position.node->prev;
position.node->prev->next = tmp;
position.node->prev = tmp;
return tmp;
}
//reverse颠倒顺序
void reverse()
{
iterator start = begin();
iterator stop = end();
if(size() == 0 || size() == 1)
return;
start++;
while(start != stop)
{
iterator next = start + 1;
splice(begin(), start);
start = next;
}
}
//与x交换成员 交换完成后原来两个list上的迭代器要注意
void swap(list& x)
{
link_type tmp = x.node;
x.node = this->node;
this->node = tmp;
}
下面举一个例子对插入函数insert()进行图文分析:假设在以下list链表中节点5之前插入节点9,具体实现见下图步骤:注:图中的箭头旁边的数字表示语句的执行步骤
第一步:首先初始化节点9,并为其分配节点空间;
第二步:调整节点5指针方向,使其与节点9连接;
第三步:调整节点5的前驱结点7指针的方向,使其与节点9连接
下面举一个例子对擦除函数erase()进行图文分析:假设在以下list链表中删除节点5,具体实现见下图步骤:图中的箭头旁边的数字表示语句的执行步骤
第一步:首先调整待删除节点直接前驱指针,使其指向待删除节点的直接后继节点;
第二步:调整待删除节点直接后继指针方向,使其指向待删除节点的直接前驱节点;
第三步:释放待删除节点对象,回收待删除节点内存空;
list容器的重要成员函数
transfer()
以下对迁移操作transfer()进行分析,该函数不是公共接口,属于list容器的保护成员函数,作用就是将特定范围的元素移动到position之前,说白了也是链表操作,只是看起来比较吃力。这个transfer作为内部的函数,为splice、sort、merge等奠定了良好的基础。它为拼接函数服务,拼接函数的核心就是迁移函数;
//将[first,last)之间的元素移动到position之前
void transfer(iterator position, iterator first, iterator last)
{
if (last == position)
return;
link_type last_node = last.node->prev;
//将first last取下来
first.node->prev->next = last.node;
last.node->prev = first.node->prev;
link_type prev_node = position.node->prev;
prev_node->next = first.node;
first.node->prev = prev_node;
last_node->next = position.node;
position.node->prev = last_node;
}
transfer操作如下图:
splice()
transfer并非工开接口。splice提供所谓的接合操作:将某连续范围的元素从一个list移动到另一个list的某一定点。
//x接合与position之前
void splice(iterator position, list& x)
{
if(x.empty())
return;
transfer(position, x.begin(), x.end());
}
//将某一个位置上的元素接合到position之前
void splice(iterator position, iterator i)
{
iterator j = i;
j++;
transfer(position, i, j);
}
//
void splice(iterator position, iterator first, iterator last)
{
if(position == last)
return;
transfer(position, first, last);
}
merge()
merge 将x 与*this合并,两个list必须经过递增排序
void merge(list& x)
{
iterator p = begin();
iterator x_start = x.begin();
while(p != end() && x_start != x.end())
{
std::cout<<*p<<std::endl;
if(*x_start > *p)
p++;
else {
iterator next = x_start + 1; //注意:迭代器跟着节点跑,会脱离原来的list
splice(p, x_start);
x_start = next;
}
}
//如果x没完就添加到尾巴上
if(!x.empty())
splice(end(), x_start, x.end());
}
sort()
list容器不能使用泛型算法sort(),因为list作为链表不支持随机存取,因此,list只能使用自己的sort,暂时写了插入排序,待完善:
//sort list不能使用STL的sort,应为他的迭代器是不是ramdon的
//这里使用的是插入排序
void sort()
{
if(size() == 0 || size() == 1)
return;
list<T, Alloc> tmp;
iterator q = begin();
while(!empty())
{
iterator p = tmp.begin();
while(p!= tmp.end() && *p < *q)
p++;
tmp.splice(p, q);
q = begin();
}
//将tmp赋给本list
swap(tmp);
}
总结
与vector容器不同的是,list容器在进行插入操作或拼接操作时,迭代器并不会失效;且不能以普通指针作为迭代器,因为普通指针的+或-操作只能指向连续空间的后移地址或前移个地址,不能保证指向list的下一个节点,迭代器必须是双向迭代器,因为list容器具备有前移和后移的能力。
list 和 vector 是两个最常被使用的容器 , 什么时机最适合使用哪种容器 , 必须视元素的多寡,元素的构造 复杂度 ( 有无 non-trivial copy constructor, non-tirivial copy assigiunen operator) 、元素存取行为的特性而定。
End