1、名词解释:
- 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在图中的数据元素,我们称之为顶点(Vertex),顶点集合有穷非空。在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
- 图按照边的有无方向分为无向图和有向图。无向图由顶点和边组成,有向图由顶点和弧构成。弧有弧尾和弧头之分,带箭头一端为弧头。
- 图按照边或弧的多少分稀疏图和稠密图。如果图中的任意两个顶点之间都存在边叫做完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。
- 图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度。有向图顶点分为入度和出度。
- 图上的边或弧带有权则称为网。
- 图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复的叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称为强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称为强连通分量。
- 无向图中连通且n个顶点n-1条边称为生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。
2、图的存储结构—-邻接矩阵
图的邻接矩阵的表示方式需要两个数组来表示图的信息,一个一维数组表示每个数据元素的信息,一个二维数组(邻接矩阵)表示图中的边或者弧的信息。
如果图有n个顶点,那么邻接矩阵就是一个n*n的方阵,方阵中每个元素的值的计算公式如下:
邻接矩阵表示图的具体示例如下图所示:
首先给个无向图的实例:
下面是一个有向图的实例:
OK,到这里为止,我们给出一个无向图的邻接矩阵和一个有向图的邻接矩阵,我们可以从这两个邻接矩阵得出一些结论:
- 无向图的邻接矩阵都是沿对角线对称的
- 要知道无向图中某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
- 对于有向图,要知道某个顶点的出度,其实就是这个顶点vi在邻接矩阵中第i行的元素之和,如果要知道某个顶点的入度,那就是第i列的元素之和。
但是,如果我们需要表示的图是一个网的时候,例如假设有个图有n个顶点,同样该网的邻接矩阵也是一个n*n的方阵,只是方阵元素的值的计算方式不同,如下图所示:
这里的wij表示两个顶点vi和vj边上的权值。无穷大表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。下面是具体示例,表示的一个有向网的图和邻接矩阵:
实例代码:
package main
import (
"fmt"
)
type VertexType string //定义顶点的数据类型
type EdgeType int //边的类型
const MAXVEX = 100 //最大的顶点数
const MAXVALUE = 65535 //无效数 (无穷大,也就是说此边不通)
type MGraph struct {
vexs [MAXVEX]VertexType //定义一个数组,保存对应的顶点,可以保存65535个顶点
arc [MAXVEX][MAXVEX]int //定义一个二维数组,保存顶点对应的边,二维数组可以保存[65535][65535]
numVer, numEdg int //边的数量,顶点的数量
isTrav [MAXVEX] bool //设定某个节点是否遍历过,主要用于节点的遍历
GType byte //定义图的类型,0:无向图 1:有向图
}
/*
创建图:
*/
func createMGraph(mg *MGraph) {
fmt.Println("请输入顶点数:")
fmt.Scan(&mg.numVer)
for i := 0; i < mg.numVer; i++ {
fmt.Printf("请输入第%d个顶点:\n", i+1)
var tempChar VertexType
fmt.Scan(&tempChar)
mg.vexs[i] = tempChar
//fmt.Print("\n输入的内容为:", mg.vexs[i])
}
//顶点组成边,初始化
for i := 0; i < mg.numVer; i++ {
for j := 0; j < mg.numVer; j++ {
mg.arc[i][j] = MAXVALUE
}
}
fmt.Println("请输入边数:")
fmt.Scan(&mg.numEdg)
for i := 0; i < mg.numEdg; i++ {
//fmt.Println("请分别输入坐标E(i,j)以及权重")
fmt.Printf("请输入第%d条边的坐标E(i,j)以及权重\n", i+1)
var i, j, weight int
fmt.Scan(&i)
fmt.Scan(&j)
fmt.Scan(&weight)
mg.arc[i][j] = weight
if mg.GType == 0 {
mg.arc[j][i] = weight //无向图:数据依据对角线对称
}
//fmt.Printf("输入的权重为:%d", mg.arc[i][j])
}
/*for i := 0; i < mg.numVer; i++ {
for j := i; j < mg.numVer; j++ {
fmt.Println("----------")
fmt.Println(mg.arc[i][j])
//fmt.Println(mg.arc[j][i])
}
}
for i := 0; i < mg.numEdg; i++ {
fmt.Println(mg.vexs[i])
}*/
}
/*
清空图:1)清空边对应的权重 2)清空顶点组成的数组
*/
func ClearGraph(mg *MGraph) {
for i := 0; i < mg.numVer; i++ {
for j := 0; j < i; j++ {
mg.arc[i][j] = MAXVALUE
}
}
for i := 0; i < mg.numVer; i++ {
mg.vexs[i] = ""
}
}
func ShowGraph(mg *MGraph) {
fmt.Println()
for i := 0; i < mg.numVer; i++ {
fmt.Printf("\t\t%s", mg.vexs[i])
}
fmt.Println()
for i := 0; i < mg.numVer; i++ {
fmt.Printf("%s\t\t", mg.vexs[i])
for j := 0; j < mg.numVer; j++ {
if mg.arc[i][j] == MAXVALUE {
fmt.Printf("%s\t\t", "Z")
} else {
fmt.Printf("%d\t\t", mg.arc[i][j])
}
}
fmt.Println()
}
}
/*
图的遍历:遍历图就是逐个访问图中的所有节点
深度优先遍历算法思想:
1)从数组isTrav中选择一个未被遍历的顶点V(Vi),将其标记为true,表示已经访问过。
2)从Vi的一个未被访问的邻接点出发进行深度优先遍历;
3)重复 2),直至图中的所用和Vi有路径相通的顶点都被访问过。
4)重复1)-3)的操作,直到图中所用节点都被访问过
*/
/*
fun:从第 n个节点开始,深度遍历图
*/
func DeepTraOne(mg *MGraph, n int) {
mg.isTrav[n] = true
fmt.Print("---->", mg.vexs[n])
for i := 0; i < mg.numVer; i++ {
if mg.arc[n][i] != MAXVALUE && mg.isTrav[n] != true {
DeepTraOne(mg, i)
}
}
}
/*
fun:通过 DeepTraOne()函数,遍历所有的节点
*/
func DeepTraGraph(mg *MGraph) {
//清除遍历标志位
for i := 0; i < mg.numVer; i++ {
mg.isTrav[i] = false
}
for i := 0; i < mg.numVer; i++ {
if mg.isTrav[i] == false {
DeepTraOne(mg, i)
}
}
fmt.Println()
}
func main() {
mg := MGraph{}
createMGraph(&mg)
ShowGraph(&mg)
fmt.Println("遍历图:")
DeepTraGraph(&mg)
}
运行效果:
注:图的类型没有设置,默认为无向图。如果要设置,添加 mg.GType=1即可。