欢迎光临我的Blog,虽然这里还很简陋,但未来一定会焕发生机的!

855学习记录数据结构向量(2)空间管理——炎泽汐$de$ Blog

数据结构 yanzexi 1年前 (2023-10-04) 196次浏览 已收录 0个评论 扫描二维码
文章目录[隐藏]

向量的空间管理

静态空间管理的弊端


      内部数组所占物理空间的容量,若在向量的生命期内不允许调整,则称作静态空间管理策略。该策略的空间效率难以保证。一方面,容量固定可能在此后的某一时刻,无法加入更多的新元素,即上溢$(overflow)$。另一方面如果为降低风险而预留部分空间,也很难明确界定一个合理的预留量。

      向量实际规模与其内部数组容量的比值$($_$size/$_$capacity)$,亦称作装填因子$(loadfactor)$,它是衡量空间利用率的重要指标。从这一角度,静态空间管理的困境可归纳为:如何才能保证向量的装填因子既不致于超过 1,也不致于太接近于 0?

扩容算法


      针对静态空间管理的困境可以改用动态空间管理策略,比较简单的方法如下代码所示:

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 为基本类型,或已重载赋值操作符'=')
    delete [] oldElem; //释放原空间
}

      此策略在每次调用$insert()$接口插入新元素之前,都要先进行调用检查内部数组的可用容量。一旦当前数据区已满(_$size == $_$capacity$),则将原数组替换为一个更大的数组。

  • 均摊分析

  •       与常规数组实现相比,可扩充向量更加灵活:只要系统尚有可用空间,其规模将不再受限于初始容量。每一次由$n$到$2n$的扩容,都需要花费$O(2n) = O(n)$插入操作所需时间。表面看来,这一扩容策略似乎效率很低,但实际上随着向量规模的不断扩大,需要进行扩容的概率也将迅速降低。

          假定向量的初始容量为某一常数$N$且初始规模也为$N$,此时不断向向量中插入新元素,向量容量以$2$为比例按指数速度增长。假设通过$n$次插入后向量容量达到$capacity(n)$,此前共做过$\varTheta (log_2n)$次扩容,每次扩容所需时间线性正比于当时的容量(或规模),且同样以$2$为比例按指数速度增长。因此,消耗于扩容的时间累计不过:

    $$
    T\left( n \right) =2N+4N+\cdot \cdot \cdot capacity\left( n \right) <2\cdot capacity\left( n \right) ~\varTheta \left( n \right) $$       将其分摊到其间的连续$n$次操作,单次操作所需的分摊运行时间应为$O(1)$。

    缩容算法


          导致低效率的另一情况是,向量的实际规模可能远远小于内部数组的容量。装填因子极有可能远远小于 100%,甚至非常接近于 0。当装填因子低于某一阈值时,称向量发生了下溢($underflow$)。尽管其并非必须解决的问题,但在某些场景,发生下溢时也有必要适当缩减内部数组容量。以下是一个缩容算法的代码:

    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; //释放原空间
    }
    

          与$expand()$操作类似,尽管单次$shrink()$操作需要线性量级的时间,但其分摊复杂度亦为$O(1)$。实际上$shrink()$过程等效于$expand()$的逆过程,这两个算法相互配合,在不致实质地增加接口操作复杂度的前提下,保证了向量内部空间的高效利用。

    喜欢 (1)
    [炎泽汐de收款码]
    分享 (0)

    您必须 登录 才能发表评论!