数据结构和算法学习之路——图的详解(C++版)

数据结构和算法之图的详解——(C++语言)

高质量的代码就是对程序自己最好的注释。当你打算要添加注释时,问问自己,“我如何能改进编码以至于根本不需要添加注释?”改进你的代码,然后才是用注释使它更清楚。
——Steve McConnell, 软件工程师,作家, 出自 《Code Complete》

文章目录

  • 数据结构和算法之图的详解——(C++语言)
  • (一)图的知识框架
  • (二)图的表示
    • 2.1 图的邻接矩阵表示
      • 2.1.1 原始图的数组表示
      • 2.1.2 邻接矩阵的部分代码实现
    • 2.2 图的邻接表表示
      • 2.2.1 图的邻接表的创建
  • (三)图的遍历
    • 3.1图的深度优先搜索DFS
      • 3.1.1 DFS的代码实现
    • 3.2 图的广度优先搜索BFS
    • 3.3 DFS和BFS的效率分析:
  • (四)图的应用
    • 4.1 最小生成树
      • 4.1.1 Prim算法思路
      • 4.1.2 Kruskal算法思路

(一)图的知识框架

在我们的实际应用与研究中经常可以看见图的身影,下面我们来看看图有哪些知识点吧

数据结构和算法学习之路——图的详解(C++版)_第1张图片

(二)图的表示

2.1 图的邻接矩阵表示

图是如何用邻接矩阵来表示的呢?下面我们来看一看有向图,无向图以及网,还有他们的邻接矩阵形式吧
数据结构和算法学习之路——图的详解(C++版)_第2张图片
好的,我们可以看到,无论是有向图,无向图还是网,每个顶点(也就是图中圆圈内的数字)自己和自己是不算相互连接的,那么对于无向图和有向图,我们就可以规定若两个顶点相互连接,我们
就把这两个点对应位置的矩阵元素标记为1,反之标记为0,而网和它们的区别就在于,不相邻用∞符号表示,相邻的时候不再是标记为1了,而是改为这两个点之间的权值。
但是,重点来了
对于无向图有的概念,度就是与顶点连接的边或弧的数量,比如说上图(b)中,顶点3的度为3,顶点1的度为2,但是,对于有向图而言,又有出度入度之分,所谓出度,就是看该顶点指向外边的弧的数量,而入度就是指向该顶点的弧的数量,我们看图(a),顶点1指向了顶点2,所以1和2是连接的,此时1顶点是起始顶点,2顶点是终止顶点,然而,2和1我们却并不认为他们连接,这就是有向图和无向图表示的很大的不同

那么对于无向图(b),1顶点与2顶点连接,那么反过来2顶点同样也和1顶点连接,所以无向图的邻接矩阵一定是对称的(除非化成上三角或者下三角)
但是对于有向图(a),1顶点与2顶点连接,但是2顶点却不与1顶点连接,所以有向图的邻接矩阵不一定是对称矩阵

2.1.1 原始图的数组表示

想必大家也会和我一样在一开始有这样的疑惑:我知道怎么写图的邻接矩阵了,可是在实际问题中我们在程序里如何表示一个原始的图呢?我们先上无向图(b)的代码

int graph_b[12][2] = {{1,2},{2,1},{1,4},{4,1},{2,3},{3,2},{3,4},{4,3},{2,5},{5,2},{3,5},{5,3}};

对于无向图(b),顶点1,2的连接关系,我们应该用{1,2}和{2,1}来表示,至于为啥呢,貌似是计算机表示上的原因,以后搞明白了再和大家分享一下,也欢迎大家在评论区交流一下

下面是有向图(a)的代码

int graph_a[4][2] = {{1,2},{1,3},{3,4},{4,1}};

对于有向图(a),顶点1,2的连接关系用{1,2}表示,前面那个数字表示起始顶点,后面那个数字表示终止顶点

2.1.2 邻接矩阵的部分代码实现

这里以无向图(b)为例

int graph_b[12][2] = {{1,2},{2,1},{1,4},{4,1},{2,3},{3,2},{3,4},{4,3},
                                 {2,5},{5,2},{3,5},{5,3}};               //这里是原始图的数组表示
 int i, j, tempi, tempj;
 int a[6][6] = {0};                              //用于存放邻接矩阵
 for(i = 0; i < 12; i++)                         //这个for循环用于读取图中的信息
 { 
     tempi = graph_b[i][0];
     tempj = graph_b[i][1];
     a[tempi][tempj] = 1;                       //若两个顶点相邻,则邻接矩阵的对应位置设置为1
 }
 for(i = 1; i < 6; i++)                        //注意这里是从1开始的!
 {
     for(j = 1; j < 6; j++)
     {
         cout<<a[i][j]<<" ";
     }
     cout<<endl;
 }

好的,到这儿,我们就成功把无向图用邻接矩阵的形式表示出来啦!有向图的表示也是类似的

2.2 图的邻接表表示

我们知道,如果有一个图是稀疏图(有很少条边或弧),那么用邻接矩阵的方式将会造成一些空间上的浪费,而这时,有邻接表(其实也是链表)的方式存储图将会很好的解决这个问题。

2.2.1 图的邻接表的创建

首先我们得声明一个顶点的结构体,这个结构体的成员包括顶点的值,还有指向下一个顶点的同样是这个结构体类型的next指针。我们还需要定义一个结构体数组,一般来说,图中有几个顶点,这个结构体数组就是多长,但是C++中由于数组第一个元素的下标从0开始,为了方便理解,我们其实也可以令数组的长度比顶点数多1
数据结构和算法学习之路——图的详解(C++版)_第3张图片
下面开始建立邻接表

