图
图的定义
图是由 顶点的有穷非空集合 和 顶点之间的边的集合 组成,通常表示为: G = (V,E) ,其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。*
顶点:图中数据元素称为顶点(vertex),顶点必须是有穷的非空集合,因此一个图至少有一个顶点。
边:任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边(edge)来表示,边集可以是空的。
图的分类
图分为无向图和有向图
无向边:若顶点Vi 到Vj 的边没有方向,则称这条边为无向边,用无序偶对(Vi ,Vj)来表示。
无向图:若如果图中任意两个顶点之间的边都是无向边,则称该图为无向图。
无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n(n-1)/2条边。
有向边:若从顶点Vi 到Vj的边有方向,则称这条边为有向边,也称为弧(Arc)。用有序偶对<Vi ,Vj>来表示。Vi称为弧尾(Tail)或初始点,Vj称为弧头(Head)或终端点。
有向图:如果图中任意顶点之间的边都是有向边,则称该图为有向图。
有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n(n-1) 条边。
权: 有时图的边或弧具有与它相关的数,这种与图的边或弧相关的数叫做权。
网:带权的图通常称为网。
稀疏图: 有很少条边或弧的图。
稠密图: 有很多条边或弧的图。
子图: 假设两个图G=(V,E)和G1=(V1,E1),如果V1⊆V且E1⊆E则G1为G的子图
度: 顶点的度是指和该顶点关联的边的数目。无向图边的数目等于各顶点度的和的一半。
入度: 有向图中以顶点(v)为头的弧的数目,称为(v)的入度。
出度: 有向图中以顶点(v)为尾的弧的数目,称为(v)的出度。
弧的数量 = 各个顶点的出度和 = 各个顶点的入度和
邻接点:对于无向图,同一边上的两个顶点称为邻接点。
路径的长度: 路径上的边或弧的数目。
连通图的相关术语
在无向图G=(V,E)中,如果从顶点v到顶点w有路径,则称v和w是相通的。如果对图中任意两个顶点Vi和Vj 属于E,则两个顶点是连通的,则称G是连通图。
如下图1,它的顶点A都顶点B、C、D都是连通的,但显然顶点A与顶点E或F就无路径,因此不能算是连通图。而图2,顶点A、B、C、D相互都是连通的,所以它本身是连通图。
连通图生成树
连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
图的顺序存储——邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。
一个一维数组存储图中顶点信息,
一个二维数组(邻接矩阵)存储图中的边或弧的信息。
设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
看一个实例,下图左就是一个无向图。
从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。
从这个矩阵中,很容易知道图中的信息。
(1)判断任意两顶点是否有边无边;
(2)某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点;
无向图的存储——邻接矩阵
1. 顺序存储
2. 结构体创建
typedef char VertexInfo[9]
struct graph
{
//图的顶点数组 可以是二维矩阵,也可以是一维
VertexInfo vertex[MaxVertex];
//图的边
//边的数组 二维矩阵
int edge[MaxVertex][MaxVertex];
//顶点的个数
int vertexNum;
//边的条数
int edgeNum;
};
3. 图的创建——初始化边的二维数组——给具体二维数组中的内容赋值
4. 打印测试
有向图的存储——邻接矩阵
而有向图讲究入度和出度,顶点v2的入度为2,正好是第i列各数之和。顶点v2的出度为1,即第i行的各数之和。
1. 顺序存储
2. 结构体和无向图完全一样
只不过在创建的时候,多加了 弧尾 弧头 和权重
3. 图的创建 二维矩阵 不是 对称的
4. 打印测试
示例
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#define MaxVertex 50
typedef char VertexInfo[9];
// 定义图的结构
struct Graph
{
// 顶点数组 - 存储顶点的名字
VertexInfo vertex[MaxVertex];
// 边的数组
int edge[MaxVertex][MaxVertex];
// 顶点的个数
int vertexNum;
// 边的条数
int edgeNum;
};
// 求用户输入的顶点在顶点数组中的位置
int LocalVertex(Graph &g, VertexInfo v)
{
// 遍历顶点数组
for (int i = 0; i < g.vertexNum; ++i)
{
if (strcmp(v, g.vertex[i]) == 0)
{
// 找到了,返回元素的下标
return i;
}
}
// 没找到
return -1;
}
// 构建一个图
void CreateGraph(Graph &g)
{
cout << "请输入图的顶点数和边数: 顶点 边" << endl;
cin >> g.vertexNum >> g.edgeNum;
cout << "请输入" << g.vertexNum << "个顶点的值" << endl;
for (int i = 0; i < g.vertexNum; ++i)
{
cin >> g.vertex[i];
}
// 初始化所有边都不存在
for (int i = 0; i < g.vertexNum; ++i)
{
for (int j = 0; j < g.vertexNum; ++j)
{
g.edge[i][j] = INT_MAX;
}
}
// <B, A>
cout << "请输入" << g.edgeNum << "条边, 弧尾 弧头 权重" << endl;
int w;
VertexInfo v1, v2;
for (int i = 0; i < g.edgeNum; ++i)
{
cin >> v1 >> v2 >> w;
// 求用户输入的顶点在顶点数组中的位置
int m = LocalVertex(g, v1);
int n = LocalVertex(g, v2);
// 边对应的二维数组赋值
g.edge[m][n] = w;
}
}
// 打印图 -
void PrintGraph(Graph& g)
{
// 水平表头
cout << "\t";
for (int i = 0; i < g.vertexNum; ++i)
{
cout << g.vertex[i] << "\t";
}
for (int i = 0; i < g.vertexNum; ++i)
{
cout << endl;
// 垂直的
cout << g.vertex[i] << "\t";
for (int j = 0; j < g.vertexNum; ++j)
{
if (g.edge[i][j] == INT_MAX)
{
cout << "∞" << "\t";
}
else
{
cout << g.edge[i][j] << "\t";
}
}
}
cout << endl;
}
void test01()
{
Graph g;
CreateGraph(g);
PrintGraph(g);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
图的链式存储——邻接表
邻接矩阵是不错的一种图存储结构,但是,对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。因此,找到一种数组与链表相结合的存储方法称为邻接表。
邻接表的存储方式是这样的:
(1) 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,
不过,数组可以较容易的读取顶点的信息,更加方便。
(2) 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。
数据结构定义:
1. 有三个结构体标示邻接表
1)顶点头结点结构体
2)邻接点结构体
3)图的结构体
2. 创建图
3. 测试 无向图和有向图
示例
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
#if 1
#define MaxVertex 100
// 邻接点的结构体
struct edgeNode
{
// 当前顶点在顶点数组中的位置
int position;
// 指向后继节点的指针
struct edgeNode* next;
// 节点相关的信息 - info
int weight;
};
// 顶点数组的结构体
struct Vertex
{
// 顶点的名字
char name[9];
// 指向临接点结构体指针
struct edgeNode* first;
};
// 邻接表图结构
struct GraphList
{
// 顶点数组
Vertex head[MaxVertex];
// 顶点的个数
int vertexNum;
// 边的条数
int edgeNum;
};
int LocalVertex(GraphList&g, char* name)
{
for (int i = 0; i < g.vertexNum; ++i)
{
if (strcmp(name, g.head[i].name) == 0)
{
return i;
}
}
return -1; // 没找到
}
// 创建一个图
void CreateGraph(GraphList &g)
{
cout << "请输入图的顶点数和边数: 顶点 边" << endl;
cin >> g.vertexNum >> g.edgeNum;
cout << "请输入" << g.vertexNum << "个顶点的值" << endl;
for (int i = 0; i < g.vertexNum; ++i)
{
cin >> g.head[i].name;
g.head[i].first = NULL; // 目前没有邻接点
}
cout << "请输入" << g.edgeNum << "条边, 顶点1 顶点2" << endl;
char v1[9], v2[9];
for (int i = 0; i < g.edgeNum; ++i)
{
cin >> v1 >> v2;
// 以M为头结点的链表, n是m的;邻接点
// 求用户输入的顶点在顶点数组中的位置
int m = LocalVertex(g, v1);
int n = LocalVertex(g, v2);
// 链表中添加邻接点
edgeNode* pNew = new edgeNode;
// init pNew
pNew->position = n; // 当前的节点在顶点数组中的位置
// pNew添加到头结点数组第m个元素 对应的链表中
// 头插法 尾插法需要遍历到尾部 ,麻烦,因此用头插法
pNew->next = g.head[m].first;
g.head[m].first = pNew;
#if 1 //if 1 关闭掉就是有向图了
// 以N为头结点的链表, m是n的;邻接点
edgeNode* pNew1 = new edgeNode;
// init pNew1
pNew1->position = m; // 当前的节点在顶点数组中的位置
// pNew添加到头结点数组第m个元素 对应的链表中
// 头插法
pNew1->next = g.head[n].first;
g.head[n].first = pNew1;
#endif
}
}
// 打印图
void PrintGraphList(GraphList& g)
{
for (int i = 0; i < g.vertexNum; ++i)
{
edgeNode* pNode = g.head[i].first;
cout << g.head[i].name << ": ";
while (pNode != NULL)
{
int index = pNode->position;
cout << g.head[index].name << " ,";
pNode = pNode->next;
}
cout << endl;
}
cout << endl;
}
图的遍历
图的遍历和树的遍历类似,希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫图的遍历。
对于图的遍历来说,如何避免因回路陷入死循环,就需要科学地设计遍历方案,通常有两种遍历次序方案:深度优先遍历和广度优先遍历。
图的深度优先遍历(深度优先搜索 Deepth First Search DFS)
深度优先遍历,也有称为深度优先搜索,简称DFS。其实,就像是一棵树的前序遍历。
它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
深度优先搜索是通过栈来实现的。
下图中的数字显示了深度优先搜索顶点被访问的顺序
为了实现深度优先搜索,首先选择一个起始顶点并需要遵守三个规则:
-
如果可能,访问一个邻接的未访问顶点,标记它,并把它放入栈中。
-
当不能执行规则1时,如果栈不空,就从栈中弹出一个顶点。
-
如果不能执行规则1和规则2,就完成了整个搜索过程。
过程
从一个顶点开始,进行遍历
利用栈进行深度优先遍历
对应创建:是否访问过的数组——和顶点数组对应
访问第一个顶点,入栈,只要栈不空,进入循环
找该顶点的邻接点,并且没有被访问过的顶点
访问这个顶点,并且标记为TRUE,找这个顶点的邻接点,入栈,循环
如果满足条件的点,出栈,直到访问第一个点的下一个邻接点
如果栈为空,遍历结束
示例
// 深度优先搜索
void DFS(GraphList& g)
{
// 保证顶点不被重复遍历
bool* visited = new bool[g.vertexNum];
// init
for (int i = 0; i < g.vertexNum; ++i)
{
visited[i] = false;
}
// 从顶点数组中找一个顶点, 开始遍历 - 0
stack<int> st; // int - 顶点在顶点数组中的下标
st.push(0);
// 访问
visited[0] = true;
cout << g.head[0].name << " ";
// 当栈为空, 遍历完成
while (!st.empty())
{
// 顶点在顶点数组中的下标取出来
int top = st.top();
// 找下标对应的顶点的邻接点
edgeNode* pNode = g.head[top].first;
while (pNode)
{
// 如果节点被遍历过了
while (pNode && visited[pNode->position])
{
// 指针后移
pNode = pNode->next;
}
// 找到了没有被访问的
if (pNode)
{
// 访问
visited[pNode->position] = true;
cout << g.head[pNode->position].name << " ";
// 找新的顶点pNode->position的邻接点
// 链表和链表直接做跳转
pNode = g.head[pNode->position].first;
st.push(pNode->position);
}
}
st.pop();
}
delete[] visited;
}
图的广度优先遍历(广度优先搜索 Breadth First Search BFS)
广度优先遍历,又称为广度优先搜索,简称BFS。图的广度优先遍历就类 似于树的层序遍历 了。
在深度优先搜索中,算法表现得好像要尽快地远离起始点似的。相反,在广度优先搜索中,算法好像要尽可能地靠近起始点。它首先访问起始顶点的所有邻接点,然后再访问较远的区域。它是用队列来实现的。
下面图中的数字显示了广度优先搜索顶点被访问的顺序。
实现广度优先搜索,也要遵守三个规则:
-
访问下一个未来访问的邻接点,这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中。
-
如果因为已经没有未访问顶点而不能执行规则1时,那么从队列头取一个顶点,并使其成为当前顶点。
-
如果因为队列为空而不能执行规则2,则搜索结束。
过程
利用队列的先进先出的数据结构,进行广度优先遍历
任意一个顶点,标记为访问,入队
进行大的循环,只要队列不为空
将队头的所有邻接点都访问,并且标记为已经访问,入队
队头元素已经没有邻接点可访问后,出队,有了新的队头元素
再进入循环,访问新的队头元素的所有未访问的邻接点
直到队列再次为空,遍历结束
测试广度有点遍历结果
示例
void BFS(GraphList& g)
{
// 保证顶点不被重复遍历
bool* visited = new bool[g.vertexNum];
// init
for (int i = 0; i < g.vertexNum; ++i)
{
visited[i] = false;
}
// 从顶点数组中找一个顶点, 开始遍历 - 0
queue<int> q; // int - 顶点在顶点数组中的下标
q.push(0);
// 访问
visited[0] = true;
cout << g.head[0].name << " ";
// 队列为空,遍历完成
while (!q.empty())
{
// 取出队头元素值, 顶点在顶点数组中的下标
int front = q.front();
// 找队头元素对应的定点的所有的邻接点
edgeNode* pNode = g.head[front].first;
while (pNode)
{
// 如果没有被访问
if (!visited[pNode->position])
{
visited[pNode->position] = true;
cout << g.head[pNode->position].name << " ";
// 邻接点入队列
q.push(pNode->position);
}
pNode = pNode->next;
}
// 所有的临界点发全部被访问
q.pop();
}
delete[] visited;
}