
- •Алгоритм
- •3. Этапы решения задач на эвм.
- •4. Структу́рное программи́рование
- •6. Типизированные указатели.
- •7.Ссылочный тип.
- •8.Перечислимый тип.
- •10.Процедуры ввода-вывода. Потоковый ввод-вывод.
- •13.Условный оператор. Оператор выбора.
- •14.Операторы безусловного перехода.
- •16.Циклические программы. Вложенные циклы.
- •17.Глобальные и локальные переменные.
- •18.Функции. Механизм передачи параметров.
- •19.Вложенные функции. Рекурсия.
- •20.Область видимости и время жизни переменной.
- •21.Фактические и формальные параметры.
- •22.Перегрузка функций.
- •23.Шаблоны функций.
- •24.Понятие модуля. Преимущества модульного программирования. Структура модуля.
- •25.Пример модуля. Способ использования модуля.
- •27.Символьный тип данных. Строковые массивы. Способы обработки.
- •28.Основные операции над строками. Функции для работы со строками.
- •29.Структуры. Работа со структурами. Примеры.
- •30.Массивы структур. Особенности обработки. Примеры.
- •31.Файлы. Виды файлов. Файловая переменная. Общая схема работы с файлами.
- •33.Текстовые файлы. Функции обработки для текстовых файлов.
- •34.Бинарные файлы. Функции для работы с бинарными файлами.
- •35.Статическая и динамическая память.
- •37.Алгоритмы и методы сортировки: оценка эффективности алгоритма.
- •38.Сортировка выбором.
- •43.Алгоритмы и методы поиска в отсортированном массиве данных.
19.Вложенные функции. Рекурсия.
Рекурсивные функции
Язык C++ предоставляет возможность написания рекурсивных функций, однако целесообразность применения рекурсии оставляется на усмотрение программиста. Как правило, рекурсивные алгоритмы применяются там, где имеется явное рекурсивное определение обрабатываемых данных. Не будем отступать от традиции и рассмотрим функцию факториала n!. Как правило, в программировании её определяют как произведение первых n целых чисел:
n! = 1 * 2 * 3 * ... * n
Такое произведение можно легко вычислить с помощью итеративных конструкций, например, оператора цикла for (листинг 7.14).Код C++
1
2
3
4
5 Листинг 7.14. Итеративная функция вычисления факториала
long Fact(int k)
{ long f; int i; for(f = 1, i = 1; i<k; i++) f*= i;
return f;
}
Однако существует также другое (математическое) определение факториала, в котором используется рекуррентная формула и которое имеет такой вид:
0! = 1
n > 0 n! = n*(n-1)!
Если для факториала первое (итеративное) определение может показаться проще, то для чисел Фибоначчи рекурсивное определение
F(1) = 1,
F(2) = 1,
n > 2 F(n) = F(n-1) + F(n-2)
выглядит для вычислений гораздо лучше, чем прямая формула.
Понятно, что организовать вычисления по рекуррентным формулам можно и без использования рекурсии. Однако, как видно на примерах, представленных в [Шалыто-Программист], преобразование естественной рекурсивной формы в итеративную – довольно сложная задача. Использование рекурсии позволяет легко (почти автоматически) запрограммировать вычисления по рекуррентным формулам. Например, рекурсивная функция для вычисления факториала n! имеет следующий вид (листинг 7.15).Код C++
1
2
3
4
5 Листинг 7.15. Рекурсивная функция вычисления факториала
long Fact(int k)
{ if (k==0) return 1;
return (k*Fact(k-1)); // рекурсивный вызов !
}
Аналогично, по указанному определению легко написать функцию вычисления чисел Фибоначчи (листинг 7.16).Код C++
1
2
3
4
5 Листинг 7.16. Рекурсивная функция вычисления чисел Фибоначчи
long Fibo(int k)
{ if ((k==2)||(k==1)) return 1;
return (Fibo(k-1)+Fibo(k-2)); // рекурсивный вызов !
}
Рекурсивной функцией называется функция, вызывающая саму себя в своем теле. Необходимо ещё раз подчеркнуть, что «самовызов» будет рекурсивным только в том случае, если находится в теле функции. Как мы видели ранее, «самовызов» в списке параметров не является рекурсивным.
Обычно различают прямую и косвенную рекурсию. Если в теле функции явно используется вызов той же самой функции, то имеет место прямая рекурсия (self-calling), как в приведенных примерах. Если две или более функций взаимно вызывают друг друга, то имеет место косвенная рекурсия. Обычно косвенная рекурсия возникает при реализации программ синтаксического анализа методом рекурсивного спуска.
Прекрасным примером рекурсивной функции является быстрая сортировка Хоора. Сама схема алгоритма является рекурсивной, поэтому написать итеративную программу достаточно сложно, что можно увидеть в книге Н.Вирта [9] и в [30]. Схема функции выглядит так:Код C++
1
2
3
4
5
6 void QuickSort(A, 1, n)
{ //--Выбрать разделяющий элемент с номером 1<k<n
//--Разделить массив А относительно k-го элемента
QuickSort(A, 1, k-1); //-рекурсивный вызов с левой частью
QuickSort(A, k+1, n); //-рекурсивный вызов с правой частью
}
Есть большое количество традиционных «игрушечных» задач (ханойские башни, расстановка ферзей и т.п.), которые анализируются в литературе [9,34] для демонстрации рекурсии. Мы не будем на них останавливаться — гораздо интереснее рассмотреть типично итеративные алгоритмы, которые можно представить рекурсивным образом. В принципе, любой цикл можно заменить эквивалентной рекурсивной программой. В качестве примера рассмотрим рекурсивную реализацию (листинг 7.17) функции вычисления длины строки (см. листинг 3.11).Код C++
1
2
3
4
5 Листинг 7.17. Рекурсивная функция вычисления длины строки
unsigned int Length (char *s)
{ if (*s==0) return 0; // можно просто !(*s)
else return 1+Len(s+1);
}
Вызов такой функции абсолютно ни в чём не отличается от вызова аналогичной итеративной, например: Код C++
1 cout<<Len(“1234567890”);
на экран совершенно правильно выводится 10. Работа рекурсивных функций обычно имеет несколько «мистический» оттенок, поэтому не будем пока в деталях разбирать работу этой функции, но обратим внимание на несколько важных моментов:
1. Цикла в функции нет – вместо него у нас имеется рекурсивный вызов. Таким образом, явное повторение заменяется неявным – рекурсивным.
2. Параметр, который является указателем, в рекурсивном вызове увеличивается, перемещаясь к следующему символу.
3. Чтобы такое увеличение не происходило до бесконечности, имеется в наличии условие окончания рекурсии — в операторе if проверяется достижение конца строки.
Эту же функцию можно написать несколько иначе:Код C++
1
2
3
4 unsigned int Length (char *s)
{ if (*s) return 1+Len(s+1);
else return 0;
}
И здесь мы наблюдаем те же три перечисленных выше особенности:
1. Вместо цикла имеется рекурсивный вызов;
2. Параметр в рекурсивном вызове изменяется;
3. Условие окончания заменено условием продолжения.
Прежде, чем делать выводы, рассмотрим еще один пример – последовательную обработку файла. Пусть открытие и закрытие файла выполняются в главной программе, а обработка – в отдельной функции, которая последовательно читает записи файла и обрабатывает их. Традиционно такая обработка выполняется в цикле следующего вида:Код C++
1
2
3
4 while (не конец файла)
{ //-- читать запись
//--обработать запись
}
Попробуем написать рекурсивную процедуру без использования цикла. Пусть потоковая переменная имеет имя f. Файл представляет собой файл строк – исходный текст самой этой программы, который находится в том же каталоге, что и исполняемая программа (листинг 7.18).Код C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14 Листинг 7.18. Рекурсивная функция обработки текстового файла
void ReadFile(ifstream &f)
{ char s[100]; //--буфер для строки
getline(f,s); //--читаем строку
cout<<s; //--обработка строки
if (!f.eof()) //-пока не конец файла
ReadFile(f); //--рекурсивный вызов
}
int main(void)
{ ifstream f("recurs.cpp");
ReadFile(f);
f.close();
return 0;
}
Как мы видим, функция ReadFile несколько отличается от приведенных выше функций – отсутствует явное изменение параметра. Параметр-то все равно изменяется, но неявно — как состояние потока f при чтении. Как и в предыдущих случаях, цикл заменен рекурсивным вызовом, и присутствует условие продолжения.