You are on page 1of 85

Податочни структури и

анализа на алгоритми
Предавање #3

Линеарни податочни структури


Линеарни податочни структури
Секвенцијални / Поврзани

 Податочните структури на физичко (имплементациско) ниво можат фундаментално да се


поделат на:
• секвенцијални

• поврзани

 СЕКВЕНЦИЈАЛНИТЕ зафаќаат континуирани сегменти од линеарниот мемориски простор за да


запишат вектори, матрици, heap простори (run-time) и hash табели.

 ПОВРЗАНИТЕ зафаќаат ланци од парчиња (соодветни на елементите) поврзани помеѓу себе со


поинтери (референци). Тие на физичко ниво имплементираат логички агрегации како листи,
дрва и графовски матрици на соседства.
 Фундаментална линеарна ФИЗИЧКА структура е НИЗА (поле, array). Хиерархиски тоа е прва и
елементарна организација на основните типови податоци и под соодветни услови овозможува
имплементација на скоро сите други структури.
 Фундаментална линеарна ЛОГИЧКА структура е ЛИСТА!

2
Линеарни податочни структури
Низи/Полиња (Arrays)

 Основни особини на низите (во сите програмски имплементации) се:


• хомогеност - сите елементи се од ист (примитивен/сложен) податочен тип
• фиксна должина - статична (најавена) димензија за време на извршување
• директен пристап - преку ефикасно индексирање (array [index])

 Предности на континуалните (секвенцијални) алокации на полиња во меморијата:


• константно време на пристап - директно преку имплицитно мапирање со индексот
• просторна ефикасност - целокупната содржина на низата е податочна (нема поинтери)
• мемориска локалност - физичката соседност на елементите овозможува максимално брзо
итерирање низ низите со што директно се ползува брзината на cache мемориите

 Хомогеноста и секвенцијалноста во алокацијата овозможуваат имплицитно определување на


локацијата на било кој елемент и без употреба на индексот. Имено, со познавањето на
локацијата на првиот елемент a и прецизноста на елементите b (изразена во бајти), i-тиот
елемент се лоцира со константниот временски трошок a + (i-1) · b  O(1)!

3
Линеарни податочни структури
Низи/Полиња (Arrays)

 Суштинска негативност на низите е неможноста динамички да се менува нивната должина


при run-time. Доколку предвидената димензија n не гарантира акомодација на бројот на
елементи кои би притребале при извршување, постои можност програмот да падне при првиот
обид да се алоцира локацијата со индекс n+1.

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


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

 Решение е механизмот на динамички низи - имплементација на ефикасно зголемување за


време на run-time. Штом низата се пополни во целост и се појави потреба од ново вметнување,
се алоцира нова поголема низа и старата се копира во новата! Потоа старата се ослободува и
новата продолжува со работа. Бидејќи тоа значи задолжително трошење на време во
моментите на "зголемување", Java уште при декларацијата обезбедува резерва за низата над
декларираната димензија во кодот при алокацијата.

4
Линеарни податочни структури
Операции со низи

Вообичаени операции со елементите на низите опсервирани како парови (вредност, индекс) се:

• креирање празна низа


• пребарување вредност преку индексот
• пребарување индекс преку вредност (секвенцијално, бинарно)
• додавање/вметнување елемент
• бришење елемент
• сортирање

Илустрација со следната низа:

5
Линеарни податочни структури
Операции со низи

 Вметнување (insertion)
Наједноставно вметнување на нов елемент во низата е додавање на крајот (после последниот).
Во тој случај индексирањето обезбедува пристап во константно време O(1)!
Вметнување меѓутоа на било која друга позиција (во подредена низа) бара поместување на сите
од таа позиција НАДЕСНО што имплицира FOR/WHILE јамка за поединечните поместувања, а
што продуцира O(n) трошок!

6
Линеарни податочни структури
Операции со низи

 Бришење (deletion)
Бришење на елемент на било која позиција (освен крајната) бара поместување на сите по таа
позиција НАЛЕВО што имплицира FOR/WHILE јамка за поединечните поместувања, а што
повторно продуцира O(n) трошок!

7
Линеарни податочни структури
Операции со низи

 Пребарување (searching)
Во најлош случај O(n) при секвенцијално испитување на секој елемент (несортирана низа),
бидејќи треба да се изминат сите елементи!
При бинарно пребарување,
(под услов низата да е сортирана)
трошокот е O(lg n):

/** Vrakja pozicija na element so vrednost k vo sortirana


niza A. Ako k ne postoi se vrakja A.length. */
static int binary(int[] A, int k)
{
int l = -1; // l e indeksot
int r = A.length; // r e dolzinata na nizata
while (l+1 != r) // zapri koga l i r kje se sretnat
{
int i = (l+r)/2; // proveri ja sredinata
if (k < A[i]) r = i; // levo e
if (k == A[i]) return i; // pronajden!
if (k > A[i]) l = i; // desno e
}
return A.length; // nema takov element vo A
} 8
Линеарни податочни структури
Комплексност и примена на низите

 Може да се заклучи, дека низите се прва (и најлесна за имплементација) опција доколку


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

 Доколку критична важност има пребарувањето - треба да се одржуваат сортирани заради


брзото бинарно пребарување.
 Доколку критична важност има вметнувањето - треба да се користат несортирани.
 Бришењето елемент е бавно во обете варијанти!
 Класата vector во Java го решава проблемот на динамичко одржување на потребната должина
на низите - значајно успорување на извршувањето во моментите на зголемување на должината
поради копирањето во новата низа.
9
Линеарни податочни структури
Примена на низи - 1D вектор

 Низата нативно акомодира ВЕКТОРИ - логичката репрезентација одговара со физичката.

Вредноста на елементот Ai при s-бајтна прецизност на податочниот тип се добива со


индексирање во константно време:

Ai = AI + (i - 1) · s

• ефикасен пристап
• неефикасно вметнување и бришење

10
Линеарни податочни структури
Примена на низи - 2D матрица

 Низата линеарно акомодира МАТРИЦИ со ефикасен пристап.


Eлементот Aij при s-бајтна прецизност на податочниот
тип се добива со индексирање во константно време.
Целата матрица физички е вектор во меморијата  O(1)!
• Линеаризација по редови (row-major):

Ai,j = A1,1 + ((i - 1) · N + j - 1) · s   f(M)

1рв ред i-ти ред М-ти ред


(сите редови пред i плус колоната пред j - i и j се бројат од 0)

• Линеаризација по колони (column-major):

Ai,j = A1,1 + ((j - 1) · M + i - 1) · s   f(N)

1рва колона j-та колона N-та колона


11
Линеарни податочни структури
Примена на низи - nD матрица

 Лексикографски подредена по редови N-димензионална низа:

X[1, ..., 1, 1] X[1, ..., 1, 2] X[1, ..., 1, un]


X[1, ..., 2, 1] X[1, ..., 2, 2] X[1, ..., 2, un]
...
X[u1, ..., un-1, 1] X[u1, ..., un-1, 2] X[u1, ..., un-1, un]

 Низата линеарно акомодира N-димензионални вектори/матрици со ефикасен пристап:

Ai1,.., in = A1,..,1 + ((i1 - 1)u2u3...un + (i2 - 1) u3u4...un +…+ (in-1 - 1)un + in - 1)s

