• 图(一):无向图的深度优先遍历、广度优先遍历及连通分量


    无向图:

    一些关于图的定义:

    图是由一组顶点和一组能够将两个顶点相连的边组成。

    连通图:如果从任意一个顶点都存在一条路径到达另一个任意顶点,就称为连通图,一个非连通图由若干连通的部分组成,都称为极大连通子图。

    无向图:即连接两个顶点的边是没有方向的。

    无向图的数据结构:

    使用邻接表来表示图:

    如上图所示,使用一个链表数组来表示图,其中数组的索引表示所有的顶点,每个数组中存放的链表表示所有与此顶点相连的顶点,也可以理解为边。

    数据结构如下:

    先看链表:

    public class LinkedList<T> {
        private Integer N = 0;
        private Node<T> root;
        
        //链表的结点
        @SuppressWarnings("hiding")
        class Node<T> {
            T value;
            Node<T> next;
            
            public Node(T t,Node<T> node) {
                this.value = t;
                this.next = node;
            }
        }
        
        //添加一个结点
        public void add(Node<T> node) {
            if(root == null)
                root = node;
            else {
                Node<T> temp = root;
                root = node;
                root.next = temp;
            }
            N++;
        }
        
        public void add(T t) {
            Node<T> node = new Node<T>(t,null);
            if(root == null)
                root = node;
            else {
                Node<T> temp = root;
                root = node;
                root.next = temp;
            }
            N++;
        }
        
        //链表的节点数
        public Integer size() {
            return N;
        }
        
        public Node<T> getRoot() {
            return root;
        }
        
        
        //遍历链表
        public void traverse() {
            Node<T> node = root;
            while(node != null) {
                System.out.print(node.value + " ");
                node = node.next;
            }
        }
        
        
        //获取链表上的所有节点的值,返回一个数组
        public Integer[] getAllValue(LinkedList<T> linkedList) {
            if(linkedList.root == null)
                return null;
            
            Integer[] result = new Integer[linkedList.size()];
            Node<T> node = linkedList.root;
            Integer i = 0;
            while(node != null) {
                result[i++] = (Integer)node.value;
                node = node.next;
            }
            return result;
        }
    }

    图的数据结构:

    public class UndiGraphBase {
        //顶点数目
        private Integer V;
        
        //边的数目
        private Integer E;
        
        //邻接表
        public LinkedList<Integer>[] adj;
        
        //创建一个含有V个顶点但不含有边的图
        @SuppressWarnings("unchecked")
        void graph(Integer V) {
            this.V = V;
            this.E = 0;
            adj = (LinkedList<Integer>[]) new LinkedList[V];
            for(int v=0;v<V;v++) {
                adj[v] = new LinkedList<Integer>();
            }
        }
        
        public Integer V() {
            return V;
        }
        
        public Integer E() {
            return E;
        }
        
        
        //添加一条边
        public void addEdge(Integer v,Integer w) {
            adj[v].add(w);
            adj[w].add(v);
            E++;
        }
        
        //遍历
        public void traverse() {
            for(int i=0;i<V;i++) {
                System.out.print(i + ": ");
                adj[i].traverse();
                System.out.println();
            }
        }
        
        
        //获取某个顶点所有相邻的顶点
        public Integer[] getAllValueByIndex(Integer i) {
            return adj[i].getAllValue(adj[i]);
        }
    }

    只要静下心来看,这些代码都比较简单,并且都有注释,就不多说了。

    深度优先遍历

    深度优先遍历类似于一个人走迷宫:

    如图所示,从起点开始选择一条边走到下一个顶点,没到一个顶点便标记此顶点已到达。

    当来到一个标记过的顶点时回退到上一个顶点,再选择一条没有到达过的顶点。

    当回退到的路口已没有可走的通道时继续回退。

    先给出一个已经构建好的图,之后的代码都建立在这个图的实例上:

    public class Instance {
        public static UndiGraphBase getInstance() {
            UndiGraphBase undiGraphBase = new UndiGraphBase();
            undiGraphBase.graph(13);
            undiGraphBase.addEdge(0, 5);
            undiGraphBase.addEdge(4, 3);
            undiGraphBase.addEdge(0, 1);
            undiGraphBase.addEdge(9, 12);
            undiGraphBase.addEdge(6, 4);
            undiGraphBase.addEdge(5, 4);
            undiGraphBase.addEdge(0, 2);
            undiGraphBase.addEdge(11, 12);
            undiGraphBase.addEdge(9, 10);
            undiGraphBase.addEdge(0, 6);
            undiGraphBase.addEdge(7, 8);
            undiGraphBase.addEdge(9, 11);
            undiGraphBase.addEdge(5, 3);
            return undiGraphBase;
        }
    }

     

    该实例表示的图如下:

    深度遍历的顺序如下图所示:

    数组marked表示索引所代表的顶点是否被访问过

    数组edgeTO[]表示到达索引所代表的顶点的上一个顶点,通过对此数组的处理即可得到路径,即可以用循环,也可以用栈,因为知道每个顶点的上一个顶点,所以可以得到路径

    遍历的代码如下:

    public class DepthFirstPath {
        //所有顶点组成的数组,如果起点到某顶点可达,则为true,否则为false
        private boolean[] marked;
        
        //起点
        private final Integer startV;
        
        //用数组来表示路径树,表示起点到可达顶点的路径。保存的是到达此顶点的上一个顶点
        private Integer[] edgeTo;
        
        public DepthFirstPath(UndiGraphBase G,Integer startV) {
            marked = new boolean[G.V()];
            edgeTo = new Integer[G.V()];
            this.startV = startV;
            dfs(G,startV);
        }
        
        public void dfs(UndiGraphBase G,Integer v) {
            marked[v] = true;
            Integer[] allNodeValues = G.getAllValueByIndex(v);
            if(allNodeValues == null)
                return;
            
            for(int i=0;i<allNodeValues.length;i++) {
                if((!marked[allNodeValues[i]])) {
                    edgeTo[allNodeValues[i]] = v;
                    dfs(G,allNodeValues[i]);
                }
            }
        }
        
        
        //获取顶点到v的路径
        public void getPath(UndiGraphBase G,Integer v) {
            if(!hasPathTo(v))
                return;
            
            //存储路径
            Stack<Integer> path = new Stack<Integer>();
            for(int i=v;i!=startV;i = edgeTo[i])
                path.push(i);
            
            //加入起点
            path.push(startV);
            
            //打印栈(即起点startV到v的路径)
            System.out.println(path.toString());
        }
        
        //从起点startV是否有路径通往v(即是否可达)
        public boolean hasPathTo(Integer v) {
            return marked[v];
        }
        
        public static void main(String[] args) {
            UndiGraphBase G = Instance.getInstance();
            DepthFirstPath depthFirstPath = new DepthFirstPath(G,0);
            depthFirstPath.getPath(G, 3);
        }
    }

    其实整个代码是比较好懂的,主要是edgeto数组需要理解一下。

    广度优先遍历

    广度优先遍历其实比深度优先遍历好理解,即从起点开始,遍历所有与起点连通的顶点,再遍历与这些顶点连通的顶点,即先搜索距离起点距离为1的顶点,再遍历与起点距离为2的顶点.....

    其中经edgeTo数组的意义与深度优先中edgeTo数组一样。

    而distTo[]数组则表示与起点的距离,图中使用该数组只是为了便于理解,代码中可以不用,因为距离可以通过遍历edgeTO[]数组得到

    广度优先遍历的代码:

    public class BreadthFirstPath {
        //到达该顶点的最短路径是否已知
        private boolean[] marked;
        
        //到达该顶点的已知路径上的最后一个顶点
        private Integer[] edgeTo;
        
        //起点
        private final Integer start;
        
        public BreadthFirstPath(UndiGraphBase G,Integer start) {
            marked = new boolean[G.V()];
            edgeTo = new Integer[G.V()];
            this.start = start;
            bfs(G,start);
        }
        
        private void bfs(UndiGraphBase G,Integer start) {
            Queue<Integer> queue = new java.util.LinkedList<Integer>();
            //标记起点
            marked[start] = true;
            queue.add(start);
            
            while(!queue.isEmpty()) {
                //从队列中删除下一顶点
                Integer v = queue.remove();
                
                Integer[] allNodeValues = G.getAllValueByIndex(v);
                for(int i=0;i<allNodeValues.length;i++) {
                    //对于每个未被标记的顶点
                    if(!marked[allNodeValues[i]]) {
                        //保存最短路径的最后一个顶点
                        edgeTo[allNodeValues[i]] = v;
                        //标记它,因为最短路径已知
                        marked[allNodeValues[i]] = true;
                        queue.add(allNodeValues[i]);
                    }
                }
            }
        }
        
        public boolean hasPathTo(Integer v) {
            return marked[v];
        }
        
        //获取顶点到v的路径
        public void getPath(UndiGraphBase G,Integer v) {
            if(!hasPathTo(v))
                return;
            
            //存储路径
            Stack<Integer> path = new Stack<Integer>();
            for(int i=v;i!=start;i = edgeTo[i])
                path.push(i);
            
            //加入起点
            path.push(start);
            
            //打印栈(即起点startV到v的路径)
            System.out.println(path.toString());
        }
        
        public static void main(String[] args) {
            UndiGraphBase G = Instance.getInstance();
            BreadthFirstPath breadthFirstPath = new BreadthFirstPath(G,0);
            breadthFirstPath.getPath(G, 3);
        }
    }

     

    使用深度优先遍历获取图中的所有连通分量

    public class ConnectedComponent {
        //顶点是否联通
        private boolean[] marked;
        
        //联通分量标识符
        private Integer[] id;
        
        //联通分量数目
        private Integer count = 0;
        
        public ConnectedComponent(UndiGraphBase G) {
            marked = new boolean[G.V()];
            id = new Integer[G.V()];
            for(int i=0;i<G.V();i++) {
                if(!marked[i]) {
                    dfs(G,i);
                    count++;
                }
            }
        }
        
        public void dfs(UndiGraphBase G,Integer v) {
            marked[v] = true;
            id[v] = count;
            
            Integer[] allNodeValues = G.getAllValueByIndex(v);
            for(int i=0;i<allNodeValues.length;i++) {
                if((!marked[allNodeValues[i]]))
                    dfs(G,allNodeValues[i]);
            }
        }
        
        //判断v和w是否联通
        public boolean connected(Integer v,Integer w) {
            return id[v] == id[w];
        }
        
        //连通分量数
        public Integer count() {
            return count;
        }
        
        //v顶点所在的联通分量的标识符(0-count()-1)
        public Integer id(Integer v) {
            return id[v];
        }
        
        public Integer[] getID() {
            return id;
        }
        
        public static void main(String[] args) {
            UndiGraphBase G = Instance.getInstance();
            ConnectedComponent cc = new ConnectedComponent(G);
            
            Integer m = cc.count();
            System.out.println(m + " components");
            
            Integer[] id = cc.getID();
            
            for(int j=0;j<m;j++) {
                for(int i=0;i<id.length;i++) {
                    if(id[i] == j) {
                        System.out.print(i + " ");
                    }
                }
                System.out.println();
            }
    
        }
    }

     

    其中marked数组表示索引所代表的顶点是否已经被访问

    Id数组表示索引所代表的顶点属于哪个连通分量,假如一个图总共有n个分量,那么各个连通分量分别用0,1,2....n-1表示,id数组的值即是该顶点属于第几个连通分量。

    广度优先遍历与深度优先遍历的区别

    深度优先遍历不断深入图中并在栈中保存了所有分叉的顶点;广度优先遍历则像扇面一般扫描图,用一个队列保存访问过的最前端的顶点。深度遍历的方式是寻找离起点更远的顶点,只在碰到死胡同时才访问进出的顶点,广度遍历则会首先覆盖起点附近的顶点,只在临近的顶点都被访问完后,才去访问更远的顶点。

    深度优先遍历的路径通常较长而曲折,广度优先遍历的路径通常短而直接。

    关于广度优先遍历和深度优先遍历的使用场景与效率如下图所示:

  • 相关阅读:
    java 8 stream sql left join =》 jooq & Flink & Scala
    Maven error: lambda expressions are not supported in -source 1.7
    error C2039: 'SetWindowTextA' : is not a member of 'CString'
    循环队列(循环数组)中元素个数的计算
    数据结构之堆
    理解C语言声明的优先级规则
    内联汇编中的asm和__asm__
    程序启动时的堆栈
    局部变量与堆栈
    BCD码干什么用的?
  • 原文地址:https://www.cnblogs.com/edwinchen/p/4806325.html
Copyright © 2020-2023  润新知