图的结构和算法很常用,基于《算法4》中的描述,总结手撕了一波;
包含以下图的基础算法:
无向图:深度优先遍历、广度优先遍历、连通分量、最短路径;
有向图:深度优先遍历、广度优先遍历、可达性分析、最短路径;

一些概念 

图:顶点和边构成;
自环:一条连接一个顶点和其自身的边;
平行边:连接同义对顶点的两条边;
连通分量(无向图):互相连通的顶点;
可达性(有向图):一个顶点可以到达另外一个顶点;
最短路径:一个顶点连通(到达)另外一个顶点最少需要走过的顶点集合;
二分图:能够将所有节点分为两部分的图,其中每条边所连接的两个顶点都分别属于不同的部分;

邻接矩阵和邻接表 

图的结构,可使用邻接矩阵和邻接表,两者区别:
邻接矩阵:二维数组标识顶点之间的连接,数组的长宽都为顶点数,所以存储n个节点,需要n*n的内存;
邻接表:数组+集合,类似HashMap的数据+链表结构;
邻接表节省空间,适合存储顶点数量大的图,比如100w级别+的顶点,支持自环和平行边;这也是《算法4》中推荐使用邻接表数据结构的原因。

demo中用到的无向图结构: 

有向图 深度学习 有向图深度优先_数据结构

demo中用到的有向图结构: 

有向图 深度学习 有向图深度优先_System_02

 

上代码:

抽象图类 Graph.java

/**
 * @Author: ltx
 * @Description:
 */
public abstract class Graph {
    public int v;  //顶点数量
    public int e;  //边数量
    public List<Integer>[] adj;    //邻接表

