【数据结构:线性表——2.1 向量】

更好的阅读体验 \color{red}{更好的阅读体验} 更好的阅读体验


往期文章:


2.1.1 从数组到向量


数组

C、C++和Java等程序设计语言,都将数组作为一种内置的数据类型,支持对一组相关元素的存储组织与访问操作。

具体地,若集合 Sn 个元素组成,且各元素之间具有一个线性次序,可将它们存放于起始于地址 A、物理位置连续的一段存储空间,并统称作数组(array)。

通常以 A 作为该数组的标识。具体地,数组 A[] 中的每一元素都唯一对应于某一下标编号,其中,对于任何 0 < i < j < n , A[i] 都是 A[j] 的前驱(predecessor),A[j] 都是 A[i] 的后继(successor)。

特别地,对于任何 i > 1 , A[i - 1] 称作 A[i] 的直接前驱(immediate predecessor),对于任何 i <= n - 2 , A[i + 1] 称作 A[i] 的直接后继(immediate successor)。

任一元素的所有前驱构成其前缀(prefix),所有后继构成其后缀(suffix)。

采用这一编号规范,不仅可以使得每个元素都通过下标唯一指代,而且可以使我们直接访问到任一元素。这里所说的"访问"包含读取、修改等基本操作,而"直接"则是指这些操作都可以在常数时间内完成。只要从数组所在空间的起始地址 A 出发,即可根据每一元素的编号,经过计算获得待访问元素的物理地址。

具体地,若数组 A[] 存放空间的起始地址为 A,且每个元素占用 s 个单位的空间,则元素 A[i] 对应的物理地址为 A + i * s ,因其中元素的物理地址与其下标之间满足这种线性关系,故亦称作线性数组(linear array)。

向量

按照面向对象思想中的数据抽象原则,可对以上的数组结构做一般性推广,使得其以上特性更具普遍性。

向量(vector)就是线性数组的一种抽象与泛化,它也是由具有线性次序的一组元素构成的集合。其中的元素分别由秩(rank)相互区分。例如:元素 e 的前驱元素共计 r 个, 则其秩就是 r

各元素的秩互异,且均为 [0,n) 内的整数。即,我们通过 r 可唯一确定 e 。这是向量特有的元素访问方式,,作"循秩访问"(call-by-rank)。

经如此抽象之后,我们不再限定同一向量中的各元素都属于同一基本类型,它们本身可以是来自于更具一般性的某一类的对象。


2.1.2 接口


ADT接口

在这里插入图片描述


Vector 模板类

#include <iostream>
#include <cstdlib>
using namespace std;

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();  //装填因子过小时压缩空间

public:
    //构造函数
    Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0){  //默认构造
        _elem = new T[_capacity = c];
        for (_size = 0; _size < s; _elem[_size ++] = v);
    }
    //复制构造接口
    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; } //删除数组
    
    //其他接口函数

    //只读接口
    T get(Rank r);  //获取秩为r的元素值
    int capacity() const{ return _capacity; }  //获取容量
    Rank size() const { return _size; }        //返回最大秩
    bool empty() const { return !_size; }      //判空
    Rank find(T const &e, Rank lo, Rank hi) const;             //无序向量区间查找
    Rank find(T const &e) const { return find(e, 0, _size); }  //无序向量整体查找
    Rank search(T const &e, Rank lo, Rank hi) const;     //有序向量区间查找
    Rank search(T const &e) const { return (_size <= 0) ? -1 : search(e, 0, _size); }  //有序向量整体查找

    //可写入接口
    T &operator[](Rank r) const;              //重载[]操作符,使其能够像数组一样引用元素
    Vector<T> &operator=(Vector<T> const &);  //重载=操作符,使其能够向数组一样赋值
    void put(Rank r, T const &e);  //向量修改
    void unsort() { unsort(0, _size); }  //向量置乱
    void unsort(Rank lo, Rank hi);       //对[lo, hi)区间置乱
    void reverse() { reverse(0, _size); }  //向量逆序
    void reverse(Rank lo, Rank hi);            //对[lo, hi]区间逆序
    Rank insert(Rank r, T const &e);                     //在秩为r的位置插入元素e
    Rank insert(T const &e) { return insert(_size, e); } //默认在末尾插入元素e
    int remove(Rank lo, Rank hi);  //删除区间[lo,hi)的元素,并返回删除的元素个数
    T remove(Rank r);               //删除秩为r的元素,并返回被删除的元素值
    int deduplicate();  //无序去重
    int uniquify();     //有序去重

    //遍历操作
    void traverse(void (*)(T &)); //使用函数指针操作
    template <typename VST>
    void traverse(VST &); //使用函数对象操作

    //排序
    bool bubble(Rank lo, Rank hi);      //冒泡扫描交换
    void bubbleSort(Rank lo, Rank hi);  //冒泡排序
    void mergeSort(Rank lo,Rank hi);        //归并排序
    void merge(Rank lo, Rank mi, Rank hi);  //二路归并

};  //Vector

2.1.3 构造与析构


由于向量的特性,我们选择数组这一结构作为向量类的基本元素单元,因为数组在内存中的物理地址与其逻辑次序一致。

向量在内部维护一个元素为 T 的私有数组 _elem[]:其容量由私有变量 _capacity 指示,有效元素数量由 _size 指示,此外进一步约定:

  • 向量中秩为 r 的元素,对应内部数组中的 _elem[r],其物理地址为 _elem + r

因此,向量对象的构造与析构将围绕这些私有变量和数据区的初始化与销毁展开。


默认构造

与所有对象一样,向量在使用前也需首先被系统创建。

对于构造,我们重载了多个构造函数,其中默认构造方法是:

  • 首先根据创建时的初始容量向系统申请空间,以创建私有数组 _elem[];
  • 若容量未确定,则使用默认值 DEFAULT_CAPACITY
  • 最后由于向量不包含任何元素,故指示规模的变量 _size 初始化为 0 0 0

整个过程没有任何迭代,忽略用于分配数组空间的时间,共需常数时间。

//默认构造
Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0){  
    _elem = new T[_capacity = c];
    for (_size = 0; _size < s; _elem[_size ++] = v);
}

