Вы находитесь на странице: 1из 177

http://asladkikh.narod.ru/index.

htm

Алгоритм Кнута — Морриса — Пратта (КМП-алгоритм)


Алгоритм Кнута — Морриса — Пратта (КМП-алгоритм) — алгоритм
поиска образца (подстроки) в строке. Алгоритм был открыт Д.
Кнутом и В. Праттом и, независимо от них, Д. Моррисом.
Результаты своей работы они опубликовали совместно в 1977 году.
Постановка задачи
Поставим следующую задачу: имеется образец S и строка T, и
нужно определить индекс, начиная с которого строка S содержится
в строке T. Если S не содержится в T — вернуть индекс, который
не может быть интерпретирован как позиция в строке (например,
отрицательное число). При необходимости отслеживать каждое
вхождение образца в текст имеет смысл завести дополнительную
функцию, вызываемую при каждом обнаружении образца.
Префикс-функция. Определение
Дана строка s[0 ... n-1]. Требуется вычислить для неё префикс-
функцию, т.е. массив чисел П[0 ... n-1], где П[i] определяется
следующим образом: это длина наибольшего собственного суффикса
подстроки s[0..i], совпадающего с её префиксом (собственный
суффикс — значит не совпадающий со всей строкой). В частности,
значение П[0] всегда равно нулю.
Математически определение префикс-функции можно записать
следующим образом:

Например, для строки "abcabcd" префикс-функция равна: [0, 0, 0,


1, 2, 3, 0]. Для строки "aabaaab"она равна: [0, 1, 0, 1, 2, 2,
3].
Описание алгоритма и оценка времени работы
Рассмотрим сравнение строк на позиции i, где образец S[ 0, m -
1 ] сопоставляется с частью текстаT[ i, i + m - 1 ].
Предположим, что первое несовпадение произошло между T[ i + j
] и S[ j ], где1 < j < m. Тогда T[ i, i + j - 1 ] = S[ 0, j - 1
] = P и a = T[ i + j ] != S[ j ] = b.
При сдвиге вполне можно ожидать, что префикс (начальные
символы) образца S сойдется с каким-нибудь суффиксом (конечные
символы) текста P. Длина наиболее длинного префикса,
являющегося одновременно суффиксом, есть префикс-функция от
строки S для индекса j.
Это приводит нас к следующему алгоритму: пусть П [ j ] —
префикс-функция от строки S[ 0, m - 1 ]для индекса j. Тогда
после сдвига мы можем возобновить сравнения с места T[ i + j
] и S[ П [ j ] ] без потери возможного местонахождения образца.
Средствами амортизационного анализа можно показать, что
таблица П может быть вычислена за O ( m ) сравнений перед
началом поиска. А поскольку строка T будет пройдена ровно один
раз, суммарное время работы алгоритма будет равно O (m + n),
где n - длина текста T.
Быстрая сортировка (англ. quicksort)

Ref: https://dic.academic.ru/dic.nsf/ruwiki/46738

Aнимированная схема алгоритма

Анимированная схема алгоритма


Быстрая сортировка (англ. quicksort), часто называемая qsort по имени реализации в
стандартной библиотеке языка Си — широко известный алгоритм сортировки, разработанный
английским информатиком Чарльзом Хоаром в 1960 году. Один из быстрых известных
универсальных алгоритмов сортировки массивов (в среднем O(n log n) обменов при
упорядочении n элементов), хотя и имеющий ряд недостатков.

Краткое описание алгоритма


• выбрать элемент, называемый опорным.
• сравнить все остальные элементы с опорным, на основании сравнения разбить множество на
три — «меньшие опорного», «равные» и «большие», расположить их в порядке меньшие-
равные-большие.
• повторить рекурсивно для «меньших» и «больших».
Примечание: на практике обычно разделяют сортируемое множество не на три, а на две
части: например, «меньшие опорного» и «равные и большие». Такой подход в общем случае
оказывается эффективнее, так как для осуществления такого разделения достаточно одного
прохода по сортируемому множеству и однократного обмена лишь некоторых выбранных
элементов.

Алгоритм
Быстрая сортировка использует стратегию «разделяй и властвуй». Шаги алгоритма таковы:
1. Выбираем в массиве некоторый элемент, который будем называть опорным элементом. С
точки зрения корректности алгоритма выбор опорного элемента безразличен. С точки зрения
повышения эффективности алгоритма выбираться должна медиана, но без дополнительных
сведений о сортируемых данных её обычно невозможно получить. Известные стратегии:
выбирать постоянно один и тот же элемент, например, средний или последний по положению;
выбирать элемент со случайно выбранным индексом.

Медиа́на (50-й процентиль, квантиль 0,5) — возможное значение признака, которое делит
ранжированную совокупность (вариационный ряд выборки) на две равные части: 50 %
«нижних» единиц ряда данных будут иметь значение признака не больше, чем медиана, а
«верхние» 50 % — значения признака не меньше, чем медиана.

1. Операция разделения массива: реорганизуем массив таким образом, чтобы все


элементы, меньшие или равные опорному элементу, оказались слева от него, а
все элементы, большие опорного — справа от него. Обычный алгоритм
операции:
1. Два индекса — l и r, приравниваются к минимальному и максимальному
индексу разделяемого массива соответственно.
2. Вычисляется индекс опорного элемента m.
3. Индекс l последовательно увеличивается до тех пор, пока l-й элемент
не превысит опорный.
4. Индекс r последовательно уменьшается до тех пор, пока r-й элемент не
окажется меньше либо равен опорному.
5. Если r = l — найдена середина массива — операция разделения
закончена, оба индекса указывают на опорный элемент.
6. Если l < r — найденную пару элементов нужно обменять местами и
продолжить операцию разделения с тех значений l и r, которые были
достигнуты. Следует учесть, что если какая-либо граница (l или r) дошла
до опорного элемента, то при обмене значение m изменяется на r-й или
l-й элемент соответственно.
2. Рекурсивно упорядочиваем подмассивы, лежащие слева и справа от опорного
элемента.
3. Базой рекурсии являются наборы, состоящие из одного или двух элементов.
Первый возвращается в исходном виде, во втором, при необходимости,
сортировка сводится к перестановке двух элементов. Все такие отрезки уже
упорядочены в процессе разделения.

Поскольку в каждой итерации (на каждом следующем уровне рекурсии) длина


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

Интересно, что Хоар разработал этот метод применительно к машинному переводу:


дело в том, что в то время словарь хранился на магнитной ленте, и если упорядочить
все слова в тексте, их переводы можно получить за один прогон ленты. Алгоритм был
придуман Хоаром во время его пребывания в Советском Союзе, где он обучался в
Московском университете компьютерному переводу и занимался разработкой русско-
английского разговорника (говорят, этот алгоритм был подслушан им у русских
студентов).[1]

Оценка эффективности
QuickSort является существенно улучшенным вариантом алгоритма сортировки с
помощью прямого обмена (его варианты известны как «Пузырьковая сортировка» и
«Шейкерная сортировка»), известного, в том числе, своей низкой эффективностью.
Принципиальное отличие состоит в том, что после каждого прохода элементы делятся
на две независимые группы. Любопытный факт: улучшение самого неэффективного
прямого метода сортировки дало в результате эффективный улучшенный метод.

• Лучший случай. Для этого алгоритма самый лучший случай — если в каждой
итерации каждый из подмассивов делился бы на два равных по величине массива. В
результате количество сравнений, делаемых быстрой сортировкой, было бы равно
значению рекурсивного выражения CN = 2CN/2+N, что в явном выражении дает
примерно N lg N сравнений. Это дало бы наименьшее время сортировки.

• Среднее. Даёт в среднем O(n log n) обменов при упорядочении n элементов. В


реальности именно такая ситуация обычно имеет место при случайном порядке
элементов и выборе опорного элемента из середины массива либо случайно.
На практике (в случае, когда обмены являются более затратной операцией, чем
сравнения) быстрая сортировка значительно быстрее, чем другие алгоритмы с
оценкой O(n lg n), по причине того, что внутренний цикл алгоритма может быть
эффективно реализован почти на любой архитектуре. 2CN/2 покрывает расходы по
сортировке двух полученных подмассивов; N — это стоимость обработки каждого
элемента, используя один или другой указатель. Известно также, что примерное
значение этого выражения равно CN = N lg N.

• Худший случай. Худшим случаем, очевидно, будет такой, при котором на каждом
этапе массив будет разделяться на вырожденный подмассив из одного опорного
элемента и на подмассив из всех остальных элементов. Такое может произойти, если
в качестве опорного на каждом этапе будет выбран элемент либо наименьший, либо
наибольший из всех обрабатываемых.
Худший случай даёт O(n²) обменов. Но количество обменов и, соответственно, время
работы — это не самый большой его недостаток. Хуже то, что в таком случае глубина
рекурсии при выполнении алгоритма достигнет n, что будет означать n-кратное
сохранение адреса возврата и локальных переменных процедуры разделения
массивов. Для больших значений n худший случай может привести к исчерпанию
памяти во время работы алгоритма. Впрочем, на большинстве реальных данных
можно найти решения, которые минимизируют вероятность того, что понадобится
квадратичное время.

Достоинства и недостатки
Достоинства:
• Один из самых быстродействующих (на практике) из алгоритмов внутренней сортировки
общего назначения.
• Прост в реализации.
• Требует лишь O(Lgn) дополнительной памяти для своей работы. (Не улучшенный
рекурсивный алгоритм в худшем случае O(n) памяти)
• Хорошо сочетается с механизмами кэширования и виртуальной памяти.
• Существует эффективная модификация (алгоритм Седжвика) для сортировки строк —
сначала сравнение с опорным элементом только по нулевому символу строки, далее
применение аналогичной сортировки для «большего» и «меньшего» массивов тоже по
нулевому символу, и для «равного» массива по уже первому символу.
• Работает на связанных списках и других структурах с последовательным доступом.

Недостатки:
• Сильно деградирует по скорости (до n2) при неудачных выборах опорных элементов, что
может случиться при неудачных входных данных. Этого можно избежать, используя такие
модификации алгоритма, как Introsort, или вероятностно, выбирая опорный элемент случайно,
а не фиксированным образом.
• Наивная реализация алгоритма может привести к ошибке переполнения стека, так как ей
может потребоваться сделать O(n) вложенных рекурсивных вызовов. В улучшенных
реализациях, в которых рекурсивный вызов происходит только для сортировки меньшей из
двух частей массива, глубина рекурсии гарантированно не превысит
.
• Неустойчив — если требуется устойчивость, приходится расширять ключ.

( Устойчивая (стабильная) сортировка — сортировка, которая не меняет относительный


порядок сортируемых элементов, имеющих одинаковые ключи).
Academia de Studii Economice din Republica Moldova
Facultatea de Cibernetică, Statistică și Informatică Economică

Referat
la Structuri de Date si Algoritmi

Tema: “Limbajul C versus Limbajul


C++”

1
Elaborat:
Golban Lilian
Caraman Olga
gr. INFa-111

Chisinau ,2012

2
Limbajul C versus Limbajul C++
Limbajul si istoria sa

C++ este un limbaj de programare de uz general proiectat pentru


programatori profesionisti. Cu exceptia unor detalii minore, C++ este bazat
pe limbajul C si este un superset al acestuia. C++ pastreaza forta C-ului
si flexibilitatea acestuia in raport cu interfata hardware-software si
programarea de sistem de nivel scazut (low-level programming). C++
pastreaza eficienta, economia si expresivitatea C-ului. C++ ofera o
platforma pentru a suporta programarea orientata pe obiecte (Object
Oriented Programming - OOP) dar si posibilitatea de abstractizare a
problemelor de nivel inalt (high-level programming). C++ merge mai
departe decit ADA in suportul OOP si este similar cu Modula-2 in
simplitate si suport pentru modularitate.
C++ este un limbaj hibrid:
a) se poate programa folosind paradigma OOP in mod pur.
b) se poate folosi C++ ca un limbaj procedural ("obisnuit") ce contine
citeva "imbunatatiri" peste C.
Aceasta dualitate reflecta si o schimbare asupra modului de
rezolvare a problemelor pentru programatori: nu se schimba numai
"unealta" ci si metoda si modul de gindire.
C++ se considera a fi "descendentul direct" al limbajelor BCPL si
Simula67. Unele notiuni (subclase, clase derivate, functii virtuale) sint
luate din Simula, altele ("suprapunerea" operatorilor) sint reminiscente din
Algol68. Este clar ca C++ (ca si alte limbaje moderne) reprezinta o
evolutie si o rafinare a citorva din cele mai bune caracteristici ale
limbajelor anterioare. Cel mai aproape este de C (bineinteles).
C++ a fost "inventat" la inceputul anilor '80 la Bell Laboratories de
Bjarne Stroustrup. Versiuni anterioare ale limbajului (cunoscute ca "C cu
clase") au fost utilizate inca inainte de 1980. Limbajul a fost dezvoltat
pentru scrierea unor aplicati complexe de simulare a proceselor ("event
driven"). Prima "aparitie" in afara grupului de la Bell Labs a fost in iulie '83.
Numele de C++ (dat de Rick Mascitti) sugereaza natura evolutiva a
schimbarilor fata de C. "++" este operatorul de incrementare. Un nume
mai scurt ca de exemplu "C+" ar fi fost o "eroare de sintaxa"; un alt nume
(de exemplu "D") ar "rupe" legatura profunda cu C-ul.
O decizie chieie in proiectarea C++ a fost pastrarea compatibilitatii
cu C-ul. Acesta permite pastrarea integritatii tuturor aplicatiilor deja scrise
3
in C (biblioteci, unelte software, etc.) si totodata ofera o platforma
programatorilor de C pentru a se putea adapta usor noului limbaj.
De fapt chiar C-ul a fost influentat de noul C++, astfel incit noul
standard ANSI prevede citeva caracteristici esentiale provenite din C++
(de exemplu prototipurile de functii).
C++ este proiectat pentru a dezvolta software de sistem (pe scara
larga) oferind un suport superior C-ului (si tuturor celorlalte limbaje) pentru
programarea in echipa.
Ca limbaj C++ inca mai evolueaza. Nu exista inca un standard
formal al limbajului. Chiar "inventatorul" limbajului (Bjarne Stroustrup) si
echipa de la Bell Labs continua sa faca rafinamente ale limbajului.

4
Diferente intre limbajele C si C++

In afara suportului pentru OOP (care este diferenta esentiala) C si


C++ difera si prin anumite imbunatatiri aduse de C++ limbajului procedural
astfel incit deosebirile dintre cele doua limbaje se pot clasifica in :
a) deosebiri majore: noi tehnici de constructie a software-ului

b) deosebiri minore: noi caracteristici non-OOP ale C++

Deosebiri "majore" (OOP)

Deosebirile ce se refera la un nou mod de a proiecta si construi software


