C++及数据结构复习笔记(十一)(向量)

第二章 C++数据结构 

       本章主要介绍了C++基本的数据结构,包括向量、列表、栈与队列、二叉树和图。主要的总结均来自于邓俊辉老师的《数据结构C++语言版》,在每一小节的背后,给出了一些在网上总结的面试题,以加强我们对C++数据结构的理解。并且每一小节均给出了典型的C++程序代码,理解这些代码是读懂文章的关键。

2.1 向量

2.1.1 向量的概念和接口

一、概念

      向量vector就是线性数组的一种抽象与泛化,它也是具有线性次序的一组元素构成的集合 ,其中的元素分别由秩相互区分。若元素e的前驱元素共有r个,则e的秩就是r。

二、抽象数据类型(ADT)接口

       insert(r,e)//e作为秩为r的元素插入,原后继元素依次后移

       put(r,e)//用e替换秩为r的元素的数值

       get(r)//获取秩为r的元素

       remove(r)//删除秩为r的元素,返回所删除的元素的秩

       ……

       向量接口的具体实现见:https://blog.csdn.net/qq_23912545/article/details/70216948。

2.1.2 构造与析构

       由Vector类模板可知,向量结构在内部维护一个元素类型为T的私有数组_elem[],其容量由私有变量_capacity指示,向量当前的实际规模由_size指示。向量中秩为r的元素,对应于内部数组中的_elem[r],其物理地址为_elem+r。

       与所有对象一样,向量在使用之前需要借助构造函数做初始化。构造方法有2种,一是默认构造方法:根据指定的初始容量创建内部私有数组_elem[],若未指定容量,则使用默认值DEFAULT_CAPACITY。二是基于复制的构造方法,以某个已有的向量为蓝本,进行局部或整体的克隆。

例 2.1.1基于复制的构造的copyFrom()方法

template <typename T>//元素类型
void vector<T>::copyFrom(T const* A, Rank lo, Rank hi)
//定义数组区间A[lo,hi]为蓝本复制向量
{
  _elem =new T[_capacity=2*(hi-lo)]; _size=0;//分配空间,规模清0
  while(lo<hi)
_elem[_size++]=A[lo++];//复制至_elem[0,hi]
}

       析构方法:~Vector():只释放用于存放元素的内部数组_elem[],将其占用的空间交还给操作系统。_capacity和_size之类的内部变量无需做任何处理。

2.1.3 动态空间管理

       内部数组所占物理空间的容量,若在向量的生命周期内不允许调整,则称作静态空间管理策略。但是容量固定的话,在更多新元素到来时,会产生上溢。定义装填因子为向量实际规模与其内部数组容量的比值,即_size/_capacity。该指标可以衡量空间利用率。为使装填因子不超过1也不接近0,需要改用动态空间管理策略。动态空间管理策略有2种方法:

动态扩容算法

动态缩容算法

当空间耗尽时,就动态地扩大内部数组的容量,但不是在原有物理空间的基础上追加空间,而是另外申请一个容量更大的数组B,并将原数组A中成员集体搬迁进来。当然,原数组所占空间也需要及时释放。expand()扩容算法具体代码实现如下:

当向量的实际规模远远小于内部数组的容量时,装填因子接近于0.当装填因子低于某一阈值时,我们称数组发生了下溢。此时应当缩减内部数组容量。shrink()动态缩容算法如下:

template <typename T>

void Vector<T>::expand()  //向量空间不足时扩容

{

  if (_size<_capacity) return;  //尚未满员时不扩容

  if (_capacity <DEFAULT_CAPACITY ) _capacity=DEFAULT_CAPACITY;

//容量不能低于最小容量

  T* oldElem=_elem;  //将原来的数据保存

  _elem=new T[_capacity<<=1];  //容量加倍

  for (int i=1;i<_size;i++)

{

_elem[i]=oldElem[i];//复制原向量内容

  }

  delete [] oldElem;  //释放原空间,delete

}

template <typename T>

