一文搞定图

常见类型与术语

图的表示

邻接矩阵

邻接表

基础操作

基于邻接矩阵的实现

基于邻接表的实现

遍历

广度优先

深度优先


是一种非线性数据结构,由 顶点 组成。
相较于线性关系的链表和分治关系的树,网络关系的图自由度更高

常见类型与术语

根据边是否具有方向,可分为
无向图:
有向图:
根据所有顶点是否连通,可分为
连通图:
非连通图:
根据是否 为边添加“权重”变量,可分为
无权图:
有权图:
常用术语:
邻接:当两顶点之间存在边相连时,称这两顶点“邻接”。
路径:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。
:一个顶点拥有的边数。对于有向图, 入度 表示有多少条边指向该顶点, 出度
表示有多少条边从该顶点指出。

图的表示

邻接矩阵

这个图的邻接矩阵为:

邻接表

这个图的邻接表为:

基础操作

基于邻接矩阵的实现

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

/* 基于邻接矩阵实现的无向图类 */
class GraphAdjMat {
    private List<Integer> vertices; // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”
    private int[][] adjMat; // 邻接矩阵,行列索引对应“顶点索引”

    /* 构造方法 */
    public GraphAdjMat(int[] vertices, int[][] edges) {
        this.vertices = new ArrayList<>();
        this.adjMat = new int[vertices.length][vertices.length]; // 初始化邻接矩阵
        
        // 添加顶点
        for (int val : vertices) {
            addVertex(val);
        }
        // 添加边
        for (int[] e : edges) {
            addEdge(e[0], e[1]);
        }
    }

    /* 获取顶点数量 */
    public int size() {
        return vertices.size();
    }

    /* 添加顶点 */
    public void addVertex(int val) {
        vertices.add(val);
        // 扩展邻接矩阵
        int n = size();
        int[][] newAdjMat = new int[n][n];
        
        // 复制原有邻接矩阵的内容
        for (int i = 0; i < n - 1; i++) {
            System.arraycopy(adjMat[i], 0, newAdjMat[i], 0, n - 1);
        }
        
        // 设置新顶点的邻接关系为0
        for (int i = 0; i < n; i++) {
            newAdjMat[i][n - 1] = 0; // 新列
            newAdjMat[n - 1][i] = 0; // 新行
        }
        
        adjMat = newAdjMat; // 更新邻接矩阵引用
    }

    /* 删除顶点 */
    public void removeVertex(int index) {
        if (index >= size()) {
            throw new IndexOutOfBoundsException();
        }
        
        // 在顶点列表中移除索引 index 的顶点
        vertices.remove(index);
        int n = size();
        int[][] newAdjMat = new int[n - 1][n - 1];
        
        for (int i = 0, newRow = 0; i < n; i++) {
            if (i != index) {
                for (int j = 0, newCol = 0; j < n; j++) {
                    if (j != index) {
                        newAdjMat[newRow][newCol++] = adjMat[i][j]; // 复制不包含被删除顶点的行列
                    }
                }
                newRow++;
            }
        }
        
        adjMat = newAdjMat; // 更新邻接矩阵引用
    }

    /* 添加边 */
    // 参数 i, j 对应 vertices 元素索引
    public void addEdge(int i, int j) {
        // 索引越界与相等处理
        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
            throw new IndexOutOfBoundsException();
        }
        // 在无向图中,邻接矩阵关于主对角线对称
        adjMat[i][j] = 1;
        adjMat[j][i] = 1;
    }

    /* 删除边 */
    // 参数 i, j 对应 vertices 元素索引
    public void removeEdge(int i, int j) {
        // 索引越界与相等处理
        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
            throw new IndexOutOfBoundsException();
        }
        adjMat[i][j] = 0;
        adjMat[j][i] = 0;
    }

    /* 打印邻接矩阵 */
    public void print() {
        System.out.print("顶点列表 = ");
        System.out.println(vertices);
        System.out.println("邻接矩阵 =");
        for (int[] row : adjMat) {
            for (int val : row) {
                System.out.print(val + " ");
            }
            System.out.println();
        }
    }
}

