这篇文章来分享用 JS 语言来实现 kruskal 算法。kruskal很简单,快来看看
这个图来自王道考研的
数据结构
视频课程中的截图。B 站可搜王道考研数据结构
实现思路:先找出图中所有的权值最小的边,将边两头的节点放进最小生成树的集合里面;然后在剩余边里面继续找出最小的边,并且将第二小的边两头的节点放入最小生成的树的集合里面;继续寻找下一个最小的边。
如果最小边的两头节点,已经存在于最小生成树的集合里,说明这两个节点之间,有更小的边在之前已经被添加进来了,所以不做任何处理
准备数据
无向图的点集合,边集合
javascript
/**@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],
[2, 4],
],
4: [
[1, 3],
[3, 6],
[5, 6],
],
5: [
[2, 2],
[3, 4],
],
};
准备两个数据结构,
graphNodes
表示点的集合;graphEdges
表示边的集合;graphEdges
是一个对象,对象的 key 表示边的起点,对应的 value 表示边的终点和边的权重,即[destination,power]
并查集
因为 kruskal 涉及到并查集的操作,所以需要并查集的数据结构和操作。
了解更多并查集的内容,可以看这篇文章:🥳每日一练--两种并查集的JS实现 - 掘金
javascript
// 创建一个长度为8的数组,用-1填充,并用map方法将索引添加到数组中
const unionArray = Array(8)
.fill(-1)
.map((item, index) => index);
// 定义一个查找根节点的函数,传入节点,返回根节点
const findRoot = (unionArray, node) => {
while (unionArray[node] !== node) node = unionArray[node];
return node;
};
// 定义一个检查两个节点是否属于同一 union 的函数,传入两个节点,返回布尔值
const checkSameUnion = (unionArray, node1, node2) => {
return findRoot(unionArray, node1) == findRoot(unionArray, node2);
};
// 定义一个合并两个 union 的函数,传入两个节点,不返回任何值
const combineUnion = (unionArray, node1, node2) => {
const union1 = findRoot(unionArray, node1);
const union2 = findRoot(unionArray, node2);
if (union1 == union2) return; // 如果两个节点的根节点相同,说明它们属于同一 union,无需合并
unionArray[union2] = union1; // 将其中一个节点的根节点设置为另一个节点的根节点
};
unionArray
存储集合关系的结构findRoot
函数:查找节点的根节点checkSameUnion
函数:检查两个节点是否属于同一union
。传入两个节点,它会使用findRoot
函数查找它们的根节点,然后比较它们的根节点是否相同。如果相同,则说明它们属于同一union
combineUnion
函数:合并两个union
。传入两个节点,它会使用findRoot
函数查找它们的根节点,然后将其中一个节点的根节点设置为另一个节点的根节点。这样,它们就属于同一union
了
准备最小生成树的数据结构
javascript
// 定义一个空数组,用于存储最小生成树的边
const minTreeEdges = [];
// 定义一个空集合,用于存储最小生成树中的节点
const minTreeNodes = new Set();
// 定义一个函数,用于将新节点添加到最小生成树中
const addNodeToMinTree = (newNode, otherNode, power) => {
// 如果新节点的索引在 minTreeEdges 中不存在,则初始化一个空数组
minTreeEdges[newNode] = minTreeEdges[newNode] || [];
// 将新节点的邻接边添加到 minTreeEdges 中
minTreeEdges[newNode].push([otherNode, power]);
// 将新节点和邻接节点添加到 minTreeNodes 中
minTreeNodes.add(newNode);
minTreeNodes.add(otherNode);
};
minTreeEdges
:用来存储最小生成树的边
minTreeNodes
:用于存储最小生成树中的节点。像上面准备的无向图的数据一样。
还定义了一个函数 addNodeToMinTree
,用于将新节点添加到最小生成树中。它接受三个参数:newNode
(新节点)、otherNode
(邻接节点)和 power
(边权)
kruskal
javascript
// 定义一个函数,用于实现 Kruskal 算法
const kruskal = (graphEdges) => {
/** @typedef {number} nodeValue */
/** @typedef {number} power */
/** @type {[nodeValue, nodeValue, power ]} */
// 将图的边转换为数组形式,每个元素都是一个包含三个元素的数组:[node1, node2, power]
let array = Object.entries(graphEdges)
.reduce((res, [node, edges]) => {
res.push(...edges.map((edge) => [node, ...edge]));
return res;
}, []);
// 对数组进行排序,按照边权从小到大排序
array.sort((pre, next) => pre[2] - next[2]);
// 对数组中的每一条边进行处理
array.map(([node1, node2, power]) => {
// 将节点转换为数字类型
node1 = Number(node1);
node2 = Number(node2);
// 如果两个节点不属于同一 union,则合并它们
if (!checkSameUnion(unionArray, node1, node2)) {
// 将两个节点的根节点合并
combineUnion(unionArray, node1, node2);
// 将新边添加到最小生成树的边数组中
addNodeToMinTree(node1, node2, power);
}
});
};
首先准备一个数组,这个数组里面存放着所有边,并且按照边的权值升序。
按照 kruskal 的思想,从权值最小的边开始构建最小生成树。所以排序是必要的。
然后就开始遍历这个数组了,遍历过程的代码很简单,注释也很清晰,就不讲解了。
执行函数
javascript
kruskal(graphEdges);
console.log(minTreeNodes, minTreeEdges);
// Set(6) { 0, 3, 5, 2, 4, 1 }
// [
// [ [ 3, 1 ] ],
// <2 empty items>,
// [ [ 2, 4 ], [ 1, 5 ] ],
// [ [ 1, 3 ] ],
// [ [ 2, 2 ] ]
// ]
将生成的内容最后输出,其中所有的点都遍历到了,构建的边只有几条,那具体的最小生成树长什么样子呢?如下图:
可以和minTreeEdges
输出内容一一对应。
minTreeEdges
的解读方式和上文最开始准备的无向图的数据一致
将最小生成树转成邻接表
javascript
/**
* 定义一个函数,用于生成一个邻接表
*
* @param {number[]} graphNodes 节点数组
* @param {number[][]} graphEdges 边数组
* @returns {nodeType[]} 返回图的节点数组
*/
const generateGraph = (graphNodes, graphEdges) => {
// 将节点数组转换为对象,方便添加边
const graph = graphNodes.map((item, index) => {
const tempNode = { value: index, next: null };
return tempNode;
});
// 定义一个函数,用于添加边
const addNode = (graph, fromNode, toNode, power) => {
// 添加从 fromNode 到 toNode 的边
let currentNode = graph[fromNode];
while (currentNode.next) {
// 如果已经有边连接这两个节点,则跳过
if (currentNode.next.value == toNode) return;
currentNode = currentNode.next;
}
currentNode.next = { value: toNode, next: null, power };
// 添加从 toNode 到 fromNode 的边
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(graph, value, toNode, power);
});
});
// 返回图的节点数组
return graphNodes;
};
const minTree = generateGraph([...minTreeNodes], minTreeEdges);
console.log(minTree);
这里将minTreeNodes
和minTreeEdges
的数据转成邻接表的数据结构,就能在数据层面上更清晰了。
下面是最后输出的邻接表的 json 结构
数据有点长,
代码栏
可以展开和关闭哦
json
[
{
value: 0,
next: {
value: 3,
next: null,
power: 1,
},
},
{
value: 1,
next: {
value: "3",
next: {
value: "4",
next: null,
power: 3,
},
power: 5,
},
},
{
value: 2,
next: {
value: "3",
next: {
value: "5",
next: null,
power: 2,
},
power: 4,
},
},
{
value: 3,
next: {
value: "0",
next: {
value: 2,
next: {
value: 1,
next: null,
power: 5,
},
power: 4,
},
power: 1,
},
},
{
value: 4,
next: {
value: 1,
next: null,
power: 3,
},
},
{
value: 5,
next: {
value: 2,
next: null,
power: 2,
},
},
]
完整代码
javascript
//准备数据
/**@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],
],
};
//并查集
const unionArray = Array(8)
.fill(-1)
.map((item, index) => index);
const findRoot = (unionArray, node) => {
while (unionArray[node] !== node) node = unionArray[node];
return node;
};
const checkSameUnion = (unionArray, node1, node2) => {
return findRoot(unionArray, node1) == findRoot(unionArray, node2);
};
const combineUnion = (unionArray, node1, node2) => {
const union1 = findRoot(unionArray, node1);
const union2 = findRoot(unionArray, node2);
if (union1 == union2) return;
unionArray[union2] = union1;
};
//最小生成树
const minTreeEdges = [];
const minTreeNodes = new Set();
const addNodeToMinTree = (newNode, otherNode, power) => {
minTreeEdges[newNode] = minTreeEdges[newNode] || [];
minTreeEdges[newNode].push([otherNode, power]);
minTreeNodes.add(newNode);
minTreeNodes.add(otherNode);
};
//kruskal实现
const kruskal = (graphEdges) => {
/** @typedef {number} nodeValue */
/** @typedef {number} power */
/** @type {[nodeValue, nodeValue, power ]} */
let array = Object.entries(graphEdges)
.reduce((res, [node, edges]) => {
res.push(...edges.map((edge) => [node, ...edge]));
return res;
}, [])
.sort((pre, next) => pre[2] - next[2]);
array.map(([node1, node2, power]) => {
node1 = Number(node1);
node2 = Number(node2);
if (!checkSameUnion(unionArray, node1, node2)) {
combineUnion(unionArray, node1, node2);
addNodeToMinTree(node1, node2, power);
}
});
};
kruskal(graphEdges);
const minTree = generateGraph([...minTreeNodes], minTreeEdges);
总结
这篇文章分享了分享了如何用 JS 代码实现 kruskal,文中过程清晰,代码也有详细的注释。相信你一定可以看得懂。
事实证明 kruskal 还是很简单的😄