You are on page 1of 11

Алгоритми и структури на податоци

– белешки од предавања –

5. Еднодимензионални податочни
структури
- стек, редица, приоритетна редица-

5.1 Апстрактни податочни типови

Пред да започнеме со проучувањето на еднодимензионалните податочни типови, да


се осврнеме на поимот апстрактен податочен тип. За разлика од низите и листите, за
кои велиме дека се фундаментални податочни типови, се појавува потреба од
дефинирање на нови податочни типови за одредени намени. Кај фундаменталните
типови, фокусот беше ставен на нивната имплементација. На пример, низата беше
опишана како последователни мемориски локации каде се чуваат податоците. Кај
апстрактните податочни типови, фокусот не е на имплементацијата. Токму затоа и се
викаат апстрактни, бидејќи во нивното дефинирање не се грижиме за нивната
имплементација (ја апстрахираме), туку само за тоа какви податоци треба да
зачуваме и како истите да ги обработуваме.
За апстрактните податочни типови карактеристично е:
- Специфицирање на множеството вредности за податочниот тип
- Специфицирање на можните операции врз тоа множество вредности
При тоа, воопшто не се навдува ништо за имплементацијата на податочниот тип.
За секој апстрактен податочен тип, всушност се прави одреден “договор”. Овој
договор ги содржи главните карактеристики на типот: множеството податоци и
операции со тие податоци. Во него се нагласува какви ќе бидат податоците и какво е
посакуваното однесување од операциите дефинирани над тие податоци.
Овој концепт на апстрактни податочни типови овозможува јасно раздвојување на
двете основни компоненти: спецификација и имплементација на податочниот тип.
Спецификацијата претставува опис на можните вредности, како и опис на
операциите над нив (нивното посакувано однесување). Имплементацијата пак ги
содржи деталите со преставувањето на податоците (со некоја од фундаменталните
структури или преку други апстрактни типови), како и деталите поврзани со
алгоритмите кои се користат за имплементирање на операциите врз тие податоци.
Ваквата поделба овозможува постоење на алтернативни имплементации за еден ист
договор. При тоа, доколку различните имплементации стриктно го почитуваат
договорот, можно е нивно наизменично користење, во зависност од потребата и
примената, остварувајќи при тоа баланс помеѓу користење на ресурси (на пример
меморија) и перформанси. Најважна придобивка од апстракцијата е раздвојувањето
на одговорностите. Оној кој бара одредена податочна структура да биде изработена
за да може да ја користи во своите апликации, се што треба е добро да ја
специфицира. Оној пак кој имплементира одредена структура, треба само да го

1
испочитува постигнатиот договор. Јасното одвојување на одговорностите е особено
важно кога станува збор за големи проекти и комплексни апликации.

5.2 Стек - stack


Стекот преставува еднодимензионална линеарна секвенца од елементи која работи по
принципот последен-внесен-прв-изваден (last-in-first-out). Ова значи дека додавањето
на нови елементи и нивното вадење од линеарната секвенца се врши само од еден
крај. Овој крај на структурата се нарекува врв на стекот. Аналогијата се прави со
стог од сено, каде сеното се става и вади само на горната страна – на врвот на стогот.
Под длабочина на стек се подразбира бројот на елементи што се складирани во него.
Празниот стек има длабочина 0.
На сликата е даден еден пример на стек од книги. Под претпоставка дека се работи за
тешки книги, додавањето и вадењето на книгите е можно само на горната страна –
врвот на стекот.

По додавање
По додавање
на уште една
На почеток: По вадење на на книга:
книга
книга:

2001
Misérables Misérables
Rob Roy
War & Peace War & Peace War & Peace War & Peace
Moby Dick Moby Dick Moby Dick Moby Dick

Слика 5.1. Пример за стек

Основните побарувања (договорот) за апстрактниот податочен тип стек се:


1. Празнење на целиот стек
2. Проверка дали стекот е празен
3. Додавање на елемент на врвот на стекот (операција “push”)
4. Вадење на елемент од врвот на стекот (операција “pop”)
5. Дополнително, проверка на првиот елемент во стекот без негово вадење.

За апстрактниот тип стек, ќе дадеме 2 имплементации, со двата различни


фундаментални податочни типови: низа и поврзана листа.

Имплементацијата на стек со низа преставува имплементација на ограничен стек,


чија максимална длабочина може да биде еднаква на големината на низата со која
истиот се имплементира.
Имплементацијата содржи променлива depth која ја содржи тековната длабочина на
стекот, како и низа од елементи elems со должина maxdepth во која се содржат
елементите на позиците elems[0..depth-1].
На сликата е даден изгледот на оваа имплементација, како и пример за празен стек и
стек од неколку елементи.

2
врв празно

0 1 depth–1 depth maxdepth–1


element element element
Опис

depth=0 1 maxdepth–1
Празен стек

0 1 2 depth=3 4 5
Пример Moby War & Rob
(maxdepth = 6): Dick Peace Roy

Слика 5.2 Имплементација на стек со низа

Алтернативна имплементација на стекот е со помош на поврзана листа. Оваа


имплементација претставува неограничен стек, каде првиот елемент од листата го
претставува врвот на стекот (односно сите операции се извршуваат на почетокот на
листата). На сликата е претставена имплементацијата со поврзана листа.

врв

Опис element element element

Празен стек

Rob War & Moby


Пример Roy Peace Dick

Слика 5.3 Имплементација на стек со поврзана листа

Постојат голем број практични примери на стек. Тесен магацин, со ширина еднаква
на товарот кој во него се складира, и кој има еден влез, преставува стек. Во него,
последно внесениот товар треба да се извади прв.
Стекот има особено голема примена во самите компјутерски системи, бидејќи со
негова помош е возможно реализирање на концептот на подпроцедури, нивно
повикување и враќање.
Следат неколку примери за корисноста од овој податочен тип.

Превртување на редослед на записи во една датотека.


Заради поедноставување, да претпоставиме дека се работи за текстуална датотека, во
која има редици од текст. Потребно е да се преврти редоследот на редиците во
датотеката, односно последната редица да стане прва, претпоследната втора итн., се
до првата која на крајот ќе биде последна. Ова најлесно може да се имплементира со
користење на стек. При тоа, се читаат редиците една по една од датотеката и се
поставуваат на стек. Кога ќе се испразни влезната датотека, се отвора излезна

3
датотека и се запишуваат редиците како што се вадат од стекот. Самата природа на
стекот ќе значи дека редоследот во излезната датотека ќе биде обратен од оној во
влезната. Следниот алгоритам ја илустрира оваа постапка.

Направи празен стек.


Се додека има линии во влезната датотека, повторувај:
Прочитај линија
Додај ја на врвот на стекот.
Се додека стекот има елементи, повторувај:
Извади линија од врвот на стекот
Запиши ја во излезната датотека.
Крај.

Проверка на добро поставени загради. Уште еден карактеристичен пример на


примена на стек е проверката на добро поставени загради во аритметичките изрази.
Користењето на загради во изразите треба да се врши со почитување на следните 2
правила: 1. Секоја отворена заграда мора да има затворена и 2. Доколку се користат
загради од различен тип (мали, средни, големи), тогаш пресекот на деловите од
изразот заградени со 2 типа на загради или е празен или е едниот од изразите. Ова
значи дека не смее да има поставување на заградите од ваков тип: {…(…}…), туку
тоа треба да биде или {…}…(…), или {...(...)...}.
Еве неколку примери за добро поставени загради
s  (s – a)  (s – b)  (s – c)
(– b + [b2 – 4ac]) / 2a
Следните примери се лошо поставени загради
s  (s – a)  (s – b  (s – c)
s  (s – a)  s – b)  (s – c)
(– b + [b2 – 4ac)] / 2a
Следниот алгоритам дава едно можно решение за проверка на добро поставени
загради со користење на стек.

Направи празен стек.


Се додека има симболи во изразот (од лево кон десно), повторувај:
Прочитај симбол
Ако симболот е отворена заграда.
Додај го на стекот.
Ако симболот е затворена заграда
Ако стекот е празен
врати погрешно и заврши.
Ако на врвот од стекот се наоѓа различен тип отворена заграда
врати погрешно и заврши.
Извади го елементот од врвот од стекот.
Ако стекот е празен
Врати точно и заврши
Во спротивно
Врати погрешно и заврши
Крај.

Пресметување на вредност на израз во постфикс нотација.

4
Постојат различни начини на претставување на аритметичките изрази, во однос на
тоа каде се наоѓа операторот во однос на операнците. Нам луѓето најблизок за
разбирање ни е начинот наречен инфикс, кога операторот се наоѓа помеѓу
операндите.
Следниот израз е даден во инфикс нотација.
(5+9) * 2+6 * 5
Постои и таканаречена префиксна или полска нотација, каде прво се запишуваат
операторите, а потоа операндите. Истиот израз, даден во постфикс нотација може да
се запише како
+*+592*65
При тоа се забележува дека нема потреба од постоење на загради.
Обратната нотација, каде операторите се запишуваат после операндите се нарекува
обратна полска нотација или постфикс нотација. И кај неа нема потреба од загради.
Изразот од примерот, напишан во постфикс нотација изгледа вака
59+2*65*+
Токму вака запишаните изрази лесно можат да се пресметаат (евалуираат),
користејќи стек. Алгоритмот што следи ја опишува оваа постапка

