邻接矩阵存储从入门到精通,手把手教你用C语言实现图操作

第一章:图的基本概念与邻接矩阵概述

图是计算机科学中用于表示对象之间关系的重要数据结构。它由一组顶点(或称为节点)和一组连接这些顶点的边组成。根据边是否有方向,图可分为有向图和无向图;若边具有数值属性,则称为带权图。图广泛应用于社交网络分析、路径规划、推荐系统等领域。

图的核心组成要素

  • 顶点(Vertex):表示图中的一个基本单元,如城市、用户等。
  • 边(Edge):连接两个顶点的关系,可以是有向或无向的。
  • 权重(Weight):边上的数值,常用于表示距离、成本等。

邻接矩阵的表示方法

邻接矩阵是一种用二维数组表示图的方式。对于包含 n 个顶点的图,使用一个 n×n 的矩阵 graph,其中:
  • 若存在从顶点 ij 的边,则 graph[i][j] = 1(或权重值);
  • 否则为 0
对于无向图,邻接矩阵是对称的。这种方式便于快速判断两顶点间是否存在边,但空间复杂度为 O(n²),在稀疏图中可能造成存储浪费。
顶点ABC
A011
B100
C100
// Go语言中定义一个简单的邻接矩阵
package main

import "fmt"

func main() {
    // 3x3 邻接矩阵表示无向图 A-B, A-C
    graph := [][]int{
        {0, 1, 1}, // A 连接到 B 和 C
        {1, 0, 0}, // B 只连接到 A
        {1, 0, 0}, // C 只连接到 A
    }

    fmt.Println("Adjacency Matrix:")
    for _, row := range graph {
        fmt.Println(row)
    }
}
graph TD A --> B A --> C B --> A C --> A

第二章:邻接矩阵的结构设计与初始化

2.1 图的数学定义与邻接矩阵表示原理

图在数学上被定义为一个二元组 $ G = (V, E) $,其中 $ V $ 是顶点的有限集合,$ E \subseteq V \times V $ 是边的集合。根据边是否有方向,图可分为有向图和无向图。
邻接矩阵的基本结构
邻接矩阵使用二维数组 $ A $ 表示图中顶点间的连接关系。若图包含 $ n $ 个顶点,则矩阵维度为 $ n \times n $,其中: $$ A[i][j] = \begin{cases} 1, & \text{若存在从 } i \text{ 到 } j \text{ 的边} \\ 0, & \text{否则} \end{cases} $$
  • 无向图的邻接矩阵是对称的
  • 有向图的矩阵可能不对称
  • 自环对应对角线元素
代码实现示例
type Graph struct {
    vertices int
    matrix   [][]int
}

func NewGraph(n int) *Graph {
    mat := make([][]int, n)
    for i := range mat {
        mat[i] = make([]int, n)
    }
    return &Graph{n, mat}
}

func (g *Graph) AddEdge(u, v int) {
    g.matrix[u][v] = 1 // 设置边存在
}
该Go语言结构体使用二维切片存储邻接矩阵,AddEdge方法在指定位置置1,表示边的存在。矩阵初始化时间复杂度为 $ O(n^2) $,适合稠密图的表示。

2.2 C语言中邻接矩阵的数据结构设计

在C语言中,邻接矩阵通常采用二维数组实现,用于表示图中顶点之间的连接关系。该结构适用于稠密图,访问任意边的时间复杂度为O(1)。
基本结构定义

#define MAX_VERTICES 100

