Author Archives: fangfang12138

数据结构学习4——图

1 图的结构

1.1 图的逻辑结构

  1. 图(graph)由顶点(vertex)的非空集合和顶点之间的边(edge)的集合组成。
无向(undirected)边、有向边
无向图、有向图、稠密(dense)图、稀疏(sparse)图 、子图(subgraph)
简单图 不存在顶点到自身的边,不存在重复出现的边
完全图 任意两顶点存在边,无向完全图(数量为 (n-1)n/2 )、有向完全图(双向 数量为 (n-1)n )
邻接与依附 无向:顶点互为邻接(adjacent)点,边依附(adhere)于两顶点;有向:起点邻接到终点,终点是起点的邻接点
无向:依附于顶点的边的数量;有向:出度(作为起点)、入度(作为终点)
权值与网 权值(weight):边的意义;网:带权值的图(有向网、无向网)
路径 路径、路径长度、回路(路径起终点相同)、简单路径(无重复顶点)、简单回路(只重复起终点)
连通图 连通分量 无向图中,任意两顶点存在路径;非连通图的极大连通子图为连通分量
强连通图 强连通分量 有向图中,任意两顶点双向存在路径;非强连通图的极大强连通子图为强连通分量
生成树 连通图的极小连通子图(n个顶点和最少的边)、有向图的子图(全部顶点且仅一顶点入度为0,其余入度为1)
生成森林 连通分量的生成树,组成森林
  1. 遍历的方式
    ♥ 寻找邻接点
    ♥ 设置访问标志,避免重复访问
    ♥ 多个邻接点选择方式不同,分为深度优先(推荐使用栈)和广度优先(推荐使用队列)

1.2 图的存储结构

邻接矩阵(栅格)

  1. 深度优先遍历
    //1、初始化各顶点未被访问
        for(int i=0;i<vertexNum;i++)
            visited[i]=0;
    //2、第一个顶点入栈
        stack<int> st;
        for(int i=0;i<vertexNum;i++)
            if(visited[i]==0)
            {
                st.push(i);     
                DFS(st.top());
                st.pop();
            }
    //3、递归
        void DFS(int v)
        {
            visited[v]=1;               //确认已遍历
            for(int i=0;i<vertexNum;i++)
            {
                if((visited[i]==0) && (arc[v][i]==1))//找到该顶点的邻接点
                {
                    st.push(i);
                    DFS(st.top());
                    st.pop();
                }
            }
        }
  2. 广度优先遍历
    //1、初始化各顶点未被访问
        for(int i=0;i<vertexNum;i++)
            visited[i]=0;
    //2、第一个顶点入队
        queue<int> que;
        for(int i=0;i<vertexNum;i++)
            if(visited[i]==0)
            {
                que.push(i);
                visited[i]=1;
                break;
            }
    //3、循环:依次入队,依次处理
        while(!que.empty()) 
        {
            int v = que.front();
            for(int i=0;i<vertexNum;i++)
            {
                if((visited[i]==0) && (arc[v][i]==1))//找到该顶点的邻接点
                {
                    que.push(i);
                    visited[i]=1;
                }
            }
            que.pop();
        }

1.3 测试程序

邻接矩阵 深度优先遍历

针对无向连通图。

#include <iostream>
#include <stack>

using namespace std;

int vertexNum=5;
int visited[5]={0};
int arc[5][5]={0,1,0,1,1,   //邻接矩阵
               1,0,1,0,1,
               0,1,0,0,0,
               1,0,0,0,0,
               1,1,0,0,0};
stack<int> st;

void DFS(int v)
{
    visited[v]=1;               //确认已遍历
    for(int i=0;i<vertexNum;i++)
    {
        if((visited[i]==0) && (arc[v][i]==1))//找到该顶点的邻接点
        {
            st.push(i);
            cout<<"push:"<<i<<" "<<st.size()<<endl; 
            DFS(st.top());
            st.pop();
            cout<<"pop:"<<i<<" "<<st.size()<<endl;
        }
    }
}

int main()
{
    //1、初始化各顶点未被访问
        for(int i=0;i<vertexNum;i++)
            visited[i]=0;
    //2、第一个顶点入栈
        for(int i=0;i<vertexNum;i++)
        {
            if(visited[i]==0)
            {
                st.push(i); 
                cout<<"push:"<<i<<" "<<st.size()<<endl; 
                DFS(st.top());
                st.pop();
                cout<<"pop:"<<i<<" "<<st.size()<<endl;  
            }
        }

}

邻接矩阵 广度优先遍历

针对无向连通图。

#include <iostream>
#include <queue>

using namespace std;

int vertexNum=5;
int visited[5]={0};
int arc[5][5]={0,1,0,1,1,   //邻接矩阵
               1,0,1,0,1,
               0,1,0,0,0,
               1,0,0,0,0,
               1,1,0,0,0};

int main()
{
    //1、初始化各顶点未被访问
        for(int i=0;i<vertexNum;i++)
            visited[i]=0;
    //2、第一个顶点入队
        queue<int> que;
        for(int i=0;i<vertexNum;i++)
            if(visited[i]==0)
            {
                que.push(i);
                visited[i]=1;
                cout<<"push:"<<i<<" "<<que.size()<<endl;
                break;
            }

    //3、循环:依次入队,依次处理
        while(!que.empty()) 
        {
            int v = que.front();
            cout<<"front:"<<que.front()<<" "<<que.size()<<endl; 

            for(int i=0;i<vertexNum;i++)
            {
                if((visited[i]==0) && (arc[v][i]==1))//找到该顶点的邻接点
                {
                    que.push(i);
                    visited[i]=1;
                    cout<<"push:"<<i<<" "<<que.size()<<endl;    
                }
            }
            que.pop();
        }        
}

2 最小生成树

2.1 最小生成树(MST,minimal spanning tree)

无向连通网的生成树中,权值代价最小的生成树,称为最小权重生成树,简称最小生成树
(无向图,连通图,权值与网,生成树)
应用:n个城市之间铺设电缆,目标使得任意两个城市互通,不同城市之间铺设电缆代价不同,求代价最小的铺设方法。这是一道面试题:(

2.2 Prim算法

基本思想

  1. 设无向连通网 G=(V,E) ,最小生成树 T=(U,TE)
  2. T 初值: U={v0}v0∈VTE 为空
  3. 重复:
    对每一个 V-U 中的顶点(设n个顶点),遍历 U 中顶点,找到一条最短边,n个顶点形成一个候选最短边集
    在候选最短边集中,找到最最短的一条边
    V-U 中的这一点,放入 U ,同时,把对应边放入 TE ,直到 VU 相等。
  4. 算法复杂度O(n2),适于稠密连通网。
  5. 关键:候选最短边集(基本过程的case和代码的shortEdge数组)

基本过程

在这里插入图片描述————————->在这里插入图片描述

  1. 初始状态
    在这里插入图片描述
  2. 将点 v5 和边 (v0,v5)2 加入最小生成树
    在这里插入图片描述
  3. 将点 v1 和边 (v5,v1)1 加入最小生成树
    在这里插入图片描述
  4. 将点 v2 和边 (v1,v2)2 加入最小生成树
    在这里插入图片描述
  5. 将点 v3 和边 (v5,v3)4 加入最小生成树
    在这里插入图片描述
  6. 将点 v4 和边 (v3,v4)1 加入最小生成树
    在这里插入图片描述

代码(非测试)

//1、初始化辅助数组,辅助数组用于标记 V-U中顶点 到 U中顶点 的 最短代价和对应顶点,功能类似于基本过程中的cost
for(int i=1;i<vertexNum;i++)
{
    //初始取v0加入集合U,因此最短代价依次是与v0的代价值,对应顶点为i和v0
    shortEdge[i].lowcost = arc[0][i];   //V-U中顶点 到 U中顶点 的最短代价
    shortEdge[i].adjvex = 0;            //V-U中顶点 到 U中顶点 的对应顶点
}

//2、加入集合的方式
shortEdge[0].lowcost = 0;               //代价为0时,认为改顶点已经在最小生成树中了,这里v0已经在其中了

//3、循环
for(int i=1;i<vertexNum;i++)            //这里是循环vertexNum-1次,每次添加一个顶点到U
{
    //3.1、根据辅助数组确定最最短边
    int k;                              //最最短边对应的 V-U中顶点
    for(int j=1;j<vertexNum;j++)        //初始化k
        if(shortEdge[j].lowcost != 0)
        { k=j; break; }
    for(int j=1;j<vertexNum;j++)        //更新k
        if(shortEdge[j].lowcost != 0 && shortEdge[j].lowcost < shortEdge[k].lowcost)
            k=j;

    //3.2、加入集合
    shortEdge[k].lowcost = 0;

    //3.3、更新辅助数组
    for(int j=1;j<vertexNum;j++)
        if(shortEdge[j].lowcost > arc[k][j])//比较新加入的顶点,是否代价更小(类似于RRT*的重选父节点)
        {
            shortEdge[j].lowcost = arc[k][j];   //V-U中顶点 到 U中顶点 的最短代价
            shortEdge[j].adjvex = k;            //V-U中顶点 到 U中顶点 的对应顶点
        }

}

2.3 Kruscal算法

基本思想

  1. 设无向连通网 G=(V,E) ,最小生成树 T=(U,TE)
  2. T 初值: U=VTE 为空,即每个顶点自成一个连通分量
  3. E 中权值有小到大排序
  4. 重复:
    E 中选择最短边,判断该边的两顶点是否在两个连通分量上——边集数组
    是:加入 TE
    否:舍弃此边
  5. 算法复杂度O(elbe),适于稀疏连通网。
  6. 关键:边集数组,如何判断两个点是否在不同的连通分量(利用边集数组)。

基本过程

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

代码(非测试)

//1、定义、初始化、排序从小到大的边集数组
struct Edge
{
    Edge(){}
    Edge(int from_,int to_,int cost_,bool ok_):from(from_),to(to_),cost(cost_),ok(ok_){}
    int from;
    int to;
    int cost;
    bool ok;
};

Edge edges[edgeNum];
edges[0]=Edge{0,1,4,false};//...

bool cmp(const Edge &a,const Edge &b)
    return a.cost<b.cost;
sort(edges,edges+edgeNum,cmp);//按权值排序

//2、遍历边集数组
for(int i=0;i<edgeNum;i++)
{
    //2.1、判断该边的两个顶点是否在一个连通分量中
    //2.2、合并两个连通分量
    if(find_parent(edges[i].from)!=find_parent(edges[i].to))
    {
        merge(edges[i].from, edges[i].to); 
        edges[i].ok=true;
    }
    else
        edges[i].ok=false;
}

2.4 测试程序

Prim算法

#include <iostream>

int vertexNum=6;
int arc[6][6]={100,4,100,100,5,2,   //邻接矩阵
               4,100,2,100,100,1,
               100,2,100,10,100,3,
               100,100,10,100,1,4,
               5,100,100,1,100,8,
               2,1,3,4,8,100};

struct Cost
{
    int lowcost;
    int adjvex;
};

int main()
{

    Cost shortEdge[vertexNum];

    //1、初始化辅助数组,辅助数组用于标记 V-U中顶点 到 U中顶点 的 最短代价和对应顶点,功能类似于基本过程中的cost
    for(int i=1;i<vertexNum;i++)
    {
        //初始取v0加入集合U,因此最短代价依次是与v0的代价值,对应顶点为i和v0
        shortEdge[i].lowcost = arc[0][i];               //V-U中顶点 到 U中顶点 的最短代价
        shortEdge[i].adjvex = 0;                        //V-U中顶点 到 U中顶点 的对应顶点
    }

    //2、加入集合的方式
    shortEdge[0].lowcost = 0;                           //代价为0时,认为改顶点已经在最小生成树中了,这里v0已经在其中了

    //3、循环
    for(int i=1;i<vertexNum;i++)                        //这里是循环vertexNum-1次,每次添加一个顶点到U
    {
        //3.1、根据辅助数组确定最最短边
        int k;                                          //最最短边对应的 V-U中顶点
        for(int j=1;j<vertexNum;j++)                    //初始化k
            if(shortEdge[j].lowcost != 0)
            { 
                k=j; break; 
            }
        for(int j=1;j<vertexNum;j++)                    //更新k
            if(shortEdge[j].lowcost != 0 && shortEdge[j].lowcost < shortEdge[k].lowcost)
                k=j;

        std::cout<<"get vertex="<<k<<" with "<<shortEdge[k].adjvex<<",cost="<<shortEdge[k].lowcost<<std::endl;
        //3.2、加入集合
        shortEdge[k].lowcost = 0;

        //3.3、更新辅助数组
        for(int j=1;j<vertexNum;j++)
            if(shortEdge[j].lowcost > arc[k][j])        //比较新加入的顶点,是否代价更小(类似于RRT*的重选父节点)
            {
                shortEdge[j].lowcost = arc[k][j];       //V-U中顶点 到 U中顶点 的最短代价
                shortEdge[j].adjvex = k;                //V-U中顶点 到 U中顶点 的对应顶点
            }

    }

    return 0;

}

Kruscal算法

#include <iostream>
#include <algorithm>

int vertexNum=6;
int edgeNum=10;
int arc[6][6]={100,4,100,100,5,2,   //邻接矩阵
               4,100,2,100,100,1,
               100,2,100,10,100,3,
               100,100,10,100,1,4,
               5,100,100,1,100,8,
               2,1,3,4,8,100};
int parent[6];

//边集数组
struct Edge
{
    Edge(){}
    Edge(int from_,int to_,int cost_,bool ok_):from(from_),to(to_),cost(cost_),ok(ok_){}
    int from;
    int to;
    int cost;
    bool ok;
};

//边集数组:成员排序方式
bool cmp(const Edge &a,const Edge &b)
{
    return (a.cost) < (b.cost);
}

//幷查集:查找顶层双亲
int find_parent(int v)
{
    return parent[v]==v ? v : find_parent(parent[v]);
}

//幷查集:修改顶层双亲
void merge(int from,int to)
{
    parent[find_parent(to)]=find_parent(from);
}

int main()
{
//1、定义、初始化、排序从小到大的边集数组
    Edge edges[edgeNum];
    edges[0]=Edge{0,1,4,false};
    edges[1]=Edge{0,4,5,false};
    edges[2]=Edge{0,5,2,false};
    edges[3]=Edge{1,2,2,false};
    edges[4]=Edge{1,5,1,false};
    edges[5]=Edge{2,3,10,false};
    edges[6]=Edge{2,5,3,false};
    edges[7]=Edge{3,4,1,false};
    edges[8]=Edge{3,5,4,false};
    edges[9]=Edge{4,5,8,false};

    std::sort(edges,edges+edgeNum,cmp);//按权值排序

//2、初始化每个顶点的顶层双亲节点,用于判断是否在一个集合
    for(int i=0;i<vertexNum;i++)
        parent[i]=i;

//3、遍历边集数组
    for(int i=0;i<edgeNum;i++)
    {
        //3.1、判断该边的两个顶点是否在一个连通分量中
        //3.2、合并两个连通分量
        if(find_parent(edges[i].from)!=find_parent(edges[i].to))
        {
            merge(edges[i].from, edges[i].to); 
            edges[i].ok=true;
        }
        else
            edges[i].ok=false;

        std::cout<<"get edge: from "<<edges[i].from<<" to "<<edges[i].to
                 <<" cost:"<<edges[i].cost<<" ok:"<<edges[i].ok<<std::endl;
        for(int i=0;i<vertexNum;i++)
            std::cout<<i<<" parent:"<<find_parent(i)<<std::endl;
    }

    return 0;
}

3 最短路径问题

3.1 Dijkstra算法

  1. 移步这里https://blog.csdn.net/fangfang12138/article/details/107949923

数据结构学习3——树

1 树的结构

1.1 树的逻辑结构

在这里插入图片描述
1) 树的定义与特点