void Vector<T>::shrink()//装填因子过小时压缩向量所占空间

{

if (_capacity<DEFAULT_CAPACITY<<1)

//不至收缩到DEFAULT_CAPACITY以下

if (_size<<2> _capacity) return;

//以25%为装填因子的阈值

T* oldElem=_elem;

_elem=new T[_capacity>>=1];//容量减半

for (int i=0;i<_size;i++)

{

  _elem[i]=oleElem[i];

}

delete [] oldElem;

}

在调用insert()接口插入新元素之前,都要先调用该算法检查可用容量。

2.1.4 常规向量

一、直接引用元素

       可以通过重载运算符“[ ]”,可以通过下标直接访问元素。

Eg:

template<typename T>
T&Vector<T>::operator[ ](Rank r) const //重载运算符
{return_elem[r];}

二、置乱器

       向量整体置乱算法permuted()用于随机向量的生成,其不仅可以枚举出同一向量的所有可能排列,而且能够保证生成的各种排列概率均等。

三、无序查找

       Vector::find(e)接口的功能是查找与对象e相等的元素,即向量元素可通过相互对比判断是否相等。仅支持对比但未必支持比较的向量称作无序向量。从最后一个元素开始向前,逐一选取各个元素与目标元素e比较,直至发现与之相等者(查找成功),或者直至检查完所有元素仍未找到相等者(查找失败)。这种逐个比较的查找方式称为顺序查找。

例 2.1.2向量区间的顺序查找算法(无序向量元素查找接口find())

template <typename T>
//无序向量的顺序查找,返回最后一个元素e的位置,失败则返回lo-1
Rank Vector<T>::find(T const &e, Rank lo, Rank hi) const
{    
//assert: 0<=lo<hi<=_size
while((lo<hi--)&&(e!=elem[hi]))  //从后向前,顺序查找
return hi;//若hi<lo,则意味着失败
}

四、插入

       根据ADT定义,插入操作符insert(r,e)负责将任意给定的元素e插到指定的秩为r的单元。向量元素插入接口的insert()代码如下:

template <typename T>  //将e作为秩为r的元素插入
Rank Vector<T>::insert(Rank r, T const& e)
{
  //assert:0<=r<=_size
  expand();  //若有必要,扩容
  for (int i=_size;i>r;i--) 
_elem[i]=_elem[i-1];  //自后向前,后继元素以此后移一个单元
_elem[r]=r;//置入新元素
  _size++;  //更新容量
  return r;//返回秩
}

五、删除

       删除操作经过重载有2个接口,remove(lo,hi)表示删除区间[lo,hi)内的元素,remove(r)删除秩为r的单个元素。应将单元素删除视为区间删除的特例。

向量区间删除接口remove(lo,hi)

向量单元素删除接口remove(r)

template <typename T>
int Vector<T>::remove(Rank lo, Rank hi)
//删除区间(lo,hi)
{
  if (lo==hi) return 0;
//出于效率考虑,单独处理退化情况,比如remove(0,0)
while(hi<_size)
  _elem[lo++]=_elem[hi++];
  //[hi,_size]依次前移hi-lo个单元
_size=_size-hi+lo;//更新规模
shrink();//若有必要则缩容
return hi-lo;//返回被删除元素数目
}

利用remove(lo,hi)通用接口,通过重载即可实现另一同名接口remove(r)

template <typename T>
T Vector<T>::remove(Rank r)
//删除向量中秩为r的元素
{
  T e=_elem[r];//备份删除元素
  remove(r,r+1);
//删除r元素等效于对区间(r,r+1)的删除
  return e;
}

六、唯一化

       向量的唯一化处理指的是删除其中重复的元素,即deduplicate()接口的功能。无序向量的唯一化算法如下
template <typename T>
int Vector<T>::deduplicate()
{
  int oldSize=_size;//记录原规模
  Rank i=1;//从elem[1]开始
  while(i<_size)  //自前向后逐一考虑各元素_elem[i]
(find(_elem[i],0,i)<0)?  i++:remove(i) ;
//在前缀中寻找与_elem[i]的相同者,但至多只找一个。若没有找到则继续考察后继(令i加一),否则删除相同者
  return oldSize-_size;//返回被删除元素个数
}

