Algoritme Dhe Struktura E Te Dhenave LIBRI

April 17, 2017 | Author: Iliriana S. Kukaj | Category: N/A
Share Embed Donate


Short Description

Download Algoritme Dhe Struktura E Te Dhenave LIBRI...

Description

Algoritmet dhe strukturat e të dhënave

Avni Rexhepi

Prishtinë – 2014 1

Avni Rexhepi

2

Algoritmet dhe strukturat e të dhënave

Parathënie Ky libër u dedikohet studentëve të “Fakultetit të Inxhinierisë Elektrike dhe Kompjuterike”, të Universitetit të Prishtinës, mirëpo natyrisht se mund të përdoret edhe nga të gjithë të interesuarit për këtë lëmi. Ky është botimi i parë dhe vërejtjet e sygjerimet e lexuesve janë të mirëseardhura. Të gjithë shembujt në libër, janë marrë më shumë për qëllime shkollore, për të shërbyer si udhëzime në realizimin e detyrave të caktuara, e jo si projekt i gatshëm për përdorim apo pjesë të ndonjë projekti. Emrat e përdorur si shembuj janë të rastit dhe përjashtohet mundësia e keqpërdorimit të qëllimshëm. Për vërejtjet dhe sygjerimet mund të na kontaktoni përmes postës elektronike, në adresën: [email protected].

3

Avni Rexhepi

4

Algoritmet dhe strukturat e të dhënave

Hyrje Algoritmet dhe strukturat e të dhënave janë “veglat/pajisjet” e programerëve për kryerjen e punëve. Ato definohen dhe përdoren (në programe) për të realizuar llogaritjet e nevoshme për zgjidhjen e problemeve nga jeta reale, përmes përdorimit të programeve dhe kompjuterëve. Algoritmet na mundësojnë kryerjen e operacioneve/veprimeve llogaritëse në një mënyrë të caktuar. Këto llogaritje i bëjnë me të dhënat e thjeshta ose me “strukturat e të dhënave” të cilat na shërbejnë që të krijojmë “objektet abstrakte” në programe, të cilat pasqyrojnë në mënyrën më të mirë të mundshme “objektet konkrete” (reale, fizike) nga bota reale dhe jeta e përditshme.

Algoritmet Çka është algoritmi? Algoritmi është procedurë hap pas hapi, për zgjidhjen e problemit. Algoritmi është proceudra e kryerjes së ndonjë detyre të caktuar. Algoritmi është idea prapa cilitdo program kompjuterik. Këto do të ishin disa prej definicioneve më të thjeshta lidhur me atë se çka është algoritmi. Përndryshe ekzistojnë edhe shumë definicione të tjera, të cilat në mënyra të ndryshme e japin shpjegimin ose mundohen ta sqarojnë se çka është algoritmi. Algoritmi definohet edhe si: Algoritmi është bashkësi e rregullave për kryerjen e llogaritjeve me dorë ose me ndonjë pajisje. Algoritmi është një procedurë e përcaktuar hap pas hapi për arritjen e një rezultati të caktuar. Algoritmi është një varg i hapave llogaritës që e transformojnë hyrjen në dalje. Algoritmi është një varg i operacioneve të kryera në të dhënat që duhet të jenë të organizuara në struktura të të dhënave. Algoritmi është një abstraksion i programit që duhet të ekzekutohet në një makinë fizike (modeli i llogaritjes), etj. Algoritmi më i njohur në histori daton që nga koha e Greqisë antike: ky është “Algoritmi i Euklidit” për llogaritjen e pjestuesit më të madh të përbashkët të dy numrave të plotë.

5

Avni Rexhepi Termi algoritëm konsiderohet të ketë ardhur nga emri i dijetarit islam, matematikanit arab Abū Ja’far ʿAbdallāh Muḥammad ibn Mūsā al-Khwārizmī, i cili jetoi në vitet 780-850 në Bagdad. Ai ishte një matematikan që shkroi për numrat indo-arab dhe ishte ndër të parët që e përdori zeron si “pozicion” në notacionin bazë të pozicioneve për numrat. Nga punimi i tij “Hisab al-jabr wa’l-muqabala”, që konsiderohet si libri i parë i shkruar për algjebrën, e ka prejardhjen termi algjebër. Al-Khwarizmi, i përkthyer në latinisht si “Algoritmi” ose “Algaurizin”, ishte matematikan, astronom dhe gjeograf gjatë perandorise Abaside (Kalifati Abasid, ishte kalifati i tretë islam që pasoi Profetin Muhamed) dhe ishte dijetar, studiues dhe shkencëtar në “Shtëpinë e diturisë/urtësisë” (Dār al-Ḥikma), në Bagdad. Në shekullin e dymbëdhjetë, përkthimet e punës së tij në latinishte për numrat indian prezentuan sistemin numerik pozicional decimal në botën përëndimore. Libri i tij “Përmbledhje e llogaritjeve me kompletim dhe balansim” prezentoi zgjidhjen e parë sistematike të ekuacioneve lineare dhe kuadratike. Në kohën e renesansës evropiane, ai konsiderohej si zbuluesi origjinal i algjebrës, edhe pse tash dihet se puna e tij bazohej në burime më të vjetra indiane dhe të greqisë antike. Edhe fjalët e mbetura prej punimeve të tij flasin për kontributin e tij në matematikë. Fjala algjebër, që rrjedhë prej fjalës “al-jabr”, që ishte njëri prej dy operacioneve që ai përdori për të zgjidhur ekuacionet kuadratike. Poashtu, termi “Algorism” dhe “Algorithm”, buron prej formës latine të emrit të tij. Punimi i tij në latinisht ishte quajtur “Algoritmi de numero indorum”. Në hyrje të librit të tij, ai kishte shkruar:

“Dashuria për shkencë… dashamirësia dhe përfillja të cilën Zoti e tregon për të diturit, ajo përpikmëri me të cilën ai i mbronë dhe përkrahë ata në sqarimin e paqartësive dhe eleminimi e vështirësive, më ka inkurajuar që të përpiloj një punim të shkurtër për llogaritjen me “al-jabr” dhe “almuqabala”, duke u kufizuar në atë që është më e lehta dhe më e dobishmja në aritmetikë.” (al-jabr do të thotë "kthim, restaurim", duke iu referuar procesit të largimit të pjesës së zbritur në anën tjetër të ekuacionit; al-muqabala është "krahasimi" dhe i referohet zbritjes/thjeshtimit të vlerave të njëjta në të dy anët e ekuacionit).

Abu Ja'far Abdallah Muhammad ibn Musa Al-Khwarizmi [rreth 780-850 në Baghdad]

Kur në ndonjë gjuhë programuese shkruajmë programe për kompjuter, ne në përgjithësi implementojmë metodën që është zbuluar (shpikur, krijuar) më parë, 6

Algoritmet dhe strukturat e të dhënave për zgjidhjen e ndonjë problemi, gjegjësisht algoritmin për zgjidhjen e problemit. Çdo program është ilustrim i ndonjë algoritmi. Kjo metodë është zakonisht e pavarur prej gjuhës programuese dhe prej kompjuterit të veçantë që do të përdoret dhe zakonisht është njësoj e përshtatshme për shumë kompjuterë dhe për shumë gjuhë programuese. Në fakt, më shumë është metoda sesa vetë programi kompjuterik që duhet të studiohet për të mësuar se si është duke u “atakuar” problemi. Termi algoritëm përdoret në shkencat kompjuterike për të përshkruar metodën e përshtatshme për zgjidhjen e problemit dhe për ta implementuar si program kompjuteri. Algoritmet janë “material” (lëndë e parë, lëndë pune) për shkencat kompjuterike. Ato janë objekti qëndror i studimit në të gjitha pjesët e kësaj fushe. Shumica e algoritmeve të rëndësishme përfshijnë metodat për organizimin e të dhënave të përfshira në llogaritje. Objektet e krijuara në këtë mënyrë quhen struktura të të dhënave dhe këto janë gjithashtu objekte qëndrore të studimit në shkencat kompjuterike. Prandaj, algoritmet dhe strukturat e të dhënave, shkojnë “dorë për dore” (së bashku). Pra, për të kuptuar algoritmet duhet studiuar edhe strukturat e të dhënave. Ka raste kur algoritmet e thjeshta “nxjerrin në pah” struktura të komplikuara dhe anasjelltas, algoritmet e komplikuara mund të përdorin struktura të thjeshta të të dhënave. Parimisht, do të studiohen dhe prezentohen tiparet (vetitë, karakteristikat) e shumë strukturave të të dhënave. Kur përdorim kompjuterin për të zgjidhur një problem, zakonisht ballafaqohemi me një numër të qasjeve të ndryshme të mundshme për zgjidhjen e problemit. Për problemet e vogla, rrallë herë është me rëndësi se cila qasje përdoret, përderisa e kemi atë që e zgjidhë problemin si duhet. Mirëpo, për problemet e mëdha (ose për aplikacionet ku duhet njënumër i madh i problemeve të vogla), shpejt motivohemi që të krijojmë metoda të cilat përdorin kohën dhe hapësirën (memorike) në mënyrë sa më efikase të mundshme. Arsyeja kryesore për studimin e dizajnit të algoritmeve është se kjo disciplinë na jep potencialin për të bërë kursime të shumta edhe deri në pikën e mundësimit të kryerjes së detyrave të cilat ndryshe do të ishte e pamundur të kryhen. Në një aplikacion ku procesohen miliona objekte, nuk është e pazakontë që të bëhet një program miliona herë më i shpejtë, duke përdorur një algoritëm të dizajnuar mirë. Kurse, investimi në blerjen e kompjuterit të ri me performansa më të mira, për të njëjtin problem, ka potencial të përshpejtimit me faktor prej vetëm 10 ose 100 herë. Dizajni i kujdesshëm i algoritmit është pjesë jashtëzakonisht efektive e procesit të zgjidhjes së problemeve të mëdha, në çdo sferë të aplikimit. Kur duhet zhvilluar një program jashtëzakonisht i madh ose i komplikuar, duhet “investuar” shumë përpjekje në të kuptuarit dhe definimin e problemit që duhet 7

Avni Rexhepi zgjidhur, menaxhimin e kompleksitetit dhe dekompozimin (zbërthimin) në nënprobleme të vogla të cilat mund të implementohen me lehtësi. Shpeshherë, shumë prej algoritmeve, janë të lehta për t’u implementuar pas dekompozimit. Mirëpo, në shumicën e rasteve, janë disa algoritme, zgjedhja e të cilave është kritike, sepse shumica e resurseve të sistemit do të shpenzohet në ekzekutimin e tyre. Pra, është me rëndësi të studiohen algoritmet themelore të cilat janë të dobishme për zgjidhjen e problemeve në një spektër të gjerë të sferave të aplikimeve. Shumë gjuhë programuese tani kanë libraritë e implementimeve të shumë algoritmeve themelore, si p.sh. STL (Standard Template Library) e C++-it, mirëpo ne do të mirremi me implementimin e versioneve të thjeshta të algoritmeve themelore, përmes së cilave ato kuptohen më mirë dhe pastaj më lehtë përdoren për të “akorduar” (përmirësar në detaje) versionet e gatshme nga libraritë. Ç’është më e rëndësishme, mundësia e reimplementimit të algoritmeve bazike paraqitesh shumë shpesh. Arsyeja primarë për të vepruar kështu qështë se shumë shpesh ballafaqohemi me ambient tërësisht të ri hardverik dhe softverik, me veti të cilat implementimet e vjetra nuk mund t’i përdorin për të përfituar sa më shumë. Me fjalë tjera, shpeshherë implementojmë algoritmet bazike të “qepura” për problemin tonë, sesa të varemi nga një rutinë (nënprogram) sistemor, për t’i bërë zgjidhjet më portabile dhe më afatgjata. Një arsye tjetër e shpeshtë për të reimplementuar algoritmet bazike është se përkundër avantazheve të inkorporuara në C++, mekanizmat që përdoren për bashkëpërdorim (sharing) të softverit nuk janë gjithmonë mjaft të fuqishme për të na lejuar që të përshtasim programet e librarive që të performojnë efektivisht në detyra specifike. Programet kompjuterike janë shpeshherë të tejoptimizuara (angl. overoptimized). Mund të mos ia vlenë që të mirret mundimi për t’u siguruar që një implementim i një algoritmi të caktuar është më efikasi i mundshëm, përveq nëse ai algoritëm do të përdoret për detyra jashtëzakonisht të mëdha ose do të përdoret shumë herë. Përndrsyhe, një implementim relativisht i thjeshtë, i zgjedhur me kujdes, do të mjaftojë. Mund të presim që ai do të punojë dhe me gjasë do të jetë pesë apo dhjetë herë më i ngadalshëm sesa versioni më i mirë i mundshëm, por kjo do të thotë se do të marrë disa sekonda kohë shtesë për ekzekutim. Për kontrast, zgjidhja e duhur e algoritmit në vend të parë, mund të bëjë ndryshimin për faktorë 100 ose 1000 apo më shumë herë, gjë që mund të përkthehet në minuta, orë ose edhe më shumë kohë për ekzekutim. Kryesisht do të koncentrohemi në implementimet më të thjeshta të arsyeshme të algoritmeve më të mira. Zgjedhja e algoritmit më të mirë për ndonjë detyrë të caktuar mund të jetë proces i komplikuar, ndoshta duke kërkuar analizë matematikore të sofistikuar. 8

Algoritmet dhe strukturat e të dhënave Dega e shkencave kompjuterike e cila përfshinë studimin e pyetjeve të tilla, quhet “analiza e algoritmeve”. Shumë prej algoritmeve përmes analizës së tillë janë treguar që kanë performansë të shkëlqyeshme, ndërsa të tjerat thjeshtë dihet se punojnë mirë, nga përvoja. Qëllimi kryesor është që të mësohen algoritmet e arsyeshme për detyrat e rëndësishme, mirëpo duke u kujdesur për krahasimin e performansave të metodave. Nuk duhet të përdoret një algoritëm pa pasur ide se sa resurse mund të konsumojë dhe duhet përpjekur që të jemi të vetëdijshëm për atë se si mund të pritet të performojë algoritmi. Algoritmet manipulojnë me të dhënat, të cilat mund të jenë vlera të veçanta të tipeve të thjeshta të të dhënave ose të quajtura ndryshe primitive (primitive data), si bitat, karakteret, numrat natyral, numrat real, etj dhe mund të jenë të dhëna të strukturuara në forma më të avansuara, për të ju përshtatur nevojave nga realiteti, të ashtuquajtura struktura të të dhënave (angl. Data Structures).

Strukturat e të dhënave Organizimi i të dhënave për përpunim (angl. processing- përpunim, procesim, shqyrtim), është detyrë thelbësore në zhvillimin e programeve kompjuterike. Shumë algoritme kërkojnë përdorimin e reprezentimit të duhur të të dhënave për të qenë efektive. Ky reprezentim i të dhënave dhe operacionet përcjellëse për to, njihen si struktura të të dhënave. Secila strukturë e të dhënave mundëson insertimin arbitrar por dallojnë në atë se si mundësojnë qasjen në anëtarët e grupit. Disa struktura të të dhënave lejojnë qasjen dhe fshirjen arbitrare, gjersa të tjerat imponojnë kufizime, si lejimi i qasjes vetëm në elementin e fundit të insertuar ose vetëm në elementin e parë të insertuar në grup. Struktura e të dhënave mundëson arritjen e një prej qëllimeve të programimit të orientuar në objekte: ripërdorimi i komponenteve. Secila strkuturë e të dhënave e implementuar një herë, mund të ripërdoret përsëri në aplikacione të ndryshme. Struktura e të dhënave pra është reprezentimi i të dhënave dhe operacioneve në ato të dhëna. Shumë struktura të të dhënave ruajnë një koleksion të objekteve dhe pastaj ofrojnë metodat për të shtuar objekte, për të larguar objektet ekzistuese ose për të ju qasur objekteve të koleksionit. Standardi i C++-it kërkon që të gjitha implementimet të ofrojnë libraritë përkrahëse të njohura si Standard Template Library (Libraria Standarde e Shablloneve, shkurt STL). STL ofron koleksionin e strukturave të të dhënave dhe ofron disa algoritme themelore, si p.sh., sortimi. Si tregon edhe vet emri, STL përdorë me të madhe shabllonet. Për shumë aplikacione, zgjedhja e strukturës së duhur të të dhënave është vendimi i vetëm i rëndësishëm i përfshirë në implementim: kur të jetë bërë zgjedhja, algoritmet e nevojshme janë të thjeshta. Për të njëjtat të dhëna, ndonjë 9

Avni Rexhepi strukturë e të dhënave mund të kërkojë më shumë ose më pak hapësirë sesa të tjerat; për ndonjë operacion (veprim) me të dhënat, disa struktura mund të çojnë në algoritme më efikase ose më pak efikase, se të tjerat. Zgjedhja e algoritmit dhe e strukturës së të dhënave janë të ndërlidhura ngushtë dhe vazhdimisht kërkojmë mënyra për të kursyer kohën ose hapësirën, duke bërë zgjedhjen e duhur. Struktura e të dhënave nuk është objekt pasiv. Ne duhet të marrim në konsiderim edhe operacionet të cilat do të kryhen në të (dhe algoritmin e përdorur për këto operacione). Ky koncept është i formalizuar me nocionin: tipi i të dhënave (angl. data type). Interesimi primar është në implementimin konkret të qasjeve themelorë të cilat përdoren për strukturimin e të dhënave. Shqyrtojmë metodat themelore të organizimit dhe metodat për manipulimin e të dhënave, përmes shembujve specifik të cilët ilustrojnë përfitimet për secilin dhe çështjet e ndërlidhura, si mengaxhimi i memories. Gjithashtu, do të diskutohen tipet abstrakte të të dhënave (ADT-Abstract Data Types), ku ndahen definicionet e tipeve të të dhënave prej implementimeve. Do të diskutohen tiparet e vargjeve, listave të lidhura dhe stringjeve. Këto struktura klasike të të dhënave kanë përdorim të gjerë. P.sh., tek pemët (struktura e të dhënave, në formë peme), ato praktikisht formojnë bazën për pothuajse të gjitha algoritmet. Do të shqyrtohen edhe operacionet e ndryshme primitive për manipulimin e këtyre strukturave të të dhënave, për të zhvilluar një bashkësi themelore (angl. basic set) të veglave të cilat mund të përdoren për zhvillimin e algoritmeve të sofistikuara për problemet e vështira. Studimi i ruajtjes së të dhënave si objekte me madhësi të ndryshueshme (angl. variable-size objects) dhe në strukturat e lidhura të të dhënave kërkon njohuri për mënyrën se si sistemi e menaxhon hapësirën e ruajtjes (hapësirën memorike - angl. storage) të cilën ua alokon (ndanë) programeve për të dhënat e tyre. Në fakt, diskutohet qasja e menaxhimit të hapësirës dhe disa mekanizmave themelorë të përgjithshëm, sepse shumë elemente janë të varura nga vet sistemet dhe pajisjet që përdoren. Do të shohim mënyrat specifike për të cilat përdoren mekanizmat e C++-it për alokim të hapësirës. Poashtu, do të shqyrtohen disa shembuj të strukturave të përbëra, si vargjet e listave të lidhura dhe vargjet e vargjeve. Nocioni i ndërtimit të mekanizmave abstrakt të rritjes së kompleksitetit nga nivelet e ulëta është temë që përsëritet. Shembujt, pastaj mund të shërbejnë si bazë për algoritme më të avansuara. Këto struktura të të dhënave janë të blloqe ndërtimi të rëndësishme (angl. building blocks) të cilat mund të përdoren në mënyrë natyrale në C++ dhe në shumë gjuhë të tjera programuese. Vargjet, stringjet, listat e lidhura dhe pemët, janë elementet themelore të ndërtimit të shumë algoritmeve. Reprezentimi 10

