标签


向量(vector)

2014年08月28日

这篇文章记录了我学习列表找到的一些有用的资料以及edX上Data Structures and Algorithms第二周的笔记.

接口与实现

1.ADT/implementation
2.Abstract Data Type vs Data Structure
(1)ADT = 数据模型 + 定义在该模型上额度一组操作
(抽象定义,外部逻辑特性,不考虑复杂度,不涉及数据储存方式)
(2)数据结构 = 基于某种特定语言,实现ADT的一整套算法
(具体实现,完整算法,考虑复杂度,考虑数据储存机制)

3.从数组到向量
(1)Array的每一个元素都可直接访问
(2)A[i]=A+i*s (s为单个元素占用的空间量)
(3)故称作线性数组(linear array)

4.向量是数组的抽象与泛化,由一组元素按现行次序封装而成
(1)各元素与[0,n)内的秩(rank)一一对应 (call-by-rank)
(2)元素的类型不限于基本类型

5.ADT操作实例
(1)disordered():输出逆序对(inversion)的个数
(2)search():返回不超过输入元素(parameter)的最大的那个元素的最后的rank
(3)uniquify():删除有序的向量重复的元素

6.Vector 模板类

/*Vector*/

typedef int Rank;           //秩
#define DEFAULT_CAPACITY 3  //默认初始向量(实际应用中可设置为更大,大概32?)

template<typename T> 
class Vector{
  private:
    Rank _size;         //规模
    int  _capacity;     //容量
    T *  _elem;         //数据区

  protected:
  /*...内部函数*/

  public:
  /*构造函数*/
  /*析构函数*/
  /*只读接口*/
  /*可写接口*/
  /*遍历接口*/
}

//构造函数
Vector<T>::Vector(int c = DEFAULT_CAPACITY){            //默认构造函数
    _elem = new T[_capacity = c];//首地址交给_elem
    _size = 0;
}

Vector::Vector(T const * A,Rank lo,Rank hi){            //数组区间复制
    copyFrom(A,lo,hi);
}           

Vector<T>::Vector(Vector<T> const & V,Rank lo,Rank hi)  //向量区间复制
{
    copyFrom(V._elem,lo,hi);
}

Vector<T>::Vector(Vector<T> const & V){                 //向量整体复制
    copyFrom(V._elem)
}

Vector<T>::~Vector(){                                   //析构
    delete [] _elem;
}

//*关键:copyFrom
template<typename T>
void Vector<T>::copyFrom(T* const A,Rank lo,Rank hi){
    _elem = new T[_capacity = 2*(hi-lo)];   //2倍空间, 
                                            //可以hi-lo是因为[lo,hi)是[闭,开)空间
    _size = 0;                              //规模清0
    while(lo < hi){                         //A[lo,hi) traverse
        _elem[_size++] = A[lo++];           //复制至_elem[0,hi-lo)
    }
}

可扩充向量

1.目前静态空间管理,容量capacity固定,有缺陷
(1)上溢(overflow): elem[]空间不够,尽管系统仍有足够空间
(2)下溢(underflow):
elem[]元素很少,装填因子(load factor) lambda = _size/
capacity << 50%
*一般应用环境难以准确预测空间的需求量

2.动态空间管理
(1)在即将发生overflow时,适当地扩大内部数组的容量

/*扩容算法*/
template <typename T>
void Vector<T>::expand(){           //向量空间不足时扩容
    if(_size < _capacity) return;

    //_size == _capacity的时候就扩容 
    _capacity = max(_capacity,DEFAULT_CAPACITY); //不低于最小容量
    T * oldElem = _elem;
    _elem = new T[_capacity <<= 1];              //左移,容量加倍, 应该写成_capacity<<1吧?

    for(int i=0;i<_size;i++){
        _elem[i] = oldElem[i];                   //T为基本类型,或已重载'='
    }
    delete [] oldElem;                           //释放原空间
}