注意

  • 将默认构造函数在类声明中内联实现,创建向量时默认调用该函数。

基于复制的构造

向量的另一种典型创建方式,是以某已有的向量或数组为蓝本,进行(局部或整体)的克隆。

我们对于复制构造进行重载了多个接口,只要接口合法,就可以调用复制构造的核心方法 copyFrom()

//copyFrom()方法
template <typename T>
void Vector<T>::copyFrom(T const *A, Rank lo, Rank hi){
    _elem = new T[_capacity = 2 * (hi - lo)]; //申请空间
    _size = 0;  //规模置零
    while (lo < hi){
        _elem[_size ++] = A[lo ++]; //逐个复制
    }
}

可提供的合法接口:

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); } //复制向量区间

解释

  • copyFrom() 首先根据待复制区间的边界,换算出新向量的初始规模。
  • 再以双倍的容量为内部数组 _elem[] 申请空间。
  • 最后通过一次选代,完成区间 A[1o,hi) 内各元素的顺次复制。

若忽略开辟新空间所需的时间,运行时间应正比于区间宽度,即 O ( h i − l o ) = O ( _ s i z e ) \mathcal{O}(hi - lo) = \mathcal{O}(\_size) O(hilo)=O(_size)

注意

  • 由于向量内部含有动态分配的空间默认的运算符 = 不足以支持向量之间的直接赋值。故通过默认赋值运算符并不能复制向量内部的数据区。

为适应此类赋值操作的需求,我们重载 =[] 操作符:

//重载 =
template <typename T>
Vector<T> &Vector<T>::operator=(const Vector<T> &V){
    delete[] _elem; //删除原有空间,因为下面会申请新的空间
    copyFrom(V._elem, 0, V._size);
    return *this; //返回值为引用便于链式赋值
}
//重载 []
template <typename T>
T &Vector<T>::operator[](Rank r) const{
    return _elem[r];  //返回值为引用,这样就可以实现链式赋值(即连等)
}

析构

与所有对象一样,不再需要的向量应借助析构函数(destructor)及时清理(clean up), 以释放其占用的系统资源。与构造函数不同,同一对象只能有一个析构函数,且不得重载

向量对象的析构,只需释放用于存放元素的内部数组 _elem[],将其占用的空间交还操作系统。_capacity_size 之类的内部变量无需做任何处理,它们将作为向量对象自身的一部分被系统回收,此后既无需也无法被引用。

//析构函数
~Vector() { delete[] _elem; } //删除数组

若不计系统用于空间回收的时间,整个析构过程只需 O ( 1 ) \mathcal{O}(1) O(1) 的时间。


2.1.4 动态空间管理


动态扩容原理

每次插入元素时,我们要检查向量空间大小,若空间不足以插入新的元素,则要扩充向量(extendable vector)。

若要动态实现扩容,我们不能直接在原有的物理空间基础上追加空间。因为数组特有的定址方式要求,物理空间必须地址连续,而我们无法保证,当前向量尾部预留了足够的空间可供扩展。

一种可行的方法如下,我们可以申请一个容量更大的数组 B[],并且将原数组 A[] 中的成员集体搬迁至新的空间,再删除原来的数组 A[]。此后即可顺利地在 B[] 中插入的新元素 e 从而不会导致上溢(overflow)。

在这里插入图片描述


动态扩容策略

那么我们要申请的新容量多少才合适?

有以下两种策略:

  • 容量递增策略:每次扩容,追加固定增量。
  • 容量加倍策略:每次扩容,容量加倍。

容量递增策略

假设我们每次开辟新空间时,比原有的空间增加固定的大小 INCREMENT

  • T* oldElem = _elem; _elem = new T[ _capacity += INCREMENT ] //容量递增;

我们考虑最坏的扩容情况:

  • 在初始容量为 0 0 0 的空向量中,连续插入 n = m × I ≫ 2 n = m\times I \gg 2 n=m×I2个元素,且无删除操作。

则:

  • 我们在第 I , I + 1 , 2 I + 1 , 3 I + 1 … , ( m − 1 ) I + 1 I,I +1,2I+1,3I+1\dots,(m-1)I+1 I,I+1,2I+1,3I+1,(m1)I+1 次插入时,都需扩容。

在这里插入图片描述

即便不计申请空间操作,各次扩容过程中复制原向量的时间成本依次为:

  • 0 , I , 2 I , 3 I , … , ( m − 1 ) I 0,I,2I,3I,\dots,(m-1)I 0,I,2I,3I,,(m1)I,算数级数。

时间复杂度: O ( n 2 ) \mathcal{O}(n^2) O(n2),每次(insert/remove)操作的分摊成本为 O ( n ) \mathcal{O}(n) O(n)


容量加倍策略

假设我们每次开辟新空间时,增加原有空间一倍的大小。

  • T* oldElem = _elem; _elem = new T[ _capacity <<= 1 ]; //容量加倍

我们考虑最坏的扩容情况:

  • 在初始容量为 0 0 0 的空向量中,连续插入 n = 2 m ≫ 2 n = 2^m \gg 2 n=2m2 个元素,且无删除操作。

则:

  • 我们在第 1 , 2 , 4 , 8 , … , 2 m − 1 1,2,4,8,\dots,2^{m-1} 1,2,4,8,,2m1 次插入时,都需要扩容。

在这里插入图片描述

不计申请空间操作,各次扩容过程中复制原向量的时间成本依次为:

  • 1 , 2 , 4 , 8 , … , 2 m − 1 , 2 m = n 1,2,4,8,\dots,2^{m-1},2^m = n 1,2,4,8,,2m1,2m=n ,几何级数。

时间复杂度: O ( n ) \mathcal{O}(n) O(n),每次(insert/remove)操作的分摊成本为 O ( 1 ) \mathcal{O}(1) O(1)


动态扩容算法

由动态扩容对比可知,显然容量加倍的动态扩容策略更好,因此我们选择该策略实现向量扩容:

//加倍扩容expend()
template <typename T>
void Vector<T>::expand(){
    while (_size == _capacity){  //若实际规模等于容量
        T *oldElem = _elem;
        _elem = new T[_capacity <<= 1];  //申请两倍的新的空间
        for (int i = 0; i < _size; i++){
            _elem[i] = oldElem[i]; //若T为非基本类型,则该类型需重载=操作符
        }
        delete[] oldElem; //释放原空间
    }
}

动态缩容算法

上述的扩容策略能够很好的保证我们的向量空间不足时,以较高的效率进行扩容。

导致低效率的另一情况是,向量的实际规模可能远远小于内部数组的容量。比如在连续的一系列操作过程中,若删除操作远多于插入操作,则装填因子极有可能远远小于 100 % 100\% 100%,甚至非常接近于 0 0 0。当装填因子低于某一阀值时,我们称数组发生了下溢(underflow)。

尽管下溢不属于必须解决的问题,但在格外关注空间利用率的场合发生下溢时,我们有必要适当缩减内部数组容量。

接下来提供一种可行的缩容算法 shrink()

//缩容shrink()
template <typename T>
void Vector<T>::shrink(){
    while (_size << 2 < _capacity){ //若实际规模不到容量的1/4,则缩容
        T *oldElem = _elem;
        _elem = new T[_capacity >>= 1]; //申请原来一半的空间
        for (int i = 0; i < _size; i ++){
            _elem[i] = oldElem[i]; //若T为非基本类型,则该类型需重载=操作符
        }
        delete[] oldElem; //释放原空间
    }
}

每次删除操作之后,一旦空间利用率已降至某一阈值以下,该算法随即申请一个容量减半的新数组,将原数组中的元素逐一搬迁至其中,最后将原数组所占空间交还操作系统。

这里以 25 % 25\% 25% 作为装填因子的下限,实际应用中,为避免频繁缩容,可使用更低阀值,取 0 0 0 时即为禁止缩容。

expand() 操作类似,尽管单次 shrink() 操作需要线性量级的时间,但其分摊复杂度亦为 O ( 1 ) \mathcal{O}(1) O(1)。实际上 shrink() 过程等效于 expand() 的逆过程。

这两个算法相互配合,在不致实质地增加接口操作复杂度的前提下,保证了向量内部空间的高效利用。在对单次操作的执行速度极其敏感的应用场合,以上策略并不适用,其中缩容操作甚至可以完全不予考虑。


2.1.5 置乱器


向量置乱算法

从待置乱区间的末元素开始,逆序地向前逐一处理各元素。对每一个当前元素 V[i - 1],先通过调用 rand()函数在[0,i) 之间等概率地随机选取一个元素,再令二者互换位置。

注意

  • 使用 rand() 需导入头文件 <cstdlib>
  • 这里的交换操作 swap(),隐含了三次基于重载操作符 [] 的赋值。

于是每经过一步这样的选代,置乱区间都会向前拓展一个单元。因此经过 O ( n ) \mathcal{O}(n) O(n) 步选代之后,即实现了整个向量的置乱。

//向量置乱
template <typename T>
void permute(Vector<T>& V){
    for(int i = V.size(); i > 0; i --){
        swap(V[i - 1],V[rand() % i]);
    }
}

解释

  • 理论上来说,该算法不仅可以枚举出同一向量的所有可能的排列,且可以保证生成各种排列的概率相等。

封装置乱算法

我们不妨将其封装至 ADT 中:

//封装置乱
template <typename T>
void Vector<T>::unsort(Rank lo, Rank hi){
    T *V = _elem + lo;  //调整指针
    for (Rank i = hi - lo; i > 0; i --){
        std::swap(V[i - 1], V[rand() % i]);
    }
}

通过该接口,可以均匀地置乱区间 [lo,hi] 内的元素。


封装逆序算法

同理,我们也可以封装一个就地逆置方法:

//封装逆序
template <typename T>
void Vector<T>::reverse(Rank lo, Rank hi){
    T *V = _elem;  //调整指针
    Rank l = lo, r = hi - 1;
    while(l < r){
        std::swap(V[l],V[r]);
        l ++, r--;
    }
}

该方法可以实现对区间 [lo,hi) 的元素逆序,且不占用额外的空间。


2.1.6 查找与修改


无序向量的顺序查找

对于无序的向量,查找任意指定元素 e 时,由于没有更多的信息可以借助。故在最坏的情况下,对所有元素进行遍历,直到找到该元素。这里针对无序向量的整体或区间查找重载了两个 find() 接口,整体查找作为特例可直接调用区间查找来完成。因此,我们只需实现区间查找的接口:

//顺序查找
template <typename T>
Rank Vector<T>::find(T const &e, Rank lo, Rank hi) const{
    while ((lo < hi --) && (e != _elem[hi])); //当匹配到对应的e后停止,并返回秩
    return hi;  //若查找失败,会返回lo - 1
}

解释

  • 当同时有多个命中元素时,我们统一约定返回其中秩最大者, 之后的查找接口 find() 亦是如此。
  • 采用自后向前的查找次序,一旦命中即可立即返回,从而省略掉不必要的比对。
  • 查找失败时约定统一返回 − 1 -1 1。这不仅简化了对查找失败情况的判别,同时也使此时的返回结果更加易于理解。
  • while循环的控制逻辑由两部分组成,首先判断是否已抵达通配符,再判断当前元素与目标元素是否相等。得益于C/C++语言中逻辑表达式的短路求值特性,在前一判断非真后循环会立即终止,而不致可能因试图引用已越界的秩 ( − 1 -1 1)而出错。

最坏情况下,查找终止于首元素 _elem[1o],运行时间为 O ( h i − l o ) = O ( n ) \mathcal{O}(hi-lo) = \mathcal{O}(n) O(hilo)=O(n)。最好情况下,查找命中于末元素 _elem[hi - 1],仅需 O ( 1 ) \mathcal{O}(1) O(1) 时间。对于规模相同、内部组成不同的输入,渐进运行时间却有本质区别。故此类算法也称作输入敏感的(input sensitive)算法。


有序向量的二分查找

