大话西游之王道考研数据结构第八讲---图以及最小生成树

                                                     第八讲--图以及最小生成树

复习

1.树有哪几种表示方法

2.树、森林、二叉树之间转换中左右孩子的定义

3.54 32 47 49 51 48构造二叉排序树,并删除47,然后计算成功和失败的平均查找长度。

4.a:30 s:31 b:20 c:10 x:3 q:7 d:6 构造哈夫曼树并计算WPL

5.为什么哈夫曼树中没有一个码是其他码的前缀

6.n个结点构造哈夫曼以后,有多少个结点

一、图

1.1 图的一些定义

图一般可以分为有向图和无向图(树可以是空树,但是图不能是空图)。有向图也就是图中的边是有方向的,边集合一般是E={<1,2>,<2,1>,<3,1>}。无向图最也就是图中的边是没有方向的,边集合一般是E={<1,2>,<3,2>,<3,1>}

(定义可以看书上,没什么好讲的)。

1.什么是完全图。含有n个顶点的无向完全图有几条边?

2.什么是连通图,什么是极小连通子图,什么是极大连通子图,这是针对有向图还是无向图。

3.什么是强联通图,什么是强联通分量,这是针对有向图还是无向图

4.如果一个图有n个顶点,并且有小于n-1条边,那么这个图是连通的还是不连通的?

5.连通图的生成树是包含图中全部顶点的一个极小连通子图。也就是用最少的边把所有顶点连起来,那么最小的边数是多少(如果有n个顶点)。

6.顶点的度,针对有向图是什么概念,针对无向图是什么概念。

7.无向图全部顶点的度之和和边数的关系是什么

1.2 图的存储结构

图主要的有两种存储结构,邻接矩阵和邻接链表(十字链表等那些野路子就不要记了)。看过书可能感觉得到,最近学到的线性表、栈、队列、树的存储结构一般都是由两种,一种是用数组存,一种是用链表存。

举个栗子:

我们有如下关系:唐僧认识猴哥,猴哥认识玉皇 ,猴哥认识阎王,如来佛祖认识玉皇。

我们一共出现了唐僧、猴哥、玉皇,阎王,如来五个人。

图也是如此,领接矩阵就是数组,只不过是二维数组,领接链表就是链表,只不过一个结点一个链表。

先看下领接矩阵:

#define MAX_VERTEX_NUM 20
typedef struct
{
    char vexs[MAX_VERTEX_NUM];//点集
    int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //边集
    int vex_num,arc_num;
}MGraph,*Graph;

很简单,arcs[2][3] 就是存的第3个结点和第4个结点(下标从0开始),当结点没有权重这些东西时候(权重在最小生成树和最短路里面用到),arcs[2][3] = 1表示 第3个结点和第4个结点是连通的,如果是无向图的话,必还有arcs[3][2] = 1。有向图的话就可以反向没有。

这是一个无向图,A认识B,肯定有B认识A(假设不存在A是你,B是王力宏,这种你认识他,他不认识你的情况)。主对角线都是0,这是我们的规定,方便统计。

再看下领接链表:

