При просмотре видео курса 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);
}
2
Stacky 6 Сен 2016 в 03:15

4 ответа

Лучший ответ

Кажется, его больше интересует, как это выполняется, а не как написан код . Между этими двумя есть большая разница, но это совсем другой разговор (но, в частности, некоторые языки будут компилировать рекурсии как итерации, например).

В данном случае это итерация , когда вы удерживаете все состояние в одном месте и многократно работаете с этим одним фрагментом данных. Это рекурсия , когда у вас есть стек состояний и вы добавляете его в стек, а затем в конечном итоге свертываете стек обратно до ответа.

В своем примере в 31:00 он показывает, что это итерация, когда есть один лист бумаги, который фиксирует все состояние проделанной на данный момент работы, и любой человек может взять его и в конечном итоге дать окончательный ответ.

В примере с рекурсией 32:20 у Джо есть собственные заметки по проблеме, и он передает только заметки по части проблемы. Тогда у Гарри будет достаточно информации для его части проблемы, но вся проблема по-прежнему требует, чтобы Джо держался за свою собственную информацию, чтобы обрабатывать результаты Гарри, когда он получает их от Гарри.

У вас есть целая группа людей, и еще больше людей добавляются в стопку, пока у одного из них не возникнет проблема, достаточно простая, чтобы решить ее самостоятельно, и он может сразу же вернуть свой ответ, что означает, что у второго последнего человека теперь есть более простой проблема и теперь может возвращать свой ответ, и так далее, пока вся группа людей не превратится в одного последнего (первого) человека, который затем даст окончательный ответ.

5
Will Ness 8 Сен 2016 в 12:32

В том смысле, что функция вызывает сама себя, она рекурсивна. Однако он имеет важный атрибут, заключающийся в том, что результат вызова зависит исключительно от результата вызова другой функции; никакие значения из текущего стека не нужны. Результат обеспечивается

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

Не из чего-то вроде

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

Что потребовало бы «возврата» из рекурсивного вызова, выполнения каких-либо действий с результатом, а затем его возврата. Поскольку результат ничего не требует от текущего кадра стека, реализация (на некоторых языках, в зависимости от семантики) может исключить или повторно использовать текущий кадр стека. Это называется исключение хвостового вызова и является обязательным для некоторых языков, например 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));
    }
}

Правильный батут

Как отметил Уилл Несс, батут на самом деле не знает о состояниях, используемых в вычислениях; ему просто нужно что-то вызвать, пока не будет возвращена не вызываемая вещь. Вот версия, которая это делает.

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

Смотрите также:

2
Will Ness 21 Фев 2020 в 05:08

Здесь используется два разных значения слова «рекурсивный». Один из них синтаксический - любая вызывающая функция сама по себе синтаксически (то есть синтаксически) рекурсивна.

Другой - о существенном поведении вычислительного процесса, закодированного данным фрагментом кода - независимо от того, выполняется ли он в постоянном пространстве стека (так что по сути итеративен) или нет (так что по существу рекурсивен).

Схема имеет оптимизацию хвостового вызова , поэтому ваш код на самом деле

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, потому что хвостовой рекурсивный вызов функции повторно использует кадр вызова функции.

3
Will Ness 8 Сен 2016 в 11:56

Я думаю, это основано на определениях в SICP. Вот соответствующий раздел. < / a> Короче говоря, рекурсивная функция может генерировать итеративный процесс , если рекурсивный вызов находится в хвостовой позиции: ни одно из текущих значений локальных переменных не нужно запоминать, и их пространство может быть очищено / повторно использовано (это несколько легче увидеть с LISP, где все является выражением, и можно увидеть, как размер выражения не увеличивается в итеративном процессе).

Рекурсивный процесс, напротив, еще не завершен после завершения рекурсивного вызова. Например, эта функция

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

Сгенерирует рекурсивный процесс, так как необходимо выполнить дополнительную работу (++()).

Реализует ли компилятор на самом деле оптимизацию хвостового вызова (TCO) - другой вопрос. AFAIK, на сегодняшний день JVM его не поддерживает. Функция, вызывающая себя в хвостовой позиции, в целом легко оптимизировать (как цикл). Сложность возникает, когда одна функция вызывает другую, а другая вызывает обратно первую функцию и т. Д.

2
Will Ness 8 Сен 2016 в 12:15