2.1.5 有序向量

       若向量S[0,n]中的所有元素不仅按线性次序存放,而且其数值大小也按此次序单调分布,则称作有序向量。有序向量通常约定其中的元素自前向后构成一个非降序列,即对任意0≤i<j<n,都有S[i]<S[j]。

一、有序性甄别

       作为无序向量的特例,有序向量自然可以沿用无序向量的查找算法。然而得益于元素之间的有序性,有序向量的查找和唯一化等操作都可更快速地完成。因此在实施此类操作前,有必要先判断当前序列是否有序,以便确定是否采用更高效的接口。有向序列甄别算法如下:

template <typename T>
int Vector<T>::disordered() const
//返回向量中逆序相邻对的总数
{
  int n=0; //计数器
  for (int i=1;i<_size;i++) //逐一检查_size-1对相邻元素
if (_elem[i-1]>_elem[i]) n++; //逆序则计数
  return n; //有序时返回n=0
}

二、有序向量的唯一化

       相对于无序向量,有序向量中清除重复元素的操作更为重要。处于效率的考虑,在清除无序向量中重复元素的时候,一般做法是先将其转化为有序向量。唯一化的实现有低效和高效2种形式。

低效版

(有序向量uniquify()接口的平凡实现)

高效版

(有序向量uniquify()接口的高效实现)

template <typename T>
int Vector<T>:::uniquify()
//有序向量重复元素剔除算法(低效版)
{
  int oldSize=_size;
  int i=1;//当前对比元素的秩i,从1开始
  while(i<_size)
//从前向后,逐一对比个相邻元素
_elem[i-1]==__elem[i]? remove(i):i++;
//若相同,则删除后者,否则i++
  return oldSize-_size;//返回删除元素个数
}
template <typename T>
int Vector<T>::uniquify()
//有序向量重复元素剔除算法(高效版)
{
  Rank i=0,j=0;//各对互异相邻元素的秩
  while(++j<_size) //逐一扫描,直至末元素
if(_elem[i]!=_elem[j])//跳过相同者
_elem[++i]=_elem[j];
//发现不同元素时,向前移至紧邻前者右侧
  _size=++I;shrink();
  return j-i;
}

该算法效率与无序向量的唯一化算法是一样的,需要改进。

在有序向量中,每一组重复元素都必然紧邻地集中分布,因此可以以区间为单位成批的删除,并将其后继元素统一的大跨度前移。

三、有序向量的查找

       有序向量S中的元素不再随机分布,秩r是S[r]在S中按大小的相对次位。一般的,若小于、等于S[r]的元素各有i,k个,则该元素及其相同元素应集中分布于S[i,i+k]。为区别于无序向量查找接口find(),有序向量的查找接口统一命名为search()。该接口的具体实现算法如下:

template <typename T>//在有序向量的区间[lo,hi]内,确定不大于e的最后一个节点的秩
Rank Vector<T>::search(T const& e, Rank lo, Rank hi) const
//assert:0<=lo<hi<=_size
{
  return (rand()%2)? binSearch(_elem,e,lo,hi):fibSearch(_elem,e,lo,hi);
  //各有一半的概率采用二分查找或Fibonacci查找
}

二分查找

Fibonacci查找

使用“减而治之”的策略来实现有序向量的查找。以任意元素S[mi]为界,可将区间分为3部分,根据此时的有序性,便有:

S[lo,mi]≤S[mi]≤S[mi,hi]将目标元素e与x作比较,则有三种可能的情况,直到e=x才终止查找。

事实上,减治策略本身并不要求子向量切割点mi必须居中,改进二分法的思路,不妨按黄金分割比来确定mi。简单起见,假设向量长度为n=fib(k)-1。

/

