Алгоритми/Рандомизација

Пошто детерминистички алгоритми долазе до својих крајњих граница приликом решавања тешких проблема, корисна техника за убрзање обраде је рандомизација. У рандомизованим алгоритмима, алгоритам има приступ извору случаја (random source) који може да буде замишљен као бацање новчића током извршења. У зависности од резултата бацања, алгоритам може да раздвоји путању обраде.
Постоје два основна типа рандомизованих алгоритама: Лас Вегас и Монте Карло. Код Лас Вегас алгоритама случај се користи за убрзање извршења, али алгоритам увек мора да да исправан резултат за задате улазе. Монте Карло алгоритми немају претходно ограничење, што значи да они могу да дају и погрешан резултат. Ипак, давање ''погрешног'' резултата мора да буде ''мало вероватно'', јер би у супротном Монте Карло алгоритми били потпуно бескорисни.
Многи апроксимациони алгоритми користе рандомизацију.
1 Статистика редова
Пре но што покријемо технике рандомизације, почећемо са детерминистичким проблемом који ће нас довести до проблема који користи рандомизацију. Претпоставимо да имамо несортирани вектор вредности и да желимо на нађемо:
* максималну вредност,
* минималну вредност и
* средњу вредност.
Да поменем бесмртне речи једног од наших бивших професора "Како би то урадили?"
1.1 find-max
Прво, релативно је просто наћи највећи елемент:
''// find-max -- враћа највећи елемент''
function find-max(array ''vals''[1..''n'']): element
let ''result'' := ''vals[1]''
for ''i'' from ''2'' to ''n'':
''result'' := max(''result'', ''vals[i]'')
repeat
return ''result''
end
Иницијализација ''result'' на <math>-\infty</math> би такође била прихватљива, али бескорисна, пошто би први позив функције max као резлтат дао први елемент вектора. Иницијализација променљиве ''result'' као у горњем примеру, омогућава извршење само ''n-1'' поређења. (Штавише, у језицима који пружају могућност мета-програмирања, тип податка не мора обавезно да буде нумерички и тада не постоји добар начин да се престави ; коришћење vals[1] је безбедно у односу на тип елемената.)
Слична рутина за проналажење најмањег елемента може да се направи позивом функције min уместо max.
1.2 find-min-max
Предпоставимо да хоћемо истовремено да нађемо и min и max. Ево једног решења:
''// find-min-max -- враћа најмањи и највећи елемент у датом низу''
function find-min-max(array ''vals''): pair
return pair {find-min(''vals''), find-max(''vals'')}
end
Пошто и find-max и find-min праве по n-1 позива функције max односно min (када vals садржи n елемената) укупан број поређења функције find-min-max је <math>2n-2</math>.
Међутим, овде се извршавају и нека редундантна поређења. Ове редундасе могу да се уклоне заједничким „ткањем“ функција min и max:
''// find-min-max -- враћа најмањи и највећи елемент у датом низу''
function find-min-max(array ''vals''[1..''n'']): pair
let ''min'' := <math>\infty</math>
let ''max'' := <math>-\infty</math>
if ''n'' is odd:
''min'' := ''max'' := ''vals''[1]
''vals'' := ''vals''[2,..,''n''] ''// сада можемо претоставити а је n паран број''''n'' := ''n'' - 1
fi
for ''i'':=1 to ''n'' by 2: ''// узети у обзир вредности парова у vals''
if ''vals''[''i''] < ''vals''[''i'' + 1]:
let ''a'' := ''vals''[''i'']
let ''b'' := ''vals''[''i'' + 1]
else:
let ''a'' := ''vals''[''i'' + 1]
let ''b'' := ''vals''[''i''] ''// инваријанте: a <= b''
fi
if ''a'' < ''min'': ''min'' := ''a'' fi
if ''b'' > ''max'': ''max'' := ''b'' fi
repeat
return pair {''min'', ''max''}
end
Овде се кроз петљу пролази <math>n/2</math> пута уместо ''n'' пута, али је за сваки пролаз потребно извршити три поређења. Тако је број поређења који треба да се изведе <math>(3/2)n = 1.5n</math>, што доводи до убрзања за <math>3/4</math> у односу на оригинални алгоритам.
Потребно је извршити само три поређења уместо четири, зато што је према конструкцији алоритма, увек <math>a\le b</math>. (У првом делу if-а, добро знамо да је <math>a < b</math>, али у else делу можемо само да закључимо да је <math>a\le b</math>.) Ово својство је искоришћено тако што нема више потребе да се ''a'' пореди са текућим максимумом, зато што је b већ веће или једнако ''a'' и исто тако нема потребе за поређењем ''b'' са текућим минимумом, зато што је ''a'' већ мање или једнако ''b''.
У софтверској струци постоји борба између коришћења библиотека и писања посебних (специјалних) алгоритама који су максимално прилагођени за решавање конкретног проблема. У нашем случају библиотечке функције min и max нису коришћене за добијање брже рутине find-min-max. Коришћење ових функција, вероватно не би представљало уско грло у реланом животу, међутим, ако се тестирањем открије да рутина треба да буде бржа, онда би требало да се примени приступ који је и овде примењен. Решења која користе библиотеке су углавном боља од специјалних решења. Технике као што су отворене имплементације и аспектно-оријентисано програмирање могу да помогну у разрешењу наведене дилеме, коришћењем оног најбољег из оба приступа. Без обзира на све, важно је да се разлика између коришћења стандардних библиотечких функција и писања специјалног кода јасно препозна.
1.3 find-median
Коначно, треба да размотримо како да нађемо средњу вредност. Један од могућих приступа је да сортирамо дати вектор и онда средњу вредност издвојимо из ''vals''[''n''/2]:
''// find-median -- враћа средњу вредност елемената у vals''
function find-median(array ''vals''[1..''n'']): element
assert (''n'' > 0)
sort(''vals'')
return ''vals''[''n'' / 2]
end
Ако наши бројеви нису блиски по вредности или не могу да се сортирају позиционим сортирањем (radix sort) горњи алгоритам ће захтевати <math>O(n\log n)</math> корака.
Међутим, могуће је добити статистику ''n''-тог реда у <math>O(n)</math> времена. Кључно је да се избаци сортирање: није нам, заправо, потребно да цео вектор буде сортран да би нашли средњу вредност, и зато постоје одређени губици када се прво сортира цео вектор. Техника коју ћемо користити да би ово постигли је случајност.
Пре ного што прикажемо функцију find-median без сортирања, увешћемо операцију у стилу подели и владај, познату као партиционисање. Оно што желимо је рутина која налази случајни елемент вектора и онда партиционише (дели) вектор на три дела:
# елементи који су мањи или једнаки случајном елементу;
# елементи који једнаки случајном елементу; и
# елементи који су већи или једнаки случајном елементу.
Ова три дела су означена целобројним променљивим: ''j'' и ''i''. Партиционисање се изведи "на месту", у вектору:
''// partition -- разбија низ у три партиције од случајног елемента''
function partition(array ''vals''): pair{''j'', ''i''}
Приметимо да у случају да се изабрани случајни елемент појављује у вектору три или више пута, могуће је да се вредности овог елемента налазе у све три партиције вектора. Иако ова операција не делује превише корисно, она има снажну особину која може да се искористи: када се операција партиционисања заврши, случајно изабрани елемент ће се налазити на истом месту у вектору, на ком би се налазио да је извршено потпуно сортирање вектора!
Ова особина можда не делује тако снажно, али сетите се оптимизације функције find-min-max: тада смо приметили да узимањем елемената из вектора у паровима и прво њиховим међусобним поређењем, можемо смањити укупан, потребан број поређења (зато што текуће вредности min и max требају да се пореде са само једном вредношћу, а не са две). Сличан концепт је примењен и овде.
Док код за partition и није нека магија, он има неке сложене граничне случајеве:
''// partition -- разбија низ у три партиције од случајног елемента''
function partition(array ''vals''): pair{''j'', ''i''}
let ''m'' := 0
let ''n'' := ''vals''.length - 2 // за низ vals, vals[vals.length-1] је последњи елемент, који носи партицију,
// тако да је последњи сортирани елемент vals[vals.length-2]
let ''irand'' := random(''m'', ''n'') ''// враћа било коју вредност од m до n''
let ''x'' := ''vals''[''irand'']
swap( ''irand'',''n''+ 1 ) // n+1 = vals.length-1 , који је најдешњији елемент и представља складиште за партиционе елемента и стражара за m
// вредности у 'vals''[''n''..] су веће од ''x''
// вредности у ''vals''[0..''m''] су мање од ''x''
while (m <= n ) // погледати објашњење у брзом сортирању зашто је m <= n уместо да је m < n у случају 2 елемента,
// vals.length -2 = 0 = n = m, али ако је случај 2-елемента неуређен у односу на уређен, онда мора постојати друга акција.
// имплицирањем, друга акција се јавља заједно са петљом, тако треба да процесира m = n случај.
while ''vals''[''m''] <= ''x'' // у случају 2-елемента, други елемент је партиција, а први елемент је у m. Ако је уређен, m ће се инкрементирати
''m''++
endwhile
while ''x'' < ''vals''[''n''] && ''n'' > 0 // стаје ако vals[n] припада левој партицији или погоди почетак низа
''n''--
endwhile
if ( m >= n) break;
swap(''m'',''n'') // замени''vals''[''n''] и ''vals''[''m'']
''m''++ // не освежавај замењене елементе
''n''--
endwhile
// партиција: [0..''m''-1] [] [''n''+1..] приметити да ''m''=''n''+1
// ако је потребан непразан низ:
swap(''m'',''vals''.length - 1) // ставити пратиционирани елемент између леве и десне партиције
// у случају 2-елемента који су неуређени, m=0 (није инкрементиран у петљи), и први и последњи (други) елементи ће размени места.
// партиција: [0..''n''-1] [''n''..''n''] [''n''+1..]
end
Операцију partition можемо да користимо и као подрутину за општу операцију find:
''// find -- помера елементе у vals тако да локација k садржи вредности када буду сортирани''
function find(array ''vals'', integer ''k'')
assert (0 <= ''k'' < ''vals''.length) ''// k мора бити валидан индекс''
if ''vals''.length <= 1:
return
fi
let pair (''j'', ''i'') := partition(''vals'')
if ''k'' <= ''i'':
find(''a''[0,..,''i''], ''k'')
else-if ''j'' <= ''k'':
find(''a''[''j'',..,''n''], ''k'' - ''j'')
fi
end
Што нас доводи до поенте:
''// find-median -- враћа средњу вредност у vals''
function find-median(array ''vals''): element
assert (''vals''.length > 0)
let ''median_index'' := ''vals''.length / 2;
find(''vals'', ''median_index'')
return ''vals''[''median_index'']
end
Можда вам је кроз главу прошла следећа мисао „а да ли је неопходан случајни позив?“. На пример, уместо да узмемо случајну тачку ослонца, увек можемо да уместо ње изаберемо средњи елемент. Узимајући у обзир да наш алгоритам ради са свим могућим векторима, можемо да закључимо да ће његово време извршења за све могуће улазе у просеку бити исто као и приликом коришћења рандомизоване функције. Овде је резон да је узимајући у обзир скуп свих могућих вектора, средњи елемент исто тако „случајан“ као и било који други. Али у оваквом размишљању постоји замка: типично, улаз у неки алгоритам није уопште случајан. На пример, велика је вероватноћа да улазни вектор већ буде сортиран и то не случајно. Исто тако, пошто се ради о реалним подацима из реалних програма, подаци могу да дођу у најразличитијим облицима који могу да доведу до подоптималних резултата. Рачено на другачији начин: за рандомизовани алгорита за налажење средње вредности, постоји мала вероватноћа да ће његово извршење бити подоптимално, независно од тога какав је улаз, док за детерминистички алгоритам који узима средњи елемент, постоји већа шанса да ће имати лоше перформансе са неким од честих облика улаза. То нас доводи до следеће препоруке:
Препорука о рандомизацији:
Ако ваш алгоритам зависи од случајности, обавезно уведите случајност сами, уместо да завистите од случајних података.
Треба приметити да постоје технике „дерандомизације“, које могу да претворе алгоритам који је брз за просечне случајеве у потпуно детерминистички алгоритам. Понекад је допунски трошак дерандомизације толико велик, да су потребни веома велики скупови података да би се добили било какви добици. Упркос томе, дерандомизација сама по себи има теоријску вредност.
Рандомизовани алгоритам find је изумео британски научник Сер Чарлс Ентони Ричард Хор. Иако је Хор важна фигура у рачунарској науци, оно по чему је највише познат у ширим круговима је алгоритам за брзо сортирање (quicksort algorithm) о коме ћемо расправљати у следећем одељку.
2 Брзо сортирање
Алгоритам за проналажење средње вредности партиционисањем, који је описан у претходном одељку, је, уствари, врло близу имплементације алгоритма сортирања у пуном опсегу. Изградњу алгоритма за брзо сотирање остављамо читаоцу да уради као вежбу и препоручујемо да то учини пре читања следећег одељка (брзо сортирање је „брутално“ у односу на сортирање спајањем које не користи рандомизацију).
Кључни део алгоритма брзог сортирања је избор праве средње вредности. Али да би га брзо направили, почнимо са претпоставком да вектор није сортиран и да је вероватноћа да крајњи десни елемент било ког вектора садржи средњу вредност, иста као и за све остале елементе и да смо потпуно оптимистични у погледу тога да се не дешава да крајње десни елемент има највећи кључ, што би значило да би уклањали само један елемент (елемент партиције) у сваком кораку и не би имали десни вектор за сортирање, већ само леви дужине n-1.
Управо због овога је ''рандомизација'' важна за брзо сортитрање, т.ј. ''избор оптималнијег кључа партиције'' је прилично важно за ефикасан рад брзог сотирања.
Упоредите број поређења неопходних за брзо сортирање у односу на сортирање уметањем.
Код сортирања уметањем, просечан број поређења потребних за проналажење најмањег првог елемента у узлазном сортирању рандомизованог вектора је n /2 .
Просечан број поређења за други елемент је (n-1)/2;
за трећи елемент ( n- 2) / 2.
Укупан број поређења је [ n + (n - 1) + (n - 2) + (n - 3) .. + (n - [n-1]) ] подељено са 2, што је [ n x n - (n-1)! ] /2, или око O(n на квадрат).
У брзом сортирању, број поређења ће бити преполовљен на сваком кораку поделе ако је изабрана истинска средња вредност, пошто партиција леве половине не треба да се пореди са десном, али у сваком кораку, број елемената свих партиција направљених на претходном нивоу партиционисања ће опет бити n.
Број нивоа поређења n елемената је број корака дељења n са два, док не буде n = 1. Или уназад, 2 ^ m ~ n, so m = log2(n) n.
Тако је укупан број поређења n (елемената) x m (нивоа скенирања) или n x log2n,
Следи да је број поређења O(n x log 2(n) ), што је мање него код сортирања убацивањем где је тај број O(n^2) или O( n x n ).
(Поређењем O(n x log 2(n)) са O( n x n ), заједнички фактор n може да се елиминише и пореди се log2(n) у односу на n, што чини експоненцијалну разлику како n расте, на пример упоредите n = 2^16 , или 16 са 32768, или 32 у односу 4 гига.)
Да би се имплементирало партиционисање „на месту“ на делу вектора који је одређен претходним рекурзивним позивом, потребно је скенирање са оба краја партиције, замена локација кад год је вредност текуће локације левог скенирања већа од врендости партиције и увек када је вредност текуће локације десног скенирања мања од вредности партиције. Тако је почетни корак:
Доделити вредност партиције крајњем десном елементу, заменити их ако то је потребно.
Корак партиционисања је:
инкрементирати леви показивач, док тренутна вредност није мања од вредности партиције.
декрементирати десни показивач, док тренутна вредност не буде већа од вредности партиције,
или док није једнака или већа од крајњој левој локацији.
изаћи из петље уколико су се показиваћи сусрели( l >= r),
ИНАЧЕ
заменити места елементима на којима леви и десни показивачи показују,
на вредностима где је вредност левог показивача већа од партиције,
и где је вредност десног показивача мања од партиције.
КОначно, након што изађемо из петље, јер су се леви и десни показивач сусрели,
''заменити места ''крајњој десној'' вредности партиције,
са последњом локацијом левог показивача који иде унапред '',
и тако се завршава између леве и десне партиције.
Уверите се у овом тренутку да су после последње замене случајеви два елемента у уређеном и два елемента у неуређеном вектору исправно обрађени, што би требало да значи да су сви случајево коректно обрађени. Ово је добар корак за отклањање евентуалних грешака у алгоритму.
За случај два уређена елемента, леви показивач се зауставља на партицији или на другом елементу пошто је пронађена вредност партиције. Десни показивач, скенирајући уназад, почиње од првог елемента пре партиције и зауставља се пошто је у крајње левој позицији.
Показивачи се укрштају и излази се из петље пре него што се направи замена у петљи. Ван петље, садржаји левог показивача на крајње десној позицији и показивача партиције, такође на крајње десној позицији, се замењују, чиме се постиже да нема промене у случају два уређена елемента.
За случај два неуређена елемента, леви показивач се зауставља на првом елементу, пошто је већи од вредности партиције (вредност добијена левим скенирањем зауставља замену вредности које су веће од вредности партиције).
Десни показивач креће и зауставља се на првом елементу, пошто је достигао крајње леви елемент.
Из петље се излази зато што су леви и десни показивачи једнаки и показују на прву позицију и садржај на који показује леви показивач (прва позиција) се замењује са вредношћу партиције на крајње десној позицији. Тиме претходно неуређени елементи постају уређени.
Још једно питање имплементације је, како померати показиваче за време скенирања. Померање на крају спољне петље делује логично.
partition(a,l,r) {
v = a[r];
i = l;
j = r -1;
while ( i <= j ) { // потребно је такође скенирати када осмотрити када је i = j као и када је i < j ,
// у 2 уређеном случају,
// тако да је i инкементиран партицији
// и ништа се не дешава у завршној замени места са партицијама на r.
while ( a[i] < v) ++i;
while ( v <= a[j] && j > 0 ) --j;
if ( i >= j) break;
swap(a,i,j);
++i; --j;
}
swap(a, i, r);
return i;
Са унарним операторима преинкрементирања / декрементирања, скенирање може да се уради непосредно пре тестирања услова while петље, али то значи да показивачи, на почетку, морају да се помере за -1 однодно +1 респективно, тако да сада алгоритам изгледа овако:
partition (a, l, r ) {
v=a[r]; // v је вредност партиције на позицији a[r]
i=l-1;
j=r;
while(true) {
while( a[++i] < v );
while( v <= a[--j] && j > l );
if (i >= j) break;
swap ( a, i, j);
}
swap (a,i,r);
return i;
}
и алгоритам за qsort је
qsort( a, l, r) {
if (l >= r) return ;
p = partition(a, l, r)
qsort(a , l, p-1)
qsort( a, p+1, r)
}
и коначно, рандомизација елемента партиције.
random_partition (a,l,r) {
p = random_int( r-l) + l;
// median of a[l], a[p] , a[r]
if (a[p] < a[l]) p =l;
if ( a[r]< a[p]) p = r;
swap(a, p, r);
}
Ово може бити позвано непосредно пре позива финкције partition у qsort().
3 Мешање (елемената) вектора
Ово одржава податак у током мешања
temporaryArray = { }
Ово This евидентира да ли је елемент измешан
usedItemArray = { }
Број елемената у низу
itemNum = 0
while ( itemNum != lengthOf( inputArray) ){
usedItemArray[ itemNum ] = false Ни један од елемената није био измешан
itemNum = itemNum + 1
}
itemNum = 0 искористићемо га опет
itemPosition = randdomNumber( 0 --- (lengthOf(inputArray) - 1 ))
while( itemNum != lengthOf( inputArray ) ){
while( usedItemArray[ itemPosition ] != false ){
itemPosition = randdomNumber( 0 --- (lengthOf(inputArray) - 1 ))
}
temporaryArray[ itemPosition ] = inputArray[ itemNum ]
itemNum = itemNum + 1
}
inputArray = temporaryArray
4 Хеш табеле
Хеширање се ослања на функцију hashcode која обезбеђује случајно и равномерно распоређивање кључева за расположиве слотове. У Јави се то ради прилично директним методом сабирања простог броја средње величине (31 * 17) са целобројним кључем по модулу величине хеш табеле. За кључеве типа низа симбола, почетна вредност хеш броја се добија сабирањем резултата множења редног броја сваког симбола са 31.
Вики књига Структуре података у поглављу Хеш табеле добро покрива ову тему.
5 Скип листе
Речник или мапа, је општи концепт у коме се вредност убацује под неким кључем и налази на основу тог кључа. На пример, у неким језицима концепт речника је уграђен у сам језик (Python) док је у другим он део основних библиотека (C++ стандардна библиотека шаблона и Јава стандардна библиотека колекција). Језици који реализују концет речника у оквиру библиотека, обично пружају могућност програмеру да изабере између хеш алгоритма и имплементације засноване на уравнотеженом бинарном стаблу (црвено-црна стабла). Недавно су понуђене скип листе (листе са прескакањем) које пружају велике предности приликом имплементација у оквиру апликација у конкурентном окружењу са више нити извршења.
Хешовање је техника која зависи од случајности кључева који се пропуштају кроз хеш функцију да би се нашла вредност која одговара индексу линеарне табеле.
Хешовање ради онолико брзо колико и хеш функција, али ради добро само ако су убачени кључеви ревномерно распоређени у вектору, као и било који кључеви који се хеширају на исти индекс. Мора да постоји договор око проблема хеш судара, на пример чување уланчане листе судара за сваки слот табеле и итерација кроз листу да би се упоредио пуни кључ за сваки пар кључ-вредност у односу на кључ претраживања.
Недостатак хешовања је у томе што симетрични (in-order) обилазак није могућ са оваквом структуром података.
Бинарна стабла могу да се користе за представљање речника, а симетрични обилазак бинарног стабла је могућ рекурзивном посетом чворова (посета левог детета, посета корену и посета десног детета). Бинарна стабла се лоше претражују када су „неуравнотежена“, нпр. кључеви парова кључ-вредност су уметнути по растућем или по опадајућем редоследу, тако да ефективно изгледају као уланчане листе без леве деце, само са десном децом. Самоуравнотежујућа (''self-balancing'') бинарна стабла могу да се направе пробабилистички (користећи случајност) или детерминистички (бојењем веза са децом у црвено или црно), преко операција ротације локалних стабала са три чвора. Ротација је једноставно замена чвора родитеља са чвором дететом, али уз очување поретка. На пример, за ротацију левог детета, његово десно дете постаје лево дете родитеља, а родитељ постаје његово десно дете.
Црвено-црна стабла могу лакше да се разумеју ако се испита одговарајуће 2-3-4 стабло. 2-3-4 стабло може да има 2-је, 3-је или 4-оро деце, с тим да чворови са 3-је деце имају 2 кључа између 3 детета и чварови са 4-оро деце (4-чворови) имају 3 кључа између 4 детета. 4-чворови могу да се поделе на 3 2-чвора са једним кључем и средњи 2-чвор додаје навише да би се спојио са чвором родитеља који, ако je био 2-чвор са једним кључем постаје 3-чвор са два кључа, или ако је био 3-чвор са два кључа постаје 4-чвор, који ће касније бити подељен (на путу навише). Чин поделе 4-чвора са три кључа је уствари операција поновног уравнотежавања која спречава да се низ од 3 чвора: пра-родитељ (деда), родитељ и дете, појављују без уравнотежујуће ротације. 2-3-4 стабла су ограничен пример B-стабала, који обично имају довољно чворова да се сместе у оквиру једног физичког блока на диску и да олакшају кеширање веома великих индекса који не могу да стану у оперативну меморију RAM (данас је то мање битно).
Црвено-црно стабло је бинарна представа 2-3-4 стабла, где су 3-чворови моделирани помоћу родитеља са једним црвеним дететом, а 4-чворови помоћу родитеља са 2 црвена детета. Цепање 4-чвора је представљено родитељем са 2 црвена детета и преобраћањем (flipping) црвене деце у црну, а самог себе у црвено. Не постоји случај у коме је родитељ већ црвен, зато што се извршавају операције уравнотежавања код којих, ако постоји пра-родитељ са црвеним дететом (родитељем) које има црвено дете, пра-родитељ се ротира тако да постаје дете родитеља (свог детета) и родитељ постаје црн, а пра-родитељ постаје црвен; ово обједињује претходни сценарио преобраћања (flipping) једног 4-чвора представљеног са 2 црвена детета. Заправо, можда ова стандардизација 4-чворова са обавезном ротацијом искривљених или цик-цак 4-чворова, доводи до поновног уравнотежења бинарног стабла.
Новија оптимизација је да се улево ротира било које десно црвено и лево црвено дете, тако да само десна ротација левих искривљења у оквиру 4-чворова (3 црвена чвора) може икада да је појави, чиме се поједностављајући код за поновно уравнотежавање.
Скип листе су моделоване на уснову једноструко уланчаних листа, осим што су чворови вишенивоски. Чворови вишег нивоа (високи чворови) су ређи, али операција уметања обезбеђује повезаност чворова на сваком нивоу.
Имплементација скип листа захтева стварање насумично високих вишенивоских чворова и затим њихово уметање.
Чворови се стварају итерацијом случајне функције, у којој се чворови вишег нивоа појављују у каснијим итерацијама и ређи су, зато што је итерацијама већ пређен велики број прагова случајности (на пример 0,5, ако је случајни број између 0 и 1).
Уметање захтева привремени вектор претходног чвора нивоа генерисаног чвора за уметање. Он се користи за чување последњег показивача за задати ниво, који има кључ мањи од кључа уметања.
Скенирање почиње од почетка скип листе, на највишем нивоу и наставља се док се не нађе чвор чији је кључ већи од кључа уметања и док се претходни показивач не сачува у привремени вектор претходног чвора. Тада се прелази на следећи, нижи, ниво који се скенира и тако даље, идући у цик-цак, док се не достигне најнижи ниво.
Уметање се врши на сваком нивоу привременог вектора претходног чвора, тако да наредни чвор претходног чвора на сваком нивоу постаје наредни чвор за уметнути чвор, а уметнути чвор постаје наредни за претходни чвор.
Претраживање подразумева итерације од највишег до најнижег нивоа почетног чвора и скенирање дуж листе на сваком нивоу док се не пронађе чвор чија је вредност већа од траженог кључа, померајући се наниже на следећи ниво и настављајући скенирање док се не пронађе чвор са већим кључем од траженог на најнижем нивоу, или се пронађе тражени кључ.
Стварање чворова случајне висине који су мање учестали када су на вишем нивоу и процес повезивања са другим чворовима на сваком нивоу, је оно што чини скип листу корисном структуром.
5.1 метода за имплементацију скип листе: имплементирајте једноструко уланчану листу, тестирајте, затим је трансформишите у скип листу, онда поново тестирајте и упоредите перформансе
Оно што следи је имплементација скип листи у језику Python. Прво је имплементирана једноструко уланчана листа која на следећи чвор увек гледа као на текући, а затим следи имплементација скип листе уз минималне модификације и, на крају, поређење које помаже да се разјасни имплементација.
#copyright SJT 2014, GNU
#почети имплементацијом једноструко поверане листе:
#главни чвор има показивач на почетак листе и тренутни чвор испитује следећи чвор.
#Ово је много лакше од тога да је главни чвор један од ускладиштених чворова.
class LN:
"a list node, so don't have to use dict objects as nodes"
def (self):
self.k=None
self.v = None
self.next = None
class single_list2:
def (self):
self.h = LN()
def insert(self, k, v):
prev = self.h
while not prev.next is None and k < prev.next.k :
prev = prev.next
n = LN()
n.k, n.v = k, v
n.next = prev.next
prev.next = n
def show(self):
prev = self.h
while not prev.next is None:
prev = prev.next
print prev.k, prev.v, ' '
def find (self,k):
prev = self.h
while not prev.next is None and k < prev.next.k:
prev = prev.next
if prev.next is None:
return None
return prev.next.k
#онда након тестирања једноструко-повезане листе, моделовати SkipList након тога.
# Главне услове које треба запамтити када трансформишете код једноструко-повезане листе у код скип листе:
# * чворови виших-нивоа су убачени
# * главни чворови морају бити висине чворова коју су убачени
# * креће се уназад по доњим нивоима, од највишег до најнижег, када се врши убацивање или претрага,
# ово је основа ефикасности алгоритма, како се виши чворови ређе појављују и шире разилазе.
import random
class SkipList3:
def (self):
self.h = LN()
self.h.next = [None]
def insert( self, k , v):
ht = 1
while random.randint(0,10) < 5:
ht +=1
if ht > len(self.h.next) :
self.h.next.extend( [None] * (ht - len(self.h.next) ) )
prev = self.h
prev_list = [self.h] * len(self.h.next)
# уместо да само prev.next у једноструко-повезаној листи, сваки ниво i има prev.next
for i in xrange( len(self.h.next)-1, -1, -1):
while not prev.next[i] is None and prev.next[i].k > k:
prev = prev.next[i]
#евидентирати претходни показивач за сваки ниво
prev_list[i] = prev
n = LN()
n.k,n.v = k,v
# креирати next показивач који показује на висину чвора за тренутни чвор.
n.next = [None] * ht
#print "prev list is ", prev_list
# уместо да се само повезују чворови у једноструко-повезаној листи, нпр. n.next = prev.next, prev.next =n
# урадити за сваки ниво n.next користећи n.next[i] и prev_list[i].next[i]
# може бити различитих prev чворова за сваки ниво, али исти нивои морају бити повезани,
# тако се индекс [i] појављује два пута у prev_list[i].next[i].
for i in xrange(0, ht):
n.next[i] = prev_list[i].next[i]
prev_list[i].next[i] = n
#print "self.h ", self.h
def show(self):
#print self.h
prev = self.h
while not prev.next[0] is None:
print prev.next[0].k, prev.next[0].v
prev = prev.next[0]
def find(self, k):
prev = self.h
h = len(self.h.next)
#print "height ", h
for i in xrange( h-1, -1, -1):
while not prev.next[i] is None and prev.next[i].k > k:
prev = prev.next[i]
#if prev.next[i] <> None:
#print "i, k, prev.next[i].k and .v", i, k, prev.next[i].k, prev.next[i].v
if prev.next[i] <> None and prev.next[i].k == k:
return prev.next[i].v
if pref.next[i] is None:
return None
return prev.next[i].k
def clear(self):
self.h= LN()
self.h.next = [None]
#driver
if == "":
#l = single_list2()
l = SkipList3()
test_dat = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
pairs = enumerate(test_dat)
m = [ (x,y) for x,y in pairs ]
while len(m) > 0:
i = random.randint(0,len(m)-1)
print "inserting ", m[i]
l.insert(m[i][0], m[i][1])
del m[i]
# l.insert( 3, 'C')
# l.insert(2, 'B')
# l.insert(4, 'D')
# l.insert(1, 'A')
l.show()
n = int(raw_input("How many elements to test?") )
if n <0: n = -n
l.clear()
import time
l2 = [ x for x in xrange(0, n)]
random.shuffle(l2)
for x in l2:
l.insert(x , x)
l.show()
print
print "finding.."
f = 0
t1 = time.time()
nf = []
for x in l2:
if l.find(x) == x:
f += 1
else:
nf.append(x)
t2 = time.time()
print "time", t2 - t1
td1 = t2 - t1
print "found ", f
print "didn't find", nf
dnf = []
for x in nf:
tu = (x,l.find(x))
dnf.append(tu)
print "find again ", dnf
sl = single_list2()
for x in l2:
sl.insert(x,x)
print "finding.."
f = 0
t1 = time.time()
for x in l2:
if sl.find(x) == x:
f += 1
t2 = time.time()
print "time", t2 - t1
print "found ", f
td2 = t2 - t1
print "factor difference time", td2/td1
5.2 Улога случајности
Идеја да се чворови вишег нивоа праве случајно са геометријски мањом вероватноћом, значи да на вишим нивоима постоји мањи број кључева које треба поредити, a пошто су чворови случајно изабрани, требало би да будемо поштеђени проблема дегенерисаних улаза који у алгоритмима стабла захтевају да се оно уравнотежава. Пошто листе вишег нивоа имају шире раздвојене елементе, а алгоритам претраживања се премешта на нижи ниво сваки пут када заврши са текућем нивоу, виши ниво помаже да се „прескоче“ претраживања ранијих елемената у листама нижег нивоа. Пошто има више нивоа прескакања, постаје мање вероватно да оскудан прескок на вишем нивоу неће бити компензован са бољим прескоцима на нижим нивоима. Пју (Pugh) тврди да су опште перформансе алгоритма претраживања скип листе O(logN).
Концептуално, да је лакше разумети скип листе од уравнотежавања стабала и стога их лакше имплементирати? Развој идеја бинарних стабала, уравнотежених бинарних стабала, 2-3 стабала, црвено-црних стабала и B-стабала, дају чвршћу концептуалну основу, али и спорост у развоју, тако да вероватно, једном када се разумеју црвено-црна стабла она поседују више концептуалног контекста који може да помогне или освежи памћење.
5.2.1 примена конкурентног приступа
Сем употребе рандомизације ради унапређења основних структура података као што је уланчана листа, скип листа може да се прошири као глобална структура података која се користи у вишепроцесорким применама. Види допунску тему на крају поглавља.
5.3 Идеја за вежбу
Замени сасвим солидан распоређивач (диспечер) Linux-а заснован на имплементацији црвено-црног стабла са скип листом и види како твоја врста Linux-а ради после рекомпилације.
6 Декартова стабла
Декартово стабло је бинарно стабло са два кључа, које користи други, случајно генерисани кључ и претходно размотрене операције ротације родитељ-дете, да насумично ротира стабло, тако да је оно на крају постане уравнотежено. Сетимо се да бинарна стабла раде тако што садрже све чворове мање од задатог у левом подстаблу, а све веће у десном. Сетимо се, такође, да ротација чворова не нарушава овај поредак (неки ово зову инваријантност рорације) али мења однос родитеља и детета, тако што, ако је родитељ био мањи од десног детета, онда родитељ постаје лево дете свог, претходно десног детета. Идеја хип стабла или Декартовог стабла је да се бинарна хип релација одржава између родитеља и деце и да чвор родитеља има већи приоритет од чворова своје деце, што није исто као и леви и десни поредак кључева код бинарног стабла. Стога новоуметнути чвор листа у бинарно стабло, за који се деси да има високи случајни приоритет, може да буде ротиран тако да буде на релативно вишој позицији у стаблу и да при томе нема родитеља са нижим приоритетом.
Декартово стабло је алтернатива и црвено-црним стаблима и скип листама, као самоуравнотежујућа, сортирана структура података.
6.0.1 пример имплементације Декартовог стабла у Јави
// Treap example: 2014 SJT, copyleft GNU .
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;
public class Treap1<K extends Comparable<K>, V> {
public Treap1(boolean test) {
this.test = test;
}
public Treap1() {}
boolean test = false;
static Random random = new Random(System.currentTimeMillis());
class TreapNode {
int priority = 0;
K k;
V val;
TreapNode left, right;
public TreapNode() {
if (!test) {
priority = random.nextInt();
}
}
}
TreapNode root = null;
void insert(K k, V val) {
root = insert(k, val, root);
}
TreapNode insert(K k, V val, TreapNode node) {
TreapNode node2 = new TreapNode();
node2.k = k;
node2.val = val;
if (node == null) {
node = node2;
} else if (k.compareTo(node.k) < 0) {
node.left = insert(k, val, node.left);
} else {
node.right = insert(k, val, node.right);
}
if (node.left != null && node.left.priority > node.priority) {
// десна ротација (ротирањем левог чвора на горе, тренутни чвор постаје десно дете)
TreapNode tmp = node.left;
node.left = node.left.right;
tmp.right = node;
node = tmp;
} else if (node.right != null && node.right.priority > node.priority) {
// лева ротација (ротирањем десног чвора на горе, тренутни чвор постаје лево дете)
TreapNode tmp = node.right;
node.right = node.right.left;
tmp.left = node;
node = tmp;
}
return node;
}
V find(K k) {
return findNode(k, root);
}
private V findNode(K k, Treap1<K, V>.TreapNode node) {
if (node == null)
return null;
if (k.compareTo(node.k) < 0) {
return findNode(k, node.left);
} else if (k.compareTo(node.k) > 0) {
return findNode(k, node.right);
} else {
return node.val;
}
}
public static void main(String[] args) {
LinkedList<Integer> dat = new LinkedList<Integer>();
for (int i = 0; i < 15000; ++i) {
dat.add(i);
}
testNumbers(dat, true); // нема балансирања случајног приоритета
testNumbers(dat, false);
}
private static void testNumbers(LinkedList<Integer> dat,
boolean test) {
Treap1<Integer, Integer> tree= new Treap1<>(test);
for (Integer integer : dat) {
tree.insert(integer, integer);
}
long t1 = System.currentTimeMillis();
Iterator<Integer> iter = dat.iterator();
int found = 0;
while (iter.hasNext()) {
Integer j = desc.next();
Integer i = tree.find(j);
if (j.equals(i)) {
++found;
}
}
long t2 = System.currentTimeMillis();
System.out.println("found = " + found + " in " + (t2 - t1));
}
}
6.0.2 Поређење Декартових и раширених стабала
''Раширена стабла'' су слична Декартовим у томе што се користи ротација да се чвор већег приоритета доведе на врх без да се при томе мења основни редослед кључева, осим што уместо употребе случајног кључа за приоритет, ротира се последњи чвор коме се приступило у корен стабла, тако да ће се чворови којима се чешће приступа налазити близу врха. То значи да ће у Декартовм стаблима уметнути чворови ротирати највише до приоритета који им одређује случајни кључ приоритета, док код раширених стабала, уметнути чвор ротира до корена, и свако претраживање у раширеном стаблу ће резлтирати у поновном уравнотежавању, што није случај код Декартовог стабла.
7 Дерандомизација
8 Вежбе
1. Напишите функцију find-min и извршите је са више различити улаза да би показали њену исправност.
9 Допунска тема: скип листе и вишепроцесорски алгоритми
Вишепроцесорски хардвер обезбеђује атомичке операције CAS (упореди и постави) или CMPEXCHG (упореди и размени) (intel manual 253666.pdf, стр. 3-188), где се очекивана вредност пуни у акумулатор, чији се садржај пореди са садржајем циљне меморијске локације и, ако су исти, садржај изворне меморијске локације се пуни у циљну меморијску локацију и подиже се (сигнална) заставица нуле (zero flag set), у супротном, ако су различити, садржај циљне меморијске локације се враћа у акумулатор и заставица нуле се спушта (zero flag unset), означавајући да је, на пример, дошло до покушаја истовремног закључавања. У intel архитектури инструкција LOCK се издаје пре CMPEXCHG, која за следећу инскрукцију, или закључава кеш за конкурентне приступе ако је меморијска локација кеширана, или закључава локацију заједничке меморије ако локација није у кешу.
CMPEXCHG може да се користи за имплементацију закључавања од којих су спинлокови најједноставнији (нпр. непрестани покушаји, док се заставица нуле не подигне).
Приступ без закључавања повећава ефикасност, тако што се избегава чекања на закључавање.
Стандардна библиотека језика Јава има имплементацију неблокирајућих конкурентних скип листа, засновану на раду под називом "Прагматична имплементација неблокирајућих једноструко уланчаних листа“
Имплементација за скип листе је проширење имплементације за једноструко уланчане листе без закључавања. Опис следи:
Операција insert је: X -> Y уметни N , N -> Y, X -> N и очекивани резултат је X -> N -> Y.
Утркивање (race condition) је ако се M умеће између X и Y и убацивање M се прво завршава, а затим се завршава уметање N, тако да се добија следећа ситуација X -> N -> Y <- M.
M није у листи. Операција CAS избегава ово, зато што се пре ажурирања X ->, прво проверава копија -> Y у односу на тренутну вредност X ->.
Ако N успе први да ажурира X -> онда, када M покуша да ажурира X ->, копија X -> Y, која се прави пре M -> Y, не одговара X -> N, тако да CAS враћа подигнуту заставицу не-нула (non-zero flag set). Процес који је пробао да уметне M, може поново да покуша уметање после X, али сада CAS проверава да ли је ->N следећи показивач на X, тако да је после поновљеног покушаја X->M->N->Y и ниједно уметање није изгубљено.
Ако M прво ажурира X->, копија X->Y која се прави пре уметања N, не одговара X -> M, тако да ће CAS и овде бити неуспешан и горе поменути поновни покушај уметања N имаће као резултат X ->N -> M -> Y.
Операција delete зависи од посебног „логичког“ корака брисања, пре „физичког“ брисања. „Логичко“ брисање подразумева да CAS изврши претварање показивача на следећи чвор у „обележени“ показивач. Имплементација у Јави замењује ово са атомичким уметањем обележеног заступајућег (proxy) чвора испред следећег чвора.
Ове спречава будућа уметања после чвора који има „обележен“ показивач на следећи и тиме чини да је потоњи чвор „логички“ избрисан.
Операција insert се ослања на другу функцију ''search'' која приликом позива враћа 2 unmarked, показивача чвора: први који показује на чвор чији је показивач на следећи једнак другом.
Први чвор је чвор пре места уметања.
Операција ''insert'' CAS-а обезбеђује да текући показивач првог чвора на следећи одговара необележеној референци на други, тако да ће бити „логички“ неиспешна ако показивач на следећи првог чвора постане обележен после позива функције search, зато што је први чвор истовремено логички избрисан.
''Ово задовољава захтев да се спречи појава истовременог уметања, пошто је чвор већ обрисан.''
Ако је операција уметања у CAS-у неуспешна због показивача на следећи претходног чвора, тражење места уметања поново почиње од почетка целе листе, пошто нови необележени претходни чвор мора да се пронађе, а не постоји показивач на претходни пошто је листа једноструко уланчана.
Операција delete изложена горе, такође се ослања на операцију ''search'' која враћа два необележена чвора, а две CAS операције брисања: једна за логичко брисање или обележавање показивача на следећи другог показивача и друга за физичко брисање тако што чини да показивач на следећи првог чвора показује исто што и необележени показивач на следећи другог чвора. Прва CAS операција брисања се дешава само после провере да ли је копија оригиналног показивача на следећи другог чвора необележена и обезбеђује да успе само једно конкурентно брисање, које чита текући показивач на следећи другог чвора који је исто тако необележен.
Друга CAS операција утврђује да претходни чвор није логички обрисан, пошто његов показивач на следећи није исти као необележени показивач на текући други чвор, који враћа функција search, тако да се само активни показивач на следећи претходног чвора „физички“ ажурира према копији оригиналног, необележеног показивача на следећи чвора који се брише (чији је показивач на следећи већ обележен у првом CAS-у).
Ако други CAS не успе, онда је претходни чвор логички обрисан, а његов показивач на следећи је обележен, исто као и показивач на следећи текућег чвора. Позив функције ''search'', поново све доводи у ред, пошто настојећи да нађе кључ текућег чвора и да врати суседне необележене показиваче на претходни и текући чвор и док то ради скраћује низове логички обрисаних чворова.
9.0.1 Питања програмирања без закључавања
Исцрпљивање је могуће када неуспешна уметања морају да се понављају од почетка листе. Без чекања (wait-freedom) је концепт у коме алгоритам има све нити извршења заштићене од исцрпљивања.
Код ABA проблема, где сакупљач смећа (garbage collector) рециклира показивач A, али се адреса пуни на различите начине и показивач се поновно додаје у тачки где се врши провера А од стране друге нити извршења која чита А и извршава CAS да утврди да се А није променио; Адреса је иста и није обележена, али је садржај А промењен.


Алгоритми/"Подели, па владај"

Прва међу главним алгоритамским техникама коју ћемо покрити је подели и владај (''divide et impera''). Део вештине прављења доброг алгоритма подели и владај је одређивање како задати проблем може да буде подељен на два или више сличних, али мањих потпроблема. Генерално речено, када правимо алгоритам подели и владај, предузимамо следеће кораке:
Методологија подели и владај
Први алгоритам који ћемо приказати коришћењем ове методологије је сортирање спајањем.
1 Сортирање спајањем
Проблем који сортирање спајањем решава је опште сортирање задатог неуређеног вектора елемената. Овај алгоритам као резултат даје вектор сортираних елемената. Тачније, за неки вектор a са индексом 1 до n, у случају да важи услов да
:за свако ''i'', ''j'' такво да је 1 ≤''i'' < ''j''≤''n'' онда ''a''[''i''] ≤''a''[''j'']
може рећи да је a сортиран. Овде је приказан интерфејс:
''// sort -- враћа копију сортираног низа a''
function sort(array ''a''): array
Следећи методологију подели и владај, како сортирање a да поделимо на мање потпроблеме? Пошто је a вектор од n елемената, можда можемо започети дељењем вектора на два, величине n/2 елемената. Ови мањи вектори ће такође бити несортирани и има смисла да сваки сортирамо засебно; тако ћемо њих сматрати „сличним“. Занемарујући за сада базни случај, на овај начин сводимо проблем на други: како ћемо објединити два задата сортирана вектора у један сортирани вектор који садржи све њихове елементе:
''// merge -- дато a и b (претпоставља се да су сортирани) враћа спојени низ који
// одржава редослед
function merge(array ''a'', array ''b''): array
До сада смо следећи методологију дошли до овде, али шта је са базним случајем? Базни случај је део алгоритма који се односи на то шта се дешава када проблем не може више да се подели на мање потпроблеме. Овде је базни случај када вектор садржи само један елемент. Доле је приказан алгоритам за сортирање вектора који садржи 0 или 1 елемент:
''// base-sort -- дат је низ од једног елемента (или празан), враћа копију
// сортираног низа''
function base-sort(array ''a''[1..''n'']): array
assert (''n'' <= 1)
return ''a''.copy()
end
Састављајући приказане делове кода, добијамо до чега нас је до сада довела методологија:
''// sort -- враћа копију сортираног низа a''
function sort(array ''a''[1..''n'']): array
if ''n'' <= 1: return ''a''.copy()
else:
let ''sub_size'' := ''n'' / 2
let ''first_half'' := sort(''a''[1,..,''sub_size''])
let ''second_half'' := sort(''a''[''sub_size'' + 1,..,''n''])
return merge(''first_half'', ''second_half'')
fi
end
И осим неимплементираног кода за потпрограм merge, овај алгоритам сортирања је готов! Пре него што покажемо како овај алгоритам ради, приказаћемо како може да буде написан merge:
''// merge -- дато a и b (претпоставља се да су сортирани) враћа спојени низ који
// одржава редослед
function merge(array ''a''[1..''n''], array ''b''[1..''m'']): array
let ''result'' := new array[''n'' + ''m'']
let ''i'', ''j'' := 1
for ''k'' := 1 to ''n'' + ''m'':
if ''i'' >= ''n'': ''result''[''k''] := ''b''[''j'']; ''j'' += 1
else-if ''j'' >= ''m'': ''result''[''k''] := ''a''[''i'']; ''i'' += 1
else:
if ''a''[''i''] < ''b''[''j'']:
''result''[''k''] := ''a''[''i'']; ''i'' += 1
else:
''result''[''k''] := ''b''[''j'']; ''j'' += 1
fi
fi
repeat
end
2 Анализа
Нека је <math>T(n)</math> број корака који је потребан алгоритму за величину улаза <math>n</math>.
Спајање троши линеарно време и ми вршимо рекурзију сваки пут на два потпроблема двапут мање величине од изворног, тако да
:<math>T(n) = 2\cdot T\left(\frac{n}{2}\right) + O(n).</math>
Према Главној теореми види се да ова рекурзија има уравнотежено стабло, тако да је време извршења:
:<math>T(n) = O(n \cdot \log n).</math>
До овога се може доћи и интуитивно, запитајући се колико пута треба n поделити са 2, пре него што дужина вектора за сортрање постане једнака 1? Наравно, зашто m пута?
2m = n , што је еквивалентно log 2m = log n, и даље m x log22 = log 2 n, и пошто је log2 2 = 1, следи да је m = log2n.
Пошто је m број половљења вектора пре него што се он подели на под-векторе који садрже 1 елемент, биће потребно m нивоа спајања неког под-вектора са суседом, где ће сумарна дужина под-вектора бити n на сваком нивоу. Требаће да се изврши тачно n/2 поређења за спајање на сваком нивоу са m ( log2n ) нивоа тако да важи да је:
O(n/2 x log n ) <=> O ( n log n).
3 Итеративна верзија
Приказани алгоритам сортирања спајањем може да се претвори у итеративни, итеративним спајањем сваког следећег пара, затим група од четири итд. Итеративни алгоритам је у пракси бржи пошто нема допунске „трошкове“ великог броја позива функција. Ипак, пошто је стабло позива рекурзивне верзије алгоритма логаритамске дубине, он у току извршења не заузима много простора на стеку. За сортирање чак 4 гига елемената, потребно је само 32 записа за позиве на стеку, што је веома скроман захтев. Ако узмемо да је један запис величине чак 256 бајта, требаће нам само 8 килобајта простора на стеку.
Итеративна верзија сеортирања спајањем представља мању модификацију рекурзивне верзије, а можемо да искористимо и постојећу функцију merge. Алгоритам ради тако што спаја мале, сортиране делове изворног вектора, и од њих прави веће сортиране делове. Да би ово постигли, итерирамо кроз вектор са сукцесивно све већим „корацима“.
''// sort -- враћа сортирану копију низа a''
function sort_iterative(array ''a''[1..''n'']): array
let ''result'' := ''a''.copy()
for ''power'' := 0 to log2(''n''-1)
let ''unit'' := 2^power
for ''i'' := 1 to ''n'' by ''unit''*2
if i+''unit''-1 < n:
let ''a1''[1..''unit''] := ''result''[i..i+''unit''-1]
let ''a2''[1..''unit''] := ''result''[i+''unit''..min(i+''unit''*2-1, ''n'')]
''result''[i..i+''unit''*2-1] := merge(''a1'',''a2'')
fi
repeat
repeat
return ''result''
end
Ово ради зато што је свака подлиста дужине 1 неког вектора, по дефиницији сортирана. Свака итерација кроз вектор (користећи бројач ''i'') дуплира величину сортираних подлиста спајањем суседних подлиста у веће верзије. Тренутна величина сортираних подлиста алгоритма представља промењљива ''unit''.
3.1 просторна неефикасност
Једноставно, сортирање спајањем захтева простор од 2 x n: n да запамти 2 два мања сортирана вектора и n да упише коначни резултат спајања. Али ово сортирање је и дање одговарајуће за груписања спајања.
4 Бинарно претраживање
Када је неки вектор сортиран, коришћењем бинарног претраживања брзо можемо да лоцирамо његове елементе. Бинарно претраживање је другачије од осталих алгоритама типа подели и владај, у томе што се углавном заснива на подели (није потребно ништа победити). Концепт на коме се базира бинарно претраживање, биће користан за разумевање алгоритама партиционисања и брзог сортирања приказаних у поглављу о рандомизацији.
Налажење ставке у већ сортираном вектору је слично проналажењу имена у телефонском именику; можете почети отварајући књигу на средини. Ако је име које тражите на отвореној страници, заустављате се. Ако сте отишли предалеко, можете започети исти процес са првом половином књиге. Ако се име које тражите појављује касније у књизи, почињете процес са другом половином књиге. Исти процес понављате сужавајући сваки пут простор претраживања за пола, док не нађете оно што тражите (или док не нађете где би оно што тражите требало да се налази да постоји. Следећи алгоритам прецизно описује поменути поступак:
''// binary-search -- враћа индекс вредности у датом низу или''''// -1 ако вредност не може бити пронађена. Претпоставља се да је низ сортиран у растућем поретку''
function binary-search(''value'', array ''A''[1..''n'']): integer
return search-inner(''value'', ''A'', 1, ''n'' + 1)
end
''// search-inner -- тражи делове низа; крај је један поред''''// lпоследњег елемента ''
function search-inner(''value'', array ''A'', ''start'', ''end''): integer
if ''start'' == ''end'':
return -1 ''// није пронађен''
fi
let ''length'' := ''end'' - ''start''
if ''length'' == 1:
if ''value'' == ''A''[''start'']:
return ''start''
else:
return -1
fi
fi
let ''mid'' := ''start'' + (''length'' / 2)
if ''value'' == ''A''[''mid'']:
return ''mid''
else-if ''value'' > ''A''[''mid'']:
return search-inner(''value'', ''A'', ''mid'' + 1, ''end'')
else:
return search-inner(''value'', ''A'', ''start'', ''mid'')
fi
end
Приметите да се сви рекурзивни позиви појављују на крају функције (терминална, крајња рекурзија), тако да је алгоритам итеративан. Можемо експлицитно да уклонимо ове позиве, ако програмски језик то већ не уради за нас претварајући вредности аргумената прослеђених рекурзивним позивима у додељивање вредности, и затим поново скочимо на почетак тела функције:
''// binary-search -- враћа индекс вредности у датом низу, или''''// -1 ако вредност не може бити пронађена. Претпоставља се да је низ сортиран у растућем поретку''
function binary-search(''value'', array ''A''[1,..''n'']): integer
let ''start'' := 1
let ''end'' := ''n'' + 1
loop:
if ''start'' == ''end'': return -1 fi ''// није пронађен''
let ''length'' := ''end'' - ''start''
if ''length'' == 1:
if ''value'' == ''A''[''start'']: return ''start''
else: return -1 fi
fi
let ''mid'' := ''start'' + (''length'' / 2)
if ''value'' == ''A''[''mid'']:
return ''mid''
else-if ''value'' > ''A''[''mid'']:
''start'' := ''mid'' + 1
else:
''end'' := ''mid''
fi
repeat
end
Иако имамо један итеративни алгоритам, лакше је промишљати о рекурзивној верзији. Ако је број корака алгоритма <math>T(n)</math>, онда имамо следећу рекурентност (понављање) коју дефинише <math>T(n)</math>:
:<math>T(n) = 1\cdot T\left(\frac{n}{2}\right) + O(1).</math>
Величина сваког рекурзивног позива зависи од половине величине улаза (<math>n</math>), а постоји и константна количина времена која се троши ван рекурзије (на пример за израчунавање ''length'' и ''mid'' ће се утрошити иста количина времена, без обзира на то колико елемената вектор садржи). Према Главној теореми ова рекурентност има вредности, што представља уравнотежено стабло, па је
:<math>T(n) = \Theta(n^k\cdot\log n) = \Theta(\log n).</math>
Тако долаазимо до тога да овај алгоритам користи логаритамско време. Типично, чак и када је n велик, безбедно је да се допусти да стек расте за активационих записа за рекурзивне позиве.
4.1 Потешкоће у иницијално исправним имплементацијама бинарног претраживања
Чланак на Википедији о бинарном претраживању помиње потешкоће приликом писања исправног кода за овај алгоритам. На пример, имплементација преоптерећене Јава функције Arrays.binarySearch(..) врши итеративно бинарно претраживање које не ради када код целобројне промењљиве (large integer) долази до прекорачења у једноставном изразу за израчунавање mid, mid = ( end + start) / 2, т.ј. када је end + start > max_positive_integer. Стога је горњи алгоритам исправнији када користи да је length = end - start и дода половина length на start. Јава алгоритам за бинарно претраживање враћа резултат који је користан за налажење позиције најближег кључа који је већи од кључа по ком се претражује, т.ј. позиције на који кључ претраживања може да се уметне.
То јест, враћа се (keypos+1) ако кључ претраживања није пронађен, али је потребна позиција за уметање кључа претраживања (insertion_point = -return_value – 1). Гледајући граничне вредности, позиција уметања може да се налази одмах на почетку листе (ip = 0, return value = -1) до позиције одмах иза последњег елемента (ip = length(A), return value = - length(A) - 1).
Покушајте да имплементирате функционалност горњег итеративног алгоритма за бинарно претраживање као вежбу која ће бити корисна за боље разумевање изложеног.
5 Школско множење
Како да представимо велики цео број састављен из више речи? Можемо да имамо бинарни приказ користећи вектор речи (или додељени блок меморије) које представљају битове великог целог броја. Претпоставимо да имамо два цела броја, и и желимо да их помножимо. Ради једноставности, усвојићемо да су и и дужине битова (ако је неки од њих краћи, увек можемо да га попунимо нулама на почетку). Основни начин да се помноже цели бројеви је да се користи школски алгоритам монжења. Ово је још једноставније у бинарном систему јер множимо само са 1 или 0:
x6 x5 x4 x3 x2 x1 x0
&пута; y6 y5 y4 y3 y2 y1 y0
-----------------------
x6 x5 x4 x3 x2 x1 x0 (када y0 је 1; 0 иначе)
x6 x5 x4 x3 x2 x1 x0 0 (када y1 је 1; 0 иначе)
x6 x5 x4 x3 x2 x1 x0 0 0 (када y2 је 1; 0 иначе)
x6 x5 x4 x3 x2 x1 x0 0 0 0 (када y3 је 1; 0 иначе)
... итд.
Овако би изгледало описано множење као алгоритам:
''// multiply -- враћа производ два бинарна цела броја, који су дужине n''
function multiply(bitarray ''x''[1,..''n''], bitarray ''y''[1,..''n'']): bitarray
bitarray ''p'' = 0
for ''i'':=1 to ''n'':
if ''y''[''i''] == 1:
''p'' := add(''p'', ''x'')
fi
''x'' := pad(''x'', 0) ''// додаје се још једна нула на крај броја x''
repeat
return ''p''
end
Подрутина add сабира два бинарна цела броја и враћа резултатa, а подрутина pad поставља допунску цифру на крај броја (додавање нуле је исто што и шифтовање броја улево за једно место, што је истоветно његовом множењу са два). Овде пролазимо петљу ''n'' пута и у најгорем случају исто толико пута позивамо add. Бројеви који се прослеђују подрутини add биће, највише, дужине <math>2n</math>. Даље, можемо да очекујемо да подрутина add може да се изврши у линеарном времену. Следи, да ако се направи ''n'' позива подрутине <math>O(n)</math>, цео алгоритам ће утрошити <math>O(n^2)</math> времена.
6 Множење подели и владај
Као што можете да приметите, ово није крај приче. Приказали смо „очигледан“ алгоритам множења. Погледајмо да ли нам стратегија подели и владај пружа нешто више. Један правац који желимо да испитамо је да покушамо је да поделимо цео број на два дела. На пример цео број ''x'' може да се подели на два дела <math>x_{h}</math> и <math>x_{l}</math>, који представљају половине вишег и нижег реда <math>x</math>. На пример ако if <math>x</math> има ''n'' битова, имамо
:<math>x = x_{h}\cdot 2^{n/2} + x_{l}</math>
Исто можемо да урадимо и за:
:<math>y = y_{h}\cdot 2^{n/2} + y_{l}</math>
Али из ове поделе, није јасно како да помножимо делове на начин који омогућава да се добијени парцијални резултати обједине у коначан резултат решења проблема.
Напишимо прво да би <math>x\times y</math> требало да буде:
:<math>x\times y = x_h\times y_h\cdot (2^{n/2})^2 + (x_h\times y_l + x_l\times y_h)\cdot (2^{n/2}) + x_l\times y_l</math>
Ово потиче од простог множења нових hi/lo репрезентација за <math>x</math> и <math>y</math> . Множење мањих делова означено је симболом "<math>\times</math>". Приметимо да је множење са <math>2^{n/2}</math> и <math>(2^{n/2})^2 = 2^n</math> и да захтева право множење: уместо тога можемо само да број попунимо десно са одговарајућим бројем нула. Ово доводи до следећег алгоритма подели и владај:
''// multiply -- враћа производ два бинарна цела броја, који су дужине n''
function multiply(bitarray ''x''[1,..''n''], bitarray ''y''[1,..''n'']): bitarray
if ''n'' == 1: return ''x''[1] * ''y''[1] fi ''// помножити поједине цифре: O(1)''
let ''xh'' := ''x''[''n''/2 + 1, .., ''n''] ''// сечење низа, O(n)''
let ''xl'' := ''x''[0, .., ''n'' / 2] ''// сечење низа, O(n)''
let ''yh'' := ''y''[''n''/2 + 1, .., ''n''] ''// сечење низа, O(n)''
let ''yl'' := ''y''[0, .., ''n'' / 2] ''// сечење низа, O(n)''
let ''a'' := multiply(''xh'', ''yh'') ''// рекурзивни позив; T(n/2)''
let ''b'' := multiply(''xh'', ''yl'') ''// рекурзивни позив; T(n/2)''
let ''c'' := multiply(''xl'', ''yh'') ''// рекурзивни позив; T(n/2)''
let ''d'' := multiply(''xl'', ''yl'') ''// рекурзивни позив; T(n/2)''''b'' := add(''b'', ''c'') ''// регуларно додавање; O(n)''''a'' := shift(''a'', ''n'') ''// подлога нула; O(n)''''b'' := shift(''b'', ''n''/2) ''// подлога нула; O(n)''
return add(''a'', ''b'', ''d'') ''// регуларно додавање; O(n)''
end
Користићемо Главну теорему за анализу времена извршења алгоритма. Под претпоставком да је време извршења алгоритма <math>T(n)</math>, коментари показују колико времена троши сваки од корака. Пошто имамо четири рекурзивна позива, сваки са улазом величине <math>n/2</math>, имамо:
:<math>T(n) = 4T(n/2) + O(n)</math>
где су <math>a=4, b=2, k=1</math> и знајући да је <math>4>2^1</math> добијамо да се алгоритам налази у стаблу разгранатом при дну. За овај случај Главна теорема даје:
:<math>T(n)=O(n^{\log_2 4}) = O(n^2).</math>
Дакле, после свог уложеног труда, још увек нисмо бољи од школског алгоритма! На срећу, бројеви и полиномијали представљају скуп података о коме имамо допунске информације. У ствари можемо смањити време извршења применом неких математичких трикова.
Прво заменимо <math>2^{n/2}</math> промењљивом ''z'':
:<math>x\times y = x_h*y_h z^2 + (x_h*y_l + x_l*y_h)z + x_l*y_l</math>
Добијамо квадратну једначину за коју знамо да су потребна само три коефицијента или тачке на графу да би се она једнозначно описала. Ипак, у горњем алгоритму користили смо укупно четири множења. Хајде да покушамо да преобликујемо <math>x</math> и <math>y</math> у линеарне функције:
:<math>P_x(z) = x_h\cdot z + x_l</math>:<math>P_y(z) = y_h\cdot z + y_l</math>
Сада, за <math>x\times y</math> треба само да израчунамо <math>(P_x\cdot P_y)(2^{n/2})</math>. Проценићемо <math>P_x(z)</math> и <math>P_y(z)</math> на три тачке. Три погодне тачке за евалуацију функције биће на <math>(P_x\cdot P_y)(1), (P_x\cdot P_y)(0), (P_x\cdot P_y)(-1)</math>.
7 Конверзија основе броја
Уз бинарни систем бројева, рачунарске науке користе и бројеве са основом 8 (октални систем) и 16 (хексадецимални систем). Веома је лако конвертовати бројеве из једног у други систем. Бројеви аписани октално и хексадецимално су значајно краћи у приказу од бинарних бројева.
Да би приказали првих 8 бројева у бинарном систему потребна су нам 3 бита. Тако имамо 0=000, 1=001, 2=010, 3=011, 4=100, 5=101, 6=110, 7=111. Узмимо M=(2065)8. Да би добили бинарни приказ, заменићемо сваку од четири цифаре са одговарајућом тројком битова: 010 000 110 101. По уклањању водећих нула, бинарна представа је директна: M=(10000110101)2. (Конверзија бројева хексадецималног система је слична, осим што се користи приказ од 4 бита за бројеве мање од 16.) Ова чињеница следи из општег алгоритма за конверзију и запажања да је 8=<math>2^3</math> (и наравно 16=<math>2^4</math> ). Изгледа да је најкраћи начин за конверзију бројева у бинарни систем, да се прво изврши конверзија у октални или хексадецимални систем. Погледајмо сада како се општи алгоритам за конверзију програмски имплементира.
Ради референце, представљање броја у систему са основом (базом) N може да се састоји само од цифара мањих од N.
Тачније, ако:
:<math>(1) M = a_kN^k+a_{k-1}N^{k-1}+...+a_1N^1+a_0</math>
где је <math>0 <= a_i < N</math> имамо представљање броја М у систему са основом N и записујемо:
:<math>M = (a_ka_{k-1}...a_0)N</math>
Ако препишемо (1) као:
:<math>(2) M = a_0+N*(a_1+N*(a_2+N*...))</math>
алгоритам за добијање коефицијената постаје очигледнији. На пример, <math>a_0=M\ modulo\ n</math> и <math>a_1=(M/N)\ modulo\ n</math> и тако даље.
8 Рекурзивна имплементација
Прикажимо алгоритам мнемонички: (резултат је низ симбола, у који једну по једну додајемо израчунате цифре). је празан низ. Можда се сетите да је то нулти елемент при конкатенацији низова. Овде ћемо проверити да ли процедура конверзије терминира. Она се зауставља када је M мање од N и у том случају је M цифра (или неки изабрани симбол за N>10) и нема потребе за допунским акцијама. Само је додајте испред осталих цифара које сте раније добили. Знак '+' означава конкатенацију. Ако M није мање од N, прво ћемо издвојити остатак дељења са N, додати добијену цифру резултату, као што је претходно описано, и доделити нову вредност М једнаку M/N. Цео процес треба да се понови почев од корака 2. Треба нам функција Conversion која има два аргумента M и N и која враћа приказ броја M у систему са основом N. Ова функција може да изгледа отприлике овако:
1 String Conversion(int M, int N) // враћа стринг, осим два цела броја
2 {
3 if (M < N) // see if it's time to return
4 return new String(""+M); // ""+M прави стринг као низ цифара
5 else // још увек није време за то
6 return Conversion(M/N, N) +
new String(""+(M mod N)); // наставити
7 }
Ово је практично радна верзија Јава функције и скоро исто би изгледала у C++, а за реализацију у језику C су потребне мање модификације. Као што се види у неком тренутку функција позива саму себе са другачијим првим аргументом. Може да се каже да је функција дефинисана у односу на саму себе. Такве функције се зову рекурзивне. (Најпознатија рекурзивна функција је факторијал: n!=n*(n-1)!.) Функција позива саму себе и примењује се са својим аргументима и затим се (природно) примењује са својим новим аргументима и онда ... и тако даље. Можемо да будемо сигурни да ће се процес на крају зауставити зато што низ аргумената (први аргумент) опада. Тако ће, пре или касније, први аргумент постати мањи од другог и процес ће корак по корак излазити из рекурзије.
9 Итеративна имплементација
Не дозвољавају сви језици рекурзивне позиве функција. Рекурзивне функције могу да буду непожељне у случају да се очекује прекид процеса из било ког разлога. На пример, код проблема Ханојске куле, корисник може да пожели да прекине демонстрацију зато што хоће да провери своје разумевање решења. Постоје компликације условљене начином на који рачунари извршавају програме, које се јављају када је потребно да се искочи из више нивоа рекурзивних позива одједном.
Приметимо ипак да добијени низ који производи алгоритам конверзије има погрешан редослед: све цифре се прво рачунају и онда уписују у низ тако што је последња цифра прва. Рекурзивна имплементација са овим лако излази на крај. Са сваким позивом функције Conversion, рачунар прави ново окружење у којем се памте вредности које се предају M и N и новоизрачунато S. По окончању позива функције, т.ј. по повратку из функције, налазимо да је окружење исто као што је било и пре позива. Рекурзивне функције имплицитно памте секвенце обраде. Елиминисање рекурзивних позива подразумева да морамо израчунате цифре да сачувамо експлицитно и да их касније вратимо обрнутим редом.
У рачунарској науци се овај механизам назива LIFO (Last In First Out) – последњи који је ушао, први излази. Овај механизам се најбоље имплементира помоћу структуре података типа стек. Стек има само две операције: push (стави на стек) and pop (извади са стека). Интуитивно, стек се може визуализовати како гомила објеката који су сложени један преко другог. Ако је потребан неки одређени објекат морају се прво уклонити сви објекти изнад њега. Очигледно је да је једини објекат који може одмах да се извади, онај који је на гомилу последњи стављен.
Итеративна имплементација функције Conversion могла би да изгледа као што је приказано ниже.
1 String Conversion(int M, int N) // враћа стринг, осим два цела броја
2 {
3 Stack stack = new Stack(); // креира стек
4 while (M >= N) // јасно се види петља која се понавља
5 {
6 stack.push(M mod N); // ускладишти цифру
7 M = M/N; // find new M
8 }
9 // сада треба скупити све цифре заједно
10 String str = new String(""+M); // креирати стринг са једном цифром M
11 while (stack.NotEmpty())
12 str = str+stack.pop() // узети са стека следећу цифру
13 return str;
14 }
10 Најближи пар тачака
Ако желите да из неког скупа тачака у равни нађете пар тачака које су најближе, треба да упоредите сваку тачку са сваком другом у времену <math>O(n^2)</math>, или да употребите алгоритам подели и победи.
11 Увод
Приступ „грубом силом“ проблему најближег пара (т.ј. провера сваког могућег пара тачака) користи квадратно време. Желели би сада да представимо бржи „подели и владај“ алгоритам за решавање проблема најближег пара. За задати скуп тачака у равни S, наш приступ ће бити поделимо скуп на два приближно једнака дела (S1 и S2) за које већ имамо решење и онда да спојимо половине у линеарном времену да би добили један O(nlogn) алгоритам. Међутим, стварно решење је далеко од очигледног. Могуће је да тражени пар има једну тачку у S1, а другу у S2. Не присиљава ли нас ово да поново проверимо све могуће парове тачака? Приступ подели и владај, који је овде приказан, представља директну генерализацију једнодимензијалног алгоритма који је описан у претходном применом одељку.
12 Најближи пар у равни
У реду, генерализоваћемо наш једнодимензионални 1-D алгоритам колико год је могуће директније (погледај слику 3.2). Дати скуп тачака S у равни делимо на два скупа S1 и S2 вертикалном линијом l таквом да су тачке из S1 лево од l, а оне из S2 десно од l.
Сада рекурзивно решавамо проблем над ова два скупа и добијамо минимална растојања d1 (за S1) и d2 (за S2). Допустићемо да је d њихов минимум.
Сада, исто као и у случају 1-D, ако се најближи пар целог скупа састоји из по једне тачке из сваког подскупа, онда ове две тачке морају да буду у оквиру d од l. Ова зона је представљена са две траке P1 и P2 са обе стране од l. До сада смо били потпуно у корак са случајем 1-D. Сада, међутим, допунска димензија проузрокује проблеме. Желимо да утврдимо да ли је нека тачка, рецимо из P1, удаљена мање од d од друге тачке из P2. Међутим у равни ми немамо луксуз који смо имали на линији, када смо видели да само једна тачка у сваком скупу може да буде у оквиру d од средње линије. У ствари, у две димензије, све тачке могу да буду у траци! Ово је катастрофално, зато што ћемо морати да упоређујемо n2 парова тачака да би спојили скуп, и стога нам наш алгоритам подели и владај неће пружити већу ефикасност. Срећом, сада можемо да направимо још једну спасоносну опсервацију. За сваку поједину тачку p у једној траци, само тачке из друге траке које одговарају следећим ограничењима морају да се провере:
* оне тачке у оквиру d од p у правцу друге траке
* оне у оквиру d од p у правцу позитивне и негативне y осе.
Једноставно зато што тачке ван овог граничног оквира не могу бити мање d удаљене од p (види слику 3.3). Тако се дешава, да зато што је свака тачка у граничном оквиру најмање удаљена за d, у оквиру може да буде само шест тачака.
Сада нема потребе да се проверава свих n2 тачака. Све што треба да урадимо је да сортирамо тачке из траке по њиховим y координатама и да редом скенирамо тачке проверавајући сваку тачку у односу на максимум 6 суседних. Ово значи да је највише 6*n поређења потребно да би се проверили парове кандидате. Међутим, пошто смо сортирали тачке из траке по њиховим y координатама, процес спајања наша два скупа није линеаран, већ узима O(nlogn) времена. Стога наш комплетан алгоритам још увек није O(nlogn), али је ипак побољшање у односу на квадратне перформансе приступа грубом силом (као што ћемо видети у следећем одељку). У одељку 3.4, показаћемо како да се овај алгоритам направи да буде још ефикаснији, поткрепљивањем нашег рекурзивног подрешења.
13 Кратак преглед и анализа 2-D алгоритама
Овде ћемо корак по корак, представити кратак преглед алгоритма приказаног у претходном одељку, а затим и анализу његових перформанси. Алгоритам је једноставно написан у облику листе, зато што сматрам да је псеудо-код оптерећујући и непотребан за разумевање алгоритма. Приметите да смо пре-сортирали тачке по њиховим x координатама и сачували посебну структуру која садржи тачке сортиране по вредностима y (за корак 4), које за себе узимају O(nlogn) времена.
ClosestPair (најближи пар) скупа тачака:
# Поделите скуп на два једнака дела линијом l и рекурзивно израчунајте минималну раздаљину у сваком делу.
# Нека је d минимум два минимална растојања.
# Елиминишите тачке које леже даље од d у односу на l.
# Размотрите преостале тачке у односу на њихове y координате.
# Скенирајте остале тачке уређене по y и израчунајте растојање сваке тачке у односу на суседне које су удаљене не више од d (зато нам је потребно да буду пре-сортиране по y). Приметите да нема више од таквих тачака (види претходни одељак).
# Ако је било које од ових растојања мање од d, ажурирајте вредност d.
Анализа:
* Ефикасност алгоритма ћемо означити са T(n)
* Корак 1 узима 2T(n/2) (примењујемо алгоритам на обе половине)
* Корак 3 узима O(n) времена
* Корак 5 узима O(n) времена (као што смо видели у предходном одељку)
тако да је,
<math>T(n) = 2T(n/2) + O(n)</math>
што према Главној теореми, резултира са:
<math>T(n) \isin O(nlogn)</math>
Стога над спајањем под-решења доминира сортирање у кораку 4 и стога и зато узима O(nlogn) времена.
Код алгоритма подели и владај, ово мора да се понови једанпут за сваки ниво рекурзије и стога цео алгоритам ClosestPair троши O(logn*nlogn) = O(nlog2n) времена.
14 Усавршавање алгоритма
Можемо мало да побољшамо алгоритам смањењем времна потребног за сортирање по y координати у кораку 4. То се ради тако што се тражи да рекурзивно решење у кораку 1 враћа тачке сортиране по њиховим y координатама. То ће дати две сортиране листе тачака које само треба спојити (линеарна временска функција) у кораку 4 да би се добила комплетна сортирана листа. Стога ревидирани алгорита укучује следеће измене: Корак 1: Step 1: Подели скуп на ..., и рекурзивно израчунај раздаљину у сваком од делова, враћајући тачке у скупу сортиране y координати. Корак 4: Спојити две сортиране листе у времену O(n). Као што се види процесом спајања сада доминирају кораци линеарног времена чиме се добија O(nlogn) алгоритам за налажење најближег пара из скупа тачака у равни.
15 Проблем Ханојских кула
Постоји n дискова различите величине и три клина. Дискови су постављени на леви клин редоследом који је одређен њиховом величином. Најмањи је на врху, док је највећи на дну. Циљ игре је да се сви дискови преместе са левог клина.
15.1 Правила
1) У савом кораку може да се премести само један диск
2) Само диск који је на врху може да се премешта.
3) Сваки диск може да се постави само на већи диск.
15.2 Решење
Интуитивна идеја
Да би преместили највећи диск са левог клина на средњи, прво треба преместити мање дискове на десни клин. Пошто је највећи диск премештен, мањи дискови могу да се преместе са десног на средњи клин.
15.3 Рекурентност
Претпоставимо да је n број дискова.
Да би преместили n дискова са клина а на клин b потребно је:
1) за n>1 треба премстити n-1 дискова са клина a на клин c
2) преместити n-и диск са клина а на клин b
3) за n>1 преместити n-1 дискова са клина c на клин a
15.3.1 Псеудокод
void hanoi(n,src,dst){
if (n>1)
hanoi(n-1,src,pegssrc,dst});
print "move n-th disc from src to dst";
if (n>1)
hanoi(n-1,pegssrc,dst},dst);
}
==
2 Анализа
==
Анализа је тривијална. <math> T(n) = 2T(n-1) + O(1) = O(2^n)</math>
<noinclude>
----


Predikatska logika

Predikatska logika je formalni sistem u koji proširuje uvođenjem , i . Predikati omogućavaju izražavanje svojstava i odnosa između objekata, dok kvantifikatori omogućavaju iskazivanje tvrdnji o svim ili nekim objektima u datom domenu. Predikatska logika se koristi u mnogim oblastima, uključujući , i .
1 Istorija
Razvoj predikatske logike započet je u 19. veku radovima logičara poput i . Kasniji doprinosi i u delu ''Principia Mathematica'' postavili su temelje savremenoj formalnoj logici.
2 Elementi
Predikatska logika koristi sledeće osnovne elemente:
Predikati – funkcije koje definišu svojstva ili odnose među objektima (npr. ''je veći od'', ''je crveno'').
Varijable – simboli koji predstavljaju objekte u domenu (npr. ''x'', ''y'').
Kvantifikatore – univerzalni (∀) i egzistencijalni (∃) kvantifikatori za iskazivanje opštih ili egzistencijalnih tvrdnji.
Logičke veze – uključujući (∧), (∨), (¬) i (→).
3 Primer
Sledeći izraz u predikatskoj logici:
: ∀x (Čovek(x) → Smrtan(x))
se može interpretirati kao: ''"Za sve ''x'', ako je ''x'' čovek, onda je ''x'' smrtan."''
4 Primena
Predikatska logika je ključna u mnogim disciplinama:
Matematika – formalizacija teorema i dokaza.
Računarstvo – jezici formalnih specifikacija, i .
Filozofija – analiza jezika i struktura argumentacije.


