- •Самитов р.К.
- •Рекурсивное программирование
- •Задача: фишку можно продвинуть за один шаг на одну или на две клетки вперед (по линии), сколько существует способов продвижения фишки на n клеток вперед?
- •Базовые функции манипулирования списками таковы, что список хорошо просматривать от начала к концу, а хорошо строить – от конца к началу.
Самитов р.К.
Рекурсивное программирование
Factorial(n) = Factorial(n-1)*n, Factorial(1) = 1
PROGRAM pp;
FUNCTION Factorial(n:INTEGER):INTEGER;
BEGIN IF n<2 THEN Factorial:=1 ELSE
Factorial:= Factorial(n-1)*n END;
BEGIN WRITELN(Factorial(4)) END.
Семантика: дерево подзадач; два этапа - построение плана (дерева), вычисление по плану; важность требования - конечный обрыв всех ветвей.
Рекурсивный анализ: параметризация задачи, поиск базового случая и его решения, декомпозиция общего случая и обоснование конечного обрыва.
Рекурсия - специфическая структура управления: может устранить необходимость использования оператора цикла; ... всегда может - реализация WHILE-оператора рекурсией.
WHILE E DO S ~ ProcWhile
PROCEDURE ProcWhile;
BEGIN IF E THEN BEGIN S ; ProcWhile END END
Интеллектуальность+Неэкономность: первый этап (построение плана) иногда мог бы провести программист и не перегружать ЭВМ этой работой.
FILE OF REAL вывести в обратном порядке (определение - опираясь на развертку цикла)
Revers <Файла f> =
R EAD(f,x’); READ(f,x”); ... WRITE(g,x”); WRITE(g,x’)
= Revers <Остатка файла f>
PROGRAM pp; VAR f,g:FILE OF REAL;
PROCEDURE Revers; VAR x:REAL;
BEGIN IF NOT EOF(f) THEN
BEGIN READ(f,x); Revers; WRITE(g,x) END END;
BEGIN RESET(f); REWRITE(g); Revers END.
Дерево подзадач в виде вызовов процедур (с возвратами и отображением локальных переменных).
Рекурсия+Локализация данных - специфическая структура данных: может устранить необходимость использования массивов.
Осторожность в оценках неэкономичности рекурсии - в данной задаче нет явной лишней работы, но есть проблема - организовать экономичное хранение незавершенных копий вызываемой процедуры.
Factorial(n) = Fi(1,1,n), где
Fi(y,i,n) = Fi(y*(i+1), i+1,n), Fi(y,n,n)=y
Рассуждения в связи с заморочкой о смысле Fi().
( 1) Fact(n)= 1*2*...(n-1)*n.
Fact(n-1)
(2) Invers<f,f,...> =
READ(f); READ(f);...WRITE(f); WRITE(f)
Invers<f,...>
F i(1,1,n)
( 3) Fact(n)= 1*2*...i*(i+1)*(i+2)*...(n-1)*n.
y
F i(y,i,n) = y*(i+1)*(i+2)*...n.
y
Fi(y,i+1,n) = Fi(y*(i+1), i+1,n)
Fi(y,n,n)=y
происхождение идеи - рассуждение при написании традиционной императивной программы в терминах преобразования состояний (y,i,n); использование дополнительных аргументов в качестве дополнительной памяти - накапливающие параметры (возможность их использования для более изощренных целей - явного хранения фрагментов плана).
PROGRAM pp;
FUNCTION Fi(y,i,n:INTEGER):INTEGER;
BEGIN IF i=n THEN Fi:=y ELSE
Fi:= Fi(y*(i+1),i+1,n) END;
FUNCTION Factorial(n:INTEGER):INTEGER;
BEGIN Factorial:= Fi(1,1,n) END;
BEGIN WRITELN(Factorial(4)) END.
Дерево подзадач и возможность отсечения второго этапа.
Редуктивный и индуктивный стили рекурсивного программирования; «неэкономичность» рекурсии - как следствие неудачного использования этого инструмента программистом или неудачной реализации семантики этого инструмента транслятором (на данной ЭВМ).
Невырожденное применение рекурсии - задача о синтаксическом анализе на «Арифметическое выражение»; рекурсивность диаграммы Вирта и адекватность соответствующей рекурсивной программы. Программа с использованием функций типа BOOLEAN (ExprA, Variable, Symbol) и глобальных переменных - входной файл и текущий символ.
PROGRAM Prj8;{PROGRAM\RECURS\PRJ08\PRJ8.DPR}
{$B-}
uses SysUtils,Dialogs;
TYPE TSetOfChar= SET OF CHAR;
VAR Vh{входной текст}: FILE OF CHAR;
x{текущий символ}: CHAR; bb:BOOLEAN;
FUNCTION Symbol(Prm:TSetOfChar): BOOLEAN;
BEGIN IF (x IN Prm) THEN BEGIN Symbol:=TRUE; READ(Vh,x) END
ELSE Symbol:=FALSE
END;
FUNCTION Variable: BOOLEAN;
BEGIN Variable:=Symbol(['a'..'z']) END;
FUNCTION Operation: BOOLEAN;
BEGIN Operation:=Symbol(['+','-','*','/']) END;
FUNCTION ExprA: BOOLEAN;
BEGIN ExprA:=Variable OR
Symbol(['(']) AND ExprA AND Operation
AND ExprA AND Symbol([')'])
END;
BEGIN AssignFile(Vh,'p8Vh.TXT'); RESET(Vh); bb:=FALSE;
IF NOT EOF(Vh) THEN BEGIN READ(Vh,x); bb:=ExprA END;
IF bb AND (x='$') THEN ShowMessage('Правильное АВ')
ELSE ShowMessage('Неправильное АВ');
CloseFile(Vh)
END.
Функциональный вид и адекватность программы; структура арифметического выражения и дерево подзадач, порядок его разворачивания (обхода), итеративный характер процесса построения фрагментов плана и соответствующих вычислений по плану.
Замечания о необходимости аккуратно добавить охрану к оператору READ в связи с возможностью EOF.
Использование функций с «побочным эффектом», что не приветствует современная методология программирования.
Использование неклассической процедурной семантики логических выражений.
Причины отмеченных недостатков и проблемы их устранения с сохранением функционального вида программы:
рекурсивное программирование требует внимательного отношения к проектированию состава и вида опорных функций;
язык Pascal допускает рекурсию, но не является ориентированным на функциональное рекурсивное программирование.
Задача «Вычислить значение арифметического выражения». Выбор способа представления данных и набора базовых функций - пока безотносительно к способу их реализации.
Представление данных:
СПИСОК (S1,S2,...Sk) - последовательность элементов, где элемент - либо АТОМ (неделимое на составные части данное), либо СПИСОК.
Представление данных для задачи «Вычислить значение арифметического выражения»:
Пример для (A+(B-C))/((D/E)*F)
(операция, (аргумент1, аргумент2))
( /,( (+,(A, (-,(B,C)) )) , (*,( (/,(D,E)) ,F)) ))
(операция, аргумент1, аргумент2)
( /, (+, A, (-,(B,C)) ) , (*, (/,(D,E)) ,F ) )
линейное (посимвольное) представление (атомы подчеркнуты, чтобы отличить от метасимволов)
( (,A,+,(,B,-,C,),),/,(,(,D,/,E,),*,F,) )
ниже будем использовать только первое
Список - универсальная структура данных, позволяет представить традиционные - ARRAY, RECORD, FILE
Базовые функции
Селекторы компонентов списка (в роли компонентных переменных языка Pascal)
CAR(x) или HEAD(x) - первый (головной) элемент списка x : CAR( (S1,S2,...) )=S1
CDR(x) или TAIL(x) – «хвост» списка x : CDR( (S1,S2,...) )= (S2,S3,...)
Пример селекции компонента:
пусть Expr=(/,((+,(A,(-,(B,C)))),(*,((/,(D,E)),F))))
тогда CDR(CDR(CAR(CDR(Expr))))= (-,(B,C))
Константа NIL - пустой список; CDR( (S1) )=NIL, CAR(NIL)=NIL, т.е. одновременно NIL - пустой атом
Логические функции (предикаты)
ATOM(x) - x является атомом; значением функции CAR может быть список или атом, аргументы у CAR и CDR должны быть списками (атомы не допускаются)
EQ(x,y) - x и y одинаковые атомы, хотя бы один аргумент должен быть атомом - реализация более общего более сложного случая возлагается на программиста; символ = применим только к атомам
Конструктор списков (в роли описателя типов данных языка Pascal)
CONS(x,y) - список получающийся из списка y добавлением x в качестве первого элемента; x - может быть атомом или списком, но y - только списком; CONS( S, (S1,S2,...) )=(S,S1,S2,...)
NB. CAR(CONS(x,y))=x, CDR(CONS(x,y))=y, CONS(CAR(x),CDR(x))=x (!!! точнее, копия x).
Отметим, что список – это не стек, а много более общая структура данных, но приведенный набор базовых функций обеспечивает именно стековый доступ к компонентам списка.
CAR – позволяет просматривать значение элемента в вершине стека. CDR – соответствует операции «Вытолкнуть верхний элемент из стека». CONS – операции «Положить элемент в вершину стека».
Программа «Вычислить значение арифметического выражения»:
FUNCTION Вычислить(L:LIST):REAL; VAR m,n:LIST;
BEGIN IF ATOM(L) THEN Вычислить:=Значение(L) ELSE
BEGIN m:=CAR(CAR(CDR(L))) {NB.CDR(L)=((арг1,арг2))};
n:=CAR(CDR(CAR(CDR(L))));
CASE CAR(L) OF
’+’: Вычислить:=Вычислить(m) + Вычислить(n);
’-’: Вычислить:=Вычислить(m) - Вычислить(n);
’*’: Вычислить:=Вычислить(m) * Вычислить(n);
’/’: Вычислить:=Вычислить(m) / Вычислить(n) END END END
Получили весьма компактную и прозрачную программу для совсем не примитивной задачи... благодаря чему?
Воспользовались функцией «Значение», позволяющей извлечь значение переменной. Для реализации этой функции придется разработать структуру данных для хранения этих значений...
Воспользовались рекурсией.
Воспользовались хорошо структурированным подходящим входным представлением арифметических выражений. Построить такое представление по обычному текстовому – тоже совсем не примитивная задача.
Еще раз рассмотрим прием использования накапливающих параметров для рекурсивного определения функций.