图的基本概念

一、什么是图?

图是一种非线性的数据结构,表示多对多的关系。

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V, E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

在图中需要注意的是:

线性表和树可以看做特殊的图。
线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)
线性表可以没有元素,称为空表;树中可以没有节点,称为空树;但是,在图中不允许没有顶点(有穷非空性)
线性表中的各元素是线性关系,树中的各元素是层次关系,而图中各顶点的关系是用边来表示(边集可以为空)。

二、图的分类

1. 无向图

顾名思义,无向图就是图上的边没有方向。

上图就是一个无向图。该图的顶点集为 V = { 1 , 2 , 3 , 4 , 5 , 6 } ,边集 E = { ( 1 , 2 ) , ( 1 , 5 ) , ( 2 , 3 ) , ( 2 , 5 ) , ( 3 , 4 ) , ( 4 , 5 ) , ( 4 , 6 ) }。在无向图中,边 ( u , v )和边 ( v , u )是一样的,也就是说和方向无关。

1.1 无向完全图

在无向图中,如果任意两个顶点之间都存在边(两个顶点直接连接),则称该图为无向完全图。

有n个顶点的无向完全图有n(n-1)/2 条边。

对这个边的理解

​ 假设一个图的顶点有n个,那么其中一个的顶点连接的边就有n-1条边,又因为共有n个顶点,所以共有n*(n-1)条边,但又因为是无向图所以再除以2才是真正的边即n(n-1)/2条边

1.2 连通图(无向图)
  • 在无向图G中,如果从顶点u到顶点v有路径(可以不是直接连接,只要能够从u到v即可),则称u和v是连通的。

  • 如果对于图中任意两个顶点u、v,都有(u, v)∈E(即u和v都是连通的),则称G是连通图。

  • 无向图中的极大连通子图???称为连通分量

    连通分量需要满足:

    1. 必须是子图;

​ 2. 必须是连通的;

​ 3. 含有极大顶点数;

​ 4. 包含依附于这些顶点的所有边。

image-20230326182014598

image-20230326182035675

上图中,图1是无向非连通图(因为A与E不连通,即不满足图上任意两个顶点连通),但是有两个连通分量,即图2和图3。而图4,尽管是图1的子图,但是它却不满足连通子图的极大顶点数(图2满足)???。 因此它不是图1的无向图的连通分量。

这里,补充一个概念。
关节点(割点):某些特定的顶点对于保持图或连通分支的连通性有特殊的重要意义。如果移除某个顶点将使图或者分支失去连通性,则称该顶点为关节点。如下图中的 顶点c 。

1.3 无向图的度

无向图的边是无向图度的总和的一半!!

对于无向图G= (V, E), 如果边(v,v’)属于E, 则称顶点v和v‘互为邻接点,即(v,v’)与顶点v和v’相关联。顶点v的度是与v相关联的边的数目。如下面这个无向图,顶点A 的度为3。各个顶点度的和=3+2+3+2=10。而此图的边数是5,推敲后发现,边数其实就是各顶点度数和的一半,多出的一半是因为重复两次计数。

2.有向图

顾名思义,有向图就是图上的边有方向。

image-20230326182102608上图就是一个有向图。该图的顶点集为 V = { A , B , C , D } ,边集 E = { < B , A > , < B , C > , < C , A > , < A , D > }。在有向图中,边 < u , v >和边 < v , u >是不一样的。

通常情况下,有向图中的边用< >表示,无向图中的边用( )表示。

2.1 有向完全图

有向无环图

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条边,则称该图为有向完全图。n个顶点的有向完全图含有n*(n-1)条边。

2.2 强连通图(有向图)

image-20230326182117586

在有向图G中,如果对于每一对顶点vi、vj且vi≠vj,从vi到vj和从vj到vi存在路径,则称G是强连通图
有向图中的极大强连通子图称做有向图的强连通分量
强连通图具有如下定理:

​ 一个有向图G是强连通的,当且仅当G中有一个回路,它至少包含每个节点一次。
如上图所示,图1不是强连通图,图2是强连通图。图2也可以看做是图1的强连通分量。

2.3 有向图的度

对于有向图G = (V, E),如果边<v,v’>属于E,则称顶点v邻接到顶点v’,顶点v’邻接自顶点v的边<v,v’>和顶点v, v’相关联
从顶点v出发的边的数目称为v的出度;到达顶点v的边的数目称为v的入度,顶点v的度=出度+入度。

以下面这个有向图为例:
顶点A的入度是2 (从B到A的边,从C到A的边),出度是1(从A到D的边),所以顶点A的度为2+1=3。此有向图的边有4 条,而各顶点的出度和为1+2+1+0=4,各顶点的入度和=2+0+1+1=4。

也就是说,有向图中的入度和 == 出度和 == 1/2*(有向图的度之和)

有向图的度

3. 稀疏图和稠密图

按照边的多少来分稀疏图和稠密图。假设一个图的顶点数为n,如果边数大于n*log n,则该图为稠密图,反之则为稀疏图。