对于一个有序向量 S,其中的元素不再随机分布,秩 rS[r]S 中按大小的相对位次,位于 S[r] 前(后)方的元素均不致于更大(小)。当所有元素互异时,r 即是 S 中小于 S[r] 的元素数目。一般地,,若小于、等于 S[r] 的元素各有ik 个,则该元素及其雷同元素应集中分布于 S[i, i + k)

利用上述性质,有序向量的查找操作可以利用二分查找高效地完成。为区别于无序向量的查找接口 find(),有序向量的查找接口将统一命名为 search()。与 find() 一样,也针对有序向量的整体或区间查找重载了两个 search() 接口,且前者作为特例可直接调用后者。

// 二分查找
template <typename T>  
Rank Vector<T>::search(T const &e, Rank lo, Rank hi) const{  //在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
    T* A = _elem;
    while ( lo < hi ){   //每步迭代仅需做一次比较判断,有两个分支       
        Rank mi = ( lo + hi ) >> 1; //以中点为轴点
        ( e < A[mi] ) ? hi = mi : lo = mi + 1; //经比较后确定深入[lo, mi)或(mi, hi)
    } //成功查找不能提前终止
    Rank p = -- lo;  //循环结束时,lo为大于e的元素的最小秩,故lo - 1即不大于e的元素的最大秩
    if(A[p] == e) return p; //有多个命中元素时,总能保证返回秩最大者
    return -1;  //查找失败时,返回 -1
}

解释

  • 只有当有效区间的宽度缩短至 0 0 0 时,查询结束。
  • 在每次转入后端分支时,由于子向量的左边界取作 mi + 1 而不是 mi,通过数学归纳可以证明,循环体内具有如下不变性:A[0,lo) 中的元素皆不大于 eA[hi,n) 中的元素皆大于 e。故不会忽略 A[mi]
  • 循环终止时,lo = hi,即 A[1o - 1] 为原向量中不大于 e 的最后一个元素。因此在循环结束之后,无论成功与否,只需返回lo - 1
  • 有多个命中元素时,总能保证返回秩最大者;查找失败时,能够返回 − 1 -1 1

综上,整个算法时间复杂度仍保持在 O ( log ⁡ 2 n ) \mathcal{O}(\log_2^n) O(log2n)


向量元素的获取和修改

按照 ADT 的定义,提供 get(r),获取向量秩为 r 的元素的值;提供 put(r, e),修改向量秩为 r 的元素为 e

//获取秩为r的元素值
template <typename T>
T Vector<T>::get(Rank r){
    T value = _elem[r]; //备份将被删除的元素
    return value;       //反回被删除的元素
}
//修改
template <typename T>
void Vector<T>::put(Rank r, T const &e){
    _elem[r] = e;
}

2.1.6 插入和删除


插入

按照 ADT 的定义,插入操作 insert(r,e) 负责将任意给定的元素 e 插入到任意指定秩为 r 的单元。我们同样重载接口,来同时实现尾部插入,故作为区间插入的特例,我们只需实现指定秩的元素插入:

//插入
template <typename T>
Rank Vector<T>::insert(Rank r, T const &e){  //将e作为秩为r元素插入
    expand();  //若需要,先扩容
    for (int i = _size; i > r;) _elem[i] = _elem[i - 1];  //整体后移一位,从后向前
    _elem[r] = e, _size ++;  //置入e并更新容量
    return r;  //返回秩
}

解释

  • 插入单个元素,届时需要检查容量是否支持该次插入操作,不支持时需要扩容。

该操作所需时间主要在于后继元素的后移,线性正比于后缀的长度。新插入元素越靠后(前)所需时间越短(长)。当 r 取最大值 _size 时为最好情况,只需 O ( 1 ) \mathcal{O}(1) O(1) 时间,r 取最小值 0 0 0 时为最坏情况,需要 O ( _ s i z e ) \mathcal{O}(\_size) O(_size) 时间。

一般地,若插入位置等概率分布,则平均运行时间为 O ( n ) \mathcal{O}(n) O(n),线性正比于向量的实际规模。


删除

删除操作重载有两个接口,remove(lo,hi)用以删除区间 [lo,hi) 内的元素,而 remove(r) 用以删除秩为 r 的单个元素。因数组中元素的地址必须连续,故每删除一个元素时,所有后继元素都需向前移动一个单元。若后继元素共有 m = _size - hi 个,则对 remove(r) 的每次调用都需移动 m 次,对于整个区间,元素移动的次数累计将达到 m*(hi - lo),为后缀长度和待删除区间宽度的乘积。故对于这两个接口,我们应将单元素删除视作区间删除的特例,并基于后者来实现前者:

//区间删除
template <typename T>
int Vector<T>::remove(Rank lo, Rank hi){
    if (lo == hi) return 0;        //出于效率考虑,单独处理退化情况,如remove(0,0)
    while(hi < _size){
        _elem[lo ++] = _elem[hi ++]; //整体前移,若删除区间大于其后缀区间,未覆盖部分不做处理,下次缩容时自动消除
    }
    _size = lo;                    //确定新界限
    shrink();                      //若装填因子过小,缩容
    return hi - lo;                //返回删除元素的个数
}

//删除秩为r的元素
template <typename T>
T Vector<T>::remove(Rank r){
    T re_elem = _elem[r]; //备份将被删除的元素
    remove(r, r + 1);     //调用区间删,等效为对区间[r, r + 1)的删除
    return re_elem;       //反回被删除的元素
}

解释

  • 区间删除元素,删除后,检查当前的容量是否过大,当若实际规模不到容量的25%,则缩容。

remove(lo, hi) 的计算成本,主要消耗于后续元素的前移,线性正比于后缀的长度。区间删除操作所需的时间,应该仅取决于后继元素的数目,而与被删除区间本身的宽度无关。

特别地,基于该接口实现的单元素删除接口 remove(r) 需耗时 O ( _ s i z e − r ) \mathcal{O}(\_size-r) O(_sizer)。也就是说,被删除元素在向量中的位置越靠后 (前)所需时间越短(长),最好为删除末尾元素,只需 O ( 1 ) \mathcal{O}(1) O(1) 时间,最坏情况下删除首元素,需要 O ( _ s i z e ) \mathcal{O}(\_size) O(_size) 时间。


