数据结构与算法分析——java语言描述第三版图论部分java实现,使用邻接表实现图论相关算法。

本文介绍了使用Java实现《数据结构与算法分析》书中第九章的图论部分,通过邻接表来实现图的相关算法。包括广度优先搜索用于解决无圈图的单源最短路径问题和关键路径分析,深度优先搜索在无向图中寻找割点,以及欧拉环游和Prim、Kruskal最小生成树算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇文章是笔者,学习《数据结构与算法分析——java语言描述第三版》一书的第九章图论部分,根据书中的提示加上自己的理解,编写的源代码。

注意点:

1.使用HashMap + LinkedList的方式来实现邻接表。

2.实现了广度优先搜索,及其应用(无圈图中的单源最短路径问题,关键路径分析),深度优先搜索及其应用(无向图中的割点寻找)

3.后续还会陆续更新第九章的其他问题。

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;


/**
 * 
 * 这次数据结构上的改进使用在ArrayVetex邻接表表示法中的改进想法:
 * 1. 不再使用数组结构来组织多个点的邻接表了。
 * 2. 改为使用HashMap + LinKedList来组织所有的顶点数据以及顶点之间的邻接顺序。
 * 
 * 选用这两种的组合时基于以下一个重要的理由:
 * 1. 每个顶点的邻接点个数数目在实际情况中一定不会多,也就是linkedList的size会是一个小常数。
 * 所以我们可以任何linkedList使用get,contain等遍历链表的线性方法,在这里可以看成是常数操作。
 * 
 * @version v1.0 无权图版本
 * 后期我再想想怎么更好的组织,融入有权版本,估计还是得新建一个内部私有类(类似ArrayVerex.Edge类)来实现。
 * 
 * @version v2.0 
 * 哎,还是老老实实的使用标准库中的hashmap吧,自己的虽然不错,但是我自己独立开发的hashmap功能实在是不健全,尤其是没有迭代器
 * 太伤了。
 * 
 * 
 * @author 25040
 *
 */
public class HashMapAdjacencyList<T> {
	
	public static void main(String[] args) {
		HashMapAdjacencyList<String> graph = new HashMapAdjacencyList<>(7);
		/*无权无圈图测试数据
		graph.builtAdjacency("v1", new String[] {"v2","v4"}, new int[] {1,1});
		graph.builtAdjacency("v2", new String[] {"v4","v5"}, new int[] {1,1});
		graph.builtAdjacency("v3", new String[] {"v1","v6"}, new int[] {1,1});
		graph.builtAdjacency("v4", new String[] {"v3","v5","v6","v7"}, new int[] {1,1,1,1});
		graph.builtAdjacency("v5", new String[] {"v7"}, new int[] {1});
		graph.builtAdjacency("v6", new String[] {}, new int[] {});
		graph.builtAdjacency("v7", new String[] {"v6"}, new int[] {1});
		*/
		/*有权无圈图的测试数据
		graph.builtAdjacency("v1", new String[] {"v2","v4"}, new int[] {2,1});
		graph.builtAdjacency("v2", new String[] {"v4","v5"}, new int[] {3,10});
		graph.builtAdjacency("v3", new String[] {"v1","v6"}, new int[] {4,5});
		graph.builtAdjacency("v4", new String[] {"v3","v5","v6","v7"}, new int[] {2,2,8,4});
		graph.builtAdjacency("v5", new String[] {"v7"}, new int[] {6});
		graph.builtAdjacency("v6", new String[] {}, new int[] {});
		graph.builtAdjacency("v7", new String[] {"v6"}, new int[] {1});
		*/
		/*割点测试数据
		graph.builtAdjacency("A", new String[] {"B","D"},new int[] {1,1});
		graph.builtAdjacency("B", new String[] {"A","C"},new int[] {1,1});
		graph.builtAdjacency("C", new String[] {"B","D","G"},new int[] {1,1,1});
		graph.builtAdjacency("D", new String[] {"A","C","E","F"},new int[] {1,1,1,1});
		graph.builtAdjacency("E", new String[] {"D","F"},new int[] {1,1});
		graph.builtAdjacency("F", new String[] {"D","E"},new int[] {1,1});
		graph.builtAdjacency("G", new String[] {"C"},new int[] {1});
		*/
		graph.builtAdjacency("start", new String[] {"A","B"},new int[] {3, 2});
		graph.builtAdjacency("A", new String[] {"C","D"},new int[] {3, 2});
		graph.builtAdjacency("B", new String[] {"D","E"},new int[] {2, 1});
		graph.builtAdjacency("C", new String[] {"F"},new int[] {3});
		graph.builtAdjacency("D", new String[] {"F","G"},new int[] {3, 2});
		graph.builtAdjacency("E", new String[] {"G","K"},new int[] {2, 4});
		graph.builtAdjacency("F", new String[] {"H"},new int[] {1});
		graph.builtAdjacency("G", new String[] {"H"},new int[] {1});
		graph.builtAdjacency("H", new String[] {"end"},new int[] {1});
		graph.builtAdjacency("K", new String[] {"H"},new int[] {1});
		graph.builtAdjacency("end", new String[] {},new int[] {});
		//System.out.println(graph.toString());
		//System.out.println(graph.breathFirstSearch("v3", "v7"));
		//System.out.println(graph.getShortestPathWithWeightedGraph("v3", "v5"));
		//System.out.println(graph.inDegree("v4"));
		//graph.depthFirstSearchAndShowPath("v1");
		//graph.findArt("A");
		graph.criticalPathAnalysis("start","end");
		
		
		
	}
	
	//属性存储一个HashMap
	private HashMap<T, OneVetexAdjacencyList<T>> map;
	private int currentSize;
	
	/**
	 * 构造
	 * @param 图的当前
	 */
	public HashMapAdjacencyList(int vetexNum) {
		this.map = new HashMap<>(vetexNum);
		this.currentSize = 0;
	}
	
	/**
	 * 清空邻接表
	 */
	public void makeEmpty() {
		this.currentSize = 0;
		map.clear();
	}
	
	/**
	 * 建立两个顶点的连接。
	 * 注意时有向的,前一个参数代表的顶点->后一个参数代表的顶点
	 * 
	 * @param vetex
	 * @param otherVetex
	 */
	public void bulitAdjacency(T selfVetex, T otherVetex) {
		//调用map的put方法,如果self顶点不存在,则新建邻接表加入到map中,返回true,否则如果self顶点存在,不作操作,返回false
		if(this.map.put(selfVetex, new OneVetexAdjacencyList<>(selfVetex)) != null) {
			++this.currentSize;
		}
		OneVetexAdjacencyList<T> adjacencyList = this.map.get(selfVetex);
		
		adjacencyList.insertAdjcancyVetex(otherVetex);
		
	}
	
	/**
	 * 建立当前self顶点与其他多个顶点的邻接关系
	 * 
	 * @param selfVetex
	 * @param otherVetexs
	 */
	public void builtAdjacency(T selfVetex, T[] otherVetexs, int[] allEdgeWeight) {
		if(this.map.put(selfVetex, new OneVetexAdjacencyList<>(selfVetex)) != null) {
			++this.currentSize;
		}
		OneVetexAdjacencyList<T> adjacencyList = this.map.get(selfVetex);


		adjacencyList.insertAdjacencyVetex(otherVetexs, allEdgeWeight);
	}
	
	/**
	 * 格式化输出
	 */
	public String toString() {
		return this.map.toString();
	}
	
	/**
	 * 返回当前图中有多个少顶点,即有多少个邻接表
	 * @return
	 */
	public int size() {
		return this.currentSize;
	}
	
