Файл: А. Б. Шнейвайс основы программирования.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 06.12.2023

Просмотров: 319

Скачиваний: 6

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

2.1.2
Нерекурсивная и рекурсивная ФОРТРАН-функции расчета n!
program tstfactor; implicit none
!
Файл tstfactor.for interface function factorn(n) result(w); real(8) w; integer n end function factorn recursive function factorr(n) result(w); real(8) w; integer n;
end function factorr function factornB(n); real(8) factornB; integer n end function factornB
end interface integer n; real(8) rn, rr, rb write(*,*) ’введи n:’; read (*,*) n; write(*,*) ’ n=’,n rn=factorn(n); rr=factorr(n); rb=factornB(n); write(*,1000) rn, rr, rb rn=factorn(n); rr=factorr(n); rb=factornB(n); write(*,1000) rn, rr, rb
1000 format(1x,’ rn=’,g15.7,’
rr=’,g15.7,’
rb=’,g15.7)
end program tstfactor function factorn(n) result(w); implicit none
! factorn - нерекурсивная real(8) w; integer n, i
!
функция расчета n!
w=1; do i=1,n; w=w*i; enddo end function factorn recursive real(8) function factorr(n) result(w); ! factorr1 - рекурсивная.
implicit none; integer n data w /1.0d0/
if (n.eq.0) then;
w=1; else;
w=n*factorr(n-1); endif end function factorr function factornB(n); implicit none
! factornB - нерекурсивная real(8) w,
factornB; integer n, i
!
функция расчета n!
data w /1.0d0/
!
ОПАСНЫЙ вариант !!!
do i=1,n; w=w*i; enddo factornB=w end function factornB
введи n:
n=
5
rn=
120.0000
rr=
120.0000
rb=
120.0000
rn=
120.0000
rr=
120.0000
rb=
14400.00
Внимание! В ФОРТРАН-учебниках можно встретить фразу, что опе- ратор data предназначен для задания начальных значений. Иногда его применяют для задания начального значения какой-нибудь локальной переменной той или иной процедуры (например, расчёта факториала:
data w /1.0d0/), что при повторном вызове процедуры может привести к неожиданным результатам (см., результат второго вызова процедуры factornB(5), который оказался равен 1440).
59

Дело в том, что присваивание data w /1.0/ выполняется компи- лятором, а не при работе программы. Поэтому после первого вызова factornB в w окажется не единица, а 120, а после второго — 1440.
На примере factornB видно, что описание result(w), имеющееся в factorn и factorr, принципиально важно, так как в этом случае компи- лятор на наличие в тексте процедуры оператора data w /1.0/, сообщит:
data w /1.0d0/
1
ошибка: DATA attribute conflicts with RESULT attribute in ’w’ at (1)
Нельзя значение переменной задавать оператором data, если она пред- назначена для возврата результата.
2.1.3
Схема вызовов при расчете f actorr1(3)
fr1:=factorr1(3)
:=-->3* factorr1(2)
:=-->2*factorr1(1)
:=-->1*factr(0)
1 * 1
найден factr(0);
2 *
1 <----------------- найден factr(1);
3 *
2
<----------------------------- найден factr(2);
6
<-------------------------------------------- найден factr(3),
fr:=
6
это значение и возвращается в главную программу.
Замечание: Часто спрашивают: “Почему 0!=1 и 1!=1 ?”
Факториал представляет собой частный случай широко использую- щейся в математике Гамма-функции (интеграл Эйлера):
Γ(x) =
Z

0
e
−t t
x−1
dt
,
значение которой при натуральном x равно соответствующему фактори- алу, именно
Γ(x + 1) = x! .
В частности,
0! = Γ(1) =
Z

0
e
−t dt = 1
,
1! = Γ(2) =
Z

0
e
−t
· tdt = 1 .
60


2.1.4
Достоинства и недостатки рекурсивного описания.
1. Рекурсивное описание функции можно заменить нерекурсивным.
2. Рекурсивность – это не свойство функции, а свойство ее описания.
3. “Какой способ описания выгоднее?” – решать программисту!
Достоинства
Недостатки определяет сколь-угодно как правило, работает медленнее много объектов через нерекурсивного из–за временных конечное высказывание;
затрат на повторные обращения;
записывается фактически не всегда переход на нерекурсивное прямо по описанию описание так же прост, как при алгоритма;
описании f actorn(n);
текст легче воспринимается отладка может оказаться непростой,
пользователем особенно при сложной рекурсии
61