Направи празен стек.


Се додека има симболи во изразот (од лево кон десно), повторувај:
Прочитај симбол
Ако симболот е операнд
Додај го на стекот.
Ако симболот е оператор
Извади елемент од стекот
Извади елемент од стекот
Примени ја операцијата што ја дефинира операторот врз двата
операнди извадени од стекот
Додај го резултатот на стекот
Извади го елементот од стекот
Печати го како резултат

На сликата е даден пример за извршување на овој алгоритам

Слика 5.4 Извршување на алгоритамот за изразот од примерот

5.3 Редица - queue


Редицата преставува еднодимензионална линеарна секвенца од елементи која работи
по приципот прв-внесен-прв-изваден. Тоа значи дека елементите се додаваат на
едниот крај од линеарната секвенца (опаш), а се вадат на другиот крај (глава).
Дожина на редицата преставува бројот на елементи што ги содржи во себе. Празната
редица има должина 0.
5
Основните побарувања (договорот) за апстрактниот податочен тип редица се:
1. Празнење на целата редица
2. Проверка дали редицата е празна
3. Додавање на елемент на опашот од редицата (операција “en-queue”)
4. Вадење елемент од главата на редицата (операција “de-queue”)
5. Дополнително, проверка на елементот на главата на редицата без негово
вадење.
И за редицата ќе дадеме 2 имплементации, со двата фундаментални податочни
типови.
Редицата имплементирана со низа преставува ограничена редица, со должина помала
или еднаква на должината на низата со која се имплементира maxlen. Содржи
променлива length, која ја содржи моменталната должина, потоа променливи глава
(front) и опаш (rear), како и низа од елементи elems, кои ги содржат елементите на
редицата на позициите elems[front…rear-1].
На сликата е даден пример за имплементација на редица со низа.

празно глава опаш празно

0 front rear–1 maxlen–1


Опис: element element element

0 front=rear maxlen–1
Празна
редица

Слика 5.5 Имплементација на редица со низа

На следната слика е даден пример на последователни вметнувања и вадења на


елементи од редица, имплементирана со низа од 6 елементи.

На почеток: По додавање на Homer, Marge, Bart, Lisa:


0 1 2 3 4 5 0 1 2 3 4 5
elems elems Homer Marge Bart Lisa

front 0 rear 0 length 0 front 0 rear 4 length 4

По додавање на Maggie: По вадење на елемент од главата:


0 1 2 3 4 5 0 1 2 3 4 5
elems Homer Marge Bart Lisa Maggie elems Marge Bart Lisa Maggie

front 0 rear 5 length 5 front 1 rear 5 length 4

По вадење на елемент од главата: По додавање на Ralph:


0 1 2 3 4 5 0 1 2 3 4 5
elems Bart Lisa Maggie elems Bart Lisa Maggie Ralph

front 2 rear 5 length 3 front 2 rear 0 length 4

6
Слика 5.6 Пример за последователни додавања и вадења на елементи во редица

Разгледувајќи ја конечната состојба на примерот од сликата (по додавањето на Ралф),


се наметнува прашањето што ако сега сакаме да додадеме уште еден елемент?
Максималниот капацитет не е исполнет (низата има 6 елементи, а редицата
моментално само 4). Каде треба да се додаде новиот елемент? Едно можно решение
на овој проблем е да ја модифицираме операцијата на вадење на елемент, при што за
секој изваден елемент, останатите елементи би се поместувале во лево, за да се
порамнат на почетокот на низата. Но ваквата операција би имала временска
сложеност O(n), иста со операцијата бришење на елемент од низа. Дали постои
поефикасен начин?
Решение на овој проблем претставуваат цикличните низи. Циклична низа со должина
n е низа во која секој елемент има и претходни и следбеник. При тоа, претходник на
a[0] е а[n-1], а следбеник на a[n-1] е a[0].
На сликата е дадена визуелна престава на ваквите низи.
7 0

6 1

5 2
0 1 2 3 4 5 6 7
4 3

Слика 5.7 Визуелизација на циклична низа

Во тој случај, имплементација преставува ограничена редица, со капацитет помал


или еднаков на однапред дефинирана максимална должина (maxlen), содржи
променлива length, која ја содржи моменталната должина, променливи глава (front) и
опаш (rear), како и циклична низа од од елементи elems, кои ги содржат елементите
на редицата на позициите elems[front…rear-1] или elems[front…maxlen–1] и
elems[0…rear–1].
На сликата е даден пример за имплементација со циклична низа

