1. Булеви функции. Теорема на Пост-Яблонски за пълнота. Нека J2 = { 0, 1}. Всяка функция f : J2n  J



страница16/29
Дата11.01.2018
Размер5.91 Mb.
#44141
1   ...   12   13   14   15   16   17   18   19   ...   29

Двоично дърво от тип T е рекурсивна структура от данни, която е или празна или е образувана от:

  • данна от тип T, наречена корен на двоичното дърво;

  • двоично дърво от тип T, наречено ляво поддърво на двоичното дърво;

  • двоично дърво от тип Т, наречено дясно поддърво на двоичното дърво.

Множеството на върховете (възлите) на едно двоично дърво се определя рекурсивно: празното двоично дърво няма върхове, а върховете на непразно дърво са неговият корен и върховете на двете му поддървета.

Ще разгледаме примери. Нека a, b, c, d, e, f и g са данни от тип T. Тогава следните графични представяния определят двоични дървета от тип T.



Посоката на линиите, свързващи върховете с поддървета позволява да се различи ляво от дясно поддърво. Следните две двоични дървета са различни:



В първия случай дясното поддърво е празно, а в другия случай дясното поддърво не е празно.



Листата на двоичното дърво са върховете с две празни поддървета. Например, на първата рисунка листата на трите дървета са a; d, f, g, c; d съответно.

Вътрешни върхове на двоичното дърво са върховете, различни от корена и листата. Например, на първата рисунка първото дърво няма вътрешни върхове, вътрешните върхове на второто дърво са b, e и на третото дърво са b, c.

Ляв наследник на един връх е коренът на лявото му поддърво (ако то е непразно). Десен наследник на един връх е коренът на дясното му поддърво (ако то е непразно). Ясно е, че листата на едно дърво са точно онези върхове, които нямат нито ляв, нито десен наследник. Ако a е наследник на b (ляв или десен), казваме, че b е родител (баща) на a. Ясно е, че всеки връх освен корена има точно един баща.

На всеки връх в дървото може да се съпостави ниво. Коренът на дървото има ниво 1 (или 0). Ако един връх има ниво i, то неговите наследници имат ниво i+1. С други думи, нивото на един връх е броят на върховете, които трябва да бъдат обходени за да се стигне до този връх от корена. Максималното ниво на едно дърво се нарича негова височина. Над структурата от данни двоично дърво са възможни следните операции:



  • достъп до връх – възможен е пряк достъп до корена и непряк достъп до останалите върхове;

  • включване и изключване на връх – възможни са на произволно място в двоичното дърво, резултатът трябва отново да е двоично дърво от същия тип;

  • обхождане – това е метод, позволяващ да се осъществи достъп до всеки връх на дървото един единствен път.

Обхождането е рекурсивна процедура, която се осъществява чрез изпълнение на следните три действия, в някакъв фиксиран ред:

  • обхождане на корена;

  • обхождане на лявото поддърво;

  • обхождане на дясното поддърво.

Най-разпространени са смесеното обхождане (първо лявото поддърво, после корена и най-накрая дясното поддърво), низходящото обхождане (първо корена, после лявото поддърво и най-накрая дясното поддърво) и възходящо обхождане (първо лявото поддърво, после дясното поддърво и най-накрая корена).

Основно се използват три начина за представяне на двоично дърво – свързано, верижно и чрез списък на бащите.

Свързаното представяне се реализира чрез указател към кутия с три полета – информационно, съдържащо стойността на корена и две адресни, съдържащи представянията на лявото и дясното поддърво.

При верижното представяне се използват три масива – a[N], b[N] и c[N]. Тук N е броят на върховете в дървото, върховете са номерирани от 0 до N-1. Елементът a[i] на масива a съдържа стойността на i-тия връх на дървото. Елементът b[i] на масива b съдържа индекса на левия наследник на i-тия връх (-1, ако той няма ляв наследник), елементът c[i] на масива c съдържа индекса на десния наследник на i-тия връх (-1, ако той няма десен наследник). Отделно се пази и индекса на корена на двоичното дърво.

При представянето чрез списък на бащите се използва един масив p[N]. Тук N е броят на върховете в дървото, върховете са номерирани от 0 до N-1. Елементът p[i] е единствения баща на

i-тия връх на дървото (-1, ако този връх е коренът).

Сега ще дефинираме клас, който реализира свързаното представяне на двоично дърво.

