每日一练,图的DFS遍历

这篇文章用JS实现图的DFS遍历,也就是深度优先遍历

什么是深度优先遍历呢,这篇文章讲得很清楚(图文详解深度优先遍历 - 掘金),我就不赘述了。不过这篇文章讲的是树的深度游优先遍历,而不是图的。

但图的优先遍历和树的思想是一致的,就是不撞南墙不回头,它沿着一条路一直走下去,直到走不动了然后返回上一个岔路口选择另一条路继续前进,一直如此,直到走完所有能够到达的地方!它的名字中深度 两个字其实就说明了一切,每一次遍历都是走到最深。

这里有一段描述了图的深度优先遍历的过程,可以去看看:9.3 图的遍历 - Hello 算法

其他内容就不用看了,省得晕了

深度优先遍历的非递归版本

准备数据

首先准备一个邻接表数据,用来遍历用的。当然也可以用邻接矩阵,看个人意愿。

邻接表长这个样子:

用一个数组存下所有的顶点,每个数组中的项都有一个指针指向与该项连接的一条边。

像上图中的V0,它有一条边指向V3,所以在邻接表中,V0这一项就有一个指针指向V3。

如果一个顶点有两个与之相连的点,像V1,那么也是相类似的表示方式。

总的来说,数组中的每一项,每一项都代表一个点,如果该点可以通往另一个点,那么这一项就会有一个指针,这个指针会指向一条链表,这个链表上有所有与该点相连接的点

下面会基于这个图来创建邻接表:

代码实现:

javascript 复制代码
/**@typedef {{value: number, next: nodeType}} nodeType */

/**@type {nodeType[]} */
const graphNodes = Array(7)
	.fill(0)
	.map((item, index) => {
		const temp = {};
		temp.value = index;
		temp.next = null;
		return temp;
	});

const graphEdges = { 1: [2, 4], 2: [5], 3: [5, 6], 4: [2] };

const graph = Object.entries(graphEdges).map(([nodeKey, edges]) => {
	graphNodes[nodeKey].next = edges.reduce((res, nextEdge) => {
		const temp = { value: nextEdge, next: null };
		let tempRes = res;
		while (tempRes.next) tempRes = tempRes.next;
		tempRes.next = temp;
		return res;
	}, {}).next;
});

这段代码的主要目的是创建一个图数据结构,其中包括了节点和它们之间的连接关系。图的节点存储在 graphNodes 数组中,节点之间的边关系存储在 graphEdges 对象中,然后通过遍历 graphEdges 来构建图的结构。

首先声明一个名为 graphNodes 的数组,类型为 nodeType[]。这个数组用于存储图的节点,初始化长度为 7,数组的每一项都是包含 value 和 next 属性的对象。

这里使用 JSDoc 注释语法定义了一个类型别名 nodeType。它表示一个节点对象,包括一个数值属性 value 和一个指向下一个节点的引用属性 next。这个类型用于构建图数据结构。 关于JSDoc的用法可以看这篇文章如何在JavaScript项目中使用TypeScript的能力 - 掘金。不看也没关系,不影响本篇内容的阅读

然后使用graphNodes定义图的边关系。这个对象表示图中节点之间的边关系,以节点编号为key,value表示与该节点相邻的节点的数组。

其中{ 1 : [2, 4] },表示,与1直接相连的点有两个,分别是2和4。

最后就是将节点和边的关系组装起来了,代码很简单,不做解释。

最后将graphNodes打印出来:

javascript 复制代码
console.log(graphNodes);

这里需要注意一个点,点的编号是从1开始的,所以数组中的0下标的位置只用作占位,不做任何作用。

实现DFS

javascript 复制代码
/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} baseNode - 基准节点,表示从该节点开始遍历
 * @returns {number|null} - 返回从基准节点开始的第一个未被访问的相邻节点的值,如果没有则返回 null
 * 
 * 这个函数用于获取从基准节点开始的第一个未被访问的相邻节点的值。
 */
const getFirstNode = (graph, baseNode) => {
  if (graph[baseNode].next === null) return null;
  return graph[baseNode].next.value;
};

/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} baseNode - 基准节点,表示从该节点开始遍历
 * @param {number} currentNode - 当前节点,表示要查找下一个相邻节点的起始位置
 * @returns {number|null} - 返回从当前节点开始的下一个未被访问的相邻节点的值,如果没有则返回 null
 * 
 * 这个函数用于获取从当前节点开始的下一个未被访问的相邻节点的值。
 */
const getNextNode = (graph, baseNode, currentNode) => {
  let node = graph[baseNode].next;
  while (node.value !== currentNode) node = node.next;
  if (node.next === null) return null;
  return node.next.value;
};

/** 
 * 创建一个数组 isVisited,用于记录节点是否已被访问。
 * 初始状态下,所有节点都未被访问,所以数组的初始值为 false。
 */
const isVisited = Array(7).fill(false);