Algoritmet dhe strukturat e të dhënave konkret i zhvilluar në ndërtimin e tipeve abstrakte të të dhënave plotëson nevojat e shumë apliacioneve.

Strukturat themelore të të dhënave Të dhënat ruhen në memorie. Kur kemi për të ruajtur vlera të veçanta, interpretimi logjik përputhet shumë lehtë me realitetin fizik, sepse p.sh., kur deklarojmë ‘int x=10;’ themi që kemi deklaruar një numër të plotë, me emrin x dhe i kemi dhënë vlerën 10. Është lehtë të imagjinohet, se diku në memorie, do të ruhet vlera 10. Kur kemi një bashkësi të të dhënave, që dëshirojmë ta ruajmë si një tërësi, p.sh., notat e studentit, pagat e punëtorëve, etj., atëherë e krijojmë një varg. Anëtarët e vargut janë të njëjtë për nga tipi dhe lokalizohen në memorie në lokacione të njëpasnjëshme. Deklarimi i vargut, bën që të rezervohet hapësira e duhur në memorie dhe pastaj aty vendosen vlerat e anëtarëve të vargut. Më vonë, përmes qasjes direkte ose pointerëve, mund të bëhet qasja në anëtarët e vargut. Edhe në këtë rast, interpertimi logjik është i thjeshtë, sepse e imagjinojmë vargun e lokacioneve të njëpasnjëshme në memorie, ku i kemi të vendosura disa vlera. Mirëpo, për arsye të ndryshme, ndonjëherë nuk ka mundësi ose nuk është e përshtatshme që të gjitha vlerat e bashkësisë të ruhen në lokacione të njëpasnjëshme në memorie. Atëherë kemi “mospërputhje” ndërmjet realitetit fizik dhe interpretimit logjik nga ana e jonë. Pra, të dhënat e një bashkësie, për nga pozicionimi fizik në memorie, mund të jenë: -

Në lokacione të njëpasnjëshme (sekuenciale) në memorie Në lokacione të shpërndara (jo-sekuenciale).

Vargu është strukturë me anëtarë të vendosur në lokacione sekuenciale. Listat janë me anëtarë në lokacione josekuenciale. Nëse të dhënat nuk janë të vendosura fizikisht në lokacione të njëpasnjëshme në adresat e memories, por ato logjikisht duhet të përcillen si anëtarë të njëpasnjëshëm të bashkësisë, atëherë krijojmë strukturën e të dhënave, e cila më nuk përmbanë vetëm vlerat (të dhënat) e tipit të caktuar, por secili anëtarë është i përcjellur edhe me informacione plotësuese, të cilat mundësojnë ndërlidhjen logjike me anëtërët e tjerë të bashkësisë. Këto “tërësi” të reja, tani përveq vlerës, kanë edhe “elementin për ndërlidhje”, pra elementin e ri plotësues (pointerin), ashtu që të na mundësojnë që të “lëvizim” prej një anëtari në tjetrin, ngjashëm sikur lëvizim nëpër anëtarët e vargut të zakonshëm, prej një lokacioni të memories në tjetrin (në fakt duke kaluar prej një anëtari në tjetrin). Kjo tërësi e re, e krijuar prej vetë vlerës dhe prej pointerëve të cilët e lidhin me anëtarin e përparshëm dhe/ose atë 11

Avni Rexhepi të ardhshëm, zakonisht quhet “Nyje” (angl. Node). Pra, nyja përmbanë vlerën (angl. value) ose të dhënat (angl. data) dhe pointerin ose pointerët, që e ndërlidhin atë me nyjen e ardhshme dhe atë të përparshme, ashtu që edhe pse fizikisht të vendosur në lokacione të ndryshme të memories, logjikisht anëtarët përsëri krijojnë një listë me anëtarë të njëpasnjëshëm. Ky organizim i të dhënave të renditura, ku anëtarët e njëpasnjëshëm janë në lokacione të shpërndara të memories, por janë të “lidhur” mes vete përmes pointerëve quhet listë e lidhur. Pra, lidhjen prej një anëtari (lokacioni të memories) deri tek anëtari tjetër (lokacioni tjetër në memorie), e realizojmë përmes pointerëve, të cilët tregojnë pozitën e anëtarit të ardhshëm ose atij të përparshëm (Rikujtojmë se pointeri ruan adresa, kështu që pra tregon adresën se ku ndodhet anëtari përkatës). Nëse struktura e të dhënave, për secilin anëtarë (nyje) definon vetëm vlerën dhe pointerin për në pozitën e ardhshme, themi se kemi të bëjmë më listën e lidhur njëfish, pasi që lëdhja është vetëm nënjërin kah (drejtim). Nëse struktura për secilin anëtarë të vetin, ka vlerën dhe dy pointerë, njëri për anëtarin e përparshëm dhe tjetrin për anëtarin e ardhshëm në listë, atëherë kemi të bëjmë me listën e lidhur dyfish.

Figura 1 – Krahasimi i vargut dhe listës së lidhur Për nga aspekti i renditjes logjike të anëtarëve, strukturat mund të jenë: -

Lineare (vargu, listat e lidhura, steku, rreshti i pritjes, etj), Jo-lineare (pemët, grafet).

Strukturat lineare janë lineare në atë që ndërmjet objekteve në strukturë ruhet renditja lineare. Relacioni linear është logjik, në atë që për dallim prej vargjeve, nuk mund të bëhet ndonjë presupozim për lidhjen ndërmjet renditjes lineare të objekteve dhe lokacioneve të tyre aktuale në memorie. Strukturat lineare dallojnë prej njëra tjetrës për nga kufizimet në mënyrat te qasjes në anëtarët e tyre. 12

Algoritmet dhe strukturat e të dhënave Në varësi të zgjedhjeve të opcioneve për numrin e lidhjeve (pointerëve për lidhje) dhe për lidhjen e pointerit të fundit në strukturë, janë katër lloje të reprezentimit të listave. Për nga mënyra e lidhjes së pointerit të fundit, janë dy mundësi: ose pointeri i fundit bëhet “Null” (angl. Null-asgjë, nuk ekziston) ose kthehet në anëtarin e parë në strukturë. Nëse anëtari i fundit tregon në “Null”, thuhet se struktura është e “tokëzuar” dhe paraqitet zakonisht me simbolin elektronik të tokëzimit. Nëse pointeri i fundit ktheht në anëtarin e parë në strukturë, atëherë thuhet se struktura është qarkore (cirkulare). Për nga numri i lidhjeve, mund të ketë vetëm një pointer për në elementin e ardhshëm në strukturë ose dy pointerë, që pointojnë njëri në elementin e përparshëm dhe tjetri në elementin e ardhshëm. Struktura lineare e lidhur me vetëm një element për lidhje (pointer) quhet listë e lidhur në një kahje ose listë e lidhur një-fish. Struktura me dy lidhje formon listën e lidhur në dy kahje ose listën e lidhur dy-fish. Nga kjo del se listat e lidhura mund të jenë: një-fishe të tokëzuara, një-fishe cirkulare, dy-fishe të tokëzuara dhe dy-fishe cirkulare. ●

● ●

● ●



● ● ●



...

● ●

● ●

● ●

...

...

...



...

...

● ●

● ●



● ●

● ●

● ●

Figura 2 – Llojet e listave të lidhura Për nga aspekti i krijimit/rezervimit vargjet/listat/strukturat ndahen në: -



hapësirës



memorie,

Statike, dhe Dinamike.

Kur bëhet deklarimi i zakonshmëm i vargut, si p.sh., ‘inta A[10];’, në fakt bëhet përcaktimi i tipit dhe numrit të anëtarëve dhe rezervohet hapësira e nevojshme në memorie (në lokacione të njëpasnjëshme). Gjatë ekzekutimit të programit, madhësia e vargut dhe lokacioni në memorie nuk ndryshojnë, kështë që themi se kemi të bëjmë me varg/strukturë statike. 13

Avni Rexhepi Kur deklarimi i vargut/listës bëhet në kohën e ekzekutimit, përmes përmes operatorit ‘new’(i cili përcakton lokacionin në memorie dhe pointerin për atë lokacion) dhe gjatë ekzekutimit shtohen ose largohen anëtarët e listës, atëherë themi se kemi të bëjmë më strukturë dinamike. Krijimi i tipit abstrakt të të dhënave (angl. Abstract Data Type – ADT) na mundëson që të krijojmë struktura logjike, të cilat i përshtaten nevojave të programit dhe realitetit nga jeta e përditshme, kurse realizimi fizik i tyre (“ në prapavi”) përsëri mbetet i bazuar në atë që është e realizueshme fizikisht, si bashkësi e lokacioneve të njëpasnjëshme ose të atyre të shpërndara në memorie. Nëse lokacionet janë të krijuara dinamikisht (gjatë ekezekutimit) dhe rezervohen në pozita të ndryshme në memorie, atëherë përmes pointerëve të tyre, i përcjellim lokacionit e të dhënave, si në figurën vijuese.

Figura 3a - Vendosja e katër elementeve në memorie

Figura 3b - Një mënyrë e ruajtjes së pointerëve për përcjellje të lokacioneve Kur krijojmë ADT dhe deklarojmë strukturën përkatëse, më nuk kemi të bëjmë vetëm më vlerën (të dhënën) që ruhet në memorie, por edhe me të gjitha 14

Algoritmet dhe strukturat e të dhënave elementet përcjellëse, të cilat mundësojnë trajtimin logjik të të dhënave, siç janë pointerët të cilët mundësojnë lëvizjen nëpër dhe përcjelljen e anëtarëve si dhe funksioneve përkatëse, të cilat shërbejnë për ‘t’i dhënë jetë’ të dhënave/elementeve të strukturës. Funksionet krijohen për operacionet e zakonshme të cilat ndodhin me të dhënat: insertimi, leximi, shtypja, editimi, fshirja (largimi), etj. Kështu strukturat e kompletuara, të realzuara në C++, si strukturë ose klasë ose edhe ato të gatshmet nga STL-i, i kanë të gjitha këto funksione. Të gjitha realizohen duke u bazuar në konceptet e programimit të orientuar në objekte.

Konceptet themelore të programimit të orientuar në objekte Programimi i orientuar në objekte - POO (angl. Object orientet programming – OOP), karakterizohet me konceptet e klasave, objekteve, trashëgimisë, abstraksionit, ripërdorimit, polimorfizmit, etj. Klasat Klasa është një strukturë (struct) e zgjeruar, që ofron tiparet e orientuara në objekte të C++-it. Klasa definohet nga shfrytëzuesi duke përshkruar një bashkësi të të dhënave (vlerave) që mund t’i përfaqësojë dhe një bashkësi të funksioneve të cilat mund të veprojnë (operojnë) në ato të dhëna. Këto të dhëna dhe funksione të klasës quhen anëtarë të klasës (angl. class members). Klasat janë tipe të të dhënave nga të cilat krijohen objektet. Klasat enkapsulojnë (angl. encapsulate-futë në kapsulë) të dhënat përmes përdorimit të anëtarëve të dhëna dhe anëtarëve funksione. Objektet Bashkimi i të dhënave dhe funksioneve është koncepti në prapavi të gjuhëve programuese të orientuara në objekte. Njësia e tillë (e bashkuar) quhet objekt. Objekti është një instancë e klasës (një rast konkret, një konkretizim i klasës). Klasa ka relacion të njëjtë me objektet sikur tipet themelore të të dhënave me variablat e tipit të tyre. Një objekt mund të definohet në mënyrë unike përmes një emri specifik (identifikatori). Objekteve u ndahet memoria dhe një objekt mund të përmbajë disa atribute. Trashëgimia Trashëgimia (angl. inheritance) është një prej vetive më të fuqishme të programimit të orientuar në objekte. Trashëgimia është procesi përmes të cilit 15

Avni Rexhepi një klasë mund të trashëgojë vetitë e një klase tjetër. Klasa ekzistuese quhet klasë bazë, ndërsa klasa e re quhet klasë trashëguese. Trashëgimia përdoret për të redukuar kodin burimor në programimin e orientuar në objekte. Pa përdorim të trashëgimisë, secila klasë do të duhet të definojë të gjitha karakteristikat e veta në mënyrë eksplicite. (angl. explicit – i caktuar, i hollësishëm, i qartë, i saktë; implicit-i nënkuptuar, i padyshimtë). Klasa bazë i përmbledhë elementet e përbashkëta për një grup të klasave trashëguese. Klasat trashëguese përveq që i ekzekuton elementet e përbashkëta që i trashëgon, i ekzekuton gjithashtu edhe ato që i ka karakteristike të vetat. Grupimi i karakteristikave të përbashkëta dhe vendosja e tyre në një vend, në vend të përsëritjes së tyre në të gjitha vendet ku ato ndodhin, në një mënyrë e redukon madhësinë e programeve. Ripërdorimi Kur klasa të jetë shkruar, krijuar dhe debug-uar (debaguar), ajo mund të shpërndahet edhe tek programerët e tjerë për përdorim në programet e tyre. Kjo veti referohet si ripërdorshmëri (angl. reusability; nga use-përdorim dhe abilitymundësi, aftësi, pra aftësi e të qenit e ripërdorshme). Programerët mund të marrin një klasë ekzistuese dhe pa e modifikuar atë, t’i shtojnë karakteristika dhe aftësi plotësuese. Kjo veti referohet si ‘extensibility’ – zgjerueshmëri (angl. extensibility-zgjerueshmëri, zgjatshmëri). Kjo bëhet duke derivuar (trashëguar) një klasë të re nga një klasë ekzistuese. Klasa e re do të trashëgojë tiparet e vjetrës dhe poashtu do të shtojë tipare të veta të reja. Enkapsulimi Enkapsulimi ose enkapsulimi i të dhënave (angl. Data encapsulation) është një prej vetive më të rëndësishme të klasave. Në programimin e orientuar në objekte, një objekt krijohet duke përfshirë të dhënat dhe funksionet për lexim (hyrje) dhe shtypje (dalje) të të dhënave. Objekti përkrahë enkapsulimin. Enkapsulimi është procesi i kombinimit të funksioneve anëtare të klasës dhe të dhënave (vlerave) anëtare të klasës, si dhe mbajtjes së tyre të sigurta nga interferencat (ndërhyrjet) nga jashtë. Të dhënat nuk janë të qasshme nga jashtë dhe vetëm funksionet të cilat janë brenda klasës mund të ju qasen atyre. Izolimi i të dhënave nga qasja direkte, nga programerët quhet “fshehje e të dhënave” (angl. data hiding). Abstraksioni Abstraksioni ose abstraksioni i të dhënave (angl. Data abstraction) është mundësia e krijimit të tipeve të të dhënave të shfrytëzuesit për të modeluar objektet e botës reale, duke përdorur tipet e brenshme të të dhënave. Abstraksioni i të dhënave ndihmon për të ju qasjur të dhënave dhe funksioneve së bashku, gjë që definon tip të ri të të dhënave të quajtur tip abstrakt i të 16

Algoritmet dhe strukturat e të dhënave dhënave (angl. Abstract data type-ADT) me setin e vet të operacioneve. Abstraksioni i të dhënave i referohet veprimit të reprezentimit të vetive themelore, pa i përfshirë detajet ose shpjegimet. Klasat ndihmojnë në krijimin e tipeve abstrakte të të dhënave. Klasat përdoren për abstraksionin e të dhënave duke fshehur implementimin e tipit në pjesën private të definicionit të klasës dhe duke ofruar interfejsin përmes pjesës publike të funksioneve. Polimorfizmi Polimorfizmi është vetija që lejon që një emër të përdoret për dy ose më shumë qëllime të ndërlidhura, por teknikisht të ndryshme. (Fjala polimorfizëm rrjedh nga greqishtja e vjetër: poli-shumë, morphe-formë). Polimorfizmi lejon që një emër të specifikohet për veprimet e përgjithshme të klasës. Polimorfizmi do të thotë që një pjesë e kodit (zakonisht funksion) ose operacionie apo objekte, kanë sjellje të ndryshme në kontekste të ndryshme. Në klasën e përgjithshme, ndonjë veprim specifik që duhet të aplikohet, përcaktohet (varet) nga tipi i të dhënave. Përparësia e polimorfizmit është se ndihmon në zvogëlimin e kompleksitetit të programit duke lejuar që një interfejs të specifikoj një klasë të përgjithshme të veprimeve. Në C++, polimorfizmi kryesisht i referohet përdorimit të funksioneve ‘virtuele’. Mund të ketë dy objekte të krijuara nga nje klasë, por të cilat funksionin virtuel (me emër të njëjtë për të dyjat), e përdorin për llogaritje të ndryshme. Mbingarkimi është veti shumë e dobishme në C++. Wshtë e mundur që të përdoret emri i njëjtë i funksionit për qëllime të ndryshme. Funksioni i duhur do të thirret bazuar në numrin, radhën ose tipin e parametrave (argumenteve) të funksionit. Ky proces referohet si mbingarkim i funksioneve ose polimorfizëm i funksioneve. Polimorfizmi mund të aplikohet poashtu edhe në operatorët e ndryshëm dhe ky proces njihet si mbingarkimi i operatorëve ose polimorfizmi i operatorëve.

Përparësitë e programimit të orientuar në objekte Programimi i orientuar në objekte ofron shumë përparësi për programerët dhe shfrytëzuesit. POO zgjidhë shumë probleme të ndërlidhura me zhvillimin e softverit, ofron kualitetit të përmirësuar dhe softver me çmime më të ulëta. Përparësitë e POO janë: 1. Programet e orientuara në objekte mund të promovohen (angl. upgrade) në çdo kohë dhe me lehtësi. 2. Duke përdorur trashëgiminë, mund të eliminohen kodet redundante (të tepërta) dhe mund të vazhdohet përdorimi klasave të definuara më parë.

17

Avni Rexhepi 3. Vetia e fshehjes së të dhënave i mundëson programerit që të dizajnojë dhe të zhvillojë programe të sigurta të cilat nuk e çrregullojnë kodin në pjesët tjera të programeve. 4. Vetia e enkapsulimit u lejon programerëve që të definojnë klasën me shumë funksione dhe karakteristika, ndërsa vetëm disa funksione i ekspozohen shfrytëzuesit. 5. Të gjitha gjuhët programuese të orientuara në objekte mund të krijojnë pjesë të zgjeruara dhe të ripërdorshme të programeve. 6. POO zgjeron procesin e të menduarit të programerëve duke dërguar në zhvillimin e shpejtë të softverit të ri në kohë më të shkurtë.