(OOP) sint de fapt noi caracteristici ale limbajului, necunoscute C-ului.
Acestea sint:
1) constructia obiectelor si a claselor de obiecte

2) functii constructori si functii destructori

3) incapsularea datelor

4) clase derivate, mostenirea

5) functii virtuale, polimorfismul

6) un nou sistem de I/O

1)Constructia obiectelor si a claselor de obiecte

Clasa este un tip abstract de date. Noţiunea a apărut din


necesitatea de ascundere a datelor în definiţi tipului abstract. Datele
clasei pot fi protejate prin utilizarea modificatorilor de protecţie.
Modificatori de protecţie sunt public, private, protected. Pentru a aplica
un aşa modificator la o dată careva, se scrie numele modificatorului
urmat de semnul : în faţa declaraţiei datei respective. De exemplu:
private: int x; sau protected: int y, z; double w;
Definiţia unei clase este asemănătoare definiţiei unei structuri.
Cuvântul struct este înlocuit cu class. De exemplu:

class complex
5
{ private: double re;

double im;

public: void add(complex a, complex b);

void sub(complex a, complex b);

double modul( )

{ return sqrt(re * re + im * im);

double arg( ); };

O clasă defineşte un tip abstract de date. Protecţia datelor în clase


este la un nivel mai înalt decât ascunderea datelor în module. Clasa are
o parte privată şi una publică. Partea privată exprimă specificul datelor
din clasă şi asigură implementarea lor. Datele şi funcţiile din partea
privată pot fi accesate numai de funcţiile membru ale clasei. Partea
publică asigură legătura clasei cu restul programului, aducă este
interfaţa dintre clasă şi program. Interfaţa conţine funcţii membru care
sunt publice. Partea publică poate conţine şi date membru, nu numai
funcţii. Dar aceste date nu sunt protejate, la ele pot avea acces şi alte
funcţii din program, nu numai metodele clasei. Folosirea lor nu este
recomandată.

Variabilele ale cărui tip se definesc printr-o clasă se numesc obiecte.


Deci, un obiect este o variabilă de tipul unei clase, de un tip abstract de
date. Aşa, pentru tipul abstract complex, definit mai sus; putem declara
variabile: complex z, w, *p, x[10]; Se pot declara variabile simple,
pointeri, tablouri etc.

2)Functii constructori si functii destructori

Iniţializarea obiectelor este mai dificilă decât a variabilelor de tipuri


predefinite. Pentru iniţializarea obiectelor sunt folosite funcţii membru
speciale ale clasei respective. Aşa funcţie se numeşte constructor.
6
Funcţia constructor are acelaşi nume ca şi clasa respectivă. Ea nu
returnează nici-o valoare şi în acelaşi timp nu se specifică void pentru
tipul returnat. O clasă poate avea mai mulţi constructori, atunci ei sunt
funcţii supraîncărcate. Constructorul este apelat automat la orice
instanţiere a unui obiect de clasa respectivă, el creează obiectul şi
eventual îl iniţializează.

Exemplu de constructor pentru clasa complex:

class complex {

double re;

double im;

// urmeaza functia constructor, care

// implicit initializeaza nr. complex cu 0

public: complex( double x = 0, double y = 0 )

{ re = x;

im = y; }

… };

Destructorul este o funcţie membru specială a clasei, care


acţionează în sens invers constructorului. Ea distruge obiectele, adică le
dealocă, eliberează memoria ocupată de ele. O clasă poate avea un
singur destructor. Numele destructorului coincide cu numele clasei
respective, dar este precedat de caracterul ‘~’. Destructorul nu are
parametri. Ca şi constructorul, funcţia destructor nu returnează nici-o
valoare, dar nu foloseşte cuvântul void pentru a specifica acest lucru.
Deci, antetul destructorului va fi ~nume_clasa( ) , în definiţia clasei, şi
nume_clasa::~nume_clasa( ) înafara definiţiei clasei respective. Nu se
poate determina adresa funcţiei destructor. Dacă o clasă nu conţine
explicit un destructor,

atunci compilatorul va genera automat un destructor implicit pentru ea.

3)Incapsularea datelor
7
Procedeul de ascundere a datelor şi funcţiilor în clase prin
protejarea lor şi permiterea accesului la ele numai metodelor clasei se
numeşte incapsulare.

4)Clase derivate, mostenirea

Moştenirea este una din caracteristicile de bază ale programării


orientate pe obiecte. Moştenirea permite ca o clasă să includă în
declaraţia sa o altă clasă. Ea permite construirea unei ierarhii de clase,
trecerea de la clase generale la clase particulare. Moştenirea permite ca
un obiect să preia proprietăţile altui obiect. Acest lucru permite o
clasificare a obiectelor.
Moştenirea face posibil ca un obiect să fie un exemplar specific al
unui caz mai general. În procesul de moştenire , pentru început se
defineşte o clasă de bază (baza), care stabileşte calităţile comune ale
tuturor obiectelor ce vor deriva din bază. Apoi, se definesc alte clase,
care includ toate caracteristicile clasei de bază şi, în plus, au calităţile
sale proprii. Aceste clase se numesc clase derivate din clasa de bază. În
literatură se mai întâlnesc termenii părinte (baza) şi copil (derivata).

5)Functii virtuale, polimorfismul

Polimorfismul este capacitatea unui obiect de aşi modifica forma în


timpul execuţiei programului. El se realizează prin utilizarea funcţiilor
virtuale. Polimorfismul implementează principiul „o interfaţă, mai multe
metode”.

O funcţie, care este declarată în clasa de bază cu cuvântul virtual


în faţa antetului sau prototipului, şi apoi este redefinită într-o clasă
derivată, se numeşte funcţie virtuală. Funcţia redefinită în clasa derivată
modifică funcţia respectivă din bază şi are prioritate faţă de ea. Funcţia
virtuală, declarată în clasa de bază, specifică o mulţime generală de
acţiuni asupra datelor şi declară forma unică a interfeţei, legăturii dintre
program şi datele claselor. La redefinire în clasa derivată, această
funcţie este concretizată cu o metodă specifică, o funcţie concretă care
realizează operaţiile caracteristice derivatei.

6)Un nou sistem de I/O

Extragerea datelor la ecran. Am văzut cum se extrage la ecran o


constantă şir de caractere:
8
cout << ”Acest sir este extras la ecran”;

În clasa ostream operaţia << este definită pentru toate tipurile


predefinite de date din C++ şi pentru tipul char *, adică pentru şiruri de
caractere.

Fişierul iostream.h conţine definiţia clasei istream, care defineşte


fluxul de intrare. Implicit citirea datelor se face de la intrarea standard
care este claviatura. Cu fluxul de intrare este asociat obiectul cin al
clasei istream. El controlează citirea datelor de la claviatură. Când
folosim cin, trebuie indicate numele variabilelor în care se citesc datele.
Aceste ariabile trebuie să fie declarate preventiv în program. Astfel,

int x; cin >> x;

Deosebiri "minore" (non-OOP)

Deosebirile non-OOP intre C si C++ sint urmatoarele:


1) cuvinte cheie noi

2) delimitator de comentarii la sfirsitul unei linii

3) numele unei structuri compuse (struct sau class) este un nume de tip

4) structuri de tip "union" anonime (fara nume)

5) numele unei enumerari este un nume de tip

6) tipul "void"

7) conversii explicite de tip (cast)

8) compatibilitati noi intre tipurile de date

9) specificatorul "const"

9
10) un nou operator pentru delimitarea domeniului de vizibilitate

11) o declaratie intr-un bloc este o instructiune

12) tipul referinta; parametri referinta in functii

13) prototipuri de functii

14) functii cu numar nespecificat de parametri

15) specificatorul "inline"

16) valori implicite pentru parametri functiilor

17) pointeri la functii; functii ca parametri

18) "suprapunerea" numelor de functii

19) "suprapunerea" operatorilor

20) noii operatori "new" si "delete"

1. Cuvinte cheie noi

Pentru a evidentia noile caracteristici adaugate C-ului au fost create noi


cuvinte cheie (rezervate) pentru C++. Orice program C care foloseste ca
indentificatori unul dintre acesta trebuie sa le schimbe inainte de compilare
cu C++. Aceste cuvinte cheie sint:
asm catch class delete friend

inline new operator private protected

public template this virtual

2. Comentarii

In C++ exista doua tipuri de comentarii:


a) Comentariile C traditionale /* ... */. Sint recomandate pentru
comentariile ce se intind pe mai multe linii.

b) Comentariile specifice C++ delimitate de simbolul "//". Sint folosite


pentru comentariile pe un singur rind: tot ce se afla la dreapta
simbolului "//" (pina la sfirsitul liniei) este considerat comentariu.

10
Exemplu:
/*
program: iterare
scop : afiseaza intregii de la 1 la 9
*/

int main (void)


{
int i ;
for (i=0;i<10;++i)
cout <<i <<"\n"; // afisare i
return 0;
}

3. Numele unei structuri compuse (struct sau union)

In C++ numele unei structuri compuse "struct" sau "union" este considerat
ca nume de tip (ca si cind ar fi fost declarat in C cu instructiunea
"typedef").
Exemplu:
In C In C++
------------------------------------------------------------------------------------------
struct interesectie { struct intersectie {
int culoare_semafor ; int culoare_semafor;
int nr_masini ; int nr_masini;
}; };
struct intersectie retea[50]; intersectie retea[50];

Se declara intii o structura cu numele "intersectie" si apoi se creaza o


instanta (o variabila) cu numele "retea". In C++ nu mai este necesar sa se
puna cuvintul cheie "struct" sau "union" inaintea numelui structurii (se
creaza un nou tip de date). Totusi, pentru pastrarea compatibilitatii cu C
se accepta si vechea sintaxa.

5. Tipuri enumerare

Tipul enumerare este tratat putin diferit in C++ fata de C. Astfel numele
unei enumerari este considerat numele unui tip nou si poate fi folosit ca si
"struct" si "union" pentru declararea unor variabile;
Exemplu:
enum semafor {rosu, galben, verde};
11
semafor s1, s2;
C defineste tipurile enumerate ca fiind tipuri intregi (int). In C++ insa
fiecare tip enumerare este considerat ca unul separat! Acesta inseamna
ca C++ nu permite unei valori de tip "int" sa fie atribuita automat unei
valori de tip "enum". Totusi o valoare "enumerare" poate fi folosita in loc
de "int".
Exemplu:
enum Place {First, Second, Third};
Place John=First; // Ok
int Winner=John; // se poate in acest sens
Place Tom=1; // EROARE in C++, dar merge in C!
Place Mark=(Place) 1; // merge cu modificarea tipului
A treia atribuire este gresita in C++ pentru ca 1 nu e valoare definita
pentru Place.

6. Tipul "void"

Tipul void reprezinta "multimea vida", adica un obiect de tipul "void" nu are
nici o valoare. Chiar daca pare ciudat sa existe un tip pentru care nu
exista valori definite, acesta este foarte util astfel incit chiar si ANSI C l-a
incorporat. Aceasta pentru 2 utilizari remarcabile:
a) o functie ce nu intoarce nici o valoare utilizata de apelant poate fi
declarata ca intorcind o valoare "void". Aceasta elimina posibilitatea ca
o astfel de functie sa intoarca "fortat" o valoare nefolositoare (de obicei
de tip int);

b) "void" poate fi utilizat pentru a defini un pointer la un articol generic (de


un tip oarecare). Un pointer de tip "void*" este utilizat ca pointer la
oricare tip.

8. Compatibilitatile intre tipuri

Spre deosebire de C care este foarte flexibil in definirea compatibilitatilor


intre tipuri, C++ este mai riguros. C++ defineste aceleasi tipuri predefinite
ca si C:
- char, unsigned char, signed char

- int, short int (short), long int (long)


- float, double

- unsigned short int, unsigned int, unsigned long int

12
Dar spre deosebire de C "short int", "int", "long int" sint tipuri DIFERITE!
Chiar daca "short int" e identic ca format si marime cu "int", C++ le
considera diferite, iar pentru asignari trebuie realizate conversii explicite de
tip (cast). La fel si pentru "unsigned char", "char", "signed char" (au
dimensiunea 1) dar nu sint identice in C++. Acest mod de evaluare a
tipurilor este o precautie suplimentara pentru scrierea unor programe cu
adevarat portabile, deoarece diversele arhitecturi pot implementa diferit
aceste tipuri. In plus, pentru tipul "char" se modifica esential dimensiunea
variabilelor de acest tip:

9. Specificatorul "const"

Specificatorul const se foloseste atit in C cit si in C++ pentru a identifica o


"variabila" ce nu poate fi modificata (o constanta). In C cel mai adesea
"const" se foloseste pentru a inlocui constantele literale definite cu
macroinstructiunea "#define".
In C++ valorile declarate cu "const" sint mult mai flexibile: pot inlocui orice
constanta literala. Aceasta posibilitate se foloseste pentru a crea
constante cu tip in locul folosirii macroinstructiunii "#define " ce nu au nici
o informatie de tip.
Exemple:
1) Se poate folosi in C++, dar NU si in C

const int ArraySize=100;


int Array[ArraySize];//Nu merge in C
2) Este ilegala secventa :

const float x = 14.7; // val. lui X este" inghetata"


X = 19; // ilegal !
3) Variable structurate (ca de exemplu tablourile) pot fi declarate cu const:

const float data[] = {1,1,2,2};


const int vector[] = {1,1,1,1};
Nu se poate modifica nici o componenta a celor doua tabele.
4) Se poate folosi "const" si la declararea parametrilor unei functii, avind
ca urmare interzicerea modificarii acelui parametru in functie (chiar daca
este transmis prin referinta):
void print_value (const int value)
{
printf ("\n % d",value);
//in corpul acestei functii nu se poate modifica value
13
}

10. Operatorul pentru definirea domeniului de vizibilitate

Regulile de vizibilitate in C++ se pastreaza din C. In plus insa se introduce


un nou operator "::" pentru a accesa un element ascuns domeniului
curent.
Exemple:
1. Exemplu de redefinire a unei variabile globale.

#include <stdio.h>

char ch='D'; // variabila globala

void hide_identifier(void)
{
//...alte declaratii, eventual
char ch; // se "ascunde" var. globala
ch='e'; // var. locala
printf("Valoarea locala pt ch=%c",ch);
//...alte prelucrari
}

void main(void)
{
hide_identifier();
printf("Valoarea globala pt ch=%c",ch);
}

In esenta operatorul "::" spune: "Nu folosi variabila locala; foloseste


variabila declarata in afara acestui domeniu". Acest operator se mai
foloseste in definirea functiilor specifice OOP (metodele).

12. Tipul referinta

C++ suporta un tip special de identificator numit referinta. O referinta


poate fi vazuta ca o alternativa la numele unei variabile (identificator). O
referinta se indica prin folosirea operatorului "&" in acelasi mod in care se

