java - 如何使用堆栈安全(基于堆)递归将二叉树转换为java中的列表?
Posted
技术标签:
【中文标题】java - 如何使用堆栈安全(基于堆)递归将二叉树转换为java中的列表?【英文标题】:How to transform a binary tree into a list in java using stack safe (heap based) recursion? 【发布时间】:2022-01-17 08:18:36 【问题描述】:问题是:如何使用尾递归实现下面的方法(toListPreOrderLeft
)?
public List<A> toListPreOrderLeft()
return toListPreOrderLeft_(this, list()).eval();
private TailCall<List<A>> toListPreOrderLeft_(TreeNode<A> tree, List<A> list)
return ???
详情:
假设我有这棵树:
使用左前序算法将其转换为列表将生成列表:[4,2,1,3,6,5,7]
我可以在树类中使用堆栈递归来实现这个算法:
public List<A> toListPreOrderLeft()
return list(this.value)
.concat(this.left.toListPreOrderLeft())
.concat(this.right.toListPreOrderLeft());
其中concat
只是连接两个列表,this
指的是我在其上调用该方法的树节点。 (我正在使用我的 List 和 Tree 的自定义实现)。
但是如果我按以下顺序插入一棵树,则此实现将溢出堆栈:100000,99999,99998,99997,...,3,2,1。
这是我用来表示尾递归调用的类:
public abstract class TailCall<T>
public abstract TailCall<T> nextCall();
public abstract T eval();
public abstract boolean isIntermediate();
//this constructor is to prevent this class from being extended by other classes
private TailCall()
public static <T> TailCall<T> terminalCall(T t)
return new TerminalCall<>(t);
public static <T> TailCall<T> intermediateCall(Supplier<TailCall<T>> nextCall)
return new IntermediateCall<>(nextCall);
private static class TerminalCall<T> extends TailCall<T>
private final T t;
private TerminalCall(T t)
this.t = t;
@Override
public T eval()
return t;
@Override
public TailCall<T> nextCall()
throw new IllegalStateException("Terminal has no next call");
@Override
public boolean isIntermediate()
return false;
private static class IntermediateCall<T> extends TailCall<T>
private final Supplier<TailCall<T>> nextCall;
private IntermediateCall(Supplier<TailCall<T>> nextCall)
this.nextCall = nextCall;
@Override
public T eval()
TailCall<T> tailCall = this;
while (tailCall.isIntermediate())
tailCall = tailCall.nextCall();
return tailCall.eval();
@Override
public TailCall<T> nextCall()
return nextCall.get();
@Override
public boolean isIntermediate()
return true;
下面是一个示例,说明如何使用中序遍历和尾递归实现将树转换为列表的方法(我们不断旋转树,直到它变成有序链表):
public List<A> toListInOrderLeft()
return toListInOrderLeft_(this, list()).eval();
private TailCall<List<A>> toListInOrderLeft_(TreeNode<A> tree, List<A> accumulator)
return tree.isEmpty()
? TailCall.terminalCall(accumulator)
: tree.right().isEmpty()
? intermediateCall(() -> toListInOrderLeft_(tree.left(), accumulator.prepend(tree.value()))) //prepend adds value at the start of a list
: intermediateCall(() -> toListInOrderLeft_(tree.rotateLeft(), accumulator));
这里对问题中的代码做一个简单的解释:
TailCall
是一个代表递归调用的类,因此我们不是通过推送一个新的堆栈帧来进行递归调用,而是创建一个TailCall
的实例,它可以是中间子类或终端子类,具体取决于递归调用是否是最后一次调用或将使用供应商(实现惰性评估)嵌入下一个调用(在TailCall
对象中)。所以在任何时间点,我们要么有 1 个 referenced 对象代表整个递归链,要么至多 2 个 referenced 对象代表它(后一种情况发生在我们开始时使用eval
方法评估TailCall
的下一次调用
话虽如此,很容易看出示例方法toListInOrderLeft
是如何工作的,它调用一个帮助方法,该方法返回一个TailCall
(表示第一个递归调用),然后在返回时调用eval
对象来评估递归调用直到结束。至于辅助方法,它采用 accumulator (最初是一个空列表)并开始将取自树节点的值附加到它。当达到终止条件时,我们返回这个累加器,该累加器包装在辅助方法的终端调用中。
【问题讨论】:
我不会阅读你所有的 java 代码,但堆栈非常适合深度优先搜索。只需维护一堆节点即可访问。最初,此堆栈仅包含根。当您访问一个节点时:1)将其值附加到列表中; 2)将右孩子压入堆栈; 3)将左孩子压入堆栈。 @Stef 我说的是 java 的函数调用堆栈,它在 1000-3000 次调用(aprox)时溢出,你说的是基于堆的堆栈(我创建一个变量我自己)。没关系,我知道我可以做到这一点,但我问的是基于尾递归的基于堆的方法,所以如果你有办法做到这一点?我不是在问最好的方法,我是在问这个具体的方法 “基于堆”:在讨论树时这是一个模棱两可的术语。而是说动态分配,因为您指的不是数据结构,而是内存池。 "...而且我什至不使用堆栈":嗯,您的TailCall
实例是自定义链表中的节点,这是一个实现堆栈的方法(使用堆内存)。如果您可以接受,那么我不明白为什么按照上面的建议创建堆栈是不行的。
好吧,您刚刚接受并奖励了一个回答,该回答暗示了我也提出的建议。我同意作者的观点,即(ab)使用你的尾调用类是可怕的。第二种方法是我想到的,减去使用正在构建的结果列表本身作为元素堆栈的可能优化。我很高兴问题解决了,反正我自己也没时间写答案,只是觉得这个问题很有趣。
【参考方案1】:
TailCall
不能真正用作堆栈来记住事物。它可以对尾递归进行建模,但toListPreorderLeft
的递归实现不仅仅是尾递归。
许多语言都提供类似于TailCall
的“一元”类,但具有类似TailCall.then(...)
的方法,您可以使用该方法将内容链接在一起。如果没有此功能,您将需要创建另一个堆栈 - 显式或隐式。
你的toListInorderLeft
方法也有同样的问题,但是你通过改变树来欺骗,有效地使用树作为你的堆栈。这个技巧不适用于预购。
您可以通过将递归算法转换为“延续传递风格”来解决这个问题。这隐含地生成了一堆 lambda 函数:
class TreeNode<A>
TreeNode<A> left;
TreeNode<A> right;
A value;
...
public TailCall<List<A>> toListPreOrderLeft()
return _toListPreOrderLeft(new ArrayList<>(), null);
private TailCall<List<A>> _toListPreOrderLeft(List<A> accumulator, Function<List<A>, TailCall<List<A>>> continuation)
accumulator.add(this.value);
if (this.left != null && this.right != null)
final Function<List<A>, TailCall<List<A>>> cont2 = (accum) ->
this.right._toListPreOrderLeft(accum, continuation);
return TailCall.intermediateCall(() -> this.left._toListPreOrderLeft(accumulator, cont2));
else if (this.right != null)
return TailCall.intermediateCall(() -> this.right._toListPreOrderLeft(accumulator, continuation));
else if (this.left != null)
return TailCall.intermediateCall(() -> this.right._toListPreOrderLeft(accumulator, continuation));
else if (continuation != null)
return TailCall.intermediateCall(() -> continuation.apply(accumulator));
return TailCall.terminalCall(accumulator);
continuation
参数捕获当前调用结束后仍有待完成的工作。从cont2
到continuation
的链接形成一个隐式堆栈。
请注意,这是一种避免堆栈溢出的可怕方法。理解算法并编写一个很好的迭代实现会更好:
public List<A> toListPreOrderLeft()
final List<A> accumulator = new ArrayList<>();
final List<TreeNode<A>> treeStack = new ArrayList<>();
TreeNode<A> n = this;
for (;;)
accumulator.add(n.value);
if (n.left != null)
if (n.right != null)
treeStack.add((n.right));
n = n.left;
else if (n.right != null)
n = n.right;
else if (!treeStack.isEmpty())
n = treeStack.remove(treeStack.size()-1);
else
break;
return accumulator;
【讨论】:
我知道我需要另一个TailCall
以递归方式将函数应用于正确的分支,但不知道该怎么做。原来如此,函数式编程方式有时会很混乱,谢谢。以上是关于java - 如何使用堆栈安全(基于堆)递归将二叉树转换为java中的列表?的主要内容,如果未能解决你的问题,请参考以下文章
编程将二叉树(子女右兄弟链)转换为树然后将树转换为二叉树(直接修改) 非递归实现
无序binarytree二叉树堆调整成小顶堆,基于节点图,非数组内操作,非递归,python