	/**
	 * 无权无圈图的广度优先搜索求最短路径问题。
	 * 使用LinkedList和HashMap来完成。其中linkedList完成的更像是队列的工作。
	 * 
	 * 那么问题来了,为什么这样做,返回的是最短路径。
	 * 因为,其实理由和书上一样的。当我们添加currDist + 1的那些邻接点,更远的邻接点时,我们让它从队尾入队。
	 * 这样保证了一定是先处理完成了当前currDist的所有节点后,在处理更远的邻接点。而更早处理的点,更早占据map的位置,
	 * 我们自然可以认为它相对于后来者具有更短的路径。也就是书中已经known了。
	 * 
	 * 返回路径的边对象的链表。储存了最短路径存储了经过了那些节点,其权值的含义在这里变成了各节点到起点的间距。
	 * 如果没有路径能从起点到终点,返回null
	 * 
	 * @param startVetex
	 * @return
	 */
	public List<T> breathFirstSearch(T startVetex, T endVetex) {
		HashMap<T,T> pathMap = new HashMap<>();
		LinkedList<T> q = new LinkedList<>();
		//让起点入队。
		q.addLast(startVetex);
		while(! q.isEmpty()) {
			//获取当前的节点的邻接点的列表,并且从队列中删除它
			T nowNode = q.removeFirst();
			OneVetexAdjacencyList<T> adjList = this.map.get(nowNode);
			LinkedList<Edge<T>> edgeList = adjList.getAdjacencyVetexList();
			//遍历所有的邻接节点
			for (Edge<T> edge : edgeList) {
				T adjVetex = edge.getNextVetex();
				//判断当前节点是否已经known了,即已经被遍历过了
				if(pathMap.get(adjVetex) == null) {
					//known为false的节点,存储了节点路径。
					pathMap.put(adjVetex, nowNode);
					q.addLast(adjVetex);
				}
			}
		}
		
		pathMap.put(startVetex, null);
		
		return getPathFromMap(pathMap, startVetex, endVetex);
	}
	
	/**
	 * 从生成的map路径表中,返回正向的路径。
	 * @param pathMap
	 * @param startVetex
	 * @param endVetex
	 * @return
	 */
	private LinkedList<T> getPathFromMap(HashMap<T, T> pathMap, T startVetex, T endVetex) {
		LinkedList<T> result = null;
		
		if(!pathMap.isEmpty()) {
			result = new LinkedList<>();
			//从尾部开始
			for(T vetex = endVetex;vetex != null;vetex = pathMap.get(vetex))
				result.addFirst(vetex);
		}
		
		return result;
	}
	
	/**
	 * 使用广度优先搜索解决有权无圈图中的单源最短路径问题。莫名其妙相除的版本。
	 * 其实是我错误地理解书上的意思,误打误撞出来的。过程根本不贪,但结果是贪婪的。
	 * 
	 * 图中不能包含负值边。
	 * 
	 * 思路还是根广度优先搜索的思路一样,按照邻接点的层次层层往更远的节点扩展,直到覆盖所有节点。
	 * 该种算法是我根据书中的算法自己想的,其根书上有点不太一样,我目的是从起点出发遍历更新所有的边。
	 * 跟贪婪算法不太一样的地方在于,该算法不要求每轮都选择最短距离的未知的节点。而是跟广度优先搜索一样。遍历所有的边。
	 * known用来判断该点是否进入队列,即其所有的邻接点是否都被处理的一遍。
	 * 同时只要 邻接点的路径信息满足  pathMap.get(nowNode).dv + weight < adjVetexInfo.dv  就表示该邻接点可以由更短的路径过来。
	 * 因此更新其pv和dv值,不论该邻接点是否known。
	 * 因此分析的时间界限为O(E),跟顶点数无关
	 * 测试通过。
	 * 
	 * @param startVetex 输入节点
	 * @param endVetex 输出节点
	 * @return
	 */
	public List<Edge<T>> getShortestPathWithWeightedGraph(T startVetex, T endVetex){
		/**
		 * 方法内部类
		 * @author 25040
		 *
		 */
		class pathInfo{
			//标记节点是否被标记过了
			boolean known;
			//记录当前节点到起点的最短权值
			int dv;
			//用于最短路径的回溯
			T prevNode;
			
			public pathInfo() {
				this(false, Integer.MAX_VALUE, null);
			}
			
			public pathInfo(boolean known, int dv, T prevNode) {
				super();
				this.known = known;
				this.dv = dv;
				this.prevNode = prevNode;
			}
			
			public String toString() {
				return ("known:" + known + ", dv:" + dv + ", pv:" + prevNode + "\n");
			}
		}
		
		/*
		 * 下面正式开始进行搜索算法。 
		 */
		
		//变量初始化
		HashMap<T, pathInfo> pathMap = new HashMap<>();
		LinkedList<T> queue = new LinkedList<>();
		
		//遍历所有的邻接点,将它在table中初始化下
		for(Entry<T,OneVetexAdjacencyList<T>> entry : this.map.entrySet()) {
			pathMap.put(entry.getKey(), new pathInfo());
		}
		//将起点设置好
		pathMap.put(startVetex, new pathInfo(true, 0, null));
		
		queue.addLast(startVetex);
		
		/*
		 * 使用广度优先搜索方式来遍历所有边,更新所有的节点距离。
		 */
		while(! queue.isEmpty()) {
			//获取当前的节点的邻接点的列表,并且从队列中删除它
			T nowNode = queue.removeFirst();
			//当前节点known状态置为true,这神级麻烦的map修改方式
			pathInfo nowNodeInfo = pathMap.get(nowNode);
			nowNodeInfo.known = true;
			
			//获取邻接表
			OneVetexAdjacencyList<T> adjList = this.map.get(nowNode);
			LinkedList<Edge<T>> edgeList = adjList.getAdjacencyVetexList();
			
			//遍历当前节点所有的有权边
			for(Edge<T> eachEdge : edgeList) {
				//获取当前边对应的节点,以及权值
				T adjVetex = eachEdge.getNextVetex();
				int weight = eachEdge.getWeight();
				
				//从路径信息表中取出当前邻接点对应的数据
				pathInfo adjVetexInfo = pathMap.get(adjVetex);
				//判断当前边的权值 + 当前的节点过往的总路径(权值和)是否 < 邻接点记录的值
				//更新数据和进队分开
				if(pathMap.get(nowNode).dv + weight < adjVetexInfo.dv) {
					//更新数据,并且入队。
					adjVetexInfo.dv = pathMap.get(nowNode).dv + weight;
					adjVetexInfo.prevNode = nowNode;
					//如果该节点还没有进队,用于之后遍历该邻接点的所有邻接点
					if(!adjVetexInfo.known)
						queue.addLast(adjVetex);
				}
			}
		}
		
		/*
		 * 通过回溯来显示正常的路径
		 */
		LinkedList<Edge<T>> correctPath = null;
		if(!pathMap.isEmpty()) {
			correctPath = new LinkedList<>();
			for(T vetex = endVetex; vetex != null; vetex = pathMap.get(vetex).prevNode) {
				correctPath.addFirst(new Edge<>(pathMap.get(vetex).dv, vetex));
			}
		}
		return correctPath;
	}
	
	/**
	 * 计算单个节点的入度。
	 * 计算入度就比较麻烦了。得遍历所有节点的所有邻接表来确认。
	 * 
	 * 
	 * @param x
	 * @return
	 */
	public int inDegree(T x) {
		int result = 0;
		
		//遍历所有邻接表
		for(Entry<T, OneVetexAdjacencyList<T>> entry : this.map.entrySet()) {
			//如果是当前节点的邻接表完全可以跳过。
			if(x.equals(entry.getKey())) {
				continue;
			}
			//获取边的list
			OneVetexAdjacencyList<T> thisAdjList = entry.getValue();
			LinkedList<Edge<T>> edgeList = thisAdjList.getAdjacencyVetexList();
			//遍历所有边,即所有当前邻接点的所有顶点
			for (Edge<T> edge : edgeList) {
				//如果别的顶点的邻接表内的邻接点是x的画,计数器+1
				if(x.equals(edge.getNextVetex())) {
					++result;
				}
			}
		}
		return result;
	}
	