主要功能

  1. 添加顶点:增加新的顶点并扩展邻接矩阵。
  2. 删除顶点:移除指定索引的顶点和对应的邻接关系。
  3. 添加边:在邻接矩阵中设置两个顶点之间的边。
  4. 删除边:移除两个顶点之间的边。
  5. 打印邻接矩阵:以易于阅读的格式输出邻接矩阵。

基于邻接表的实现

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/* 顶点类 */
class Vertex {
    int val; // 顶点值

    public Vertex(int val) {
        this.val = val;
    }

    // 重写 equals 和 hashCode,方便在 Map 中使用。
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Vertex)) return false;
        Vertex other = (Vertex) obj;
        return this.val == other.val;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(val);
    }
}

/* 基于邻接表实现的无向图类 */
class GraphAdjList {
    // 邻接表,key:顶点,value:该顶点的所有邻接顶点
    Map<Vertex, List<Vertex>> adjList;

    /* 构造方法 */
    public GraphAdjList(Vertex[][] edges) {
        this.adjList = new HashMap<>();
        // 添加所有顶点和边
        for (Vertex[] edge : edges) {
            addVertex(edge[0]);
            addVertex(edge[1]);
            addEdge(edge[0], edge[1]);
        }
    }

    /* 获取顶点数量 */
    public int size() {
        return adjList.size();
    }

    /* 添加边 */
    public void addEdge(Vertex vet1, Vertex vet2) {
        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1.equals(vet2)) {
            throw new IllegalArgumentException();
        }
        // 添加边 vet1 - vet2
        adjList.get(vet1).add(vet2);
        adjList.get(vet2).add(vet1);
    }

    /* 删除边 */
    public void removeEdge(Vertex vet1, Vertex vet2) {
        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1.equals(vet2)) {
            throw new IllegalArgumentException();
        }
        // 删除边 vet1 - vet2
        adjList.get(vet1).remove(vet2);
        adjList.get(vet2).remove(vet1);
    }

    /* 添加顶点 */
    public void addVertex(Vertex vet) {
        if (!adjList.containsKey(vet)) {
            // 在邻接表中添加一个新链表
            adjList.put(vet, new ArrayList<>());
        }
    }

    /* 删除顶点 */
    public void removeVertex(Vertex vet) {
        if (!adjList.containsKey(vet)) {
            throw new IllegalArgumentException();
        }
        // 在邻接表中删除顶点 vet 对应的链表
        adjList.remove(vet);
        // 遍历其他顶点的链表,删除所有包含 vet 的边
        for (List<Vertex> list : adjList.values()) {
            list.remove(vet);
        }
    }

    /* 打印邻接表 */
    public void print() {
        System.out.println("邻接表 =");
        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {
            List<Integer> tmp = new ArrayList<>();
            for (Vertex vertex : pair.getValue()) {
                tmp.add(vertex.val);
            }
            System.out.println(pair.getKey().val + ": " + tmp + ",");
        }
    }
}

// 示例使用
class Main {
    public static void main(String[] args) {
        Vertex v1 = new Vertex(1);
        Vertex v2 = new Vertex(2);
        Vertex v3 = new Vertex(3);
        Vertex[][] edges = {{v1, v2}, {v2, v3}, {v1, v3}};
        
        GraphAdjList graph = new GraphAdjList(edges);
        graph.print();
    }
}

这个类实现了基于邻接表的无向图,包括了添加、删除顶点和边的功能,并且重写了 Vertex 类的 equals 和 hashCode 方法,以便在 HashMap 中正确使用。打印功能将输出每个顶点及其邻接顶点列表。

遍历

广度优先

广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外 扩张
/* 广度优先遍历 */
List<Integer> graphBFS(GraphAdjMat graph, int startIdx) {
    // 顶点遍历序列
    List<Integer> res = new ArrayList<>();
    // 哈希集,用于记录已被访问过的顶点
    Set<Integer> visited = new HashSet<>();
    visited.add(startIdx); // 将起始顶点标记为已访问
    // 队列用于实现 BFS
    Queue<Integer> que = new LinkedList<>();
    que.offer(startIdx); // 将起始顶点入队

    // 以顶点 startIdx 为起点,循环直至访问完所有顶点
    while (!que.isEmpty()) {
        int idx = que.poll(); // 队首顶点出队
        res.add(graph.vertices.get(idx)); // 记录访问顶点

        // 遍历该顶点的所有邻接顶点
        for (int j = 0; j < graph.size(); j++) {
            if (graph.adjMat[idx][j] == 1 && !visited.contains(j)) { // 判断是否相邻且未访问
                que.offer(j); // 只入队未访问的顶点
                visited.add(j); // 标记该顶点已被访问
            }
        }
    }
    // 返回顶点遍历序列
    return res;
}

主要功能

  1. 输入参数:接收一个GraphAdjMat对象和起始顶点的索引。
  2. 结果列表:使用一个列表res来记录访问的顶点。
  3. 访问记录:使用一个哈希集visited来记录已经访问过的顶点,避免重复访问。
  4. 队列实现BFS:使用Queue来按层次遍历图的顶点。

注意事项

  • 确保在构造图时,已知起始顶点的索引。
  • 广度优先遍历的结果将返回按照层次顺序访问的顶点,适合用于查找最短路径或层级关系等应用场合。

深度优先

深度优先遍历是一种优先走到底、无路可走再回头的遍历方式
/* 深度优先遍历辅助函数 */
void dfs(GraphAdjMat graph, Set<Integer> visited, List<Integer> res, int index) {
    res.add(graph.vertices.get(index)); // 记录访问顶点
    visited.add(index); // 标记该顶点已被访问

    // 遍历该顶点的所有邻接顶点
    for (int j = 0; j < graph.size(); j++) {
        if (graph.adjMat[index][j] == 1 && !visited.contains(j)) { // 判断是否相邻且未访问
            dfs(graph, visited, res, j); // 递归访问邻接顶点
        }
    }
}

/* 深度优先遍历 */
// 使用邻接矩阵表示图,以便获取指定顶点的所有邻接顶点
List<Integer> graphDFS(GraphAdjMat graph, int startIdx) {
    // 顶点遍历序列
    List<Integer> res = new ArrayList<>();
    // 哈希表,用于记录已被访问过的顶点
    Set<Integer> visited = new HashSet<>();
    // 调用辅助函数进行深度优先遍历
    dfs(graph, visited, res, startIdx);
    return res;
}

主要功能

  1. 辅助函数 dfs

    • 该函数负责递归访问图中的顶点。
    • 记录当前顶点并将其标记为已访问。
    • 遍历所有邻接的顶点,如果相邻顶点未被访问,则递归调用 dfs
  2. 主函数 graphDFS

    • 接收一个 GraphAdjMat 对象和起始顶点的索引。
    • 初始化结果列表 res 和已访问顶点集合 visited
    • 调用 dfs 辅助函数开始深度优先遍历并返回最终的访问顺序。

注意事项

  • 确保在构造图时已知起始顶点的索引。
  • 深度优先遍历适合用于搜索路径、分析连通性等场景。

 

文章记录了学习Krahets的《Hello 算法》的轨迹,代码均使用Java语言,原书支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、 Rust、C 和 Zig 等语言。

教程链接:krahets/hello-algo: 《Hello 算法》:动画图解、一键运行的数据结构与算法教程。支持 Python, Java, C++, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 代码。简体版和繁体版同步更新,English version ongoing (github.com)icon-default.png?t=O83Ahttps://github.com/krahets/hello-algo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值