2.1.7 去重


无序向量的唯一化

若想对无序向量进行去重操作,我们只需在当前元素的前缀中寻找相同的元素。如找到,则删除该元素,如没有找到,则转到该元素的后继,继续重复上述操作。

//无序向量去重
template <typename T>
int Vector<T>::deduplicate(){
    int oldSize = _size; //记录原始规模
    Rank i = 1;
    while (i < _size){               //从前向后依次检查_elem[i]
        (find(_elem[i], 0, i) < 0) ? //在其前缀中寻找相同元素
        i ++ : remove(i);            //若查找到,删除该元素并检查其后继元素
    }
    return oldSize - _size;          //返回删除的元素个数
}

解释

  • 在循环体内,有如下不变性:在当前元素的前缀 _elem[0,i) 内,不存在重复元素。
  • 随着循环进行,当前元素的后继不断减少,经过 n - 2 步迭代后结束。

这里所需时间主要消耗于 find()remove() 两个接口。每次迭代的时间为 O ( n ) \mathcal{O}(n) O(n),总体的时间复杂度为 O ( n 2 ) \mathcal{O}(n^2) O(n2)


有序向量的唯一化

将无序向量唯一化,我们通常会将其转化为有序向量,然后对其有序向量进行去重操作。

对于有序向量,重复的元素必然是连续的区间,因此我们可以对重复的元素进行区间删除,从而实现有序向量的去重操作。

//有序向量去重
template <typename T>
int Vector<T>::uniquify(){
    int i = 0, j = 0;
    while(++ j < _size){//逐一扫描,直至末元素
        if(_elem[i] != _elem[j]){  //跳过雷同元素
            _elem[++i ] = _elem[j];//发现不同元素时,向前移至紧邻于前者右侧
        }
    }
    _size = ++ i; shrink();//直接截去尾部多余的元素
    return j - i;
}

整个算法中,每经过一次迭代, j 都必然加一,故最多迭代 n 次,最终算法的时间复杂度为 O ( n ) \mathcal{O}(n) O(n),比无序去重效率提高了一个线性因子。


2.1.8 遍历


向量往往作为整体进行统一操作,如输出向量的所有元素,或按照某种流程统一修改所有元素值。针对这些批量操作,我们都为其提供接口。

遍历向量,对每个元素执行函数指针提供的操作:

// 方法一:
template <typename T> 
void Vector<T>::traverse(void (*visit)(T&)){  // 借助函数指针机制
    for (int i = 0; i < _size; i ++){
        visit(_elem[i]);
    }
}

遍历向量,对每个元素执行函数对象(一种重载()操作符的特殊类,其实例对象可以像函数一样调用)提供的操作:

// 方法二:
template <typename T>  // 元素类型
template <typename VST> // 操作器
void Vector<T>::traverse(VST& visit){ // 借助函数对象机制
    for (int i = 0; i < _size; i ++){
        visit(_elem[i]);  // 遍历变量
    }
}

我们可以自定义函数指针和函数对象来执行批量的任务。

实例

实现遍历输出:

template <typename T, typename VST>
void traverse(VST& visit, T& V, Rank lo, Rank hi) {
    for(Rank i = lo; i <= hi; i ++){
        visit(V[i]);
    }
}

//输出操作器的实现
void show(int e) {
    cout << e << " ";
}

调用:

traverse(show, A, l, r);  //遍历输出[l,r]的元素

2.1.9 排序


  • 将无序向量变为有序向量,我们需要借助排序算法。
  • 本节提供一种稳定的排序方法——冒泡排序,以及高效实用的归并排序算法。

排序的稳定性

  • 稳定性(stability)是对排序算法更为细致的要求。
  • 具体地,在将向量 A 转换为有序向量 S 之后,设 A[i] 对应于 S[ki]。若对于 A 中每一对重复元素 A[i] = A[j] (相应地 S[ki] = S[kj]),都有 i < j 当且仅当 ki< kj 则称该排序算法是稳定算法 (stable algorithm)。
  • 简而言之,稳定算法的特征是,重复元素之间的相对次序在排序前后保持一致。反之,不具有这一特征的排序算法都是不稳定算法(unstable algorithm)。
  • 稳定的排序算法,可用以实现同时对多个关键码按照字典序的排序。

冒泡排序

提供接口 bubbleSort() 封装在 ADT 中,调用时对向量区间 [lo,hi)进行冒泡排序。

//冒泡扫描交换
template <typename T> 
void Vector<T>::bubbleSort(Rank lo, Rank hi)
{ while (!bubble(lo,hi --)); } //逐趟做扫描交换,直至全序

//冒泡排序
template <typename T> 
bool Vector<T>::bubble(Rank lo, Rank hi){
    bool sorted = true; //整体有序标志
    while (++ lo < hi){ //自左向右,逐一检查各对相邻元素
        if (_elem[lo - 1] > _elem[lo]){ //若逆序
            sorted = false;
            swap(_elem[lo - 1], _elem[lo]); //交换
        }
    }
    return sorted; //返回有序标志
}

解释

  • 冒泡排序过程中元素相对位置有所调整的唯一可能是:某元素 _elem[i - 1] 严格大于其后继 _elem[i],故属于稳定算法。

该算法最好的情况在向量乱序限于 [0,sqrt(n)]时,仍需 O ( n 3 2 ) \mathcal{O}(n^{\frac{3}{2}}) O(n23),否则需要 O ( n 2 ) \mathcal{O}(n^2) O(n2) 的时间。


归并排序

归并排序(merge sort)是第一个可以在最坏情况下依然保持 O ( n log ⁡ 2 n ) \mathcal{O}(n\log_2^n) O(nlog2n) 运行时间的确定性排序算法。

归并排序基于分治策略

  • 先取需要排序的向量中间位置,将其划分为左右两个子区间。 递归重复上述处理操作,直至区间无法继续划分。
  • 递归返回时,分别对左右两个区间进行排序,并反复调用二路归并算法,将相邻等长的子区间不断合并成更大的有序区间,直到最终得到整个有序向量。

