🥳每日一练-prim-生成最小生成树-JS

这篇文章将会分享用 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 节点边信息添加到最小生成树中minTreeNodesminTreeEdges
  • 在函数的末尾再递归调用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 下来进行测试。

相关推荐
new出一个对象3 小时前
uniapp接入BMapGL百度地图
javascript·百度·uni-app
你挚爱的强哥4 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
LNTON羚通4 小时前
摄像机视频分析软件下载LiteAIServer视频智能分析平台玩手机打电话检测算法技术的实现
算法·目标检测·音视频·监控·视频监控
前端Hardy5 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189115 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
哭泣的眼泪4086 小时前
解析粗糙度仪在工业制造及材料科学和建筑工程领域的重要性
python·算法·django·virtualenv·pygame
Microsoft Word7 小时前
c++基础语法
开发语言·c++·算法
天才在此7 小时前
汽车加油行驶问题-动态规划算法(已在洛谷AC)
算法·动态规划
小镇程序员8 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
莫叫石榴姐8 小时前
数据科学与SQL:组距分组分析 | 区间分布问题
大数据·人工智能·sql·深度学习·算法·机器学习·数据挖掘