//优势:封装性
//扩容之后数据区物理地址有所改变,却不会出现野指针
//一般数组动态重新分配地址后,原先指向它内部某些元素的一些指针可能无效,
//虽然它指向一个地址,但其中并没有存放我们所需要的数值
//e.g. *(array +3)指向第三个元素,重新分配后失效,
//     *(vector+3)依然能指向第三个元素,都是用_elem统一指示

(2)一个关注点

//是否可以将向量扩容代码中的:
for (int i = 0; i < _size; i++) _elem[i] = oldElem[i];
//替代为:
memcpy(_elem, oldElem, _size * sizeof(T));

/*答案*/
//当T为非基本类型且有对应的复制构造函数以执行深复制时,前一段代码会调用复制构造函数而后一段只能进行浅复制。

3.递增式扩容 (incrementing)
(1)追加固定数额

T * oldElem = _elem;
_elem = new T[_capacity += INCREMENT];

//每经过INCREMENT次插入,都得扩容
//总体耗时:0,I,2I,...,(m-1)I=( (m-1)I+0 )*m/2 = O(n^2)  (总和是末项的平方)
//每次分摊成本O(n)

4.容量加倍策略 (doubling)

T * ldElem = _elem;
_elem = new T[_capacity<<1];

//每经过n = 2^m 次插入,就扩容
//总体耗时:1,2,4,...,2^m = O(n)  (总和和末项是等阶的)
//每次分摊成本:O(1)

5.分析这两种策略

            递增策略    倍增策略
累计时间:    O(n^2)      O(n)
分摊时间:    O(n)        O(1)
load factor: ~100%       >50%

//倍增策略load factor=50%的时候是即将发生overflow刚加倍扩容的瞬间
//doubling是通过空间的效率做了一个适当的牺牲,换取时间方面巨大的收益

6.平均分析 vs 分摊分析
1.平均复杂度/期望复杂度(average/expected complexity)
(1)根据各种操作出现概率的分布,将对应的成本加权平均
(2)割裂了操作之间的相关性和连贯性
(3)不能准确评判数据结构和算法的真实性能

2.分摊复杂度(amortized complexity)
(1)对数据结构连续地实施足够多次操作,所需总体成本分摊至单次操作
(2)从实际角度做整体考量
(3)忠实刻画可能出现的操作序列
(4)更为精准地评判真实性能

无序向量

1.向量模板

template <typename T> Vector{}

Vector<int/float/double> myVector;
Vector<BinTree> forest;

2.元素访问

/*overload operator[]*/

template <typename T>   //0 <= r < _size
T & Vector<T>::operator[](Rank r) const 
{
    return _elem[r];
}

/*call by rank*/
//此后对外的V[r]即对应于内部的V._elem[r]
//右值: T x = V[r]+U[s]*W[t];
//左值: V[r]=T(2*x+3);

3.插入

template <typename T>
Rank Vector<T>::insert(Rank r,T const & e){     //O(n-r)
    expand();//if necessary

    for(int i=_size;i>r;i--){                   //自后向前
        _elem[i] = _elem[i-1];
        _elem[r] = e;
        _size++;
    }

    return r;
}

4.区间删除

/*删除区间[lo,hi)*/

template <typename T>   
int Vector<T>::remove(Rank lo,Rank hi){ //O(n-hi),把区间后面的元素left shift
    if(lo==hi) return 0;

    while(hi<_size) 
        _elem[lo++] = elem[hi++];

    _size = lo;                         //这里因为lo++了之后变成区间最大的位置指示了
    shrink();                           //更新规模,有必要则shrink
    retrun hi-lo;                       //返回被删除元素的数目

}

5.单元素删除
(1)区间删除操作的特例 [r] = [r,r+1)

template <typename T>
T Vector<T>::remove(Rank r){
    T e = _elem[r];
    remove(r,r+1);
    retrun e;           //返回被删除元素
}

