无向图基础

无向图是一种最简单的图模型,在这种图模型中,仅仅是两个顶点之间的连接。我们用v-w的记法表示连接v和w的边,而w-v是这条边的另一种表示方法。

特殊的图。

  • 自环:即一条连接一个顶点和其自身的边;
  • 连接同一对顶点的两条边称为平行边。

简单术语介绍

当两个顶点通过一条边相连时,我们称这两个顶点是相邻的,并称这条边依附于这两个顶点。某个顶点的度数即为依附于它的边的总数。子图是由一幅图的所有边的一个子集(以及它们所依附的所有顶点)组成的图。

路径是由边顺序连接的一系列顶点。简单路径是一条没有重复顶点的路径。是一条至少含有一条边且起点和终点相同的路径。简单环是一条(除了起点和终点必须相同之外)不含有重复顶点和边的环。路径或者环的长度为其中所包含的边数。

如果从任意一个顶点都存在一条路径到达另一个任意顶点,我们称这幅图是连通图。一幅非连通的图由若干连通的部分组成,它们都是其极大连通子图。

树是一幅无环连通图。互不相连的树组成的集合称为森林。连通图的生成树是它的一幅子图,它含有图中的所有顶点且是一棵树。图的生成树森林是它的所有连通子图的生成树的集合。

《无向图基础》
一棵树
《无向图基础》
生成树森林

树的数学性质

当且仅当一幅含有V个结点的图G满足下列五个条件之一时,它就是一棵树:

  • G有V-1条边且不含有环
  • G有V-1条边且是连通的
  • G是连通的,但删除任意一条边都会使它不再连通
  • G是无环图,但添加任意一条边都会产生一条环
  • G中的任意一对顶点之间仅存在一条简单路径。

图的密度是指已经连接的顶点对占所有被连接的顶点对的比例。在稀疏图中,被连接的顶点对很少;而在稠密图中,只有少部分顶点对之间没有边连接。一般来说,如果一幅图中不同的边的数量在顶点总数V的一个小常数倍以内,那么我们就认为这幅图是稀疏的。

二分图是一种能够将所有结点分为两部分的图,其中图的每条边所连接的两个顶点都分别属于不同的部分。

表示无向图的数据类型

要开发处理图问题的各种算法,我们首先来看一份定义了图的基本操作的API

《无向图基础》
《无向图基础》 无向图的API

最常用的图处理代码

计算v的度数

public static int degree(Graph G, int v){
	int degree = 0;
	for (int w : G.adj(v))	degree++;
	return degree;
}

计算所有顶点的最大度数

public static int maxDegree(Graph G){
	int max = 0;
	for (int v = 0; v < G.V(); v++)
		if (degree(G,v) > max)
			max = degree(G,v);
	return max;
}

计算所有顶点的平均度数

public static double avgGegree(Graph G){
	return 2.0 * G.E() / G.V();
}

计算自环的个数

public static int numberOfSelfLoops(Graph G){
	int count = 0;
	for (int v = 0; v < G.V(); v++)
		for(int w : G.adj(v))
			if( v == w)		count++;
	return count/2;				//每条边被记录过两次
}

图的邻接表的字符串表示(Graph的实例方法)

public String toString(){
	String s = V + "vertices," + E + " edges\n";
	for (int v = 0; v < V; v++){
		s += v + ": ";
		for(int w : this.adj(v))
			s += w + " ";
		s += "\n";
	}
	return s;
}

图的几种表示方法

在这里我们需要面对的问题是用哪种方式(数据结构)来表示图并实现API,这包含以下两个要求。

  • 它必须为可能在应用中碰到的各种类型的图预留出足够的空间;
  • Graph的实例方法的实现一定要快——它们是开发处理图的各种用例的基础。

这些要求 比较模糊,但它们仍然能够帮助我们在三种图的表示方法中进行选择。

  • 邻接矩阵。我们可以用一个V乘V的布尔矩阵。当顶点v和顶点w之间有相连接的边时,定义v行w列的元素为true。这种表示方法不符合第一个条件——含有上百万个顶点的图是很常见的,V^2个布尔值所需要的空间是不能满足的。
  • 边的数组。我们可以使用一个Edge类,它含有两个int实例变量。这种表示方法很简洁但不满足第二个条件——要实现adj()需要检查图的所有边。
  • 邻接表数组。我们可以使用一个以顶点为索引的列表数组,其中的每个元素都是和该顶点相邻的顶点列表,如图。这种数据结构能够同时满足上面两点。

《无向图基础》
邻接表数组示意(无向图)

邻接表的数据结构

非稠密图的标准表示称为邻接表的数据结构,它将每个顶点的所有相邻顶点都保存在该顶点对应的元素所指向的一张链表中。我们使用这个数组就是为了快速访问给定顶点的邻接顶点列表。我们使用Bag这个抽象数据类型来实现这个链表,这样我们就可以在常数时间内添加新的边或遍历任意顶点的所有相邻顶点。

这种Graph的实现的性能有如下特点:

  • 使用的空间可V+E成正比
  • 添加一条边所需要的时间为常数
  • 遍历顶点v的所有相邻顶点所需要的时间和v的度数成正比(处理每个相邻顶点所需的时间为常数)

Graph数据类型

public class Graph {
	private final int V;		//顶点数目
	private int E;			//边的数目
	private Bag<Integer>[] adj;	//邻接表
	public Graph(int V) {
		this.V = V;	this.E = 0;
		adj = (Bag<Integer>[]) new Bag[V];			//创建邻接表
		for(int v = 0; v < V; v++)				//将所有链表初始化为空
			adj[v] = new Bag<Integer>();
	}
	public Graph(In in) {
		this(in.readInt());					//读取V并将图初始化
		int E = in.readInt();					//读取E
		for(int i = 0; i < E; i++ ) {
			//添加一条边
			int v = in.readInt();				//读取一个顶点
			int w = in.readInt();				//读取另一个顶点
			addEdge(v,w);					//添加一条连接他们的边
		}
	}
	public int V()	{	return V;	}
	public int E()	{	return E;	}
	public void addEdge(int v, int w) {
		adj[v].add(w);
		adj[w].add(v);
		E++;
	}
	public Iterable<Integer> adj(int v){
		return adj[v];
	}
}

这份Graph的实现使用了一个由顶点索引的整数链表数组。每条边都会出现两次,即当存在一条连接v与w的边时,w会出现在v的链表中,v也会出现在w的链表中。第二个构造函数从输入流中读取一幅图,开头是V,然后是E,再然后是一列整数对,大小在0到V-1之间

实际操作情况

在实际应用中还有一些操作可能是很有用的,例如

  • 添加一个顶点
  • 删除一个顶点

实现这些操作的一种方法是扩展之前的API,使用符号表(ST)来代替由顶点索引构成的数组(这样修改之后就不需要约定定点名必须是整数了)。

  • 删除一条边
  • 检查图是否含有边v-w

要实现这些方法可能需要使用SET来替代Bag来实现邻接表。我们称这种方法为邻接集

《无向图基础》
《无向图基础》 典型Graph实现的性能复杂度

    原文作者:SeYuFac
    原文地址: https://zhuanlan.zhihu.com/p/62410528
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