Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Основная книга по С++й.doc
Скачиваний:
16
Добавлен:
28.10.2018
Размер:
2.07 Mб
Скачать

Int nodWhile (int m, int n)

{

int x;

while (n != 0)

{

m = m % n;

x = m;

m = n;

n = x; //меняем местами m и n – по алгоритму Евклида

}

return(m);

}

Итак, программа, использующая написанные функции будет следующей:

Int nodWhile (int m, int n)

{

int x;

while (n != 0)

{

m = m % n;

x =m;

m =n;

n =x; // меняем местами m и n

}

return(m);

}

int NOD (int m, int n)

{

if (n == 0) return(m);

else return(NOD(n,m % n));

}

Int main()

{

int m,n;

printf("Введите М\n");

scanf("%d", &m);

printf("Введите N\n");

scanf("%d", &n);

printf("n=%d m=%d NOD=%d\n", n, m, NOD(m,n));

printf("n=%d m=%d NODWhile=%d\n", n, m,NODWhile(m,n));

printf("n=%d m=%d NOK=%d\n", n, m, ceil(m*n/NOD(m,n)));

}

Пример 31. Напишем функции рекурсивного и не рекурсивного вычисления факториала числа и вывода чисел Фибоначчи.

Напомним, факториалом числа N называется функция, обозначаемая, как N!, которая последовательно вычисляет произведение всех целых чисел от 1 до N включительно, т.е. . Для приближенного вычисления факториала больших чисел N может использоваться формула Стирлинга: .

Ряд Фибоначчи определяется так: это последовательность натуральных чисел, в которой первые два числа равны 1, а третье число ряда и последующие числа определяются, как сумма двух предыдущих чисел. Т.е. переходя от словесного описания можно сразу задать рекуррентную формулу для чисел Фибоначчи:

Найдем рекуррентное соотношение для вычисления функции факториала. Для этого заметим, что 1!=1, N!=(N-1)!N. Таким, образом:

Алгоритм вычисления чисел Фибоначчи без рекурсии основывается на том, что необходимо запомнить в переменных prelast, last два последних числа Фибоначчи, чтобы вычислить следующее. Затем предыдущее можно «забыть» и перейти к вычислению следующего:

  1. Начало

  2. prelast = 1; last = 1;

  3. Если n > 2, то сделать (n − 2) раз:

    1. c = prelast + last;

    2. prelast = last;

    3. last = c;

  4. Выдать ответ last;

  5. Конец

Вычисление чисел Фибоначчи также осуществляется по формуле Бине:

. Это выражение всегда принимает целые значения.

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

Приведем код программы для вычисления указанных функций рекурсивно и без рекурсии.

int Fib (int n)

{

if ((n == 1) || (n == 2)) return(1);

else return(Fib(n-1)+Fib(n-2));

}

int FibCircle (int n)

{

int prelast,last,c,i;

if ((n == 1) || (n == 2)) return(1);

else

{

prelast = 1;

last = 1;

for(i=1;i<n-2;i++)

{

c = prelast + last;

prelast = last;

last = c;

}

}

return(last);

}

int FibBine (int n)

{

return(ceil(1/sqrt(5) * (pow((1+sqrt(5))/2,n) - pow((1-sqrt(5))/2,n))));

}

int Factorial (int n)

{

if (n == 1) return(1);

else return(Factorial(n-1)*n);

}

int FactorialNotRec (int n)

{

int i,f = 1;

for(i=1;i<n;i++)

{

f = f*i;

}

return(f);

}

int FactorialStirling (int n)

{

return(ceil(pow(n,n)*sqrt(2*M_PI*n)/exp(n)));

}

int _tmain(int argc, _TCHAR* argv[])

{

int i,n;

printf("Введите N:\n");

scanf("%d", &n);

for(i=1;i<n;i++)

{

printf("FibR[%d]\n", Fib(i));

printf("FibC[%d]\n", FibCircle(i));

printf("FibB[%d]\n", FibBine(i));

}

printf("Recurs : %d! = %d\n", n, Factorial(n));

printf("Circle : %d! = %d\n", FactorialNotRec(n));

printf("Stirling : %d! = %d\n", FactorialStirling(n));

system("pause");

}