offset = 0
for j = 1 to n do
offset = U [j ] offset + I [j ] - 1
end_for
A = A1,..,1 + s * offset 12
Линеарни податочни структури
Примена на низи - Триаголни и ретки (sparse) матрици

 Ретките матрици се чест реален случај на големи матрици со значителен (доминантен) број на
нули - во општ случај рандомизирано позиционирани.
 Чести облици се и триаголните и тридијагоналните матрици:

 За долно-триаголната матрица ефикасно адресирање се постигнува со линеаризација по


редови:
X[i,j] = 0 , i < j  X[1,1] X[2,1] X[2,2] X[3,1] X[3,2] X[3,3] ...
 Адресирање за ненулевите елементи (заштеда од околу 50% простор):

Ai,j = A1,1 + (i · (i - 1)/2 + j - 1) · s  ij


13
Линеарни податочни структури
Примена на низи - Триаголни и ретки (sparse) матрици

 Една ретка матрица со (просечни) димензии 100.000  100.000 од тип float (4 бајти) зафаќа
значаен простор од 40 GB. Ако е пола празна тоа имплицира некои 14 Gb непотребен
мемориски трошок!
 Ако матрицата е РЕТКА, тогаш со задржана ЕФИКАСНОСТ, а сепак и ЗАШТЕДЕН ПРОСТОР може
да се претстави преку вектор од координати и вредности на ненултите елементи, во формат
(row, column, value):
R C V

0 0 4 0 0 0 0 1 3 4

2 4 5
0 0 0 5 0 11 0
2 6 11
X = 0 0 0 0 0 0 0
4 1 9
9 0 0 8 0 0 0
4 4 8
0 0 0 0 0 0 15 5 7 15

 Векторот од 6 тројки спасува 35 – 18 = 17 елементи  48% заштеда во простор! Пристапот ја


има истата ефикасност со намалена комплексност n!
14
Линеарни податочни структури
Примена на низи - Ретки (sparse) матрици

 Постојат повеќе стандарди за ефикасно (компресирано) манипулирање со ретките матрици:

BCCS Block Compressed Column Storage format DNS Dense format


BCRS Block Compressed Row Storage format ELL Ellpack-Itpack generalized diagonal format
BND Linpack Banded format JAD Jagged Diagonal format
BSR Block Sparse Row format LNK Linked list storage format
CCS Compressed Column Storage format MSR Modified Compressed Sparse Row format
COO Coordinate format NSK Nonsymmetric Skyline format
CRS Compressed Row Storage format SSK Symmetric Skyline format
CSC Compressed Sparse Column format SSS Symmetric Sparse Skyline format
CSR Compressed Sparse Row format USS Unsymmetric Sparse Skyline format
DIA Diagonal format VBR Variable Block Row format

 Заедничко за сите е линеарната репрезентација реализирана преку низи или листи!

15
Линеарни податочни структури
Примена на низи - Ретки (sparse) матрици

 Еден од најраспространетите е CSR форматот (Compressed Sparse Row)

 Корисен за ретки матрици од општа форма.

 Користи три вектори за опис на матрицата:


• val - ненултите вредности подредени ред-по-ред
• col_ind - колоните на респективните вредности во [val]
• row_ptr - позициите во [val] кои преминуваат во наредниот ред

 Вака дефинираните sparse-матрици овозможуваат ефикасни (обично линеарни) алгоритми за


манипулации со податоците.

16
Линеарни податочни структури
Примена на низи - Претставување на полиноми

 Работата со полиноми побарува ефикасен начин за репрезентација во меморијата.


Доколку редот на полиномите е познат однапред, најефикасна репрезентација во
меморијата би била со употреба на низи во кои би се внесувале коефициентите и степените.
m
 Дефиниција на полином: P ( x)   bi x ei  bm x em  ...  b0 x 0
i 0

 Еден начин на репрезентација: P1(x) = [m, em, bm, ..., e0, b0]

 Или просторно поефикасно: P2(x) = [m, bm, ..., b0]

 Пример: P( x)  3x 5  7 x 4  2 x 2  9
P1 ( x)  5 3 4 7 3 0 2  2 1 0 0 9
P2 ( x)  5 3 7 0  2 0 9
 Проблем настанува ако при извршување се појави полином од ред за кој не е предвиден
соодветен простор!
17
Линеарни податочни структури
Array ADT

 Во Java постои дефинирана класа за низи (java.util.Arrays), со дефинирани методи кои се


корисни за разните операции со низите, бидејќи ги апстрахираат незгодните имплементации на
"физичко" ниво. Дел од тие методи се:
• equals (A, B) - враќа TRUE ако низите A и B се еднакви (идентични елементи и редослед)
• fill (A, x) - ја пополнува низата A со елементот x (под услов да е соодветен тип)
• copyOf (A, n) - враќа низа со првите n елементи од A (дополнува/padding со default
вредноста од типот на A при n > A.length)
• copyOfRange (A, s, t) - враќа низа со [t-s] елементи од A[s] до A[t-1] (со padding при
t > A.length)
• sort(A) - ја сортира низата A согласно природата на типот кој го содржи
• toString(A) - враќа string репрезентација на A каде елементите се одделени со запирка
• итн...

18
Линеарни податочни структури
Листи

 ЛИСТА е конечна линеарна секвенца од податочни елементи (објекти) кои се подредени по


некаков редослед.

 Од дефиницијата произлегува суштинскиот концепт - ПОЗИЦИЈА во листата! Од конечноста


следи дека секој елемент има конкретна позиција - ПРВ, ТЕКОВЕН и ПОСЛЕДЕН елемент.

 Хомогеноста е вообичаена, но нема пречки за реализација на листи со елементи од


различни податочни типови.

 Нотација за листа од n елементи  <a0 , a1 , ..., an-1>

 Терминологија:
• empty (листа) - празна, без ниеден податочен елемент (објект)
• length (должина) - моменталниот број на елементи во листата
• head - почетокот на листата (може да е елемент)
• tail / trailer - крајот на листата
• сортирана листа - елементите се подредени по растечки редослед (согласно типот)
• несортирана листа - нема релација помеѓу податокот и позицијата на елементите

19
Линеарни податочни структури
Листи

 При избор на листата потребно е да се утврдат операциите кои треба да бидат подржани од
имплементацијата. Интуитивно се очекува дека листата треба да може да се
издолжува/скратува со вметнување/бришење на елементи; да се пристапи и модифицира
ефикасно било кој елемент; да се креира и (ре-)иницијализира. Најбитно, да може
едноставно да се достапат претходниот и наредниот елемент од тековниот!

 Со дефинирана спецификација на методите кои треба да се имплементираат, и утврдена


логичка спецификација на податочните објекти кои се процесираат, може да се дефинира ADT
за листата.

 Во Java најпогодно е ADT да се специфицира преку функционален интерфејс, "позади" кој


може потоа да се реализираат најразлични имплементации!

 ЛИСТИТЕ може да се имплементираат преку:


1. Низи (Array List) - статичка имплементација (предефинирана максимална должина)
2. Поврзани листи (Linked List) - динамичка имплементација (неограничена должина)

20
Линеарни податочни структури
List ADT

 Ефикасна манипулација со листа овозможува следниот интерфејс со клучен концепт


тековна (current) позиција - секоја акција се реализира на тековната позиција:

public interface List<E>


{
public void clear();
public void insert(E item);
public void append(E item);
public E remove();
public void moveToStart();
public void moveToEnd();
public void prev();
public void next();
public int length();
public int currPos();
public void moveToPos(int pos);
public E getValue();
}
21
Линеарни податочни структури
List ADT - Апликативна употреба

 Вметнување на елементот 99 во листата <12 | 32, 15> ("|" означува тековна позиција - во
овој случај 32):

L.insert(99);  <12 | 99, 32, 15>


 Итерирање низ целата листа:
for (L.moveToStart(); L.currPos()<L.length(); L.next())
{
it = L.getValue();
doSomething(it);
}
 Секвенцијално пребарување на вредност k:
/** Vrakja true ako k go ima vo listata L, inaku false */
public static boolean find(List<Integer> L, int k)
{
for (L.moveToStart(); L.currPos()<L.length(); L.next())
if (k == L.getValue())
return true;
return false; // k ne e najden
}

22
Линеарни податочни структури
List ADT - Имплементација со низа (AList)

 Имајќи ги предвид спецификите на процесирање на елементи во низи (секвенцијални


поместувања при вметнување/бришење, итн.), една имплементацијата на ЛИСТА со низа
може да е:
class AList<E> implements List<E>
{
private static final int defaultSize = 10;
private int maxSize;
private int listSize;
private int curr;
private E[] listArray;

// Konstruktori
AList() { this(defaultSize); }

AList(int size)
{
maxSize = size;
listSize = curr = 0;
listArray = (E[])new Object[size];
}
23
Линеарни податочни структури
List ADT - Имплементација со низа (AList)

 Имајќи ги предвид спецификите на процесирање на елементи во низи (секвенцијални


поместувања при вметнување/бришење, итн.), една имплементацијата на ЛИСТА со низа
може да е:
public void clear() { listSize = curr = 0; }
public void moveToStart() { curr = 0; }
public void moveToEnd() { curr = listSize; }
public void prev() { if (curr != 0) curr--; }
public void next() { if (curr < listSize) curr++; }
public int length() { return listSize; }
public int currPos() { return curr; }

public void moveToPos(int pos)


{
assert (pos>=0) && (pos<=listSize) : "Ilegalna pozicija";
curr = pos;
}
public E getValue()
{
assert (curr >= 0) && (curr < listSize) : "Nema element";
return listArray[curr];
} 24
Линеарни податочни структури
List ADT - Имплементација со низа (AList)

 Имајќи ги предвид спецификите на процесирање на елементи во низи (секвенцијални


поместувања при вметнување/бришење, итн.), една имплементацијата на ЛИСТА со низа
може да е:

/** Insert "it" na tekovnata pozicija */


public void insert(E it)
{
assert listSize < maxSize : "Kapacitetot e nadminat";
for (int i=listSize; i>curr; i--)
listArray[i] = listArray[i-1]; //pomesti gi site nadesno
listArray[curr] = it;
listSize++; //brojot na elementi zgolemen
}

/** Append "it" na tekovnata pozicija */


public void append(E it)
{
assert listSize < maxSize : "Kapacitetot e nadminat";
listArray[listSize++] = it;
}
25
Линеарни податочни структури
List ADT - Имплементација со низа (AList)

 Имајќи ги предвид спецификите на процесирање на елементи во низи (секвенцијални


поместувања при вметнување/бришење, итн.), една имплементацијата на ЛИСТА со низа
може да е:

/** Izvadi go i prosledi go tekovniot element */


public E remove()
{
if ((curr < 0) || (curr >= listSize))
return null;
E it = listArray[curr];
for(int i=curr; i<listSize-1; i++)
listArray[i] = listArray[i+1];
listSize--;
return it;
}

26
Линеарни податочни структури
Поврзана листа (Linked List)

 Поврзаната листа е динамичката имплементација на листите - колекција од линеарно


подредени јазли (nodes, links) кои се поврзани помеѓу себе преку поинтери, формирајќи
притоа ланец.

 Јазлите се составени од податочен дел (data, info) и поинтерски дел (links) кој покажува(ат)
на соседните јазли. Линковите на терминалните јазли покажуваат на NULL.

 Суштинска карактеристика на поврзаните листи е динамичката алокација на меморија за


јазлите - истата се резервира при креирање на јазол и се ослободува при неговото бришење.

 Ги решава недостатоците на секвенцијалната имплементација!

 Според начинот на поврзување поврзаните листи се делат на:


• еднострано поврзани листи
• двојно поврзани листи
• кружно поврзани листи (еднострано или двојно)

27
Линеарни податочни структури
Единечно поврзана листа (Singly Linked List - SLL)

 Јазлите на единечно поврзаната листа (SLL) имаат информационен дел и еден линк
(покажувач) за поврзување со наредниот во серијата.

head tail

 SLL има почетен јазол (head) и краен (tail). Референцата на листата покажува на почетниот, а
покажувачот на крајниот покажува на NULL преку што се утврдува крајот на изминувањето.

 Секој јазол освен крајниот има следбеник и секој освен првиот има претходник.

 SLL без ниеден јазол се нарекува празна листа! Постои само референцата спрема истата
(list).

 SLL има редослед како и низите, но нема предефинирана димензија и нема индекс поради
што тековната позиција сама по себе не дава информација за редниот број на јазелот!

 Во SLL може да се додаваат, бришат и изминуваат јазлите ПОЕФИКАСНО од кај низите.


Самиот редослед може брзо да се измени само со манипулација на линковите!
28
Линеарни податочни структури
Операции со SLL

 Изминување на SLL: со итерирање на currnext

head

curr

 Чекор напред: curr = currnext  O(1)!

head

curr

 Чекор назад: нема начин да се референцира претходникот па мора повторно да се итерира


од head  O(n)!

head

curr
29
Линеарни податочни структури
Операции со SLL

 Додавање јазол пред head или после tail е едноставно (само нивно преместување)  O(1)!
 Додавање јазол ПОСЛЕ тековниот:

head

curr

1. се креира јазолот new: newinfo = data


2. се дефинира неговото link поле: newnext = currnext
3. се вметнува после тековниот: currnext = new

head

curr

 Комплексност O(1)!
30
Линеарни податочни структури
Операции со SLL

 Додавање јазол ПРЕД тековниот: редовната процедура бара повторно изминување  O(n)!

head

curr

 Но, има можност за реализација и без ново изминување - се додава новиот јазол ПОСЛЕ
тековниот и двата си ги разменуваат вредностите:
1. се дефинира линкот на јазолот new: newnext = currnext
2. се дефинира неговото info поле: newinfo = currinfo
3. се разменува вредноста: currinfo = data
4. се вметнува во листата: currnext = new

head

curr

 Комплексност O(1)! 31
Линеарни податочни структури
Операции со SLL

 Бришење на ПРВИОТ е едноставно, само се поместува линкот head на наредниот  O(1)!


 Бришење на ТЕКОВНИОТ јазол: потребен е линкот на претходникот, што повторно бара
целосно изминување  O(n)!

head

curr
Но, може да се употреби истиот трик - тековниот копира од наредниот и се брише наредниот:

head

curr
1. тековниот јазол го превзема наредното info поле: currinfo = curr next info
2. тековниот јазол го превзема линкот од наредниот: currnext = currnext next
3. се ослободува меморијата зафатена од наредниот
 Комплексност O(1)!
32
 Проблем при бришење на ПОСЛЕДНИОТ! - Ќе мора пак до претпоследниот  O(n)!
Линеарни податочни структури
Операции со SLL - Заклучок