14
foloseste operatorul "*" pentru a indica un pointer. Notatia "X&" inseamna
"referinta de tipul X".
Exemple:
int i=1;
int& r=i; // r si i se refera la acelasi obiect int
int x=r; // x=1
r=2; // i=2
Referintele sint asemanatoare pointerilor, in sensul ca ele contin adresele
altor variabile. Spre deosebire de pointeri referintele sint implicit dereferite
la utilizare (prin dereferire se intelege indicarea continutului referit de
"ceva") deoarece referinta, prin numele ei indica de fapt CONTINUTUL si
in acest fel ele arata ca niste variabile "uzuale".
O referinta trebuie initializata cind este declarata - astfel aceasta indica
"ceva" (ce deja exista - are alocata memorie) cu un alt nume (numele
referintei). De exemplu, mai sus i deja exista, iar aceasta variabila
(continutul de memorie de la acea adresa) poate fi identificata ulterior si
prin numele (referinta) r.

13. Prototipuri de functii

Un prototip de functie in C++ este o declaratie ce defineste atit tipul


rezultatului "intors" de functie, cit si tipul argumentelor acesteia.
In C functiile erau declarate astfel:
int functie()
Aceasta declaratie nu specifica in nici un fel tipul sau numarul
argumentelor.
In C++ o astfel de functie trebuie declarata astfel:
int functie(char* str, unsigned int len);
sau
int functie(char*, unsigned int);
In acest mod se poate controla eficient tipul si numarul parametrilor formali
cu cei actuali.
Compilatorul de C++ genereaza intern nume pentru functii si informatii
despre tipurile parametrilor. Aceste informatii sint necesare cind mai multe
functii au acelasi nume.

Exemplu:

15
In C++ un program ce utilizeaza 2 functii are structura:

#include <...>

void f1(int, int);


int f2(char*, int);

void main(void)
{
// functia main
}

void f1(int a, int b)


{
// functia f1
}

int f2(char* str, int len)


{
// functia f2
}

14. Functii fara argumente; functii cu nr. arbitrar de parametri

Atit in C cit si in C++ este permisa definirea de functii fara argumente.


Modul de definire este diferit:
/* in C */ /* in C++ */
int doit ( ); int doit (void);
In C++ o functie ce are lista parametrilor vida nu poate accepta argumente
Daca se doreste o lista "deschisa " de parametri atunci trebuie declarata
functia in mod corespunzator:
/* in C *//* in C++ */
int multiparam(); int multiparam(...);
Se observa ca in C++ trebuie folosit neaparat simbolul"..." In aceste
cazuri nu se mai face testarea numarului si tipului argumentelor cu care se
apeleaza functia .

15. Specificatorul "inline"

Avind in vedere procesele ce au loc in momentul apelului unei functii


(plasarea argumentelor pe stiva, executia unei instructiuni call, etc.)
16
precum si la terminarea acesteia (eliberarea stivei de argumente si
variabile locale, revenire la apelant, etc.), uneori, in programe critice din
punct de vedere al timpului de executie este necesar sa nu se foloseasca
aceste procese. Dar, pentru mentinerea structurarii, se permite si se
incurajeaza folosirea functiilor de un tip special: functii "inline".
Aceste functii nu sint compilate ca "bucati separate de cod", ci acolo unde
apare un apel al functiei compilatorul insereaza CORPUL efectiv al
functiei.
Exemplu:

inline int aduna(int a, int b)


{
return a+b;
}

Timpul "salvat" de acest tip de functie se "plateste" prin cresterea


dimensiunii programului. Daca functia are multe linii de cod si este
apelata de multe ori se recomanda folosirea acestui tip de functie cu
maxima atentie.
Exista citeva reguli importante referitoare la folosirea acestui tip de functie:
a) functiile inline trebuie definite inainte de a fi utilizate (nu este de ajuns
prototipul, trebuie definirea completa)
a) cuvintul "inline " este doar o sugestie pentru compilator (la fel ca
"register"). Daca functia ce se doreste a fi "inline" este foarte complexa
sau daca sint foarte multe functii "inline" si spatiul disponibil scade
foarte mult, este posibil ca aceasta sugestie sa nu fie luata
considerare de compilator!

18. Suprapunerea numelor de functii

Limbajul C ofera posibiltatea crearii unor structuri de date noi, definite de


programator (ca struct, union, etc.). Dar operatiile ce se pot executa PE
aceste structuri definite de programator se implementeaza NUMAI ca
functii "exterioare", facind in acest fel programul mult mai greu de inteles si
foarte greu de "extins" si "adaptat".
Limbajul C++ ofera posibilitatea existentei (si definirii) atit a functiilor cit SI
a unor operatori care sa accepte argumente de tipuri definte de
programatori. Aceste functii si operatori se "suprapun" peste cele deja

17
existente si de aceea se numesc "functii suprapuse" si "operatori
suprapusi".
In C (ca de fapt in majoritatea limbajelor de programare) fiecare functie
trebuie sa aiba un nume UNIC. Acest lucru poate fi necorespunzator in
anumite situatii; de exemplu functia "abs" ce intoarce valoarea absoluta a
unei valori numerice. Avind in vedere diferitele tipuri ale argumentului si
unicitatii numelui de functie, exista in C 3 functii diferite:
int abs(int i);
long labs(long l); // in C
double fabs(double d);
Toate avind acelasi rol este "ciudat" ca au nume diferite. In C++ este
posibil sa existe 3 functii cu acelasi nume dar tipul argumentului /
argumentelor diferite:
int abs(int i);
long abs(long l);
double abs (double d);
O astfel de functie se numeste "functie suprapusa" (overload function),
deoarece are mai multe definitii, un nume de functie putind astfel fi
interpretat in mai multe moduri. O functie poate fi "suprapusa" in 2 moduri:
a) definind efectiv mai multe functii ce au acelasi nume;

b) definind un tip de date ("class") ce contine una sau mai multe functii cu
numele unei functii deja existente;
Exemplu:
In functie de tipul parametrilor compilatorul modifica numele functiilor
pentru a apela corect "versiunea" ceruta de tipul parametrului efectiv,
astfel incit
abs(-10); // apeleaza int abs(int i);
abs(-100000); // apeleaza long abs(long l);
abs(-12.34); // apeleaza double abs(double d);
Daca se apeleaza "abs" cu alt tip de argument pt. rezolvarea ambiguitatii
se foloseste procesul general din C++: se apeleaza implementarea care
ofera cele mai usoare conversii.

19. Suprapunerea operatorilor

Un tip particular, dar foarte important de functii il reprezinta


"functiile-operatori" sau mai simplu "operatorii". Aceste "functii-operatori"
sint functii utilizate pentru a extinde setul de operatori predefiniti de C++. O
"functie- operator" este atasata unui operator predefinit si atunci cind se
18
foloseste operatorul respectiv intr-o expresie in mod AUTOMAT este
apelata "functia-operator" corespunzatoare. De remarcat ca unui operator
predefinit i se pot "asocia" mai multe "functii- operatori" (pentru diverse
tipuri de operanzi), de aici provenind denumirea acestora de "operatori
suprapusi".
Definirea unor astfel de operatori este foarte utila cind se implementeaza
operatii pe tipuri de date definite de programator.
Exemplu:
Sa se implementeze tipul de date "complex": structura de date
corespunzatoare SI operatorii asociati acesteia.
Structura de date este evidenta:
struct complex {
double real,imag;
};

Pe aceasta structura definim, in maniera traditionala C, de exemplu, 3


functii:
cplx_set : pentru atribuirea de valori nr. complexe (setare)
cplx_add : pentru adunarea a 2 numere complexe
cplx_sub : pentru scaderea a 2 numere complexe

complex cplx_set(double r, double i)


{
complex temp;
temp.real=r;
temp.imag=i;
return temp;
}

complex cplx_add(complex c1, complex c2)


{
complex temp;
temp.real=c1.real+c2.real;
temp.imag=c1.imag+c2.imag;
return temp;
}

complex cplx_sub(complex c1, complex c2)


{
complex temp;
19
temp.real=c1.real-c2.real;
temp.imag=c1.imag-c2.imag;
return temp;
}

Cu aceste functii se poate folosi structura de date "complex", de exemplu


astfel:

void main(void)
{
complex a, b, c, d;
a=cplx_set(1.0,1.0); // a= 1.0 + i*1.0
b=cplx_set(2.0,2.0); // b= 2.0 + i*2.0
c=cplx_add(a,b); // c=a+b
d=cplx_sub(a,b); // d=a-b

// afisare c, d
}

Se observa ca pentru realizarea OPERATIILOR de adunare, scadere, etc.


se apeleaza 2 FUNCTII, specificindu-se argumente, etc. Mult mai natural
ar fi fost sa se scrie, de Exemplu:
c = a+b;
d = a-b;
adica in locul apelului unei functii obisnuite sa se apeleze o
"functie-operator". Acest lucru este permis in C++ datorita posibiltatii de
definire a propriilor "functii-operatori" care sa inlocuiasca (pentru un tip de
date definit de programator) operatorul predefinit si sa apeleze AUTOMAT
(in momentul folosirii acestuia) a "functiei-operator" corespunzatoare.
Acest mecanism se numeste "suprapunerea operatorilor".
Sintactic o "functie-operator" se declara astfel:

<tip_rezultat> operator<op> ( <lista_de_argumente> )


{
<corpul functiei>
}

20
Se observa ca au aceeasi sintaxa cu cea a functiilor uzuale, numai ca
numele functiei-operator este "operator<op>".
Exemplu:
Pentru structura "complex" definita anterior se pot defini 2 functii-operator
pentru adunarea si scaderea numerelor complexe:

complex operator+ (complex c1, complex c2)


// inlocuieste operatorul "+" pentru numere complexe
{
complex temp;
temp.real=c1.real+c2.real;
temp.imag=c1.imag+c2.imag;
return temp;
}

complex operator- (complex c1, complex c2) // inlocuieste "-"


{
complex temp;
temp.real=c1.real-c2.real;
temp.imag=c1.imag-c2.imag;
return temp;
}

Din acest moment operatorii predefiniti in C++ "+" si "-" (folositi pentru
numere intregi/reale) pot fi folositi si pentru numere "complexe" (definite de
programator), astfel:
c = a+b;
d = a-b;
Aceste "instructiuni" (expresii) pot fi interpretate (si chiar folosite daca se
considera neaparat necesar) astfel:
c = operator+(a,b);
d = operator-(a,b);

Ori de cite ori se va intilni, din momentul definirii, operatorul "+" sau "-" (in
acest caz, sau altul daca a fost definit) se va testa TIPUL OPERANZILOR
implicati in expresie:
- daca operanzii sint de tip predefinit (int, float, etc.) se realizeaza
operatia cunoscuta (predefinita) C++;

21
- daca operanzii sint de tip definit de programator (aici "complex") se
apeleaza functia-operator corespunzatoare:
pentru "+" ----> operator+
pentru "-" ----> operator-

Observatii:

1. Nu se pot defini functii-operator decit pentru operatorii C++ deja definiti.


Nu se pot "crea" operatori noi ! Deci prin declararea unei
functii-operator automat se "suprapune" un operator existent.

2. Se pot suprapune toti operatorii C++, cu exceptia

a) . .* :: ?:

3. Alegerea "corecta" a operatorului (in functie de tipul operanzilor) se


face dupa aceleasi reguli ca la functiile suprapuse.

4. Prioritatea operatorilor suprapusi este aceeasi cu prioritatea


operatorului intern (predefinit). Astfel, de exemplu, "*" va fi
INTOTDEAUNA mai prioritar decit "+".

5. Operatorii definiti de programator pot fi functii "in-line", si este foarte


recomandabil sa fie asa (performante superioare).
6. Cel putin un argument al unui operator definit de programator trebuie
sa fie de un tip definit de acesta !

7. Nu se pot "combina" operatori pentru a crea unul nou. Deci se poate


suprapune operatorul "+=" (de exemplu) ca UN operator si NU se pot
combina operatorii "+" si "=" pentru a crea alt "+=".

8. Un operator suprapus, redefinit de programator, poate avea mai multe


definitii (implementari), ca si functiile uzuale, diferind insa aritatea si/sau
tipul argumentelor:

complex operator+(complex c1, complex c2);


complex operator+(complex c1, float c2); // etc...

20. Operatorii de gestionare dinamica a memoriei: new & delete

In programele traditionale C toate operatiile legate de alocarea/dealocarea


dinamica a memoriei se face prin intermediul unor FUNCTII de biblioteca,
ca de exemplu "malloc" si "free". C++ defineste o noua METODA de
22
gestiune dinamica a memoriei prin intermediul OPERATORILOR
predefiniti "new" si "delete".
Exemplu:
a) In C utilizarea (gestionarea) memoriei dinamice se face:

#include <stdio.h>
void func(void)
{
int* i; // i este pointer la un intreg
i=malloc(sizeof(int)); // alocare dinamica
*i=10;
printf("intregul alocat dinamic = %d",*i);
free(i); // apel de functie de biblioteca
}

b) In C++ aceeasi functie arata astfel:

#include <iostream.h>
void func(void)
{
int* i = new int; // declarare & alocare dinamica
*i=10;
cout << "intregul alocat dinamic" << *i;
delete i; // dealocare cu operatorul predefinit delete
}

Se observa ca operatorul "new" a inlocuit functia "malloc", iar operatorul


"delete" functia de biblioteca (standard) "free".
Sintaxa celor 2 operatori este:
<pointer_la_tip> = new <tip>
si
delete <pointer_la_tip>
unde:
pointer_la_tip = numele pointerului la zona de memorie alocata
dinamic, pentru o variabila de tipul "tip";

23
tip = tipul variabilei ce se aloca dinamic (simplu sau compus -
orice structura, tablou, etc!!!)
Daca operatorul "new" nu poate aloca cantitatea de memorie solicitata (de
dimensiunea tipului "tip") va intoarce (pentru pointer) valoarea "NULL".
Motivul pentru care s-au definit acesti doi operatori noi este flexibilitatea
sporita ce o ofera. Spre deosebire de functiile C-ului
(malloc,calloc,free,etc.), in C++ orice noua structura de date definita de
utilizator (clasa) poate redefini acesti operatori (ca de altfel orice alt
operator). In plus, daca NU se definesc noi "versiuni" pentru acestia, se
pot folosi operatorii predefiniti ! Prin aceasta se da programatorului o
foarte mare flexibilitate in gestionarea dinamica a memoriei.
Avind in vedere ca functiile de biblioteca nu le-a "sters" nimeni, se pot
folosi intr-un program ambele metode de gestionare dinamica a memoriei.
Acest lucru poate conduce insa la probleme serioase (in special daca
"new" si/sau "delete" au fost redefiniti) si mai ales poate sa conduca la
inconsistente. De aceea este recomandabila folosirea metodei C++.
Un alt motiv pentru care este recomandabila folosirea operatorului "new"
este aceea ca nu trebuie sa i se transmita acestuia dimensiunea obiectului
ce se aloca (ca la functiile C "malloc","calloc",etc).
Operatorul "new" poate fi folosit pentru alocarea diferitelor tipuri de
obiecte:
int* p = new int; // se aloca un intreg
int* vec = new int[10]; // se aloca un tablou de 10 intregi

