МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ ВОЛЖСКИЙ ПОЛИТЕХНИЧЕСКИЙ ИНСТИТУТ (ФИЛИАЛ) ФЕДЕРАЛЬНОГО ГОСУДАРСТВЕННОГО БЮДЖЕТНОГО ОБРАЗОВАТЕЛЬНОГО УЧРЕЖДЕНИЯ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ «ВОЛГОГРАДСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ» КАФЕДРА «ИНФОРМАТИКИ И ТЕХНОЛОГИИ ПРОГРАММИРОВАНИЯ» Д.Н. Лясин, О.Ф. Абрамова Использование рекурсивных вызовов в программах на языке Си Методические указания Волгоград 2012 УДК 004.056 Рецензент: канд. тех. наук доцент В. И. Капля Издается по решению редакционно-издательского совета Волгоградского государственного технического университета Лясин, Д.Н. Использование рекурсивных вызовов в программах на языке Си [Электронный ресурс]: методические указания / Д.Н. Лясин, О.Ф. Абрамова//Сборник «Методические указания» Выпуск 3.-Электрон. текстовые дан.(1файл:207 Kb) – Волжский: ВПИ (филиал) ВолгГТУ,2012.Систем.требования:Windows 95 и выше; ПК с процессором 486+; CD-ROM. Содержатся сведения, необходимые для изучения рекурсивных алгоритмов и их программной реализации на языке Си: дано определение рекурсии, рассмотрены примеры использования рекурсии в различных отраслях знания, объяснены основные принципы работы рекурсивных алгоритмов, а также ограничения на их использование, приведены примеры решения задач с использованием рекурсии. Приведены варианты заданий к лабораторным работам, рассмотрен пример выполнения лабораторной работы. Предназначены для студентов, обучающихся по направлению 230100 "Информатика и вычислительная техника" и направлению 231000 "Программная инженерия" всех форм обучения в рамках курса «Основы программирования».CD-ROM Волгоградский государственный технический университет, 2012 Волжский политехнический институт, 2012 Лабораторная работа N8. Использование рекурсивных вызовов в программах на языке Си. Цель работы: Познакомиться с одним из эффективных способов решения сложных задач – рекурсией. Приобрести навыки программирования с ее использованием. Общие сведения: Очень часто, разрабатывая программу, удается свести исходную задачу к более простым. Среди этих задач может оказаться и первоначальная, но в упрощенной форме. Например, вычисление функции F(n) может потребовать вычисления F(n-1) и еще каких-то операций. Иными словами, частью алгоритма вычисления функции будет вычисление этой же функции. Алгоритм называется рекурсивным, если он прямо или косвенно обращается к самому себе. Часто в основе такого алгоритма лежит рекурсивное определение какого-то понятия. Например, о факториале числа N можно сказать, что N! = N*(N – 1)!, если N > 0 и N! = 1 если N = 0. Это – рекурсивное определение. Любое рекурсивное определение состоит из двух частей. Эти части принято называть базовой и рекурсивной частями. Базовая часть является нерекурсивной и задает определение для некоторой фиксированной части объектов. Рекурсивная часть определяет понятие через него же и записывается так, чтобы при цепочке повторных применений она редуцировалась бы к базе. Всякая функция в языке Си имеет реентерабельный (повторно входимый) код, что позволяет ей обращаться к самой себе непосредственно или через другие функции. Такие обращения называются рекурсивными вызовами или рекурсией. При каждом очередном рекурсивном вызове в специальной области памяти программы, называемой стеком, создается новая копия параметров функции, а также определенных в ее теле автоматических и регистровых переменных. Внешние и статические переменные, имеющие глобальное время существования, сохраняют при этом свои прежние значения и размещение памяти. Несмотря на то, что стандарт языка Си, формально не налагают никакого ограничения на количество рекурсивных обращений, тем не менее, оно практически всегда существует для любых типов ЭВМ, так как каждый новый вызов требует дополнительной памяти из ресурса программного стека. Если количество вызовов (глубина рекурсии) излишне велико, возникает переполнение сегмента стека и операционная 3 система уже не может создать очередного экземпляра локальных объектов функции, что ведет, как правило, к аварийному завершению программы. Рекурсивные вызовы разделяют на прямые и косвенные. При прямой рекурсии функция вызывает сама себя по следующему шаблону: void func() { … func(); … } При использовании косвенной рекурсии вызов осуществляется опосредованно, через вызов другой функции: void func1() { … func(); //вызываем стороннюю функцию … } int func() { … func1(); //рекурсивно вызывается func1 … } В последнем примере, очевидно, косвенно вызываются как функция func(), так и функция func(). Чтобы глубина рекурсии не была бесконечной, рекурсивный вызов всегда подразумевает условие окончания (продолжения) рекурсии. Поэтому рекурсивный вызов (как прямой, так и приводящий к косвенному возврату) должен быть обусловлен: void func() { … if (условие_продолжения_рекурсии) func(); … } Рекурсивный вызов по своему функциональному назначению близок циклическому процессу: и в том и в другом многократно повторяются некоторые действия. В некоторых языках программирования (например, в функциональных) нет операторов цикла, их заменяет рекурсия. Вернемся к рекурсивному определению алгоритма вычисления факториала целого числа. Функция рекурсивного вычисления факториала может быть такой: unsigned int fact (unsigned int n) 4 { unsigned int f; f=(n<=1)?1:n*fact(n-1); return f; } Функцию можно вызвать для получения значения факториала целого числа: int main() { … cout<<fact(5); … } Рассмотрим, как рекурсивные будут последовательно возникать рекурсивные вызовы функций для обращения fact(5): fact(5) рекурсивные вызовы функции fact fact(4) fact(3) fact(2) N=4 N=4 N=3 N=2 f=4*fact(4) f=4*fact(3) f=3*fact(2) f=2*fact(1) 24 6 2 fact(1) N=1 f=1 1 Возвращаемые значения 120 Рисунок 1. Последовательные вызовы функции fact для вычисления 5! Из рисунка видно, что глубина рекурсии равна 5. Условие окончания рекурсивного вызова сформулировано как n<=1, поскольку вычисление факториала чисел 0 и 1 является тривиальным. В терминах рекурсии вычисление 0! и 1! является базой, относительно которой формулируется и вычисляется факториал любого другого натурального числа. Рекурсивным шагом здесь будет вычисление факториала числа, на 1 меньше аргумента функции. Рассмотрим примеры рекурсий в различных отраслях знания. Для математики уже рассмотрено рекурсивное определение факториала. Также рекурсивно определение чисел Фибоначчи: 5 1, если n=1 или n=2; f n= fn-1+fn-2, если n>2 Необходимо правда отметить, что при программной реализации алгоритма вычисления i-го числа Фибоначчи использование рекурсии будет неэффективным в связи с избыточными повторными вычислениями для формирования чисел i-1 и i-2. Эффективнее здесь использовать приемы динамического программирования, рассмотрение которых выходит за рамки данных методических указаний. Замечательным примером рекурсивного (причем бесконечного) построения объекта, являются фракталы. Фрактал — это бесконечно самоподобная геометрическая фигура, каждый фрагмент которой повторяется при уменьшении масштаба. На рис. 2 можно увидеть графическое представление множества Мандельброта, определение которого рекурсивно, так как использует для вычисления новых координат точки z на комплексной плоскости их предыдущие значения: z=z2+c Рисунок 2. Множество Мандельброта. Более тривиален принцип построения кривой Коха (рис.3): берём единичный отрезок, разделяем на три равные части и заменяем средний интервал равносторонним треугольником без этого сегмента. В результате образуется ломаная, состоящая из четырех звеньев длины 1/3 от начального. На следующем шаге повторяем операцию для каждого из четырёх получившихся звеньев и т. д… 6 Рисунок 3. Этапы построения кривой Коха. Каждому с детства известен пример рекурсии из лингвистики, которым является замечательное стихотворение Р. Бернса «Дом, который построил Джек» в переводе С. Маршака Вот дом, Который построил Джек. А это пшеница, Которая в темном чулане хранится В доме, Который построил Джек А это веселая птица-синица, Которая часто ворует пшеницу, Которая в темном чулане хранится … … В каждой новой строфе этого стихотворения в виде вложения присутствует – с незначительными изменениями – предыдущая строфа. В результате в последней строфе сосредотачивается все предшествующее содержание и отчетливо просматривается способ построения очередной строфы. В физике по рекурсивному принципу строятся электрические усилители на автогенераторах. В классическом определении элемента бинарного дерева можно также заметить элементы рекурсии: struct node { int value; //информационная часть узла дерева node *left, *right; // адресная часть узла } 7 Действительно, объявление узла включает в себя указатели на узлы, корневые для левого и правого поддерева, то есть дерево самоподобно. Не случайно алгоритмы обработки в бинарных деревьях преимущественно рекурсивны. Рассмотрим, например, функцию вывода на экран всех элементов бинарного дерева: void walkTree(node * p) { if(p) { walkTree(p->left); //обойти левое поддерево cout << p->value << ' '; //вывод информационной части узла walkTree(p->right); //обойти правое поддерево } } Рассмотрим алгоритм, реализация которого без использования рекурсии будет неэффективной. Очень популярный алгоритм быстрой обменной сортировки по Хоару, который используется в том числе в стандартной библиотеке языка Си (функция qsort), имеет рекурсивную природу. Описание работы алгоритма быстрой обменной сортировки: 1. Выбираем в массиве некоторый элемент, который будем называть опорным элементом. 2. Выполняем операцию разделения массива: реорганизуем массив таким образом, чтобы все элементы, меньшие или равные опорному элементу, оказались слева от него, а все элементы, большие опорного — справа от него. Это можно сделать за один проход по массиву. 3. Рекурсивно упорядочиваем подмассивы, лежащие слева и справа от опорного элемента. Рассмотрим пример работы алгоритма. Сначала разберемся с алгоритмом разделения массива. Пусть задан массив M: 40 80 30 50 60 20 10 Назначим разделитель, например, первый элемент. Будем двигаться от начала массива направо, пока не найдем числа, больше разделителя. Будем двигаться от конца массива влево, пока не найдем числа, меньше разделителя. Отметим разделитель красным цветом, метку движения справа налево - фиолетовым, а слева направо – зеленым. Нашли первые числа, удовлетворяющие условиям поиска: 40 80 30 50 60 20 10 Поменяем их местами: 40 10 30 50 60 20 80 Продолжим движение, находим еще одну удовлетворяющую условиям поиска пару чисел: 40 10 30 50 60 20 80 Меняем их местами: 8 40 10 30 20 60 50 80 Когда левая отметка зайдет за правую, останавливаем движение. 40 10 30 20 60 50 80 Меняем число над правой отметкой (из двух отмеченных оно теперь левее!) местами с разделителем 20 10 30 40 60 50 80 и разделение массива закончено Теперь аналогичную процедуру необходимо проделать над левой и правой половинками массива, при условии, что они имеют длину более одного элемента. Таким образом, базой рекурсии для рассмотренного алгоритма будет подмассив размером 1 или 0 элементов, который, очевидно, не требует сортировки. Шаг рекурсии – сортировка левого и правого подмассивов после операции разделения исходного массива. Программная реализация алгоритма сортировки по Хоару представлена в листинге 1. Листинг 1. Сортировка массива методом быстрой обменной сортировки. #include<iostream.h> #include <stdlib.h> #include<conio.h> int m[10]={96,95,93,45,96,17,64,78,21,19}; /* sorting – функция разделения массива. Прнимает параметры left и right – границы подмассива для разделения. Через параметр middle возвращает вызвавшей функции место разделтеля по итогам разделения */ sorting(int left,int right,int& middle) { int sep=left,t, RightBorder=right; while(left<=right) { while(m[left]<=m[sep]&&left<=RightBorder) left++; while(m[right]>=m[sep]&&right>sep) right--; if (left<right) {t=m[left]; m[left]=m[right]; m[right]=t; } } t=m[sep]; m[sep]=m[right]; m[right]=t; middle=right; } /*hoer – функция сортировки массива m по возрастанию методом Хоара. 9 Параметры задают индексы начала и конца сортируемого подмассива */ hoer(int left,int right) { int mid; if (left<right) //пока подмассив больше 1-го элемента {sorting(left,right,mid);//разделение массива hoer(left,mid-1); //сортировка левого подмассива hoer(mid+1,right); //сортировка правого подмассива } } int main() { clrscr(); randomize(); for(int i=0;i<10;i++) cout<<m[i]<<' '; сout<<'\n'; hoer(0,9); //сортируем весь массив m for(i=0;i<10;i++) cout<<m[i]<<' '; return 0; } Рекурсия – очень мощный инструмент, позволяющий решать широкий спектр задач. Из приведенных выше примеров должен стать понятен принцип применения рекурсии на практике: если решение задачи можно свести к решению аналогичной задачи с меньшим объемом данных (меньшим количеством операций), то здесь может быть эффективна рекурсия. Однако необходимо учитывать, что зачастую использование рекурсивных методов в чистом виде может оказаться неэффективным изза слишком большого количества вариантов рекурсивных спусков. В этом случае рекурсию дополняют такими методами решения задач, как динамическое программирование, отсечение и др. 10 Контрольные вопросы 1. Что такое рекурсия? 2. Приведите примеры рекурсии в различных отраслях знаний. 3. Что общего и в чем разница между циклическим и рекурсивным способами организации вычислений? 4. Объясните термины «база рекурсии» и «шаг рекурсии». Определите базу рекурсии и шаг рекурсии для своей задачи. 5. Что такое «рекурсивное зацикливание»? К каким последствиям оно приводит? 6. Каково главное ограничение при использовании рекурсии? 7. Что такое явная и косвенная рекурсии? 8. Оцените, от чего зависит глубина рекурсии в алгоритме решения вашей задачи. Порядок выполнения работы 1. Ознакомьтесь с теоретическими основами разработки и программной реализации рекурсивных алгоритмов в настоящих указаниях и конспектах лекций. 2. Получите вариант задания у преподавателя. 3. Составьте алгоритм решения задачи согласно варианту задания, оформите его в графической форме. 4. Используя разработанный алгоритм, напишите программу. 5. Отладьте разработанную программу и покажите результаты работы программы преподавателю. 6. Составьте отчет по лабораторной работе. 7. Отчитайте работу преподавателю. Содержание отчета Отчет по лабораторной работе должен содержать следующие сведения: - название и цель работы; - вариант задания; - графическую схему алгоритма решения задачи; - листинг разработанной программы с комментариями; - результаты работы программы 11 Пример выполнения лабораторной работы. Написать рекурсивную функцию printd(), печатающую целое число в виде последовательности символов ASCII (т. е. цифр, образующих запись этого числа), а также вычисляющую сумму цифр в десятичной записи числа: начало printd вход: целый num выход: целый sumTmp sumTmp=0 нет num<0 да Вывод ‘-’ num=-num i=num div 10 нет i<>0 да sumTmp=printd(i) Вывод кода цифры num mod 10 sumTmp=sumTmp+num mod 10 конец printd Рисунок 4. Блок-схема функции посимвольного вывода целого числа #include <stdio.h> #include <conio.h> int printd(int num) /* параметр num - печатаемое число */ { int i, sumTmp=0; if (num < 0) {putchar('-'); num = -num;} if ((i = num/10) != 0) // пока в числе есть цифры sumTmp=printd(i); /* Рекурсивный вызов функции */ putchar(num % 10 + '0');//преобразуем цифру числа в //код символа и выводим на экран sumTmp+=num%10; //накапливаем сумму цифр return sumTmp; 12 } int main() { int value, sum; scanf("%d", &value); sum=printd(value); printf("\nSum=%d", sum); _getch(); return 0; } Варианты заданий. Примечание: при ращении задач обязательно использовать рекурсивный вызов. 1. Найти первые N чисел Фибоначчи двумя способами: с помощью рекурсии и с помощью итерации. Сравнить эффективность алгоритмов. 2. Написать функцию сложения двух чисел, используя только прибавление единицы. 3. Написать функцию умножения двух чисел, используя только операцию сложения. 4. Вычислить сумму элементов одномерного массива. 5. Вычислить несколько значений функции Аккермана для неотрицательных чисел m и n: 6. Написать коэффициентов функцию C(m,n) вычисления по следующей формуле: биномиальных 7. Проверить, является ли фрагмент строки с i-го по j-й символ палиндромом. 8. Подсчитать количество цифр в заданном числе. 9. Вычислить, используя рекурсию, выражение 13 10. Написать функцию которая методом деления отрезка пополам (методом дихотомии) находит с точностью eps корень уравнения f(x) = 0 на отрезке [a,b] ( ). Метод дихотомии определяется следующим образом. Если f(a) и f(b) имеют разные знаки, то между точками a и b существует корень R. Пусть – средняя точка в интервале Если f(m) = 0, то корень R=m. Если нет, то либо f(a) и f(m) имеют разные знаки , либо f(m) и f(b) имеют разные знаки . Если , то корень лежит в интервале В противном случае он лежит в интервале Теперь выполним это действие для нового интервала – половины исходного интервала. Процесс продолжается до тех пор, пока интервал не станет меньше eps. 11. Ввести последовательность чисел (окончание ввода – 0) и вывести их в обратном порядке. 12. Подсчитать сумму цифр в десятичной записи заданного числа. Дополнительные варианты заданий. 1*. Расстояния между городами заданы матрицей (Если между городами i,j есть прямой путь с расстоянием N, то элементы матрицы A(i,j) и A(j,i) содержат значение N, иначе 0). Написать программу поиска минимального пути для произвольной пары городов. 2*. Вычислить определитель матрицы, пользуясь формулой разложения по первой строке: где матрица Bk получается из A вычеркиванием первой строки и k-го столбца. 3*. Реализовать рекурсивный алгоритм построения цепочки из имеющегося набора костей домино. 4*. Несколько человек должны перейти ночью реку через мост. По мосту одновременно могут пройти только 2 человека, в наличии имеется лишь один фонарик (двое переходят мост, обязательно с фонарем, затем один должен вернуться назад с фонариком). Если заданы скорости движения каждого человека si, написать программу, которая предложит схему прохождения всех людей через мост за наименьшее время. 5*. Задан набор слов. Построить из них любую цепочку таким образом, чтобы символ в конце слова совпадал с символом в начале следующего. 6*. Написать процедуру печати всех перестановок из n символов. 14 Литература 1. Березин Б. И., Березин С. Б. Начальный курс С и С++. М.: ДиалогМИФИ, 2007 г., - 288с. 2. Головешкин В. А., Ульянов М. В.. Теория рекурсии для программистов. М.: ФИЗМАТЛИТ, 2006 г. – 296с. 3. Кормен Т., Лейзерсон Ч., Ривест Р., Штайн К. Алгоритмы. Построение и анализ М: Вильямс, 2011 г. – 1296с. 4. Страуструп Б. Язык программирования С++. Специальное издание. СПб.: Бином, 2008 г., 1104с. 5. Фомин С.С. Подбельский В.В. Программирование на языке Си: Учебное пособие. М.:Финансы и статистика, 2007 г. – 600с. 6. Гагарина Л. Г., Колдаев В. Д. Алгоритмы и структуры данных. М.: Финансы и статистика, 2009 г., - 304 с. 15 Учебное издание Дмитрий Николаевич Лясин Оксана Федоровна Абрамова Использование рекурсивных вызовов в программах на языке Си Методические указания План электронных изданий 2012 г. Поз. № Подписано на « Выпуск в свет» 13.02.12. Уч-изд. л. 1,08. На магнитоносителе. Волгоградский государственный технический университет. 400131, г. Волгоград, пр. Ленина, 28, корп. 1.