从数组到向量
- 向量(vector) 就是线性数组的一种抽象与泛化,它也是由具有线性次序的一组
元素构成的集合V = { v0, v1, …, vn-1 },其中的元素分别由秩相互区分。 - 各元素的秩(rank) 互异,且均为[0, n)内的整数。具体地,若元素e的前驱元素共计r个,则其秩就是r。
接口
ADT接口
动态空间管理
- 向量实际规模与其内部数组容量的比值(即_size/_capacity) ,亦称作装填因子(load factor) , 它是衡量空间利用率的重要指标。
问题
- 如何才能保证向量的装填因子既不致于超过1,也不致于太接近于0?
- 如何实现扩容?新的容量取作多少才算适宜?
解决方法(可扩充向量)
首先解决第一个问题。直接在原有的物理内存空间的基础上追加空间,不现实。数组特定的定址方式,物理空间必须连续,我们无法保证,其尾部总是留有足够的空间供其扩展。
一种可行的方法是如上图所示,申请一个容量足够大的数组,并将原来数组中的数据进行复制,此后便可以插入新元素e而不至于溢出。当然,原数组所占的空间要进行释放交还操作系统。
扩容算法
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 = 0; i < _size; i++ )
_elem[i] = oldElem[i]; //复制原向量内容(T为基本类型,或已重载赋值操作符'=')
/*DSA*/ //printf("\n_ELEM [%x]*%d/%d expanded and shift to [%x]*%d/%d\n", oldElem, _size, _capacity/2, _elem, _size, _capacity);
delete [] oldElem; //释放原空间
}
该程序的关键是,新数组的容量总是取原数组容量的两倍,也正是第二个问题的答案。
缩容算法
template <typename T> void Vector<T>::shrink() { //装填因子过小时压缩向量所占空间
if ( _capacity < DEFAULT_CAPACITY << 1 ) return; //不致收缩到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] = oldElem[i]; //复制原向量内容
delete [] oldElem; //释放原空间
}
注:函数体中上面两行没有看明白,若有小伙伴看到了,麻烦告知一下!!!感谢。
常规向量
直接引用元素
使用访问数组的方式访问向量,可以通过重载操作符[]进行重载。
template <typename T> T & Vector<T>::operator[] ( Rank r ) //重载下标操作符
{ return _elem[r]; } // assert: 0 <= r < _size
置乱算法
置乱算法
template <typename T> void permute(Vector<T>& V) { //随机置乱向量,使各元素等概率出现亍殏一位置
for (int i = V.size(); i > 0; i--) //自后向前
swap(V[i - 1], V[rand() % i]); //V[i - 1]不V[0, i)中某一随机元素交换
}
区间治乱接口
无序查找
- 无序向量:某种数据类型,仅支持比对,不支持比较的向量,称为无序向量。
顺序查找的算法
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,则意味着失败;否则hi即命中元素的秩
}
注:while循环的条件自减什么时候进行,这里有点疑问。
while循环的控制逻辑由两部分组成,首先判断是否已抵达通配符,再判断当前元素与目标元素是否相等。得益于C/C++语言中逻辑表达式的短路求值特性,在前一判断非真后循环会立即终止,而不致因试图引用已越界的秩(-1) 而出错。
插入
插入算法
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] = e; _size++; //置入新元素并更新容量
return 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 = lo; //更新规模,直接丢弃尾部[lo, _size = hi)区间
shrink(); //若有必要,则缩容
return hi - lo; //返回被删除元素的数目
}
单元素删除
template <typename T> T Vector<T>::remove ( Rank r ) { //删除向量中秩为r的元素,0 <= r < size
T e = _elem[r]; //备份被删除元素
remove ( r, r + 1 ); //调用区间删除算法,等效于对区间[r, r + 1)的删除
return e; //返回被删除元素
}
唯一化
程序
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 ); //若无雷同则继续考查其后继,否则删除雷同者
return oldSize - _size; //向量规模变化量,即被删除元素总数
}