这篇文章将会分享用 JS 代码实现 prim 算法,生成最小生成树
这个图来自王道考研的
数据结构
视频课程中的截图。B 站可搜王道考研数据结构
上面红线的连接的节点构成的树,就是这个图的最小生成树。最小生成树表示各个边的权值之和是最小的
思想:先从某个点开始,表示最小生成树只有一个点。然后开始找与最小生成树
连接最短的边中,找到最小生成树
下一个节点。循环往复,直到找完图中所有的节点
下面开始用 JS 代码实现
准备数据
无向图数据
下面将会按照这张图来生成数据:
javascript
/**
定义一个节点类型,包含一个数值属性和一个指向下一个节点的指针
@typedef {{value: number, next: nodeType}} nodeType
*/
/**
初始化一个包含6个元素的数组,用于表示图的节点
使用map方法将数组的所有元素初始化为它们的索引值
@type {number[]}
*/
let graphNodes = Array(6)
.fill(0)
.map((item, index) => index);
/**
定义一个表示图的边的关系的二维数组
其中,每个元素是一个数组,表示一个节点可以到达的下一个节点及其权重
@type {number[][]}
*/
const graphEdges = {
0: [
[1, 6],
[3, 1],
[2, 5],
],
3: [
[1, 5],
[0, 1],
[2, 4],
],
4: [
[1, 3],
[3, 6],
[5, 6],
],
5: [
[2, 2],
[3, 4],
],
};
/**
生成一个无向图的函数
@param {number[]} graphNodes - 图的节点数组
@param {number[][]} graphEdges - 图的边的关系的二维数组
@returns {nodeType[]} - 返回图的节点数组,每个节点都有一个指向下一个节点的指针
*/
const generateGraph = (graphNodes, graphEdges) => {
// 将图的节点数组转换为具有value属性和next指针的节点类型
graphNodes = graphNodes.map((item) => {
const tempNode = { value: item, next: null };
return tempNode;
});
// 定义一个添加边的函数,用于在图的节点之间添加边
const addNode = (graph, fromNode, toNode, power) => {
// 遍历当前节点的next指针,查找是否已经存在从当前节点到目标节点的边
let currentNode = graph[fromNode];
while (currentNode.next) {
// 如果已经存在边,则返回
if (currentNode.next.value == toNode) return;
currentNode = currentNode.next;
}
// 如果不存在边,则添加从当前节点到目标节点的边
currentNode.next = { value: toNode, next: null, power };
// 遍历目标节点的next指针,查找是否已经存在从目标节点到当前节点的边
currentNode = graph[toNode];
while (currentNode.next) {
currentNode = currentNode.next;
}
// 如果不存在边,则添加从目标节点到当前节点的边
currentNode.next = { value: fromNode, next: null, power };
};
// 遍历图的边的关系的二维数组,将每个边添加到图的节点之间
Object.entries(graphEdges).map(([value, edges]) => {
edges.map(([toNode, power]) => {
addNode(graphNodes, value, toNode, power);
});
});
// 返回生成的图的节点数组
return graphNodes;
};
// 调用生成图的函数,将生成的图的节点数组赋值给graphNodes变量
graphNodes = generateGraph(graphNodes, graphEdges);
上面代码会生成一个无向图的邻接表数据结构。
向generateGraph
传递图的点数据graphNodes
,以及边数据graphEdges
之后,函数内部会遍历边数据,每碰到一个边edge
,就会用addNode
向邻接表插入两条边,一条是当前节点到目标节点,另一条是目标节点到当前节点,相当于双向的有向边。
生成过程,就是用双向的有向边表示无向边的性质
辅助数据
javascript
const isVisited = Array(6).fill(false); // 初始化一个包含6个元素的布尔数组,用于表示每个节点是否已被访问过
const lowestPower = Array(6) // 初始化一个包含6个元素的数组,用于存储每个节点的最低权重及其来源节点
.fill(-1) // 将所有元素的值初始化为-1
.map((item) => ({ power: -1, nodeFrom: null })); // 初始化一个对象,包含power属性(用于存储节点的最低权重)和nodeFrom属性(用于存储权重来源的节点)
const minTreeEdges = []; // 初始化一个空数组,用于存储最小生成树的边
const minTreeNodes = new Set(); // 初始化一个空集合,用于存储最小生成树的节点
const addNodeToMinTree = (newNode, otherNode, power) => { // 定义一个函数,用于将新节点添加到最小生成树的节点集合和边集合中
minTreeEdges[newNode] = minTreeEdges[newNode] || []; // 如果新节点已经在minTreeEdges中,则直接获取其值
minTreeEdges[newNode].push([otherNode, power]); // 将新节点的邻接节点及其权重添加到minTreeEdges中
minTreeNodes.add(newNode); // 将新节点添加到minTreeNodes集合中
minTreeNodes.add(otherNode); // 将邻接节点添加到minTreeNodes集合中
};
获取最小生成树
下面开始实现生成最小生成树的代码getMinTree
javascript
/**
* get min tree
* @param {nodeType[]} graph
* @param {number} currentNode
* @returns void
*/
const getMinTree = (graph, currentNode) => {
...
}
生成最小生成树的过程:
- 首先初始化开始节点的数据,假设从
0 节点
开始,那么0 节点
的在lowestPower 数组
中的值就是 0,表示当前节点到0 节点
的距离是 0; - 然后开始更新其他与
0 节点
直接相连的节点在lowestPower 数组
的值,表示这些相连节点到0 节点
的距离。距离数据存放在邻接表中每个节点的 power 中 - 在
lowestPower 数组
中找出距离0 节点
最近的节点(设为 n),将 n 节点,以及 n 节点与0 节点
边信息添加到最小生成树中minTreeNodes
、minTreeEdges
- 在函数的末尾再递归调用
getMinTree
,并且从 n 节点开始
下面开始实现上面的过程
初始化
javascript
//初始化
const startNode = 0;
isVisited[startNode] = true;
lowestPower[startNode].power = 0;
lowestPower[startNode].nodeFrom = 0;
minTreeNodes.add(startNode);
更新 lowestPower 数组,更新连接的边的数据
javascript
//update power
// 更新节点的权重
for (let node = graph[currentNode].next; node !== null; node = node.next) {
if (isVisited[node.value] == false) {
const newPower = node.power;
if (lowestPower[node.value].power == -1 || lowestPower[node.value].power > newPower) {
lowestPower[node.value].power = newPower;
lowestPower[node.value].nodeFrom = currentNode;
}
}
}
遍历与当前节点相连的节点,并且更新它们在lowestPower 数组
中的值。
如果遍历到的节点没有被遍历到过,或者更新后的距离大于lowestPower 数组
的值,就更新值,并且更新其中的 nodeFrom
,表示权值来自哪个节点。否则就不更新。为接下来的找到最近的边做准备
找到目前最近的边
javascript
// find minIndex
// 找到具有最低权重的未访问节点
let minPower = Infinity;
let minIndex = -1;
for (let i = 0; i < graph.length; i++) {
if (isVisited[i] == false && lowestPower[i].power !== -1 && lowestPower[i].power < minPower) {
minPower = lowestPower[i].power;
minIndex = i;
}
}
if (minIndex == -1) return; // there was not left node, then return
遍历lowestPower 数组
,意图找到最近的节点。如果没有找到,说明已经找完了所有的节点,停止遍历
往最小生成树添加节点,和边
javascript
//add new node in graph
isVisited[minIndex] = true;
addNodeToMinTree(lowestPower[minIndex].nodeFrom, minIndex, lowestPower[minIndex].power);
找到最近的节点后,就将该节点添加到最小生成树中,并且将isVisited
的值设为 ture,表示该节点已经被添加到最小生成树中了
继续递归找最小生成树的下一个节点
javascript
getMinTree(graph, minIndex);
完整代码
javascript
/**
* get min tree
* @param {nodeType[]} graph
* @param {number} currentNode
* @returns void
*/
const getMinTree = (graph, currentNode) => {
//update power
for (let node = graph[currentNode].next; node !== null; node = node.next) {
if (isVisited[node.value] == false) {
const newPower = node.power;
if (lowestPower[node.value].power == -1 || lowestPower[node.value].power > newPower) {
lowestPower[node.value].power = newPower;
lowestPower[node.value].nodeFrom = currentNode;
}
}
}
// find minIndex
let minPower = Infinity;
let minIndex = -1;
for (let i = 0; i < graph.length; i++) {
if (isVisited[i] == false && lowestPower[i].power !== -1 && lowestPower[i].power < minPower) {
minPower = lowestPower[i].power;
minIndex = i;
}
}
if (minIndex == -1) return; // there was not left node, then return
//add new node in graph
isVisited[minIndex] = true;
addNodeToMinTree(lowestPower[minIndex].nodeFrom, minIndex, lowestPower[minIndex].power);
getMinTree(graph, minIndex);
};
执行函数
javascript
const getMinTree = (graph, currentNode) => {
...
}
getMinTree(graphNodes, startNode);
//输出最小生成树的数据
console.log(minTreeNodes, minTreeEdges);
//输出结果:
// Set(6) { 0, 3, 2, 5, 1, 4 }
// [ [ [ 3, 1 ] ], [ [ 4, 3 ] ], [ [ 5, 2 ] ], [ [ 2, 4 ], [ 1, 5 ] ] ]
生成的最小生成树就是这个样子:
代码中输出的最小生成树的节点和边信息可能不太直观,下面将这些信息转成邻接表的数据结构。借助的是刚开始准备数据用到的generateGraph
函数。
将最小生成树变成邻接表结构
javascript
const minTree = generateGraph([...minTreeNodes], minTreeEdges);
console.log(minTree);
可以将下面的邻接表的 json 结构与上面图片对比来看
json
[
{
value: 0,
next: {
value: 3,
next: null,
power: 1,
},
},
{
value: 3,
next: {
value: 4,
next: {
value: "3",
next: null,
power: 5,
},
power: 3,
},
},
{
value: 2,
next: {
value: 5,
next: {
value: "3",
next: null,
power: 4,
},
power: 2,
},
},
{
value: 5,
next: {
value: "0",
next: {
value: 2,
next: {
value: 1,
next: null,
power: 5,
},
power: 4,
},
power: 1,
},
},
{
value: 1,
next: {
value: "1",
next: null,
power: 3,
},
},
{
value: 4,
next: {
value: "2",
next: null,
power: 2,
},
},
]
完整代码
javascript
/**
定义一个节点类型,包含一个数值属性和一个指向下一个节点的指针
@typedef {{value: number, next: nodeType}} nodeType
*/
/**
初始化一个包含6个元素的数组,用于表示图的节点
使用map方法将数组的所有元素初始化为它们的索引值
@type {number[]}
*/
let graphNodes = Array(6)
.fill(0)
.map((item, index) => index);
/**
定义一个表示图的边的关系的二维数组
其中,每个元素是一个数组,表示一个节点可以到达的下一个节点及其权重
@type {number[][]}
*/
const graphEdges = {
0: [
[1, 6],
[3, 1],
[2, 5],
],
3: [
[1, 5],
[0, 1],
[2, 4],
],
4: [
[1, 3],
[3, 6],
[5, 6],
],
5: [
[2, 2],
[3, 4],
],
};
/**
生成一个无向图的函数
@param {number[]} graphNodes - 图的节点数组
@param {number[][]} graphEdges - 图的边的关系的二维数组
@returns {nodeType[]} - 返回图的节点数组,每个节点都有一个指向下一个节点的指针
*/
const generateGraph = (graphNodes, graphEdges) => {
// 将图的节点数组转换为具有value属性和next指针的节点类型
graphNodes = graphNodes.map((item) => {
const tempNode = { value: item, next: null };
return tempNode;
});
// 定义一个添加边的函数,用于在图的节点之间添加边
const addNode = (graph, fromNode, toNode, power) => {
// 遍历当前节点的next指针,查找是否已经存在从当前节点到目标节点的边
let currentNode = graph[fromNode];
while (currentNode.next) {
// 如果已经存在边,则返回
if (currentNode.next.value == toNode) return;
currentNode = currentNode.next;
}
// 如果不存在边,则添加从当前节点到目标节点的边
currentNode.next = { value: toNode, next: null, power };
// 遍历目标节点的next指针,查找是否已经存在从目标节点到当前节点的边
currentNode = graph[toNode];
while (currentNode.next) {
currentNode = currentNode.next;
}
// 如果不存在边,则添加从目标节点到当前节点的边
currentNode.next = { value: fromNode, next: null, power };
};
// 遍历图的边的关系的二维数组,将每个边添加到图的节点之间
Object.entries(graphEdges).map(([value, edges]) => {
edges.map(([toNode, power]) => {
addNode(graphNodes, value, toNode, power);
});
});
// 返回生成的图的节点数组
return graphNodes;
};
// 调用生成图的函数,将生成的图的节点数组赋值给graphNodes变量
graphNodes = generateGraph(graphNodes, graphEdges);
const isVisited = Array(6).fill(false); // 初始化一个包含6个元素的布尔数组,用于表示每个节点是否已被访问过
const lowestPower = Array(6) // 初始化一个包含6个元素的数组,用于存储每个节点的最低权重及其来源节点
.fill(-1) // 将所有元素的值初始化为-1
.map((item) => ({ power: -1, nodeFrom: null })); // 初始化一个对象,包含power属性(用于存储节点的最低权重)和nodeFrom属性(用于存储权重来源的节点)
const minTreeEdges = []; // 初始化一个空数组,用于存储最小生成树的边
const minTreeNodes = new Set(); // 初始化一个空集合,用于存储最小生成树的节点
const addNodeToMinTree = (newNode, otherNode, power) => { // 定义一个函数,用于将新节点添加到最小生成树的节点集合和边集合中
minTreeEdges[newNode] = minTreeEdges[newNode] || []; // 如果新节点已经在minTreeEdges中,则直接获取其值
minTreeEdges[newNode].push([otherNode, power]); // 将新节点的邻接节点及其权重添加到minTreeEdges中
minTreeNodes.add(newNode); // 将新节点添加到minTreeNodes集合中
minTreeNodes.add(otherNode); // 将邻接节点添加到minTreeNodes集合中
};
/**
* get min tree
* @param {nodeType[]} graph
* @param {number} currentNode
* @returns void
*/
const getMinTree = (graph, currentNode) => {
//update power
for (let node = graph[currentNode].next; node !== null; node = node.next) {
if (isVisited[node.value] == false) {
const newPower = node.power;
if (lowestPower[node.value].power == -1 || lowestPower[node.value].power > newPower) {
lowestPower[node.value].power = newPower;
lowestPower[node.value].nodeFrom = currentNode;
}
}
}
// find minIndex
let minPower = Infinity;
let minIndex = -1;
for (let i = 0; i < graph.length; i++) {
if (isVisited[i] == false && lowestPower[i].power !== -1 && lowestPower[i].power < minPower) {
minPower = lowestPower[i].power;
minIndex = i;
}
}
if (minIndex == -1) return; // there was not left node, then return
//add new node in graph
isVisited[minIndex] = true;
addNodeToMinTree(lowestPower[minIndex].nodeFrom, minIndex, lowestPower[minIndex].power);
getMinTree(graph, minIndex);
};
//初始化
const startNode = 0;
isVisited[startNode] = true;
lowestPower[startNode].power = 0;
lowestPower[startNode].nodeFrom = 0;
minTreeNodes.add(startNode);
getMinTree(graphNodes, startNode);
//输出最小生成树的数据
console.log(minTreeNodes, minTreeEdges);
//输出结果:
// Set(6) { 0, 3, 2, 5, 1, 4 }
// [ [ [ 3, 1 ] ], [ [ 4, 3 ] ], [ [ 5, 2 ] ], [ [ 2, 4 ], [ 1, 5 ] ] ]
总结
这篇文章分享了 prim 算法求最小生成树的思想,以及 JS 的代码实现。文章末尾附上了完整版代码,可以直接 copy 下来进行测试。