【问题标题】:Could an iterative function call itself?迭代函数可以调用自身吗?
【发布时间】:2017-01-13 07:43:04
【问题描述】:

在观看下面的 MIT 6.001 课程视频时,讲师在 28:00 将此算法标记为迭代。但是,在 30.27 他说这和实际的“递归”算法都是递归的。 该函数使用基本情况调用自身,那么这次迭代如何?

https://www.youtube.com/watch?v=dlbMuv-jix8&list=PLE18841CABEA24090&index=2

private int iterativeSum(int x, int y)
{
    System.out.println("x "+x+" y "+y);
    if(x == 0)
    {
        return y;
    }
    return iterativeSum(--x, ++y);
}

【问题讨论】:

标签: java recursion iteration scheme sicp


【解决方案1】:

讲师似乎更感兴趣的是它是如何执行的,而不是代码是如何编写的。这两者之间有很大的不同,但这是另一回事(但值得注意的是,例如,某些语言会将递归编译为迭代)。

在这种情况下,当您将整个状态保存在一个地方并重复处理该一条数据时,它就是 迭代。当你有一堆状态并添加到堆栈中时,它是递归,然后最终将堆栈折叠回一个答案。

在 31:00 的讲师示例中,当有一张纸保存了迄今为止完成的工作的全部状态时,他将其显示为迭代,并且任何人都可以接受它并最终产生最终答案.

在 32:20 的递归示例中,Joe 对问题有自己的注释,并且只传递了关于问题的一小部分的注释。然后,Harry 有足够的信息来解决他的问题子部分,但整个问题仍然需要 Joe 保留自己的信息,以便在从 Harry 那里取回 Harry 的结果时对其进行处理。

你有一大堆人,越来越多的人被添加到堆栈中,直到其中一个人的问题简单到可以自己解决,并且他可以立即返回答案,这意味着现在倒数第二个人有一个更简单的问题,现在可以返回 他的 答案,以此类推,直到整个堆栈的人倒塌成最后一个(第一个)人,然后他产生最终答案。

【讨论】:

  • 谢谢,但在 java 版本中,堆栈中的每个“人”似乎都在返回值。例如,在 sum(3,2) 中: sum(2,3) 将从后续调用中获得的结果返回给 sum(3,2)。那么这不是递归的吗?不太确定 LISP 如何以课程中所示的方法返回:(DEFINE (+ X Y) (IF (= X 0) Y (+ (-1 + X) (1 + Y))))。在 java 中而不是在 lisp 中可以递归吗?我怀疑
  • @Stacky 是的,Java 的语义使这个递归的堆栈很大,但关键是一旦你到达最后一行,即返回 iterativeSum(...);,你就不需要任何来自堆。例如,它不是返回 iterativeSum(...) + 2;。您可以轻松地将其转换为蹦床版本,例如,pastebin.com/uvGcKGw1
【解决方案2】:

我认为这是基于 SICP 中的定义。这里是the relevant section. 简而言之,如果递归调用处于尾部位置,递归函数可以生成迭代过程:不需要记住任何局部变量的当前值并且可以清除/重用它们的空间(使用 LISP 更容易看出一切都是表达式,并且可以看到表达式的大小在迭代过程中不会增长)。

相比之下,递归过程在递归调用完成后还没有完成。比如这个函数

private int recursiveSum(int x, int y)
{
    if(x == 0)
    {
        return y;
    }
    return ++(recursiveSum(--x, y));
}

将生成递归过程,因为仍然需要完成额外的工作 (++())。

编译器是否会真正实现尾调用优化 (TCO) 是另一回事。 AFAIK,迄今为止 JVM 不支持它。在尾部位置调用自身的函数通常很容易优化(作为循环)。当一个函数调用另一个函数,而另一个函数又调用第一个函数时,困难就来了,等等。