	/**
	 * 计算单个阶段的出度。即返回当前顶点的邻接表内输出边的条数
	 * @param x
	 * @return
	 */
	public int outDegree(T x) {
		OneVetexAdjacencyList<T> thisList = this.map.get(x);
		return thisList.getAdjacencyVetexList().size();
	}
	
	/**
	 * 使用深度优先搜索的方式搜索某个节点,并且返回搜索路径。
	 * 为了实现书中每个节点有个visited数据域。为了将他方便的集成,
	 * 我们在方法内部建立一个map<T,boolean>这样的hashMap来实现对各节点的访问标记。
	 * 虽然现在看上去这个例程还没有什么实际的用处和意义,书上后面应该会讲。
	 * 
	 * @param startVetex
	 * @return
	 */
	public void depthFirstSearchAndShowPath(T startVetex) {
		HashMap<T,Boolean> visited = new HashMap<>();
		//将map初始化为全false
		for(T key:this.map.keySet()) {
			visited.put(key, false);
		}
		
		dps(visited, startVetex);
	}
	
	/**
	 * 私有的递归的深度优先搜索例程。
	 * @param visited
	 * @param x
	 */
	private void dps(HashMap<T,Boolean> visited, T x) {
		//将当前节点的访问标记置为true
		visited.put(x, true);
		
		//bug 1 遍历当前节点的邻接表,而不是遍历所有节点
		//获取对应顶点的邻接表
		LinkedList<Edge<T>> adjVetexList = this.map.get(x).getAdjacencyVetexList();
			
		//遍历所有的边。即所有的顶点
		for (Edge<T> edge : adjVetexList) {
			//检查判断该店是否已经被访问过了
			T nowAdjNode = edge.getNextVetex();
			if(! visited.get(nowAdjNode)) {
				//递归进入邻接点的深度优先搜索
				System.out.print(nowAdjNode);
				dps(visited, nowAdjNode);
			}
		}
		//test
		System.out.println();
	}
	
	/*
	 * 为了完成割点的寻找,需要在每个添加额外的数据域。我们使用hashMap<T,additionalInfo>的形式来存储。
	 * additionalInfo的信息以方法内部类的形式来存储。
	 */
	private class ArtInfo{
		int num;//搜索编号
		int low;//当前节点所能达到的最低节点
		T parent;
		boolean visited;
		
		public ArtInfo(int num, int low, T parent, boolean visited) {
			super();
			this.num = num;
			this.low = low;
			this.parent = parent;
			this.visited = visited;
		}
	}
	/*
	 * 为了给深度优先生成树的节点编号,需要一个类变量来记录
	 */
	private int count;
	
	/**
	 * 使用深度优先搜索来检查无向图中的割点
	 * 还未进行起点是否是割点的特殊判断
	 * @param startNode 深度优先开始的起点
	 */
	public void findArt(T startNode) {
		//初始化额外添加数据域的HashMap
		HashMap<T, ArtInfo> addInfo = new HashMap<>();
		for(T key : this.map.keySet()) {
			addInfo.put(key, new ArtInfo(0, 0, null, false));
		}
		count = 0;
		assignNum(startNode, addInfo);
		//重置所有节点的访问标志 bug 1
		for(T key : this.map.keySet()) {
			ArtInfo nodeInfo = addInfo.get(key);
			nodeInfo.visited = false;
		}
		//bug 2 将A的parent置为本身
		ArtInfo startInfo = addInfo.get(startNode);
		startInfo.parent = startNode;
		//再进行low的深度优先搜索设置,
		assignLow(startNode, addInfo);
	}
	
	/**
	 * 还是以深度优先搜索的方式递归的遍历所有的节点。
	 * 1. 通过判断节点visited数据域来避免重复。
	 * 2. 通过判断当前节点v和邻接点w的num数据域可以得知当前的边是前向边,还是背向边。
	 * 	   2.1 当w.num > v.num && w.visited时,为前向边.
	 * 	   2.2 当w.num < v.nums时,并且当v.parent == w时,为已经处理过需要忽略的边,并且当v.parent != w 时,就是背向边了。
	 * 3. 在2.1中即前向边的情况下,根据深度优先的思想,直接递归到下一节点,然后递归完成后,判断w.low >= v.num说明v为割点。
	 *    可以画出深度优先生成树来理解,>=代表的深度更深,如果邻接点的能达到的最低深度都等于当前节点,那么当前节点一定时割点。
	 *    大于的话,说明还有其他割点。最后,更新当前节点的low值。由于深度优先搜索的关系,较小的low值会从深度较深的地方,一直向上更新。
	 *    因此,min(w.low, v.low)即可。
	 * 4.而在2.2中,背向边的情况,只需要把当前节点low值更新好就行。即v.low = min(v.low, w.num)
	 * 
	 * 值得注意的是,当判断搜索起始点是否是割点,不能使用上诉条件,需要加上一条。
	 * 即深度优先搜索树的根节点有一个以上的儿子(两个及以上),则它是割点。因为,如果这两个儿子如果能够通过其他路径到达对方。
	 * 那么一定不可能需要返回到A来转移。说明两儿子之间必须通过根节点联通,这种情况下根节点就是割点。
	 * 再次遍历根节点的邻接表,查看邻接点的parent,如果parent = root的邻接点数目>1,那么根节点为割点,否则就不是
	 * 
	 * @param v
	 * @param addInfo
	 */
	private void assignLow(T v, HashMap<T, ArtInfo> addInfo) {
		//首先,二话不说先初始化下当前节点low值
		ArtInfo vInfo = addInfo.get(v);
		if(vInfo.num == 0) {
			throw new IllegalArgumentException("请先对各节点进行深度优先搜索编号!");
		}
		vInfo.low = vInfo.num;
		//bug 3 又Tm忘记置未被访问标志了
		vInfo.visited = true;
		//遍历当前节点的邻接表
		LinkedList<Edge<T>> adjList = this.map.get(v).getAdjacencyVetexList();
		for (Edge<T> edge : adjList) {
			//获取邻接点的名字
			T w = edge.getNextVetex();
			//获取邻接点的信息
			ArtInfo adjInfo = addInfo.get(w);
			//前向边的情况,在书上伪代码的基础上,添加了对visited数据域的检查。
			if(!adjInfo.visited && adjInfo.num > vInfo.num) {
				//深度优先搜索递归
				assignLow(w, addInfo);
				//更新当前节点的low值
				vInfo.low = Math.min(adjInfo.low, vInfo.low);
				//递归搜索完成后判断当前节点是否是割点
				if(adjInfo.low >= vInfo.num) {
					//当v是根节点的时候,需要进行特殊的测试
					if(v.equals(vInfo.parent)) {
						int cnt = 0;
						for(Edge<T> twiceEdge : adjList) {
							if(addInfo.get(twiceEdge.getNextVetex()).parent.equals(v)) {
								++cnt;
							}
						}
						if(cnt <= 1) {
							continue;
						}
					}
					
					System.out.println("割点:" + v);
				}
			}
			else {
				//判断是否是背向边,注意到背向边的w节点是已经访问过的
				//如果当前节点的父亲等于邻接表,那么代表这条边已经背访问过了。不需要反向重复操作
				//bug 2 对null调用eqaul方法,将起点的parent置为它本身
				if(! vInfo.parent.equals(w)) {
					vInfo.low = Math.min(vInfo.low, adjInfo.num);
				}
			}
		}
	}