Първо ще дефинираме помощна структура Node, която реализира тройната кутия, чрез която се представят върховете на дървото.

За по-голяма общност, дефинираме структурата Node и класа Tree като шаблони.

template

struct Node {

T inf;


Node *left;

Node *right;

};

Сега вече сме готови да дефинираме класът Tree. Тъй като двоичното дърво се реализира в динамичната памет, за този клас трябва изрично да се реализира каноничното представяне – деструктор, обикновени конструктори, конструктор за присвояване и операторна функция за присвояване.



template

class Tree {

public:

Tree(const T&);



Tree();

~Tree();


Tree(const Tree &);

Tree& operator= (const Tree &);

bool empty () const;

bool RootTree (T&) const;

bool LeftTree (Tree &) const;

bool RightTree (Tree &) const;

void Create();

void Create3(const T&, const Tree &, const Tree &);

void PreOrder() const;

void InOrder() const;

void PostOrder() const;

private:


Node *root;

void DeleteTree (Node *&);

void CopyTree (Node * &, const Node *);

void Preord (const Node *);

void Inord (const Node *);

void Postord (const Node *);

void CreateTree (Node *&);

};

template



Tree::Tree (const T& x)

{ root = new Node ;

root -> inf = x;

root -> left = root -> right = NULL;

}

template



Tree::Tree ()

{ root = NULL; }

template

Tree::~Tree ()

{ DeleteTree (root); }

template

Tree::Tree (const Tree &t)

{ CopyTree (root, t.root); }

template

Tree& Tree::operator= (const Tree &t)

{ if (this != &t)

{ DeleteTree (root);

CopyTree (root, t.root);

}

return *this;



}

template

void Tree::DeleteTree (Node * &t)

{ if (t) {

DeleteTree (t -> left);

DeleteTree (t -> right);

delete t;

t = NULL;

}

}

template



void Tree::CopyTree (Node * &p, const Node *t)

{ if (t)


{ p = new Node ;

p -> inf = t -> inf;

if (t -> left) CopyTree (p -> left, t -> left);

else p -> left = NULL;

if (t -> right) CopyTree (p -> right, t -> right);

else p -> right = NULL;

}

}

template



bool Tree::empty () const

{ return root == NULL; }

template

bool Tree::RootTree (T &x) const

{ if (!root) return false;

x = root -> inf;

return true;

}

template



bool Tree::LeftTree (Tree &t) const

{ if (!root) return false;

DeleteTree (t.root);

CopyTree (t.root, root -> left);

return true;

}

template



bool Tree::RightTree (Tree &t) const

{ if (!root) return false;

DeleteTree (t.root);

CopyTree (t.root, root -> right);

return true;

}

template



void Tree::PreOrder () const

{ Preord (root);

cout << endl;

}

template



void Tree::InOrder () const

{ Inord (root);

cout << endl;

}

template



void Tree::PostOrder () const

{ Postord (root);

cout << endl;

}

template



void Tree::Preord (const Node *t)

{ if (t)


{ cout << t -> inf << ‘ ‘;

Preord (t -> left);

Preord (t -> right);

}

}



template

void Tree::Inord (const Node *t)

{ if (t)

{ Inord (t -> left);

cout << t -> inf << ‘ ‘;

Inord (t -> right);

}

}

template



void Tree::Postord (const Node *t)

{ if (t)


{ Postord (t -> left);

Postord (t -> right);

cout << t -> inf << ‘ ‘;

}

}



template

void Tree::Create3(const T&rt, const Tree &l, const Tree &r)

{ DeleteTree (root);

root = new Tree;

root -> inf = rt;

CopyTree (root -> left, l.root);

CopyTree (root -> right, r.root);

}

template



void Tree::Create()

{ DeleteTree (root);

CreateTree (root);

}

template



void Tree::CreateTree (Node * &t)

{ T x; char c;

cout << “root: “;

cin >> x;

t = new Node ;

t -> inf = x;

cout << “Left tree of: “ << x << “ (y/n)? “;

cin >> c;

if (c == ‘y’ || c == ‘Y’) CreateTree (t -> left); else t -> left = NULL;

cout << “Right tree of: “ << x << “ (y/n)? “;

cin >> c;

if (c == ‘y’ || c == ‘Y’) CreateTree (t -> right); else t -> right = NULL;

}
Предполагаме, че за елементите на типа T е установена линейна наредба.