Strukturat e të dhënave dhe reprezentimi i tyre Strukturat e të dhënave klasifikohen si lineare dhe jo-lineare. Struktura e të dhënave thuhet se është lineare nëse elementet e saj të të dhënave formojnë sekuencë. Kjo do të thotë që nëse e dijmë adresën e elementit të parë, ne mund të marrim (përfitojmë) të dytin, të tretin, e kështu me radhë deri në elementin e fundit të të dhënave. Shembuj të strukturave lineare janë vargjet, steku, queue, listat e lidhura, etj. Ndërsa në strukturat jo-lineare, elementet e të dhënave ruhen në hierarki ose në nivele. Shembuj të tyre janë pemët dhe grafet. Mënyrat e ruajtjes Synimi kryesor i strukturave të të dhënave ësthë ruajtja e disa të dhënave (vlerave) ose organizimi i të dhënave në ndonjë formë të veçantë. Për nga mënyra se si ruhen ose mirëmbahen në memorie të kompjuterit, janë dy mënyra të reprezentimit (përfaqësimit) të strukturave të të dhënave (lineare ose jolineare) në memorie. Njëra është metoda sekuenciale e ruajtjes ndërsa tjetra është metoda e alokimit të lidhur. Ato poashtu quhen edhe si alokimi statik dhe alokimi dinamik, repsektivisht. Metoda e ruajtjes sekuenciale: nëse elementet janë të ruajtura në lokacione të njëpasnjëshme në memorie, ato njihen si metoda e ruajtjes sekuenciale. Kjo do të thotë që së brendshmi është e ruajtur në një varg një-dimensional. Poashtu quhet edhe si alokimi statik, pasi që memoria në esencë është një varg i adresave ose lokacioneve të memories. Reprezentimi i lidhur: nëse elementet do të ruhen në lokacione të ndryshme (të shpërndara) në memorie dhe pointerët do të japin (krijojnë) renditjen lineare, kjo quhet reprezentimi i lidhur.

18

Algoritmet dhe strukturat e të dhënave

Operacionet në strukturat e të dhënave Për çfarëdo strukture të të dhënave, mund të kryhen operacionet themelore vijuese: 1. 2. 3. 4. 5. 6.

Krijimi ose insertimi Fshirja/largimi Paraqitja/përshkimi Sortimi Kërkimi Bashkimi

Në rastin e vargjeve, këto operacione kryhen si vijon. Krijimi ose insertimi. Kur struktura është fillimisht e zbrazët dhe në të insertohet elementi i parë, atëherë bëhet krijimi. Në vazhdim, kur ajo më nuk është e zbrazët, për çdo element të ri kemi operacionin e insertimit. Le të supozojmë se dëshirojmë të insertojmë një element të ri në varg, në pozitën e kërkuar ndërmjet 0 dhe (n-1). Logjika është që të zhvendoset (lëvizet) elementi i (n-1)-të në pozitën e n-të, ai i (n-2)-të në atë të (n-1)-të e kështu me radhë, deri sa të arrihet në pozitën e insertimit. Pastaj vendoset/insertohet elementi i ri në atë pozitë. Kështu, numri total i elementeve rritet për një. Fshirja. Fshirja bën largimin e një elementi nga pozita e kërkuar. Pas fshirjes së elementit, duhet të zhvendosim/lëvizim të gjitha elementet pasuese për nga një pozitë majtas dhe numri total i elementeve zvogëlohet për një. Përshkimi i vargut. Përshkimi (angl. Traversal) nënkupton shtypjen ose procesimin e secilit element të vargut. Nëse vargu është i zbrazët atëherëe shtypet porosia se vargu është i zbrazët, përndryshe shtyen elementet prej të parit deri tek i fundit. Sortimi. Përmes një procedure (funksioni) të sortimit renditen elementet e vargut në renditje prej të voglit kah i madhi ose anasjelltas.

Lista e lidhur Lista e lidhur është koleksion linear i elementeve të të dhënave të quajtura nyje, ku renditja lineare realizohet përmes pointerëve. Secila nyje është e ndarë në dy ose më shumë pjesë, të quajtura fushat e informacionit dhe fushat e adresave. Fusha e informacionit ose fusha e të dhënave (vlerave) përdoret për të ruajtur informacionin ose elementin e të dhënës, ndërsa fusha e adresave përdoret për të ruajtur adresën e ndonjë nyjes tjetër në listë. Secila nyje do të ketë adresë unike. Nëse nyja përmbanë një fushë të të adresës dhe një ose më shumë fusha të të dhënave thuhet se kemi listë të lidhur njëfish (angl. single linked list) ose zingjirë në një kahje. Nëse nyja përmbanë dy fusha adresash dhe një ose më 19

Avni Rexhepi shumë fusha të dhënash, thuhet se kemi të bëjmë me listë të lidhur dyfish (angl. double linked list) ose zingjirë dy-kahësh. Info

Lidhja

Lidhja_m

a. Nyja me një lidhje

Info

Lidhja_d

b. Nyja me dy lidhje Figura 4 – Nyjet

Në figurën ‘a’ është paraqitur nyja e listës së lidhur njëfish, e cila ka dy fusha: ‘Info’ dhe ‘Lidhja’. Fusha ‘Info’ përdoret për të ruajtur informacionin (vlerën, të dhënën), ndërsa fusha ‘Lidhja’ përdoret për të ruajtur adresën e nyjes së ardhshme. Nëse nyja është e fundit, atëherë në fushën e lidhjes adresa është NULL, që do të thotë se nuk tregon në ndonjë nyje. Në figurën nën ‘b’ është paraqitur nyja e listës së lidhur dyfish dhe ajo përmbanë dy fusha të adresave, të emërtuara ‘Lidhja_m’ (lidhja majtas) dhe ‘Lidhja_d’ (lidhja djathtas). Zakonisht emërtohen edhe ‘ePërparshme’ dhe ‘eArdhshme’, për të treguar në nyjen e përparshme dhe nyjen e ardhshme në listë. Fusha ‘Info’ përdoret për të ruajtur informacionin aktual. Supozojmë se ‘p’ është adresa e nyjes së listës së lidhur njëfish. Atëherë, qasja në të dhënën (vlerën) e nyjes bëhet përmes ‘p->Info’, ndërsa qasja në fushën e lidhjes, përmes ‘p->Lidhja’. Në mënyrë të njëjtë, për nyjen q të listës së lidhur dyfish, do të kemi: ‘q->Infor’, ‘q->Lidhja_d’ dhe ‘q->Lidhja_m’.

Lista e lidhur njëfish Lista e lidhur një-fish (ose lista një-kahëshe) përbëhet prej nëj bashkësie të renditur të elementeve, të cilat mund të ndryshojnë në numër. Një mënyrë e thjeshtë për të reprezentuar listën lineare është që të paraqiten nyjet të cilat përmbajnë të dhënat dhe lidhjet gjegjësisht pointerët për tek nyja e ardhshme. Për të përcjellur adresën e fillimit të listës, zakonisht përdoret edhe një variabël ndihmëse (një pointer) me emrin ‘Fillimi’, i cili jep lokacionin e nyjes së parë në listë. Nyja e fundit në listë nuk ka ndonjë nyje pasardhëse, kështu që në fushën e saj nuk duhet ndonjë adresë. Në raste të tilla në fushën e adresës zakonisht “ruhet” NULL. 10

12

18

...

Fillimi

Figura 5 – Lista e lidhur njëfish 20

22

Algoritmet dhe strukturat e të dhënave Le të supozojmë se të dhënat në listë mirëmbahen në renditje rritëse dhe sipas fushës me informacion dhe për thjeshtësi le të marrim vlerat të plota (integer). Secila nyje mund të ruaj një numër të plotë. Në këtë strukturë të të dhënave mund të kryhen operacione të ndryshme, si insertimi, fshirja paraqitja ose përshkimi i listës dhe kërkimi. Në nënprogramin (funksionin) e insertimit ose krijimit, së pari krijojmë nyjen e re dhe e vendosim në pozitën e duhur në listë. Në rast se nyja insertohet në listë të zbrazët, fusha e saj e lidhjes (pointeri për në nyjen e ardhëshme) do të ketë vlerën ‘NULL’ ndërsa adresa e kësaj nyje ruhet (vendoset) në variablën (pointerin) ‘Fillimi’, që tregon nyjen e parë të listës. Rasti i dytë është insertimi i nyjes në fillim të listës. Në këtë rast, adresa e variablës ‘Fillimi’ ruhet (vendoset) në fushën e lidhjes (Pointerit) për në nyjen e ardhshme, ndërsa adresa e adresa e nyjes së parë (të re) vendoset ën variablën ‘Fillimi’. Rasti tjetër është insertimi ndërmjet dy nyjeve në listë ose në fund të listës. Në këtë rast, përshkojmë listën duke krahasuar elementin e insertuar me secilën nyje me radhë në listë deri sa të vije në pozitën e duhur (vlera më e madhe ose baraz me nyjen aktuale). Kur të jetë gjetur vendi i duhur, insertohet nyja e re ndërmjet nyjes paraardhëse të nyjes aktuale dhe nyjes aktuale. Sipozojmë se adresa e nyjes paraardhëse është e ruajtur dhe adresa e nyjes aktuale është ‘ptr’. Atëherë kjo adresë ruhet në fushën e adresës së nyjes së re dhe adresa e nyjes së re në fushën e lidhjes së nyjes paraprake. Fshirja: për të fshirë nyjen e kërkuar (që ka vlerën e kërkuar), së pari e kërkojmë në listë. Edhe këtu rast paraqiten raste të ndryshme. Një është fshrija prej listës së zbrazët dhe ajo bëhen “nën-rrjedhë”. Nëse elementi i fshirë është në fillim të listës, atëherë pjesa e saj e lidhjes (adresa) ruhet në variablën ‘Fillimi’. Përndryshe, kërkohet nyja që duhet të fshihet dhe poashtu nyja paraardhëse e saj. Pastaj, adresa e nyjes së ardhshme të nyjes që fshihet, vendoset në adresën e nyjes së ardhshme të nyjes para-ardhëse, e kështu me radhë.

Lista e lidhur dyfish Lista e lidhur dyfish (ose lista dy-kahëshe) është e ngjashme me listën njëkahëshe, përveq se ajo e lidhë nyjen në të dy kahjet, edhe me nyjen paraardhëse edhe me atë pasardhëse. Lista është koleksion linear i nyjeve me lidhje të dyfishta dhe prandaj quhet lista e lidhur dyfish, lista dy-kahëshe ose zingjiri dykahësh. Në figurën xxx është paraqitur lista e lidhur dyfish.

21

Avni Rexhepi 10

20

30

40 Fundi

Fillimi

Figura 6 - Lista e lidhur dy-fish Lista e lidhur dy-fish përmbanë dy variabla pointer, të quajtura ‘Fillimi’ dhe ‘Fundi’ (ose edhe ‘Koka’ dhe ‘Bishti’, apo ‘Skaji i majtë’ dhe ‘Skaji i djathtë’). Pointeri i majtë ‘Lidhja_m’ i nyjes së parë ka vlerën ‘NULL, ashtu si edhe pointeri i djathtë i nyjes së fundit ‘Lidhja_d’. Përparësi e kësaj lise është se mund të qarkullohet në të dy kahjet. Edhe në këtë strukturë të të dhënave kryhen operacionet si: insertimi, fshirja, kërkimi, përshkimi dhe bashkimi. Për të krijuar ose insertuar, së pari do të krijojmë nyjen e parë të re, me operatorin ‘Neë’ dhe pastaj e ruajmë vlerën e elementit në fushën e vlerës (informacionit) dhe e vendosim nyjen në pozitën e duhur në listë. Kjo nyje vendoset në listën e zbrazët ose si nyje e skajit të majtë (fillimit) apo skajit të djathtë (fundit) ose diku në listë, ndërmjet nyjeve ekzistuese dhe i azhurohen vlerat e lidhjeve (pointerëve). Për të fshirë nyjen, së pari verifikohet mos është lista e zbrazët dhe pastaj kërkohet vlera që duhet të fshihet. Nëse gjindet, ajo nyje fshihet përmes urdhërit ‘Delete’ dhe pastaj azhurohen pointerët e nyjeve fqinje. Ngjashëm kryhen edhe operacionet tjera. Për nga mënyra mënyra e organizimit dhe për nga mënyra se si e lejojnë ose si e mundësojnë qasjen në të dhëna (në elemente, në anëtarë), dallojmë struktura të ndryshme.

