图论基础:探索节点与关系的复杂网络

图是比线性表和树更为复杂的一种非线性数据结构。在线性表中,数据元素之间是线性关系;在树形结构中,元素之间是明显的层次关系(父子节点)。而在中,任意两个数据元素之间都可能存在关系,这使它能够完美地建模现实世界中各种复杂的关系网络,如社交网络、交通路线、任务依赖等。

一、 图的基本概念
  • 顶点 :图中的数据元素,通常称为顶点节点
  • :图中连接两个顶点的线,表示两者之间的关系。边可以是有方向的(有向图 ),也可以是无方向的(无向图)。
  • :边可以有一个数值权重,表示连接的代价、距离或强度,这样的图称为带权图
    • 无向图中,一个顶点的度是指与其相连的边的数量。
    • 有向图 中,度分为入度 (指向该顶点的边的数量)和出度(从该顶点指出的边的数量)。
  • 路径与环 :从一个顶点经过一系列边到达另一个顶点构成的序列称为路径 。如果路径的起点和终点是同一个顶点,则称之为

图的存储方式主要有两种:

  1. 邻接矩阵:使用一个二维数组来存储边的关系。简单直观,但空间复杂度高(O(V²)),适合稠密图。
  2. 邻接表:为每个顶点维护一个链表,存储所有与其相邻的顶点。空间效率高(O(V+E)),是更常用的表示方法,尤其适合稀疏图。

二、 图的遍历

图的遍历是指从图中某一顶点出发,按照某种策略访问图中所有顶点,且每个顶点仅被访问一次。遍历是许多图算法的基础。主要有两种策略:

1. 广度优先搜索(BFS)

  • 核心思想: "一圈一圈"地向外探索。先访问起始顶点,然后访问其所有未访问过的邻接顶点,再按顺序访问这些邻接顶点的邻接顶点,以此类推。
  • 数据结构队列。用于存储已被访问但其邻接点尚未被检查的顶点。
  • 算法步骤
    1. 将起始顶点标记为已访问,并放入队列。
    2. 当队列不为空时:
      • 从队列中取出一个顶点 v
      • 访问 v 的所有未被访问过的邻接顶点,将它们标记为已访问并依次放入队列。
  • 应用: 寻找无权图中的最短路径、检查图的连通性、社交网络中的"好友推荐"。
  • 复杂度: 时间复杂度 O(V+E),空间复杂度 O(V)。

代码示例(BFS 伪代码):

java 复制代码
def BFS(graph, start_vertex):
    visited = set()          # 记录已访问的顶点
    queue = Queue()          # 创建一个队列

    visited.add(start_vertex)
    queue.enqueue(start_vertex)

    while not queue.is_empty():
        current_vertex = queue.dequeue()
        print(f"访问顶点:{current_vertex}") # 处理当前顶点

        for neighbor in graph.adjacent_vertices_of(current_vertex):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.enqueue(neighbor)

2. 深度优先搜索(DFS)

  • 核心思想: "一条路走到黑"。从起始顶点出发,沿着一条路径尽可能深地探索,直到没有未访问的邻接顶点,然后回溯到上一个顶点,继续探索其他路径。
  • 数据结构(递归调用栈或显式栈)。体现了"后进先出"的回溯特性。
  • 算法步骤(递归版本)
    1. 访问当前顶点 v,并将其标记为已访问。
    2. 遍历 v 的所有邻接顶点 w
      • 如果 w 未被访问,则递归地执行 DFS(w)。
  • 应用: 寻找路径、检测图中是否存在环、拓扑排序。
  • 复杂度: 时间复杂度 O(V+E),空间复杂度 O(V)(主要取决于递归深度)。

代码示例(DFS 递归伪代码):

java 复制代码
def DFS_recursive(graph, v, visited):
    visited.add(v)
    print(f"访问顶点:{v}") # 处理当前顶点

    for w in graph.adjacent_vertices_of(v):
        if w not in visited:
            DFS_recursive(graph, w, visited)

# 初始化调用
visited = set()
DFS_recursive(graph, start_vertex, visited)

三、 最小生成树(MST)

在一个带权的、连通的、无向图 中,最小生成树是指一个无环的子图,它连接了图中所有的顶点,并且其所有边的权重之和最小。

应用场景: 要在多个城市之间铺设光缆,如何用最短的总线路连接所有城市(顶点)?这就是一个典型的最小生成树问题。