Борци с територије Општине Горњи Милановац пали у Српско-турским ратовима (1876-1878)
Састав Ибарске војске која се борила у Првом (познатом и као „Јаворски”) (1876-1877) и Другом Српско-турском рату (1877-1878), чиниле су Чачанска, Рудничка, Ужичка и Шабачка бригада и Добровољачки кор. Војници су регрутовани из народа, без посебне обуке, према старости разврстани у I, II и III класу.
Ибарска војска борила се на два ратишта: јаворском и јужноморавском. Јаворско се протезало од Соколовице код Новог Пазара до Шаргана на Тари. Епитафи војницима палим на јужноморавском ратишту најчешће помињу Адровац, Самоково, Шиљеговац, Шуматовац, Кревети, Суви рт, Прокупље, Врање, Златокоп, Ниш, Борје, Велики извор и Радешки вис. Према епитафима, прве су жртве пале 24. јуна 1876. на Јавору, Кладници и Пазару, а последње 22. јануара 1878. године на Самокову код Куршумлије.
Натписи уклесани на надгробнике и крајпуташе представљају драгоцена историјска сведочења:
1 Бољковци
Споменик Алексију Илићу (†1876) ''(Бољковци, крај пута)'': Приђи те прочитај овај спомен. Показује војника II класе АЛЕКСИЈА ИЛИЋА из Бољковаца кои поживи 25 [година] а погибе у рату на Кладници 19. јула 1876 г. Овај спомен подиже му тужна мати његова једином сину 29. јуна 1878. г.
2 Брђани
Споменик Милосаву Вујовићу (†1876) ''(Брђани, крај пута)'': Мила браћо и читаоци приђите прочитајте спомен војника III чете II бат. Рудничке бриг. МИЛОСАВ ВУЈОВИЋ рођ[ен] 1845. из Брђана поживио 25. г. а погибе на Кладници борећи се са Турцима 1876. [године]. Споменуше га зет Сретен Јоваше[вић] из Прељ[ине] [и] јаран Тешић из Брђана 1895. г.
Споменик Крсти Топаловићу (†1876) ''(Брђани, крај пута)'': Овај спомен показује војника КРСТУ ТОПАЛОВИЋА калфу терзијск[ог] род[ом] из Брђана. Он је био у трнавском батаљону чачаске 1-бригаде. Кои е у 25. год[ини] свог живота [погинуо] за крст часни и за отаџбинство на Погледи 21. авгус[та] 1876. Овај спомен сродника палог вој[ника] [подигоше] ковачи Милановца 21. августа 1877. г.
Споменик Милану Дамљановићу (†1876) ''(Брђани, крај пута)'': Овај спомен показује војника II чете III батаљина I позива наро[дне] војск[е] МИЛАНА ДАМЉАНОВИЋА из Луњевице ср[ез] Таковски. Поживи 26 [година] па погибе борећи се са турцима на Јавору 1876. г. Овај спомен му подиже кћи Ката и зет Лазар Тодосијевић.
Споменик Милији Глишићу (†1876) ''(Брђани, крај пута)'': Овај спомен показује десетара II чете II батаљона народне војске рудничке бригаде МИЛИЈУ ГЛИШИЋА из Брђана. Поживи 25 г. а погибе борећи се са Турцима на Кладници 24. јуна 1876. г. Овај му спомен подиже синовац Василије 6. маја 1906. г.
3 Брезна
Споменик Јовану Васовићу (†1876) ''(Брезна, црквена порта)'': ЈОВАН ВАСОВИЋ из Брезне. [Поживе] 33 г. Погибе у рату на Јавору 1876. г. Спомен подиже му син Милош.
4 Брусница
Споменик Симеону Јелићу (†1876) ''(Млаковац, крај пута)'': СИМЕОН ЈЕЛИЋ из Бруснице. Поживи 31 годину. Погинуо у Јаворском рату 1876. г. Споменик му подиже жена Милева и син Милан.
5 Семедраж
Споменик Стевану Марковићу из Семедражи (†1876) ''(Брђани, засеок Парлог)'': Спомен овај подиже му Никола Марковић.