 Очигледно клучен квалитет во операциите со SLL (во смисла на минимизирање на


комплексноста O) е поинтерот curr да не покажува на тековниот туку на ПРЕТХОДНИОТ јазол,
бидејќи така преку ЛИНКОТ на претходниот истовремено го достапува и ТЕКОВНИОТ!

 Покрај постоењето на head поинтерот со кој се референцира SLL, погодно е да се има и tail
поинтер со кој веднаш се адресира последниот јазол, а со што се овозможени сите варијанти
на додавање јазол во SLL да се извршуваат во O(1) време!

 Некои од предвидените методи за процесирање на SLL имаат потреба од познавање на


ДОЛЖИНАТА, што е скапа операција ако мора постојано да се пребројува. Затоа е паметно да
се одржува бројач cnt кој ќе се ажурира после секоја операција која го менува бројот на јазли!

 Друг специјален случај кој може да направи проблем во алгоритмите на методите е ПРАЗНА
SLL! Во таа ситуација сите поинтери (head, curr, tail) немаат на што да покажуваат (NULL).
Наместо да се комплицираат методите со предвидување на ваквите специјални случаи,
наједноставно е да се дефинира празен јазол ВОДАЧ со линк head кон кој во случај на празна
SLL покажуваат поинтерите.

 При дизајнирањето на Java интерфејсот за ADT за SLL добро би било е да се креира посебна
генеричка класа за јазлите (Link) која би можела да прими објект од било кој тип. 33
Линеарни податочни структури
Имплементација на SLL јазол (Link)

 Објектот на Link има еден податочен елемент и еден поинтер кон следниот јазол:

class Link<E>
{
private E element; // Vrednost na ovoj jazol
private Link<E> next; // Pointer kon sledniot jazol
// Konstruktori
Link(E it, Link<E> nextval)
{
element = it;
next = nextval;
}
Link(Link<E> nextval) { next = nextval; }
Link<E> next() { return next; } // Daj go next
Link<E> setNext(Link<E> nextval) // Setiraj go next
{ return next = nextval; }
E element() { return element; } // Daj go element
E setElement(E it) { return element = it; } // Setiraj go element
}
34
Линеарни податочни структури
List ADT - Имплементација со SLL (LList)

 Имплементација на листата дефинирана со интерфејсот List преку SLL класата LList. LList
мора да ги имплементира сите методи од преземениот интерфејс:

class LList<E> implements List<E>


{
private Link<E> head; // pointerite
private Link<E> tail;
protected Link<E> curr;
int cnt; // dolzinata

// Konstruktori
LList(int size) { this(); }
LList()
{
curr = tail = head = new Link<E>(null); // prazna lista vodac
cnt = 0;
}

...

35
Линеарни податочни структури
List ADT - Имплементација со SLL (LList)

 Имплементација на листата дефинирана со интерфејсот List преку SLL класата LList. LList
мора да ги имплементира сите методи од преземениот интерфејс:
public void clear()
{
head.setNext(null);
curr = tail = head = new Link<E>(null);
cnt = 0;
}
public void moveToStart() { curr = head; }
public void moveToEnd() { curr = tail; }
public int length() { return cnt; }
public void next()
{
if (curr != tail)
curr = curr.next();
}
public E getValue()
{
assert curr.next() != null : "Nema elementi";
return curr.next().element(); // curr pokazuva na prethodnikot
} 36
Линеарни податочни структури
List ADT - Имплементација со SLL (LList)

 Имплементација на листата дефинирана со интерфејсот List преку SLL класата LList. LList
мора да ги имплементира сите методи од преземениот интерфејс:
// Insert "it" na tekovnata pozicija (12)
public void insert(E it)
{
curr.setNext(new Link<E>(it, curr.next())); // stavi go po curr
if (tail == curr)
tail = curr.next(); // ako bil tail pomesti go tail po nego
cnt++;
}
// Append "it" na krajnata pozicija (tail)
public void append(E it)
{
tail = tail.setNext(new Link<E>(it, null));
cnt++;
}

37
Линеарни податочни структури
List ADT - Имплементација со SLL (LList)

 Имплементација на листата дефинирана со интерфејсот List преку SLL класата LList. LList
мора да ги имплементира сите методи од преземениот интерфејс:

// Remove na "it" i negovo prosleduvanje


public E remove()
{
if (curr.next() == null) // da ne e tail?
return null;
E it = curr.next().element();
if (tail == curr.next()) // da ne pokazuva na tail?
tail = curr;
curr.setNext(curr.next().next());
cnt--;
return it;
}

38
Линеарни податочни структури
List ADT - Имплементација со SLL (LList)

 Имплементација на листата дефинирана со интерфејсот List преку SLL класата LList. LList
мора да ги имплементира сите методи од преземениот интерфејс:

// Pomesti go curr nalevo, osven ako ne e na pocetokot (head)


public void prev()
{
if (curr == head)
return; // Nema prethoden element
Link<E> temp = head;
// Setaj po listata do curr (prethodnikot)
while (temp.next() != curr)
temp = temp.next();
curr = temp;
}

// Pomesti go curr nadesno, osven ako ne e na krajot (tail)


public void next()
{
if (curr != tail)
curr = curr.next();
} 39
Линеарни податочни структури
List ADT - Имплементација со SLL (LList)

 Имплементација на листата дефинирана со интерфејсот List преку SLL класата LList. LList
мора да ги имплементира сите методи од преземениот интерфејс:

// Daj ja tekovnata pozicija


public int currPos()
{
Link<E> temp = head;
int i;
for (i=0; curr != temp; i++)
temp = temp.next(); // mora da se prebrojat site pred curr
return i;
}

// Odi na odredena pozicija


public void moveToPos(int pos)
{
assert (pos>=0) && (pos<=cnt) : "Nevozmozna pozicija";
curr = head;
for(int i=0; i<pos; i++)
curr = curr.next(); // mora da se izminat site pos
} 40
Линеарни податочни структури
Сортираност на SLL

 Кај сортираните листи јазлите се подредени најчесто по растечки (опаѓачки) редослед


согласно типот на податокот (објектот).

 Одржување на сортираноста на листата имплицира пребарување на точната локација пред


секое вметнување. Значи, единствена insertion метода!

 Останатите операции се генерално исти.

 Пребарувањето останува линеарно (O(n)) поради неможноста да се скока на одредена


позиција директно - мора да се дојде до неа со изминување на ланец од јазли. Кај подредените
низи поради директното индексирање (O(1)) можно е бинарното пребарување (O(lg n))!

41
Линеарни податочни структури
Примена на SLL - Freelists

