数据结构-图

图(Graph)是数据结构中一种非常强大且应用广泛的非线性结构。与线性表(一对一)和树(一对多)不同,图主要用于描述事物之间多对多的复杂关系。

定义

图 G 是由两个集合组成的二元组:G = (V, E)

  • V (Vertex):顶点的有穷非空集合。顶点是图中的基本单元,也称为节点(Node)。
  • E (Edge):边的集合,表示顶点之间的关系。边是连接两个顶点的线段,也可以称为弧(Arc)。

术语

  • 顶点 (Vertex):图中的数据元素,是构成图的基本单位。

  • 边 (Edge):连接两个顶点的线段,表示它们之间存在关系。

  • 权 (Weight) :与边相关的数值,可以表示距离、耗费、时间等度量。带有权重的图被称为网 (Network)

  • 度 (Degree)

    • 无向图中,一个顶点的度是与它相连的边的数量。
    • 有向图 中,度分为入度 (指向该顶点的边的数量)和出度(从该顶点出发的边的数量)。
  • 路径 (Path):从一个顶点到另一个顶点所经过的顶点序列。路径的长度是路径上边的数量(或带权图中所有边的权重之和)。

  • 邻接点 (Adjacent Vertex):如果两个顶点由一条边直接相连,则称它们互为邻接点。

分类

按边的方向性划分

  • 无向图 (Undirected Graph) :边没有方向。例如,(A, B)(B, A) 代表同一条边。这可以用来表示微信好友关系(双向的)。
  • 有向图 (Directed Graph) :边有方向,也称为弧。例如,<A, B> 表示从顶点 A 指向顶点 B 的一条弧,这与 <B, A> 是不同的。这可以用来表示微博的关注关系(单向的)。

按边的权重划分

  • 无权图 (Unweighted Graph):边没有附带权重,只表示连接关系。
  • 带权图 / 网 (Weighted Graph / Network):边附带有权重,表示某种具体的度量,如城市间的距离。

按连通性划分

  • 连通图 (Connected Graph):在无向图中,如果任意两个顶点之间都存在路径,则该图是连通图。
  • 强连通图 (Strongly Connected Graph):在有向图中,如果任意两个顶点之间都存在双向路径(即从 A 能到 B,从 B 也能到 A),则该图是强连通图。

存储结构

在计算机中,图主要有两种存储方式,它们各有优劣,适用于不同的场景。

存储方式 核心思想 优点 缺点 适用场景
邻接矩阵 使用一个二维数组 matrix[i][j] 来表示顶点 ij 之间是否存在边。 判断两点是否相邻非常快,时间复杂度为 O(1)。 空间复杂度高,为 O(V²),会浪费大量空间存储不存在的边。 稠密图(边很多的图)
邻接表 为每个顶点建立一个链表,链表中存储所有与它相邻的顶点。 节省空间,空间复杂度为 O(V+E),遍历邻接点效率高。 判断两点是否相邻需要遍历链表,效率较低。 稀疏图(边很少的图)

遍历

遍历是图算法的基础,目的是系统地访问图中的每一个顶点。

  • 深度优先搜索 (DFS):类似于"一条路走到黑",尽可能深地搜索图的分支。通常用递归或栈实现。
  • 广度优先搜索 (BFS):类似于"水波扩散",从起始点开始,逐层向外访问。通常用队列实现。

最小生成树 (MST)

在一个带权的连通图中,找到一个包含所有顶点的子图,它是一棵树,并且所有边的权重之和最小。

  • Prim 算法 :从一个顶点开始,逐步添加边来构建树,适合稠密图
  • Kruskal 算法 :从权重最小的边开始,逐步构建树,适合稀疏图

拓扑排序

针对有向无环图 (DAG) ,将所有顶点排成一个线性序列,使得图中任意一条有向边 <u, v>u 都出现在 v 之前。常用于解决任务调度和依赖关系问题。

应用场景

  • 社交网络:用户是顶点,好友关系是边。用于实现好友推荐、社区发现等功能。
  • 地图导航:地点是顶点,道路是边,道路长度是权重。用于计算两点间的最短或最快路径。
  • 任务调度:任务是顶点,任务间的依赖关系是边。用于确定任务的执行顺序,如软件编译、课程安排。
  • 推荐系统:用户和商品是顶点,购买或浏览行为是边。用于分析用户偏好,进行商品或内容推荐。
  • 知识图谱:实体是顶点,实体间的关系是边。用于构建结构化的知识库,支持智能搜索和问答。

使用示例