    /**
     * 构造方法-初始化图
     * @param v
     */
    public Graph(int v) {
        //初始化顶点数
        this.v = v;
        //初始化边数
        this.e = 0;
        //初始化邻接表
        this.adj = new List[v];
        for (int i = 0; i < v; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    /**
     * 添加节点
     * @param v
     * @param w
     */
    public abstract void addEdge(int v, int w);

    /**
     * 顶点v的度数
     * 度数:顶点的边
     *
     * @param v
     * @return
     */
    public int degree(int v) {
        int count = adj[v].size();
        System.out.printf("顶点 %d 的出度数: %d\n", v, count);
        return count;
    }

    /**
     * 最大度数
     *
     * @return
     */
    public int maxDegree() {
        int v = 0;//顶点
        int max = 0;//度数
        for (int i = 0; i < adj.length; i++) {
            if (adj[i].size() > max) {
                v = i;
                max = adj[i].size();
            }
        }
        System.out.printf("度数最多顶点: %d, 度数: %d\n", v, max);
        return max;
    }

    /**
     * 打印邻接表结构
     */
    public void show() {
        System.out.printf("%d 个顶点, %d 条边\n", v, e);
        for (int i = 0; i < v; i++) {
            System.out.println(i + ": " + adj[i]);
        }
    }

    /**
     * 平均度数
     * 无向图
     * 有向图-出度数
     *
     * @return
     */
    public abstract String avgDegree();

    /**
     * 自环个数
     *
     * @return
     */
    public abstract int numberOfSelfLoops();
}

无向图类 UndigraphGraph.java

/**
 * 无向图
 */
public class UndigraphGraph extends Graph {
    public UndigraphGraph(int v) {
        super(v);
    }

    /**
     * 添加边
     *
     * @param v 起点
     * @param w 终点
     */
    @Override
    public void addEdge(int v, int w) {
        //注意-当v==w时,形成自环
        adj[v].add(w);
        adj[w].add(v);
        e++;
    }

    /**
     * 所有顶点平均度数
     *
     * @return
     */
    @Override
    public String avgDegree() {
        //顶点数/边数 * 2
        BigDecimal bdV = new BigDecimal(v);
        BigDecimal bdE = new BigDecimal(e);
        BigDecimal avg = bdV.divide(bdE, 2, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(2));
        System.out.printf("所有顶点平均度数: %s\n", avg);
        return avg.toString();
    }

    /**
     * 自环个数(自环: 顶点自己连接自己)
     *
     * @return
     */
    @Override
    public int numberOfSelfLoops() {
        int count = 0;
        for (int i = 0; i < v; i++) {
            for (Integer w : adj[i]) {
                if (w.equals(i)) {
                    count++;
                }
            }
        }
        System.out.printf("自环个数: %d\n", count / 2);
        //每条边会被记录2次
        return count / 2;
    }

    /**
     * 初始化无向图
     * @return
     */
    public static UndigraphGraph init() {
        UndigraphGraph graph = new UndigraphGraph(13);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(0, 5);
        graph.addEdge(0, 6);
        graph.addEdge(3, 4);
        graph.addEdge(3, 5);
        graph.addEdge(4, 5);
        graph.addEdge(4, 6);
        graph.addEdge(7, 8);
        graph.addEdge(9, 10);
        graph.addEdge(9, 11);
        graph.addEdge(9, 12);
        graph.addEdge(11, 12);
        //平行边(多条边连接两个顶点)
//        graph.addEdge(11, 12);
        //自环(自己连接自己)
//        graph.addEdge(5, 5);
        return graph;
    }

    public static void main(String[] args) {
        System.out.println("################无向图################");
        //初始化一个图(来自《算法4》的无向图demo)
        Graph graph = UndigraphGraph.init();
        System.out.println("-------------------邻接表结构-------------------");
        graph.show();
        //图自环个数
        graph.numberOfSelfLoops();
        //平均度数
        graph.avgDegree();
        //顶点度数
        graph.degree(3);
        //最大度数顶点
        graph.maxDegree();

        //深度优先遍历
        System.out.println("-------------------深度优先遍历-------------------");
        DepthFirstSearch dfs = new DepthFirstSearch(graph);
        dfs.startDfs(0);
        System.out.println("遍历顺序: " + dfs.searchLs);
        //连通分量
        dfs.weight();
        System.out.printf("连通分量: %d 个\n", dfs.connectedWeightCount);
        dfs.printWeight();
        System.out.printf("%d和%d是否连通: %s\n", 0, 1, dfs.contented(0, 1));
        System.out.printf("%d和%d是否连通: %s\n", 0, 7, dfs.contented(0, 7));
        //广度优先遍历
        System.out.println("-------------------广度优先遍历-------------------");
        BreadthFirstSearch bfs = new BreadthFirstSearch(graph);
        bfs.startBfs(0);
        System.out.println("遍历顺序: " + bfs.searchLs);
        //利用广度优先遍历得到最短路径
        System.out.println("0 -> 4, 最短路径之一:");
        System.out.println(bfs.pathTo(4));
        //计算连通分量
        bfs.weight();
        System.out.printf("连通分量: %d 个\n", bfs.connectedWeightCount);
        bfs.printWeight();
        System.out.printf("%d和%d是否连通: %s\n", 0, 1, dfs.contented(0, 1));
        System.out.printf("%d和%d是否连通: %s\n", 0, 7, dfs.contented(0, 7));
    }
}

有向图类 DirectedGraph.java

/**
 * 有向图
 */
public class DirectedGraph extends Graph {

    public DirectedGraph(int v) {
        super(v);
    }

    /**
     * 添加边
     *
     * @param v 起点
     * @param w 终点
     */
    @Override
    public void addEdge(int v, int w) {
        //注意-当v==w时,形成自环
        adj[v].add(w);
        //有向图和无向图的区别
//        adj[w].add(v);
        e++;
    }

    /**
     * 所有顶点平均度数
     *
     * @return
     */
    @Override
    public String avgDegree() {
        BigDecimal bdV = new BigDecimal(v);
        BigDecimal bdE = new BigDecimal(e);
        //顶点数/边数
        BigDecimal avg = bdV.divide(bdE, 2, BigDecimal.ROUND_HALF_UP);
        System.out.printf("所有顶点平均度数: %s\n", avg);
        return avg.toString();
    }

    /**
     * 自环个数(自环: 顶点自己连接自己)
     *
     * @return
     */
    @Override
    public int numberOfSelfLoops() {
        int count = 0;
        for (int i = 0; i < v; i++) {
            for (Integer w : adj[i]) {
                if (w.equals(i)) {
                    count++;
                }
            }
        }
        System.out.printf("自环个数: %d\n", count);
        return count;
    }

    /**
     * 反转有向图,可以方便的得到指向顶点的边
     * 在图的强连通分量里面有用
     *
     * @return
     */
    public DirectedGraph reverse() {
        DirectedGraph R = new DirectedGraph(v);
        for (int i = 0; i < v; i++) {
            for (Integer w : adj[i]) {
                R.addEdge(w, i);
            }
        }
        return R;
    }

    /**
     * 初始化有向图
     *
     * @return
     */
    public static DirectedGraph init() {
        DirectedGraph graph = new DirectedGraph(13);
        graph.addEdge(0, 1);
        graph.addEdge(0, 5);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 2);
        graph.addEdge(3, 5);
        graph.addEdge(4, 2);
        graph.addEdge(4, 3);
        graph.addEdge(5, 4);
        graph.addEdge(6, 0);
        graph.addEdge(6, 4);
        graph.addEdge(6, 9);
        graph.addEdge(7, 6);
        graph.addEdge(7, 8);
        graph.addEdge(8, 7);
        graph.addEdge(8, 9);
        graph.addEdge(9, 10);
        graph.addEdge(9, 11);
        graph.addEdge(10, 12);
        graph.addEdge(11, 4);
        graph.addEdge(11, 12);
        graph.addEdge(12, 9);
        //平行边(多条边连接两个顶点)
//        graph.addEdge(12, 9);
        //自环(自己连接自己)
//        graph.addEdge(5, 5);
        return graph;
    }

    public static void main(String[] args) {
        System.out.println("################有向图################");
        //初始化一个图(来自《算法4》的有向图demo)
        DirectedGraph graph = DirectedGraph.init();
        System.out.println("-------------------邻接表结构-------------------");
        graph.show();
        //反向图
//        Graph r = graph.reverse();
//        System.out.println("反向图:");
//        r.show();
        //图自环个数
        graph.numberOfSelfLoops();
        //平均度数
        graph.avgDegree();
        //顶点度数
        graph.degree(3);
        //最大度数顶点
        graph.maxDegree();
        //深度优先遍历
        System.out.println("-------------------深度优先遍历-------------------");
        DepthFirstSearch dfs = new DepthFirstSearch(graph);
        dfs.startDfs(0);
        System.out.println("遍历顺序: " + dfs.searchLs);
        System.out.println("---可达性分析---");
        //可达性分析
        dfs.accessibility();
//        //广度优先遍历
        System.out.println("-------------------广度优先遍历-------------------");
        BreadthFirstSearch bfs = new BreadthFirstSearch(graph);
        bfs.startBfs(0);
        System.out.println("遍历顺序: " + bfs.searchLs);
        //利用广度优先遍历得到最短路径
        System.out.println("0 -> 2, 最短路径之一:");
        System.out.println(bfs.pathTo(2));
        System.out.println("---可达性分析---");
        //可达性分析
        bfs.accessibility();
    }
}

深度优先搜索类 DepthFirstSearch.java

/**
 * 深度优先搜索
 */
public class DepthFirstSearch {
    private int s;//起点
    private Graph graph;
    public boolean marked[];//标记已经遍历过的顶点
    public int count;//和v连通的顶点总数
    public List<Integer> searchLs; //遍历顶点顺序, 和v顶点连通的顶点集合
    public int connectedWeightCount; //连通分量总数
    public int[] id;//连通分量

