一、图的定义:
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),G是一个图,V是图中的顶点集合,E是图中边的集合。
注意:
1、线性表中把数据元素叫做元素,树中叫做结点,图中则称为顶点。
2、线性表中没有元素为空表;树中没有结点,叫空树;在图中,不允许没有顶点,强调了顶点集合的有穷非空。
3、线性变相邻元素具有线性关系;树的上下两层具有层次关系;图的任意两个顶点之间的关系可以用边来表示。
各种图的定义:
无向边:图中两个顶点之间的连线没有方向就称为无向边,可以用(A,D)(D,A)无需对偶表示。如果任意两点的边都是无向边,图为无向图
有向边:图中v1到v2顶点之间的边有方向,这条边也叫弧,用有序对偶<v1,v2>,v1是弧尾,v2是弧头。如果任意两点的边都是有向边,图为有向图
简单图:图中不存在顶点到自身的边,且同一条边不重复出现,这样的图称为简单图。
无向完全图:无向图中任意两个顶点之间都存在边,则为无向完全图。n个顶点有n*(n-1)/2条边。
有向完全图:有向图中任意两个顶点之间都存在弧,则为有向完全图。n个顶点有n*(n-1)条边。
图中顶点与边的关系:
对于无向图,顶点v的度是和v相关联的边的数目。对于有向图,以顶点v为头的弧的数目称为v的入度;以v为尾的弧的数目为v的出度。
图中一个顶点到另一个顶点的路径是不唯一的,路径的长度为路径上边或弧的数目。
第一个顶点和最后一个顶点相同的路径称为回路或环。顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,叫做简单回路。
连通图:
如果图中任意两个顶点都有路径(都是连通的)则称图为连通图。
无向图中的极大连通子图称为连通分量。
有向图中任意两个顶点都有<A,B><B,A>,则称图为强连通图。极大联通分量称为强连通分量。
二、图的存储结构
1、邻接矩阵
用一个一维数组存储顶点;用一个二维数组存储边存在就为1,不存在就为0。所以无向图的边数组是一个对称矩阵。
有了这个矩阵,就可以知道任意两点是否有边;知道某个顶点度就是顶点Vi在矩阵中第i行或列的元素之和;求顶点Vi的所有邻接点就是矩阵中的第i行元素扫描一遍,为1就是邻接点。
#pragma once
typedef char VertexType;//顶点类型可以自己定义
typedef size_t EdgeType; //边的权值类型
#define MAXVEX 20 //最大顶点数
//邻接矩阵的结构定义
typedef struct MGraph
{
VertexType vexs[MAXVEX];
EdgeType arc[MAXVEX][MAXVEX];
int numVertexs, numEdges;
}MGraph;
//创建无向图的邻接矩阵
void CreateMGraph(MGraph *g)
{
cout << "输入顶点数和边数";
cin >> g->numVertexs;
cin >> g->numEdges;
for (size_t i = 0; i < g->numVertexs; i++)
{
cin >> g->vexs[i];
}
for (size_t i = 0; i < g->numVertexs; i++)
{
for (size_t j = 0; j < g->numVertexs; j++)
{
g->arc[i][j] = 0;
}
}
int i, j, w;
for (size_t k = 0; i < g->numEdges; k++)
{
cout << "输入边(vi,vj)上的下标i,下标j,和权值w";
scanf("%d %d %d", &i, &j, &w);
g->arc[i][j] = w;
g->arc[i][j] = g->arc[j][i];
}
}
2、邻接表
对于边数相对顶点较少的图,对存储空间造成大量浪费。因此可以考虑把弧或边用链表来存储。
(1)图中的顶点用一维数组存储。不过每个数组还元素需要一个指向第一个邻接点的指针。
(2)每个顶点vi和它的邻接点构成一个单链表,无向图称为顶点Vi的边表,有向图称为顶点Vi作为弧尾的出边表。
顶点表的各个节点是由data域和firstedge指针域构成firstedge指向边表的第一个节点。边表节点是由adjvex(邻接点域,存储该节点在顶点表的下标)和next指向下一个节点的指针。
对于有向图,以顶点为弧尾存储边表。
//邻接表的结构定义
//顶点表的结构定义
typedef struct VertexNode
{
VertexType data;
EdgeNode* firstEdge;//边表头指针
}VertexNode;
//边表结点的结构定义
typedef struct EdgeNode
{
int adjvex; //邻接点域,存储该顶点的下标
EdgeType w; //权值
EdgeNode* next; //下一个邻接点
}EdgeNode;
typedef struct
{
VertexNode *adjList;
int numVertex, numEdge;
}GraphAdjList;
3、十字链表---有向邻接表的优化
结合邻接表,重新定义顶点表和边表结点。
//十字链表的结构定义
//顶点表的结构定义
typedef struct Cross_VertexNode
{
VertexType data;
Cross_EdgeNode *firstIn, *firstOut; //入边表和出边表的第一个节点
}VertexNode;
//边表结点的结构定义
typedef struct Cross_EdgeNode
{
int tailvex; //弧起点在顶点表的下标
int headvex; //弧终点在顶点表的下标
EdgeType w; //权值
EdgeNode* headlink; //终点相同的下一条表
EdgeNode* taillink; //起点相同的下一条表
}EdgeNode;
typedef struct
{
Cross_VertexNode *adjList;
int numVertex, numEdge;
}GraphCrossAdjList;
三、图的遍历
从图的某一顶点出发,访问图中其余顶点,且每个顶点仅被访问一次。
1、深度优先遍历 DepthFirstSearch 是一个递归的过程
用邻接矩阵实现。
从顶点A开始,走过以后做上标记,根据右手原则访问它的邻接点,若该顶点没有未访问过的邻接点,就退回上一个顶点,知道所有顶点都被访问一遍。
bool visited[MAXVEX];//访问标志的数组
//深度优先访问顶点的递归算法
void DFS(MGraph G, int i)
{
cout << G.vexs[i]; //如果该顶点没有被访问,则打印该顶点
visited[i] = true; //标志设成已访问过
for (size_t j = 0; j < G.numVertexs; j++)
{
//访问存在的边表并且邻接点未访问
if (G.arc[i][j] == 1 && !visited[j])
{
DFS(G, j);
}
}
}
void DFSTraverse(MGraph G)
{
size_t i;
for (i = 0; i < G.numVertexs; i++)
{
visited[i] = false; //初始化把所有顶点设为可以访问
}
for (i = 0; i < G.numVertexs; i++)
{
//对未访问的顶点调用DFS方法
if (!visited[i])
{
DFS(G, i);
}
}
}
2、广度优先遍历
BreadthFirstSearch
从顶点A开始,依次访问A的各个未被访问的邻接点,分别以这些邻接点出发,依次访问他们未被访问的邻接点。直到所有顶点都被访问。
借助队列用邻接矩阵实现的算法思想
(1)首先访问V0,并设置访问标志,然后将v0入队;
(2)只要对不空,重复以下操作:
队头结点出队,对对头结点所有未访问过的邻接点进行入队。
//邻接矩阵实现图的广度优先遍历
//遍历递归算法
void BFS(MGraph G, int i)
{
Link_Queue queue;
QueueInit(&queue);
for (size_t i = 0; i < G.numVertexs; i++)
{
visited[i] = false;
}
for (size_t i = 0; i < G.numVertexs; i++)
{
if (!visited[i])
{
cout << G.vexs[i];
visited[i] = true;
QueuePush(&queue, G.vexs[i]);
while (!QueueEmpty(&queue))
{
QueuePop(&queue);
for (size_t j = 0; j < G.numVertexs; j++)
{
if (G.arc[i][j] == 1 && !visited[j])
{
cout << G.vexs[j];
visited[j] = true;
QueuePush(&queue, G.vexs[j]);
}
}
}
}
}
}