数据结构与算法--图的实现(邻接表、邻接矩阵、边的数组)

数据结构与算法--图的实现(邻接表、邻接矩阵、边的数组)

应该用哪种数据结构实现图呢?主要有如下三种:

邻接矩阵

对一个拥有V个顶点的图,建立一个V*V的布尔数组,如果顶点i到j之间有边连接,则定义i行j列的元素值为true,否则为false,如果是带有权值的图,那么将true改成相应的权值,false改成一个不太可能出现的值比如Integer.MAX_VALUE。还可以专门用一个数组或者表,用来存放顶点信息,因为我们直接用0~N - 1的值代表了每个顶点,但这些数值具体指代了什么意思可以去顶点信息数组查找。不过邻接矩阵表示对于顶点数目很多(比如上百万)的图,N*N个值的空间是不能满足的。

如上,左边的无向图可以转换成右边的邻接矩阵。顶点0~3的信息存在顶点信息数组里。由于这里用的是大话数据结构(C语言)中的图,0其实就是false,1就是true。顶点v0和v1有边相连,所以在矩阵中a[0][1]a[1][0]的值为true,而v1和v3之间没有边相连所以a[1][3]a[3][1]为false。仔细观察可以发现主对角线的值全是0,这是因为我们讨论的是简单图,暂时不考虑自环的情况。以主对角线为对称轴,矩阵左下a[i][j]和对应右上的a[j][i]值是一样的,这其实是一个对称矩阵。通过邻接矩阵,我们还可以获得一些其他信息。

  • 某个顶点i的度其实就是矩阵中a[i]那行中true的个数。
  • 与顶点i相邻的顶点就是矩阵中a[i]那行中所有值为true的列下标。

邻接矩阵对于有向图也适用,只是矩阵不再是对称矩阵了。

如图v0到v3有路径,所以a[0][3]为true,但是v3到v0不存在路径,所以a[3][0]为false。在有向图中我们说到度,一般区分出度和入度。这些信息也可以从矩阵中看出。

  • 顶点i的出度是矩阵a[i]那行中值为true的个数。
  • 顶点i的入度是矩阵a[][i]那列中值为true的个数。

如果图的边是带有权值的(加权图),邻接矩阵可以使用一个二维int数组,如果两个顶点之间存在路径就用该边的权值代替原布尔数组中的true,如果两个顶点间没不存在路径就用一个不大可能出现的值代替false,由于权值可能为负数,我们选用Integer.MAX_VALUE

图中的“无穷”符号,就是我们选用的Integer的最大值。主对角线依然全是0,因为某个顶点到自身并不需要花费什么代价(可以理解为权值为0)。

虽然邻接矩阵在顶点数巨大的时候,所用空间令人发指,而且它还存了那么多没用的值——两个顶点不存在路径也存入了false或者一个不太可能出现的大值。但是无向图、有向图、加权无向图、加权有向图它都能实现,所以还是有必要动手敲一敲。

package Chap7;

import java.util.ArrayList;
import java.util.List;

/**
 * 无向图 -- 邻接矩阵
 * @param <Item> 顶点类型
 */
public class AdjMatrixGraph<Item> {
    private int vertexNum;
    private int edgeNum;
    // 邻接矩阵
    private boolean[][] adj;
    // 存放所有顶点信息
    private Item[] vertexInfo;

    // 初始化有V个顶点的图,还未加边
    public AdjMatrixGraph(Item[] vertexInfo) {
        this.vertexNum = vertexInfo.length;
        this.vertexInfo = vertexInfo;

        adj = new boolean[vertexNum][vertexNum];
    }

