Notat Exhaustive mbi Algoritmet, Strukturat e Të Dhënave dhe Analizën e Performancës
Hyrje në Algoritme dhe Konceptet Bazë
Përkufizimi i Algoritmit
- Algoritmet janë hapat e rregullt, të qartë dhe të mirëpërcaktuar që përdoren për të zgjidhur një problem të caktuar ose për të përpunuar të dhëna.
- Përbëjnë bazën e çdo programi kompjuterik dhe mundësojnë ndarjen e një problemi kompleks në pjesë më të thjeshta, të zbatueshme dhe të verifikueshme.
Rëndësia e Studimit të Algoritmeve
- Përdoren në fusha të shumta të teknologjisë dhe shkencës:
- Internet: Motorë kërkimi, rrjete sociale.
- Biologji: Analiza e sekuencave gjenetike dhe modelimi.
- Kompjuterikë: Kompajlerë, sisteme skedari (file systems).
- Grafikë Kompjuterike: Lojëra, filma, realitet virtual.
- Siguri: Kriptografi, pagesa online.
- Zhvillojnë aftësitë intelektuale dhe logjike duke stimuluar një mënyrë të menduari të saktë, strukturueshme dhe të organizuar.
- Përdoren në fusha të shumta të teknologjisë dhe shkencës:
Kriteret e Vlerësimit të Algoritmeve
- Korrektësia (Correctness):
- Një algoritëm është i saktë nëse jep gjithmonë rezultatin e duhur për çdo input të mundshëm dhe të vlefshëm.
- Provohet përmes rasteve të testimit (test cases), provave matematikore ose argumenteve logjike për çdo hap.
- Efikasiteti (Efficiency):
- Efikasiteti Kohor (Time Complexity): Tregon kohën e nevojshme për përfundimin e ekzekutimit në varësi të madhësisë së inputit . Shprehet me notacionin Big , si për shembull , , , .
- Efikasiteti Hapësinor (Space Complexity): Tregon sasinë e memories RAM që kërkohet për përpunimin e inputit dhe ruajtjen e të dhënave të ndërmjetme (variablat, matricat, listat, stekat).
- Korrektësia (Correctness):
Reprezentimi Vizual dhe Tekstual i Algoritmeve
- Flowchart: Mënyrë vizuale grafike për të treguar hapat e algoritmit përmes simboleve standarde:
- Ovali / Formë e rrumbullakosur: Përdoret për fillimin dhe fundin e një algoritmi.
- Paralelogrami: Përdoret për leximin/futjen e variablave dhe shtypjen e vlerave të nxjerra.
- Drejtkëndëshi: Përdoret për llogaritje të vlerave dhe veprime procesuese.
- Rombi: Përdoret për vendimmarrje dhe pyetje Po/Jo që përcaktojnë degëzimin e veprimeve të mëtejme.
- Shigjetat: Tregojnë drejtimin dhe rrjedhën e ekzekutimit.
- Pseudokodi: Version tekstual i afërt me kodin real, por pa sintaksë strikte të ndonjë gjuhe programimi; lehtëson analizimin e logjikës.
- Kodi Real: Implementimi konkret në një gjuhë programimi (Java, Python, C++), i përdorur për testim me të dhëna reale dhe matje të kohës së ekzekutimit.
- Flowchart: Mënyrë vizuale grafike për të treguar hapat e algoritmit përmes simboleve standarde:
Strukturat e Të Dhënave: Array, Stack dhe Queue
Roli i Strukturave të Të Dhënave
- Programet ekzekutohen në CPU, por të dhënat e nevojshme tërheqen nga memoria RAM.
- Strukturat e të dhënave përcaktojnë mënyrën se si ruhen, qasen dhe modifikohen informacionet në RAM.
- Kërkimi (searching) dhe renditja (sorting) janë dy operacione thelbësore: fillimisht bëhet sortimi i të dhënave në një rend logjik që kërkimi i mëvonshëm të jetë maksimalisht efikas.
Tipet Abstrakte të Të Dhënave (ADT - Abstract Data Types)
- Përfaqësojnë koncepte logjike se si manipulohen të dhënat dhe përcaktohen përmes API-ve (Application Programming Interface).
- Nuk ekzistojnë si struktura fizike fikse në RAM, por implementohen përmes strukturave konkrete.
1. Array (Vargu)
- Koleksion elementesh të llojit të njëjtë të të dhënave me madhësi statike (përcaktohet gjatë krijimit dhe nuk ndryshon).
- Qasja në elemente bëhet drejtpërdrejt përmes indeksit (numër i plotë integer), duke e bërë operacionin primitive dhe jashtëzakonisht të shpejtë.
- Iterimi mbi elemente realizohet përmes cikleve (loops).
2. Stack (Masa / Stiva - LIFO)
- Funksionon sipas parimit LIFO (Last In First Out) — elementi i fundit që hyn është i pari që del.
- Operacionet Bazë:
- : Shton një element në krye të stack-ut.
- : Heq dhe kthen elementin më të fundit nga maja.
- : Kontrollon nëse stack-u është bosh për të shmangur gabimet.
- : Kthen numrin e elementeve ekzistuese.
- Mënyrat e Implementimit të Stack:
- Përmes Linked List:
- Çdo element (node) ruan të dhënat dhe një referencë / pointer (variabla e rezervuar ) drejt elementit pasardhës.
- Ka madhësi dinamike; lejon shtimin dhe heqjen e elementeve pa kufizim paraprak.
- Nëse referenca e një node bëhet null dhe shkëputet pa u fshirë saktë, shkaktohet loitering (kujtesa mbetet e zënë në RAM por e paarritshme).
- Llojet e Linked List: Singly Linked List (një referencë ), Doubly Linked List (dy referenca: dhe ), Circular Linked List (node-i i fundit lidhet me të parin).
- Maja e stack-ut () përfaqëson pikën ku kryhen dhe .
- Përmes Array me Kapacitet të Fiksuar (Resizing Array):
- Përdor një array për të ruajtur objekte.
- vendos elementin e ri në .
- kthen elementin nga .
- Overflow: Ndodh kur tentohet të shtohet element në një stack të mbushur. Zgjidhet duke dyfishuar madhësinë e array-t () automatikisht.
- Underflow: Ndodh kur tentohet të hiqet element nga një stack bosh. Për të optimizuar hapësirën, kur stiva mbetet vetëm () e mbushur, madhësia përgjysmohet ().
- Përmes Linked List:
- Përdorimet Reale të Stack:
- Mekanizmi Undo/Redo në dokumente.
- Butoni "Back" në shfletuesit e internetit (web browsers).
- Menaxhimi i thirrjeve të funksioneve në kompajlerë (Call Stack).
- Llogaritjet e shprehjeve aritmetike (p.sh., algoritmi i Dijkstra-s me dy stiva).
3. Queue (Rreshti - FIFO)
- Funksionon sipas parimit FIFO (First In First Out) — elementi i parë që hyn është i pari që del.
- Operacionet Kryesore:
- : Shton një element në fund të rreshtit.
- : Heq elementin nga fillimi i rreshtit.
- : Verifikon nëse rreshti është bosh.
- : Kthen numrin e elementeve.
- Mënyrat e Implementimit të Queue:
- Përmes Linked List: Përdor dy pointera — për fillimin dhe për fundin. shton te , ndërsa heq nga . Të dyja operacionet ekzekutohen në kohë konstante .
- Përmes Array Resizable: Përdor dy indekse, dhe . Dyfishohet kur mbushet plotësisht dhe përgjysmohet kur bie në të kapacitetit.
Implementimi në Java dhe Generics
- Përdoren Generics (p.sh., ) për të garantuar sigurinë e tipeve në kohë përkthimi dhe për të shmangur gabimet në kohë ekzekutimi (run-time errors).
- Implementimet përdorin ndërfaqet dhe me metodat dhe , duke mundësuar iterimin e thjeshtë me cikle .
Krahasimi i Performancës dhe Memories
- Implementimi i Stack/Queue me Linked List konsumon rreth për çdo node (pa përfshirë objektin që referohet).
- Operacionet , , , dhe kanë kohë konstante në rastin më të keq.
Analiza e Performancës së Algoritmeve
Arsyet e Analizës së Algoritmeve
- Krahasimi i zgjidhjeve të ndryshme për të përzgjedhur më efikasen.
- Shmangia e gabimeve të performancës ("performance bugs") ku kodi është korekt matematikisht, por shumë i ngadaltë për përdorim praktik.
- Parashikimi i sjelljes së programit me input-e të mëdha (Sipas Donald Knuth).
Metoda Shkencore në Analizë
- Vëzhgimi (Observation): Identifikimi dhe matja e sjelljes së programit.
- Hipoteza (Hypothesis): Ndërtimi i një modeli matematik që shpjegon vëzhgimet.
- Parashikimi (Prediction): Përdorimi i modelit për të parashikuar kohën për input-e të reja.
- Verifikimi (Verification): Kryerja e eksperimenteve të reja për testim.
- Validimi (Validation): Përsëritja e procesit deri në përputhshmëri të plotë.
- Eksperimentet duhet të jenë të riprodhueshme dhe hipotezat të mund të rrëzohen (falsifiability).
Modeli Matematikor i Kohës së Ekzekutimit (Running Time)
- Koha totale = .
- Çmimi i operacionit varet nga hardware-i dhe kompajlleri; frekuenca varet nga algoritmi dhe madhësia e inputit .
Kostoja e Operacioneve Themelore (Matjet në MacBook Pro 2.2GHz)
- Deklarim variable (
int a): - Udhëzim caktimi (
a = b): - Krahasim numrash të plotë (
a < b): - Qasje në element vargu (
a[i]): - Gjatësia e vargut (
a.length): - Alokim i vargut 1D (
new int[N]): - Alokim i vargut 2D (
new int[N][N]): - Mbledhje numrash të plotë (
a + b): - Shumëzim numrash të plotë (
a * b): - Pjesëtim numrash të plotë (
a / b): - Mbledhje me pikë të lëvizshme (
a + b): - Shumëzim me pikë të lëvizshme (
a * b): - Pjesëtim me pikë të lëvizshme (
a / b): - Llogaritja e Sines (
Math.sin(theta)): - Llogaritja e Arktangjentit (
Math.atan2(y,x)):
- Deklarim variable (
Shembuj të Llogaritjes së Kompleksitetit
- 1-SUM: Kontrollon dhe numëron elementet zero në varg; rritet në mënyrë lineare .
- 2-SUM: Kontrollon çifte elementesh me dy cikle të mbivendosura; kompleksitet kuadratik .
- 3-SUM: Kontrollon treshe elementesh me tre cikle të mbivendosura; kompleksitet kubik .
Shkalla e Rritjes (Order of Growth)
- — Konstant (p.sh., mbledhja e dy numrave:
a = b + c;) - — Logaritmik (p.sh., kërkimi binar / binary search:
while(N>1) {N=N/2;}) - — Linear (p.sh., gjetja e maksimumit me një cikël:
for(int i=0; i<N; i++)) - — Linearitmik (p.sh., Mergesort me teknikën divide and conquer)
- — Kuadratik (p.sh., kontrolli i çifteve me cikël të dyfishtë)
- — Kubik (p.sh., kontrolli i tresheve me cikël të trefishtë)
- — Eksponencial (p.sh., kërkim kombinatorik / kontroll i të gjitha nënbashkësive)
- — Konstant (p.sh., mbledhja e dy numrave:
Notacionet Asimptotike
- Big O (): Përfaqëson kufirin e sipërm (rastin më të keq / worst case).
- Big Omega (): Përfaqëson kufirin e poshtëm (rastin më të mirë / best case).
- Big Theta (): Përfaqëson kufirin e saktë asimptotik (rastin mesatar / average case).
Binary Search (Kërkimi Binar)
- Përdoret vetëm në vargje të renditura.
- Krahason vlerën e kërkuar me elementin e mesit: nëse është më e vogël kërkon në gjysmën e majtë, nëse është më e madhe kërkon në të djathtën.
- Kompleksiteti kohor është , dukshëm më i shpejtë se kërkimi linear .
Përdorimi i Memories në Java
- Tipet Primitive:
byte,boolean:char:int:double:
- Objektet:
- Overhead-i bazë i çdo objekti: .
- Çdo referencë objekti (pointer): .
- Padding: Rrumbullakos madhësinë e objektit në shumëfishin më të afërt të .
- Shembull: Një objekt me tre variabla
intpërdor ().
- Vargjet (Arrays):
- Kërkojnë overhead shtesë për strukturën e tyre.
- Një varg 2D
int[N][N]përdor afërsisht . - Formula e memories totale për disa struktura: afërsisht .
- Tipet Primitive:
Algoritmet Elementare të Sortimit
Koncepti i Sortimit
- Process i rirregullimit të një vargu me elemente sipas një rendi të caktuar (zakonisht nga më i vogli tek më i madhi).
- Ndërfaqja Comparable në Java:
- Implementohet metoda .
- Kthen nëse objektet janë të barabarta.
- Kthen nëse objekti aktual është më i vogël se .
- Kthen nëse është më i madh se .
- Metoda kthen vetëm
trueosefalse.
1. Selection Sort (Sortimi me Përzgjedhje)
- Skanon vargun nga e majta në të djathtë.
- Në çdo hap , kërkon elementin minimal në pjesën e pabarazuar (nga deri në ) dhe e shkëmben (swap) me elementin në pozicionin
- Karakteristikat:
- Kërkon gjithmonë krahasime, pavarësisht nëse vargu është i sortuar apo jo.
- Bën vetëm shkëmbime (swaps /
exch()). - Kompleksiteti kohor është gjithmonë .
- Nuk është algoritëm i qëndrueshëm (unstable) dhe nuk është efikas për lista të mëdha.
2. Insertion Sort (Sortimi me Futje)
- Merr çdo element me radhë dhe e fut në pozicionin e duhur në pjesën e majtë që është e sortuar, duke e lëvizur majtas përmes shkëmbimeve të njëpasnjëshme.
- Karakteristikat:
- Rasti më i mirë (varg pothuajse i sortuar): krahasime dhe shkëmbime ().
- Rasti më i keq (varg në rend zbritës): krahasime dhe shkëmbime.
- Rasti mesatar: krahasime dhe shkëmbime.
- Më praktik se Selection Sort; algoritëm i qëndrueshëm (stable).
3. Shell Sort (h-Sort)
- Version i përmirësuar i Insertion Sort që lejon shkëmbimin e elementeve me distancë të madhe ().
- Zgjidhet një sekuencë zvogëluese e (p.sh., përmes formulës : ).
- Kryhet për çdo , dhe në fund bëhet një Insertion Sort final me
- Kompleksiteti kohor varet nga sekuenca e : rasti më i mirë , ndërsa rasti më i keq është nënkuadratik .
Algoritmet e Avancuara të Sortimit: Merge Sort dhe Quick Sort
Merge Sort (Sortimi me Bashkim)
- Bazohet në parimin Divide and Conquer (Pjesëto dhe Stërvit):
- Ndaj vargun në dy gjysma.
- Sorto secilën gjysmë në mënyrë rekursive.
- Bashko (merge) dy gjysmat e sortuara në një varg të vetëm të renditur.
- Rol i Indekseve gjatë Merge:
- : Indeksi i fillimit të nënvargut të majtë.
- : Pika e mesit (fundi i nënvargut të majtë: ).
- : Indeksi i fundit i nënvargut të djathtë ().
- Kompleksiteti Kohor dhe Hapësinor:
- Rasti më i keq:
- Rasti më i mirë:
- Rasti mesatar:
- Hapësira shtesë: (kërkon varg ndihmës, prandaj nuk është in-place).
- Optimizimet Praktike në Java:
- Testimi nëse vargu është i sortuar:
if (!less(a[mid+1], a[mid])) return;pëlqen ndërprerjen e merge. - Cutoff te Insertion Sort për nënvargje të vogla (zakonisht me gjatësi elemente).
- Testimi nëse vargu është i sortuar:
- Varianted e Merge Sort:
- Bottom-Up Mergesort: Version jo-rekursiv. Kryen bashkime të nënvargjeve me madhësi Punon më ngadalë se Top-Down, por është i thjeshtë.
- Natyral Mergesort: Identifikon sekuencat natyrale të sortuara ("runs") dhe i bashkon ato. Ka kompleksitet linear për vargje pjesërisht të sortuara.
- Timsort: Kombinim i Natyral Mergesort, Binary Insertion Sort dhe optimizimeve të tjera. Përdoret si algoritëm standard në Python, Java 7+ (
Arrays.sort()), dhe Android.
- Bazohet në parimin Divide and Conquer (Pjesëto dhe Stërvit):
Quick Sort (Sortimi i Shpejtë)
- Considerohet si një nga 10 algoritmet më të rëndësishëm të shekullit XX.
- Mënyra e Funksionimit:
- Kryen përzierjen e rastësishme (shuffle) të vargut për të shmangur rastin më të keq.
- Zgjedh një element si pivot (p.sh., ).
- Particionimi: Vendos të gjitha elementet më të vogla se pivoti në të majtë dhe elementet më të mëdha në të djathtë përmes dy pointerave dhe .
- Sorton dy pjesët në mënyrë rekursive.
- Logjika e Pointerave dhe :
- Inkrementohet përsa kohë .
- Dekrementohet përsa kohë .
- Kur kushtet ndalojnë, shkëmbehen me . Procesi përfundon kur dhe kryqëzohen, dhe pivoti shkëmbehet me .
- Karakteristikat e Quick Sort:
- Është algoritëm In-Place (nuk kërkon memorie shtesë për vargje ndihmëse).
- Nuk është i qëndrueshëm (unstable).
- Rasti më i mirë: (pivoti ndan vargun përgjysmë).
- Rasti mesatar: .
- Rasti më i keq: (ndodh kur vargu është i sortuar dhe nuk është bërë shuffle).
- Problemi i Çelësave të Dyfishtë dhe Particionimi 3-Kalimësh (Dijkstra 3-Way Partitioning):
- Kur vargu përmban shumë elemente të përsëritura, Quick Sort klasik degradon.
- Zgjidhja sipas Dijkstra-s ndan vargun në 3 pjesë përmes pointerave , , dhe :
- Elementet më të vogla se pivoti ().
- Elementet e barabarta me pivotin ().
- Elementet më të mëdha se pivoti ().
- Rregullat e Lëvizjes:
- Nëse : shkëmbe me ; rrit dhe .
- Nëse : shkëmbe me ; zvogëlo .
- Nëse : rrit .
Stabiliteti i Algoritmeve të Sortimit
- Aftësia e algoritmit për të ruajtur renditjen relative fillestare të elementeve me çelësa të barabartë.
- Algoritme Stable: Insertion Sort, Merge Sort, Timsort, Bubble Sort, Counting/Radix Sort.
- Algoritme Unstable: Selection Sort, Shell Sort, Quick Sort, Heapsort.
Tabela Përmbledhëse e Algoritmeve të Sortimit
| Algoritmi | In-place? | Stable? | Rasti më i mirë | Rasti Mesatar | Rasti më i keq | Shënime / Vërejtje | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | Selection | Po | Jo | | | | Kryen vetëm shkëmbime (exchanges). | | Insertion | Po | Po | | | | Efikas për të vogël ose vargje pjesërisht të sortuara. | | Shell | Po | Jo | | Ndryshon | | Kod kompakt; kompleksitet nënkuadratik. | | Merge | Jo | Po | | | | Garanci ; kërkon hapësirë shtesë. | | Timsort | Jo | Po | | | | Përmirëson Merge Sort kur ekziston renditje paraprake. | | Quick | Po | Jo | | | | Garanci probabilistike; më i shpejti në praktikë. | | 3-way Quick | Po | Jo | | | | Përmirëson Quick Sort kur ka shumë çelësa të dyfishtë. |
Radhët me Prioritet (Priority Queues) dhe Heap-i Binar
Koncepti i Priority Queue (PQ)
- Strukturë abstrakte të dhënash ku çdo element ka një prioritet të caktuar.
- Mbështet dy operacione kryesore: shtimi i elementit () dhe heqja e elementit me prioritetin maksimal ().
- Elementet duhet të implementojnë ndërfaqen .
- Përdorimet: Algoritmet e grafikëve (Dijkstra, Prim), simulime me ngjarje, balancim ngarkese, procesim statistikash, në IA, filttrim .
Krahasimi i Implementimeve të Priority Queue
- Array i Pasortuar: , , .
- Array i Sortuar: , , .
- Binary Heap: , , .
Struktura e Binary Heap (Heap-i Binar)
- Përfaqësohet konceptualisht si një pemë binare e plotë (Complete Binary Tree), por ruhet fizikisht në një array (ku indeksi fillon nga ).
- Rregulli i Plotësisë: Çdo nivel është plotësisht i mbushur, përveç mundësisht nivelit të fundit.
- Heap Order Property: Vlera e prindit është gjithmonë më e madhe ose e barabartë me vlerën e fëmijëve të tij. Rrënja përmban gjithmonë elementin maksimal.
- Lartësia e një complete binary tree me nyje është .
Lidhjet Matematikore të Indekseve në Array
- Për nyjen në pozicionin :
- Indeksi i prindit është
- Indeksi i fëmijës të majtë është
- Indeksi i fëmijës të djathtë është
- Për nyjen në pozicionin :
Operacionet Themelore në Heap
- Swim (Noton lart):
- Përdoret gjatë insertimit. Elementi i ri vendoset në fund të vargut () dhe krahasohet me prindin ().
- Nëse është më i madh se prindi, shkëmbehen vendet. Procesi vazhdon lart derisa të vendoset rregulli i heap-it.
- Kostoja maksimale: krahasime.
- Sink (Zhytet poshtë):
- Përdoret gjatë . Rrënja shkëmbehet me elementin e fundit , dhe elementi i fundit fshihet.
- Elementi i ri në rrënjë zhytet poshtë duke u shkëmbyer me fëmijën më të madh nga të dy fëmijët ( ose ).
- Kostoja maksimale: krahasime.
- Swim (Noton lart):
Immutability (Të Qenit i Pandryshueshëm) në Java
- Tipat immutable nuk mund të modifikohen pas krijimit (p.sh., , , ).
- Tipat mutable mund të modifikohen (p.sh., vargjet, ).
- Në PQ, përdorimi i objekteve immutable garanton se çelësat nuk ndryshojnë vlerën gjatë qëndrimit në heap, duke parandaluar prishjen e renditjes.
Heapsort
- Algoritëm sortimi in-place që përdor strukturën e Heap-it me dy faza:
- Ndërtimi i Heap-it (Heap Construction): Shndërron vargun e paorjentuar në një max-heap duke përdorur teknikën bottom-up (thirrja e për nyjet jo-gjethe nga teposhtë). Kostoja: .
- Faza e Sortimit (Sortdown): Shkëmben vazhdimisht rrënjën (maksimumin) me elementin e fundit të vargut, zvogëlon madhësinë e heap-it dhe aplikon në rrënjë. Kostoja: .
- Algoritëm sortimi in-place që përdor strukturën e Heap-it me dy faza:
Tabelat Simbolike (Symbol Tables)
Koncepti i Symbol Table (ST)
- Strukturë abstrakte të dhënash e përbërë nga çifte çelës-vlerë (Key-Value pairs).
- Gjeneralizon vargjet klasike ku çelësat mund të jenë çfarëdo lloj objekti (p.sh., , ).
- Shembull real: DNS (Domain Name System) ku çelësi është emri i domain-it dhe vlera është IP adresa.
API e Symbol Table
- : Fut çiftin e ri ose mbishkruan vlerën nëse çelësi ekziston.
- : Kthen vlerën për çelësin e dhënë, ose
nullnëse mungon. - : Fshin çelësin dhe vlerën (Lazy delete vendos ).
- : Kthen
truenëse - , ,
Konventat dhe Rregullat mbi Çelësat
- Vlerat nuk mund të jenë
null. - Çelësat duhet të jenë Immutable.
- Metoda në Java duhet të plotësojë 4 vetitë:
- Refleksive:
x.equals(x) == true - Simetrike:
x.equals(y) == y.equals(x) - Transitive: Nëse
x.equals(y)dhey.equals(z), atëherëx.equals(z) - Jo-null:
x.equals(null) == false
- Refleksive:
- Vlerat nuk mund të jenë
Implementimet Standarde të Symbol Table
- Linked List e Pasortuar (Sequential Search):
- Kërkimi () kërkon skanimin e të gjithë listës: .
- Futja () kërkon skanim për të verifikuar nëse ekziston: .
- Array i Sortuar (Binary Search):
- Kërkimi () përdor kërkim binar: .
- Futja () dhe fshirja () kërkojnë zhvendosjen e elementeve: .
- Linked List e Pasortuar (Sequential Search):
API për Symbol Table të Sortuar
- Ofron metoda shtesë: , , (çelësi më i madh ), (çelësi më i vogël ), (numri i çelësave shtesë $< k$), , , .
Pemët Binare të Kërkimit (Binary Search Tree - BST)
Struktura e BST
- Një pemë binare ku çdo nyje përmban një çelës, një vlerë, dy referenca (subtree dhe ), dhe një variabël (madhësia e nënpemës).
- Rregulli i Renditjes Simetrike:
- Çdo çelës në nënpemën e majtë () është strictly më i vogël se çelësi i nyjes.
- Çdo çelës në nënpemën e djathtë () është strictly më i madh se çelësi i nyjes.
Operacionet në BST
- Search (get): Bën krahasim rekursiv me çelësin e nyjes aktuale. Nëse është më i vogël shkon majtas, nëse është më i madh shkon djathtas.
- Rasti mesatar: krahasime.
- Rasti më i keq: krahasime (kur druri shndërrohet në listë linearish të lidhur).
- Insert (put): Kërkon çelësin; nëse e gjen e përditëson, përndryshe shton nyjen e re në lidhjen
nullpërkatëse dhe përditëson variablën . - Min dhe Max: gjendet duke ecur gjithmonë majtas deri te null link; duke ecur gjithmonë djathtas.
- Floor dhe Ceiling:
- : Nëse , floor është . Nëse , floor është në nënpemën e majtë. Nëse , floor është në nënpemën e djathtë (nëse ekziston), përndryshe është vetë nyja.
- Search (get): Bën krahasim rekursiv me çelësin e nyjes aktuale. Nëse është më i vogël shkon majtas, nëse është më i madh shkon djathtas.
Traversimi i Pemës (Tree Traversal)
- Inorder (LVR): Traversim majtas nyja djathtas. Kthen çelësat të renditur në mënyrë rritëse.
- Preorder (VLR): Nyja majtas djathtas.
- Postorder (LRV): Majtas djathtas nyja.
Fshirja në BST (Delete Operations)
- Lazy Delete: Vendos vlerën në
null, por e lë çelësin si "tombstone". Përkeqëson performancën sepse nyjet e fshira nuk çlirohen kurrë. - DeleteMin: Shkon majtas deri te nyja pa fëmijë të majtë, e zëvendëson me fëmijën e djathtë dhe përditëson numëruesit.
- Hibbard Delete (Fshirja e çfarëdo nyjeje):
- Nëse nyja ska fëmijë ( fëmijë): Fshihet lidhja me të.
- Nëse nyja ka fëmijë: Zëvendësohet me fëmijën e saj.
- Nëse nyja ka fëmijë: Zëvendësohet me pasardhësin e saj të drejtpërdrejtë (minimi i nënpemës së djathtë). Nyja e fshirë çlirohet për .
- Lazy Delete: Vendos vlerën në
Pemët e Balancuara të Kërkimit: 2–3 Tree dhe Red-Black BST
1. Pemët 2–3 (2–3 Trees)
- Pemë kërkimi e balancuar në mënyrë perfekte.
- Llojet e nyjeve:
- 2-node: Përmban 1 çelës dhe ka 2 fëmijë.
- 3-node: Përmban 2 çelësa dhe ka 3 fëmijë.
- Balancimi Perfekt: Çdo rrugë nga rrënja () deri te një null link ka ekzaktësisht të njëjtën gjatësi.
- Lartësia e drurit është në të gjitha rastet.
- Insertimi: Nëse futet në një 2-node, shndërrohet në 3-node. Nëse futet në një 3-node, krijohet përkohësisht një 4-node e cila ndahet (split), duke ngritur çelësin e mesëm te prindi.
2. Red–Black BST (Pemët e Kuqe-zi)
- Implementim i logjikës së pemës 2–3 përmes një BST standarde me link-a të ngjyrosur (të kuq dhe të zi).
- Rregullat e Red-Black BST:
- Link-at e kuq anojnë gjithmonë majtas (Left-Leaning).
- Asnjë nyje nuk mund të ketë dy link-a të kuq të njëpasnjëshëm (në seri).
- Çdo rrugë nga root te null link përmban numër të njëjtë link-ash të zi (Perfect Black Balance).
- Performanca e Red–Black BST:
- Pasqyrim me pemët 2–3.
- Lartësia e drurit është në rastin më të keq, dhe në praktikë.
- Operacionet , , dhe ekzekutohen gjithmonë në kohë logaritmike garantuar .
Tabelat Hash (Hash Tables)
Konceptet Bazë të Hashing
- Elementet ruhen në një varg të indeksuar ku indeksi llogaritet përmes një funksioni hash mbi çelësin.
- Komponentët: Funksioni Hash, Testi i Barazisë (), Strategjia e Zgjidhjes së Kolizioneve.
Hash Code në Java
- Të gjitha klasat trashëgojnë metodën , e cila kthen një numër të plotë .
- Kërkesa kryesore: Nëse
x.equals(y), atëherë detyrimishtx.hashCode() == y.hashCode(). - Horner's Method për String Hashing:
- Uniform Hashing Assumption: Çdo çelës ka gjasa të barabarta të mapohet në cilindo indeks nga deri në
- Birthday Problem: Tregon se kolizionet ndodhin shumë shpejt edhe në tabela të mëdha.
Metodat e Zgjidhjes së Kolizioneve
- 1. Closed Addressing (Separate Chaining - Zinxhirimi i Ndarë):
- Çdo indeks i vargut kryesor përmban një listë të lidhur (linked list).
- Kur dy çelësa kanë të njëjtin hash, shtohen në zinxhirin e atij indeksi.
- Resizing: Dyfishohet kur raporti ; përgjysmohet kur
- 2. Open Addressing (Adresimi i Hapur):
- Të gjitha çiftet ruhen drejtpërdrejt në vargun kryesor ().
- Linear Probing: Nëse pozicioni është i zënë, provohet indeksi tjetër sekuencial (). Shkakton problem Clustering (grumbullimi i blloqeve të zëna).
- Quadratic Probing: Përdor formulën
- Double Hashing: Përdor dy funksione hash: . Eliminon clustering-un.
- Resizing në Open Addressing: Dyfishohet kur ; përgjysmohet kur
- 3. Variante të Avancuara:
- Two-Probe Hashing: Hashohet çelësi në dy pozicione të ndryshme dhe vendoset në listën më të shkurtër (reducion gjatësinë në ).
- Cuckoo Hashing: Përdor dy vende të mundshme; nëse janë të zëna, shpronëson (evicts) çelësin ekzistues. Garanton kërkim në rastin më të keq.
- 1. Closed Addressing (Separate Chaining - Zinxhirimi i Ndarë):
Sulmet me Kompleksitet Algoritmik (Algorithmic Complexity Attacks / DoS)
- Sulmuesit targetojnë funksionin hash duke dërguar çelësa që prodhojnë të njëjtin vlerë hash, duke e degraduar tabelën nga në .
- Zgjidhje: Përdorimi i funksioneve hash kriptografike të sigurta njëkahëshe (SHA-256, MD5) me veti të efektit ortek (Avalanche effect).
Krahasimi Përfundimtar: Hash Tables vs. Balanced Search Trees
| Karakteristika | Hash Tables | Balanced Search Trees (Red-Black) |
| :--- | :--- | :--- |
| Koha e Kërkimit / Futjes | mesatare | garantuar |
| Renditja e Çelësave | Nuk mbështetet | Mbështetet (Inorder traversal) |
| Varësia | Varet nga funksioni Hash | Struktura është e balancuar vetvetiu |
| Përdorimi i Memories | Më pak memorie (pa pointera shtesë) | Më shumë memorie (përmban pointera) |
| Implementimi në Java | HashMap, HashSet, IdentityHashMap | TreeMap, TreeSet |