【数据结构与算法学习笔记】PART2 向量(接口与实现,可扩充向量,无序向量,有序向量)

本文深入探讨了向量数据结构的设计与实现,包括基本概念、接口定义、动态空间管理策略及其实现细节。此外,还详细介绍了无序与有序向量的特点、操作方法,特别是高效的唯一化算法和查找算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vector  向量   最基本的线性结构—线性序列


  • (a)接口与实现

根据统一的接口规范定制一个统一的数据结构,如何通过更加有序的算法使得对外的接口能够更加高效的工作


抽象数据类型(ADT,Abstract data type):数据模型+定义在改模型上的一组操作

抽象定义外部的逻辑特性操作&予以

数据结构:基于某种特定语言,实现ADT的一整套算法


ADT

Application ——> Interface——>Implementation


从数组到向量:

C/C++语言中,数组A[]中的元素与[0,n)内的编号一一对应

向量结构必须提供的接口:




http://dsa.cs.tsinghua.edu.cn/~deng/ds/src_link/vector/vector.h.htmVector模板类:

typedef int Rank; //秩
 #define DEFAULT_CAPACITY  3 //默认的初始容量(实际应用中可设置为更大)
 
 template <typename T> class Vector { //向量模板类
 protected:
    Rank _size; int _capacity;  T* _elem; //规模、容量、数据区
    void copyFrom ( T const* A, Rank lo, Rank hi ); //复制数组区间A[lo, hi)
    void expand(); //空间不足时扩容
    void shrink(); //装填因子过小时压缩
    bool bubble ( Rank lo, Rank hi ); //扫描交换
    void bubbleSort ( Rank lo, Rank hi ); //起泡排序算法
    Rank max ( Rank lo, Rank hi ); //选取最大元素
    void selectionSort ( Rank lo, Rank hi ); //选择排序算法
    void merge ( Rank lo, Rank mi, Rank hi ); //归并算法
    void mergeSort ( Rank lo, Rank hi ); //归并排序算法
    Rank partition ( Rank lo, Rank hi ); //轴点构造算法
    void quickSort ( Rank lo, Rank hi ); //快速排序算法
    void heapSort ( Rank lo, Rank hi ); //堆排序(稍后结合完全堆讲解)
 public:
 // 构造函数
    Vector ( int c = DEFAULT_CAPACITY, int s = 0, T v = 0 ) //容量为c、规模为s、所有元素初始为v
    { _elem = new T[_capacity = c]; for ( _size = 0; _size < s; _elem[_size++] = v ); } //s<=c
    Vector ( T const* A, Rank n ) { copyFrom ( A, 0, n ); } //数组整体复制
    Vector ( T const* A, Rank lo, Rank hi ) { copyFrom ( A, lo, hi ); } //区间
    Vector ( Vector<T> const& V ) { copyFrom ( V._elem, 0, V._size ); } //向量整体复制
    Vector ( Vector<T> const& V, Rank lo, Rank hi ) { copyFrom ( V._elem, lo, hi ); } //区间
 // 析构函数
    ~Vector() { delete [] _elem; } //释放内部空间
 // 只读访问接口
    Rank size() const { return _size; } //规模
    bool empty() const { return !_size; } //判空
    int disordered() const; //判断向量是否已排序
    Rank find ( T const& e ) const { return find ( e, 0, _size ); } //无序向量整体查找
    Rank find ( T const& e, Rank lo, Rank hi ) const; //无序向量区间查找
    Rank search ( T const& e ) const //有序向量整体查找
    { return ( 0 >= _size ) ? -1 : search ( e, 0, _size ); }
    Rank search ( T const& e, Rank lo, Rank hi ) const; //有序向量区间查找
 // 可写访问接口
    T& operator[] ( Rank r ) const; //重载下标操作符,可以类似于数组形式引用各元素
    Vector<T> & operator= ( Vector<T> const& ); //重载赋值操作符,以便直接克隆向量
    T remove ( Rank r ); //删除秩为r的元素
    int remove ( Rank lo, Rank hi ); //删除秩在区间[lo, hi)之内的元素
    Rank insert ( Rank r, T const& e ); //插入元素
    Rank insert ( T const& e ) { return insert ( _size, e ); } //默认作为末元素插入
    void sort ( Rank lo, Rank hi ); //对[lo, hi)排序
    void sort() { sort ( 0, _size ); } //整体排序
    void unsort ( Rank lo, Rank hi ); //对[lo, hi)置乱
    void unsort() { unsort ( 0, _size ); } //整体置乱
    int deduplicate(); //无序去重
    int uniquify(); //有序去重
 // 遍历
    void traverse ( void (* ) ( T& ) ); //遍历(使用函数指针,只读或局部性修改)
    template <typename VST> void traverse ( VST& ); //遍历(使用函数对象,可全局性修改)
 }; //Vector


  • (b)可扩充向量
