图的邻接矩阵存储详解:掌握这一种方法,轻松应对95%的图算法题

第一章:图的邻接矩阵存储详解:掌握这一种方法,轻松应对95%的图算法题

什么是邻接矩阵

邻接矩阵是一种用二维数组表示图中顶点之间连接关系的存储方式。对于包含 n 个顶点的图,邻接矩阵是一个 n×n 的布尔或数值矩阵,其中 matrix[i][j] 表示从顶点 i 到顶点 j 是否存在边(无权图)或边的权重(有权图)。该结构特别适合稠密图和需要频繁查询边是否存在的情况。

邻接矩阵的构建与操作

构建邻接矩阵时,首先初始化一个全为0的二维数组。对于每条边 (u, v),将对应位置的值设为1(无权图)或权重值(有权图)。如果是无向图,还需同时设置对称位置。
  1. 初始化 n×n 矩阵,所有元素为0
  2. 遍历所有边,更新矩阵对应位置
  3. 若为无向图,确保 matrix[u][v] = matrix[v][u]
// Go语言实现无向图的邻接矩阵构建
package main

import "fmt"

func main() {
    n := 4 // 顶点数
    graph := make([][]int, n)
    for i := range graph {
        graph[i] = make([]int, n)
    }

    edges := [][2]int{{0, 1}, {1, 2}, {2, 3}, {3, 0}}
    for _, e := range edges {
        u, v := e[0], e[1]
        graph[u][v] = 1 // 设置边
        graph[v][u] = 1 // 无向图对称
    }

    // 输出邻接矩阵
    for i := 0; i < n; i++ {
        fmt.Println(graph[i])
    }
}

邻接矩阵的优缺点对比

特性优点缺点
查询效率O(1) 时间判断边是否存在-
空间消耗-O(n²),稀疏图浪费空间
增删边O(1)需重建矩阵处理顶点增减
graph TD A[开始] --> B[初始化n×n矩阵] B --> C{读取每条边} C --> D[设置matrix[u][v]=1] D --> E[若无向图,设置matrix[v][u]=1] E --> F[结束]

第二章:邻接矩阵的基本原理与C语言实现

2.1 图的基本概念与邻接矩阵定义

图是描述对象之间关系的重要数学结构,由顶点集合和边集合构成。根据边是否有方向,图可分为有向图和无向图。邻接矩阵是一种用二维数组表示图中顶点间连接关系的方式,适用于顶点数量固定的稠密图。
邻接矩阵的存储结构
对于包含 $n$ 个顶点的图,邻接矩阵是一个 $n \times n$ 的布尔矩阵,若顶点 $i$ 与顶点 $j$ 之间存在边,则矩阵元素 $A[i][j] = 1$,否则为 0。
顶点ABC
A011
B100
C100
代码实现示例
// 初始化邻接矩阵
func NewGraph(n int) [][]int {
    graph := make([][]int, n)
    for i := range graph {
        graph[i] = make([]int, n)
    }
    return graph
}
上述 Go 代码创建一个 $n \times n$ 的二维切片,用于存储无向图的连接状态。每个元素初始化为 0,插入边时将对应位置设为 1,实现简单但空间复杂度为 $O(n^2)$。

2.2 邻接矩阵的数据结构设计

邻接矩阵是一种基于二维数组表示图中顶点间连接关系的数据结构,适用于边密集的图场景。
数据结构定义
使用二维布尔数组或整型数组存储边的存在性与权重:

#define MAX_VERTICES 100
int adjMatrix[MAX_VERTICES][MAX_VERTICES];
该代码定义了一个静态邻接矩阵,adjMatrix[i][j] 表示从顶点 i 到 j 是否存在边。若为带权图,则存储权重值;否则用 0/1 表示无边或有边。
空间与时间特性
  • 空间复杂度为 O(V²),V 为顶点数,适合顶点规模较小的图;
  • 查询任意两点间是否有边的时间复杂度为 O(1);
  • 添加或删除边的操作也仅需常量时间。
对于稀疏图,此结构会造成大量空间浪费,后续章节将探讨更高效的稀疏表示方法。

2.3 C语言中二维数组的动态分配策略

在C语言中,静态定义的二维数组大小固定,无法满足运行时动态需求。因此,常采用动态内存分配实现灵活的二维数组管理。
使用指针数组逐行分配
该方法先分配一个指针数组,再为每行单独分配内存:

int **arr = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {
    arr[i] = malloc(cols * sizeof(int));
}
此方式逻辑清晰,访问语法与静态数组一致(arr[i][j]),但存在多次内存请求开销。
单块连续内存分配
为提升性能,可一次性分配所有数据空间:

int *data = malloc(rows * cols * sizeof(int));
int **arr = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {
    arr[i] = &data[i * cols];
}
这种方法保证数据在内存中连续存储,有利于缓存优化,适合大规模数值计算场景。

2.4 初始化与重置邻接矩阵的操作实现

