图的常见算法

一、图结构

图有很多种存储方式,主要有邻接表和邻接矩阵两种。邻接表以点集为单位,邻接矩阵以边集为单位。

//图的大结构
public class Graph	{
	public HashMap<Integer,Node> nodes;   //点集
	public HashSet<Edge> edges;           //边集
	
	public Graph(){
		nodes = new HashMap<>();
		edges = new HashSet<>();
	}
}
//点结构
public class Node {
	public int value;
	public int in;
	public int out;
	public ArrayList<Node> nexts;
	public ArrayList<Edge> edges;
	
	public Node(int value){
		this.value = value;
		this.in = 0;
		this.out = 0;
		this.nexts = new ArrayList<>();
		this.edges = new ArrayList<>();
	}
}
//边结构
public class Edges {
	public int value;
	public Node from;
	public Node to;

	public 	Edge(int weight, Node from, Node to){
		this.value = weight;
		this.from = from;
		this.to = to;
	}
}

//转换接口例子:用一个二维数组来表示图,格式为: 权值 = matrix[0][0]  from点 = matrix[0][1]  to点 = matrix[0][2]
public class Graph createGraph(Integer[][] matrix){
	Graph graph = new Graph();
	for(int i = 0; i < matrix.length; i++){
		//先从数组中获得form点、to点、权值的值
		Integer weight = matrix[i][0];
		Integer from = matrix[i][1];
		Integer to = matrix[i][2];

		//将from点和to点加到图里
		if(!graph.nodes.containskey(from)){
			graph.nodes.put(from,new Node(from));
		}
		if(!graph.nodes.containsket(from)){
			graph.nodes.put(to,new Node(to));
		}
		//获取from点和to点
		Node fromNode = graph.nodes.get(from);
		Node toNode = graph.nodes.get(to);
		//form点、to点和权重组成一条边
		Edge newEdge = new Edge(weight,fromNode,toNode);
		//from点的邻接点集加入to点
		fromNode.nexts.add(toNode);
		//from点出度加一
		formNode.out++;
		//to点出度加一
		toNode.in++;
		//将这条边加入form点属于的边集里
		formNode.edges.add(newEdge);
		//将这条边加入图的边集里
		graph.edges.add(newEdge);
	}
	return graph;
}

二、图的宽度优先遍历

思路:
// 1.利用队列实现
// 2.从源节点开始一次按照宽度进队,然后弹出
// 3.每弹出一个点,就把该节点所有的没有进过队列的直接邻接节点放进队列
// 4.一直弹出直到队列为空