说明
子树、空树、有序树、无序树、森林 空树无结点,有序无序看子树交换顺序是不是同一棵
叶子结点、分支结点、根结点、孩子结点、双亲结点、兄弟结点、堂兄弟结点
结点的度、树的度 度即分叉数,最大的结点的度即树的度
路径长度、祖先、子孙 分支数为路径长度,路径若存在必唯一
结点的层数、树的深度/高度 层数从1开始
结点的层序编号 从上到下、从左到右、从1开始、连续自然数编号

2) 树的遍历
前序遍历:从根结点,从左到右前序遍历子树,ABEFCGHID
后序遍历:从叶子结点,从左到右后序遍历子树,EFBHIGCDA
层序遍历:按层序编号,ABCDEFGHI

1.2 树的存储结构

1) 双亲表示法:每个结点,除自身数据外,还记录双亲结点的下标,方便求解双亲,但求结点的孩子需要遍历。
在这里插入图片描述

2) 孩子链表表示法:每个结点,除自身数据外,还记录一个链表,链表记录从左到右孩子结点的下标,优缺点有双亲表示法相反
在这里插入图片描述

3) 孩子兄弟表示法:每个结点,除自身数据外,还记录第一个孩子结点和右侧的第一个兄弟结点
在这里插入图片描述

1.3 二叉树的逻辑结构

在这里插入图片描述

1) 二叉树的定义与说明

说明
二叉树 左子树、右子树、空二叉树 左右子树互不相交,结点的度最大为2
斜树 左斜树、右斜树 所有分支都只有左子树/右子树,斜树 =》结点个数等于树的深度
满二叉树 所有分支结点都有左右子树,所有叶子结点都在同一层
完全二叉树 按层序编号,倒序从满二叉树中去掉任意数量结点,满二叉树=》完全二叉树

2) 二叉树的性质
3) 二叉树的遍历
前序遍历:从根结点,从左到右前序遍历左右子树,ABDGECF
中序遍历:先遍历根结点的左子树,访问根结点,遍历右子树,DGBEACF ,在遍历子树的过程中,也是按照:先左,然后结点,然后右,(NDGBEANCF)
后序遍历:从叶子结点,从左到右后序遍历左右子树,GDEBFCA
层序遍历:按层序编号,ABCDEFG

1.4 二叉树的存储结构(递归遍历)

1) 顺序存储结构
根据层序编号,使用一维数组存储。适用于完全二叉树,对于非完全二叉树,需要补充空结点生成完全二叉树,存在空间浪费的情况,特别是右斜树。
2) 二叉链表
对比树结构的孩子链表示法,结点使用二叉链表,每个结点包括数据域 和 左右两个指针域指向左右分支结点。
(二叉链表的遍历、构造和析构都可以递归的方式进行,以结点指针为空作为结束标志)
(二叉链表的构造:将二叉树进行扩展,使得原二叉树的结点的度都是2,然后再递归创建)

pLeftChild data pRightChild

在这里插入图片描述

template <class T>
struct BiNode{
    T data;       
    BiNode<T> *lchild, *rchild;
};

//构造
template <class T>
BiNode<T> *BiTree<T>::Creat(BiNode<T> *bt) 
{
    char ch;
    cout<<"请输入创建一棵二叉树的结点数据:"<<endl;
    cin>>ch;
    /*'#'代表空二叉树*/
    if(ch == '#') 
        return NULL;
    else 
    { 
        bt = new BiNode<T>; /*生成新结点*/
        bt->data = ch;
        bt->lchild = Creat(bt->lchild); /*递归创建左子树*/
        bt->rchild = Creat(bt->rchild); /*递归创建右子树*/
    } 
    return bt;
}

//析构
template <class T>
void BiTree<T>::Release(BiNode<T> *bt) 
{
    /*按照后序遍历的顺序释放二叉树*/
    if(bt != NULL) 
    {                  
        Release(bt->lchild); /*释放左子树*/
        Release(bt->rchild); /*释放右子树*/
        delete bt; /*删除根结点*/
    }  
}

//前序遍历
template <class T>
void BiTree<T>::PreOrder(BiNode<T> *bt) 
{
    if(bt == NULL) /*递归调用的边界条件*/
        return;
    else 
    {       
        cout<<bt->data<<" "; /*访问根结点*/
        PreOrder(bt->lchild); /*前序递归遍历左子树*/
        PreOrder(bt->rchild); /*前序递归遍历右子树*/
    }
}

//中序遍历
template <class T>
void BiTree<T>::InOrder(BiNode<T> *bt) 
{
    if(bt == NULL)  
        return; /*递归调用的边界条件*/             
    else 
    {   
        InOrder(bt->lchild); /*中序递归遍历左子树*/
        cout<<bt->data<<" "; /*访问根结点*/
        InOrder(bt->rchild); /*中序递归遍历的右子树*/
    }
}

//后序遍历
template <class T>
void BiTree<T>::PostOrder(BiNode<T> *bt) 
{
    if(bt == NULL)  
        return; /*递归调用的边界条件*/
    else 
    {   
        PostOrder(bt->lchild); /*后序递归遍历左子树*/
        PostOrder(bt->rchild); /*后序递归遍历右子树*/
        cout<<bt->data<<" "; /*访问根结点*/
    }
}

//层序遍历
template <class T>
void BiTree<T>::LevelOrder(BiNode<T> *bt)
{
    BiNode<T> *q;
    queue<BiNode<T> *> Q;
    if(bt == NULL) 
        return;
    else 
    {
        Q.push(bt); /*bt入队*/
        /*队列非空时循环*/
        while(Q.empty() != 1) 
        {
            q = Q.pop(); /*队头出队*/
            cout<<q->data<<" "; /*访问队头*/    
            if(q->lchild != NULL) /*如果队头有左孩子,左孩子入队*/
                Q.push(q->lchild);      
            if(q->rchild != NULL) /*如果队头有右孩子,右孩子入队*/
                Q.push(q->rchild);
        }
    }
}

3) 三叉链表
在二叉链表的基础上,增加指向双亲结点的指针。

pLeftChild data pRightChild pParent

在这里插入图片描述

2 二叉树应用

2.1 非递归遍历二叉树

//前序
template <class T>
void BiTree<T>::preOrder(BiNode<T> *bt) 
{
    stack<BiNode<T> *> S;//使用栈保存访问过的结点
    while(bt != NULL || S.empty() != 1)
    {
        while(bt != NULL)
        {
            cout<<bt->data<<endl;
            S.push(bt);
            bt = bt->lchild;
        }
        if(S.empty() != 1)
        {
            bt = S.pop();
            bt = bt->rchild;
        }
    }
}

//中序
template <class T>
void BiTree<T>::inOrder(BiNode<T> *bt) 
{
    stack<BiNode<T> *> S;//使用栈保存访问过的结点
    while(bt != NULL || S.empty() != 1)
    {
        while(bt != NULL)   //bt == NULL 说明左子树为空,或左子树已访问完
        {
            S.push(bt);
            bt = bt->lchild;
        }
        if(S.empty() != 1)
        {
            bt = S.pop();
            cout<<bt->data<<endl;//先打印左子树,再打印结点
            bt = bt->rchild;
        }
    }
}

//后序1:使用标志位,给结点绑定一个flag,访问完左子树,flag为1,左右子树都访问完后,flag为2
template <class T>
void BiTree<T>::postOrder(BiNode<T> *bt) 
{
    stack<Element> S;
    Element e;
    /*bt不为空或者栈不为空*/
    while(bt != NULL || S.Empty() == 0) 
    {
        while(bt != NULL) 
        {
            e.ptr = bt;
            e.flag = 1;
            S.posh(e);
            bt = bt->lchild;
        }
        /*栈不为空并且栈顶的flag为2时,出栈并访问*/
        while((S.empty() != 1)&&(S.top()).flag == 2) 
        {
            e = S.Pop();
            cout<<e.ptr->data<<" ";
        }
        /*栈不为空,并且栈顶的flag为1时,将栈顶的flag更改为2,并访问其右孩子*/
        if(S.empty() != 1) 
        {
            e = S.pop();
            bt = e.ptr->rchild;
            e.flag = 2;
            S.push(e);
        }
    }
}

//后序2:不使用标志位
template <class T>
void BiTree<T>::postOrder(BiNode<T> *bt) 
{
    stack<BiNode<T> *> s;
    stack<BiNode<T> *> output;
    s.push(bt);

    while (!s.empty()) 
    {
        bt = s.pop();
        output.push(bt);

        if (bt->lchild)
            s.push(bt->lchild);
        if (bt->rchild)
            s.push(bt->rchild);
    }

    while (!output.empty()) 
    {
        bt = output.pop();
        cout << bt->data << " ";
    }
}

2.2 重建二叉树

//由前序序列和中序序列
template <class T>
BiNode<T>* BiTree<T>::Rebuild(T *preOrder, T *inOrder, int n) 
{
    if(n == 0) 
        return NULL;

    T c = preOrder[0];/*获得前序遍历的第一个结点*/ 
    BiNode<T> *node = new BiNode<T >;/*创建根结点*/
    node->data = c;
    node->lchild = NULL;
    node->rchild = NULL;

    int i;
    for(i = 0; i < n && inOrder[i] != c; i++)/*在中序遍历序列中寻找根结点的位置*/
        ;

    int lenLeft = i;/*左子树结点个数*/ 
    int lenRight = n - i - 1; /*右子树结点个数*/

    if(lenLeft > 0)/*左子树不为空,递归重建左子树*/ 
        node->lchild = Rebuild(&preOrder[1], &inOrder[0], lenLeft);

    if(lenRight > 0) /*右子树不为空,递归重建右子树*/
        node->rchild = Rebuild(&preOrder[lenLeft+1], &inOrder[lenLeft+1], lenRight);

    return node;
}