Steku Për vargjet/listat, bazuar në idetë nga jeta reale, kemi listat ku anëtarët janë sikur një grumbull i librave, njëri mbi tjetrin ose sikur fishekët në karikator. I fundit i vendosur, është i pari në rend për qasje. Këto struktura njihen si strukturat LIFO (Last In, First Out – I fundit brenda, i pari jashtë) (ose në renditjen e kundërt FILO (First In, Last Out – I pari brenda, i fundit jashtë). Lista LIFO është e njohur si ‘Stack’ (Stek) ose ‘PushDown Stack’ (Steku shtyje poshtë) (angl. Stack – grumbull, mullar, gyp, raft, etj). Steku mund të imagjinohet edhe si një gyp i mbyllur, në të cilin anëtarët futen një nga një me radhë dhe mund të shtyhen deri në ‘fund’ të stekut, por qasje kemi vetëm në elementin në krye (të fundit të futur në stek), që quhet ‘top’ (kreu) i stekut. Insertimi dhe tërheqja e anëtarëve bëhet vetëm në një pozitë (në një skaj të gypit).

22

Algoritmet dhe strukturat e të dhënave Operacioni i insertimit quhet “Push” (angl. Push-shtyje, godite, etj), kurse ai i nxjerrjes “Pop” (angl. Pop – nxjerrje, tërheqje). Këto janë dy operacionet e stekut, të cilat programohen përmes funksioneve përkatëse.

Figura 7 - Steku

Rreshti, radha e pritjes Nëse imagjinojmë gypin e hapur në të dy anët, por ku lëvizet vetëm në një drejtim (kahje), atëherë anëtarët futen në “listë” në njërën anë, kurse tërhiqen/nxirren nga skaji tjetër, thuhet se kemi të bëjmë me strukturën e quajtur “Queue” (lexohet si “Kju”) (angl. Queue – radhë e pritjes, rresht, etj). Ky është organizimi i njëjtë sikur vargu i pritjes për bileta të teatrit, për pagesa në arkë, etj., ku i pari që vije i pari shërbehet. Kjo njihet si strukturë FIFO (First In, First Out – I pari brenda, i pari jashtë) (ose në të kundërtën, LILO (Last In, Last Out – i fundit brenda, i fundit jashtë). Queue lejon qasjen në dy skaje të gypit, në njërin për insertim dhe në tjetrin për nxjerrje. Skaji i insertimit, quhet “Back” (Fundi, skaji ose pjesa e pasme), kurse skaji i nxjerrjes njihet si “Front” (Fronti, fillimi). Operacionet e insertimit dhe nxjerrjes edhe tek queue, quhen “Push” dhe “Pop”. Nënkuptohet që insertimi bëhet vetëm në fund të radhës, kurse nxjerrja vetëm në fillim të radhës. Nëse “gypi” do të jetë lejohet qasja nga të dy anët, edhe për insertim edhe për nxjerrje, atëherë kemi të bëjmë me ‘deque’ (Double Ended Queue – rreshti me dy skaje). Operacionet përkatëse, për insertim dhe nxjerrje tani do të quhen: “push back”, “pop back”, “push front”, dhe “pop front”.

23

Avni Rexhepi Stack - Steku ... push

pop

FIFO queue - rreshti ... pop

push Deque – Rreshti dy-kahorë ... popFront pushFront

pushBack popBack

Figura 8 - Operacionet në stek dhe në queue. Nëse nuk është përcaktuar ndryshe, radha e insertimit, përcakton edhe radhën e nxjerrjes nga queue. P.sh., nëse printeri është në rrjetë dhe shumë kompjuterë kanë qasje në të, atëherë me queue ruhet lista e punëve për shtypje në printer. I pari që jep urdhërin për shtypje, i pari e merre rezultatin. Mirëpo, në raste të ndryshme, ka anëtarë më të rëndësishëm, të cilëve ju jipet prioritet dhe pavarësisht renditjes në hyrje në listën e pritjes, ata shërbehen jashtë radhës, me prioritet. P.sh., ngashëm me atë që kemi në realitet tek lëvizja në trafik e makinave me përparësi kalimi (ndihma e shpejtë, policia, zjarrfikësit dhe ata “të madhërishmit” që pa nevojë e shfrytëzojnë këtë përparësi (presidenca, qeveria), etj). Në këtë rast, kemi të bëjmë me “Priority Queue” (radha me prioritet). Në këtë rast, anëtareve ju bashkangjitet edhe “informacioni” për prioritetin, i cili përcakton radhën e “ekzekutimit”. Fizikishit, steku dhe queue mund të realizohen edhe si struktura statike, përmes vargut edhe si struktura dinamike, përmes listave të lidhura (njëfishe ose dyfishe). Për nga aspekti i renditjes logjike të anëtarëve, u tha se strukturat mund të jenë Lineare ose Jo-lineare. Tek strukturat lineare, dallohet anëtari i fillimit ose fundit dhe kalimi prej anëtari në anëtarë, mund të bëhet vetëm në mënyrë të renditur (lineare) dhe zakonisht vetëm nëpër një rrugë të mundshme. Kryesisht, lidhja prej nyjes në nyje është pointeri përkatës, që mundëson kalimin tek nyja e përparshme ose ajo e ardhshme. Steku dhe Queue janë struktura lineare. 24

Algoritmet dhe strukturat e të dhënave Përveq strukturave lineare, shfrytëzohen edhe strukturat jolineare, si: Pemët (angl. Trees) dhe Grafet (angl. Graphs). Në esencë, pema është një rast special i grafit. Tek strukturat jolineare, ekzistojnë rrugë të ndryshme të lidhjeve të anëtarëve mes veti dhe lëvizja prej njërës nyje tek tjetra mund të realizohet nëpër rrugë të ndryshme. Pastaj, një nyje mund të jetë e lidhur me disa nyje të tjera, e jo vetëm me atë paraprake dhe atë të ardhshme (si në rastin e listave të lidhura). A

A

B D G

C

C

D

F H

B

I

H

F G

Figura 9 - Pema dhe Grafi Pema është strukturë e organizuar e nyjeve dhe degëve (rrugëve) që lidhin nyjet mes veti. Pema zakonisht është strukturë tek e cila dallohet nyja fillestare, e quajtur rrënjëa e pemës dhe pastaj prej saj shpërndahen nyjet tjera, të organizuar në formë të pemës. Në aspektin hierarkik, nëse i shikojmë si trung familjar, dallojmë nyjet prind dhe nyjet fëmijë. Pema mund të jetë me organizime speciale, si pemë binare (çdo nyje, është prind për dy fëmijë), pemë n-are (çdo nyje është prind për n-fëmijë), pemë e balansuar (çdo anë e pemës, është me numër të balansuar të nyjeve), etj. Grafi, është strukturë e organizuar, e nyjeve dhe degëve që lidhin nyjet mes veti. Nuk ka nyje fillestare të përcaktuar sikur te pema. Nyjet mund të jenë të lidhura mes veti me rrugë të ndryshme (sikur në realitet, rrugët e qytetit, pikat e caktuara në qytet), etj. Lidhjet mes degëve mund të jenë të orientuara (me lëvizje vetëm në kahun e caktuar, që në graf paraqitet me shenjë të shigjetës) ose të paorientuara (lidhjet janë dykahëshe). Degët e grafit, mund të plotësohen me informacione plotësuese, që quhen pesha ose kostoja e rrugës, si p.sh., koha e kalimit prej nyjes në nyje, kualiteti i rrugës, shpejtësia e lëvizjes, etj. Të gjitha këto struktura do të përdoren nëpër algoritme të ndryshme për llogaritje të ndryshme varësisht prej nevojës. Me rëndësi është që për rastin konkret të problemit që duhet zgjidhur, të zgjidhet edhe struktura adekuate, e cila në mënyrën më të mirë do t’a pasqyrojë dhe do ta interpretojë logjikisht strukturën reale të problemit. Këto janë strukturat themelore, të cilat pastaj përdoren në mënyra të ndryshme, për realizimin e strukturave të tjera. Në praktikë, gjithmonë synohet që natyra e 25

Avni Rexhepi problemit të interpretohet dhe të zbërthehet ashtu që të mund të paraqitet përmes këtyre strukturave themelore, për të cilat janë krijuar dhe standardizuar algoritmet dhe funksionet për kryerjen e operacioneve të ndryshme dhe përpunimin e të të dhënave. Nëse bëhet nja paraqitje e kategorizuar e strukturave të të dhënave, mund të bëhet një ndarje si në figurën 10. Strukturat e të dhënave

Të brendshme (Built-in)

Numrat e plotë (Integer)

Numrat jo të plotë (Float)

Karakteret (Char)

Të definuara prej shfrytëzuesit (User-Defined)

Pointer

Vargjet

Listat

Listat lineare

Steku

Queue

Figura 10 – Strukturat e të dhënave

26

Fajllat

Listat jo-lineare

Pemët

Grafet

Algoritmet dhe strukturat e të dhënave

1. Memoria, Tipet Abstrakte të të dhënave dhe Adresat Shtrohet pyetja: Sa është numri maksimal i tentimeve për të gjetur një emër në listën prej një milion emrash? Idea e parë do të ishte një milion! Mirëpo, kjo nuk është aspak afër, sepse përgjigja e saktë është 20, nëse lista ka një strukturë që e bën kërkimin të lehtë dhe nëse kërkimi bëhet me një strukturë efikase. Kërkimi i listës është një prej mënyrave se si strukturat e të dhënave na ndihmojnë për të manipuluar të dhënat e ruajtura në memorie të kompjuterit. Mirëpo, për të pasur të qartë funksionimin, duhet pasur të qartë se si funksionon memoria e kompjuterit apo si dhe përse vetëm zerot dhe njëshet ruhen në memorie. Në fillim, do ta bëjmë një vështrim mbi gjërat themelere të cilat duhet të dihen, për të vijuar punën. Për të punuar me algoritmet dhe strukturat e të dhënave, duhet të jenë tërësishtë të qarta konceptet e memories, lokacioneve të memories, adresave të tyre, ndarja e lokacionieve në memorie (alokimi i memories), mënyrës së ruajtjes së të dhënave në memorie, qasja në to (për insertim, lexim, editim, fshirje), pastaj alokimi statik dhe dinamik, etj. Poashtu do të shohim edhe një herë gjërat e nevojshme për pointerët, përdorimin dhe funksionimin e tyre.

Vështrim mbi memorien Memoria e kompjuterit është e ndarë në tri seksione: -

memoria kryesore (angl. main memory), kesh memoria (angl. cache memory) në procesor (angl. CPU Central Processing Unit), dhe memoria e përhershme (disku).

Memoria kryesore, e njohur edhe si RAM (Random Access Memory – memoria me qasje të rastit) është vendi ku ruhen instruksionet (programet) dhe të dhënat (angl. data). Memoria kryesore është volatile (e paqëndryeshme) sepse instruksionet dhe të dhënat në të fshihen (humben) porsa të ndalet furnizimi (fiket kompjuteri). Kesh memoria në CPU përdoret për të ruajtur instruksionet e shpeshta dhe të dhënat që janë, do të jenë, ose kanë qenë duke u përdorur nga CPU-ja. Një segment i kesh memories në CPU quhet “regjistër” (angl. register). Regjistri është një pjesë e vogël e memories përbrenda CPU-së dhe përdoret për ruajtjen e përkohshme të instruksioneve dhe të dhënave.

27

Avni Rexhepi “Bus”-i (zbrarra, lidhja) e lidhë procesorin dhe memorien kryesore. Bus-i është një bashkësi (set) e fijeve përquese të gravuara në pllakën kryesore të kompjuterit (angl. motherboard – pllaka amë) e ngjashme me autostradën dhe transporton instruksionet dhe të dhënat ndërmjet procesorit dhe memories dhe pajisjeve të tjera të lidhura në kompjuter. CPU – Central Processing Unit (Njësia qëndrore procesuese) Main memory – memoria kryesore Data bus – bus-i i të dhënave Address bus – bus-i i adresave Control bus – bus-i i kontrollës Input/Output devices – pajisjet hyrëse/dalëse (Ekrani, tastiera, memoria e qëndrueshme/disku, lidhjet e rrjetës, etj)

Figura 1.1: Bus-i e lidhë procesorin, memorien kryesore dhe diskun, si dhe pajisjet tjera. Disku – (angl. Persistent storage(memoria e qëndrueshme, persistente), ose Hard Disk (disku i fortë))) është pajisje e jashtme (eksterne) për ruajtje të instruksioneve dhe të dhënave. Memoria e qëndrueshme është jovolatile, që do të thotë se instruksionet dhe të dhënat mbesin të ruajtura edhe kur kompjuteri është i fikur. Disku shpeshherë përdoret nga sistemi operativ edhe si memorie virtuele. Memoria virtuele (angl. Virtual Memory) është teknikë e sistemit operativ për të rritur kapacitetin e memories kryesore përtej kufijve të RAM-it përbrenda kompjuterit. Kur kapaciteti i memories tejkalohet, sistemi operativ përkohësisht kopjon përmbajtjen e bllokut të memories në disk. Nëse programi ka nevojë për të ju qasur instruksionev ose të dhënave në atë bllok, sisktemi operativ e shkëmben bllokun e vendosur në disk me ndonjë nga memoria, e që nuk është duke u përdorur për momentin. Pra, një pjesë e diskut, funksionon thuajse është pjesë e RAM-it (memories). Keshi është tipi i memories me qasjen më të shpejtë. Me shpejtësi të përafërt, e dyta, është memoria kryesore. Disku është i treti, por shumë larg për nga shpejtësia, pasi që përfshinë procese mekanike, gjë që kufizon/pengon transferin e shpejtë të të dhënave. 28

Algoritmet dhe strukturat e të dhënave Memoria kryesore (RAM-i) është lloji i memories që përdoret nga strukturat e të dhënave, edhe pse strukturat e të dhënaev dhe teknikat e manipulimit të tyre mund të aplikohen edhe në “file systems” (sistemet e fajllave) në disk.

Të dhënat dhe memoria Të dhënat që përdoren nga programi janë të ruajtura në memorie dhe manipulohen nga teknika të ndryshme të stukturave të të dhënave, varësisht prej natyrës së programit. Të shohim se si ruhen të dhënat në memorie para se të hulumtojmë manipulimin e tyre. Memoria është një grumbulli ndërprerësave elektronik, të quajtur transistorë, të cilët mund të vendosen në njërën prej dy gjendjeve të mundshme: kyçur ose çkyçur (ndezur/fikur). Gjendja e ndërprerësit fiton kuptim, kur secilës gjendje i ndahet një vlerë, gjë që bëhet përmes përdorimit të sistemit numerik binar. Sistemi binar përbëhet prej dy shifrave, zero dhe një, të quajtura shifra binare (angl. binary digit) ose shkurt bit. Pra, ndërprerësi në gjendjen e fikur reprezenton zeron dhe kur është i kyçur/ndezur, paraqet “një”-shin. Pra, kjo do të thotë se transistori reprezenton njërën prej shifrave. Sidoqoftë, dy shifra nuk ofrojnë të dhëna të mjaftueshme për të bërë gjë tjetër përveq ruajtjes së zerove dhe njësheve nëmemorie. Mirëpo grupimi logjik i “ndërprerësave” të memories, mudnëson ruajtjen e të dhënave me kuptim logjik. Për shembull, dy ndërprerësa mudnësojnë ruajtjen e dy shifrave binare, që që mudnëson katër kombinime të ndryshme, si në tabelën vijuese dhe këto kombinime mund të ruajnë tri vlera numerike: 0 deri në 3. Shifrat janë me bazë zero, gjë që do të thotë se shifra e parë në sistemin numerik binar është zeroja. Memoria organizohet në grupe prej tetë bitave, të quajtuara bajta (angl. bytebajt). Kjo mundëson 256 kombinime të zerove dhe njësheve të cilat mund të ruajnë numrat prej 0 deri në 255, pasi që kombinimet me gjatësi tetë prej 2 elementve janë 28=256. Tabela 1-1: Kombinimet e dy bitave dhe ekuivalenti i tyre decimal Switch 1 0

Switch 2 0

Decimal Value 0

0

1

1

1

0

2

1

1

3

29

Avni Rexhepi

Sistemi numerik binar Sistemi numerik është një mënyrë e numërimit të gjërave dhe kryerjes së veprimeve aritmetike. Për shembull, njerëzit e përdorin si më të përshtatshëm sistemin decimal, ndërsa kompjuterët atë binar. Të dy sistemet numerike bëjnë të njëjtën gjë: mundësojnë numërmin e gjërave dhe kryerjen e aritmetikës. Mund të mbledhet, zbritet, shumëzohet, pjesëtohet dhe do të arrihet tek përgjigjet e njëjta sikur të jetë përdorur sistemi numerik decimal. Mirëpo, ekziston një diference e dukshme ndërmjet sistemit numerik decimal dhe atij binar: sistemi decimal përbëhet prej 10 shifrave (0 deri në 9) ndërsa sistemi binar përbehet prej vetëm dy shifrave (0 dhe 1). Për të rikujtuar, sigurisht e mbani mend të mësuarit e mbledhjes dhe bartjes në nivelin më të lartë (mbajtjes në mend, p.sh. 9+1=0 e 1 në mend, gjegjësisht 1 në nivelin më të lartë)! Pra, kur arrihet velra maksimale ose kufiri, kalojmë në nivel më të lartë. P.sh., nëse në kolonën e djathtë kishim 9 dhe shohej edhe 1, atëherë ndryshohet 9 në 0 dhe vendoset 1 në të majtë të zeros, për të fituar 10:

Teknika e njëjtë e bartjes në nivel më të lartë përdoret edhe gjatë mbledhjes në sistemin binar, me dallimin që tash në vend të kufirit maksimal 9, kemi 1. Nëse kemi 1 në kolonën e djathtë dhe shtojmë 1, atëherë e ndërromë 1 në 0 dhe vendosim 1 në të majtë të 0, për të fituar 10 binar.

Tani fillon ngatërrimi. Të dy numrat duket se kanë shifrat e njëjta: 10. Mirëpo, për numrin decimal ky është reprezentimi i vlerës 10, kurse për sistemin binar, shifrat 10 nuk paraqesin vlerën decimale 10, por vlrën binare 2. Shifrat në sistemin numerik binar reprezentojnë gjendjen e ndërprerësit. Kompjuteri kryen aritmetikën duke përdorur sistemin numerik binar për të ndryshuar gjendjen e bashkësive të ndërprerësave (transistorëve).

Rezervimi i memories Njësia e memories mund të mbajë një bajt, ndërsa të dhënat në program mund të jenë më të mëdha sesa bajti dhe kërkojnë 2, 4 ose 8 bajta për t’u ruajtur nëmemorie. Para se ndonjë e dhënë të ruhet në memorie, duhet treguar 30

Algoritmet dhe strukturat e të dhënave kompjutrit se sa hapësirë të rezervojë për të dhënat, duke përdorur një tip abstrakt të të dhënave (angl. Abstract Data Type-ADT). ADT është një fjalë e rezervuar e gjuhëve programuese e cila specifikon sasinë nevojshme të memories për të ruajtur të dhënat dhe tipin e të dhënave që do të ruhet në atë lokacion të memories. Sidoqoftë, një ADT nuk i tregon kompjuterit se sa bajta të rezevojë për të dhënat. Numri i bajtave të rezervuar për një ADT ndryshon, varësisht prej gjuhës programuese të përdorur për shkruarjen e programit përkatës dhe nga tipi i kompjuterit që përdoret për të kompajluar programin (angl. compile-hartoj, përpiloj). Në C dhe C++, madhësia e një ADT-je është e bazuar në madhësinë e regjistrave të kompjuterit të përdorur për kompajlim të programit. Në Java, ADT-të kanë madhësi fikse, për t’u ekzekutuar në të gjitha ambientet ekzekutuese të Java-s (Java runtime environment). Imagjinojeni një ADT si termin “pako mollash”. Nëse i thoni menagjerit të shitjes se duhet rezevuar hapësirë në rafta për pesë pako mollash, ai e di se sa rafta duhet t’i rezervojë sepse e di madhësinë e pakove të mollave. E njëjta vlenë edhe për tipet abstrakte të të dhënave. Ju i tregoni kompjuterit që të rezervojë hapësirën për një numër të plotë, duke përdorur ADT-në (tipin) “int” (shkurtesa për integer-numër i plotë). Kompjuteri veç e di se sa memorie duhet të rezervojë për të ruajtur një numër të plotë (integer). ADT-ja poashtu i tregon kompjuterit tipin e të dhënave që do të ruhet në atë lokacion të memories. Kjo është e rëndësishme sepse kompjuterët i manipulojnë të dhënat e një tipi të caktuar ndryshe nga të dhënat e tipit tjetër abstrakt. Kjo është ngjashëm me atë se si menagjeri i shitjes i trajton pakot e letrave dhe lëngjeve ndryshe nga ato të mollëve. Tabela 1-2 përmbanë listën e tipeve abstrakte të të dhënave. Kolona e parë përmbanë fjalët e rezervuara (angl. keyword) për seclilin tip. Kolona e dytë tregon numrin gjegjës të bitave të cilët rezervohen në memorie. Kolona e tretë tregon rangun e vlerave që mund të ruhen në atë tip abstrakt dhe kolona e fundit tregon grupin të cilit i takon tipi përkatës. Tabela 1-2: Tipet e thjeshta të të dhënave Tipi byte

Madhësia në Rangu i vlerave bita 8 –128 to 127

short 16

16

–32,768 to 32,767

Grupi Integers Integers

31

Avni Rexhepi Tabela 1-2: Tipet e thjeshta të të dhënave Tipi

Madhësia në bita

Rangu i vlerave

Grupi

int 32

32

–2,147,483,648 2,147,483,647

÷

Integers

long 64

64

– 9,223,372,036,854,775,808 ÷ 9,223,372,036,854,775,807

Integers

char

16 (Unicode) 65,536 (Unicode)

Characters

float 32

32

3.4e-038 to 3.4e+038

Floating-point

double 64

64

1.7e-308 to 1.7e+308

Floating-point

boolean 1

1

0 or 1

Boolean

16 (Unicode)

Programeri e zgjedhë tipin abstrakt të të dhënave i cili më së miri i përshtatet të dhënave që dëshiron t’i ruaj në memorie dhe e përdorë tipin përkatës në urdhërat e deklarimit, për të deklaruar variablat dhe tipet e tyre. Variabla është një referencë për në lokacionin e memories i cili rezervohet duke përdorur urdhërin e deklarimit. Gjithmonë duhet rezervuar sasinë e duhur të memories së nevojshme për të ruajtur të dhënat, sepse mund të humben të dhënat nëse rezervohet hapësirë shumë e vogël. Kjo është njësoj si të dërgohen 10 pako mollësh në vendin ku është rezervaur hapësira për 5 të tilla. Sigurisht që 5 pako do të “hedhen” diku anash.

Grupet e tipeve abstrakte të të dhënave Ju përcaktoni sasinë e memories që duhet rezervuar duke përcaktuar grupin e duhur për tipin e të dhënave abstrakte dhe duke vendosur se cili tip përbrenda grupit është i duhuri për të dhënat. Janë katër grupe të tipeve të të dhënave:  

32

Integer ruan numrat e plotë dhe numrat me shenjë. Shumë i mirë për ruajtjen vlerave të plota të eurove, kur nuk nevojiten vlerat decimale. Floating-point ruan numrat real (vlerat e thyesave, pjesët e të plotave). Perfekt për ruajtjen e depozitave bankare, kur centët (pjesët e euros) mund të mblidhen së bashku.

Algoritmet dhe strukturat e të dhënave  

Character ruan karakteret (shkronjat, numrat, shenjat e pikësimit). Ideal për ruajrne e emrave. Boolean ruan velrën “true” ose “false” (saktë ose pasaktë, e vërtetë ose jo e vërtetë). value. Është zgjidhja e duhur për të ruajtur përgjigjet po ose jo, apo e saktë ose jo e saktë, për pyetjen e bërë.

Integer Grupi ADT integer përbëhet nga katër tipe abstrakte të të dhënave të përdorura për të rezevuar memorie për ruajtje të numrave të plotë: byte , short , int , dhe long , si është përshkruar në tabelën Tabelën 1-2. Varësisht nga natyra e të dhënave, ndonjëherë velra e plotë duhet të ruhet duke përdorur edhe shenjën pozitive ose negative, si +10 ose -5. Ndonjë herë tjetër një numër i plotë supozohet të jetë pozitiv, ashtu që nuk është i nevojshëm përdorimi i shenjës. Numri i ruajtur me shenjë, quhet “signed number” (numër me shenjë) ndërsa ai që nuk ruhet me shenjë quhet “unsigned number” (numër pa shenjë). Problemi është që shenja e zë 1 bit të memories, e cila përndryshe do të mund të përdorej për të reprezentuar vlerën. Për shembull, bajti i ka 8 bita dhe të gjithë ata mund të përdoren për të ruajtur numrat pa shenjë prej 0 deri në 255. Nurmi me shenjë mund të ruhet në rangun -128 deri në +127. C dhe C++ përkrahin numrat me shenjë, ndërjsa Java jo. Një numër “unsigned integer” është vlerë që është e nënkuptuar se është pozitive. Shenja plus nuk ruhet në memorie. Ndërsa në Java, të gjithë numrat paraqiten me shenjë. Zero ruhet si numër pozitiv. Bajti - byte Tipi abstrakt i të dhënave “byte” (bajt) është më i vogli në grupin integer dhe deklarohet përmes fjalës së rezervuar byte, Fig. 1.2. Programerët zakonisht e përdorin tipin byte për dërgimin ose pranimin e të dhënave nga fajllat ose nëpër rrjetë. Tipi byte poashtu zakonisht përdoret kur punohet me të dhëna binare të cilat mund të mos jenë kompatibile (të pajtueshme) me tip tjetër abstrakt të të dhëanve. Zgjedhni tipin byte sa herë që keni nevojë për të lëvizur të dhënat në dhe nga fajlli ose nëpër rrjetë.

Figura 1.2: byte rezervon 8 bita (1 bajt) në memorien kryesore.

33

Avni Rexhepi short Tipi “short” (angl. short-i shkurtër) është ideal për përdorim në programet që ekzekutohen në kompjuterët 16-bitësh, Fig. 1.3. Mirëpo, shumica e kompjuterëve të tillë sot janë në mbeturina dhe janë zëvendësuar me 32 ose 64bitësh. Prandaj, short është tipi më së paku i përdorur. Zgjedheni këtë tip nëse programi do të ekzeutohet në kompjuter të vjetër.

Figura 1.3: short – rezervon 16 bita (2 bajta) në memorien kryesore. int Tipi “int” (Fig. 1.4) është tipi i përdorur më së shpeshti për grupin integer, për një numër arsyesh:   

Për variabla kontrolluese të unazave Në vargje të indeksave Kur performohet matematikë me numra të plotë

Figura 1.4: int rezervon 32 bita (4 bajta) në memorien kryesore. long Tipi “long” (angl. long-i gjatë) (Figura 1-5) përdoret sa herë që përdoren numrat e plotë të cilët janë përtej rangut të vlerave të tipit int. Zgjedheni si tip, për të ruajtur vlerën e pagës së Bill Gate-it p.sh.

Floating-Point Grupi “floating-point” përdoret për të ruajtur numrat real, në memorie. Numri real përmbanë vlerën decimale. Janë dy lloje të tipeve të të dhanve floating point: float dhe double (shiko tabelën 1-2). Tipi “float” është numër me precizitet të njëfishtë dhe “double” është me precizitet të dyfishtë. Preciziteti i numrit është numri i vendeve pas presjes decimale, që përmbanë vlerën e saktë. 34

Algoritmet dhe strukturat e të dhënave

Figura 1.5: tipi long rezervon 64 (8 bajta) bita në memorien kryesore. Termi floating-point i referohet mënyrës se si referohen decimalet në memorie. Janë dy pjesë të numrit floating-point: pjesa reale, e cila ruhet si numër i plotë dhe pozita e presjes/pikës decimale brenda numrit të plotë. Kjo është arsyeja pse thuhet se pika decimale “floats” (angl. float-noton, lëvizë, pluskon) përbrenda numrit. Për shembull, vlera 43.23 ruhet si 4323 (pa pikë decimale). Krahas me të shkon referenca në numër e cila tregon se pika decimale është e vendosur pas shifrës së dytë. float ADT-ja float (Figura 1.6) përdoret për numrat real të cilët kërkojnë precizitet të njëfishtë, si është rasti me vlerat monetare. Preciziteti i njëfishtë (angl. single precision) do të thotë se vlera është precize deri në 7 shifra djathtas prej decimales. Për shembull, supozojmë se vlera €53.50 ndahet në pjesë të barabarta për 17 persona. Secili person do të marrë nga €3.147058823529. shifrat përtej (djathtas) vlerës €3.1470588 nuk garantohet të jenë precize për shkak të mënyrës se si ruhet në memorie vlera float. Tipi float zgjedhet sa herë që të nevojitet të ruhet vlera decimale ku vetëm 7 shifra djathtas pikës decimale duhet të jenë të sakta.

Figura 1.6: float rezervon 32 bita (4 bajta) të memories kryesore.

35

Avni Rexhepi

double Tipi double (Figura 1.7) përdoret për të ruajtur numrat real të cilët janë shumë të mëdhenj ose shumë të vegjël dhe kërkojnë sasi të dyfishtë të memories e cila rezervohet për tipin float. Zgjedhni tipin double, sa herë që duhet ruajtur vlerë decimale me saktësi më shumë se 7 shifra pas pikës decimale.

Figura 1.7: double rezervon 64 bita (8 bajta) të memories kryesore.

Character Tipi “character” (karakter – Figura 1.8) reprezentohet si një vlerë integer e cila i përgjigjet bashkësisë (setit) të karaktereve. Seti karakter ia ndanë një vlerë integer secilit karakter, simbol dhe shenjë pikësimi të gjuhës.

Figura 1.8: char rezervon 16 bita të memories kryesore. Për shembull, shkronja A ruhet në memorie si vlera 65, e cila i përgjigjet shkronjës A në setin e karaktereve. Kompjuteri di ta trajtojë vlerën 65 si shkronja A e jo si numri 65 për shkak se memoria është rezervuar duke përdorur tipin char. Fjala e rezervuar char i tregon kompjuterit se numri integer i ruajtur në atë lokacion trajtohet si karakter e jo si numër. Janë dy sete të karaktereve që përdoren në programim: American Standard Code for Information Interchange (ASCII) dhe Unicode. ASCII është “gjyshi” i seteve të karaktereve dhe përdorë një bajt për të reprezentuar maksimalisht 256 karaktere. Mirëpo, pas disa viteve të përdorimit, ishte evident problemi i paraqitjes së karaktereve të gjuhëve të ndryshme, si arabe, japoneze, kineze, etj, të cilat kanë më shumë se 256 karaktere në gjuhën e tyre. Për të zgjidhur këtë problem, u zhvillua një set i ri i karaktereve, i quajtur Unicode. Unicode përdorë

36

Algoritmet dhe strukturat e të dhënave 2 bajta për të reprezentuar secilin karakter. Zgjedhni char sa herë që duhet ruajtur një karakter të vetëm në memorie.

Boolean ADT “bool”-ean (Figura 1.9) rezervon memorie për të ruajtur një vlerë booleane, e cila është e saktë ose e pasaktë (e vërtetë ose fals) dhe reprezentohet si zero ose një. Zgjedhni tipin bool-ean sa herë që duhet ruajtur një prej dy mundësive në memorie.

Figura 1.9: ADT bool-ean rezervon 1 bit të memories kryesore.

Adresat e memories Imagjinoni memorien kryesore si një seri në dukje të pakufijshme të fushave (katrorëve) të organizuar në grupe me nga tetë. Secilit grup prej tetë fushave (1 bajti) i ndahet një numër unik i quajtur “adresë e memories” (angl. memory address, si në Figurën 1.10). Kjo është shumë me rëndësi gjatë mësimit të strukturave të të dhënave, përndryshe mund të shkaktohet konfuzion.

Figura 1.10: Adresa e memories e bajtit të parë përdoret si referencë për të gjithë bajtat e rezervuar për një tip abstrakt të të dhënave. Adresa e memories përdoret direkt ose indirekt përbrenda programit për të ju qasur të gjithë tetë katrorëve. Për shembull, nëse programi i tregon/urdhëron kompjuterit që dëshiron të kopjojë të dhënat e ruajtura në lokacionin 423 të memories, d.t.th, katrorit me adresë 423. Kompjuteri shkon tek ai lokacion i memories dhe i kopjon të dhënat (zerot dhe njëshet) nga katrori 423 dhe shtatë katrorët vijues. Këta shtatë katrorët vijues nuk kanë adresë të memories. Ndryshe thuhet se këta shtatë katrorët tjerë e ndajnë bashkarisht adresën e katrorit 423. 37

Avni Rexhepi

Adresat reale të memories Edhe pse adresat e memories u prezentuan si vlera decimale, si më parë “katrori 423”, në realitet adresat e memories janë numra 32-bitësh ose 64-bitësh, varësisth nga sistemi operativ i kompjuterit dhe atë reprezentohen si vlera heksadecimale. Sistemi Hexadecimal është sistem numerik i ngajsëhëm me sistemin decimal dhe atë binar. Kjo do të thotë se vlerat heksadecimale përdoren për të numëruar dhe kryer veprimet aritmetike. Sistemi numerik heksadecimal ka 16 shifra, prej 0 deri në 9 dhe prej A deri në F, të cilat përfaqësojnë numrat prej 10 deri në 15. Për shembull, adresa e memories 258,425,506 reprezentohet në foramtin heksadecimal si 0x0F6742A2.

ADT dhe adresat e memories Më parë u tha se rezervimi i memories për të dhënat bëhet duke përdorur tipin abstrakt të të dhënave. Disa ADT rezervojnë memorie në madhësi më të madhe se 1 bajt. Pasi që secili bajt i memories e ka adresën e vet memorike, mund të supozohet se tipi “short” i ka dy adresa të memories sepse i përdorë 2 bajta të memories. Mirëpo kjo nuk ndodhë. Kompjuteri e përdorë adresën e memories së bajtit të parë për të ju referuar cilit do tipi abstrakt të të dhënaveqë rezervon bajta të shumëfishtë në memorie. Le të themi se në memorie është rezervuar hapësira për tipin “short” (shih Fig. 1-10). Me këtë rast rezervohen dy lokacione të memories, me adresat 400 dhe 401. Mirëpo, vetëm adresa e memories 400 përdoret për të ju referuar vlerës “short”. Kompjuteri automatikisht e di se vlera e ruajtur në adresën 401 është pjesë e vlerës së ruajtur në adresën 400, sepse hapësira është rezervuar duke përdorur tipin abstrakt “short”. Prandaj, kompjuteri i kopjon të gjithë bitat nga adresa e memories 400 dhe të gjithë bitat nga adresa 401, sa herë që nga programi i bëhet kërkesë që të kopjojë numrin/vlerën e ruajtur në adresën e memories 400.

Variablat dhe Pointerët Edhe pse me të përmendur pointerët, disa programerë kanë idenë se kjo çështë shumë komplekse, në esencë pointerët janë thjeshtë tregues që pointojnë (tregojnë, shenjojnë) në adresat e memories, si një fëmijë i vogël që tregon me gisht (pointon) në gjërat që i dëshiron. Pra, pointeri është variabël që përdoret për të pointuar në adresat e memories, përmbajtjen e të cilave dëshirojmë ta përdorim në program. 38

Algoritmet dhe strukturat e të dhënave

Deklarimi i variablave dhe objekteve Memoria rezervohet përmes përdorimit të urdhërit për deklarim të variablave, duke përdorur tipin e të dhënave. Forma e deklarimit varet nga gjuha programuese që përdoret. Në C++ dhe në Java, urdhëri i deklarimit të një variable është p.sh., si në vijim: int VariablIme;

Janë tri pjesë (elemente) në këtë urdhër të deklarimit:   

Tipi i të dhënave (Data type) – që tregon se sa memorie rezervohet për këtë llojë të të dhënave që do të ruhet në atë lokaicon të memories, Emri i variablës (Variable name) – emri që përdoret në program, për të ju referuar përmbajtjes (vlerës) në atë lokacion të memories, Pikëpresja (Semicolon) – i tregon kompjuterit se ky është një urdhër (instruksion) dhe është shenja e fundit të urdhërit përkatës.

Tipet primitive të të dhënave dhe tipet e definuara prej shftyrëzuesit Është sqaruar koncepti i tipeve abstrakte të të dhënave, të cilat përdoren për të rezervuar memorien kompjuterike. Tipet abstrakte të të dhëanave ndahen në dy kategori: tipet primitive të të dhënave dhe tipet e të dhënave të definuara prej shfrytëzuesit. Tipet primitive janë të definuara prej gjuhës programuese dhe janë ato që i përmendëm më parë: char, int, short, float, double, etj., të cilat njihen edhe si “built-in data types” (angl. built-in – të ndërtuara së brendshmi, të brendshme, etj). Tipet e definuara prej shfrytëzuesit, janë grup i të dhënave primitive të definuara nga programeri. Për shembull, nëse duhet të ruhen të dhënat e studentit në memorie, atëherë do të duhen disa elemente të të dhënave, si : ID e studentit, Emri, Mbiemri, Nota, etj. Për secilën veç e veç mund të përdoren tipet primitive të të dhënave, mirëpo tipet primitive nuk janë të grupuara së bashku. Secila prej tyre ekziston në elemente të ndara të të dhënave. Qasje më e mirë është që të dhënat primitive të grupohen në të dhëna të definuara prej shfrytezuesit për të formuar një “record” (angl. record – regjistrim, shënim, dosje, dokument, koleksion të dhënash, etj., por do të përdorim termin rekord, si në origjinal). Termi rekord është i zakonshëm në bazat e të dhënave. Baza e të dhënave përbëhet nga një ose më shumë tabela. Tabela, përbëhet nga rreshtat dhe kolonat. Një rresht i tabelës njihet si “rekord”, kurse kolona si “fushë”. Ngjashëm si në tabelë, kolonat/fushat e të cilës do të ishin: ID, Emri, Mbiemri, Nota, etj., një rresht (të gjitha fushat e rreshtit) do të përmbante të dhënat e një studenti. Pra, tipi i të dhënave i definuar prej 39

Avni Rexhepi shfrytëzuesit, definon kolonat/fushat (tipet primitive të të dhënave) të cilat përbëjnë rreshtin/rekordin (tipin e të dhënave të definuar prej shfrytëzuesit). Mënyra apo forma e përdorur për të definuar të dhënat e definuara prej shfrytëzuesit ndryshon varësisht prej gjuhës programuese që përdoret për të shkruar programin. Disa gjuhë programuese. Disa gjuhë programuese, si Java, nuk i përkrahin fare tipet e definuara prej shftytëzuesit. Në vend të kësaj, përdoren atributet e klasave, për të grupuar tipet primitive të të dhënave. Në gjuhët programuese C dhe C++, definimi i tipit të definuar prej shftytëzusit bëhet përmes definimit të “structure” (strukturës). Paramendojeni strukturën si një shabllon, p.sh., shablloni për shkronjën A. Shablloni nuk është shkronja A, por ai e definon si duket shkronja A. Nëse ju duhet shkronja A, e vendosni shabllonin mbi një letër dhe e “vizatoni” shkronjën A. Nëse duhet edhe një shkronjë A, e përdorni shabllonin e njëjtë dhe e përsëritni procesin e njëjtë. Pra, duke e përdorur shabllonin mund të bëni sa të doni shkronja A. E njëjta vlenë edhe për strukturën. Kur ju duhet një grup i të dhënave primtive, që përfaqësohen nga një strukturë, ju e krijoni një “instance” (një instancë, një rast, një shembull) të strukturës. Instanca është njësoj si shkronja A që paraqitet në letër, pasi të largoni shabllonin. Secila instancë përmbanë të njëjtat të dhëna primitive të cilat janë definuar në strukturë, edhe pse secila instancë ka kopjen e vetë të këtyrë të dhënave primitive.

Definimi i tipeve të definuara nga shfrytëzuesi Definicioni i strukturës përbëhet nga katër elemente:  





struct (fjala e rezervuar “struct”) I tregon kompjuterit se jeni duke definuar një strukturë Emri i strukturës (Structure name) – Emri që përdoret për të identifikuar në mënyrë unike strukturën dhe që përdoret për të deklaruar instancat e strukturës, Trupi i strukturës (Structure body) – kllapa e madhe e hapur dhe e mbyllur, brenda të cilave ndodhen tipet primitive të të dhënave të cilat deklarohen kur deklarohet një instancë e klasës, Pikëpresja (Semicolon) – I tregon kompjuterit se ky është një urdhër (instruksion).

Trupi i strukturës mund të mbajë çfarëdo kombinimi të tipeve të të dhënave primitive dhe tipeve të definuara më parë nga shfrytëzuesi, varësisht prej natyrës së të dhënave që kërkohen në programin konkret. P.sh, definojmë strukturën e 40

Algoritmet dhe strukturat e të dhënave cila definon rekordin e studentit, i cili përbëhet nga numri i studentit dhe nota. Emri i kësaj strukture të definuar prej shfrytëzuesit është StudentRecord: struct StudentRecord { int numriStudentit; char nota; };

Deklarimi i tipit të definuar prej shftyëzuesit Deklarimi i një instance të tipit të definuar prej shfrytëzuesit bëhet në mënyrë të njëjtë si deklarimi i varibalave. Mirëpo, në këtë rast, me rastin e deklarimit, pra në urdhërin e deklarimit, në vend të tipit primitiv të të dhënave, përdoret emri i strukturës. Le të marrim se dëshirojmë të krijojmë një instance të strukturës StudentRecord, të definuar më parë. Ja, si duhet të bëhet deklarimi dhe përdorimi në program: #include using namespace std; struct StudentRecord { int numriStudentit; char nota; } ; int main() { StudentRecord studenti1; studenti1.numriStudentit = 10; studenti1.nota = 'A'; cout next ) printItem( p->item );

dhe në çdo pikë mund të shtojmë një element të ri të fundit x, si në vijim: //last=iFundit; new=iRi,eRe last->next = new Node;// Shto një nyje të re last = last->next; // Përshtate të fundit-last last->item = x; // Vendose x-in në nyje last->next = NULL; // Ky është i fundit, kështu që bëje // next=NULL A0

A1

First (Fillimi, i/e par-i/a)

A3

A2

Last (Fundi, i/e fundit)

Fig. 1.20 – Lista e lidhur Kështu, elementet mund të mos jenë në lokacione të njëpasnjëshme në memorie, mirëpo për të gjetur një element të listës, më nuk mundmi me vetëm një çasje, si në rastin e vargut të zakonshëm, kur përmes indeksit, secili element mund të gjindej direkt, me vetëm një qasje. Në vend të kësaj, duhet të “skenojmë” (angl. scan-hetim, kërkim, kqyrje etj.) listën prej fillimit e tutje. Dallimi është i ngjashëm me atë të qasjes në të dhënat (p.sh., këngët) në CD (një qasje) dhe në shirit (sekuenciale). P.sh., për të dëgjuar këngën e 3, në CD mundemi direkt, kurse në kasetofon të vjetër, duhet rrotulluar shiritin prej fillimit e deri te kënga e tretë. Në anën tjetër, insertimi i një elementi të ri ndërmjet dy elementeve ekzistuese kërkon shumë më pak lëvizje të të dhënave në listën e lidhur sesa në një varg. P.sh., për të shtuar një element të ri në mes të dy anëtarëve ekzistues, duhet 74

Algoritmet dhe strukturat e të dhënave kopjuar pjesa prapa e vargut, për t’u ruajtur dhe zhvendosur në pozitat pas insertimit të anëtarit të ri, kurse, në rastin e listës së lidhur, kjo gjë realizohet vetëm duke i ndërruar pointerët e elementit para dhe atij pas elementit të ri mes tyre, që të krijohet renditja e re. Avantazhi i listave të lidhura është më pak hapësirë e përdorur për objektet e mëdha sesa në teknikën e dyfishimit të vargut. “Dënimi” që paguhet është që qasja në element nuk është më konstante në kohë. Listat e lidhura do të diskutohen detajisht më vonë.

1.7.1 Kontejnerët Kontejneri është strukturë e të dhënave e cila mbanë disa objekte të cilat zakonisht janë të tipit të njëjtë (angl. contain-përmbaj, zë, përfshij; angl. container – enë, kuti). Tipet e ndryshme të kontejnerëve organizojnë objektet përbrenda tyre në mënyra të ndryshme. Edhe pse numri i organizimeve të ndryshme teoritikisht është i pakufizuar, vetëm një numër i kufizuar i tyre ka rëndësi praktike dhe ato që përdoren më së shpeshti janë të përfshira në STL. STL i përmbanë kontejnerët vijues: deque, list, map, multimap, set, multiset, stack, queue, priority_queue dhe vector. Kontejnerët e STL-it janë të implementuar si klasa shabllone (template classes) të cilat përfshijnë një numër funksionesh të cilat specifikojnë se cilat operacione mund të kryhen në elementet e ruajtura në strukturën e të dhënave të specifikuar prej kontejnerit ose në vetë strukturën e të dhënave. Disa operacione mund të gjinden në të gjithë kontejnerët, edhe pse ato mund të jenë të implementuar ndryshe. Funksionet e zakonshme të të gjithë kontejnerëve përfshijnë konstruktorin e zakonshëm, konstruktorin e kopjimit (copy constructor), destruktorin, empty() (zbraze), max_size() (madhësia maksimale), size() (madhësia), sëap() (shkëmbe), operatorin = dhe përveq ‘priority_queue’ gjashtë operatorët relacional të mbingarkuar. Për më tepër, funksionet e zakonshme në të gjithë kontejnerët, përveq stack, queue dhe priority_queue, përfshijnë edhe funksionet: begin() (fillimi), end() (fundi), rbegin(), rend(), erase() (fshije) dhe clear() (pastro). Elementet e ruajtura në kontejnerë mund të jenë të çfarëdo tipi dhe ato duhet të ofrojnë së paku konstruktorin e zakonshëm, destruktorin dhe operatorin e ndarjes së vlerë (=). Kjo është posaqërisht e rëndësishem për tipet e definuara prej shfrytëzuesit. Disa kompajlerë mund të kërkojnë mbingarkimin e disa operatorëve (së paku ‘= =’ dhe ‘’ poashtu) edhe pse programi nuk i përdorë ato. Gjithashtu, ‘copy construcor-i’ dhe operatori i funksionit ‘=’ duhet të ofrohen nëse të dhënat janë pointerë, sepse operacionet e insertimit përdorin kopjen e një elementi që është duke u insertuar, e jo vetë elementin. 75

Avni Rexhepi

1.7.2 Iteratorët Iteratori (angl. interate – përsëris), është një objekt që përdoret për të ju referuar një elementi të ruajtur në kontejner. Prandaj, iteratori është një përgjithsim i pointerit. Një iterator mundëson qasjen në informacionin e përmbajtur në kontejner ashtu që opercioni i dëshiruar të mund të kryhet në këto elemente. Si përgjithësim i pointerëve, iteratorët mbajnë notacionin e njëjtë të dereferencimit. Për shembull, ‘*i’ është një element i referencuar nga iteratori ‘i’. Poashtu, aritmetika e iteratorëve është e ngjashme me atë të pointerëve, edhe pse të gjitha operacionet në iteratorë nuk lejohen në të gjithë kontejnerët. Për kontejnerët: stack, queue dhe priority_queue nuk përkrahet asnjë iteratorë. Operacionet e iteratorëve për klasat list, map, multimap, set dhe multiset, janë si vijon (i1 dhe i2 janë iteratorë, n është numër): i1++, ++i1, i1--, --i1 i1=i2 i1 == i2, i1 != i2, *i1

Përveq këtyre operacioneve, operacionet e iteratorëve për klasat deque dhe vector janë si vijon: i1 < i2, i1 i2, i1 >= i2 i1 + n, i1 - n i1 += n, i1 -= n, i1[n]

1.7.3 STL Algoritmet STL ofron afër 70 funksione të përgjithshme, të cilat mund të aplikohen në kontejnerë dhe vargje, duke u përfshirë në program përmes direktivës #include . Këto funksionie (të quajtura edhe algoritme) janë operacione që përdoren shumë shpeshë në shumicën e programeve, si p.sh. lokalizimi i një elementi në kontejner, insertimi i elementeve, largimi i elementeve, modifikimi i elementeve, krahasimi i elementeve, gjetja e vlerës bazuar në sekuencën e elementeve, sortimi i elementeve, e kështu me radhë. Pothuajse të gjitha STL algoritmet përdorin iteratorët për të treguar rangun e elementeve në të cilat ato operojnë. Iteratori i parë i referohet elementit të parë në rang, i dyti elementit pas elementit të parë. Prandaj, supozohet se është gjithmonë e mundur që të arrihet poizita e treguar me iteratorin e dytë duke inkrementuar iteratorin e parë. Për shembull, thirrja e funksioneve: 76

Algoritmet dhe strukturat e të dhënave random_shuffle(c.begin(), c.end());

i renditë në mënyrë të rastit të gjitha elementet e kontejnerit ‘c’. Thirrja: i3 = find (i1, i2, el);

kthen një iteratorë i cili tregon pozitën e elementit ‘el’ në rangun prej i1 deri në i2. Thirrja: n = count_if(i1, i2, oddNum);

numëron përmes algoritmit ‘count_if’ elementet në rangun e treguar përmes iteratorëve i1 dhe i2, për të cilët funksioni me një argument, i definuar nga shfrytëzuesi, ‘oddNum()’, kthen ‘true’. Algoritmet e STL-it janë funksione të cilat janë plotësim për funksionet e ofruara nga kontejnerët. Sidoqoftë, disa algoritme jantë të definuara si funksione anëtare të klasave, për të ofruar performansë më të mirë.

Aritmetika e pointerëve Pointerët përdoren për të kaluar nëpër memorie sekuencialisht duke përdorur aritmetikën e pointerëve dhe operatorin e inkrementimit (++) dhe operatorin e dekrementimit (--). Operatori i inkrememntimit e rritë vlerën e variablës për 1, ndërsa operatori i dekrementimit e zovëlon vlerën e variablës për 1. Në shembullin vijues, vlera e variablës numriStudentit rritet për 1, duke bërë që vlera finale të jetë 1235: int numriStudentit = 1234; numriStudentit++;

ngjashëm, shembulli vijues, e zvogëlon vlerën e variablës numriStudentit për 1, duke rezultuar në velrën finale 1233. int numriStudentit = 1234; numriStudentit--;

Aritmetika e pointerëve përdorë operatorin e inkrementimit dhe atë të dekrementimit, në mënyrë të ngjashme, por pak më ndryshe. Urdhërat vijues deklarojnë dy variabla të përdorura për të ruajtur numrat e studentëve dhe dy pointerë, ku secili pointon në njërën prej këtyre variablave. int numriStudentit1 = 1234; int numriStudentit2 = 5678; int *ptNumriStudentit1; int *ptNumriStudentit2; ptNumriStudentit1 = &numriStudentit1;

77

Avni Rexhepi ptNumriStudentit2 = &numriStudentit2;

Sa do të jetë vlera e ruajtur në pointerin ptNumriStudentit1 nëse ptNumriStudentit1 inkrementohet për 1 duke përdorur urdhërin vijues: ptNumriStudentit1++;

Kjo është pak problematike, sepse vlera e ptNurmriStudentit1 është 0. Nëse ajo inkrementohet për 1, vlera e re do të duhet të ishte 1. Mirëpo, adresa e memories 2 është pjesa e dytë e lokacionit të memories së rezervuar për numriStudentit1. Kjo do të thotë se ptNumriStudentit1 do të pointonte në mes të vlerës së numriStudentit1, gjë që nuk ka kuptim. Ja se çka ndodhë në realitet. Kompjuteri përdorë aritmetikën e pointerëve. Vlerat inkrementohen dhe dekrementohen në aritmetikë të pointerëve duke përdorur madhësinë e tipit të të dhënave. Pra, nëse adresa e memories përmbanë një vlerë integer dhe adresa e memories inkrementohet, kompjuteri e shton madhësinë e një integeri ndaj lokacionit aktual të adresës së memories. Kjo i bie, që lëvizja para/prapa bëhet me hapin e tipit të pointerit, e secili tip i pointerit ka madhësinë e hapit varësisht prej tipit të variablës në të cilën pointon. Kjo i bie që pointeri i tipit int, ka hapin 4 bajta (kalon nga 4 bajta), ai i tipit double ka hapin 8 bajta (kalon nga 8 bajta) dhe ai i tipit karakter ka hapin 1 bajt (gjegjësisht, lëvizë me hap prej 1 bajti). Në rastin e strukturave, pointeri i struktorës lëvizë me hapin e madhësisë totale të strukturës. Figura 1.21 paraqet një varg a, pointerin ptr dhe urdhërin ptr=a. Këtu përforcohet idea se vlera e ruajtur në a është vetëm lokacioni i memories ku qëndron elementi i “zero-t” (elementi i pare, me indeks 0) i vargut dhe se elementet e vargut garantohet të jenë të ruajtura në lokacione të njëpasnjëshme (konsekutive, të afërta) dhe në rritje të memories. Nëse a është një varg i karaktereve, a[1] është i ruajtur në lokacionin e memories a+1, sepse karakteret përdorin një bajt. Për ketë arsye, ++ptr do të rriste pointerin ptr për 1, duke rezultuar në lokacionin e memories së a[1]. Prandaj, shtimi i një integjeri në variablën pointer ka kuptim në një varg të karaktereve. Nëse a do të ishte varg i integjerëve 4-bajtësh, shtimi i 1-shit në ptr do të dukej se bën që pointri të lëvizë një bajt më tutje. Mirëpo, për interpretim më të lehtë, thuhet se: ++ptr ia shton adresës së pointerit ptr, madhësinë e objektit në të cilin pointon. Ky interpretim bartet në operacionet tjera me pointer. Kështu, shprehja x=&a[3] bën që pointeri x të pointojë në a[3]. Nuk ka nevojë për kllapa të vogla përreth, si është thënë më herët. Shprehja y=x+4, bën që pointeri y të pointojë në a[7]. Kështu, do të mund të përdorim pointerët për të përshkuar vargun, në vend të metodës së zakonshme të iteracionit me indeksat e vargut. 78

Algoritmet dhe strukturat e të dhënave a

a[0] a[1]

ptr

a[2] a[3] a[4] a[5]

x

y

a[6] a[7] a[8] a[9]

Figura 1.21 Aritmetika e pointerëve: x=&a[3]; y=x+4 ; Nëse p është pointer dhe x është i tipit integer, g+x vlerësohet si adresa ‘g’ objekte përtej x-it. Kjo adresë është gjithashtu lokacioni i memories së g[x].

Edhe pse mbledhja apo zbritja e tipit integer prej tipit pointerit ka kuptim, mbledhja e dy pointerëve nuk ka kuptim. Mirëpo, zbritja e dy pointerëve funksionon: y-x do të vlerësohej 4 (në shembullin e mëparshëm, lartë – sepse zbritja është operacioni invers i mbledhjes). Prandaj, pointerët mund të zbriten, por jo të mblidhen. Për dy pointerë, x dhe y, xpush(30); for(int i=0; ienqueue(10); queue->enqueue(20); queue->enqueue(30); for(int i=0; ieArdhshme=NULL; // Te pointeri, nyja e ardhshme=NULL //(d.m.th., s'ka nyje tjetër ne vazhdim) cout vlera; }

212

Algoritmet dhe strukturat e të dhënave void listaeLidhur :: insertoNeFillim() { nyje *temp; temp=new nyje [1]; temp->pNeA=fillimi; fillimi=temp; couttemp->vlera; } int listaeLidhur :: largo() { nyje *temp,*ePerparshme=NULL; //Pointeret, per nyjen ndihmese dhe nyjen e perparshme int x; if(fillimi==NULL) { coutpNeA; if (temp==NULL) { coutpNeA; coutprevious = back; back = n; } } void LinkedList::displayNodes() { cout data; Node* temp = front; if(front->next == NULL) { back = NULL; front = NULL; } else { front = front->next; front->previous = NULL; } delete temp; return retVal; } bool QueueLinkedList::isEmpty() { if(front == NULL) { return true; } else { return false; } }

Funksionet plotësuese për stek dhe queue: Insert, Delete, Peek, Find Sikur në rastin e mësimit të programimit, ku në fillim mësohen vetëm gjërat elementare e pastaj kalohet në programe më komplekse, edhe në rastin e listave të lidhura, pasi u sqaruan funksionet themelore për punë me listat e lidhura, është radha të shohim edhe funksionet plotësuese, të fuqishme, të cilat nevojiten për të ndërtuar aplikacione të kompletuara. Do të shohim funksionet për insertim, nxjerrje, fshirje dhe kërkim të cilat do të mundësojnë përdorimin e stekut dhe queue-s nëpër aplikacione. 242

Algoritmet dhe strukturat e të dhënave

Klasa e zgjeruar LinkedList Për të rritur efikasitetin e klasës LinkedList dhe për ta bërë më të lehtë për përdorim, do të rrisim funksionalitetin e klasës së definuar më parë. Si është thënë më parë, klasa LinkedList krijon instancë të strukturës ‘Node’ që është definuar në header fajllin LinkedList.h. Struktura ‘Node’ ka tri elemente: të dhënën (vlerën) e ruajtur në nyje, pointerin për në nyjen e ardhshme dhe pointerin për në nyjen e përparshme në listën e lidhur. Klasa LinkedList që u përdor në pjesën e përparshme përmbanë dy anëtarë të të dhënave (dy vlera) dhe gjashtë funksione anëtare. Anëtarët e të dhënave janë referenca (pointerë) për në nyjen që ndodhet në fillim (front) të listës dhe për nyjen që ndodhet në fund (back) të listës së lidhur. Klasa LinkedList ha funksionet të cilat e shtojnë nyjen në listën e lidhur dhe paraqesin të dhënat (vlerat) e nyjeve në renditje natyrale dhe të kundërt, si dhe funksionin për asgjësim të listës së lidhur. Si plotësim, klasa LinkedList ka edhe konstruktorin dhe destruktorin. Kët anëtarë (vlera dhe funksione) janë skeleti i nevojshëm për operim të klasës LinkedList. Tani do të ndërmarrim disa hapa plotësues për të rritur funksionalitetin e klasës LinkedList duke e bërë më të përdorshme dhe më të dobishme për punë me aplikacionet me lista të lidhura. Plotësimi i parë është definimi i anëtarit të ri të të dhënave, të quajtur ‘size’. Anëtari ‘size’ është një numër i plotë (integer) që përfaqëson numrin e nyjeve që ndodhen në listën e lidhur. (Poërdoret indeksi, por ‘size’ përcakton nëse është lista e zbrazët ose nëse është përcjellur indeksi jo valid). Kjo vlerë mund të përdoret sa herë që aplikacioni ka nevojë të dijë madhësinë e listës së lidhur. Pastaj, duhet të definohen funksionet plotësuese, i pari prej të cilave do të jetë funksioni removeNode() (angl. remove node – largo nyjen). Funksioni removeNode() largon nyjen dhe e rilidhë listën e lidhur. Ky funksion është ‘protected’ (i mbrojtur) sepse përdoret vetëm në mënyrë interne (përbrenda). Shfrytëzuesi i kësaj klase nuk do të dinte pointerët në nyjet individuale. Ky është funksion i përshtatshëm për largimin e nyjeve. Një funksion tjetër i dobishëm që do të definohet, është funksioni removeNodeAt(), i cili e largon nyjen që ndodhet në një lokacion të caktuar të listës së lidhur. Radha në të cilën paraqiten nyjet e listës referohet si ‘index order’ (renditja e indeksuar). Pozita e nyjes në listën e lidhur referohet si index-i i nyjes. Indeksi përcillet funksionit removeNodeAt() për të specifikuar nyjen e cila do të largohet nga lista e lidhur. Atëherë funksioni removeNodeAt() e largon nyjen dhe i rivendosë lidhjet në listën e lidhur. Nyja në fillim (front) të 243

Avni Rexhepi listës e ka indeksin 0; pastaj indeksi inkrementohet për një, duke lëvizur përpara kah fundi (back) i listës. Funksioni appendNode() i shton nyjet në fund (back) të listës së lidhur, kështu që kjo mund të imagjinohet si varg dinamik që lëvizë prej fillimit kah fundi. Ndonjëherë mund të mos dihet indeksi i nyjes që dëshirojmë të lagrohet nga lista e lidhur. Në këtë rast, duhet të definohet një funksion tjetër i cili e largon nyjen bazuar në vlerën (të dhënën) e nyjes, jo në indeksin e nyjes. Ky funksion do të quhet deleteNode(). Funksioni deleteNode() dallon nga funksioni removeNodeAt() për nga mënyra se si funksioni e identifikon nyjën që duhet të largohet nga lista e lidhur. Funksioni removeNodeAt() e lokalizon nyjen që duhet larguar duke përdorur vlerën e indeksit të nyjes. Funksioni deleteNode() e lokalizon nyjen që duhet larguar duke përdorur vlerën e të dhënës së nyjes, e cila i përcillet funksionit deleteNode(). Deri më tani, nyjet në listën e lidhur janë qasur në mënyrë sekuenciale. Mirëpo, në disa aplikacione reale, nyjet qasen edhe në mënyrë të rastit. Funksioni i ardhshëm që do të definohet për klasën LinkedList mundëson qasjen në një nyje specifike. Ky funksion do të quhet findNode() dhe përdoret kur dihet e dhëna (vlera) e përmbajtur në nyje por nuk dihet pozicioni i nyjës në listën e lidhur (nuk dihet indeksi). Për të lokalizuar nyjen, funksionit i jepet e dhëna (vlera) e ruajtur në nyje, kurse funksioni findNode() e kthen indeksin e nyjes. Lista origjinale LinkedList është e aftë që të shtojë një nyje të re në listën e lidhur, duke e shtuar atë në fund të listës. Mirëpo, do të ketë situata kur duhet të insertohet një nyje e re diku në mest të listës së lidhur. Për të bërë këtë, duhet të definohet funksioni insertNodeAt() (inserto nyjen në). Funksioni insertNodeAt() do të kërkojë dy parametra. Parametri i parë është indeksi i nyjes që do të lëvizet (zhvendoset) në listën e lidhur, për t’i bërë vend nyjes së re. Ky indeks, bëhet indeksi i nyjes së re. Parametri i dytë është vlera (e dhëna) që do t’i ndahet nyjes së re. Funksioni insertNodeAt() krijon nyjen e re dhe përshtatë referencat (pointerët) në listën e lidhur, për të lidhur nyjen e re me nyjet tjera në listën e lidhur. Një plotësim i rëndësishëm i klasës LinkedList është “nxjerrja” (angl. retrieverigjej, rifitoj) e vlerës (të dhënës) që është e ruajtur në nyjen specifike. Më parë kishim dy funksionet ‘display’ të cilat përdoreshin për të “shtypur” të dhënat në listën e lidhur. Funksioni i ri, që do të quhet peek() (angl. peek-shikoj vjedhurazi, përgjoj). Funksioni peek() kërkon që t’i përcillet indeksi i nyjes që e përmbanë vlerën të cilën dëshironi ta nxirrni (shihni, tërhiqni). Ai pastaj e kthen vlerën (të dhënën) që ndodhet në atë nyje. Plotësimi i fundit që do të bëhet në klasën LinkedList është definimi i funksionit që kthen numrin e nyjeve të listës së lidhur. Ky funksion do të quhet getSize() dhe do të përdoret sa herë që ka nevojë për të ditur madhësinë e listës së lidhur. 244

Algoritmet dhe strukturat e të dhënave Në vijim do të paraqitet header fajlli i ripërpunuar LinkedList.h, i cili përmbanë definicionet e strukturës së nyjes dhe klasës së zgjeruar LinkedList. Vëreni se anëtari ‘size’ dhe funksioni removeNode() janë të vendosur në zonën e qasjes së mbrojtur (protected) të definicionit të klasës. Kjo për arsye se asnjëri nuk përdoret drejtpërdrejt nga aplikacioni. Në vend të kësaj, ata përdoren nga funksionet anëtare të klasës LinkedList dhe nga funksionet anëtare të klasave që e trashëgojnë klasën LinkedList. Të gjitha funksionet janë vendosur në pjesën zonën me qasje publike të definicionit të klasës LinkedList dhe janë në dispozicion për qasje direkte nga aplikacioni. Më vonë do të paraqiten detajet për mënyrën e funksionimit të funksioneve anëtare. //LinkedList.h // Node=Nyje; data=vlera; // previous=ePerparshme; next=eArdhshme struct Node { int data; Node* previous; Node* next; }; class LinkedList { protected: Node* front; Node* back; int size; void removeNode(Node* node); public: LinkedList(); virtual ~LinkedList(); void appendNode(int); void displayNodes(); void displayNodesReverse(); void destroyList(); void removeNodeAt(int); int findNode(int); void deleteNode(int); void insertNodeAt(int,int); int peek(int); int getSize(); };

245

Avni Rexhepi

Funksionet removeNode(), removeNodeAt(), dhe deleteNode() Largimi i nyjes nga lista e lidhur është operacion i ndërlikuar. Së pari duhet të “zgjidhet” nyja (të largohen lidhjet e nyjes) nga lista e lidhur. Mirëpo, duke bërë këtë, prishen lidhjet, sepse më nuk ka asgjë që e lidhë nyjen e përparshme (previuos) dhe nyjen e ardhshme (next), sepse nyja që largohet ishte lidhja ndërmjet tyre. Kjo do të thotë që, pas largimit të nyjes, duhet të lidhen mes veti nyja e përparshme dhe nyja e ardshme, e nyjes që largohet. Klasa LinkedList mund të zgjerohet për të përmbajtur tri funksione të cilat e largojnë nyjen nga lista e lidhur dhe pastaj i lidhin mes veti nyjen e përparshme dhe nyjen e ardhshme të saj. Këto funksione janë: removeNode(), removeNodeAt() dhe deleteNode(). Funksionit removeNode() i përcillet referenca (pointeri) për në nyjen që duhet larguar nga lista e lidhur dhe thirret nga funksioni removeNodeAt() dhe funksioni deleteNode(). Funksioni removeNode() nuk mund të thirret direkt nga aplikacioni sepse është anëtarë i mbrojtur i klasës. Funksioni removeNodeAt() e përdorë indeksin e nyjes për të lokalizuar nyjen që duhet të largohet. Kur të gjindet nyja, referenca e saj (pointeri në të) i përcillet funksionit removeNode(). Ngjashëm, funksioni deleteNode() e përdorë vlerën e nyjes për të lokalizuar nyjen. Kur të gjindet nyja, funksioni deleteNode() e nxjerrë referencën e nyjes (pointerin), i cili pastaj i përcillet funksionit removeNode(). Për shembujt në këtë pjesë, do të përdoret lista e lidhur e paraqitur në figurën 4.16, e cila ka pesë nyje: NodeA deri në NodeE, repsektivisht. Secila nyje e mbanë pozitën në listën e lidhur dhe secila pozitë identifikohet nga një indeks. Vlerat e indeksave fillojnë prej zeros dhe janë paraqitur përmbi emrin e secilës nyje (në figurën 4.16).

Figura 4.16 - Lista e lidhur që përmbanë pesë nyje, ku secila nyje identifikohet përmes indeksit. Fillojmë me definimin e funksionit removeNode(), i cili është ilustruar në kodin 246

Algoritmet dhe strukturat e të dhënave vijues. Referenca për në nyjen që do të largohet (pointeri) i përcillet funksionit removeNode(). Funksioni removeNode() duhet të vendosë se cili prej katër proceseve duhet të përdoret për të larguar nyjen. Procesi i parë që duhet të përcaktohet (kontrollohet, verifikohet) nga funksioni removeNode() është nëse nyja është e vetmja nyje në listën e lidhur. Ky e bën këtë përcaktim duke vlerësuar nëse nyjet: e përparshme (previous) dhe e ardhshme (next) janë NULL. Nëse janë, nyja që fshihet është e vetmja nyje në listë. Nyja pastaj largohet duke ia ndarë vlerën NULL anëtarëve ‘front’ dhe ‘back’ të klasës LinkedList. Si ju kujtohet, funksionet që i “nxjerrin” të dhënat nga lista e lidhur gjithmonë verifikojnë anëtarët ‘front’ dhe ‘back’ për të përcaktuar nëse janë të dy NULL. Nësë po, atëherë funksioni e di se lista e lidhur nuk ka asnjë nyje. Nëse nyja nuk është e vetmja nyje në listën e lidhur, funksioni removeNode() duhet të verifikojë pastaj nëse nyja që largohet është në fillim (front) të listës së lidhur. Ai e kontrollon këtë duke verifikuar anëtarin e përparshëm (previuous) të nyjes. Nëse nyja ndodhet në fillim (front) të listës së lidhur, atëherë anëtari ‘previuous’ është NULL dhe funksioni removeNode() i ndërmerr hapat vijues për të larguar nyjen: 1. Nyja në të cilën pointohet nga anëtari anëtari ‘next’ i nyjes së fshirë (larguar), i ndahet anëtarit ‘front’ të listës së lidhur. Kjo e bënë atë ‘front’ (fillim) të listës së lidhur. 2. Anëtarit ‘previous’ të nyjes që tani është në fillim (front) të listës së lidhur i caktohet vlera NULL, për të treguar se nuk ka nyje të përparshme, sepse është larguar nyja e përparshme e saj. Kjo bëhet si në vijim. Mund të duket pak konfuze, por është e lehtë të kuptohet nëse e ndani këtë urdhër: node->next->previous = NULL;

3. Le të themi se largohet NodeD. Nyja e ardhshme për të është NodeE. Tani, zëvendësoni emrat për termat në këtë shprehjeje: NodeD->NodeE->previous = NULL;

Është e qartë se anëtari ‘previous’ i përket nyjes NodeE. Në procesin e tretë, funksioni removeNode() e kontrollon (përcakton) nëse nyja që largohet është në fund (back) të listës së lidhur. Këtë e bënë duke krahasuar vlerën e anëtarit ‘next’ (të ardhshëm) me NULL. Nëse anëtari ‘next’ është NULL, atëherë nyja që largohet është nyja e fundit në listën e lidhur. 247

Avni Rexhepi Vlera e anëtarit ‘previuous’ (e përparshme) të nyjes pastaj i ndahet anëtarit ‘back’ të klasës LinkedList. Kjo e lëvizë nyjen e përparshme në fund të listës dhe në fakt (si rezultat) e largon nyjen që i përcillet funksionit removeNode(), nga lista e lidhur. Vlera e anëtarit ‘next’ të nyjes së përparshme pastaj caktohet në NULL, për të treguar se nuk ka nyje tjetër pas saj sepse ajo është në fund (back) të listës. Urdhëri që e kruen këtë operacion mund të duket konfuz, por duke zëvendësuar referencat (pointerët) në nyje dhe ‘previuos’ me numrin e nyjes, duhet të jetë e qartë se çka ndodhë. Urdhëri është: node->previous->next = NULL;

Le të themi se largohet nyja NodeC. Nyje e përparshme (previuous) është NodeB. Tani zëvendësoni emrat për termat në këtë urdhër: NodeC->NodeB->next = NULL;

Nëse nyja që fshihet nuk është nyja e vetme në listën e lidhur dhe nuk është nyja në fillim ose në fund të listës së lidhur, atëherë mundësia e vetmë është që nyja ndodhet diku tjetër përbrenda listës. Procesi i katërt është largimi i nyjes në mes të listës së lidhur dhe pastaj lidhja e nyjes së përparshme dhe të ardhsme të saj. Edhe kjo ilustrohet më mirë përmes shembullit. Le të themi se do të largohet NodeC. Nyje e përparshme është NodeB dhe nyje e ardhshme është NodeD. Së pari, lidhet nyja NodeB me NodeD, duke përdorur urdhërin vijues: node->previous->next = node->next;

Zëvendësoni ‘node’, ‘previuos’ dhe ‘next’ me emrat e nyjeve aktuale, për të kuptuar më mirë veprimin: NodeC->NodeB->next = NodeC->NodeD;

Tani që NodeB është lidhur me NodeD, duhet të lidhet edhe NodeD me NodeB: node->next->previous = node->previous;

Përsëri, zëvendësoni emrat e nyjeve: NodeC->NodeD->previous = NodeC->NodeB;

Të dyja, NodeB dhe NodeD tani janë të lidhura mes tyre dhe NodeC është larguar nga lista e lidhur. Edhe pse nyja e përcjellur në funksionin removeNode() nuk është më në listën e lidhur, ajo akoma mbetet në memorie. Prandaj, ajo duhet të largohet nga memoria duke thirrur operatorin ‘delete’. 248

Algoritmet dhe strukturat e të dhënave Hapi i fundit është që të përshtatet vlera e anëtarit ‘size’ (madhësia) e klasës LinkedList, për të reflektuar një nyje më pak në listën e lidhur. Kjo do të bëhet përmes dekrementimit të vlerës ‘size’. Figura 4.17 paraqet listën e lidhur pas ekzekutimit të funksionit removeNode(). Vëreni se NodeC nuk është më në listën e lidhur dhe vlerat e indeksave janë përshtatur për të reflektuar numrin e ri të nyjeve në listën e lidhur.

Figura 4.17: Lista e lidhur pas largimit të NodeC

void removeNode(Node* node) { if(node->previous == NULL && node->next == NULL) { back = NULL; front = NULL; } else if(node->previous == NULL) { front = node->next; node->next->previous = NULL; } else if(node->next == NULL) { back = node->previous; node->previous->next = NULL; } else { node->previous->next = node->next; node->next->previous = node->previous; } delete node; size--; }

249

Avni Rexhepi

Funksioni removeNodeAt() Funksioni removeNodeAt() largon nyjen duke përdorur indeksin e nyjes (e jo pointerin për në nyje, në memorie). Mbani mend, indeksi është pozita e nyjes në listën e lidhur. Le të themi se dëshironi të largoni nyjen e tretë në listën e lidhur. Atëherë, thjeshtë ia përcillni indeksin 2 funksionit removeNodeAt() dhe removeNode() e kryen operacionin në mënyrë interne. Nuk mund ta thirrni direkt funksionin removeNode(), pjesërisht për arsye se është i mbrojtur, por edhe për arsye se jashtë kësaj klase ju nuk keni njohuri për vlerat aktuale të pointerëve. Pasi që indeksat fillojnë prej zeros, ju nuk keni nevojë të dini pointerin aktual në nyje të cilën dëshironi ta largoni. Kjo është ilustruar në kodin vijues. Hapi i parë në funksionin removeNodeAt() është të kontrollojë nëse indeksi është valid. Për të bërë këtë, funksioni removeNodeAt() kontrollon nëse indeksi është më i vogël se zero ose më i madh se madhësia e listës minus një. Ai e përdorë vlerën e anëtarit ‘size’ të klasës LinkedList për të përcaktuar madhësinë e listës së lidhur. Nëse ndonjëri prej kushteve është ‘true’, atëherë indeksi është jo valid dhe nuk tentohet të fshihet nyja. Mirëpo, nëse të dy kushtet janë ‘false’, atëherë funksioni removeNodeAt() fillon procesin e largimit të nyjes prej listës. Ky proces i ka dy hapa. Së pari, indeksi lokalizon referencën (pointerin) për në nyjen korresponduese dhe së dyti, thirret funksioni removeNode() dhe i përcillet refernca (pointeri). Funksioni removeNodeAt() fillon kërkimin për referencën (pointerin) në nyje duke deklaruar pointerin e përkohshëm për në nyje, të quajtur temp_node dhe duke ia ndarë atij referencën (pointerin) për në nyjen në fillim (front) të listës së lidhur. Pastaj, unaza ‘for’ kalon nëpër secilën nyje në listën e lidhur deri sa të gjindet nyja e reprezentuar nga indeksi. Gjatë secilës përsëritje (iteracion), pointerit temp_node i ndahet nyja e pointuar nga anëtari ‘next’ i ‘temp_node’ aktual. Kur të arrihet indeksi, vlera e temp_node është referencë (pointer) për në nyjen e cila i përgjigjet (korrespondon) indeksit që i përcjellet funksionit removeNodeAt(). Thirret funksioni removeNode() dhe i përcillet ‘temp_node’. void removeNodeAt(int index) { if(index < 0 || index > size-1) { return; }

250

Algoritmet dhe strukturat e të dhënave Node* temp_node = front; for(int i=0; inext; } removeNode(temp_node); }

Funksioni deleteNode() Funksioni deleteNode() përdorë vlerën e ruajtur në nyje për të gjetur dhe larguar nyjen gjegjëse prej listës së lidhur. Funksioni deleteNode() pastaj kërkon listën e lidhur për të lokalizuar dhe larguar nyjen. Procesi funksionon si në vijim. Së pari, deklarohet nyja e përkohshme e quajtur temp_node dhe i ndahet referenca (pointeri) për në nyjen që ndodhet në fillim (front) të listës së lidhur. Nëse temp_node nuk është NULL, atëherë lista nuk është e zbrazët dhe funksioni përcakton (kontrollon) nëse vlera (e dhëna në të) përshtatet me vlerën (të dhënën) e nyjes aktuale. Nëse po, atëherë temp_node i përcillet funksionit removeNode() dhe funksioni përfundon. Nëse të dhënat (vlerat) nuk përshtaten, atëherë nyja e ardhshme (next) i ndahet asaj temp_node dhe procesi vazhdon deri sa të gjindet nyja që përmbanë vlerën ose funksioni deleteNode() arrin fundin e listës së lidhur (dmth vlera nuk gjindet). void deleteNode(int data) { Node* temp_node = front; while(temp_node != NULL) { if(temp_node->data == data) { removeNode(temp_node); return; } else { temp_node = temp_node->next; } } }

251

Avni Rexhepi

Funksioni findNode() Nëse duhet të qaset një nyje e veçantë në listën e lidhur, por ju nuk e dini referencën (pointerin) për në atë nyje e as pozitën e nyjës në listën e lidhur, mirëpo e dini vlerën (të dhënën) që ruhet në nyje, atëherë nyja mund të lokalizohet duke thirrur funksionin findNode() (angl. find-gjej). Funksioni findNode() kërkon që t’ia përcjellni vlerën e ruajtur në nyje. Ai pastaj e përdor vlerën (të dhënën) për të lokalizuar nyjen dhe kthen indeksin e nyjes, siç paraqitet në shembullin vijues. Procesi i gjetjes së nyjes fillon kur deklarohet një variabël e indeksit të cilës në fund do t’i ndahet indeksi i nyjes, nëse ajo gjindet. Deklarohet gjithasthtu edhe nyja e përkohshme dhe i ndahet referenca (pointeri) për në nyjen në fillim (front) të listës së lidhur. Gjersa temp_node nuk është NULL, funksioni findNode() iteron (kalon me përseritje, në unazë) nëpër listën e lidhur. Me secilin iteracion (përsëritje) vlera e nyjes aktual krahasohet me vlerën e përcjellur si argument në funksionin findNode(). Nëse të dyja janë të barabarta, atëherë kthehet vlera aktuale e indeksit, që është indeksi i nyjes. Nëse nuk janë të barabartë, atëherë vlera e anëtarit të ardhshëm të nyjes aktuale i ndahet asaj temp_node dhe inkrementohet indeksi. Nëse vlera (e dhëna) nuk është gjetur në listën e lidhur, kthehet ‘-1’, sepse vlera ‘-1’ nuk mund të jetë asnjëherë vlerë e kthyer valide (e vlefshme). int findNode(int data) { int index = 0; Node* temp_node = front; while(temp_node != NULL) { if(temp_node->data == data) { return index; } else { temp_node = temp_node->next; index++; } } return -1; }

252

Algoritmet dhe strukturat e të dhënave

Funksioni insertNodeAt() Funksioni insertNodeAt() inserton një nyje të re në lokacionin e caktuar të listës së lidhur. Dihet se secila pozitë në listën e lidhur identifikohet përmes indeksit dhe lokacioni i parë ka vlerën e indeksit 0, i dyti 1, e kështu me radhë. Për të specifikuar lokacionin ku dëshironi të vendosni (insertoni, shtoni) nyjen e re në listën e lidhur, përdoret indeksi. Funksioni insertNodeAt() kërkon dy argumente: lokacionin ku do të insertohet nyja në listën e lidhur dhe vlera (e dhëna) që do të ruhet në atë nyje. Shembulli në vijim paraqet mënyrën e vendosjes së nyjes në listën e lidhur. Hapi i aprë është që funksioni insertNodeAt() të kontrollojë nëse indeksi i përcjellur në funksion është valid. Ai e bënë këtë duke kontrolluar nëse indeksi është më i vogël se zero ose më i madh se madhësia e listës (kjo informatë gjindet në anëtarin ‘size’ të klasës LinkedList). Nëse indeksi është invalid, atëherë funksioni insertNodeAt() ndërprehet dhe kthen përgjigjen në urdhërin që e ka thirrur. Ka një ndryshim të vogël në rastin e kontrollimit të indeksave, në krahasim me funksionin removeNodeAt(). Nëse indeksi do të ishte i barabartë me ‘size’, kjo do të bënte që nyja të shtohet në listën e lidhur. I bie që indeksi është për 1 jashtë kufijve të indeksave të vargut, por kjo është në rregull, sepse në fakt në këtë rast do të shtohet një nyje e re në fund të listës së lidhur, ndërsa funksioni removeNodeAt() kërkonte indeks valid në rangun nga 0 deri në ‘size1’, që është nyja e fundit në listën e lidhur. Kur funksioni insertNodeAt() e verifikon se indeksi është valid, ai vazhdon me krijimin e nyjes së re dhe e inserton nyjën në listën e lidhur. Ky proces fillon me krijimin në instance të strukturës së nyjes (node) dhe duke i ndarë asaj vlerën e përcjellur në funksion si argument. Kjo instancë pastaj i ndahet pointerit new_node (nyja e re). Pastaj, duhet të verifikohet a ka ndonjë nyje në listën e lidhur. Kjo bëhet duke kontrolluar vlerën e anëtarit ‘size’ të klasës LinkedList. Nëse vlera është zero, atëherë lista e lidhur është e zbrazët dhe nyja e re do të bëhet nyja e parë (e vetme) në listë. Nyja e re vendoset në listë duke ia ndarë pointerin new_node të dy anëtarëve, ‘front’ dhe ‘back’, të klasës LinkedList. Anëtarët ‘previous’ dhe ‘next’ të nyjes veq janë caktuar në NULL (si vlerë e nënkuptuar, default) ashtu që nuk duhet bërë asgjë në nyjen e re. front = new_node; back = new_node;

253

Avni Rexhepi Nëse lista e lidhur ka një ose më shumë nyje, atëherë funksioni insertNodeAt() kontrollon nëse nyja e re duhet të insertohet në pozitën e parë në listë, duke vlerësuar vlerën e indeksit të përcjellur në funksion. Nëse vlera e indeksit është zero, atëherë nyja e re do të bëhet nyje e parë në listën e lidhur. Kjo bëhet si në vijim. Nyja e re (new_node) i ndahet anëtarit ‘previous’ (të përparshëm) të nyjes së ndarë anëtarit ‘front; të klasës LinkedList. Pastaj, anëtarit ‘next’ të new_node (nyjës së re) i ndahet nyja e ndarë anëtarit ‘front’ të klasës LinkedList. Në fund, anëtarit ‘front’ i ndahet new_node. front->previous = new_node; new_node->next = front; front = new_node;

Nëse nyja e re nuk do të bëhet nyje e parë e listës së lidhur, atëherë funksioni insertNodeAt() vendosë nëse nyja do të bëhet nyje e fundit e listës së lidhur, duke krahasuar indeksin me madhësinë (anëtarin ‘size’) e klasës LinkedList. Nëse këto dy vlera janë të barabarta, atëherë nyja e re vendoset në fund të listës së lidhur. Rikujtojmë se indeksi 0 është fillimi i listës (front) dhe indeksi (size-1) është fundi i listës (back). Kjo bëhet si në vijim. ‘new_node’ i ndahet anëtarit ‘next’ të nyjes që aktualisht ndodhet në fund të listës, ‘back’. Pastaj, nyja në fund, ‘back’, i ndahet anëtarit ‘previous’ (të përparshëm) të nyjes së re. Në fund, nyja e re (new_node) i ndahet anëtarit ‘back’ të klasës LinkedList. back->next = new_node; new_node->previous = back; back = new_node;

Në këtë pikë, nëse nyja e re nuk është insertuar as në fillim (front) e as në fund (back) të listës së lidhur, atëherë funksioni insertNodeAt() supozon se nyja e re do të insertohet diku në pjesën ndërmjet, të listës së lidhur. Ky proces fillon me deklarimin e pointerit të quajtur ‘temp’ dhe në fillim duke i ndarë atij nyjen në fillim (front) të listës së lidhur. Pastaj, funksioni e gjenë nyjen në indeksin e dhënë. Kjo nyje zhvendoset (lëvizet) djathtas (për të krijuar vendin për nyjen e re). sidoqoftë, nuk caktoeht në ‘previous’, caktohet në poiztën e indeksit dhe nyja në atë pozitë lëvizet djathtas. Kjo bëhet duke përdoru unazën ‘for’. Për secilin iteracion, nyja e ndarë anëtarit ‘next’ të nyjes ‘temp’ i ndahet nyjes ‘temp’. Kjo tingëllon çuditshëm, por do të jetë e qartë kur të shiqohet se çka po ndodhë. Le të themi se janë pesë nyje në listën e lidhur, si është paraqitur në figurën 4.16. Nyja e fillimt (front) është NodeA dhe vlera filestare e ‘temp’ është NodeA. Le të themi se dëshironi të insertoni nyjen e re NodeN në pozitën me indeks 2. 254

Algoritmet dhe strukturat e të dhënave Para iteracionit të parë, front=NodeE. Gjatë iteracionit të parë, ja se çka ndodhë: temp = temp->next temp = NodeA->NodeB temp = NodeB

Pas iteracionit të parë, temp-it i është ndarë NodeB dhe vlera e ‘i’-së është 1, që është më pak sesa vlera e indeksit (2), kështu që ekzekutohet edhe një iteracion. Ja se çka ndodhë: temp = temp->next temp = NodeB->NodeC temp = NodeC

Tani pointeri ‘temp’ pointon në NodeC dhe vlera e ‘i’-së është 2, që është e barabartë me vlerën e indeksit, kështu që nuk ka më iteracione plotësues dhe pointeri ‘temp’ pointon në NodeC. Tani që jemi në lokacionin e dëshiruar në listën e lidhur, është koha që të shkëmbehen pointerët përreth, për të insertuar nyjen e re në listë. Ja si bëhet kjo: new_node->next = temp; new_node->previous = temp->previous; temp->previous->next = new_node; temp->previous = new_node;

Për sqarim më të lehtë, le të ndërrojmë nyjet për pointerët: new_node->next = NodeC; new_node->previous = NodeC->NodeB; NodeC->NodeB->next = new_node; NodeC->previous = new_node;

Figura 4.18 paraqet listën e lidhur pas insertimit të nyjes së re NodeN, në pozitën me indeksin 2 në listë.

Figura 4.18: Nyja e re e quajtur NodeN është vendosur në pozitën me indeks 2 në listën e lidhur.

255

Avni Rexhepi Hapi i fundit është që të inkrementohet anëtari ‘size’ (madhësia) e klasës LinkedList, për të reflektuar nyjën e re. Definicioni i plotë i funksionit insertNodeAt(), është: void insertNodeAt(int index, int data) { if(index < 0 || index > size) { return; } Node* new_node = new Node(); new_node->data=data; if(size == 0) { front = new_node; back = new_node; } else if(index == 0) { front->previous = new_node; new_node->next = front; front = new_node; } else if(index == size) { back->next = new_node; new_node->previous = back; back = new_node; } else { Node* temp = front; for(int i=0; inext; } new_node->next = temp; new_node->previous = temp->previous; temp->previous->next = new_node; temp->previous = new_node; } size++; }

256

Algoritmet dhe strukturat e të dhënave

Funksioni peek() Funksioni peek() ‘nxjerrë’ (lexon, tërheqë) vlerën e ruajtur në nyjen e specifikuar përmes indeksit që i përcillet funksionit peek(). Funksioni peek() e kërkon një argument, i cili është indeksi i pozitës në listën e lidhur i cili përmbanë vlerën (të dhënën) që duhet nxjerrur. Në shembullin vijues do të ruhet dhe pastaj do të “lexohet” një vlerë e tipi integer, por mund të ruhet dhe “lexohet” çfarëdo tipi i të dhënave, vetëm duke e ndryshuar tipin e të dhënave në definicionin e nyjës. Le të shohim si punon funksioni peek(). Ai fillon me vlerësimin e indeksit duke përdorur procedurën e njëjtë të vlerësimit sikur ajo e diskutuar në funksionin removeNodeAt(), me ndryshimin që funksioni peek() i verifikon vlerat brenda rangut. Nëse indeksi është jovalid, atëherë funksioni kthen zero. Nëse indeksi është valid, atëherë në fillim deklarohet pointeri i emërtuar ‘temp’ dhe i ndahet nyja e cila ndodhet në fillim (front) të listës së lidhur. Funksioni peek() pastaj vazhdon të lëvizë nëpër listën e lidhur deri sa të arrinë tek nyja në të cilën jemi të interesuar. Ky proces është i njëjtë me atë në funksionin insertNodeAt(). Kur funksioni peek() del prej unazës ‘for’, pointeri ‘temp’ pointon në nyjen e cila ka vlerën (të dhënën) që e kthen funksioni peek(). Pastaj pointoni në vlerën e nyjes në urdhërin return, për të kthyer vlerën (të dhënën) tek urdhëri i cili e ka thirrur funksionin peek(). Definicioni i plotë i funksionit peek() është si vijon: int peek(int index) { if(index < 0 || index > size-1) { return 0; } Node* temp = front; for(int i=0; inext; } return temp->data; }

257

Avni Rexhepi

Funksioni getSize() Funksioni getSize() e merr vlerën e anëtarit ‘size’ të klasës LinkedList. Do të vëreni se ky funksion ka vetëm një urdhër, i cili thjeshtë e kthen vlerën e anëtarit ‘size’. Shtrohet pyetja, atëherë përse duhet funksioni getSize(), pasi që do të kishte mundësi që anëtari ‘size’ të ketë qasje publike, duke u vendosur në pjesën publike ët klasës dhe do të lexohej direkt nga aplikacioni! Kjo për arsye se anëtarët e klasës duhet të kenë qasje vetëm nga funksionet anëtare përbrenda klasës ose nga klasët e derivuara (trashëguese). Në këtë mënyrë, ju gjithmonë kontrolloni qasjen në të dhëna dhe i mbroni ato edhe nga ndryshimet e paqëllimshme. Lejimi i ndryshimit nga jashtë, nga shfrytëzuesit e klasës, do të mund të dërgonte në gabime. int getSize() { return size; }

Klasa e zgjeruar LinkedList Pasi u pa se si funksionojnë zgjerimet individuale të klasës LinkedList, ta shohim në vazhdim aplikacionin në tërësi. Aplikacioni është ndarë në tre fajlla: demo.cpp, LinkedList.h dhe LinkedList.cpp. të tre fajllat janë paraqitur në kodin në vijim. Fajllat LinkedList.h dhe LinkedList.cpp mund të përdoren me fajlla specifik për stek dhe queue, që janë parë më herët. Fajlli demo.cpp përmbanë aplikacionin në C++ i cili përdorë klasën e zgjeruar (plotësuar) LinkedList, për të krijuar dhe manipuluar listën e lidhur. Fajlli LinkedList.h përmbanë definicionet e nyjes dhe klasës LinkedList. Fajlli LinkedList.cpp përmbanë definicionet e funksioneve anëtare të klasës LinkedList. Në fajllin demo.cpp zhvillohet “aksioni”. Aplikacioni fillon me deklarimin e instancës së klasës LinkedList dhe pastaj ndarjen e instancës pointerit të quajtur ‘list’. Pastaj, thirret pesë herë funksioni appendNode(), i cili është funksion origjinal i klasës LinkedList dhe shton nyje të re në listën e lidhur. Lista e lidhur e krijuar pas thirrjes së fundit të funksionit appendNode() duket si ajo në krye të figurës 4.19

258

Algoritmet dhe strukturat e të dhënave

Figura 4.19 - Në krye ndodhet lista e lidhur para se të largohet nyja. Në mes ëshët lista pas thirrjes së funksionit removeNodeAt(3). Në fund, lista pas thirrjes së funksionit deleteNode(20). Kur të jetë krijuar lista e lidhur, aplikacioni thërret funksionin removeNodeAt(3) për të larguar nyjen e lokalizuar në indeksin 3. Pastaj, aplikacioni e thërret funksionin findNode(20), për të lokalizuar indeksin e nyjës që përmbanë vlerën 20 (e dhëna e saj, është 20). Bazuar në listën e paraqitur në figurën 4.19, funksioni findNode(20) kthen vlerën e indeksit 1. Pastaj thirret funksioni deleteNode(20) thirret dhe largon nga lista e lidhur nyjën e cila ka vlerën 20, si element i të dhënës së saj. Lista e lidhur e paraqitur në fund të figurës 4.19 ilustron gjendjen pas thirrjes së funksionit deleteNode(20). Pastaj, në listën e lidhur insertohet një nyje e re, duke thirrut funksionin insertNodeAt(1,35). Ky funksion inserton një nyje të re në indeksin 1 në listën e lidhur dhe ia ndanë vlerën 35. Figura 4.20 paraqet listën e lidhur pas thirrjes së funksionit insertNodeAt(1,35). Në vazhdim, thirret funksioni peek(3) për të nxjerrë (lexuar, shikuar) vlewrn e nyjës në pozitën me indeks 3, në listën e lidhur. Bazuar në listën e paraqitur në figurën 4.20, funksioni peek(3) do të kthejë ‘50’ si vlerë (e dhënë) e nyjës në pozitën me indeks 3. 259

Avni Rexhepi

Figura 4.20 - Lista e lidhur pas thirrjes së insertNodeAt(1). Funksioni i fundit që thirret është funksioni getSize(), i cili kthen madhësinë e listës së lidhur. Si shihet në fig. 4.20, lista e lidhur ka katër nyje, prandaj funksioni getSize() kthen vlerën 4. Urdhëri i fundit në aplikacionin ‘demo’ përdorë operatorin ‘delete’ për të larguar instancën e klasës LinkedList nga memoria. //demo.cpp #include using namespace std; //LinkedList.h // Node=Nyje; data=vlera; // previous=ePerparshme; next=eArdhshme struct Node { int data; Node* previous; Node* next; }; class LinkedList { protected: Node* front; Node* back; int size; void removeNode(Node* node); public: LinkedList(); virtual ~LinkedList(); void appendNode(int); void displayNodes(); void displayNodesReverse(); void destroyList(); void removeNodeAt(int); int findNode(int); void deleteNode(int); void insertNodeAt(int,int);

260

Algoritmet dhe strukturat e të dhënave int peek(int); int getSize(); }; //LinkedList.cpp //#include "LinkedList.h" (Nese i bartim ne header fajll te veçant) LinkedList::LinkedList() { front = NULL; back = NULL; size = 0; } LinkedList::~LinkedList() { destroyList(); } void LinkedList::appendNode(int data) { Node* n = new Node(); n->data=data; if(back == NULL) { back = n; front = n; } else { back->next = n; n->previous = back; back = n; } size++; } void LinkedList::displayNodes() { cout previous == NULL) { front = node->next; node->next->previous = NULL; } else if(node->next == NULL) { back = node->previous; node->previous->next = NULL; } else { node->previous->next = node->next; node->next->previous = node->previous;

262

Algoritmet dhe strukturat e të dhënave } delete node; size--; } void LinkedList::removeNodeAt(int index) { if(index < 0 || index > size-1) { return; } Node* temp_node = front; for(int i=0; inext; } removeNode(temp_node); } int LinkedList::findNode(int data) { int index = 0; Node* temp_node = front; while(temp_node != NULL) { if(temp_node->data == data) { // kthe indeksin e nyjes return index; } else { temp_node = temp_node->next; index++; } } return -1; } void LinkedList::deleteNode(int data) { Node* temp_node = front;

263

Avni Rexhepi while(temp_node != NULL) { if(temp_node->data == data) { removeNode(temp_node); return; } else { temp_node = temp_node->next; } } } void LinkedList::insertNodeAt(int index, int data) { if(index < 0 || index > size) { return; } Node* new_node = new Node(); new_node->data=data; if(size == 0) { front = new_node; back = new_node; } else if(index == 0) { front->previous = new_node; new_node->next = front; front = new_node; } else if(index == size) { back->next = new_node; new_node->previous = back; back = new_node; } else { Node* temp = front; for(int i=0; inext; }

264

Algoritmet dhe strukturat e të dhënave new_node->next = temp; new_node->previous = temp->previous; temp->previous->next = new_node; temp->previous = new_node; } size++; } int LinkedList::peek(int index) { if(index < 0 || index > size-1) { return 0; } Node* temp = front; for(int i=0; inext; } return temp->data; } int LinkedList::getSize() { return size; } //Programi kryesor int main() { LinkedList* list = new LinkedList(); list->appendNode(10); list->appendNode(20); list->appendNode(30); list->appendNode(40); list->appendNode(50); list->displayNodes(); list->removeNodeAt(3); list->displayNodes(); list->displayNodesReverse(); int index = list->findNode(20); list->deleteNode(20); list->insertNodeAt(1, 35); list->displayNodes();

265

Avni Rexhepi int data = list->peek(3); coutRight() != NULL ) addNode(key, leaf->Right()); else { Node* n = new Node(); n->setKey(key); n->setParent(leaf); leaf->setRight(n); } } } // Gjeje nyjen [O(lartesia e pemes) mesatarisht] Node* Tree::findNode(int key, Node* node) { if ( node == NULL ) return NULL; else if ( node->Key() == key ) return node; else if ( key Key() ) findNode(key, node->Left()); else if ( key > node->Key() ) findNode(key, node->Right()); else return NULL; } // Shtype pemen void Tree::walk(Node* node) { if ( node ) { cout Key() Left()); walk(node->Right()); } }