0 front rear–1 maxlen–1


Опис: element element element

0 rear–1 front maxlen–1


или: element element element element

0 front=rear maxlen–1
Празна
редица:

Слика 5.8 Имплементација на редица со циклична низа

На сликата е даден пример на додавање и вадење на елементи во редица


имплементирана со циклична низа

7
На почеток: По додавање на Homer, Marge, Bart, Lisa:
0 1 2 3 4 5 0 1 2 3 4 5
elems elems Homer Marge Bart Lisa

front 0 rear 0 length 0 front 0 rear 4 length 4

По додавање на Maggie: По вадење на елемент од главата:


0 1 2 3 4 5 0 1 2 3 4 5
elems Homer Marge Bart Lisa Maggie elems Marge Bart Lisa Maggie

front 0 rear 5 length 5 front 1 rear 5 length 4

По вадење на елемент од главата: По додавање на Ralph:


0 1 2 3 4 5 0 1 2 3 4 5
elems Bart Lisa Maggie elems Bart Lisa Maggie Ralph

front 2 rear 5 length 3 front 2 rear 0 length 4

По додавање на Nelson: По додавање на Martin:


0 1 2 3 4 5 0 1 2 3 4 5
elems Nelson Bart Lisa Maggie Ralph elems Nelson Martin Bart Lisa Maggie Ralph

front 2 rear 1 length 5 front 2 rear 2 length 6

По вадење на елемент од главата:


0 1 2 3 4 5
elems Nelson Martin Lisa Maggie Ralph

front 3 rear 2 length 5

Слика 5.9 Пример за последователни додавања и вадења на елементи во редица


имплементирана со циклична низа

Втората имлементација на редицата е имплементацијата со поврзана листа. Притоа се


добива неограничена редица, преставена со поврзана листа, каде првиот елемент е
главата на редицата, а чие заглавие содржи два покажувачи: еден кон првиот елемент
(глава - front) и еден кон последнот елемент (опаш - rear) и промелива length.
На сликата е даден пример за имплементација на редицата со поврзана листа
front element element element
Опис:
rear

Празна front
редица: rear

front Homer Marge Bart Lisa


Пример:
rear

Слика 5.10 Имплементација на редица со поврзана листа

Редицата исто така има голем број на примени. Секоја редица на чекање на добивање
одреден сервис всушност се претставува со апстрактниот тип редица. Во
компјутерските системи, редиците се користат кај мрежните печатачи, каде повеќе

8
корисници испраќаат документи за печатење на делен печатач, потоа кај хард
дисковите, односно во нивните драјвери, каде повеќе процеси бараат пристап до
податоците од дискот. Уште една примена е во оперативните системи, каде повеќе
процеси чекаат на ред да бидат опслужени од процесорот. Системот наречен
распределувач (scheduler) ги распределува процесите кои чекаат во листата и им
обезбедува пристап до процесорот.
Еден ислустративен пример за примена на редицата е поделба на подредена датотека
со податоци, во две или повеќе повторно подредени датотеки, според даден
критериум.
На пример, дадена е датотека со податоци за студентите, подредена според нивниот
број на индекс. За одредена обработка, потребно е да се подели оваа датотека на две,
една со машки, една со женски студенти, но сепак да се задржи подредувањето
според бројот на индекс. Кодот во продолжение го прави токму тоа, користејќи 2
редици.

Направи празни редици машки и женски


Се додека има податоци во влезната датотека, повторувај:
Прочитај запис од влезна датотека
Ако атрибут пол е машко
Постави запис во редица машки
Во спротивно
Постави запис во редица женски
Се додека има податоци во редица машки, повторувај:
Извади запис од редица
Запиши во датотека машки
Се додека има податоци во редица женски, повторувај:
Извади запис од редица
Запиши во датотека женски
Крај.

Еве уште два примери за размислување. На две различни железнички станици


постојат пруги како на сликата. Размислете за постапка за подредување на вагоните
според нивниот реден број, користејќи ги шините на располагање на секоја од
станиците.

Слика 5.11 Прва станица

Слика 5.12 Втора станица