4. 有环图和无环图

先了解一些概念:

  • 路径(path): 依次遍历顶点序列之间的边所形成的轨迹。注意,依次就意味着有序,先1后2和先2后1不一样。

  • 简单路径: 没有重复顶点的路径称为简单路径。说白了,这一条路径中没有出现绕了一圈回到同一顶点的情况。

  • : 包含相同的顶点两次或者两次以上。例如,下图中路径 < 1 , 2 , 4 , 3 , 1 >,其中1出现了两次,那么这条路径就是一个环路。

    在这里插入图片描述

因此,顾名思义,有环图就是图上有环,无环图就是没有环的图。
特别地,有向无环图有,又叫做DAG(Directed Acyline Graph),具有一些很好的性质,很多动态规划的问题都可以转化成DAG中的最长路径、最短路径或者路径计数的问题。

5. 加权图和无权图

首先需要了解一下什么是权。

有些图的边上具有与它相关的数字,这种与图的边相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。因此加权图就是边上带有权重的图,与其对应的是无权图,或叫等权图,即边上没有权重信息。如果一张图不含权重信息,我们就认为边与边之间没有差别。

通常情况下,加权图会被称为网络。

三、图的存储结构

1. 邻接矩阵

图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点的信息,一个二维数组(称为邻接矩阵)存储图中边的信息。
(1)下图是使用邻接矩阵存储无向图。如图所示,设置两个数组,顶点数组为vertex[4] = {v0, v1, v2, v3},边数组arc[4][4]实际上是一个矩阵。对于矩阵的主对角线的值,即arc[0][0]、arc[1][1]、arc[2][2]、arc[3][3]全为0,这是因为顶点上不存在自环的边。通过这个例子可以看出,无向图的邻接矩阵是一个对称矩阵。(有边则为1,无边则为0)

image-20230326182134499

#define maxvex 100
typedef struct
{
char vexs[maxvex]; //用于记录顶点的数组
int arc[maxvex][maxvex]; //用于记录边数组的二维矩阵
int vertex,edges; //用于记录定点数和边的数目
}MGraph;
//无向图的建立,是对称矩阵,所以arc[i][j] = arc[j][i]
#define maxvexs 100
#define infinity 65535//用65535来表示∞
typedef struct
{
    char vexs[maxvexs];
    int arc[maxvexs][maxvexs];
    int vertexes,edges;
}mgraph;
 
/*c 版本*/
void creatgraph(mgraph *g)
{
    int i,j,k,w;
    printf("输入顶点数和边数:\n");
    scanf("%d,%d",&g->vertexes,&g->edges);
    for(i=0;i<g->vertexes;i++)//读入顶点信息,建立顶点表
        scanf("%c",&g->vexs[i]);
    for(i=0;i<g->vertexes;i++)
        for(j=0;j<g->vertexes;j++)
            g->arc[i][j]=infinity;//初始化邻接矩阵
    for(k=0;k<g->vertexes;k++)//读入edges条边,建立邻接矩阵
    {
        printf("输入边(Vi,vj)上的下标i,下标j,和权w:\n");
        scanf("%d%d%d",&i,&j,&w);
        g->arc[i][j]=w;
        g->arc[j][i]=w;//无向图,矩阵对称
    }
}

/* c++版本*/
void creatgraph(mgraph &g){
    int i,j,k,w;
    cout<<"输入顶点数和边数:"<<endl;
    cin>>g.vertexes>>g.edges; //输入
    for(i = 0;i<g.vertexes;i++)
        cin>>g.vexs[i]; //该数组用于记录顶点的值
    for(i = 0;i<g.vertexes;i++)
    	for(j = 0;j<g.vertexes;j++)
            g.arc[i][j] = infinity; //初始化邻接矩阵,将矩阵的值全部置为正无穷
    for(k=0;k<g->vertexes;k++)//读入edges条边,建立邻接矩阵
    {
        cout<<"输入边(Vi,vj)上的下标i,下标j,和权w:"<<endl;
        cin>>i>>j>>w;
        g->arc[i][j]=w;
        g->arc[j][i]=w;//无向图,矩阵对称
    }
}

(2)下图是使用邻接矩阵存储有向图。如图所示,设置两个数组,顶点数组为vertex[4] = {v0, v1, v2, v3},边数组arc[4][4]实际上是一个矩阵。对于矩阵的主对角线的值,即arc[0][0]、arc[1][1]、arc[2][2]、arc[3][3]全为0,这是因为顶点上不存在自环的边。通过这个例子可以看出,有向图的邻接矩阵并不是一个对称矩阵。

image-20230326182155912

2. 邻接表

数组与链表相结合的存储方法

邻接表由表头节点表节点两部分组成,图中每个顶点均对应一个存储在数组中的表头节点。如果这个表头节点所对应的顶点存在邻接节点,则把邻接节点依次存放于表头节点所指向的单向链表中。
(1)下图所示的就是一个无向图的邻接表结构。从该图可以看出,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。例如:v1顶点与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2。