2.2
Понятие глубины рекурсии. Алгоритм Евклида.
Глубина рекурсии – это количество промежуточных вызовов функции в процессе ее вычисления для данного аргумента.
Например, factorr1(3) помимо начального вызова активирует еще три рекурсивных: factorr1(2), factorr1(1), factorr1(0), т.е. рекурсия имеет глубину в три уровня. Для функции n! глубина рекурсии при любом n очевидна. К сожалению, это – исключение. Даже в простом алгоритме
Евклида поиска наибольшего общего делителя двух натуральных чисел a и b глубина рекурсии не очевидна.
2.2.1
Нерекурсивная и рекурсивная СИ-функции поиска НОД.
В соответствие с определением 4 из введения к этой главе имеем:
#include
using namespace std;
int nodn(int,int); int nodr(int,int);
int main()
{int a, b, nn, nr;
cout<<"введите a, b:"<cin>>a>>b;
cout<<" a="<b="<nn=nodn(a,b);
nr=nodr(a,b);
cout<<" nodn="<nodr="<return 0;
}
int nodn(int a, int b)
// Нерекурсивная функция
{int r;
// расчета НОД(a,b).
while (b)
{ r=a%b; a=b; b=r; }
return a;
}
int nodr(int a, int b)
// Рекурсивная функция
{ if (a// расчета НОД(a,b).
else
{ if (b) return nodr(b,a%b);
else return a;
}
}
введите a, b:
// Результат C++ тестирования nodn и nodr:
85 51
a=85
b=51
nodn=17
nodr=17 62

2.2.2
Нерекурсивная и рекурсивная ФОРТРАН-функции поиска НОД.
program tstevclid; implicit none
!
Программа тестирующая interface
!
нерекурсивную и
integer function nodn(a,b) result(w) !
рекурсивную функции integer a, b
!
поиска НОД(a,b)
end function nodn recursive integer function nodr(a,b) result (w); integer a, b end function nodr integer function nodn1(a,b); integer a, b; end function nodn1
end interface integer a, b, nn, nr, n1, n2; write(*,*) ’введи a, b:’; read (*,*) a, b write(*,’(i29,i7)’) a, b write(*,’(17x,"НОД(a,b)",3x,"a",5x,"b")’)
nn=nodn(a,b);
write(*,’(a,i5,i7,i7)’) ’
nodn ( a , b )=’,nn, a, b nr=nodr(a,b);
write(*,’(a,i5,i7,i7)’) ’
nodr ( a , b )=’,nr, a, b n1=nodn1((a),(b)); write(*,’(a,i5,i7,i7)’) ’
nodn1((a),(b))=’,n1, a, b n2=nodn1(a,b);
write(*,’(a,i5,i7,i7)’) ’
nodn1( a , b )=’,n2, a, b nn=nodn(a,b);
write(*,’(a,i5,i7,i7)’) ’
nodn ( a , b )=’,nn, a, b end program tstevclid integer function nodn(aa,bb) result(w)
!
Нерекурсивная функция implicit none; integer aa, b, bb, r
!
поиска НОД (aa,bb)
w=aa; b=bb; do while (b/=0); r=mod(w,b);
w=b;
b=r; enddo end function nodn recursive function nodr(a,b) result(w)
! Рекурсивная функция implicit none; integer a, b, w
! поиска НОД(aa,bb)
if (a.lt.b) then; w=nodr(b,a)
else; if (b.ne.0) then if (mod(a,b).ne.0) then; w=nodr(b, mod(a,b))
else; w=b endif;
else; w=a endif endif end function nodr integer function nodn1(a,b); implicit none !
Нерекурсивная функция integer a, b, r
!
поиска НОД (aa,bb)
do while (b/=0); r=mod(a,b);
a=b;
b=r; enddo nodn1=a end function nodn1
введи a, b:
! Вспомнить о разных принципах
85 51
! передачи аргументов процедурам
НОД(a,b)
a b
! в ФОРТРАНе и СИ.
Ответить:
nodn ( a , b )=
17 85 51
! Почему NODN и NODR не меняют nodr ( a , b )=
17 85 51
! значения аргументов после nodn1((a),(b))=
17 85 51
! завершения работы, а NODN1 --- nodn1( a , b )=
17 17 0
! только, если фактические nodn ( a , b )=
17 17 0
! заключены в круглые скобки.
63


2.3
Пример задачи неразрешимой без рекурсии.
Напечатать вводимые числа в обратном порядке.
Если можно использовать массив, то нерекурсивное решение дается про- сто распечаткой элементов массива от последнего до первого. Однако, ис- пользование массива запрещено и заранее не известно сколько чисел будет вводится. Выберем в качестве признака окончания последователь- ности вводимых чисел ввод нуля. Кажется, что задача неразрешима. Но c рекурсией решение до удивления просто.
2.3.1
СИ-решение:
int revers();
int main()
{ revers();
return 0; }
#include
using namespace std;
int revers()
{ int a;
cin>>a; if (a) { revers();
cout<
"; } }
2.3.2
ФОРТРАН-решение:
program tstrevers; implicit none; call revers; end program tstrevers recursive subroutine revers; implicit none integer a read(*,’(i2$)’) a; if (a.ne.0) then; call revers; write(*,’(i2$)’) a; endif end subroutine revers
Символ $ в форматном фрагменте операторов ввода(вывода) обеспечи- вает возможность набора(вывода) данных в одной строке.
1 2 3 4 5 6 0
//
Результат работы tstrevers
6 5 4 3 2 1

1   2   3   4   5   6   7   8   9   ...   23

Как работает revers?
1. Тело процедуры revers cостоит из раздела описания переменной a и раздела операторов, выполняемых при вызове процедуры.
2. Переменная a – внутренняя локальная переменная процедуры.
3. При очередном вызове revers под a выделяется новая ячейка опера- тивной памяти. Это выделение происходит при каждом очередном рекурсивном вызове процедуры, то есть динамически.
64

4. При завершении очередного вызова revers ячейка, хранящая теку- щее a, высвобождается из под ведения и процедуры, и вызывающей её программы, и может быть, например, передана операционной си- стемой вообще какой–то другой программе).
5. При очередном выходе из revers высвобождается как раз та ячейка,
которая была зафрахтована последней.
6. Структура данных из последовательно выделяемых в оперативной памяти ячеек под переменную a при рекурсивных вызовах revers или освобождаемых при завершении таковых называется:
стек или магазин или одноконцовая очередь.
Стек – single top ended queue.
2.4
Немного о стеке.
Стек – это динамическая структура данных, которая состоит из перемен- ного числа компонент одинакового типа. Компоненты стека извлекаются по принципу LIFO:
последним вошел (last in) – первым вышел (first out)
Принцип FIFO: первым вошел (first in) – первым вышел (first out)
определяет другую динамическую структуру – привычную нам очередь.
Стек – одна из наиболее употребительных стуктур данных в систем- ном программировании.
При вызове рекурсивных процедур стековая структура данных обра- зуется автоматически, не требуя своего явного описания.
В языках ПАСКАЛЬ, СИ (а теперь и современный ФОРТРАН) есть средство, позволяющее непосредственно организовывать в оперативной памяти всякие динамические структуры данных (в частности, и стек).
Примерами стековой организации могут служить:
1. известный во всем мире рожок (магазин) автомата Калашникова;
2. тупиковое ответвление от железнодорожного полотна (выгодное для переприцепки тепловоза к противоположному концу поезда);
3. хорошо знакомая с детских лет каждому игрушка – пирамидка из колечек.
65

2.5
Задача о ханойской башне.
Задача излагается во многих учебных пособиях (например, [17], [18]).
Её рекурсивное решение несравнимо проще нерекурсивного.
Задача. Даны три стержня. На первом в виде пирамиды надето n колец в строгом порядке увеличения их диаметра к основанию пирами- ды. Необходимо всю пирамиду перенести с первого стержня на третий
(второй стержень рабочий), соблюдая два закона:
1. Кольца можно перемещать только по одному.
2. Никогда нельзя класть большее кольцо на меньшее.
Требуется разработать программу, которая печатала бы: с какого стерж- ня на какой нужно переносить очередной диск, чтобы добиться в конце концов полного перемещения пирамиды с первого стержня на третий.
2.5.1
Уяснение ситуации.
Рассмотрим для начала простейшие частные случаи, которые помогут разработать стратегию переноса:
1) на первом стержне одно кольцо. Пирамида из одного кольца пере- мещается за один перенос.
2) на первом стержне два кольца (сначала переносим меньшее кольцо с 1-го на 2-ой; затем самое большое кольцо с 1-го на 3-ий и, нако- нец, меньшее кольцо со 2-го на 3-ий). Так что перенос двуколечной пирамиды осуществлен за три переноса.
3) на первом стержне три кольца (перемещение за семь переносов):
I перенос кольца с первого стержня на третий;
II перенос кольца с первого стержня на второй;
III перенос кольца с третьего стержня на второй;
IV перенос кольца с первого стержня на третий;
V перенос кольца со второго стержня на первый;
VI перенос кольца со второго стержня на третий;
VII перенос кольца с первого стержня на третий.
66