    /**
     * 初始化深度优先遍历
     *
     * @param graph 无向图
     */
    public DepthFirstSearch(Graph graph) {
        this.graph = graph;
        //初始化遍历顶点list
        searchLs = new ArrayList<>();
        //初始化marked数组
        this.marked = new boolean[graph.v];
        id = new int[graph.v];
//        System.out.printf("起点: %d, ", s);
        //深度优先遍历
//        dfs(graph, s);
    }

    /**
     * 顶点v是否和s连通
     *
     * @param v
     * @return
     */
    public boolean marked(int v) {
        return marked[v];
    }

    /**
     * 发起-深度优先遍历
     *
     * @param s
     */
    public void startDfs(int s) {
        System.out.printf("起点: %d, ", s);
        //设置起点
        this.s = s;
        dfs(s);
    }

    /**
     * 深度优先遍历
     *
     * @param v 起点
     */
    public void dfs(int v) {
        //记录遍历顺序
        searchLs.add(v);
        //标记已走
        marked[v] = true;
        //和v连通的顶点总数
        count++;
        //标记连通分量
        id[v] = connectedWeightCount;
        for (int w : graph.adj[v]) {
            if (!marked[w]) {
                dfs(w);
            }
        }
    }

    /**
     * 连通分量
     */
    public void weight() {
        //初始化一下marked
        for (int i = 0; i < marked.length; i++) {
            marked[i] = false;
        }
        for (int i = 0; i < id.length; i++) {
            if (!marked[i]) {
                //深度优先遍历
                dfs(i);
                connectedWeightCount++;
            }
        }
    }

