回溯法求树的路径

101 阅读3分钟

工作中遇到一个问题:给定一棵树 和 一个节点的 id,找到从根节点到该节点的路径

分 3 种情况

  1. 树的每个节点有指向父节点的引用

  • 输入:一个节点(节点中有父节点的引用)
  • 输出:一个数组,路径中的每个节点按顺序放到数组中

画成图就是:

20240629_135223__.png

树节点的定义如下:

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;
}
测试

构造树如下:

20240629_145414_image.png

路径如下:

20240629_150243_image.png

  1. 树的每个节点保存父节点的 ID

  • 输入:一棵树 tree,和指定节点的 id
  • 输出:一个数组,路径中的每个节点按顺序放到数组中

画成图就是:

20240629_140452_image.png

树节点的定义如下:

class TreeNode {
  constructor(id, parentId, children) {
    this.id = id;
    this.parentId = parentId;
    this.children = children;
  }
}
思路

由于子树没有指向父节点的引用,只有父节点的 ID,所以无法直接找到父节点

  1. 创建一个 Map,根据 ID 找到节点,
  2. 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;
}
测试

构造树如下:

20240629_145414_image.png

路径如下:

20240629_150320_image.png

  1. 树的每个节点没有保存父节点相关的属性

  • 输入:一棵树 tree,和指定节点的 id
  • 输出:一个数组,路径中的每个节点按顺序放到数组中

画成图就是:

20240629_140452_image.png

树节点的定义如下:

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;
}
测试

构造一棵树,分别验证要找的节点为 叶子节点内部节点 的情况

20240629_144414_image.png

当要找到的节点是 叶子节点

dfsFn(tree3, 6, [], []);

结果如下:

20240629_150409_image.png

当要找到的节点是 内部节点

dfsFn(tree3, 3, [], []);

结果如下:

20240629_150206_image.png