在这里插入图片描述

接下来我们分别提供递归处理区间的接口和二路归并的接口,以适应不同需求:

//分治策略
template <typename T>
void Vector<T>::mergeSort(Rank lo, Rank hi){
    if(hi - lo < 2) return;
    int mi = (lo + hi) >> 1;
    mergeSort(lo, mi);//对前半区间排序
    mergeSort(mi, hi);//对后半区间排序
    merge(lo, mi, hi);//两个区间的归并
}
//归并的实现
template <typename T>
void Vector<T>::merge(Rank lo, Rank mi, Rank hi){
    T* A = _elem + lo;
    int lb = mi - lo; T* B = new T[lb];
    for(Rank i = 0; i < lb; i ++) B[i] = A[i];
    int lc = hi - mi; T* C = _elem + mi;
    for(Rank i = 0, j = 0, k = 0; (j < lb) || (k < lc);){
        if((j < lb) && (!(k < lc) || (B[j] <= C[k]))) A[i ++] = B[j ++];
        if((k < lc) && (!(j < lb) || (C[k] < B[j]))) A[i ++] = C[k ++];     
    }
    delete [] B;
}


2.1.10 向量测试


#include <iostream>
#include <cstdlib>
using namespace std;

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();  //装填因子过小时压缩空间

public:
    //构造函数
    Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0){  //默认构造
        _elem = new T[_capacity = c];
        for (_size = 0; _size < s; _elem[_size ++] = v);
    }
    //复制构造接口
    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; } //删除数组
    
    //其他接口函数

    //只读接口
    T get(Rank r);  //获取秩为r的元素值
    int capacity() const{ return _capacity; }  //获取容量
    Rank size() const { return _size; }        //返回最大秩
    bool empty() const { return !_size; }      //判空
    Rank find(T const &e, Rank lo, Rank hi) const;             //无序向量区间查找
    Rank find(T const &e) const { return find(e, 0, _size); }  //无序向量整体查找
    Rank search(T const &e, Rank lo, Rank hi) const;     //有序向量区间查找
    Rank search(T const &e) const { return (_size <= 0) ? -1 : search(e, 0, _size); }  //有序向量整体查找

    //可写入接口
    T &operator[](Rank r) const;              //重载[]操作符,使其能够像数组一样引用元素
    Vector<T> &operator=(Vector<T> const &);  //重载=操作符,使其能够向数组一样赋值
    void put(Rank r, T const &e);  //向量修改
    void unsort() { unsort(0, _size); }  //向量置乱
    void unsort(Rank lo, Rank hi);       //对[lo, hi)区间置乱
    void reverse() { reverse(0, _size); }  //向量逆序
    void reverse(Rank lo, Rank hi);            //对[lo, hi]区间逆序
    Rank insert(Rank r, T const &e);                     //在秩为r的位置插入元素e
    Rank insert(T const &e) { return insert(_size, e); } //默认在末尾插入元素e
    int remove(Rank lo, Rank hi);  //删除区间[lo,hi)的元素,并返回删除的元素个数
    T remove(Rank r);               //删除秩为r的元素,并返回被删除的元素值
    int deduplicate();  //无序去重
    int uniquify();     //有序去重

    //遍历操作
    void traverse(void (*)(T &)); //使用函数指针操作
    template <typename VST>
    void traverse(VST &); //使用函数对象操作

    //排序
    bool bubble(Rank lo, Rank hi);      //冒泡扫描交换
    void bubbleSort(Rank lo, Rank hi);  //冒泡排序
    void mergeSort(Rank lo,Rank hi);        //归并排序
    void merge(Rank lo, Rank mi, Rank hi);  //二路归并

};  //Vector

//copyFrom()方法
template <typename T>
void Vector<T>::copyFrom(T const *A, Rank lo, Rank hi){
    _elem = new T[_capacity = 2 * (hi - lo)]; //申请空间
    _size = 0;  //规模置零
    while (lo < hi){
        _elem[_size ++] = A[lo ++]; //逐个复制
    }
}

//重载 =
template <typename T>
Vector<T> &Vector<T>::operator=(const Vector<T> &V)
{
    delete[] _elem; //删除原有空间,因为下面会申请新的空间
    copyFrom(V._elem, 0, V._size);
    return *this; //返回值为引用便于链式赋值
}

//重载 []
template <typename T>
T &Vector<T>::operator[](Rank r) const{
    return _elem[r];  //返回值为引用,这样就可以实现链式赋值(即连等)
}

//加倍扩容expend()
template <typename T>
void Vector<T>::expand(){
    while (_size == _capacity){  //若实际规模等于容量
        T *oldElem = _elem;
        _elem = new T[_capacity <<= 1];  //申请两倍的新的空间
        for (int i = 0; i < _size; i++){
            _elem[i] = oldElem[i]; //若T为非基本类型,则该类型需重载=操作符
        }
        delete[] oldElem; //释放原空间
    }
}

//缩容shrink()
template <typename T>
void Vector<T>::shrink(){
    while (_size << 2 < _capacity){ //若实际规模不到容量的1/4,则缩容
        T *oldElem = _elem;
        _elem = new T[_capacity >>= 1]; //申请原来一半的空间
        for (int i = 0; i < _size; i ++){
            _elem[i] = oldElem[i]; //若T为非基本类型,则该类型需重载=操作符
        }
        delete[] oldElem; //释放原空间
    }
}

//向量置乱
// template <typename T> 
// void permute(Vector<T>& V){
//     for(int i = V.size(); i > 0; i --){
//         swap(V[i - 1],V[rand() % i]);
//     }
// }

//封装置乱
template <typename T>
void Vector<T>::unsort(Rank lo, Rank hi){
    T *V = _elem + lo;  //调整指针
    for (Rank i = hi - lo; i > 0; i --){
        std::swap(V[i - 1], V[rand() % i]);
    }
}

//封装逆序
template <typename T>
void Vector<T>::reverse(Rank lo, Rank hi){
    T *V = _elem;  //调整指针
    Rank l = lo, r = hi - 1;
    while(l < r){
        std::swap(V[l],V[r]);
        l ++, r--;
    }
}

