归并排序(C++ mpi 并行实现)

文章目录

  • 主要思路
  • 1. 串行归并排序
  • 2. 进程的分发
  • 3. 对接收到的子数组进行排序
  • 4. 合并数组
  • 5.输出排序后的数组
  • 6.进程分发部分的优化
  • 7.完整代码

主要思路

  1. 我们首先实现串行的归并排序;
  2. 实现进程的分发;
  3. 排序其中的每个子部分;
  4. 进程的合并通信,并实现对有序子数组的归并(注意,这里的合并复杂度应该是O(n)的,不然并行就失去了意义)。

通过以上4步,就可以实现并行的归并排序了。

1. 串行归并排序

// 归并排序,输入一个vector的引用
void mergeSort(vector<int>& vec) {if (vec.size() <= 1) return;int mid = vec.size() / 2;vector<int> left(vec.begin(), vec.begin() + mid);vector<int> right(vec.begin() + mid, vec.end());mergeSort(left);mergeSort(right);int i = 0, j = 0, k = 0;while (i < left.size() && j < right.size()) {if (left[i] < right[j]) {vec[k++] = left[i++];}else {vec[k++] = right[j++];}}while (i < left.size()) {vec[k++] = left[i++];}while (j < right.size()) {vec[k++] = right[j++];}
}

2. 进程的分发

进程如何分发?看下面这张图:

在这里插入图片描述

这里就需要计算数组的划分次数,到底划分多少份?每个进程一份。

我起初实现了一个很简单的做法,就是由 0进程 来划分,然后分发给其它进程。

    if (myrank == 0) {// 计算每个进程分配完,多余出来的数量int remain = numberNums % processNum;// 主进程 排序 自己的 + 多余出来的subVec = vector<int>(vec.begin(), vec.begin() + n + remain);for (int i = 1; i < processNum; i++) {MPI_Send(vec.data() + i * n + remain, n,MPI_INT, i, 0, MPI_COMM_WORLD);}}else {// 接收数组MPI_Recv(subVec.data(), n, MPI_INT,0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);}

其中的 numberNums 表示数组大小,processNum 表示进程数量。

值得注意的是,如果数组没有被进程刚好划分,也就是有余数(remain),我是这样处理的,让 0进程 来多排序一下,最终划分结果即:

  • 0进程 :排序 n+remain 个;
  • 其它进程 :排序 n 个。

3. 对接收到的子数组进行排序

这一步比较简单,对于每一个进程都是相同的,即:排序(干活)。

    // 对接收到的子数组进行排序    mergeSort(subVec);

4. 合并数组

数组的合并的主要思路如下:

在这里插入图片描述

就是两两进行合并,注意,这里不是一个进程合并所有其它的,那样的时间复杂度是 O ( n 2 ) O(n^2) O(n2),即合并操作是在一个进程中完成的。

我们需要让合并的操作再多个进程中完成,这样时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn):这里没有考虑进程通信的成本,也没有考虑的必要)

我们首先需要计算出合并的次数,合并的次数是由进程的数量决定的。

如果有 8 个进程,那么我们两两合并,需要合并 3 = l o g 2 ( 8 ) 3=log_2(8) 3=log2(8) 次。

那么如果有 7 个进程呢?合并过程如下图所示:

在这里插入图片描述

树的层数(合并的)是没有发生变化的。

如果有 6 个进程,合并过程如下图:

在这里插入图片描述

树的层数(合并次数)同样没有发生变化。

因此我们的合并次数应该为进程数对2的对数,然后向上取整就不难理解了。

m e r g e T i m e s = c e i l ( l o g 2 ( p r o c e s s N u m ) ) mergeTimes=ceil(log_2(processNum)) mergeTimes=ceil(log2(processNum))

