線性結構——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

綜合,首先通過差值查找縮小範圍,中規模數據採用折半查找,小規模可順序查找

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