JavaScript 数据结构与算法——图的遍历

414 阅读3分钟

JavaScript 数据结构与算法——图的遍历

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 15 天,点击查看活动详情

介绍

和树的遍历类似,图的遍历也是遍历图当中的每个节点,但是遍历的方式不同,树有三种遍历方式:前序遍历、中序遍历、后序遍历,而图也有三种遍历方式:深度优先遍历、广度优先遍历、拓扑遍历。本篇文章我们重点来了解图当中广度优先遍历和深度优先遍历的实现方式。

图遍历的算法思想是:必须追踪每一个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于探索完全的节点,下次遇到我们应该忽略掉。因此,我们可以发现,一个节点有三种状态:未访问已访问已探索。为了保证算法的效率,一个节点最多被访问两次,连通图当中的每条边和每个顶点都会被访问到。

广度优先遍历和深度优先遍历算法思路上基本一样,但是用来存储待访问节点的数据结构不一样,广度优先遍历使用队列来保存待访问的节点,而深度优先搜索使用栈来保存待访问的节点。

当要标注已经访问过的顶点时,我们使用三种颜色来表示节点的三种状态:

  • 白色 未访问
  • 灰色 已访问但是未探索
  • 黑色 已探索
/**
 * 节点的颜色集合
 */

const NodeColors = {
  WHITE: 0, //白色 未被访问
  GREY: 1, // 灰色,已经被访问,但是没有被探索完
  BLACK: 2, // 黑色 已经被访问了,并且被探索完
};

此外,我们还需要一个函数来初始化节点列表

/**
 * 初始化节点颜色
 */

function initNodeColor<T>(nodeList: string[]): { [propName: string]: number } {
  const color: { [propName: string]: number } = {}; // 存放所以节点颜色的集合

  for (let i = 0; i < nodeList.length; i++) {
    color[nodeList[i]] = NodeColors.WHITE;
  }

  return color;
}

广度优先遍历

/**
 *
 * @param graph 需要遍历的图
 * @param startVertex 开始的顶点
 * @param callback 遍历节点执行的回调函数
 */
const breadthFirstSearch = (
  graph: Graph<string>,
  startVertex: string,
  callback: (node: string) => void
) => {
  //将图当中的所有节点先初始化颜色
  const nodeColorList = initNodeColor(graph.vertices);
  //节点列表
  const vertices = graph.getVertices();
  // 邻接表
  const adjList = graph.getAdjList();

  //定义一个存储的队列
  const queue = [];

  //将第一个顶点存入队列
  queue.push(startVertex);

  //通过while循环来遍历队列
  while (queue.length !== 0) {
    //出队列,探索这个节点
    const node = queue.shift();

    //获取当前节点的所有相邻节点
    const neighbors = adjList.get(node);

    //表示当前节点已经被访问,但是还没有被探索
    nodeColorList[node] = NodeColors.GREY;

    //探索当前节点
    for (let i = 0; i < neighbors.length; i++) {
      const nearNode = neighbors[i];
      //只有当前被探索到的节点值为白色,才会将他添加到队列当中
      if (nodeColorList[nearNode] === NodeColors.WHITE) {
        //将探索到的节点存入队列
        queue.push(nearNode);

        //将添加队列的节点颜色变成灰色,表示他们已经被访问,但是还没有探索
        nodeColorList[nearNode] = NodeColors.GREY;
      }
    }

    //当前节点探索完毕,将他标记为黑色,已经被探索完毕的状态
    nodeColorList[node] = NodeColors.BLACK;

    //节点探索完毕执行回调函数
    if (callback) {
      callback(node);
    }
  }
};

//测试
breadthFirstSearch(graph, "A", (node: string) => {
  console.log(node); // A B C D E F G H I
});

本代码当中使用的graph在上篇文章当中以及实现了,所以这里就不把代码贴出来的,感兴趣的可以去上篇文章复制下来。

深度优先遍历

/**
 * 深度优先遍历
 * @param graph 需要遍历的图
 * @param callback 遍历节点是执行的回调函数
 */
const depthFirstSearch = (
  graph: Graph<string>,
  callback: (node: string) => void
) => {
  //先获取图当中的节点列表
  const vertices = graph.getVertices();

  //获取图当中的邻接表
  const adjList = graph.getAdjList();

  //将节点列表的状态进行初始化
  const nodeColorList = initNodeColor(vertices);

  //对节点列表当中节点进行遍历
  for (let i = 0; i < vertices.length; i++) {
    //假如节点当前的颜色是白色,那么节点处于未被访问的状态,我们要对它进行访问
    if (nodeColorList[vertices[i]] === NodeColors.WHITE) {
      depthFirstSearchList(
        vertices[i],
        nodeColorList,
        vertices,
        adjList,
        callback
      );
    }
  }
};
/**
 *
 * @param node 需要深度遍历的节点
 * @param nodeColorList 节点颜色列表
 * @param vertices 图当中的节点列表
 * @param adjList 图的邻接表
 * @param callback 访问后的回调函数
 */
const depthFirstSearchList = (
  node: string,
  nodeColorList: { [propName: string]: number },
  vertices: string[],
  adjList: Map<string, string[]>,
  callback: (node: string) => void
) => {
  //探索当前节点,将节点状态变成访问状态
  nodeColorList[node] = NodeColors.GREY;
  if (callback) {
    callback(node);
  }

  //通过邻接表来获取当前节点的相邻节点
  const neighbors = adjList.get(node);

  //遍历邻接表
  for (let i = 0; i < neighbors.length; i++) {
    if (nodeColorList[i] === NodeColors.WHITE) {
      depthFirstSearchList(
        neighbors[i],
        nodeColorList,
        vertices,
        adjList,
        callback
      );
    }
  }

  //将当前节点的状态变更为已访问状态
  nodeColorList[node] = NodeColors.BLACK;
};

depthFirstSearch(graph, (node: string) => {
  console.log(node);
});

本代码当中使用的graph在上篇文章当中以及实现了,所以这里就不把代码贴出来的,感兴趣的可以去上篇文章复制下来。