 Операторот new за мемориска алокација и механизмот за garbage-collection во Java се


релативно скапи за употреба. Системските повици за алокација на објекти од разни димензии,
па нивното непредвидливо ослободување ја фрагментира достапната меморија и многу брзо
ги прави обете операции временски неефикасни во споредба со контролирани шеми за
управување со меморијата!
 Наместо да се ризикува ефикасноста на апликациите со таа непредвидливост, можно е да се
имплементира ефикасен механизам за чување на ослободените јазли од листите во посебна
free-листа. При секоја потреба од нов јазол пред да се повика системскиот new прво може да
се провери дали веќе постои таков во free-листата.
 Freelist механизмот гарантира ефикасност кај алгоритмите кои фреквентно ги растат и
смалуваат листите - се што треба да се направи е да не се дереференцираат избришаните
објекти, за да се избегне garbage-колекторот и да се линкуваат во free-листа!
 Freelist механизмот е исто така корисен кај алгоритмите кои користат симултано повеќе
истородни листи (со исти објекти), кои може по ослободувањето да се чуваат во заедничка
листа.
 Се работи за едноставни имплементации со најчесто само две методи - get и release, обете
со ефикасност O(1)! Free-листата е обична SLL со вметнувања на почетокот (head).

42
Линеарни податочни структури
Примена на SLL - Ретки вектори и матрици

 Иако базични како структури, листите може да се комбинираат на многу нетривијални


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

 Голема матрица со димензии m n (од редна величина 10.000+) побарува O(mn) мемориски
локации ( прецизноста во бајти)! При типичен број ненулти елементи O(m+n) едвај 0.01% од
локациите би биле искористени. Мултилистата се реализира со креирање листи за сите m
редови и n колони, и со нивно преклопување (вкрстување).
 Јазлите на мултилистата ги содржат ненултите вредности, индексите (координатите) и
поинтери за меѓуповрзувањето!
43
Линеарни податочни структури
Примена на SLL - Претставување на полиноми

 Ефикасна (мемориски компактна) просторна репрезентација:

P ( x)  3x15  8 x 7  2 x  6

 Процесирањето има максимална ефикасност доколку листата е сортирана (опаѓачки) според


експонентите!

 Каде? - Аналитички апликации кои генерираат решенија во облик на полином со однапред


непознат степен (трансфер функции, идентификација на системи, итн.). Полиномијалните
изрази се добиваат при извршување и потребно е систематично да се форматираат во
меморија за да може да се манипулираат и комбинираат.

44
Линеарни податочни структури
Кружна (circular) поврзана листа - CSLL

 Се добива со искористување на NULL линкот на ПОСЛЕДНИОТ јазол во SLL - редирекција кон


ПОЧЕТОКОТ на листата, односно кон ВОДАЧОТ!

head tail
 Кај кружната листа е овозможено циркуларно движење што дава поголема флексибилност
за некои апликации.

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


понекогаш се поставува и на последниот јазол!

 Операциите со CSLL се идентични како за SLL.

 Примена кај кружните FIFO редови!


45
Линеарни податочни структури
Двојно поврзана листа (Doubly Linked List - DLL)

 Јазлите на двојно поврзаната листа (DLL) имаат информационен дел и два линка (поинтери)
за поврзување со претходникот и следбеникот.

 Празната DLL има два неподаточни краеви (head и tail / trailer) заради избегнување на
специјалните случаи кај методите на имплементациите и со тоа олеснување на insert, append и
remove на елементи. Неподаточните краеви се нарекуваат и ЧУВАРИ (sentinels).
 Секој јазол освен ПРВИОТ и ПОСЛЕДНИОТ има и ПРЕТХОДНИК и СЛЕДБЕНИК.
 DLL поради можноста за двонасочно движење е поедноставна и преферирана за
имплементација во однос на SLL, иако има погабаритен код во методите и зафаќа повеќе
простор поради додатниот поинтер по јазол.
 Кај SLL имплементацијата поставувањето на curr на ПРЕТХОДНИКОТ на ТЕКОВНИОТ јазол
овозможува O(1) вметнување и бришење. Кај DLL поради двонасочното врзување тоа не е
потребно, но може да се задржи како принцип!
 Бројот на врски, т.е. типот на листата би требало да се апстрахира позади List интерфејсот!
46
Линеарни податочни структури
Операции со DLL

 Вметнувањето на јазол е едноставно и при постоење на head и tail чуварите идентично за


било која позиција - од првата до последната!

 Процедурата е следната (преку curr поинтер):


1. се генерира јазолот и неговото info поле: newinfo = data
2. неговиот поинтер кон следбеникот: newnext = currnext
3. неговиот поинтер кон претходникот: newprev = currnextprev
4. поинтерот на претходникот: currnext = new
5. поинтерот на следбеникот: newnextprev = new

 Комплексност O(1)!

47
Линеарни податочни структури
Операции со DLL

 Бришењето на јазол е едноставно и при постоење на head и tail чуварите идентично за


било која позиција - од првата до последната!

 Процедурата е следната (преку curr поинтер):


1. се релинкува следбеникот: currnextnext prev = curr
2. се релинкува претходникот: currnext = curr nextnext

 Комплексност O(1)!

48
Линеарни податочни структури
Операции со DLL - Заклучок

 Додатниот поинтер зафаќа додатна меморија O(n) но количината е незначителна.

 Поместувањето е директно возможно во двете насоки со комплексност O(1).

 Вметнување и пред и позади тековната позиција има сложеност O(1).

 Бришењето исто сложеност O(1).

 Пребарувањето останува O(n) бидејќи во отсуство на индекси мора да се изминува листата!

49
Линеарни податочни структури
Имплементација на DLL јазол (DLink)

 Објектот на DLink има еден податочен елемент и два поинтера кон соседните јазли:

class DLink<E>
{
private E element;
private DLink<E> next;
private DLink<E> prev;
// Konstruktori
DLink(E it, DLink<E> p, DLink<E> n)
{ element = it; prev = p; next = n; }
DLink(DLink<E> p, DLink<E> n)
{ prev = p; next = n; }
// Get i Set metodi
DLink<E> next() { return next; }
DLink<E> setNext(DLink<E> nextval) { return next = nextval; }
DLink<E> prev() { return prev; }
DLink<E> setPrev(DLink<E> prevval) { return prev = prevval; }
E element() { return element; }
E setElement(E it) { return element = it; }
} 50
Линеарни податочни структури
List ADT - Имплементација со DLL (DList)

 Insert методата е едноставна, целата работа ја завршува конструкторот на јазолот - генерира


јазол со за кој prev е curr, а next е дотогашниот next на curr! Потоа спрема новиот релинкуваат
соседите. Постоењето на чуварите гарантира дека нема специјални случаи!

/** Insert "it" at current position */


public void insert(E it)
{
curr.setNext(new DLink<E>(it, curr, curr.next()));
curr.next().next().setPrev(curr.next());
cnt++;
}
 Append е идентична, со тоа што акцијата се случува непосредно пред tail јазолот:
/** Append "it" to list */
public void append(E it)
{
tail.setPrev(new DLink<E>(it, tail.prev(), tail));
tail.prev().prev().setNext(tail.prev());
cnt++;
}
51
Линеарни податочни структури
List ADT - Имплементација со DLL (DList)

 Remove методата прво ја чита вредноста од јазолот за бришење. Потоа prev на дотогашниот
следбеник се пренасочува на curr (дотогашниот претходник), и next на curr се пренасочува на
новиот дотогашниот следбеник. Конечно, се ажурира бројот на јазли:

/** Remove and return current element */


public E remove()
{
if (curr.next() == tail)
return null; // Nema jazol po curr
E it = curr.next().element(); // Zapamti ja vrednosta
curr.next().next().setPrev(curr);
curr.setNext(curr.next().next()); // Izvadi go od lista
cnt--; // Jazol pomalce vo listata
return it; // Prosledi ja vrednosta
}