具体代码如下:

    // 合并数组// 计算合并的次数int mergeTimes = ceil(log2(processNum));for (int i = 0; i < mergeTimes; i++) {// 判断当前进程 在 第 i 个轮次 是否需要接收数据if (myrank % int(pow(2, i + 1)) == 0) {// 计算当前进程的下一个进程int nextProcess = myrank + pow(2, i);// 如果下一个进程存在,那么就接收下一个进程的数据if (nextProcess < processNum) {// 接收数组的大小int vecNum;MPI_Recv(&vecNum, 1, MPI_INT, nextProcess, 0,MPI_COMM_WORLD, MPI_STATUS_IGNORE);// 接收数组vector<int> recvVec(vecNum, 0);MPI_Recv(recvVec.data(), vecNum, MPI_INT,nextProcess, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);// 合并数组subVec = merge(subVec, recvVec);}}// 判断当前进程 在 第 i 个轮次 是否需要发送数据给前边的进程if ((myrank + int(pow(2,i))) % int(pow(2, i + 1)) == 0) {// 计算上一个进程int preProcess = myrank - pow(2, i);// 发送数组的大小int vecNum = subVec.size();MPI_Send(&vecNum, 1, MPI_INT, preProcess,0, MPI_COMM_WORLD);// 发送数组MPI_Send(subVec.data(), subVec.size(),MPI_INT, preProcess, 0, MPI_COMM_WORLD);}}

关于两个 if 条件判断的理解,主要是两两合并的思想。

1 2合并,3 4合并,5 6合并 7 8合并;
1 3 合并,5 7合并;
1 5合并。

这样合并 3 次就可以了,至于判断到底为什么这样写?主要利用了进程号合并轮次两个参数控制的。

读者需要自己想一下,然后再根据代码看一下,进行理解。

其中,merge函数部分的代码主要功能是,将两个有序数组合并成一个,代码如下:

// vec1 和 vec2 是两个有序数组,将其合并为一个有序数组
vector<int> merge(vector<int> vec1, vector<int> vec2) {int i = 0, j = 0;vector<int> res;while(i < vec1.size() && j < vec2.size()) {if (vec1[i] < vec2[j]) {res.push_back(vec1[i++]);}else {res.push_back(vec2[j++]);}}while (i < vec1.size()) {res.push_back(vec1[i++]);}while (j < vec2.size()) {res.push_back(vec2[j++]);}return res;
}

5.输出排序后的数组

最后输出排序后的数组,具体代码如下:

    // 输出排序后的数组if (myrank == 0) {// 计算排序时间cout << "time=" << (MPI_Wtime() - startTime) * 1000 << " ms" << endl;// 输出排序后的数组for (int i = 0; i < subVec.size(); i++) {cout << subVec[i] << " ";}cout << endl;}

6.进程分发部分的优化

我们是采用主进程进行数组分发的,一个进程进行数组的分发,时间复杂度为 O ( n ) O(n) O(n),相对较低。

另一种方法是,多个进程同时进行分发,时间复杂度为 O ( l o g n ) O(logn) O(logn)

主要思想如下:

在这里插入图片描述

编程的思路就是,首先接收数据。

然后计算需要划分的轮次。

根据轮次和进程号,得到每个进程需要发送消息的进程的进程号。

具体代码如下:

    // 计算划分次数int splitTimes = ceil(log2(processNum));if (myrank != 0) {// 计算当前进程的上一个进程int preProcess = getPreProcess(myrank);// 接收数组的大小int vecNum;MPI_Recv(&vecNum, 1, MPI_INT, preProcess,0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);cout << "process=" << myrank << " receive " << vecNum << " numbers from " << preProcess << endl;// 接收数组subVec = vector<int>(vecNum, 0);MPI_Recv(subVec.data(), vecNum, MPI_INT, preProcess,0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);}for (int i = splitTimes; i > 0; i--) {// 初始化 0 号进程的子数组if (myrank == 0 && i == splitTimes)subVec = vector<int>(vec.begin(), vec.end());// 如果当前进程是 2 的 i 次方的倍数,那么就发送数据if (myrank % int(pow(2, i)) == 0) {// 计算当前进程的下一个进程int nextProcess = myrank + pow(2, i - 1);// 如果下一个进程存在,那么就发送数据给下一个进程if (nextProcess < processNum) {// 发送数组的大小int vecNum = subVec.size() - subVec.size() / 2;cout<<"process="<<myrank<<" send "<<vecNum<<" numbers to "<<nextProcess<<endl;MPI_Send(&vecNum, 1, MPI_INT, nextProcess,0, MPI_COMM_WORLD);// 发送数组// subVec = vector<int>(subVec.begin() + subVec.size() / 2, subVec.end());MPI_Send(subVec.data() + subVec.size() / 2, vecNum,MPI_INT, nextProcess, 0, MPI_COMM_WORLD);subVec = vector<int>(subVec.begin(), subVec.begin() + subVec.size() / 2);}}}

