如何使用堆栈安全(基于堆)递归将二叉树转换为 java 中的列表?

How to transform a binary tree into a list in java using stack safe (heap based) recursion?

问题是:如何使用尾递归实现下面的方法 (toListPreOrderLeft)?

 public List<A> toListPreOrderLeft() {
        return toListPreOrderLeft_(this, list()).eval();
    }

    private TailCall<List<A>> toListPreOrderLeft_(TreeNode<A> tree, List<A> list) {
        return ???
    }

详情:

假设我有这棵树:

使用 pre-order-left 算法将其转换为列表将生成列表:[4,2,1,3,6,5,7]

我可以在树中使用堆栈递归来实现这个算法 class:

 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。

这是我用来表示尾递归调用的class:

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

下面是一个示例,说明如何使用中序遍历和尾递归实现将树转换为列表的方法 class(我们不断旋转树,直到它变成这样有序链表):

   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 不能真正用作堆栈来记住东西。可以对尾递归进行建模,但是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;
    }