1. 普里姆算法(Prim's Algorithm)

  • 核心思想 : 从任意一个顶点开始,一步步"生长"出一棵树。每次将连接当前生成树树外顶点的权重最小的边(以及该边对应的新顶点)加入到生成树中。
  • 数据结构优先队列(最小堆)。用于高效地找到当前可选的权重最小的边。
  • 算法步骤
    1. 任选一个顶点作为起始点,加入生成树。
    2. 将所有连接生成树内顶点与树外顶点的边加入优先队列。
    3. 从队列中取出权重最小的边。如果这条边连接的另一个顶点不在生成树中,则将该边和顶点加入生成树。
    4. 重复步骤2和3,直到所有顶点都加入生成树。

2. 克鲁斯卡尔算法(Kruskal's Algorithm)

  • 核心思想: 按权重从小到大考虑所有边,如果加入当前边不会与已选择的边构成环,则将其加入生成树。本质是并查集数据结构的完美应用。
  • 数据结构并查集。用于高效地判断两个顶点是否已经在同一连通分量(即加入边后是否会形成环)。
  • 算法步骤
    1. 将图中所有边按权重从小到大排序。
    2. 初始化一个空的生成树。
    3. 遍历排序后的边:
      • 如果当前边连接的两个顶点不在生成树的同一个连通分量中(即加入后不会形成环),则将该边加入生成树。
      • 否则,跳过该边。
    4. 当生成树中有 V-1 条边时,算法结束。

对比

  • Prim算法是"顶点导向"的,适合稠密图(边多)。
  • Kruskal算法是"边导向"的,适合稀疏图(边少)。

四、 拓扑排序

拓扑排序是针对有向无环图(DAG) 的一种线性序列。该序列需要满足:对于图中的每一条有向边 u -> v,在序列中 u 都出现在 v 的前面。

应用场景: 课程选修顺序(必须先修完高数才能修线性代数)、任务调度、编译过程中的依赖解析。

核心思想(Kahn 算法,基于BFS)

  1. 统计入度: 计算图中每个顶点的入度。
  2. 初始化队列 : 将所有入度为0的顶点加入队列。这些顶点是"不依赖任何其他任务的任务"。
  3. 处理队列
    • 从队列中取出一个顶点 u,将其输出到拓扑序列中。
    • 遍历 u 的所有邻接顶点 v,将 v 的入度减1(相当于移除边 u->v)。
    • 如果某个顶点 v 的入度减为0,则将其加入队列。
  4. 检查结果: 如果最终的拓扑序列包含了图中所有的顶点,则排序成功;如果序列中顶点数少于总顶点数,说明图中存在环,无法进行拓扑排序。

算法示例

假设有如下课程依赖图(A->B 表示修B前需先修A):

复制代码
数学 -> 物理
数学 -> 编程
物理 -> 电子
编程 -> 电子
编程 -> 算法

一种可能的拓扑排序是:[数学, 物理, 编程, 电子, 算法][数学, 编程, 物理, 算法, 电子]

代码示例(拓扑排序 Kahn 算法伪代码):

java 复制代码
def topological_sort(graph):
    in_degree = {v: 0 for v in graph.vertices} # 初始化入度表
    # 计算所有顶点的入度
    for u in graph.vertices:
        for v in graph.adjacent_vertices_of(u):
            in_degree[v] += 1

    queue = Queue()
    # 将所有入度为0的顶点入队
    for v in graph.vertices:
        if in_degree[v] == 0:
            queue.enqueue(v)

    topo_order = []
    while not queue.is_empty():
        u = queue.dequeue()
        topo_order.append(u)
        for v in graph.adjacent_vertices_of(u):
            in_degree[v] -= 1
            if in_degree[v] == 0:
                queue.enqueue(v)

    if len(topo_order) != len(graph.vertices):
        print("错误:图中存在环,无法进行拓扑排序!")
    else:
        return topo_order

总结

图是一种极其强大和灵活的数据结构,其相关算法是解决许多现实世界复杂问题的关键。

主题 核心思想 关键数据结构 应用场景
BFS遍历 层层扩散,先广后深 队列 最短路径(无权)、连通性
DFS遍历 深度探索,回溯前进 栈(递归) 路径查找、环检测
最小生成树 用最小代价连接所有点 Prim:优先队列 / Kruskal:并查集+排序 网络搭建、电路设计
拓扑排序 为有向无环图安排顺序 队列、入度表 任务调度、依赖管理

理解这些经典图算法,不仅能帮助你在技术面试中游刃有余,更能为你提供解决复杂系统设计问题的强大工具箱。

相关推荐
liu****3 小时前
20.哈希
开发语言·数据结构·c++·算法·哈希算法
小云数据库服务专线4 小时前
GaussDB 应用侧报no pg_hba.conf entry for host处理方法
服务器·网络·gaussdb
christine-rr4 小时前
linux常用命令——其他
linux·服务器·网络·数据库·redis·ubuntu
“αβ”4 小时前
了解“网络协议”
linux·服务器·网络·c++·网络协议·tcp/ip·tcp
_dindong4 小时前
Linux网络编程:Socket编程TCP
linux·服务器·网络·笔记·学习·tcp/ip
一匹电信狗5 小时前
【LeetCode_160】相交链表
c语言·开发语言·数据结构·c++·算法·leetcode·stl
卷卷的小趴菜学编程5 小时前
Linux网络之----序列化和反序列化
网络·序列化·反序列化·守护进程·jsoncpp·进程组·前后台进程
Java技术实践5 小时前
JPA 用 List 入参在 @Query中报错 unexpected AST node: {vector}
数据结构·windows·list
陌路205 小时前
S4双向链表
数据结构·链表