typedef char VertexType;
typedef int EdgeType;
#define MaxVex 100
typedef struct EdgeNode //边表结点
{
	int adjvex; //邻接点域,存储邻接顶点对应的下标
	EdgeType weight; //用于存储权值,对于非网图可以不需要
	struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode  //顶点表结点
{
	VertexType data;  //顶点域,存储顶点信息
	EdgeNode *firstedge; //边表头指针
}VertexNode,AdjList[MaxVex];
 
typedef struct
{
	AdjList adjList;
	int numVertexes,numEdges; //图中当前顶点数和边数
}GraphAdjList;

这个就比较麻烦了,考试不会考这些~看看就行,主要说下邻接链表的一些概念:

领接表需要一个存结点的地方,和存边的地方,好的一点是邻接表里面边是增加一个边才扩展一个空间的,就不会像邻接矩阵一样一开始就把所有可能存在的边的空间申请好,因此对于稀疏矩阵,用邻接链表存储会比较好。那么在有N个边的情况下(有向图里面1->2 2->1算两条边,无向图算一条边),无向图所需存储空间为O(|V| + 2|E|)(无向图一条边需要存两次),有向图所需存储空间为O(|V| + |E|)

1.3 图的遍历

二叉树的遍历一般有四种(前序,中序,后序,层序)。因为图中和一个结点连接的其他结点之间没有顺序关系,所以图的遍历可以分为深度优先遍历(DFS,相当于二叉树的前中后序遍历)和广度优先遍历(BFS,相当于二叉树的层序遍历)。

这两个代码不太会考,主要考给一个图,能不能把这两种遍历写出来,这里我们也把这两个遍历的代码贴上来,感兴趣的可以看看:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<queue>
#include<iostream>
#define Max 9999999
using namespace std;
int Graph[100][100];
int visit[100];
int n,m;
void InitGraph()
{
    for(int i = 0;i<n;i++)
    {
        for(int j = 0;j<n;j++)
        {
            if(i == j)
                Graph[i][j] = 0;
            else
                Graph[i][j] = Max;
        }
    }
}
void DFS(int v)
{
    printf("%d ",v);
    visit[v] = 1;
    for(int i =1;i<=n;i++)
    {
        if(visit[i] == 0)
            DFS(i);
    }
}
int main()
{
    int a,b,v;
    queue<int> q;
    scanf("%d",&n);
    InitGraph();
    scanf("%d",&m);
    for(int i= 0;i<m;i++)
    {
        scanf("%d%d%d",&a,&b,&v);
        Graph[a][b] = v;
        Graph[b][a] = v;
    }
    memset(visit,0,m*sizeof(int));
    for(int i = 1;i<=n;i++)
    {
        if(visit[i]==0)
        {
            DFS(i);
        }
    }
    printf("\n");
    memset(visit,0,(m+1)*sizeof(int));
    //下面是用到队列的层序遍历
    q.push(1);
    visit[1] = 1;
    while(!q.empty())
    {
        int t = q.front();
        printf("%d ",t);

        q.pop();
        for(int i = 1;i<=n;i++)
        {
            if(visit[i] == 0 && Graph[t][i] != 0 && Graph[t][i]!=Max)
            {
                q.push(i);
                visit[i] = 1;
            }

        }
    }
    printf("\n");
	return 0;
}

我们主要练习一下这两个遍历的结果:

遍历时候,我们需要规定遍历的起始点,这里我们定为0,(同一个结点连接的两个结点,按照编号小的先遍历,这是一般规矩,不按这个规则也不能算错)这是一个有向图。

深度遍历结果为:0,1,3,5,2,4

广度遍历结果为:0,1,2,3,5,4

如果是无向图呢?

深度遍历结果为:0,1,3,4,5,2

广度遍历结果为:0,1,2,3,5,4

1.4 判断一个图是否是连通的

通过任何一个结点,都可以找到其他所有的结点,这样的图就是连通的。判断连通时候,可以弄一个表,类似图的领接矩阵一样,如果连通就画个对号,最后如果除去主对角线元素以外,都有对号就说明是连通的。

二、最小生成树(必考大题)

前面的多是概念,看看书应该能懂。最小生成树是必考大题。我当初学的时候,老是把最小生成树和最短路弄混,尤其最小生成树又一般分为Kruskal和Prim两种算法,最短路又有Dijkstra算法。就感觉很混,我在把最短路讲完以后,对这俩东西做一个对比。

最小生成树的意思是,现在处于祖国刚刚成立,国家还不是很富有,但是我们又希望走社会主义现代化道路,带领人民实现共同富裕,俗话说要致富先修路,我们国家准备修一个铁路网,连接祖国各地的主要城市,也就是图中的这几个城市。不同城市之间,修铁路所要花费在图中标明了(武汉到北京是1块钱......西安到拉萨是3块钱)。因为我们还很穷,不能把这些铁路都修好,只能修最少的铁路,花最少的钱,使得每个城市都能到达另一个城市。那么最少修几条铁路?一共花多少钱?

Prim算法,这是一个没有大局观念的拓荒者,

1.他总是会从一个结点开始,作为他率先征服的领地,然后每次看看当前能修的铁路里面,哪个铁路造价最小(铁路的一端是拓荒者所处的位置A,另一端是还未能抵达的城市B),最小的我们就修,然后把B加入拓荒者所征服的版图内。

2.更新拓荒者能够拓荒的铁路造价表,因为新加入的B->C,他有可能会比A->C的造价小,所以需要更新。

3.重复1-2直到没有新的领地。

比如,最开始我们把北加入到征服的领地中,然后更新铁路造价表,发现武的造价最低(红色的),然后把武加入到已征服的领地中,更新铁路造价表(因为武汉可以达到拉萨和福州,而且比原来的造价小,所以更新)。这样一直做下去就好了。

有一个问题是,图中绿色的部分,我们知道北京到西安的造价是6,武汉加进来以后,武汉到西安的造价也是6。那么我们最后修的到底是北京到西安的路呢还是武汉到西安的路呢?这就说明,最小生成树的结果不止有一种。考试也会考把所有的可能画出来(这里起始点是告诉了且是固定的)。

 

可以看到Prim算法,只在乎当前的利益,每次都管当前的最小,最后达到全局最小,这也就是贪心的基本思想,其得到的结果是局部最优。

Kruskal算法,这是一个有大局观念的规划者

1.将所有的边的造价进行排序,1,2,3,4,5,5,6,6,6,6。

2.看一下,当前最小的边的两个结点是不是连通的,如果不是连通的,就把这个边加进来,然后这两个点设置为连通。

如果是连通的,看检查下一条边

3.直到所有边都检查完。

这里检查连通性,我们需要用到并查集算法,这是算法书里讲到的东西,建议可以把这个东西弄懂,有一个博客讲的非常清楚,看一看绝对懂,我整个写这一系列博客的灵感也来源于这篇博客:并查集。我就不再这篇博客面前班门弄斧了~。大家可以看看这个,说不定你会爱上算法~

最后绿色说明,这时候这四条边都可以选择。之所以在第四步->第五步时候,为什么不考虑造价为5的结点?因为北京-青岛和武汉-青岛,之前就是可以到达的,所以不能加造价为5的结点。

最后,Kruskal的结果有4种~

 

 

 

 

猜你喜欢

转载自blog.csdn.net/zhangbaodan1/article/details/81459358
今日推荐