Двоично наредено дърво от тип Т се дефинира рекурсивно по следния начин: празното двоично дърво oт тип T е наредено и непразно двоично дърво от тип T е наредено тогава и само тогава, когато всички върхове на лявото му поддърво са по-малки от корена и всички върхове на дясното му поддърво са по-малки от корена и освен това, лявото и дясното му поддърво са двоични наредени дървета от тип T.

Нека t е двоично наредено дърво от тип T. Включването на елемента a от тип T в t се осъществява по следния начин:



  • ако t е празното дърво, новото двоично наредено дърво е с корен елемента a и празни ляво и дясно поддървета;

  • ако t е непразно и a е по-малко от корена му, a се включва в лявото поддърво на t;

  • ако t е непразно и a е по-голям от корена му, а се включва в дясното поддърво на t.

Ще използваме този начин за включване на елементи за да създаваме двоичните наредени дървета. В този случай, те притежават следното свойство: смесеното обхождане сортира във възходящ ред елементите от върховете на дървото.

Изтриването на връх елемент a от двоичното наредено дърво t се извършва по следната схема:

  • ако коренът на t е по-голям от a, изтриваме a от лявото поддърво на t;

  • ако коренът на t е по-малък от a, изтриваме a от дясното поддърво на t;

  • ако коренът на t съвпада с a и t има празно ляво поддърво, то t се заменя с дясното си поддърво;

  • ако коренът на t съвпада с a и t има празно дясно поддърво, то t се заменя с лявото си поддърво;

  • ако коренът на t съвпада с a и t има непразни ляво и дясно поддърво, то се намира най-големият елемент в лявото поддърво (това става като се спуснем по най-десния клон до достигане на връх с празно дясно поддърво), разменя се неговата стойност със стойността на корена (която е a) и въпросният връх се изтрива (той има празно дясно поддърво, така че влизаме в предния случай).

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

Първо ще дефинираме помощна структура Node, която реализира тройната кутия, чрез която се представят върховете на дървото.

За по-голяма общност, дефинираме структурата Node и класа BinOrdTree като шаблони.

template

struct Node {

T inf;


Node *left;

Node *right;

};

Сега вече сме готови да дефинираме класът BinOrdTree. Тъй като двоичното наредено дърво се реализира в динамичната памет, за този клас трябва изрично да се реализира каноничното представяне – деструктор, обикновени конструктори, конструктор за присвояване и операторна функция за присвояване.



template

class BinOrdTree {

public:

BinOrdTree (const T&);



BinOrdTree ();

~BinOrdTree ();

BinOrdTree (const BinOrdTree &);

BinOrdTree& operator= (const BinOrdTree &);

bool empty () const;

bool RootTree (T&) const;

bool LeftTree (BinOrdTree &) const;

bool RightTree (BinOrdTree &) const;

void PrintSorted () const;

void AddNode (const T&);

void DeleteNode (const T&);

void Create();

private:

Node *root;

void DeleteTree (Node *&);

void Del (Node *&, const T&);

void CopyTree (Node *&, const Node *);

void Print (const Node *);

void Add (Node *&, const T&);

};

template



BinOrdTree::BinOrdTree (const T& x)

{ root = new Node ;

root -> inf = x;

root -> left = root -> right = NULL;

}

template



BinOrdTree::BinOrdTree ()

{ root = NULL; }

template

BinOrdTree::~BinOrdTree ()

{ DeleteTree (root); }

template

BinOrdTree::BinOrdTree (const BinOrdTree &t)

{ CopyTree (root, t.root); }

template

BinOrdTree& BinOrdTree::operator= (const BinOrdTree &t)

{ if (this != &t)

{ DeleteTree (root);

CopyTree (root, t.root);

}

return *this;



}

template

void BinOrdTree::DeleteTree (Node * &t)

{ if (t) {

DeleteTree (t -> left);

DeleteTree (t -> right);

delete t;

t = NULL;

}

}

template



void BinOrdTree::CopyTree (Node *&p, const Node *t)

{ if (t)


{ p = new Node ;

p -> inf = t -> inf;

if (t -> left) CopyTree (p -> left, t -> left);

else p -> left = NULL;

if (t -> right) CopyTree (p -> right, t -> right);

else p -> right = NULL;

}

}

template



bool BinOrdTree::empty () const

{ return root == NULL; }

template

bool BinOrdTree::RootTree (T &x) const