 Останатите методи од интерфејсот се скоро идентични.

52
Линеарни податочни структури
Кружна двојно поврзана листа - CDLL

 Се добива со искористување на prev и next линковите на соодветните чувари со што се


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

head tail

53
Линеарни податочни структури
Споредба на перформансите на линеарните структури

Операција Низа SLL DLL


Вметнување ПРЕД O(n) O(1) O(1)
Вметнување ПОСЛЕ O(n) O(1) O(1)
Бришење O(n) O(1), O(n) tail O(1)
Поместување НАПРЕД O(n) O(1) O(1)
Поместување НАЗАД O(n) O(n) O(1)
Пребарување O(n), O(lg n) sorted O(n) O(n)
Индексирање O(1) O(n) O(n)

54
Линеарни податочни структури
Примена
 Линеарните податочни структури наоѓаат како директна примена во ефикасно динамичко
сместување на податоците; така и индиректна - како поддршка на други структури:
• стекови (stacks, LIFO)
• редови (queues, FIFO)
• приоритетни редови (priority queues)
• речници (dictionaries)
• итн.
 НИЗИТЕ се обично првиот избор доколку обемот на податоци е релативно мал и однапред
предвидлив. Дигресија:
• доколку меморијата не е проблем, тогаш алоцирање со голема резерва
• ако е критична брзината на вметнување, тогаш несортирана низа
• ако е критично пребарувањето, тогаш сортирана низа со бинарно пребарување
• бришењето е секако бавно, а изминување (сериско процесирање) само кај сортирани
• динамичките имплементации (како Vector во Java) имаат застој при копирањето
 ПОВРЗАНИТЕ ЛИСТИ мора да се користат секогаш при непредвидлив обем на податоците или
при фреквентни вметнувања и бришења. Дигресија:
• вметнувањето е брзо кај несортирани листи
• бришењето и пребарувањето се бавни поради што се користат при релативно мал обем
на податоци (но бришењето е побрзо отколку кај низите)
• покомплицирани за имплементација од низите, но едноставни во однос на дрвата или
hash табелите 55
Линеарни податочни структури
Стекови (stacks) - LIFO

 Stack е контејнер со линеарна структура на листа кај која


елементите се вметнуваат во и вадат од еден ист крај - наречен top (врв)!

 Операции дефинирани за стекот се:


• push - вметни (на врвот)
• pop - извади (од врвот)
• top - види го елементот на врвот (позната и како peek)

 Стратегијата на функционирање на стекот се нарекува LIFO (last-in, first-out), што е многу


корисна постапка во решавањето на некои класи проблеми.

 Функционалните рестрикции го прават стекот помалку флексибилен од листите, но


истовремено го прават ЕФИКАСЕН за своите операции и лесен за имплементација.

 Може да се имплементира на разни начини согласно спецификата на конкретните потреби,


но генерално тоа се прави преку:
• низи
• поврзани листи
56
Линеарни податочни структури
Stack ADT

 Едноставен интерфејс за стек:

public interface Stack<E>


{
/** Reinicijalizacija na stekot */
public void clear();

/** Stavi go elementot "it" */


public void push(E it);

/** Izvadi go vrvniot element i prosledi ja negovata vrednost */


public E pop();

/** Procitaj go vrvniot element */


public E topValue();

/** Vrati ja dlabocinata na stekot */


public int length();
};

57
Линеарни податочни структури
Stack ADT - Имплементација со низа (AStack)

 Имплементацијата со низа повторно имплицира фиксна должина на стекот! Тековниот


поинтер е сега фиксиран на пристапот на стекот (top) и со својот индекс индиректно укажува
на неговата пополнетост (број на елементи внатре).
 AStack е всушност симплифицирана верзија на AList.
 Суштинска дизајнерска одлука е кој од двата можни краја на низата да биде избран за
пристап на стекот!
 Едната опција е првиот (нултиот) индекс на низата - Array[0]! Тоа значи дека сите push и pop
операции ќе вметнуваат и бришат на нултиот индекс, што е НЕЕФИКАСНО бидејќи обете
операции ќе треба да ги поместуваат сите останати елементи во низата во едниот или другиот
правец  O(n)! – top() секогаш има трошок O(1)!
 Другата опција е за пристап на стекот да се одбере последниот индекс - Array[n-1]! Сега и
push и pop оперираат со tail елементот со трошок  O(1)!
 Втора дилема е дали top да покажува на првиот слободен индекс или на последниот
пополнет. Во следниот пример одбрана е слободната позиција! Така, кај празен стек top[0]
(во спротивно би било top[-1]). PUSH прво става елемент па го инкрементира top, а POP прво
го декрементира top па го вади елементот покажан од top!

58
Линеарни податочни структури
Stack ADT - Имплементација со низа (AStack)

 Имплементација на стекот:

/** Array-based stack implementacija */


class AStack<E> implements Stack<E>
{
private static final int defaultSize = 10;
private int maxSize; // Max dlabocina na stekot
private int top; // Pointer kon top Object
private E [] listArray; // Nizata sto go akomodira fizicki
/** Konstruktori */
AStack() { this(defaultSize); }
// Alokacija kako niza
AStack(int size)
{
maxSize = size;
top = 0;
listArray = (E[])new Object[size]; // Kreiraj ja listArray
}
/** Reinicijaliziraj go stekot */
public void clear() { top = 0; }
59
Линеарни податочни структури
Stack ADT - Имплементација со низа (AStack)

 Имплементација на стекот:
/** Stavi go "it" vo stekot */
public void push(E it)
{
assert top != maxSize : "Stekot e poln!";
listArray[top++] = it;
}
/** Izvadi go vrvniot element */
public E pop()
{
assert top != 0 : "Stekot e prazen!";
return listArray[--top];
}
/** Procitaj go vrvniot element */
public E topValue()
{
assert top != 0 : "Stekot e prazen!";
return listArray[top-1];
}
/** Daj ja dlabocinata na stekot */
public int length() { return top; } 60
Линеарни податочни структури
Stack ADT - Имплементација со листа (LStack)

 Имплементацијата на стек со листа е едноставна - елементите се ставаат и вадат кај


почетокот (head) - јасно е дека кај tail трошокот на pop е O(n) заради изминувањето неопходно
при бришење на последниот елемент кај SLL!
top
head head

top
tail

 Водач јазол е непотребен бидејќи не постои специјални случај за празна листа (0 елементи).

 Единствен поинтер е top кај кого push и pop вметнуваат/вадат јазли  O(1)!

 PUSH прво го ажурира линкот на новиот елемент да покажува на врвот од стекот, а потоа го
ажурира top да покажува на новиот јазол!

 POP прво ја копира вредноста на врвниот елемент во temp променливата, додека ltemp го
чува линкот кон истиот. Конечно, top се ажурира спрема наредниот јазол од стекот. Вредноста
на извадениот елемент е проследена за процесирање, а самиот елемент може да се врзе за
free-листа за следниот push.
61
Линеарни податочни структури
Stack ADT - Имплементација со листа (LStack)