typedef struct {
    int vertices;                       // 顶点数量
    int adjMatrix[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵
} Graph;
上述代码定义了一个图结构体,其中 adjMatrix[i][j] 表示从顶点 i 到顶点 j 是否存在边。初始化时所有元素设为0,有边则置为1(或边的权重)。
初始化操作
  • 分配图结构内存,设置顶点数
  • 将邻接矩阵所有元素初始化为0(无边状态)
  • 支持后续动态添加边(设置对应矩阵元素值)

2.3 图的初始化与顶点边关系建模

在构建图结构时,首要步骤是完成图的初始化,并建立顶点与边之间的逻辑关系。通常使用邻接表或邻接矩阵来表示图。
邻接表实现方式

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

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

func (g *Graph) AddEdge(u, v int) {
    g.vertices[u] = append(g.vertices[u], v)
}
上述代码定义了一个基于哈希表的无向图结构,AddEdge 方法用于添加有向边。g.vertices[u] 存储从顶点 u 可达的所有相邻顶点。
顶点与边的数据组织
  • 邻接表节省空间,适合稀疏图;
  • 邻接矩阵便于判断边的存在性,适用于稠密图;
  • 加权边可通过 map[int]map[int]int 表示。

2.4 无向图与有向图的矩阵构建差异

在图论中,邻接矩阵是表示图结构的重要方式。无向图和有向图在矩阵构建上的核心差异体现在对称性上。
邻接矩阵的对称性特征
无向图的邻接矩阵是对称的,即 A[i][j] = A[j][i],因为边没有方向。而有向图的邻接矩阵通常不对称,边 i → j 存在并不代表 j → i 也存在。
# 无向图邻接矩阵示例(对称)
adj_undirected = [
    [0, 1, 1],
    [1, 0, 0],
    [1, 0, 0]
]

# 有向图邻接矩阵示例(非对称)
adj_directed = [
    [0, 1, 0],
    [0, 0, 1],
    [1, 0, 0]
]
上述代码展示了两种图的矩阵构造方式。无向图中节点0与1、2相连,因此矩阵关于主对角线对称;有向图中边具有方向性,矩阵无需满足对称性。
构建差异对比
特性无向图有向图
矩阵对称性
边存储方式双向记录单向记录

2.5 实战:用C实现图的创建与初始化

邻接矩阵表示法
在C语言中,使用二维数组实现邻接矩阵是图初始化的常用方式。该结构适合稠密图,访问边的时间复杂度为O(1)。
#define MAX_VERTICES 100
typedef struct {
    int vertices;
    int adjMatrix[MAX_VERTICES][MAX_VERTICES];
} Graph;

Graph* createGraph(int n) {
    Graph* g = (Graph*)malloc(sizeof(Graph));
    g->vertices = n;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            g->adjMatrix[i][j] = 0;
    return g;
}
上述代码中,`createGraph`函数动态分配图结构内存,并将邻接矩阵所有元素初始化为0,表示无边。`vertices`字段记录顶点数量,便于后续遍历操作。
适用场景对比
  • 邻接矩阵:适合顶点数较少且边密集的图
  • 邻接表:节省空间,适合稀疏图

第三章:基于邻接矩阵的图操作实现

3.1 插入顶点与添加边的操作逻辑

在图结构中,插入顶点和添加边是构建网络关系的基础操作。首先,插入顶点需确保唯一性,避免重复节点导致数据异常。
顶点插入流程
  • 检查顶点是否已存在于图中
  • 若不存在,则分配存储空间并初始化邻接表
  • 将顶点加入全局顶点集合
边的添加实现
func (g *Graph) AddEdge(src, dst string) {
    if !g.Contains(src) {
        g.AddVertex(src)
    }
    if !g.Contains(dst) {
        g.AddVertex(dst)
    }
    g.adjacencyList[src] = append(g.adjacencyList[src], dst)
}
上述代码展示了有向图中边的添加逻辑:先确保源和目标顶点存在,再将目标节点追加到源节点的邻接列表中。该操作时间复杂度为 O(1),适用于频繁增删的动态图场景。

3.2 删除边与顶点的处理策略

在图结构操作中,删除边与顶点需谨慎处理关联关系,避免出现悬空引用或数据不一致。
删除边的实现逻辑
对于邻接表表示的图,删除边可通过移除对应顶点邻接列表中的目标节点实现:
// 删除顶点u到v的有向边
func removeEdge(graph map[int][]int, u, v int) {
    for i, neighbor := range graph[u] {
        if neighbor == v {
            graph[u] = append(graph[u][:i], graph[u][i+1:]...)
            break
        }
    }
}
该函数通过切片操作移除指定边,时间复杂度为 O(n),适用于稀疏图场景。
顶点删除的连锁处理
删除顶点时,需同步清除所有指向该顶点的边。通常采用两步策略:
  1. 遍历图中所有顶点,删除与其相连的边;
  2. 从顶点集合中移除该顶点。
此策略确保图结构完整性,防止内存泄漏和访问异常。

3.3 实战:动态修改图结构的完整代码实现

在图计算系统中,动态修改图结构是支持实时数据更新的关键能力。本节通过一个完整的代码示例展示如何在运行时安全地增删节点与边。
核心操作接口设计
提供统一的API用于图结构变更,确保线程安全与数据一致性。

// AddVertex 动态添加顶点
func (g *Graph) AddVertex(id string, attrs map[string]interface{}) {
    g.Lock()
    defer g.Unlock()
    g.vertices[id] = attrs
}
上述方法通过互斥锁保护共享状态,防止并发写入导致数据竞争。
边的动态更新机制
支持运行时建立或删除节点之间的连接关系。

// AddEdge 添加有向边
func (g *Graph) AddEdge(src, dst string) {
    g.Lock()
    defer g.Unlock()
    if _, exists := g.edges[src]; !exists {
        g.edges[src] = make(map[string]bool)
    }
    g.edges[src][dst] = true
}
该实现使用嵌套映射存储邻接关系,时间复杂度为O(1)的边查找,适合高频更新场景。

第四章:图的遍历与典型算法应用

4.1 深度优先遍历(DFS)的矩阵实现

在图的邻接矩阵表示中,深度优先遍历通过递归或栈结构探索每个顶点的未访问邻居。矩阵的行对应当前顶点,列则表示可达性。
算法流程
  • 初始化访问标记数组 visited[],记录节点是否被访问
  • 从起始顶点开始,遍历其邻接行中的每一个元素
  • 若存在边且目标节点未被访问,则递归进入该节点
代码实现
void dfs(int matrix[][V], int start, int visited[]) {
    visited[start] = 1;
    printf("%d ", start);
    for (int i = 0; i < V; i++) {
        if (matrix[start][i] == 1 && !visited[i]) {
            dfs(matrix, i, visited);
        }
    }
}
上述函数以邻接矩阵 matrix、起始节点 start 和访问数组 visited 为参数。当 matrix[start][i] 为 1 时,表示存在边,且若 visited[i] 为假,则递归访问节点 i,确保所有连通节点被遍历。

4.2 广度优先遍历(BFS)的队列机制应用

广度优先遍历(BFS)利用队列的“先进先出”特性,逐层访问图或树的节点。从起始节点出发,将其入队,随后循环出队并访问其所有未访问邻接点,再依次入队,确保靠近起点的节点优先被处理。
队列在BFS中的核心作用
队列维持了节点的访问顺序,保证每一层节点在下一层之前被完全处理,是实现层级遍历的关键数据结构。

// BFS 遍历图的邻接表表示
func BFS(graph map[int][]int, start int) {
    visited := make(map[int]bool)
    queue := []int{start}
    visited[start] = true

    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:] // 出队
        fmt.Println(node)

        for _, neighbor := range graph[node] {
            if !visited[neighbor] {
                visited[neighbor] = true
                queue = append(queue, neighbor) // 入队
            }
        }
    }
}
上述代码中, queue 模拟队列操作, visited 防止重复访问。每次从队首取出节点,将其未访问的邻接点加入队尾,确保按层级扩展。