	/**
	 * 以深度优先搜索的方式递归给深度优先搜索生成树的相关节点编号。
	 * 用于推断计算无向图中的割点。
	 * 
	 * 思路和步骤:
	 * 1. 遍历当前节点的邻接表。
	 * 2. 当v.visited标志为false时,把当前节点的num数据域赋值编号(编号数值的大小代表该节点在生成树的深度。)否则跳过该邻接点。
	 * 3. 将1,2步骤递归到该邻接点上。
	 * 
	 * @param x
	 * @param additionalInfo
	 */
	private void assignNum(T x, HashMap<T, ArtInfo> addInfo) {
		ArtInfo thisVetexInfo = addInfo.get(x);
		thisVetexInfo.num = ++count;
		thisVetexInfo.visited = true;//bug 1 忘记给当前节点设置被访问标志了。
		//遍历该节点的邻接表,跟广度不一样,深度是递归例程,广度是迭代例程不一样
		//获取对应顶点的邻接表
		LinkedList<Edge<T>> adjVetexList = this.map.get(x).getAdjacencyVetexList();
		for (Edge<T> edge : adjVetexList) {
			//获取边对象对应的节点
			T adjVetex = edge.getNextVetex();
			//检查该节点的访问标记
			if(!addInfo.get(adjVetex).visited) {
				//获取当前邻接点相关信息。
				ArtInfo adjVetexInfo = addInfo.get(adjVetex);
				//记住父节点的路径信息。
				adjVetexInfo.parent = x;
				//以深度优先搜索的方式往下继续编号。
				assignNum(adjVetex, addInfo);
			}
		}
	}
	
	/**
	 * 同计算最短路径的算法一样,使用方法内部类 + HashMap 的方式向节点上添加额外信息。
	 * 
	 * EC:保存当前节点的最早完成事件
	 * LC:保存当前节点可以拖延的最大事件。
	 * visited:已访问标志
	 * allPreNode:链表,保存当前节点所有的前置节点。
	 * 
	 * @author 25040
	 *
	 */
	private class CriticalPathInfo {
		int EC;//
		int LC;
		int ST;
		boolean visited;
		LinkedList<T> allPreNode;//相当于构建了该图的反向邻接表
		
		public CriticalPathInfo(int eC, int lC, int ST, boolean visited) {
			super();
			EC = eC;
			LC = lC;
			this.ST = ST;
			this.visited = visited;
			this.allPreNode = new LinkedList<>();
		}
		
		public void addPreNode(T preNode) {
			this.allPreNode.addLast(preNode);
		}
	}
	
	/**
	 * 从起点开始进行关键路径分析。
	 * 请务必确保当前对象的图是有权有向无圈图,才可保证计算的正确性。
	 * 同时,该函数会将该图当成动作节点图来处理。请注意务必添加开始和结束节点。
	 * 
	 * 当前函数通过广度优先搜索的方式,使用队列来层层递进的进行计算。
	 * 所有节点的最早完成时间原理。公式和思路:
	 * 基本原理:
	 * EC_1 = 0;
	 * EC_i	= max{EC_w + weight_w,v};
	 * 上诉表达式就是说当前节点最早完成时间,由所有(前置节点的最早完成时间 + 前置节点到当前节点的权值)的最大值决定。
	 * 
	 * 设E为当前事件节点最早完成事件
	 * 
	 * 思路和基本步骤:
	 * 1. 初始化所需变量,将起点的E置为0;
	 * 2. 遍历当节点的邻接点,应用计算公式EC_w = Ec_v + edge_weight,如果计算得到的结果比该邻接点已经存储的值大,则更新。否则不更新。
	 * 3. 将未访问过的邻接点入队。
	 * 
	 * 所有节点的最大拖延时间计算原理和公式:
	 * 基本原理和公式:
	 * EL_n = EC_n;
	 * LC_v = min(LC_w - c_v,w);
	 * 
	 * 上述表达式通俗的说,就是说当前节点的最大拖延时间,是所有(后置节点的最早完成时间 - 后置节点到当前节点的权值)的最小值决定
	 * 
	 * 思路和步骤:(跟上面及其类似)
	 * 1.初始化所需变量,将尾点的LC值置为该地的EC值。
	 * 2.遍历当前节点的前置邻接表,应用计算公式EC_pre = Ec_v - edge_weight,如果计算得到的结果比该邻接点已经存储的值小,则更新。否则不更新。
	 *   同时利用公式ST_pre = LC_v - E_pre - weight,更新当前时间的最大拖延时间。
	 * 3. 将未访问过的节点入队。
	 * 
	 * 后期都写个检查函数,确保用户插入的邻接点,都有对应的节点存储。
	 * 
	 * @param startVetex
	 */
	public void criticalPathAnalysis(T startVetex, T endVetex) {
		//防止zz
		if((this.map.get(startVetex) == null) || (this.map.get(endVetex)) == null) {
			System.out.println("请正确输入起点和结束点");
			return;
		}
		//使用一个linkedList来当作队列使用
		LinkedList<T> queue = new LinkedList<>();
		HashMap<T, CriticalPathInfo> InfoMap = new HashMap<>();
		
		assignEC(queue, InfoMap, startVetex);
		assignLCAndST(queue, InfoMap, endVetex);
		
		//test output
		for(T x : this.map.keySet()) {
			if(x.equals(startVetex) || x.equals(endVetex)) {
				continue;
			}
			System.out.println("事件:" + x + "\t最早完成时间为:\t" + InfoMap.get(x).EC + 
					"\t不拖延进度的情况下最晚完成时间:\t" + InfoMap.get(x).LC + 
					"\t\t事件可以拖延的时间为:\t" + InfoMap.get(x).ST);
		}
	}
	
	/**
	 * 利用info中的allPreNode和EC,使用反向广度优先搜索的方式更新各时间节点的LC和ST值
	 * 即不拖延整体进度的情况下,各时间的最晚完成时间和各时间的弹性时间(即最大拖延时间)。
	 * @param queue
	 * @param InfoMap
	 * @param endVetex
	 */
	private void assignLCAndST(LinkedList<T> queue, HashMap<T, CriticalPathInfo> InfoMap, T endVetex) {
		//把尾节点的LC值设为En值
		CriticalPathInfo endVInfo = InfoMap.get(endVetex);
		endVInfo.LC = endVInfo.EC;
		
		//利用Info的allPreNode,反向广度优先搜索,得到所有事件的最大拖延时间
		queue.addLast(endVetex);
		while(! queue.isEmpty()) {
			//获取当前节点对象及其信息,并且标记已访问
			T vNode = queue.removeFirst();
			CriticalPathInfo vInfo = InfoMap.get(vNode);
			//注意上一轮正向广度优先搜索将访问过的节点设置为true表明已经访问过了
			//这一轮就将它设置为false表明已经访问过了
			vInfo.visited = false;
			
			//遍历当前节点的前置邻接表
			LinkedList<T>  preAdjList = InfoMap.get(vNode).allPreNode;
			for (T preNode : preAdjList) {
				//获取当前前置邻接点附加数据域
				CriticalPathInfo preInfo = InfoMap.get(preNode);
				//获取当前 前置邻接点到该节点的边权值,这一波操作,耗费了线性时间,以后看能否改进
				int weight = 0;
				LinkedList<Edge<T>> temp = this.map.get(preNode).getAdjacencyVetexList();
				for (Edge<T> edge : temp) {
					if(edge.getNextVetex().equals(vNode)) {
						weight = edge.getWeight();
					}
				}
				
				//判断公式条件,更新LC数据域,如果公式计算结果比现存的LC更小,则保存
				//与此同时,我们还可以算出当前事件的最大拖延事件
				if(vInfo.LC - weight < preInfo.LC) {
					preInfo.LC = vInfo.LC - weight;
					preInfo.ST = vInfo.LC - preInfo.EC - weight;
				}
				
				//将还未访问的前置节点入队,注意这一轮就将它设置为false表明已经访问过了
				//bug 2 前置邻接表有点混乱,写成vInfo
				if(preInfo.visited) {
					queue.addLast(preNode);
				}
			}
		}
		// TODO Auto-generated method stub
		
	}
	
