[算法系列]数据结构之——图(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;
	}	
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值