使用"邻接矩阵"实现图

查询两点是否相连非常快 O(1),但如果图很大且边很少,会浪费空间。

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class GraphMatrix {
    // 顶点列表
    private ArrayList<String> vertexList;
    // 邻接矩阵(二维数组)
    private int[][] edges;
    // 边的数量
    private int numOfEdges;
    // 标记节点是否被访问过
    private boolean[] isVisited;

    public GraphMatrix(int n) {
        edges = new int[n][n];
        vertexList = new ArrayList<>(n);
        numOfEdges = 0;
        isVisited = new boolean[n];
    }

    // 添加顶点
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    // 添加边 (无向图:i->j 和 j->i 都要设为1)
    public void insertEdge(int v1, int v2) {
        edges[v1][v2] = 1;
        edges[v2][v1] = 1;
        numOfEdges++;
    }

    // --- 核心算法:深度优先搜索 (DFS) ---
    // 重载方法:遍历所有节点(处理非连通图)
    public void dfs() {
        isVisited = new boolean[vertexList.size()]; // 重置访问标记
        System.out.print("DFS 遍历结果: ");
        // 循环检查每个节点,防止漏掉非连通的分量
        for (int i = 0; i < vertexList.size(); i++) {
            if (!isVisited[i]) {
                dfs(i);
            }
        }
        System.out.println();
    }

    // 递归实现 DFS
    private void dfs(int i) {
        // 1. 访问当前节点
        System.out.print(vertexList.get(i) + "->");
        isVisited[i] = true;

        // 2. 查找当前节点的第一个邻接节点
        int w = getFirstNeighbor(i);
        while (w != -1) {
            // 如果邻接节点没被访问过,就递归访问它
            if (!isVisited[w]) {
                dfs(w);
            }
            // 如果访问过了,或者递归回来了,继续找下一个邻接节点
            w = getNextNeighbor(i, w);
        }
    }

    // --- 核心算法:广度优先搜索 (BFS) ---
    public void bfs() {
        isVisited = new boolean[vertexList.size()];
        System.out.print("BFS 遍历结果: ");
        
        for (int i = 0; i < vertexList.size(); i++) {
            if (!isVisited[i]) {
                bfs(i);
            }
        }
        System.out.println();
    }

    private void bfs(int i) {
        int u; // 队列头节点
        int w; // 邻接节点
        LinkedList<Integer> queue = new LinkedList<>();

        // 访问初始节点并入队
        System.out.print(vertexList.get(i) + "->");
        isVisited[i] = true;
        queue.addLast(i);

        while (!queue.isEmpty()) {
            u = queue.removeFirst(); // 出队
            w = getFirstNeighbor(u);
            
            while (w != -1) {
                if (!isVisited[w]) {
                    System.out.print(vertexList.get(w) + "->");
                    isVisited[w] = true;
                    queue.addLast(w); // 入队
                }
                w = getNextNeighbor(u, w);
            }
        }
    }

    // --- 辅助方法 ---
    // 获取节点 i 的第一个邻接节点下标
    private int getFirstNeighbor(int i) {
        for (int j = 0; j < vertexList.size(); j++) {
            if (edges[i][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    // 获取节点 i 在节点 v 之后的下一个邻接节点下标
    private int getNextNeighbor(int i, int v) {
        for (int j = v + 1; j < vertexList.size(); j++) {
            if (edges[i][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    // --- 测试主函数 ---
    public static void main(String[] args) {
        String[] vertexs = {"A", "B", "C", "D", "E"};
        GraphMatrix graph = new GraphMatrix(vertexs.length);

        // 添加顶点
        for (String vertex : vertexs) {
            graph.insertVertex(vertex);
        }

        // 添加边 (构建一个五边形结构 A-B-C-D-E-A,外加 A-C)
        graph.insertEdge(0, 1); // A-B
        graph.insertEdge(1, 2); // B-C
        graph.insertEdge(2, 3); // C-D
        graph.insertEdge(3, 4); // D-E
        graph.insertEdge(4, 0); // E-A
        graph.insertEdge(0, 2); // A-C

        graph.dfs();
        graph.bfs();
    }
}

使用"邻接表"实现图

节省空间,特别是对于像社交网络这样"点很多、但每个人好友有限"的稀疏图。

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

public class GraphList {
    // 顶点数量
    private int vertices;
    // 邻接表:数组的每个元素是一个链表,存储相邻节点
    private ArrayList<Integer>[] adjList;

    @SuppressWarnings("unchecked")
    public GraphList(int vertices) {
        this.vertices = vertices;
        adjList = new ArrayList[vertices];
        // 初始化每个顶点的链表
        for (int i = 0; i < vertices; i++) {
            adjList[i] = new ArrayList<>();
        }
    }

    // 添加边 (无向图:互相添加)
    public void addEdge(int u, int v) {
        adjList[u].add(v);
        adjList[v].add(u);
    }

    // --- 深度优先搜索 (DFS) ---
    public void dfs(int start) {
        boolean[] visited = new boolean[vertices];
        System.out.print("DFS 从节点 " + start + " 开始: ");
        dfsUtil(start, visited);
        System.out.println();
    }

    private void dfsUtil(int u, boolean[] visited) {
        visited[u] = true;
        System.out.print(u + " ");

        // 遍历 u 的所有邻接点
        for (int v : adjList[u]) {
            if (!visited[v]) {
                dfsUtil(v, visited);
            }
        }
    }

    // --- 广度优先搜索 (BFS) ---
    public void bfs(int start) {
        boolean[] visited = new boolean[vertices];
        Queue<Integer> queue = new LinkedList<>();

        visited[start] = true;
        queue.add(start);

        System.out.print("BFS 从节点 " + start + " 开始: ");

        while (!queue.isEmpty()) {
            int u = queue.poll();
            System.out.print(u + " ");

            // 遍历 u 的所有邻接点
            for (int v : adjList[u]) {
                if (!visited[v]) {
                    visited[v] = true;
                    queue.add(v);
                }
            }
        }
        System.out.println();
    }

    // --- 测试主函数 ---
    public static void main(String[] args) {
        GraphList graph = new GraphList(5);
        
        // 构建图结构
        graph.addEdge(0, 1);
        graph.addEdge(0, 4);
        graph.addEdge(1, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);
        graph.addEdge(2, 3);
        graph.addEdge(3, 4);

        graph.dfs(0);
        graph.bfs(0);
    }
}

最短路径问题

计算一个顶点到另一个顶点的最短路径。

Dijkstra 算法 :用于求解非负权图中,从一个源点到其他所有顶点的最短路径。

java 复制代码
import java.util.Arrays;

public class DijkstraMatrix {
    // 表示无穷大(不可达)
    private static final int INF = Integer.MAX_VALUE;
    // 顶点数量
    private int vertices;
    // 邻接矩阵
    private int[][] matrix;

    public DijkstraMatrix(int v) {
        this.vertices = v;
        matrix = new int[v][v];
        // 初始化矩阵
        for (int i = 0; i < v; i++) {
            Arrays.fill(matrix[i], INF);
            matrix[i][i] = 0; // 自己到自己距离为0
        }
    }

    // 添加边 (无向图需添加双向,有向图只添加单向)
    public void addEdge(int u, int v, int weight) {
        matrix[u][v] = weight;
        matrix[v][u] = weight; // 如果是有向图,去掉这行
    }

    // Dijkstra 核心算法
    public void dijkstra(int start) {
        // dist[i] 存储 start 到 i 的最短距离
        int[] dist = new int[vertices];
        // visited[i] 标记是否已找到最短路径
        boolean[] visited = new boolean[vertices];

        // 1. 初始化
        Arrays.fill(dist, INF);
        dist[start] = 0;

        // 2. 循环 V-1 次(每次确定一个点的最短路径)
        for (int i = 0; i < vertices - 1; i++) {
            // 寻找当前未访问节点中距离最小的
            int u = findMinDistance(dist, visited);
            
            // 如果找不到(说明剩下的点都不可达),提前结束
            if (u == -1) break;

            // 标记该节点已访问
            visited[u] = true;

            // 3. 松弛操作:通过 u 更新其邻居的距离
            for (int v = 0; v < vertices; v++) {
                // 如果 u->v 有边,且 v 未被访问,且通过 u 过去更近
                if (matrix[u][v] != INF && !visited[v] && 
                    dist[u] != INF && dist[u] + matrix[u][v] < dist[v]) {
                    dist[v] = dist[u] + matrix[u][v];
                }
            }
        }

        // 打印结果
        printSolution(dist, start);
    }

    // 辅助方法:寻找距离最小的未访问节点
    private int findMinDistance(int[] dist, boolean[] visited) {
        int min = INF;
        int index = -1;
        for (int i = 0; i < vertices; i++) {
            if (!visited[i] && dist[i] <= min) {
                min = dist[i];
                index = i;
            }
        }
        return index;
    }

    private void printSolution(int[] dist, int src) {
        System.out.println("从起点 " + src + " 出发的最短路径:");
        for (int i = 0; i < vertices; i++) {
            System.out.println("到节点 " + i + " 的距离: " + 
                (dist[i] == INF ? "不可达" : dist[i]));
        }
    }

    // 测试
    public static void main(String[] args) {
        DijkstraMatrix graph = new DijkstraMatrix(5);
        // 构建图
        graph.addEdge(0, 1, 4);
        graph.addEdge(0, 2, 1);
        graph.addEdge(1, 2, 2);
        graph.addEdge(1, 3, 5);
        graph.addEdge(2, 3, 8);
        graph.addEdge(2, 4, 2);
        graph.addEdge(3, 4, 1);

        graph.dijkstra(0);
    }
}

Floyd-Warshall 算法 :用于求解图中任意两个顶点之间的最短路径。

java 复制代码
import java.util.*;

public class DijkstraOptimized {
    // 边类
    static class Edge {
        int to;     // 目标节点
        int weight; // 权重

        public Edge(int to, int weight) {
            this.to = to;
            this.weight = weight;
        }
    }

    // 图类
    private int vertices;
    private List<List<Edge>> adjList;

    public DijkstraOptimized(int vertices) {
        this.vertices = vertices;
        adjList = new ArrayList<>();
        for (int i = 0; i < vertices; i++) {
            adjList.add(new ArrayList<>());
        }
    }

    // 添加边
    public void addEdge(int u, int v, int weight) {
        adjList.get(u).add(new Edge(v, weight));
        adjList.get(v).add(new Edge(u, weight)); // 无向图
    }

    // 核心算法
    public void dijkstra(int start) {
        // 距离数组
        int[] dist = new int[vertices];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[start] = 0;

        // 优先队列:按照距离从小到大排序
        // 数组格式:[节点ID, 当前距离]
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
        pq.offer(new int[]{start, 0});

        // 记录是否已确定最短路径
        boolean[] visited = new boolean[vertices];

        while (!pq.isEmpty()) {
            // 取出距离最小的节点
            int[] current = pq.poll();
            int u = current[0];

            // 如果已经处理过,跳过(懒删除策略)
            if (visited[u]) continue;
            visited[u] = true;

            // 遍历邻居
            for (Edge edge : adjList.get(u)) {
                int v = edge.to;
                int weight = edge.weight;

                // 松弛操作:如果通过 u 到 v 更近,则更新
                if (!visited[v] && dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
                    dist[v] = dist[u] + weight;
                    pq.offer(new int[]{v, dist[v]});
                }
            }
        }

        // 打印结果
        System.out.println("优化版 Dijkstra 结果 (起点: " + start + "):");
        for (int i = 0; i < vertices; i++) {
            System.out.println("到节点 " + i + " 的距离: " + 
                (dist[i] == Integer.MAX_VALUE ? "不可达" : dist[i]));
        }
    }

    // 测试
    public static void main(String[] args) {
        DijkstraOptimized graph = new DijkstraOptimized(5);
        graph.addEdge(0, 1, 4);
        graph.addEdge(0, 2, 1);
        graph.addEdge(1, 2, 2);
        graph.addEdge(1, 3, 5);
        graph.addEdge(2, 3, 8);
        graph.addEdge(2, 4, 2);
        graph.addEdge(3, 4, 1);

        graph.dijkstra(0);
    }
}
相关推荐
编程之升级打怪2 小时前
有难度的关键算法
算法
tyler_download2 小时前
揉扁搓圆transformer架构:模型参数的初始化算法.
深度学习·算法·transformer
尽兴-2 小时前
机器人控制系统(RCS)核心算法深度解析:从路径规划到任务调度
算法·机器人·wms·mes·路径规划算法·冲突解决算法·任务调度算法
最贪吃的虎2 小时前
我的第一个 RAG 程序:从 0 到 1,用 PDF 搭一个最小可运行的知识库问答系统
人工智能·python·算法·机器学习·aigc·embedding·llama
421!2 小时前
C语言学习笔记——10(结构体)
c语言·开发语言·笔记·stm32·学习·算法
1104.北光c°2 小时前
Leetcode146 LRU缓存的三种写法 【hot100算法个人笔记】【java写法】
java·开发语言·笔记·算法·leetcode·hot100·lru缓存
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 239. 滑动窗口最大值 | C++ 优先队列与单调队列双解法
数据结构·算法·leetcode
dazzle4 小时前
机器学习算法原理与实践-入门(十一):基于PyTorch的房价预测实战
pytorch·算法·机器学习