线性结构——Vector

抽象数据类型=数据模型+一组操作
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)(2
x+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

综合,首先通过差值查找缩小范围,中规模数据采用折半查找,小规模可顺序查找

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章