代码思路也有些绕,其中还涉及到一个 int getPreProcess(int curProcessNum) 函数,该函数主要用来求解给当前进程发送信息的进程的进程号,我们得到了之后好去接收它。

主要就是找比当前进程数小的,2的幂次方的累加和,但注意是要贪心去找,比如:

7->4+2
6->4
5->4
4->0
3->2
2->0
1->0

具体代码如下:

int getPreProcess(int curProcessNum) {int tmp;int sum = 0;while (sum < curProcessNum) {tmp = 2;bool flag = false;while (sum + tmp < curProcessNum) {tmp *= 2;}if (tmp == 2 || sum + tmp == curProcessNum) {return sum;}else {sum += tmp / 2;}}
}

说的还是很绕。

还是下面这张图,每个进程只接收一次消息,但是可能会发送多次消息。

在这里插入图片描述

因此,我们上面可以得到这个进程接收消息的进程号。

然后再去求它发送消息的进程号就可以了,具体还是看代码。

因为划分并不是该并行算法的性能瓶颈,最大的运算量还是在各个子进程的 mergeSort函数 以及 merge函数 上面。

所以划分这块性能看上去并没有过于明显的差别。下面是我按照两种划分方法,排序100万数据量的数组,所耗费的时间(单位:ms)对比:

在这里插入图片描述

可以看到单个进程划分效率看起来更高一些。具体的原因,我推测可能是 getPreProcess函数 的时间复杂度较大导致的。

7.完整代码

