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)可扩充向量
不足:有可能出现上溢,有可能出现下溢(装填因子<<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,而不能指示失败的位置