抽象数据类型=数据模型+一组操作
Vector myvector; 注意操作而可以不用注意细节
从数组到向量
C/C+中的数组, 数组A[i]物理地址=A+i*s, 也称线性数组
向量是数组的推广和泛化,由一组元素按线性次序 封装 而成, 元素与秩(类似i)相对应
元素类型不限于基本类型
接口与实现
向量ADT接口
size() get(r) put(r,e) find(e)元素为e所在的秩 insert(0,9)在秩为0的位置插入9,其他元素后移 disordered()返回逆序对个数 search(9)顺序时9所在秩,没有时返回不大于9的最大元素所在秩或者 等等
Vector模板类的实现
typedef int Rank;//秩
#define DEFAULT_CAPACITY 3//默认初始容量
template <typename T> class Vector{ //定义一系列名字叫T的类
private :Rank_size;int _cspacity;T*_elem;//规模n 容量 数据区A
protect publlic:
构造析构
Vector(int c=DEFAULT_CAPACITY)
{_elem=new T[_cspacity=c];_size=0;} //开辟一块大小为c的数据空间
或者调用copyfrom复制数组区间 向量区间或者向量整体
析构~Vector(){delete[] _elem;}//释放空间
}
外部通过接口转化为内部数据得以封装
copyfrom函数实现
template <typename T>//T为基本类型或已重载运算符‘=’
void Vector<int>::copyfrom(T*const A ,Rank lo,Rank hi)
{
_elem=new T[_capacity=2*(hi-lo)];
_size=0;
while(lo<hi) _elem[_size++]=A[lo++]
}
**
可扩充向量
**
静态空间管理
开辟内部数组_elem[]并使用一块连续物理空间
_capacity:总容量 _size:当时的实际规模n
问题:上溢,下溢
有没有方法自适应动态改变容量?
动态空间管理
模仿蝉的脱壳,在即将发生上溢时 申请另一块空间 复制并释放原来的空间
扩容算法实现
template<typename T>
void Vector<T>::expand()
{
if(_size<_capacity) return;//未满不扩容
_capacity=max(_capacity,DEFAULT_CAPACITY);//不低于最小容量
T*oldElem=_elem;_elem=new T[_capacity<<=1];
for(int i=0;i<_size;i++)
_slem[i]=oldelem[i];
delete[]oldElem;//释放原来空间
}
得益于封装通过_elem这个指示器而不至于出现野指针
为什么要倍增而不是递增容量?
最坏情况每次插入i个 插入m次共n个元素
所有扩容过程赋值原向量的时间成本0,i,2i,,,(m-1)i ;总体耗时O(n^2),每次扩容分摊成本O(n)
倍增的最坏情况,连续插入n=2^m>>2
时间成本 1,2,4,8.,,2^m=n;总耗时O(n)分摊O(1) 倍增以空间换时间
平均复杂度:对操作出现的概率做假设,将对应成本加权平均
分摊复杂度:对数据结构连续实施多次操作,总成本分摊至单次
无序向量
Template Vector;
Vector myvector;
元素访问: V.get®和V.put(r,e)
循秩访问:重载操作符[],可沿用借助下标的访问
template//0<=r<_size
T&Vector::operator[] (Rank® const{return _elem[r];}
此后,V[r]=V._elem[r]
右值: T x=V[r]+U[s]W[t];
左值:V[r]=(T)(2x+3);
插入
template <typename>//e作为秩为r的元素插入,0<=r<=size
Rank Vector<T>::insert(Rank r,T const &e)
{
expend;
for(int i=_size>r;i++)
_elem[i]=_elem[i-1];
_elem[r]=e;_size++;//后继元素依次后移
return r;//返回秩
}
区间删除
template <typaname 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;//返回被删除元素个数
}.
单元素删除
template <typename T>
T Vector<T>::remove (Rank r)
{
T e=_elem[r];
remove(r,r+1);
return e;
}
查找
无序向量: T为可判等的基本类型,或已重载== !=
有序向量:可比较(判等和比较大小),或重载< >
template <typename T>
Rank Vector<T>::find(T const &e,Rank lo,Rank hi)const
{
while((lo<hi--)&&e!=_emel[hi]);//逆向查找
return hi;
}
唯一化(相同元素剔除)
template <typename T>
int Vector<T>::deduplicate()
{int oilsize=_size;
Rank i=1;
while(i<_size)
(find(_elem[i],0,i)<0)?i++:remove(i);
return oldsize-_size;
}
不变性:当前元素i的前缀彼此互异,数学归纳,i=1自然成立
单调性:前缀未必增加但后缀严格单调下降。所以必将终止,迭代n轮
单次不超过O[n]总O[n*n]
优化;仿照uniquify高效版思路,移动次数下降到O[n]比较不变,稳定性破坏
先对需要删除的重复元素做标记,再统一删除 稳定性不变,查找更长所以有了更多对比操作
V。sort().uniquify()
遍历
按照事先约定的visit操作遍历各元素
如何指定visit操作,将其传递到向量内部?
//**利用函数指针机制,只读或局部性修改**
template <typename T>
void Vector<T>::traverse (void(*visit)(T&))//[函数指针作形参](https://www.cnblogs.com/huangzhenxiong/p/7772627.html)
{for(int i=0;i<_size;i++)visit(_elem[i]);}
**利用函数对象机制,全局性修改**
template <typename T> template <typename VST>
void Vector<T>::traverse(VST& visit)//[函数对象](http://c.biancheng.net/view/354.html)
{for(int i=0;i<_size;i++) visit(_elem[i]);}
//以一个类的形式给出一个函数对象使向量内所有元素加一
template <typename T>//假设T可直接递增或已经重载操作符++
struct Increase//函数对象通过操作运算符()实现
{virtual void operator ()(T&e){e++;}};//加一
**此后**
template <typename T> void increase(Vector <T>&V)
{V.traverse(Increase<T>());}//通过调用接口将上面的函数对象以参数形式传入
有序向量
无序:比对 有序:比较
唯一化
有序性及其甄别
有序向量任意一对相邻元素是顺序,无序向量总有一对逆序。所以可以用相邻逆序对数目度量逆序程度。
template <typename T>//返回相邻元素逆序对的总数
int Vector<T>::disordered() const
{
int m=0;//计数器
for(int i=1;i<_size;i++)
n+=(_elem[i-1]>_elem[i]);//逆序则计数
return· n;//0则有序
}//若只判断有序,则碰见第一个逆序对就可以返回
可比较,所以无序可转化成有序
有序的重复元素相邻,所以前后比较 相同可以删除后者,不同则将注意力放到后者 再把后者与其后比较
template <typename T> int Vector<T>::uniquify()
{
int oldSize=_size;int i=0;//从首元素开始
while(i<_size-1)
(_elem[i]==_elem[i+1])?remove(i+1):i++;
return oldSize-_size;//返回删除总数
}
复杂度分析:
运行次数由while决定,次数共计_size-1
最坏情况:每次都需要调用remove(),算术级数
同一个值的元素多次重复remove!!那一步到位的将同一个值的元素统一删除前移呢?!!
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;//返回规模变化量,循环跳出时j=_size i=实际规模
}
比较n-1次,在秩为1 5 9。。n时复制,复杂度也不过为线性
二分查找
无序Vector::find(e,lo,hi)
有序Vector::search(e,lo,hi)
template<typename T>//查找算法的统一接口
Rank Vector <T>::search(T const &e,Rank lo,Rank hi)const
{
return (rand()%2)?
binSearch(_size,e,lo,hi)//二分查找
:fibSearch(_size,e,lo,hi)//Fibonacci查找
}
如何处理特殊情况?目标元素不存在或者反过来目标元素存在多个
便于自身维护:V.insert(1+V.search(e),e) ,即便查找失败也应给出适当的插入位置
给出约定:在有序向量V[lo,hi]中,确定不大于e的最后一个元素,这样不论不存在还是重复都可正确插入
更为特殊的当e比V[lo]还小,返回lo-1;比V[hi]还大返回hi-1,插在hi的位置
原理:分而治之,以S[mi]为分界一分为二,递归查找
template <typename T>
static Rank binSearch(T*A,T const &e,Rank lo,Rank hi)
{ while(lo<hi)
{
Rank mi=(lo+hi)>>1;
if(e<A[mi]) hi=mi;
elae if(A[mi]<e) lo=mi+1;
elae return mi;
}
return -1;//查找失败
}
迭代实例快速得到比较次数
线性递归:减半+O(1)=O(logn) ,logn次 优于顺序查找
递归跟踪:当轴点总取中电,递归深度O(logn),各递归实例均耗时O(1)
查找长度 如何更为精细评估算法性能?也就是O(n)的系数
考察关键码的比较次数(if语句次数),即查找长度
通常针对查找成功或者失败,从最好 最坏 平均(O(1.5logn))等角度评估
递归实例中标注成功失败可能情况和所需比较次数,快速求解复杂度
Fibonacci查找
以上版本向左或右分支成本不一却递归深度相同,那能否使深度向操作数少的分支偏移呢?
设n=fib(k)-1,取mi=fib(k-1)-1,则两段长度分别为fib(k-1)-1和fib(k-2)-1
template <typename T>
static Rank fibSearch(T*A,T const &e,Rank lo,Rank hi)
{
Fib fib (hi-lo);//创建Fib数列
while(lo<hi)
{
while(hi-lo<fib.get()) fib.prev();//至多迭代? 通过向前顺序查找确定形如Fib(k)-1的轴点(分摊O(1))
Rank mi=lo+fib.get()+1;//按黄金比例切分,每次取斐波那契数的下一项-1
if(e<A[mi]) hi=mi;
elae if(A[mi]<e) lo=mi+1;
elae return mi;
}
return -1;//查找失败
}
Fibonacci查找的平均查找长度,在常系数意义上优于二分查找
最优
递推式:a(x)logn=x[1+a(x)log(xn)]+(1-x)*[2+a(x)*log((1-x)n)] *******a(x)为系数,x和(1-x)所占概率,1 2是进入该分支所需操作数,求得x为黄金分割数,a(x)为1.44
二分查找(改进)
如果无论向左向右均1次?e<轴点:必属于左侧 ;x<=e:属于右侧
template <typename T> static RankbiSearch(T*A,T const &e,Rank lo,Rank hi)
{
while(1<hi-lo)//有效查找宽度缩短为1时算法终止
{
Rank mi=(lo+hi)>>1;//以终点为轴点比较后深入
(e<A[mi])?hi=mi:lo=mi;//[lo,mi)或[mi,hi)
}//循环结束hi=lo+1,只有1个元素
return(e==A[lo])?lo:-1;//返回目标秩或者-1
}//相较改进前版本最好情况变坏(最好可直接命中,不需等至有效范围为1)最坏情况变好,整体更趋稳定
search()接口语义约定:返回不大于e的最后一个元素
只有遵循这一约定,才可支持其他算法 比如:V.insert(1+V.search(e),e)
1)多个相同元素,返回秩最大 2)失败时返回小于e的最大者,即引入的哨兵[lo-1]
template <typename T> static RankbiSearch(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;//[lo,mi)或(mi,hi)
}循环结束,A[lo=hi]为大于e的最小元素
return --lo;//lo-1,不大于e的最后一个元素
}
正确定证明:
不变性:A[0,lo)<=e<A[hi,n),初始成立;第一次产生分支取前半段 hi=mi后e<A[hi,n);取后半段lo=mi+1后A[0,lo)<=e即A[mi]<=e
单调性显而易见,有效查找区间会不断减小至0,不如说0时分界线,左侧<=e,右侧>e
差值查找
之前无论是Fibonacci查找还是二分查找都是选取固定轴点,那可不可以根据元素分布规律动态选取轴点呢?
最坏情况:O(hi-lo)=O(n)
平均情况:每经过一次查找,n缩至根号n n,n1/2,。。。n(1/2)^k,,推出O(loglogn)
字宽折半估算复杂度:n二进制位宽度logn ,再分(1/2)^klogn
综合,首先通过差值查找缩小范围,中规模数据采用折半查找,小规模可顺序查找