// vec indica primul element


int* *pi = new int*[10]; // se aloca un tablou de 10 pointeri

Operatorul "delete" este folosit pentru dealocarea spatiului de memorie


alocat dinamic, dar NUMAI cu operatorul "new":
delete p; // se dealoca un intreg
delete vec; // ATENTIE: se dealoca numai primul intreg
delete[10] vec; // se dealoca TOT tabloul, de 10 intregi
delete[10] pi; // se dealoca spatiul ocupat de 10 pointeri

Observatii:
1. Pentru dealocarea unui tablou operatorului "delete" trebuie sa i se
specifice numarul de elemente ale tabloului respectiv (ca in exemplul
24
anterior). Aceasta dimensiune se specifica in paranteze drepte imediat
dupa numele operatorului.

2. Chiar daca este un operator, "new" poate fi folosit SINTACTIC ca o


functie (daca este mai sugestiv asa):

int* p;
p=new(int[10]);// p=pointer la un tablou de 10 intregi
3. Memoria alocata dinamic NU se elibereaza "automat", ci numai dupa
apelul operatorului "delete".

4. Nu se poate elibera memoria detinuta de un obiect ce NU a fost alocat


cu "new".

Exemplu:
Sa se gestioneze informatiile despre un numar oarecare de salariati ai
unei firme; numarul se cunoaste la rulare (gestionare dinamica).
Informatiile despre salariati sint: numele, virsta, salariul.

#include <stdio.h>

struct angajat {
char nume[40];
int virsta;
float salariu;
};

void main(void)
{
angajat* ang_ptr;
int nr;

printf("Citi salariati :"); scanf("%d",&nr);


ang_ptr = new angajat[nr]; // se aloca un tablou
for(int i=0;i<nr;++i) {
printf("\nDatele pt. salariatul %d",i+1);
printf("\nNumele :"); scanf("%s",ang_ptr[i]->nume);
printf("\nVirsta :"); scanf("%d",ang_ptr[i]->virsta);
printf("\nSalariu :"); scanf("%f",ang_ptr[i]->salariu);
}
// urmeaza alte prelucrari ...
25
delete[nr] angajat; // se dealoca spatiul
}

26
Laborator 4 „Utilizarea diferitelor structuri de date”. (Obligatoriu)

1. Creați un tablou bidimensional de tip întreg. Introduceți date inițiale în fiecare element al tabloului
(masivului) de la tastatură. Creați posibilitatea de a modifica valoarea elementului dorit de la
tastatură, adică de la tastatură se întroduc coordonatele (rândul și coloana) elementului care urmează
să fie modificat, apoi, de la tastatură, se introduce și valoarea noua a elementului tabloului.

2. Creați un vector (tablou unidimensional), elementele căruia vor fi structuri, ce vor conține informații
despre un student, și anume:
• Nume;
• Prenume;
• Anul, luna și ziua nașterii;
• Grupa în care învață.
Introduceți, de la tastatură informații despre fiecare student. Creați posibilitatea de a modifica
informația despre student la apelul utilizatorului, de la tastatură.

3. Punctul precedent realizaț-il folosind clasele.

4. De îndeplinit punctual 1, dar folosind pointeri.

Лабораторная работа 4 „Использование различных структур данных” (Обязательно)

1. Создать двумерную таблицу целого типа. Исходные данные вводятся с клавиатуры.


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

2. Создать вектор (одномерная таблица), элементы которого являются структурами


содержащими следующую информацию о студенте:
• Имя;
• Фамилия;
• Год, Месяц, День рождения;
• Номер группы где обучается.
Введите с клавиатуры информацию касающуюся каждого студента. Предусмотреть
возможность изменения информации на запрос пользователя при помощи клавиатуры.

3. Реализовать предыдущий пункт используя классы.

4. Реализовать первый пункт используя указатели.

27
http://49l.ru/a/algoritm_boyera_mura_-_opisanie_algoritma

Алгоритм Бойера Мура - Описание


алгоритма
Алгоритм основан на трёх идеях.

1. Сканирование слева направо, сравнение справа налево. Совмещается начало текста и


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

Если же какой-то символ шаблона не совпадает с соответствующим символом строки,


шаблон сдвигается на несколько символов вправо, и проверка снова начинается с последнего
символа.

Эти «несколько», упомянутые в предыдущем абзаце, вычисляются по двум эвристикам.

2. Эвристика стоп-символа. Предположим, что мы производим поиск слова «колокол». Первая


же буква не совпала — «к». Тогда можно сдвинуть шаблон вправо до последней буквы «к».

Строка: * * * * * * к * * * * * *

Шаблон: к о л о к о л

Следующий шаг: к о л о к о л

Если стоп-символа в шаблоне вообще нет, шаблон смещается за этот стоп-символ.

Строка: * * * * * а л * * * * * * * *

Шаблон: к о л о к о л

Следующий шаг: к о л о к о л

В данном случае стоп-символ — «а», и шаблон сдвигается так, чтобы он оказался прямо за
этой буквой. В алгоритме Бойера-Мура эвристика стоп-символа вообще не смотрит на
совпавший суффикс, так что первая буква шаблона окажется под «л», и будет проведена
одна заведомо холостая проверка.

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

28
!

Строка: * * * * к к о л * * * * *

Шаблон: к о л о к о л

Следующий шаг: к о л о к о л ?????

В таких ситуациях выручает третья идея АБМ — эвристика совпавшего суффикса.

3. Эвристика совпавшего суффикса. Если при сравнении строки и шаблона совпало один или
больше символов, шаблон сдвигается в зависимости от того, какой суффикс совпал.

Строка: * * т о к о л * * * * *

Шаблон: к о л о к о л

Следующий шаг: к о л о к о л

В данном случае совпал суффикс «окол», и шаблон сдвигается вправо до ближайшего


«окол». Если подстроки «окол» в шаблоне больше нет, но он начинается на «кол», сдвигается
до «кол», и т. д.

Обе эвристики требуют предварительных вычислений — в зависимости от шаблона поиска


заполняются две таблицы. Таблица стоп-символов по размеру соответствует алфавиту;
таблица суффиксов — искомому шаблону. Именно из-за этого алгоритм Бойера-Мура не
учитывает совпавший суффикс и несовпавший символ одновременно — это потребовало бы
слишком много предварительных вычислений.

Опишем подробнее обе таблицы.

Таблица стоп-символов
Считается, что символы строк нумеруются с 1.
В таблице стоп-символов указывается последняя позиция в needle каждого из символов
алфавита. Для всех символов, не вошедших в needle, пишем 0. Например,
если needle=«abcdadcd», таблица стоп-символов будет выглядеть так.

Символ a b c d

Последняя позиция 5 2 7 6 0

Обратите внимание, для стоп-символа «d» последняя позиция будет 6, а не 8 — последняя


буква не учитывается. Это известная ошибка, приводящая к неоптимальности. Для АБМ она
не фатальна, но фатальна для упрощённой версии АБМ — алгоритма Хорспула.

Если несовпадение произошло на позиции i, а стоп-символ c, то сдвиг будет i-StopTable.


Таблица суффиксов

29
Для каждого возможного суффикса S шаблона needle указываем наименьшую величину, на
которую нужно сдвинуть вправо шаблон, чтобы он снова совпал с S. Если такой сдвиг
невозможен, ставится |needle|. Например, для того же needle=«abcdadcd» будет:

Суффикс d cd dcd ... abcdadcd

Сдвиг 1 2 4 8 ...
8

Иллюстрация

было ? ?d ?cd ?dcd ...


abcdadcd

стало abcdadcd abcdadcd abcdadcd abcdadcd ...


abcdadcd

Если шаблон начинается и заканчивается одной и той же комбинацией букв, |needle| вообще
не появится в таблице. Например, дляneedle=«колокол» для всех суффиксов сдвиг будет
равен 4.

Суффикс л ол ... олокол колокол

Сдвиг 1 4 4 ... 4
4

Иллюстрация

было ? ?л ?ол ... ?олокол


колокол

стало колокол колокол колокол ... колокол


колокол

Существует быстрый алгоритм вычисления таблицы суффиксов. Этот алгоритм использует


префикс-функцию строки.

m = length

pi = префикс-функция

pi1 = префикс-функция)

for j=0..m

suffshift = m - pi

for i=1..m

j = m - pi1

suffshift = min

30
Здесь suffshift соответствует всей совпавшей строке; suffshift — пустому суффиксу.
Поскольку префикс-функция вычисляется за O операций, вычислительная сложность этого
шага также равняется O.

31
Algoritmul HeapSort. –Structuri si algoritmi de prelucrarea
datelor.
Algoritmul HeapSort.

Este un algoritm, care a primit denumirea de aranjarea piramidala (HeapSort). Ideea sa, consta
din: in loc de completare aborele se formeaza un sir a[1], a[2],…,a[n] aranjat in piramida ,
aranjarea consta ca fiecare a[i] sa indeplineasca conditia a[i]<=a[2i]si a[i]<=a[2i+1]. Apoi
piramida se foloseste pentru aranjare. Cea mai clara metoda de constructi a piramidei,se uita la
aranjarea sirul intr-un arbore, prezentat in fig.2.9. Sirul este reprezentat ca arbore binar a carui
virf corespunde cu elimentul sirului a[1]. La nivelul doi se gasesc elementele a[2]si a[3]. La
nivelul trei a[4],a[5], a[6] a[7] si asa mai departe. Se observa ca petru un sir cu un numar impar
de elemente, arborele va fi echilibrat, dar pentr-un un sir cu un numar par de elimente, n elimente
a[n] va fi in partea stinga a foii asemanator cu un arbore

a1=8

a2=23 a3=5

a4=65 a5=44 a6=33 a7=1

Pasul 0
a8=6

Fig2.9

În mod evident,la construirea piramidei suntem interesati de elementele a[n / 2], a[n/2-1], ...,
a[1] pentru sirurii cu un număr par de elemente, și elementele a[(n-1) 2], a[(n-1) / 2-1], ...,a[1]
pentru sirurii cu un număr impar de elemente (ca numai aceste sunt elemente esențiale pentru a

32
limita piramida). Fie i cel mai mare indice, printre indicii de elemente care sunt limitări
importante ale piramidei. Apoi se ia elementul a[i] din arborele,apoi urmeaza procedura de
screening(depistare), care constă în faptul că ramura a arborelui este selectata corespunzator, min
(a[2i], a[2i+1]), precum și valoarea a[i] este schimbata cu valoarea elementului corespunzător.
Dacă acest element nu este o ramura arborelui, pentru el se face aceiasi procedură etc. Aceste
acțiuni sunt realizate succesiv pentru a [i], a [i-1], ..., a [1]. Este ușor de observat că rezultatul
este o vizualizare arborescentă a piramidei pentru sirul original (succesiune de etape utilizate în
exemplele de sir este prezentată în figurile 2.10 - 2.13).

a1=8

a2=23 a3=5

a4=6 a5=44 a6=33 a7=1

a8=65
Pasul 1
Fig. 2.10.

33
a1=8

a2=23 a3=1

a4=6 a5=44 a6=33 a7=5

a8=65
Pasul 2
Fig. 2.11

a1=8

a2=6 a3=1

a4=23 a5=44 a6=33 a7=5

a8=65
Fig. 2.12. Pasul 3

34
a1=1

a2=6 a3=8

a4=23 a5=44 a6=33 a7=5

a8=65
Pasul 4
Fig. 2.13.

În 1964, Floyd a propus o metoda de a construi o piramida, fără construirea explicită a


arborelui (deși metoda se bazează pe aceleași idei.)Construirea de piramide de Floyd pentru sirul
noastru standard, se arată în tabelul

Тabelul 2.7 Exemplu de construiere a piramidei

Starea initiala a sirului 8 23 5 |65| 44 33 1 6

Pasul 1 8 23 |5| 6 44 33 1 65

Pasul 2 8 |23| 1 6 44 33 5 65

Pasul 3 |8| 6 1 23 44 33 5 65

1 6 8 23 44 33 5 65
Pasul 4
1 6 5 23 44 33 8 65

Tabelul 2.8 arată cum să sortați utilizând piramide construite.Esența algoritmului este
urmatoarea. Fie i - cel mai mare indice a sirului, pentru care sunt semnificativ condițiile de
piramida. Apoi, incepind cu a[1]pina la a[i],se efectuiaza următoarele acțiuni. La fiecare pas, se
alege ultimul element al piramidei (în cazul nostru, primul element este selectat a[8]). Se
schimba cu locul cu a[1], apoi pentru a[1]se utilizeaza de screening(depistarea). La fiecare pas,
numărul de elemente din piramida este redus cu 1 (după prima etapă a piramidei sunt considerate
ca elemente ale a[1], a[2], ..., a [n-1], după a două a[1] a[2], ..., a [n-2], și așa mai departe, până

35
când piramida ramine doar un element). Este ușor de vazut acest lucru (ilustrat în Tabelul 2.8),
care rezultatul va fi un sir aranjat în ordine descrescătoare. Aveți posibilitatea să modificați
metoda de construcție a piramidelor și sortarea pentru a obține comenzi în ordine crescătoare,
dacă modificați starea unei piramide pe a[i]> = a[2i] si a[1]> = a[2i+1] pentru toate valorile
semnificative a indicelui i..

Таbelul 2.8 Sortarea cu ajutorul piramidei

Piramida initiala 1 6 5 23 44 33 8 65

65 6 5 23 44 33 8 1

Pasul 1 5 6 65 23 44 33 8 1

5 6 8 23 44 33 65 1

65 6 8 23 44 33 5 1

Pasul 2 6 65 8 23 44 33 5 1

6 23 8 65 44 33 5 1

33 23 8 65 44 6 5 1
Pasul 3
8 23 33 65 44 6 5 1

44 23 33 65 8 6 5 1
Pasul 4
23 44 33 65 8 6 5 1

65 44 33 23 8 6 5 1
Pasul 5
33 44 65 23 8 6 5 1

65 44 33 23 8 6 5 1
Pasul 6
44 65 33 23 8 6 5 1

Pasul 7 65 44 33 23 8 6 5 1

Algoritmul sortari utilizand o piramida necesita îndeplinirea ordinea de trepte nxlog n (log -
binar) în cel mai rau caz, ceea ce îl face deosebit de atractiv pentru sortarea tablouri mari.

36
Быстрая сортировка (англ. quicksort)

Ref: https://dic.academic.ru/dic.nsf/ruwiki/46738

Aнимированная схема алгоритма

Анимированная схема алгоритма


Быстрая сортировка (англ. quicksort), часто называемая qsort по имени реализации в
стандартной библиотеке языка Си — широко известный алгоритм сортировки, разработанный
английским информатиком Чарльзом Хоаром в 1960 году. Один из быстрых известных
универсальных алгоритмов сортировки массивов (в среднем O(n log n) обменов при
упорядочении n элементов), хотя и имеющий ряд недостатков.

Краткое описание алгоритма


• выбрать элемент, называемый опорным.
• сравнить все остальные элементы с опорным, на основании сравнения разбить множество на
три — «меньшие опорного», «равные» и «большие», расположить их в порядке меньшие-
равные-большие.
• повторить рекурсивно для «меньших» и «больших».
Примечание: на практике обычно разделяют сортируемое множество не на три, а на две
части: например, «меньшие опорного» и «равные и большие». Такой подход в общем случае
оказывается эффективнее, так как для осуществления такого разделения достаточно одного
прохода по сортируемому множеству и однократного обмена лишь некоторых выбранных
элементов.

Алгоритм
Быстрая сортировка использует стратегию «разделяй и властвуй». Шаги алгоритма таковы:
2. Выбираем в массиве некоторый элемент, который будем называть опорным элементом. С
точки зрения корректности алгоритма выбор опорного элемента безразличен. С точки зрения
повышения эффективности алгоритма выбираться должна медиана, но без дополнительных
сведений о сортируемых данных её обычно невозможно получить. Известные стратегии:
выбирать постоянно один и тот же элемент, например, средний или последний по положению;
выбирать элемент со случайно выбранным индексом.

Медиа́на (50-й процентиль, квантиль 0,5) — возможное значение признака, которое делит
ранжированную совокупность (вариационный ряд выборки) на две равные части: 50 %
«нижних» единиц ряда данных будут иметь значение признака не больше, чем медиана, а
«верхние» 50 % — значения признака не меньше, чем медиана.

4. Операция разделения массива: реорганизуем массив таким образом, чтобы все


элементы, меньшие или равные опорному элементу, оказались слева от него, а
все элементы, большие опорного — справа от него. Обычный алгоритм
операции:

37
1. Два индекса — l и r, приравниваются к минимальному и максимальному
индексу разделяемого массива соответственно.
2. Вычисляется индекс опорного элемента m.
3. Индекс l последовательно увеличивается до тех пор, пока l-й элемент
не превысит опорный.
4. Индекс r последовательно уменьшается до тех пор, пока r-й элемент не
окажется меньше либо равен опорному.
5. Если r = l — найдена середина массива — операция разделения
закончена, оба индекса указывают на опорный элемент.
6. Если l < r — найденную пару элементов нужно обменять местами и
продолжить операцию разделения с тех значений l и r, которые были
достигнуты. Следует учесть, что если какая-либо граница (l или r) дошла
до опорного элемента, то при обмене значение m изменяется на r-й или
l-й элемент соответственно.
5. Рекурсивно упорядочиваем подмассивы, лежащие слева и справа от опорного
элемента.
6. Базой рекурсии являются наборы, состоящие из одного или двух элементов.
Первый возвращается в исходном виде, во втором, при необходимости,
сортировка сводится к перестановке двух элементов. Все такие отрезки уже
упорядочены в процессе разделения.

Поскольку в каждой итерации (на каждом следующем уровне рекурсии) длина


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

Интересно, что Хоар разработал этот метод применительно к машинному переводу:


дело в том, что в то время словарь хранился на магнитной ленте, и если упорядочить
все слова в тексте, их переводы можно получить за один прогон ленты. Алгоритм был
придуман Хоаром во время его пребывания в Советском Союзе, где он обучался в
Московском университете компьютерному переводу и занимался разработкой русско-
английского разговорника (говорят, этот алгоритм был подслушан им у русских
студентов).[1]

Оценка эффективности
38
QuickSort является существенно улучшенным вариантом алгоритма сортировки с
помощью прямого обмена (его варианты известны как «Пузырьковая сортировка» и
«Шейкерная сортировка»), известного, в том числе, своей низкой эффективностью.
Принципиальное отличие состоит в том, что после каждого прохода элементы делятся
на две независимые группы. Любопытный факт: улучшение самого неэффективного
прямого метода сортировки дало в результате эффективный улучшенный метод.

• Лучший случай. Для этого алгоритма самый лучший случай — если в каждой
итерации каждый из подмассивов делился бы на два равных по величине массива. В
результате количество сравнений, делаемых быстрой сортировкой, было бы равно
значению рекурсивного выражения CN = 2CN/2+N, что в явном выражении дает
примерно N lg N сравнений. Это дало бы наименьшее время сортировки.

• Среднее. Даёт в среднем O(n log n) обменов при упорядочении n элементов. В


реальности именно такая ситуация обычно имеет место при случайном порядке
элементов и выборе опорного элемента из середины массива либо случайно.
На практике (в случае, когда обмены являются более затратной операцией, чем
сравнения) быстрая сортировка значительно быстрее, чем другие алгоритмы с
оценкой O(n lg n), по причине того, что внутренний цикл алгоритма может быть
эффективно реализован почти на любой архитектуре. 2CN/2 покрывает расходы по
сортировке двух полученных подмассивов; N — это стоимость обработки каждого
элемента, используя один или другой указатель. Известно также, что примерное
значение этого выражения равно CN = N lg N.

• Худший случай. Худшим случаем, очевидно, будет такой, при котором на каждом
этапе массив будет разделяться на вырожденный подмассив из одного опорного
элемента и на подмассив из всех остальных элементов. Такое может произойти, если
в качестве опорного на каждом этапе будет выбран элемент либо наименьший, либо
наибольший из всех обрабатываемых.
Худший случай даёт O(n²) обменов. Но количество обменов и, соответственно, время
работы — это не самый большой его недостаток. Хуже то, что в таком случае глубина
рекурсии при выполнении алгоритма достигнет n, что будет означать n-кратное
сохранение адреса возврата и локальных переменных процедуры разделения
массивов. Для больших значений n худший случай может привести к исчерпанию
памяти во время работы алгоритма. Впрочем, на большинстве реальных данных
можно найти решения, которые минимизируют вероятность того, что понадобится
квадратичное время.

39
Достоинства и недостатки
Достоинства:
• Один из самых быстродействующих (на практике) из алгоритмов внутренней сортировки
общего назначения.
• Прост в реализации.
• Требует лишь O(Lgn) дополнительной памяти для своей работы. (Не улучшенный
рекурсивный алгоритм в худшем случае O(n) памяти)
• Хорошо сочетается с механизмами кэширования и виртуальной памяти.
• Существует эффективная модификация (алгоритм Седжвика) для сортировки строк —
сначала сравнение с опорным элементом только по нулевому символу строки, далее
применение аналогичной сортировки для «большего» и «меньшего» массивов тоже по
нулевому символу, и для «равного» массива по уже первому символу.
• Работает на связанных списках и других структурах с последовательным доступом.

Недостатки:
• Сильно деградирует по скорости (до n2) при неудачных выборах опорных элементов, что
может случиться при неудачных входных данных. Этого можно избежать, используя такие
модификации алгоритма, как Introsort, или вероятностно, выбирая опорный элемент случайно,
а не фиксированным образом.
• Наивная реализация алгоритма может привести к ошибке переполнения стека, так как ей
может потребоваться сделать O(n) вложенных рекурсивных вызовов. В улучшенных
реализациях, в которых рекурсивный вызов происходит только для сортировки меньшей из
двух частей массива, глубина рекурсии гарантированно не превысит
.
• Неустойчив — если требуется устойчивость, приходится расширять ключ.

( Устойчивая (стабильная) сортировка — сортировка, которая не меняет относительный


порядок сортируемых элементов, имеющих одинаковые ключи).

40
Алгоритм QuickSort. - Структуры и алгоритмы
компьютерной обработки данных.
Оглавление

Алгоритм QuickSort.
Метод сортировки разделением был предложен Чарльзом Хоаром в 1962 г. Этот метод является
развитием метода простого обмена и настолько эффективен, что его стали называть "методом
быстрой сортировки - Quicksort". Основная идея алгоритма состоит в том, что случайным образом
выбирается некоторый элемент массива x, после чего массив просматривается слева, пока не
встретится элемент a[i] такой, что a[i] > x, а затем массив просматривается справа, пока не
встретится элемент a[j] такой, что a[j] < x. Эти два элемента меняются местами, и процесс
просмотра, сравнения и обмена продолжается, пока мы не дойдем до элемента x. В результате
массив окажется разбитым на две части - левую, в которой значения ключей будут меньше x, и
правую со значениями ключей, большими x. Далее процесс рекурсивно продолжается для левой и
правой частей массива до тех пор, пока каждая часть не будет содержать в точности один
элемент.Понятно, что как обычно, рекурсию можно заменить итерациями, если запоминать
соответствующие индексы массива. Проследим этот процесс на примере нашего стандартного
массива (таблица 2.6). Таблица 2.6. Пример быстрой сортировки

1. Начальное состояние массyначальное состояние мамаммамассива 8 23 5 65 |44| 33 1 6

|--------|

8 23 5 6 44 33 1 65
Шаг 1 (в качестве x выбирается a[5])
|---|

8 23 5 6 1 33 44 65

8 23 |5| 6 1 33 44 65

|--------|

Шаг 2 (в подмассиве a[1], a[5] в качестве x выбирается a[3]) 1 23 5 6 8 33 44 65

|--|

1 5 23 6 8 33 44 65

1 5 23 |6| 8 33 44 65

Шаг 3 (в подмассиве a[3], a[5] в качестве x выбирается a[4]) |----|

1 5 8 6 23 33 44 65

1 5 8 |6| 23 33 44 65

Шаг 4 (в подмассиве a[3], a[4] выбирается a[4]) |--|

1 5 6 8 23 33 44 65
Алгоритм недаром называется быстрой сортировкой, поскольку для него оценкой числа сравнений
и обменов является O(nlog n). На самом деле, в большинстве утилит, выполняющих сортировку
массивов, используется именно этот алгоритм.

Улучшения
41
Hа практике для увеличения быстроты, но не асимптотики, можно произвести несколько
улучшений:

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


середине. Такой выбор улучшает оценку среднего времени работы, если массив упорядочен лишь
частично. Hаихудшая для этой реализации ситуация возникает в случае, когда каждый раз при
работе partition в качестве центрального выбирается максимальный или минимальный элемент.
1'. Можно выбрать средний из первого, последнего и среднего элементов. Maxim Razin: Тогда
количество проходов уменьшится в 7/6 раз.

2. Для коротких массивов вызывается сортировка вставками. Из-за рекурсии и других "накладных
расходов" быстрый поиск оказывается не столь уж быстрым для коротких массивов. Поэтому, если
в массиве меньше 12 элементов, вызывается сортировка вставками. Пороговое значение не
критично - оно сильно зависит от качества генерируемого кода.
3. Если последний оператор функции является вызовом этой функции, говорят о хвостовой
рекурсии. Ее имеет смысл заменять на итерации - в этом случае лучше используется стек.
4. После разбиения сначала сортируется меньший раздел. Это также приводит к лучшему
использованию стека, поскольку короткие разделы сортируются быстрее и им нужен более
короткий стек. Требования к памяти уменьшаются с n до log n

Оглавление

42
Алгоритм HeapSort. - Структуры и алгоритмы
компьютерной обработки данных.
Алгоритм HeapSort.
Имеется более совершенный алгоритм, который принято называть пирамидальной сортировкой
(Heapsort). Его идея состоит в том, что вместо полного дерева сравнения исходный массив a[1],
a[2], ..., a[n] преобразуется в пирамиду, обладающую тем свойством, что для каждого a[i]
выполняются условия a[i] <= a[2i] и a[i] <= a[2i+1]. Затем пирамида используется для
сортировки. Наиболее наглядно метод построения пирамиды выглядит при древовидном
представлении массива, показанном на рисунке 2.9. Массив представляется в виде двоичного
дерева, корень которого соответствует элементу массива a[1]. На втором ярусе находятся
элементы a[2] и a[3]. На третьем - a[4], a[5], a[6], a[7] и т.д. Как видно, для массива с нечетным
количеством элементов соответствующее дерево будет сбалансированным, а для массива с
четным количеством элементов n элемент a[n] будет единственным (самым левым) листом "почти"
сбалансированного дерева.

a1=8

a2=23 a3=5

a4=65 a5=44 a6=33 a7=1

a8=6
Pasul 0
Рис. 2.9.

Очевидно, что при построении пирамиды нас будут интересовать элементы a[n/2], a[n/2-1], ...,
a[1] для массивов с четным числом элементов и элементы a[(n-1)/2], a[(n-1)/2-1], ..., a[1] для
массивов с нечетным числом элементов (поскольку только для таких элементов существенны
ограничения пирамиды). Пусть i - наибольший индекс из числа индексов элементов, для которых
существенны ограничения пирамиды. Тогда берется элемент a[i] в построенном дереве и для него
выполняется процедура просеивания, состоящая в том, что выбирается ветвь дерева,
соответствующая min(a[2?i], a[2?i+1]), и значение a[i] меняется местами со значением
соответствующего элемента. Если этот элемент не является листом дерева, для него
выполняется аналогичная процедура и т.д. Такие действия выполняются последовательно для a[i],
a[i-1], ..., a[1]. Легко видеть, что в результате мы получим древовидное представление пирамиды
для исходного массива (последовательность шагов для используемого в наших примерах массива
показана на рисунках 2.10-2.13).

43
a1=8

a2=23 a3=5

a4=6 a5=44 a6=33 a7=1

a8=65
Pasul 1
Рис. 2.10.

a1=8

a2=23 a3=1

a4=65 a5=44 a6=33 a7=5

a8=6
Pasul 2
Рис. 2.11.

44
a1=8

a2=6 a3=1

a4=23 a5=44 a6=33 a7=5

a8=65
Рис. 2.12. Pasul 3

a1=1

a2=6 a3=8

a4=23 a5=44 a6=33 a7=5

a8=65
Pasul 4
Рис. 2.13.

В 1964 г. Флойд предложил метод построения пирамиды без явного построения дерева (хотя
метод основан на тех же идеях). Построение пирамиды методом Флойда для нашего стандартного
массива показано в таблице

Таблица 2.7 Пример построения пирамиды

45
Начальное состояние массива 8 23 5 |65| 44 33 1 6

Шаг 1 8 23 |5| 6 44 33 1 65

Шаг 2 8 |23| 1 6 44 33 5 65

Шаг 3 |8| 6 1 23 44 33 5 65

1 6 8 23 44 33 5 65
Шаг 4
1 6 5 23 44 33 8 65
В таблице 2.8 показано, как производится сортировка с использованием построенной пирамиды.
Суть алгоритма заключается в следующем. Пусть i - наибольший индекс массива, для которого
существенны условия пирамиды. Тогда начиная с a[1] до a[i] выполняются следующие действия.
На каждом шаге выбирается последний элемент пирамиды (в нашем случае первым будет выбран
элемент a[8]). Его значение меняется со значением a[1], после чего для a[1] выполняется
просеивание. При этом на каждом шаге число элементов в пирамиде уменьшается на 1 (после
первого шага в качестве элементов пирамиды рассматриваются a[1], a[2], ..., a[n-1]; после второго -
a[1], a[2], ..., a[n-2] и т.д., пока в пирамиде не останется один элемент). Легко видеть (это
иллюстрируется в таблице 2.8), что в результате мы получим массив, упорядоченный в порядке
убывания. Можно модифицировать метод построения пирамиды и сортировки, чтобы получить
упорядочение в порядке возрастания, если изменить условие пирамиды на a[i] >= a[2?i] и a[1] >=
a[2?i+1] для всех осмысленных значений индекса i.

Таблица 2.8 Сортировка с помощью пирамиды

Исходная пирамида 1 6 5 23 44 33 8 65

65 6 5 23 44 33 8 1

Шаг 1 5 6 65 23 44 33 8 1

5 6 8 23 44 33 65 1

65 6 8 23 44 33 5 1

Шаг 2 6 65 8 23 44 33 5 1

6 23 8 65 44 33 5 1

33 23 8 65 44 6 5 1
Шаг 3
8 23 33 65 44 6 5 1

44 23 33 65 8 6 5 1
Шаг 4
23 44 33 65 8 6 5 1

65 44 33 23 8 6 5 1
Шаг 5
33 44 65 23 8 6 5 1

65 44 33 23 8 6 5 1
Шаг 6
44 65 33 23 8 6 5 1

Шаг 7 65 44 33 23 8 6 5 1
Процедура сортировки с использованием пирамиды требует выполнения порядка nxlog n шагов
(логарифм - двоичный) в худшем случае, что делает ее особо привлекательной для сортировки
больших массивов.
46
Algoritmul QuickSort
Metoda de sortare a fost propusă de către Charles Hoare în 1962. Această metodă
este dezvoltarea unei metode simple de schimb şi atât de eficientă, încât a devenit
cunoscută sub numele de “metoda de sortare rapidă – QuickSort”. Ideea de bază a
algoritmului constă în faptul că selectează la întîmplare un element x al matricei,
după aceasta matricea se analizează din stînga, pînă cînd nu întîlneşte elementul
a[i] astfel încît a[i]>x , şi apoi matricea se studiază din partea dreaptă, pînă cînd nu
întîlneşte elementul a[j] astfel încît a[j]<x. Aceste 2 elemente sunt schimbate,
precum şi procesul de vizualizare, compararea şi schimbul continuă pînă cînd vom
ajunge la elementul x. În rezultat, matricea se divide în două părţi – stînga şi
dreapta, în partea stînga valorile cheie vor fi mai mici decât x, iar în partea dreaptă
valorile cheie vor fi mai mari decât x. În continuare procesul continuă recursiv
pentru partea stîngă şi dreaptă a matricei, pînă cînd fiecare parte nu va conţine
exact un element. Cunoaştem, ca de obicei , recursia poate fi înlocuită prin iteraţii,
dacă să păstrăm în memorie indicii corespunzători din matrice. Să urmăm acest
proces, ca de exemplu, în matricea noastră standard (tabel 2.6).Tabelul 2.6.
Exemplu QuickSort.

8 23 5 65 |44| 33 1
1. Starea initială a matricei
6
|--------|

8 23 5 6 44 33 1 65
Pasul 1 (în calitate de x se alege a[5])
|---|

8 23 5 6 1 33 44 65
8 23 |5| 6 1 33 44
65

Pasul 2 (în submatricea a[1], a[5] în calitate de x se alege |--------|


a[3])
1 23 5 6 8 33 44 65

|--|

47
1 5 23 6 8 33 44 65
1 5 23 |6| 8 33 44
65
Pasul 3 (în submatricea a[3], a[5] în calitate de x se alege
a[4]) |----|

1 5 8 6 23 33 44 65
1 5 8 |6| 23 33 44
65
Pasul 4 (în submatricea a[3], a[4] se alege a[4])
|--|

1 5 6 8 23 33 44 65

Algoritmul nu este numit fara motiv sortarea rapidă, întrucît pentru evaluarea lui
numărul de comparaţii şi schimburi este O(nlogn). De fapt, de cele mai multe ori,
tablourile de sortare utilizează anume acest algoritm.

Îmbunătăţiri
În practică pentru a creşte viteza, dar nu cea asimptotică, putem face cîteva
optimizări:

1. În cazul funcţiei de partiţie elementul central este selectat în mijloc. Această


alegere îmbunătăţeşte estimarea timpului mediu, în cazul în care matricea
este ordonată parţial. O metodă mai puţin eficientă pentru această situaţie
apare atunci cînd de fiecare dată se lucrează cu funcţia de partiţie şi în
calitate de element central se alege cel mai mare sau cel mai mic element.

1'. Putem selecta media din primele elemente, ultimele şi cele mijlocii. Maxim
Razin: În acest caz, numărul de treceri va scădea de 7/6 ori.

2. Pentru matrici mai scurte se numeşte sortare prin inserare. Din cauza
recursiei şi a altor „calcule adaugătoare” QuickSort nu este un algoritm
eficient pentru tablouri mici. De aceasta, dacă în masiv sunt mai puţin de 12
elemente, se numeşte sortare prin inserare. Valoarea pragului nu este critică
– aceasta este extrem de dependentă de calitatea codului generat.

48
3. Dacă ultimul operator al funcţiei este un apel la această funcţie, atunci se
numeşte coadă-recursivă. Aceasta are sens să fie înlocuită prin iteraţii – în
acest caz mai bine de utilizat stiva.

4. După rupere mai întîi se sortează prima secţiune. Aici, de asemenea, mai
bine de utilizat stiva, întrucît secţiunile mici se sortează mai rapid şi ele au
nevoie de o stivă mai mică. Cerinţele de memorie sunt reduse de la n la logn.

49
Arbori AVL
A elaborat: Andrie ș Patricia-Georgiana
                    Pîrlea Ana-Maria