681

Avni Rexhepi // Gjeje nyjen me çeles (vlere) minimale // Pershko nen-pemen e majte ne menyre rekurzive // deri sa nen-pema e majte te jete e zbrazet, per te marre vleren min Node* Tree::min(Node* node) { if ( node == NULL ) return NULL; if ( node->Left() ) min(node->Left()); else return node; } // // Gjeje nyjen me çeles (vlere) maksimale // Pershko nen-pemen e djathte ne menyre rekurzive // deri sa nen-pema e djathte te jete e zbrazet, per te marre vleren max Node* Tree::max(Node* node) { if ( node == NULL ) return NULL; if ( node->Right() ) max(node->Right()); else return node; } // Gjeje nyjen pasuese te nyjes // Gjeje nyjen, merre nyjen me vlere max // per nen-pemen e djathte, per ta marre nyjen pasuese Node* Tree::successor(int key, Node *node) { Node* thisKey = findNode(key, node); if ( thisKey ) return max(thisKey->Right()); } // Gjeje nyjen paraardhese te nyjes // Gjeje nyjen, merre nyjen me vlere max // per nen-pemen e majte, per ta marre nyjen paraardhese Node* Tree::predecessor(int key, Node *node) { Node* thisKey = findNode(key, node); if ( thisKey ) return max(thisKey->Left()); } // Fshije nyjen // (1) Nese eshte gjete, vetem fshije // (2) Nese ka vetem nje femije, fshije nyjen dhe zevendesoje me femijen