Село Кривача, списак изгинулих и помрлих у ратовима (1912-1918) сегмент је из „Споменице изгинулих и помрлих Драгачеваца у ратовима за ослобођење и уједињење 1912-1918” Милисава Д. Протића-Гучанина, објављене у књизи „Драгачево и његови славни синови” 1940. године.
1 Списак
# Варагић Драгић, редов, умро у К. Митровици 24. фебруара 1913.
# Варагић Јездимир, регрут, умро у возу испред Сталаћа 18. маја 1915.
# Варагић Тихомир, редов, пог. на Говедарнику 10. јула 1913.
# Варагић Милан, обвезн. X п. I поз, умро у чачанској болници 20. децембра 1914.
# Варагић Милован, регрут, умро у приштинској болници 10. фебруара 1915.
# Варагић Светомир, регрут, умро преко Албаније 1915.
# Варагић Станиша, умро код куће од ратних последица 12. новембра 1918.
# Виторовић Милорад, умро у ропству у Хаисгрину - Чешка 12. јануара 1916.
# Виторовић Тадија, послед. одбрана младића, умро код куће од тифуса 4. марта 1915.
# Ивановић Велисав, пог. турско-бугарског рата 1912.
# Ивановић Милорад, обвезн. посл. одбране, умро код куће 1913.
# Ивановић Милутин, регрут, пог. на Ветернику 1915.
# Илинчић Бошко, регрут, умро у Бизерти августа 1918.
# Миловановић Светислав, обвезн. I поз. ст. кадра, пог. у срп-аустр. рату 22. октобра 1914.
# Славковић Јован-Јовић, умро у аустријском ропству у Хаисгрину 1917.
# Танасковић Младен, умро у ваљевској болници од рана 15. августа 1914.
# Тимотијевић Агатон, редов послед. одбр, нераспоређен, умро у Петроварадину 3. фебруара 1918.
# Тимотијевић Величко, обвезн. III поз, умро од тифуса код куће 2. марта 1915.
:''Општина Вичка, податке прикупио прота Милан Недовић из Виче.''


