如何在深度优先搜索算法中正确回溯?
Posted
技术标签:
【中文标题】如何在深度优先搜索算法中正确回溯?【英文标题】:How to properly backtrack in Depth First Search Algorithm? 【发布时间】:2021-12-24 04:22:04 【问题描述】:我正在尝试解决一个问题:在二叉树中查找特定节点的所有祖先。
Input: root, targetNode
Output: An array/list containing the ancestors
假设,我们以上面的二叉树为例。我们想找到节点 4 的祖先。输出应该是 [3, 5, 2, 4]。如果节点为8,则输出为[3, 1, 8]
为了解决这个问题,我编写了一个实现 DFS 的函数。
var ancestor = function(root, target)
var isFound = false;
const dfs = (node, curr) =>
if (node === null)
return curr;
if (node.val === target.val)
curr.push(node.val);
isFound = true;
return curr;
curr.push(node.val);
const left = dfs(node.left, curr);
if (!isFound)
const right = dfs(node.right, curr);
curr.pop();
return right;
else
curr.pop();
return left;
console.log(dfs(root, []));
;
但它没有返回正确的输出。例如,如果targetNode为7,则输出为[3],如果targetNode为8,则输出也是[3]。如果我删除curr.pop()
行,输出也无效。对于 targetNode 7,它是 [3, 5, 6, 2, 7]。我想我发现了我犯错的问题。在回溯时,我在删除curr
数组中推送的节点时做错了。如果我传递一个字符串而不是数组,它会正确打印输出。
var ancestor = function(root, target)
var isFound = false;
const dfs = (node, curr) =>
if (node === null)
return curr;
if (node.val === target.val)
curr += node.val;
isFound = true;
return curr;
const left = dfs(node.left, curr + node.val + '->);
if (!isFound)
const right = dfs(node.right, curr + node.val + '->);
return right;
else
return left;
console.log(dfs(root, ''));
上面的代码用字符串而不是数组正确打印输出,如果我通过targetNode 7,输出是3->5->2->7
我的问题是,如何在这里正确取消选择/回溯?还是我做错了什么?提前致谢。
【问题讨论】:
【参考方案1】:自然环境中的递归
递归是一种函数式遗产,因此将其与函数式风格一起使用会产生最佳效果。这意味着要避免诸如push
和cur += node.val
之类的突变、isFound = true
之类的变量重新分配以及其他副作用之类的命令性事情。我们可以将ancestor
写成一个简单的基于生成器的函数,它将每个节点添加到递归子问题的输出之前 -
const empty =
Symbol("tree.empty")
function node(val, left = empty, right = empty)
return val, left, right
function* ancestor(t, q)
if (t == empty) return
if (t.val == q) yield [t.val]
for (const l of ancestor(t.left, q)) yield [t.val, ...l]
for (const r of ancestor(t.right, q)) yield [t.val, ...r]
const mytree =
node(3, node(5, node(6), node(2, node(7), node(4))), node(1, node(0), node(8)))
for (const path of ancestor(mytree, 7))
console.log(path.join("->"))
3->5->2->7
使用模块
最后,我会为这段代码推荐一种基于模块的方法 -
// tree.js
const empty =
Symbol("tree.empty")
function node(val, left = empty, right = empty)
return val, left, right
function* ancestor(t, q)
if (t == empty) return
if (t.val == q) yield [t.val]
for (const l of ancestor(t.left, q)) yield [t.val, ...l]
for (const r of ancestor(t.right, q)) yield [t.val, ...r]
function insert(t, val)
// ...
function remove(t, val)
// ...
function fromArray(a)
// ...
// other tree functions...
export empty, node, ancestor, insert, remove, fromArray
// main.js
import node, ancestor from "./tree.js"
const mytree =
node(3, node(5, node(6), node(2, node(7), node(4))), node(1, node(0), node(8)))
for (const path of ancestor(mytree, 7))
console.log(path.join("->"))
3->5->2->7
私有生成器
在前面的实现中,我们的模块为ancestor
的公共接口公开了一个生成器。另一种选择是在找不到节点且没有祖先时返回undefined
。考虑这个隐藏生成器并要求调用者对结果进行空检查的替代实现 -
const empty =
Symbol("tree.empty")
function node(val, left = empty, right = empty)
return val, left, right
function ancestor(t, q)
function* dfs(t)
if (t == empty) return
if (t.val == q) yield [t.val]
for (const l of dfs(t.left)) yield [t.val, ...l]
for (const r of dfs(t.right)) yield [t.val, ...r]
return Array.from(dfs(t))[0]
const mytree =
node(3, node(5, node(6), node(2, node(7), node(4))), node(1, node(0), node(8)))
const result =
ancestor(mytree, 7)
if (result)
console.log(result.join("->"))
else
console.log("no result")
3->5->2->7
【讨论】:
非常感谢您对递归副作用的回答和洞察。我不熟悉 javascript 中的生成器概念,我现在一定会研究一下。 我会尽量避免递归中的变异,但是你能告诉我我写的代码哪里做错了吗?【参考方案2】:需要检查右孩子的DFS是否找到了节点。 修复:
const left = dfs(node.left, curr);
if (!isFound)
const right = dfs(node.right, curr);
if(isFound)
return right;
curr.pop();
return; // return nothing, backtracking
return left;
【讨论】:
感谢您的回复。我已经尝试过您的解决方案,它仍然给我错误的输出。对于目标节点 7,输出为 [3,5,6,2,7] @h_a 那是因为我忘记包含 pop()-s,修复它。 成功了。谢谢你。 :-) 所以如果我总结一下我做错了什么,即使我找到了解决方案,我还是从curr
数组中弹出了该项目。那是我的问题,对吧?如果它不能引导我找到正确的解决方案,我应该只 pop() 项目吗?
@h_a 是的,您不需要在 DFS 中返回任何特定值,因此修复代码的一种方法是,就像您说的那样,仅弹出当前的如果它没有得到到节点【参考方案3】:
在数组示例中,您的循环以 DFS 方式遍历节点,因此每个节点都以这种方式连接。如果我们计算 DFS 算法中的树节点,[3 , 5, 6, 2, 7] 实际上是按 1, 2, 3, 4 和 5 的顺序排列的。这样,您在数组中的整个树应该是这样的; [3、5、6、2、7、4、1、0、8]。
所以当你找到目标值时,你会从当前节点弹出,并在 DFS 算法中将其全部追溯到头节点。
我要么建议找到一种方法来解决这个问题,要么你可以保存每个节点的父节点。这意味着您可以使用元组而不是 int 数组(如果可以接受的话)。索引可能如下所示 = (node value, parent value)
[(3,NULL),(5,3),(6,5),(2,5)...]
然后相应地回溯……
【讨论】:
谢谢。我会这样想。以上是关于如何在深度优先搜索算法中正确回溯?的主要内容,如果未能解决你的问题,请参考以下文章
递归/回溯/深度优先搜索/广度优先搜索 /动态规划/二分搜索/贪婪算法