//由后序序列和中序序列
template <class T>
BiNode<T>* BiTree<T>::Rebuild(T *postOrder, T *inOrder, int n) 
{
    if(n == 0) 
        return NULL;

    T c = postOrder[n-1];/*获得后序遍历的最后一个结点*/ 
    BiNode<T> *node = new BiNode<T>; /*创建根结点*/
    node->data = c;
    node->lchild = NULL;
    node->rchild = NULL;

    int i;
    for(i = 0; i < n && inOrder[i] != c; i++)/*在中序遍历序列中寻找根结点的位置*/
        ;

    int lenLeft = i;/*左子树结点个数*/
    int lenRight = n - i - 1; /*右子树结点个数*/

    if(lenLeft > 0)/*左子树不为空,重建左子树*/ 
        node->lchild = Rebuild(&postOrder[0], &inOrder[0], lenLeft);

    if(lenRight > 0)/*右子树不为空,重建右子树*/
        node->rchild = Rebuild(&postOrder[lenLeft], &inOrder[lenLeft+1], lenRight);
    return node;
}

3 二叉搜索树 / 堆 / 红黑树

系列参考

1) 《数据结构(C++)边学边做》任平红等著
2) 《剑指offer》何海涛著
3) http://bookshadow.com/weblog/2015/01/19/binary-tree-post-order-traversal/

数据结构学习2——线性表

1 顺序表

1) 定义顺序表模板类

const int MAXSIZE= 100; //顺序表最大容量
template <class T>
class SeqList
{
    public:
        SeqList() {length = 0;}             //构造函数:建立空顺序表
        SeqList(T d[], int len);            //构造函数:建立长度为len,数据为d[]的顺序表
        ~SeqList(){};                       //析构函数

        int GetLength(){return length};     //表长:获取表长
        T GetValue(int index);              //查找:根据索引获取对应数据
        int GetIndex(T x);                  //查找:根据值获得对应的索引
        void Insert(int index, T x);        //插入:在i位置,插入数据x
        void Delete(int index);             //删除:删除i位置数据
        void PrintList();                   //遍历

    private:
        int length;                         //表长
        T data[MAXSIZE];                    //存放列表元素(数组)
}

2) 成员函数实现

template <class T>
SeqList<T>::SeqList(T d[], int len) //构造函数:建立长度为len,数据为d[]的顺序表
{
    if(len<0 || len>MAXSIZE)
        throw "参数非法";

    length = len;

    for(int i=0;i<len;i++)
        data[i] = d[i];
}

template <class T>
T SeqList<T>::GetValue(int index)       //查找:根据索引获取对应数据
{
    if(index<0 || index>length-1)
        throw "参数非法";
    else
        return data[index];
}

template <class T>
int SeqList<T>::GetIndex(T x)           //查找:根据值获得对应的索引
{
    for(int i=0;i<length;i++)
        if(data[i] == x)
            return i

    return -1;
}

template <class T>
void SeqList<T>::Insert(int index, T x)//插入:在i位置,插入数据x
{
    if(length+1 > MAXSIZE)
        throw "列表已满,无法插入";

    if(index<0 || index>length)
        throw "参数非法";

    for(int i=length-1;i>index-1;i--)
            data[i+1] = data[i];        

    data[index] = x;
    length+=1;
    return;
}

template <class T>
void SeqList<T>::Delete(int index)      //删除:删除i位置数据
{
    if(0 == length)
        throw "列表为空,无法删除";

    if(index<0 || index>length-1)
        throw "参数非法";

    for(int i=index;i<length-1;i++)
            data[i] = data[i+1];

    length--;
}

template <class T>
void SeqList<T>::PrintList()            //遍历
{
    for(int i=0;i<length;i++)
        std::cout<<data[i]<<" ";
}

3) 有序表重复元素删除

template <class T>
void DelDup(SeqList<T> &L)
{
    for(int i=0;i<L.GetLength()-1;i++)
        while(L.GetValue(i)==L.GetValue(i+1))       //删除元素后,后续元素前移,因此依然是比较 i和i+1
            L.Delete(i+1);
}

4) 有序表合并

//不修改原表,假设合成的表有足够的空间容纳两个表的元素
template <class T>
void Merge(const SeqList<T> &L1, const SeqList<T> &L2,SeqList<T> &L3)
{
    int len1 = L1.GetLength();
    int len2 = L2.GetLength();
    int i=j=0;  //两个指针
    int k=0;
    while(i<len1 && j<len2)
    {
        if(L1[i] <= L2[j])
            L3.Insert(k++,L1[i++]);
        else
            L3.Insert(k++,L2[j++]);
    }
    while(i<len1)
        L3.Insert(k++,L1[i++]);

    while(j<len2)
        L3.Insert(k++,L2[j++]);

}

//在原表的基础上,添加,假设原表有足够的空间容纳添加的表
template <class T>
void Merge(SeqList<T> &L1, const SeqList<T> &L2)
{
    int len1 = L1.GetLength();
    int len2 = L2.GetLength();
    int i=len1-1;
    int j=len2-1;
    int k=len1+len2-1;

    while(i>-1 && j>-1)
    {
        if(L1[i]>L2[j])
            L1[k--]=L1[i--];
        else
            L1[k--]=L2[j--];        
    }

    while(j>-1)
        L1[k--]=L2[j--];
}

2 链表

链表与顺序表

1) 空间上,顺序表静态分配内存,结构紧凑,但需要预估内存大小,不够灵活, 另外空间连续性会导致存储碎片
2) 时间上,顺序表按位存取快,两者按值存取均为O(n);顺序表增删数据效率不高,链表的增删时间主要花费在指针移动

单链表

data next

表达:链表由结点构成,以头结点为链表起始,结点包括数据和下一结点的指针,头结点数据为null,尾指针为null(为使得空链表与非空链表中结点表达方式的统一,引入头结点)

1) 定义链表模板类

template <class T>
class LinkList
{
    public:
        LinkList();                         //构造函数:只有头结点的空单链表
        LinkList(T d[], int len);           //构造函数:以数组元素建立链表
        ~LinkList(){};                      //析构函数

        int GetLength();                    //表长
        T GetValue(int index);              //查找:根据索引获取对应数据
        int GetIndex(T x);                  //查找:根据值获得对应的索引
        void Insert(int index, T x);        //插入:在i位置,插入数据x
        void Delete(int index);             //删除:删除i位置数据
        void PrintList();                   //遍历

    private:
        Node<T>* first;     //头指针
}

2) 成员函数实现

表长/查找/插入/删除/构造/析构:遍历即工作指针的后移与非空的判断,在遍历的基础上计数、查找、删除,注意删除和析构中,先备份再delete

template <class T>
LinkList<T>::LinkList()             //构造函数:只有头结点的空单链表
{
    first = new Node<T>;            //生成头结点
    first->next = NULL;             //指向空值
}

template <class T>
LinkList<T>::LinkList(T d[], int len)//构造函数:以数组元素建立链表,这个算法的核心就是不断申请新的节点,申请后,再考虑数据域和指针域,最后考虑首尾的处理
{
    if(len<1)
        throw "参数非法";

    Node<T> *p,*q;
    first = new Node<T>;            //头结点,注意不是首元结点
    p = first;  

    for(int i=0;i<len;i++)
    {
        q= new Node<T>;             //新申请一个结点
        q->data = d[i];             //写入数据
        p->next = q;                //把地址给前一个结点的指针域
        p = q;                      //把当前节点更新成上一个节点
    }   
    p->next = NULL;                 //单链表的尾指针为空
}

template <class T>
LinkList<T>::~LinkList()            //析构函数
{
    Node<T> *q;
    while(first != NULL) 
    {
        q = first;
        first = first->next;
        delete q;
    }
}

template <class T>
void LinkList<T>::PrintList()       //遍历
{
    Node<T> *p = first->next;
    while(p != NULL)
    {
        cout<<p->data;
        p = p->next;
    }
}

template <class T>
int LinkList<T>::GetLength()        //表长
{
    int cnt = 0;
    Node<T> *p = first->next;
    while(p != NULL)
    {
        cnt++;
        p = p->next;
    }
    return cnt;
}

template <class T>
T LinkList<T>::GetValue(int index)  //查找:根据索引获取对应数据
{
    int i = 0;
    Node<T> *p = first->next;
    while(p!=NULL)
    {
        if(i == imdex)
            return p->data;

        i++;
        p = p->next;
    }

    throw "参数非法";
}

template <class T>
int LinkList<T>::GetIndex(T x)      //查找:根据值获得对应的索引
{
    int index = 0;
    Node<T> *p = first->next;
    while(p!=NULL)
    {
        if(p->data == x)
            return index;

        index++;
        p = p->next;
    }

    return -1;
}

template <class T>
void LinkList<T>::Insert(int index, T x)//插入:在i位置,插入数据x
{
    int i = 0;
    Node<T> *p = first->next;
    while(p!=NULL)
    {
        if((i+1) == index)      //指针定位到index结点的前一个结点
            break;

        i++;
        p = p->next;
    }

    if(p==NULL)
        throw "参数非法";

    Node<T> *q = new Node<T>;
    q->data = x;
    q->next = p->next;
    p->next = q;
}

template <class T>
void LinkList<T>::Delete(int index) //删除:删除i位置数据
{
    int i = 0;
    Node<T> *p = first->next;
    while(p!=NULL)
    {
        if((i+1) == index)      //指针定位到index结点的前一个结点
            break;

        i++;
        p = p->next;
    }

    if(p==NULL)
        throw "参数非法";

    Node<T> *q = p->next;   //备份待删除的结点的指针
    p->next = q->next;
    delete q;   
}

3) 单链表原地逆置

//接着在类里定义逆置函数
//另存当前点的后继,然后修改为前驱
template <class T>
void LinkList<T>::Reverse() //原地逆置
{
    Node<T> *pLast, *pCur, *pNext;
    pLast = first;          //上一个结点:初始化为头结点
    pCur = first->next;     //当前结点:初始化为首元结点
    pNext = NULL;

    while(pCur != NULL)
    {
        pNext = pCur->next;     //保存下一个结点的指针
        pCur->next = pLast;     //当前点把前驱作为自己的后继

        pLast = pCur;           //更细当前点和前一个点
        pCur = pNext;
    }
    first->next = pLast;
}   

4) 单链表有序性、排序、合并、删除重复

1) 有序性

template <class T>
int LinkList<T>::IsOrdering() 
{
    Node<T> *p,*q = NULL;
    p = first->next;
    int flag = 1;
    if(p) 
        q = p->next;

    /*比较相邻的结点是否逆序*/
    while((p != NULL) && (q != NULL)) 
    {
        if(p->data > q->data) 
        {
            flag = 0;
            break;
        }
        else
        {
            p = q;
            q = q->next;
        }
    }
    return flag;
}

2) 排序

//保留首元结点,从第二个结点开始依次前插
//每插入一个值,都从first开始遍历当前排好序的链表
template <class T>
void LinkList<T>::Sort() 
{
    Node<T> *p,*q,*r,*s;
    p = first->next;

    if(p != NULL) 
    {
        //在首元结点后断开
        q = p->next;
        p->next = NULL; 

        //依次将q插入到有序单链表中
        while(q)
        {
            r = q->next;    //保存后继
            s = first;      //从first开始遍历
            while((s->next) && ((s->next->data) < (q->data))) //符合顺序则s后移,逆序则退出循环,执行插入操作
                s = s->next;

            q->next = s->next;
            s->next = q;

            //q后移
            q = r; 
        }
    }    
}

3) 有序表合并

template <class T>
void Merge(LinkList<T> &L1, LinkList<T> &L2) 
{
    /*将单链表L1和L2合并至L1*/
    Node<T> *first1, *first2;
    first1 = L1.GetFirst(); /*获取单链表L1的头指针*/
    first2 = L2.GetFirst(); /*获取单链表L2的头指针*/ 
    Node<ElemType> *p,*q,*r;
    r = first1; /*单链表L1的尾指针*/
    p = first1->next;
    q = first2->next;
    while(p != NULL && q != NULL) 
    {
        if(p->data <= q->data) 
        {
            r->next = p;
            r = p;
            p = p->next;
        }
        else 
        {
            r->next = q;
            r = q;
            q = q->next;
        }
    }
    while(p != NULL) 
    {
            r->next = p;
            r = p;
            p = p->next;
    }
    while(q != NULL) 
    {
            r->next = q;
            r = q;
            q = q->next;
    }
    r->next = NULL;
}

4) 删除重复

//两个指针,比较,相等则删除后一个,删除之前要另存下下个结点
template <class T>
void LinkList<T>::LinkListDelDup() 
{
    Node<T> *p,*q,*r;
    p = first->next;
    while(p != NULL) 
    {
        q = p->next;
        while(q != NULL && p->data == q->data) 
        {
            /*删除q*/
            r = q->next;
            p->next = q->next;
            delete q;
            q = r;
        }
        p = p->next;
    }
}

5) 无序表删除重复

//对每个结点,比较后面的所有节点,只向后比较,不向前比较,O(n2)
template <class T>
void LinkList<T>::LinkListDelNormalDup() 
{
    Node<T> *p,*q,*r;
    p = first->next;
    while(p != NULL)
    {
        q = p;
        while(q->next != NULL) 
        {
            if(p->data == q->next->data) 
            {
                r = q->next;                
                q->next = r->next; 
                delete r;
            }
            else
                q = q->next;    
        }
        p = p->next;
    }
}

6) 循环链表:尾标志指向头结点

双向链表