Борци с територије Општине Ивањица пали у Српско-турским ратовима (1876-1878)

Борци с територије Општине Ивањице пали у Српско-турским ратовима (1876-1878)
Састав Ибарске војске која се борила у Првом (познатом и као „Јаворски”) (1876-1877) и Другом Српско-турском рату (1877-1878), чиниле су Чачанска, Рудничка, Ужичка и Шабачка бригада и Добровољачки кор. Војници су регрутовани из народа, без посебне обуке, према старости разврстани у I, II и III класу.
Ибарска војска борила се на два ратишта: јаворском и јужноморавском. Јаворско се протезало од Соколовице код Новог Пазара до Шаргана на Тари. Епитафи војницима палим на јужноморавском ратишту најчешће помињу Адровац, Самоково, Шиљеговац, Шуматовац, Кревети, Суви рт, Прокупље, Врање, Златокоп, Ниш, Борје, Велики извор и Радешки вис. Према епитафима, прве су жртве пале 24. јуна 1876. на Јавору, Кладници и Пазару, а последње 22. јануара 1878. године на Самокову код Куршумлије.
Натписи уклесани на надгробнике и крајпуташе представљају драгоцена историјска сведочења:
1 Ерчеге
Споменик Антонију Јелићу (†1876) ''(Рокци – Палибрчки гроб)'': Овај биљег показује АНТОНИЈА ЈЕЛИЋА из Ерчега бивши поднаредника пешачког који је 3. године у Београду Књаза и отечество своје усрдно и верио одслужијо затим постављеног за водника треће чете баталиона Моравичког Ужичке бригаде прве кл[а]се који у 25. год. живота у најлепшем цвету младости своје за Славу и слободу и независност Србије јуначки борећи се против Турака погибе у борби на Калипољу 24. јуниа 1876. год. Бог да му душу прости. Овај вјечни споменик подигоше му браћа његова Филип и Радош.
Споменик Милану Спасовићу (†1879) ''(Рокци – Палибрчки гроб)'': Приђи ближе путниче не зажали труда мало што ћеш стати и читати. Слова ова показују вредног Срба овог МИЛАНА СПАСОВИЋА из села Ерчега бивш. каплара и писара 1. пољске батерије који се у рату 1876. и 1877. г. за владе Књаза Милана Обрено. против Турака храбро боријо и у 22. години живота у најлепшем цвету младости своје умро у месту рођења свог 1. јануара 1879. год. Бог да му душу прости. Овај спомен подиже му отац Марко и стриц Коста. Писа и реза Миле Поповић Свештице.
2 Лиса
Споменик Ранку Боровићу (†1878) ''(Лиса – Вратоње)'': Овај биљег показује тело почив РАНКА БОРОВИЋА из Села Лисе којие у 21-ој год Живота свог храбро борећисе у рату погинуо на соколовини од турака 1 Јануара 1878. г. Бог даму душу прости Свом једином незаборављеном сину знак овај подигоше ожалошћени родитељи Спасое и Стојка.
Споменик Јовану Комадини (†1878) ''(Свештица)'': Овај спомен показује храброг србског војника ЈОВАНА сина Марка Комадине из села Лисе ср. драгачевског окр. чачанс. Бив. артиљериског наредника кои је у србско-турском рату год. 1876. у кланцима јаворски позиција на опште задовољство јуначки се боријо и 1878. г. у бившој Старој а сада Новој Србији такође за ослобођење и независност миле своје имовине Србије у званом месту Прокупљу 14. јануара 1878. год. у 28ој год. живота у најлепшем цвету своје младости на велику тугу и жалост фамилије, особито старог му оца Марка драгоцени му живот у рату окончо.
3 Луке
Споменик Добрици Коџопељи (†1876) ''(Луке – Лазовићи)'': Споменик ДОБРИЦЕ КОЏОПЕЉЕ из Села Лука бив храброг војника прве класе баталијона Драгачевскоg Који је у 32 год. живота свог за отаџбину славу и слободу Браће своје Срба борећи се против турака погинуо у битци на Шиљеговцу округ Алексиначки 15. Сеп. 1876. г. Овај знак подижему ожалошћена мати Пауна Написо Мијаило Поповић из Свештице под управљањем стараоца Его Андрије Коџопеље.
Споменик Луки Лазовићу (†1876) ''(Лазовића брдо – крај пута)'': То ест человек. Оваи знак показуе име ЛУКА ЛАЗОВИЋ бившег воиника II чете I класе драгачевск. Баталиона кои поживи 22. г а погибе на шиљеговцима октобр. 1876-г борећи се с турцима за веру и отечество Србско (...) Оваи спомен подижему брат јовица и мајка неранџа.
Споменик Петру Зучковићу (†1878) ''(Ивањица – крај Ветеринарске станице)'': Овај спомен показује тјело ПЕТРА ЗУЧКОВИЋА из села Лука бив. храброг војника друге класе баталиона Драгачевског који је у 40-тој год. Живота погинуо за славу и слободу Срба. Јуначки борећи се противу Турака погинуо у рату на Соколовици 1. јануара 1878. год. Бог да му душу прости. Знак овај подигоше му брат Сретен и синови Коста, Богосав и Милован. Писао Ми[јаило] Поповић из Свјештице.
4 Осоница
Споменик Аћиму Милутиновићу (†1878) ''(Ивањица – крај Ветеринарске станице)'': Овај биљег показује вјечну успомену почивш. АЋИМА МИЛУТИНОВИЋА из села Осонице, бившег храброг војника друге кл. Баталиона Драгачевског коњ. У 45. годин. живота свог јуначки борећи се против Турака за славу и слободу своје браће Срба, погинуо у битки код Пазара 1. јануара 1878. год. Бог да му душу прости. Овај спомен подигоше синови Владимир и Никола са млађом браћом.


