JavaScript实现Prim与Kruskal最小生成树算法详解

作者:拾光时间:2026-10-22 20:25:53浏览:110

本文使用JavaScript语言实现Prim和Kruskal两种经典的最小生成树算法。文章首先介绍权重图与邻接矩阵的构建,随后详细讲解Prim算法从顶点出发逐步扩展的逻辑及Kruskal算法基于边集排序并避免回环的实现方式,包含完整代码示例与图解。

JavaScript实现Prim与Kruskal最小生成树算法详解

以往阅读书籍时,多数示例采用C++语言编写,但作为前端开发者,使用JavaScript重新实现这两个经典的最小生成树算法显得尤为必要。

权重图与最小生成树概念

一、权重图和最小生成树

权重图是指图中每条边都带有特定的权重值。

最小生成树是指在连通图的所有可能生成树中,所有边的权重之和最小的那一棵生成树。

本文所采用的示例图如下所示:

JavaScript实现Prim与Kruskal最小生成树算法详解

该图对应的最小生成树结构如下:

JavaScript实现Prim与Kruskal最小生成树算法详解

图的邻接矩阵表示法

二、邻接矩阵

邻接矩阵是一种用于表示图的矩阵数据结构,其中矩阵的下标代表顶点,矩阵中的数值代表边之间的权重(或表示是否存在边、边的方向等信息)。

在构建本文的邻接矩阵时,约定使用Number.MAX_SAFE_INTEGER表示两个节点之间不存在边,使用Number.MIN_SAFE_INTEGER表示当前节点没有自环。

具体代码实现如下:


/**
 * 邻接矩阵
 * 值为顶点与顶点之间边的权值,0表示无自环,一个大数表示无边(比如10000)
 * */
const MAX_INTEGER = Number.MAX_SAFE_INTEGER;//没有的边
const MIN_INTEGER = Number.MIN_SAFE_INTEGER;//没有自环
 
const matrix= [
  [MIN_INTEGER, 9, 2, MAX_INTEGER, 6],
  [9, MIN_INTEGER, 3, MAX_INTEGER, MAX_INTEGER],
  [2, 3, MIN_INTEGER, 5, MAX_INTEGER],
  [MAX_INTEGER, MAX_INTEGER, 5, MIN_INTEGER, 1],
  [6, MAX_INTEGER, MAX_INTEGER, 1, MIN_INTEGER]
];

上述邻接矩阵所表示的图结构如下:

边的对象封装

三、 边的表示

由于一条边包含权重、起点和终点三个属性,因此可以创建一个类(或对象)来封装这些属性,具体实现如下:


/**
 * 边对象
 * */
function Edge(begin, end, weight) {
  this.begin = begin;
  this.end = end;
  this.weight = weight;
}
 
Edge.prototype.getBegin = function () {
  return this.begin;
};
Edge.prototype.getEnd = function () {
  return this.end;
};
Edge.prototype.getWeight = function () {
  return this.weight;
};
 
/*class Edge {
  constructor(begin, end, weight) {
    this.begin = begin;
    this.end = end;
    this.weight = weight;
  }
  getBegin() {
    return this.begin;
  }
  getEnd() {
    return this.end;
  }
  getWeight() {
    return this.weight;
  }
}*/

注:JavaScript语言本身没有严格的私有变量概念,此处编写get方法仅为模拟私有变量的访问方式。实际上可以直接通过属性访问获取值,无需如此封装。

Prim算法实现逻辑

四、Prim算法

关于该算法的讲解文章非常多,此处不再赘述其理论细节。

该算法的核心思路是:以某个顶点为起点,逐步寻找各顶点上权值最小的相邻边来构建最小生成树,同时将邻接点纳入生成树的顶点集合中,只要确保顶点不重复添加即可。

具体实现代码如下:


/**
 * Prim算法
 * 以某顶点为起点,逐步找各顶点上最小权值的边构建最小生成树,同时其邻接点纳入生成树的顶点中,只要保证顶点不重复添加即可
 * 使用邻接矩阵即可
 * 优点:适合点少边多的情况
 * @param matrix 邻接矩阵
 * @return Array 最小生成树的边集数组
 * */
