Revisit the breadth-first search of data structures and algorithms

foreword

Breadth First Search ( BFS for short) is an algorithm for traversing the graph storage structure, which is applicable to both undirected graphs and directed graphs.

The basic idea of ​​the BFS algorithm is to start from a starting vertex, visit its adjacent unvisited vertices in turn, and add them to a queue, then take a vertex from the queue as a new starting vertex, repeat The above process until the queue is empty or the target vertex is found.

The characteristics of the BFS algorithm are:

  • Starting from a vertex, visit its adjacent vertices hierarchically, and then visit the vertices of the next layer until all vertices are traversed.
  • Use the queue to store the vertices to be accessed, and ensure the first-in-first-out order.
  • Able to find the shortest path, suitable for problems such as pathfinding.

The advantages of the bfs algorithm are:

  • It is always guaranteed to find a solution to the problem, or find the optimal solution if there are multiple solutions.
  • It runs fast and does not require backtracking operations.

The disadvantages of the BFS algorithm are:

  • It needs to store the vertices of each layer, which takes up a lot of space and may exceed the memory limit.
  • For deep graphs, it may take a long time.

The BFS algorithm has many application scenarios, such as:

  • Find the shortest path, such as maze wayfinding, map navigation, etc.
  • Determine whether two vertices are connected or belong to the same connected component.
  • Find cycles in a graph or determine whether a graph is bipartite.
  • Search strategies in artificial intelligence, such as breadth-first crawling, robot exploration, etc.
  • Minimum spanning tree.
  • Garbage collection.
  • network flow.

1. Realization

1.1 Core steps and complexity

The core steps of the BFS algorithm are as follows:

  • Starting from a starting node, add it to a queue (queue).
  • Repeat the following until the queue is empty or the target node is found:
    • Pop the first node of the queue, visit it, and mark it as visited.
    • Add all unvisited neighbor nodes of this node to the end of the queue.
  • Returns the visited node or the target node found.

The time complexity of BFS is O(V+E), where V is the number of vertices and E is the number of edges. Each vertex is visited at most once, and each edge is also visited at most once, so the total time complexity is O(V+E).

The space complexity depends on the data structure that stores the access state and vertices to be visited. For an adjacency list representation of a graph, the space complexity is O(V+E), where V is the number of vertices and E is the number of edges. It is necessary to use a collection to record the vertices that have been visited, and a queue to store the vertices to be visited. In the worst case, all vertices will be visited, and all vertices will be enqueued, so the space complexity is O(V+E).

1.2 Pseudo code and java example

The pseudocode is as follows:

# 伪码
BFS(start, target):
  # 创建一个队列
  queue = new Queue()
  # 创建一个集合,用于记录已访问的顶点
  visited = new Set()
  # 将起始顶点加入队列和集合
  queue.enqueue(start)
  visited.add(start)
  # 循环直到队列为空或找到目标顶点
  while queue is not empty:
    # 弹出队列的第一个顶点
    node = queue.dequeue()
    # 访问该顶点
    visit(node)
    # 如果该顶点是目标顶点,返回
    if node == target:
      return node
    # 遍历该顶点的所有未访问的邻居顶点
    for neighbor in node.neighbors:
      # 如果邻居顶点没有被访问过,将其加入队列和集合
      if neighbor not in visited:
        queue.enqueue(neighbor)
        visited.add(neighbor)
  # 如果没有找到目标顶点,返回空
  return null

The java sample code is as follows:

// Java
import java.util.*;

public class BFS {
    
    
  // 定义图的顶点类
  static class Node {
    
    
    int val; // 顶点的值
    List<Node> neighbors; // 顶点的邻居列表

    public Node(int val) {
    
    
      this.val = val;
      this.neighbors = new ArrayList<>();
    }
  }

  // BFS 算法
  public static Node bfs(Node start, Node target) {
    
    
    // 创建一个队列
    Queue<Node> queue = new LinkedList<>();
    // 创建一个集合,用于记录已访问的顶点
    Set<Node> visited = new HashSet<>();
    // 将起始顶点加入队列和集合
    queue.offer(start);
    visited.add(start);
    // 循环直到队列为空或找到目标顶点
    while (!queue.isEmpty()) {
    
    
      // 弹出队列的第一个顶点
      Node node = queue.poll();
      // 访问该顶点
      visit(node);
      // 如果该顶点是目标顶点,返回
      if (node == target) {
    
    
        return node;
      }
      // 遍历该顶点的所有未访问的邻居顶点
      for (Node neighbor : node.neighbors) {
    
    
        // 如果邻居顶点没有被访问过,将其加入队列和集合
        if (!visited.contains(neighbor)) {
    
    
          queue.offer(neighbor);
          visited.add(neighbor);
        }
      }
    }
    // 如果没有找到目标顶点,返回空
    return null;
  }

  // 访问顶点的方法,打印顶点的值
  public static void visit(Node node) {
    
    
    System.out.println(node.val);
  }

  // 测试方法
  public static void main(String[] args) {
    
    
    // 创建一个图
    Node n1 = new Node(1);
    Node n2 = new Node(2);
    Node n3 = new Node(3);
    Node n4 = new Node(4);
    Node n5 = new Node(5);
    Node n6 = new Node(6);
    n1.neighbors.add(n2);
    n1.neighbors.add(n3);
    n2.neighbors.add(n4);
    n3.neighbors.add(n4);
    n3.neighbors.add(n5);
    n4.neighbors.add(n6);
    n5.neighbors.add(n6);
    // 调用 BFS 算法,从顶点 1 开始,寻找顶点 6
    Node result = bfs(n1, n6);
    // 打印结果
    if (result != null) {
    
    
      System.out.println("找到了目标顶点:" + result.val);
    } else {
    
    
      System.out.println("没有找到目标顶点");
    }
  }
}

1.3 Animation Example

BFS

2. Application

2.1 Finding the shortest path

The BFS algorithm can be used to find the shortest path between two vertices in a graph, that is, the path that passes through the least number of edges. This is because the BFS algorithm traverses the graph according to the level, and the vertices of each level are the same distance from the starting vertex, so when the target vertex is found, it is the shortest path.

In order to find the shortest path, we need to make some modifications on the basis of the BFS algorithm:

  • In addition to recording the visited vertices, we also need to record the predecessor vertices of each vertex, that is, from which vertex to reach the vertex.
  • When the target vertex is found, we need to start from the target vertex, follow the linked list of the predecessor vertex, and output the shortest path in reverse order.

The pseudocode is as follows:

# 伪码
BFS(start, target):
  # 创建一个队列
  queue = new Queue()
  # 创建一个集合,用于记录已访问的顶点
  visited = new Set()
  # 创建一个字典,用于记录每个顶点的前驱顶点
  prev = new Map()
  # 将起始顶点加入队列和集合
  queue.enqueue(start)
  visited.add(start)
  # 循环直到队列为空或找到目标顶点
  while queue is not empty:
    # 弹出队列的第一个顶点
    node = queue.dequeue()
    # 访问该顶点
    visit(node)
    # 如果该顶点是目标顶点,返回
    if node == target:
      # 创建一个列表,用于存储最短路径
      path = new List()
      # 从目标顶点开始,沿着前驱顶点的链表,逆序输出最短路径
      while node != null:
        # 将当前顶点加入路径的开头
        path.insert(0, node)
        # 更新当前顶点为其前驱顶点
        node = prev[node]
      # 返回最短路径
      return path
    # 遍历该顶点的所有未访问的邻居顶点
    for neighbor in node.neighbors:
      # 如果邻居顶点没有被访问过,将其加入队列和集合,并记录其前驱顶点
      if neighbor not in visited:
        queue.enqueue(neighbor)
        visited.add(neighbor)
        prev[neighbor] = node
  # 如果没有找到目标顶点,返回空
  return null

2.2 Topological sort

The BFS algorithm can also be used to topologically sort a directed acyclic graph (DAG), that is, to arrange the vertices into a linear sequence according to the dependencies of the vertices, so that for any directed edge (u, v), u is in in front of v.

In order to perform topological sorting, we need to make some modifications on the basis of the BFS algorithm:

  • We need to record the in-degree of each vertex, that is, how many edges point to this vertex.
  • We need to start from the vertex with an in-degree of 0, add it to the queue, then pop the vertex in the queue one by one, add it to the topological sequence, and reduce the in-degree of all its neighbors by one, if the in-degree of the neighbor vertex becomes 0, it is also enqueued.
  • If the number of vertices in the final topological sequence is equal to the number of vertices in the graph, the topological sorting is successful; otherwise, there is a cycle in the graph and the topological sorting cannot be performed.

The pseudocode is as follows:

# 伪码
BFS(graph):
  # 创建一个队列
  queue = new Queue()
  # 创建一个列表,用于存储拓扑序列
  topo = new List()
  # 创建一个字典,用于记录每个顶点的入度
  indegree = new Map()
  # 遍历图中的每个顶点,初始化其入度
  for node in graph.nodes:
    indegree[node] = 0
  # 遍历图中的每条边,更新每个顶点的入度
  for edge in graph.edges:
    indegree[edge.to] += 1
  # 遍历图中的每个顶点,将入度为 0 的顶点加入队列
  for node in graph.nodes:
    if indegree[node] == 0:
      queue.enqueue(node)
  # 循环直到队列为空
  while queue is not empty:
    # 弹出队列的第一个顶点
    node = queue.dequeue()
    # 将该顶点加入拓扑序列
    topo.append(node)
    # 遍历该顶点的所有邻居顶点,将其入度减一,如果入度为 0,将其加入队列
    for neighbor in node.neighbors:
      indegree[neighbor] -= 1
      if indegree[neighbor] == 0:
        queue.enqueue(neighbor)
  # 如果拓扑序列中的顶点数等于图中的顶点数,返回拓扑序列,否则返回空
  if len(topo) == len(graph.nodes):
    return topo
  else:
    return null

2.3 Minimum spanning tree

The BFS algorithm can also be used to solve the minimum spanning tree (MST) of an undirected connected graph, that is, an acyclic subgraph containing all vertices in the graph such that the sum of the weights of its edges is minimized.

In order to solve the minimum spanning tree, we need to make some modifications on the basis of the BFS algorithm:

  • We need to assign a weight to each edge, indicating the length or cost of the edge.
  • We need to start from any vertex, add it to the queue, then pop up the vertices in the queue one by one, add it to the vertex set of the minimum spanning tree, and add the edges of all its neighbor vertices to a priority queue, according to the weight of the edge. to the big sort.
  • We need to loop until the priority queue is empty or the vertex set of the minimum spanning tree is equal to the vertex set in the graph, and each time the smallest edge is taken from the priority queue, if the two vertices of the edge are already in the vertex set of the minimum spanning tree , indicating that this edge will form a ring, skip this edge, otherwise add this edge to the edge set of the minimum spanning tree, and add another vertex of this edge to the queue.
  • If the vertex set of the final minimum spanning tree is equal to the vertex set in the graph, it means that the minimum spanning tree is successfully constructed; otherwise, the graph is disconnected and the minimum spanning tree cannot be constructed.

The pseudocode is as follows:

# 伪码
BFS(graph):
  # 创建一个队列
  queue = new Queue()
  # 创建一个优先队列,用于存储边,按照权值从小到大排序
  pq = new PriorityQueue()
  # 创建一个集合,用于存储最小生成树的顶点
  mst_nodes = new Set()
  # 创建一个列表,用于存储最小生成树的边
  mst_edges = new List()
  # 从图中的任意一个顶点开始,将其加入队列和最小生成树的顶点集合
  start = graph.nodes[0]
  queue.enqueue(start)
  mst_nodes.add(start)
  # 循环直到队列为空或者最小生成树的顶点集合等于图中的顶点集合
  while queue is not empty and len(mst_nodes) < len(graph.nodes):
    # 弹出队列的第一个顶点
    node = queue.dequeue()
    # 遍历该顶点的所有邻居顶点,将其边加入优先队列
    for neighbor in node.neighbors:
      edge = get_edge(node, neighbor) # 获取两个顶点之间的边
      pq.enqueue(edge)
    # 循环直到优先队列为空或者找到一条合适的边
    while pq is not empty:
      # 从优先队列中取出最小的边
      edge = pq.dequeue()
      # 如果该边的两个顶点都已经在最小生成树的顶点集合中,跳过该边
      if edge.from in mst_nodes and edge.to in mst_nodes:
        continue
      # 否则将该边加入最小生成树的边集合
      mst_edges.append(edge)
      # 并将该边的另一个顶点加入队列和最小生成树的顶点集合
      if edge.from in mst_nodes:
        queue.enqueue(edge.to)
        mst_nodes.add(edge.to)
      else:
        queue.enqueue(edge.from)
        mst_nodes.add(edge.from)
      # 跳出循环
      break
  # 如果最后最小生成树的顶点集合等于图中的顶点集合,返回最小生成树的边集合,否则返回空
  if len(mst_nodes) == len(graph.nodes): 
      return mst_edges 
  else: 
      return null

3. LeetCode actual combat

3.1 Level order traversal of binary tree

102. Level order traversal of binary tree

Given the root node of your binary tree , return a level-order traversalroot of its node values . (ie layer by layer, visit all nodes from left to right).

public List<List<Integer>> levelOrder(TreeNode root) {
    
    
    List<List<Integer>> ans = new ArrayList<>();
    if (root == null) return ans;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root); // 将根节点入队
    queue.offer(null); // 将空元素入队作为分隔符
    List<Integer> tmp = new ArrayList<>(); // 存储每一层的节点值
    while (!queue.isEmpty()) {
    
    
        TreeNode node = queue.poll(); // 出队一个节点
        if (node == null) {
    
     // 如果节点是空元素,说明一层结束了
            ans.add(tmp); // 将当前层的列表添加到结果中
            tmp = new ArrayList<>(); // 重置当前层的列表
            if (!queue.isEmpty()) queue.offer(null); // 如果队列不为空,再次添加一个空元素作为分隔符
        } else {
    
     // 如果节点不是空元素,说明还在当前层
            tmp.add(node.val); // 将节点值添加到当前层的列表中
            if (node.left != null) queue.offer(node.left); // 将左子节点入队
            if (node.right != null) queue.offer(node.right); // 将右子节点入队
        }
    }
    return ans;
}

3.2 Find the value in the lower left corner of the tree

513. Find the value in the lower left corner of the tree

Given the root node root of a binary tree , find the value of the bottommost leftmost node of the binary tree.

Assume there is at least one node in the binary tree.

public int findBottomLeftValue(TreeNode root) {
    
    
    int leftmost = root.val; // 初始化最左边的节点值为根节点值
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root); // 将根节点入队
    while (!queue.isEmpty()) {
    
    
        int size = queue.size(); // 记录当前层的节点个数
        for (int i = 0; i < size; i++) {
    
    
            TreeNode node = queue.poll(); // 出队一个节点
            if (i == 0) leftmost = node.val; // 如果是当前层的第一个节点,更新最左边的节点值
            if (node.left != null) queue.offer(node.left); // 将左子节点入队
            if (node.right != null) queue.offer(node.right); // 将右子节点入队
        }
    }
    return leftmost; // 返回最左边的节点值
}

3.3 Word Solitaire

127. Word Solitaire

A conversion sequencewordList from words and in dictionary beginWordis a sequence formed according to the following specification :endWordbeginWord -> s1 -> s2 -> ... -> sk

  • Each pair of adjacent words differs by only one letter.
  • For 1 <= i <= k, each siis wordListin . Note that beginWordit does not need to be wordListin .
  • sk == endWord

Given you two words beginWordand endWordand a dictionary wordList, return the number of words in the shortest transition sequence from beginWordtoendWord . Returns if no such conversion sequence exists 0.

Each word in the word set is a node, and only one word with a different position letter can be connected. Finding the shortest path length between the starting point and the ending point can be equivalent to finding the shortest path in an undirected graph. Wide search is the most suitable, as long as you find it end point, then it must be the shortest path . Because wide search is a search that spreads from the center of the starting point to the surroundings. There are two more points to note:

  1. Undirected graphs need to use marker bits to mark whether the nodes have passed through, otherwise there will be an endless loop.
  2. The collection is an array type, which can be converted into a Set structure, and the search is faster.
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    
    
    int ans = 0;
    Set<String> words = new HashSet<>(wordList); // 单词列表转为Set

    if (!words.contains(endWord)) {
    
    
        return 0; // 如果目标单词不在单词集合中,直接返回0
    }
    Set<String> nextLevel = new HashSet<>();
    nextLevel.add(beginWord); // 将起始单词加入到第一个队列中
    ans++; // 初始化路径长度为1
    while (!nextLevel.isEmpty()) {
    
     // 使用一个循环,直到队列为空
        ans++; // 路径长度加一
        Set<String> tmpSet = new HashSet<>(); // 创建一个临时队列,用于存储下一层的搜索路径
        for (String s: nextLevel) {
    
     // 遍历第一个队列中的每个单词
            char [] chs = s.toCharArray(); // 将单词转换为字符数组
            for (int i = 0; i < chs.length; i++) {
    
     // 遍历每个字符的位置
                char old = chs[i]; // 保存原来的字符,避免重复创建字符数组
                for (char ch = 'a'; ch <= 'z'; ch++) {
    
     // 遍历每个可能的字符
                    chs[i] = ch; // 直接修改字符数组,避免创建新的字符串
                    String tmp = new String(chs); // 将字符数组转换为新的单词
                    if (nextLevel.contains(tmp)) {
    
    
                        continue; // 如果第一个队列中包含了新的单词,跳过
                    }
                    if (words.contains(tmp)) {
    
     // 如果单词集合中包含了新的单词
                        tmpSet.add(tmp); // 将它加入到临时队列中
                        words.remove(tmp); // 从单词集合中移除已经访问过的单词,避免重复访问
                    }
                    if (tmp.equals(endWord)) {
    
     // 如果新的单词等于目标单词,说明找到了一个最短路径,返回路径长度
                        return ans;
                    }
                }
                chs[i] = old; // 恢复原来的字符
            }

        }
        nextLevel = tmpSet; // 将临时队列赋值给第一个队列,作为下一层的搜索路径
    }

    return 0; // 如果循环结束,说明没有找到路径,返回0
}

reference

  1. https://leetcode.cn/tag/breadth-first-search/problemset/

Guess you like

Origin blog.csdn.net/qq_23091073/article/details/129521662