十六、图
1.定义
由点和线连成的一张图,点是有穷非空的
术语:
顶点:线性表中数据元素叫元素,树中叫节点,而图中叫顶点(Vertex)
ps.线性表中相邻的点只有顺序关系,树中相邻的点只有层次关系,而图不管哪两个顶点都可能有关系,关系用边来表示,边可以一条都没有。
无向边:两个点之间的边没有方向
有向边:同理,也称为”弧”
有向图G2=(V2,{ E2 }),其中顶点集合V2={
A,B,C,D},弧集合E2={ <A,D>,<B,A>,<C,A>,<B,C>}
这里有向边用的是<>,无向边用的是()
简单图:①不存在顶点到其自身的边②同一条边不重复出现
完全图:任意两个顶点都存在边,无向的叫无向完全图。有向且方向互为相反的叫有向完全图
稠密图、稀疏图
网:边上的数字是权,带权的图叫网
子图
2.顶点与边的关系
度:有多少边连着这个点,这个点就有多少度
边数=度数总和/2
有向图中指向点的弧叫做的数目称为顶点的入度,相反称为出度
度=入度+出度
边数=入度和=出度和
路径的长度=边数(弧数)
回路、环:从某个顶点出发,最后回到这个顶点的路径称为回路或环。
除了出发点,其他点不重复的叫简单回路或简单环,连出发点都不重复的叫简单路径
3、连通图
任意两个点都连通
连通分量:①要是子图
②子图是连通的
③连通子图含有极大顶点数
④具有极大顶点数的连通子图包含依附于这些顶点的所有边
强连通图:任意两个点之间都存在路径,有向图的极大强连通子图称为强连通分量
生成树:n个顶点n-1条边
有向树:有向图中恰有一个顶点入度为0,其它顶点入度为1
4.图的存储结构
1)邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。
一个一维数组存储图中顶点信息
一个二维数组存储图中边或弧的信息
举例:
如上这样一个无向图,它的顶点通过一维的顶点数组来存放,边通过二维的矩阵来存放
其中对角线为0代表每个顶点都不存在自身的边,两个点对应的位置为1代表两点之间有边,为0代表没有,其中v1和v3为0,代表两点之间没有边
因此,我们可以通过这个矩阵获得以下信息:
**①判断两点之间有无边非常容易
**
**②某个顶点的度就是这个顶点在矩阵中对应那一行元素的和,v1的度为1+0+1+0=2
**
**③求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍。a[i][j]为1就是邻接点
**
以上是有向图的存储方式
那么如果是一个带权值的网该如何存储呢?
如图左是一个网,在矩阵中我们用0表示不存在自身的边,用数值表示权值,用∞表示不存在权值
那么如何用代码来表示这种存储结构呢?
图的基本结构和准备
typedef char VertexType;//顶点类型由用户自定义
typedef int EdgeType;//边类型
#define MAXVEX 100//最大顶点数
#define INFINITY 65535//用65535代表∞
typedef struct
{
VertexType vexs[MAXVEX];//顶点表
EdgeType arc[MAXVEX][MAXVEX];//邻接矩阵
int numVertexes, numEdges;//当前的顶点数和边数
}MGraph;
CreateMGraph方法,建立邻接矩阵存放图信息
void CreateMGraph(MGraph *G)
{
int i, j, k, w;
printf(“输入顶点数和边数:n”);
scanf_s(“%d, %d”, &G->numVertexes,
&G->numEdges);//输入顶点和边数
for (i = 0; i < G->numVertexes; i++)//读入顶点信息,建立顶点表
scanf_s(&G->vexs[i]);
for (i = 0; i < G->numVertexes; i++)
{
for (j = 0; j < G->numVertexes; j++)
G->arc[i][j] = INFINITY;//初始化矩阵,全部复制为65535
}
for (k = 0; k < G->numEdges; k++)
{
printf(“输入边(vi,vj)上的下标i,下标j和权w:n”);
scanf_s(“%d, %d, %d”, &i, &j, &w);//输入边(vi,vj)上的权w
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j];//因为是无向图,矩阵对称
}
}
2)邻接表
如果要处理一个稀疏有向图,邻接矩阵会显得很浪费空间。
因此联系链表和孩子表示法,可以创造出新的存储方式,称为邻接表
处理方法:
①图中顶点用一个一位数组存储,并且每个数据元素还需要指向第一个邻接点的指针,以便于查找边信息。
②图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数补丁,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。
顶点表中的节点都是由data和firstedge两个域表示的,data用于存放顶点信息,firstedge是指针域,用于指向第一个邻接点。
边表节点由adjvex和next两个域组成。adjvex存储某顶点的下标,next指向顶点的下一邻接点。
有向图的邻接表和逆邻接表,逆邻接表用来表示顶点的入度
对于带权值的网图,边表节点多加一个weight数据域就OK了
算法:
边表节点的结构
//边表节点
typedef struct EdgeNode
{
int adjvex;//邻接点域,用于存放邻接点的下标
EdgeType weight;//权值域
struct EdgeNode * next;//链域,指向下一邻接点
}EdgeNode;
//顶点表节点
typedef struct VertexNodes
{
VertexType data;//顶点与
EdgeNode * firstEdge;//边表头指针
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes, numEdges;
}GraphAdjList;
CreateALGraph方法,利用头插法,对于无向图,一条边对应都是两个顶点
void CreateALGraph(GraphAdjList *G)
{
int i, j, k;
EdgeNode * e;
printf(“输入顶点数和边数:n”);
scanf_s(“%d,%d”, &G->numVertexes, &G->numEdges);
for (i = 0; i < G->numVertexes; i++)
{
scanf_s(&G->adjList[i].data);//输入顶点信息
G->adjList[i].firstedge = NULL;//将第一个指针置为空
}
for (k = 0; k < G->numEdges; k++)
{
printf(“输入边(vi,vj)上的顶点序号:n”);
scanf_s(“%d,%d”, &i, &j);
e = (EdgeNode
*)malloc(sizeof(EdgeNode));//申请内存空间,创造边表节点
e->adjvex = j;//下标为j
e->next =
G->adjList[i].firstedge;//e的指针指向的下一个是顶点表节点指向的空
G->adjList[i].firstedge = e;//顶点表节点的指针指向e
e = (EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex = i;//下标为i
e->next =
G->adjList[j].firstedge;//e的指针指向的下一个对象是上面的e
G->adjList[j].firstedge = e;//顶点表节点的下一指针指向e
}
}
3)十字链表
之前的邻接表是由缺陷的,它只关心了出度的问题,而入度却必须遍历整个图才能知道。反之,逆邻接表也解决值解决了入度的问题。
那么有没有能把邻接表和逆邻接表结合起来的方法呢?
答案就是十字链表
这个是重新定义的顶点表节点结构
其中firstin是入度,指向第一个入度点,firstout是出度
这个是重新定义的边表节点结构
其中tailvex是当前顶点的下标
headvex是顶点指向的下一点的下标
headlink是指入边表指针域,指向终点相同的下一条边
taillink是指边表指针域,指向起点相同的下一条边
对于顶点v0,有两个入边对于v1和v2,因此v0的firstin指向v1中边表节点headvex为0的节点,然后headlink指向v2中headvex为0的节点,如图①②。同理可得后面三种
4)邻接多重表
虽然邻接表能够优化存储方式,但是一旦需要进行增加删除等操作,就会显得十分麻烦,如图:
要删除v0和v2这条边,就必须删除邻接表中两个节点,那么有没有更好的数据结构来解决这个问题呢?
如图我们可以把边表节点改成这样的结构,
其中ivex和jvex是某条边依附的两个顶点的下标
· ilink指向ivex顶点的下一条边,jlink同理
首先连接 ①②③④,将顶点一一对应边
接着顶点v0的(v0,v1)的邻边有(v0,v3)(v0,v2),因此⑤⑥指向下一条依附于顶点v0的边的目标。同理可得⑦⑧⑨⑩
5)边集数组
边集数组是由两个一维数组组成。一个存储顶点信息,另一个存储边信息,这个边数组每个数据元素由一条边的起点下标,终点下表和权组成
其中begin存储起点下标,end存储终点下表,weight存储权
5.图的遍历
1)定义
从某个顶点访问其余顶点,且每个顶点只访问一次
2)分类
①深度优先遍历(Depth_First_Search)
仔细搜查每个角落
从顶点v开始,先输出该顶点,然后访问未被访问的邻接点,如果图中尚有点未被访问,则另选图中一个未曾访问的顶点作为起点,重复上述步骤
算法:
邻接矩阵的DFS结构和算法
typedef int Boolean; //Boolean是布尔类型,其值是TRUE或FALSE
Boolean visited[MAX]; //访问标识的数组
//邻接矩阵的深度优先递归算法
void DFS(MGraph G, int i)
{
int j;
visited[i] = TRUE;
printf(“%c “, G.vexs[i]); //打印顶点,也可以其他操作
for (j = 0; j < G.numVertexes; j++)
{
if (G.arc[i][j] == 1 && !visited[j])
DFS(G, j); //递归访问邻接点
}
}
DFSTraverse方法,遍历邻接矩阵
//邻接矩阵深度遍历操作
void DFSTraverse(MGraph G)
{
int i;
for (i = 0; i < G.numVertexes; i++)
{
visited[i] = FALSE; //将每个顶点都初始化为未访问状态
}
for (i = 0; i < G.numVertexes; i++)
{
if (!visited[i])
{
DFS(G, i); //对未访问的顶点执行DFS
}
}
}
邻接表的结构和算法
//邻接表的深度优先递归算法
void DFS(GraphAdjList GL, int i)
{
EdgeNode * p;
visited[i] = TRUE;
printf(“%c “,GL->adjList[i].data); //打印顶点,也可以其他操作
p = GL->adjList[i].firstedge;
while(p)
{
if (!visited[p->adjvex])
DFS(GL, p->adjvex); //递归访问邻接点
p = p->next;
}
}
邻接矩阵的算法为O(n^2^),邻接表为O(n+e),从效率的角度上来讲邻接矩阵优于邻接表
②广度优先遍历(Breadth_First_Search)
广度遍历是先把每个房间简单看一下,如果没找到再逐渐深入。
如上图,把图的层次变换成右边那样
如果说DFS是树的前序遍历,那么BFS则是树的层序遍历
邻接矩阵的算法
void BFSTraverse(MGraph G)
{
int i, j;
Queue Q;
for (i = 0; i < G.numVertexes; i++)
{
visited[i] = FALSE;
}
InitQueue(&Q); //初始化一辅助用的队列
for (i = 0; i < G.numVertexes; i++) //对每一个顶点做循环
{
if (!visited[i]) //若是未访问过就处理
{
visited[i] = TRUE; //访问后设置已访问
printf(“%c “, G.vexs[i]); //打印顶点
EnQueue(&Q, i); //将此顶点入队
while (!QueueEmpty(Q)) //若队列不为空
{
DeQueue(&Q, &i); //将队中元素出队,赋值给i
for (j = 0; j < G.numVertexes; j++)
{
//判断其他顶点若与当前顶点存在边且未访问过
if (G.arc[i][j] == 1 && !visited[j])
{
visited[j] = TRUE; //将找到的此顶点标记为已访问
printf(“%c “, G.vexs[j]); //打印顶点
EnQueue(&Q, j); //将找到的此顶点入队
}
}
}
}
}
}
邻接表的算法
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode * p;
Queue Q;
for (i = 0; i < GL->numVertexes; i++)
{
visited[i] = FALSE;
}
InitQueue(&Q); //初始化一辅助用的队列
for (i = 0; i < GL->numVertexes; i++) //对每一个顶点做循环
{
if (!visited[i]) //若是未访问过就处理
{
visited[i] = TRUE; //访问后设置已访问
printf(“%c “, GL->adjList[i].data); //打印顶点
EnQueue(&Q, i); //将此顶点入队
while (!QueueEmpty(Q)) //若队列不为空
{
DeQueue(&Q, &i); //将队中元素出队,赋值给i
p = GL->adjList[i].firstedge; //找到当前顶点边表链表头循环
while(p)
{
//判断其他顶点若与当前顶点存在边且未访问过
if (!visited[p->adjvex])
{
visited[p->adjvex] = TRUE; //将找到的此顶点标记为已访问
printf(“%c “, GL->adjList[p->adjvex].data); //打印顶点
EnQueue(&Q, p->adjvex); //将找到的此顶点入队
}
p = p->next
}
}
}
}
}
6.最小生成树
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!