Заметим, что перемещения I – III по сути дела привели к переносу ба- шенки из двух верхних колец с первого стержня на второй. Затем был осуществлен перенос самого нижнего и единственного кольца первого стержня на третий стержень. После этого осталось лишь перенести ба- шенку из двух колец со второго стержня на третий, используя свободный первый стержень в качестве рабочего. Ясно, что это ровно столько же перемещений, сколько было при переноске I – III. Так что перенос n колец распадается на три этапа:
1. Перенос башенки из n − 1 кольца с первого стержня на второй;
2. Перенос одного кольца с первого стержня на третий;
3. Перенос башенки из n − 1 кольца со второго стержня на третий.
Так что башню из n − 1 кольца придется переносить дважды: один раз с 1-го стержня на 2-ой, используя в качестве рабочего 3-ий, а затем еще раз со 2-го на 3-ий, используя в качестве рабочего 1-ый.
Таким ообразом, алгоритм должен уметь переносить кольца с задан- ного колышка, например, с номером p на колышек с номером q, исполь- зуя в качестве рабочего оставшийся колышек. Легко заметить, что номер этого рабочего колышка просто вычисляется по формуле w = 6 − p − q.
Видим, что алгоритм распадается на две задачи:
1. одну простую – перенос одного кольца;
2. другую – перенос башни.
Решение обеих оформим процедурами. Простую назовем ring. У нее два входных аргумента: номера исходного и принимающего колышков.
У второй процедуры (назовем ее tower) будет три входных аргумента:
m (количество колец в башне) и номера колышков p и q (поставляющего кольца и принимающего их соответственно). Заметим, что рекурсивная процедура tower должна дважды вызывать сама себя так как башню из m − 1 кольца придется переносить дважды.
67

