2.1数组到向量、可扩充向量、无序向量

一、抽象数据类型与数据结构的比较

  • 抽象数据类型 = 数据模型 + 定义在该模型上的一组操作
    • 是抽象定义,关注外部的逻辑特性,定义操作&语义
    • 是一种定义,不考虑时间复杂度不涉及数据的存储方式
  • 数据结构 = 基于某种特定语言,实现ADT的一整套算法
    • 是具体实现,涉及内部的表示与实现,是完整的算法
    • 可以有多种实现,与复杂度密切相关,要考虑数据的具体存储机制
  • ADT类似于一种接口规范,数据接口是具体的接口,二者连接起用户和功能实现者。

二、从数组到向量

  • 数组
    • c/c++语言中,数组A[ ]中的元素与[0,n)内的编号一一对应
    • 反之,每个元素均由(非负)编号唯一指代,并可直接访问
    • A[i]的物理地址 = A + i * s, s为单个单元占用的空间量,故称为线性数组( linear array)
  • 向量
    • 向量是数组元素的抽象与泛化,由一组元素按线性次序封装而成
    • 循秩访问(call-by-rank):各元素的秩(rank)一一对应
    • 元素的类型不限于基本类型
    • 操作、管理维护更加简化、统一与安全
    • 可更为便捷地参与复杂数据结构的定制与实现
  • 向量ADT接口
    向量接口
  • 向量操作实例
    向量接口实例

三、可扩充向量

  • 静态空间管理
    • 开辟内部数组_elem[ ]并使用一段连续的物理空间
    • _capacity:总容量
    • _size:当前的实际规模n
  • 若采用静态空间管理策略,容量_capacity固定,则有明显不足
    • 上溢(overflow):_elem[ ]不足以存放所以元素,尽管此时系统仍有足够的空间
    • 下溢(underflow):_elem[ ]中的元素寥寥无几,装填因子(load factor) = _size / _capacity << 50%
  • 一般的应用环境中难以准确预测空间的需求量->动态空间管理
  • 动态空间管理
    • 法一:在即将发生上溢时,通过开辟一个新的数组将原数组内容复制的形式,适当扩大内部数组的容量
