C++及数据结构复习笔记(十二)(列表)

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作为当前节点的后继插至列表。道理和前插是一样。

template <typename T>
ListNodePosi(T) List<T> :: insertAsPred (T const &e)
//将e紧靠当前节点之前插入
{
  ListNodePosi(T) x=new ListNode (e, pred , this);
  //创建新节点
  pred->succ=x;pred=x;//设置正向链接
  return x;
}
template <typename T>
ListNodePosi(T) ListNode<T>:: insertAsSucc (T const &e)
//将e紧随当前节点之后插入
{
  ListNodePosi(T) x=new ListNode (e, this, succ);
  succ->pred=x; succ=x;
  return x;
}

五、基于复制的构造

       通过复制某一已有列表来构造新列表,在输入参数合法的前提下,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从前缀中取出并作为首元素插入后缀。

猜你喜欢

转载自blog.csdn.net/lao_tan/article/details/81007716