开辟内部数组_elem[]并使用一段地址连续的物理空间,_capacity:总容量   _size:当前实际规模n

不足:有可能出现上溢,有可能出现下溢(装填因子<<50%,λ= _size/_capacity)


动态空间管理:起点

在即将发生上溢时,适当地扩大内部数组的容量







为何必须采用“容量加倍”策略?其它策略不行吗(容量递增)?   不大行!

容量递增:算术级数

容量加倍:几何级数

平均分析VS分摊分析

平均复杂度或期望复杂度:

        根据数据结构各种操作出现概率的分布,兼顾敌营的成本加权平均,各种可能的操作,作为独立事件分别考察,割裂了操作之间的相关性和连贯性,往往不能准确地评判数据结构和算法的真实性能。

分摊复杂度:

        对数据结构连续的实施足够多次操作,所需总体成本分摊至单次操作,从实际可行的角度,对一系列操作做整体的考量,更加忠实的刻画了可能出现的操作序列,可以更为精确地评判数据结构和算法的真实性能

  • (c)无序向量:向量的最基本形式  ——如何定义和实现相应的操作接口

Template <typename T> Vector { /**具体实现**/} ; //使用的时候可以灵活指定类型

Vector <   int     > myVector;

Vector <   float     > myVector;

Vector <   char     > myVector;

Vector <   BinTree    >   forest;



向量元素的访问    通过get 和put接口 ,已然可以读写向量元素,但就便捷性而言,远不如数组元素的访问方式

可以采用数组的访问方式,为此,需要重载下标操作符[]

template <typename T> //0<r<=size;

 T & Vector <T>::operator[](Rank r) const {return _elem[r];}

此后,对外的v[r]即对应于内部的v._elem[r]

可以作为右值,也可以作为左值(得益于对其的访问方式是引用“&”)    循秩访问方式


向量的插入算法



区间删除



单元素删除



向量的查找算法



向量的唯一化算法



向量的遍历操作

遍历向量,统一对个元素分别实施visit操作



  • (d1)有序向量:唯一化
有序:比较
无序:比对

有序性及其甄别:  序列中,任意已对相邻的元素必然是顺序的,有序
相邻逆序对的数目,可以用来度量向量的逆序程度
template <typename T> int Vector<T>::disordered() const { //返回向量中逆序相邻元素对的总数
    int n = 0; //计数器
    for ( int i = 1; i < _size; i++ ) //逐一检查_size - 1对相邻元素
       if ( _elem[i - 1] > _elem[i] ) n++; //逆序则计数
    return n; //向量有序当且仅当n = 0
 }

低效算法:
观察:在有序向两种,重复的元素必然相互紧邻构成一个区间,因此,每一区间只需要保留单个元素即可
template <typename T> int Vector<T>::uniquify() { //有序向量重复元素剔除算法(低效版)
    int oldSize = _size; int i = 1; //当前比对元素的秩,起始于首元素
    while ( i < _size ) //从前向后,逐一比对各对相邻元素
       _elem[i - 1] == _elem[i] ? remove ( i ) : i++; //若雷同,则删除后者;否则,转至后一元素
    return oldSize - _size; //向量规模变化量,即被删除元素总数
 }
复杂度:运行时间主要取决于while循环,最坏的情况下,每次都需要调用remove ,累计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; //向量规模变化量,即被删除元素总数
共计n-1次迭代,每次常数时间,累计O(n)时间




</pre>忙了一个周之后又放了两个周的暑假,啥也没学,罪过罪过<p></p><p></p><p>有序向量的查找算法:</p><p></p><p>统一接口</p><p></p><pre name="code" class="cpp">template <typename T> //在有序向量的区间[lo, hi)内,确定不大于e的最后一个节点的秩
 Rank Vector<T>::search ( T const& e, Rank lo, Rank hi ) const { //assert: 0 <= lo < hi <= _size
    return ( rand() % 2 ) ? //按各50%的概率随机使用二分查找或Fibonacci查找
           binSearch ( _elem, e, lo, hi ) : fibSearch ( _elem, e, lo, hi );
 }

语义约定


至少应该便于有序向量自身的维护 V.insert (1+V.search(e),e)  

即便失败,也应该给出新元素适当的插入位置

若允许重复元素,则每一组也需按其插入的次序排列  


约定:在有序向量区间中,确定不大于e的最后一个元素,如果e小于区间内任意数,则返回lo-1(左侧哨兵),如果大于,则返回hi-1(末元素 = 右侧哨兵左邻)


