Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Билеты по проге.docx
Скачиваний:
5
Добавлен:
01.07.2025
Размер:
630.61 Кб
Скачать

Когда не нужно применять рекурсию

Следует избегать рекурсии, где есть очевидное итеративное решение.

  1. Программы, в которых следует избегать алгоритмов рекурсии, характеризуются наличием единственного вызова функции в конце (или начале) - так называемой хвостовой рекурсии. int f(n){ if (n==0) return(1); else return (n*f(n-1); }

Пример: алгоритм вычисление факториала чисел.

Подобный вид рекурсии примечателен тем, что может быть легко заменён на итерацию путём формальной и гарантированно корректной перестройки кода функции.

int fac_times (int n, int acc) {

return (n==0) ? acc : fac_times(n - 1, acc * n);

}

int factorial (int n) {

return fac_times (n, 1);

}

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

 Но если рекурсивный вызов является последней операцией перед выходом из вызывающей функции и результатом вызывающей функции должен стать результат, который вернёт рекурсивный вызов, сохранение контекста уже не имеет значения — ни параметры, ни локальные переменные уже использоваться не будут, а адрес возврата уже находится в стеке. Поэтому в такой ситуации вместо полноценного рекурсивного вызова функции можно просто заменить значения параметров в стеке и передать управление на точку входа. До тех пор, пока исполнение будет идти по этой рекурсивной ветви, будет, фактически, выполняться обычный цикл. Когда рекурсия завершится (то есть исполнение пройдёт по терминальной ветви и достигнет команды возврата из функции) возврат произойдёт сразу в исходную точку, откуда произошёл вызов рекурсивной функции. Таким образом, при любой глубине рекурсии стек переполнен не будет.

  1. Когда в ней встречаются повторяющиеся вычисления (Нахождение чисел Фибоначчи)

СТР 176 ВИРТ

Число вызовов растёт экспоненциально .

Ф(0)=0, ф(1)=1, при n>=2 ф(n) = f(n-1)+f(n-2). Наша функция вынуждена сохранить в стеке результат работы первой вызванной функции (Рез1), дождаться результата выполнения второй функции (Рез2). Извлечь Рез1 из стека, вычислить сумму Рез1 и Рез2.

Не хвостовая:

начало | функция Фиб принимает целое (n), возвращает целое

  если n = 0 вернуть 0

  если n < 3 вернуть 1

  вернуть Фиб(n - 1) + Фиб(n - 2)

конец

Хвостовая:

начало | функция Фиб принимает целое (n), возвращает целое

вернуть Фиб(n, 1, 1, 0)

конец

 

начало  | функция Фиб(n, k, cur, prev)

    | принимает 4 целочисленных

    | аргумента, возвращает целое

  если k == n вернуть cur

  вернуть Фиб(n, k + 1, cur + prev, cur)

конец

Преобразованная компилятором вторая:

начало | функция Фиб принимает целое (n), возвращает целое

  cur := 1; prev := 0

  цикл: для всех k от 1 до n

    tmp := cur

    cur := cur + prev

    prev := tmp

  конец цикла

  вернуть cur

конец

Билет 12. Алгоритмы перебора с возвратом.

Основная идея перебора с возвратом: Построение решения по одному компоненту и выяснение, может ли дальнейшие построение привести к решению. Если да – решение продолжается путем выбора первого допустимого варианта для следующего компонента. Если таких вариантов нет, то такие варианты для всех оставшихся компонентов не рассматриваются. Алгоритм в этой ситуации возвращается к последнему построенному компоненту и заменяет его следующим допустимым компонентом этого уровня.

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

Поиск с возвратом – это метод перебора всех возможных решений для нахождения так называемого множества полных решений. Каждое частичное решение определяет некоторое подмножество полных решений. Метод перебора с возвратом основан на том, что при поиске частичного решения многократно делается попытка расширить так называемое текущее неполное решение. Если расширение невозможно, то на текущем шаге поиска происходит возврат к предыдущему, более короткому текущему неполному решению и делается попытка его расширить, но уже другим способом.

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

Решение задачи методом поиска с возвратом сводится к последовательному расширению частичного решения. Если на очередном шаге такое расширение провести не удается, то возвращаются к более короткому частичному решению и продолжают поиск дальше. Данный алгоритм позволяет найти все решения поставленной задачи, если они существуют. Для ускорения метода стараются вычисления организовать таким образом, чтобы как можно раньше выявлять заведомо неподходящие варианты. Зачастую это позволяет значительно уменьшить время нахождения решения.

Общий алгоритм перебора с возвратом:

void try ( ) { инициализация ( ); do { выбора_варианта ( ): if (подходит) { запись_варианта ( ); if (решение неполное) { try(n+-…) if ( ! удача) стирание_варианта ( ); } } } while ( !(удача || нет_вариантов)); }

Пример – задача о 8 ферзях. (приложение).

Один из типовых алгоритмов решения задачи — использование поиска с возвратом: первый ферзь ставится на первую горизонталь, затем каждый следующий пытаются поставить на следующую так, чтобы его не били ранее установленные ферзи. Если на очередном этапе постановки свободных полей не оказывается, происходит возврат на шаг назад — переставляется ранее установленный ферзь. Сложность экспоненциальная, поскольку алгоритм переборный, 64^N. (92 решения на 8*8). Однако время нахождения решения может быть очень велико даже при небольших размерностях задач.

Способы сокращения перебора