数据结构のJavaScript 实现之最小生成树MST( Kruskal算法和Prim算法)
先感谢查阅博客时给予我启发的一些博主:
- https://www.cnblogs.com/skywang12345/p/3711496.html 这篇问题说得比较清楚,kruskal算法的重点在于怎么判断是否形成了回路,而判断是否形成回路要用到并查集的概念。
- https://www.cnblogs.com/zhangming-blog/p/5414514.html 这篇例子很好,prim算法讲的很清晰,配图画的很好哈哈。
- https://www.cnblogs.com/xzxl/p/7226557.html 这篇关于并查集的讲解十分生动有趣,受到广泛好评,必须一看。
- https://github.com/trekhleb/javascript-algorithms/blob/master/README.zh-CN.md 这是github上JavaScript算法实现集合
然后,说下我对两个算法的理解:最大的区别 :Prim算法是“按点生成”,Kruskal算法是“按边生成”.
Prim算法,将所有顶点分成两坨,一坨是用过的点,一坨是还没用的。任意选一个点开始,他就是第一个用过的点,从这个点找权最小的边,通过这个边找到另一个点,把这个点也归入到用过的点集合中。以此类推,每增加一边就增加一个用过的点。
Kruskal算法,将所有边按权值大小排列,从中选最小的边,如果遇到成环的就丢掉,继续下一个最小权重的边。
JavaScript简单实现
定义图形(就是上面链接1里的图)
// 顶点名称
let vertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
// 边和权重 [0, 1, 12] 表示A到B的权为12
let edges = [
[0, 1, 12],
[0, 5, 16],
[0, 6, 14],
[1, 2, 10],
[1, 5, 7],
[2, 3, 3],
[2, 4, 5],
[2, 5, 6],
[3, 4, 4],
[4, 5, 2],
[4, 6, 8],
[5, 6, 9]
];
定义两个方法
/**
* @description 找到最小权重的边
* @param {Array<edge>} edges
* @returns {edge} edge
*/
function findMinEdge(edges) {
let min = null;
for (const edge of edges) {
min = min ? edge[2] < min[2] ? edge : min : edge;
}
return min;
}
/**
* @description 找到两组点集合之间的所有边,这个方法只有prim算法用得到
* @param {Array<vertex>} srcs 源顶点数组
* @param {Array<vertex>} objs 目标顶点数组
* @param {Array<edge>} edges 所有边
* @param {Array<vertex>} vertices 所有顶点
* @returns {Array} edges
*/
function findEdgesIn(srcs, objs, edges, vertices) {
let edgesBetweenSrcObj = [];
for (const edge of edges) {
for (const src of srcs) {
srcIndex = vertices.indexOf(src);
for (const obj of objs) {
objIndex = vertices.indexOf(obj);
if (edge[0] === srcIndex && edge[1] === objIndex || edge[0] === objIndex && edge[1] === srcIndex) { // 无向
edgesBetweenSrcObj.push(edge);
}
}
}
}
return edgesBetweenSrcObj;
}
prim算法实现
function prim(edges, vertices, startVertex) {
// 命名灵感:僵尸传染人类,找到最小代价,已经传染的人成为僵尸
let infected = []; // 顶点
let remained = vertices.slice(0); // 顶点
let mstree = []; // 边
let cur = startVertex;
while (remained.length !== 1) {
infected.push(cur);
remained.splice(remained.indexOf(cur), 1);
let min = findMinEdge(findEdgesIn(infected, remained, edges, vertices));
mstree.push(min);
cur = infected.indexOf(vertices[min[0]]) === -1 ? vertices[min[0]] : vertices[min[1]]; // 更新
}
return mstree;
}
// 打印树
let mstree = prim(edges, vertices, 'A');
console.log(mstree);
并查集的简单实现(没有实现路径压缩,直接让A扎向B的屁股)
最重要的三个方法:1.makeSet(根据顶点初始化并查集) 2. unionSet(union两个点集) 3. find(找到一个点的掌门人)
// 非连通集
function DisjoinSet() {
this.items = {};
this.makeSet = function (vertices) {
for (const vertex of vertices) {
this.items[vertex] = {
parent: null,
value: vertex
};
}
}
this.unionSet = function (vertex_1, vertex_2) {
const rootA = this.find(vertex_1);
const rootB = this.find(vertex_2);
if (rootA === null || rootB === null) {
throw new Error('no exist vertex');
}
if (rootA !== rootB) {
rootA.parent = rootB
return true;
}
return false;
}
this.find = function (vertex) {
let p = this.items[vertex];
if (p) {
return p.parent === null ? p : this.find(p.parent.value);
}
throw new Error('not exist vertex');
}
}
kruskal算法实现
function kruskal(edges, vertices) {
let mstree = [];
let edgesCopy = edges.slice(0);
let disjoinSet = new DisjoinSet();
disjoinSet.makeSet(vertices);
while (mstree.length < vertices.length - 1) {
let min = findMinEdge(edgesCopy);
// 不构成环,并查集的理念,为每一个顶点指定一个终点,判断新加两点的终点是否一致
if (disjoinSet.unionSet(vertices[min[0]], vertices[min[1]])) {
mstree.push(min);
}
edgesCopy.splice(edgesCopy.indexOf(min), 1);
}
return mstree;
}
// 打印
let mstree = kruskal(edges, vertices);
console.log(mstree);
两种算法殊途同归,最终得到的结果应该是一致的
[ [ 0, 1, 12 ],
[ 1, 5, 7 ],
[ 4, 5, 2 ],
[ 3, 4, 4 ],
[ 2, 3, 3 ],
[ 4, 6, 8 ] ]