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

来源:互联网 发布:拓扑算法 编辑:程序博客网 时间:2024/05/20 07:34

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,而不能指示失败的位置
0 0
原创粉丝点击