Интересно, что числа Фибоначчи были придуманы математиком Леонардо Фибоначчи в начале XXIII века, когда он размышлял над вопросом: «Сколько пар кроликов в один год родится от одной пары». В последствии в науке, искусстве, природе, во многих других приложениях и сферах деятельности человека числа Фибоначчи получили широкое распространение, порой даже мистическое. Например, выяснилось, что в расположении листьев на ветке семян подсолнечника, шишек сосны проявляет себя ряд Фибоначчи. Закономерности на основе ряда проявляются в энергетических переходах элементарных частиц, в строении некоторых химических соединений, в планетарных и космических системах, в генных структурах живых организмов. Закономерности на основе ряда Фибоначчи есть в строении отдельных органов человека и тела в целом, а также проявляются в биоритмах и функционировании головного мозга и зрительного восприятия, в строении молекулы ДНК. С помощью этого ряда нашли закономерность и порядок в расстояниях между планетами солнечной системы. Однако один случай, который, казалось бы, противоречил закону: между Марсом и Юпитером не было планеты. Сосредоточенное наблюдение за этим участком неба привело к открытию пояса астероидов. Пропорции построения Египетских пирамид также основываются на ряде Фибоначчи. На основе чисел Фибоначчи получено золотое сечение (золотая симметрия) – это пропорции, применяемые в архитектуре, живописи. Например, известная картина Леонардо да Винчи «Мона Лиза» буквально «испещрена» такими пропорциями.

Приведем еще несколько примеров рекурсивного задания функций:

1.

2.

3.

4.

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

Анализ трудоемкости рекурсивных алгоритмов

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

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

Рассмотрим пример для функции вычисления факториала N!=F(N) при N=5 (рис.34).

Цепочка рекурсивных возвратов

(рекурсивный подъем)

Цепочка рекурсивных вызовов (рекурсивный спуск)

Рис.34. Дерево рекурсии при вычислении факториала числа 5

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

Дерево рекурсивных вызовов может иметь и более сложную структуру, если на каждом вызове порождается несколько обращений. Например, дерево рекурсий для чисел Фибоначчи Ф(N) при N=6 представлено на рис.35:

При вычислении значения Ф(6) будут вызваны процедуры вычисления Ф(5) и Ф(4). В свою очередь, для вычисления последних потребуется вычисление двух пар Ф(4), Ф(3) и Ф(3), Ф(2). Можно заметить, что Ф(3) вычисляется три раза, Ф(2) – пять раз. Если рассмотреть вычисление Ф(n) при больших n, то повторных вычислений будет очень много. Это и есть основной недостаток рекурсии – повторные вычисления одних и тех же значений. Кроме того, с рекурсивными функциями может быть связана одна серьезная ошибка: дерево рекурсивных вызовов может оказаться бесконечным и компьютер «зависнет». Важно, чтобы процесс сведения задачи к более простым операциям когда-нибудь заканчивался, т.е. существовало корректное условие выхода из рекурсии.

Следует понимать, что при каждом вызове любой подпрограммы в особую область памяти, выделенной операционной системой программе и называемой стеком, записывается адрес инструкции, которая будет выполнена после завершения работы рекурсивной функции, а также параметры, передаваемые рекурсивной функции. Освобождение этой памяти происходит только при выходе из функции. Поэтому, если при вызове рекурсивной подпрограммы в стек заносится M байт, а размер стека N байт, то после K вызовов, примерно равном N/M – программа прекратит работу. Не смотря на то что, объемы оперативной памяти в настоящее время достаточно большие, существует вероятность переполнения стека при неправильных условиях завершения рекурсии, что грозит зависанием программы и операционной системы.

Чтобы избавиться от проблемы повторных вычислений, нужно запоминать найденные значения, например в массиве, и таким образом не вычислять их каждый раз заново. Для этого придётся активно использовать память, и осуществлять дополнительные проверки – вычислено или нет нужное число. Тогда дерево рекурсий сведется к более простому виду (рис.36):

Например, для оптимизации рекурсивного алгоритма нахождения чисел Фибоначчи необходимо добавить следующие действия:

  • создать глобальный массив FCalculated первоначально состоящий из нулей, в котором будут храниться уже найденные числа;

  • после вычисления очередного числа Фибоначчи Ф(n) поместить его значение в FCalculated [n];

  • в начале рекурсивной процедуры сделать проверку на то, что FCalculated [n] = 0. Если FCalculated [n] <> 0, то вернутся из рекурсивной функции со значением FCalculated [n] в качестве результата, иначе приступить к рекурсивному вычислению Ф (n).

Такая рекурсия с запоминанием результатов промежуточных шагов иногда называется динамическим программированием сверху.

const int NFib = 300; //Максимальный размер массива с числами Фибоначчи

// описываем глобальный массив для запоминания результатов рекурсии