public static void bfs(Node node){
	if(head == null){
		return;
	}
	Queue<Node> queue = new LinkedList<>();
	HashSet<Node> set = new HashSet<>();
	queue.add(node);
	set.add(node);
	while(!queue.isEmpty()){
		Node cur = queue.poll();
		System.out.println(cur.value);
		for(Node next : cur.nexts){
			if(!set.contains(next){
				queue.add(next); //吧该节点的所有邻接节点加入队列
				set.add(next); 
			}
		}
	}
}

三、图的深度优先遍历

// 1.利用栈实现
// 2.从源节点开始把节点按照深度方式放进栈中,打印,然后弹出
// 3.每弹出一个节点,把该节点下一个没有进过栈的邻接节点放进栈中,打印,弹出
// 4.重复上述过程,直到栈变空
ublic static void dfs(Node node){
	if(node == null){
		return;
	}
	Stack<Node> stack = new Stack<>();
	HashSet<Node> set = new HashSer<>();
	stack.add(node);
	set.add(node);
	System.out.println(node.value);
	while(!stack.isEmpty()){
		//弹出栈顶节点
		Node cur = stack.pop();
		//从栈顶节点的邻接节点集中寻找未遍历的next节点
		for(Node next : cur.nexts){
			if(!set.contains(next){
				//找到后,先将刚刚弹出的节点重新压回栈中,再将next节点放进栈中
				//这样做到原因是每次一条路走到底之后,往回走时,检查是否还有没有走过的路径
				stack.push(cur);
				stack.push(next);
				//set里加入next节点,表示已遍历此节点
				set.add(next);
				//next点加入后打印
				System.out.println(next.value);
				//找到一个节点后就break,直接走下一个节点
				break;
			}
		}
	}
}

四、图的拓扑排序算法

什么是拓扑排序?
在现实生活中,我们经常会同一时间接到许多任务去完成,但是这些任务的完成是有先后次序的。
某校的选课系统规定,每门课可能有若干个先修课,如果要修读某一门课程,则必须要先 修完所有的先修课才能修读。假设一个学生同时只能报一门课程,那么选课系统允许他修完所有课程的顺序就是一个拓扑序

从上述小例子中可以看出,拓扑排序是一个有效的任务顺序,每一门课对应有向图的一个顶点, 先修关系对应有向图的一条边。

拓扑排序:
给定一副有向图,将所有的顶点排序,使的所有的有向边均从排在前面的元素指向排在后面的元素,此时就可以明确的表示出每个顶点的优先级,下列是一副拓扑排序的示意图。

  • 有向无环图(DAG)才有拓扑排序,非 DAG 图没有拓扑排序
  • 若A在序列中排在B的前面,则在图中不存在从B到A的路径。
    在这里插入图片描述
    在这里插入图片描述
拓扑排序的步骤:
1.按照一定的顺序进行构造有向图,记录后个节点的入度;
2.从图中选择一个入度为0的顶点,输出该顶点;
3.从图中删除该顶点及所有与该顶点相连的边;
4.重复上述两步,直至所有顶点输出;
5.或者当前图中不存在入度为0的顶点为止。此时可说明图中有环;
6.因此,也可以通过拓扑排序来判断一个图是否有环。

//适用范围:要求有向无环图,且有入度为0的节点
//有向无环图中所有节点的线性序列满足下列要求:
// 1.每个节点出现且只出现一次
// 2.若存在一条A->B的边,则在排序序列中,节点A排在节点B前面

//算法步骤:
// 1.先找入度为0的点,加入result集
// 2.消除入度为0的点的影响(属于的边和点)
// 3.寻找下一个入度为0的点,重复以上过程
public static List<Node> sortedTopology(Graph graph){
	//用hashmap存节点和入度
	//key:代表某个节点  value:代表该节点剩余入度
	HashMap<Node,Integer> inMap = new HashMap<>();
	//存入度为0的节点的队列
	Queue<Node> zeroInQueue = new LinkedList<>();
	//遍历图集中的点集,将点和其入度存进inMap
	for(Node node : graph.nodes.values()){
		inMap.put(node,node.in);
		//如果遇到入度为0的node,加入
		if(node.in == 0){
			zeroInQueue.add(node);
		}
	}
	//拓扑排序结果依次,存入result
	List<Node> result = new ArrayList<>();
	while(!zeroInQueue.isEmpty()){
		//弹出入度为空节点的队列的一个节点到result里
		Node cur = zeroInQueue.poll();
		result.add(cur);
		//遍历该节点的邻接节点集
		for(Node next : cur.nexts){
			//消除cur节点的影响,即其next节点们的入度都要减一
			inMap.put(next,inMap.get(next)-1);
			//如果next节点的入度更新为0,则加入队列
			if(inMap.get(next) == 0){
				zeroInQueue.add(next);
			}
		}
		return result;
	}
}

五、图生成最小生成树

关于图的几个概念定义:

  • 连通图:在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。

  • 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。

  • 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。

求最小生成树的两种算法:
1. Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

//最小生成树:在保证连通性下,整体权值最小
//(无向图)k算法:从边角度出发,依次选择最小的边,如果加上这条边会形成环,则不加这条边,如果不会形成环,则加上这条边

//不用并查集版本:定义一个myset结构
public static class MySets{
	//key=节点  value=节点当前所在的集合
	public HashMap<Node, List<Node>> setMap;
	public MySets(List<Node> nodes){
		for(Node cur : nodes){
			List<Node> set = new ArrayList<Node>();
			set.add(cur);
			setMap.put(cur,set);
		}
	}
	//判断两个节点是否在同一个集合
	public boolean isSameSet(Node from,Node to){
		List<Node> fromSet = setMap.get(from);
		List<Node> toSet = setMap.get(to);
		return fromSet == toSet;
	}
	
	//将两个节点加在同一个集合里
	public void union(Node from,Node to){
		List<Node> fromSet = setMap.get(from);
		List<Node> toSet = setMap.get(to);
		//遍历to节点所在集合里的所有元素
		for(Node toNode : toSet){
			//from节点所在的集合加入to节点所在集合的元素
			fromSet.add(toNode);
			//在setmap里更新这些节点当前所在的集合
			setMap.put(toNode,fromSet);
		}
	}
	//比较器
	public static class EdgeComparator implements Comparator<Edge> {
		@Override
		public int compara(Edge o1,Edge o2){
			return o1.weight-o2.weight;
		}
	}
	//k算法
	public static Set<Edge> kruskalMST(Graph graph){
		//将图里的点集放进mysets结果初始化,每个节点就是一个集合
		MySets mysets = new MySets(graph.nodes.values());
		//优先级队列
		PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
		//把图里的边集中所有边放进优先级队列里按weight权值从小到大排序
		for(Edge edge : graph.edges){
			priorityQueue.add(edge);
		}
		Set<Edge> result = new HashSet<>();
		while(!priorityQueue.isEmpty()){
			//队列每次弹出最小边
			Edge edge = priorityQueue.poll();
			//如果边的from和to不在一个集合就:
			if(!mysets.isSameSet(edge.from,edge.to)){
				//结果集里加入这条边
				result.add(edge);
				//将边的to点所在集合里的点放进from点所在的集合里
				mysets.union(edge.from,edge.to);
			}
		}
		return result;
	}
}

2. Prim算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

//(无向图)p算法:从一个点出发,从他的边集里解锁边,再从这些边里挑选权值最小的边,然后解锁下一个节点,再从边集里解锁并挑选权值最小的边(包括之前解锁的边),如果边的to点是已经解锁的节点,则不要这条边,然后重复上述过程,最后形成最小生成树

//比较器
public static class EdgeComparator implements Comparator<Edge>{
	@Override
	public int compara(Edge o1,Edge o2){
		return o1.weight-o2.weight;
	}
}
public static Set<Edge> primMST(Graph graph){
	//解锁的所有边都放进小根堆排序
	PriorityQueue<Edge> priortyQueue = new PriorityQueue<>(new
	EdgeComparator());
	//解锁过的点都放进set集里
	HashSet<Node> set = new HashSet<>();
	//选择后的边都放进result里
	Set<Edge> result = new HashSet<>();
	//遍历所有点集是要防止森林的情况,每一次从一个点出发只会形成一棵树
	for(Node node : graph.nodes.values()){
		if(!set.contains(node){
			//将解锁了的node放进set里记录
			set.add(node);
			//将node属于的所有边放进优先级队列里排序
			for(Edge edge : node.edges){
				priorityQueue.add(edge);
			}
			while(!priorityQueue.isEmpty()){
				//从优先级队列里弹出最小的边
				Edge edge = priorityQueue.poll();
				//获得node的边的to节点
				Node toNode = edge.to;
				//如果set里没有to节点,则解锁to节点
				if(!set.contains(toNode)){
					//将解锁的to节点放到set里
					set.add(toNode);
					//result集里加入node和to这条最短的边
					result.add(edge);
					//再将to节点解锁的所有边放进优先级队列,继续根据队列里最小的边解锁下一个节点和其边集
					for(Edge nextEdge : toNode.edges){
						priorityQueue.add(nextEdge);
					}
				}
			}
		}
		return result;
	}

六、图中求单元最短路径(Dijkstra算法)

//求从出发节点到其他节点的最短路径
//适用范围:没有累加和为负数的环

public static HashMap<Node,Integer> dijkstra(Node node){
	//hashmap表记录从head出发到所有点的最小距离
	//key:从head出发到达的点   value:从head出发到达key的最小距离
	//如果在hashmap中,没有T的记录,含义是从head到T这个点的距离为∞
	HashMap<Node, Integer> distanceMap = new HashMap<>();
	distanceMap.put(head,0);
	//已经锁住,确定求得最短距离的节点,存在selectedNodes里
	HashSet<Node> selectedNodes = new HashSet<>();
	//从distanceMap里找没有被锁住并且离head最小距离的节点记录
	Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedMap);
	While(minNode != null){
		//先拿到head到minNode的距离
		int distance = distanceMap.get(minNode);
		//遍历minNode的边集
		for(Edge edge : minNode.edges){
			//拿到minNode的边的to点
			Node toNode = edge.to;
			//如果toNode是没有锁住的点
			if(!distanceMap.containskey(toNode)){
				//在distanceMap里更新to点的信息,距离为head到minNode的距离加上边的weight权值
				distanceMap.put(toNode, distance +  edge.weight);
			}
			//如果toNode是已经锁住的点,比较原来的distance和加上权值后的distance,选更小的那个
			distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
		}
		//更新完minNode的所有路径距离后,将minNode锁住
		selectedNodes.add(minNode);
		//继续从distanceMap里找一个没有被锁住并且离head最小距离的节点来继续遍历
		minNode = getMinDistanceAndUnseletedNode(distanceMap,selectedNodes);
	}
	return distanceMap;				
}

//从距离记录表中寻找一条从head到T的最短距离,且T点是还没有被锁住的,返回T
public static Node getMinDistanceAndUnselectedNode(HashMap<Node,Integer> distanceMap, HashSet<Node> touchedNodes){
	//最短距离的节点
	Node minNode = null;
	//最短距离
	int minDistance = Integer.MAx_VALUE;
	//遍历距离记录表
	for(Entry<Node, Integer> : distanceMap.entrySet()){
		//得到每个节点和其当前到head的最短距离
		Node node = entry.getKey();
		int distance = entry.getValue();
		//如果节点还没被锁并且新解锁边的距离小于minDistance
		if(!touchedNodes.contains(node) && disatance < minDistance){
			//更新最小节点和最小距离
			minNode = node;
			minDistance = distance;
		}
	}
	return minNode;
}	
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值