{ if (!root) return false;

x = root -> inf;

return true;

}

template



bool BinOrdTree::LeftTree (BinOrdTree &t) const

{ if (!root) return false;

DeleteTree (t.root);

CopyTree(t.root, root -> left);

return true;

}

template



bool BinOrdTree::RightTree (BinOrdTree &t) const

{ if (!root) return false;

DeleteTree (t.root);

CopyTree(t.root, root -> right);

return true;

}

template



void BinOrdTree::PrintSorted () const

{ Print (root);

cout << endl;

}

template



void BinOrdTree::Print (const Node *t)

{ if (t)


{ Print (t -> left);

cout << t -> inf << ‘ ‘;

Print (t -> right);

}

}



template

void BinOrdTree::AddNode (const T& x)

{ Add (root, x); }

template

void BinOrdTree::Add (Node * &t, const T& x)

{ if (!t)

{ t = new Node ;

t -> inf = x;

t -> left = t -> right = NULL;

}

else if (x < t -> inf) Add (t -> left, x);



else if (x > t -> inf) Add (t -> right, x);

else cout << “Duplicate cannot be inserted!” << endl;

}

template



void BinOrdTree::DeleteNode (const T x)

{ Del (root, x); }

template

void BinOrdTree::Del (Node * &t, const T& x)

{ if (!t) return;

if (x < t -> inf) Del (t -> left, x);

else if (x > t -> inf) Del (t -> right, x);

else {


Node *p;

if (!(t -> left)) { p = t; t = t -> right; delete p; }

else if (!(t -> right)) { p = t; t = t -> left; delete p; }

else { p = t -> left;

while (p -> right) p = p -> right;

t -> inf = p -> inf;

Del (t -> left, p -> inf);

}

}



}

template

void BinOrdTree::Create ()

{ DeleteTree (root);

T x; char c;

do {


cout << “Enter element: “;

cin >> x;

AddNode (x);

cout << “Enter more (y/n)? “;

cin >> c;

} while (c == ‘y’ || c == ‘Y’);

}
14. Основни конструкции в езиците за функционално програмиране. Дефиниране и използване на функции. Списъци. Функции от

по-висок ред за работа със списъци. Потоци.
Във функционалното програмиране програмата се изгражда в дескриптивен стил – описват се свойствата на желания резултат, докато алгоритъмът за неговото намиране не е съществен. Най-често програмата е поредица от дефиниции на функции, които имат за цел да пресмятат някакъв резултат. Тези функции не предизвикват странични ефекти подобно на програмите при процедурното програмиране. Това води до по лесно намиране и отстраняване на грешки. В програмите няма операции за цикли, за условен или безусловен преход, операции за присвояване. Итерациите се реализират чрез рекурсия. При функционалното програмиране има две основни дейности – дефиниции на функции и прилагане на функции за извличане на резултат.

Тук ще разглеждаме езика Scheme, който е пример за нестрог функционален език и е диалект на езика Lisp.

Математически основи на езика Lisp се основава на -смятането на Чърч. Основната структура в този език са списъците и чрез тях се моделират обекти от реалния свят.
Езикът Scheme притежава три механизма за изразяване:


  • примитивни изрази;

  • средства за комбинация;

  • средства за абстракция.

Основна синтактична единица в езика Scheme е изразът. Неформално работният цикъл на интерпретатора на Scheme е следния:

  • чете израз (read);

  • оценява израз (evaluate);

  • извежда оценката (print).

Изразите могат да бъдат примитивни или съставни. Примитивните изрази още се наричат атомарни изрази или атоми, а съставните се наричат комбинации.
Примитивните изрази се делят на:

  • числа – с фиксирана или плаваща точка – на практика с неограничен размер;

  • символни низове – поредица от знаци, заградени в кавички (“ “);

  • вградени функции – например +, -, *, / и др.;

  • символни атоми – в процедурните езици са известни като идентификатори – поредица от букви или цифри, която трябва да започва с буква.

Оценката на примитивен израз директно се свързва с неговото име:

  • оценката на число е самото число;

  • оценката на символен низ е самия низ;

  • оценката на вградена функция е така наречения функционален (процедурен) обект – двойка съдържаща указател към среда и поредица от машинни инструкции, които реализират съответната функция;

  • за да има оценка един символен атом, той трябва да означава име на променлива или функция, т.е. предварително трябва да е дефиниран, ако не е дефиниран оценката на символния атом е неопределена.