在图结构的实现中,邻接矩阵作为核心数据存储形式,其初始化与重置操作至关重要。合理的初始化确保图结构处于已知状态,而重置则用于恢复矩阵到初始空状态,便于重复利用。
邻接矩阵的初始化逻辑
初始化操作通常将矩阵所有元素设为0(无边),或根据需求设为无穷大(表示不可达)。对于具有 V 个顶点的图,需创建 V×V 的二维数组。
int** initGraph(int V) {
    int** matrix = (int**)malloc(V * sizeof(int*));
    for (int i = 0; i < V; i++) {
        matrix[i] = (int*)calloc(V, sizeof(int)); // 初始化为0
    }
    return matrix;
}
该函数动态分配内存并使用 calloc 确保所有值初始化为0,表示初始无连接。
重置操作的实现方式
重置操作可复用现有内存,将所有边权重归零或恢复默认值。
  • 遍历每一行和列,设置 matrix[i][j] = 0
  • 适用于频繁清空图结构的场景,避免重复内存分配

2.5 边的插入与删除操作详解

在图结构中,边的插入与删除是维护节点关系的核心操作。合理的实现方式能显著提升图的动态性能。
边的插入逻辑
向图中添加一条边需校验顶点存在性及避免重边。以邻接表为例:
// InsertEdge 插入一条无向边
func (g *Graph) InsertEdge(u, v int) {
    if !g.HasVertex(u) || !g.HasVertex(v) {
        return
    }
    g.adjList[u] = append(g.adjList[u], v)
    g.adjList[v] = append(g.adjList[v], u) // 无向图双向连接
}
该实现确保两个顶点均存在后,在彼此的邻接表中追加对方,时间复杂度为 O(1)(忽略重复检查)。
边的删除处理
删除边需遍历邻接表移除对应节点。例如从 u 的邻接表中删除 v:
  • 遍历 g.adjList[u] 查找目标节点 v
  • 使用切片重组跳过匹配项
  • 对无向图同样处理反向边
此操作平均时间复杂度为 O(degree),适用于稀疏图场景。

第三章:邻接矩阵的核心操作与性能分析

3.1 顶点与边的遍历方法对比

在图结构处理中,顶点与边的遍历策略直接影响算法效率。常见的遍历方式包括深度优先搜索(DFS)和广度优先搜索(BFS),二者在访问顺序和资源消耗上存在显著差异。
遍历方式特性对比
  • DFS:利用栈结构实现,适合探索路径连通性;
  • BFS:基于队列,适用于最短路径查找;
  • 边遍历更关注关系处理,常用于图神经网络中的消息传递。
代码示例:DFS遍历顶点
// 使用递归实现DFS
func DFS(graph map[int][]int, visited map[int]bool, node int) {
    visited[node] = true
    fmt.Println("Visited:", node)
    for _, neighbor := range graph[node] {
        if !visited[neighbor] {
            DFS(graph, visited, neighbor)
        }
    }
}
上述代码通过递归访问每个未标记顶点,graph 存储邻接表,visited 跟踪状态,确保每个顶点仅被处理一次。

3.2 时间复杂度与空间复杂度深度剖析

算法效率的量化标准
时间复杂度和空间复杂度是衡量算法性能的核心指标。时间复杂度反映算法执行时间随输入规模增长的变化趋势,常用大O符号表示;空间复杂度则描述算法所需内存空间的增长规律。
常见复杂度对比
  • O(1):常数时间,如数组随机访问
  • O(log n):对数时间,如二分查找
  • O(n):线性时间,如遍历数组
  • O(n²):平方时间,如嵌套循环比较
// 冒泡排序示例:时间复杂度O(n²),空间复杂度O(1)
func bubbleSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        for j := 0; j < n-i-1; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            }
        }
    }
}
该代码通过双重循环实现排序,外层控制轮数,内层执行比较交换。每轮将最大元素“冒泡”至末尾,无需额外存储空间,故空间复杂度为O(1)。

3.3 稍密图与稀疏图下的适用性讨论

在图算法设计中,图的密度显著影响数据结构与算法的选择。稠密图边数接近顶点数的平方,而稀疏图边数远小于该量级。
数据结构选择对比
  • 邻接矩阵适用于稠密图,支持 O(1) 边查询
  • 邻接表更适合稀疏图,空间复杂度为 O(V + E)
典型算法性能差异
算法稠密图复杂度稀疏图优化方案
DijkstraO(V²)使用堆优化至 O((V + E) log V)
代码实现示例
// 基于优先队列的稀疏图Dijkstra实现
func dijkstra(graph map[int][]Edge, start int) map[int]int {
    dist := make(map[int]int)
    heap := &MinHeap{}
    heap.Push(Node{start, 0})

    for heap.Len() > 0 {
        u := heap.Pop()
        if _, exists := dist[u.id]; exists { continue }
        dist[u.id] = u.dist
        for _, e := range graph[u.id] {
            if _, seen := dist[e.to]; !seen {
                heap.Push(Node{e.to, u.dist + e.weight})
            }
        }
    }
    return dist
}
上述实现利用最小堆将稀疏图中的 Dijkstra 算法优化至接近线性对数时间,避免了稠密图中频繁更新带来的开销。

第四章:基于邻接矩阵的经典图算法实战

4.1 深度优先搜索(DFS)的矩阵实现