2.5.2
СИ-решение:
#include
using namespace std;
void hanoi(int, int, int); void ring(int,int);
int main()
{ int n; cout<< " введи число колец"<>n; cout<<"
n="<hanoi(n,1,3);
return 0;
}
#include
using namespace std;
void ring(int from, int where)
{ cout<< " с "<void hanoi(int n, int p, int q)
{ if (n==1) ring(p,q); else { hanoi(n-1,p,6-p-q); ring(p,q);
hanoi(n-1,6-p-q,q);
}
}
2.5.3
ФОРТРАН-решение:
program tsthanoi; implicit none integer n write(*,*) ’введи число колец:’; read (*,*) n; write(*,*) ’ n=’,n call hanoi(n,1,3)
end subroutine ring(from,where); implicit none integer from, where write(*,*) ’ c ’,from,’ на ’,where end subroutine ring recursive subroutine hanoi(n,p,q); implicit none integer n, p, q if (n.eq.1) then; call ring(p,q)
else; call hanoi(n-1,p,6-p-q); call ring(p,q);
call hanoi(n-1,6-p-q,q)
endif end subroutine hanoi введи число колец
// Результат CИ-пропуска для m=4:
4
n=4 1. с 1 на 2 4. с 1 на 2 7. с 1 на 2
A. с 2 на 1
D. с 1 на 2 2. с 1 на 3 5. с 3 на 1 8. с 1 на 3
B. с 3 на 1
E. с 1 на 3 3. с 2 на 3 6. с 3 на 2 9. с 2 на 3
C. с 2 на 3
F. с 2 на 3 68