Списък в Scheme е редица от обекти, оградени в кръгли скоби и разделени с един или повече интервали, допуска се влагане на списъци и празен списък, който се означава с ().

Семантично списъците биват няколко вида и оценката им зависи от техния вид, ако искаме независимо от вида на списъка оценката му да бъде самия списък, използваме цитат по следния начин: (quote <списък>) или ‘<списък>.


Съставните изрази (комбинациите) са обръщения към функции. Функциите могат да бъдат вградени или дефинирани от потребителя. Съставните изрази синтактично се означават чрез списък по следния начин:

(<функция> <ФактП 1> <ФактП 2> ... <ФактП n>).

Числото n е броят на допустимите аргументи на функцията. Често първият аргумент в съставен израз се нарича оператор, а фактическите параметри се наричат операнди.

Оценяването на комбинация се извършва по следното общо правило:



  • отляво надясно се оценяват отделните подизрази на комбинацията;

  • оценката на оператора, която задължително е функция, се прилага върху оценките на операндите, които трябва да бъдат коректни по брой и тип.

Има изключение от общото правило при така наречените специални форми. Всяка специална форма си има собствено правило за оценяване.
Чрез средствата за абстракция, обектите на езика могат да се именуват и обработват като едно цяло. Основно такова средство е специалната форма define. Тя има две разновидности: за дефиниране на променливи и за дефиниране на процедури.

Синтаксисът за дефиниране на променливи е следния:

(define <име_на_променлива> <израз>).

Тук <име_на_променлива> е произволен символен атом, <израз> е произволен израз. Оценката на израза е <име_на_променлива>, но като страничен ефект се получава свързване на символния атом <име_на_променлива> с оценката на <израз> в специална работна памет, наречена среда. След като този израз е оценен, оттам нататък оценката на <име_на_променлива> ще бъде оценката на <израз>.

Особеността на define е, че нейният втори аргумент не се оценява, затова тя е специална форма.
Синтаксисът за дефиниране на процедури е следния:

(define (<име_на_процедура> <ФормП1> …<ФормПn>) <тяло>).

Тук <име_на_процедура> и <ФормПi>, i = 1, …, n са символни атоми, <тяло> е редица от изрази, разделени с един или повече интервали. Оценката на израза е <име_на_процедура>, но като страничен ефект в средата, в която се извършва свързването се създава нов процедурен обект – той съдържа указател към текущата среда и код. Кодът се състои от параметрите

(<ФормП1>, …, <ФормПn>) и тяло (изразите в редицата <тяло>).


За да може да се реализира именуването е нужно да се поддържа памет, където да се съхраняват двойки от вида (име, обект).

В езика Scheme при оценяването се използва т.н. модел на средите. При него, паметта, която служи за съхраняване на двойките (име, обект) се нарича среда. В началото на оценяването имаме дефинирана само една среда – наричаме я глобална. В процеса на оценяване могат да възникнат и други среди. Когато се създава нова среда тя винаги се явява разширение на вече съществуваща среда. Когато се извършва оценяване, то винаги се прави в контекста на някоя среда. Например, нека имаме средите E0, E1, …, En, като E0 е глобалната среда и средата Ei е разширение на средата Ei-1, i = 1, …, n.

Да предположим, че търсим оценката на името A в En. В модела на средите това търсене се извършва по следния начин: започвайки от En, последователно обхождаме средите в обратен ред на разширяването (от Ei към Ei-1), до първия момент в който достигнем до среда Ek, в която е дефинирана двойка (А, обект). Тогава процесът на търсене спира и оценката на A е съответният обект. При това е възможно да е дефинирана двойка (А, обект1) в среда Ej при j < k, но това не променя нищо (локалните имена скриват глобалните).
Както вече казахме, оценяването на един съставен израз (комбинация) представлява обръщения към процедура.

Нека имаме следната комбинация:

(<процедура> <ФактП1> …<ФактПn>).

Ще опишем как се извършва нейното оценяване в модела на средите. Нека оценяването на комбинацията се извършва в средата E. <процедура> е израз, който определя процедурата, към която се обръщаме, <ФактПi>, i = 1, …, n са фактическите параметри на обръщението. Първото което се прави е в средата E да се оценят <процедура> и <ФактПi>, i = 1, …, n. За да е коректно оценяването, оценката на <процедура> задължително трябва да е процедурен обект и фактическите параметри трябва да съответстват на формалните параметри по брой, тип и смисъл.