【讨论】:

    【解决方案3】:

    在函数调用自身的意义上,它是递归的。然而,它有一个重要的属性,即调用的结果完全依赖于另一个函数调用的结果;不需要当前堆栈中的值。结果由

    提供
    return iterativeSum(--x, ++y);
    

    不是来自类似的东西

    return iterativeSum(--x, ++y) + x;
    

    这需要从递归调用中“回来”,对结果做一些事情,然后返回。因为结果不需要当前堆栈帧中的任何内容,所以实现(在某些语言中,取决于语义)可以消除或重用当前堆栈帧。这称为 tail-call 消除,在某些语言(如 Scheme)中是强制要求的。这就是该算法的 Scheme 实现本质上是迭代的原因:它不需要无限量的堆栈空间。

    在Scheme中,尾调用消除意味着实现本质上如下,其中iterativeSumDriver是一种蹦床,或者是iterativeSumInternal提供的结果的迭代驱动程序强>。

    public class IterativeSummer {
        /**
         * Returns a sum, computed iteratively.
         *
         * @param x the augend
         * @param y the addend
         * @return the sum of the augend and addend
         */
        public int iterativeSumDriver(int x, int y) {
            int[] state = new int[] { x, y };
            while (state.length == 2) {
                state = iterativeSumInternal(state[0], state[1]);
            }
            return state[0];
        }
    
        /**
         * Returns the new computation state of a iterative sum
         * computation.  If x is 0, then returns an array of just y.
         * Otherwise, returns an an array of x-1 and y+1.
         *
         * @param x the augend
         * @param y the addend
         * @return the next interal state
         */
        int[] iterativeSumInternal(int x, int y) {
            if (x == 0) {
                return new int[] { y };
            }
            else {
                return new int[] { x-1, y+1 };
            }
        }
    
        public static void main(String[] args) {
            int x = 5;
            int y = 6;
            int sum = new IterativeSummer().iterativeSumDriver(x,y);
            System.out.println(String.format("%d + %d = %d", x, y, sum));
        }
    }
    

    合适的蹦床

    正如Will Ness pointed out,一个合适的蹦床并不真正了解计算中使用的状态;它只需要有一些东西可以调用,直到一个不可调用的东西被返回。这是一个可以做到这一点的版本。

    public class Trampoline {
        /**
         * State of a computation for a trampoline.
         * 
         * @param <T> the type of value
         */
        public interface TrampolineState<T> {
            /**
             * Returns whether the state is a finished state.
             * 
             * @return whether the state is a finshed state
             */
            boolean isFinished();
    
            /**
             * Returns the value, if this state is finished.
             * 
             * @return the value
             * @throws IllegalStateException if the state is not finished
             */
            T getValue() throws IllegalStateException;
    
            /**
             * Returns the next state, if this state is not finished.
             * 
             * @return the next state
             * @throws IllegalStateException if the state is finished
             */
            TrampolineState<T> getNext() throws IllegalStateException;
        }
    
        /**
         * Executes a trampolined state and its successors until a finished state is
         * reached, and then returns the value of the finished state.
         * 
         * @param state the state
         * @return the value
         */
        public <T> T trampoline(TrampolineState<T> state) {
            while (!state.isFinished()) {
                state = state.getNext();
            }
            return state.getValue();
        }
    
        /**
         * Returns the state for for sum computation. 
         * 
         * @param x the augend
         * @param y the addend
         * @return the state
         */
        private TrampolineState<Integer> getSumTrampolineState(int x, int y) {
            return new TrampolineState<Integer>() {
                @Override
                public boolean isFinished() {
                    return x == 0;
                }
    
                @Override
                public Integer getValue() {
                    if (!isFinished()) {
                        throw new IllegalStateException();
                    }
                    return y;
                }
    
                @Override
                public TrampolineState<Integer> getNext() {
                    if (isFinished()) {
                        throw new IllegalStateException();
                    }
                    return getSumTrampolineState(x - 1, y + 1);
                }
            };
        }
    
        /**
         * Returns a sum, computed by a trampolined computation.
         * 
         * @param x the augend
         * @param y the addend
         * @return the sum
         */
        public int sum(int x, int y) {
            return trampoline(getSumTrampolineState(x, y));
        }
    }
    

    另见:

    【讨论】:

    • 蹦床必须返回并处理what-to-do-next 参数。是的,它的工作量更大,但是,you 提到了 T 字,如果它在你的代码中明确显示,那就太好了,即使它是多余的。 :)
    • @WillNess 这就是我说“某种蹦床”的原因。但可以肯定的是,我们可以做一个真正的蹦床(只需几秒钟)。
    • @WillNess pastebin.com/hBWFeWKR 怎么样(也添加到答案中)?我还在回答 Common Lisp 问题How do I jump out of a function in Lisp? 时使用了蹦床。另外,对于其他读者,看看这个问题:What is a trampoline function?
    • 顺便说一句,我当时并不反对使用while。 :) 所以,像handle = SUM; { NEXT: ; goto handle; SUM: sum += state.y; state.x -=1; state.y +=1; if (state.x &lt; 0) { handle = STOP; } { handle = SUM; } goto NEXT; STOP: break; } 这样的东西是我想到的(在伪代码中;可能真的用switch 编码,在C 中)。当然,在这种特殊情况下与您的第一个变体等效,只是蹦床的性质在其中比我上面的 sn-p 更模糊。 :)
    【解决方案4】:

    这里使用的“递归”一词有两种不同的含义。一个是语法 - 任何调用自身的函数都是语法(即语法)递归的。

    另一个是关于由给定代码段编码的计算过程的基本行为——它是否在恒定的堆栈空间中运行(因此本质上是迭代的),或者不是(本质上是递归的)。

    方案有尾调用优化,所以你的代码其实是

    private int iterativeSum(int x, int y)
    {
    ITERATIVE_SUM:
        System.out.println("x "+x+" y "+y);
        if(x == 0)
        {
            goto RETURN;
        }
        --x;    // return iterativeSum(--x, ++y);
        ++y;
        goto ITERATIVE_SUM;
    RETURN:
        return y
    }
    

    相当于标准的while循环,因为尾递归函数调用重用函数调用帧。

    【讨论】:

      猜你喜欢
      • 2010-10-03
      • 1970-01-01
      • 2021-07-09
      • 2016-05-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-10
      相关资源
      最近更新 更多