struct link
{
    int val;
    link *next;
};
link head[6];
int main()
{
    link *newnode, *ptr;
    int data[14][2]={{1,2},{2,1},{2,5},{5,2},     //图形数组声明,大家可以画一下看看是怎么样的一个图
                    {2,3},{3,2},{2,4},{4,2}, {3,4},{4,3},{3,5},{5,3},{4,5},{5,4}};
    int i, j, k;
    for(i = 1; i < 6; i++)
    {
        head[i].val = i;
        head[i].next = NULL;
        cout<<head[i].val<<"->";
        ptr = &(head[i]);
        for(j = 0; j < 14; j++)
        {
            if(data[j][0] == i)       //说明这一行的两个数是相互连接的
            {
                newnode = new link;
                newnode->val = data[j][1];
                newnode->next = NULL;
                while(ptr->next != NULL)
                {
                    ptr = ptr->next;
                }
                ptr->next = newnode;
                cout<<newnode->val<<"->";
            }
        }
         cout<<endl;
    }
}

其实如果是网的话也很简单,就是在上面那个node结构里面加一项权值就OK了
下面再举一个其他的例子,便于日后理解
数据结构和算法学习之路——图的详解(C++版)_第4张图片

(三)图的遍历

3.1图的深度优先搜索DFS

原理:假设初始状态是图的所有顶点都没有被访问,那么DFS可以从图中的某个顶点出发,先访问此顶点,然后依次从该顶点未被访问的邻接点出发往深处遍历图,直到图中所有和出发顶点有路径相通的顶点全部被访问为止
下面我们来看一张动画描述DFS
数据结构和算法学习之路——图的详解(C++版)_第5张图片
首先,起始顶点是0,0访问完了之后,我们看看0顶点的右边,与之相邻的是1顶点,那么我们去访问1顶点,然后继续往深处走,到了3顶点,然后去到2顶点

这时,有趣的事情发生了

我们伫立在2顶点的位置,发现我们四周相邻的最近的顶点都被访问过了,也就是到了死胡同,这是,我们就应该回到上一次访问的顶点(回溯),看看还有没有其他路径,我们惊喜的发现,与3顶点连接的还有4顶点,那么自然,我们就去访问4顶点,至此DFS过程结束
输出的结果是 0 1 3 2 4

3.1.1 DFS的代码实现

DFS过程其实是一个递归的过程

int run[6] = {0}//用来记录那些顶点值被访问了
void dfs(int num)
{   
    link *ptr;
    cout<<head[num].val<<"->";
    run[num] = 1;                //把访问过的顶点记录为1,没访问过的是0;
    ptr = head[num].next;
    while(ptr != NULL)
    {
        if(run[ptr->val] == 0)
        {
            dfs(ptr->val);
        }
    }
    ptr = ptr->next;
}

3.2 图的广度优先搜索BFS

BFS类似于树的按层次遍历的过程,实现BFS需要用到队列的结构
数据结构和算法学习之路——图的详解(C++版)_第6张图片
其实我们可以这样理解:BFS是从某一个起始节点开始,以辐射的形式一层一层地向外扩散,从而依次访问每一层的顶点,以上图为例BFS的结果为:0 1 2 3 4

3.3 DFS和BFS的效率分析:

遍历图的过程实质上是对每个顶点查找其邻接点的过程,因此遍历图所耗费的时间和图的存储结构有关,当我们使用邻接矩阵的形式存储图的时候,DFS或BFS的时间复杂度为O(n²),n为图的顶点数;当我们使用邻接表的形式存储图的时候,DFS或BFS的时间复杂度为O(n + e),n为图中的顶点数,e为无向图中边的数目或者是有向图中弧的数目

(四)图的应用

4.1 最小生成树

生成树的概念:若同时满足边集中的所有边既能够使全部顶点连通而又不形成任何回路,则称子图G’是原图G的一棵生成树。
我们得清楚,一棵生成树的代价就是树上各个边的代价之和

4.1.1 Prim算法思路

我们把一个图的顶点分成两类,一类是已经被访问过的顶点,我们记为A,另一类是还没有被访问到的顶点,我们记为B,下面是构造步骤

  1. 在所有A中寻找一条边,使得A到B的路径代价最小(关键)
  2. 把该路径上的B归为A 类
  3. 继续重复上述步骤

下面我们来看一个具体的例子

数据结构和算法学习之路——图的详解(C++版)_第7张图片
算法分析:

  1. Prim算法的时间复杂度为O(n²)
  2. 其于网中的边数无关,适用于边稠密的最小生成树

4.1.2 Kruskal算法思路

  1. 将所有边按照权值的大小进行升序排序
  2. 依次对这些由两个顶点组成的边的权值进行判断,选择最小的那个边开始
  3. 当两条不同的边的顶点可以连接时,判断:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去
  4. 继续上述步骤

下面时Kruskal算法的流程图
数据结构和算法学习之路——图的详解(C++版)_第8张图片

这里有几个地方要提醒一下
我们看到(e)->(f)的过程,按照权值的排序,现在应该轮到权值为5的边了,但是我们看到原图权值为5的边有两条,但是1,4顶点之间的边不能选,因为会构成回路

你可能感兴趣的:(数据结构和算法)