数据结构与算法基础-第4章 图论基础

第4章 图论基础

图是一种比树更复杂的非线性数据结构,它由顶点和边组成。图可以用来表示各种关系网络,比如社交网络、交通网络、计算机网络等。本章将介绍图的基本概念、表示方法和遍历算法。

4.1 图的基本概念

Graph Structure

图由顶点集合V和边集合E组成,记作G=(V,E)。顶点是图中的数据元素,边是顶点之间的连接关系。根据边是否有方向,图分为无向图和有向图。无向图的边没有方向,两个顶点之间可以双向通行。有向图的边有方向,从一个顶点指向另一个顶点。

根据边是否带有权值,图分为无权图和带权图。无权图的边只表示连接关系,没有额外的数值信息。带权图的边带有权值,可以表示距离、成本、时间等信息。带权图也叫网络图。

class Graph:  
    def __init__(self, directed=False):  
        self.directed = directed  
        self.vertices = set()  
        self.edges = {}  
      
    def add_vertex(self, vertex):  
        self.vertices.add(vertex)  
        if vertex not in self.edges:  
            self.edges[vertex] = {}  
      
    def add_edge(self, v1, v2, weight=1):  
        self.add_vertex(v1)  
        self.add_vertex(v2)  
        self.edges[v1][v2] = weight  
        if not self.directed:  
            self.edges[v2][v1] = weight  
      
    def get_vertices(self):  
        return list(self.vertices)  
      
    def get_edges(self):  
        edge_list = []  
        for v1 in self.edges:  
            for v2 in self.edges[v1]:  
                if not self.directed and (v2, v1) in edge_list:  
                    continue  
                edge_list.append((v1, v2, self.edges[v1][v2]))  
        return edge_list  
      
    def get_neighbors(self, vertex):  
        return self.edges.get(vertex, {})

图的几个重要概念需要理解。度是指与顶点相连的边的数量。在有向图中,度分为入度和出度。入度是指指向该顶点的边的数量,出度是指从该顶点发出的边的数量。路径是从一个顶点到另一个顶点的顶点序列,序列中相邻顶点之间有边相连。路径的长度是路径上边的数量或权值之和。简单路径是不包含重复顶点的路径。环是起点和终点相同的路径。连通是指两个顶点之间存在路径。连通图是指任意两个顶点之间都连通的图。

4.2 图的表示方法

图的存储方式主要有邻接矩阵和邻接表两种。邻接矩阵使用二维数组表示图,矩阵的行和列对应顶点,矩阵元素表示顶点之间是否有边。邻接矩阵的优点是判断两个顶点是否相连只需O(1)时间,缺点是空间复杂度为O(V²),对于稀疏图浪费空间。

class AdjacencyMatrix:  
    def __init__(self, num_vertices, directed=False):  
        self.num_vertices = num_vertices  
        self.directed = directed  
        self.matrix = [[0] * num_vertices for _ in range(num_vertices)]  
      
    def add_edge(self, v1, v2, weight=1):  
        self.matrix[v1][v2] = weight  
        if not self.directed:  
            self.matrix[v2][v1] = weight  
      
    def has_edge(self, v1, v2):  
        return self.matrix[v1][v2] != 0  
      
    def get_weight(self, v1, v2):  
        return self.matrix[v1][v2]  
      
    def get_neighbors(self, v):  
        neighbors = []  
        for i in range(self.num_vertices):  
            if self.matrix[v][i] != 0:  
                neighbors.append(i)  
        return neighbors  
      
    def display(self):  
        for row in self.matrix:  
            print(' '.join(str(x) for x in row))  

graph = AdjacencyMatrix(5)  
graph.add_edge(0, 1)  
graph.add_edge(0, 2)  
graph.add_edge(1, 3)  
graph.add_edge(2, 4)  
graph.display()

邻接表使用数组或字典存储每个顶点的邻居列表。邻接表的优点是空间复杂度为O(V+E),适合稀疏图。缺点是判断两个顶点是否相连需要遍历邻居列表,时间复杂度为O(V)。

class AdjacencyList:  
    def __init__(self, directed=False):  
        self.directed = directed  
        self.vertices = {}  
      
    def add_vertex(self, vertex):  
        if vertex not in self.vertices:  
            self.vertices[vertex] = []  
      
    def add_edge(self, v1, v2, weight=1):  
        self.add_vertex(v1)  
        self.add_vertex(v2)  
        self.vertices[v1].append((v2, weight))  
        if not self.directed:  
            self.vertices[v2].append((v1, weight))  
      
    def get_neighbors(self, vertex):  
        return self.vertices.get(vertex, [])  
      
    def display(self):  
        for vertex in self.vertices:  
            neighbors = ', '.join(f'{v}({w})' for v, w in self.vertices[vertex])  
            print(f'{vertex}: [{neighbors}]')  