Село Горњи Дубац, списак изгинулих и помрлих у ратовима (1912-1918) сегмент је из „Споменице изгинулих и помрлих Драгачеваца у ратовима за ослобођење и уједињење 1912-1918” Милисава Д. Протића-Гучанина, објављене у књизи „Драгачево и његови славни синови” 1940. године.
1 Списак
# Гиловић Владимир, редов II ч. I б. X п. I поз, пог. на Дренку 18. јуна 1913.
# Драгићевић Радомир, редов, умро као тешки рањеник код куће 1915.
# Драгићевић Чедомир, редов II ч. I б. XI п, умро као рањеник у Врањи 1. децембра 1914.
# Дукић Будимир, редов, пог. на Буковој Глави 28. јуна 1913.
# Дукић Добросав, редов II ч. I б. XI п, пог. на Ветернику 28. фебруара 1917.
# Дукић Сретен, редов I ч. I б. XI п, пог. на Ветернику 13. фебруара 1917.
# Луковић Милован, редов II ч. I б. X п. III поз, умро у Загребу 6. марта 1916.
# Милинковић Вељко, редов, умро у Краљеву 24. августа 1912.
# Милутиновић Драгиша, редов, умро код куће на боловању 13. фебруара 1915.
# Пантелић Борисав, редов II ч. I б. XI п. I поз, пог. на Дренку 17. јуна 1913.
# Пантелић Јеленко, редов I ч. I б. X п, умро у Чачку 11. новембра 1918.
# Пантелић Мирко, редов I поз, пог. на Дренку 18. јуна 1913.
# Пантелић Миљко, редов II ч. I б. X п, пог. на Камен-Долу 24. новембра 1914.
# Петровић Живко, редов II ч. I б. X п, пог. на Камен-Долу 24. новембра 1914.
# Радовић Светозар, редов IV ч. XI п, пог. на Ветернику 7. јула 1917.
# Рудинац Јездимир, редов I пољ. батерије, умро у Чачку 30. октобра 1918.
# Рудинац Љубомир, п. наредник II ч. I б. X п. I поз, умро на Крфу 1917.
# Рудинац Миливоје, редов II ч. III б. X кадров. пука, пог. на Конатици 1914.
# Рудинац Светозар, редов, нестао 1914.
# Сретеновић Будимир, редов II ч. I б. X п. II поз, умро у Драчу 1915.
:''Општина Дубачка, податке прикупио претседник општине Војислав Велисављевић и Мил. В. Милинковић, деловођа.''


