1、图论基础概念 Graph Theory
图 :是由由 节点 和 边 组成的数据模型,它有两个重要部分
- 1、节点
- 2、边
节点是两个村, 边表示两个村直接连通的道路
或者节点是人, 边表示人与人之间的关系。
点是一个域名, 边是域名之间的调整
无向图:边是没有方向的(如两个村是否有道路连接)
有向图:边有方向(人际关系网,你认识他,他不认识你)
有向图会使图更加复杂。具有不对称性。
可以把无向图认为是一种特殊有向图,是双向的。
无权图:每条边是只表示一种状态的,没有值,如人际关系网中,如果两个人之间是否连接只是表示认识与不认识,则无权图即可。如下示例:
有权图:如果两个人之间的连线除了表示 认识与不认识的状态,还要表示认识的程度,就可以加上一个数值表示, 5分熟还是9分熟。如下示例:
自环边:两个城市之间,不止一条了,可能有三条
平行边:一个城市自己向外扩散的路。
平行边和自环边不会改变 点与点的连通性,通常在较复杂图中才会涉及。
图的连通性: 图中的各个节点是否连通。
邻接矩阵:一个矩阵, 适合表示稠密图(Dense Graph),矩阵中每个位置标示两个点的连接状态。
邻接表:每一行是一个链表, 适合表示 稀疏图(Sparse Graph)。和节点0相连的只有节点1, 所以节点0的链表就只有 节点1一个点, 而节点1 和 0、2、3 三个节点相连,则这个链表存有三个节点。
稠密图:在所有节点中, 每个节点都和其中很多的其他节点连接,即每个点的边的数量 不会比节点数量少很多。
稀疏图(Sparse Graph):在所有节点中, 每个节点都和其中较少的其他节点连接,每个点的边的数量 远少于节点数量。
完全图:每个节点都和其他所有节点相连。
2 图的基本实现
邻接矩阵的实现(稠密图):
/**
* 用邻接矩阵来表示 无向图(用于稠密图)
* @author admin
*
*/
public class DenseGraph {
private int n;//存放图的节点数量
private int m;//存放图的边数量
private boolean directed;//是有向图还是无向图
private int[][] g;//创建矩阵
public DenseGraph(int n, boolean directed) {
this.n = n;
this.m = 0;
this.directed = directed;
g = new int[n][n];//先创建一个n阶矩阵,由于是int型, 所有值默认为0
}
/**
* 返回有多少节点
*/
public int getV() {
return n;
}
/**
* 返回有多少条边
*/
public int getE() {
return m;
}
/**
* 连接两个节点,添加两个节点的边
* @param v
* @param w
*/
public void addEdge(int v, int w) {
g[v][w] = 1;//两点连接, 用1表示
if(!directed) {
g[w][v] = 1;//如果是无向表,则两个方向的值都是相等的
}
if(!hasEdge(v, w)) {
m++; //边增加一条
}
}
/**
* 判断两个点是否有边
*/
private boolean hasEdge(int v, int w) {
return g[v][w] == 1;
}
}
邻接表的实现(稠密图):
/**
* 用邻接表 来表示 无向图(用于稀疏图)
* @author admin
*
*/
public class SparseGraph {
private int n;//存放图的节点数量
private int m;//存放图的边数量
private boolean directed;//是有向图还是无向图
ArrayList[] g;//邻接表
public SparseGraph(int n, boolean directed) {
this.n = n;
this.m = 0;
this.directed = directed;
g = new ArrayList[n];//创建n行的邻接表
for(int i=0; i<n; i++) {
g[i] = new ArrayList<>();
}
}
/**
* 返回有多少节点
*/
public int getV() {
return n;
}
/**
* 返回有多少条边
*/
public int getE() {
return m;
}
/**
* 连接两个节点,添加两个节点的边
* @param v
* @param w
*/
public void addEdge(int v, int w) {
g[v].add(w);//v和m相连, 则将m添加到v的链表中。
if(v!= w && !directed) { //本例中避免自环边的添加
g[w].add(v);
}
if(!hasEdge(v, w)) {
m++; //边增加一条
}
}
/**
* 判断两个点是否有边,避免平行边
*/
private boolean hasEdge(int v, int w) {
return g[v].contains(w);
}
}
3 图的常见操作----遍历邻边
按照上述实现的代码,在邻接矩阵中只需要遍历每个数组中哪些位置是1即可得到 与之直接相连的点。而邻接表中, 只需要 遍历每一行的链表即可得到直接连接的点, 很好实现。
但是,为了不对外暴露我们图的内部的具体实现,我们可以用一种 很常见很好用的设计模式– "迭代器模式, 来完全屏蔽我们内部对数据的实现,也不用对外直接暴露数据,防止数据被任意篡改,甚至能将接口往上进行抽象, 让不同图对外暴数据的接口相同,从而更好的抽象类。
通过迭代器,我们可以遍历整个图中的数据,去得到想查找的点 有哪些邻边。
在稠密图的实现中,我们增加一个内部类 作为迭代器:
static class GraphIterator {
private DenseGraph graph;//要遍历的图
private int n;//要遍历的节点
private int index;//脚标
public GraphIterator(DenseGraph graph, int n) {
this.graph = graph;
this.n = n;
}
public int begin() {
index = -1;
return next();
}
/**
* 返回相连节点的编号
* @return
*/
public int next() {
for(index += 1; index < graph.g[n].length; index++ ) {
if(graph.g[n][index] == 1) {
return index;
}
}
return -1;
}
public boolean isEnd() {
return index >= graph.g[n].length;
}
}
同理的,我们在稀疏图的实现中,增加一个内部类:
static class GraphIterator {
private SparseGraph graph;//要遍历的图
private int n;//要遍历的节点
private int index;//脚标
public GraphIterator(SparseGraph graph, int n) {
this.graph = graph;
this.n = n;
}
public int begin() {
index = -1;
return next();
}
/**
* 返回相连节点的编号
* @return
*/
public int next() {
for(index += 1; index < graph.g[n].size(); index++ ) {
if((int)graph.g[n].get(index) == 1) {
return index;
}
}
return -1;
}
public boolean isEnd() {
return index >= graph.g[n].size();
}
}