目录
一、图(Graph)
1.概念
2.有向图
3.出度、入度
4.无向图
5.简单图、多重图
6.无向完全图
7.有向完全图
8.有权图
9.连通图
10.连通分量(无向图)
11.强连通图(有向图)
12.强连通分量
13.邻接矩阵
1.基本概念
2.邻接矩阵--有权图
14.邻接表
1.基本概念
2.有权图
15.图的实现
图的基础接口
添加边addEdge
图的总边(edgesSize)
图的点的接口
图的边的接口
删除边removeEdge
完整代码
二、图的遍历
新的图接口
三、广度优先搜索(BFS)
1.图解
编辑 2.思路
3.代码实现
四、深度优先搜索(DFS)
1.前序遍历
2.图解
3.递归实现
4.通过栈-----非递归实现
1)步骤
2)图解
3)代码实现
五、AOV网(Activity On Vertex Network)
六、拓扑排序(Topological Sort)
1.简介
2.思路
3.代码实现
package graph;
public interface Graph {
// 边的数量
int edgesSize();
// 顶点数量
int verticesSize();
// 添加顶点
void addVertex(V v);
// 添加边(无权值)
void addEdge(V from,V to);
// 添加边(有权值)
void addEdge(V from,V to,E weight);
// 删除顶点
void removeVertex(V v);
// 删除边
void removeEdge(V from,V to);
interface vertexVisitor{
boolean visit(V v);
}
void print();
}
/**
* 添加无权值的边
* @param from
* @param to
*/
@Override
public void addEdge(V from, V to) {
addEdge(from,to,null);
}
/**
* 添加有权值的边
* @param from
* @param to
* @param weight
*/
@Override
public void addEdge(V from, V to, E weight) {
//根据传入的参数from找到出发点,如果不存在则创建
Vertex fromVertex=vertices.get(from);
if (fromVertex==null){
fromVertex=new Vertex<>(from);
//将点和对应的点关系存入
vertices.put(from,fromVertex);
}
//根据传入参数to找到终点,如果找不到则创建
Vertex toVertex=vertices.get(to);
if (toVertex==null){
toVertex=new Vertex<>(to);
//将点和对应的点关系存入
vertices.put(to,toVertex);
}
//根据出发点和终点,创建边
Edge edge=new Edge<>(fromVertex,toVertex);
edge.weight=weight;//有权值加上,无权值则为null
//不管原来是否存在,都先删除,在添加进去
if (fromVertex.outEdges.contains(edge)){ //说明存在
toVertex.inEdges.remove(edge);
//在整个图中的边减少
edges.remove(edge);
}
fromVertex.outEdges.add(edge);
toVertex.inEdges.add(edge);
//在整个图中的边增加
edges.add(edge);
}
public int edgesSize() {
return edges.size();
}
/**
* 顶点
* @param
* @param
*/
private static class Vertex{
//顶点值
V value;
Set> inEdges=new HashSet<>();
Set> outEdges=new HashSet<>();
public Vertex(V value) {
this.value = value;
}
/**
* 比较两个顶点是否相等
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
return Objects.equals(value,((Vertex)obj).value);
}
@Override
public int hashCode() {
return value==null ? 0: value.hashCode();
}
}
/**
* 边
* @param
* @param
*/
private static class Edge {
Vertex from;
Vertex to;
E weight;
public Edge(Vertex from, Vertex to) {
this.from = from;
this.to = to;
}
/**
* 判断两条边是否相同
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
Edge edge=(Edge) obj;
return Objects.equals(from,edge.from) && Objects.equals(to,edge.to);
}
/**
* 如果equals的结果是true说明一样,则hashcode值也一样
* @return
*/
@Override
public int hashCode() {
return from.hashCode()+to.hashCode();
}
}
/**
* 删除边
* @param from
* @param to
*/
@Override
public void removeEdge(V from, V to) {
// 根据传入的from获得起点,不存在则不需要删除
Vertex fromVertex=vertices.get(from);
if (fromVertex ==null){
return;
}
// 根据传入的to找到终点,不存在则不需要删除
Vertex toVertex=vertices.get(to);
if (toVertex==null) return;
// 根据起点和终点获得边,然后删除
Edge edge=new Edge<>(fromVertex,toVertex);
if (fromVertex.outEdges.remove(edge)){ //表示删除成功
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
}
package graph;
import java.util.*;
public class ListGraph implements Graph{
// 传入的V与顶点类Vertex的映射【要记录该点的出入是inEdges还是outEdges】
private Map> vertices = new HashMap<>();
// 边的Set集合
private Set> edges = new HashSet<>();
public void print(){
System.out.println("[顶点]-------------------");
vertices.forEach((V v, Vertex vertex) -> {
System.out.println(v);
System.out.println("out-----------");
System.out.println(vertex.outEdges);
System.out.println("int-----------");
System.out.println(vertex.inEdges);
});
System.out.println("[边]-------------------");
edges.forEach((Edge edge) -> {
System.out.println(edge);
});
}
@Override
public int edgesSize() {
return edges.size();
}
@Override
public int verticesSize() {
return vertices.size();
}
@Override
public void addVertex(V v) {
// put(key,value)
vertices.put(v,new Vertex<>(v));
}
/**
* 添加无权值的边
* @param from
* @param to
*/
@Override
public void addEdge(V from, V to) {
addEdge(from,to,null);
}
/**
* 添加有权值的边
* @param from
* @param to
* @param weight
*/
@Override
public void addEdge(V from, V to, E weight) {
//根据传入的参数from找到出发点,如果不存在则创建
Vertex fromVertex=vertices.get(from);
if (fromVertex==null){
fromVertex=new Vertex<>(from);
//将点和对应的点关系存入
vertices.put(from,fromVertex);
}
//根据传入参数to找到终点,如果找不到则创建
Vertex toVertex=vertices.get(to);
if (toVertex==null){
toVertex=new Vertex<>(to);
//将点和对应的点关系存入
vertices.put(to,toVertex);
}
//根据出发点和终点,创建边
Edge edge=new Edge<>(fromVertex,toVertex);
edge.weight=weight;//有权值加上,无权值则为null
//不管原来是否存在,都先删除,在添加进去
if (fromVertex.outEdges.contains(edge)){ //说明存在
toVertex.inEdges.remove(edge);
//在整个图中的边减少
edges.remove(edge);
}
fromVertex.outEdges.add(edge);
toVertex.inEdges.add(edge);
//在整个图中的边增加
edges.add(edge);
}
/**
* 删除点
* @param v
*/
@Override
public void removeVertex(V v) {
//根据传入的值找到点并且删除,不存在则不做操作
Vertex vertex=vertices.remove(v);
if (vertex==null) return;
// 迭代器遍历集合vertex.outEdges,删除所有从该点出去的边
// iterator.hasNext():相当于i++
for (Iterator> iterator = vertex.inEdges.iterator();iterator.hasNext();){
Edge edge=iterator.next();//遍历从该点出去的边
edge.to.inEdges.remove(edge);//获取终点进入的边,并从中删除遍历到的边
//这个remove方法是将当前的这个边删除
iterator.remove();//将当前遍历到当前遍历到的元素edge从集合vertex.outEdges中删除
edges.remove(edge);
}
// 迭代器遍历集合vertex.inEdges, 删除所有进入该点的边
for (Iterator> iterator = vertex.inEdges.iterator(); iterator.hasNext();) {
Edge edge = iterator.next(); // 遍历到的进入该点的边
edge.from.outEdges.remove(edge); // 获取起点出去的边,并从中删除遍历到的边
iterator.remove(); // 将当前遍历到的元素edge从集合vertex.inEdges中删掉
edges.remove(edge);
}
}
/**
* 删除边
* @param from
* @param to
*/
@Override
public void removeEdge(V from, V to) {
// 根据传入的from获得起点,不存在则不需要删除
Vertex fromVertex=vertices.get(from);
if (fromVertex ==null){
return;
}
// 根据传入的to找到终点,不存在则不需要删除
Vertex toVertex=vertices.get(to);
if (toVertex==null) return;
// 根据起点和终点获得边,然后删除
Edge edge=new Edge<>(fromVertex,toVertex);
if (fromVertex.outEdges.remove(edge)){ //表示删除成功
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
}
/**
* 顶点
* @param
* @param
*/
private static class Vertex{
//顶点值
V value;
Set> inEdges=new HashSet<>();
Set> outEdges=new HashSet<>();
public Vertex(V value) {
this.value = value;
}
/**
* 比较两个顶点是否相等
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
return Objects.equals(value,((Vertex)obj).value);
}
@Override
public int hashCode() {
return value==null ? 0: value.hashCode();
}
}
/**
* 边
* @param
* @param
*/
private static class Edge {
Vertex from;
Vertex to;
E weight;
public Edge(Vertex from, Vertex to) {
this.from = from;
this.to = to;
}
/**
* 判断两条边是否相同
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
Edge edge=(Edge) obj;
return Objects.equals(from,edge.from) && Objects.equals(to,edge.to);
}
/**
* 如果equals的结果是true说明一样,则hashcode值也一样
* @return
*/
@Override
public int hashCode() {
return from.hashCode()+to.hashCode();
}
}
}
图的遍历
从图中某一顶点出发访问图中其余顶点,且每一个顶点仅被访问一次
图有2种常见的遍历方式(有向图、无向图都适用)
发明“深度优先搜索”算法的2位科学家在1986年共同获得计算机领域的最高奖:图灵奖。
package graph;
import java.util.List;
import java.util.Set;
public interface Graph {
// 边的数量
int edgesSize();
// 顶点数量
int verticesSize();
// 添加顶点
void addVertex(V v);
// 添加边(无权值)
void addEdge(V from,V to);
// 添加边(有权值)
void addEdge(V from,V to,E weight);
// 删除顶点
void removeVertex(V v);
// 删除边
void removeEdge(V from,V to);
void print();
void bfs(V begin, vertexVisitor visitor); // 广度优先搜索
/**
* 非递归版的深度优先搜索
* @param begin
* @param visitor
*/
void dfs(V begin, vertexVisitor visitor);
List topologicalSort(); // 拓扑排序
interface vertexVisitor{
boolean visit(V v);
}
}
之前所学的二叉树层序遍历就是一种广度优先搜索。
注:BFS结果不唯一。
经历一层可以访问到的
将该点从队列取出来,然后再将其所子节点依次放入队列
从某个点开始,将它可以到达的点放入队列,如果已经访问过则跳过,然后从队列中取出点重复该过程。
第一层:假设从点A开始,它可以到达B、F,则将B、F入队。
此时队列中元素 [B、F]
第二层:队头B出队,B可以到达C、I、G,将C、I、G入队。
此时队列中元素 [F、C、I、G]
第三层:队头F出队,F可以到达G、E,但G已访问过,将E入队。
此时队列中元素 [C、I、G、E]
第四层:队头C出队,C可以到达I、D,但I已访问过,将D入队。
此时队列中元素 [I、G、E、D]
第五层:队头I出队,I可以到达D,但D已访问过,不执行操作。
此时队列中元素 [G、E、D]
第六层:队头G出队,G可以到达D、H,但D已访问过,将H入队。
此时队列中元素 [E、D、H]
第七层:队头E出队,E可以到达D、H、F,都访问过,不执行操作。
此时队列中元素 [D、H]
第八层:队头D出队,D可以到达C、H、E,都访问过,不执行操作。
此时队列中元素 [H]
第九层:队头H出队,H可以到达D、G、E,都访问过,不执行操作。
此时队列中元素 []
队列为空,广度优先搜索结束。
/**
* 广度优先搜索:BFS
* @param begin
* @param visitor
*/
@Override
public void bfs(V begin, vertexVisitor visitor) {
if (visitor ==null) return;
//根据传入的值begin找到顶点
Vertex beginVertex=vertices.get(begin);
//该顶点不存在,不做操作
if (beginVertex==null) return;
//存放已经访问过的节点
//因为如果不存放,则遍历到下一个顶点的时候,会将刚刚出队列的数值再一次放入
Set> visitedVertices=new HashSet<>();
//临时存放
Queue> queue=new LinkedList<>();
queue.offer(beginVertex); //元素入队
//将已经加入过队列的元素标记一下
visitedVertices.add(beginVertex);
//思考参考二叉树层次遍历,队列存放每一层的顶点,用集合记录已经访问过的点
while (!queue.isEmpty()){
//从队列中取出第一个顶点
Vertex vertex=queue.poll();
if (visitor.visit(vertex.value)) return;
//遍历【从队列中取出的顶点】的出去的边,将【这些边的终点】入队,并且标记为已经访问过
for (Edge edge:vertex.outEdges){
//如果集合中已经记录该顶点,说明已经访问过,跳过进行下一轮
if (visitedVertices.contains(edge.to)) continue;
queue.offer(edge.to);
visitedVertices.add(edge.to);
}
}
}
之前所学的二叉树前序遍历就是一种深度优先搜索。
注:DFS结果不唯一。
/**
* 递归实现--->深度优先遍历:DFS
* @param begin
* @param visitedVertices 已经访问过的节点
*/
/**
* 递归实现深度优先搜索DFS
*/
public void dfs(V begin) {
Vertex beginVertex = vertices.get(begin); // 根据传入的值获取顶点
if (beginVertex == null) return; // 顶点不存在则不执行操作
dfs(beginVertex, new HashSet<>()); // 传入的集合,用来记录访问过的顶点
}
private void dfs(Vertex vertex, Set> vistedVertices){
System.out.println(vertex.value);
vistedVertices.add(vertex);
for(Edge edge : vertex.outEdges){
if(vistedVertices.contains(edge.to)) continue;
//dfs会自己后退
dfs(edge.to, vistedVertices);
}
}
1.从outEdge中选择一条边
2.将选择边的from和to边,按顺序入栈【先入from再入to】
3.打印选择边的to
4.将to边添加到已经访问的范围中
5.break
/**
* 非递归版(通过栈实现):深度优先搜索(DFS)
* @param
*/
@Override
public void dfs(V begin, vertexVisitor visitor) {
if (visitor ==null) return;
//查看该点是否存在
Vertex beginVertex=vertices.get(begin);
if (begin==null) return;
//创建一个set存放已经访问过的节点
Set> visitedVertices=new HashSet<>();
//创建一个栈临时存放
Stack> stack=new Stack<>();
//先访问起点
stack.push(beginVertex);
visitedVertices.add(beginVertex);
//如果该节点已经访问过,则不再放入
if (visitor.visit(begin)) return;
while (!stack.isEmpty()){ //如果栈不为空
Vertex vertex=stack.pop();
for (Edge edge :vertex.outEdges){ //访问出发点的每一条边
if (visitedVertices.contains(edge.to)) continue; //表示这条边已经访问过
//找了一条适合且未遍历过的边
stack.push(edge.from);//将出发点加入栈中
stack.push(edge.to);//将出度点加入栈中
//将加入栈中的节点加入已经访问过发范围中
visitedVertices.add(edge.to);
if (visitor.visit(edge.to.value)) return;
break;
}
}
}
一项大的工程常被分为多个小的子工
子工程之间可能存在一定的先后顺序,即某些子工程必须在其他的一些子工程完成后才能开始。
在现代化管理中,人们常用有向图来描述和分析一项工程的计划和实施过程,子工程被称为活动(Activity)
以顶点表示活动、有向边表示活动之间的先后关系,这样的图简称为 AOV 网。【有很强的依赖关系】
标准的AOV网必须是一个有向无环图(Directed Acyclic Graph,简称 DAG)
前驱活动:有向边起点的活动称为终点的前驱活动
后继活动:有向边终点的活动称为起点的后继活动
可以使用卡恩算法(Kahn于1962年提出)完成拓扑排序。
假设 L 是存放拓扑排序结果的列表:
- ① 把所有入度为 0 的顶点放入 queue 中,然后把这些顶点从图中去掉,放入list中
- ② 然后找到已经从queue中移除的入度为0的节点的下一个节点,将下一个节点的入度-1,如果-1后,入度为0,则直接加入queue
- ③ 重复操作 ①,直到找不到入度为 0 的顶点
- 如果此时 L 中的元素个数和顶点总数相同,说明拓扑排序完成
- 如果此时 L 中的元素个数少于顶点总数,说明原图中存在环,无法进行拓扑排序
/**
* 拓扑排序
* @return
*/
@Override
public List topologicalSort() {
List list = new ArrayList<>();
Queue> queue = new LinkedList<>();
Map, Integer> ins = new HashMap<>();
// 初始化(将度为0的节点放入队列)
vertices.forEach((V v, Vertex vertex) -> {
int indegree = vertex.inEdges.size(); // 入度
if(indegree == 0) { // 入度为0,放入队列
queue.offer(vertex);
} else { // 入度不为0,用map记录它的入度
ins.put(vertex, indegree);
}
});
while(!queue.isEmpty()){ // 从队列中取节点
//取出队列中的第一个元素
Vertex vertex = queue.poll();
list.add(vertex.value); // 放入返回结果中
//遍历取出的节点的出度边
for (Edge edge : vertex.outEdges){
// 队列中取出节点所通向节点的入度
int toIndegree = ins.get(edge.to) - 1;
if(toIndegree == 0) { // 入度为0,放入队列
queue.offer(edge.to);
} else { // 入度不为0,用map记录它的入度
ins.put(edge.to, toIndegree);
}
}
}
return list;
}