Нека указателят в процедурния обект, който е оценка на <процедура> сочи към средата E. Тогава се създава нова среда E, която е разширение на E и в E всички формалните параметри на процедурния обект се свързва с оценките на съответните им фактически параметри. След това в контекста на средата E се извършва последователно оценка на изразите от тялото на процедурния обект. Оценката на последния израз от тялото е оценка на първоначалната комбинация в средата E.

Описаният модел на средите реализира т.н. апликативен модел на оценяване (неговата същност е, че първо се оценяват операндите и след това към получените оценки се прилага оценката на оператора). Съществува и друг модел на оценяване – т.н. нормален модел. При него, при извършване на процедурно обръщение се оценява само оператора, но не и операндите – те се заместват в тялото на оценката на оператора на мястото на съответните им формални параметри без да се оценяват предварително и след това се оценява тялото на оценката на оператора.

Може да се докаже, че ако процесът на оценяване по апликативния модел завършва, то процесът на оценяване по нормалния модел също завършва и в резултат се получава една и съща оценка. Съществуват, обаче, много програми, които завършват изпълнението си по нормалния модел на оценяване, но не завършват по апликативния. Затова казваме, че нормалният модел е по-общ от апликативния. Езикът Scheme използва моделът на средите, в основата на който стои апликативният модел.

В някои случаи, обаче, например при работа с отложени безкрайни обекти Scheme използва нормалният модел.


На всяка дефинирана процедура може да се гледа както декларативно, така и императивно (като процес, който ще доведе до получаване на търсения резултат). Процес – това е абстрактно понятие, описващо развитието на пресмятането във времето, т.е. преобразуването на входните данни стъпка по стъпка до получаване на резултата. Можем да класифицираме процесите в два основни класа – рекурсивни и итеративни. Рекурсивните процеси се делят на линейно рекурсивни и дървовидно рекурсивни. Итеративните се делят на линейни итеративни и нелинейни итеративни (логаритмични и др.).

За да сравним линейно рекурсивните и линейно итеративните процеси ще реализираме по два начина функцията факториел.

(define (fact n)

(if (= n 0) 1 (* n (fact (- n 1)))))

(define (fact n)

(define (iterfact result m)

(if (> m n) result (iterfact (* result m) (+ m 1))))

(iterfact 1 1)

)

Оценяването на (fact 3) поражда следните процеси:



(fact 3)  (* 3 (fact 2))  (* 3 (* 2 (fact 1)))  (* 3 (* 2 (* 1 (fact 0)))) 

 (* 3 (* 2 (* 1 1)))  (* 3 (* 2 1))  (* 3 2)  6,

(fact 3)  (iterfact 1 1)  (iterfact 1 2)  (iterfact 2 3)  6.

Забелязваме, че в първия случай имаме разширяване и след това свиване на изчислителния процес, като при това имаме редица отложени операции. Порядъка на отложените операции е линейна функция на размера на входа и затова процесът в този случай процесът се нарича линейно рекурсивен.

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

Когато в своята дефиниция една процедура извиква себе си повече от веднъж, то забелязваме че броят на отложените операции става експоненциален. Процесите, които пораждат такива процедури наричаме дървовидно рекурсивни. Добър пример е процедурата за намиране на n-тото число на Фибоначи:

(define (fib n)

(if (= n 0) 1

(if (= n 1) 1

(+ (fib (- n 1)) (fib (- n 2))))))

Оценяването на (fib 4) поражда следния процес:

Вижда се, че това е твърде неефективна реализация, тъй като се правят излишни изчисления – (fib 2) се оценява 2 пъти, (fib 1) се оценява 3 пъти, (fib 0) се оценява 2 пъти.

Много по-ефективно е следното решение:

(define (fib n)

(define (iterfib a b counter)

(if (= counter n) а

(iterfib b (+ a b) (+ counter 1))))

(iterfib 1 1 0))

Оценяването на комбинацията (fib 4) се извършва по следния начин: (fib 4)  (iterfib 1 1 0)  (iterfib 1 2 1)  (iterfib 2 3 2)  (iterfib 3 5 3)  (iterfib 5 8 4)  5. Процесът е линейно итеративен.
Всяка процедура, която манипулира с процедурни обекти се нарича процедура от по-висок ред. По-специално процедурите от по-висок ред получават процедурни обекти като параметри или връщат процедурни обекти като резултат.

