本文所有的图的操作都是针对上面所示的这个不带权无向图。
对于这个图我采用的是邻接矩阵来进行存储
1.图的结构定义:
下面是一个接口,我将图的基本方法进行了抽象。
public interface Graph {
/**
* 获得顶点数
* @return
*/
int getVex();
/**
* 获得边数
* @return
*/
int getEdge();
/**
* 判断两点间是否有边
* @param x 顶点x
* @param y 顶点y
* @return true:x,y之间有边,false:x,y之间没有边
*/
boolean hasEdge(int x,int y);
/**
* 添加一条边
* @param x 顶点x
* @param y 顶点y
*/
void addEdge(int x,int y);
/**
* 得到某一个顶点的邻接边节点
* @param v 顶点v
* @return 返回与顶点v相连接的顶点的集合
*/
List<Integer> adj(int v);
/**
* 打印这个图
*/
void showGraph();
下面是一个稠密图的结构定义,里面有成员变量vex表示这个图一共有多少个顶点,edge表示这个图一共有多少条边,directed表示这个图是否是有向图。如果directed为true表示该图是个有向图,false则表示该图是无向图。上面说过对于稠密图,我采用的方法是邻接矩阵,对于邻接矩阵本身就像一个二维表,所以我采用一个布尔类型的二维数组来表示。如果graph[x][y]==true则表示x顶点与y顶点之间存在一条边。
public class DenseGraph implements Graph {
private int vex;
private int edge;
private boolean directed;//是否为有向图
private boolean[][] graph;
public DenseGraph(int vex,int edge,boolean directed)
{
assert vex>=0;
this.vex=vex;
this.edge=edge;
this.directed=directed;
graph=new boolean[vex][vex];
}
@Override
public int getVex() {
return vex;
}
@Override
public void show() {
for(int i=0;i<vex;i++)
{
for(int j=0;j<vex;j++)
{
System.out.print(graph[i][j]+"\t");
}
System.out.println();
}
}
@Override
public int getEdge() {
return edge;
}
//对于添加一条边,如果是有向则按照方向添加一条边,由于本案例所涉及的是无向图所以x到y有边,同时y到x也有边
@Override
public void addEdge(int x, int y) {
assert x>=0&&x<vex;
assert y>=0&&y<vex;
//x,y之间存在边
if(hasEdge(x, y))
{
return;
}
if(!directed)
{
graph[y][x]=true;
}
graph[x][y]=true;
edge++;
}
@Override
public boolean hasEdge(int x, int y) {
assert x>0&&x<=vex;
assert y>0&&y<=vex;
return graph[x][y];
}
@Override
public List<Integer> adj(int v) {
assert v>=0&&v<vex;
List<Integer> vexList=new ArrayList<Integer>();
for(int i=0;i<vex;i++)
{
if(graph[v][i])
{
vexList.add(i);
}
}
return vexList;
}
}
2.计算联通分量
首先先来解释一下联通分量,如最上面的图所示,从任意一个节点出发都可以跑遍所有顶点(路路径可以重复),此时这个区域就是一个联通分量。如果旁边出现了一个与这个图一模一样的图 (读者可以自己画画)且旁边的图没有一个节点可以与上面的图相连,那么当我们把这两个图看作一个大的图时,这时大图中就有了两个联通分量。
计算联通分量的思路:本案例采用的是深度优先搜索来计算的。从任意点出发进行一次深度优先搜索遍历总是可以遍历整个图(前提是这个图是从一个节点可以跑遍所有节点的,即是一个联通分量),利用深度优先遍历的特点,我们只需要记录深度优先遍历开始的次数即可。
判断两个节点是否可以联通:在本案例中我有record数组来记录每一个节点所属的联通分量区域,如果两个节点在一个联通区域中那么这两个节点必然可以连通
//计算联通分量
public class Components {
//统计这个图的联通分量
private int count;
//记录是否被访问
private boolean[] visited;
//记录每个节点所对应的联通分量
private int[] record;
//图的引用
private Graph g;
//初始化信息
public Components(Graph g)
{
this.g=g;
count=0;
visited=new boolean[g.getVex()];
record=new int[g.getVex()];
for(int i=0;i<g.getVex();i++)
{
visited[i]=false;
record[i]=-1;
}
}
//深度优先遍历
private void dfs(int v)
{
//该节点被访问过了
if(visited[v])
return;
//该节点没有被访问
visited[v]=true;
record[v]=count;
List<Integer> adj = g.adj(v);
for(Integer i:adj)
{
dfs(i);
}
}
//计算联通分量
public int calculate()
{
for(int i=0;i<g.getVex();i++)
{
if(!visited[i])
{
dfs(i);
count++;
}
}
return count;
}
//求两个节点是否可以联通
public boolean isConnection(int x,int y)
{
assert x>=0&&x<g.getVex();
assert y>=0&&y<g.getVex();
return record[x]==record[y];
}
}
3.计算最短路径
最短路径:在无权图中,最短路径就是指从一个节点到另一个节点所经历节点数最少的那条路径。
//求无权无向图的最短路径
public class ShortestPath {
//图的引用
private Graph g;
//起始点
private int start;
//记录路径
private int[] from;
//记录长度
private int[] length;
//记录节点是否被访问过
private boolean[] visited;
//初始化图,进行寻路
public ShortestPath(Graph g,int start)
{
this.g=g;
this.start=start;
from=new int[g.getVex()];
length=new int[g.getVex()];
visited=new boolean[g.getVex()];
for(int i=0;i<g.getVex();i++)
{
visited[i]=false;
from[i]=-1;
length[i]=0;
}
//广度优先搜索遍历图,进行寻路
List<Integer> list=new ArrayList<Integer>();
list.add(start);
length[start]=0;
visited[start]=true;
while(!list.isEmpty())
{
Integer p = list.remove(0);
List<Integer> adj = g.adj(p);
for(int i=0;i<adj.size();i++)
{
int v=adj.get(i);
if(!visited[v])
{
list.add(v);
visited[v]=true;
from[v]=p;
length[v]=length[p]+1;
}
}
}
}
//判断从start到end是否存在路径
public boolean hasPath(int end)
{
return visited[end];
}
//将从start到end的路径存储到list中
public List<Integer> getPath(int end)
{
//不存在路径返回空
if(!hasPath(end))
return null;
Stack<Integer> s=new Stack<Integer>();
s.push(end);
int p=from[end];
while(p!=-1)
{
s.push(p);
p=from[p];
}
List<Integer> list=new ArrayList<Integer>();
while(!s.isEmpty())
{
list.add(s.pop());
}
return list;
}
//打印从start到end的路径
public void printPath(int end)
{
//判断是否存在路径
if(!hasPath(end))
{
System.out.println("no exist path between "+start+" and "+end);
return ;
}
List<Integer> path = getPath(end);
for(int i=0;i<path.size();i++)
{
System.out.print(path.get(i));
if(i!=path.size()-1)
{
System.out.print("->");
}
else
{
System.out.println();
}
}
}
//得到从start到end的路径长度
public int pathLength(int end)
{
//不存在路径返回-1
if(!hasPath(end))
{
System.out.println("no exist path between "+start+" and "+end);
return -1;
}
return length[end];
}
}