Рекурсивные методы решения задач (С) - метод. указания к ЛР
.pdfМИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ
Брянский государственный технический университет
Утверждаю Ректор университета
_______________О.Н. Федонин
«______»_____________2013 г.
ЯЗЫКИ ПРОГРАММИРОВАНИЯ
РЕКУРСИВНЫЕ МЕТОДЫ РЕШЕНИЯ ЗАДАЧ
Методические указания к выполнению лабораторной работы
для студентов очной формы обучения специальностей 090303 – « Информационная безопасность
автоматизированных систем», 090900 – « Информационная безопасность»
Брянск 2013
УДК 004.43
Языки программирования. Рекурсивные методы решения задач: методические указания к выполнению лабораторной работы для студентов очной формы обучения специальностей 090303 – «Информационная безопасность автоматизированных систем», 090900
– « Информационная безопасность». – Брянск: БГТУ, 2013. – 19 с.
Разработали:
Ю.А. Леонов, к.т.н., доц., Е.А. Леонов, к.т.н., доц.
Рекомендовано кафедрой «Компьютерные технологии и системы» БГТУ (протокол № 2 от 19.09.2013)
3
1. ЦЕЛЬ РАБОТЫ
Целью работы является овладение практическими навыками решения задач с использованием рекурсивных алгоритмов.
Продолжительность работы – 3 ч.00мин.
2. ТЕОРЕТИЧЕСКАЯ ЧАСТЬ
2.1. Основные понятия рекурсии
Рекурсия – это способ определения объекта, при котором он частично или полностью определяется через самого себя. Рекурсивно можно определять не только объекты, но и математические функции.
Функция называется рекурсивной, если ее значение определяется через эту же функцию. Например:
∙ вычисление факториала:
n×(n-1), |
где n>0; |
n!= |
где n=0; |
1, |
|
|
|
∙ вычисление показательной функции:
|
|
an = a×an-1, |
где n>0; |
1, |
где n=0. |
|
|
В программировании рекурсия – это способ организации вычислительного процесса, при котором функция (метод) вызывает саму себя.
Существует два вида рекурсивных вызовов:
∙прямая рекурсия – это способ организации рекурсивного алгоритма, при котором подпрограмма вызывает саму себя;
∙косвенная рекурсия – это способ организации рекурсивного алгоритма, при котором подпрограмма p вызывает подпрограмму q, которая прямо или косвенно вызывает p.
Приведем основные рекурсивные понятия. Рекурсивный спуск – процесс рекурсивных вызовов.
Рекурсивный возврат – процесс возврата из рекурсивных вызовов. Глубина рекурсии – максимальное число вложенных рекурсивных
вызовов.
Текущий уровень рекурсии – число вложенных рекурсивных вызовов в данный момент выполнения программы.
4
2.2Формы рекурсивных подпрограмм
Вобщем, случае любая рекурсивная функция F включает в себя некоторое множество операторов S и один или несколько операторов рекурсивного вызова F.
Выделим следующие формы рекурсивных подпрограмм:
1) форма с выполнением действий до рекурсивного вызова (рекурсивный спуск)
<тип> F(<формальные параметры>)
{
<S – операторы>
if (<условие>) F(<фактические параметры>);
}
2) форма с выполнением действий после рекурсивного вызова (рекурсивный возврат)
<тип> F(<формальные параметры>)
{
if (<условие>) F(<фактические параметры>); <S – операторы>
}
3) форма с выполнением действий до и после рекурсивного вызова (рекурсивный спуск, и рекурсивный возврат)
<тип> F(<формальные параметры>)
{
<S1 – операторы>
if (<условие>) F(<фактические параметры>); <S2 – операторы>
}
Рассмотрим несколько примеров рекурсивных подпрограмм.
Пример 1. Бесконечная рекурсия
static void P()
{
Console.WriteLine("1"); P();
}
5
При вызове функции P произойдёт рекурсивное зацикливание: так как после вывода числа 1, процедура снова начнёт вызывать себя и т.д. до бесконечности.
При каждом вызове процедуры в программный стек помещается адрес данного вызова, в итоге программный стек переполнится, и программа завершится с ошибкой. Таким образом, чтобы рекурсия завершалась, необходимо, чтобы рекурсивный вызов происходил не всегда, а лишь при выполнении некоторого условия.
Пример 2. Рекурсия с выполнением действий на спуске
static void P(int n)
{
Console.Write("{0, 2}", n); if (n > 0) P(n - 1);
}
В данной программе действия осуществляются на рекурсивном спуске.
При вызове функции P(5) произойдёт вывод числа 5, после чего вызовется P(4) – вывод числа 4 и т.д. до вызова P(0), который выведет число 0. Рекурсивный вызов завершится, так как условие n>0 станет ложным. Итак, в результате вызова P(5) на экран будет выведено: 5 4 3
2 1 0.
Таким образом, использование рекурсии позволяет заменить цикл. Отметим, что в некоторых примерах такая замена неэффективна, так как накладные расходы на рекурсивный вызов функции (копирование параметров в программный стек, запоминание адреса возврата и т.д.) намного превосходят затраты на организацию цикла.
Пример 3. Рекурсия с выполнением действий на возврате
static void P(int n)
{
if (n > 0) P(n - 1); Console.Write("{0, 2}", n);
}
В данной программе действия выполняются на рекурсивном возврате.
При вызове P(5) вначале проверится условие n > 0 и, поскольку оно истинно, вызовется P(4). Затем P(3) и т.д. до P(0). Так как при вызове P(0) условие n > 0 уже не выполняется, то произойдет вывод числа 0 и выход из вызова P(0) в вызов P(1) сразу после условного
6
оператора. Далее осуществится вывод числа 1 и выход из вызванной процедуры P(1). Процесс возврата из уже сделанных рекурсивных вызовов продолжится, пока не будет осуществлен выход из вызова P(5). В результате на экран будет выведено: 0 1 2 3 4 5
Схема рекурсивных вызовов в примерах 2 и 3 изображена на рисунке:
Рис. 1. Схема рекурсивных вызовов
На практике все виды рекурсивных подпрограмм находят применение.
3. ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ РЕКУРСИВНЫХ
АЛГОРИТМОВ
3.1. Простые примеры использования рекурсии Пример 4. Вычисление показательной функции
Необходимо построить рекурсивный алгоритм вычисления показательной функции, используя только операции умножения и деления.
Рассмотрим рекурсивное определение an:
1, если n = 0, |
|
||||
(an / 2 )2 , если n > 0, |
n - четное, |
||||
|
|
|
|
|
|
an = |
|
|
|
|
n - нечетное, |
a × an−1 , если n > 0, |
|||||
|
|
n |
|
, если n < 0. |
|
|
|
|
|||
1 a |
|
|
|
|
Реализация также не вызывает затруднений:
static double Power(double a, int n)
{
if (n == 0) return 1;
else if (n < 0) return 1 / Power(a, Math.Abs(n)); else if (n % 2 != 0) return a * Power(a, n - 1);
7
else return Math.Pow(Power(a, n / 2), 2);
}
При программировании рекурсивных алгоритмов необходимо следить за глубиной рекурсии: она не должна быть очень большой, чтобы программный стек не переполнился.
При вычислении a16 функция Power последовательно вызывается для аргументов 16, 8, 4, 2, 1, 0, при этом глубина рекурсии равна 6:
Рис. 2. Схема рекурсивных вызовов для функции Power
При вычислении же a15 функция Power последовательно вызывается для аргументов 15, 14, 7, 6, 3, 2, 1, 0, глубина рекурсии составляет 8.
Пример 5. Нахождение минимального элемента в массиве.
Чтобы найти минимальный элемент в массиве A из n+1 элементов, где n – индекс последнего элемента, достаточно найти минимальный элемент в массиве из первых n элементов, после чего выбрать минимальный из данного минимума и последнего элемента массива. Если в массиве всего один элемент, то он – минимальный.
Запишем данный алгоритм в виде рекурсивной функции MinA:
A[n], n = 0, MinA( A, n) =
MinA( A, MinA( A, n −1)), n > 1.
Реализация повторяет рекурсивное определение:
static int MinA(int[] A, int n)
{
if (n == 0) return A[n]; else
{
int min = MinA(A, n - 1);
if (A[n] < min) return A[n]; else return min;
}
}
8
3.2. Пример некорректного использования рекурсии
Пример 6. Числа Фибоначчи.
Числа Фибоначчи вычисляются для натуральных чисел в случае, когда число отрицательное, то число Фибоначчи не определено.
Как известно, определение чисел Фибоначчи рекурсивно:
|
0, n = 0; |
|
|
|
=1; |
fn |
1, n |
|
= |
+ fn−2 , n ³ 2; |
|
|
fn−1 |
null, n < 0.
Данное определение легко переводится в рекурсивную функцию:
static int? Fib(int n)
{
if (n == 0) return 0;
else if (n == 1) return 1;
else if (n >= 2) return Fib(n - 1) + Fib(n - 2); else return null;
}
Однако, такое «лобовое» решение крайне неэффективно, поскольку содержит большое количество повторяющихся вычислений. Изобразим дерево рекурсивных вызовов для вызова F(6):
Рис. 3. Дерево рекурсивных вызовов
Из рисунка видно, что, например, F(4) вычисляется дважды, F(3) – трижды, F(2) – 5 раз и т.д., и количество повторных вызовов представляет собой последовательность чисел Фибоначчи.
9
3.3. Более сложные примеры использования рекурсии
Пример 7. Ханойские башни.
Классический пример задачи, у которой имеется простое рекурсивное решение, а нерекурсивное решение является существенно более громоздким и дает лишь незначительный выигрыш в эффективности.
Формулировка задачи: имеется три столбика, на одном из которых пирамида из n дисков, причем, меньшие диски лежат на больших дисках. Представим эту задачу графически:
Рис. 4. Графическое представление задачи "Ханойские башни"
Цель задачи: требуется, переложить всю пирамиду с первого на второй столбик, за наименьшее количество ходов, при этом за один ход мы можем перемещать только один диск. Запрещается класть большой диск на маленький диск.
Предположим, что существует решение для n-1 диска. Тогда алгоритм для перекладывания n дисков:
1)перекладываем n-1 диск;
2)перекладываем n-й диск на свободный штырь;
3)перекладываем стопку из n-1 диска, полученную в пункте (1) поверх n-го диска.
Создадим рекурсивную функцию, выводящую на экран всю последовательность перекладываний для заданного количества дисков, где n – количество дисков; tower1, tower2, tower3 – номера столбиков;
Перекладывание производится со столбика tower1 на tower2 при вспомогательном столбике tower3.
static void MoveTowers(int tower1, int tower2, int tower3, int n)
{
10
if (n == 0) return;
MoveTowers(tower1, tower3, tower2, n - 1); Console.WriteLine("Переместить диск со стержня {0} на стержень {1}", tower1, tower2);
MoveTowers(tower3, tower2, tower1, n - 1);
}
Если вызывать рекурсивную функцию MoveTowers следующим образом:
MoveTowers(1, 2, 3, 6);
то на экран выведется алгоритм перемещения шести дисков со столбика – 1 на столбик – 2 при вспомогательном столбике – 3.
Нетрудно показать, что глубина рекурсии равна n, а количество перемещений дисков составляет 2n – 1.
Пример 8. Быстрая сортировка
Алгоритм быстрой сортировки – один из самых производительных и часто используемых алгоритмов сортировки.
Основная идея алгоритма состоит в следующем. Первый шаг
Выбирается некоторый опорный элемент x, относительно которого переупорядочиваются остальные элементы массива.
Переупорядочение осуществляется следующим образом:
∙все элементы, меньшие x, переставляются перед x, а больше или равные x – после. В итоге массив оказывается, разбит на две части;
∙после к первой и второй частям рекурсивно применяется алгоритм быстрой сортировки до тех пор, пока в каждой части не останется по одному элементу. Желательно выбрать элемент x таким, чтобы количество элементов в первой и во второй части было примерно одинаковым.
Второй шаг
Введем два индекса i и j, указывающие соответственно на первый и последний элементы массива A вначале выполнения алгоритма. Увеличивая i, найдем элемент A[i], больше чем x. Затем, уменьшая j, найдем элемент A[j], меньше чем x. Такие элементы всегда найдутся: в крайнем случае, им будет сам элемент х.