682

Algoritmet dhe strukturat e të dhënave // (3) Nese ka 2 femije. Gjeje paraardhesen(ose pasardhesen). // Fshije paraardhesen(ose pasardhesen). Zevendesoje nyjen // qe duhet te fshihet me paraardhesen (ose pasardhesen). void Tree::deleteNode(int key) { // Gjeje nyjen. Node* thisKey = findNode(key, root); // (1) if ( thisKey->Left() == NULL && thisKey->Right() == NULL ) { if ( thisKey->Key() > thisKey->Parent()->Key() ) thisKey->Parent()->setRight(NULL); else thisKey->Parent()->setLeft(NULL); delete thisKey; } // (2) if ( thisKey->Left() == NULL && thisKey->Right() != NULL ) { if ( thisKey->Key() > thisKey->Parent()->Key() ) thisKey->Parent()->setRight(thisKey->Right()); else thisKey->Parent()->setLeft(thisKey->Right()); delete thisKey; } if ( thisKey->Left() != NULL && thisKey->Right() == NULL ) { if ( thisKey->Key() > thisKey->Parent()->Key() ) thisKey->Parent()->setRight(thisKey->Left()); else thisKey->Parent()->setLeft(thisKey->Left()); delete thisKey; } // (3) if ( thisKey->Left() != NULL && thisKey->Right() != NULL ) { Node* sub = predecessor(thisKey->Key(), thisKey); if ( sub == NULL ) sub = successor(thisKey->Key(), thisKey); if ( sub->Parent()->Key() Key() ) sub->Parent()->setRight(sub->Right()); else sub->Parent()->setLeft(sub->Left()); thisKey->setKey(sub->Key()); delete sub; }

683

Avni Rexhepi } // Programi kryesor int main() { Tree* tree = new Tree(); //Shto nyjet tree->addNode(300); tree->addNode(100); tree->addNode(200); tree->addNode(400); tree->addNode(500); // Pershko pemen coutRoot()); cout findNode(500, tree->Root()) ) cout
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF