【算法】算法学习四:图 | 广度优先搜索 | 深度优先搜索

文章目录

  • 一、什么是图?
  • 二、广度优先搜索
  • 三、什么是队列
  • 四、广度优先搜索的实现
    • 4.1 实现全部的代码
    • 4.2 队列的实现
  • 五、深度优先搜索
  • 六、图的运行时间
    • 6.1 广度优先搜索
    • 6.2 深度优先搜索

一、什么是图?

在计算机科学中,图(Graph)是由节点(Vertex)和连接节点的边(Edge)组成的一种数据结构。图是一种非常常见且广泛应用的数据结构,用于表示物理、社交、网络等各种关系和连接。

图可以用来描述各种实际问题,如社交网络中的用户关系、电子网络中的设备连接、道路交通网络中的路线等。在图中,节点表示实体,边表示节点之间的关系或连接。

图可以分为有向图(Directed Graph)和无向图(Undirected Graph)两种类型:

  1. 有向图:在有向图中,边是有方向性的,从一个节点指向另一个节点。例如,社交网络中的关注关系可以表示为有向图,其中节点表示用户,有向边表示关注关系。
  2. 无向图:在无向图中,边没有方向性,可以在两个节点之间双向连接。例如,电子网络中的设备连接可以表示为无向图,其中节点表示设备,无向边表示设备之间的连接。

图还可以具有权重(Weighted Graph)的概念,权重表示节点之间的关联强度或边的代价。例如,道路交通网络中的路段可以使用权重表示距离或时间。

图的常见术语包括:

  • 节点(Vertex):图中的一个元素,也被称为顶点。节点可以表示实体、对象或特定概念。
  • 边(Edge):连接图中两个节点的线段,表示节点之间的关系或连接。
  • 相邻节点(Adjacent Vertex):对于一个节点,与它直接连接的节点称为相邻节点。
  • 路径(Path):由一系列边连接的节点序列,表示从一个节点到另一个节点的通路。
  • 环(Cycle):至少包含一条边的路径,其起点和终点相同。
  • 连通图(Connected Graph):在无向图中,如果从一个节点到另一个节点存在一条路径,那么这个图被称为连通图。
  • 强连通图(Strongly Connected Graph):在有向图中,如果从任意一个节点到达其他任意节点都存在路径,那么这个图被称为强连通图。

图的表示方法有多种,包括邻接矩阵(Adjacency Matrix)、邻接表(Adjacency List)等。

图论是研究图及其性质、算法和应用的数学分支,广泛应用于计算机科学、网络分析、优化问题等领域。图的算法包括图的遍历、最短路径、最小生成树、网络流等,这些算法在解决实际问题中起着重要的作用。

二、广度优先搜索

广度优先搜索(BFS,Breadth-First Search)是一种用于图形和树结构的遍历算法。它从根节点开始,逐层扩展搜索,先访问根节点的所有邻居节点,然后依次访问它们的邻居节点,以此类推,直到遍历完整个图形或树。

BFS使用队列数据结构来保存待访问的节点。具体的算法步骤如下:

  1. 创建一个空队列,并将根节点入队。
  2. 如果队列不为空,则执行以下步骤:

(1)出队一个节点,并访问该节点。

(2)将该节点的所有未访问过的邻居节点入队。

  1. 重复步骤2,直到队列为空。

BFS的特点是按照层级逐层扩展搜索,因此可以用来解决一些问题,例如:

  1. 针对无权图的最短路径问题:在无权图中,BFS可以找到从起点到目标节点的最短路径。
  2. 判断图是否连通:通过BFS可以检测图是否连通,即是否存在从一个节点到达其他所有节点的路径。
  3. 生成迷宫的最短路径:将迷宫抽象成图,利用BFS可以找到从起点到终点的最短路径。
  4. 搜索状态空间:在某些问题中,状态可以表示为图的形式,BFS可以用于搜索状态空间,找到目标状态。

需要注意的是,BFS对于大规模图形可能会消耗较多的内存,因为需要保存所有已经访问过的节点。在实际应用中,可以根据具体情况选择合适的搜索算法。

三、什么是队列

队列(Queue)是一种常见的数据结构,遵循先进先出(FIFO,First-In-First-Out)的原则。队列可以看作是一种线性的、有限的序列,其中数据项按照添加的顺序排列,并且从队列的一端(称为队尾)添加数据项,从另一端(称为队首)移除数据项。

队列具有两个基本操作:

  1. 入队(enqueue):将数据项添加到队尾。
  2. 出队(dequeue):从队首移除并返回数据项。

除了这两个基本操作,队列还可以支持其他常用的操作,如获取队首元素、判断队列是否为空以及获取队列的大小等。

队列的应用非常广泛,特别适用于需要按照顺序处理数据的场景,例如:

  1. 广度优先搜索(BFS):在图或树的遍历中,利用队列可以按层级扩展搜索。
  2. 任务调度:多个任务按照顺序加入队列,按照先后顺序依次执行。
  3. 缓冲区管理:用于处理输入输出请求的缓冲区,保持请求的顺序。
  4. 消息传递:在并发编程中,消息队列用于实现不同线程或进程之间的通信。

队列可以使用不同的数据结构来实现,常见的实现方式有两种:

  1. 数组实现:使用数组作为底层数据结构,通过维护队首和队尾的索引来完成入队和出队操作。数组实现的队列具有固定的容量,当队列已满时无法添加新的数据项。
  2. 链表实现:使用链表作为底层数据结构,每个节点包含数据项以及指向下一个节点的引用。链表实现的队列可以动态地添加和移除节点,没有固定的容量限制。

队列的选择取决于具体的需求和场景。如果需要快速访问队首和队尾元素,而不需要频繁地插入和删除中间元素,数组实现可能更高效。如果需要频繁地插入和删除元素,并且不确定队列的最大容量,链表实现可能更适合。

总而言之,队列是一种简单而实用的数据结构,可以在很多应用中提供有序、按序处理数据的能力。

四、广度优先搜索的实现

4.1 实现全部的代码

下面是一个使用Python编写的广度优先搜索算法的示例代码,用于在无向图中找到从给定起点到目标节点的最短路径:

from collections import deque

def bfs(graph, start, target):
    queue = deque()  # 创建一个空队列
    visited = set()  # 记录已访问过的节点
    queue.append((start, [start]))  # 初始节点入队

    while queue:
        node, path = queue.popleft()  # 出队一个节点
        visited.add(node)  # 将节点标记为已访问

        if node == target:
            return path  # 找到目标节点,返回路径

        neighbors = graph[node]  # 获取当前节点的邻居节点
        for neighbor in neighbors:
            if neighbor not in visited:
                queue.append((neighbor, path + [neighbor]))  # 邻居节点入队,将路径更新

    return None  # 没有找到路径

# 测试代码
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

start_node = 'A'
target_node = 'F'
shortest_path = bfs(graph, start_node, target_node)
if shortest_path:
    print(f"Shortest path from {start_node} to {target_node}: {' -> '.join(shortest_path)}")
else:
    print(f"There is no path from {start_node} to {target_node}.")

输出结果为:

Shortest path from A to F: A -> C -> F

在这个示例代码中,我们使用了一个字典来表示图形,其中每个节点都与其邻居节点列表相关联。bfs函数接受图形、起点和目标节点作为输入,并返回起点到目标节点的最短路径(如果存在)。

在测试代码部分,我们定义了一个图形,并指定起点和目标节点。然后调用bfs函数来找到最短路径,并将结果打印出来。

请注意,这只是一个简单的示例,实际应用中可能需要根据具体情况对代码进行适当修改和扩展。

4.2 队列的实现

deque(全称为"double-ended queue",双端队列)是Python标准库collections模块中的一个类,它提供了一个高效的数据结构,用于在两端进行添加和删除操作。

deque类是基于双向链表实现的,因此在添加和删除元素时具有较高的性能,时间复杂度为O(1)。它提供了一系列方法来操作双端队列,包括在队尾和队首添加或删除元素、获取队首和队尾的元素等。

deque类的常用方法包括:

  1. append(x):在队尾添加元素x。
  2. appendleft(x):在队首添加元素x。
  3. pop():移除并返回队尾的元素。
  4. popleft():移除并返回队首的元素。
  5. extend(iterable):在队尾批量添加可迭代对象中的元素。
  6. extendleft(iterable):在队首批量添加可迭代对象中的元素。
  7. clear():移除队列中的所有元素。
  8. count(x):统计队列中等于x的元素个数。
  9. remove(value):移除队列中第一次出现的值为value的元素。
  10. rotate(n=1):将队列向右循环移动n步(如果n为负数,则向左移动)。

使用deque的一个常见应用场景是实现队列或栈数据结构。通过将deque视为队列,我们可以使用append和popleft操作来实现FIFO(先进先出)的队列。通过将deque视为栈,我们可以使用append和pop操作来实现LIFO(后进先出)的栈。

下面是一个使用deque实现队列的简单示例代码:

from collections import deque

queue = deque()  # 创建一个空队列

# 入队
queue.append(1)
queue.append(2)
queue.append(3)

# 出队
while queue:
    item = queue.popleft()
    print("Dequeued:", item)

输出结果为:

Dequeued: 1
Dequeued: 2
Dequeued: 3

在这个示例中,我们使用deque创建了一个空队列,并使用append方法将元素1、2和3依次添加到队尾。然后使用popleft方法循环遍历队列并打印出出队的元素,实现了FIFO的队列行为。

通过使用deque,我们可以方便地实现双端队列的功能,并且具有高效的性能。在需要进行频繁的队首和队尾操作时,deque是一个很好的选择。

五、深度优先搜索

深度优先搜索(DFS,Depth-First Search)是一种用于图形和树结构的遍历算法。它从根节点开始,尽可能深地访问图的分支,直到到达最深的节点,然后回溯到上一层节点,再继续深入未访问过的分支。

DFS使用栈(或递归调用栈)来保存待访问的节点。具体的算法步骤如下:

  1. 创建一个空栈,并将根节点入栈。
  2. 如果栈不为空,则执行以下步骤:

(1)出栈一个节点,并访问该节点。

(2)将该节点的所有未访问过的邻居节点入栈。

  1. 重复步骤2,直到栈为空。

DFS的特点是尽可能深入地搜索,直到到达最深的节点或无法继续前进为止。它常用于解决以下问题:

  1. 图的连通性:通过DFS可以检测图是否连通,即是否存在从一个节点到达其他所有节点的路径。
  2. 拓扑排序:DFS可以用于对有向无环图进行拓扑排序,得到一种节点的线性排序。
  3. 图的遍历:DFS可以遍历图中的所有节点,将它们按照某种顺序进行访问。
  4. 状态空间搜索:在某些问题中,状态可以表示为图的形式,DFS可以用于搜索状态空间,找到目标状态。

需要注意的是,DFS可能会进入无限循环,因为它没有像BFS那样限制搜索的层数。在实际应用中,可能需要进行合适的剪枝操作或设置最大搜索深度。

以下是一个使用递归实现的DFS的示例代码,用于在无向图中遍历所有节点:

def dfs(graph, node, visited):
    visited.add(node)  # 将节点标记为已访问
    print("Visited node:", node)

    neighbors = graph[node]  # 获取当前节点的邻居节点
    for neighbor in neighbors:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)  # 递归调用DFS

# 测试代码
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

visited = set()  # 记录已访问过的节点
dfs(graph, 'A', visited)

输出结果为:

Visited node: A
Visited node: B
Visited node: D
Visited node: E
Visited node: F
Visited node: C

在这个示例代码中,我们使用递归方式实现了DFS。我们传入一个图形、起始节点和一个集合visited,用于记录已经访问过的节点。在每次递归调用时,我们将当前节点标记为已访问,并打印出访问的节点。然后,递归地访问当前节点的邻居节点,如果邻居节点未被访问过,则进行递归调用。

以上是深度优先搜索的基本原理和一个示例代码。DFS在图和树的遍历、路径搜索和状态空间搜索等问题中具有重要的应用。

六、图的运行时间

6.1 广度优先搜索

广度优先搜索(BFS,Breadth-First Search)的运行时间取决于图的规模和结构。

在最坏情况下,当搜索遍历整个图时,BFS需要访问图中的所有节点和边。假设图有 V 个节点和 E 条边,则时间复杂度为 O(V + E)。

具体来说,在BFS中,每个节点最多被访问一次,每条边最多被访问两次(一次作为起点节点的邻居,一次作为终点节点的邻居)。因此,对于一个连通图(每个节点都可以从起始节点达到),时间复杂度可以表示为 O(V + E)。

对于非连通图,BFS需要对每个连通分量都进行一次搜索。因此,如果图由 k 个连通分量组成,则时间复杂度为 O(k(V + E))。

需要注意的是,时间复杂度中的常数因子和系数是难以准确估计的,因为它们取决于具体的实现和环境。实际运行时间可能受到计算机的性能、图的规模、图的密度等因素的影响。

总结起来,广度优先搜索的运行时间是与节点数和边数成正比的,时间复杂度为 O(V + E),其中 V 表示节点数,E 表示边数。

6.2 深度优先搜索

深度优先搜索(DFS,Depth-First Search)的运行时间取决于图的规模和结构。

在最坏情况下,当搜索遍历整个图时,DFS需要访问图中的所有节点和边。假设图有 V 个节点和 E 条边,则时间复杂度为 O(V + E)。

具体来说,在DFS中,每个节点最多被访问一次,每条边最多被访问两次(一次作为起点节点的边,一次作为终点节点的边)。因此,对于一个连通图(每个节点都可以从起始节点达到),时间复杂度可以表示为 O(V + E)。

对于非连通图,DFS需要对每个连通分量都进行一次搜索。因此,如果图由 k 个连通分量组成,则时间复杂度为 O(k(V + E))。

需要注意的是,时间复杂度中的常数因子和系数是难以准确估计的,因为它们取决于具体的实现和环境。实际运行时间可能受到计算机的性能、图的规模、图的密度等因素的影响。

总结起来,深度优先搜索的运行时间是与节点数和边数成正比的,时间复杂度为 O(V + E),其中 V 表示节点数,E 表示边数。

你可能感兴趣的:(算法类,算法,学习,深度优先,广度优先)