Skip to content

1. 图的概述

图的定义

图 (Graph) 是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。

和线性表,树的差异:

  • 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点 (Vertex)。

  • 线性表可以没有元素,称为空表;树中可以没有节点,称为空树;但是,在图中不允许没有顶点 (有穷非空性)。

  • 线性表中的各元素是线性关系,树中的各元素是层次关系,而图中各顶点的关系是用边来表示 (边集可以为空)。

图的分类

  • 有向图:如果给图的每条边规定一个方向,那么得到的图称为有向图

  • 无向图:在有向图中,与一个节点相关联的边有出边和入边之分。相反,边没有方向的图称为无向图

  • 单图:一个图如果任意两顶点之间只有一条边(在有向图中为两顶点之间每个方向只有一条边);边集中不含环,则称为单图

基本术语

  • 顶点的度:顶点 Vi 的度 (Degree) 是指在图中与 Vi 相关联的边的条数。对于有向图来说,有入度 (In-degree) 和出度 (Out-degree) 之分,有向图顶点的度等于该顶点的入度和出度之和。

  • 邻接:

    • 若无向图中的两个顶点 V1 和 V2 存在一条边 (V1,V2),则称顶点 V1 和 V2 邻接 (Adjacent);
    • 若有向图中存在一条边<V3,V2>,则称顶点 V3 与顶点 V2 邻接,且是 V3 邻接到 V2 或 V2 邻接到 V3;
  • 路径:在无向图中,若从顶点 Vi 出发有一组边可到达顶点 Vj,则称顶点 Vi 到顶点 Vj 的顶点序列为从顶点 Vi 到顶点 Vj 的路径 (Path)。

  • 连通:若从 Vi 到 Vj 有路径可通,则称顶点 Vi 和顶点 Vj 是连通 (Connected) 的。

  • 权 (Weight):有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权 (Weight)。

  • 连通:如果从 V 到 W 存在一条(无向)路径,则称 V 和 W 是连通的;

  • 路径:V 到 W 的路径是一系列顶点{V, v 1, v 2, …, v n, W}的集合,其中任一对相邻的顶点间都有图中的边。路径的长度是路径中的边数(如果带 权,则是所有边的权重和)。如果 V 到 W 之间的所有顶点都不同,则称简单路径;

  • 回路:起点等于终点的路径;

  • 连通图:图中任意两顶点均连通

2. 图的表示

2.1 邻接矩阵(数组存储)

邻接矩阵 G[N][N]——N 个顶点从 0 到 N-1 编号,若结点 V~i~ 和 结点 V~j~ 是 G 中的边,这 G[i][j] = 1,否则等于 0,由此得出的 N * N 的矩阵为邻接矩阵。

java
// matrix[x][y] 记录 x 是否有一条指向 y 的边
boolean[][] matrix;
// matrix[x][y] 记录 x 是否有一条指向 y 的边
boolean[][] matrix;

邻接矩阵

表示无向图时:

邻接矩阵

对于无向图来说,V~i~ 和 V~j~ 的结果和 V~j~ 和 V~i~ 的结果是对称的。

不足:由于存在 n 个顶点的图需要 n*n 个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法将会出现大量 0 元素,这会造成极大的空间浪费。这时,可以考虑使用邻接表表示法来存储图中的数据。

2.2 邻接表(链表存储)

java
// graph[x] 存储 x 的所有邻居节点
List<Integer>[] graph = new LinkedList[n];
// graph[x] 存储 x 的所有邻居节点
List<Integer>[] graph = new LinkedList[n];

邻接表

邻接矩阵与邻接表比较

对于邻接表,好处是占用的空间少。邻接矩阵里面空着那么多位置,肯定需要更多的存储空间。

但是,邻接表无法快速判断两个节点是否相邻。

3. 图的遍历

3.1 深度优先遍历〔Depth First Search, DFS〕

DFS:核心思想就是一条路找到底,然后回退一步换一个方向继续。有一个细节是,有时需要在出递归时把回退到的当前节点标为可访问。

深度优先遍历图的方法是,从图中某顶点 v 出发: (1)访问顶点 v; (2)依次从 v 的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和 v 有路径相通的顶点都被访问; (3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

DFS 算法框架:

java
public static void DFS(int[][] graph, int s) {
    boolean visited[] = new boolean[graph.length];
    traverse(graph, s, visited);
}


static void traverse(int[][] graph, int s, boolean visited[]) {
    visited[s] = true;

    // 访问该结点
    // doSomething();

    // 遍历 s 的相邻结点
    for (int i = 0; i < graph.length; i++) {
        if (graph[s][i] == 1 && !visited[i]) {
            traverse(graph, i, visited);
        }
    }
}
public static void DFS(int[][] graph, int s) {
    boolean visited[] = new boolean[graph.length];
    traverse(graph, s, visited);
}


static void traverse(int[][] graph, int s, boolean visited[]) {
    visited[s] = true;

    // 访问该结点
    // doSomething();

    // 遍历 s 的相邻结点
    for (int i = 0; i < graph.length; i++) {
        if (graph[s][i] == 1 && !visited[i]) {
            traverse(graph, i, visited);
        }
    }
}

3.2 广度优先搜索〔Breadth First Search, BFS〕

BFS:广度优先搜索的搜索过程有点像一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。

BFS 常常用来求解无权图的最短路径问题

在程序实现 BFS 时需要考虑以下问题:

  • 队列:用来存储每一轮遍历得到的节点;
  • 标记:对于遍历过的节点,应该将它标记,防止重复遍历。

BFS 算法框架:

java
static void BFS(int[][] graph, int s) {
    // 标记所有节点为未访问状态
    boolean visited[] = new boolean[graph.length];

    // 创建一个队列来存储需要遍历的节点
    LinkedList<Integer> queue = new LinkedList<Integer>();

    // 将起始节点加入队列,并标记已访问过
    visited[s] = true;
    queue.add(s);

    while (queue.size() != 0) {
        // 从队列中取出要访问的节点
        s = queue.poll();

        // 访问该结点
        // doSomething();

        // 遍历与该节点相邻且未被访问的节点
        for (int i = 0; i < graph.length; i++) {
            if (graph[s][i] == 1 && !visited[i]) {
                visited[i] = true;
                queue.add(i);
            }
        }
    }
}
static void BFS(int[][] graph, int s) {
    // 标记所有节点为未访问状态
    boolean visited[] = new boolean[graph.length];

    // 创建一个队列来存储需要遍历的节点
    LinkedList<Integer> queue = new LinkedList<Integer>();

    // 将起始节点加入队列,并标记已访问过
    visited[s] = true;
    queue.add(s);

    while (queue.size() != 0) {
        // 从队列中取出要访问的节点
        s = queue.poll();

        // 访问该结点
        // doSomething();

        // 遍历与该节点相邻且未被访问的节点
        for (int i = 0; i < graph.length; i++) {
            if (graph[s][i] == 1 && !visited[i]) {
                visited[i] = true;
                queue.add(i);
            }
        }
    }
}

4. 相关算法题

5. 参考资料