	/**
	 * 使用广度优先搜素的方式来计算各节点的EC值,即当前动作完成的最早时间
	 * @param queue
	 * @param InfoMap
	 * @param startVetex
	 */
	private void assignEC(LinkedList<T> queue, HashMap<T, CriticalPathInfo> InfoMap, T startVetex) {
		//初始化变量
		queue.addLast(startVetex);
		for(T vetex : this.map.keySet()) {
			InfoMap.put(vetex, new CriticalPathInfo(0, Integer.MAX_VALUE, 0, false));
		}
		
		//广度优先搜索,得到所有检查的最短完成事件。其实相当于遍历了所有的边
		while(! queue.isEmpty()) {
			//获取当前节点对象及其信息,并且标记已访问
			T vNode = queue.removeFirst();
			CriticalPathInfo vInfo = InfoMap.get(vNode);
			vInfo.visited = true;
			
			//遍历当前节点的邻接表
			LinkedList<Edge<T>> adjList = this.map.get(vNode).getAdjacencyVetexList();
			for (Edge<T> edge : adjList) {
				//获取当前的邻接点,边权值和邻接点的附加数据域
				T adjw = edge.getNextVetex();
				int weight = edge.getWeight();
				CriticalPathInfo wInfo = InfoMap.get(adjw);
				
				//判断并且更新邻接点的EC数据域
				if(vInfo.EC + weight > wInfo.EC) {
					wInfo.EC = vInfo.EC + weight;
				}
				
				//对未访问过的邻接点压入队列中
				if(! wInfo.visited) {
					queue.addLast(adjw);
				}
				
				//把当前节点插入到该邻接点的allPreNode链表中,用于后续的计算
				wInfo.addPreNode(vNode);
			}
		}
		
	}
	
	/**
	 * 把当前对象中的有权无圈图当成动作节点图,建立对应的时间节点图。
	 * 并且打印显示出对应的事件节点图
	 * 
	 * 步骤2这样做的原因书中原话:在一个动作依赖于多个前置动作的情况下,可能需要插入哑边和哑节点来避免引进假相关性的概念。
	 * 
	 * 思路步骤:
	 * 1. 计算所有节点入度,并且将入度大于1的节点,记录在一个map<T, boolean>中
	 * 2. 返回map的size,用于生成的图的总节点数。
	 * 3. 从起点开始,遍历每一个节点的邻接表。即遍历原图中所有的边。思路还是广度优先搜索那套,使用一个queue来完成。
	 * @return
	 */
	public HashMapAdjacencyList<T> bulitEventNodeGraph(){
		return null;
	}
}


/**
 * 单独一个邻接表的类
 * 
 * @author 25040
 *
 * @param <T>
 */
class OneVetexAdjacencyList<T>{
	//邻接表的自身顶点的属性
	/*
	 * 这个数据域确实在使用hashMap的时候没点用处。
	 */
	private T self;
	//邻接表中右self指向的所有邻接点,使用HashMap后,我们就不需要跟早
	private LinkedList<Edge<T>> edges;
	
	/**
	 * 构造一个邻接表。
	 */
	public OneVetexAdjacencyList(T self) {
		this.self = self;
		this.edges = new LinkedList<>();
	}
	
	/**
	 * 返回当前邻接点的当前顶点
	 * @return
	 */
	public T getSelfVetex() {
		return this.self;
	}
	
	/**
	 * 检查当前顶点的邻接点是否为空
	 * @return
	 */
	public boolean hasAdjacencyVetex() {
		return this.edges.isEmpty();
	}
	
	/**
	 * 获取当前顶点的有几个邻接点,即当前顶点的邻接点数目
	 * @return
	 */
	public int adjacencyVetexNum() {
		return this.edges.size();
	}
	
	/**
	 * 在当前顶点的邻接表中插入到邻接点,并且设置该边的权值为1
	 * @param otherVetex
	 */
	public void insertAdjcancyVetex(T otherVetex) {
		insertAdjacencyVetex(otherVetex, 1);
	}
	
	/**
	 * 在当前顶点的邻接表中插入到邻接点,并且设置该边的权值
	 * @param otherVetex
	 */
	public void insertAdjacencyVetex(T otherVetex, int weight) {
		this.edges.addLast(new Edge<>(weight, otherVetex));
	}
	
	/**
	 * 在当前顶点的邻接表中插入多个邻接点
	 * @param otherVetexs
	 * @throws java.lang.IllegalArgumentException-欲插入邻接点数组和权值数组的数目不匹配
	 */
	public void insertAdjacencyVetex(T[] otherVetexs, int[] allEdgesWeight) {
		if(otherVetexs.length != allEdgesWeight.length) {
			throw new IllegalArgumentException("欲插入邻接点数组和权值数组的数目不匹配!");
		}
		for(int i = 0;i < otherVetexs.length; ++i) {
			insertAdjacencyVetex(otherVetexs[i],allEdgesWeight[i]);
		}
	}
	
	/**
	 * 格式化输出当前顶点的邻接表
	 */
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append("[ ");
		
		//sb.append("self:").append(getSelfVetex()).append("->(");
		for(int i = 0;i < adjacencyVetexNum();++i) {
			sb.append(this.edges.get(i)).append(", ");
		}
		
		sb.append(" ]");
		return sb.toString();
	}
	
	public LinkedList<Edge<T>> getAdjacencyVetexList(){
		return this.edges;
	}
}


/**
 * 
 * @author 25040
 *
 */
class Edge<T>{
	private int weight;
	private T nextVetex;
	
	/**
	 * 供无权图使用
	 * @param nextVetex
	 */
	public Edge(T nextVetex) {
		this(1, nextVetex);
	}
	
	/**
	 * 供有权图构造边对象使用
	 * @param weight
	 * @param nextVetex
	 */
	public Edge(int weight, T nextVetex) {
		super();
		this.weight = weight;
		this.nextVetex = nextVetex;
	}
	
	
	
	public int getWeight() {
		return weight;
	}


	public T getNextVetex() {
		return nextVetex;
	}


	/**
	 * 边对象的格式化输出
	 */
	public String toString() {
		return (nextVetex +"("+ weight +")"); 
	}
}

--------分割线-------------

割点判断(注意:还是基于上述图的框架编写的,即HashMap+LinkedList)

