图的实现和遍历BFS和DFS

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 +
                    '}';
        }
    }

image-20200416170749410
  • 在图的接口中,使用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);
        }
    }

总结:

删除图的边要删除三个地方的边:

  1. 起点的OutEdges
  2. 终点的InEdges
  3. 整个图的 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、遍历

  1. bfs : Breadth first search

思路:

和二叉树的层序遍历很像,应用队列,一层一层的遍历

注意在图中,下一层指的是 此节点一步能到达的位置

image-20200416202510101

不过这里变成了遍历顶点的 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);
            }
        }
    }
  1. DFS : depth frist search
  • 递归实现

思路:我们在想递归的时候千万不要往深了想,其实还是很简单的

image-20200416204139165

  • 代码:
//深度优先搜素,相当于前序遍历!
    @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去重和上面的去重思路差不多!

总结:如何理解循环中的递归:

image-20200416204409099

如果是上述图我们从 节点 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节点的值···

猜你喜欢

转载自www.cnblogs.com/bingstudy/p/12728734.html