 Имплементација на стекот:

/** Linked stack implementacija */


class LStack<E> implements Stack<E>
{
private Link<E> top; // Pointer kon prviot element
private int size; // Tekovna dlabocina na stekot

/** Konstruktori */
public LStack() { top = null; size = 0; }
public LStack(int size) { top = null; size = 0; }

/** Reinicijalizacija na stekot */


public void clear() { top = null; size = 0; }

/** Stavi go elementot "it" */


public void push(E it)
{
top = new Link<E>(it, top);
size++;
} 62
Линеарни податочни структури
Stack ADT - Имплементација со листа (LStack)

 Имплементација на стекот:
/** Izvadi go vrvniot element */
public E pop()
{
assert top != null : "Stekot e prazen!";
E it = top.element();
top = top.next();
size--;
return it;
}

/** Procitaj go vrvniot element */


public E topValue()
{
assert top != null : "Stack is empty";
return top.element();
}

/** Daj ja dlabocinata na stekot */


public int length() { return size; }
} 63
Линеарни податочни структури
Stack ADT - Компаративна дискусија

 Сите операции во обете имплементации на стекот имаат константна временска ефикасност


O(1).

 Просторната ефикасност е идентична на респективните имплементации на поврзаните


листи - (1) стекот со низа мора однапред да си ја познава максималната можна длабочина со
што се губи на неискористената резерва; додека (2) стекот со листа со растењето ја зголемува
загубата (overhead) во линкови.

 Стекот имплементиран со низа може да го исцрпи капацитетот (OVERFLOW) и да предизвика


пад на системот. Стекот имплементиран со листа е имун на такво сценарио (нема лимит во
користење на расположивата меморија), но внесува временска латентност поради операциите
на креирање и поврзување односно бришење на јазлите.

 Multi-stack имплементациите може ефикасно да се реализираат преку низи. Нормално,


мора да се предвидат максималните побарувачки, како и да се синхронизираат стекови кои
работат во "против фаза" - кога едниот расте, другиот опаѓа!

64
Линеарни податочни структури
Примена на стекови

 Генерално, стековите се користат во апликации за процесирање на дрва-структури на


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

 Пример - ротација на редоследот на податоци


• Парсирање на стрингови заради word-reversing  буквите од зборот се сместуваат во
стек еден по еден и се вадат по обратен редослед.
• Парсирање на низи заради ротација (mirroring) на елементите по обратен редослед.
• Парсирање на серии податоци од датотека (mirroring)  сместување по обратен
редослед во друга датотека.

 Пример - балансирање на симболи


• Парсирање на source-code листинг за синтаксна проверка  компајлерот ги издвојува
заградите и сместува на стек за да ја утврди синтаксната конзистентност.
• Балансирање на таговите во HTML листинг.

65
Линеарни податочни структури
Примена на стекови

 Пример - имплементација на постфиксна (postfix) нотација


• Постфиксната нотација овозможува парсирање на аритметички изрази без употреба на
загради и без потреба од повеќекратно изминување на изразите (евалуација "од прва").
• Конверзија на нативната инфиксна (infix) нотација во постфиксна

 Пример - менаџирање на повиканите методи (функции) при извршување на програмот


• При повик на некоја метода за извршување за истата се формира stack-frame. Во него се
сместуваат адресата на која треба да продолжи извршувањето на кодот од каде е
генериран повикот, како и сите нејзини локални променливи (за да се заштитат
променливите на повикувачката рутина). Функцијата ги гледа само објектите во својот
stack-frame кои ги прифаќаат и проследените параметри. По враќање на функцијата од
стекот се елиминира нејзиниот frame.

 Пример - реализација на рекурзија


• Еквивалентна на претходниот пример - рекурзивниот развој на повици се до основниот
случај се поставуваат на стек. По решавање на основниот случај неговиот резултат е
најгоре, па рекурзивните повици се решаваат спрема иницијалниот.

66
Линеарни податочни структури
Примена на стекови

 Псевдокод за mirroring на датотека (важи и за стрингови и низи)

initialize stack
open InFile, OutFile
element data
while (!EOF)
data = read (InFile)
push (data)
close InFile
while (!emptyStack)
data = pop (stack)
write (OutFile, data)
close OutFile

67
Линеарни податочни структури
Примена на стекови

 Илустрација за балансирање на загради - парсерот ги сместува ОТВОРЕНИТЕ загради на стек


штом му наидат како карактери. Штом наиде ЗАТВОРЕНА заграда проверува дали на врвот на
стекот има соодветна отворена - ако ДА ја вади, ако НЕ генерира грешка!

a{b(c[d]e)f}

68
Линеарни податочни структури
Примена на стекови

 Парсирање на аритметички изрази - нотации:


• infix - нативна, операторот е помеѓу операндите  a+b
• prefix - полска, операторот е пред операндите  +ab
• postfix - инверзна полска, операторот е по операндите  ab+

postfix парсирање на изразот:

Рекурзивниот алгоритам би бил:


• додека доаѓа операнд се става на стек
• штом налета оператор се вадат горните операнди колку што се потребни за операцијата
• резултатот се враќа на стек

69
http://csis.pace.edu/~wolf/CS122/infix-postfix.htm

70
Линеарни податочни структури
Редови (queues) - FIFO

 Queue е контејнер со линеарна структура на листа кај која


елементите се вметнуваат во едниот крај (back) и вадат од
другиот (front)!

 Операции дефинирани за стекот се:


• enqueue - вметни елемент (на back)
• dequeue - извади елемент (од front)

 Стратегијата на функционирање на редот се нарекува FIFO (first-in, first-out), што значи ги


процесира елементите по истиот редослед по кои влегле.

 Може да се имплементира на разни начини согласно спецификата на конкретните потреби,


но генерално тоа се прави преку:
• низи
• поврзани листи

71
Линеарни податочни структури
Queue ADT

 Едноставен интерфејс за ред:

/** Queue ADT */


public interface Queue<E>
{
/** Reinicijalizacija na redot */
public void clear();

/** Vmetni element vo redot (kaj back) */


public void enqueue(E it);

/** Izvadi go naredniot element od redot (kaj front) */


public E dequeue();

/** Procitaj go naredniot element za vadenje */


public E frontValue();

/** Vrati ja dolzinata na redot */


public int length();
}
72
Линеарни податочни структури
Queue ADT - Имплементација со низа (AQueue)

 Имплементацијата со низа е малце незгодна од аспект на ефикасноста! Суштинската дилема


е изборот на влезот и излезот во редот.

 Ако се одбере back[0] и front[n-1], тогаш dequeue O(1), но enqueue O(n) поради
неопходното поместување налево!

 Ако се одбере back[n-1] и front[0], тогаш enqueue O(1), но dequeue O(n) поради
неопходното поместување налево!

 Поефикасно решение е да се дозволи n-те елементи да не мора да се секогаш на почетокот


на низата, туку да шетаат со задржана секвенцијалност  enqueue/dequeue O(1)!

 Но, се јавува голем проблем - front/rear континуирано се движат кон крајот на низата и во
еден момент редот ќе пријави дека е преполн иако всушност не е - drifting queue!

73
Линеарни податочни структури
Queue ADT - Имплементација со низа (AQueue)
 Решение се циркуларните редови!
Низата има споени краеви AQ[n-1]AQ[0].

