工作中遇到一个问题:给定一棵树 和 一个节点的 id,找到从根节点到该节点的路径
分 3 种情况
-
树的每个节点有指向父节点的引用
- 输入:一个节点(节点中有父节点的引用)
- 输出:一个数组,路径中的每个节点按顺序放到数组中
画成图就是:
树节点的定义如下:
class TreeNode {
constructor(id, parent, children) {
this.id = id;
this.parent = parent;
this.children = children;
}
}
思路
看作 链表 的遍历,从指定的节点一直遍历到根节点
实现
function withParent(node) {
if (!node) {
return;
}
let current = node;
const path = [];
while (current) {
path.push(current);
current = current.parent;
}
return path;
}
测试
构造树如下:
路径如下:
-
树的每个节点保存父节点的 ID
- 输入:一棵树
tree,和指定节点的id - 输出:一个数组,路径中的每个节点按顺序放到数组中
画成图就是:
树节点的定义如下:
class TreeNode {
constructor(id, parentId, children) {
this.id = id;
this.parentId = parentId;
this.children = children;
}
}
思路
由于子树没有指向父节点的引用,只有父节点的 ID,所以无法直接找到父节点
- 创建一个
Map,根据ID找到节点, - 用
parentId来找父节点,从而遍历出路径
实现
创建 Map 的实现如下
function initMap(tree, map) {
if (!tree) {
return map;
}
tree.forEach((item) => {
map.set(item.id, item);
initMap(item.children, map);
});
return map;
}
寻找路径的实现如下:
function withParentId(nodeId, tree) {
if (!nodeId) {
return;
}
const map = new Map();
initMap(tree, map);
let current = map.get(nodeId);
const path = [];
while (current) {
path.push(current);
current = map.get(current.parentId);
}
return path;
}
测试
构造树如下:
路径如下:
-
树的每个节点没有保存父节点相关的属性
- 输入:一棵树
tree,和指定节点的id - 输出:一个数组,路径中的每个节点按顺序放到数组中
画成图就是:
树节点的定义如下:
class TreeNode {
constructor(id, children) {
this.id = id;
this.children = children;
}
}
思路
遍历树,可以用 DFS 的方式;生成路径,需要用 回溯 法;所以,这是一个典型的 回溯 问题。
树的 DFS 遍历的模板代码如下:
function dfs(tree) {
if (!tree) {
return;
}
tree.forEach((item) => {
dfs(item.children);
});
}
回溯 就是在普通的 DFS 的基础上,保存遍历过的节点,可以用数组 cur 来保存。可能会产生多个符合条件的数组,所以需要另外用一个二维数组 prev 来存放符合条件的数组。
遍历子树前,往当前数组 cur 中添加当前节点。遍历子树后,从当前数组 cur 中移除当前节点。
如果找到了满足条件的数组 cur,由于后续可能会移除当前节点,所以需要把当前数组 cur 复制一份,再保存到 prev 数组中
由于树的遍历直接递归遍历子树即可,不需要用标志位记录某个节点是否已遍历,所以树的回溯的模板代码如下:
function dfs(tree,<其他必须的参数>, prev, cur) {
if (!tree) {
return;
}
tree.forEach((item) => {
cur.push(item);
if (<满足业务条件>) {
prev.push([...cur]);
}
dfs(item.children, <其他必须的参数>, prev, cur);
cur.pop();
});
return prev;
}
树中的路径是唯一的,所以从 prev 数组中取第 0 项就是路径数组。
由于遍历时,从根节点开始添加到路径中,所以需要对 cur 数组倒置。
实现
function dfsFn(tree, nodeId, prev, cur) {
const path = dfs(tree, nodeId, prev, cur);
const result = path;
return result[0];
}
function dfs(tree, nodeId, prev, cur) {
if (!tree) {
return;
}
tree.forEach((item) => {
cur.push(item);
if (item.id === nodeId) {
prev.push([...cur]);
}
dfs(item.children, nodeId, prev, cur);
cur.pop();
});
return prev;
}
测试
构造一棵树,分别验证要找的节点为 叶子节点 和 内部节点 的情况
当要找到的节点是 叶子节点 时
dfsFn(tree3, 6, [], []);
结果如下:
当要找到的节点是 内部节点 时
dfsFn(tree3, 3, [], []);
结果如下: