1.接口与实现
1.1 ADT和DS
学习实现vector之前,首先明确两个概念
Abstract Data Type抽象数据类型:数据模型+定义在其上的一组操作
Data Structure数据结构:基于某种特定的语言实现ADT的一整套算法
Application:Interface接口+Implementation实现
1.2 vectorADT接口
向量是数组的抽象与泛化,由一组元素按线性次序封装而成,各元素与[0,n)内的Rank(秩)一一对应,它具有以下接口
1.3 vector模板类
typedef int Rank;//秩
#define DEFAULT_CAPACITY 3//默认初始容量
template <typename T> class Vector{
private:Rank _size;int _capacity;T* _elem;//规模、容量、数据区
protected:
/*...内部函数*/
public:
/*...构造函数*/
/*...析构函数*/
/*...只读接口*/
/*...可写接口*/
/*...遍历接口*/
};
下图是vectorADT的图例
从哪里开始下手呢?当然是从构造函数和析构函数开始啦。这是数据结构的基础
思考应该实现哪些构造呢?
1.默认构造函数
2.数组区间复制
3.数组整体复制
4.向量区间复制
5.向量整体复制
/*默认构造函数*/
vector(int c=DEFAULT_CAPACITY){
_elem=new T[_capacity=c];
_size=0;
}
/*复制构造函数*/
//数组区间复制
vector(T const *A,Rank lo,Rank hi){
copyFrom(A,lo,hi);
}
//数组整体复制
vector(T const *A,Rank n){
copyFrom(A,0,n);
}
//向量区间复制
vector(vector<T> const &V,Rank lo,Rank hi){
copyFrom(V._elem,lo,hi);
}
//向量整体复制
vector(vector<T> const &V){
copyFrom(V._elem,0,V._size);
}
内部函数 void copyFrom(T const*,Rank,Rank);
template <class T>//T为基本类型,或已重载赋值操作符‘=’
void vector<T>::copyFrom(T const *A,Rank lo,Rank hi){
_elem=new T[_capacity=2*(hi-lo)];//分配空间
_size=0;//规模清零
while(lo<hi)//A[lo,hi)内的元素逐一
_elem[_size++]=A[lo++];//复制至_elem[0,hi-lo)
}
然后是析构函数
~vector(){delete [] _elem;} //释放内部空间
2.可扩充向量
2.1 分析
静态空间管理:即_capacity固定了,想想如果一个东西固定不定,管理起来肯定不方便
数据要是少了(这个少是怎么界定的呢loadfactor 装填因子<<50%),underflow下溢,浪费空间;
数据要是多了,overflow上溢,会导致程序崩溃
那怎么办呢?让固定不变的容量动态变化
动态空间管理:在即将overflow时,适当扩大内部数组的容量
2.2 实现
内部函数 void expand();
template <class T>
void vector<T>::expand(){
if(_size<_capacity) return;//尚未满员
_capacity=max(_capacity,DEFAULT_CAPACITY);//这里之所以要比较,是因为之前构造时_capacity被赋值了
T *oldelem=_elem;//保存原始数据
_elem=new T[_capacity<<1];//容量加倍
for(int i=0;i<_size;i++)
_elem[i]=oldelem[i];
delete [] oldelem;//释放原空间
}
可以看到这里采取了容量加倍的扩容方法(下图的紫色部分)
有没有什么其他的方法呢,还有递增的方法,即每次快要上溢时,再加一个原先的容量(下图的蓝色部分)
两种方法对比
这里出现了新概念分摊
3.无序向量
注意:这里的无序并不表示向量中的元素没有顺序或是向量中的元素可能不能排成顺序
3.1 元素访问
虽然在向量的接口里有v.get( r )和v.put( r,e ),可以实现读和写,但是不够便捷!要追求简洁高效!那么怎么办,之前介绍过向量是数组的抽象和泛化,诶~有点想法了,重载[ ]操作符 !
为什么要返回T&呢?因为这样定义之后,结果既可以作为右值赋值给其他元素,也可以作为左值被赋值
&引用:是对象的别名,如一个人叫张三,小张是他的别名,你打小张,相当于就是打张三
template <class T>
T& vector<T>::operator[](Rank r) const{return _elem[r];}
3.2 插入(从后往前)
可写接口Rank insert(Rank,T const&);
template <class T>
Rank vector<T>::insert(Rank r,T const & e){
expand();//若有要扩容
for(int i=_size;r<i;i--)//自后向前,如果颠倒次序,后果很严重,数据有可能被覆盖
_elem[i]=_elem[i-1];//后继元素顺次后移
_elem[r]=e;//置入新元素
_size++;//更新容量
return r;
}
3.3 区间删除(从前往后)
可写接口 int remove(Rank,Rank);
template <class T>
int vector<T>::remove(Rank lo,Rank hi){
if(lo==hi) return 0;
while(hi<_size)//这里的次序仍然不能颠倒
_elem[lo++]=_elem[hi++];
_size=lo;//更新容量
shrink();//若有必要,缩容
return hi-lo;//返回被删除元素的个数
}
3.4 单元素删除(区间删除的特列)
可写接口 T remove(Rank); 这里重载了remove接口
template <class T>
T vector<T>::remove(Rank r){
T e=_elem[r];//备份被删除的元素
remove(r,r+1);//调用区间删除算法
return e;//返回被删除元素
}
3.5 查找
查找有个前提条件:
若是无序向量:T可判等或 重载了 == 或 !=
若是有序向量:T可比较或 重载了 < 或 >
只读接口 Rank find(T const&,Rank,Rank) const;
template <class T>
Rank vector<T>::find(T const & e,Rank lo,Rank hi) const{
//在命中多个元素时可返回秩最大者
while((lo<hi--)&&(e!=_elem[hi]));//逆向查找
return hi;
}
这里的查找方法的复杂度,和输入的值有很大的关系,这里称之为input-sensitive输入敏感
3.6 deduplicate唯一化(在元素前面查找,若有雷同移除本身)
可写接口 int deduplicate();
template <class T>//删除重复元素,返回被删除元素个数
int vector<T>::deduplicate(){
int oldsize=_size;
Rank i=1;//从_elem[1]开始
while(i<_size)
(find(_elem[i],0,i)<0) ? //在前缀中找雷同者
i++://没找到,考察后继
remove(i);//找到了,移除雷同者
return oldsize-_size;
}
4.有序向量
4.1 uniquify唯一化(在元素后面查找,若雷同移除后者)
首先我们得判别出这个向量是否有序,这里采用的是计数器的方法,记录逆序对的数量
只读接口 int disordered() const;
template <class T>
int vector<T>::disordered() const{
int n=0;
for(int i=1;i<_size;i++)
n+=(_elem[i]<_elem[i-1]);//逆序则计数器加一
return n;
}
判断完有序之后,开始编写唯一化算法
4.1.1 低效算法(单个移除)
怎么实现?就是用第一个元素与它的后面的元素作比较,若后面的和该元素一样那么就删掉后者,删掉时调用了remove接口
template <class T>
int oldSize=_size;int i=0;//从首元素开始
while(i<_size-1)
(_elem[i]==_elem[i+1]) ? remove(_elem[i]) : i++;
return oldSize-_size;
低效的原因?重复操作太多了!怎么办?找出重复区间,一起删除!
4.1.2 高效算法(成批移除)
只读接口 int uniquify();
template <class T>//返回被删除元素总数
int vector<T>::uniquify(){
Rank i=0,j=0;
while(j++<_size)
if(_elem[i]!=_elem[j]) _elem[i++]=_elem[j];//就是这里!直接截取尾部多余元素
_size=++i;
return j-i;
}
4.2 查找
定义一个统一接口
只读接口 Rank search(T const&,Rank,Rank) const;
template <class T>
Rank vector<T>::search(T const & e,Rank lo,Rank hi) const{
return (rand()%2) ?//按50%的概率随机取用
binSearch(_elem,e,lo,hi)//二分查找
: fibSearch(_elem,e,lo,hi);//fibonacci查找
}
这里返回的是Rank,为什么?语义约定:便于维护
4.2.1 二分查找(查找长度折半缩减)
首先思考,怎么找?如果从头到尾遍历,效率肯定不高,怎么办?突破点:有序!
减而治之
A版:分成三种情况
实现:
template <class T>
static Rank binSearch(T *A,T const & e,Rank lo,Rank hi){
while(lo<hi){
Rnak mi=(lo+high)>>1;//折半
if (e<A[mi] hi=mi;
else if(e>A[mi] lo=mi;
else return mi;//找到了
}
return -1;//没找到
}
看起来很不错,但是比较了3次,还能再精简吗?能,比较2次!
B版:分成两种情况
实现:
template <class T>
static Rank binSearch(T *A,T const & e,Rank lo,Rank hi){
while(lo<hi){
Rank mi=(lo+hi)>>1;//折半
(e<A[mi]) ? hi=mi:lo=mi;
}//出口时hi=lo+1
return (e==A[lo])?lo:-1;
}
功能已经完善了,但是语义好像不满足,没找到时返回的是-1啊,而不是不大于e的最后一个元素,怎么办?
C版:功能、语义、实现都满足
实现:
template <class T>
static Rank binSearch(T *A,T const & e,Rank lo,Rank hi){
while(lo<hi){
Rank mi=(lo+hi)>>1;//折半
(e<A[mi]) ? hi=mi:lo=mi+1;
}//循环完毕,A[lo=hi]为大于e的最小元素
return --lo;//lo-1为不大于e的元素的最大秩
}
这个看起来貌似忽略了A[mi]其实并没有,看看最后的返回值
4.2.2 Fibonacci查找(查找长度调用fib()缩减)
与二分查找的区别就是查找长度的切割方法不一样,这里是对A版的改进,其余版本都是一样的方法,就是把mi的取值改成黄金比例切分
实现:
static Rank fibSearch(T *A,T const & e,Rank lo,Rank hi){
Fib fib(hi-lo);
while(lo<hi){
while(hi-lo<fib.get()) fib.prev();
Rank mi=lo+fib.get()-1;//黄金比例切分,重点!
if (e<A[mi]) hi=mi;
else if (A[mi]<e) lo=mi;
else return mi;
}
return -1;//查找失败
}
这里的Fib类要自己编写,能运行,但是不知道是否符合要求啊
#ifndef FIB_H
#define FIB_H
#include<stdexcept>
class Fib{
public:
Fib(int n):curr_n(n){result(n);}
int get();
void prev();
protected:
void result(int n);
private:
int curr_n;
int value;
};
void Fib::result(int n){
int f=0,g=1;
if(n==0) value=0;
else{
while(1<n--){
g=g+f;
f=g-f;
}
value=g;
}
}
int Fib::get(){
return value;
}
void Fib::prev(){
if(this->curr_n==0) this->result(0);
else this->result(this->curr_n-1);//前移
}
#endif
4.2.3 插值查找(查找长度按线性趋势缩减)
优势不明显,当查找范围极大、或者比较操作成本极高,则可先采取插值查找缩小范围,再进行二分查找
5.排序
激动人心的排序来了!!!顾名思义,排序就是 无序—>有序
先定义一个统一接口,在这部分只实现了起泡排序和归并排序,所以其他的先注释掉
可读接口 void sort(Rank lo,Rank hi);
template <class T>
void vector<T>::sort(Rank lo,Rank hi){
switch(rand()%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;//快速排序
}
}
5.1 起泡排序
A版:首先想到的是之前实现过的起泡排序
内部接口
void bubbleSort(Rank,Rank);//起泡排序
Bool bubble(Rank,Rank);
template <class T>
void vector<T>::bubbleSort(Rank lo,Rank hi){
while(!bubble(lo,hi--));//逐趟扫描交换
}
template <class T>
Bool vector<T>::bubble(Rank lo,Rank hi){
bool sorted=true;
while(++lo<hi)//自左向右,逐一检查相邻元素
if(_elem[lo]<_elem[lo-1]) {//若逆序,则
sorted=false;//有序标志置于否定状态
swap(_elem[lo-1],_elem[lo]);//交换
}
return sorted;//最右侧的逆序对位置
}
但是考虑到存在下图的情况:如果后半段已经有序了,如果是上面的算法,显然降低了效率,那么怎么办呢?记录最右侧逆序对的位置!
B版:
内部接口
void bubbleSort(Rank,Rank);//起泡排序
Rank bubble(Rank,Rank);
template <class T>
void vector<T>::bubbleSort(Rank lo,Rank hi){
while(lo<(hi=bubble(lo,hi)));//hi=最右侧的逆序对位置
}
template <class T>
Rank vector<T>::bubble(Rank lo,Rank hi){
bool last=lo;//最右侧的逆序对初始化为[lo-1,lo]
while(++lo<hi)//自左向右,逐一检查相邻元素
if(_elem[lo]<_elem[lo-1]) {//若逆序,则
last=lo;//更新最右侧逆序对位置记录,并
swap(_elem[lo-1],_elem[lo]);//交换
}
return last;//最右侧的逆序对位置
}
5.2 归并排序
内部接口
void mergeSort(Rank,Rank);//归并排序
void merge(Rank,Rank,Rank);
template <class T>
void vector<T>::mergeSort(Rank lo,Rank hi){
if(hi-lo<2) return;//递归基
Rank mi=(lo+hi)>>1;
mergeSort(lo,mi);//对前半部分排序
mergeSort(mi,hi);//对后半部分排序
merge(lo,mi,hi);//归并
}
这里最重要的就是 void merge(Rank,Rank,Rank);的实现
怎么归并呢?看图
根据图示可以写出以下的算法
template <class T>
void vector<T>::merge(Rank lo,Rank mi,Rank hi){
T *A=_elem+lo;//合并后的向量A[0,hi-lo)=_elem[lo,hi) ,A是归并后的向量,让它指向区间的起点A[lo]
Rank lb=mi-lo; T *B=new T[lb];//前子向量B[0,lb)=_elem[lo,mi)
for(Rank i=0;i<lb;B[i]=A[i++]);//复制前子向量B
int lc=hi-mi;
T *C=_elem+mi;//后子向量C[0,lc)=_elem[mi,hi),指向A[mi]
for(Rank i=0,j=0,k=0;j<lb;){//这里的比较其实有个技巧,就是先让k值和lc比较
if(k < lc&&(C[k]< B[j]))A[i++]=C[k++];//判断条件是C还没到最后而且C中元素大于B中元素
if(lc<=k ||(B[j]<=C[k]))A[i++]=B[j++];//判断条件是C已经排序完或者B中元素大于C中元素
}//为什么没有B越界的情况?因为当B越界时,B[j]=正无穷,即末尾的哨兵!C[k]肯定小于正无穷!
delete [] B;//释放空间B
}
下图的C并不是新创建的,只是放在这里做图示,它指向A中的元素