9
5.4 Приоритетна редица - priority queue
Приоритетната редица преставува еднодимензионална линеарна секвенца од
елементи, каде секој елемент има свој приоритет. Карактеристично за неа е што
елементот со најголем приоритет секогаш прв се вади од листата. Должина на
приоритетна редица е бројот на елементи што ги содржи, при што празната
приоритетна редица има должина 0.
Приоритетната редица има примена секаде каде што е потребно да се имплементира
ред на чекање, но каде постојат разлики во приоритетот на оние кои чекаат во тој
ред. Карактеристичен пример е претходно споменатиот распределувач на процеси во
оперативните системи. Не сите процеси во еден оперативен систем имаат исто
значење. Постојат процеси на таканареченото јадро на оперативниот систем за кои е
посебно важно веднаш да добијат пристап до процесорот, за разлика од
корисничките процеси, кои не се суштински за функционирањето на системот. Токму
овој принцип се имплементира со приоритетна редица.
Уште еден пример на примена на приоритетната редица е подредувањето.

Што прави постапката опишана во следните чекори?


1. Отвори датотека
2. Се додека има елементи
a. Прочитај елемент и постави во приоритетна редица
3. Затвори датотека
4. Отвори излезна датотека
5. Се додека има елементи во приоритетна редица
a. Извади елемент од приоритетна редица (оној со најголем приоритет) и
запиши го во излезна датотека
6. Затвори излезна датотека

Како ќе изгледа излезната датотека?


Лесно се увидува дека излезната датотека ќе ги содржи елементите од влезната
датотека, но ќе биде подредена според приоритетот, бидејки елементот со најголем
приоритет прв ќе биде изваден од редицата и запишан, потоа тој со следен најголем
приоритет итн.
Основите побарувања за апстрактниот податочен тип приоритетна редица се:
1. Празнење на целата приоритетна редица
2. Проверка дали приоритетната редица е празна
3. Додавање на елемент во приоритетната редица
4. Вадење на елементот со наголем приоритет од редицата
5. Дополнително, проверка на елементот со најголем приоритет во редицата без
негово вадење.
При изборот за имплементација на приоритетната редица, независно од тоа кој
фундаметален тип ќе се користи, постојат 2 алтернативи:
1. подредена редица
2. неподредена редица
која ќе се имплементира како неограничена редица – со листа, или ограничена
редица – со низа.

Првата варијанта е имплементација на подредена редица, имплементирана со


подредена низа или подредена поврзана листа. Независно од тоа што ќе се користи за
имплементација, во оваа варијанта додавањето на нов елемент ќе значи наоѓање на
позицијата каде тој треба да се смести и негово вметнување.

10
При имплементација со низа: пребарувањето ќе биде со сложеност О(n) ако се
пребарува линеарно. Ако се земе во предвид подреденоста, може да се пребарува и
поинтелигентно, на пример бинарно, при што сложеноста е O(log n). Но на ова мора
да се додаде сложеноста на вметнување на елемент во низа, која е O(n).
При имплементација со листа: пребарувањето мора да биде од почеток и линеарно,
значи O(n). Самото вметнување е O(1).

Втората варијанта е имплементација со неподредена низа или листа. При тоа,


додавањето на елементот е на првата позиција (кога станува збор за поврзана листа),
односно на последната позиција (кај имплементацијата со низа). И во двете
имплементации, се додава онаму каде тоа е најефтино: кај низа на крај, кај поврзана
листа на почеток, со сложеност O(1). Проблемот овде настанува при вадењето на
елементот. Тогаш е потребно да се измине целата структура за да се најде елементот
со наголем приоритет. И во двете имплементации, наоѓањето на елементот со
најголем приоритет значи поминување на сите елементи, од почеток до крај, што
значи O(n).
Во табелата е дадена споредба на сложеноста на операциите во двете варијанти,
секоја имплементирана со низа и поврзана листа.

Операција Подредениа Неподредена Подредена Неподредена


поврзана листа поврзана листа низа низа

Додај O(n) O(1) O(n) O(1)

Извади O(1) O(n) O(1) O(n)

Табела 5.1 Споредба на сложеноста на додавање и вадење на елемент кај


приоритетна листа

Ако се вратиме назад на претходниот пример, каде елементите од една датотека ги


поставувавме еден по еден во приоритетна редица, а потоа ги вадевме еден по еден и
ја анализираме сложеност на таа постапка, при различните имплементации, лесно се
заклучува дека додавањето на n елементи, а потоа нивното вадење ќе има сложеност:
 При имплементација со подредена низа или листа, тоа значи n*O(n) + n*O(1),
односно О(n2)
 При имплементација со неподредена низа или листа, тоа значи n*O(1) +
n*O(n), односно О(n2)
Се поставува прашање дали е можно ова да се реши со малку поинаква
имплементација на приоритетната редица? Одговорот на ова прашање ќе го добиете
во втората половина на курсот, кога ќе зборуваме за дрвата и нивната примена.

11

You might also like