graph = AdjacencyList()  
graph.add_edge('A', 'B', 1)  
graph.add_edge('A', 'C', 2)  
graph.add_edge('B', 'D', 3)  
graph.add_edge('C', 'D', 4)  
graph.display()

选择哪种表示方法取决于具体应用。如果图比较稠密,边的数量接近V²,邻接矩阵更合适。如果图比较稀疏,边的数量远小于V²,邻接表更节省空间。如果需要频繁查询两个顶点是否相连,邻接矩阵效率更高。如果需要频繁遍历某个顶点的所有邻居,邻接表效率更高。

4.3 广度优先搜索

Graph Traversal

广度优先搜索是一种图的遍历算法,它从起始顶点开始,先访问所有邻居,然后访问邻居的邻居,以此类推,层层向外扩展。BFS使用队列来保证按层次顺序访问顶点,保证先访问距离起始顶点近的顶点。

def bfs(graph, start):  
    visited = set()  
    queue = [start]  
    result = []  
      
    while queue:  
        vertex = queue.pop(0)  
        if vertex in visited:  
            continue  
          
        visited.add(vertex)  
        result.append(vertex)  
          
        for neighbor in graph.get_neighbors(vertex):  
            if neighbor not in visited:  
                queue.append(neighbor)  
      
    return result  

def bfs_shortest_path(graph, start, end):  
    visited = set([start])  
    queue = [(start, [start])]  
      
    while queue:  
        vertex, path = queue.pop(0)  
          
        if vertex == end:  
            return path  
          
        for neighbor in graph.get_neighbors(vertex):  
            if neighbor not in visited:  
                visited.add(neighbor)  
                queue.append((neighbor, path + [neighbor]))  
      
    return None

BFS的一个重要应用是寻找无权图中的最短路径。因为BFS按层次遍历,第一次到达目标顶点时,经过的路径长度最小。在社交网络中,BFS可以找到两个人之间的最短关系链。在迷宫问题中,BFS可以找到从起点到终点的最短路径。

BFS的时间复杂度为O(V+E),因为每个顶点和每条边最多被访问一次。空间复杂度为O(V),因为需要存储访问标记和队列。

4.4 深度优先搜索

深度优先搜索也是一种图的遍历算法,它从起始顶点开始,沿着一条路径尽可能深入,直到无法继续前进,然后回溯到上一个顶点,继续探索其他路径。DFS使用栈或递归来实现,体现出回溯的思想。

def dfs_recursive(graph, start, visited=None):  
    if visited is None:  
        visited = set()  
      
    visited.add(start)  
    result = [start]  
      
    for neighbor in graph.get_neighbors(start):  
        if neighbor not in visited:  
            result.extend(dfs_recursive(graph, neighbor, visited))  
      
    return result  

def dfs_iterative(graph, start):  
    visited = set()  
    stack = [start]  
    result = []  
      
    while stack:  
        vertex = stack.pop()  
        if vertex in visited:  
            continue  
          
        visited.add(vertex)  
        result.append(vertex)  
          
        for neighbor in reversed(graph.get_neighbors(vertex)):  
            if neighbor not in visited:  
                stack.append(neighbor)  
      
    return result

DFS的应用比BFS更加广泛。检测图的连通性,判断图中是否有环,寻找拓扑排序,检测二分图等都可以使用DFS。DFS还可以用于求解迷宫问题,但找到的路径不一定是最短的。

def has_cycle_dfs(graph):  
    def dfs(vertex, visited, parent):  
        visited.add(vertex)  
          
        for neighbor in graph.get_neighbors(vertex):  
            if neighbor not in visited:  
                if dfs(neighbor, visited, vertex):  
                    return True  
            elif neighbor != parent:  
                return True  
          
        return False  
      
    visited = set()  
    for vertex in graph.vertices:  
        if vertex not in visited:  
            if dfs(vertex, visited, None):  
                return True  
    return False

DFS的时间复杂度为O(V+E),空间复杂度为O(V)。递归实现的DFS需要额外的栈空间,深度可达V。对于深度很大的图,使用迭代实现可以避免栈溢出。

4.5 最短路径算法

Shortest Path

在带权图中寻找两个顶点之间的最短路径是一个经典问题。Dijkstra算法是最著名的最短路径算法,它可以找到从一个顶点到其他所有顶点的最短路径。Dijkstra算法使用贪心策略,每次选择距离最近的未访问顶点,更新其邻居的距离。