image-20230326182224691

(2)下图是使用邻接表存储有向图。值得注意的是,由于有方向的,因此有向图的邻接表分为出边表和入边表(又称逆邻接表),出边表的表节点存放的是从表头节点出发的有向边所指的尾节点;入边表的表节点存放的则是指向表头节点的某个顶点。

image-20230326182327953

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。

img

显而易见,如果图是一个稀疏图,用邻接表进行存储比较合适,如果图是一个稠密图,则用邻接矩阵更合适。

/*  邻接表节点定义*/
typedef struct EdgeNode
{
    int adjvex; //邻接点域,存储该顶点对应的下标
    int weight; //用于存储权值,对于非网图可以不需要
    struct EdgeNode *next;  //链域,指向下一个邻接点
}EdgeNode;//边表结点
 
typedef struct VertexNode //结点包括顶点信息和一个链表
{
    char data; //顶点域,存储顶点信息,也就是顶点的值
    EdgeNode *firstedge; //边表头指针
}VertexNode,AdjList[MAXVEX]; //顶点表结点
 
/*
AdjList = VertexNode[MAXVEX] 是一个确定长度的数组
*/
typedef struct
{
    AdjList adjList;
    int numVertexes,numEdges;//图中当前顶点数和边数
}GraphAdjList; //图的结构定义
 

四、图的遍历

1.深度优先遍历(DFS)

从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。(根,左,右,也就是深度优先遍历)

img

/************************邻接表的深度优先********************************/
void DFS(ALGraph G,int v)

{//从顶点v(v为图中位序)深度优先遍历图
	cout<< G.vertices[v].data<<" ";
	visited[v] = true;
	//FirstAdjVex(G,v)取的是传入结点v的邻接点链表的头节点所指向的顶点位置 
	//NextAdjVex(G,v,w)取得是刚刚取的头节点的下一个结点,直到没有返回-1 
	for(int w=FirstAdjVex(G,v);w>0;w=NextAdjVex(G,v,w)){
 		if(!visited[w])
		 DFS(G,w);
}
}

int DFSTraverse(ALGraph G,Status (*Visit)(int v)) //在主函数中调用的是这个函数,**不加后面的参数也可以**
/*
Status (*Visit)(int v)意味着:
传入一个函数名,并赋给Visit,因为是引用所以使用指针成了*Visit
但该函数也是有限制的,必须只有一个形参 (正如上面的(int v))且返回值是Status (也是从上面看到的)

所以就变成了 Status (VisitOutput) (int v)  
*/

{
	//从v开始,深度优先遍历图   创建了一个bool visited[MAX_LENGTH] ;visit[]是一个布尔类型的数组,只是用于记录
 //首先将状态数组的全部设为false
 for(int i=0;i<G.vexnum;++i)
 	visited[i] = false;
 for(int i=0;i<G.vexnum;++i)
 	if(!visited[i])
	 	DFS(G,i); 

}

/*调用时使用的是:
BFSTraverse(G,VisitOutput);
*/

就上面的**Status (*Visit)(int v)**解释引申:

举个栗子:
#include<stdio.h>
#include<stdlib.h> 
#include<string.h>
#define ERROR 0
#define OK 1


int add(int a,int b){
    return a+b;
}
int multiply(int a,int b){
    return a*b;
}

int main() {
    int (*fun)(int,int);
    fun=add;     ======================>通过传入函数名字就可以得到不同的功能,只是对参数和返回值有一定的限制
    printf("%d\n",fun(5,6));
    fun=multiply;
    fun(5,6);
    printf("%d\n",fun(5,6));
    return 0;
}
2.广度优先遍历(BFS)

类似于树的层次遍历。

img

img

/*****************邻接表,连通图的广度优先遍历*********************/
void BFSTraverse(ALGraph G,Status (*Visit)(int v)){

 //广度优先遍历
    SqQueue Q; //首先创建一个队列,先进先出 
    int u;
    int v = 0;//仍然是从数组索引为0开始 
    cout<<G.vertices[v].data<<" "; 
    visited[v] = true;
    InitQueue(Q);
    EnQueue(Q,v); //首先件索引为0 的压入到队列中 
    while(!QueueEmpty(Q)){ //当队列不为空就进行此循环 
       DeQueue(Q,u); //弹出 ,弹出的值给了u
       for(int w = FirstAdjVex(G,u);w>=0;w = NextAdjVex(G,u,w)) //接着是将与索引为0的邻接点分别压入到队列当中,并将遍历到的点进行标记 
        /* 
        依次检查u的所有邻接点w
        FirstAdjVex(G,u)表示u的第一个邻接点
		w>=0表示存在邻接点
		NextAdjVex(G,u,w)表示u的相对于w的下一个邻接点
        */   

       if(!visited[w]){
           cout<<G.vertices[w].data<<" "; 
	       visited[w] = true;
          EnQueue(Q,w);
       } 
   } //while
 } // BFSTraverse
  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值