最小生成树算法:Prim 算法、Kruskal 算法

本文介绍了最小生成树算法,包括Prim算法(顶点驱动)和Kruskal算法(边驱动)的区别,它们的数据结构实现(优先队列和并查集),以及在稠密图和稀疏图中各自的时间复杂度优势。提供了这两种算法的代码示例。
摘要由CSDN通过智能技术生成

最小生成树算法

最小生成树(Minimum Spanning Tree,MST)是图论中一个重要的概念,表示连接图中所有顶点的树,同时保证总权值最小。

比较:

  1. 贪心策略的不同

    • Prim 算法是一种顶点驱动的贪心算法,从一个初始顶点开始,每次选择距离当前生成树最近的顶点,并将其加入生成树中。这意味着 Prim 算法每次都是在已经形成的生成树上加入一个新的顶点,直到所有顶点都被加入。
    • Kruskal 算法是一种边驱动的贪心算法,它首先将所有边按照权值从小到大排序,然后依次选择权值最小的边,如果选择的边不会形成环路,就将其加入生成树中。这意味着 Kruskal 算法是在所有边的集合上进行操作,直到生成树中包含了所有顶点。
  2. 数据结构的不同

    • Prim 算法通常使用优先队列或最小堆来实现,以快速找到距离当前生成树最近的顶点。
    • Kruskal 算法通常使用并查集来检测是否形成环路,以保证生成树的连通性。
  3. 时间复杂度

    • 在稠密图(边数量接近顶点数量的平方)上,Prim 算法的时间复杂度为 O(V^2),其中 V 是顶点的数量。
    • 在稀疏图(边数量远小于顶点数量的平方)上,Prim 算法的时间复杂度可以优化到 O(E log V),其中 E 是边的数量。
    • Kruskal 算法的时间复杂度为 O(E log E),其中 E 是边的数量,因为它需要对所有边进行排序。
  4. 适用性

    • 当图是稠密图时,Prim 算法通常比较适用,因为它的时间复杂度不受边的数量的影响。
    • 当图是稀疏图时,Kruskal 算法通常比较适用,因为它的时间复杂度与边的数量相关,而稀疏图的边数量相对较少。

Prim算法

Prim算法是一种贪心算法,它从一个初始顶点开始,逐步扩展生成树,每次选择与当前树相邻的权值最小的边加入。

算法步骤:

  1. 选择一个初始顶点作为生成树的根节点。
  2. 初始化一个空的生成树集合和一个优先队列(或最小堆),将初始顶点及其相邻边加入优先队列。
  3. 从优先队列中选择权值最小的边,将其加入生成树集合,并将其相邻的顶点及边加入优先队列。
  4. 重复步骤3,直到生成树包含所有顶点。

代码示例:

class PriorityQueue {
  /**
   * 优先队列构造函数
   */
  constructor() {
    // 初始化队列为空数组
    this.queue = [];
  }

  /**
   * 将顶点及其权重添加到优先队列
   * @param {string} vertex - 顶点
   * @param {number} weight - 权重
   */
  enqueue(vertex, weight) {
    this.queue.push({ vertex, weight });
    this.sort();
  }

  /**
   * 从优先队列中删除并返回顶点及其权重
   * @returns {Object} - 包含顶点及其权重的对象
   */
  dequeue() {
    return this.queue.shift();
  }

  /**
   * 对队列进行排序
   */
  sort() {
    this.queue.sort((a, b) => a.weight - b.weight);
  }

  /**
   * 检查队列是否为空
   * @returns {boolean} - 如果队列为空则返回true,否则返回false
   */
  isEmpty() {
    return this.queue.length === 0;
  }
}

/**
 * 使用Prim算法查找最小生成树
 * @param {Object} graph - 表示图的邻接表
 * @returns {Array} - 包含最小生成树的边的数组
 */
function prim(graph) {
  const visited = {};
  const mst = [];
  const startVertex = Object.keys(graph)[0];
  const priorityQueue = new PriorityQueue();

  visited[startVertex] = true;

  // 将起始顶点的所有相邻顶点及权重添加到优先队列
  for (const neighbor in graph[startVertex]) {
    const weight = graph[startVertex][neighbor];
    priorityQueue.enqueue(neighbor, weight);
  }

  // 遍历优先队列,直到队列为空
  while (!priorityQueue.isEmpty()) {
    const { vertex, weight } = priorityQueue.dequeue();
    if (!visited[vertex]) {
      visited[vertex] = true;
      // 将顶点、权重和起始顶点添加到最小生成树中
      mst.push({ from: startVertex, to: vertex, weight });
      // 将顶点的所有未访问相邻顶点及其权重添加到优先队列
      for (const neighbor in graph[vertex]) {
        if (!visited[neighbor]) {
          const weight = graph[vertex][neighbor];
          priorityQueue.enqueue(neighbor, weight);
        }
      }
    }
  }

  return mst;
}