/*
	 * 为了完成割点的寻找,需要在每个添加额外的数据域。我们使用hashMap<T,additionalInfo>的形式来存储。
	 * additionalInfo的信息以方法内部类的形式来存储。
	 */
	private class ArtInfo{
		int num;//搜索编号
		int low;//当前节点所能达到的最低节点
		T parent;
		boolean visited;
		
		public ArtInfo(int num, int low, T parent, boolean visited) {
			super();
			this.num = num;
			this.low = low;
			this.parent = parent;
			this.visited = visited;
		}
	}
	/*
	 * 为了给深度优先生成树的节点编号,需要一个类变量来记录
	 */
	private int count;
	
	/**
	 * 使用深度优先搜索来检查无向图中的割点
	 * 还未进行起点是否是割点的特殊判断
	 * @param startNode 深度优先开始的起点
	 */
	public void findArt(T startNode) {
		//初始化额外添加数据域的HashMap
		HashMap<T, ArtInfo> addInfo = new HashMap<>();
		for(T key : this.map.keySet()) {
			addInfo.put(key, new ArtInfo(0, 0, null, false));
		}
		count = 0;
		assignNum(startNode, addInfo);
		//重置所有节点的访问标志 bug 1
		for(T key : this.map.keySet()) {
			ArtInfo nodeInfo = addInfo.get(key);
			nodeInfo.visited = false;
		}
		//bug 2 将A的parent置为本身
		ArtInfo startInfo = addInfo.get(startNode);
		startInfo.parent = startNode;
		//再进行low的深度优先搜索设置,
		assignLow(startNode, addInfo);
	}
	
	/**
	 * 还是以深度优先搜索的方式递归的遍历所有的节点。
	 * 1. 通过判断节点visited数据域来避免重复。
	 * 2. 通过判断当前节点v和邻接点w的num数据域可以得知当前的边是前向边,还是背向边。
	 * 	   2.1 当w.num > v.num && w.visited时,为前向边.
	 * 	   2.2 当w.num < v.nums时,并且当v.parent == w时,为已经处理过需要忽略的边,并且当v.parent != w 时,就是背向边了。
	 * 3. 在2.1中即前向边的情况下,根据深度优先的思想,直接递归到下一节点,然后递归完成后,判断w.low >= v.num说明v为割点。
	 *    可以画出深度优先生成树来理解,>=代表的深度更深,如果邻接点的能达到的最低深度都等于当前节点,那么当前节点一定时割点。
	 *    大于的话,说明还有其他割点。最后,更新当前节点的low值。由于深度优先搜索的关系,较小的low值会从深度较深的地方,一直向上更新。
	 *    因此,min(w.low, v.low)即可。
	 * 4.而在2.2中,背向边的情况,只需要把当前节点low值更新好就行。即v.low = min(v.low, w.num)
	 * 
	 * 值得注意的是,当判断搜索起始点是否是割点,不能使用上诉条件,需要加上一条。
	 * 即深度优先搜索树的根节点有一个以上的儿子(两个及以上),则它是割点。因为,如果这两个儿子如果能够通过其他路径到达对方。
	 * 那么一定不可能需要返回到A来转移。说明两儿子之间必须通过根节点联通,这种情况下根节点就是割点。
	 * 再次遍历根节点的邻接表,查看邻接点的parent,如果parent = root的邻接点数目>1,那么根节点为割点,否则就不是
	 * 
	 * @param v
	 * @param addInfo
	 */
	private void assignLow(T v, HashMap<T, ArtInfo> addInfo) {
		//首先,二话不说先初始化下当前节点low值
		ArtInfo vInfo = addInfo.get(v);
		if(vInfo.num == 0) {
			throw new IllegalArgumentException("请先对各节点进行深度优先搜索编号!");
		}
		vInfo.low = vInfo.num;
		//bug 3 又Tm忘记置未被访问标志了
		vInfo.visited = true;
		//遍历当前节点的邻接表
		LinkedList<Edge<T>> adjList = this.map.get(v).getAdjacencyVetexList();
		for (Edge<T> edge : adjList) {
			//获取邻接点的名字
			T w = edge.getNextVetex();
			//获取邻接点的信息
			ArtInfo adjInfo = addInfo.get(w);
			//前向边的情况,在书上伪代码的基础上,添加了对visited数据域的检查。
			if(!adjInfo.visited && adjInfo.num > vInfo.num) {
				//深度优先搜索递归
				assignLow(w, addInfo);
				//更新当前节点的low值
				vInfo.low = Math.min(adjInfo.low, vInfo.low);
				//递归搜索完成后判断当前节点是否是割点
				if(adjInfo.low >= vInfo.num) {
					//当v是根节点的时候,需要进行特殊的测试
					if(v.equals(vInfo.parent)) {
						int cnt = 0;
						for(Edge<T> twiceEdge : adjList) {
							if(addInfo.get(twiceEdge.getNextVetex()).parent.equals(v)) {
								++cnt;
							}
						}
						if(cnt <= 1) {
							continue;
						}
					}
					
					System.out.println("割点:" + v);
				}
			}
			else {
				//判断是否是背向边,注意到背向边的w节点是已经访问过的
				//如果当前节点的父亲等于邻接表,那么代表这条边已经背访问过了。不需要反向重复操作
				//bug 2 对null调用eqaul方法,将起点的parent置为它本身
				if(! vInfo.parent.equals(w)) {
					vInfo.low = Math.min(vInfo.low, adjInfo.num);
				}
			}
		}
	}

	/**
	 * 以深度优先搜索的方式递归给深度优先搜索生成树的相关节点编号。
	 * 用于推断计算无向图中的割点。
	 * 
	 * 思路和步骤:
	 * 1. 遍历当前节点的邻接表。
	 * 2. 当v.visited标志为false时,把当前节点的num数据域赋值编号(编号数值的大小代表该节点在生成树的深度。)否则跳过该邻接点。
	 * 3. 将1,2步骤递归到该邻接点上。
	 * 
	 * @param x
	 * @param additionalInfo
	 */
	private void assignNum(T x, HashMap<T, ArtInfo> addInfo) {
		ArtInfo thisVetexInfo = addInfo.get(x);
		thisVetexInfo.num = ++count;
		thisVetexInfo.visited = true;//bug 1 忘记给当前节点设置被访问标志了。
		//遍历该节点的邻接表,跟广度不一样,深度是递归例程,广度是迭代例程不一样
		//获取对应顶点的邻接表
		LinkedList<Edge<T>> adjVetexList = this.map.get(x).getAdjacencyVetexList();
		for (Edge<T> edge : adjVetexList) {
			//获取边对象对应的节点
			T adjVetex = edge.getNextVetex();
			//检查该节点的访问标记
			if(!addInfo.get(adjVetex).visited) {
				//获取当前邻接点相关信息。
				ArtInfo adjVetexInfo = addInfo.get(adjVetex);
				//记住父节点的路径信息。
				adjVetexInfo.parent = x;
				//以深度优先搜索的方式往下继续编号。
				assignNum(adjVetex, addInfo);
			}
		}
	}

-----------分割线--------------

欧拉环游例程