在图的邻接矩阵表示下,深度优先搜索通过递归或栈结构遍历所有可达顶点。矩阵的行和列分别代表图中的顶点,元素值表示边的存在与否。
邻接矩阵存储结构
使用二维数组 `graph[V][V]` 表示图,若 `graph[i][j] == 1`,则顶点 i 与 j 相连。
DFS递归实现

void dfs(int graph[][V], int v, int visited[]) {
    visited[v] = 1;
    printf("Visit %d\n", v);
    for (int i = 0; i < V; i++) {
        if (graph[v][i] && !visited[i]) {
            dfs(graph, i, visited);
        }
    }
}
该函数从顶点 v 开始,标记其为已访问,并递归访问所有未访问的邻接顶点。参数 `graph` 为邻接矩阵,`visited` 数组防止重复访问。
时间与空间复杂度分析
  • 时间复杂度:O(V²),因需扫描整个矩阵每行
  • 空间复杂度:O(V),用于存储 visited 数组与递归调用栈

4.2 广度优先搜索(BFS)的队列结合应用

广度优先搜索通过队列先进先出的特性,确保按层级遍历图或树结构。在实现中,队列用于存储待访问节点,逐层扩展搜索范围。
基本实现结构

from collections import deque

def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)
    
    while queue:
        node = queue.popleft()
        print(node)
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
上述代码中,deque 提供高效的出队和入队操作,visited 集合避免重复访问,确保每个节点仅处理一次。
应用场景对比
场景是否适用BFS原因
最短路径(无权图)逐层扩展可最早到达目标节点
拓扑排序更适合使用DFS或Kahn算法

4.3 Dijkstra最短路径算法的代码实现

算法核心思想
Dijkstra算法通过贪心策略,从源点出发逐步扩展最短路径树。使用优先队列维护待处理节点,确保每次取出距离最小的顶点进行松弛操作。
Python实现示例
import heapq

def dijkstra(graph, start):
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    pq = [(0, start)]  # (distance, node)
    
    while pq:
        cur_dist, u = heapq.heappop(pq)
        if cur_dist > dist[u]:
            continue
        for v, weight in graph[u]:
            new_dist = cur_dist + weight
            if new_dist < dist[v]:
                dist[v] = new_dist
                heapq.heappush(pq, (new_dist, v))
    return dist
上述代码中,graph为邻接表表示的图结构,dist数组记录起点到各点最短距离,优先队列pq按距离排序,确保贪心选择最优。
时间复杂度分析
  • 使用二叉堆优化后,时间复杂度为 O((V + E) log V)
  • 适用于非负权有向图或无向图的单源最短路径问题

4.4 Floyd-Warshall算法求解多源最短路径

Floyd-Warshall算法是一种动态规划算法,用于求解图中所有顶点对之间的最短路径。适用于带权有向图或无向图,可处理负权边(但不能有负权环)。
算法核心思想
通过中间顶点逐步优化路径:对于每一对顶点 (i, j),检查是否存在经过顶点 k 使得距离更短,即:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
该状态转移方程在三重循环中枚举所有可能的中间点 k、起点 i 和终点 j。
算法实现
初始时,邻接矩阵存储直接边权值,不可达设为无穷大。经过 n 轮迭代后,矩阵中每个元素 dist[i][j] 即为最短距离。
节点对初始距离最终最短距离
A → B65
B → C33
A → C8

第五章:总结与图算法学习路径建议

构建扎实的图论基础
掌握图算法前,需深入理解图的基本结构与表示方式。邻接表和邻接矩阵是两种核心存储结构,适用于不同场景。例如,在稀疏图中使用邻接表可节省空间:

type Graph struct {
    vertices int
    adjList  map[int][]int
}

func NewGraph(v int) *Graph {
    return &Graph{
        vertices: v,
        adjList:  make(map[int][]int),
    }
}

func (g *Graph) AddEdge(src, dest int) {
    g.adjList[src] = append(g.adjList[src], dest)
}
循序渐进的学习路线
建议按以下顺序掌握关键算法,确保理论与实践结合:
  1. 图的遍历:深度优先搜索(DFS)与广度优先搜索(BFS)
  2. 最短路径:Dijkstra、Bellman-Ford、Floyd-Warshall
  3. 最小生成树:Prim 与 Kruskal 算法
  4. 拓扑排序与有向无环图(DAG)应用
  5. 网络流算法:Ford-Fulkerson 与最大流最小割定理
实战项目推荐
通过真实场景提升理解深度。例如,使用 Neo4j 构建社交网络关系图谱,分析用户间最短连接路径;或在物流系统中应用 Dijkstra 算法优化配送路线。下表列出典型应用场景与对应算法:
应用场景推荐算法技术栈示例
社交网络影响力分析PageRankNeo4j + Python
交通导航系统A* 算法C++ + OpenStreetMap
任务调度依赖解析拓扑排序Java + JGraphT
持续进阶资源建议
参与开源图计算项目如 Apache Giraph 或 JanusGraph,阅读《Algorithm Design》中图相关章节,并在 LeetCode 上刷题强化实现能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值