🥳每日一练-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 下来进行测试。

相关推荐
CSCN新手听安1 小时前
list的常用操作
数据结构·list
梅茜Mercy3 小时前
数据结构:链表(经典算法例题)详解
数据结构·链表
88号技师3 小时前
2024年12月一区SCI-加权平均优化算法Weighted average algorithm-附Matlab免费代码
人工智能·算法·matlab·优化算法
IT猿手3 小时前
多目标应用(一):多目标麋鹿优化算法(MOEHO)求解10个工程应用,提供完整MATLAB代码
开发语言·人工智能·算法·机器学习·matlab
青春男大3 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
88号技师3 小时前
几款性能优秀的差分进化算法DE(SaDE、JADE,SHADE,LSHADE、LSHADE_SPACMA、LSHADE_EpSin)-附Matlab免费代码
开发语言·人工智能·算法·matlab·优化算法
Zer0_on3 小时前
数据结构栈和队列
c语言·开发语言·数据结构
一只小bit3 小时前
数据结构之栈,队列,树
c语言·开发语言·数据结构·c++
我要学编程(ಥ_ಥ)4 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
埃菲尔铁塔_CV算法4 小时前
FTT变换Matlab代码解释及应用场景
算法