function prim(matrix) {
  const rows = matrix.length,
    cols = rows,
    result = [],
    savedNode = [0];//已选择的节点
  let minVex = -1,
    minWeight = MAX_INTEGER;
  for (let i = 0; i < rows; i++) {
    let row = savedNode[i],
      edgeArr = matrix[row];
    for (let j = 0; j < cols; j++) {
      if (edgeArr[j] < minWeight && edgeArr[j] !== MIN_INTEGER) {
        minWeight = edgeArr[j];
        minVex = j;
      }
    }
 
    //保证所有已保存节点的相邻边都遍历到
    if (savedNode.indexOf(minVex) === -1 && i === savedNode.length - 1) {
      savedNode.push(minVex);
      result.push(new Edge(row, minVex, minWeight));
 
      //重新在已加入的节点集中找权值最小的边的外部边
      i = -1;
      minWeight = MAX_INTEGER;
 
      //已加入的边,去掉,下次就不会选这条边了
      matrix[row][minVex] = MAX_INTEGER;
      matrix[minVex][row] = MAX_INTEGER;
    }
  }
  return result;
}

Kruskal算法实现逻辑

五、Kruskal算法

介绍该算法的资料同样丰富,此处不作过多理论展开。

其主要思路是:遍历所有边,按权值从小到大进行排序,每次选取当前权值最小的边,只要该边的加入不构成回环,则将其加入生成树。

5.1 邻接矩阵转成边集数组

与Prim算法不同,Kruskal算法是从权值最小的边开始处理的,因此使用边集数组更为便捷。需要将邻接矩阵转换为边集数组,并按照边的权重从小到大进行排序。


/**
 * 邻接矩阵转边集数组的函数
 * @param matrix 邻接矩阵
 * @return Array 边集数组
 * */
function changeMatrixToEdgeArray(matrix) {
  const rows = matrix.length,
    cols = rows,
    result = [];
  for (let i = 0; i < rows; i++) {
    const row = matrix[i];
    for(let j = 0 ; j < cols; j++) {
      if(row[j] !== MIN_INTEGER && row[j] !== MAX_INTEGER) {
        result.push(new Edge(i, j, row[j]));
        matrix[i][j] = MAX_INTEGER;
        matrix[j][i] = MAX_INTEGER;
      }
    }
  }
  result.sort((a, b) => a.getWeight() - b.getWeight());
  return result;
}

5.2 Kruskal算法的具体实现

Kruskal算法的一个关键点在于避免环路,这里采用一个数组来保存已纳入生成树的顶点和边(连线),该数组的下标对应边(连线)的起点,下标对应的元素值对应边(连线)的终点。若下标对应的元素值为0,表示还没有以该顶点为起点的边(连线)。

连线:表示一条或多条边前后连接形成的一条路径,该路径中不包含环路。


/**
 * kruskal算法
 * 遍历所有的边,按权值从小到大排序,每次选取当前权值最小的边,只要不构成回环,则加入生成树
 * 邻接矩阵转换成边集数组
 * 优点:适合点多边少的情况
 * @param matrix 邻接矩阵
 * @return Array 最小生成树的边集数组
 * */
function kruskal(matrix) {
  const edgeArray = changeMatrixToEdgeArray(matrix),
    result = [],
    //使用一个数组保存当前顶点的边的终点,0表示还没有已它为起点的边加入
    savedEdge = new Array(matrix.length).fill(0);
 
  for (let i = 0, len = edgeArray.length; i < len; i++) {
    const edge = edgeArray[i];
    const n = findEnd(savedEdge, edge.getBegin());
    const m = findEnd(savedEdge, edge.getEnd());
    console.log(savedEdge, n, m);
    //不相等表示这条边没有与现有生成树形成环路
    if (n !== m) {
      result.push(edge);
      //将这条边的结尾顶点加入数组中,表示顶点已在生成树中
      savedEdge[n] = m;
    }
  }
  return result;
}
 
/**
 * 查找连线顶点的尾部下标
 * @param arr 判断边与边是否形成环路的数组
 * @param start 连线开始的顶点
 * @return Number 连线顶点的尾部下标
 * */
function findEnd(arr, start) {
  //就是一直循环,直到找到终点,如果没有连线,就返回0
  while (arr[start] > 0) {
    start = arr[start];
  }
  return start;
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章