Предаването на процедурите като параметри дава възможност за осъществяване на по-голямо ниво на абстракция в програмите на Scheme. Синтактично, то се осъществява по начина, по който се предават обикновени параметри. Като пример ще реализираме абстракцията f (a) + f (next (a)) … + f (b). Тук чрез next се получава следващата стъпка, сумирането започва от a и продължава до b.

(define (sum a b next f)

(if (> a b) 0

(+ (f a) (sum (next a) b next f))))

По-ефективен е следният линейно итеративен вариант:

(define (sum a b next f)

(define (itersum i result)

(if (> i b) result

(itersum (next i) (+ result (f i)))))

(itersum a 0)

)

Примерно използване: (sum 1 5 (lambda (x) (+ x 1)) (lambda (x) x)) се оценява до 1+2+3+4+5 = 15.



Оценката на една комбинация може да бъде процедурен обект. Това е съвсем лесно за реализация – оценката на последният израз в тялото тряба да е процедурен обект. Като пример ще дефинираме процедура, която има един аргумент процедура и връща нейната n-та експонента (функцията, приложена n-пъти върху аргумента си):

(define (exponent f n)

(if (= n 0) (lambda (x) x)

(lambda (x) (f ((exponent f (- n 1)) x)))))

В случая е използвана специалната форма lambda, която не оценя аргументите си и се използва за създаване на анонимен процедурен обект в текущата среда.

Примерно използване:

((exponent (lambda (x) (* x x)) 2) 3) се оценява до (32)2 = 81.
Основното предназначение на езика за функционално програмиране Scheme е работа със списъци. Списъците се реализират чрез точкови двойки. Точковата двойка е съставен тип данни в Scheme и тя представлява указател към двойна клетка, състояща се от лява и дясна част. Тези части сами по себе си могат да са точкови двойки или атоми. За да можем да работим с точкови двойки са необходими операция за конструиране на точкова двойка, операция за извличане на лявата част на точкова двойка и операция за извличане на дясната част на точкова двойка. Въпреки, че програмистите могат сами да си дефинират такива операции, за по-голяма ефективност те обикновено са вградени. В Scheme имаме:

(cons a b) – конструира точкова двойка с лява част оценката на a и дясна част оценката на b;

(car x) – извлича лявата част на оценката на x, която трябва задължително да е точкова двойка;

(cdr x) – извлича дясната част на оценката на x, която трябва задължително да е точкова двойка.



Списъкът е крайна редица от обекти от произволен тип, възможно празна. В Scheme списъците се дефинират посредством точкови двойки по следния начин: празният списък () е списък и ако L е списък и a е произволен обект, то точковата двойка с лява част a и дясна част L е списък. С вградените операции на Scheme списъкът 1>, 2>, …, n> може да се конструира така:

(cons 1> (cons 2> …(cons n> ())…)). Понякога не е удобно да се конструира списък по този начин и тогава може да се използва примитивната процедура list: (list 1> 2> …n>). Също може да се използва и специалната форма quote:

(quote (1> 2> … n>)) или в съкратен вариант

‘(1> 2> … n>).

Отделните елементи на списъците се извличат с помощта на подходящо комбиниране на car и cdr. Примери: (предполагаме, че оценката на <списък> е някакъв списък)

(car <списък>) – връща първия елемент на списъка;

(cdr <списък>) – връща списъка с премахнат първи елемент.

И в двата случая, ако оценката на <списък> е (), то оценката на комбинацията е неопределена. Други примери:

(car (cdr <списък>)) – връща втория елемент на списъка;

(car (cdr (cdr <списък>))) – връща третия елемент на списъка.

За синтактично удобство се допускат специални форми от вида

cxxxxr, където x е една от буквите a или d. Например:

(cadr <списък>)  (car (cdr <списък>));

(caddr <списък>)  (car (cdr (cdr <списък>))).

В езика Scheme има множество вградени операции за работа със списъци, но за пълнота ще разгледаме реализациите на някои операции.

Намиране на дължината на списък:

(define (length l)

(if (null? l) 0 (+ 1 (length (cdr l)))))

Вграденият предикат null? приема един аргумент и връща #t, ако този аргумент е празният списък и #f, в противен случай.