    public AdjMatrixGraph(Item[] vertexInfo, int[][] edges) {
        this(vertexInfo);
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public AdjMatrixGraph(int vertexNum) {
        this.vertexNum = vertexNum;
        adj = new boolean[vertexNum][vertexNum];
    }

    public AdjMatrixGraph(int vertexNum,int[][] edges) {
        this(vertexNum);
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public void addEdge(int i, int j) {
        // 对称矩阵,所以a[i][j] = a[j][i]
        adj[i][j] = true;
        adj[j][i] = true;
        edgeNum++;
    }

    public int vertexNum() {
        return vertexNum;
    }

    public int edgenum() {
        return edgeNum;
    }

    public Item getVertexInfo(int i) {
        return vertexInfo[i];
    }
    // 求某顶点的所有邻接顶点
    public List<Integer> adj(int i) {
        List<Integer> vertexAdj = new ArrayList<>();
        for (int j = 0; j < adj[i].length; j++) {
            if (adj[i][j]) {
                vertexAdj.add(j);
            }
        }
        return vertexAdj;
    }

    // 某顶点的度
    public int degree(int i) {
        int degree = 0;
        for (int j = 0; j < adj[i].length; j++) {
            if (adj[i][j]) {
               degree++;
            }
        }
        return degree;
    }
    // 求图的最大度数
    public int maxDegree() {
        int max = 0;
        for (int i = 0; i < vertexNum; i++) {
            if (degree(i) > max) {
                max = degree(i);
            }
        }
        return max;
    }
    // 求图的平均度数
    // 边的条数 = 顶点度之和的一半  因为一条边对应两个顶点,这两个顶点的度数之和为2,所以边的数量是度之和的一半这样的关系
    // edgeNum = sum / 2, 则sum = 2 * edgeNum, 于是avgDegree = sum / vertexNum
    public double avgDegree() {
        return 2.0 * edgeNum / vertexNum;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(vertexNum).append("个顶点, ").append(edgeNum).append("条边。\n");
        for (int i = 0; i < vertexNum; i++) {
            sb.append(i).append(": ").append(adj(i)).append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        String[] vertexInfo = {"v0", "v1", "v2", "v3", "v4"};
        int[][] edges = {{0, 1}, {0, 2}, {0, 3},
                {1, 3}, {1, 4},
                {2, 4}};
        AdjMatrixGraph<String> graph = new AdjMatrixGraph<>(vertexInfo,edges);

        System.out.println("顶点3的度为" + graph.degree(3));
        System.out.println("顶点3的邻接点为"+graph.adj(3));
        System.out.println("该图的最大度数为" + graph.maxDegree());
        System.out.println("该图的平均度数为" + graph.avgDegree());
        System.out.println("邻接矩阵如下:\n" + graph);
    }
}

/* Outputs
顶点3的度为2
顶点3的邻接点为[0, 1]
该图的最大度数为3
该图的平均度数为2.4
邻接矩阵如下:
5个顶点, 6条边。
0: [1, 2, 3]
1: [0, 3, 4]
2: [0, 4]
3: [0, 1]
4: [1, 2]

*/

我们的实现中有两个构造器,其中一个接收一个参数,传入顶点信息数组,以顶点信息个数作为图的顶点数。另外一个还可以接收表示所有相邻顶点的二维数组,比如edges[0] = {0, 1}表示顶点0和顶点1相邻,由于addEdge方法中已经考虑了对称矩阵,所以这里传参的时候就用不着传入{0, 1}后再传入{1, 0}了,只要保证前一个数比后一个数小就可以避免重复添加。

这里重点说一下求图的平均度数的方法avgDegree,我们有一个结论:图的边的条数 = 顶点度之和的一半,这是因为每一条边对应着两个顶点,而这两个顶点对于这条边,度之和为2。所以边的条数是所有顶点度之和的一半,即edgeNum = sum / 2,则sum = 2 * edgeNum, 于是avgDegree = sum / vertexNum

邻接表

邻接数组的缺点是所用空间太多,而且存放的信息很多是多余——顶点没有相邻也非得用一个false值或者不太可能出现的大值去填补数组中的位置,为何不直接留下相邻顶点就行了?比如上例中的a[0],可以从矩阵中看出与顶点0相邻的有顶点1、2、3

    0       1       2       3       4       5       
0   false   true    true    true    false   false

为什么不直接存储为a[0] = [1, 2, 3](就像上面打印的一样),这不是直观了很多嘛。由于每个顶点拥有的邻接点数目不同,使用数组实现就浪费空间了。所以存放某个顶点所有邻接点的容器,使用可变容量的表是个不错的选择,这里我就用链表了。回想树的孩子表示法,和这是一个道理,只是孩子表示法中存放的是结点对象(Node),这里存放的是用整数表示的顶点。邻接表不像邻接矩阵那样容量固定,如果某幅图要添加、删除某个顶点或某条边是相当方便的。所以在之后的实现中,如果没有特殊需求,将会一直使用邻接表。

package Chap6;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * 无向图
 * @param <Item>
 */
public class UndiGraph<Item> {
    private int vertexNum;
    private int edgeNum;
    // 邻接表
    private List<List<Integer>> adj;
    // 顶点信息
    private List<Item> vertexInfo;

    public UndiGraph(List<Item> vertexInfo) {
        this.vertexInfo = vertexInfo;
        this.vertexNum = vertexInfo.size();
        adj = new ArrayList<>();
        for (int i = 0; i < vertexNum; i++) {
            adj.add(new LinkedList<>());
        }
    }

    public UndiGraph(List<Item> vertexInfo, int[][] edges) {
        this(vertexInfo);
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public int vertexNum() {
        return vertexNum;
    }

    public int edgeNum() {
        return edgeNum;
    }

    public void addEdge(int i, int j) {
        adj.get(i).add(j);
        adj.get(j).add(i);
        edgeNum++;
    }
    // 不需要set,所以不用返回List,返回可迭代对象就够了
    public Iterable<Integer> adj(int i) {
        return adj.get(i);
    }

    public Item getVertexInfo(int i) {
        return vertexInfo.get(i);
    }

    public int degree(int i) {
        return adj.get(i).size();
    }

    public int maxDegree() {
        int max = 0;
        for (int i = 0;i < vertexNum;i++) {
            if (degree(i) > max) {
                max = degree(i);
            }
        }
        return max;
    }

    public double avgDegree() {
        return 2.0 * edgeNum / vertexNum;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(vertexNum).append("个顶点, ").append(edgeNum).append("条边。\n");
        for (int i = 0; i < vertexNum; i++) {
            sb.append(i).append(": ").append(adj.get(i)).append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        List<String> vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4");
        int[][] edges = {{0, 1}, {0, 2}, {0, 3},
                {1, 3}, {1, 4},
                {2, 4}};

        UndiGraph<String> graph = new UndiGraph<>(vertexInfo, edges);
        
        System.out.println("顶点3的度为" + graph.degree(3));
        System.out.println("顶点3的邻接点为"+graph.adj(3));
        System.out.println("该图的最大度数为" + graph.maxDegree());
        System.out.println("该图的平均度数为" + graph.avgDegree());
        System.out.println("邻接表如下:\n" + graph);
    }

}

程序输出和上面邻接矩阵实现的输出完全一样。各个方法的实现其思想和邻接矩阵实现类似,比较简单就不解释了。

顺便把有向图也用邻接表实现了。

package Chap7;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * 无向图
 *
 * @param <Item>
 */
public class DiGraph<Item> {
    private int vertexNum;
    private int edgeNum;
    // 邻接表
    private List<List<Integer>> adj;
    // 顶点信息
    private List<Item> vertexInfo;

    public DiGraph(List<Item> vertexInfo) {
        this.vertexInfo = vertexInfo;
        this.vertexNum = vertexInfo.size();
        adj = new ArrayList<>();
        for (int i = 0; i < vertexNum; i++) {
            adj.add(new LinkedList<>());
        }
    }

    public DiGraph(List<Item> vertexInfo, int[][] edges) {
        this(vertexInfo);
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public DiGraph(int vertexNum) {
        this.vertexNum = vertexNum;
        adj = new ArrayList<>();
        for (int i = 0; i < vertexNum; i++) {
            adj.add(new LinkedList<>());
        }
    }

    public DiGraph(int vertexNum, int[][] edges) {
        this(vertexNum);
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public int vertexNum() {
        return vertexNum;
    }

    public int edgeNum() {
        return edgeNum;
    }

    public void addEdge(int i, int j) {
        adj.get(i).add(j);
        edgeNum++;
    }

    // 不需要set,所以不用返回List,返回可迭代对象就够了
    public Iterable<Integer> adj(int i) {
        return adj.get(i);
    }

    public DiGraph<Item> reverse() {
        DiGraph<Item> R = new DiGraph<>(vertexNum);
        for (int v = 0; v < vertexNum; v++) {
            for (int w: adj(v)) {
                R.addEdge(w, v);
            }
        }
        return R;
    }

    public Item getVertexInfo(int i) {
        return vertexInfo.get(i);
    }

    public int degree(int i) {
        return adj.get(i).size();
    }

    public int maxDegree() {
        int max = 0;
        for (int i = 0; i < vertexNum; i++) {
            if (degree(i) > max) {
                max = degree(i);
            }
        }
        return max;
    }

    public double avgDegree() {
        return 2.0 * edgeNum / vertexNum;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(vertexNum).append("个顶点, ").append(edgeNum).append("条边。\n");
        for (int i = 0; i < vertexNum; i++) {
            sb.append(i).append(": ").append(adj.get(i)).append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        List<String> vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4");
        int[][] edges = {{0, 1}, {0, 2}, {0, 3},
                {1, 3}, {1, 4},
                {2, 4}};

        DiGraph<String> graph = new DiGraph<>(vertexInfo, edges);

        System.out.println("顶点3的度为" + graph.degree(3));
        System.out.println("顶点3的邻接点为" + graph.adj(3));
        System.out.println("该图的最大度数为" + graph.maxDegree());
        System.out.println("该图的平均度数为" + graph.avgDegree());
        System.out.println("邻接表如下:\n" + graph);
    }

}

addEdge方法少了一行,有向图嘛,边也是有方向的,i -> j有边不一定j -> i有边。另外新增了一个反向图的reverse方法,改变了所有边的方向,并返回原图的反向图。代码中主要做的是对每个顶点v,以及v的所有邻接顶点w,本来是v -> w的方向,现在新图中调用addEdge(w, v),将方向变成w -> v,实现反向。

至于其他方法,和无向图完全一样。

边的数组

这种方法实现起来很简单,顾名思义它更关注,我们可以用一个Edge来抽象边,它有两个int成员表示该边的两个顶点,如果是加权图,再多一个int型的weight成员就行了。将所有边存放到一个列表List<Edge>中,就是我们所说的边的数组了。

package Chap7;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class EdgeGraph<Item> {

    public static class Edge {
        private int either;
        private int other;

        public int either() {
            return either;
        }

        public int other() {
            return other;
        }

        public Edge(int either, int other) {
            this.either = either;
            this.other = other;
        }

        @Override
        public String toString() {
            return "Edge{" +
                    "either=" + either +
                    ", other=" + other +
                    '}';
        }
    }

    private int vertexNum;
    private int edgeNum;
    private List<Item> vertexInfo;
    private List<Edge> edges;

    public EdgeGraph(List<Item> vertexInfo) {
        this.edges = new ArrayList<>();
        this.vertexInfo = vertexInfo;
        this.vertexNum = vertexInfo.size();
    }

    public EdgeGraph(List<Item> vertexInfo, int[][] edges) {
        this(vertexInfo);
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public EdgeGraph(int vertexNum) {
        this.edges = new ArrayList<>();
        this.vertexNum = vertexNum;
    }

    public EdgeGraph(int vertexNum, int[][] edges) {
        this(vertexNum);
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public void addEdge(int i, int j) {
        Edge edge = new Edge(i, j);
        this.edges.add(edge);
        edgeNum++;
    }

    public List<Integer> adj(int i) {
        List<Integer> adj = new ArrayList<>();

        for (Edge edge : edges) {
            if (edge.either == i) {
                adj.add(edge.other);
            } else if (edge.other == i) {
                adj.add(edge.either);
            }
        }
        return adj;
    }

    public int degree(int i) {
        return adj(i).size();
    }

    public int maxDegree() {
        int max = 0;
        for (int i = 0; i < vertexNum; i++) {
            if (degree(i) > max) {
                max = degree(i);
            }
        }
        return max;
    }

    public double avgDegree() {
        return 2.0 * edgeNum / vertexNum;
    }

    public Item getVertexInfo(int i) {
        return vertexInfo.get(i);
    }

    public int vertexNum() {
        return vertexNum;
    }

    public int edgeNum() {
        return edgeNum;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(vertexNum).append("个顶点, ").append(edgeNum).append("条边。\n");
        for (int i = 0; i < vertexNum; i++) {
            sb.append(i).append(": ").append(adj(i)).append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        List<String> vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4");
        int[][] edges = {{0, 1}, {0, 2}, {0, 3},
                {1, 3}, {1, 4},
                {2, 4}};
        EdgeGraph<String> graph = new EdgeGraph<>(vertexInfo, edges);
        System.out.println("顶点3的度为" + graph.degree(3));
        System.out.println("顶点3的邻接点为" + graph.adj(3));
        System.out.println("该图的最大度数为" + graph.maxDegree());
        System.out.println("该图的平均度数为" + graph.avgDegree());
        System.out.println("邻接表如下:\n" + graph);
    }
}

自然输出和前面都一样。

只说addEdge(int i, int j)方法和adj(int i)方法。前者给图中两个顶点添加一条边,传入两个顶点,紧接着就new一个对应Edge,再将其存入边的列表即可。后者获取某个顶点所有邻接点,遍历边的列表,因为不知道边中哪个顶点和i相等,所以需要判断一下,只要有一个顶点和i相等,就将另一个存入待返回的列表中。

现在也知道了该实现有个缺陷:要知道某个顶点的所有邻接点,必须遍历整个边数组,效率不是很高。如果我们经常进行对顶点的操作,可以说获取某顶点所有邻接点是非常频繁的,边的数组不太适合经常对图的顶点进行操作的场合,更适合经常对边进行依次操作的场合。

在后面加权图的实现中,我们会用到边的数组的思想,因为权值在边上嘛,邻接矩阵实现起倒是简单,但是对于邻接表来说,由上面可以知道它定义为List<List<Integer>>,内层列表存放的是顶点的所有邻接点,那么权值存在哪里?这时候我们就需要一个Edge类了。差不多像下面这样。

public class Edge {
    private int either;
    private int other;
    private int weight;
}

邻接表随之也变成了List<List<Edge>>。这里只是稍微提一下,以后学到加权图的时候再具体来说。


by @sunhaiyu

2017.9.17

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容