Особине декларативног програмирања

Декларативно програмирање је парадигма програмирања у којој програмер дефинише ''шта'' програм треба да уради, а не ''како'' да то постигне. За разлику од императивног програмирања, које се фокусира на кораке које треба предузети како би се решио проблем, декларативно програмирање подразумева описивање жељеног резултата, док све остало (како ће то бити постигнуто) остаје на окружењу или . Ова парадигма омогућава програмерима да се фокусирају на проблеме вишег нивоа, чинећи код лакшим за разумевање, одржавање и проширење.
1 Кључне особине декларативног програмирања
1.1 Апстракција контроле
У декларативном програмском окружењу, детаљи о томе ''како'' ће се нешто извршити, као што су редослед операција или конкретне имплементације, апстраховани су од самог програма. Програмер се не бави техничким аспектима имплементације, већ само дефинише крајњи резултат. На пример, у CКЈУЕЛ-у програмер описује који подаци су му потребни, а не начин на који ће се подаци из базе обрађивати или сортирати.
1.2 Оптимизација извршења
Декларативни језици често остављају могућност оптимизације кода на интерпретатору или окружењу у којем програм ради. На пример, СКЈУЕЛ упити могу бити аутоматски оптимизовани од стране система за управљање базама података (ДБМС), што програмеру омогућава да се фокусира на формулисање упита, а не на детаљну оптимизацију.
1.3 Фокус на декларацију стања, а не на процедуру
Декларативни приступ подразумева да се програм не бави директним описивањем како ће се нешто десити (нпр. каква ће бити процедура за решавање проблема), већ само описује какво стање или резултат треба да буде постигнут. То омогућава већу флексибилност и лакшу измену у будућности.
1.4 Богатство израза и декларација
У декларативним језицима, као што су СКЈУЕЛ и ХТМЛ, изрази који дефинишу структуру и однос података су веома лако разумљиви и лако се модификују. На пример, у СКЈУЕЛ-у програмер дефинише упит као скуп услова и операција над подацима, али не разматра ниједан конкретан корак који ће се користити за претраживање или сортирање података.
2 Декларативни језици
# СКЈУЕЛ – Језик за рад са релационим базама података, где програмер описује који подаци су му потребни (упити), али не разматра како се подаци претражују или сортирају.
# ХТМЛ – Језик за структуру веб страница, где се описује који елементи треба да буду на страници, али не и како ће се ти елементи рендеровати.
# Функционално програмски језици као што су Хаскел и Ерланг – користе декларативне принципе за рад са функцијама и подацима, често избегавајући промену стања.
3 Предности и недостаци декларативног програмирања
3.1 Предности:
* Читљивост и лакше одржавање: Код је лакши за разумевање и одржавање, јер програмер описује само шта жели да постигне, не бавећи се детаљима имплементације.
* Лакша оптимизација: Окружје може аутоматски оптимизовати код (на пример, СКЈУЕЛ упити се аутоматски оптимизују за брже извршавање).
3.2 Недостаци:
* Мала контрола над перформансама: Програмер не може увек да контролише како ће бити изведене оптимизације, што може довести до неефикасности у неким случајевима.
* Сложеност за комплексне задаке: За веома сложене и нестандардне задатке, декларативни приступ може бити мање флексибилан и теже примењив.
+, Карактеристика, Декларативно програмирање, Императивно програмирање
Опис процеса, Описује ''шта'' треба урадити, не и ''како'', Описује конкретне кораке који требају бити предузети
Фокус, На резултату и стању система, На корацима који воде до резултата
Примена, Управљање подацима, графички интерфејс, web development, Системи са високом потребом за контролом извршења
Контрола над перформансама, Ограничена, оптимизацију ради окружење, Велика контрола над перформансама
4 Пример декларативне парадигме
* СКЈУЕЛ : За извођење упита у релационим базама података, програмер може написати:
<syntaxhighlight>
SELECT * FROM корисници WHERE старост > 18;
</syntaxhighlight>Овде се дефинише ''шта'' треба добити (сви корисници старији од 18), али не и како ће упит бити извршен.
* ХТМЛ : Структура веб странице може бити изражена као:
<syntaxhighlight>

<title>Моја веб страница