4.3 最短路径问题:Floyd算法原理与编码

算法核心思想
Floyd算法用于求解图中所有顶点对之间的最短路径,适用于带权有向图或无向图。其核心是动态规划思想:通过引入中间顶点逐步优化任意两点间的距离估计。
算法实现
def floyd_warshall(n, edges):
    # 初始化距离矩阵
    dist = [[float('inf')] * n for _ in range(n)]
    for i in range(n):
        dist[i][i] = 0
    for u, v, w in edges:
        dist[u][v] = w

    # 动态规划更新最短路径
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    return dist
代码中, dist[i][j] 表示从顶点 ij 的最短距离。三重循环枚举中间点 k,尝试通过 k 缩短路径。时间复杂度为 O(n³),适合中小规模稠密图。

4.4 最小生成树:Prim算法详解与实现

Prim算法用于在加权无向图中构造最小生成树(MST),其核心思想是从一个起始顶点出发,逐步扩展生成树,每次选择连接已选顶点集与未选顶点集中权值最小的边。
算法流程概述
  • 初始化:任选一个起始顶点加入生成树集合
  • 重复操作:选取连接已访问与未访问顶点的最小权重边
  • 更新顶点状态,直至所有顶点都被覆盖
Python实现示例
import heapq

def prim(graph, start):
    mst = []
    visited = set([start])
    edges = [(weight, start, to) for to, weight in graph[start]]
    heapq.heapify(edges)

    while edges:
        weight, frm, to = heapq.heappop(edges)
        if to not in visited:
            visited.add(to)
            mst.append((frm, to, weight))
            for neighbor, w in graph[to]:
                if neighbor not in visited:
                    heapq.heappush(edges, (w, to, neighbor))
    return mst
该实现使用优先队列维护候选边,确保每次取出最小权重边。graph以邻接表形式存储,时间复杂度为O(E log E),适用于稠密图场景。

第五章:性能分析与邻接矩阵的适用场景总结

邻接矩阵在稠密图中的优势
对于边数接近顶点数平方的稠密图,邻接矩阵表现出优异的查询效率。任意两点间是否存在边可通过 O(1) 时间判断,适合频繁查询的应用场景。
图类型空间复杂度边查询时间适用场景
稠密图O(V²)O(1)社交网络全连接分析
稀疏图O(V²)O(1)不推荐使用
实际内存开销对比
以 10,000 个顶点为例,布尔型邻接矩阵需占用约 100MB 内存(10⁴ × 10⁴ / 8 / 1024²),若使用 float64 存储权重则高达 800MB。这在嵌入式系统中可能不可接受。
  • 图像处理中像素邻域连接常采用邻接矩阵建模
  • 航班网络若包含所有城市对直飞信息,适合用矩阵存储
  • 实时路径规划系统中,预计算的邻接矩阵可加速最短路径查询
代码实现中的优化技巧
使用位压缩技术可减少空间占用,以下为 Go 语言示例:

// 使用 bitset 压缩存储布尔邻接矩阵
type BitMatrix struct {
    data []uint64
    size int
}

func (bm *BitMatrix) Set(i, j int) {
    idx := i*bm.size + j
    bm.data[idx/64] |= 1 << (idx % 64)
}

func (bm *BitMatrix) Get(i, j int) bool {
    idx := i*bm.size + j
    return (bm.data[idx/64] & (1 << (idx % 64))) != 0
}
[顶点A] —— 矩阵行 ——> [1 0 1 1] [顶点B] —— 矩阵行 ——> [0 1 0 1] 每一行代表从该顶点出发的所有连接状态
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值