6.查找

//无序向量: T为可判等的基本类型,或已重载操作符'=='或'!='
//有序向量: T为可比较的基本类型,或已重载操作符'<'或'>'

template <typename T>
Rank Vector<T>::find(T const & e,Rank lo,Rank hi) const {   //找到多个元素时返回rank最大者
    while( (lo<hi--)&&(e!=_elem[hi]) );
    return hi;      //hi<lo意味着失败
}

//输入敏感的算法(input sensitive):
//最好O(1),最坏O(n)

7.唯一化(deduplicate)
(1)应用实例:网络搜索的局部结果经过去重操作(not redundant)

/*删除重复元素,返回被删除元素数目,繁琐版+错误版*/

template <typename T>   
int Vector<T>::deduplicate(){
    int oldSize = _size;
    Rank i = 1;
    while(i<_size){
        (find(_elem[i],0,i)<0)?i++:remove(i);
    }

    return oldSize - _size;
}

//分析:
//find遍历前面,remove遍历后面
//所以整个复杂度是O(n^2)

//进一步优化:
//1.标记需删除的重复元素,统一删除:比对操作增加
//2.V.sort().uniquify(),O(nlogn)

8.遍历
(1)遍历向量,统一对各元素visit
-如何指定visit并且传到向量内部?

/*
visit ver1
函数指针,只读或局部性修改
*/

template<typename T>
visit Vector<T>::traverse( void (*visit)(T &) ){    //function pointer
    for(int i=0;i<_size;i++){
        visit(_elem[i]);
    }
}

/*
visit ver2
函数对象,全局性修改
通用性更强
*/

template <typename T> template<typename VST>
void Vector<T>::traverse(VST & visit)               //函数对象function object,overload bracket()
{
    for(int i=0;i<_size;i++){
        visit(_elem[i]);
    }
}
/*实例*/
//对所有元素分别+1

//首先实现一个可使单个T类型元素+1的类
//假设T可直接++或已重载"++"
template <typename T>
struct Increase{
    virtual void operator(T & e){
        e++;
    }
}

//此后
template <typename T>
void increase(Vector<T> & V){
    V.trverse(Increase<T>());
}

有序向量:唯一化 (ordered vector:uniquify?)

1.有序性及其甄别
(1)有序序列,任意一对相邻元素顺序
(2)无序序列,总有一对元素逆序(inversion)
(3)相邻逆序对的总数,可以度量向量的逆序程度

template <typename T>
int Vector<T>::disordered() const {
    int n = 0;  //counter

    for(int i=1;i<_size;i++){
        n += (_elem[i-1]>_elem[i]);     //有inversion就计数
    }

    return n;       //向量有序iff n==0
}//若只需判断是否有序,首次遇到inversion后可终止

2.唯一化

/*低效版*/

template<typename T> 
int Vector<T>::uniquify(){      //uniquify只针对有序的
    int oldSize = _size;
    int i = 0;

    while(i<(_size-1)){
        (_elem[i]==_elem[i+1])?remove(i+1):i++;
    }

    return oldSize - _size;
}//O(n^2)
/*高效版*/

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;
}//O(n)

二分查找

1.语义约定
(1)便于有序向量自身的维护:V.insert(1+V.search(e),e) (确保插入后还是有序向量,即使失败,也需要给新元素插入的适当位置)
(2)前人的方法,在V[lo,hi)中,确定 不大于(<= less than or equal)e的最后一个 元素的rank
-若e< V[lo],返回lo-1
-若e>V[hi-1],返回hi-1

/*binary search ver1*/

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;       //建议都用小于号,因为和左右次序吻合
        else if(A[mi]>e) lo = mi+1;
        else             return mi;
    }

    return -1;  //查找失败,可以优化
}O(logn)