Добродошли!
</syntaxhighlight>Овде, програмер дефинише структуру странице, али не разматра начин на који ће веб прегледач рендеровати страницу.
5 Историјат декларативног програмирања
Декларативно програмирање почиње са раним логичким језицима као што је ЛИСП из 1958. године, који је омогућио већу апстракцију у програмирању. Током 1970-их, Пролог (1972) је популаризовао декларативни приступ у решавању логичких проблема. У 1980-им, развој СКЈУЕЛ језика за рад са базама података додатно је унапредио декларативне методе. Данас се декларативни принципи користе у широком спектру области, од веб развоја (ХТМЛ, ЦСС) до управљања подацима и функционалног програмирања.
6 Погледај такође
* Императивно програмирање
* СКЈУЕЛ
* Функционално програмирање


Село Толишница, списак изгинулих и помрлих у ратовима (1912-1918) сегмент је из „Споменице изгинулих и помрлих Драгачеваца у ратовима за ослобођење и уједињење 1912-1918” Милисава Д. Протића-Гучанина, објављене у књизи „Драгачево и његови славни синови” 1940. године.
1 Списак
# Васовић Исаило, редов 2 ч. 2 б. X п, умро у Чачку 16. новембра 1918.
# Дмитровић Ђорђе, редов 2 ч. X п, умро код куће на боловању 30. новембра 1915.
# Котуровић Јеврем, редов 2 ч. 1 б. X пука, умро у Краљеву 8. новембра 1918.
# Маринковић Крстомир, редов, 2 ч. 1 б. X п., умро код куће на боловању 2 марта 1915.
# Маринковић Секула, редов 2 ч. 1 б. X п., умро у Драчу 22 децембра 1912.
# Маринковић Танасије, редов 2 ч. 1 б. X п, умро код куће на боловању 31. марта 1915.
# Милетић Витомир, редов 2 ч. 1 б. X. п, пог. на Камен-Долу 25. новембра 1914.
# Милинковић Сава, редов митраљ. оделења, умро у Чачку 14. новембра 1918.
# Николић Глигорије, редов 2 ч. 1 б. X п. 1 позива, умро у Младеновцу 1914.
# Николић Исидор, редов 2 ч. 1 б. X п. 1 поз, умро у Приштини 2. фебруара 1915.
# Николић Миладин, редов 2 ч. 1 б. X п. 1 поз, умро у Приштини 1. фебруара 1915.
# Николић Срећко, каплар 2 ч. 1 б. X п, пог. у Косовици - Албанија 4. новембра 1915.
# Николић Тихомир, редов 3 ч. 3 б. XII п, умро у Мачку 11. новембра 1918.
# Радовић Милорад, редов, умро код куће на боловању 24. септембра 1913.
# Сарић Владета, п. наредник муниц. кол. хауб. пука, умро у Мустаф-Паши 21. јануара 1913.
# Сарић Владимир, редов 2 ч. 1 б. X п. 2 поз, пог. на Власини 9. јула 1913.
:''Општина Дубачка, податке прикупио претседник општине Војислав Велисављевић и Мил. В. Милинковић, деловођа.''


Село Доњи Дубац, списак изгинулих и помрлих у ратовима (1912-1918) сегмент је из „Споменице изгинулих и помрлих Драгачеваца у ратовима за ослобођење и уједињење 1912-1918” Милисава Д. Протића-Гучанина, објављене у књизи „Драгачево и његови славни синови” 1940. године.
1 Списак
# Зечевић Милован, редов резерв. крушевач. трупе, умро у Сиднабагах 1916.
# Караџић Вукомир, редов 2 ч. 1 б. X п. 2 поз, умро на боловању код куће 23. априла 1915.
# Караџић Миливоје, редов 2 ч. 2 б. XI п. 1 позива, умро у Приштини 27. јануара 1915.
# Караџић Милорад, редов 2 ч. 1 б. XI п, умро у чачанској болници 20. фебруара 1914.
# Караџић Милосав, п. наредник 1 ч. 3 б. X п. 2 поз, пог. на Буковој Глави 23. јуна 1913.
# Караџић Миљојко, редов 2 ч. 1 б. X п. 1 поз, умро у Дордрехту - Холандија 7. фебруара 1919.
# Котуровић Александар, редов 2 ч. 1 б. X п. 1 поз, пог. на Дренку 18. јуна 1913.
# Котуровић Гојко, редов 2 ч. 1 б. X п. 1 поз, умро код куће на болов. 3. марта 1915.
# Котуровић Срећко, редов 2 ч. 1 б. X п. 1 позива, умро код куће на боловању 6. марта 1915.
# Милић Витомир, редов 2 ч. 1 б. X п. 1 поз, умро у ропству 1916.
# Милић Вукојица, редов 2 ч. 1 б. X п. 1 поз, умро у ропству 1916.
# Милић Милован, редов 2 ч. 1 б. X п. 2 поз, умро у ропству у Мађарској 1916.
# Милић Радомир, п. наредник 2 ч. 1 б. X кадр. пука, умро на боловању 2. марта 1915.
# Николић Мијаило, нестао 1914.
# Стојановић Драгољуб, наредник 4 ч. 1 б. X п. 2 поз, умро од задобијених рана у Рашкој 8. октобра 1912.
# Тадић Благоје, редов 2 ч. 1 б. X п. 3 поз, умро у К. Митровици 11. новембра 1912.
# Тадић Богдан, редов, умро код куће на боловању 25. јуна 1914.
# Тадић Велимир, редов 2 ч. 1 б. X п, 2 поз, умро на боловању код куће 1917.
# Тадић Јово, обешен од Аустријанаца у Гучи 26. децембра 1915.
# Тадић Милосав, редов 2 ч. I б. X п. 3 поз, умро у Браун-Нау 1916.
# Тадић Мирко, редов, умро у ропству 1916.
:''Општина Дубачка, податке прикупио претседник општине Војислав Велисављевић и Мил. В. Милинковић, деловођа.''


Село Живица, списак изгинулих и помрлих у ратовима (1912-1918) сегмент је из „Споменице изгинулих и помрлих Драгачеваца у ратовима за ослобођење и уједињење 1912-1918” Милисава Д. Протића-Гучанина, објављене у књизи „Драгачево и његови славни синови” 1940. године.
1 Списак
# Живковић Михаило, обвезн. X п. III поз, умро у Пожаревцу 15. јануара 1915.
# Зечевић Владимир, обвезн. X пука, умро од тифуса код куће на боловању 9. марта 1915.
# Зечевић Милинко, обвезн. X п. I поз, умро код куће на боловању 1915.
# Зечевић Спасоје, пог. у срп-аустр. рату 5. августа 1914.
# Јечменица Савко, редов II поз, умро у ужичкој болници 1. октобра 1914.
# Јовичић Антоније, редов II поз, умро у ужичкој болиици 16. августа 1915.
# Јовичић Илија, обвезн. V прекобр. п. I поз, пог. на Крупњу 8. септембра 1914.
# Јоровић Адам, регрут, нестао преко Албаније 20. новембра 1915.
# Јоровић Драгомир, редов X п. I поз, пог. у Дреновом Селу 8. септембра 1914.
# Јоровић Станиша, обвезн. X п, умро у повлачењу 1915.
# Матовић Аврам, послед. одбр. стараца, умро на боловању код куће 2. јуна 1915.
# Матовић Бранислав, обвезн. X п. I поз, умро на боловању код куће 9. фебруара 1915.
# Матовић Витомир, обвезн. X п. I поз, пог. у повлачењу 1915.
# Матовић Јанићије, пог. у срп-аустр. рату 3. августа 1914.
# Матовић Крстомир, редов II ч. X п, пог. у Тирани 10. јануара 1913.
# Матовић Милан, обвезн. X п. I поз, умро у Драчу 1. фебруара 1913.
# Микановић Драгић, редов, умро код куће на боловању 26. маја 1913.
# Микановић Драгомир, редов X п. II поз, умро у чачанској болници 12. децембра 1914.
# Микановић Милисав, обвезн. X п. I поз, умро у ваљевској болници 6. фебруара 1915.
# Микановић Милош, обвезн. последње одбране, умро код куће на боловању 11. марта 1915.
# Микановић Миљко, обвезн, умро на боловању код куће 1915.
# Милић Драгутин, обвезн, пог. на Мачковој Глави 1914.
# Милутиновић Божидар, редов X п. 1 поз, умро у возу пред Сталаћем 31. децембра 1914.
# Милутиновић Гвозден, редов II ч. X п, пог. код Вишеграда 10. октобра 1914.
# Милутиновић Милутин, регрут, пог. код Књажевца 6. октобра 1915.
# Милутиновић Средоје К, кадровац XI п, умро од тифуса код куће 16 фебруара 1915.
# Милутиновмћ Тихомир Р, обвезн. X п. I поз. телеф. одел, умро код куће 25. фебруара 1915.
# Мишовић Милисав, редов III ч. XI п, пог. на Гучеву 15. новембра 1914.
# Мишовић Милош, редов III поз. X п, умро на боловању код куће 1. децембра 1912.
# Мишовић Тихомир, п. наредник X п. I поз, пог. на Шапцу 13. новембра 1915.
# Николић Милан, пог. у срп-аустр. рату 3. августа 1914.
# Павловић Кузман, обвезн. II пољ. батер. II поз, умро у ваљевској болници 8. марта 1915.
# Павловић Савко, обвезн. X п. III поз, умро пред Лозницом 8. децембра 1914.
# Пајовић Милош, дечак, убијен на Велесу-Гуча од Аустријанаца 10. новембра 1916.
# Солдатовић Витомир, обвезн. X п. III поз, умро у Нишу 24. марта 1915.
# Стефановић Секула, редов III ч. X п, умро од тифуса у Приштини 8. марта 1915.
# Страњанац Вукосав, обвезн. X п. III поз, умро у пожаревачкој болници 15. марта 1915.
# Страњанац Милојко, обвезн. X п. II поз, умро на боловању код куће 16. марта 1915.
# Страњанац Милутин, регрут I ч. X п, умро преко Албаније 14. јануара 1915.
# Страњанац Обрад, редов III поз, умро код куће на боловању 30. јануара 1915.
# Страњанац Раденко, редов X п. I поз, умро код куће на боловању 30. јануара 1915.
# Страњанац Радосав О, регрут, умро код куће на боловању 17. јануара 1915.
# Танасковић Живко, обвезн. X п. III поз, умро у См. Паланци 25. марта 1915.
:''Општина Горачићска, податке прикупио прота Милан Недовић из Виче.''


Село Марковица, списак изгинулих и помрлих у ратовима (1912-1918) сегмент је из „Споменице изгинулих и помрлих Драгачеваца у ратовима за ослобођење и уједињење 1912-1918” Милисава Д. Протића-Гучанина, објављене у књизи „Драгачево и његови славни синови” 1940. године.
1 Списак
# Дивљак Мијаило, п. наредник I ч. I б. X п. I поз, пог. на Катунцу 1916.
# Дивљак Милисав, редов I ч. I б. X п. III поз, умро у Чачку 14. јула 1915.
# Дивљак Тихомир, редов I ч. I б. X п. III поз, умро у Ашаху 18. октобра 1918.
# Јаковљевић Михаило, редов I ч. I б. X п. I позива, пог. на Власини 7. јула 1913.
# Јовашевић Аксентије, редов I ч. I б. X п. III позива, умро код куће на боловању 1915.
# Јовашевић Ивко, редов I ч. I б. X п. I поз, пог. на Коритнику 17. септембра 1913.
# Јовашевић Љубомир, редов резервиста крушевач. трупе, умро преко Албаније 1915.
# Јовашевић Милија, редов I ч. I б. X п. I поз, пог. на Причиновићу 6. септембра 1914.
# Јовашевић Ратко, редов I ч. I б. X п. III поз, пог. на Смедереву 1914.
# Јовашевић Чедомир П, редов I ч. I б. X п. III поз, пог. на Смедереву 1914.
# Ковачевић Никола, редов 4 бат. Шумад. пољс. пука, пог. у Лопатници 1915.
# Луковић Витомир, редов I ч. I б. X п. III поз, умро у ропству 1916.
# Милинковић Здравко, редов I ч. IV б. X п. I позива, пог. на Шапцу 6. августа 1914.
# Митровић Миладин, каплар IV ч. I б. X кадров. пука, пог. у Рековцу 16. октобра 1915.
# Обреновић Боривоје, п. наредник I ч. I б. X п. I поз, умро код куће на боловању 1920.
# Обреновић Велимир, п. наредник комитске чете мајора Поповића, пог. на Јевремовцу 24. октобра 1914.
# Обреновић Деспот, редов I ч. I б. X п. III поз, умро у ропству 1916.
# Обреновић Драгутин, каплар I ч. I б. X п. I поз, пог. на Катунцу 1916.
# Обреновић Живан, редов I ч. I б. X п. III поз, умро у Крагујевцу 14. јула 1912.
# Обреновић Јеремија, редов резерв. крушев. трупе, умро преко Албаније 1915.
# Обреновић Коста, редов резервиста крушев. трупе, умро преко Албаније 1915.
# Обреновић Миленко, редов I ч. I б. XI п. I поз, пог. на Степојевцу 14. новембра 1914.
# Обреновић Миливоје, редов I ч. I б. X кадров. пука, умро у Приштини 17. јануара 1915.
# Обреновић Милован, редов резерв. крушев. трупе, умро на боловању код куће 1916.
# Обреновић Милосав, редов послед. одбране стараца, умро на боловању код куће 1915.
# Обреновић Момчило, ђак VI разр. гимназије, умро преко Албаније 1915.
# Савковић Бранко, редов I ч. I б. X п. II поз, умро у Битољу 7. фебруара 1915.
# Савковић Илија, каплар I ч. 1 б. V прекобр. пука, умро на боловању код куће 18. октобра 1918.
# Томић Милан, п. наредник II ч. II б. X п. II поз, умро на боловању код куће 1915.
:''Општина Дучаловска, податке прикупио претседник општине Ранко Љ. Поповић и Вељко Р. Ђунисијевић, деловођа.''