Курс за второй семестр. Абстрактные типы данных

Вид материалаЗадача

Содержание


Деревья поиска.
Другие обходы дерева. Обход в ширину.
Рекурсивные процедуры и функции.
Граф называется деревом
Рекурсивная версия задачи о вычислении
Поиск в дереве.
Поиск в дереве поиска
Пример с факториалом
Проблемы с семантикой рекурсии.
Функциональным программированием
Введение в машинно-ориентированное (ссылочное) программирование.
Создание новых структурных операторов.
Формальная семантика
Подобный материал:
1   2   3   4   5   6   7   8

Деревья поиска.

Задача поиска значения по ключу существенно более простая: в бинарном дереве – это идея дихотомического поиска. Продолжим эту идею на обратную задачу.

Бинарное дерево называется деревом поиска, если для каждой вершины дерева слева от него хранятся меньшие, а справа – большие значения в некотором порядке на типе хранимых значений.







Procedure Dpoisk (root:pNode;x:tInfo;var pt:pNode;var found:boolean);

Begin

found:=false;

pt:=root;

while not found and (pt<>nil) do

begin

if pt.info=x then found:=true

else

if pt.info>x then pt:=pt.left

else pt:=pt.right;

end;

end;


Замечание. Данный поиск будет действительно дихотомическим, если исходное дерево полное (идеально сбалансированное).


Включение в дерево поиска.





Включим 13 в дерево.


Procedure Vstavka(var root:pNode;x:tInfo);

var found:boolean;

begin

pt:=root;

found:=false;

while not found and (pt<>nil) do

begin

if pt.info…

Аналогична задаче поиска, но сохраняет ссылку на предыдущую вершину pt.pred:pNode. Как только pt=nil – pt.pred – ссылка на вершину, после которой надо вставить данное значение.

Завести дополнительную ссылку pt.pred, указывающую на последнюю пройденную вершину.

pt.pred:=pt;

Когда pt=nil pt.pred указывает на нужный элемент, то есть pt.pred указывает на вершину, после которой нужно вставить вершину.

Упражнение: завершить реализацию вставки в дерево поиска. Построить дерево поиска.

Тривиальное решение: создать из первого элемента входной последовательности корень, затем, пока не кончилась входная последовательность, включать очередной элемент в построенное к этому шагу дерево.

К сожалению, предыдущее решение не строит сбалансированное дерево. Вставка с сохранением сбалансированности – сложная задача (в этом случае используются специальные типы деревьев). Хотя задача построения сбалансированного дерева не столь сложна:

1) Найти среднее значение во входной последовательности и сделать его корнем дерева.

2) Сделать то же для всех элементов, меньших корня (левое поддерево) и больших корня (правое поддерево).

Формулировка алгоритма рекурсивная. (см. Рекурсия)


Другие обходы дерева. Обход в ширину.

Есть задачи, в которых порядок обхода важен. В задаче о вычислении нижние вершины должны быть обработаны раньше, чем верхние.




Л П



К<Л<П Л П К - корень КЛП-обход


Порядок обработки: корень, левая ветка, правая ветка.

ЛПК-обход – задача о вычислении (ПЛК).

Обход КЛП даёт самую простую спецификацию обхода дерева.

1) КЛП – наиболее важный (см. Рекурсия).

2) Обходы “в ширину”, то есть такие порядки, в которых последовательности одинаковой длины соседствуют друг с другом (сгруппированы).

Задача. Найти в дереве вхождение заданного значения от ближайшего корня.

Procedure Поиск_в_ширину (root:pNode;x:tInfo;var found:boolean;var pt:pNode);

var level:{последовательность вершин одного уровня};






Л П


Л П


Begin

found:=false;

put(level,root);

while {не кончились уровни} and not found and level<>0 do

begin

{искать на текущем уровне нужные значения}

{обеспечить доступ на следующий уровень}

while not {кончились вершины уровня} and not found and level<>0 do

begin

get(level,pt);

{достать текущую вершину уровня}

if pt.info=x then found:=true

else begin

if pt.left<>nil then put(newlevel,pt.left);

ifpt.right<>nil then put(newlevel,pt.right);