prior data next

1) 插入: 指针修改顺序,未被处理的部分不断开
2) 删除
在这里插入图片描述

系列参考

1) 《数据结构(C++)边学边做》任平红等著
2) 《剑指offer》何海涛著

数据结构学习1——准备&时间复杂度

  数据结构 + 算法 = 程序

准备

1.1 数据结构的概念

在这里插入图片描述

1.2 算法分析

1、算法分析包括对算法的时间空间分析,一般更注重时间复杂度的分析。

2、算法分析包括对算法的事前事后分析,受软硬件环境和机器代码等的影响,通过设计算法来计算的准确时间一般不够客观,因而通过事前估计的方式得出时空复杂度。

1.2.1 时间复杂度的概念

1、时间复杂度
  采用事前估计,用基本语句执行次数的规模来表示算法的运行时间,以 T(n) 表示,同时,更关注随着问题规模 n 的增长,算法消耗时间的增长趋势,因而一般使用渐进时间复杂度,采用O 表示法

2、大O表示法
  设算法运行时间为T(n) ,若存在两个正的常数c和n0,对于任意的n≥n0,都有T(n) ≤ c×f(n),则称T(n) = O(f(n)),也称为函数T(n) 以 f(n)为上界。
  换句话说,实际运行时间不超过 f(n) 的c倍,使用f(n)近似,能够反映变化趋势。

3、时间复杂度的计算
  首先,计算基本语句的执行次数,如下,T(n) = 2n+2。

int sum = 0;            //  1
for(int i=0;i<n;i++)    //  1 + n
    sun+=i;             //  n

  然后, 忽略常量、低次幂和最高次幂的系数,得到f(n) = n,使用大O表示法,则T(n) = O(n)。

4、常见时间复杂度
  常见时间复杂度的比较如下,O(1)的时间复杂度与规模无关,耗时最短,2的指数幂以及耗时更长的算法一般不采用。
在这里插入图片描述
  常见时间复杂度的举例如下,

//O(1):以下语句与规模n无关,执行时间是一个常数,因而时间复杂度为1
    int i=1;
    i+=1;
    int j=1;
    j+=i;
//O(log(n))
    for(int i=1;i<n;i*=2)
        sum+=i;
//O(n)
    for(int i=1;i<n;i++)
        sum+=i;
//O(nlog(n))
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j*=2)
            sum=sum+i+j;    
//O(n2)
    for(int i=1;i<n;i++)
        for(int j=1;j<n;j++)
            sum=sum+i+j;    
//O(n3)
    for(int i=1;i<n;i++)
        for(int j=1;j<n;j++)
            for(int k=1;k<n;k++)
                sum=sum+i+j+k;

1.2.2 实战练习(不断补充…)

  递归计算x的n次方,时间复杂度O(log(n)):

int func(int x,int n)
{
    if(n==0)
        return 1;

    int t = func(x,n/2)
    if(n%2==1)
        return t*t*x;

    return t*t;
}

  调和级数,时间复杂度O(nlog(n))

for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j+=i)
        sum=sum+i+j;            // n + n/2 + n/3 + ... + n/n = n(1+ 1/2 + 1/3 + ... + 1/n) = n(ln(n+1)+r)

系列参考

1) 时间复杂度的计算
2) 递归算法的时间复杂度?
3) 《数据结构(C++)边学边做》任平红等著
4) 《剑指offer》何海涛著

C++_3——库

  这一系列文章的目的是在学习了C++基础后,继续补充一些C++基础和进阶的知识点,包括C++11的相关内容。
以C++11标准为基础。

C++网站:http://www.cplusplus.com/reference/

1 ctime

    //包含头文件
    #include <ctime>

    //获取系统时间
    clock_t time = clock();

    //获取系统时间对应的秒数
    int sec = time/CLOCKS_PER_SEC;

2 string

string value transform

    #include <string>

    int std::stoi(const string& str, size_t* idx = 0, int base = 10); 
    int std::stol(const string& str, size_t* idx = 0, int base = 10);
    int std::stoul(const string& str, size_t* idx = 0, int base = 10);
    int std::stoll(const string& str, size_t* idx = 0, int base = 10);
    int std::stoull(const string& str, size_t* idx = 0, int base = 10);
    int std::stof(const string& str, size_t* idx = 0);
    int std::stod(const string& str, size_t* idx = 0);
    int std::stold(const string& str, size_t* idx = 0); 

    // 以下方法 仅保留6位小数
    string std::to_string (int val);
    string std::to_string (long val);
    string std::to_string (long long val);
    string std::to_string (unsigned val);
    string std::to_string (unsigned long val);
    string std::to_string (unsigned long long val);
    string std::to_string (float val);
    string std::to_string (double val);
    string std::to_string (long double val);

Iterators operate

    // 正向迭代器
    iterator begin() noexcept;
    iterator end() noexcept;
    // 反向迭代器
    iterator begin() noexcept;
    iterator end() noexcept;
    // 常量正向迭代器
    const_iterator cbegin() noexcept;
    const_iterator cend() noexcept;
    // 常量反向迭代器
    const_iterator crbegin() noexcept;
    const_iterator crend() noexcept;

size & resize & empty & clear

    //
    size_t size() const noexcept;
    size_t length() const noexcept;

    void resize (size_t n);
    void resize (size_t n, char c); // 空闲位置用字符 c 补充

    void clear() noexcept;
    bool empty() const noexcept;

Element operate

    //返回引用,可作为左值修改
    char& operator[] (size_t pos);
    char& at (size_t pos);
    char& back();
    char& front();

swap & assign & insert & erase & replace

    void push_back (char c);
    void pop_back();            //删除最后一个字符
    void swap (string& str);    //交换
    //以下有较多重载函数
    string& append (const string& str);             //
    string& assign (const string& str);             //替换
    string& insert (size_t pos, const string& str); //插入
    string& erase (size_t pos = 0, size_t len = npos);
    string& replace (size_t pos, size_t len, const string& str);

copy & find & substr & compare

    const char* c_str() const noexcept;
    const char* data() const noexcept;
    size_t copy (char* s, size_t len, size_t pos = 0) const;

    // 一个字符 搜索第一次出现的位置find 和 搜索最后一次出现位置 rfind
    size_t find (const string& str, size_t pos = 0) const noexcept; //pos 是搜索起始位置
    size_t find (const char* s, size_t pos = 0) const;
    size_t rfind (const string& str, size_t pos = npos) const noexcept; 
    size_t rfind (const char* s, size_t pos = npos) const;

    // 字符串任意字符 搜索第一次的位置 和 搜索最后一次出现位置
    size_t find_first_of (const string& str, size_t pos = 0) const noexcept;
    size_t find_first_of (const char* s, size_t pos = 0) const;
    size_t find_last_of (const string& str, size_t pos = npos) const noexcept;
    size_t find_last_of (const char* s, size_t pos = npos) const;
    size_t find_first_not_of (const string& str, size_t pos = 0) const noexcept;
    size_t find_first_not_of (const char* s, size_t pos = 0) const;
    size_t find_last_not_of (const string& str, size_t pos = npos) const noexcept;
    size_t find_last_not_of (const char* s, size_t pos = npos) const;

    // 子串
    string substr (size_t pos = 0, size_t len = npos) const;

    //比较,重载较多,也可比较其中一部分字符串
    int compare (const string& str) const noexcept;

3 cmath

gamma函数

    double tgamma (     double x);
    float tgamma (      float x);
    long double tgamma (long double x);
    double tgamma (T x);           // additional overloads for integral types
  1. 举个栗子
    /* tgamma example */
    #include <stdio.h>      /* printf */
    #include <math.h>       /* tgamma */

    int main ()
    {
      double param, result;
      param = 0.5;
      result = tgamma (param);
      printf ("tgamma(%f) = %f\n", param, result );
      return 0;
    }
  1. 输出
    tgamma (0.500000) = 1.772454

取整

    // floor向下取整
    // ceil向上取整
    // round四舍五入
    // 返回的是double,不是int
        cout<<floor(4.4)<<endl;//4
        cout<<floor(4.5)<<endl;//4
        cout<<ceil(4.4)<<endl;//5
        cout<<ceil(4.5)<<endl;//5
        cout<<round(4.4)<<endl;//4
        cout<<round(4.5)<<endl;//5

4 random

正态分布

    std::default_random_engine generator;                       //定义引擎
    std::normal_distribution<double> distribution(均值,标准差);  //定义正态分布
    double number = distribution(generator);                    //产生分布值
  1. 举个栗子
    // normal_distribution
    #include <iostream>
    #include <string>
    #include <random>

    int main()
    {
      const int nrolls=10000;  // number of experiments
      const int nstars=100;    // maximum number of stars to distribute

      std::default_random_engine generator;
      std::normal_distribution<double> distribution(5.0,2.0);

      int p[10]={};

      for (int i=0; i<nrolls; ++i) {
        double number = distribution(generator);
        if ((number>=0.0)&&(number<10.0)) ++p[int(number)];
      }

      std::cout << "normal_distribution (5.0,2.0):" << std::endl;

      for (int i=0; i<10; ++i) {
        std::cout << i << "-" << (i+1) << ": ";
        std::cout << std::string(p[i]*nstars/nrolls,'*') << std::endl;
      }

      return 0;
    }
  1. 输出
    normal_distribution (5.0,2.0):
    0-1: *
    1-2: ****
    2-3: *********
    3-4: ***************
    4-5: ******************
    5-6: *******************
    6-7: ***************
    7-8: ********
    8-9: ****
    9-10: *

5 algorithm

sort & reverse & rotate

    // 排序原理:快速排序
    // 比较函数comp:小于为升序排列,大于为降序排列,不输入比较函数,默认为升序排列
    void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
    // e.g.
    bool myfunction (int i,int j) 
    { 
        return (i<j); 
    }

    std::sort (myvector.begin(), myvector.end(), myfunction);

    // 排序原理:middle之前的元素,为,按顺序,升序排列最小的n个元素,或降序排列最大的n个元素;middle之后没有特定顺序
    // 比较函数comp作用同上
    void partial_sort (RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last, Compare comp);

    // 倒序排列
    void reverse (BidirectionalIterator first, BidirectionalIterator last);

    // 旋转容器,以middle为第一个元素,之前元素,有序移动到尾部
    ForwardIterator rotate (ForwardIterator first, ForwardIterator middle, ForwardIterator last);
    //e.g.  1 2 3 4 5 6 7 8 9 ---> 4 5 6 7 8 9 1 2 3

min & max & min_element & max_element

    // 比较大小
    // 比较函数comp:返回值为 a是否小于b
    const T& min (const T& a, const T& b, Compare comp);
    // 比较函数comp:返回值为 a是否小于b,是的,还是“是否小于“
    const T& max (const T& a, const T& b, Compare comp);

    // 找最大/最小值的位置,返回的是指向该值的指针
    ForwardIterator min_element (ForwardIterator first, ForwardIterator last, Compare comp);
    ForwardIterator max_element (ForwardIterator first, ForwardIterator last, Compare comp);

merge

    // 合并两个排序序列
    // result 是合并到的容器的某个位置,一般是起始位置
    OutputIterator merge (InputIterator1 first1, InputIterator1 last1,
                          InputIterator2 first2, InputIterator2 last2,
                          OutputIterator result, Compare comp);

copy

    // 复制first到last范围的内容 到 起始位置result
    OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result);

    // 有选择地复制
    OutputIterator copy_if (InputIterator first, InputIterator last,
                            OutputIterator result, UnaryPredicate pred);
    //e.g.
    std::vector<int> foo = {25,15,5,-5,-15};
    std::vector<int> bar (foo.size());
    auto it = std::copy_if (foo.begin(), foo.end(), bar.begin(), [](int i){return !(i<0);} );

    // 倒着来
    BidirectionalIterator2 copy_backward (BidirectionalIterator1 first,
                                          BidirectionalIterator1 last,
                                          BidirectionalIterator2 result);

move & swap

    // 移动到result后,原位置的元素,处于未指定但有效状态
    OutputIterator move (InputIterator first, InputIterator last, OutputIterator result);
    BidirectionalIterator2 move_backward (BidirectionalIterator1 first,
                                          BidirectionalIterator1 last,
                                          BidirectionalIterator2 result);

    // C++11之后,swap交换基于move
    void swap (T& a, T& b);
    void swap (T (&a)[N], T (&b)[N]);
    //交换一定范围的值
    ForwardIterator2 swap_ranges (ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2);

replace & fill & generate & remove

    // 单值替换单值:将范围内的 old_value 替换成 new_value
    void replace (ForwardIterator first, ForwardIterator last, const T& old_value, const T& new_value);

    // 单值替换条件值(多值)
    void replace_if (ForwardIterator first, ForwardIterator last, UnaryPredicate pred, const T& new_value );

    // 范围内填充单值 val
    void fill (ForwardIterator first, ForwardIterator last, const T& val);
    OutputIterator fill_n (OutputIterator first, Size n, const T& val); //返回最后一个填充值的下一个位置(C++11)

    // 范围内填充生成值
    void generate (ForwardIterator first, ForwardIterator last, Generator gen);
    //e.g.
    // function generator:
    int RandomNumber () { return (std::rand()%100); }
    std::vector<int> myvector (8);
    std::generate (myvector.begin(), myvector.end(), RandomNumber);

    // 移除范围内的值
    // 坑:这是假移除,符合移除要求的值被移动到末尾,最后返回的位置指向 尾部 所有移除元素的第一个元素位置
    ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val);