//顺序查找
template <typename T>
Rank Vector<T>::find(T const &e, Rank lo, Rank hi) const{
    while ((lo < hi --) && (e != _elem[hi])); //当匹配到对应的e后停止,并返回秩
    return hi;  //若查找失败,会返回lo - 1
}

// 二分查找
template <typename T>  
Rank Vector<T>::search(T const &e, Rank lo, Rank hi) const{  //在有序向量的区间[lo, hi)内查找元素e,0 <= lo <= hi <= _size
    T* A = _elem;
    while ( lo < hi ){   //每步迭代仅需做一次比较判断,有两个分支       
        Rank mi = ( lo + hi ) >> 1; //以中点为轴点
        ( e < A[mi] ) ? hi = mi : lo = mi + 1; //经比较后确定深入[lo, mi)或(mi, hi)
    } //成功查找不能提前终止
    Rank p = -- lo;  //循环结束时,lo为大于e的元素的最小秩,故lo - 1即不大于e的元素的最大秩
    if(A[p] == e) return p; //有多个命中元素时,总能保证返回秩最大者
    return -1;  //查找失败时,返回 -1
}

//获取秩为r的元素值
template <typename T>
T Vector<T>::get(Rank r){
    T value = _elem[r]; //备份将被删除的元素
    return value;       //反回被删除的元素
}

//修改
template <typename T>
void Vector<T>::put(Rank r, T const &e){
    _elem[r] = e;
}

//插入
template <typename T>
Rank Vector<T>::insert(Rank r, T const &e){  //将e作为秩为r元素插入
    expand();  //若需要,先扩容
    for (int i = _size; i > r;) _elem[i] = _elem[i - 1];  //整体后移一位,从后向前
    _elem[r] = e, _size ++;  //置入e并更新容量
    return r;  //返回秩
}

//区间删除
template <typename T>
int Vector<T>::remove(Rank lo, Rank hi){
    if (lo == hi) return 0;        //出于效率考虑,单独处理退化情况,如remove(0,0)
    while(hi < _size){
        _elem[lo ++] = _elem[hi ++]; //整体前移,若删除区间大于其后缀区间,未覆盖部分不做处理,下次缩容时自动消除
    }
    _size = lo;                    //确定新界限
    shrink();                      //若装填因子过小,缩容
    return hi - lo;                //返回删除元素的个数
}

//删除秩为r的元素
template <typename T>
T Vector<T>::remove(Rank r){
    T re_elem = _elem[r]; //备份将被删除的元素
    remove(r, r + 1);     //调用区间删,等效为对区间[r, r + 1)的删除
    return re_elem;       //反回被删除的元素
}

//无序向量去重
template <typename T>
int Vector<T>::deduplicate(){
    int oldSize = _size; //记录原始规模
    Rank i = 1;
    while (i < _size){               //从前向后依次检查_elem[i]
        (find(_elem[i], 0, i) < 0) ? //在其前缀中寻找相同元素
        i ++ : remove(i);            //若查找到,删除该元素并检查其后继元素
    }
    return oldSize - _size;          //返回删除的元素个数
}

//有序向量去重
template <typename T>
int Vector<T>::uniquify(){
    int i = 0, j = 0;
    while(++ j < _size){//逐一扫描,直至末元素
        if(_elem[i] != _elem[j]){  //跳过雷同元素
            _elem[++i ] = _elem[j];//发现不同元素时,向前移至紧邻于前者右侧
        }
    }
    _size = ++ i; shrink();//直接截去尾部多余的元素
    return j - i;
}

// 方法一:
template <typename T> 
void Vector<T>::traverse(void (*visit)(T&)){  // 借助函数指针机制
    for (int i = 0; i < _size; i ++){
        visit(_elem[i]);
    }
}

// 方法二:
template <typename T>  // 元素类型
template <typename VST> // 操作器
void Vector<T>::traverse(VST& visit){ // 借助函数对象机制
    for (int i = 0; i < _size; i ++){
        visit(_elem[i]);  // 遍历变量
    }
}


template <typename T, typename VST>
void traverse(VST& visit, T& V, Rank lo, Rank hi) {
    for(Rank i = lo; i <= hi; i ++){
        visit(V[i]);
    }
}

//输出操作器的实现
void show(int e) {
    cout << e << " ";
}

//冒泡扫描交换
template <typename T> 
void Vector<T>::bubbleSort(Rank lo, Rank hi)
{ while (!bubble(lo,hi --)); } //逐趟做扫描交换,直至全序

//冒泡排序
template <typename T> 
bool Vector<T>::bubble(Rank lo, Rank hi){
    bool sorted = true; //整体有序标志
    while (++ lo < hi){ //自左向右,逐一检查各对相邻元素
        if (_elem[lo - 1] > _elem[lo]){ //若逆序
            sorted = false;
            swap(_elem[lo - 1], _elem[lo]); //交换
        }
    }
    return sorted; //返回有序标志
}

//分治策略
template <typename T>
void Vector<T>::mergeSort(Rank lo, Rank hi){
    if(hi - lo < 2) return;
    int mi = (lo + hi) >> 1;
    mergeSort(lo, mi);//对前半区间排序
    mergeSort(mi, hi);//对后半区间排序
    merge(lo, mi, hi);//两个区间的归并
}

//归并的实现
template <typename T>
void Vector<T>::merge(Rank lo, Rank mi, Rank hi){
    T* A = _elem + lo;
    int lb = mi - lo; T* B = new T[lb];
    for(Rank i = 0; i < lb; i ++) B[i] = A[i];
    int lc = hi - mi; T* C = _elem + mi;
    for(Rank i = 0, j = 0, k = 0; (j < lb) || (k < lc);){
        if((j < lb) && (!(k < lc) || (B[j] <= C[k]))) A[i ++] = B[j ++];
        if((k < lc) && (!(j < lb) || (C[k] < B[j]))) A[i ++] = C[k ++];     
    }
    delete [] B;
}

int num[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};  //有序数组num