end;

level:=newlevel;

end;

end;

end;


Структура tLevel – последовательность (динамическая) с двумя операциями – get и put и проверка пустоты. В данном случае неважно, стек это или очередь.

Другой вариант того же алгоритма:

uses Queue;

var q:tQueue;

begin

put(q,root);

found:=false;

{q содержит ссылки на все не пройденные вершины списка}

while not empty(q) and not found do

begin

get(q,pt);

if pt.info=x then found:=true

else begin

if pt.left<>nil then put(q,pt.left);

if pt.right<>nil then put(q,pt.left);

end;

end;

end;

Это непременно очередь. Потомки текущей вершины должны обрабатываться после всех вершин текущего уровня.


Рекурсивные процедуры и функции.

Примеры применения.

Паскаль допускает обращение функции внутри её определения (прямая рекурсия), либо обращение друг к другу двух или более определяемых функций – процедур (косвенная рекурсия).

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

Граф называется деревом, если он имеет вид:

- вершина.






- деревья


Пример: Рекурсивный вариант обхода дерева.

procedure Print(root:pNode);

begin

if root<>nil then

begin

write(root.info);

print(root.left);

print(root.right);

end;

end;


КЛП-обход, в случае дерева-выражения получим префиксную запись выражения.

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

Рекурсивная версия задачи о вычислении:

function ExpVal(root:pNode;VarVal:tVarVal:integer);

begin

if term(root) then result:=TermVal(root);

else begin

result_1:=ExpVal(root.left,VarVal);

result_2:=ExpVal(root.right,VarVal);

result:=AtomVal(root.info,result_1,result_2);

end;

end;


Поиск в дереве.

procedure Poisk(root:pNode;x:tInfo;var found:boolean;var pt:pNode);

begin

found:=false;

if root.info=x then

begin

found:=true;

pt:=root;

end

else if root.left<>nil then Poisk(root.left,x,found,pt)

else if not found and root.right<>nil then

Poisk(root.right,x,found,pt);

end;

end;


Поиск в дереве поиска:

procedure Poisk(root:pNode;x:tInfo;var found:boolean;var pt:pNode);

begin

if root=nil then found:=false

else if root.info=x then

begin

found:=true;

pt:=root;

end

else if x
else Poisk(root.right,x,found,pt);

end;

Пример с факториалом:




1, n=0

fact(n)=

fact(n-1)*n, n>0


function fact(n integer):longint;

begin

if n=0 then fact:=1

else fact:=n*fact(n-1);

end;


Проблемы с семантикой рекурсии.

В случае не рекурсивных вызовов семантика обращений строилась с помощью замены обращения модифицированным телом процедуры. Фактически мы описывали её (семантику) как построение некоторого выражения – программы на базовом Паскале. Этот способ работал и для кратных обращений.

Использование процедуры процедурой:


P

P1

P2
Чётко выделялись два этапа:

1) Построение выражения;

2) Его вычисление.

В этом случае такое дерево не может быть построено.

Вспомним алгоритм вычисления выражения. Как показал наш анализ, для того, чтобы вычислить значение выражения, необязательно строить его полностью. Более того, мы не обязаны для этого обходить всё дерево.

Пример: Если значение левого терма – ноль, а операция у нас – умножение, то значение правого терма вычислять не нужно.

Любая рекурсия связана с последовательным построением и вычислением выражения. Действительно, в любом случае можно считать, что рекурсивный вызов выполняется в случае истинности некоторого условия. При каждом рекурсивном вызове вычисляется выражение, и дерево достраивается лишь в случае его истинности.

Таким образом, любой рекурсивный алгоритм эквивалентен построению и обходу некоторого дерева.


P1



P1

P2

P1

P2

P2
1 2 B1+ B2+ if B1 then P1 if B2 then P2

1 2 2

B1+ B2+ B2+


В случае двух вызовов получаем бинарное дерево.

КЛП-обход или стековый обход.

Описали семантику рекурсии с помощью циклов. Теоретически возможно описание семантики циклов через рекурсию.

S, B(s)=false

while B do S(s)

S, while B do S, B(s)=true

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

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


