[算法系列]數據結構之——圖(1):接口定義與實現+DFS+BFS

本文以實際代碼一步步地定義和實現了圖的基本定義與操作。本部分包括圖的定義,兩種遍歷,拓撲排序相關知識與實現。
本文中圖的實際結構與部分教材的鄰接表結構有微小差異,主要是用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。

下面還得判斷啥呢? 判斷這個圖中是否本來就包含一條從fromto的邊。 可以如下操作:

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方法中:

現在fromVertextoVertex是保證存在了,我們需要判斷是否在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爲存放拓撲排序的列表

  1. 把所有入度爲0的頂點放入L中,然後把這些頂點從圖中去掉
  2. 重複操作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;
	}	
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章