实验要求:
针对无向图,通过邻接多重表方式进行存储。以节点A为起始节点,输出图的广度优先遍历的过程。
例如,对下图广度优先遍历的过程为:A-B-D-C-E。
程序输入:
文件data.txt
A,B,C,D,E,F,G,H
A-B
A-C
C-D
C-E
D-E
F-G程序输出:
图的广度优先遍历过程。
1.实验内容
针对无向图,通过邻接多重表方式进行存储。以节点A为起始节点,输出图的广度优先遍历的过程。例如,对下图广度优先遍历的过程为:A-B-D-C-E。
2.实验目的
1.掌握无向图的邻接多重表存储结构。
2.掌握图的广度优先搜索算法。
3.邻接多重表定义
当用邻接表存储时,访问或删除一条边(Vi,Vj) 时需要同时访问两个链表i 和j 并分别找到对应的边结点,这给针对图的边的操作(标记或删除)带来不便利。邻接多重表因此而演变过来的。
定义邻接多重表类GraphMultiplyTable ,类中有三个内部类Vertex、Edge、GraphEdge 。Vertex 为图的顶点类;String 类型的属性vName 指明了该顶点的标签,Edge 的属性firstEdge 存储第一条边的指针;int 类型的color 表明当前结点的颜色,用于遍历时使用。
/**
* vName-->顶点名称
* firstEdge-->顶点边链表的头结点
*/
public static class Vertex
{
private String vName;
private Edge firstEdge;
private int color;
public String getvName()
{
return vName;
}
public int getColor()
{
return color;
}
public void setColor(int color)
{
this.color = color;
}
public Vertex(String name)
{
this.vName = name;
}
public Edge getFirstEdge()
{
return firstEdge;
}
}
Edge 类为图的边类,int 类型的属性iVex 和jVex 指明了当前边所连的两个结点在结点列表中的索引;Edge 类型的属性iLink 和jLink 保存两个顶点的边链表指针。
/**
* iVex-->边的其中一个顶点A
* <p>
* iLink-->边中顶点A的边链表的指针
* <p>
* jVex-->边的另一个顶点B
* <p>
* jLink-->边中顶点B的边链表的指针
*/
public static class Edge
{
private int iVex;
private Edge iLink;
private int jVex;
private Edge jLink;
public int getiVex()
{
return iVex;
}
public Edge getiLink()
{
return iLink;
}
public int getjVex()
{
return jVex;
}
public Edge getjLink()
{
return jLink;
}
public Edge()
{
}
public Edge(int iVex, int jVex)
{
this.iVex = iVex;
this.jVex = jVex;
}
}
GraphEdge 类具有两个String 类型的属性vex 和otherVex ,用来初始化具有指明的结点的边。
/**
* 边的两个顶点
*/
public static class GraphEdge
{
private String vex;
private String otherVex;
public GraphEdge(String vexName, String otherVexName)
{
this.vex = vexName;
this.otherVex = otherVexName;
}
}
GraphMultiplyTable 类中还有属性vertexArr ,用来保存图的结点。
private List<Vertex> vertexArr; //顶点列表
4.源码+注释
4.1图的初始化
GraphMultiplyTable 类中具有init 方法,接收顶点集vexArr 和边集edgeList ,来初始化一个图。对于顶点集的初始化较为简单,只需将vexArr 中的标签分别创建vertex 对象并保存在vertexArr 中即可。初始化边集时,首先获取边的两个顶点vex 和otherVex ,若在顶点集中包含这两个顶点的话,则获取这两个顶点,并构造两个顶点的边,然后采用头插法将新边插入到两个顶点的边链表当中。
/**
* 初始化整个图
*
* @param vexArr 顶点集
* @param edgeList 边集
*/
public void init(List<String> vexArr, List<GraphEdge> edgeList)
{
initVexArr(vexArr);
initEdge(edgeList);
}
/**
* 初始化顶点集
*
* @param vexArr 顶点集
*/
private void initVexArr(List<String> vexArr)
{
vertexArr = new ArrayList<>(vexArr.size());
for (int i = 0; i < vexArr.size(); i++)
{
Vertex vertex = new Vertex(vexArr.get(i));
vertexArr.add(vertex);
}
}
/**
* 初始化边集
*
* @param edgeList 边集
*/
private void initEdge(List<GraphEdge> edgeList)
{
for (int i = 0; i < edgeList.size(); i++)
{
GraphEdge graphEdge = edgeList.get(i);
//如果顶点集中包含这两个顶点
if (isContains(graphEdge.vex) && isContains(graphEdge.otherVex))
{
//获取顶点的下标
int vIndex = getVexIndex(graphEdge.vex);
int oIndex = getVexIndex(graphEdge.otherVex);
//获取顶点
Vertex vertex = vertexArr.get(vIndex);
Vertex oVertex = vertexArr.get(oIndex);
//构造两个顶点的边
Edge edge = new Edge(vIndex, oIndex);
//头插法插入vertex的边
if (vertex.firstEdge == null) vertex.firstEdge = edge;
else
{
Edge vexNextEdge = vertex.firstEdge;
edge.iLink = vexNextEdge;
vertex.firstEdge = edge;
}
//头插法插入oVertex的边
if (oVertex.firstEdge == null) oVertex.firstEdge = edge;
else
{
Edge oVexNextEdge = oVertex.firstEdge;
edge.jLink = oVexNextEdge;
oVertex.firstEdge = edge;
}
}
}
}
4.2 BFS算法
bfs方法接收一个图的邻接表graph和开始遍历的结点source。首先对图的结点做预处理,将所有的结点设为白色。接下来初始化一个遍历队列queue,将源节点source加入到queue当中,并将source的颜色设为灰色。然后调用subBfs方法进行遍历,当队列不空时进行循环,首先取出队首元素赋给vertex,获取vertex的边链表指针edge。对于边链表中的每一条边edge,当edge不为null时,如果该edge的另一个顶点的颜色为WHITE,则将其加入到queue中并将其颜色设为灰色。当edge为null时,说明已经遍历完与vertex相连的所有的边,于是将vertex的颜色设为BLACK,并输出vertex的标签vName,而后继续循环。当该方法执行结束时,以BFS的策略输出了graph当中的一个连通分量。当以指定的source调用subBfs结束时,需检查vertexArr中的所有节点是否均不为WHITE,若还有节点为WHITE,则说明该图graph有多个连通分量,需继续遍历。
/*
* @Title:
* @Package
* @Description:
* @author yangf257
* @date 2022/1/2 10:57
*/
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
public class BFS
{
private static final int BLACK = 0;
private static final int WHITE = 1;
private static final int GRAY = 2;
public void bfs(GraphMultiplyTable graph, GraphMultiplyTable.Vertex source)
{
List<GraphMultiplyTable.Vertex> vertexArr = graph.getVertexArr();
//预处理 所有节点设为白色
for (GraphMultiplyTable.Vertex vertex : vertexArr)
vertex.setColor(WHITE);
//初始化队列,源节点设为灰色
Queue<GraphMultiplyTable.Vertex> queue = new ArrayDeque<>();
queue.add(source);
source.setColor(GRAY);
subBfs(queue, graph);
//检查是否还有未遍历结点
for (GraphMultiplyTable.Vertex vertex : vertexArr)
{
if (vertex.getColor() == WHITE)
{
Queue<GraphMultiplyTable.Vertex> queue1 = new ArrayDeque<>();
queue1.add(vertex);
vertex.setColor(GRAY);
subBfs(queue1, graph);
}
}
}
private void subBfs(Queue<GraphMultiplyTable.Vertex> queue,
GraphMultiplyTable graph)
{
//队列不空循环
while (!queue.isEmpty())
{
GraphMultiplyTable.Vertex vertex = queue.poll();
GraphMultiplyTable.Edge edge = vertex.getFirstEdge();
while (edge != null)
{
if (graph.getVertexArr().get(edge.getjVex()).getColor() == WHITE)
{
queue.add(graph.getVertexArr().get(edge.getjVex()));
graph.getVertexArr().get(edge.getjVex()).setColor(GRAY);
}
edge = edge.getiLink();
}
vertex.setColor(BLACK);
System.out.print(vertex.getvName() + " ");
}
System.out.println();
}
}
5.算法测试结果
读取data.txt中的数据,顶点为A,B,C,D,E,F,G,H,边集为A-B,A-C,C-D,C-E,D-E,F-G可知该图有三个连通分量ABCDE、FG、H。输入程序当中进行遍历,输出结果如下图所示
6.实验过程中遇到的困难及收获
通过本次实验实现了无向图的邻接多重表存储方式,对图的各种存储方式有了更深的理解,采用邻接表在对边进行操作时(如删除),需要两次操作,因为一条边在两个链表里面存储,而邻接多重表的优点就在于对边操作时只需要一次操作,这就意味着边只存储一次,可以更便捷的遍历;但是邻接多重表的实现复杂,结点中引入了许多指针域。
7.实验源代码
BFS.java
/*
* @Title:
* @Package
* @Description:
* @author yangf257
* @date 2022/1/2 10:57
*/
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
public class BFS
{
private static final int BLACK = 0;
private static final int WHITE = 1;
private static final int GRAY = 2;
public void bfs(GraphMultiplyTable graph, GraphMultiplyTable.Vertex source)
{
List<GraphMultiplyTable.Vertex> vertexArr = graph.getVertexArr();
//预处理 所有节点设为白色
for (GraphMultiplyTable.Vertex vertex : vertexArr)
vertex.setColor(WHITE);
//初始化队列,源节点设为灰色
Queue<GraphMultiplyTable.Vertex> queue = new ArrayDeque<>();
queue.add(source);
source.setColor(GRAY);
subBfs(queue, graph);
//检查是否还有未遍历结点
for (GraphMultiplyTable.Vertex vertex : vertexArr)
{
if (vertex.getColor() == WHITE)
{
Queue<GraphMultiplyTable.Vertex> queue1 = new ArrayDeque<>();
queue1.add(vertex);
vertex.setColor(GRAY);
subBfs(queue1, graph);
}
}
}
private void subBfs(Queue<GraphMultiplyTable.Vertex> queue,
GraphMultiplyTable graph)
{
//队列不空循环
while (!queue.isEmpty())
{
GraphMultiplyTable.Vertex vertex = queue.poll();
GraphMultiplyTable.Edge edge = vertex.getFirstEdge();
while (edge != null)
{
if (graph.getVertexArr().get(edge.getjVex()).getColor() == WHITE)
{
queue.add(graph.getVertexArr().get(edge.getjVex()));
graph.getVertexArr().get(edge.getjVex()).setColor(GRAY);
}
edge = edge.getiLink();
}
vertex.setColor(BLACK);
System.out.print(vertex.getvName() + " ");
}
System.out.println();
}
}
GraphMultiplyTable.java
/*
* @Title:
* @Package
* @Description:
* @author yangf257
* @date 2022/1/2 11:00
*/
import java.util.ArrayList;
import java.util.List;
/**
* 无向图的邻接多重表的实现<p>
* 优点: 使用邻接表在对边进行操作时(如删除),需要两次操作,因为一条边在两个链表里面存储,
* 而邻接多重表的优点就在于对边操作时只需要一次操作,这就意味着边只存储一次
*/
public class GraphMultiplyTable
{
/**
* vName-->顶点名称
* firstEdge-->顶点边链表的头结点
*/
public static class Vertex
{
private String vName;
private Edge firstEdge;
private int color;
public String getvName()
{
return vName;
}
public int getColor()
{
return color;
}
public void setColor(int color)
{
this.color = color;
}
public Vertex(String name)
{
this.vName = name;
}
public Edge getFirstEdge()
{
return firstEdge;
}
}
/**
* iVex-->边的其中一个顶点A
* <p>
* iLink-->边中顶点A的边链表的指针
* <p>
* jVex-->边的另一个顶点B
* <p>
* jLink-->边中顶点B的边链表的指针
*/
public static class Edge
{
private int iVex;
private Edge iLink;
private int jVex;
private Edge jLink;
public int getiVex()
{
return iVex;
}
public Edge getiLink()
{
return iLink;
}
public int getjVex()
{
return jVex;
}
public Edge getjLink()
{
return jLink;
}
public Edge()
{
}
public Edge(int iVex, int jVex)
{
this.iVex = iVex;
this.jVex = jVex;
}
}
/**
* 边的两个顶点
*/
public static class GraphEdge
{
private String vex;
private String otherVex;
public GraphEdge(String vexName, String otherVexName)
{
this.vex = vexName;
this.otherVex = otherVexName;
}
}
private List<Vertex> vertexArr; //顶点列表
public List<Vertex> getVertexArr()
{
return vertexArr;
}
/**
* 初始化整个图
*
* @param vexArr 顶点集
* @param edgeList 边集
*/
public void init(List<String> vexArr, List<GraphEdge> edgeList)
{
initVexArr(vexArr);
initEdge(edgeList);
}
/**
* 初始化顶点集
*
* @param vexArr 顶点集
*/
private void initVexArr(List<String> vexArr)
{
vertexArr = new ArrayList<>(vexArr.size());
for (int i = 0; i < vexArr.size(); i++)
{
Vertex vertex = new Vertex(vexArr.get(i));
vertexArr.add(vertex);
}
}
/**
* 初始化边集
*
* @param edgeList 边集
*/
private void initEdge(List<GraphEdge> edgeList)
{
for (int i = 0; i < edgeList.size(); i++)
{
GraphEdge graphEdge = edgeList.get(i);
//如果顶点集中包含这两个顶点
if (isContains(graphEdge.vex) && isContains(graphEdge.otherVex))
{
//获取顶点的下标
int vIndex = getVexIndex(graphEdge.vex);
int oIndex = getVexIndex(graphEdge.otherVex);
//获取顶点
Vertex vertex = vertexArr.get(vIndex);
Vertex oVertex = vertexArr.get(oIndex);
//构造两个顶点的边
Edge edge = new Edge(vIndex, oIndex);
//头插法插入vertex的边
if (vertex.firstEdge == null) vertex.firstEdge = edge;
else
{
Edge vexNextEdge = vertex.firstEdge;
edge.iLink = vexNextEdge;
vertex.firstEdge = edge;
}
//头插法插入oVertex的边
if (oVertex.firstEdge == null) oVertex.firstEdge = edge;
else
{
Edge oVexNextEdge = oVertex.firstEdge;
edge.jLink = oVexNextEdge;
oVertex.firstEdge = edge;
}
}
}
}
/**
* 顶点列表中是否包含当前顶点
*
* @param vName 顶点名称
*/
private boolean isContains(String vName)
{
for (Vertex vertex : vertexArr)
{
if (vertex.vName.equals(vName)) return true;
}
return false;
}
private int getVexIndex(String vName)
{
for (int i = 0; i < vertexArr.size(); i++)
{
if (vertexArr.get(i).vName.equals(vName)) return i;
}
return -1;
}
public void print()
{
for (Vertex vertex : vertexArr)
{
System.out.println("顶点 " + vertex.vName + " 的所有边: ");
int vIndex = getVexIndex(vertex.vName);
Edge cursor = vertex.firstEdge;
while (cursor != null)
{
System.out.print(cursor.iVex + "---" + cursor.jVex + " ||");
if (cursor.iVex == vIndex)
{
cursor = cursor.iLink;
}
else
{
cursor = cursor.jLink;
}
}
System.out.println();
}
}
}