    /**
     * 可达性分析-有向图
     */
    public void accessibility() {
        for (int i = 0; i < id.length; i++) {
            //初始化一下marked
            for (int j = 0; j < marked.length; j++) {
                marked[j] = false;
                id[j] = -1;
            }
            //深度优先遍历-可达性分析
            dfsAccessibility(i, i);
            List<Integer> ls = new ArrayList<>();
            for (int j = 0; j < id.length; j++) {
                if (id[j] == i) {
                    ls.add(j);
                }
            }
            System.out.printf("%d 可达: %s\n", i, ls);
        }
    }

    /**
     * 深度优先遍历-可达性分析-有向图
     *
     * @param v 递归点
     * @param s 起点
     */
    public void dfsAccessibility(int v, int s) {
        marked[v] = true;
        //标记连通分量
        id[v] = s;
        for (int w : graph.adj[v]) {
            if (!marked[w]) {
                dfsAccessibility(w, s);
            }
        }
    }

    /**
     * 打印连通分量
     */
    public void printWeight() {
        for (int temp = 0; temp < connectedWeightCount; temp++) {
            System.out.printf("第 %d 个: ", temp + 1);
            for (int i = 0; i < id.length; i++) {
                if (this.id[i] == temp) {
                    System.out.printf("%d, ", i);
                }
            }
            System.out.println();
        }
    }

    /**
     * 两个顶点是否连通
     *
     * @param v 顶点
     * @param w 顶点
     * @return
     */
    public boolean contented(int v, int w) {
        return id[v] == id[w];
    }
}

 广度优先搜索类 BreadthFirstSearch.java

/**
 * 广度优先搜索
 */
public class BreadthFirstSearch {
    private int s;//起点
    private Graph graph;
    public boolean marked[];//标记已经遍历过的顶点
    public int count;//和起点s连通的顶点总数
    public List<Integer> searchLs; //遍历顶点顺序, 和v顶点连通的顶点集合
    public Integer edgTo[];//路径
    public int[] id;//连通分量
    public int connectedWeightCount; //连通分量总数

    /**
     * 连通分量
     */
    public void weight() {
        //初始化一下marked
        for (int i = 0; i < marked.length; i++) {
            marked[i] = false;
        }
        for (int i = 0; i < id.length; i++) {
            if (!marked[i]) {
                //广度优先遍历
                bfs(i);
                connectedWeightCount++;
            }
        }
    }

    /**
     * 打印连通分量
     */
    public void printWeight() {
        int temp = --connectedWeightCount;
        while (temp >= 0) {
            for (int i = 0; i < id.length; i++) {
                if (this.id[i] == temp) {
                    System.out.printf("%d, ", i);
                }
            }
            System.out.println();
            temp--;
        }
    }