/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} startNode - 起始节点,表示从该节点开始进行深度优先搜索
 * 
 * 这个函数实现了深度优先搜索(DFS)算法,用于从起始节点开始遍历图的节点。
 * 它使用递归方式进行遍历,并在访问每个节点时将其标记为已访问。
 */
const DFS = (graph, startNode) => {
  console.log(startNode); // 打印当前节点
  isVisited[startNode] = true; // 标记当前节点为已访问

  // 遍历当前节点的所有相邻节点
  for (let node = getFirstNode(graph, startNode); node !== null; node = getNextNode(graph, startNode, node)) {
    if (isVisited[node] === false) {
      // 如果相邻节点未被访问,则递归调用DFS函数继续遍历
      DFS(graph, node);
    }
  }
};

这段代码实现了深度优先搜索(DFS)算法,用于从指定的起始节点开始遍历图的节点。它使用递归方式进行遍历,并通过 isVisited 数组来标记已经访问过的节点,以避免重复访问。希望这些注释和解释有助于理解代码的目的和功能

这是递归版本的DFS,深度遍历的过程中,可能会遇到已经访问过的节点,这时候可以使用isVisited中的信息将其跳过。使用这种方法,可以完美破解环形遍历

测试:

javascript 复制代码
DFS(graphNodes, 1);//	从1号节点开始访问
javascript 复制代码
DFS(graphNodes, 3); // 从3号节点开始

可以看到,从不同的节点出发开始遍历,会得到不同的结果。并且都无法将所有节点遍历完成。所以想要遍历所有的节点,我们需要不止一次执行DFS。

举个更好理解的例子,现在往上面的图添加一个7号节点,这个7号节点不与任何点连接。像下面这样:

显而易见,我们只执行一次DFS,7号是遍历不到的。

优化:

javascript 复制代码
/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * 
 * 这个函数实现了图的深度优先搜索(DFS)遍历。
 * 它遍历图中所有未被访问的节点,并在每次遍历中调用 DFS 函数来完成深度优先搜索。
 */
const TraverseDFS = (graph) => {
    for (let i = 1; i < isVisited.length; i++) {
        if (isVisited[i] === false) {
            // 如果节点未被访问,则调用 DFS 函数以从该节点开始深度优先搜索。
            DFS(graph, i);
        }
    }
};

这个函数遍历图中所有未被访问的节点,并在每次遍历中调用 DFS 函数来完成深度优先搜索。通过迭代所有未被访问的节点,它确保整个图中的每个节点都被遍历到。

即每次完成DFS,就会查找isVisited中是否还有没被访问的节点,如果有,就从这个节点开始新一轮的DFS

实现DFS,非递归版本

javascript 复制代码
/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} startNode - 起始节点,表示从该节点开始进行深度优先搜索
 * 
 * 这个函数实现了深度优先搜索(DFS)算法,使用栈数据结构进行迭代遍历图的节点。
 */
const DFS2 = (graph, startNode) => {
    // 创建一个栈用于迭代遍历图的节点。
    const stack = [];

    // 打印起始节点,并将其标记为已访问。
    console.log(startNode);
    isVisited[startNode] = true;

    // 将起始节点压入栈中。
    stack.push(startNode);

    // 当栈不为空时,继续迭代遍历图的节点。
    while (stack.length !== 0) {
        // 弹出栈顶的节点作为基准节点。
        const baseNode = stack.pop();

        // 获取基准节点的相邻节点。
        let node = graph[baseNode].next;

        // 遍历基准节点的相邻节点。
        for (; node !== null; node = node.next) {
            if (isVisited[node.value] !== true) {
                // 如果相邻节点未被访问,则标记为已访问,打印其值,并将其压入栈中。
                isVisited[node.value] = true;
                console.log(node.value);
                stack.push(node.value);
            }
        }
    }
};

非递归版本借用了栈来遍历,其实递归版本也借用了栈,只是不明显(递归就是天然的栈)。

这篇文章(9.3 图的遍历 - Hello 算法)中有DFS的非递归版本的图片演示,可以去看看,帮助理解代码

全部代码

下面是文中涉及到的完整代码,可以直接copy下面运行:

js 复制代码
/**@typedef {{value: number, next: nodeType}} nodeType */

/**@type {nodeType[]} */
const graphNodes = Array(7)
	.fill(0)
	.map((item, index) => {
		const temp = {};
		temp.value = index;
		temp.next = null;
		return temp;
	});

const graphEdges = { 1: [2, 4], 2: [5], 3: [5, 6], 4: [2] };

const graph = Object.entries(graphEdges).map(([nodeKey, edges]) => {
	graphNodes[nodeKey].next = edges.reduce((res, nextEdge) => {
		const temp = { value: nextEdge, next: null };
		let tempRes = res;
		while (tempRes.next) tempRes = tempRes.next;
		tempRes.next = temp;
		return res;
	}, {}).next;
});

/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} baseNode - 基准节点,表示从该节点开始遍历
 * @returns {number|null} - 返回从基准节点开始的第一个未被访问的相邻节点的值,如果没有则返回 null
 * 
 * 这个函数用于获取从基准节点开始的第一个未被访问的相邻节点的值。
 */