find

    // 测试所有元素满足条件
    bool all_of (InputIterator first, InputIterator last, UnaryPredicate pred);
    // 测试有元素满足条件
    bool any_of (InputIterator first, InputIterator last, UnaryPredicate pred);
    // 测试所有元素不满足条件
    bool none_of (InputIterator first, InputIterator last, UnaryPredicate pred);
    // 对每个元素执行一次函数fn
    Function for_each (InputIterator first, InputIterator last, Function fn);

    // 找到元素第一次出现的位置
    InputIterator find (InputIterator first, InputIterator last, const T& val);
    // 按条件第一次出现的位置
    InputIterator find_if (InputIterator first, InputIterator last, UnaryPredicate pred);

    // 出现次数
    count (InputIterator first, InputIterator last, const T& val);
    count_if (InputIterator first, InputIterator last, UnaryPredicate pred);

    // 在1号范围内,第一次出现2号范围内元素的位置,  可自定义pred 判断是否符合匹配情况
    ForwardIterator1 search (ForwardIterator1 first1, ForwardIterator1 last1,
                             ForwardIterator2 first2, ForwardIterator2 last2,
                             BinaryPredicate pred);

6 thread & future

(新开一个博客写多线程…)

C++_2——thread & Linux多线程

线程的创建、资源回收

#include <thread>
// 构造函数
    // 默认构造
    thread() noexcept;
    // 显式构造:输入重载函数的函数指针,导致编译错误
    template <class Fn, class... Args>
    explicit thread (Fn&& fn, Args&&... args);
#include <thread>

void func1(){}
void func2(int arg){}
void func3(int arg1, int arg2){}

// 创建
std::thread t1(func1);
std::thread t2(func2, 10);
std::thread t3(func3, 10, 20);

// 资源回收
if(t1.joinable())
    t1.join();
if(t2.joinable())
    t2.join();
if(t3.joinable())
    t3.join();

// 线程后台运行,不再受程序控制
std::thread t4(func1);
t4.detach();

摘抄1
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。

摘抄2
是主线程等待子线程的终止。也就是说主线程的代码块中,如果碰到了t.join()方法,此时主线程需要等待(阻塞),等待子线程结束了(Waits for this thread to die.),才能继续执行t.join()之后的代码块。

摘抄3
detach调用之后,目标线程就成为了守护线程,驻留后台运行,与之关联的std::thread对象失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权。当目标线程主函数执行完之后,目标线程就结束了,运行时库负责清理与该线程相关的资源。
当一个thread对象到达生命期终点而关联线程还没有结束时,则thread对象取消与线程之间的关联,目标线程线程则变为分离线程继续运行。
detach使主线程不用等待子线程可以继续往下执行,但即使主线程终止了,子线程也不一定终止。
————————————————
版权声明:本文为CSDN博主「AllenSun-1990」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xikangsoon/article/details/104511675/

线程锁

c++之多线程中“锁”的基本用法

#include <thread>
#include <mutex>

/**************
    std::mutex
    对于非原子操作,多线程访问一个内存变量,任意时刻允许一个线程上锁
    易产生死锁,锁多了较为繁杂
*/
std::mutex mtx;         
mtx.lock()              //上锁失败则阻塞直到成功
mtx.try_lock()          //上锁失败则线程不阻塞
mtx.unlock()

/**************
    std::lock_guard
    构造时,调用传入对象的lock()
    析构时,调用传入对象的unlock()
    解决了死锁的问题
*/  
std::lock_guard<std::mutex> lg(mtx);        //此时调用mtx.lock()
                                            //当抛出异常或离开lg的作用域,lg被析构之后,调用mtx.unlock()

/**************
    std::uniqu_lock
    以std::lock_guard为基础,更为灵活
*/                                              
std::unique_lock<std::mutex> ul(mtx)
//提前解锁
ul.unlock()                             
//尝试锁,并判断是否锁住
std::uniqu_lock<std::mutex> ul(mtx.std::try_to_lock)    
ul.owns_lock    //bool量                                     

线程池

摘抄4
线程池(thread pool):一种线程的使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

本人太菜,写不出来,这里贴几个开源线程池:

  1. https://github.com/kanade2010/ThreadPool
    这个比较简单易懂,应对一般的问题应该够了。
  2. https://github.com/mtrebi/thread-pool
    这个功能比较强大,测试也比较严谨,就是看起来让人头皮发麻。

摘抄5
notify_one()与notify_all()常用来唤醒阻塞的线程。
notify_one():只唤醒等待队列中的第一个线程;不存在锁争用,所以能够立即获得锁。其余的线程不会被唤醒,需要等待再次调用notify_one()或者notify_all()。
notify_all():会唤醒所有等待队列中阻塞的线程,存在锁争用,只有一个线程能够获得锁。其余未获取锁的线程继续尝试获得锁(类似于轮询),而不会再次阻塞。当持有锁的线程释放锁时,这些线程中的一个会获得锁。而其余的会接着尝试获得锁。
————————————————
版权声明:本文为CSDN博主「吃素的施子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/feikudai8460/article/details/109604690

线程通信与同步

  1. 信号量:https://blog.csdn.net/ljianhui/article/details/10813469
  2. 互斥量:见线程锁部分
  3. 条件变量:condition_variable
    [c++11]多线程编程(六)——条件变量(Condition Variable)
  4. 读写锁:shared_lock
    C++实现读写锁ReadWriteLock
    C++11读写锁的实现 是上一篇的修改版
    C++多线程-读写锁 看文章总结部分

摘抄6
互斥量可以保护共享数据的修改,如果线程正在等待共享数据的某个条件出现,仅用互斥量的话就需要反复对互斥对象锁定解锁,以检查值的变化,这样将频繁查询的效率非常低。
条件变量可以让等待共享数据条件的线程进入休眠,并在条件达成时唤醒等待线程,提供一种更高效的线程同步方式。条件变量一般和互斥锁同时使用,提供一种更高效的线程同步方式。
————————————————
版权声明:本文为CSDN博主「低头看天,抬头走路」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/rusbme/article/details/97620877

摘抄7
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。读写锁可以专门处理这种多读少写的情况。
————————————————
版权声明:本文为CSDN博主「cwl_java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_42528266/article/details/103913191

线程安全

摘抄8
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程与进程

  1. 进程是系统进行任务调度和资源分配的最小单位,进程之间可以并发执行。
  2. 线程是程序执行流的最小单元,一个进程可以有多个线程,进程内的线程在其他进程不可见。
  3. 线程切换快于进程切换。
  4. 任务调度:时间片轮转,每个任务执行一个时间片的时间长度,时间片结束后,强制暂停,执行另一个时间片的程序,等到该任务的时间片再次到来。

进程间的通信方式

  1. 匿名管道:https://blog.csdn.net/ljianhui/article/details/10168031
    半双工,父子进程

    //创建
    #include <unistd.h>
    int pipe (int fd[2]);   //返回:成功返回0,出错返回-1    fd参数返回两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。
    //创建子进程
    pid_t id = fork(); 

在这里插入图片描述

  1. 消息队列:https://blog.csdn.net/ljianhui/article/details/10287879
  2. 有名管道:https://blog.csdn.net/ljianhui/article/details/10202699
  3. 信号量:https://blog.csdn.net/ljianhui/article/details/10243617
  4. 信号:https://blog.csdn.net/ljianhui/article/details/10128731
  5. 共享内存:https://blog.csdn.net/ljianhui/article/details/10253345
  6. 数据报套接字:https://blog.csdn.net/ljianhui/article/details/10697935
  7. 数据流套接字:https://blog.csdn.net/ljianhui/article/details/10477427

其他摘抄和参考

摘抄1
从C++11开始,标准库里已经包含了对线程的支持,std::thread是C++11标准库中的多线程的支持库,pthread.h是标准库没有添加多线程之前的在Linux上用的多线程库,而之前在windows上的多线程支持要包含wndows.h

  1. [c++11]多线程编程(一)——初识
  2. [c++11]多线程编程(二)——理解线程类的构造函数
  3. C++ thread用法总结(整理)
  4. Thread之三:Thread Join()的用法
  5. 基于C++11实现线程池的工作原理
  6. C++11条件变量:notify_one()与notify_all()的区别
  7. c++11 之emplace_back 与 push_back的区别
  8. 进程与线程
  9. 进程间的通信方式——pipe(管道)
  10. Linux进程间通信——使用消息队列

C++_1——基本数据类型

  这一系列文章的目的是在学习了C++基础后,继续补充一些C++基础和进阶的知识点,包括C++11的相关内容。

A 简单数据类型

a1 整型

  c++提供的基本整型包括char、short、int、long、long long(c++11)及其无符号版本,共十种。

 整型长度问题

  c++中,整型的长度不是固定的,取决于系统,基本情况如下面的表所示,可以使用sizeof()来确定具体系统中整型的长度。 整型变量 长度
short 至少16位
int 至少与short一样长
long 至少32位,且至少与int一样长
long long 至少64位,且至少与long一样长

  实际上,一个字节(bytes)未必是8位(bit),具体的取值取决于字符集的数目,比如ASCII这个字符集,8位即可表示所有字符集中的字符,因此是8位,而在Unicode中,可能会用到更大的字符集,8位就不够了。一般,在8位字节系统中,short为2字节,int和long为4字节,long long 为8字节。

 cout输出不同的进制

    std::cout<<std::hex;    //后续的数值以16进制显示
    std::cout<<std::oct;    //后续的数值以8进制显示

 整型常量

整型常量类型一般是int,除非有特定后缀或值过大。
  ① 后缀包括(u无符号,L长整型,LL及其组合),如23333L,23456uL。
  ② 值过大,对于十进制整型,会选择能装下该值的有符号最小类型,对于8/16进制整型,会选择能装下该值的无符号最小类型。

 char & wchar_t

1 字符型char:
  ① 8位,满足常见的ASCII或者EBCDIC字符集(Unicode的一个小子集),可用单引号行形式表示字符,可直接表示(‘A’)、或通过反斜杠进制数(‘\012’、‘\0xa’)。
  ② char既可能有符号,也可能无符号,取决于c++实现,但可以显式地指定。存储ASCII可用默认,因为ASCII只需要低七位,128个字符。

2 宽字符型wchar_t:
  ① 需求无法满足,位数需要多于8位,表示扩展字符集。
  ② 与之对应的是wcin和wcout,用于处理wchar_t流。
  ③ 显式表示宽字符常量或字符串常量,加前缀L,如表示宽字符P:L‘P’。

 ASCII、Unicode、ISO10646

  ① ASCII是Unicode子集。
  ② Unicode 为各种字符和符号提供标准数值编码,每个字符的编号称为码点
以\u开头 以\U开头
用8个十六进制位表示的通用编码名 用16个十六进制位表示的通用编码名

  ③ ISO10646和Unicode相似,都是通用字符编码,两者标准同步。

 C++11 新整型

1、char16_t / char32_t char16_t char32_t
无符号,长16位 无符号,长32位
前缀u 前缀U

2、size_t
  1)size_t在不同架构下的定义不同。
  2)32位系统下一般定义为unsigned int ,占4字节;64位系统下一般定义为unsigned long,占8字节。
  3)size_t方便移植程序,如计数等操作,保证能容纳实现所建立的最大对象的字节大小。

3、long long 和 unsigned long long

a2 浮点型

  ① 浮点型包括float、double、long double。
  ② 浮点型常量一般是double。
  ③ 指数范围一般是-37~37。
  ④ 浮点型的位数表示的是有效位数,float一般32位,double一般64位。

a3 类型转换

 整型提升

    1 如果有一个操作数的类型是 long double,则将另一个操作数转换为 long double。
    2 否则,如果有一个操作数的类型是 double,则将另一个操作数转换为 double。
    3 否则,如果有一个操作数的类型是 float,则将另一个操作数转换为 float。
    4 否则,说明操作数都是整型,因此执行整型提升。
    5 在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。
    6 如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型。
    7 否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型。
    8 否则,将两个操作数都转换为有符号类型的无符号版本。

 强制转换

//1 通用 
    (typename) value    
    typename (value)        
 //2 C++11
    static_cast<typename> (value)

a4 类型别名

C++允许给类型建立别名,建立方式有两种:

    #define aliasName typeName
    #define BYTE char

    typedef typeName aliasName;
    typedef char byte;

但前者只是简单替换,有时不适用,举例如下

    #define FLOAT_POINTER float*

    FLOAT_POINTER a,b;  //等价于 float* a,b; 这样b只是一个float

B 复合数据类型

b1 数组