版本A:

原理  

  减而治之:以任意元素 x=s[mi]为界,都可将待查找区间分为三部分  

这个地方就是那种最简单的二分查找的算法,不写了。

</pre><pre name="code" class="cpp">// 二分查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 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; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
   return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;查找失败时,简单地返回-1,而不能指示失败的位置

查找长度:

如何更为精细的评估查找算法的性能?  考察关键码的比较次数,即查找长度

通常,需要分别针对成功与失败查找,从最好、最坏、平均等角度评估


改进:失败的话,总是在最深处,左右转向不平等

思路:斐波那契查找


</pre><pre name="code" class="cpp">// 二分查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 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; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
   return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;查找失败时,简单地返回-1,而不能指示失败的位置
#include "..\fibonacci\Fib.h" //引入Fib数列类
 // Fibonacci查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 template <typename T> static Rank fibSearch ( T* A, T const& e, Rank lo, Rank hi ) {
    Fib fib ( hi - lo ); //用O(log_phi(n = hi - lo)时间创建Fib数列
    while ( lo < hi ) { //每步迭代可能要做两次比较判断,有三个分支
       while ( hi - lo < fib.get() ) fib.prev(); //通过向前顺序查找(分摊O(1))——至多迭代几次?
       Rank mi = lo + fib.get() - 1; //确定形如Fib(k) - 1的轴点
       if      ( e < A[mi] ) hi = mi; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
    return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;失败时,简单地返回-1,而不能指示失败的位置




</pre><pre name="code" class="cpp">// 二分查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 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; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
   return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;查找失败时,简单地返回-1,而不能指示失败的位置

</pre>忙了一个周之后又放了两个周的暑假,啥也没学,罪过罪过<p></p><p></p><p>有序向量的查找算法:</p><p></p><p>统一接口</p><p></p><pre name="code" class="cpp">template <typename T> //在有序向量的区间[lo, hi)内,确定不大于e的最后一个节点的秩
 Rank Vector<T>::search ( T const& e, Rank lo, Rank hi ) const { //assert: 0 <= lo < hi <= _size
    return ( rand() % 2 ) ? //按各50%的概率随机使用二分查找或Fibonacci查找
           binSearch ( _elem, e, lo, hi ) : fibSearch ( _elem, e, lo, hi );
 }

语义约定


至少应该便于有序向量自身的维护 V.insert (1+V.search(e),e)  

即便失败,也应该给出新元素适当的插入位置

若允许重复元素,则每一组也需按其插入的次序排列  


约定:在有序向量区间中,确定不大于e的最后一个元素,如果e小于区间内任意数,则返回lo-1(左侧哨兵),如果大于,则返回hi-1(末元素 = 右侧哨兵左邻)


版本A:

原理  

  减而治之:以任意元素 x=s[mi]为界,都可将待查找区间分为三部分  

这个地方就是那种最简单的二分查找的算法,不写了。

</pre><pre name="code" class="cpp">// 二分查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 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; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
   return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;查找失败时,简单地返回-1,而不能指示失败的位置

查找长度:

如何更为精细的评估查找算法的性能?  考察关键码的比较次数,即查找长度

通常,需要分别针对成功与失败查找,从最好、最坏、平均等角度评估


改进:失败的话,总是在最深处,左右转向不平等

思路:斐波那契查找


</pre><pre name="code" class="cpp">// 二分查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 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; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
   return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;查找失败时,简单地返回-1,而不能指示失败的位置
#include "..\fibonacci\Fib.h" //引入Fib数列类
 // Fibonacci查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 template <typename T> static Rank fibSearch ( T* A, T const& e, Rank lo, Rank hi ) {
    Fib fib ( hi - lo ); //用O(log_phi(n = hi - lo)时间创建Fib数列
    while ( lo < hi ) { //每步迭代可能要做两次比较判断,有三个分支
       while ( hi - lo < fib.get() ) fib.prev(); //通过向前顺序查找(分摊O(1))——至多迭代几次?
       Rank mi = lo + fib.get() - 1; //确定形如Fib(k) - 1的轴点
       if      ( e < A[mi] ) hi = mi; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
    return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;失败时,简单地返回-1,而不能指示失败的位置




</pre><pre name="code" class="cpp">// 二分查找算法(版本A):在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
 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; //深入前半段[lo, mi)继续查找
       else if ( A[mi] < e ) lo = mi + 1; //深入后半段(mi, hi)继续查找
       else                return mi; //在mi处命中
    } //成功查找可以提前终止
   return -1; //查找失败
 } //有多个命中元素时,不能保证返回秩最大者;查找失败时,简单地返回-1,而不能指示失败的位置
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值