/**
	 * 用于在欧拉环游中,为了便于对访问过的边进行标记。
	 * 其针对的问题就是,边<2,3>与边<3,2>中在无向图中是等价的。
	 * 而在欧拉环游中,这两个在不同的邻接表中,不太好表示。
	 * 同时改变邻接表的本来结构。
	 * 
	 * @author 25040
	 *
	 * @param <T>
	 */
	private class EdgeUsingInEularTour<T>{
		T v1;
		T v2;
		
		public EdgeUsingInEularTour(T v1, T v2) {
			super();
			this.v1 = v1;
			this.v2 = v2;
		}
		
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result + ((v1 == null) ? 0 : v1.hashCode());
			result = prime * result + ((v2 == null) ? 0 : v2.hashCode());
			return result;
		}
		
		/**
		 * 必须要重新定义equals和hashcode让哈希表能够正确判断两个边是否相等
		 */
		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			EdgeUsingInEularTour other = (EdgeUsingInEularTour) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (v1 == null) {
				if (other.v1 != null)
					return false;
			} else if (!v1.equals(other.v1))
				return false;
			if (v2 == null) {
				if (other.v2 != null)
					return false;
			} else if (!v2.equals(other.v2))
				return false;
			return true;
		}

		private HashMapAdjacencyList getOuterType() {
			return HashMapAdjacencyList.this;
		}
		
		
	}
	
	/**
	 * 对无权无向图的欧拉环游分析。
	 * 
	 * 
	 * 
	 * 基本思路和步骤:
	 * 1. 从用户指定的起点开始,进行深度优先搜索。并且对访问过的边进行标记。
	 * 2. 直到重新访问到了圈起点,这样深度优先搜索访问过的边形成了一个圈。这样形成了一条子路径。
	 * 	   我们用链表来保存这个圈。
	 * 3. 我们取出上述链表的倒数第二个节点,再次进行深度优先搜索,注意。不能重复访问标记了的边。
	 *    然后把这次深度优先搜索访问形成的圈,插入上面哪个链表中。我感觉可以使用iterator.add方法来完成
	 * 4.重复1 - 3步骤,直到所有的边都访问过了。
	 * 
	 * 一个问题是:如何保存一条边已经访问过这个信息。
	 * 
	 * 第二个想法,干脆重建一个无权邻接表算了。使用上诉哈希表貌似更加麻烦
	 * 
	 * @param startVetex 开始进行欧拉回路分析的起点。
	 */
	public void eularTour(T startVetex) {
		
		if(!checkHasEularTour()) {
			System.out.println("该图不存在欧拉环游!");
			return;
		}
		
		//用链表来储存结果路径
		LinkedList<T> resultPath = new LinkedList<>();
		//为了便于拼接路径,我们使用迭代器来管理
		ListIterator<T> iter = resultPath.listIterator();
		//用于标记各无向边是否已经被访问过了
		HashMap<EdgeUsingInEularTour<T>,Boolean> allEdgeIsVisited = new HashMap<>();
		//定义一个计数器,用于记录当前图中还有几条边为未被访问
		int edgesUnvisited;
		
		//初始化各变量
		//遍历邻接表,将所有边都插入到用于保存边访问状态的哈希表中
		for(T x: this.map.keySet()) {
			//获取该节点对应的邻接表
			LinkedList<Edge<T>> adjList = this.map.get(x).getAdjacencyVetexList();
			//遍历邻接表中所有的有权边
			for(Edge<T> edge : adjList) {
				//将所有有向边都放入visitedMap中,注意在后续处理中,访问后标记需要同时置两条边都为true
				allEdgeIsVisited.put(new EdgeUsingInEularTour<T>(x, edge.getNextVetex()), false);
			}
		}
		//初始化其他变量
		//获取全图边的条数
		edgesUnvisited = allEdgeIsVisited.size();
		//先插入起点
		iter.add(startVetex);
		T nextStrat = startVetex;
		//反复进行深度优先搜索,直到遍历所有的边
		while((resultPath.size() - 1) != edgesUnvisited / 2) {
			//重新设置迭代器,使他指向正确的位置,保证拼接路径的正确性,同时寻找下一个深度优先搜索的起始点
			nextStrat = iter.previous();
			//bug 4 这里的逻辑应该是当被测节点没有其他可见边的时候,迭代器向前移动
			while(! hasUnvisitedEdege(nextStrat, allEdgeIsVisited)) {
				nextStrat = iter.previous();
			}
			//由于在深度优先搜索中,一开始就会插入一个该圈的起点,所有需要删除这个寻找点,这样才不会重复插入
			iter.remove();
			//欧拉环游的深度优先搜索
			eularTourDps(nextStrat, null, allEdgeIsVisited, nextStrat, iter);
		}
		System.out.println(resultPath);
		
	}
	
	/**
	 * 检查当前图是否有欧拉环游。
	 * 无论是有权图还是无权图都可以欧拉环游。
	 * 即检查当前图中所有的邻接表的size都为偶数,这样就可以保证每个点的出度和入度都为偶数
	 * @return
	 */
	private boolean checkHasEularTour() {
		//遍历图中所有的节点
		for(T vetex : this.map.keySet()) {
			//遍历当前节点的邻接表
			LinkedList<Edge<T>> adjList = this.map.get(vetex).getAdjacencyVetexList();
			if(adjList.size() % 2 != 0) {
				return false;
			}
		}
		return true;
	}

	/**
	 * 用于深度优先搜索的递归例程。同时在递归的过程中对访问过的边机型标记,并且插入到结果中
	 * 
	 * @param Vetex             深度优先搜索的当前节点
	 * @param lastVetex         用于记录上个节点,这样可以防止在深度优先搜索时,回溯的问题。
	 * 							之前是因为可以通过节点visited数据域来控制它不会这样。
	 * @param allEdgeIsVisited  记录着边是否被访问的哈希表
	 * @param resultPath        用于记录搜索路径的链表
	 * @param startVetex        本次搜索的起点,用于判断是否形成了一个圈。
	 * @param iter              结果链表中的迭代器,记录深度优先搜索的路径
	 */
	private void eularTourDps(T Vetex, T lastVetex, HashMap<EdgeUsingInEularTour<T>, Boolean> allEdgeIsVisited,
			T startVetex, ListIterator<T> iter) {
		//将当前节点插入路径中
		iter.add(Vetex);
		//为了保证插入到链表的末尾,需要next一下
		//iter.next();
		
		//获取当前节点的邻接表
		LinkedList<Edge<T>> adjList = this.map.get(Vetex).getAdjacencyVetexList();
		
		//初始化各变量,
		for(Edge<T> edge : adjList) {
			//获取邻接点
			T adjV = edge.getNextVetex();
			
			/*
			 * //判断当前邻接点是否还有未访问的边,如果有,则递归到它进行深度优先搜索
			//bug 2 直接A-B-A回来了。。。必须保证不能回访,即
			//bug 5 不仅仅要深度优先搜索的下一个邻接点是否有未访问的边,同时,还应该判断从当前点是否有为访问的边能到邻接点
			//防止它往上个过来的节点直接回去了,即避免形成A-B-A这种圈
			//&& hasUnvisitedEdege(adjV, allEdgeIsVisited)) {这个条件判断是多余的,因为只要它能够有路径到达下一个邻接点,
			//由于在前面已经检查了所有节点的度为偶数,所以,能进一定能出
			 * 
			 */
			if(! allEdgeIsVisited.get(new EdgeUsingInEularTour<T>(Vetex, adjV)) && !adjV.equals(lastVetex)) {
				//将该边置为已访问
				updateVisitedMap(Vetex, adjV, allEdgeIsVisited);
				//基准情形,如果形成了一个圈,即深度优先搜索回到了起点,那么就结束递归
				if(adjV.equals(startVetex)) {
					//最后再插入一次,直接返回
					iter.add(adjV);
					break;
				}
				eularTourDps(adjV, Vetex, allEdgeIsVisited, startVetex, iter);
				//bug 3 形成一个圈后,比如A-B-C-A,它会回到C节点,遍历邻接表,寻找下一个开始深度优先搜索的节点
				//所以,找到一个完整的圈后,直接break退出循环。
				break;
			}
			
		}
	}
	
	/**
	 * 更新已访问边的哈希表,将当前访问边置为true。
	 * 注意,双向都要置为true
	 * @param vetex
	 * @param adjV
	 * @param allEdgeIsVisited
	 */
	private void updateVisitedMap(T vetex, T adjV,
			HashMap<HashMapAdjacencyList<T>.EdgeUsingInEularTour<T>, Boolean> allEdgeIsVisited) {
		//bug 1 写成了false,弱智了
		allEdgeIsVisited.put(new EdgeUsingInEularTour<>(vetex, adjV), true);
		allEdgeIsVisited.put(new EdgeUsingInEularTour<>(adjV, vetex), true);
	}

	/**
	 * 判断当前该节点的邻接表中是否还有未访问的边
	 * 
	 * @param adjV
	 * @return
	 */
	private boolean hasUnvisitedEdege(T adjV,HashMap<EdgeUsingInEularTour<T>, Boolean> allEdgeIsVisited) {
		//获取邻接表
		LinkedList<Edge<T>> adjList = this.map.get(adjV).getAdjacencyVetexList();
		//遍历邻接表
		for (Edge<T> edge : adjList) {
			//新建一各暂存量,用于判断邻接表是否还有未访问的边
			EdgeUsingInEularTour<T> eularEdge = new EdgeUsingInEularTour<>(adjV, edge.getNextVetex());
			//有一条边未访问,就返回true
			if(!allEdgeIsVisited.get(eularEdge)) {
				return true;
			}
		}
		return false;
	}

-----------------分割线-------------------------

Prim最小生成树