Введение в машинно-ориентированное (ссылочное) программирование.

Оператор Goto.

Синтаксис: goto k, где k – метка (имя, указатель) на некоторый оператор программы, соответствующее обозначение оператора. Выглядит как k:S.

В стандартном Паскале метками могут являться натуральные числа (не связаны с характером выполняемых действий). Все метки обязаны быть объявлены в программе в специальной области label. В стандартном Паскале это самая первая область объявлений, то есть до const.

Неформальная семантика: «Я нарушаю естественный ход выполнения программы, после меня выполняется не следующий оператор, а оператор, помеченный соответствующей меткой».


Создание новых структурных операторов.

Разбор случаев (нечто вроде case):

B1:S1;

…….

Bn:Sn

иначе Sn+1

где Bi – условие (предикат), Si – оператор.



S1






S2



Sn


Sn+1



if B1 then S1;

goto 1;

if B2 then S2;

goto 1;

…………..

if Bn then Sn

else Sn+1

1:

Другой вариант: вложенный if.

c:char;

read(c);{чтение цифры}

if not c in [‘0’..’9’] then

begin

write(‘ожидалась цифра’);

goto 1;

end;

1:end;

Все подобные естественные использования goto в развитых языках программирования возлагаются на новые операторы с отличным синтаксисом.

Третий пример: программирование неструктурных алгоритмов.

while uslovie1 and uslovie2 do

begin

{что-то}

{проверка условия uslovie1}

if not uslovie1 then goto 1;{побочный выход из цикла}

{проверка условия uslovie2}

end;

В целом использование goto свидетельствует о плохом стиле программирования.


Формальная семантика goto и неструктурных программ.

Оператор goto не изменяет пользовательские переменные. В реальности goto изменяет системную переменную, счетчик команд, содержащую указатель (метку, имя, адрес) следующего в порядке выполнения оператора.

В терминах блок-схем goto – это просто стрелка.

Такая семантика даёт возможность точно определить семантику произвольных блок-схем. Пусть блок-схема B содержит n блоков S1,…,Sn. Неявно мы ввели нумерацию блоков (вместо индексов подошли бы любые иные указатели). Наша задача: построить структурную блок-схему, функционально эквивалентную данной.

Procedure StrucruredB ( );

var BlockName: tBlockName; {указатель на выполняемый блок}

bool:boolean;{значени текущего условного блока}

BlockEnd:tBlockName;{ещё один указатель на блок}

{этих переменных нет в исходной блок-схеме}

begin

BlockName:=cFirstBlockName;{указатель на кружок «н»}

while BlockName<>cLastBlockName {указатель на кружок «К»}do

begin

{Разбор случаев – для каждого операторного блока два случая}

BlockName=ThisBlock : SThisBlock;

BlockName:=ThisBlockEnd;

BlockName=ThisBlockName : BlockName:=NextBlockName;

{метка оператора следующего блока}

{С каждым условным блоком связываем 3 случая}

BlockName=ThisBlock : Bool:=BThisBlock;

BlockName:=ThisBlockEnd;

BlockName=ThisBlockEnd and Bool=true : BlockName:=PlusBlockName;

{метка на блок, на который указывает стрелка + }

BlockName=ThisBlockName and Bool=false : BlockName:=MinusBlockName;

end;

end;

Данная программа пошагово эквивалентна исходной.

Наши рассуждения фактически доказываю более сильные утверждения:

1) Любая программа может быть записана с единственным циклом и единственным разбором случаев. Такая форма программы называется нормальной.

2) Этот переход можно осуществить алгоритмически – написать интерпретатор блок-схем, который по произвольной блок-схеме (для него – входные данные) исполняет алгоритм, описанный этой блок-схемой.

Procedure Structure(B:tFlowChart);

Упражнение*. Сделай это! ;)

В теории программирования этот результат называется теоремой об универсальной функции (теоремой о существовании компьютера).

Каковы же минимальные средства программирования? Сколько операторов, типов данных, переменных и так далее должен содержать язык программирования, чтобы на нём можно было написать любую программу?

Для прояснения этого вопроса создадим искусственный машинный язык с «паскалеобразным» синтаксисом.