数据结构之向量
1.简述
数据结构是数据项的结构化集合,其结构性表现为数据项之间的相互联系及作用,也可以理解为定义数据项之间的某种逻辑次序。而根据这种逻辑次序的复杂程度,大致可以将各种逻辑结构大致分为三大类。
- 线性结构
- 半线性结构
- 非线性结构
最为基本的线性结构统称为序列,分为向量和列表。
- 在向量中,所有数据项的物理存放位置与逻辑次序完全吻合,此时的逻辑次序也称作秩。
- 在列表中,逻辑上相邻的数据项在物理上未必相邻,而采用间接定址的方式通过封装后的位置相互引用。
2.接口
操作接口 | 功能 | 适用对象 |
---|---|---|
size() | 报告规模 | 向量 |
get(r) | 获取秩为r的元素的数值 | 向量 |
insert(r) | e作为秩为r的元素 | 向量 |
remove(r) | 删除秩为r的元素 | 向量 |
disorder() | 判断所有元素是否已按非降序排列 | 向量 |
sort() | 调整各元素位置,使其按非降序排列 | 向量 |
find(e) | 查找目标元素e,返回不大于e且秩最大的元素 | 向量 |
search(e) | 查找目标元素e,返回不大于e且秩最大的元素 | 有序向量 |
deduplicate() | 删除重复元素 | 向量 |
uniquify() | 删除重复元素 | 有序向量 |
traverse() | 遍历向量并统一处理所有元素 | 向量 |
模板类
#include ""
#include<>
#include<iostream>
using namespace std;
typedef int Rank;
#define DEFAULT_CAPACITY 3
#define IsValid(x,lo,hi) ((x)<=hi||(x)>=lo)
template<typename T>class vector
{
protected:
T* _elem; int _size; int _capacity;
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); }//容量为c,规模s,内容为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>& v) { copyFrom(v._elem, 0, v._size); }//基于向量的整体复制
vector(vector<T>& v, Rank lo, Rank hi) { copyFrom(v._elem, lo, hi); }//基于向量的区间复制
//析构函数
~
vector() {
delete[] _elem;
}
//只读接口
int size() const { return _size; }//规模
bool empty() const { return !_size; }//判空
int dissorted() const;//是否无序
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 (0>=_size)?-1:Search(e, 0, _size); }//有序向量整体查找
//可写访问接口
Rank insert(T const& e, Rank r) ;//插入指定位置
Rank insert(T const& e) { return insert(e, _size); }//尾插入
int remove(Rank lo, Rank hi);
T remove(Rank r);
T& operator[](Rank r);
vector<T> & operator= (vector<T> const&);
void sort(Rank lo, Rank hi);//区间排序
void sort() { sort(0, _size); }//整体排序
int deduplicate();//无序向量唯一化
int uniquify();//有序向量唯一化
template<typename VST>void traverse(VST&);//遍历
private:
void copyFrom(T const* A, Rank lo, Rank hi);
void expand();
void shrink();
Rank max(Rank lo, Rank hi);
void selectionSort(Rank lo, Rank hi);
bool bubble(Rank lo, Rank hi);
void bubbleSort(Rank lo, Rank hi);
//void insertionSort(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);
};
3.2 构造与析构
向量结构在内部维护一个元素类型为T的私有数组_elem[];其容量由变量_capacity指定;有效元素的数量,则由_size变量进一步指定。因此,向量对象析构销毁数据区_elem[]即可。
3.3基于复制的构造方法
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=(vector<T> const &v)
{
if (_elem) delete[] _elem;
copyFrom(v._elem, 0, ());
return *this;
}
3.4动态空间管理
内部数组所占物理空间的容量,在向量生命周期内不允许调整,一经申请,容量已有_capacity确定,因此称为静态空间管策略。一方面,既然容量固定,总有可能在此后的某一时刻,无法加入更多的新元素——导致发生上溢。另一方面,无法提前预知需要多少预留空间来解决上溢问题。取_size/_capacity作为装填因子。
上述问题即可描述为:
- 如何才能保证向量的装填因子既不致于超过1,也不致于太接近于0
3.5扩容
template<typename T>
void vector<T>::expand()
{
if (_size < _capacity) return;//成员未满时,不必扩容
if (_capacity < DEFAULT_CAPACITY) _capacity = DEFAULT_CAPACITY;//不低于最小容量
T* old = _elem; _elem = new T[_capacity <<= 1];//容量加倍
for (int i = 0; i < _size; i++)//一趟迭代
_elem[i] = old[i];
delete [] old;
}
这里的关键在于,新数组的容量总是取作原数组的两倍——这正是上述问题的解。
3.5.1扩容策略
如果不选取加倍容量策略,凭借一般的思路,具体分析。
取两种策略:
- 1.每次追加固定空间
- 2.每次追加双倍空间
采取第一种扩容策略,假定每次追加C个单元,于是,只要每隔固定的常数K次操作就发生一次扩容,则初始容量为n0的向量经过连续的N(>>K)次操作后,容量将增加至:
n=n0+C(N/K)
总时间为:
T(N)=(n0+C)+(n0+2C)+….+(n0+C(N/K))=n0*(N/K)+C(N/K)(N/K-1)
均摊至单次操作:
T(N)/N=n0/K+(C/K)(N/K-1)=n/K-C/K=O(n)
考察初始容量为0的向量,假设每次追加一个单元,则扩容需要的次数恰好为几何级数,采用均摊分析,共需O(n)时间,换句话说,这种情况的确有可能发生。
而采用第二张扩容策略,每次追加双倍空间,假定数组的初始容量为某一常数n0——即将溢出。经过连续的
N(>>K)次操作后,容量将增加至:
n=2^N*n0
T(N)=n0+2n0+4n0+…+2^N*n0=n0*(2^N-1)
而所需扩容的次数,也成以2的公比的等比数列增长。故
T(n)=2N+4N+8N+…+capacity(n)<2*capacity(n)=O(n)
T(n)/n=O(1)
也就是说采用第二种扩容方式需要O(1)的时间。
3.5.2装填因子
从装填因子的角度考虑,每次加倍扩容,装填因子不会到达100%也不至低于50%。
3.6判等器与比较器
template<typename T>static bool lt(T* a,T* b){return lt(*a,*b);}
template<typename T>static bool lt(T& a,T& b){return a<b;}
template<typename T>static bool mt(T& a,T& b){return a>b;}
template<typename T>static bool equals(T* a,T* b){return equals(*a,*b);}
template<typename T>static bool equals(T& a,T& b){return a==b;}
3.7无序查找
template<typename T>
Rank vector<T>::find(T const & e, Rank lo, Rank hi) const
{
if(IsValid(lo,0,_size)&&IsValid(hi,0,_size)){
while ((lo < hi--) && (e != _elem[hi]));
return hi;
}
}
由后向前顺序查找(秩必须合法),直到发现与之相等者(即e),则查找成功。对于多个元素命中时,返回秩最大者。查找失败时,将越过边界lo,统一返回lo-1即-1。
3.8循秩访问
template<typename T>
T & vector<T>::operator[](Rank r)
{
return _elem[r];
}
重载后的[]返回的是数组元素的引用,可以取代get()和set()操作。