    /**
     * 两个顶点是否连通
     *
     * @param v 顶点
     * @param w 顶点
     * @return
     */
    public boolean contented(int v, int w) {
        return id[v] == id[w];
    }

    /**
     * 广度优先遍历-初始化
     *
     * @param graph 无向图
     */
    public BreadthFirstSearch(Graph graph) {
        this.graph = graph;
        //初始化遍历顶点list
        searchLs = new ArrayList<>();
        //初始化marked数组
        marked = new boolean[graph.v];
        edgTo = new Integer[graph.v];
        id = new int[graph.v];
    }

    /**
     * 发起-广度优先遍历
     *
     * @param s
     */
    public void startBfs(int s) {
        System.out.printf("起点: %d, ", s);
        //设置起点
        this.s = s;
        bfs(s);
    }

    /**
     * 广度优先遍历
     *
     * @param v 起点
     */
    public void bfs(int v) {
        //利用队列FIFO特性处理
        Queue<Integer> queue = new LinkedList<>();
        searchLs.add(v);//起点加入路径
        marked[v] = true;//标记起点
        count++;
        //标记连通分量
        id[v] = connectedWeightCount;
        queue.add(v);//起点加入队列
        while (!queue.isEmpty()) {
            Integer temp = queue.remove();
            for (int w : graph.adj[temp]) {
                if (!marked[w]) {
                    edgTo[w] = temp;
                    searchLs.add(w);
                    marked[w] = true;
                    count++;
                    //标记连通分量
                    id[w] = connectedWeightCount;
                    queue.add(w);
                }
            }
        }
    }

    /**
     * 可达性分析-有向图
     */
    public void accessibility() {
        for (int i = 0; i < id.length; i++) {
            //初始化一下marked
            for (int j = 0; j < marked.length; j++) {
                marked[j] = false;
                id[j] = -1;
            }
            //广度优先遍历-可达性分析
            bfsAccessibility(i, i);
            List<Integer> ls = new ArrayList<>();
            for (int j = 0; j < id.length; j++) {
                if (id[j] == i) {
                    ls.add(j);
                }
            }
            System.out.printf("%d 可达: %s\n", i, ls);
        }
    }

    /**
     * 广度优先遍历-可达性分析-有向图
     *
     * @param v 递归点
     * @param s 起点
     */
    public void bfsAccessibility(int v, int s) {
        //利用队列FIFO特性处理
        Queue<Integer> queue = new LinkedList<>();
        marked[v] = true;//标记起点
        id[v] = s;
        queue.add(v);//起点加入队列
        while (!queue.isEmpty()) {
            Integer temp = queue.remove();
            for (int w : graph.adj[temp]) {
                if (!marked[w]) {
                    marked[w] = true;
                    //标记连通分量
                    id[w] = s;
                    queue.add(w);
                }
            }
        }
    }

    /**
     * 顶点v是否和s连通
     *
     * @param v
     * @return
     */
    public boolean marked(int v) {
        return marked[v];
    }

    /**
     * 起点s->顶点v的最短路径
     *
     * @param v
     * @return
     */
    public List<Integer> pathTo(int v) {
        System.out.printf("%d -> %d, ", s, v);
        //先判断是否连通
        if (!marked(v)) {
            return null;
        }
        List<Integer> path = new ArrayList<>();
        Stack<Integer> st = new Stack<>();
        Integer temp = edgTo[v];
        //放终点
        st.push(v);
        while (temp != s) {
            //放中间的路径
            st.push(temp);
            temp = edgTo[temp];
        }
        //放起点
        st.push(s);
        //栈逆序到list,构成路径
        while (!st.isEmpty()) {
            path.add(st.pop());
        }
        return path;
    }
}

无向图main执行结果:  

有向图 深度学习 有向图深度优先_算法_03

有向图main执行结果: 

有向图 深度学习 有向图深度优先_有向图 深度学习_04