8.2、总结图的实现
1、图(graph)是由什么组成的?
- 节点
- 边
2、边和节点怎么构建?
- 边和节点肯定是Graph的内部类
- 节点:上包含 value 值 和以它为终点的边,还有以它为起点的边
- 边:边可能有权重,就是上图的weight 肯定包括 到达的顶点(to) 和开始的顶点(from)
边和顶点的创建
//顶点内部类
private static class Vertex<V, E> {
//节点存储的值
V value;
//从节点出去的边,用set来存储,去重
Set<Edge<V, E>> outEdges = new HashSet<>();
//指向节点的边
Set<Edge<V, E>> inEdges = new HashSet<>();
public Vertex(V value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
return Objects.equals(value, ((Vertex<V, E>)o).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> {
Vertex<V, E> from; //边的起点
Vertex<V, E> to; //边的终点
E weight; //边的权重
public Edge(Vertex<V, E> from, Vertex<V, E> to) { //构造方法
this.from = from;
this.to = to;
}
@Override
public boolean equals(Object obj) { //重写 equals和hashCode方法,因为边不能重复存储在哈希表中
// if (this == o) return true;
// if (this.getClass() != o) return false;
// Edge<?, ?> edge = (Edge<?, ?>) o;
// return from.equals(edge.from) &&
// to.equals(edge.to);
if (this == obj) return true;
if (this.getClass() != obj.getClass()) return false;
Edge<V, E> edge = (Edge<V, E>) obj;
return Objects.equals(from, edge.from) && Objects.equals(to, edge.to);
}
@Override
public int hashCode() {
return from.hashCode() * 31 + to.hashCode();
}
@Override
public String toString() { //toSting方法,from会调用它自己的toString
return "Edge{" +
"from=" + from +
", to=" + to +
", weight=" + weight +
'}';
}
}
- 图
- 在图的接口中,使用map来存储顶点,使用set来存储边
//Map中的key,对应 它的顶点,用来存储顶点
private Map<V, Vertex<V, E>> vertices = new HashMap<>();
//set用来存储边!
private Set<Edge<V, E>> edges = new HashSet<>();
1、创建边和顶点
@Override
public void addVertex(V v) {
if (vertices.get(v) != null) {
return;
}
vertices.put(v, new Vertex<>(v));
}
@Override
public void addEdge(V from, V to, E weight) { //创建边要传递起点和终点
Vertex<V, E> vertexFrom = vertices.get(from);
//如果顶点为空,则我们创建顶点
if (vertexFrom == null) {
vertexFrom = new Vertex<>(from);
vertices.put(from, vertexFrom);
}
Vertex<V, E> vertexTo = vertices.get(to);
if (vertexTo == null) {
vertexTo = new Vertex<>(to);
vertices.put(to, vertexTo);
}
Edge<V, E> edge = new Edge<>(vertexFrom, vertexTo);
edge.weight = weight;
//这里再起点和终点添加上这条边,先删除再添加!,因为更新了权值
if (vertexFrom.outEdges.remove(edge)) {
vertexTo.inEdges.remove(edge);
edges.remove(edge);
}
//更新了weight,真正添加
vertexFrom.outEdges.add(edge);
vertexTo.inEdges.add(edge);
edges.add(edge);
}
@Override
public void addEdge(V from, V to) {
addEdge(from, to, null);
}
2、删除边和顶点
- 删除边
@Override
public void removeEdge(V from, V to) {
//怎么删除边?边是怎么确定:from 和 to确定一条边,两点确定一条边
//我们需要再from的outEdges中删了,和to的inEdges上了,还有总集合 edges上也删了!
//Vertex<V, E> vertexFrom = vertices.get(from);
Vertex<V, E> vertexFrom = vertices.get(from);
if (vertexFrom == null) return;
Vertex<V, E> vertexTo = vertices.get(to);
if (vertexTo == null) return;
//先创造出来再删除
Edge<V, E> edge = new Edge<>(vertexFrom, vertexTo);
if (vertexFrom.outEdges.remove(edge)) {
vertexTo.inEdges.remove(edge);
edges.remove(edge);
}
}
总结:
删除图的边要删除三个地方的边:
- 起点的OutEdges
- 终点的InEdges
- 整个图的 edges
并且我们删除的时候用了先创建再删除的方法!
- 删除顶点
@Override
public void removeVertex(V v) {
//删除顶点,要把它包含的边都删了
//怎么删? 遍历 OutEdges集合:删除它到达顶点的InEdges
Vertex<V, E> vertex = vertices.remove(v);
if (vertex == null)
return;
Set<Edge<V, E>> outEdges = vertex.outEdges;
Set<Edge<V, E>> inEdges = vertex.inEdges;
for (Iterator<Edge<V, E>> iterator = outEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.to.inEdges.remove(edge);
// 将当前遍历到的元素edge从集合vertex.outEdges中删掉
iterator.remove();
edges.remove(edge);
}
for (Iterator<Edge<V, E>> iterator = inEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.from.outEdges.remove(edge);
// 将当前遍历到的元素edge从集合vertex.outEdges中删掉
iterator.remove();
edges.remove(edge);
}
}
总结:要删除一个顶点要把它所占的边全部删除,也就是它的inEdges 和 OutEdges集合上的边都删除
怎么删?遍历它的outEdges每个边的终点的inEdge删除这个条边,inEdge类似:
Set<Edge<V, E>> outEdges = vertex.outEdges;
Set<Edge<V, E>> inEdges = vertex.inEdges;
for (Iterator<Edge<V, E>> iterator = outEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.to.inEdges.remove(edge);
// 将当前遍历到的元素edge从集合vertex.outEdges中删掉
iterator.remove();
edges.remove(edge);
}
for (Iterator<Edge<V, E>> iterator = inEdges.iterator(); iterator.hasNext();) {
Edge<V, E> edge = iterator.next();
edge.from.outEdges.remove(edge);
// 将当前遍历到的元素edge从集合vertex.outEdges中删掉
iterator.remove();
edges.remove(edge);
}
3、遍历
- bfs : Breadth first search
思路:
和二叉树的层序遍历很像,应用队列,一层一层的遍历
注意在图中,下一层指的是 此节点一步能到达的位置
不过这里变成了遍历顶点的 outEdges 依次入队,不过当图有环的时候可能重复出现斯循环,所以我们用set来去重!
- 代码
//遍历图,使用DFS算法!
public void bfs(V value) {
Vertex<V, E> vertex = vertices.get(value); //遍历的起点
if (vertex == null) return;
Queue<Vertex<V, E>> queue = new LinkedList<>(); //使用队列,java中队列由双向链表实现
Set<Vertex<V, E>> set = new HashSet<>(); //用set去重,怎么去?入队的同时在set中加上
queue.offer(vertex);
set.add(vertex);
while (!queue.isEmpty()) {
Vertex<V, E> poll = queue.poll();
System.out.println(poll.value);
for (Edge<V, E> outEdge : poll.outEdges) { //outEdges的终点就是下一层节点
if (set.contains(outEdge.to)) continue;
queue.offer(outEdge.to);
set.add(outEdge.to);
}
}
}
- DFS : depth frist search
- 递归实现
思路:我们在想递归的时候千万不要往深了想,其实还是很简单的
- 代码:
//深度优先搜素,相当于前序遍历!
@Override
public void dfs(V begin) {
Vertex<V, E> vertex = vertices.get(begin);
if (vertex == null)
return;
dfs(vertex, new HashSet<>());
}
private void dfs(Vertex<V,E> vertex, Set<Vertex<V, E>> set) {
if (vertex == null)
return;
System.out.println(vertex.value);
set.add(vertex);
for (Edge<V, E> outEdge : vertex.outEdges) {
if (set.contains(outEdge.to)) continue;
dfs(outEdge.to, set);
}
}
又要使用set去重和上面的去重思路差不多!
总结:如何理解循环中的递归:
如果是上述图我们从 节点 1 开始遍历:上来就会进入for循环,遍历了
dfs(3) 、 dfs(5) 、dfs(2) 、dfs(0)
进入dfs(3) 又是进行深度优先搜索,遍历 7 ,遍历完3,才会遍历 dfs(5) ···
就这么理解就行!
3、DFS非递归实现
所有的递归都能转化成迭代,不过有的很难
为什么呢?
因为递归就是函数JVM压栈出栈的过程,而我们数据结构中的栈也是这个特点,所以很多复杂的递归都能使用栈和迭代来实现!
代码:主要是想到要用到栈,结合栈先进后出的特点进行深度思考!
public void dfs(V begin, Visitor<V, E> visitor) {
if (visitor == null) return;
Vertex<V, E> vertex = vertices.get(begin);
if (vertex == null) {
return;
}
Stack<Vertex<V, E>> stack = new Stack<>();
Set<Vertex<V, E>> visited = new HashSet<>();
stack.push(vertex);
if (visitor.visit(vertex.value)) return;
visited.add(vertex);
while (!stack.isEmpty()) {
Vertex<V, E> pop = stack.pop();
for(Edge<V, E> edge : pop.outEdges) {
if (visited.contains(edge.to)) continue;
stack.push(pop);
stack.push(edge.to);
if (visitor.visit(edge.to.value)) return;
visited.add(edge.to);
break;
}
}
}
总结:set是无序的所以不知道先遍历那条边,为什么要 把from 和 to都放进去在Break,我们每次只访问to节点的值···