更为精细地评估查找算法的性能:
(1)考察渐近复杂度logn前面的那个常系数-考察关键码的比较次数,即查找长度(search length) (其实就是在其中执行的if语句的次数)
(2)版本1,平均查找长度大约是O(1.5logn)
(3)每次大概都要if判断1.5次,所以还能改进

Fibonacci查找

1.二分查找ver1的效率仍有改进余地,因为转向左,右分支前的关键码比较次数不等,而递归深度却相同
(向左侧比较1次,右侧却要2次;所以可以把往左边的查找次数多增加一些)

2.若能通过递归深度的不均衡,对转向成本的不均衡进行补偿,平均查找长度能进一步缩短

3.比如,若设n=fib(k)-1,则可取mi=fib(k-1)-1,
于是前后子向量的长度分别是:fib(k-1)-1,fib(k-2)-1

/*Fibonacci search*/

template <typename T>
static Rank fibSearch(T * A,T const & e,Rank lo,Rank hi){
    Fib fib(hi-lo); //用O(logn)=O(log(hi-lo))时间创建Fib数列
    while(lo<hi){
        while(hi-lo<fib.get()) fib.prev();
        //通过向前顺序查找,确定形如 Fib(k)-1的轴点
        Rank mi = lo + fib.get() - 1;
        if          (e<A[mi]) hi = mi;
        else if     (A[mi]<e) lo = mi+1;
        else                  return mi;
    }

    return -1;
}

4.查找长度
(1)Fibonacci查找的ASL(average search length),在常系数的意义上由于binary search

5.0.618
(1)通用策略:对于任何的A[0,n),总是选取A[lambda*n]作为轴点(pivot)
(2)binary search : lambda = 0.5
(3)Fibonacci search: lambda = 0.618
(4)最优lambda:
设平均查找长度为alpha(lambda)*logn,使得alpha(lambda)最小
lambda=0.618

二分查找(改进) Binary search

1.二分查找中左,右分支转向代价不平衡,可直接解决
2.比如每次迭代(递归),仅作1此关键码比较,所有分支只有2个方向,而不再是3个
3.轴点(pivot)mi取作重点,
(1)e < x, 深入S[lo,mi)
(2)x <=e, 深入S[mi,hi)
(3)当元素数目hi-lo=1时,才判断该元素是否命中

4.实现

/*binary search ver2*/

template<typename T> static Rank binSearch(T * A,T const & e,Rank lo,Rank hi){

    while(1<hi-lo){
        Rank mi = (lo+hi)>>1;
        (e<A[mi])? hi=mi:lo=mi;     //[lo,mi) or [mi,hi)
    }//出口时hi=lo+1,查找区间仅含一个元素A[lo]

    return(e==A[lo]) ? lo:-1;
}
//相对于ver1,最好情况下更坏,最坏情况下更好:各种情况下Search Length更加接近,整体性能更稳定

5.语义定义
(1)search语义约定:返回不大于e的最后一个元素 (收敛之后S[lo-1])
(2)支持相关算法: V.insert(1+V.search(e),e)

/*Binary search ver3 近乎完美*/

template <typename T> static Rank BinSearch(T * A,T const & e,Rank lo,Rank hi){
    while(lo<hi){                   //不变性A[0,lo)<=e<A[hi,n)
        Rank mi = (lo+hi)>>1;
        (e<A[mi])? hi=mi:lo=mi+1;   //[lo,mi) or [mi+1,hi)
    }

    return --lo;    //lo-1即为不大于e的元素的最大rank
}

//与ver2的差异
//(1)待查找区间宽度缩短至0而非1时,算法才结束
//(2)转入右侧子向量时,左边界取作 mi+1 而非mi,A[mi]会被遗漏? (不会的,因为在右区间搜完了返回--lo刚好就停在A[mi])
//(3)无论成功与否,返回的rank严格符合接口的语义约定

补充材料:
C++中的static:
静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
http://www.360doc.com/content/09/0919/15/61809_6173677.shtml