图的基本概念
图(graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
- 图中的数据称为顶点,有别于线性表的元素,树中的结点;
- 在图结构中,不允许没有顶点,v为有穷非空;
- 在图中任意两个顶点之间都可能有关系,顶点之间的逻辑关系用 边 来表示;边集可以是空集。
图的相关定义
无向边:若顶点v1和v2之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(v0,v1)来表示。
无向图:图中任意两个顶点之间的边都是无向边;
有向边:若顶点从v0到v1的边有方向,则称这条边为有向边,也成为弧,用有序偶<v0,v1>来表示,v0为弧尾,v1为弧头;
有向图:图中任意两个顶点之间的边都是有向边
简单图:在图中不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图
无向完全图:在无向图中,如何任意两个顶点之间都存在边,则称为无向完全图
有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图
权:与图的边或弧相关的数叫做权
网:带权的图称为网
路径:图中顶点间存在路径,两个顶点存在路径则说明是连同的
环:如果路径最终回到起始点则称为环
简单路径:两个顶点间的连通不存在重复的路径
连通图:任意两个顶点都是连通的图
连通分量:无向图中的极大连通子图
强连通图:任意两个顶点都是连通的有向图
强连通分量:有向图中的极大连通子图称做有向图的强连通分量
连通图的生成树:一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
有向树:如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一颗有向树
有向图的生成森林:由若干棵有向树组成,含有图中全部顶点,但只有足以构成若个棵不相交的有向树的弧。
图的存储结构
邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图;一个一维数组存储图中顶点信息,一个二维数组储存图中的边或弧的信息。
1. 无向图
2. 有向图
3. 有向网图
邻接表
上边的邻接矩阵是个不错的图存储结构,简单直接一目了然,但是如果边的数目比较少的时候,比如极端的情况下有N个顶点,但是只有一条边或者弧,那么边数组就会产生极大的浪费,我们只需要记录一条边或者弧却要使用一个N*N的二位数组来表示;面对这种情况,很容易想到用链表来代替上面的二维数组,这就是邻接表。
1. 无向图的邻接表结构(adjvex是顶点的下表)
2. 有向图的邻接表结构
在有向图的邻接表结构中,我们需要用两种表(邻接表和逆邻接表)才能完全的表示一个完整的有向图,这个还是挺麻烦的,不过后面会有另外的方式来解决这个问题。
3. 带权的网图邻接表结构
没啥好说的,就是在边链表Node中添加一个成员来表示权。
4. 来看下邻接表的数据结构如何表示:
#define NUM x
typedef T_v VertexType; //顶点类型
typedef T_w WeightType;//权的数据类型
//边表的结点
typedef struct EdgeNode
{
int adjvex;//顶点下表
WeightType weight;//权值
struct EdgeNode* next;//下一个边表结点
}EdgeNode;
//顶点表结点
typedef struct VertexNode
{
VertexType data;
EdgeNode* firstedge;
}VertexNode,AdjList[NUM];typedef struct
{
AdjList adjList;
int numVertexes,numEdges;
}GraphAdjList;
边集数组
边集数组是由两个一维数组构成;一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标、终点下标和权组成。
边数组元素的结构:
begin:起点的下标
end:终点的下标
weight:权 值
图的遍历
1. 深度优先遍历
他从图中的某个顶点v出发,访问此顶点,然后从v的未访问的邻接点再出发进行访问,直至所有的顶点全部被访问。
a. 对用邻接矩阵表示的图进行遍历(主要理解其思想):
#define NUM 5
boolean visited[NUM];
//邻接矩阵的深度优先递归算法
void DFS(MGraph G,int i)
{
int j;
visited[i] = true;//设置该顶点为已访问
printf("%c",G.vexs[i]);
//遍历边数组的i行
for(j = 0; j < G.numVertexes; j++)
{
if( G.arc[i][j] == 1 && !visited[j] )//arc[i][j] 为1 表示 i j,是一个边或者弧
DFS( G, j );//遍历该顶点
}
}
void DFSTraverse(MGraph G)
{
int i;
for(i = 0; i < numVertexes;i++)
visited[i] = false;
for(i = 0; i < G.numVertexes; i++)
if(!visited[i])//对未访问过的顶点调用DFS,这个可以有效处理非连通图的问题
DFS(G,i);
}
b. 对用邻接表存储的图遍历:
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = true;
printf("%c",GL->adjList[i].data);
p = GL->sdjList[i].firstedge;//p指向该顶点的边数组
while(p)
{
if( !visited[p->adjvex] )//判断边数组中的顶点是否被遍历过,没有的话遍历
DFS( GL, p->adjvex );
p = p->next;//指向边数组的下一个元素
}
}void DFSTraverse(GraphAdjList GL)
{
int i;
for(i = 0; i < GL->numVertexes;i++)
visited[i] = false;
for(i = 0; i < GL->numVertexes;i++)
if(!visited[i])
DFS(GL,i);
}
2. 广度优先遍历
看图理解:
1. 从任一点开始(这里是A),对应下图中的第一行只有A
2. A的下一个顶点有两个 B 、F ,对应第二行 A出 B、F进
3. B的下一个顶点有三个:C、I、G,对应第三行 B 出,C、I、G进
4. ...以此类推,直到H出;
5. 综上得出遍历的顺序为:A、B、F、C、I、G、E、D、H
a. 邻接矩阵结构的广度优先遍历:
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(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);//入队列
}
}}
}
}
}
}
b. 邻接表的广度遍历算法
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;
}
}
}
}
}
以上参考:
- 大话数据结构