图的定义
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为: G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
- 线性表中把数据元素叫元素,树中将数据元素叫结点,在图中数据元素称之为顶点(Vertex)
- 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。**在图结构中,不允许没有顶点。**在定义中,强调了顶点集合V有穷非空。
- 线性表中,相邻的数据元素之间具有线性关系。树结构中,相邻两层的结点具有层次关系。图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
图的术语
无向图
若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge), 用无序偶对(Vi, Vj) 来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)。
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有nx(n-1)/2条边。
有向图
若顶点Vi到Vj之间的边有方向,则称这条边为有向边(Edge), 也称作弧。用有序偶<Vi, Vj> 来表示。Vi称作弧尾,Vj称作弧头。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Undirected graphs)。连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A, D>表示弧
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有nx (n-1) 条边。
网
与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网(Network)。
图的顶点与边的关系
对于无向图G= (V,{E}), 顶点v的度(Degree) 是和v相关联的边的数目,记为TD (v)。 顶点A的度为3。
对于有向图G= (V,{E}), 以顶点v为头的弧的数目称为v的入度(InDegree),记为ID (v); 以v为尾的弧的数目称为v的出度(OutDegree),记为0D (v); 顶点v的度为TD (v) =ID (v) +OD (v)。 顶点A的入度是2 (从B到A的弧,从C到A的弧),出度是1 (从A到D的弧),所以顶点A的度为2+1=3.
**第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle)。 序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。**两个图的粗线都构成环,左侧的环因第一个顶点和最后一个顶点都是B,且C、D、A没有重复出现,因此是一个简单环。而右侧的环,由于顶点C的重复,它就不是简单环了。
连通图
在无向图G中,如果从顶点v到顶点v'有路径,则称v和v'是连通的。如果对于图中任意两个顶点Vi、 vj∈E, Vi和Vj 都是连通的,则称G是连通图。
无向图中的极大连通子图称为连通分量。注意连通分量的概念,它强调:
-
要是子图;
-
子图要是连通的;
-
连通子图含有极大顶点数;
-
具有极大顶点数的连通子图包含依附于这些顶点的所有边。
在有向图G中,如果对于每一对Vi. vj∈V、Vi≠Vj, 从Vi到vj和从Vj到Vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
生成树
所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树
图的定义和总结
图按照有无方向分为无向图和有向图。无向图由顶点和边构成,有向图由顶点和弧构成。弧有弧尾和弧头之分。
图按照边或弧的多少分稀疏图和稠密图。
如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。
图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度,有向图顶点分为入度和出度。 图上的边或弧上带权则称为网。
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。
无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。
图的存储结构
邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
存储结构
static class MGraph {
String[] vexs; // 存储顶点的数组
int[][] arc; // 存储边的数组。
int numVertexes; // 顶点数
int numEdges; // 边数
public MGraph() {
this.vexs = new String[5];
this.arc = new int[5][5];
this.numVertexes = 5;
this.numEdges = 6;
}
}
创建图
static void createMGraph(MGraph m) {
// 由用户输入改为固定值
String[] inputVertexe = {"V0", "V1", "V2", "V3", "V4"};
int[][] inputEdge = {{0, 4, 6},{1, 0, 9},{1, 2, 3},{2, 0, 2},{2, 3, 5},{3, 4, 1}};
for(int i=0; i<m.numVertexes; i++) { // 存储顶点信息
m.vexs[i] = inputVertexe[i];
}
for (int i=0; i<m.numVertexes; i++) { // 矩阵初始化
for (int j = 0; j < m.numVertexes; j++) {
if(i == j) {
m.arc[i][j] = 0;
} else {
m.arc[i][j] = INFINITY;
}
}
}
for (int i = 0; i < m.numEdges; i++) { // 读入 inputEdge条边,建立邻接矩阵
int[] input = inputEdge[i];
int i1 = input[0];
int j1= input[1];
m.arc[i1][j1] = input[2];
//无向图矩阵对称,需要满足aij = aji 由于上图是有向图,注释
//m.arc[j1][i1] = m.arc[i1][j1];
}
}
测试
public static void main(String[] args) {
MGraph m = new MGraph();
createMGraph(m);
Arrays.stream(m.vexs).forEach(item -> System.out.printf("%-3s", item));
System.out.println();
Arrays.stream(m.arc).forEach(item -> {
Arrays.stream(item).forEach(item1 ->System.out.printf("%-6s", item1));
System.out.println();
});
}
**n个顶点和e条边的无向网图的创建,时间复杂度为0(n+n2+e),其中对邻接矩阵的初始化耗费了0(n2)的时间。 **
**无向网图和有向网图创建区别 **
创建矩阵时,无向网图需要满足aij = aji。有向网图则不需要
邻接表
邻接表的处理办法
1.图中顶点用一个一维数组存储。对于顶点数组,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
2.图中每个顶点Vi的所有邻接点构成-一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点Vi 的边表,有向图则称为顶点VI 作为弧尾的出边表。
存储结构
// 边表结点
static class EdgeNode {
int adjvex;
int weight;
EdgeNode next;
}
// 顶点表结点
static class VertexNode {
String data;
EdgeNode firstdge;
public VertexNode(String data) {
this.data = data;
this.firstdge = null;
}
}
static class GraphAdjList {
VertexNode[] adjList;
int numVertexes; // 顶点数
int numEdges; // 边数
public GraphAdjList() {
this.adjList = new VertexNode[5];
this.numVertexes = 5;
this.numEdges = 6;
}
}
创建图
static void createMGraph(GraphAdjList graph) {
String[] inputVertexe = {"V0", "V1", "V2", "V3", "V4"};
int[][] inputEdge = {{0, 4, 6},{1, 0, 9},{1, 2, 3}, {2, 0, 2},{2, 3, 5},{3, 4, 1}};
for(int i=0; i<graph.numVertexes; i++) {
VertexNode node = new VertexNode(inputVertexe[i]);
graph.adjList[i] = node;
}
for (int i = 0; i < graph.numEdges; i++) {
int[] input = inputEdge[i];
int vi = input[0];
int vj = input[1];
EdgeNode newNode = new EdgeNode();
newNode.adjvex = vj;
newNode.weight = input[2];
newNode.next = graph.adjList[vi].firstdge;
graph.adjList[vi].firstdge = newNode;
// 无向图时满足对称
// EdgeNode newNode1 = new EdgeNode();
// newNode1.adjvex = vi;
// newNode1.weight = input[2];
// newNode1.next = graph.adjList[vj].firstdge;
// graph.adjList[vj].firstdge = newNode1;
}
}
测试
public static void main(String[] args) { GraphAdjList m = new GraphAdjList(); createMGraph(m); Arrays.stream(m.adjList).forEach(item -> { if(item.firstdge != null) { System.out.print("顶点" + item.data + ": "); EdgeNode node = item.firstdge; while(node != null) { System.out.print("V" + node.adjvex + " -> weight:" + node.weight + " "); node = node.next; } System.out.println(); } });}
十字链表(对有向图邻接表的改造)
对于有向图来说,邻接表是有缺陷的。关心了出度问题,了解入度就必须要遍历整个图才能知道。逆邻接表解决了入度却不了解出度的情况。
于是就有了十字链表(Orthogonal List)。
左侧为邻接表的存储结构,右侧为十字链表的存储结构。
firstin表示入边表头指针,指向该顶点的入边表中第一个结点, firstout 表示出边表头指针,指向该顶点的出边表中的第一个结点。其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,tallink是指边表指针域,指向起点相同的下一条边。如果是网,还可以再增加一个weight域来存储权值。
顶点依然是存入一个-维数组{V0,V1,V2,V3},就以顶点V0来说,firstout 指向的是出边表中的第一个结点V3。所以V0边表结点的headvex=3,而tailvex 其实就是当前顶点V0的下标0,由于vo只有一个出边顶点,所以headlink和tillink都是空。
虚线箭头的含义,其实就是此图的逆邻接表的表示。对于v0来说,它有两个顶点v1和V2的入边。因此V0的firstin指向顶点V1的边表结点中headvex为0的结点,上图中的①。接着由入边结点的headlink指向下一个入边顶点V2,如图中的②.对于顶点v1,它有一个入边顶点V2,所以它的firstin指向顶点V2的边表结点中headvex为1的结点,如图中的③。顶点V2和V3也是同样有一个入边顶点,如图中④和⑤。
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起, 这样既容易找到以v为尾的弧,也容易找到以V为头的弧,因而容易求得顶点的出度和入度。
邻接多重表(对无向图邻接表的改造)
无向图的邻接表,关注的重点是顶点。邻接多重表关注的重点是边。
重新定义边表节点结构
ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,jlink 指向依附顶点jvex的下一条边。这就是邻接多重表结构。
结构图
边集数组
边集数组是由两个一维数组构成。一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、 终点下标(end) 和权(weight)组成。
边集数组关注的是边的集合
具体使用会在最小生成树中提到
图的遍历
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历(Traversing Graph)。
深度优先遍历(DFS)
深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。
**从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。**事实上,我们这里讲到的是连通图,对于非连通图,只需要对它的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后, 若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
实现
创建数组标记顶点是否被访问 private static Boolean[] visited;
邻接矩阵遍历
static void DFSTraverse(MGraph m) {
for (int i=0; i<m.numVertexes; i++) {
visited[i] = false;
}
for (int i = 0; i < m.numVertexes; i++) {
if(!visited[i]) {
DFS(m, i);
}
}
}
static void DFS(MGraph m, int i) {
visited[i] = true;
System.out.print(m.vexs[i] + " "); // 打印顶点,也可以进行其他操作
for (int j = 0; j < m.numVertexes; j++) {
if(!visited[j] && (m.arc[i][j] > 0 && m.arc[i][j] < INFINITY)) {
DFS(m, j);
}
}
}
邻接表遍历
static void DFSTraverse(GraphAdjList m) {
for (int i=0; i<m.numVertexes; i++) {
visited[i] = false;
}
for (int i = 0; i < m.numVertexes; i++) {
if(!visited[i]) {
DFS(m, i);
}
}
}
static void DFS(GraphAdjList m, int i) {
visited[i] = true;
System.out.print(m.adjList[i].data + " "); // 打印顶点,也可以进行其他操作
EdgeNode node = m.adjList[i].firstdge;
while(node != null) {
if(!visited[node.adjvex]) {
DFS(m, node.adjvex);
}
node = node.next;
}
}
邻接表使用的是链表的方式存储的。创建图时使用的是头插法,输出结果可能会因为输入的边的顺序而发生变化。
区别
对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要0(n2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是0(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。
对于有向图而言,区别只是弧存在或不存在,算法上没有变化,可以通用。
广度优先遍历(BFS)
广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。
将图第一幅图稍微变形,变形原则是顶点A放置在最上第一层,让与它有边的顶点B、F为第二层,再让与B和F有边的顶点C、I、G、E为第三层,再将这四个顶点有边的D、H放在第四层,如第二幅图所示。此时在视觉上感觉图的形状发生了变化,其实顶点和边的关系还是完全相同的。
实现
创建visited数组标记是否访问过,queue队列
private static Boolean[] visited = new Boolean[9];
private static Queue<Integer> queue = new ArrayDeque();
邻接矩阵遍历
static void BFSTraverse(MGraph m) {
for (int i = 0; i < m.numVertexes; i++) {
visited[i] = false;
}
for (int i=0; i<m.numVertexes; i++) { // 对每一个结点做循环
if(!visited[i]) { // 为访问过就处理
visited[i] = true;
System.out.print(m.vexs[i] + " ");
queue.add(i); // 将顶点的下标加入队列
while(!queue.isEmpty()) {
i = queue.remove(); // 出队,吧下标赋值给i,
for (int j = 0; j < m.numVertexes; j++) {
// 判断其他顶点与当前顶点存在边且顶点未访问过
if(!visited[j] && m.arc[i][j] > 0 && m.arc[i][j] < INFINITY) {
visited[j] = true; // 将找到的顶点标记为已访问
System.out.print(m.vexs[j] + " ");
queue.add(j); // 将找到的顶点加入队列
}
}
}
}
}
}
邻接表遍历
static void BFSTraverse(GraphAdjList m) {
for (int i = 0; i < m.numVertexes; i++) {
visited[i] = false;
}
for (int i=0; i<m.numVertexes; i++) { // 对每一个结点做循环
if(!visited[i]) {
visited[i] = true;
System.out.print(m.adjList[i].data + " ");
queue.add(i);
while(!queue.isEmpty()) {
i = queue.remove();
EdgeNode edgeNode = m.adjList[i].firstdge; // 找到当前顶点边表链表头指针
while(edgeNode != null) {
if(!visited[edgeNode.adjvex]) { // 若该顶点未被访问
visited[edgeNode.adjvex] = true;
System.out.print(m.adjList[edgeNode.adjvex].data + " ");
queue.add(edgeNode.adjvex);
}
edgeNode = edgeNode.next; // 指向下一个邻接点
}
}
}
}
}
深度优先遍历和广度优先遍历区别
图的深度优先遍历与广度优先遍历算法,在时间复杂度上是一样的,不同之处仅仅在于对顶点访问的顺序不同。
深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。