本文以实际代码一步步地定义和实现了图的基本定义与操作。本部分包括图的定义,两种遍历,拓扑排序相关知识与实现。
本文中图的实际结构与部分教材的邻接表结构有微小差异,主要是用Set替代了顶点中的链表,Map替代了存放顶点的顺序表。
public interface Graph<V,E> {
int edgesSize(); //边的个数
int verticesSize(); //顶点的个数
void addVertex(V v); //添加一个顶点,传入顶点值
void addEdge(V from , V to); //添加一条边,传入起始和终点的顶点
void addEdge(V from, V to , E e ); //添加一条边,传入起始和终点的顶点,同时传入权值e
void removeVertex(V v); // 根据顶点值删除一个顶点
void removedEdge(V from , V to); // 根据两个端点删除一条边
void bfs(V v);
void dfs(V v);
void dfs_iter(V v); //迭代版深搜
}
在具体实现中顶点和边需要两个类进行封装
问:为什么要对顶点Vertex
和边Edge
的封装?
public class ListGraph<V,E> implements Graph<V, E> {
/*
* 顶点类里面除了有值,还应该存有边
*/
private static class Vertex<V,E>{
V value;
public Vertex(V value) {
this.value = value;
}
/*
顶点中存入度和出度,由于没有顺序关系,这里用Set进行存储,访问速度更快
回忆以前的教材中,常常使用的是链表存出度
*/
Set<Edge<V, E>> inEdges = new HashSet<>();
Set<Edge<V, E>> outEdges = new HashSet<>();
}
/*
* 边类里面除了有权重,还应该有两个端点
*/
private static class Edge<V,E>{
E weight;
Vertex<V, E> from;
Vertex<V, E> to;
}
... ...
除此之外,我们试想,当添加一个顶点时,传入了实参V类型,内部应该会根据这个V去查找是否已经存在这个顶点,只有当不存在时才添加这个(新的)结点。
在以前的数据结构教材中,我们常常使用一个顺序表来存储所有的顶点,每次查询时遍历这个顺序表,这里用一个Map进行维护
/*
* 每一个V应该对应一个vertex
*/
private Map<V,Vertex<V,E>> vertices = new HashMap<>();
/*
* 维护所有的边 //方便我们计算edges的个数
*/
private Set<Edge<V, E>> edges = new HashSet<>();
自然地,顶点的个数即这个map的大小
@Override
public int verticesSize() {
return vertices.size();
}
@Override
public void addVertex(V v) {
//如果包含了直接返回
if(vertices.containsKey(v) && v != null) return;
//往顶点的hashmap中添加一对k-v
vertices.put(v, new Vertex<>(v));
}
@Override
public void addEdge(V from, V to) {
addEdge(from, to , null);
}
重点实现:
@Override
public void addEdge(V from, V to, E e) {
//判断from,to顶点是否存在 ,若没有,则要添加到vertices这个map中
//先要拿到from对应的vertex
Vertex<V, E> fromVertex = vertices.get(from);
//如果没有,就新创建起点顶点
if(fromVertex == null) {
fromVertex = new Vertex<>(from);
vertices.put(from, fromVertex);
}
//对终点同理
Vertex<V, E> toVertex = vertices.get(to);
if(toVertex == null) {
toVertex = new Vertex<>(to);
vertices.put(to, toVertex);
}
...
上述代码在添加边之前保证了是否存在传入的起点from
和终点to
存在对应的vertex。
下面还得判断啥呢? 判断这个图中是否本来就包含一条从from
到to
的边。 可以如下操作:
在fromVertex
中的outEdges
这个set去查一下,是否有一条到toVertex
的边
fromVertex.outEdges.contains(一条到toVertex的边);
这里有一个问题在于:本来contains内部是由equals实现的,这里判断两个顶点是否相等,是通过顶点的值来判断的,我们来完善顶点类:
private static class Vertex<V,E>{
V value;
Set<Edge<V, E>> inEdges = new HashSet<>();
Set<Edge<V, E>> outEdges = new HashSet<>();
public Vertex(V value) {
this.value = value;
}
//顶点vertex相等 取决于 传进来的值是否相等
@Override
public boolean equals(Object obj) {
return Objects.equals(value, ((Vertex<V, E>)obj).value) ;
}
@Override
public int hashCode() {
return value == null ? 0 : value.hashCode();
}
}
判断两个边是否相等,应该是起点相等并且终点相等.
完善Edge类:
private static class Edge<V,E>{
E weight;
Vertex<V, E> from;
Vertex<V, E> to;
public Edge(Vertex<V, E> from , Vertex<V, E> to) {
this.from = from;
this.to = to;
}
//边相等 = 起点相等并且终点相等
@Override
public boolean equals(Object obj) {
Edge<V, E> edge = (Edge<V,E>) obj;
return Objects.equals(from, edge.from) && Objects.equals(to,edge.to);
}
//重写hashCode方法
@Override
public int hashCode() {
return from.hashCode() * 31 + to.hashCode();
}
}
回到addEdge
方法中:
现在fromVertex
和toVertex
是保证存在了,我们需要判断是否在fromVertex中有一条到达toVertex的边:
//判断这个图中是否本来就包含一条从from 到to的边
Edge<V, E> edge = new Edge<>(fromVertex , toVertex);
if(fromVertex.outEdges.contains(edge)) {
//拿出那条边,更新权值
}
用于我们重写了equals方法和hashCode方法,现在contains底层检查边是否相等,不会因为是new 出来新的地址不同就返回false,而是实实在在检查两个顶点是否相等,检查两个顶点是否相等即检查顶点的值是否相等
//新建立一条从from 到to的边
Edge<V, E> edge = new Edge<>(fromVertex , toVertex);
edge.weight = weight;
//删掉老的那条边(如果有)
if(fromVertex.outEdges.contains(edge)) {
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
//将新创建的有新权值的加进去
fromVertex.outEdges.add(edge);
toVertex.inEdges.add(edge);
edges.add(edge);
ListGraph中添加打印图的函数:
public void print() {
vertices.forEach((V v, Vertex<V,E> vertex) ->{
System.out.println(v);
});
edges.forEach((Edge<V, E> edge) ->{
System.out.println(edge);
});
}
当然也得重写两个toString方法.
重起一个Main类,将下面这个图加入:
import cn.lowfree.graph.Graph;
import cn.lowfree.graph.ListGraph;
public class Main {
public static void main(String[] args) {
Graph<String, Integer> graph = new ListGraph<>();
graph.addEdge("V1","V0",9);
graph.addEdge("V1","V2",3);
graph.addEdge("V2","V0",2);
graph.addEdge("V2","V3",5);
graph.addEdge("V3","V4",1);
graph.addEdge("V0","V4",0);
graph.print();
}
}
res:
V0
V1
V2
V3
V4
Edge [weight=3, from=V1, to=V2]
Edge [weight=5, from=V2, to=V3]
Edge [weight=1, from=V3, to=V4]
Edge [weight=0, from=V0, to=V4]
Edge [weight=9, from=V1, to=V0]
Edge [weight=2, from=V2, to=V0]
@Override
public void removedEdge(V from, V to) {
//若果传进来的起点和终点有一个为空,则不存在该边,返回即可
Vertex<V, E> fromVertex = vertices.get(from);
Vertex<V, E> toVertex = vertices.get(to);
if(fromVertex == null || toVertex == null) return;
Edge<V, E> edge = new Edge<>(fromVertex, toVertex);
//删掉老的那条边(如果有)
if(fromVertex.outEdges.contains(edge)) {
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
}
@Override
public void removeVertex(V v) {
Vertex<V, E> vertex = vertices.remove(v);
if(vertex == null) return;
//成功把顶点删掉,并且把vertx拿到,现在来删边
//1.删掉从这个顶点中出去的边
for(Iterator<Edge<V, E>> iterator = vertex.outEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.to.inEdges.remove(edge); //根据这个出去的边,去找其终点顶点中的这个入边
iterator.remove(); //删除当前遍历到的元素从集合中删除
edges.remove(edge);
}
//2.删掉从进到这个顶点的边
for(Iterator<Edge<V, E>> iterator = vertex.inEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.from.outEdges.remove(edge); //根据这个进来的边,去找其终点顶点中的出边
iterator.remove(); //删除当前遍历到的元素从集合中删除
edges.remove(edge);
}
}
从图中某一顶点出发访问图中其余顶点,且每个顶点仅被访问一次
@Override
public void bfs(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return;
//标记是否被访问
Set<Vertex<V, E>> visitedVertices = new HashSet<>() ;
Queue<Vertex<V, E>> queue = new LinkedList<>();
queue.offer(beginVertex);
visitedVertices.add(beginVertex);
while(!queue.isEmpty()) {
Vertex<V, E> vertex = queue.poll();
System.out.println(vertex.value);
for(Edge<V, E> edge : vertex.outEdges) {
//被访问过了
if(visitedVertices.contains(edge.to)) continue;
queue.offer(edge.to);
visitedVertices.add(edge.to); //标记访问
}
}
}
递归版:
@Override
public void dfs(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return;
Set<Vertex<V, E>> visitedVertices = new HashSet<>();
dfs(beginVertex , visitedVertices);
}
private void dfs(Vertex<V, E> vertex , Set<Vertex<V, E>> visitedVertices) {
System.out.println(vertex.value);
visitedVertices.add(vertex);
for (Edge<V, E> edge : vertex.outEdges) { //对于每一个穿进去的顶点,找他的出边
if(visitedVertices.contains(edge.to)) continue; //如果出边的终点没有访问过
dfs(edge.to , visitedVertices);
}
}
非递归版:
public void dfs_iter(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return;
Set<Vertex<V, E>> visitedVertices = new HashSet<>();
Deque<Vertex<V,E>> stack = new ArrayDeque<>();
//先访问起点
stack.push(beginVertex);
System.out.println(beginVertex.value);
while(!stack.isEmpty()) {
Vertex<V, E> vertex = stack.pop();
for(Edge<V,E> edge: vertex.outEdges) {
if(visitedVertices.contains(edge.to)) continue;
stack.push(edge.from); ////这里要把起点也加到栈中去
stack.push(edge.to);
visitedVertices.add(edge.to);
System.out.println(edge.to.value);
break;
}
}
}
将AOV网中所有活动排成- -个列,使得每个活动的前驱活动都排在该活动的前面。比如上图的拓扑排序结果是:A、B、C、D、E、F或者A、B、D、C、E、F(结果并不一-定是唯- -的)
思路:
L为存放拓扑排序的列表
当然,实际操作中我们不会“把这些顶点从图中去掉”,我们用一个表来存储每个顶点及其他的入度,当“移除”这个表的时候,我们就在其outEdge的终点的入度减1.
@Override
public List<V> toologicalSort() {
List<V> res = new ArrayList<>(); //存放结果
Queue<Vertex<V, E>> queue = new LinkedList<>();
Map<Vertex<V, E>, Integer> ins = new HashMap<>(); //存储每个顶点的入度
//初始化,将度为0的顶点放入队列
vertices.forEach((V v, Vertex<V,E> vertex )->{
if(vertex.inEdges.size() == 0) {
queue.offer(vertex);
}else {
ins.put(vertex, vertex.inEdges.size());
}
});
while(!queue.isEmpty()) {
Vertex<V, E> vertex = queue.poll();
//放入返回结果中
res.add(vertex.value);
for(Edge<V,E> edge : vertex.outEdges) {
int toIn = ins.get(edge.to) - 1;
if(toIn == 0) {
queue.offer(edge.to);
}else {
ins.put(edge.to, toIn);
}
}
}
return res;
}
最后,目前阶段完整代码如下:
Graph.java
package cn.lowfree.graph;
import java.util.List;
public interface Graph<V,E> {
int edgesSize();
int verticesSize();
void addVertex(V v);
void addEdge(V from , V to);
void addEdge(V from, V to , E e );
void removeVertex(V v);
void removedEdge(V from , V to);
void print();
void bfs(V begin);
void dfs(V begin);
void dfs_iter(V begin);
List<V> toologicalSort();
}
ListGraph.java
package cn.lowfree.graph;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
import javax.swing.ListModel;
public class ListGraph<V,E> implements Graph<V, E> {
/*
* 顶点类里面除了有值,还应该存有边
*/
private static class Vertex<V,E>{
V value;
Set<Edge<V, E>> inEdges = new HashSet<>();
Set<Edge<V, E>> outEdges = new HashSet<>();
public Vertex(V value) {
this.value = value;
}
//顶点vertex相等 取决于 传进来的值是否相等
@Override
public boolean equals(Object obj) {
return Objects.equals(value, ((Vertex<V, E>)obj).value) ;
}
@Override
public int hashCode() {
return value == null ? 0 : value.hashCode();
}
@Override
public String toString() {
return value == null ? "null" : value.toString();
}
}
/*
* 边类里面除了有权重,还应该有两个端点
*/
private static class Edge<V,E>{
E weight;
Vertex<V, E> from;
Vertex<V, E> to;
public Edge(Vertex<V, E> from , Vertex<V, E> to) {
this.from = from;
this.to = to;
}
//边相等 = 起点相等并且终点相等
@Override
public boolean equals(Object obj) {
Edge<V, E> edge = (Edge<V,E>) obj;
return Objects.equals(from, edge.from) && Objects.equals(to,edge.to);
}
//重写hashCode方法
@Override
public int hashCode() {
return from.hashCode() * 31 + to.hashCode();
}
@Override
public String toString() {
return "Edge [weight=" + weight + ", from=" + from + ", to=" + to + "]";
}
}
/*
* 每一个V应该对应一个vertex
*/
private Map<V,Vertex<V,E>> vertices = new HashMap<>();
/*
* 维护所有的边
*/
private Set<Edge<V, E>> edges = new HashSet<>();
@Override
public int edgesSize() {
return edges.size();
}
@Override
public int verticesSize() {
return vertices.size();
}
@Override
public void addVertex(V v) {
//如果包含了直接返回
if(vertices.containsKey(v)) return;
//往顶点的hashmap中添加一对k-v
vertices.put(v, new Vertex<>(v));
}
@Override
public void addEdge(V from, V to) {
addEdge(from, to , null);
}
@Override
public void addEdge(V from, V to, E weight) {
//1.判断from,to顶点是否存在 ,若没有,则要添加到vertices这个map中
//先要拿到from对应的vertex
Vertex<V, E> fromVertex = vertices.get(from);
//如果没有,就新创建起点顶点
if(fromVertex == null) {
fromVertex = new Vertex<>(from);
vertices.put(from, fromVertex);
}
//对终点同理
Vertex<V, E> toVertex = vertices.get(to);
if(toVertex == null) {
toVertex = new Vertex<>(to);
vertices.put(to, toVertex);
}
//2.新建立一条从from 到to的边
Edge<V, E> edge = new Edge<>(fromVertex , toVertex);
edge.weight = weight;
//删掉老的那条边(如果有)
if(fromVertex.outEdges.contains(edge)) {
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
//将新创建的有新权值的加进去
fromVertex.outEdges.add(edge);
toVertex.inEdges.add(edge);
edges.add(edge);
}
@Override
public void removeVertex(V v) {
Vertex<V, E> vertex = vertices.remove(v);
if(vertex == null) return;
//成功把顶点删掉,并且把vertx拿到,现在来删边
//1.删掉从这个顶点中出去的边
for(Iterator<Edge<V, E>> iterator = vertex.outEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.to.inEdges.remove(edge); //根据这个出去的边,去找其终点顶点中的这个入边
iterator.remove(); //删除当前遍历到的元素从集合中删除
edges.remove(edge);
}
//2.删掉从进到这个顶点的边
for(Iterator<Edge<V, E>> iterator = vertex.inEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.from.outEdges.remove(edge); //根据这个进来的边,去找其终点顶点中的出边
iterator.remove(); //删除当前遍历到的元素从集合中删除
edges.remove(edge);
}
}
@Override
public void removedEdge(V from, V to) {
//若果传进来的起点和终点有一个为空,则不存在该边,返回即可
Vertex<V, E> fromVertex = vertices.get(from);
Vertex<V, E> toVertex = vertices.get(to);
if(fromVertex == null || toVertex == null) return;
Edge<V, E> edge = new Edge<>(fromVertex, toVertex);
//删掉老的那条边(如果有)
if(fromVertex.outEdges.contains(edge)) {
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
}
@Override
public void print() {
vertices.forEach((V v, Vertex<V,E> vertex) ->{
System.out.println(v);
});
edges.forEach((Edge<V, E> edge) ->{
System.out.println(edge);
});
}
@Override
public void bfs(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return;
//标记是否被访问
Set<Vertex<V, E>> visitedVertices = new HashSet<>() ;
Queue<Vertex<V, E>> queue = new LinkedList<>();
queue.offer(beginVertex);
visitedVertices.add(beginVertex);
while(!queue.isEmpty()) {
Vertex<V, E> vertex = queue.poll();
System.out.println(vertex.value);
for(Edge<V, E> edge : vertex.outEdges) {
//被访问过了
if(visitedVertices.contains(edge.to)) continue;
queue.offer(edge.to);
visitedVertices.add(edge.to); //标记访问
}
}
}
@Override
public void dfs(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return;
Set<Vertex<V, E>> visitedVertices = new HashSet<>();
dfs(beginVertex , visitedVertices);
}
private void dfs(Vertex<V, E> vertex , Set<Vertex<V, E>> visitedVertices) {
System.out.println(vertex.value);
visitedVertices.add(vertex);
for (Edge<V, E> edge : vertex.outEdges) {
if(visitedVertices.contains(edge.to)) continue;
dfs(edge.to , visitedVertices);
}
}
public void dfs_iter(V begin) {
Vertex<V, E> beginVertex = vertices.get(begin);
if(beginVertex == null) return;
Set<Vertex<V, E>> visitedVertices = new HashSet<>();
Deque<Vertex<V,E>> stack = new ArrayDeque<>();
//先访问起点
stack.push(beginVertex);
System.out.println(beginVertex.value);
while(!stack.isEmpty()) {
Vertex<V, E> vertex = stack.pop();
for(Edge<V,E> edge: vertex.outEdges) {
if(visitedVertices.contains(edge.to)) continue;
stack.push(edge.from); //这里要把起点也加到栈中去
stack.push(edge.to);
visitedVertices.add(edge.to);
System.out.println(edge.to.value);
break;
}
}
}
@Override
public List<V> toologicalSort() {
List<V> res = new ArrayList<>(); //存放结果
Queue<Vertex<V, E>> queue = new LinkedList<>();
Map<Vertex<V, E>, Integer> ins = new HashMap<>(); //存储每个顶点的入度
//初始化,将度为0的顶点放入队列
vertices.forEach((V v, Vertex<V,E> vertex )->{
if(vertex.inEdges.size() == 0) {
queue.offer(vertex);
}else {
ins.put(vertex, vertex.inEdges.size());
}
});
while(!queue.isEmpty()) {
Vertex<V, E> vertex = queue.poll();
//放入返回结果中
res.add(vertex.value);
for(Edge<V,E> edge : vertex.outEdges) {
int toIn = ins.get(edge.to) - 1;
if(toIn == 0) {
queue.offer(edge.to);
}else {
ins.put(edge.to, toIn);
}
}
}
return res;
}
}