import heapq  

def dijkstra(graph, start):  
    distances = {vertex: float('inf') for vertex in graph.vertices}  
    distances[start] = 0  
    visited = set()  
    pq = [(0, start)]  
      
    while pq:  
        current_dist, vertex = heapq.heappop(pq)  
          
        if vertex in visited:  
            continue  
          
        visited.add(vertex)  
          
        for neighbor, weight in graph.get_neighbors(vertex):  
            new_dist = current_dist + weight  
              
            if new_dist < distances[neighbor]:  
                distances[neighbor] = new_dist  
                heapq.heappush(pq, (new_dist, neighbor))  
      
    return distances  

graph = AdjacencyList(directed=True)  
graph.add_edge('A', 'B', 4)  
graph.add_edge('A', 'C', 2)  
graph.add_edge('B', 'C', 1)  
graph.add_edge('B', 'D', 5)  
graph.add_edge('C', 'D', 8)  
graph.add_edge('C', 'E', 10)  
graph.add_edge('D', 'E', 2)  

distances = dijkstra(graph, 'A')  
print("从A出发的最短距离:")  
for vertex, dist in distances.items():  
    print(f"  到{vertex}: {dist}")

Dijkstra算法的时间复杂度为O(V²)或O(E log V),取决于实现方式。使用优先队列可以将时间复杂度优化到O(E log V)。Dijkstra算法只能用于边权为正的图,对于边权为负的图,需要使用Bellman-Ford算法。

如果只需要两个顶点之间的最短路径,而不需要到所有顶点的最短路径,可以使用A算法。A算法在Dijkstra算法的基础上加入了启发式信息,可以更快地找到目标路径。

4.6 拓扑排序

Topological Sort

拓扑排序是针对有向无环图的一种排序方法,它将图中所有顶点排成一个线性序列,使得图中每一条边的起点在终点之前。拓扑排序常用于任务调度、课程安排等场景,需要确定任务的执行顺序。

def topological_sort(graph):  
    def dfs(vertex, visited, stack, temp):  
        if vertex in temp:  
            raise ValueError("图中存在环,无法进行拓扑排序")  
          
        if vertex in visited:  
            return  
          
        temp.add(vertex)  
          
        for neighbor in graph.get_neighbors(vertex):  
            dfs(neighbor, visited, stack, temp)  
          
        temp.remove(vertex)  
        visited.add(vertex)  
        stack.append(vertex)  
      
    visited = set()  
    stack = []  
      
    for vertex in graph.vertices:  
        if vertex not in visited:  
            dfs(vertex, visited, stack, [])  
      
    return stack[::-1]  

graph = AdjacencyList(directed=True)  
graph.add_edge('A', 'B')  
graph.add_edge('A', 'C')  
graph.add_edge('B', 'D')  
graph.add_edge('C', 'D')  

order = topological_sort(graph)  
print("拓扑排序结果:", ' -> '.join(order))

拓扑排序的另一种实现方法是Kahn算法,它基于入度的概念。首先找到所有入度为0的顶点,将它们加入结果序列。然后移除这些顶点的出边,更新其他顶点的入度。重复这个过程,直到所有顶点都被加入结果序列。如果中途无法找到入度为0的顶点,说明图中存在环。

def kahn_topological_sort(graph):  
    in_degree = {vertex: 0 for vertex in graph.vertices}  
      
    for vertex in graph.vertices:  
        for neighbor, _ in graph.get_neighbors(vertex):  
            in_degree[neighbor] += 1  
      
    queue = [v for v, d in in_degree.items() if d == 0]  
    result = []  
      
    while queue:  
        vertex = queue.pop(0)  
        result.append(vertex)  
          
        for neighbor, _ in graph.get_neighbors(vertex):  
            in_degree[neighbor] -= 1  
            if in_degree[neighbor] == 0:  
                queue.append(neighbor)  
      
    if len(result) != len(graph.vertices):  
        raise ValueError("图中存在环,无法进行拓扑排序")  
      
    return result

拓扑排序的时间复杂度为O(V+E)。两种算法各有特点。DFS算法通过深度优先遍历实现,代码简洁。Kahn算法更直观,易于理解,并且可以同时检测图中是否有环。


本章我们学习了图的基本概念、表示方法和遍历算法。图是表达复杂关系的强大工具,社交网络、交通网络、依赖关系等都可以用图来表示。BFS和DFS是两种基本的图遍历算法,各有不同的应用场景。Dijkstra算法解决了最短路径问题,拓扑排序解决了依赖关系问题。下一章我们将学习排序算法,包括冒泡排序、快速排序等多种经典的排序方法。

预览时标签不可点