本文以實際代碼一步步地定義和實現了圖的基本定義與操作。本部分包括圖的定義,兩種遍歷,拓撲排序相關知識與實現。
本文中圖的實際結構與部分教材的鄰接表結構有微小差異,主要是用Set替代了頂點中的鏈表,Map替代了存放頂點的順序表。
1.圖的基本操作接口定義
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); //迭代版深搜
}
2.一種具體實現——ListGraph
2.1 頂點和邊的定義
在具體實現中頂點和邊需要兩個類進行封裝
問:爲什麼要對頂點Vertex
和邊Edge
的封裝?
- 在對外的接口中,我們只對V或者E進行操作,也就是通過頂點值或權值對頂點和邊進行添加和刪除。
- 在內部實現中,頂點和邊並不是單獨孤立的,每一個頂點的出度入度,每一個邊的起點終點。是一種你中有我、我中有你的情形。
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();
}
2.2 添加頂點addVertex
@Override
public void addVertex(V v) {
//如果包含了直接返回
if(vertices.containsKey(v) && v != null) return;
//往頂點的hashmap中添加一對k-v
vertices.put(v, new Vertex<>(v));
}
2.3添加邊addEdge
@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);
2.4 測試一下吧
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]
2.5 刪除邊
@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);
}
}
2.6 刪除頂點
@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);
}
}
3 遍歷
從圖中某一頂點出發訪問圖中其餘頂點,且每個頂點僅被訪問一次
3.1 bfs
@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); //標記訪問
}
}
}
3.2 DFS
遞歸版:
@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;
}
}
}
4. AOV 網和 拓撲排序
4.1 AOV網
- 一項大的工程常被分爲多個小的子工程
- 子工程之間可能存在一定的先後順序,即某些子工程必須在其他的- -些子工程完成後才能開始
- 在現代化管理中,人們常用有向圖來描述和分析-項工程的計劃和實施過程,子工程被稱爲活動(Activity)
- 以頂點表示活動、有向邊表示活動之間的先後關係,這樣的圖簡稱爲AOV網
- 標準的AOV網必須是- -個有向無環圖(Directed Acyclic Graph,簡稱DAG)
- 前驅活動: 有向邊起點的活動稱爲終點的前驅活動
- 後繼活動:有向邊終點的活動爲起點的後繼活動
- 只有當一個活動的前驅全部都完成後,這個活動才能進行
4.2 拓撲排序
將AOV網中所有活動排成- -個列,使得每個活動的前驅活動都排在該活動的前面。比如上圖的拓撲排序結果是:A、B、C、D、E、F或者A、B、D、C、E、F(結果並不一-定是唯- -的)
思路:
L爲存放拓撲排序的列表
- 把所有入度爲0的頂點放入L中,然後把這些頂點從圖中去掉
- 重複操作1,直到找不到入度爲0的頂點
- 如果此時L中的元素個數和頂點總數相同,則top排序完成
- 如果此時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;
}
}