#include<vector>
#include<iostream>
#include<mpi.h>using namespace std;
// 归并排序,输入一个vector的引用
void mergeSort(vector<int>& vec) {if (vec.size() <= 1) return;int mid = vec.size() / 2;vector<int> left(vec.begin(), vec.begin() + mid);vector<int> right(vec.begin() + mid, vec.end());mergeSort(left);mergeSort(right);int i = 0, j = 0, k = 0;while (i < left.size() && j < right.size()) {if (left[i] < right[j]) {vec[k++] = left[i++];}else {vec[k++] = right[j++];}}while (i < left.size()) {vec[k++] = left[i++];}while (j < right.size()) {vec[k++] = right[j++];}
}// vec1 和 vec2 是两个有序数组,将其合并为一个有序数组
vector<int> merge(vector<int> vec1, vector<int> vec2) {int i = 0, j = 0;vector<int> res;while(i < vec1.size() && j < vec2.size()) {if (vec1[i] < vec2[j]) {res.push_back(vec1[i++]);}else {res.push_back(vec2[j++]);}}while (i < vec1.size()) {res.push_back(vec1[i++]);}while (j < vec2.size()) {res.push_back(vec2[j++]);}return res;
}int getPreProcess(int curProcessNum) {int tmp;int sum = 0;while (sum < curProcessNum) {tmp = 2;bool flag = false;while (sum + tmp < curProcessNum) {tmp *= 2;}if (tmp == 2 || sum + tmp == curProcessNum) {return sum;}else {sum += tmp / 2;}}
}double startTime;int main(int argc, char* argv[]) {int myrank, processNum;char processor_name[MPI_MAX_PROCESSOR_NAME];int namelen;MPI_Init(&argc, &argv);// 当前进程的编号MPI_Comm_rank(MPI_COMM_WORLD, &myrank);// 进程总数MPI_Comm_size(MPI_COMM_WORLD, &processNum);// 数据的数量int numberNums = 10;vector<int> vec;// 初始化数据if (myrank == 0) {srand(time(NULL));// 随机生成10个数for (int i = 0; i < numberNums; i++) {vec.push_back(rand() % 100);}// 记录开始时间startTime = MPI_Wtime();}// 每个进程排序的数量int n = numberNums / processNum;// 分出来的子数字vector<int> subVec(n, 0);// 如果是主进程,那么就进行子数组的分发//if (myrank == 0) {//    // 计算每个进程分配完,多余出来的数量//    int remain = numberNums % processNum;//    // 主进程 排序 自己的 + 多余出来的//    subVec = vector<int>(vec.begin(), vec.begin() + n + remain);//    for (int i = 1; i < processNum; i++) {//        MPI_Send(vec.data() + i * n + remain, n,//            MPI_INT, i, 0, MPI_COMM_WORLD);//    }//}//else {//    // 接收数组//    MPI_Recv(subVec.data(), n, MPI_INT,//        0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);//}// 计算划分次数int splitTimes = ceil(log2(processNum));if (myrank != 0) {// 计算当前进程的上一个进程int preProcess = getPreProcess(myrank);// 接收数组的大小int vecNum;MPI_Recv(&vecNum, 1, MPI_INT, preProcess,0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);cout << "process=" << myrank << " receive " << vecNum << " numbers from " << preProcess << endl;// 接收数组subVec = vector<int>(vecNum, 0);MPI_Recv(subVec.data(), vecNum, MPI_INT, preProcess,0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);}for (int i = splitTimes; i > 0; i--) {// 初始化 0 号进程的子数组if (myrank == 0 && i == splitTimes)subVec = vector<int>(vec.begin(), vec.end());// 如果当前进程是 2 的 i 次方的倍数,那么就发送数据if (myrank % int(pow(2, i)) == 0) {// 计算当前进程的下一个进程int nextProcess = myrank + pow(2, i - 1);// 如果下一个进程存在,那么就发送数据给下一个进程if (nextProcess < processNum) {// 发送数组的大小int vecNum = subVec.size() - subVec.size() / 2;cout<<"process="<<myrank<<" send "<<vecNum<<" numbers to "<<nextProcess<<endl;MPI_Send(&vecNum, 1, MPI_INT, nextProcess,0, MPI_COMM_WORLD);// 发送数组// subVec = vector<int>(subVec.begin() + subVec.size() / 2, subVec.end());MPI_Send(subVec.data() + subVec.size() / 2, vecNum,MPI_INT, nextProcess, 0, MPI_COMM_WORLD);subVec = vector<int>(subVec.begin(), subVec.begin() + subVec.size() / 2);}}}// 对接收到的子数组进行排序    mergeSort(subVec);// 合并数组// 计算合并的次数int mergeTimes = ceil(log2(processNum));for (int i = 0; i < mergeTimes; i++) {// 判断当前进程 在 第 i 个轮次 是否需要接收数据if (myrank % int(pow(2, i + 1)) == 0) {// 计算当前进程的下一个进程int nextProcess = myrank + pow(2, i);// 如果下一个进程存在,那么就接收下一个进程的数据if (nextProcess < processNum) {// 接收数组的大小int vecNum;MPI_Recv(&vecNum, 1, MPI_INT, nextProcess, 0,MPI_COMM_WORLD, MPI_STATUS_IGNORE);// 接收数组vector<int> recvVec(vecNum, 0);MPI_Recv(recvVec.data(), vecNum, MPI_INT,nextProcess, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);// 合并数组subVec = merge(subVec, recvVec);}}// 判断当前进程 在 第 i 个轮次 是否需要发送数据给前边的进程if ((myrank + int(pow(2,i))) % int(pow(2, i + 1)) == 0) {// 计算上一个进程int preProcess = myrank - pow(2, i);// 发送数组的大小int vecNum = subVec.size();MPI_Send(&vecNum, 1, MPI_INT, preProcess,0, MPI_COMM_WORLD);// 发送数组MPI_Send(subVec.data(), subVec.size(),MPI_INT, preProcess, 0, MPI_COMM_WORLD);}}// 输出排序后的数组if (myrank == 0) {// 计算排序时间cout << "time=" << (MPI_Wtime() - startTime) * 1000 << " ms" << endl;// 输出排序后的数组for (int i = 0; i < subVec.size(); i++) {cout << subVec[i] << " ";}cout << endl;}MPI_Finalize();return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/41778.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

理解 Go 中的切片:append 操作的深入分析(篇2)

理解 Go 语言中 slice 的性质对于编程非常有益。下面&#xff0c;我将通过代码示例来解释切片在不同函数之间传递并执行 append 操作时的具体表现。 本篇为第 2 篇&#xff0c;当切片的容量 cap 不够时 func main() {// slice1 当前长度为 3&#xff0c;容量大小也为 3slice1 :…

.netcore grpc的proto文件字段详解

一、.proto文件字段概述 grpc的接口传输参数都是根据.proto文件约定的字段格式进行传输的grpc提供了多种类型字段&#xff1b;主要包括标量值类型&#xff08;基础类型&#xff09;、日期时间、可为null类型、字节、列表、字典、Any类型&#xff08;任意类型&#xff09;、One…

前端笔试+面试分享

以下是个人线下面试遇到的真实的题&#xff0c;仅供参考和学习 1. css 选择符有哪些&#xff1f;哪些属性可以继承&#xff1f;优先级算法加何计算&#xff1f; CSS选择符有很多种&#xff0c;例如类型选择器、类选择器、ID选择器、属性选择器、伪类选择器、伪元素选择器等。 …

Algorithem Review 5.2 图论

网络流 设源点为 s s s&#xff0c;汇点为 t t t&#xff0c;每条边 e e e 的流量上限为 c ( e ) c(e) c(e)&#xff0c;流量为 f ( e ) f(e) f(e)。割 指对于某一顶点集合 P ⊂ V P \subset V P⊂V&#xff0c;从 P P P 出发指向 P P P 外部的那些原图中的边的集合&a…

回归预测 | MATLAB实现基于SSA-KELM-Adaboost麻雀算法优化核极限学习机结合AdaBoost多输入单输出回归预测

回归预测 | MATLAB实现基于SSA-KELM-Adaboost麻雀算法优化核极限学习机结合AdaBoost多输入单输出回归预测 目录 回归预测 | MATLAB实现基于SSA-KELM-Adaboost麻雀算法优化核极限学习机结合AdaBoost多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本…

SSH远程连接MacOS catalina并进行终端颜色配置

一、开关SSH服务 在虚拟机上安装了MacOS catalina&#xff0c;想要使用SSH远程进行连接&#xff0c;但是使用“系统偏好设置”/“共享”/“远程登录”开关进行打开&#xff0c;却一直是正在启动“远程登录”&#xff1a; 难道是catalina有BUG&#xff1f;不过还是有方法的&…

第07天 Static关键字作用及用法

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a;每天一个知识点 ✨特色专栏&#xff1a…

【前端|Javascript第5篇】全网最详细的JS的内置对象文章!

前言 在当今数字时代&#xff0c;前端技术正日益成为塑造用户体验的关键。我们在开发中需要用到很多js的内置对象的一些属性来帮助我们更快速的进行开发。或许你是刚踏入前端领域的小白&#xff0c;或者是希望深入了解内置对象的开发者&#xff0c;不论你的经验如何&#xff0c…

MATLAB中的代数环概念

在 Simulink 模型中&#xff0c;当存在信号环并且信号环中只存在直接馈通模块时&#xff0c;将出现代数环。直接馈通表示 Simulink 需要模块输入信号的值来计算当前时间步的输出。这种信号循环会在同一时间步中产生模块输出和输入的循环依存关系。这会导致一个需要在每个时间步…

【【verilog典型电路设计之流水线结构】】

verilog典型电路设计之流水线结构 下图是一个4位的乘法器结构&#xff0c;用verilog HDL 设计一个两级流水线加法器树4位乘法器 对于流水线结构 其实需要做的是在每级之间增加一个暂存的数据用来存储 我们得到的东西 我们一般来说会通过在每一级之间插入D触发器来保证数据的联…

OpenCV-Python中的图像处理-图像特征

OpenCV-Python中的图像处理-图像特征 图像特征Harris角点检测亚像素级精度的角点检测Shi-Tomasi角点检测SIFT(Scale-Invariant Feature Transfrom)SURF(Speeded-Up Robust Features)FAST算法BRIEF(Binary Robust Independent Elementary Features)算法ORB (Oriented FAST and R…

python编程中有哪些方便的调试方法

大家好&#xff0c;给大家分享一下一个有趣的事情&#xff0c;很多人还不知道这一点。下面详细解释一下。现在让我们来看看&#xff01; 对于每个程序开发者来说&#xff0c;调试几乎是必备技能。常用Pycharm编辑器里的方法有Print大法、log大法&#xff0c;但缺少类似Matlab的…

怎么开通Tik Tok海外娱乐公会呢?

TikTok作为全球知名的社交媒体平台&#xff0c;吸引了数亿用户的关注和参与。许多公司和个人渴望通过开通TikTok直播公会进入这一领域&#xff0c;以展示自己的创造力和吸引更多粉丝。然而&#xff0c;成为TikTok直播公会并非易事&#xff0c;需要满足一定的门槛和申请找cmxyci…

【日常积累】Linux之init系统学习

init系统简介: Linux 操作系统的启动首先从 BIOS 开始&#xff0c;接下来进入 boot loader&#xff0c;由 bootloader 载入内核&#xff0c;进行内核初始化。内核初始化的最后一步就是启动 pid 为 1 的 init 进程&#xff0c;这个进程是系统的第一个进程&#xff0c;它负责产生…

银河麒麟服务器v10 sp1 .Net6.0 上传文件错误

上一篇&#xff1a;银河麒麟服务器v10 sp1 部署.Net6.0 http https_csdn_aspnet的博客-CSDN博客 .NET 6之前&#xff0c;在Linux服务器上安装 libgdiplus 即可解决&#xff0c;libgdiplus是System.Drawing.Common原生端跨平台实现的主要提供者&#xff0c;是开源mono项目。地址…

ubuntu 部署 ChatGLM-6B 完整流程 模型量化 Nvidia

ubuntu 部署 ChatGLM-6B 完整流程 模型量化 Nvidia 初环境与设备环境准备克隆模型代码部署 ChatGLM-6B完整代码 ChatGLM-6B 是一个开源的、支持中英双语的对话语言模型&#xff0c;基于 General Language Model (GLM) 架构&#xff0c;具有 62 亿参数。结合模型量化技术&#x…

力扣 322. 零钱兑换

题目来源&#xff1a;https://leetcode.cn/problems/coin-change/description/ C题解&#xff08;来源代码随想录&#xff09;&#xff1a;题目中说每种硬币的数量是无限的&#xff0c;可以看出是典型的完全背包问题。动规五部曲分析如下&#xff1a; 确定dp数组以及下标的含义…

原码、反码、补码,进制转换,有符号数和无符号数转换

计算机底层存储数据时&#xff0c;存储的是数据对应的二进制数字。对于整型数据&#xff0c;其二进制表示形式有三种&#xff0c;分别是&#xff1a;原码、反码、补码&#xff0c;而实际存储的是整型数据的补码。 原码、反码以及补码都是有符号的&#xff0c;其中最高位存放符…

带你掌握Stable Diffution商业级玩法

课程介绍 学习地址 《Stable Diffusion商业级玩法》通过详细讲解AI绘画技巧、实操演示和个性化指导&#xff0c;帮助您从零基础成为绘画高手&#xff0c;帮助您有效推广产品或服务&#xff0c;提升市场份额。教您掌握稳定扩散绘画技巧&#xff0c;开启艺术创作新篇章。

Opencv 之ORB特征提取与匹配API简介及使用例程

Opencv 之ORB特征提取与匹配API简介及使用例程 ORB因其速度较快常被用于视觉SLAM中的位姿估计、视觉里程、图像处理中的特征提取与匹配及图像拼接等领域本文将详细给出使用例程及实现效果展示 1. API 简介 创建 static Ptr<ORB> cv::ORB::create (int nfeatures 500…