【C++】数据结构——向量
向量
- ADT接口
- Vector模板类
- 初始化函数
- 运算符重载
- 动态空间管理
- 逆序与置乱
- 排序
- 插入与查找
- 有序向量查找
- 删除
- 去重
- 遍历(批量)操作
参考书籍:《数据结构(C++语言版)》邓俊辉著
向量(vector)是最基本的线性数据结构——序列(sequence)中的一种。向量中。所有数据项的物理存放位置与其逻辑次序一致,其逻辑次序被称作秩(rank)。
C++中已经有实现向量的库<vector>,我们不使用该库,而是仿照它实现一种基于向量的抽象数据类型(Abstract Data Type,ADT)。
ADT接口
下面列出需要实现的接口:
操作接口 | 功能 | 适用对象 |
---|---|---|
size ( ) | 返回向量规模 | 向量 |
get ( r ) | 获取秩为r的元素 | 向量 |
put ( r, e ) | 用e替换秩为r的元素 | 向量 |
insert ( r, e ) | 在秩为r的位置插入元素e | 向量 |
remove ( r ) | 删除秩为r的对象并返回 | 向量 |
disordered ( ) | 判断是否逆序,返回逆序数 | 向量 |
sort ( ) | 非降序重新排列 | 向量 |
find ( e ) | 查找目标元素,返回最大的秩 | 向量 |
search ( e ) | 查找目标元素,返回最大的秩 | 有序向量 |
deduplicate ( ) | 删除重复元素 | 向量 |
uniquify ( ) | 删除重复元素 | 有序向量 |
traverse ( ) | 遍历向量并统一处理 | 向量 |
Vector模板类
下面定义Vector模板类:
typedef int Rank;
#define DEFAULT_CAPACITY 3template <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); //冒泡排序的基本操作,扫描交换
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; } //删除数组//只读接口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 (_size <= 0) ? -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的元素Rank insert(Rank r, T const &e); //在秩为r的位置插入元素eRank insert(T const &e) { return insert(_size, e); } //在末尾插入元素evoid 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 &); //使用函数对象操作
};
对于只读的接口,尽量将其声明为const类型。
初始化函数
由于向量的特性,我们选择数组这一结构作为向量类的基本元素单元,因为数组在内存中的物理地址与其逻辑次序一致。
构造函数在类声明中已经内联的实现了,下面实现内部的复制函数:
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++]; //逐个复制}
}
根据模板参数T的不同,向量模板会自动请求合适的内存。
运算符重载
我们目前只需要重载=赋值操作符和[]取元素操作符:
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];
}
动态空间管理
在可用空间足够小时扩容,在剩余空间足够大时缩容,一般发生在向量修改前后:
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; //释放原空间}
}
扩容两倍的空间,这是有理论依据的。当发生足够多次的插入操作时,每次将容量扩大一倍的平摊时间成本最低,为O(1)。
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; //释放原空间}
}
装填因子 = _size / _capacity
,我们选择25%作为装填因子的下限。
逆序与置乱
在必要时,可以将向量重新随机排序(需要先引入<random>库):
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>
int Vector<T>::disordered() const
{int n = 0;for (int i = 1; i < _size; i++)if (_elem[i - 1] > _elem[i])n++;return n;
}
排序
排序算法是向量无序转有序的关键,该类算法多种多样,这里目前只实现比较简单的冒泡排序:
template <typename T>
void Vector<T>::sort(Rank lo, Rank hi)
{while (!bubble(lo, hi--)); //当返回sorted=true时,停止扫描
}
下面我们实现扫描交换的算法:
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; //判断说明下次扫描可能还会出现逆序std::swap(_elem[lo - 1], _elem[lo]);}return sorted; //只有本次扫描没有出现逆序,则下次一定不会出现逆序,可终止扫描
}
插入与查找
插入单个元素,届时检查容量是否支持该次插入操作,不支持时需要扩容:
template <typename T>
Rank Vector<T>::insert(Rank r, T const &e)
{expand(); //若需要,先扩容for (int i = _size; i > r;)_elem[i] = _elem[i - 1]; //整体后移一位,从后向前_elem[r] = e;_size++;return 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;
}
有序向量查找
二分查找是查找有序序列常见的思路,对于分切点的选取,我们给出两种策略:
- 区间中点作为切割点(三分支,比较两次):
template <typename T>
static Rank binSearch(T *A, T const &e, Rank lo, Rank hi)
{while (lo < hi){Rank mi = (lo + hi) >> 1; //取区间中点if (A[mi] > e)hi = mi; //若中点值大于目标值,转移至前驱区间[lo, mi)else if (A[mi] < e)lo = mi + 1; //若中点值小于目标值,转移至后继区间(mi, hi)elsereturn mi; //若中点值等于目标值,返回中点的秩}return -1;
}
对该算法可行性的检验可以通过列举一些退化的例子,如只有三个元素:<、<、=或<、<、>,以及=、>、>和<、>、>。
需要注意的是,最后一个元素实际不参与比较或根本不存在,只是一个哨兵元素。
- 斐波拉契(黄金分割)分段点:
二分查找本质上是一种“减而治之”的查找策略,它并不约束查找中点(mi)的位置。我们可以按照黄金分割比来确定mi的位置:
设向量长度为 n = fib(k) - 1
算法的复杂度基本仅来源于中值的比较,这两种查找方式都是O(logn)级的时间复杂度。但准确来说,以黄金分割率确立的查找策略比前者优于一个常数级。
由于是三分支的查找,至少需要设置两次比较,若目标项在向量首段,则每次迭代需要进行一次比较;若在末段,则需比较两次(因为转向后继区间的代码位于else if
,想要转向后继区间,必须先执行if
段的代码,这是无法避免的)。
相比于等分策略,黄金分割策略加长了前驱区间(只需一次比较)的长度,即减少了需比较两次的情况,因此理论上性能更优。当然我们也有其他提升性能的方法,比如改进成两分支。
删除
区间批量删除:
template <typename T>
int Vector<T>::remove(Rank lo, Rank hi)
{if (lo == hi)return 0;while (hi < _size)_elem[lo++] = _elem[hi++]; //整体前移,若删除区间大于其后缀区间,未覆盖部分不做处理,下次缩容时自动消除_size = lo; //确定新界限shrink(); //若装填因子过小,缩容return hi - lo; //返回删除元素的个数
}
单个删除操作:
template <typename T>
T Vector<T>::remove(Rank r)
{T re_elem = _elem[r]; //备份将被删除的元素remove(r, r + 1);return re_elem;
}
我们将区间批量删除作为单个删除的子操作,因为批量删除操作相比执行多次单个删除操作所需的时间成本更低。
去重
对于无序向量,只能逐次检查,每次删除一个重复元素:
template <typename T>
int Vector<T>::deduplicate() //无序向量去重
{int oldSize = _size; //记录原规模Rank i = 1;while (i < _size) //从前向后依次检查,保证每次最多检查到一个重复元素(find(_elem[i], 0, i) < 0) ? i++ : remove(i); //若查找到,删除该元素并检查其后继元素return oldSize - _size;
}
从前向后逐渐扩大查重范围。
对于有序向量,我们有更方便的去重方式,因为元素都是依次排好的:
template <typename T>
int Vector<T>::uniquify() //有序向量去重
{Rank i = 0, j = 1;while (j < _size)(_elem[i] == _elem[j]) ? j++ : _elem[++i] = _elem[j++]; //查询到首个不重复元素并将其赋值到i的后继位置_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]); //使用传入的函数对象处理
}
我们可以自定义函数指针和函数对象来执行批量的任务。
【C++】数据结构——向量相关推荐
- R语言的四种数据结构---向量
R中常用的数据结构有四种:向量.矩阵.数据库和列表 (1)基本操作 1.向量的创建 c()函数创建向量:seq()函数创建等差数列的向量:sample()从一列数字中随机抽取几个数:paste0()把 ...
- r 字符串转化为数值_【R语言】数据结构Ⅰ—向量,矩阵,数组
数据结构是为了便于存储不同类型的数据而设计的. R中常用的数据结构包括: 同质数据类型(homogeneous data types),即所存储的一定是相同类型的元素,包括向量.矩阵.数组: 异质数据 ...
- r语言逻辑向量相加_R语言基础教程——第3章:数据结构——向量
如果学过像JAVA或者C这样的高级语言,都知道,数据类型的概念,包括,整数型.浮点型.字符串.布尔类型.这些语言中,定义变量需要定义数据类型,而在R中不需要.只需要直接赋值即可.在给变量赋值时,R中可 ...
- R语言数据结构之向量
R总共是6中数据结构:向量.因子.矩阵.数组.数据框.列表.向量用于存储数值型.字符型或逻辑型数据的一维数组,是R中的最小单元.向量构成的基本元素为:数值(numeric).字符(character) ...
- 数据结构与算法之-----向量(Vector)
[ 写在前面的话:本专栏的主要内容:数据结构与算法. 1.对于初识数据结构的小伙伴们,鉴于后面的数据结构的构建会使用到专栏前面的内容,包括具体数据结构的应用,所使用到的数据结构,也是自己 ...
- R语言与数据分析(11)R语言的数据结构
数据结构 数据结构是计算机存储.组织数据的方式,数据结构是指相互之间存在一种或多种特定关系的数据元素的集合 R中的数据类型 1.数值型 数值可以用于直接结算,加减乘除 2.字符串型 可以进行连接.转换 ...
- r语言x c(-1 -2),【软件】R语言入门之向量
"R语言入门开篇,向量(vector)相关知识的介绍" R语言是一款优秀统计学编程语言,本文介绍R语言的几个重要命令,以及R语言中非常重要的一种数据结构-向量(Vector)的相关 ...
- 机器学习中的Numpy库基础——向量、矩阵和数组
1.Numpy简介 Numpy是Python机器学习技术栈的基础. Numpy可以对机器学习中常用的数据结构--向量(vector).矩阵(matrice).张量(tensor)--进行高效的操作. ...
- R数据分析——安装,数据类型转换,向量
R数据分析 R中安装与使用包 卸载与移除包 基本数据类型(六种) 数据类型转换 运算符 R中的数据结构 向量 R中安装与使用包 R中的包存储在计算机上名为library的目录下 ✓ 使用函数libPa ...
最新文章
- Visual Studio 2005 Team System: Demo Videos
- java php mysql_系统学习javaweb13----MYSQL学习(使用PHP、SQL)1
- (十)装饰器模式详解(与IO不解的情缘)
- IDEA中实用的快捷方式
- 认识VLAN,并学会VLAN的划分和网络配置实例
- 【干货下载】2020新基建展望:新战略、新动力、新格局.pdf(附下载链接)
- 机器学习实战——决策树:matplotlib绘图
- 全网首发:doubango提示text relocations错误的解决办法
- Notepad2添加到右键菜单栏
- 关于成型滤波器实现方式的简单比较
- 微信支付商户平台扫码登录说明
- Fake it till you make it: face analysis in the wild using synthetic data alone
- 【阿里云】云解析DNS
- jieba分词关键词抽取
- NIVIDIA 硬解码学习1
- 金色传说:SAP-BC-导出数据时不能选择导出文件的格式问题
- C#判断线段是否相交
- 腾讯云tca认证题库
- 停车场管理系统车牌识别中的无感支付有什么bug
- java实现手机验证码功能
热门文章
- C语言课程设计-实验室设备管理系统
- Like What You Like: Knowledge Distill via Neuron Selectivity Transfer 论文翻译
- 2018阿里巴巴基础平台研发工程师实习生笔试题:一.十进制数转换成的二进制数中有几个1. 二.输出IP所在的网段
- 【java并发探赜索隐】判断标志、死锁、守护进程
- uni-app知识点整理
- 远程登陆没有固定ip的服务器
- CSS 列表样式 (ul)
- html中如何把一张图片分块,神奇图片分割软件有哪些分割模式 图片分割器如何检验能否无缝拼图...
- Practice II 字符串
- 判断TTS语音朗读是否结束