• No se han encontrado resultados

MANUAL DEL DUEÑO Modelo SC-10030A

pointohet, në përgjithësi nevojitet për të alokuar hapësirë memorike shtesë dhe pastaj për të kopjuar pointerët e dereferencuar. Kjo kërkon rishkrimin e operatorit të ndarjes së vlerës me kopjim (angl. copy assignment operator). Detajet e implementimit të kësaj procedure jepen më vonë. Normalisht, duhet të sigurojmë edhe operatorin e krahasimit të thellë, për të implementuar testin e thellë (natyrisht, kur gjejmë se jemi duke përdorur së shumti operacionet e thella, mund të kthehemi në përdorimin e të dhënave indigjene).

1.6.1 Listat jo të vazhduara – listat e lidhura

Do të diksutojmë një teknikë të përdorur në strukturat e të dhënave. Më parë u tregua se duke përdorur vargjet dinamikisht të zgjerueshme, mund të lexojmë një numër arbitrar të të dhënave hyrëse (elementeve hyrëse). Kjo teknikë ka një problem serioz. Supozojmë se jemi duke lexuar rekorde 1000-bajtëshe dhe kemi 1,000,000 bajta të memories në dispozicion (të lirë). Gjithashtu, supozojmë se në një moment, vargu përmbanë 400 rekorde dhe është i mbushur në tërësi. Atëherë, për të dyfishuar atë, e krijojmë një varg të ri me 800 rekorde, i kopjojmë në të 400 rekordet ekzistuese dhe pastaj i fshijmë ato 400 rekordet (“të vjetrat”). Problemi është se në këtë hapin e ndërmjetëm (kalimtarë), kemi në përdorim të dy: vargun me 400 rekorde dhe vargun me 800 rekorde dhe kështu kemi në total 1200 rekorde, që i tejkalon kufijtë e memories. Në fakt, mund të mbesim pa memorie pas bashkëndarjes së përafërsisht një të tretës së memories në dispozicion.

Zgjidhja e këtij problemi, është që të lejohet që lista e rekordeve të ruhet në fomë jo të vazhduar (angl. non-contiguously – jo e afërt, jo e puthitur, jo e vazhduar, është fjala për dallim nga rasti kur të gjithë anëtarët e vargut, janë në lokacione të njëpasnjëshme, të vazhdueshme në memorie). Për secilin rekord, e mbajmë një strukturë e cila ruan rekordin (vlerën) dhe një pointer “next” (angl. next-tjetri, i ardhshmi), për në strukturën e ardhshme në listë. Shembulli

74

themelor është treguar në figurën 1.20. Struktura rezultuese është lista e lidhur klasike, e cila i ruan të dhënat me një kosto (çmim) të një pointeri për element. Definicioni i strukturës është:

//Node=Nyje; item=elementi; next=tjetri,i/e ardhshme; struct Node

{

Object item; // elementi Node *next;

}l;

Në çdo pikë, ne mund të shtypim listën, duke përdorur iteracionin (unazën)

for( Node *p = first; p != NULL; p = p->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

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

Last (Fundi, i/e fundit)

A1 A2 A3

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

75 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 ‘<’, por ndoshta edhe ‘!=’ 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.

76

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 >= 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 <algorithms>.

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:

77

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ë.

Documento similar