 Сега front и rear циркулираат.


Индексирањето се реализира со mod операцијата
(%) и така се обезбедува континуитет во кружењето.
Имено при вкупен капацитет size (позиции 0 до size-1), позициите се индексираат
преку нивниот остаток од делењето со size! После (size-1) доаѓа 0 (size % size).

 Останува проблемот на детекција дали редот е ПРАЗЕН или ПОЛН!


• Имено, што кога front и rear покажуваат на ист индекс? - Според наведената шема тоа
значи дека редот има 1 елемент.
• Следствено, за празен ред би важело rear = front - 1!
• Имајќи предвид дека поради кружната поврзаност size-1 е за 1 помал од 0, истиот
заклучок би се донел и ако front го достигне rear при полнење.
• Значи само со front и rear не може да се разликува ПРАЗЕН од ПОЛН ред!?

 Проблемот се решава со задолжителна празна пoзиција помеѓу нив или со пропратен


бројач! 74
Линеарни податочни структури
Queue ADT - Имплементација со низа (AQueue)

 Имплементација на редот:

/** Array-based queue implementacija */


class AQueue<E> implements Queue<E>
{
private static final int defaultSize = 10;
private int maxSize; // Maximum dolzina na redot
private int front; // Index na front
private int rear; // Index na rear
private E[] listArray; // Nizata koja go akomodira redot

/** Konstruktori */
AQueue() { this(defaultSize); }
AQueue(int size)
{
maxSize = size+1; // Extra pole za POLN/PRAZEN detekcija
rear = 0; front = 1;
listArray = (E[])new Object[maxSize]; // Kreiraj go listArray
}

75
Линеарни податочни структури
Queue ADT - Имплементација со низа (AQueue)

 Имплементација на редот:

/** Reinicijalizacija na redot */


public void clear()
{ rear = 0; front = 1; }

/** Stavi go "it" vo redot */


public void enqueue(E it)
{
assert ((rear+2) % maxSize) != front : "Queue is full";
rear = (rear+1) % maxSize; // Rear inkrementira pozicija
listArray[rear] = it;
}

/** Izvadi go naredniot element */


public E dequeue()
{
assert length() != 0 : "Queue is empty";
E it = listArray[front];
front = (front+1) % maxSize; // Front inkrementira pozicija
return it; 76
}
Линеарни податочни структури
Queue ADT - Имплементација со низа (AQueue)

 Имплементација на редот:

/** Procitaj ja vrednosta na naredniot za vadenje */


public E frontValue()
{
assert length() != 0 : "Queue is empty";
return listArray[front];
}

/** Vrati ja dolzinata na redot */


public int length()
{ return ((rear+maxSize) - front + 1) % maxSize; }
}

77
Линеарни податочни структури
Queue ADT - Имплементација со листа (LQueue)

 Имплементацијата на ред со листа е едноставна!

 Поинтерите front и rear покажуваат на респективните терминални јазли на листата.

 Кој крај да биде влез, а кој излез? - Од анализата на листите следи дека втората варијанта е
максимално ефикасна  O(1)!

head head
back front tail
tail
front back

 За избегнување на специјални случаи кога редот е празен добро е да се употреби водач на


листата (header). При иницијализација на празна листа обата поинтера покажуваат на водачот!

 За оваа реализација одбрано е enqueue да става јазол на tail од листата (rear), а dequeue да
вади јазол кај header-от (front).

78
Линеарни податочни структури
Queue ADT - Имплементација со листа (LQueue)

 Имплементација на редот:

/** Linked queue implementacija */


class LQueue<E> implements Queue<E>
{
private Link<E> front; // Pointer kon front
private Link<E> rear; // Pointer kon rear
private int size; // Zafatenost (dolzina) na redot

/** Konstruktori */
public LQueue() { init(); }
public LQueue(int size) { init(); } // Ignore size

/** Incijalizacija na redot */


private void init()
{
front = rear = new Link<E>(null);
size = 0;
}

79
Линеарни податочни структури
Queue ADT - Имплементација со листа (LQueue)

 Имплементација на редот:

/** Reinicijalizacija na redot */


public void clear() { init(); }

/** Vnesi element kaj rear */


public void enqueue(E it)
{
rear.setNext(new Link<E>(it, null));
rear = rear.next();
size++;
}

/** Vrati ja dolzinata na redot */


public int length() { return size; }

80
Линеарни податочни структури
Queue ADT - Имплементација со листа (LQueue)

 Имплементација на редот:

/** Izvadi go sledniot element kaj front */


public E dequeue()
{
assert size != 0 : "Redot e prazen!";
E it = front.next().element(); // Predaj ja vrednosta
front.setNext(front.next().next()); // Odi na sledniot
if (front.next() == null) rear = front;// Posleden vo redot
size--;
return it; // Vrati ja vrednosta na izvadeniot
}

/** Procitaj go naredniot za vadenje */


public E frontValue()
{
assert size != 0 : "Redot e prazen!";
return front.next().element();
}
}
81
Линеарни податочни структури
Двостран ред - DeQueue ("Deck")

 Имплементација на ред каде обете операции на вметнување/вадење на елементите се


можни на обата краја!

 Четирите фундаментални методи би биле:


• insertLeft()
• removeLeft()
• insertRight()
• removeRight()

 Со комбинирање на овие методи deck-от може да функционира и како стек и како ред!

 Сите операции над deck-от имаат временска сложеност O(1)!

82
Линеарни податочни структури
Приоритетен ред - Priority Queue

 Имплементација на ред каде елементите се вадат според својот приоритет (најчесто


растечка или опаѓачка вредност).

 Јасно е дека за да може да се реализира FIFO стратегијата на вадење, елементите мора да се


сортирани според приоритетот, што ја поскапува операцијата на вметнување  O(n):

enqueue dequeue

 Единствен "проблем" е вметнувањето, односно неговото подобрување. Тоа е можно со


имплементирање на приоритетниот ред со друга податочна структура  Heap!

83
Линеарни податочни структури
Примена на редови

 Редовите вообичаено се користат кај оперативните системи и мрежите да чуваат ред на


чекање на процесите за употреба на некаков ресурс:
• print spooler - OS имплементација за употреба на принтер, кога нема приоритет, туку
задачите се извршуваат по редоследот по кој дошле
• disk driver - OS имплементација за сервисирање на апликативните барања за пристап до
физички диск во компјутерот
• file server - мрежна имплементација кога сите корисници во мрежата имаат поеднакво
право за користење на податочни ресурси, па нивните барања се сервисираат по FIFO
• шалтерско/киоск распределување на клиенти во банка или студенти кои чекаат
слободен терминал, ...

 Кај приоритетните редови - распределување на пристапите до ресурсите на активните


процеси.

 Цела теоретска математичка дисциплина (queuing theory) се бави со пресметувачки,


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

84
Линеарни податочни структури
Queue ADT - Компаративна дискусија

 Доколку е однапред познат обемот на податоци најбрзи имплементации се со низи!


 Доколку податоците се непредвидливи, тогаш најпаметен избор се имплементации со листи!
 Сите stack/queue се евтини во смисла на време на извршување (O(1)).
 Ако е потребен приоритетен ред, тогаш можен проблем е времето на подредување при
секое вметнување - во кој случај може да се имплементира heap структура!

85

You might also like