/二分查找算法:在有序向量的区间[lo,hi]中查找元素e
template <typename T>
static Rank binSearch(T* A const& e, Rank lo, Rank hi)
{
  while(lo<hi)
  { //每一步都要做3种判断
Rank mi=(lo+hi)/2 ;//以中间为轴点
if (e<A[mi]) hi=mi;
else if (A[mi]>e) lo=mi+1;
else return mi;
  }//查找成功则可以提前结束
  return -1;//查找失败
}

暂不总结

有多个命中元素时,不能保证返回秩最大者;查找失败时,不能指示失败的位置。

2.1.6 排序与下界

       有序向量的诸如查找等操作,要率要远高于一般向量,因此在实际解决问题时普遍采用的做法是:将向量转化为有序向量,随后调用有序向量支持的各种高效算法。根据其处理的数据规模与存储的特点不同,可分为内部排序算法和外部排序算法;根据输入形式的不同,排序算法也可分为离线算法和在线算法。

       一般地,任一问题在最坏情况下的最低计算成本称为该问题的复杂度下界。一旦某一算法的性能达到这一下界,即意味着它在最坏的情况下是最优的。

2.1.7 排序器

       由于排序算法十分重要,排序操作也应当列入向量基本接口的范围,这类接口也是将无序向量转化为有序向量的基本方法和主要途径。

例 2.1.3向量排序器接口

template <typename T>
void Vector<T>::sort(Rank lo,Rank hi)
//在向量区间[lo,hi]中排序
{
  switch(rand()%5)  //随机选取5种排序算法中的一种
case 1: bubbleSort(lo,hi); break;//冒泡排序
case 2: selectionSort(lo,hi);break;//选择排序
case 3: mergeSort(lo,hi);break;//归并排序
case 4: heapsort(lo,hi);break;//堆排序
default: quicksort(lo,hi);break;//快速排序
}

一、冒泡排序

       冒泡排序的具体算法已经在第一章节中讲过了,现在只需将其集成至向量ADT中。

template <typename T>
void Vector<T>::bubbleSort(Rank lo,Rank hi)  //向量的冒泡排序
{
  while(!bubble(lo,hi--));
//逐趟作扫描交换,直至全序。由于每次都把最大值放在最后后,因此在每轮之后都需要hi--
}
template <typename T>
bool Vector<T>::bubble(Rank lo,Rank hi)
{
  bool sorted=ture;//整体有序标志
  while(++lo<hi)  //自左向右,逐一检查各对相邻元素
if (_elem[lo-1]>_elem[lo]) //若逆序
{
  sorted=false;//意味着尚未整体有序
  swap(_elem[lo-1],_elem[lo]);  //通过交换使局部有序
}
  return sorted;//返回有序标志
}

二、归并排序

       与冒泡排序通过反复调研用单趟扫描交换类似,归并排序也可以认为是通过反复调用所谓二路归并算法实现的。所谓二路归并,就是将两个有序序列合并成为一个有序序列。二路归并属于迭代算法,,在每步迭代中,只需比较2个待归并向量的首元素,将小者取出并追加到输出向量的末尾,该元素在原向量中的后继则成为新的首元素。如此往复,直到某一向量为空。最后将另一非空向量整体接至输出向量的尾端。具体实现如图2.1。

图 2.1 二路归并排序

       归并排序的主体结构属于典型的分治策略,可递归地描述和实现:

template <typename T> //向量归并排序
void Vector<T>::mergeSort(Rank lo, Rank hi)
{
  if (hi-lo<2)  return;  //单元素区间自然有序
  int mi=(lo+hi)/2;
  mergeSort(lo,mi);mergeSort(mi,hi);merge(lo,mi,hi);//分别对前后两段排序,然后归并
}

       通过对递归的不断深入,不断地均匀划分子向量,直到其规模等于1,从而抵达递归基。递归返回时,通过反复调用二路归并算法,相邻且等长的子向量不断地成对合并为规模更大的有序向量,直至得到整个有序向量。

猜你喜欢

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