2.2 列表
向量结构中,各数据的物理存放位置与逻辑次序完全对应,故可通过秩直接访问对应的元素,这称为循秩访问。为保证对列表元素访问的可行性,逻辑上互为前驱和后继的元素之间,应维护某种索引关系。这种索引关系可被抽象地理解为被索引元素的位置,故列表元素是循位置访问的。
2.2.1 向量到列表
引入列表结构的目的在于弥补向量结构在解决某些应用问题时,在功能及性能方面的不足,二者差异的根源在于其内部存储方式的不同。列表结构尽管也要求各元素在逻辑上具有线性次序,但对其物理地址却未做任何限制,这就是所谓的动态存储策略。在其生命周期内,此类数据结构将随着内部数据的需要,相应地分配或回收局部的数据空间。作为补偿,此类结构将通过指针和引用等机制,来确定各元素的实际物理地址。链表就是一种典型的动态存储结构。采用动态存储策略,至少可以大大降低动态操作的成本。
与向量中秩的地位和功能类似,列表中的位置也是指代各数据元素的一个标识性指标,借助它可以便捷地得到元素的物理存储地址。各元素的位置,通常可以表示为连接于元素之间的指针或引用。
列表与向量一样,也是由具有线性逻辑次序的一组元素构成的集合:L={a0.......an-1}。列表是链表结构的一般化推广,其中的元素称为节点Node。
2.2.2 接口
列表节点的ADT接口 | 列表的ADT接口 |
data() //当前节点所存数据对象 pred() //当前节点前驱节点的位置 succ() //当前节点后继节点的位置 insertAsPred(e) //插入前驱节点,存入被引用对象e,返回新节点位置 insertAsSucc(e) //插入后继节点,存入被引用对象e,返回新节点位置 | size()//报告列表当前的规模(节点总数) first()//返回首元素位置 insertAsFirst(e)//将e当作首节点插入 insertBefore(p,e)//将e当作节点p的直接前驱 remove(p)//删除p处节点,返回其数值 sort()//调整各节点位置,使之按非降序排列 find(e)//查找目标元素e deduplicate()//删除重复节点 |
header称为头哨兵,trailer称为尾哨兵。列表结构的实现方式类似于向量结构:通过模版参数T指定列表元素的类型,在内部设置私有变量以记录当前规模等状态信息,基于多种排序算法提供统一的sort()接口,将列表转化为有序列表。
2.2.3 列表
一、头、尾节点
List对象的内部组成与逻辑结构如图2.1所示,其中私有的头结点header和尾节点trailer始终存在,对外不可见。虚线框内是对外可见的数据节点,其中的第一个和最后一个节点分别称作首节点firstNode和末节点lastNode。头节点紧邻于首节点之前,尾节点紧邻于末节点之后。这类封装之后从外部不可见的节点称作哨兵节点。
图 2.2 首(末)节点是头(尾)节点的直接后继(前驱)
二、默认构造方法
创建List对象时,默认构造方法将调用统一初始化过程init(),在列表内部创建一对头尾哨兵节点,并适当地设置其前驱、后继指针,构成一个双向链表。该链表对外的有效部分初始为空。
template <typename T>
typedef int Rank;//自定义数据类型Rank,它是整形的
#define ListNodePosi(T) ListNode<T>* //列表节点的位置,每个节点都是一个结构体
void List<T>::init() //列表初始化,在创建列表对象时统一调用
{
header=new ListNode<T>;//创建头哨兵节点
trailer=new ListNode<T>;//创建尾哨兵节点
header->succ=trailer;//header的后继节点是trailer
header->pred=NULL;
trailer->pred=header;trailer->succ=NULL;
_size=0;//记录规模
}
三、查找
列表ADT中针对整体与区间的查找,重载了接口find(e)和find(e,p,n)。前者作为特例,可直接调用后者。
template <typename T>
ListNodePosi(T) List<T>::find(T const& e,int n,ListNodePosi(T) p) const
// ListNodePosi(T) 是列表节点的位置类型,是一个指针,在前面宏定义过
//0<=n<=rank(p)<_size
{
while(0<n--) //对于p最近的n个前驱,从右到左
if (e==(p=p->pred)->data) return p;
//逐个比对,直至命中或越界,p->pred指的是获得p指向的对象的pred成员
return NULL;
}
四、插入
将节点插入列表有多种方法,列表提供了多种接口
template <typename T>
ListNodePosi(T) List<T>::insertAsFirst(T const &e) //将e当作首节点插入
{
_size++; return header->insertAsSucc(e);
//首先使规模加1,然后调用头结点header的insertAsSucc()函数,在后面插值
}
template <typename T>
ListNodePosi(T) List<T>::insertAsLast(T const &e) //将e当作末节点插入
{
_size++; return trailer->insertAsPred(e);
}
template <typename T>
ListNodePosi(T) List<T>::insertBefore(ListNodePosi(T) p, T const &e)//将e当作p的前驱插入
{
_size++; return p->insertAsPred(e);//调用节点p的insertAsPred()函数,在前面插值
template <typename T>
ListNodePosi(T) List<T>::insertAfter(ListNodePosi(T) p, T const &e)//将e当作p的后继插入
{
_size++; return p->insertAsSucc(e);
}
列表节点对象的前插入接口 | 列表节点对象的后插入接口 |
将新元素e作为当前节点的前驱插至列表。具体操作如下:首先创造新节点new,构造函数的同时使其数据项为e,后继链接succ指向当前节点this,令其前驱链接pred指向当前节点的前驱节点;然后使new成为当前节点的前驱节点的后继,使new称为当前节点的前驱,这个次序不能颠倒。 | 将新元素e作为当前节点的后继插至列表。道理和前插是一样。 |
|
|
五、基于复制的构造
通过复制某一已有列表来构造新列表,在输入参数合法的前提下,copyNodes()首先调用init()方法,创建头尾哨兵节点并作相应的初始化处理,然后自p所指节点起,从原列表中取出n个相邻的节点,并逐一作为末节点插至新列表中。
template <typename T>
void List<T>::copyNodes(ListNodePosi(T) p, int n)
//复制列表自p开始的n项,p合法且至少有n-1个真后继节点
{
init();//创建头尾哨兵节点并初始化
while(n--)
{
insertAsLast(p->data);
p=p->succ;
}
六、删除
删除指定节点p,首先令p的前驱节点和后继节点互相连接,然后释放掉已经被孤立出来的节点p,同时相应的更新列表规模计数器_size。
template <typename T>
T List<T>::remove(ListNodePosi(T) p) //删除合法位置p处的节点
{
T e=p->data; //备份待删除节点的数据
p->pred->succ=p->succ;
//p->pred是p的前驱元素,p->pred->succ指的是p的前驱元素的后继元素
p->succ->pred=p->pred;
delete p;//释放节点p
_size--;//更新规模
return e;//返回备份的数据
}
七、析构
释放资源及清除节点,列表的析构首先要调用clear()接口删除并释放所有对外有效的节点,然后释放内部的头尾哨兵节点。
template <typename T>
List<T>::~List()
{
clear();//清空列表
delete header; delete trailer;//释放头、尾哨兵节点
}
template <typename T>
int List<T>::clear()
{
int oldSize=_size;
while(0<_size) remove(header->succ);//反复删除首节点
return oldSize;
}
八、唯一化
用于删除无序列表中重复元素的接口deduplicate(),类似于Vector::deduplicate(),都是自前向后依次处理各节点p,一旦通过find()找到相同者,就调用remove()。
template <typename T>
int List<T>::deduplicate() //删除无序列表中的重复节点
{
if(_size<2) return 0;平凡列表自然无覆盖
int oldSize=_size;//记录原规模
ListNodePosi(T) p=header; Rank r=0; //p从首节点开始
while(trailer!=(p=p->succ))
{
ListNodePosi(T) q=find(p->data,r,p); //在p的r个前驱中寻找相同的
q? remove(q):r++; //若存在则删除,否则秩加一
}
return oldSize-_size;//返回被删除的元素总数
}
2.2.4 有序列表
若列表中所有节点的逻辑次序与其大小次序完全一致,则称作有序列表。为保证节点间可以定义次序,假定元素类型T可以直接支持大小的比较。
一、唯一化
与有序向量同理,有序列表中相同的节点也必然在逻辑上彼此相邻。利用这一特性,可以实现有序列表的重复节点删除算法uniquify()。指针p和q分别指向每一对相邻的节点,若2者相同时则删除q,否则转向下一对相邻节点。如此反复直至检查过所有节点。
template <typename T>
int List<T>::uniquify() //成批删除重复元素
{
if (_size<2) return 0;
int oldSize=_size;
ListNodePosi(T) p; ListNodePosi(T) q;//定义2个指针变量,依次指向紧邻的各对节点
for (p=header, q=header->succ; trailer!=q; p=q;q=q->succ)//自左向右扫描
if (p->data=q->data) {remove(q),q=p;}//若q和p相同,则删除q
return oldSize-_size;
}
二、查找
template <typename T>
//在有序列表内节点p的n个前驱中,找到不大于e的最后者
ListNodePosi(T) List<T>::search(T const &e, int n, ListNodePosi(T) p) const
{
while(0<n--) //对于p最近的n个前驱,从右向左逐个比较
if (((p=p->pred)->data)<=e) break;
//p=p->pred,使p指向p的前驱节点
//(p=p->pred)->data为前驱节点的data成员
return p;
}
2.2.5 排序器
与无序向量一样,针对无序列表任意合法区间的排序需求,设置一个统一的排序操作接口。
template <typename T>
void List<T>::sort(ListNodePosi(T) p, int n)
{
switch(rand()%3) //随机选择三种排序方法中的一种
{
case 1: insertionSort(p,n);break;//插入排序
case 2: selectionSort(p,n);break;//选择排序
default: mergeSort(p,n);break;//归并排序
}
}
一、插入排序
插入排序算法适用于包括向量与列表在内的任何序列结构,算法的思路可简要的描述为:始终将整个序列切分为2个部分,有序的前缀和无序的后缀。经过迭代,反复地将后缀元素的首地址转移到前缀中。如此,前缀范围不断扩展,直至覆盖整个序列。
template <typename T> //对起始于位置p的n个元素排序
void List<T>::insertSort(ListNodePosi(T) p,int n)
{
for (r=0;r<n;r++)
{
insertAfter(search(p->data,r,p),p->data);//查找适当的位置并插入,将p->data作为search(p->data,r,p)的后继插入,search(p->data,r,p)返回的是一个指针
p=p->succ; remove(p->pred);//转向下一节点
}
}
二、选择排序
将序列分为无序前缀和有序后缀2部分,每次只需从前缀中选出最大者,并作为最小元素转移至后缀中,即可使有序部分的范围不断扩张。算法的初始时刻,后缀为空,于是调用无序序列的查找算法,从前缀中找到最大者M。然后将M从前缀中取出并作为首元素插入后缀。