1 数组全部初始化为零,有以下几种方式
  ① 显式全部初始化为0

    int array[3] = {0,0,0};

  ② 循环遍历每个元素初始化为0
  ③ 利用编译器的特点,显式地初始化第一个元素为0,编译器将其他元素设为0

    int array[3] = {0};

  ④ C++11新特性中,列表初始化可省略等号,当大括号内没有值时,所有元素将被初始化为0

    int array[3] = {};
    int array[3]{}; //作用同上

b2 字符串 C-string

这里讨论C-风格字符串,不讨论string类。

1 结尾空字符 \0
  ① 字符串常量结尾隐式添加 \0
  ② 字符串数组的sizeof()和strlen(),前者计算数组大小,后者只计算不包括空字符的字符串长度

    char str[10] = "abcdefg";
    sizeof(str);//10
    strlen(str);//7

2、原始字符串 raw string
  1)原始字符串将保证字符串内容不加修改地输出,转义字符将以两个常规字符的形式表示。
  2)方法是使用定界符 "()" 并通过前缀R来表示原始字符串,如下:

    std::cout<<R"(aaa "bbb" ccc"\n")"<<std::endl;
    //输出:  aaa "bbb" ccc"\n"

  3)如果字符串内容中包含 )" ,当编译器见到第一个 )" 时,会认为字符串结束,就会造成错误。解决方法是定义一个特殊的定界符,规则是在引号与括号之间增加任意字符,同时保证首尾增加的字符是一样的,如下:

    std::cout<<R"+6(aaa "bbb" ccc"\n")+6"<<std::endl;//这里定义了 "+6( 和 )+6" 的定界符,其中增加了+6这些字符
    //输出:  aaa "bbb" ccc"\n"

  当然,任意字符,不包括空格、左右括号、斜杠、控制字符(制表符、换行符等)。

b3 结构体 struct

1 C++11结构初始化

    struct myStructType
    {
        int id;
    };
    myStructType myStruct{};    //这将结构内成员都设置为0

b4 共用体 union

1 意义:数据项使用两种或更多格式的时候(不同时使用不同格式),常用于节省内存。
2 共用体的长度为其最大成员的长度。

b5 枚举 enum

1 定义,枚举含有多个枚举量,每个枚举量,对应到从0开始的整数,或显式地赋值,当显示赋值部分枚举量时,未赋值的量对应的整数为前一个加1

    enum myEnumType{a,b,c,d,e,f,g};
    enum myEnumType{a = 8,b,c,d,e,f,g};

2 枚举只有赋值运算符,枚举可提升为整型
3 整型可强制转换为枚举量,前提是整型的值在枚举范围内,枚举范围的上限是大于最大值的最小2的幂再减1,对于下限,最小值大于0,则下限是0,否则与上限规则类似
4 应用:定义符号常量用在switch等地方

    enum {a,b,c,d,e,f,g};

    int main()
    {
        int code = 1;
        switch (code)
        {
            case a : break;
            case b : break;
            default : break;
        return 0;
    }

5 C++11:作用域内枚举
  1)对于下面的情况,在同一个作用域内定义相同的枚举量,会出现冲突

    enum egg {Small, Medium, Large};
    enum apple {Small, Medium, Large};

  2)C++11提供了一种新枚举,枚举量的作用域为类或结构体,使用枚举名来限制枚举量

    enum class egg {Small, Medium, Large};
    enum class apple {Small, Medium, Large};
    enum struct peach {Small, Medium, Large};
    enum struct box {Small, Medium, Large};

    egg s = egg::Small;

  3)常规枚举能够实现整型提升,但作用域内的枚举不能隐式转换为整型。
  4)作用域内枚举的底层类为int,可以通过下述方式修改底层类型,底层类型必须为整型。

    enum class : short egg{Small, Medium, Large};

b6 指针

1 动态联编(dynamic binding)与内存泄露(memory leak)
  new/delete配对使用。
  同一个内存重复释放将导致不确定的结果,应避免,例如不能创建两个指向同一个内存块的指针。
  动态数组分配内存后,系统程序跟踪分配的内存量,但是跟踪结果不公开,不能通过sizeof来确定动态数组包含的字节数。

2 指针算数
  1)指针算数:指针变量+1,代表指向下一个元素的地址:例如指向double类型的指针,值+1,则指针指向的地址+8。
  2)数组名与指针
  一般情况下,C++将数组名解释为数组第一个元素的地址,stack[1] -> *(stack + 1)。
  特殊情况下,对数组名取地址,这个地址将指向整个数组,这与第一个元素的地址是一样的,但是指向的内存块大小不同。

int array[10];
cout<<array<<endl;  //输出 array[0] 的地址           类型为 int*            指针+1 导致 地址+4
cout<<&array<<endl; //输出 array 整个数组的地址      类型为 int(*)[10]      指针+1 导致 地址+4*10

注:默认 int为4字节。

  数组名与指针的区别:前者为常量不能修改,但可以使用sizeof查看数组长度,后者修改值代表指针指向地址的前后移动,而sizeof得到的是指针的长度。

3 指针与字符串
  C++多数表达式中,char数组名、char指针、字符串常量都被解释为字符串第一个字符的地址。要打印该地址,需要将地址量转换成(int*),否则将打印从第一个字符直至空字符。

C++11 :auto 与 decltype

  auto:自动类型推断,要求显示初始化,使得编译器能够将变量类型设置成初始值类型。一般用于复杂类型,在模板中常见,对于简单数据类型容易误用。
  decltype:将变量声明为表达式指定的类型。

    int y;
    decltype(y) x;  //定义x,使x的变量类型与y相同

  对于模板来说,只有实例化之后才能确定类型,因此decltype常用。

    template <typename T, typename U>
    void ef(T t , U u)
    {
        decltype(T*U) tu;
        //...
    }

ROS2_Foxy学习9——多机激光SLAM先导篇

  这篇文章的整体目的是在多个车上跑slam,通过ROS2_Foxy将地图集中到一个电脑下,然后拼接成一张地图。
  一开始的想法是车上用ROS2跑SLAM,由于各大开源SLAM算法对ROS2不支持或者支持不完善(又因为 能力不够,不能把代码从ROS1移植到ROS2),因而,使用ROS1跑SLAM,然后通过ROS2的包ros1_bridge在各车与电脑之间传递数据,从而实现此项目。
  在引入ros1_bridge之前,先写一写在ROS1上跑开源SLAM,以及在ROS2上跑开源SLAM的一些过程。
  因为没做完 内容较多,所以分了几个部分来写,这一篇是探索可行性的。

1 环境准备

设备 处理器 系统 ROS1版本 ROS2版本
电脑 amd64_酷睿i7 5500U_内存8GB Ubuntu 20.04 LTS Noetic Foxy
arm64_树莓派4B_内存4GB Ubuntu mate 20.04 LTS Foxy

  激光雷达使用思岚RPLidarA1,由于这个雷达频率较低,因此开源SLAM选择cartographer。没有选择Hector的原因除了RPLidarA1扫描频率太低,用Hector效果不理想外,Hector官方没有给ROS2的版本,自己移植太费时间。

2 ROS1下测试SLAM

2.1 cartographer 源码测试

  参考官方文档:https://google-cartographer-ros.readthedocs.io/en/latest/compilation.html

# 安装工具
    sudo apt-get update
    sudo apt-get install -y python3-wstool python3-rosdep ninja-build stow

# 下载源码
    mkdir catkin_ws
    cd catkin_ws
    wstool init src
    wstool merge -t src https://raw.githubusercontent.com/cartographer-project/cartographer_ros/master/cartographer_ros.rosinstall
    wstool update -t src

# 安装依赖
    sudo rosdep init
    rosdep update
    rosdep install --from-paths src --ignore-src --rosdistro=${ROS_DISTRO} -y

# 手动安装abseil-cpp库
    src/cartographer/scripts/install_abseil.sh

# 编译安装
    . /opt/ros/noetic/setup.bash    #(记得source ros1的安装位置)
    catkin_make_isolated --install --use-ninja

# 跑个包
    . install_isolated/setup.bash       #(记得source cartographer的安装位置)
    wget -P ~/Downloads https://storage.googleapis.com/cartographer-public-data/bags/backpack_2d/cartographer_paper_deutsches_museum.bag
    roslaunch cartographer_ros demo_backpack_2d.launch bag_filename:=${HOME}/Downloads/cartographer_paper_deutsches_museum.bag

  跑的bag大概是这样子:
在这里插入图片描述

2.2 rplidar_ros 源码测试

  参考官方文档:https://github.com/Slamtec/rplidar_ros

# 下载源码
    mkdir -p catkin_ws/src
    cd catkin_ws/src
    git clone https://github.com/Slamtec/rplidar_ros.git

# 编译
    . /opt/ros/noetic/setup.bash    #(记得source ros1的安装位置)
    catkin_make

# 连接雷达,并赋予设备权限(根据实际情况,选择设备名称)
    sudo chmod 777 /dev/ttyUSB0

# 跑个demo(记得source rplidar_ros的安装位置)
    . devel/setup.bash      # 因为只编译,没有安装,所以没有install文件夹
    roslaunch rplidar_ros test_rplidar.launch

  跑的是这样子:
在这里插入图片描述  再跑个rviz的demo

    roslaunch rplidar_ros view_rplidar.launch

  跑的是这样子:
在这里插入图片描述

2.3 rplidar_ros + cartographer

1、准备源码
  新建工作空间,并把cartographerrplidar_ros的源码包放到/src文件夹下。
2、编写cartographer的launch文件和lua配置文件
  直接参考源码给的demo来改,主要参考了cartographer_ros包下的launch/demo_revo_lds.launch及其对应的configuration_files/revo_lds.lua,改动的文件内容如下,这里是重新编写了两个文件,并将这两个新文件放在了跟参考文件同一个文件夹内:

launch文件:cartoForRplidar.launch <—— demo_revo_lds.launch

<launch>  
  <--!false 是因为要使用真实雷达数据,不用仿真时间 /-->
  <param name="/use_sim_time" value="false" />  

  <--!注意configuration_basename 后面的配置文件名,要与实际的lua文件名一致 /-->
  <node name="cartographer_node" pkg="cartographer_ros"  
        type="cartographer_node" args="  
            -configuration_directory $(find cartographer_ros)/configuration_files  
            -configuration_basename rplidar.lua"  
        output="screen">  
    <--!这句实际上,可以删掉 /-->
    <remap from="scan" to="scan" />  
  </node>  
  <node name="rviz" pkg="rviz" type="rviz" required="true"  
        args="-d $(find cartographer_ros)/configuration_files/demo_2d.rviz" />  
</launch>

lua文件:rplidar.lua <—— revo_lds.lua
  这个文件只改了tracking_frame和published_frame,这两个frame,一般是laser,需要根据雷达ros驱动的实际情况配置。

include "map_builder.lua"
include "trajectory_builder.lua"

options = {
  map_builder = MAP_BUILDER,
  trajectory_builder = TRAJECTORY_BUILDER,
  map_frame = "map",
  tracking_frame = "laser",
  published_frame = "laser",
  odom_frame = "odom",
  provide_odom_frame = true,
  publish_frame_projected_to_2d = false,
  use_pose_extrapolator = true,
  use_odometry = false,
  use_nav_sat = false,
  use_landmarks = false,
  num_laser_scans = 1,
  num_multi_echo_laser_scans = 0,
  num_subdivisions_per_laser_scan = 1,
  num_point_clouds = 0,
  lookup_transform_timeout_sec = 0.2,
  submap_publish_period_sec = 0.3,
  pose_publish_period_sec = 5e-3,
  trajectory_publish_period_sec = 30e-3,
  rangefinder_sampling_ratio = 1.,
  odometry_sampling_ratio = 1.,
  fixed_frame_pose_sampling_ratio = 1.,
  imu_sampling_ratio = 1.,
  landmarks_sampling_ratio = 1.,
}

MAP_BUILDER.use_trajectory_builder_2d = true

TRAJECTORY_BUILDER_2D.submaps.num_range_data = 35
TRAJECTORY_BUILDER_2D.min_range = 0.3
TRAJECTORY_BUILDER_2D.max_range = 8.
TRAJECTORY_BUILDER_2D.missing_data_ray_length = 1.
TRAJECTORY_BUILDER_2D.use_imu_data = false
TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true
TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.linear_search_window = 0.1
TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.translation_delta_cost_weight = 10.
TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.rotation_delta_cost_weight = 1e-1

POSE_GRAPH.optimization_problem.huber_scale = 1e2
POSE_GRAPH.optimize_every_n_nodes = 35
POSE_GRAPH.constraint_builder.min_score = 0.65

return options

3、编译
  catkin_make和catkin_make_isolated都可以,区别是后者使用一个隔离的构建过程,其中每个包都是独立配置、构建和加载到环境中的,解决了目标冲突、目标依赖项管理和项目之间其他不希望的交叉对话的问题,缺点是比较慢,具体参考这篇博客
  tips:编译后再修改launch或lua文件,只编译单独的包即可

    catkin_make_isolated --pkg cartographer_ros

4、跑一跑
  先跑cartographer再跑rplidar_ros,如果反了,似乎不太行,原因还没有深究。