Намиране на k-тия елемент на списък, като номерацията започва от 1 (операцията има смисъл когато k не надвишава дължината на списъка, в частност операцията няма смисъл за празен списък):

(define (kth_el l k)

(if (= k 1) (car l) (kth_el (cdr l) (- k 1))))

Конкатенация на списъци:

(define (append l1 l2)

(if (null? l1) l2 (cons (car l1) (append (cdr l1) l2))))

Обръщане на списък (вариант с append):

(define (reverse l)

(if (null? l) l

(append (reverse (cdr l)) (list (car l)))))

Обръщане на списък (итеративен вариант без append):

(define (reverse l)

(define (helprev l1 result)

(if (null? l1) result (helprev (cdr l1) (cons (car l1) result))))

(helprev l ()))

Проверка за принадлежност към списък:

(define (member x l)

(cond ((null? l) #f)

((equal? x (car l)) #t)

(else (member x (cdr l)))))

Вграденият предикат equal? приема два аргумента и връща стойност #t, ако тези два аргумента имат еднакво представяне и #f, в противен случай.

Под списък с вложения разбираме списък, елементите на който може да са списъци. Още казваме, че работим със списъци на произволно ниво.

Намиране на броя на атомите в списък на произволно ниво: (вариант, в който празният списък не се брои за атом):

(define (deep_count_atoms l)

(cond ((null? l) 0)

((atom? l) 1)

(else (+ (deep_count_atoms (car l)) (deep_count_atoms (cdr l))))))

(вариант, в който празният списък се брои за атом):

(define (deep_count_atoms l)

(cond ((null? l) 0)

((atom? (car l)) (+ 1 (deep_count_atoms (cdr l))))

(else (+ (deep_count_atoms (car l)) (deep_count_atoms (cdr l))))))

Разликата е следната: например, (deep_count_atoms ‘(()()())) в първия вариант се оценява с 0, а във втория вариант с 3.

Вграденият предикат atom? приема един аргумент и връща стойност #t, ако този аргумент не е точкова двойка (т.е. е атом) и #f, в противен случай (ако не е атом).

Обръщане на списък на произволно ниво:

(define (deep_reverse l)

(if (atom? l) l

(append (deep_reverse (cdr l)) (list (deep_reverse (car l))))))

Ще разгледаме някои процедури от по-висок ред за работа със списъци.

Първата процедура извършва комбиниране (акумулиране) на елементите на списък:

(define (accum l combiner init_value)

(if (null? l) init_value

(combiner (car l) (accum (cdr l) combiner init_value))))

С помощта на тази процедура можем да намерим сумата на елементите на списък по следния начин: (accum l + 0), произведението на елементите на списък по следния начин: (accum l * 1), можем да създадем копие на списък по следния начин: (accum l cons ()) и освен това можем да реализираме функцията append с променлив брой аргументи:

(accum ‘(1> 2> …n>) append ()).

Втората процедура е за трансформиране на списък чрез прилагане на една и съща процедура върху елементите му.

(define (map f l)

(if (null? l) l (cons (f (car l)) (map f (cdr l)))))

Третата процедура е за филтриране на елементите на списък, т.е. за образуване на подсписък от елементите на списъка, които удоволетворяват някакво условие (филтър).

Ще я реализирамe в два варианта – чрез рекурсивен процес и чрез използване на вече дефинираните функции за акумулиране и за трансформация.

Рекурсивен вариант:

(define (filter l pred)

(cond ((null? l) l)

((pred (car l)) (cons (car l) (filter (cdr l) pred)))

(else (filter (cdr l) pred))))

Решение чрез трансформация:

(define (filter l pred)

(accum (map (lambda (x) (if (pred x) (list x) ())) l) append ()))

Ако един елемент минава през филтъра, той се замества със списък от себе си, в противен случай се замества с празен списък. След това върху елементите на получения списък се изпълнява append с променлив брой аргументи.


Потокът е съставна структура от данни, която се явява обобщение на структурата списък. Потокът представлява крайна или безкрайна редица от елементи от произволен тип. Възможен е пряк достъп само до първия елемент на потока. До останалите елементи достъпът е последователен и се осъществява чрез форсиране.
Нека редицата a1, a2, …, an, … е поток. В Scheme тази редица се представя като точкова двойка, която има за лява част представянето на a1



Сподели с приятели:
1   ...   12   13   14   15   16   17   18   19   ...   29




©obuch.info 2024
отнасят до администрацията

    Начална страница