template<typename T>//容量加倍策略
void Vector<T>::expand(){//向量空间不足时扩容
	if(_size < _capacity) return;//尚未满员时,不必扩容
	_capacity = max(_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;//释放原空间
}
  • 容量递增策略
    • 最坏情况:在初始容量0的空间中,连续插入n = m*I个元素
    • 于是,在第1、I+1、2I+1、…次插入时,都需扩容
    • 即便不计申请空间操作,各次扩容过程中复制原向量的时间成本依次为0, I, 2I, …, (m-1)I
    • 总体耗时 = I * (m-1)*m/2 = O(n^2), 每次扩容的分摊成本为O(n)
T* oldElem = _elem; _elem = new T[_capacity += INCREMENT];//追加固定大小的容量
  • 容量加倍策略
    • 最坏情况:在初始容量1的满向量中,连续插入n = 2^m个元素
    • 于是,在第1、2、4、8、…次插入时都需要扩容
    • 各次扩容过程中复制原向量的时间成本依次为1,2,4,8,…,2^m = n
    • 总耗时 = O(n),每次扩容的分摊成本为O(1)
  • 平均分析与分摊分析
    • 平均复杂度或期望复杂度(average/expected complexity)
      • 根据数据结构各种操作出现的概率分布,将对应的成本加权平均。各种可能的操作,作为独立事件分别考察。割裂了操作之间的相关性与连贯性。往往不能准确地评判数据结构和算法的真实性能。
    • 分摊复杂度(amortized complexity)
      • 对数据结构连续地实施足够多次操作,所需总体成本分摊至单次操作。从实际可行的角度,对一系列操作做整体的考量。更加忠实地刻画了可能出现的操作序列。可以更为精准地评判数据结构和算法的真实性能。

四、无序向量

  • 元素的访问
    • 通过V.get( r )和V.put(r, e)接口,已然可以读、写向量元素,但就便捷性而言,远不如数组元素通过下标访问的访问方式。
    • 为使向量通过下标访问,需重载下标运算符" [ ] "
    • 此后,对外的V[r]即对应于内部的V._elem[ r ]
trmplate <typename T>// 0 <= r < r.size,实际应用中要对范围做严格的处理
T & Vector<T>::operator[](Rank r) const { return _elem[r];}
  • 向量的插入算法
template <typename T>//元素e插入到位置r处
Rank Vector<T>::insert(Rank r, T const & e){
	expand();//若有必要,扩容
	for(int i = _size; i>r ; i--)//自后向前
		_elem[i] = _elem[i-1];//后继元素顺次向后移一个单元
	_elem[r] = e;_size++;//置入新元素,更新容量
	return r;//返回秩
}
  • 区间删除
template <typename T>//删除区间[lo,hi),0<=lo<=hi<=size
int Vector<T>::remove(Rank lo, Rank hi){//O(n-hi)
	if(hi == lo) return 0;//出于效率考虑,单独处理退化情况
	while(hi < _size) _elem[lo++] = _elem[hi++];//[hi, _size)顺次前移hi-lo位
	_size = lo; shrink();//更新规模,若有必要则缩容
	return hi - lo;//返回被删除元素的数目
}
  • 单元素删除
    • 可以视作区间删除的特例:[r] = [r, r+1)
    • 为什么不是反过来,基于remove( r )接口,通过反复调用,实现remove(lo, hi)?
    • 每次循环耗时正比于删除区间的后缀长度 = n - hi = O(n)
    • 而循环次数等于区间宽度 = hi - lo = O(n)
    • 如此,将导致总体O(n^2)的复杂度
trmplate <typename T>//删除向量中秩为r的元素,0<=r<size
T Vector<T>::remove(Rank r){//O(n-r)
	T e = _elem[r];//备份被删除元素
	remove( r, r+1);//调用区间删除算法
	return e;//返回被删除元素
}
  • 查找元素
    • 无序向量:T为可判等的基本类型,或已重载操作符==或!=
    • 有序向量:T为可比较的基本类型,或已重载操作符<或>
    • 输入敏感( input - sensitive):最好O(1),最差O(n)
template <typename T>//0<=lo<=hi<_size
Rank Vector<T>::find(T const & e, Rank lo, Rank hi)const
{//O(hi-lo) = O(n),在命中多个元素时可返回秩最大者
	while((lo < hi--)&&(e!=_elem[hi]));//逆向查找
	return hi;//返回的是最后停止的位置,hi<lo意味着失败,否则hi即命中元素的秩
}//返回值供上层调用者判断是否查找成功
  • 唯一化:算法
    • 应用实例:网络搜索的局部结果经过去重操作,汇总为最终报告
    • 不变性:在当前元素V[i]的前缀V[0, i)中,各元素彼此互异(采用数学归纳法证明)
      • 初始i = 1时自然成立。在以后的操作中,新进入前缀的元素如果是被查找的元素,会被删除,如果不是则进入前缀。
    • 单调性:随着反复的while迭代
      • 当前元素前缀的长度单调非降,且迟早增至_size
      • 当前元素后缀的长度单调下降,且迟早减至0
      • 故算法必然终止,且至多迭代O(n)轮
  • 复杂度
    • 每轮迭代中find()和remove()累计消耗线性时间,故总体为O(n^2)。find针对向量中前缀部分而言,remove针对向量中的其它部分,二者在一次while循环中的时间为n,故时间复杂度为O(n ^2)
      *可进一步优化,比如…
    • 仿照uniquify()高效版的思路,元素移动的次数可降至O(n),但比较次数依然是O(n^2);而且,稳定性也会被破坏。
    • 先对需删除的重复元素做标记,然后再统一删除。稳定性保持,但因查找长度更长,从而导致更多的对比操作
    • V.sort().uniquify():简明实现最优的O(nlogn)
template <typename T>//删除重复元素,返回被删除元素数目
int Vector<T>::deduplicate(){
	int oldSize = _size;//记录原始规模
	Rank i = 1;//从elem[1]开始
	while( i<_size )//自前向后逐一考察各元素_elem[i]
		(find(_elem[i], 0, i) < 0) ? // 在前缀中寻找雷同者
		i++//若无雷同则继续考察其后继
		: remove(i);//否则删除雷同者(至多一个?在执行一次while循环时只能删除一个雷同者但是可以保证此时的前缀中没有雷同者,数学归纳法,可以保证算法执行完是没有雷同者的)
	return oldSize - _size;//向量规模变化量,即删除元素总数
}
  • 遍历
  • 遍历向量,统一对各元素分别实施visit操作。如何指定visit操作,如何将其传递到向量内部
    • 利用函数指针机制,只读或局部性修改
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]);}
  • 体会两种方法的优劣
  • 遍历:实例
    • 首先,实现一个可使单个T类型元素加一的类
template <typename T>//假设T可直接递增或已重载操作符++
struct Increase{//函数对象:通过重载操作符"()"实现
	virtual void operator()(T &e){ e++; }//加一
};
  • 此后…
template <typename T> void Increment(Vector<T> & V){
	V.traverse(Increase<T>());//即可以称之为基本操作遍历向量
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值