# 首先的首先,配置环境变量
    . /opt/ros/noetic/setup.bash
    . devel_isolated/setup.bash     # 因为只编译,没有安装,所以没有install文件夹

# 首先运行 cartographer
    roslaunch cartographer_ros cartoForRplidar.launch

# 然后运行 rplidar_ros
    roslaunch rplidar_ros test_rplidar.launch

  出图:
在这里插入图片描述

3 ROS2下测试SLAM

3.1 cartographer 安装测试(失败)

  ROS2_Foxy上使用cartographer有两种方式:命令行安装和源码编译安装。

# 命令行
    sudo apt install ros-foxy-cartographer
#源码
    https://github.com/cartographer-project/cartographer_ros/tree/ros2-dashing

  cartographer似乎正在开发一个新版本(点击了解详情),直到去年11月还在开发中…

3.2 rplidar_ros 移植到 ROS2

  参考这里:ros2激光雷达功能包移植(思岚rplidar A1)
1、下载代码,然后找到rplidar_ros的包,并拷贝到自己的工作空间的src目录下

    git clone https://github.com/DylanLN/oryxbot_ws-ros2.git

2、编译、测试

# 编译
    . /opt/ros/foxy/setup.bash
    colcon build

# 跑一跑
    . install/setup.bash
    sudo chmod 777 /dev/ttyUSB0 
    ros2 launch rplidar_ros rplidar_A1.launch.py    

  另开一个终端,跑rviz2

    . /opt/ros/foxy/setup.bash
    . install/setup.bash
    rviz2

  出图:
在这里插入图片描述因为cartographer还没有成功在ROS2上跑,所以这里没有跑slam,等后面官方移植完善了,再看看。

4 ROS1 & ROS2下测试SLAM

  好了,现在到了先导篇,最奇怪的一部分了:在ROS2上跑rplidar_ros,然后通过ros1_bridge把激光数据传到ROS1,然后在ROS1中运行cartographer进行建图。
  按照前言中讲述的内容,激光雷达与cartographer都在ROS1环境下,车辆与电脑之间应该传递的是单车slam建图的结果,那么这里为什么把激光雷达放到ROS2下,然后在ROS1中接收ROS2的激光数据进行建图呢?因为不想在树莓派上,装完ROS2,再装ROS1,所以车子的作用变成了只采集激光数据,然后通过ROS2传到电脑,电脑端在ROS1中进行SLAM。
  也许,后面会尝试在树莓派上再装个ROS1?

4.1 ros1_bridge

1、小前言
  ros1_bridge是ROS2为了兼容ROS1而开发的包,这里介绍下基本的使用方法,然后将其应用到项目中。学习就是看看官网教程,这个包的安装过程可以命令行直接安装。

    sudo apt install ros-foxy-ros1-bridge

2、跑一跑(要开四个终端)

# Shell A (ROS 1 only):
. /opt/ros/noetic/setup.bash
roscore

# Shell B (ROS 1 + ROS 2):
# Source ROS 1 first:
. /opt/ros/noetic/setup.bash
# Source ROS 2 next:
. /opt/ros/foxy/setup.bash
export ROS_MASTER_URI=http://localhost:11311    # 这里根据 Shell A 实际情况改
ros2 run ros1_bridge dynamic_bridge

# Shell C:
. /opt/ros/noetic/setup.bash
rosrun rospy_tutorials talker

# Shell D:
. /opt/ros/foxy/setup.bash
ros2 run demo_nodes_cpp listener

  出图:
在这里插入图片描述

4.2 ROS1 & ROS2 & cartographer

1、ROS1下,跑cartographer,这会同时启动roscore

    . /opt/ros/noetic/setup.bash
    . devel_isolated/setup.bash
    roslaunch cartographer_ros cartoForRplidar.launch

在这里插入图片描述2、ROS1和ROS2下,跑ros1_bridge

    . /opt/ros/noetic/setup.bash
    . /opt/ros/foxy/setup.bash
    export ROS_MASTER_URI=http://localhost:11311    
    ros2 run ros1_bridge dynamic_bridge

3、ROS2下,跑rplidar_ros

    . /opt/ros/foxy/setup.bash
    . install/setup.bash
    ros2 launch rplidar_ros2 rplidar_A1.launch.py
    # 这个包名叫rplidar_ros2而非rplidar_ros,是因为修改了launch文件,'frame_id': 'laser'
    # 以便于在下一部分与cartographer的frame对接正确

4、出图
  可以看到,ROS1的cartographer拿到数据正在建图,第二张图右上角的终端显示两个ROS正在通过话题sensor_msgs/LaserScan通信。
在这里插入图片描述在这里插入图片描述

参考

1、ROS(Kinetic和Melodic)下使用cartographer踩坑记录和部分谣言终结
2、catkin_make, cmake, catkin build区别
3、ros2激光雷达功能包移植(思岚rplidar A1)

ROS2_Foxy学习7——构建

  官方文档总是很丰富,关于构建工具,参考A universal build toolUsing colcon to build packagesament_cmake user documentation

1 前言

  ROS生态中,一个软件是由多个分离的package组成的,运行软件,首先需要构建(build)这些package。
  手动构建一组package的方法就是逐个构建,每个package都有特定的构建系统,需要根据依赖情况设置环境,然后build。
  如果package及其依赖较多的话,其效率是难以接受的,这就需要一个自动化的构建工具,只需要调用一次即可构建一组package。
  ROS1提供了多个自动构建工具,catkin_make, catkin_make_isolated, 和 catkin_tools。ROS2提供了一个自动构建工具,ament_tools。

2 构建工具与构建系统

2.1 构建工具

  构建工具在一组package上运行,它确定依赖关系图,并以拓扑顺序为每个包调用特定的构建系统。构建工具只需要知道每个包特定的构建系统是如何设置环境以构建和使用包的即可。
  构建工具例如catkin_make,catkin_make_isolated,catkin_tools,ament_tools,colcon。

2.2 构建系统

  构建系统在单个package上运行。
  构建系统例如Make,CMake,Python setuptools,ament等,ROS的catkin和ament_cmake基于CMake的。
  (一般使用包括以下步骤:cmake, make, make install)

3 构建工具 colcon

3.1 colcon背景

  colcon是ROS构建工具catkin_make、catkin_make_isolated、catkin_tools和ament_tools的迭代。colcon目标是做一个通用构建工具(universal build tool),能够构建ROS1和ROS2的包,同时也能够构建一些非ROS包。ROS中,catkin_make、catkin_make_isolated、catkin_tools、ament_tools将逐步被colcon取代。

3.2 colcon安装

$ sudo apt install python3-colcon-common-extensions

3.3 colcon使用

1、创建工作空间

$ mkdir -p ~/ros2_example_ws/src
$ cd ~/ros2_example_ws

注:-p 的意义

2、下载示例代码到src文件夹,并检查兼容性

$ git clone https://github.com/ros2/examples src/examples
# check
$ cd ~/ros2_example_ws/src/examples/
$ git checkout $ROS_DISTRO
$ cd ~/ros2_example_ws

  完成后,工作空间的文件组织结构如下

.
└── src
    └── examples
        ├── CONTRIBUTING.md
        ├── LICENSE
        ├── rclcpp
        ├── rclpy
        └── README.md

3、在工作空间的根目录编译示例代码

$ cd ~/ros2_example_ws
$ colcon build --symlink-install
# --symlink-install* 的意义么看懂?不加,也能编译成功。

  完成后,工作空间组织结构如下

.
├── build
├── install
├── log
└── src
    └── examples
        ├── CONTRIBUTING.md
        ├── LICENSE
        ├── rclcpp
        ├── rclpy
        └── README.md

4、测试示例代码

$ colcon test

5、配置环境变量,然后运行节点

$ . install/setup.bash
$ ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function

# 打开另一个终端
$ . install/setup.bash
$ ros2 run examples_rclcpp_minimal_publisher publisher_member_function

3.4 tips

1、COLCON_IGNORE:在不需要编译的包中添加文件COLCON_IGNORE,colcon将忽略这一功能包
2、colcon支持多种编译类型,推荐ament_cmake 和 ament_python,同时也支持纯cmake包。(在使用ros2 pkg create命令创建功能包时,可以使用–build-type参数来指定该功能包的编译类型,然后按模板生成功能包)

$ touch COLCON_IGNORE

3.5 colcon_cd命令

  colcon_cd命令的目的是从当前目录,换成某个已有的package目录。
1、colcon_cd安装
  参考官方,通过命令行安装二进制文件

$ sudo sh -c 'echo "deb [arch=amd64,arm64] http://repo.ros2.org/ubuntu/main <code>lsb_release -cs</code> main" > /etc/apt/sources.list.d/ros2-latest.list'
$ curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
$ sudo apt update
$ sudo apt install python3-colcon-common-extensions

2、colcon_cd使用

# 获取安装文件
$ echo "source /usr/share/colcon_cd/function/colcon_cd.sh" >> ~/.bashrc
# 提前指定包的位置
$ echo "export _colcon_cd_root=~/ros2_install" >> ~/.bashrc
# 目录变换到 ~/ros2_install 工作空间下的 some_ros_package 包
$ colcon_cd some_ros_package

4 构建系统 ament

  ament构建系统包括两个基本的子类,ament_python和ament_cmake,这里只讨论ament_cmake。
  使用ament_cmake编译系统的功能包会对应生成两个文件package.xml和CMakeLists.txt。

4.1 编写 package.xml

  package.xml描述了功能包的发行信息、依赖信息等,内容如下。
  在自动创建后,需要手动修改功能包描述、功能包版本、功能包许可证信息等发行信息。以“ _depend”结尾的标签用来描述功能包依赖,在增加新的依赖时,需要添加到其中。

<!--  功能包基础内容,不用修改   -->
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<!--  下面是功能包的信息  -->
<package format="3">
    <!--  包名,新建的时候就定了,不改  -->
    <name>cpp_srvcli</name>
    <!--  关于发行信息   -->
    <version>0.0.0</version>
    <description>这里写功能描述</description>
    <maintainer email="这里写邮箱地址">Your Name</maintainer>
    <license>这里写许可证</license>
    <!--  编译系统的依赖   -->
    <buildtool_depend>ament_cmake</buildtool_depend>
    <!--  程序所需的依赖,根据需要修改添加   -->
    <depend>rclcpp</depend>
    <!--  编译系统,新建功能包的时候可以指定   -->
    <export>
    <build_type>ament_cmake</build_type>
    </export>
</package>

4.2 编写 CMakeLists.txt

1、基本框架

cmake_minimum_required(VERSION 3.5) //cmake版本要求
project(my_project)                 //功能包名,与同一功能包下package.xml中名字一样

ament_package()                     //配置project,只调用一次,且在最后

ament_package()的作用有这么几个:
第一,安装package.xml文件,ament_package()提供了查找和解析package.xml文件的API;
第二,按ament索引,注册功能包;
第三,安装cmake配置文件(也就是生成.cmake文件,名字类似于FindXXX.cmake或者xxxConfig.cmake)以便于其他包要依赖这个包时,能被cmake找到。

2、编译目标
  编译目标包括 librariesexecutables

  对于编译库,cmake语法如下。

# A 创建库,并指定编译时的源文件
    add_library(<name> [STATIC | SHARED | MODULE]
                [EXCLUDE_FROM_ALL]
                source1 [source2 ...])
    # STATIC : 静态库
    # SHARED : 共享库
    # MODULE : 在使用 dyld 的系统有效,如果不支持 dyld,则被当作 SHARED 对待

# B 编译这个库时要包含的头文件,必须先使用add_library创建
    target_include_directories(<name> <INTERFACE|PUBLIC|PRIVATE>
                $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
                $<INSTALL_INTERFACE:include>)
    # CMAKE_CURRENT_SOURCE_DIR :当前处理的CMakeLists.txt所在的目录
    # BUILD_INTERFACE : 构建时的接口
    # INSTALL_INTERFACE :安装时的接口

# EXAMPLE
    add_library(action_server SHARED src/fibonacci_action_server.cpp)
    target_include_directories(action_server PRIVATE
                $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
                $<INSTALL_INTERFACE:include>)

  对于编译可执行文件,cmake语法如下。

# A 创建可执行文件,并指定编译时的源文件
    add_executable(<name> [WIN32] [MACOSX_BUNDLE]
                    [EXCLUDE_FROM_ALL]
                    [source1] [source2 ...])
    # WIN32 MACOSX_BUNDLE 分别是Windows和Mac系统的构建选项
    # EXCLUDE_FROM_ALL选项增加,表示不构建此可执行文件

# EXAMPLE
    add_executable(server src/add_three_ints_server.cpp)

3、编译依赖
  编译依赖有两种方式,ament_target_dependencies和target_link_libraries。
  首先推荐前者,自动包含头文件、库及其依赖项,使用覆盖工作区时,将确保正确排列所有依赖项的包含目录。
  当前者无法找到库时,使用后者。例如自定义的库文件,仍需要使用target_link_libraries的方式链接。语法类似,但需要包含必要的依赖和使用include_directories()包含必要的头文件。
  同样,目标要先创建,才能添加依赖。