/**
	 * @version <1>
	 * 使用贪婪算法解决无向图有权图中的最小生成树问题。
	 * 
	 * 使用Prim算法的思想来解决最小生成树的问题。
	 * 这里复用求有权无圈图最短路径算法的PathInfo类,来跟踪路径信息。
	 * 不过这里dv表示 当前节点到start路径中最小的边权值。pv是记录引起改变当前节点dv值的节点。
	 * 
	 * 核心:选取当前节点所有输出边的权值最小的边。所有这样的边构成的树就是最小生成树。
	 * 
	 * 算法套路我是理解了,但是还是有点不理解,为什么这样一定能形成最小生成树
	 * 
	 * 思路和步骤基本和广度优先搜索的思路基本相同:
	 * 1. 从起点开始,遍历各个节点的邻接表。
	 * 2. 针对当前节点的每个未知的邻接表,判断并且更新当前邻接点的dv和pv值
	 * 3. 直到完成所有的节点的遍历。也就是需要访问所有的边
	 * 
	 * @version <2> 2018-04-01 重新理了一遍书上的思路,发现不知道是我理解的问题,亦或是算法本身的问题。
	 * 我打算按照我自己的理解走一遍,就如同上面核心中的描述:选取当前节点所有未知节点的最小边权值。
	 * 
	 * 基本思路和步骤:
	 * 1. 从起点开始遍历该点的邻接表,选取未知边中具有最小边权值的边。记录该边权值在v节点的dv值中,该边对应的邻接点记录在pv值中。
	 * 2. 对未知的邻接点入队,重复步骤1,遍历图中所有的节点。
	 * 3. 最后根据infoMap即可输出最小生成树。
	 * 
	 * 上面想法有个大bug,不好解决成圈问题。
	 * 
	 * @version <3> 
	 * 算了,还是老老实实学习书上的贪婪算法吧,然后贪婪地求解吧。真是想不贪婪都是不行啊。
	 * 
	 */
	public void minSpanningTreeByPrim(T startVetex) {
		//声明需要的各项变量
		LinkedList<T> queue = new LinkedList<>();
		HashMap<T,PathInfo> infoMap = new HashMap<>();
		
		//初始化各变量
		for(T vetex : this.map.keySet()) {
			infoMap.put(vetex, new PathInfo(false, Integer.MAX_VALUE, null));
		}
		//将起点的pathInfo值,置为合适
		PathInfo sInfo = infoMap.get(startVetex);
		sInfo.dv = 0;
		//向队列中插入起点
		queue.addLast(startVetex);
		
		//广度优先搜索,每次都得贪婪得选取下个节点,看来这里还不得不使用二叉堆啊。
		while(! queue.isEmpty()) {
			//获取当前节点得信息
			T nowV = queue.removeFirst();
			PathInfo nowVInfo = infoMap.get(nowV);
			nowVInfo.known = true;
			
			//遍历邻接表,加入优先队列,获得当前节点所有未知输出边中,权值最小得
			PriorityQueue<Edge<T>> heap = new PriorityQueue<>(new EdgeComparatorWithWeight());
			LinkedList<Edge<T>> adjList = this.map.get(nowV).getAdjacencyVetexList();
			heap.addAll(adjList);
			
			//让优先队列按照从小到大的输出各边,直到二叉堆为空
			while(!heap.isEmpty()) {
				//取出当前边权值最小的边
				Edge<T> adjEdeg = heap.poll();
				//获取当前边的权值及其其他信息
				int edgeWeight = adjEdeg.getWeight();
				T adjV = adjEdeg.getNextVetex();
				PathInfo adjVInfo = infoMap.get(adjV);
				//必须针对未知节点,并且边权值比记录值更小
				//bug 1 针对未知邻接点要让别人入队。
				if(!adjVInfo.known && edgeWeight < adjVInfo.dv) {
					queue.addLast(adjV);
					adjVInfo.dv = edgeWeight;
					adjVInfo.prevNode = nowV;
				}
			}
		}
		
		//根据infoMap的preNode信息,既可以显示出生成树的所有路径了
		//直接遍历infoMap即可,跳过起点
		for(T vetex : infoMap.keySet()) {
			if(!vetex.equals(startVetex)) {
				System.out.printf("{%s,%s}\n", vetex, infoMap.get(vetex).prevNode);
			}
		}
		
	}
	
	/**
	 * 基于权值的边的比较器
	 * @author 25040
	 *
	 */
	private class EdgeComparatorWithWeight implements Comparator<Edge<T>>{

		@Override
		public int compare(Edge<T> arg0, Edge<T> arg1) {
			return Integer.compare(arg0.getWeight(), arg1.getWeight());
		}
		
	}

----------------------------分割线--------------------------

Kruskal最小生成树

/**
	 * 使用第二种贪婪策略。
	 * 
	 * 其基本核心思想和步骤:
	 * 1. 将所有的无向边按照边权值从小到大排序。
	 * 2. 依次测试每天是否满足条件。此条件为该边的加入是否与此时已经连接的边成圈。
	 *    也就是说,判断新插入边的两个端点是否属于一个集合中。这里就用到了第八章不相交集类的union/find例程。
	 * 
	 * 傻逼了,我根本不用把有向图化为无向图,我反正使用不相交集类,在<u,v>和<v,u>是等价的。
	 * 代价是双倍与无向边的空间和时间复杂度。
	 * 我直接把所有有向边导入到优先队列中即可。
	 * 
	 */
	@SuppressWarnings("unchecked")
	public void minSpanningTreeByKruskal() {
		//初始化包括优先队列在内的变量
		PriorityQueue<EdgeUsingKruskal> queue = new PriorityQueue<>(new EdgeComparatorWithWeight2());
		//获取当前图中的所有节点的数组。
		T[] allVetex = (T[])this.map.keySet().toArray();
		//初始化不相交集类
		DisjSets<T> set = new DisjSets<>(allVetex.length, allVetex);
		
		//遍历邻接表把所有的有向边都加入到优先队列中
		for(T vetex : this.map.keySet()) {
			LinkedList<Edge<T>> adjList = this.map.get(vetex).getAdjacencyVetexList();
			for (Edge<T> edge : adjList) {
				queue.add(new EdgeUsingKruskal(vetex, edge.getNextVetex(), edge.getWeight()));
			}
		}
		
		//对优先队列执行deleteMin操作直到优先队列为空为止,
		while(!queue.isEmpty()) {
			//获得当前队列中权值最小的边
			EdgeUsingKruskal edge = queue.remove();
			
			//判断改变是否满足条件,不属于一个集合中,即不成圈,那么改变就可以接受。
			if(!set.inSameSet(edge.v1, edge.v2)) {
				//将这两点放入一个集合中,表示它们已经连在一起了。即在一棵树上了。
				set.union(edge.v1, edge.v2);
				//test
				System.out.printf("(%s,%s)\n", edge.v1, edge.v2);
			}
			
			
		}
	}
	

	/**
	 * 又是把无向图的算法应用在有向图中的问题。我到现在还没有想到一个很好的方法。
	 * 这里复用欧拉环游的Edge类,而且集成了它关于判断边相等的equal方法.
	 * 该类用于Kruskal最小生成树算法。
	 * 
	 * @author 25040
	 *
	 */
	private class EdgeUsingKruskal extends EdgeUsingInEularTour<T>{
		//创建两个数据域,包括边的边权值和是否被采纳。
		private int edgeWeight;
		
		public EdgeUsingKruskal(T v1, T v2, int weight) {
			super(v1, v2);
			this.edgeWeight = weight;
		}
		
	}
	
	/**
	 * 还是基于边权值的比较器。
	 * 该比较器用于Kruskal最小生成树算法。
	 * @author 25040
	 *
	 */
	private class EdgeComparatorWithWeight2 implements Comparator<EdgeUsingKruskal>{

		@Override
		public int compare(HashMapAdjacencyList<T>.EdgeUsingKruskal arg0,
				HashMapAdjacencyList<T>.EdgeUsingKruskal arg1) {
			return Integer.compare(arg0.edgeWeight, arg1.edgeWeight);
		}
		
	}




评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值