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 参数捕获当前调用结束后仍有待完成的工作。从cont2continuation 的链接形成一个隐式堆栈。

请注意,这是一种避免堆栈溢出的可怕方法。理解算法并编写一个很好的迭代实现会更好:

    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中的列表?的主要内容,如果未能解决你的问题,请参考以下文章

编程将二叉树(子女右兄弟链)转换为树然后将树转换为二叉树(直接修改) 非递归实现

非递归遍历二叉树Java实现

无序binarytree二叉树堆调整成小顶堆,基于节点图,非数组内操作,非递归,python

将二叉搜索树转换为有序的双向链表

如何将二叉树就地转换为二叉搜索树,即我们不能使用任何额外的空间

利用JAVA语言,将二叉树封装在一个类中,要求实现二叉树的构造,并实现二叉树的遍历操作。