Breadth-first traversal (BFS) and depth-first traversal (DFS) based on data structure

Breadth-first traversal (BFS)

concept

Breadth First Search (BFS) is a graph traversal algorithm. It starts from a node, first visits the starting node, and then traverses all its directly connected nodes. Then traverse these connected nodes in turn until all the nodes in the graph are visited once.
The main features of breadth-first traversal are:

  • It takes advantage of the first-in-first-out feature of the queue, visiting nodes in the order they are discovered.
  • The traversed nodes will be marked to prevent repeated visits.
  • Visit the starting point first, and then traverse the surrounding nodes in order of distance from the starting point. Those who are closest have priority access.
  • Each time, the nearest node is selected from the unvisited nodes in the current node domain to visit.
  • The algorithm ends when all reachable nodes are visited.
    Implementation steps of breadth-first traversal:
  1. Initialize the queue, visit the starting point and enqueue it.
  2. When the queue is not empty, continue to iterate:
  • Dequeue the head node and visit it.
  • Enqueue all unvisited neighbors of this node.
  • Mark the node as visited.
  1. Repeat step 2 until the queue is empty and the algorithm ends.
    The time complexity of breadth-first traversal is O(V+E), and the space complexity is O(V), where V is the number of nodes in the graph, and E is the number of edges in the graph. Breadth-first traversal is mainly used in problems such as the shortest path.

Take the shortest path as an example

When it comes to finding the shortest path, the most commonly used algorithm is Dijkstra's Algorithm. This algorithm is used to find the shortest path from a starting node to all other nodes in a graph. In the following code example, JavaScript is used to implement Dijkstra's Algorithm.

function dijkstra(graph, start) {
    
    
  const distances = {
    
    };
  for (const node in graph) {
    
    
    distances[node] = Infinity;
  }
  distances[start] = 0;

  const priorityQueue = new PriorityQueue();
  priorityQueue.enqueue(start, 0);

  while (!priorityQueue.isEmpty()) {
    
    
    const {
    
     element: current_node, priority: current_distance } = priorityQueue.dequeue();

    if (current_distance > distances[current_node]) {
    
    
      continue;
    }

    for (const neighbor in graph[current_node]) {
    
    
      const distance = current_distance + graph[current_node][neighbor];
      if (distance < distances[neighbor]) {
    
    
        distances[neighbor] = distance;
        priorityQueue.enqueue(neighbor, distance);
      }
    }
  }

  return distances;
}

// 优先队列实现
class PriorityQueue {
    
    
  constructor() {
    
    
    this.queue = [];
  }

  enqueue(element, priority) {
    
    
    this.queue.push({
    
     element, priority });
    this.sort();
  }

  dequeue() {
    
    
    return this.queue.shift();
  }

  isEmpty() {
    
    
    return this.queue.length === 0;
  }

  sort() {
    
    
    this.queue.sort((a, b) => a.priority - b.priority);
  }
}

// 示例图的邻接字典表示,权重用于表示边的距离
const graph = {
    
    
  'A': {
    
     'B': 1, 'C': 4 },
  'B': {
    
     'D': 3 },
  'C': {
    
     'D': 1 },
  'D': {
    
    }
};

const startNode = 'A';
const shortestDistances = dijkstra(graph, startNode);
console.log(`Shortest distances from node ${
      
      startNode} to all other nodes:`);
console.log(shortestDistances);

In the above code, a priority queue is used to help with the priority of the nodes. The priority queue can ensure that in each iteration, the node with the shortest distance is selected as the next current node, thus ensuring the correctness of Dijkstra's Algorithm.

in this code sample graphrepresents an adjacency dictionary representation of a weighted graph. startNodeis the starting node to find the shortest path from. The algorithm returns an object containing the shortest distances from all nodes to the start node. In the example graph, Astarting from node , the algorithm returns Athe shortest distance from node to all other nodes.

It should be noted that here PriorityQueueis a simplified implementation. In fact, a more efficient priority queue implementation can be used in a production environment, such as Fibonacci Heap or binary heap. At the same time, it is assumed here that there are no negative weight edges in the graph. For graphs containing negatively weighted edges, the Bellman-Ford algorithm needs to be used to find the shortest path.

Depth-first traversal

concept

Depth First Search (DFS) is another graph traversal algorithm. Different from breadth-first traversal, it starts from the starting node, searches a path as deep as possible, until it cannot continue to traverse, and then backtracks to traverse other paths.
The main features of depth-first traversal are:

  • Using the first-in-last-out feature of the stack, the nodes are visited in the order of paths.
  • Traverse along the path until you can't go any further, then go back and try another path.
  • The traversed nodes will be marked to prevent repeated visits.
  • The order of visits to all reachable nodes is undefined.
    Implementation steps of depth-first traversal:
  1. Initialize the stack and push the starting point onto the stack.
  2. When the stack is not empty, repeat the following operations:
  • Pop the top node of the stack and visit it.
  • Push all unvisited neighbors of this node onto the stack.
  • Mark the node as visited.
  1. Repeat step 2 until the stack is empty, and the algorithm ends.
    The time complexity of depth-first traversal is also O(V+E), and the space complexity is O(V). It is mainly used for topological sorting, finding connected components, etc.
    Breadth-first traversal visits nodes hierarchically, while depth-first traversal visits nodes exploratoryly by path. Both have application scenarios.

Take the shortest path as an example

// 图使用邻接表表示
const graph = {
    
    
  'A': ['B','C'],
  'B': ['A','D','E'],
  'C': ['A','F'],
  'D': ['B'],
  'E': ['B','F'],
  'F': ['C','E']
};

// 通过递归实现深度优先遍历
function dfs(curr, end, path, shortest) {
    
    
  path.push(curr); // 加入当前节点到路径
  
  if (curr === end) {
    
     // 到达终点
    shortest = [...path]; // 更新最短路径
  } else {
    
    
    graph[curr].forEach(next => {
    
    
      if (path.indexOf(next) === -1) {
    
     // 节点未访问过
        dfs(next, end, path, shortest); // 递归
        path.pop(); // 回溯
      }
    });
  }
}

// 寻找最短路径的入口函数  
function findShortestPath(start, end) {
    
    
  const path = [];
  let shortest = null;
  
  dfs(start, end, path, shortest);
  
  return shortest;
}

const shortestPath = findShortestPath('A', 'F'); 
console.log(shortestPath); // ['A', 'C', 'F']

Summary of the two algorithms

the difference:

  • Breadth-first traversal (BFS): Start from the starting node and expand layer by layer, traverse the nodes according to the distance from near to far, and visit the nodes of one layer first before continuing to the next layer. It is suitable for problems such as finding the shortest path, connectivity detection, topological sorting, etc.
  • Depth-first traversal (DFS): Start from the starting node and follow a path as deep as possible until it can no longer be expanded, then backtrack and explore other paths. It is suitable for problems such as finding paths, connectivity detection, and graph traversal.

Algorithmic complexity:

  • The time complexity of breadth-first traversal is O(V + E), where V is the number of nodes and E is the number of edges. The space complexity is O(V), because a queue needs to be used to hold the nodes.
  • The time complexity of depth-first traversal is O(V + E), where V is the number of nodes and E is the number of edges. The space complexity is O(V), because the recursive call will use the system stack to save the call information.

Usage scenarios and defects of breadth-first traversal:

  • Usage scenarios: Find the shortest path, connectivity detection, topological sorting and other issues. Breadth-first traversal is often the more intuitive choice when finding the shortest path in a weighted or unweighted graph.
  • Defect: When the graph is large, breadth-first traversal may take up a large memory space, especially when the number of layers traversed is large. In addition, breadth-first traversal is less efficient in finding all paths or a large number of paths.

Usage scenarios and defects of depth-first traversal:

  • Usage scenarios: Finding paths, connectivity detection, graph traversal, etc. Depth-first traversal is usually more appropriate when backtracking and searching problems.
  • Defect: Depth-first traversal may fall into an infinite loop, and additional data structures (such as hash tables) need to be used to record the visited nodes to avoid repeated visits. For disconnected graphs, depth-first traversal may not traverse all nodes. Also, depth-first traversal is not guaranteed to find the shortest path, because its search order may lead to finding a longer path first than the shortest path.

Comprehensive comparison:
Breadth-first traversal and depth-first traversal have their own advantages and applicable scenarios. Breadth-first traversal performs better on problems such as finding the shortest path and connectivity detection, while depth-first traversal is more suitable for backtracking and searching problems. When selecting an algorithm, it is necessary to decide which traversal method to use according to the characteristics of the specific problem. Sometimes, the two traversal methods can also be used in combination. For example, when searching for all paths in a graph, you can first use depth-first traversal to find all possible paths, and then filter out the shortest path.

Guess you like

Origin blog.csdn.net/study_way/article/details/132005182