const getFirstNode = (graph, baseNode) => {
  if (graph[baseNode].next === null) return null;
  return graph[baseNode].next.value;
};

/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} baseNode - 基准节点,表示从该节点开始遍历
 * @param {number} currentNode - 当前节点,表示要查找下一个相邻节点的起始位置
 * @returns {number|null} - 返回从当前节点开始的下一个未被访问的相邻节点的值,如果没有则返回 null
 * 
 * 这个函数用于获取从当前节点开始的下一个未被访问的相邻节点的值。
 */
const getNextNode = (graph, baseNode, currentNode) => {
  let node = graph[baseNode].next;
  while (node.value !== currentNode) node = node.next;
  if (node.next === null) return null;
  return node.next.value;
};

/** 
 * 创建一个数组 isVisited,用于记录节点是否已被访问。
 * 初始状态下,所有节点都未被访问,所以数组的初始值为 false。
 */
const isVisited = Array(7).fill(false);

/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} startNode - 起始节点,表示从该节点开始进行深度优先搜索
 * 
 * 这个函数实现了深度优先搜索(DFS)算法,用于从起始节点开始遍历图的节点。
 * 它使用递归方式进行遍历,并在访问每个节点时将其标记为已访问。
 */
const DFS = (graph, startNode) => {
  console.log(startNode); // 打印当前节点
  isVisited[startNode] = true; // 标记当前节点为已访问

  // 遍历当前节点的所有相邻节点
  for (let node = getFirstNode(graph, startNode); node !== null; node = getNextNode(graph, startNode, node)) {
    if (isVisited[node] === false) {
      // 如果相邻节点未被访问,则递归调用DFS函数继续遍历
      DFS(graph, node);
    }
  }
};

// DFS(graphNodes, 1);
// DFS(graphNodes, 3);

/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * 
 * 这个函数实现了图的深度优先搜索(DFS)遍历。
 * 它遍历图中所有未被访问的节点,并在每次遍历中调用 DFS 函数来完成深度优先搜索。
 */
const TraverseDFS = (graph) => {
    for (let i = 1; i < isVisited.length; i++) {
        if (isVisited[i] === false) {
            // 如果节点未被访问,则调用 DFS 函数以从该节点开始深度优先搜索。
            DFS(graph, i);
        }
    }
};

TraverseDFS(graphNodes);

/** 
 * @param {nodeType[]} graph - 图数据结构,包含节点和连接关系
 * @param {number} startNode - 起始节点,表示从该节点开始进行深度优先搜索
 * 
 * 这个函数实现了深度优先搜索(DFS)算法,使用栈数据结构进行迭代遍历图的节点。
 */
const DFS2 = (graph, startNode) => {
    // 创建一个栈用于迭代遍历图的节点。
    const stack = [];

    // 打印起始节点,并将其标记为已访问。
    console.log(startNode);
    isVisited[startNode] = true;

    // 将起始节点压入栈中。
    stack.push(startNode);

    // 当栈不为空时,继续迭代遍历图的节点。
    while (stack.length !== 0) {
        // 弹出栈顶的节点作为基准节点。
        const baseNode = stack.pop();

        // 获取基准节点的相邻节点。
        let node = graph[baseNode].next;

        // 遍历基准节点的相邻节点。
        for (; node !== null; node = node.next) {
            if (isVisited[node.value] !== true) {
                // 如果相邻节点未被访问,则标记为已访问,打印其值,并将其压入栈中。
                isVisited[node.value] = true;
                console.log(node.value);
                stack.push(node.value);
            }
        }
    }
};

总结

这篇文章用JS代码,基于邻接表实现了DFS的递归遍历,和非递归版本的遍历。每个代码都有解释,应该是比较好理解。

文中涉及到的一般性的概念介绍,引用了其他文章中的解释。因为其他人解释的很好,我没必要再做一遍

相关推荐
戊辰happy37 分钟前
arcface
算法
浊酒南街2 小时前
决策树python实现代码1
python·算法·决策树
冠位观测者3 小时前
【Leetcode 热题 100】208. 实现 Trie (前缀树)
数据结构·算法·leetcode
秋雨凉人心4 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js
小王爱吃月亮糖4 小时前
C++的23种设计模式
开发语言·c++·qt·算法·设计模式·ecmascript
哥谭居民00016 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
踢足球的,程序猿6 小时前
Android native+html5的混合开发
javascript
IT猿手6 小时前
最新高性能多目标优化算法:多目标麋鹿优化算法(MOEHO)求解LRMOP1-LRMOP6及工程应用---盘式制动器设计,提供完整MATLAB代码
开发语言·算法·matlab·智能优化算法·多目标算法
kittygilr6 小时前
matlab中的cell
开发语言·数据结构·matlab
前端没钱6 小时前
探索 ES6 基础:开启 JavaScript 新篇章
前端·javascript·es6