数据结构基础之广度优先遍历(BFS),深度优先遍历(DFS)

文章目录

  • 广度优先遍历(BFS)
    • 概念
    • 以最短路径为例子
  • 深度优先遍历
    • 概念
    • 以最短路径为例子
  • 两种算法的总结

广度优先遍历(BFS)

概念

广度优先遍历(Breadth First Search, BFS)是一种图的遍历算法。它从一个节点开始,先访问起始节点,然后遍历它的所有直接相连的节点。然后对这些相连节点依次进行遍历,直到图中的所有节点都被访问一次。
广度优先遍历的主要特点是:

  • 它利用了队列的先进先出特点, visiting nodes in the order they are discovered.
  • 遍历过的节点会被标记,防止重复访问。
  • 先访问起点,然后按照距离起点的远近顺序遍历周边节点。近者优先访问。
  • 每次都从当前节点领域未被访问过的节点中选取距离最近的节点访问。
  • 直到所有可达节点访问完毕,算法结束。
    广度优先遍历的实现步骤:
  1. 初始化队列,访问起点并入队。
  2. 当队列不为空时,继续迭代:
  • 出队列头节点并访问。
  • 将该节点的所有未访问相邻节点入队。
  • 标记该节点为已访问。
  1. 重复步骤2直到队列为空,算法结束。
    广度优先遍历时间复杂度为 O(V+E),空间复杂度为 O(V),其中 V 为图的节点数,E 为图的边数。广度优先遍历主要应用在最短路径等问题中。

以最短路径为例子

当涉及到查找最短路径时,最常用的算法是 Dijkstra’s Algorithm(迪克斯特拉算法)。这个算法用于查找图中从起始节点到其他所有节点的最短路径。在下面的代码示例中,将使用 JavaScript 来实现 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);

在上面的代码中,使用了一个优先队列来帮助处理节点的优先级。优先队列可以确保在每次迭代中,都选择最短距离的节点作为下一个当前节点,从而确保 Dijkstra’s Algorithm 的正确性。

这个代码示例中的 graph 表示一个带权重的图的邻接字典表示。startNode 是要查找最短路径的起始节点。算法会返回一个包含所有节点到起始节点的最短距离的对象。在示例图中,从节点 A 出发,算法会返回节点 A 到其他所有节点的最短距离。

需要注意,这里的 PriorityQueue 是一个简化的实现,实际上在生产环境中可以使用更高效的优先队列实现,例如 Fibonacci Heap 或二叉堆。同时,这里假设图中没有负权边。对于包含负权边的图,需要使用 Bellman-Ford 算法来查找最短路径。

深度优先遍历

概念

深度优先遍历(Depth First Search, DFS)是另一种图的遍历算法。与广度优先遍历不同,它从起始节点出发,尽可能深的搜索某一条路径,直到无法继续遍历为止,然后再回溯进行其他路径的遍历。
深度优先遍历的主要特点是:

  • 利用栈的先进后出特点,按路径顺序访问节点。
  • 沿着路径遍历到不能继续走为止,再回退并试其他路径。
  • 遍历过的节点会做标记,防止重复访问。
  • 对所有可达节点的访问顺序不确定。
    深度优先遍历的实现步骤:
  1. 初始化栈,将起点入栈。
  2. 当栈不为空时,重复以下操作:
  • 出栈顶节点并访问。
  • 将该节点的所有未访问过的相邻节点入栈。
  • 标记该节点为已访问。
  1. 重复步骤2直到栈为空,算法结束。
    深度优先遍历的时间复杂度也为 O(V+E),空间复杂度为 O(V)。它主要用于拓扑排序、寻找连通分量等。
    广度优先遍历按层次展开地访问节点,而深度优先遍历按路径探索地访问节点。两者各有应用场景。

以最短路径为例子

// 图使用邻接表表示
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']

两种算法的总结

区别:

  • 广度优先遍历(BFS):从起始节点开始逐层扩展,按照距离从近到远遍历节点,先访问完一层节点再继续下一层。适合用于查找最短路径、连通性检测、拓扑排序等问题。
  • 深度优先遍历(DFS):从起始节点开始沿着一条路径尽可能深入,直到无法继续扩展,然后回溯并探索其他路径。适合用于查找路径、连通性检测、图的遍历等问题。

算法复杂度:

  • 广度优先遍历的时间复杂度为 O(V + E),其中 V 是节点数,E 是边数。空间复杂度为 O(V),因为需要使用队列来保存节点。
  • 深度优先遍历的时间复杂度为 O(V + E),其中 V 是节点数,E 是边数。空间复杂度为 O(V),因为递归调用会使用系统栈来保存调用信息。

广度优先遍历的使用场景和缺陷:

  • 使用场景:查找最短路径、连通性检测、拓扑排序等问题。在带权重或无权重的图中查找最短路径时,广度优先遍历通常是比较直观的选择。
  • 缺陷:当图较大时,广度优先遍历可能需要占用较大的内存空间,特别是在遍历的层数较多的情况下。另外,广度优先遍历在查找所有路径或路径数量较多的情况下效率较低。

深度优先遍历的使用场景和缺陷:

  • 使用场景:查找路径、连通性检测、图的遍历等问题。在回溯和搜索问题时,深度优先遍历通常更为合适。
  • 缺陷:深度优先遍历有可能陷入无限循环,需要使用额外的数据结构(例如哈希表)来记录已访问的节点,避免重复访问。对于非连通图,深度优先遍历可能不会遍历到所有的节点。此外,深度优先遍历不保证找到最短路径,因为它的搜索顺序可能会导致先找到一个较长的路径而非最短路径。

综合比较:
广度优先遍历和深度优先遍历都有各自的优势和适用场景。广度优先遍历在查找最短路径、连通性检测等问题上表现较好,而深度优先遍历在回溯和搜索问题上更加合适。在选择算法时,需要根据具体问题的特点来决定使用哪种遍历方法。有时,两种遍历方法也可以结合使用,例如在查找图中的所有路径时,可以先用深度优先遍历找到所有可能的路径,然后再筛选出最短路径。

你可能感兴趣的:(数据结构,数据结构,深度优先,广度优先,最短路径,Javascript)