# ament_target_dependencies
    find_package(dependence REQUIRED)
    ament_target_dependencies(target Eigen3)

# EXAMPLE
    find_package(rclcpp REQUIRED)
    ament_target_dependencies(action_server
      "rclcpp")

4、导出库
(这个,后面用到的时候再补充…)

    ament_export_targets(export_my_library HAS_LIBRARY_TARGET)
    ament_export_dependencies(some_dependency)

    install(
      DIRECTORY include/
      DESTINATION include
    )

    install(
      TARGETS my_library
      EXPORT export_my_library
      LIBRARY DESTINATION lib
      ARCHIVE DESTINATION lib
      RUNTIME DESTINATION bin
      INCLUDES DESTINATION include
    )

5、install命令

# A cmake的语法如下
    install(TARGETS targets... [EXPORT <export-name>]
            [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
              PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
             [DESTINATION <dir>]
             [PERMISSIONS permissions...]
             [CONFIGURATIONS [Debug|Release|...]]
             [COMPONENT <component>]
             [NAMELINK_COMPONENT <component>]
             [OPTIONAL] [EXCLUDE_FROM_ALL]
             [NAMELINK_ONLY|NAMELINK_SKIP]
            ] [...]
            [INCLUDES DESTINATION [<dir> ...]]
            )
# EXAMPLE        
    install(TARGETS
          action_server
          action_client
          ARCHIVE DESTINATION lib
          LIBRARY DESTINATION lib
          RUNTIME DESTINATION bin)
    # ARCHIVE 安装静态库到 ${CMAKE_INSTALL_LIBDIR}/lib
    # LIBRARY 安装动态库到 ${CMAKE_INSTALL_LIBDIR}/lib
    # RUNTIME 安装可执行二进制文件到 ${CMAKE_INSTALL_BINDIR}/bin

6、target_compile_options

附:除官网外的其他参考

1、https://cmake.org/cmake/help/v3.19/command/target_include_directories.html

2、ROS学习日记2

3、cmake(8):install命令详解

ROS2_Foxy学习6——进阶编程_C++

里面的例子参考官方教程,然后附带一些解释和一些推荐的便于理解的文章。

1 编写 action

新建工作空间,并新建两个功能包:动作消息功能包和动作功能包。动作消息功能包中编写action文件,然后在动作功能包中使用前者的action接口。

1.1 自定义消息 action

1、创建:在动作消息功能包(这里包名是action_tutorials_interfaces)下,创建/action文件夹(与/src同级),并在其中建立.action消息文件,有关消息内容,详见本系列第三篇《ROS2_Foxy学习(三)核心概念》

Fibonacci.action

int32 order
---
int32[] sequence
---
int32[] partial_sequence

2、在package.xml文件中添加

    <buildtool_depend>rosidl_default_generators</buildtool_depend>
    <depend>action_msgs</depend>
    <member_of_group>rosidl_interface_packages</member_of_group>

注:action_msgs必要。
3、在CMakeLists.txt文件中添加

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "action/Fibonacci.action"
)

4、消息文件测试
功能包编译后(没有节点,没有运行),可以通过命令ros2 interface show来查看消息文件。

$ ros2 interface show action_tutorials_interfaces/action/Fibonacci

:下面创建的动作功能包名是action_tutorials_cpp,附带以下依赖,其中第一个就是刚刚创建的自定义消息功能包,可以在创建包时,通过参数dependencies添加,也可以手动修改package.xml(< depend >)和CMakeLists.txt(find_package)。

action_tutorials_interfaces 
rclcpp 
rclcpp_action 
rclcpp_components

1.2 动作服务端 action server

在动作功能包的/src目录下,编写动作服务端节点,fibonacci_action_server.cpp

#include <functional>
#include <memory>
#include <thread>

#include "action_tutorials_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"

//#include "action_tutorials_cpp/visibility_control.h"

namespace action_tutorials_cpp
{
class FibonacciActionServer : public rclcpp::Node
{
public:
    using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
    using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;

    //构造函数:
    //1、初始化动作服务节点名 fibonacci_action_server
    //2、初始化动作服务器:消息类型Fibonacci,所属节点this,动作名fibonacci,回调函数
    explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()) : Node("fibonacci_action_server", options)
    {
    using namespace std::placeholders;

    this->action_server_ = rclcpp_action::create_server<Fibonacci>( //消息类型Fibonacci
    this,                                   //所属节点this,
    "fibonacci",                                //动作名fibonacci
    std::bind(&FibonacciActionServer::handle_goal, this, _1, _2),   //回调函数:handle_goal      接受目标
    std::bind(&FibonacciActionServer::handle_cancel, this, _1),     //回调函数:handle_cancel    接受目标取消
    std::bind(&FibonacciActionServer::handle_accepted, this, _1));  //回调函数:handle_accepted  接受新目标并处理
    }

private:
    rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;

    //接受目标,同时告知client,这边知晓目标了,告知client的过程被自动处理了,下同
    rclcpp_action::GoalResponse handle_goal(const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const Fibonacci::Goal> goal)
    {
        RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->order);
        (void)uuid;
        return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
    }

    //接受目标取消,同时告知client,这边知道取消了
    rclcpp_action::CancelResponse handle_cancel(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
    {
        RCLCPP_INFO(this->get_logger(), "Received request to cancel goal");
        (void)goal_handle;
        return rclcpp_action::CancelResponse::ACCEPT;
    }

    //接受新目标并处理
    void handle_accepted(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
    {
        using namespace std::placeholders;

        //因为处理过程是一个耗时且长期运行的操作,因此将其移到另一线程处理
        std::thread{std::bind(&FibonacciActionServer::execute, this, _1), goal_handle}.detach();
    }

    //处理并反馈
    void execute(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
    {
        RCLCPP_INFO(this->get_logger(), "Executing goal");

        //处理频率 1Hz
        rclcpp::Rate loop_rate(1);

        //获取目标
        const auto goal = goal_handle->get_goal();

        //定义反馈
        auto feedback = std::make_shared<Fibonacci::Feedback>();
        auto & sequence = feedback->partial_sequence;
        sequence.push_back(0);  //斐波那契数列的前两个值是 0 1  动作服务器将不断反馈当前的sequence到客户端
        sequence.push_back(1);

        //定义结果
        auto result = std::make_shared<Fibonacci::Result>();

        //计算
        for (int i = 1; (i < goal->order) && rclcpp::ok(); ++i) 
        {
            //检查目标是否被取消
            if (goal_handle->is_canceling()) 
            {
                result->sequence = sequence;    
                goal_handle->canceled(result);  
                RCLCPP_INFO(this->get_logger(), "Goal canceled");
                return;
            }

            //更新sequence
            sequence.push_back(sequence[i] + sequence[i - 1]);

            //反馈sequence
            goal_handle->publish_feedback(feedback);
            RCLCPP_INFO(this->get_logger(), "Publish feedback");

            loop_rate.sleep();
        }

        //目标达成
        if (rclcpp::ok()) 
        {
            result->sequence = sequence;
            goal_handle->succeed(result);
            RCLCPP_INFO(this->get_logger(), "Goal succeeded");
        }
    }
};  // class FibonacciActionServer

}  // namespace action_tutorials_cpp

RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionServer)


1、goal_handle:关于目标句柄goal_handle(也就是rclcpp_action::ServerGoalHandle),可以看下官方的帮助文档,这里刚学,还不是很理解,写多了也许就明白了。
2、std::thread:看这里,有关join和detach可以瞧一瞧这里
3、这里没有使用main函数,而是用下面这句,表明该节点可以在运行时动态加载,看看官方帮助文档,这里也不是很理解,后面再学习

RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionServer)

1.3 动作客户端 action client

在动作功能包的/src目录下,编写动作客户端节点,fibonacci_action_client.cpp

#include <functional>
#include <future>   //C++11 异步通信
#include <memory>
#include <string>
#include <sstream>  //C++标准库中 sstream 提供了比ANSI C的 stdio 更高级的一些功能,即单纯性、类型安全和可扩展性。

#include "action_tutorials_interfaces/action/fibonacci.hpp"

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"

namespace action_tutorials_cpp
{
class FibonacciActionClient : public rclcpp::Node
{
public:
    using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
    using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<Fibonacci>;

    //构造函数:
    //1、初始化动作客户端节点名 fibonacci_action_client
    //2、初始化动作客户端:动作类型Fibonacci、所属节点this、动作名fibonacci
    //3、初始化定时器,每500ms发送一次目标
    explicit FibonacciActionClient(const rclcpp::NodeOptions & options) : Node("fibonacci_action_client", options)
    {
        this->client_ptr_ = rclcpp_action::create_client<Fibonacci>(
        this,
        "fibonacci");

        this->timer_ = this->create_wall_timer(std::chrono::milliseconds(500), std::bind(&FibonacciActionClient::send_goal, this));
    }

    //定时器中断函数
    void send_goal()
    {
        using namespace std::placeholders;

        //取消了定时,也就是说定时器中断函数只执行一次
        this->timer_->cancel();

        //然后,看看有没有在线的同名动作服务器
        if (!this->client_ptr_->wait_for_action_server()) 
        {
            RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting");
            rclcpp::shutdown();
        }

        //定义目标
        auto goal_msg = Fibonacci::Goal();
        goal_msg.order = 10;

        RCLCPP_INFO(this->get_logger(), "Sending goal");

        //确定回调函数:请求后的响应、反馈的处理、结果的接收
        auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();

        send_goal_options.goal_response_callback =
        std::bind(&FibonacciActionClient::goal_response_callback, this, _1);

        send_goal_options.feedback_callback =
        std::bind(&FibonacciActionClient::feedback_callback, this, _1, _2);

        send_goal_options.result_callback =
        std::bind(&FibonacciActionClient::result_callback, this, _1);

        //发送目标给动作服务器
        this->client_ptr_->async_send_goal(goal_msg, send_goal_options);
    }

private:
    rclcpp_action::Client<Fibonacci>::SharedPtr client_ptr_;
    rclcpp::TimerBase::SharedPtr timer_;

    //请求后的响应
    void goal_response_callback(std::shared_future<GoalHandleFibonacci::SharedPtr> future)
    {
        auto goal_handle = future.get();
        if (!goal_handle) 
        {
            RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server");
        } 
        else 
        {
            RCLCPP_INFO(this->get_logger(), "Goal accepted by server, waiting for result");
        }
    }

    //反馈的处理
    void feedback_callback(GoalHandleFibonacci::SharedPtr, const std::shared_ptr<const Fibonacci::Feedback> feedback)
    {
        std::stringstream ss;
        ss << "Next number in sequence received: ";
        for (auto number : feedback->partial_sequence) 
        {
            ss << number << " ";
        }
        RCLCPP_INFO(this->get_logger(), ss.str().c_str());
    }

    //结果的接收
    void result_callback(const GoalHandleFibonacci::WrappedResult & result)
    {
        switch (result.code) 
        {
            case rclcpp_action::ResultCode::SUCCEEDED:  break;
            case rclcpp_action::ResultCode::ABORTED:    RCLCPP_ERROR(this->get_logger(), "Goal was aborted");   return;
            case rclcpp_action::ResultCode::CANCELED:   RCLCPP_ERROR(this->get_logger(), "Goal was canceled");  return;
            default:                    RCLCPP_ERROR(this->get_logger(), "Unknown result code");    return;
        }
        std::stringstream ss;
        ss << "Result received: ";
        for (auto number : result.result->sequence) 
        {
            ss << number << " ";
        }
        RCLCPP_INFO(this->get_logger(), ss.str().c_str());
        rclcpp::shutdown();
    }
};  // class FibonacciActionClient

}  // namespace action_tutorials_cpp

RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionClient)

注 std::future :看这里。std::thread是C++11中提供异步创建多线程的工具,但想要从线程中返回异步任务结果,一般需要依靠全局变量,从安全角度看,有些不妥,为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。

1.4 修改CMakelists.txt

在CMakelists.txt添加以下内容。

# 这里是生成共享库
add_library(action_server SHARED src/fibonacci_action_server.cpp)
add_library(action_client SHARED src/fibonacci_action_client.cpp)

target_include_directories(action_server PRIVATE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
target_include_directories(action_client PRIVATE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)

target_compile_definitions(action_server PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")
target_compile_definitions(action_client PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")

ament_target_dependencies(action_server
  "action_tutorials_interfaces"
  "rclcpp"
  "rclcpp_action"
  "rclcpp_components")
ament_target_dependencies(action_client
  "action_tutorials_interfaces"
  "rclcpp"
  "rclcpp_action"
  "rclcpp_components")

rclcpp_components_register_node(action_server PLUGIN "action_tutorials_cpp::FibonacciActionServer" EXECUTABLE fibonacci_action_server)
rclcpp_components_register_node(action_client PLUGIN "action_tutorials_cpp::FibonacciActionClient" EXECUTABLE fibonacci_action_client)

install(TARGETS
  action_server
  action_client
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin)