JavaScript 数据结构和算法——寻找图的最短路径

622 阅读3分钟

JavaScript 数据结构和算法——寻找图的最短路径

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

介绍

上一篇文章当中,我们实现了图的深度遍历和广度遍历,这里我们来实现一下寻找图的最短路径算法。图的最短路径算法就是寻找起点到图当中各个顶点的最短路径。在实际应用当中,图的最短路径算法长用于在出现时寻找最短的路径。

思想

之前我们使用 BFS 来实现图的广度遍历,我们可以根据图的广度遍历来实现最短路径算法。给定一个图 G 的源顶点 v,找出每个顶点 u 和 v 之间最短路径的距离(以边的数量计算)。

广度遍历是先遍历给定节点的相邻节点,然后再遍历相邻节点的相邻节点,我们可以定义一个对象distances来保存当前节点到源节点之间的距离,同时我们还需要定义一个predecessors来保存当前节点的前溯。这样我们就拿到了当前节点到图中每一个节点的距离。

之后,我们再计算当前节点到图中每个节点的最短路径。

实现

class Graph<T> {
  // 节点列表
  public vertices: T[];
  // 邻接表
  public adjList: Map<T, T[]>;
  // 是否是有向图
  public isDirected: boolean;

  constructor(isDirected = false) {
    this.isDirected = isDirected;

    //初始化邻接表
    this.adjList = new Map<T, T[]>();
    // 初始化节点列表
    this.vertices = [];
  }

  /**
   * 向图当中添加节点
   * @param v 添加的节点
   */
  addVertex(v: T) {
    //先判断当前图当中是否包含节点
    if (!this.vertices.includes(v)) {
      this.vertices.push(v);
      this.adjList.set(v, []);
    }
  }

  /**
   *
   * @param v 顶点v
   * @param w 顶点w
   */
  addEdge(v: T, w: T) {
    //如果图当中不存在v节点
    if (!this.vertices.includes(v)) {
      //将v节点添加到图当中
      this.addVertex(v);
    }

    //如果图当中不存在W节点
    if (!this.vertices.includes(w)) {
      //将w节点添加到图当中
      this.addVertex(w);
    }

    //如果都存在,那么对他们进行关系添加
    this.adjList.get(v).push(w);

    //判断是否为有向图,如果为无向图则添加v到w的关系
    if (!this.isDirected) {
      this.adjList.get(w).push(v);
    }
  }

  //获取图当中的顶点
  getVertices() {
    return this.vertices;
  }
  //获取图的邻接表
  getAdjList() {
    return this.adjList;
  }
}

const graph = new Graph<string>();

const myVertices: string[] = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];

for (let i = 0; i < myVertices.length; i++) {
  graph.addVertex(myVertices[i]);
}

graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("A", "D");
graph.addEdge("C", "D");
graph.addEdge("C", "G");
graph.addEdge("D", "G");
graph.addEdge("B", "E");
graph.addEdge("B", "F");
graph.addEdge("E", "I");
graph.addEdge("D", "H");

/**
 * 节点的颜色集合
 */

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 startVertext 起点
 */
const shorttesPath = (graph: Graph<string>, startVertext: string) => {
  //获取图的节点列表
  const vertices = graph.getVertices();

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

  //初始化图党建节点的状态
  const nodeColorList = initNodeColor(vertices);

  //创建保存待访问节点的队列
  const queue = [];

  //将首节点添加到队列当中
  queue.push(startVertext);
  //distances 保存起点到每个节点的距离
  const distances = {};

  //保存每个节点的前溯节点
  const predecessors = {};

  //初始化节点距离和前溯节点
  for (let i = 0; i < vertices.length; i++) {
    distances[vertices[i]] = 0;
    predecessors[vertices[i]] = null;
  }

  //进行广度优先搜索
  while (queue.length !== 0) {
    //获取队列的第一个元素
    const node = queue.shift();

    //从邻接表当中获取节点node相邻的元素
    const neighbors = adjList.get(node);

    //将节点node状态标记为灰色
    nodeColorList[node] = NodeColors.GREY;

    //探索节点的相邻节点
    for (let i = 0; i < neighbors.length; i++) {
      const adjacent = neighbors[i];

      //判断当前节点是否为白色,如果不是白色表示该节点已经被访问过
      if (nodeColorList[adjacent] === NodeColors.WHITE) {
        //将节点标记为已访问
        nodeColorList[adjacent] = NodeColors.GREY;
        //将当前节点添加到队列
        queue.push(adjacent);

        //当前节点的距离为前溯加一
        distances[adjacent] = distances[node] + 1;

        //保存前溯节点
        predecessors[adjacent] = node;
      }
    }

    //将当前被探索完的节点标记为黑色
    nodeColorList[node] = NodeColors.BLACK;
  }

  return {
    distances,
    predecessors,
  };
};

const info = shorttesPath(graph, "A");

const fromVertex = myVertices[0];

for (let i = 1; i < myVertices.length; i++) {
  const toVertex = myVertices[i];

  const path = [];

  for (let v = toVertex; v !== fromVertex; v = info.predecessors[v]) {
    path.push(v);
  }

  path.push(fromVertex);

  let s = path.pop();

  while (!path.length) {
    s += " - " + path.pop();
  }

  console.log(s);
}