void test_01(){

    cout << "##### test_01() begin #####" << endl << endl;

    cout << "Test Vector Init :" << endl;

    Vector<int> a;
    Vector<double> b;
    Vector<Vector<int>> c;

    cout << "RIGHT!" << endl << endl;

    cout << "Test Vector copyFrom() :" << endl;

    Vector<int> d(num,1,10);
    Vector<int> e = d;
    Vector<int> f(e,0,2);

    cout << "d = ";
    traverse(show, d, 0, 9);
    cout << endl;

    cout << "e = ";
    traverse(show, e, 0,9);
    cout << endl;

    cout << "f = ";
    traverse(show, f, 0,1);
    cout << endl;

    cout << "RIGHT!" << endl << endl;

    cout << "##### test_01() over #####" << endl;

}

void test_02(){

    cout << "##### test_02() begin #####" << endl << endl;

    cout << "Test Vector expend() :" << endl;

    Vector<int> a(num,0,2);
    cout << "a.size() = " << a.size() << endl;
    cout << "a.capacity() = " << a.capacity() << endl;
    cout << "RIGHT!" << endl << endl;

    Vector<int> b(num,0,10);
    cout << "b.size() = " << b.size() << endl;
    cout << "b.capacity() = " << b.capacity() << endl;
    cout << "RIGHT!" << endl << endl;

    cout << "##### test_02() over #####" << endl;

}

void test_03(){

    cout << "##### test_03() begin #####" << endl << endl;

    cout << "Test Vector unsort() :" << endl;

    Vector<int> a(num,0,10);

    cout << "a = ";
    traverse(show, a, 0, 9);
    cout << endl;

    a.unsort();  //乱序

    cout << "unsort a = ";
    traverse(show, a, 0, 9);
    cout << endl;
    cout << "RIGHT!" << endl << endl;

    Vector<int> b(num,0,10);

    cout << "b = ";
    traverse(show, b, 0, 9);
    cout << endl;

    b.unsort(2,9);

    cout << "unsor[2,9) b = ";
    traverse(show, b, 0, 9);
    cout << endl;
    cout << "RIGHT!" << endl << endl;

    cout << "Test reverse() :" << endl;

    Vector<int> c(num,0,10);

    cout << "c = ";
    traverse(show, c, 0, 9);
    cout << endl;

    c.reverse();

    cout << "reverse c = ";
    traverse(show, c, 0, 9);
    cout << endl;
    cout << "RIGHT!" << endl << endl;

    Vector<int> d(num,0,10);

    cout << "d = ";
    traverse(show, d, 0, 9);
    cout << endl;

    d.reverse(1,5);

    cout << "reverse[1,5) d = ";
    traverse(show, d, 0, 9);
    cout << endl;
    cout << "RIGHT!" << endl << endl;
    

    cout << "##### test_03() over #####" << endl;

}

void test_04(){

    cout << "##### test_04() begin #####" << endl << endl;

    cout << "Test Vector find() :" << endl;

    Vector<int> a(num,0,10);
    a.unsort();
    cout << "a = ";
    traverse(show, a, 0, 9);
    cout << endl;

    cout << "find(9) = " << a.find(9) << endl;
    cout << "find(100) = " << a.find(100) << endl;
    cout << "RIGHT!" << endl << endl;

    cout << "Test Vector search() :" << endl;

    Vector<int> b(num,0,10);
    cout << "b = ";
    traverse(show, b, 0, 9);
    cout << endl;

    cout << "search(9) = " << b.search(9) << endl;
    cout << "search(100) = " << b.search(100) << endl;
    cout << "RIGHT!" << endl << endl;

    cout << "Test Vector put() :" << endl;

    Vector<int> c(num,0,10);
    cout << "c = ";
    traverse(show, c, 0, 9);
    cout << endl;

    c.put(0,100);
    c.put(2,-10);
    c.put(9,20);
    cout << "put() c = ";
    traverse(show, c, 0, 9);
    cout << endl;

    cout << "c[4] = " << c.get(4) << endl;
    cout << "c[6] = " << c.get(6) << endl;
    cout << "RIGHT!" << endl << endl;
    

    cout << "##### test_04() over #####" << endl;

}

void test_05(){

    cout << "##### test_05() begin #####" << endl << endl;

    cout << "Test Vector insert() :" << endl;

    Vector<int> a;

    for(int i = 0; i < 5; i ++) a.insert(num[i]);

    cout << "a = ";
    traverse(show, a, 0, 4);
    cout << endl;
    cout << "RIGHT!" << endl << endl;

    cout << "Test Vector deduplicate() :" << endl;

    Vector<int> b(num,0,10);
    for(int i = 0; i < 5; i ++) b.insert(num[i]);
    b.unsort();
    cout << "b = ";
    traverse(show, b, 0, b.size() - 1);
    cout << endl;

    b.deduplicate();

    cout << "deduplicate b = ";
    traverse(show, b, 0, b.size() - 1);
    cout << endl;
    cout << "RIGHT!" << endl << endl;

    cout << "Test Vector uniquify() :" << endl;

    Vector<int> c(num,0,10);
    for(int i = 0; i < 5; i ++) c.insert(num[i]);
    c.unsort();
    
    c.mergeSort(0, c.size());
    // c.bubbleSort(0, c.size());

    cout << "c = ";
    traverse(show, c, 0, c.size() - 1);
    cout << endl;

    int s = c.uniquify();

    cout << "c = ";
    traverse(show, c, 0, c.size() - 1);
    cout << endl;    
    cout << "RIGHT!" << endl << endl;

    cout << "##### test_05() over #####" << endl;

}

int main(){

    cout << "###########################" << endl;
    test_01();
    cout << "###########################" << endl;
    cout << endl;

    cout << "###########################" << endl;
    test_02();
    cout << "###########################" << endl;
    cout << endl;

    cout << "###########################" << endl;
    test_03();
    cout << "###########################" << endl;
    cout << endl;

    cout << "###########################" << endl;
    test_04();
    cout << "###########################" << endl;
    cout << endl;

    cout << "###########################" << endl;
    test_05();
    cout << "###########################" << endl;
    cout << endl;

    system("pause");

    return 0;

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪漫主义狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值