C++笔记day24 图

224 阅读8分钟

图的定义

图是由 顶点的有穷非空集合 顶点之间的边的集合 组成,通常表示为: 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相互都是连通的,所以它本身是连通图。

image.png

连通图生成树

连通图的生成树是一个极小的连通子图它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。

图的顺序存储——邻接矩阵

图的邻接矩阵存储方式是用两个数组来表示图

一个一维数组存储图中顶点信息,

一个二维数组(邻接矩阵)存储图中的边或弧的信息。

设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:

image.png

看一个实例,下图左就是一个无向图

image.png

从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是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行的各数之和。

image.png

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作为弧尾的出边表。

数据结构定义:

image.png

image.png

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有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。

深度优先搜索是通过栈来实现的。

下图中的数字显示了深度优先搜索顶点被访问的顺序

image.png

为了实现深度优先搜索,首先选择一个起始顶点并需要遵守三个规则:

  1. 如果可能,访问一个邻接的未访问顶点,标记它,并把它放入栈中。

  2. 当不能执行规则1时,如果栈不空,就从栈中弹出一个顶点。

  3. 如果不能执行规则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。图的广度优先遍历就类 似于树的层序遍历 了。

在深度优先搜索中,算法表现得好像要尽快地远离起始点似的。相反,在广度优先搜索中,算法好像要尽可能地靠近起始点它首先访问起始顶点的所有邻接点,然后再访问较远的区域。它是用队列来实现的

下面图中的数字显示了广度优先搜索顶点被访问的顺序。

image.png

实现广度优先搜索,也要遵守三个规则:

  1. 访问下一个未来访问的邻接点,这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中。

  2. 如果因为已经没有未访问顶点而不能执行规则1时,那么从队列头取一个顶点,并使其成为当前顶点。

  3. 如果因为队列为空而不能执行规则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;
}