// 示例图
const graph = {
  A: { B: 2, D: 3 },
  B: { A: 2, C: 1, D: 1 },
  C: { B: 1, D: 4, E: 5 },
  D: { A: 3, B: 1, C: 4, E: 1 },
  E: { C: 5, D: 1 }
};

const minimumSpanningTree = prim(graph);
console.log(minimumSpanningTree);

Kruskal算法

Kruskal算法是一种贪心算法,它首先将所有边按照权值从小到大进行排序,然后依次选择权值最小且不形成环的边加入生成树,直到生成树包含所有顶点。

算法步骤:

  1. 将图中所有边按照权值从小到大进行排序。
  2. 初始化一个空的生成树集合。
  3. 依次选择排序后的边,如果该边的两个顶点不在同一个连通分量中,则将该边加入生成树,并合并两个连通分量。
  4. 重复步骤3,直到生成树包含所有顶点。

代码示例:

class DisjointSet {
  /**
   * 创建一个新的并查集
   * @param {number} n - 初始大小
   */
  constructor(n) {
    this.parent = new Array(n).fill(-1);
  }

  /**
   * 查找元素所属的集合
   * @param {number} x - 要查找的元素
   * @returns {number} - 元素所属的集合的根节点
   */
  find(x) {
    if (this.parent[x] < 0) return x;
    return this.parent[x] = this.find(this.parent[x]);
  }

  /**
   * 合并两个集合
   * @param {number} x - 第一个元素
   * @param {number} y - 第二个元素
   * @returns {boolean} - 如果两个元素属于不同的集合,则返回true;否则返回false
   */
  union(x, y) {
    // 查找节点x的根节点
    const rootX = this.find(x);
    // 查找节点y的根节点
    const rootY = this.find(y);

    // 如果两个节点不是同一个根节点
    if (rootX !== rootY) {
      // 如果节点x的根节点的父节点小于节点y的根节点的父节点
      if (this.parent[rootX] < this.parent[rootY]) {
        // 将节点x的根节点的父节点值加上节点y的根节点的父节点值
        this.parent[rootX] += this.parent[rootY];
        // 将节点y的根节点的父节点设置为节点x的根节点
        this.parent[rootY] = rootX;
        // 如果节点y的根节点的父节点小于等于节点x的根节点的父节点
      } else {
        // 将节点y的根节点的父节点值加上节点x的根节点的父节点值
        this.parent[rootY] += this.parent[rootX];
        // 将节点x的根节点的父节点设置为节点y的根节点
        this.parent[rootX] = rootY;
      }
      // 返回true,表示合并成功
      return true;
    }
    // 如果两个节点是同一个根节点,表示已经在同一个集合中,不需要合并
    return false;
  }
}

/**
* 使用Kruskal算法查找最小生成树
* @param {Object} graph - 表示图的邻接表
* @returns {Array} - 包含最小生成树的边的数组
*/
function kruskal(graph) {
  // 存储边的数组
  const edges = [];
  // 存储顶点的数组
  const vertices = Object.keys(graph);
  // 创建一个并查集
  const disjointSet = new DisjointSet(vertices.length);
  // 存储最小生成树的边
  const mst = [];

  // 遍历图中的所有边,并将它们添加到边数组中
  // 将所有边添加到边数组中
  for (const from in graph) {
    for (const to in graph[from]) {
      edges.push({ from, to, weight: graph[from][to] });
    }
  }

  // 对边数组进行排序,按照边的权重从小到大排序
  // 根据权重对边数组进行排序
  edges.sort((a, b) => a.weight - b.weight);

  // 遍历排序后的边数组,依次加入最小生成树中
  for (const edge of edges) {
    const { from, to, weight } = edge;
    // 如果两个顶点不在同一个集合中,则加入最小生成树,并合并两个集合
    if (disjointSet.union(vertices.indexOf(from), vertices.indexOf(to))) {
      mst.push(edge);
    }
  }

  // 返回最小生成树
  return mst;
}

// 示例图
const graph = {
  A: { B: 2, D: 3 },
  B: { A: 2, C: 1, D: 1 },
  C: { B: 1, D: 4, E: 5 },
  D: { A: 3, B: 1, C: 4, E: 1 },
  E: { C: 5, D: 1 }
};

const minimumSpanningTree = kruskal(graph);
console.log(minimumSpanningTree);

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿online

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值