Grupa: TI 191ro
Definirea
Arborelui Avl
v Se numeste înalțime a unui arbore ca fiind
celui mai lung drum de la nodul rădacină la
nodurile terminale.
v Se numește factor de echilibrare diferen
înalțimea subarborelui drept și înălțimea su
stâng.
v Atașând fiecărui nod un câmp care reprezint
de echilibrare al său, se spune ca arborele
echilibrat când toți factorii de echilibrare ai
sunt -1,0,+1.
Exemplu printr-
desen
Exemple de proceduri pentru calc
Înălțimii unui subarbore și a facto
echilibru 
ØÎnălțimea întregului arbore este 5. 
ØÎnălțimea subarborelui stâng al radă
ØPentru a afla factorul de echilibru al
scădem înălțimea subarborelui stâng 
subarborelui drept.
ØFactorul de echilibru al nodului cu e
ØFactorul de echilibru al nodului cu e
üPractic, arborii AVL  se comportă la fel ca arborii binari ordonați s
în cazul operațiilor de inserție și suprimare de chei .
üO  inserție  într-un  arbore  binar  ordonat  poate  duce  la  dezechili
noduri, dezechilibrare manifestată prin nerespectarea formulei | hs –
respectivele noduri.
üÎn  principiu,  o  cheie  se  inserează  într-o  primă  fază,  ca  și  într-
ordonat obișnuit, adică se pornește de la rădăcină și se urmează fi
drept,  în  funcție  de  relația  dintre  cheia  de  inserat  și  cheia  noduri
trece, până se ajunge la un fiu nul, unde se realizează inserția propri
üÎn  acest  moment  se  parcurge  drumul  invers  (care  este  unic)  și  s
drum primul nod care nu este echilibrat, adică primul nod ai cărui 
ca înălțime prin 2 unități.
üAcest nod trebuie echilibrat și el se va afla întotdeauna într-unul di
•Cazul 1-rotație simplă dreaptă
• Cazul 2-rotație simplă stangă – este simetric în o
de cazul 1-rotatie simplă dreaptă
• Cazul 3-
rotație dublă
dreaptă
•Cazul 4-rotație dublă 
stângă – simetric în 
oglindă față de cazul 
3-rotație dublă 
dreaptă 
• Dându-se un arbore cu radacina R si având subarborii stâng si drept not
de inaltimi hs si hd, la insertia unui nod în S se disting trei cazuri:
• 1. hs=hd - în urma insertiei, S si D devin de înaltimi inegale, verificând în
echilibrului;
• 2. hs<hd - în urma insertiei S si D devin de înaltimi egale, deci echilibrul 
îmbunatateste;
• 3. hs>hd - criteriul echilibrului nu se mai verifica, arborele trebuie reech
• Cazurile posibile care pot duce la necesitatea reechilibrarii se pot sintetiz
urmatoarele, tinand cont de subarborele in care se face insertia (subarbo
drept) si de locul in cadrul subarborelui.
Reechilibrarea la inserția
arbori AVL - Caz 1 stâng
• Un algoritm pentru inserție si reechilibrare depinde de maniera în care e mem
referitoare la situația echilibrului arborelui.
• O soluție e cea în care aceasta informație nu e inclusă în structura arborelui, a
de echilibrare al unui nod afectat de inserție, trebuind să fie determinat de fie
la mărirea regiei.
• Cu soluția a doua, inserția unui nod presupune etapele:
• 1. parcurgerea arborelui binar pentru determinarea locului de inser ție sau a e
cheii de inserat;
• 2. inserția noului nod și determinarea factorului de echilibrare corespunzator
• 3. revenirea pe drumul de căutare  și verificare a factorului de echilibru pentr
presupune unele verificări redundante: dacă echilibrul e stabilit, nu ar mai fi 
factorului de echilibru pentru strămoșii nodului.
• Operatia de reechilibrare constă dintr-o secvență de reasignari de pointeri ( p
schimbați ciclic, rezultând fie o simplă, fie o dublă rotație a două sau trei nod
reajustări ale factorilor de echilibru.


• Cel mai simplu se pot șterge nodurile terminale și cele care au
fiu. Daca nodul care trebuie să fie șters are doi fii, îl înlocuim 
aflat în extremă dreapta a subarborelui său stâng. Echilibrare
necesară numai dacă în urma ștergerii s-a redus înalțimea sub
cazul cel mai defavorabil ștergerea unui nod se realizează in O
operații. 
Exemplu: Se consideră arborele din imagine. Î
ștergerii nodului cu cheia 6 apare necesitatea ech
Exem
plu:
• Se va considera secvența de chei 4,5,7,2,1,3,6 care se inse
arbore AVL ini țial vid. Evoluția arborelui și echilibrările sun
Алгоритм Кнута-Морриса-Пратта

Au elaborat:
Sîrbu Eudochia CIB-112
Calmic Anna CIB-113
Razmeriţa Eugeniu CIB-112
Verificat de:
Tutunaru Serghei, conferenţiar
universitar, doctor.
Алгоритм Кнута-Морриса-
Пратта
§ Алгоритм Кнута-Морриса-Пратта - алгоритм
поиска образца P[0..m] (подстроки) в исходной
строке-тексте T[0..n], где n >= m.

T[0..n]
P[0..m]

§ Построен при помощи префикс-функции π(P).


Алгоритм является классическим примером
динамического программирования, эффективность
которогшо не O(N*M), а O(N+M).
Алгоритм Кнута-Морриса-
Пратта
§В отличие от линейных методов, алгоритм КМП
предлагает, чтобы продвижение образца P вдоль
источника T выполнялось на большее количество
позиций и в то же время запоминало текстовую
часть, которая совпадает с образцом.
§Это позволяет нам избежать ненужных
сравнений и тем самым значительно увеличить
скорость поиска.
Алгоритм Кнута-Морриса-
Пратта
§Префикс-функция π(P) – определяет
максимальный префикс P, который, в
действительности, является и суффиксом P.
prefix sufix

P
Пример:
Π(aba) = a
Π(abracadabra) = abra
Π(ab) = ξ (пустая строка)
Алгоритм Кнута-Морриса-
Пратта
Как и для чего применяется эта функция
?
ØДанная функция применяется для того,
чтобы помочь нам построить k-вектор длин
этих префиксов, после чего мы сможем
определить, содержится ли образец в
источнике и / или с каким количеством
позиций мы должны переместить образец
вправо, чтобы искать новое совпадение.
Алгоритм Кнута-Морриса-
Пратта

l– максимальная длина префикса P, который также


является суффиксом T[0..i]
I. max l΄: P[0.. l΄] – префикс и суффикс P[0..i+1]
II. P[0.. l΄-1] – префикс и суффикс P[0..l]
P[l΄] = T[i+1]
Алгоритм Кнута-Морриса-
Пратта
У нас есть два равенства, которые эквивалентны.

Несоответствие между a(из T) и b(из P) приведет к


перемещению образца вправо на расстояние
равное длине(P[0..i])-k[i].

Формируем вектор k, просматривая строку P слева


направо элемент за элементом, где k[i]
принимает разные значения, которые мы
объясним в следующем примере.
Алгоритм Кнута-Морриса-
Пратта
Изучаем ход алгоритма, используя следующую задачу:

Исходная m a m m a m m c a b m m m c a b m m c
строка:

Образец: m m c a b m m c

53
Алгоритм Кнута-Морриса-
Пратта
Получаем вектор K для нашего искомого слова-
образца:-1 -1 1 0 0 -1 -1 2

K[i]=-1, когда:
- Первый элемент всегда равен -1 ;
- p[i]=p[0].
K[i]=0, когда:
- p[0] не равно p[i] & π (p[0..i-1])=ξ(пустая строка), если
π (p[0..i-1]) существует
K[i]=длина(π (p[0..i-1]))
Алгоритм Кнута-Морриса-
Пратта
Изучаем ход алгоритма, используя следующую
ситуацию:
Исходная m a m m a m m c a b m m m c a b m m c
строка:

Образец: m m c a b m m c

Первое несоответствие появляется между символами ‘a’


и ‘m’
Мы получаем k = -1
Алгоритм требует, чтобы образец перемещался вправо
по длине (l) - k = 2

55
Алгоритм Кнута-Морриса-
Пратта
Исходная m a m m a m m c a b m m m c a b m m c
строка:

Образец: m m c a b m m c

 Первое несоответствие появляется между символами ‘a’ и ‘c’”


 a= ‘а’ и b = ‘c’ - удовлетворяет, a != b
 l=2, k=1
 Алгоритм требует правого сдвига образца с длиной ( l) – k = 1

56
Алгоритм Кнута-Морриса-
Пратта
Исходная m a m m a m m c a b m m m c a b m m c
строка:

Образец: m m c a b m m c

 Первое несоответствие появляется между символами ‘a’ и ‘m’


 Строка l = “m”
 a = ‘a’ и b = ‘m’ ( a != b)
 Поскольку a == b, и у нас нет другого варианта для l, мы получаем k = -
1
 Алгоритм требует, чтобы образец перемещался вправо по длине (l) - k
=2
 Мы можем заметить, что всякий раз, когда несоответствие происходит
на втором символе образца, оно должно быть перемещено на 2
символа вправо, независимо от исходной строки
 Эти смещения могут быть вычислены предварительно и сведены в
таблицу для каждого возможного положения несоответствия, так что
строка l не должна вычисляться каждый раз, но смещение может быть
удалено непосредственно из таблицы 57
Алгоритм Кнута-Морриса-
Пратта
Исходная m a m m a m m c a b m m m c a b m m c
строка:

Образец m m c a b m m c
:

Первое несоответствие появляется между символами ‘m’ и


‘c’
Строка l = “mmcabmm”
Если мы выберем строку l = "mm", то a = 'c' и b = 'c' (a
должно быть отличным от b)
Выберем следующий вариант для l, а именно l = "m“
a = ‘m’ и b = ‘c’ – удовлетворяет, a != b
k = длина(l΄ ) = 1
Алгоритм требует, чтобы образец был перемещен вправо
по длине (l) - k = 5
58
Алгоритм Кнута-Морриса-
Пратта
Исходная m a m m a m m c a b m m m c a b m m c
строка:

Образец: m m c a b m m c

 Первое несоответствие появляется между символами ‘m’ и ‘c’


 Строка l = “mm”
 Алгоритм требует, чтобы образец был перемещен вправо по
длине (l) - k = 1
Алгоритм Кнута-Морриса-
Пратта
Исходная m a m m a m m c a b m m m c a b m m c
строка:

Образец: m m c a b m m c

 Было найдено совпадение между исходной строкой и образцом


 На этом этапе алгоритм может завершить свою работу или
продолжить поиск других совпадений
 Если алгоритм продолжит свою работу, можно предположить,
что l равно всему образцу, l΄ вычисляет и двигает образец
вправо по длине (l) - length (l΄) (потому что у нас нет символа b)
 В представленном случае продолжение не приведет к поиску
других совпадений, потому что мы достигли конца исходной
строки.

60
Алгоритм Кнута-Морриса-
Пратта
În continuare am prezentat codul programului efectuat in C++ :
Алгоритм Кнута-Морриса-
Пратта
Алгоритм Кнута-Морриса-
Пратта
Algorimul Knuth-Morris-Pratt

Au elaborat:
Sîrbu Eudochia CIB-112
Calmic Anna CIB-113
Razmeriţa Eugeniu CIB-112
Verificat de:
Tutunaru Serghei, conferenţiar
universitar, doctor.
Algoritmul Knuth-Morris-Pratt
§ Algoritmul Knuth-Morris-Pratt reprezinta o metoda
avansata de cautare a unui sir de caractere –
model(pattern) P[0..m] intr-un sir de caractere sursa
T[0..n], evident n>=m.

T[0..n]
P[0..m]

§Construit pe functia-prefix π(P), algoritmul este un


exemplu clasic de programare dinamica, cu eficienta nu de
O(N*M), ci de O(N+M).
Algoritmul Knuth-Morris-Pratt
§Spre deosebire de metodele liniare, algoritmul KMP
propune ca deplasarea modelului P de-a lungul
sursei T sa fie efectuata cu un numar mai mari de
pozitii si ca in acelasi timp sa se memoreze partea
de text care coincide cu modelul.

§Acest lucru ne va permite sa evitam comparatiile


inutile si, astfel, va creste considerabil viteza de
cautare.
Algoritmul Knuth-Morris-Pratt
§Functia-prefix π(P) – determina prefixul maxim a
lui P, care este, defapt, si sufixului lui P.
prefix sufix

P
Exemplu:
Π(aba) = a
Π(abracadabra) = abra
Π(ab) = ξ (linie goala)
Algoritmul Knuth-Morris-Pratt
Cum se aplica aceasta functie si pentru
ce?
ØSe aplica pentru a ne ajuta sa construim o
matrice k a lungimilor acestor prefixe, dupa
care vom putea determina daca modelul se
contine in sursa sau/si cu cite pozitii trebuie
sa deplasam modelul spre dreapta pentru a
cauta o noua potrivire.
Algoritmul Knuth-Morris-Pratt

l– lungimea maxima a prefixului lui P si care este de


asemenea sufixul lui T [0..i]
I. max l΄: P[0.. l΄] – prefixul si sufixul lui P[0..i+1]
II. P[0.. l΄-1] – prefixul si sufixul lui P[0..l]
P[l΄] = T[i+1]
Algoritmul Knuth-Morris-Pratt
Avem aceste doua egalitati care putem spune ca
sunt
echivalente.
Nepotrivirea intre a(din T) si b(din P) va cauza o
deplasare a modelului spre dreapta cu o cantitate
egala cu lungimea(P[0..i])-k[i].
Formam matricea k cercetind textul de la stinga la
dreapta element cu element, unde k[i] – i-a valori
diferite, pe care le vom explica in exemplul
urmator.
Algoritmul Knuth-Morris-
Pratt
Vom studia mersul algoritmului pentru urmatoarea situatie:

Sursa: m a m m a m m c a b m m m c a b m m c

Model: m m c a b m m c

71
Algoritmul Knuth-Morris-Pratt
Avem obtinut pentru pattern-ul nostru tabloul K:
-1 -1 1 0 0 -1 -1 2
K[i]=-1, atunci cind:
- Primul element intotdeauna este egal cu -1;
- p[i]=p[0].
K[i]=0, atunci cind:
- p[0] nu este egal cu p[i] & π (p[0..i-1])=ξ(linie goala),
daca π (p[0..i-1]) exista
K[i]=lungimea(π (p[0..i-1]))
Algoritmul Knuth-Morris-
Pratt
Vom studia mersul algoritmului pentru urmatoarea situatie:

Sursa: m a m m a m m c a b m m m c a b m m c

Model: m m c a b m m c

Prima nepotrivire apare intre caracterele ‘a’ si ‘m’


Sirul l = “m”
Sirul l΄ = “” (sirul l΄ nu poate fi egal cu l)
a = ‘m’ si b = ‘m’ (a == b)
Cum a == b si nu avem alta varianta pentru l΄, rezulta k = -1
Algoritmul cere o deplasare a modelului spre dreapta cu
lungime(l) – k = 2
73
Algoritmul Knuth-Morris-Pratt
Sursa: m a m m a m m c a b m m m c a b m m c

Model: m m c a b m m c

 Prima nepotrivire apare intre caracterele ‘a’ si ‘c’


 Sirul l = “mm”
 Sirul l΄ = “m”
 a= ‘m’ si b = ‘c’ - satisfacator, a != b
 k = lungime(l΄ ) = 1
 Algoritmul cere o deplasare spre dreapta a modelului cu lungime(l) –
k=1

74
Algoritmul Knuth-Morris-Pratt
Sursa: m a m m a m m c a b m m m c a b m m c

Model: m m c a b m m c

 Prima nepotrivire apare intre caracterele ‘a’ si ‘m’


 Sirul l = “m”
 Sirul l΄ = “”
 a = ‘m’ si b = ‘m’ ( a == b)
 Cum a == b si nu avem alta varianta pentru l΄ , rezulta k = -1
 Algoritmul cere o deplasare a modelului spre dreapta cu lungime(l) – k =
2
 Anticipand, putem observa ca de fiecare data cand nepotrivirea apare la
caracterul al doilea al modelului, acesta trebuie deplasat cu 2 caractere
spre dreapta, independent de sursa
 Aceste deplasari pot fi calculate a priori si tabelate pentru fiecare posibila
pozitie a nepotrivirii, astfel incat sirul l΄ sa nu mai trebuiasca calculat de
fiecare data, ci deplasarea sa poata fi scoasa direct din tabel 75
Algoritmul Knuth-Morris-Pratt
Sursa: m a m m a m m c a b m m m c a b m m c

Model: m m c a b m m c

Prima nepotrivire apare intre caracterele ‘m’ si ‘c’


Sirul l = “mmcabmm”
Daca alegem sirul l΄ = “mm”, ar rezulta a = ‘c’ si b = ‘c’ (a
trebuie sa fie diferit de b)
Alegem urmatoarea varianta pentru l΄ , anume l΄ = “m”
a = ‘m’ si b = ‘c’ – satisfacator, a != b
k = lungime(l΄ ) = 1
Algoritmul cere o deplasare a modelului spre dreapta cu
lungime(l) – k = 5

76
Algoritmul Knuth-Morris-Pratt
 Sursa: m a m m a m m c a b m m m c a b m m c

Model: m m c a b m m c

 Prima nepotrivire apare intre caracterele ‘m’ si ‘c’


 Sirul l = “mm”
 Daca alegem sirul l΄ = “m”, ar rezulta a = ‘m’ si b = ‘c’ ( a trebuie sa
fie diferit de b)
 Alegem urmatoarea varianta pentru l΄ , anume l΄ = “m”
 a = ‘m’ si b = ‘c’ – satisfacator, a != b
 k = lungime(l΄ ) = 1
 Algoritmul cere o deplasare a modelului spre dreapta cu lungime(l) –
k= 1
Algoritmul Knuth-Morris-Pratt
Sursa: m a m m a m m c a b m m m c a b m m c

Model: m m c a b m m c

S-a gasit o potrivire intre sursa si model


In acest moment, algoritmul se poate incheia, sau poate
continua pentru gasirea altor potriviri
Daca se decide continuarea, se poate considera ca l este egal
cu intreg modelul, l΄ se calculeaza si se deplaseaza modelul
spre dreapta cu lungime(l) – lungime(l΄ ) (deoarece nu
dispunem de caracterul b)
In cazul prezentat, continuarea nu va duce la gasirea altor
potriviri, deoarece am ajuns la sfarsitul sirului sursa.

78
Algoritmul Knuth-Morris-
Pratt
În continuare am prezentat codul programului efectuat in C++ :
Algoritmul Knuth-Morris-Pratt
Algoritmul Knuth-Morris-Pratt
PROBLEMA 3 (OPȚIONAL)

Sa se găsească toate permutările


mulțimii formate din n elemente ( numărul
n se introduce de la tastatură)
A={a1, a2 ,a3,..., an} de tip INTEGER,
elementele căreia se introduc de la
tastatură. De afișat toate permutările
posibile.
Pentru a calcula N factorial am
folosit functia data:
 Aceasta functie este
recursiva si are ca
parametru local “r” de
tip integer si ia
returneaza “r”
factorial.

 Ex: a=factorr(4)=1*2*3*4=24
 b=factorr(n)=1*2*3*…*n-2*n-1*n
Primul pas al programului este sa introducem “n” de
la tastatură și elementele vectorului a[n].

- Randul 11 cere sa Ex. cum arata la executare:


introducem lungimea
vectorului, adica N.
- Randul 12 va afișa pe ecran
“Introduceti elementele
vectorului A: “.
Am introdus valoarea lui
- Randurile 13-14. Cu ajutorul
ciclului for de la 1 pina la N
n=4 si mi-a cerut sa
am introdus valorile vectorului introduc valori pentru
A de la tastiera. vectorul A de lungimea 4.
Pentru rezolvarea problemei e nevoi de un vector,
elementele lui fiind “i” factorial.

- Am creat vectorul B de la 1  Ex:


pina la N cu elementele  B[1]=1;
factorial ce au fost calculate  B[2]=2;
cu ajutorul functiei “factorr”.
 B[3]=6;
- Ultimul element al vectorului
B reprezinta numarul total de  B[4]=24;
permutari, cei atribuim lui R.  …
 B[n]=n!
Blocul de realizare a permutarilor:
- R.19 – Pentru a
reprezenta toate
permutarile am creat un
ciclu de la 1 pina la R.
- Vectorul initial este deja
una din cele R permutari,
de aceea am inceput cu
afisarea vectorului.

- Apoi apelam la vectorul B, pentru determinarea


elementelor ce se vor schimba intre ele.
- Pentru aceasta e necesar ca K(numarul permutarii) sa se
imparte la fiecare element din vectorul B fara rest, pozitia
ultimului element al vectorului B ce sa impartit la K fara
rest ai atribuim lui M (adica cuprinsa intre 1 si N).
- Este un singur caz
cind are loc schimbarea
intre 2 elemente ce nu
sunt vecine si acesta
este atunci cind K
impartit la 24 da restul
18.

- Dupa determinarea lui M, verificam daca K impartit la


24 da rest 18 si daca da, atunci se schimb cu locul
elementele: A[N] si A[N-3], iar daca nu, atunci se
schimba cu locul elementele: A[N-M+1] si A[N-M].
Exemplu pentru N=4.
Vectorul B={1,2,6,24,120}
K=1 => k%b[1]==0? Da => m=1;
k%b[2]==0? Nu, nu se executa nimic.
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Nu, nu se executa nimic.
k%24==18? Nu, se trece la intructiunea else.
K=1=> 0 1 2 4 3
K=2 => k%b[1]==0? Da => m=1;
k%b[2]==0? Da => m=2;
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Nu, nu se executa nimic.
k%24==18? Nu, se trece la intructiunea else.
K=2=>0 1 4 2 3
K=20 => k%b[1]==0? Da => m=1;
k%b[2]==0? Da => m=2;
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Nu, nu se executa nimic.
k%24==18? Nu, se trece la intructiunea else.
K=20=> 0 4 2 1 3
K=48 => k%b[1]==0? Da => m=1;
k%b[2]==0? Da => m=2;
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Da => m=4;
k%24==18? Nu, se trece la intructiunea else.
K=48=> 2 4 1 0 3
- Astfel de operatii se executa pentru fiecare K, K de
la 1 pina la R, R-nr. total de permutari.
Pentru N=5
Vectorul B[5]={1,2,6,24,120}
Vectorul “A” are “N” elemente, rezulta N pozitii:
A[5]={1,2,3, 4 , 5 }
B[5]={1,2,6,24,120}
Observatie: Vectorul B este factorial pozitiilor
elementelor vectorului A.
Pentru N=3, A[3]=3, B[3]=6.
Pentru N=4, A[4]=4, B[4]=24.
Pentru N=5, A[5]=5, B[5]=120.
Observatie: Pozitiile elementelor vectorului A
arata de cate ori se va schimba elementul de pe
aceasta pozitie, vectorul B arata la a cata
permutare va avea loc aceasta schimbare.
A[5]={1,2,3, 4 , 5 , … , n-2, n-1, n}
B[5]={1,2,6,24,120, … ,(n-2)!, (n-1)!, n!}
 Din observatia recenta putem deduce ca: Pozitia
elementelor vectorului A[N] ne arata de cate ori se
repeta precedentele permutari (B[N-1]).
 Rezolvarea pentru caz general: Pentru n=n rezulta
n! permutari, in acest caz se repeta de n ori
precedentele permutari “n-1”, dar si cu ajutorul
elementului n. Apelul la n se executa atunci cind
numarul permutarii(k) se imparte fara rest la
precedentul factorial(n-1)!.
 Algoritmul de rezolvare este ca o functie recursiva.
Razmerita Eugeniu
CIB-112
PROBLEMA 3 (OPȚIONAL)

Sa se găsească toate permutările


mulțimii formate din n elemente ( numărul
n se introduce de la tastatură)
A={a1, a2 ,a3,..., an} de tip INTEGER,
elementele căreia se introduc de la
tastatură. De afișat toate permutările
posibile.
Pentru a calcula N factorial am
folosit functia data:
 Aceasta functie este
recursiva si are ca
parametru local “r” de
tip integer si ia
returneaza “r”
factorial.

 Ex: a=factorr(4)=1*2*3*4=24
 b=factorr(n)=1*2*3*…*n-2*n-1*n
Primul pas al programului este sa introducem “n” de
la tastatură și elementele vectorului a[n].

- Randul 11 cere sa Ex. cum arata la executare:


introducem lungimea
vectorului, adica N.
- Randul 12 va afișa pe ecran
“Introduceti elementele
vectorului A: “.
Am introdus valoarea lui
- Randurile 13-14. Cu ajutorul
ciclului for de la 1 pina la N
n=4 si mi-a cerut sa
am introdus valorile vectorului introduc valori pentru
A de la tastiera. vectorul A de lungimea 4.
Pentru rezolvarea problemei e nevoi de un vector,
elementele lui fiind “i” factorial.

- Am creat vectorul B de la 1  Ex:


pina la N cu elementele  B[1]=1;
factorial ce au fost calculate  B[2]=2;
cu ajutorul functiei “factorr”.
 B[3]=6;
- Ultimul element al vectorului
B reprezinta numarul total de  B[4]=24;
permutari, cei atribuim lui R.  …
 B[n]=n!
Blocul de realizare a permutarilor:
- R.19 – Pentru a
reprezenta toate
permutarile am creat un
ciclu de la 1 pina la R.
- Vectorul initial este deja
una din cele R permutari,
de aceea am inceput cu
afisarea vectorului.

- Apoi apelam la vectorul B, pentru determinarea


elementelor ce se vor schimba intre ele.
- Pentru aceasta e necesar ca K(numarul permutarii) sa se
imparte la fiecare element din vectorul B fara rest, pozitia
ultimului element al vectorului B ce sa impartit la K fara
rest ai atribuim lui M (adica cuprinsa intre 1 si N).
- Este un singur caz
cind are loc schimbarea
intre 2 elemente ce nu
sunt vecine si acesta
este atunci cind K
impartit la 24 da restul
18.

- Dupa determinarea lui M, verificam daca K impartit la


24 da rest 18 si daca da, atunci se schimb cu locul
elementele: A[N] si A[N-3], iar daca nu, atunci se
schimba cu locul elementele: A[N-M+1] si A[N-M].
Exemplu pentru N=4.
Vectorul B={1,2,6,24,120}
K=1 => k%b[1]==0? Da => m=1;
k%b[2]==0? Nu, nu se executa nimic.
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Nu, nu se executa nimic.
k%24==18? Nu, se trece la intructiunea else.
K=1=> 0 1 2 4 3
K=2 => k%b[1]==0? Da => m=1;
k%b[2]==0? Da => m=2;
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Nu, nu se executa nimic.
k%24==18? Nu, se trece la intructiunea else.
K=2=>0 1 4 2 3
K=20 => k%b[1]==0? Da => m=1;
k%b[2]==0? Da => m=2;
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Nu, nu se executa nimic.
k%24==18? Nu, se trece la intructiunea else.
K=20=> 0 4 2 1 3
K=48 => k%b[1]==0? Da => m=1;
k%b[2]==0? Da => m=2;
k%b[3]==0? Nu, nu se executa nimic.
k%b[4]==0? Da => m=4;
k%24==18? Nu, se trece la intructiunea else.
K=48=> 2 4 1 0 3
- Astfel de operatii se executa pentru fiecare K, K de
la 1 pina la R, R-nr. total de permutari.
Pentru N=5
Vectorul B[5]={1,2,6,24,120}
Vectorul “A” are “N” elemente, rezulta N pozitii:
A[5]={1,2,3, 4 , 5 }
B[5]={1,2,6,24,120}
Observatie: Vectorul B este factorial pozitiilor
elementelor vectorului A.
Pentru N=3, A[3]=3, B[3]=6.
Pentru N=4, A[4]=4, B[4]=24.
Pentru N=5, A[5]=5, B[5]=120.
Observatie: Pozitiile elementelor vectorului A
arata de cate ori se va schimba elementul de pe
aceasta pozitie, vectorul B arata la a cata
permutare va avea loc aceasta schimbare.
A[5]={1,2,3, 4 , 5 , … , n-2, n-1, n}
B[5]={1,2,6,24,120, … ,(n-2)!, (n-1)!, n!}
 Din observatia recenta putem deduce ca: Pozitia
elementelor vectorului A[N] ne arata de cate ori se
repeta precedentele permutari (B[N-1]).
 Rezolvarea pentru caz general: Pentru n=n rezulta
n! permutari, in acest caz se repeta de n ori
precedentele permutari “n-1”, dar si cu ajutorul
elementului n. Apelul la n se executa atunci cind
numarul permutarii(k) se imparte fara rest la
precedentul factorial(n-1)!.
 Algoritmul de rezolvare este ca o functie recursiva.
Razmerita Eugeniu
CIB-112
Algoritmile Recursive
Avantajele si dezavantajele

Executat de Nastas Vasile
Grupa TI-121
2 Octombrie 2013
Ce este recursivitatea ?
 Recursivitatea functiei este un mod de a defini unele 
funcții,care folosește o referire la ea însăși
 Pe parcursul său se verifică o condiţie a cărei 
nesatisfacere implică reluarea execuţiei modulului de 
la începutul său;
 Ea e de 2 tipuri : liniara (cind apeleaza la ea insusi) 
si arbore
Unde se aplica algoritme recursive
N! (Factorial)
Numerele Fibonacci
Problema Turnurilor Hanoi
Coeficienti Binominali
Sortarea
Algoritmul lui Euclid(cel mai mare divizor 
comun)
Algoritmul Dijkstra(permutari)
Etc.
Numerele  lui Fibonacci
În șirul de numere al lui 
Fibonacci, fiecare număr 
reprezintă suma a două 
numere anterioare, 
începând cu 0 și 1. Astfel, 
șirul incepe cu 1, 1, 2, 3, 
5, 8, 13, 21, 34, 55, 89, 
144, 233, 377, 610 etc
Numerele lui Fibonacci
Iterativ Recursiv

int fib (int N)  int fib (int N)
{ int k1, k2, k3;   { if ((N == 1) || (N == 2)) 
k1 = k2 = k3 = 1;  return 1;
for (int j = 3; j <= N; j++)  else return (fib (N-1) + fib 
 { k3 = k1 + k2;  (N-2)); }
k1 = k2; k2 = k3; }
 return k3; }
Timpul de executie a 
Nr. Lui Fibonacci (timpul milisec)
N 5 25 125 625

Recursiv 0 16 136 786

Iterativ 0 2 67 344

800

700

Caracteristicile PC : 600
Win7 32 bit 500
Intel Core Duo 3.00 GHz 400
Recursiv

RAM 4.00 GB 300
Iterativ

Compiler:
200
Microsoft Visual Studio 2012
100

5 25 125 625
Schema cum compileaza programul
Numarul lui Fibonacci (Recursiv) 

fib (4) se calculează de două ori, și fib (3) se calculează de 3 ori


Codul Recursiv Integral
#include <iostream>
using namespace std;
int fib (int N)
 { if ((N == 1) || (N == 2)) return 1;
 else return (fib (N-1) + fib (N-2)); }
int main() {
        int n;
        cout << "Introduceti numarul: ";
        cin >> n;
        cout << endl << "F(" << n << ") = " << fib(n) << endl << endl;
        return 0;
}
Codul Iterativ Integral
#include <iostream>
using namespace std;
int fib (int N) 
{ int k1, k2, k3; 
k1 = k2 = k3 = 1; 
for (int j = 3; j <= N; j++)
 { k3 = k1 + k2; 
k1 = k2; k2 = k3; }
 return k3; }
int main() {
        int n;
        cout << "Introduceti numarul: ";
        cin >> n;
        cout << endl << "F(" << n << ") = " << fib(n) << endl << endl;
        return 0;
}
Turnurile Hanoi
Turnul din Hanoi sau Turnurile din Hanoi 
este un joc matematic sau puzzle. Este format 
din trei tije și un număr variabil de discuri, de 
diferite mărimi, care pot fi poziționate pe oricare 
din cele 3 tije. Jocul începe având discurile 
așezate în stivă pe prima tijă, în ordinea mărimii 
lor, astfel încât să formeze un turn. Scopul 
jocului este acela de a muta întreaga stivă de pe 
o tijă pe alta, respectând următoarele reguli:
 Doar un singur disc poate fi mutat, la un 
moment dat.
 Fiecare mutare constă în luarea celui mai de 
sus disc de pe o tija și glisarea lui pe o altă 
tijă, chiar și deasupra altor discuri care sunt 
deja prezente pe acea tijă.
 Un disc mai mare nu poate fi poziționat 
deasupra unui disc mai mic.
Turnurile Hanoi
Iterativ Recursiv
void move( int numNestedCalls, int n, char*s, char*i, char*d ) void move(int n,char x,char z,char y)
    {    static int stepNumber = 0; {
    if (n > 0)    { if(n==1) cout<<"Move disk 1 from "<<x<<" to "<<z<<endl;
    for (int j = 0; j < 3 * numNestedCalls; j++) else
    cout << " "; {
    stepNumber += 1; move(n-1,x,y,z);
    cout << "Move " << n << " disks from " << s << " to " << d << " (depth = " << cout<<"Move disk "<<n<<" from "<<x<<" to"<<z<<endl;
    numNestedCalls << ", step " << stepNumber << ")\n";} move(n-1,y,z,x);
    if( n > 0 )    { }
    move( numNestedCalls + 1, n-1,s,d,i ); }
stepNumber += 1;
    for (int j = 0; j < 3 * numNestedCalls + 3; j++)
    cout << " ";
    cout << "disk " << n << " is moved from " << s << " to " << d << " (depth = " <<
    numNestedCalls << ", step " << stepNumber << ")\n";
move( numNestedCalls + 1, n-1,i,s,d );}
    if (n > 0)    {
    stepNumber += 1;
    for (int j = 0; j < 3 * numNestedCalls; j++)
    cout << " ";
    cout << "End Move " << n << " disks from " << s << " to " << d << " (depth = " 
<<  numNestedCalls << ", step " << stepNumber << ")\n";    }    }
Timpul de executie la
 Towers of Hanoi  (timpul milisec)
N 3 5 10 15

Recursiv 67 250 8718 329984

Iterativ 16 31 1032 29703

350000

Caracteristicile PC : 300000
Win7 32 bit
250000
Intel Core Duo 3.00 GHz
RAM 4.00 GB 200000 Recursiv

Compiler: 150000 Iterativ

Microsoft Visual Studio 2012 100000

50000

3 5 10 15
Codul iterativ integral
 #include<iostream>
    using namespace std;
    void move( int numNestedCalls, int n, char*s, char*i, char*d )    {
    static int stepNumber = 0;
    if (n > 0)    {
    for (int j = 0; j < 3 * numNestedCalls; j++)
    cout << " ";
    stepNumber += 1;
    cout << "Move " << n << " disks from " << s << " to " << d << " (depth = " <<
    numNestedCalls << ", step " << stepNumber << ")\n";    }
    if( n > 0 )    {
    move( numNestedCalls + 1, n-1,s,d,i );stepNumber += 1;
    for (int j = 0; j < 3 * numNestedCalls + 3; j++)
    cout << " ";
    cout << "disk " << n << " is moved from " << s << " to " << d << " (depth = " <<
    numNestedCalls << ", step " << stepNumber << ")\n";move( numNestedCalls + 1, n-1,i,s,d );}
    if (n > 0)    {
    stepNumber += 1;
    for (int j = 0; j < 3 * numNestedCalls; j++)
    cout << " ";
    cout << "End Move " << n << " disks from " << s << " to " << d << " (depth = " <<
    numNestedCalls << ", step " << stepNumber << ")\n";    }    }
    int main()    {
    cout << "\n************************************************ **********\n";
    cout << "This C++ program is to solve the towers of hanoi problem";
    cout << "\n************************************************ **********\n";
    cout << "Enter the no. of disks ";
    int n;
    cin >> n;
    move( 1, n, "1", "2", "3" );
return 0;    }
Codul recursiv integral
 #include <iostream>
using namespace std;
void move(int n,char x,char z,char y)
{
    if(n==1) cout<<"Move disk 1 from "<<x<<" to "<<z<<endl;
    else
    {
        move(n-1,x,y,z);
        cout<<"Move disk "<<n<<" from "<<x<<" to"<<z<<endl;
        move(n-1,y,z,x);
    }
}
int main()
{
    int n;
    cout<<"Enter the number of disks inTOH:";
    cin>>n;
    move(n,'A','C','B');
    system("pause");
    return 0;
}
Avantajele
 Micsoreaza dimensiunea codului
 Se reduce complexitatea,devine mai usor de inteles 
 Se implementeaza mai usor in mai multe cazuri de tip 
real(ex. un program pentru a afișa o listă cu toate 
fișierele de sistem nu poate fi rezolvată fără 
recursivitate)
 Recursivitatea este foarte flexibil în structura de date 
cum ar fi stive, cozi, lista legat si sortare rapida.
 Evitarea de asteptare inutile de funcții
Dezavantajele
 Este nevoie de spațiu de stocare suplimentar
 Algoritmul recursiv nu este eficient in viteza de 
executie si de timp
 Soluția recursivă este întotdeauna logică și este foarte 
dificil de a urmări. (debug și înțelege)
 Trebuie de evitat valorile statice si globale
Surse de informare
 http://my.safaribooksonline.com/book/programming/c/
9788131760314/functions/section_10.20
 http://pages.cs.wisc.edu/~vernon/cs367/notes/6.RECU
RSION.html#iter
 http://benpfaff.org/writings/clc/recursion-vs-
iteration.html
 http://letslearncomputing.blogspot.com/2013/06/advan
tages-and-disadvantages-of.html
 http://www.site.uottawa.ca/~ivan/tr-educ.pdf

Вам также может понравиться