Documente Academic
Documente Profesional
Documente Cultură
CUPRINS
CAPITOLUL 1. Introducere
BIBLIOGRAFIE
1
Cândea I. Ovidiu-Radu Algoritmi de căutare
CAPITOLUL 1
INTRODUCERE
2
Cândea I. Ovidiu-Radu Algoritmi de căutare
Capitolul final reprezintă descrierea aplicaţiei ataşate acestei lucrări, aplicaţie care a
fost realizată în limbaj Java folosind KawaPro cu pachetul JDK 1.3.0.
Aplicaţia cuprinde mai multe ferestre, corespunzătoare unei anumite structuri de date,
accesul la aceste ferestre făcându-se dintr-o fereastră meniu.
Este o aplicaţie originală, partea cea mai utilă a ei în viaţa de zi cu zi fiind cea de
căutare în bază de date, putându-se constitui ca un ghid pentru găsirea informaţiilor despre
orice societate comercială din Sibiu, căutarea putându-se face după mai multe criterii.
Înregistrările acestei baze de date s-au folosit şi pentru popularea celorlalte structuri de
date pentru care au fost mai apoi implementaţi şi testaţi diferiţi algoritmi de căutare.
3
Cândea I. Ovidiu-Radu Algoritmi de căutare
CAPITOLUL 2
O definţie a noţiunii de algoritm este destul de greu de dat, însa este clar că ceea ce dă
generalitate noţiunii de algoritm este că el poate opera nu numai cu numere. Există algoritmi
algebrici, algoritmi logici, algoritmi genetici etc.
Ca o definiţie mai generală dată algoritmului s-ar putea spune ca acesta este o metodă
generală de rezolvare a unui anumit tip de problemă, metodă care se poate implementa pe
calculator. În acest context un algoritm este esenţa unei rutine.
domeniul algoritmului
Orice algoritm operează asupra unor date de intrare (Input), care sunt prelucrate
obţinându-se datele de ieşire (Output) sau altfel spus rezultatul scontat. Domeniul
algoritmului este reprezentat chiar de mulţimea obiectelor asupra cărora algoritmul lucrează
(această mulţime trebuie să fie cel mult numărabilă). În acest domeniu al algoritmului vor fi
incluse si rezultatele (datele de iesire).
4
Cândea I. Ovidiu-Radu Algoritmi de căutare
Algoritmul este alcătuit din instrucţiuni, fiecare instrucţiune este formată din operaţii
elementare care se efectueaza asupra elementelor din domeniul algoritmului. Mulţimea
instrucţiunilor formează descrierea algoritmului. Instrucţiunile sunt executate de un agent
uman, mecanic sau in cazul lucrului cu calculatorul de către un agent electronic.
Operaţiile care constituie un algoritm trebuie să fie precizate foarte clar si pe deasupra
trebuie sa poată fi executate în timp finit.
5
Cândea I. Ovidiu-Radu Algoritmi de căutare
analiza algoritmilor - de obicei se caută dacă există şi alţi algoritmi care să rezolve
problema cu pricina iar dacă există atunci se vor compara algoritmii intre ei in cea ce priveşte
funcţiile de operaţii, de locaţii, uşurinţa în înţelegere, etc; există un set de metrici bine stabilite
care măsoară diferiţi indicatori ai unui algoritm, metrici numite metrici software
ii)complexitatea algoritmilor
Eficienţa unui algoritm poate fi exprimată in trei moduri (prin trei metode):
apriori sau teoretic - se face înainte de implementarea într-un program a algoritmului, prin
determinarea teoretică a resurselor necesare (timp, memorie) funcţie de mărimea cazului
considerat;
posterior sau empiric - se face după implementarea algoritmului prin rularea lui pentru
diferite cazuri;
metode hibrid - se determină teoretic forma funcţiei care descrie eficienţa algoritmului
(funcţie de mărimea cazului); această metodă depinde de nişte parametrii care se vor
determina dupa rularea programului;
Pentru a vedea ce algoritm este mai util să fie folosit într-o anumită problemă va trebui
să comparăm aceşti algoritmi. S-ar putea crede că atunci când se compară doi algoritmi care
6
Cândea I. Ovidiu-Radu Algoritmi de căutare
rezolvă acelaşi tip de problemă, se pot face aprecieri de felul: “Algoritmul A este de doua ori
mai rapid decât algoritmul B”, dar în realitate acest tip de afirmaţie nu-şi prea are rostul.
Aceasta se întamplă din cauză că raportul se poate schimba radical atunci când se modifică
dimensiunea problemei (numarul elementelor prelucrate efectiv de cei doi algoritmi). Este
posibil ca, dacă numarul elementelor creşte cu 50%, algoritmul A să devină de trei ori mai
rapid decât B sau chiar să devină egal cu acesta.
Deci, pentru a putea compara timpul de execuţie a doi algoritmi vom avea nevoie de o
măsură legată direct de numărul de elemente asupra cărora acţionează algoritmii.
Într-un caz real timpul efectiv necesar pentru executarea instrucţiunilor unui algoritm,
notat cu T (exprimat in microsecunde sau în altă unitate de masura) este legat de viteza
microprocesorului, de eficienţa codului generat de compilator şi de alţi factori. Atunci însă
când comparăm doi algoritmi, nu ne interesează în mod deosebit performanţele
microprocesorului sau ale compilatorului; esenţial este să comparăm variaţiile duratei T
pentru diferite valori ale numărului de elemente asupra cărora acţionează algoritmii, nu să
calculăm efectiv valorile acesteia.
Sintagma bază de date reprezintă din punct de vedere informatic un set de date stocate
într-un mod organizat. Comparaţia cea mai simplă este biblioraftul (un loc fizic în care se pot
stoca date), în care însa pentru păstrarea datelor în ordine se vor folosi şi dosare, astfel datele
asociate se vor afla tot timpul într-un acelaşi dosar pentru o mai bună gestionare a acestora.
În domeniul bazelor de date, baza de date propriuzisă este reprezentată de biblioraft,
iar aceasta bază de date va fi compusă din unul sau mai multe tabele reprezentate în exemplul
dat de dosare.
Aşadar tabelul este un fişier structurat care poate stoca date de un anumit tip: o listă de
cumpărători, un catalog de produse, sau orice altă listă de informaţii. Esenţial este că datele
stocate într-un tabel sunt de acelaşi tip sau de pe aceeaşi listă.
7
Cândea I. Ovidiu-Radu Algoritmi de căutare
Fiecare tabel dintr-o bază de date are un nume care-l identifică. Acest nume este unic,
el nu mai poate fi deţinut de nici un alt tabel din baza de date.
Tabelele au caracteristici şi proprieţati ce definesc felul în care datele sunt stocate în
ele precum şi ce fel de date sunt acceptate. Acest set de informaţii care descrie un tabel poartă
denumirea de schemă a tabelului.
Tabelele sunt formate din coloane care vor conţine câte o anumită informaţie din
tabelul respectiv. Prin prezenţa unui număr sporit de coloane se contribuie la divizarea datelor
care este un concept extrem de important. Prin divizarea datelor devine posibilă sortarea sau
filtrarea datelor dupa coloane specifice.
Fiecare coloana dintr-o bază de date are asociat un tip de date, care defineşte ce fel de
date să conţină coloana (număr, dată, text, însemnări etc.). Un altfel de tip de dată decât cel
predefinit nu va fi acceptat pentru o anumită coloană.
În tabele, datele sunt stocate pe linii, fiecare înregistrare fiind stocată pe propria ei
linie. Aşadar numărul înregistrărilor dintr-un tabel va fie egal cu numărul liniilor.
Fiecare linie dintr-un tabel trebuie să aibă o coloană (sau o serie de coloane) care o
identifică în mod unic. Această coloană (coloane) va fi cheia primară a tabelului. Ea va fi
utilizată pentru a face referire la o singura linie în cadrul tabelului. Cheia primară are o
importanţa deosebită pentru că în absenţa ei actualizarea sau ştergerea de linii specifice devine
foarte dificilă deoarece nu există nici o modalitate garantat sigură pentru a face referire numai
la liniile ce vor fi afectate.
Orice coloană dintr-un tabel poate fi stabilită drept cheie primară pentru acel tabel
dacă respectă următoarele condiţii:
Două linii nu pot avea aceeaşi valoare a cheii primare
Fiecare linie trebuie să aibă o valoare a cheii primare (coloana nu poate admite valori
NULL)
Coloana care conţine valorile cheilor primare nu poate fi nicidată modificată sau
actualizata
Valorile cheilor primare nu vor fi niciodata refolosite (daca o linie este ştearsa din tabel,
cheia ei primară nu poate fi atribuită altor linii noi)
Un alt tip de cheie foarte important este cheia externă care este o coloană dintr-un
tabel ale cărei valori trebuie declarate într-o cheie primara într-un alt tabel. Cheile externe
reprezintă o parte foarte importantă din asigurarea integritaţii referenţiale dar slujesc şi unui
alt scop foarte important. După ce o cheie externă este definită, sistemul de gestionare a bazei
de date (SGBD-ul) nu permite ştergerea de linii care au asociate linii în alte tabele. Practic
8
Cândea I. Ovidiu-Radu Algoritmi de căutare
cheile externe stau la baza modelului relaţional pentru bazele de date reprezentând legăturile
(relaţiile) dintre tabele folosite în cadrul interogărilor pe două sau mai multe tabele (aici va
interveni şi noţiunea de JOIN).
Pornind de la premisa că majoritatea bazelor de date (cele cu adevărat importante)
acceptă limbajul SQL, în acest capitol numit “Căutarea în baze de date” mă voi rezuma la
cautarea în baze de date folosind limbajul neprocedural SQL.
SQL este abrevierea de la Structured Querry Language (limbaj structurat de
interogare) un limbaj conceput în mod specific pentru comunicarea cu bazele de date. Acest
limbaj a fost introdus pentru prima oară de IBM pentru a face feed-backul cu SGBD-ul
relaţional System R aflat la stadiul de prototip.
SQL este un limbaj neprocedural care a fost creat special pentru operaţii de acces la
înregistrările dintr-o bază de date relaţionala normalizată. Primul SGBD relaţional cu
adevarat comercial a fost introdus în 1979 de Oracle Corporation. Astăzi SQL a devenit un
standard industrial şi a fost declarat limbajul standard pentru sistemele de gestiune a bazelor
de date relaţionale.
Pentru a demonstra cât de răspândită este în ziua de azi folosirea limbajului SQL, iata
o listă cu unele din cele mai cunoscute aplicaţii/limbaje cu suport pentru limbajul SQL:
Allaire Cold Fusion, Allaire JRun 3.x, DB2, Informix Dynamic Server 7.x, Microsoft Acces,
Microsoft ASP, Microsoft Query, Microsoft SQL Server (6.x, 7, 2000), Microsoft Visual
Basic, Microsoft Visual C++, Oracle, Query Tool, Sybase, Java etc.
Pentru că SQL este un limbaj neprocedural, pot fi manipulate seturi întregi de date
deodată. Reducând codul de programare necesar pentru accesul la datele dintr-o bază de date,
costul pentru dezvoltarea şi menţinerea porţiunilor în care se face accesul la date într-o
aplicatie se reduce de asemenea. Şi asta este ceea ce face limbajul SQL pentru că acesta
lucrează pe câte un set de date deodată spre deosebire de celelalte modalitaţi convenţionale de
acces la date care prelucrau datele dintr-o baza de date (respectiv tabel) rând cu rând.
Sintaxa este foarte uşoară, bazată pe foarte puţine cuvinte numite cuvinte cheie care
sunt din limba engleză. Aşadar limbajul SQL este conceput pentru a face un lucru şi a-l face
bine şi anume să asigure o modalitate simplă şi eficientă de a citi şi a scrie o bază de date.
Aşa cum am mai precizat aproape toate bazele de date importante accepta limbajul
SQL şi în ciuda aparentei simplitaţi acesta este un limbaj foarte puternic care utilizat cu
inteligenţa poate efectua operaţii din cele mai complexe pe bazele de date.
Scrierea instrucţiunilor SQL necesită o buna înţelegere a felului în care a fost
proiectată baza de date. Fară a se ştii ce informaţii sunt stocate în fiecare tabel, cum sunt
9
Cândea I. Ovidiu-Radu Algoritmi de căutare
asociate tabelele între ele şi care este divizarea reala a datelor într-o linie face imposibila
scrierea unui cod eficient în limbaj SQL.
Cele mai importante patru instrucţiuni din limbajul SQL sunt: SELECT, INSERT,
UPDATE şi DELETE care sunt folosite în următoarele situaţii:
SELECT: folosită pentru regăsirea informaţiilor memorate într-o bază de date (respectiv
într-unul sau mai multe dintre tabelele bazei de date)
INSERT: folosită pentru inserarea (adăugarea) de linii într-un tabel dintr-o bază de date
UPDATE: folosită pentru actualizarea (modificarea) datelor dintr-un tabel
DELETE: folosită pentru ştergerea datelor dintr-un tabel
Având în vedere tema lucrării de faţa vom studia instrucţiunea SELECT folosită dupa
cum s-a precizat mai sus la căutarea unor anumite date memorate în unul sau mai multe
tabele.
Pentru a putea fi folosita această instrucţiune este necesar să se precizeze cel putin
două lucruri şi anume: ce date doresc a fi regăsite şi de unde anume.
Ex: SELECT nume_coloana FROM nume_tabel;
În exemplul dat se vor regăsi datele de pe o singură coloana aflată în tabelul
nume_tabel de la toate înregistrarile tabelului (mai precis vor fi întoarse toate datele care au
fost introduse în tabelul nume_tabel în coloana nume_coloana). Se observă un nou cuvânt
cheie FROM care specifică numele tabelului din care vor fi regăsite datele.
Pentru a regăsi mai multe coloane dintr-un tabel vom avea o instrucţiune de tipul:
unde în dreptul instrucţiunii SELECT pot fi scrise oricâte coloane din nume_tabel.
Dacă se doreşte regăsirea tuturor coloanelor dintr-un tabel se pot enumera toate
coloanele tabelului în dreptul instrucţiunii SELECT sau mai simplu se poate scrie o
instrucţiune de tipul:
Daca se vor încerca instrucţiunile prezentate mai sus se va putea observa că datele
returnate nu vor fi afişate într-o ordine anumită (vor fi afişate după cum se găsesc în tabelele
de bază).
10
Cândea I. Ovidiu-Radu Algoritmi de căutare
Astfel în exemplul de mai sus vor fi afişate datele din coloanele nume_coloana1,
nume_coloana2, nume_coloana3 aflate în tabelul nume_tabel, aceste date vor fi ordonate
dupa nume_coloana4 (trebuie să existe), iar dacă vor fi înregistrări care vor avea aceeaşi
valoare în această coloană, acestea vor fi afişate ordonat dupa nume_coloana2.
La fel ca în dreptul instrucţiunii SELECT şi în dreptul clauzei ORDER BY pot fi puse
oricâte nume de coloane cu condiţia ca ele să existe.
Este bine de ştiut că sortarea datelor nu se limitează la ordinea de sortare ascendentă,
care este ordinea de sortare prestabilită. Pentru a sorta în ordine descendentă, trebuie
specificat cuvântul cheie DESC.
Ex:
SELECT *
FROM nume_tabel
ORDER BY nume_coloana1 DESC, nume_coloana2;
! Cuvantul cheie DESC se aplică doar numelui de coloană care îl precede direct. Deci
în exemplul de mai sus datele vor fi sortate descrescător dupa nume_coloana1 şi crescător
după nume_coloana2.
De obicei, tabelele bazelor de date conţin volume mari de date şi rareori trebuie să
regăsim toate înregistrarile din tabel. Frecvent vom dori regăsirea unui subset de date care
respecta anumite condiţii. Regăsirea numai a datelor dorite implică specificarea condiţiilor
(sau condiţiei) de filtrare.
Într-o instrucţiune SELECT datele sunt filtrate prin specificarea criteriilor de căutare
în clauza WHERE. Aceasta este precizată imediat după numele tabelului (clauza WHERE) şi
înainte de clauza ORDER BY care este opţională.
Ex:
SELECT *
FROM nume_tabel
WHERE nume_coloana1=25
ORDER BY nume_coloana2;
11
Cândea I. Ovidiu-Radu Algoritmi de căutare
O instrucţiune de tipul celei de mai sus va găsi toate coloanele tabelului nume_tabel
dar nu va întoarce decât acele înregistrări care vor avea pe coloana nume_coloana1 valoarea
25. Datele vor fi afişate sortate ascendent după coloana nume_coloana2.
În exemplul de mai sus am testat egalitatea care determina dacă o coloană conţine o
valoare specificată. Limbajul SQL acceptă mai mulţi operatori condiţionali enumerati mai
jos:
= Egalitate
<> sau != Non-egalitate
< Mai mic decât
<= Mai mic sau egal cu
Mai mare decât
>= Mai mare sau egal cu
BETWEEN Intre două valori specificate
IS NULL Este valoarea NULL
Pentru un grad superior de control al filtrării, limbajul SQL permite specificarea mai
multor clauze WHERE. Acestea pot fi folosite în două modalitaţi, sub forma de clauze AND
sau clauze OR (operatori logici).
Dacă este folosită clauza AND vor fi returnate doar înregistrările care vor satisface
ambele condiţii legate prin AND. Operatorul OR este exact opusul operatorului AND. El
instruieste SGBD-ul să returneze liniile ce corespund oricăreia dintre condiţii.
Clauzele WHERE pot contine oricât de mulţi operatori AND şi OR. Combinarea celor
două tipuri de operatori va permite să fie efectuate filtrări complexe. Vor trebui însă folosite
parantezele rotunde pentru a specifica SGBD-ului care dintre condiţii au precedenţa de
evaluare mai ridicată.
Ex:
SELECT *
FROM nume_tabel
WHERE (nume_coloana1 IS NULL OR nume_coloana2=’Sibiu’)
AND nume_coloana3 BETWEEN 5 AND 10
ORDER BY nume_coloana2;
Instrucţiunea SQL de mai sus va returna înregistrarile din toate coloanele tabelului
ordonate dupa nume_coloana2 care vor satisface condiţiile de la ambele puncte:
nu există trecută nici o valoare în dreptul coloanei nume_coloana1 (IS NULL) sau
valoarea din dreptul coloanei nume_coloana2 este Sibiu (una sau ambele condiţii trebuie
satisfacute)
12
Cândea I. Ovidiu-Radu Algoritmi de căutare
MOD (r,n), REPLACE (c, s1 [, r2]), SIGN (n), SIN (n), SQRT (n), SUM (DISTINCT|ALL
n), UPPER(c).
Datele obţinute în urma unor introgari pot fi grupate după unul sau mai multe câmpuri
folosind două noi clauze şi anume GROUP BY şi HAVING. Folosind aceste clauze se pot
calcula diferite valori şi aplica anumite funcţii pentru subseturi de date.
Ex:
SELECT nume_coloana1,COUNT(*) AS alias
FROM nume_tabel
GROUP BY nume_coloana1
HAVING nume_coloana1=’Sibiu’;
14
Cândea I. Ovidiu-Radu Algoritmi de căutare
SELECT nume_coloana1
FROM nume_tabel1
WHERE nume_coloana1 în (SELECT nume_coloana1
FROM nume_tabel2
WHERE nume_coloana2=’Sibiu’)
Astfel de sub selecţii sunt prelucrate începand cu instrucţiunea SELECT cea mai
interioara şi deplasându-se spre exterior. Atunci când este prelucrată instrucţiunea SELECT
precedentă, practic sistemul de gestionare a bazei de date execută doua operaţii, instrucţiunea
interioara returnează un domeniu în care instrucţiunea exterioară caută valori pentru
nume_coloana_1.
La fel cum am folosit şi în exemplul de mai sus, utilizările cele mai uzuale pentru
subselecţii sunt în operatorii în ai clauzei WHERE şi în popularea coloanelor calculate
astfel:
SELECT nume_coloana1,
(SELECT COUNT(*)
FROM nume_tabel
WHERE nume_coloana1=’Sibiu’ AS total)
FROM nume_tabel
Una din caracteristicile extrem de puternice din limbajul SQL este capacitatea de a uni
tabelele din mers (aceasta unire nu este una fizică), în interogările de regăsire a datelor.
Unirile constituie unele din cele mai importante operaţii care se pot efectua folosind
instrucţiunile SELECT, o buna întelegere a lor şi a sintaxei pentru unire reprezintă o parte
esenţială din învaţarea limbajului SQL.
Se ştie că divizarea datelor în mai multe tabele permite o stocare mai eficientă, o
manipulare mai uşoara şi o scalare mai bună. Unirea tabelelor este de fapt un mecanism
folosit pentru asocierea tabelelor într-o instrucţiune SELECT care în final va returna liniile
corecte din fiecare tabel.
Crearea unei uniri este foarte simplă. Tot ceea ce trebuie facut este să se specifice
numele tuturor tabelelor (dupa clauza FROM) ce vor fi incluse şi felul în care sunt asociate
reciproc (dupa clauza WHERE).
15
Cândea I. Ovidiu-Radu Algoritmi de căutare
16
Cândea I. Ovidiu-Radu Algoritmi de căutare
17
Cândea I. Ovidiu-Radu Algoritmi de căutare
standard pentru erori (de asemenea ecran). Dacă un program citeşte din 0 şi scrie în 1 şi 2,
acesta poate realiza operaţii de citire şi scriere fară a se preocupa de deschiderea fişierelor, în
caz contrar intrarea standard şi ieşirea standard vor trebui redirecţionate către fişierele sau
canalele de transfer dorite prin metode specifice limbajului de programare în care se lucrează.
În acest caz shell-ul înlocuieste destinaţiile obişnuite ale descriptorilor de fişiere 0 şi 1
cu fişierele precizate. În mod normal descriptorul de fişier 2 rămâne atribuit ecranului, pentru
că mesajele de eroare să poată ajunge acolo. Observaţii asemănătoare sunt valabile şi pentru
operaţiile de intrare şi de ieşire prin intermediul unui canal de transfer. În toate cazurile
atribuirile fişierelor sunt modificâte de către shell, şi nu de către program. Programul nu stie
de unde provin datele sale de intrare şi nici unde merg datele sale de ieşire, atât timp cât
foloseşte fişierul 0 pentru operaţii de intrare şi fişierele 1, 2 pentru operaţii de ieşire.
Operaţiile de intrare şi ieşire folosesc apelurile sistem read şi write care sunt accesate
din cadrul unui program. La un apel pot fi citiţi sau scrişi un număr oricât de mare de octeţi.
Se va utiliza o aşanumită zonă tampon (buffer) de o anumită mărime ce poate fi schimbată
unde vor fi stocate datele sau de unde urmează să fie preluate. Cum lucrează acest buffer ?
Indiferent dacă este vorba despre o citire sau o scriere datele sunt depuse în buffer de unde vor
fi preluate de aplicaţie (la citire) sau de unde vor fi luate şi scrise în fişier (la scriere).
De cele mai multe ori numărul de octeţi folosiţi la operaţii de intrare/ieşire este 1 care
înseamnă caracter cu caracter (fără a trece prin buffer). Când se doreşte citirea sau scrierea
unei cantitati mari de informaţie se preferă folosirea bufferului pentru că vor fi efectuate mai
puţine apeluri sistem.
Operaţiile de citire şi de scriere au în mod normal un caracter secvenţial: fiecare
operaţie read sau write se efectuează la o poziţie din fişier aflată imediat După cea
precedentă. Totuşi atunci când este necesar, dintr-un fişier se poate citi sau scrie în acesta într-
o ordine arbitrară. Apelul sistem lseek furnizează o modalitate de deplasare într-un fişier fără
a citi sau a scrie o dată. Bineînţeles ca această funcţie trebuie să primeasca în lista sa de
parametrii descriptorul fişierului pe care se lucrează şi deplasamentul faţă de o anumită
origine de asemenea precizată. Operaţiile următoare de scriere sau citire vor începe la acea
poziţie la care s-a ajuns prin apelul lui lseek.
Originea poate fi precizată cu ajutorul valorilor întregi 0, 1, 2 pentru a preciza ca
deplasamentul (tot o valoare întreaga) se va masura de la început, de la poziţia curentă sau de
la sfârşitul fişierului. Această funcţie lseek va returna un long care precizează noua poziţie din
fişier.
18
Cândea I. Ovidiu-Radu Algoritmi de căutare
Prin intermediul funcţiei lseek este posibil ca fişierele sa fie tratate într-o masura mai
mare sau mai mică precum nişte tablouri mari, cu preţul unei viteze de acces mai mici.
Toate aceste funcţii sistem precizate vor putea fi apelate din cadrul programelor prin
rutine specifice fiecărui limbaj de programare în parte. Mai jos vom analiza cum se face acest
apel în limbaj C/C++ şi Java.
Aşadar înainte de a citi dintr-un anumit fişier sau a scrie în acesta, fişierul trebuie
deschis prin intermediul unei funcţii de bibliotecă (fopen() în C/C++) specifică limbajului în
care se programează. Această funcţie preia un nume extern, al fişierului pe care se va lucra,
efectuează câteva operaţii de economie interna şi negociere cu sistemul de operare şi
returnează un pointer (sau o referinţa) care să fie folosită la operaţiile ulterioare de citire şi
scriere în fişier.
Pointerul de fişier (sau referinţa) returnat indică spre o structură care conţine
informaţii despre fişier cum ar fi locaţia bufferului, poziţia caracterului curent din fişier, dacă
se citeşte din sau se scrie în fişier şi dacă au apărut erori sau dacă s-a întâlnit sfârşitul de fişier
notat de cele mai multe ori cu eof (end-of-file).
Singura declaraţie necesară pentru un pointer la fişier în limbaj C++ este:
FILE *pf;
FILE *fopen (char *nume, char *mod);
Cele de mai sus precizează că dacă pf este un pointer spre tipul FILE (fişier) şi ca
funcţia fopen returnează un pointer spre tipul FILE. De reţinut este faptul că FILE este un
nume de tip precum de exemplu int, nu un nume generic de structura; acesta este definit prin
intermediul lui typedef.
Apelul către funcţia fopen dintr-un program este: pf = fopen (nume, mod); Primul
argument al funcţiei fopen este un şir de caractere ce conţine numele fişierului. Al doilea
argument este modul, de asemenea un şir de caractere, care precizează cum să fie folosit
fişierul. Printre modurile premise se numără citirea – read(“r”), scrierea – write (“w”) şi
adăugarea – append (“a”). Pentru că unele sisteme fac distincţie între fişierele text şi cele
binare, pentru cele din urmă, trebuie adăugata litera “b” la şirul de caractere care specifică
modul.
Dacă se deschide pentru operaţii de scriere sau adăugare un fişier care nu există, acesta
va fi creat automat. Deschiderea unui fişier pentru operaţii de scriere duce la pierderea
vechiului conţinut, în timp ce deschiderea pentru operaţii de adăugare păstreaza conţinutul
anterior. Încercarea de a citi un fişier care nu există constituie o eroare (excepţie în Java).
19
Cândea I. Ovidiu-Radu Algoritmi de căutare
Dacă apare o astfel de eroare sau oricare alta (eroare va fi şi când se încearcă deschiderea
unui fişier la care nu aveţi permisiune) Funcţia fopen returnează NULL.
Fişierele sunt folosite pentru a prelua sau a scrie date în ele. Bineînţeles că datele vor
fi prelucrate înainte în cazul scrierii şi după în cazul citirii dintr-un fişier.
Aşadar un lucru absolut necesar este o modalitate de a citi din fişier sau de a scrie în
acesta o dată ce a fost deschis. Citirea precum şi scrierea se vor face cu funcţii specifice
limbajului de programare folosit (în C++: getc, putc,getchar, putchar, fscanf, fprintf).
După terminarea lucrului cu fişiere trebuie apelată o funcţie fclose() (în C/C++) pentru
întreruperea legăturii dintre pointerul de de fişier şi numele extern, eliberând pointerul de
fişier pentru a putea fi folosit de un alt fişier (această legatură a fost făcută prin apelul
funcţiei fopen()- în C/C++). Deoarece majoritatea sistemelor de operare impun o anumită
limită numărului de fişiere pe care un program le poate deschide simultan, este o idee bună
eliberarea pointerilor de fişier atunci când aceştia nu mai sunt necesari.
Mai există un motiv pentru folosirea funcţiei fclose() pentru un fişier de ieşire –
goleşte bufferul în care se trimit datele de ieşire.
Într-un program Java pentru operaţiile de scriere şi citire se folosesc aşanumitele
fluxuri.
Pentru a aduce informaţii dintr-un mediu extern cum este şi un fişier, un progam Java
trebuie să deschida un canal de comunicaţie (flux) către sursa informaţiilor şi să citească serial
informaţiile respective:
20
Cândea I. Ovidiu-Radu Algoritmi de căutare
Citirea Scrierea
deschide canal comunicaţie Deschide canal comunicaţie
while (mai sunt informaţii) { while (mai sunt informaţii) {
citeşte informaţie scrie informaţie
} }
închide canal comunicaţie; închide canal comunicaţie;
Un flux este un canal de comunicaţie unidirectional serial pe 8 sau 16 biţi între două
procese. Un flux care citeşte date se numeşte flux de intrare. Un flux care scrie date se
numeşte flux de ieşire.
Există trei tipuri de clasificare a fluxurilor:
După "direcţia" canalului de comunicaţie deschis fluxurile se împart în:
fluxuri de intrare (pentru citirea datelor)
fluxuri de ieşire (pentru scrierea datelor)
După tipul de date pe care operează:
fluxuri de octeţi (comunicare seriala realizată pe 8 biţi)
fluxuri de caractere (comunicare seriala realizeată pe 16 biţi)
După acţiunea lor:
fluxuri primare de citire/scriere a datelor (se ocupa efectiv cu citirea/scrierea datelor)
fluxuri pentru procesarea datelor
Fluxurile pentru lucrul cu fişiere sunt cele mai uşor de înteles. Clasele care
implementează aceste fluxuri sunt următoarele:
FileReader, FileWriter - caractere
FileInputStream, FileOutputStream - octeţi
Constructorii acestor clase acceptă ca argument un obiect care să specifice un anume
fişier. Acesta poate fi un şir de caractere, un obiect de tip File sau un obiect de tip
FileDesciptor.
Constructorii clasei FileReader: (fluxuri pentru citirea din fişier)
public FileReader(String fileName) throws FileNotFoundExcepţion
public FileReader(File file) throws FileNotFoundExcepţion
public FileReader(FileDescriptor fd)
Constructorii clasei FileWriter: (fluxuri pentru scrierea într-un fişier)
public FileWriter(String fileName) throws IOExcepţion
21
Cândea I. Ovidiu-Radu Algoritmi de căutare
BufferedReader(Reader în)
BufferedReader
BufferedReader(Reader in, int dim_buffer)
BufferedWriter(Writer out)
BufferedWriter
BufferedWriter(Writer out, int dim_buffer)
BufferedInputStream(InputStream în)
BufferedInputStream
BufferedInputStream(InputStream in, int dim_buffer)
BufferedOutputStream(OutputStream out)
BufferedOutputStream
BufferedOutputStream(OutputStream out, int dim_buffer)
22
Cândea I. Ovidiu-Radu Algoritmi de căutare
Identificarea tipului şi valorii unui atom lexical se face prin intermediul variabilelor:
TT_EOF - atom ce marchează sfârsitul fluxului
TT_EOL - atom ce marchează sfârsitul unei linii
TT_NUMBER - atom de tip număr
TT_WORD - atom de tip cuvânt
nval - valoarea unui atom numeric
sval - şirul conţinut de un atom de tip cuvânt
ttype - tipul ultimului atom citit din flux
Citirea atomilor din flux se face cu metoda nextToken(), care returnează tipul
atomului lexical citit şi scrie în variabilele nval sau sval valoarea corespunzătoare atomului.
23
Cândea I. Ovidiu-Radu Algoritmi de căutare
Mai jos este dat un un exemplu de program în care se caută un anumit String într-un
fişier memorat pe disc prin despărţirea datelor din fişier în fluxuri:
import java.io.*;
public class TestTokenizer {
public static void main(String args[]) throws IOExcepţion{
String cuvCautat=”unCuvant”;
int nr =25;
FileInputStream fis = new FileInputStream("test.dat");
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
StreamTokenizer st = new StreamTokenizer(br);
int tip = st.nextToken(); //citesc primul atom lexical
while (tip != StreamTokenizer.TT_EOF) {
switch (tip) {
case StreamTokenizer.TT_WORD: // cuvânt
if (st.sval.equals(cuvCautat)) System.out.println(st.sval);
break;
case StreamTokenizer.TT_NUMBER: // număr
if (st.nval= =25) System.out.println(st.nval);
}
tip = st.nextToken();// următorul atom
}
}
}
Aşadar, modul de utilizare tipic pentru un analizor lexical este într-o buclă "while" în
care se citesc atomii unul câte unul cu metoda nextToken pâna se ajunge la sfârsitul fluxului
(TT_EOF). În cadrul buclei "while" se află tipul atomului curent (întors de metoda
nextToken) şi apoi se află valoarea numerica sau şirul de caractere corespunzător atomului
respectiv.
În funcţie de necesitaţi în cadrul unui program se pot folosi una din clasele
următoare:
DataInputStream, DataOutputStream
BufferedInputStream, BufferedOutputStream
LineNumberInputStream
PushbackInputStream
PrintStream (flux de ieşire)
Toate acestea sunt clase pentru filtrarea datelor. Un flux de filtrare se ataşează
altui flux pentru a filtra datele care sunt citite/scrise de către acel flux. Clasele pentru fluxuri
de filtrare au ca superclase clasele abstracte FilterInputStream (pentru filtrarea fluxurilor de
intrare) şi FilterOutputStream (pentru filtrarea fluxurilor de ieşire).
Se observă că toate aceste clase descriu fluxuri de octeţi.
Filtrarea datelor nu trebuie văzută ca o metoda de a elimina anumiti octeţi dintr-un
flux ci de transformă aceşti octeţi în date care să poata fi interpretate sub alta forma.
Aşa cum am văzut la citirea/scrierea cu zona tampon clasele de filtrare
BufferedInputStream şi BufferedOutputStream grupează datele unui flux într-un buffer,
urmând ca citirea/scrierea sa se facă prin intermediul acelui buffer.
Aşadar fluxurile de filtrare nu elimină date citite sau scrise de un anumit flux, ci
24
Cândea I. Ovidiu-Radu Algoritmi de căutare
introduc o noua modalitate de manipulare a lor. Din acest motiv fluxurile de filtrare vor
conţine anumite metode specializate pentru citirea/scrierea datelor, altele decât cele commune
tuturor fluxurilor (metode de tip read/write).
Folosirea fluxurilor de filtrare se face prin ataşarea lor de un flux care se ocupă
efectiv de citirea/scrierea datelor:
FluxFiltrare numeFlux = new FluxFiltrare (referinţaAltFlux)
DataInputStream DataOuputStream
//Constructor //Constructor
DataInputStream(InputStream in) DataOutputStream(OutputStream out)
ReadBoolean() writeBoolean(boolean v)
ReadByte() writeByte(int v)
ReadChar() writeChar(int v)
ReadDouble() writeDouble(double v)
ReadFloat() writeFloat(float v)
ReadInt() writeInt(int v)
ReadLong() writeLong(long v)
ReadShort() writeShort(int v)
ReadUnsignedByte() writeBytes(String s)
ReadUnsignedShort() writeChars(String s)
String readUTF() writeUTF(String str)
25
Cândea I. Ovidiu-Radu Algoritmi de căutare
magnetica,etc. deşi sunt foarte utile şi pentru dispozitive în care informaţia poate fi accesată
direct.
Clasa RandomAccesFile permite accesul nesecvenţial (direct) la conţinutul unui fişier.
Ea este o clasa de sine statătoare, subclasa directă a clasei Object, se gaseşte în pachetul
java.io şi implementeaza interfetele DataInput şi DataOutput, ceea ce înseamna că sunt
disponibile metode de tipul readXXX, writeXXX (permite atât citirea cât şi scriere din/în
fişiere cu acces direct).
Ceea ce este mai important este că această clasă permite specificarea modului de acces
al unui fişier (read-only, read-write)
Constructorii acestei clase sunt:
RandomAccessFile(String numeFişier, String mod_acces) throws IOExcepţion
RandomAccessFile(String fişier, String mod_acces) throws IOExcepţion
unde mod_acces poate fi:
"r" - fişierul este deschis numai pentru citire (read-only)
"rw" - fişierul este deschis pentru citire şi scriere (read-write)
Exemple:
int skipBytes (int n) Mută pointerul fişierului înainte cu un număr specificat de octeţi
void seek (long poziţie) Poziţioneaza pointerul fişierului înaintea octetului specificat.
long getFilePointer () Returnează poziţia pointerului de fişier (poziţia de la care se citeşte/la care se scrie)
26
Cândea I. Ovidiu-Radu Algoritmi de căutare
Tablourile reprezintă structurile de date cel mai frecvent utilizate, fiind incluse în
majoritatea limbajelor de programare.
În limbajul Java tablourile nu sunt tratate că un tip primitiv de date ci ca nişte obiecte.
De aceea trebuie utilizat operatorul new pentru crearea unui tablou.
Exemplu:
Cel mai simplu algoritm pentru căutare într-un tablou este cel de căutare secvenţială,
care se face parcurgând structura de date de la început spre sfârsit, comparându-se cheia
căutată cu valorile găsite în şir la poziţia în care ne aflăm.
27
Cândea I. Ovidiu-Radu Algoritmi de căutare
i:= i+1;
EndWhile
If i >n then cod=”Eşec”;
poz:= -1;
Else cod = “Succes”;
poz:= i;
EndIf
28
Cândea I. Ovidiu-Radu Algoritmi de căutare
În cazul cel mai nefavorabil atunci când elementul cautat nu este în mulţimea M,
mulţimea va fi parcursă în întregime, numărul de comparaţii fiind n. Deci, în cazul cel mai
nefavorabil vom avea T(n)∈O(n).
29
Cândea I. Ovidiu-Radu Algoritmi de căutare
Probabilitatea că x să se afle pe poziţia i este P(x = xi) =1/(n+1) deoarece s-ar putea că
x să nu fie în M. Dacă x = xi atunci se vor parcurge exact i elemente până la găsirea lui x.
Aşadar timpul de căutare va fi dat de formula:
n +1
Targ(n)= ∑P ( x = xi ) i = n/2
i =1
30
Cândea I. Ovidiu-Radu Algoritmi de căutare
Pentru aceasta trebuia să se înceapă de la numărul 50 (jumătatea lui 100) iar apoi se va
încerca numărul 75 daca numărul cautat este mai mare său 25 daca numărul cautat este mai
mic. Se observă că fiecare încercare permite înjumătăţirea domeniului valorilor posibile (daca
numărul cautat este mai mic se va cauta numărul între 1 şi 50, iar daca acesta este mai mare
se va caută numărul între 50 şi 100, încercându-se valoarea din mijloc a acestui interval). în
final domeniul se reduce la un singur număr care este chiar numărul cautat.
Aşadar se citeşte valoarea din mijlocul tabloului; daca elementul cautat este mai mare,
căutarea se restrânge la jumătatea superioară a tabloului; analog, daca elementul este mai
mic, căutarea se va continua în jumătatea inferioară (de aici apare şi posibilitatea că acest
algoritm să poata fi scris folosind o metodă recursivă).
31
Cândea I. Ovidiu-Radu Algoritmi de căutare
} // închidere else
} // închidere while
} // sfârşitul metodei caută ()
32
Cândea I. Ovidiu-Radu Algoritmi de căutare
este o problemă serioasă atunci când rezultă o cantitate mare de date, putând conduce la
depăşirea capacităţii stivei.
Recursivitatea se foloseşte, de regulă, din cauză că simplifică problema din punct de
vedere conceptual şi nu pentru că ar reprezenta o soluţie mai eficientă.
Căutarea unui element y într-un şir X, ordonat i ∈ { 1, 2,... n } se poate face şi pe un
sistem folosind spre exemplu un număr de p procesoare în felul următor:
Se împarte şirul X în P subşiruri de lungimi aproximativ egale, fiecare procesor având în
sarcină realizarea căutării în unul din aceste subşiruri. În urma celor p căutări, fie se gaseşte
elementul egal cu y, fie se restricţionează căutarea într-unul din acele p segmente. În plus se
adaugă două elemente santinelă x0 = - ∞ şi xn = + ∞ .
În practica curentă listele înlanţuite sunt probabil cele mai frecvent folosite structuri de
date dupa tablouri.
Lista înlanţuită oferă un mecanism extrem de eficient care poate fi folosit în multe
tipuri de bază de date. Listele pot înlocui tablourile că structuri de baza pentru implementarea
altor structuri cum sunt stivele şi cozile. De fapt se poate utiliza o listă înlanţuită aproape în
orice loc unde se poate utiliza un tablou, iar atunci când trebuie să se aleagă care din aceste
structuri de date să fie folosite va fi nevoie de o analiza a aplicaţiei ce se doreşte a fi
realizată luându-se în calcul faptul că timpii de căutare, inserare, ştergere etc. pot diferi foarte
mult de la o structură de date la alta, respectiv de la un tablou la o lista înlanţuita.
liste dubluînlanţuite
O listă înlanţuită este alcatuită din o înşiruire de noduri care conţin informaţia (la fel
că elementele dintr-un tablou) specifică fiecărui nod în parte dar mai conţine şi o legătură
către nodul următor din lista la listele simpluînlanţuite, sau două legături, una către elementul
din stânga (nodul anterior) şi altul către elementul din dreapta (nodul următor) la listele
dubluînlanţuite. În limbaj Java un nod este reprezentat de fapt de o instanţă a unei clase
denumită să presupunem Link. Din cauză că există multe noduri similare în cuprinsul unei
33
Cândea I. Ovidiu-Radu Algoritmi de căutare
liste (diferite prin informaţia continuta) va trebui creată o clasă separată de obiecte pentru
acestea, distinctă de clasă care descrie lista propriuzisă.
Liste simpluînlanţuite
Vom vorbi deocamdată de listele simpluînlanţuite. Pentru acestea fiecare nod conţine
o referinţa (denumită de obicei next) către următorul nod din listă, aceasta referinţă
reprezentand de fapt legătura. Un câmp din structura care descrie lista va conţine o referinţă la
primul nod din lista.
În elementele din listă trebuie să existe o informaţie folosita drept “cheie” în cadrul
căreia să existe o relaţie de ordine totală (oricare două elemente să poata fi comparate).
class Link
{
public int iData; // informaţii
public double dData; // informaţii
public Link next; // referinţă către urmatorul nod
public Link (int id,double dd) // constructor
{
iData = id;
dData = dd;
}
public void displayLink () // metoda poate lipsi pt o şi mai mare simplificare
{
System.out.println(“{“+Data+ ”, “ +dData+ “}”);
}
}
Observăm că aceasta clasă conţine un câmp (câmpul next) de acelaşi tip că şi clasa,
câmp care este de fapt o referinţă către următorul nod din lista. Asemenea definiţie pentru o
34
Cândea I. Ovidiu-Radu Algoritmi de căutare
class LinkList
{
private Link first; // referinţa către primul element din listă
public LinkList () // constructor
{
first = null; // lista nu va conţine nici un element
}
public boolean isEmpty () // întoarce true dacă lista este goala
{
return (first = = null );
}
--------------------------------- // alte metode
}
Constructorul clasei LinkList iniţializează first cu null, deci primul element din listă
va fi null pentru că la ănceput lista este goală. Vedem că atunci când first are valoarea null
lista este goala (dacă lista ar fi continut elemente, first ar fi continut referinţa către primul
dintre acestea). Metoda isEmpty() utilizează acest fapt pentru a determina dacă lista este
goală.
Codul metodei prin care se realizează căutarea va fi scris în interiorul acestei clase
LinkList, că de altfel şi celelalte metode obişnuite în lucrul cu structuri de date (inserare,
ştergere, parcurgere...).
Ştim că într-un tablou fiecare element ocupă o anumita poziţie care este direct
accesibilă utilizând un indice numeric. Într-o lista însa singura posibilitate de a găsi un anumit
element este de a urma întreg lanţul de elemente. Va fi nevoie aşadar să traversăm lista de la
cap la coada. Aceasta traversare va trebui să fie făcută folosind relatiile dintre elemente,
35
Cândea I. Ovidiu-Radu Algoritmi de căutare
aşadar legăturile ne vor ajuta pentru a avansa de la un element la altul. Cum vom proceda:
vom începe cu primul element “first” (singurul element accesibil direct), după care continuam
cu al doilea, cu al treilea şi aşa mai departe până vom găsi elementul pe care îl căutăm.
Identificarea elementului se va face prin compararea cheii de căutare cu informaţiile din nodul
curent (nodul la care am ajuns la un moment dat).
Vedem că pentru a căuta într-o listă avem nevoie de o variabilă curent care indică (se
refera către) pe rând spre fiecare dintre noduri, (cu ajutorul ei ne deplasam în lista).
Variabila curent începe prin a-l referi pe first (care conţine o referinţă către prima
legatură din lista). Instrucţiunea current = current. next îl face pe curent să avanseze la
urmatorul element din listă.
Această instrucţiune este pusă într-un ciclu while în care se verifică la fiecare pas dacă
s-a găsit elementul căutat, adică dacă valoarea cheii elementului curent (referit de curent) este
egală cu valoarea căutată.
Dacă acest element s-a gasit metoda va întoarce o referinţa către acel nod. Dacă însa
se atinge sfarşitul listei, adică dacă câmpul next dintr-un nod conţine valoarea null, şi înca nu
s-a gasit valoarea dorită, metoda va întoarce null.
Liste dubluînlanţuite
36
Cândea I. Ovidiu-Radu Algoritmi de căutare
Ştim deja că o lista duluînlanţuita este alcătuita din o înşiruire de noduri care conţin
informaţie precum şi două referinţe (legaturi), unul către elementul din stânga (elementul
anterior) şi altul către elementul din dreapta (elementul următor). Principalul avantaj pe care îl
conferă lista dubluînlanţuita faţa de cea simpluînlanţuita este că aceasta va putea fi parcursă
atât înainte cât şi înapoi. Desigur că implementarea unei astfel de liste este ceva mai
complicată, iar fiecare dintre noduri va ocupa ceva mai multa memorie, însă de multe ori
proprietatea de a fi parcursă în ambele sensuri va avea o importanţa covârşitoare în a alege
lista dubluînlanţuita drept structura de baza a aplicaţiilor noastre.
Observăm că tot ce are în plus faţa de clasa Link a listei simpluînlanţuite este referinţa
previous spre nodul anterior. La fel clasa LinkList va fi asemănătoare cu clasa LinkList de la
lista simpluînlanţuită, doar că în cadrul constructorului va trebui să adaugăm o variabilă
“last” care va indica spre ultimul element din lista şi să iniţializăm această variabilă cu null,
ca şi pe first deoarece la construirea unei liste aceasta va fi goală.
Class LinkList
{
private Link first; // referinţa către primul element din lista
public LinkList () // constructor
{
first = null; // lista nu va conţine nici un element
37
Cândea I. Ovidiu-Radu Algoritmi de căutare
}
public boolean isEmpty () // întoarce true dacă lista este goală
{
return (first = = null );
}
--------------------- // alte metode printre care şi cea de căutare, inserare, parcurgere, ştergere etc.
}
Operaţia de căutare nu va fi însa mult influenţată de faptul că lista este dublu sau
simpluînlanţuită, atât doar că această căutare va putea fi făcută prin parcurgerea listei
începând de la capatul listei sau de la sfarşitul acesteia.
Pentru o lista înlaţuită se vor obţine timpi mult mai buni decât în cazul tablourilor
neordonate la operaţiile de inserare şi ştergere (mai ales atunci când copierea este mullt mai
lentă decat comparatia) deoarece la tablouri aceste operaţii presupune o mutare a mai multor
elemente prin copiere spre stânga sau spre dreapta.
Un alt avantaj important pe care listele înlanţuite îl au faţa de tablouri este că o listă
utilizeaza exact cantitatea de memorie de care are nevoie, pe când dimensiunea unui tablou
este fixata la crearea acestuia ceea ce conduce de cele mai multe ori la ineficienţa.
Un arbore constă din noduri, care sunt unite prin muchii şi reprezintă de fapt o
particularizare a unei categorii mai generale numită graf. Nodurile se vor reprezenta grafic
prin niste cerculeţe conectate prin nişte linii care nu sunt altceva decât muchii.
38
Cândea I. Ovidiu-Radu Algoritmi de căutare
De regulă, nivelul superior, numit nivelul 0 al unui arbore cuprinde un singur nod
(rădăcină), conectat prin muchii cu nodurile sale fiu aflate pe nivelul imediat inferior (nivelul
1). Aceste noduri pot avea şi ele la randul lor noduri fii aflate pe nivelul 2 ş.a.m.d.
Orice nod (cu excepţia radacinii) are exact o muchie orientată în sus spre un alt nod.
Nodul situat mai sus este numit părintele nodului curent.
Un nod fără fii se numeşte frunză şi orice nod poate fi considerat ca fiind rădăcină
pentru un subarbore, care cuprinde fiii acelui nod, fiii fiilor săi ş.a.m.d.
Nodurile unui arbore vor conţine obiecte, instanţe ale unor clase gata construite.
Valoarea elementului de un anumit tip, conţinută de un obiect, poate fi considerată cheie.
Această valoare poate fi utilizată pentru a cauta elementul sau pentru a executa alte operaţii
asupra sa.
Există mai multe tipuri de arbori. Printre aceştia arborele binar (arborele în care orice
nod poate avea cel mult doi fii), este cel mai simplu tip de arbore dar totodata şi cel mai des
folosit.
Cei doi fii ai unui nod se numesc fiu stâng şi respectiv fiu drept în funcţie de poziţia
lor în reprezentarea grafică a arborelui. Pot exista noduri într-un arbore binar cu mai puţin de
doi fii; astfel un nod poate avea doar un fiu stâng, sau doar un fiu drept, ori poate să nu aibă
nici un fiu (caz în care nodul este frunză).
Din punct de vedere tehnic caracteristica esenţială a unui arbore binar de căutare este
că cheia fiului stâng trebuie să fie mai mică decât cea a părintelui, iar cheia fiului drept trebuie
să fie mai mare sau egală cu cea a părintelui.
toate cheile din subarborele stâng (dacă există preced cheia din rădăcină)
39
Cândea I. Ovidiu-Radu Algoritmi de căutare
cheia din rădăcină precede toate cheile din subarborele drept (dacă există)
Căutarea unui nod: această operaţie de căutare a unui nod cu o anumită cheie este cea
mai simplă dintre cele trei operaţii de bază (inserare, ştergere, căutare).
Informaţia din câmpul cheie poate fi de orice tip de dată peste care se poate defini o
relaţie de ordine totala, iar cheile de identificare sunt întotdeauna distincte. Acestea pot
reprezenta la rândul lor spre exemplu persoane, rolul de cheie putând fi jucat de codul
numeric personal, iar printre alte câmpuri figurând numele, adresa, numărul de telefon etc.
Pentru construirea unui arbore binar vom avea nevoie în primul rând de o clasă Nod prin care
să reprezentăm nodurile arborelui. Acestea conţin atât informaţii care reprezintă atât obiectele
memorate cât şi referinţe către cei doi fii. Un exemplu este:
class Node
{
public int iData; // informaţia utilizată cu
// rol de cheie
public float fDate // alte informaţii
Node leftChild; // fiul stâng
Node rightChild; // fiul drept
public void displayNode ()
{
System.out.print(‘{’);
System.out.print(iData);
System.out.print(“,”);
System.out.print(fData);
System.out.print(“}”);
}
}
Vom avea apoi nevoie de o clasă care să reprezinte arborele însusi: acesta va fi
obiectul care va conţine toate nodurile. Un astfel de obiect are nevoie de un singur câmp: o
variabila de tip Node care sa conţină rădăcina arborelui. Nu avem nevoie de câmpuri pentru
celelalte noduri deoarece acestea sunt accesibile pornind din rădăcină. Această clasă va
conţine şi diferite alte metode printre care void find (int key) – pentru căutare; void insert (int
id, float dd) – pentru inserare; void delete (int id).
40
Cândea I. Ovidiu-Radu Algoritmi de căutare
class Tree
{
private Node root; //singurul câmp al clasei Tree
public Node find (int key)
{
..............................
}
public void insert (int id, float dd)
{
.............................
}
public void delete (int id)
{
.............................
}
// diferite alte metode
}
În continuare va fi prezentată metoda Java care permite căutarea unui nod într-un
arbore returnând apoi la sfarşit nodul în care a fost găsită cheia sau null dacă cheia nu se
gaseşte în arbore:
Aceasta metodă utilizează variabila current pentru a memora nodul curent examinat.
Parametrul key este valoarea căutată. Metoda începe căutarea din rădăcina arborelui (de fapt
numai de acolo se putea deoarece rădăcina este singurul nod accesibil), vedem deci că
valoarea iniţială a lui current va fi chiar nodul rădăcină root.
41
Cândea I. Ovidiu-Radu Algoritmi de căutare
În ciclul while se compară valoarea key cu câmpul iData (cheia) din nodul curent.
Dacă valoarea key este mai mică decât acest câmp, current va avansa în arbore la fiul stang al
nodului curent. Dacă însa valoarea key este mai mare sau egală cu câmpul iData al nodului
curent, current va avansa la fiul drept al acestuia.
Dacă valoarea lui current devine null, înseamnă că nu mai putem avansa în arbore; am
epuizat orice posibilitate de a găsi elementul dorit, deci acesta nu există. Vom întoarce aşadar
valoarea null, pentru a indica aceasta situatie.
Obsevăm însă că ciclul while se poate termina şi atunci când condiţia sa nu mai este
satisfăcută, deci când câmpul iData al lui current este egal cu valoarea key, ceea ce înseamnă
că am gasit elemntul dorit. Vom întoarce în acest caz nodul respectiv astfel încât metoda care
a apelat find () să poată prelucra toate datele pe care acesta le conţine.
Dupa cum se poate observa, timpul necesar pentru găsirea unui nod depinde de
numărul de niveluri parcurse până când acesta a fost gasit.
Într-un arbore complet, aproximativ jumatate de noduri se afla pe ultimul nivel, mai
exact pe ultimul nivel vor fi cu exact un nod mai mult decat în tot restul arborelui. Prin
urmare aproximativ jumătate din operaţiile de căutare vor viza un nod de pe ultimul nivel.
(Un alt sfert dintre operaţii vor implica accesul la un element de pe penultimul nivel).
Pe parcursul unei căutări, vom vizita câte un nod de pe fiecare nivel; prin urmare,
durata necesară operaţiei de căutare va fi direct proporţională cu numărul de niveluri.
Presupunând că arborele este complet, vom avea:
42
Cândea I. Ovidiu-Radu Algoritmi de căutare
-----------------------------------------------------------------------
1.073.741.824 noduri --------------------------------30 niveluri
Observăm deci că pentru a căuta unul din cele 1.073.741.824 noduri ale unui arbore
binar de căutare complet vom avea de parcurs doar 30 de niveluri deci vom avea de făcut doar
30 de comparaţii.
N=2L-1
N+1=2L
L=log2(N+1)
Prin urmare, timpul necesar pentru executarea operaţiei de căutare într-un arbore binar
este proporţional cu logaritmul binar al lui N. Utilizând notaţia O, putem afirma că operaţia de
căutare durează un timp de ordinul O(log N).
Dacă arborele nu este plin (complet), analiza se complică. Se observă însă că pentru
un arbore cu un anumit număr de niveluri, timpul mediu de căutare este mai scurt în cazul în
care arborele nu este plin, datorita faptului că se efectuează mai puţine căutări în nivelurile
inferioare.
43
Cândea I. Ovidiu-Radu Algoritmi de căutare
drept ai fiecărui nod să nu aibă înalţime mai mare unul decat celalat o unitate). Arborii AVL
se obţin prin aplicarea anumitor reguli operaţiilor de inserare şi de ştergere a unui nod într-un
arbore. Operaţia de căutare nu va fi schimbată deci nu va fi necesar să o mai analizăm.
Un arbore se poate implementa şi sub forma unui tablou în interiorul căruia poziţiei
fiecărui nod îi va corespunde poziţiei în arbore. Astfel nodul cu indicele 0 este rădăcina
arborelui, cel cu indicele 1 este fiul stang al radacinii ş.a.m.d., mergând de la stânga spre
dreapta pe fiecare nivel al arborelui.
Celulele care reprezinta poziţii din arbore în care nu se afla noduri vor conţine
valoarea 0 sau null.
Cu această metodă, fii şi părintele unui nod oarecare se pot determina cu ajutorul unei
expresii simple pornind de la indicele nodului curent în tablou. Dacă indicele unui anumit nod
are valoarea index, atunci fiul său stâng va avea indicele 2*index+1, iar cel drept 2* index+2,
în timp ce părintele are indicele (index-1)/2, unde operatorul “/” semnifică împarţirea întreagă,
fără considerarea restului (div în Pascal).
De cele mai multe ori tabloul în care este memorat arborele binar nu este ordonat şi se
poate face căutarea atât ca într-un vector (ceea ce nu va duce la o eficienţă prea mare) cât şi ca
într-un arbore.
44
Cândea I. Ovidiu-Radu Algoritmi de căutare
45
Cândea I. Ovidiu-Radu Algoritmi de căutare
Această metodă find() va apela mai întâi hashfunc() care nu este altceva decât
funcţia de dispersie care ne va transforma cheia de căutare, obţinând indicele hashval.
În cazul de faţă funcţia de dispersie, respectiv metoda hashfunc() aplică operatorul %
între cheia de căutare şi dimensiunea tabloului. Acest operator “modulo” se va constitui
ca o funcţie eficientă de dispersie în cazul variabilelor numerice.
Într-un ciclu while, metoda find() verifică apoi dacă celula din tablou cu indicele
hashval este sau nu ocupată (dacă nu este ocupat, are valoarea null). Dacă această celulă
este ocupată se va verifica dacă elementul din celulă are sau nu aceeaşi valoare cu cheia
de căutare. În fine, dacă elementul nu se afla în celula curenta, find() va incrementa
indicele hashval şi va relua ciclul while, verificând dacă urmatoarea celulă este sau nu
ocupată.
47
Cândea I. Ovidiu-Radu Algoritmi de căutare
48
Cândea I. Ovidiu-Radu Algoritmi de căutare
49
Cândea I. Ovidiu-Radu Algoritmi de căutare
Înlănţuire separată
Reprezintă o abordare diferită a noţiunii de tabelă de dispersie. Astfel fiecare
celulă din tabelă va conţine câte o listă înlanţuita. Cheia unui anumit element este
transformată într-un indice prin aplicarea funcţiei de dispersie, elementul fiind apoi
inserat în lista înlănţuită din celula cu indicele calculat anterior. Celelalte elemente,
50
Cândea I. Ovidiu-Radu Algoritmi de căutare
cărora le va corespunde acelaşi indice, vor fi inserate în aceeasi lista; nu va fi deci nevoie
de căutarea unei celule libere în tabela.
Înlănţuirea separată este mai simplă din punct de vedere conceptual decât
adresarea deschisă. Programul va fi însa ceva mai lung, deoarece trebuie să conţină şi
primitivele de lucru cu liste, de regulă grupate într-o clasa suplimentară.
Coeficientul de încărcare are valori diferite faţa de cazul adresării directe. În
metoda înlănţuirii separate, este normal ca o tabelă cu N celule să conţină cel putin N
elemente; coeficientul de încărcare va avea deci valori supraunitare. Această încărcare
nu reprezintă însă o problemă. Desigur în momentul în care listele conţin multe
elemente, timpul de acces creşte, din cauză că va trebui să fie parcursă întreaga listă
până la elementul ce se vrea a fi accesat.
Găsirea celulei iniţiale se efectuează într-un timp foarte scurt O(1), dar căutarea
printr-o lista înlănţuită necesită un timp proporţional cu numărul de elemente din lista
O(M). Prin urmare în cadrul procesului de căutare este de preferat ca listele tabelei de
dispersie să nu fie prea încărcate.
În schema adresării directe, performanţele se degradează foarte mult când
coeficientul de încărcare depaşeşte valoarea de 2/3. Înlănţuirea separată permite o
creştere a acestui factor la valori supraunitare, fară a afecta prea mult performanţele
tabelei de dispersie. Din acest motiv, înlănţuirea separată reprezintă o metodă robustă,
în special atunci când numărul elementelor inserate în tabela este greu de anticipat.
Valorile multiple sunt permise şi pot fii generate în procesul de umplere a tabelei.
Toate elementele cu o aceeaşi cheie vor fii inserate în aceeaşi listă. Pentru a le descoperi
este necesar să parcurgem întreaga listă ceea ce conduce la o scădere a performanţelor.
Dimensiunea tabelei nu mai trebuie să fie în acest caz număr prim cum se
recomandă în cazul sondajului pătratic şi dublei dispersii. Nemaiexistând sondaje a
dispărut şi pericolul ca un sondaj să între intr-o bucla infinită, din cauză că dimensiunea
tabelei este divizibilă cu pasul de deplasare.
O altă posibilitate, similară înlănţuirii separate este ca fiecare celulă din tabelă să
conţina un tablou (galeţi sau buckets) în locul listei înlănţuite. Aceasta soluţie este mai
ineficienta decât cea care utilizează liste, din cauza necesitatii de a stabili dimensiunea
tablourilor. Dacă acestea sunt prea mici capacitatea lor ar putea fi depăşită, iar dacă
sunt prea mari determină un consum inutil de memorie. Listele înlănţuite care alocă
memoria în mod dinamic evită acest dezavantaj.
51
Cândea I. Ovidiu-Radu Algoritmi de căutare
52
Cândea I. Ovidiu-Radu Algoritmi de căutare
53
Cândea I. Ovidiu-Radu Algoritmi de căutare
54
Cândea I. Ovidiu-Radu Algoritmi de căutare
aceste două structuri sunt utilizate într-un mod total diferit una faţă de alta (spre deosebire de
arbore grafurile nu sunt folosite ca suport de memorare pentru date ci pentru a ajuta la
rezolvarea unor anumite probleme de regulă particulare).
Din punct de vedere matematic un graf neorientat poate fi privit ca o pereche
G=(X,U) unde:
X este o mulţime finită nevidă a cărei elemente se numesc vârfuri sau noduri ale grafului.
U este o mulţime de perechi neordonate (submulţimi cu două elemente), fiecare din aceste
mulţimi reprezentând o muchie a grafului
Deci implicit dacă avem u aparţine lui U cu u=[x,y] atunci aceasta înseamnă ca există
muchia xy (notata cu u) unde atât x cât şi y sunt elemente din mulţimea X a nodurilor
grafului.
Din punct de vedere geometric un graf poate fi privit ca o figură plana în care fiecărui
vârf I se asociază un punct şi fiecărei muchii [x,y] o linie curbă care uneşte punctele ce
corespund vârfurilor x,y.
Exemplu:
Fie G=(X,U)
U={(1,2);(2,3);(1,4);(4,2)}
X={1,2,3,4}
1 2
1 3
3 4
Fig.1
! Dacă
într-un graf
neorientat G=(X,U) există muchia [x,y] atunci va exista şi muchia [y,x] (avem deci de-a face
cu o relaţie de simetrie). Acest lucru nu este însă adevărat şi pentru grafurile neorientate.
Graful desenat în exemplul de mai sus este un graf neorientat. Aceasta înseamnă ca ne
putem deplasa pe ele în orice sens. Dacă însă eram restricţionaţi la o deplasare de-a lungul
unei muchii numai într-un anumit sens atunci am fi avut de-a face cu un graf orientat (din
55
Cândea I. Ovidiu-Radu Algoritmi de căutare
punct de vedere grafic această restricţionare ar fi fost reprezentată de regulă printr-o săgeata
aflată la sfârşitul muchiei).
Rolul grafului este de a prezenta relaţiile dintre muchii şi vârfuri, cu alte cuvinte
incidenţa muchiilor pentru toate vârfurile grafului.
Vom spune că două vârfuri sunt adiacente dacă sunt conectate direct printr-o muchie.
O alta noţiune des folosită este aceea de drum care nu este altceva decât o secvenţă de
muchii. Între două vârfuri date pot exista două sau mai multe drumuri. Dacă un drum va avea
toate muchiile diferite două câte două şi se va termina în acelaşi vârf din care a început,
acesta va purta numele de ciclu. Ciclul se va numi elementar dacă oricare două vârfuri ale sale
cu excepţia primului şi al ultimului sunt diferite două câte două .
Un graf se numeşte conex dacă exista cel puţin un drum de la fiecare nod până la
fiecare alt nod. Aşadar este absolut clar că un graf conex va fi alcătuit doar din componente
conexe.
Memorarea grafurilor
Într-un program extrem de abstract, vârfurile vor fi pur şi simplu numerotate cu indici
de la 0 la N-1 şi memorate într-un tablou putând fi mai apoi referite prin indicele
corespunzător fiecăruia în parte. (N este numărul vârfurilor) în majoritatea situaţiilor însă un
vârf este un obiect din lumea reală, care va fi descris de obicei ca un obiect structurat
aparţinând unei clase predefinite continând toate informaţiile necesare.
Pentru simplitate în exemplele prezentate aici fiecare vârf va cuprinde doar o
etichetă, label de tip char prin care este identificat. Aşadar astfel va arăta clasa Vertex, clasa la
care aparţin toate vârfurile grafului:
class Vertex
{
public char label; // etichetă
public boolean wasVisited;
public Vertex (char lab) // constructor
{
label=lab;
wasVisited = false;
}
}
56
Cândea I. Ovidiu-Radu Algoritmi de căutare
Aşadar memorarea vârfurilor este foarte simplă folosindu-se pentru aceasta un simplu
tablou. Cum se vor memora însă muchiile ? După cum se poate observa un graf nu are o
organizare atât de riguroasă ca a unui arbore. Spre exempu într-un arbore binar, fiecare nod
avea cel mult doi fii aşadar ne era uşor să facem două referiri la nodurile fii. Într-un graf însă
fiecare vârf poate fi conectat cu un număr arbitrar de alte vârfuri.
Pentru modelarea acestui tip de organizare liberă, se preferă o altă reprezentare a
muchiilor fată de cazul arborilor. Există două modalitaţi de reprezentare folosite mai
frecvent.
a) Prima modalitate constă în precizarea numărului N al vârfurilor şi a matricei de
adiacentă a grafului, care este o matrice pătratică de ordinul N având elementele a ij=1 dacă
(i,j) aparţin lui U sau 0 în caz contrar.
! O observaţie care merită a fi făcută este ca matricea de adiacentă va fi simetrică în
cazul în care graful este neorientat.
Exemplu:
Pentru graful din figura următoare
1 3 4 5
2 6
Fig. 2
0 1 1 0 0 0
1 0 1 0 0 1
1 1 0 0 0 1
A =
0 0 0 0 1 1
0 0 0 1 0 1
0 1 1 1 1 0
57
Cândea I. Ovidiu-Radu Algoritmi de căutare
L1 = {2,3}
L2 = {1,3,6}
L3 = {1,2,6}
L4 = {5,6}
L5 = {4,6}
L6 = {2,3,4,5}
Pentru adăugarea unui vârf la un graf, se va crea un nou obiect de tipul Vertex, care se
va insera în tabloul de vârfuri vertexList. Adăugarea unei muchii de la vârful al i-ilea la cel al
j-lea să zicem (i,j) se va face punând aij = 1 (şi aji=1 dacă graful e neorientat) în matricea
de adiacentă sau punându-se vârful j în lista de adiacentă a vârfului al i-ilea (şi vârful i în
lista de adiacentă a elementului al j-lea în cazul unui graf neorientat).
Mai jos va fi prezentată o formă simplificată a unei clase de tip Graph, ale cărei
metode permit atât crearea unei liste de vârfuri şi a unei matrice de adiacentă, cât şi adăugarea
vârfurilor şi muchiilor la un obiect de tipul Graph:
class Graph
{
private final int MAX_VERTS=20;
private Vertex vertexList[]; // lista vârfurilor
private int adjMat[][]; // matricea de adiacentă
private int nVerts; // numărul current de vârfuri
//-----------------------------------------------------------
public Graph() // constructor
{
// se alocă matricea de adiacentă şi tabloul vârfurilor
vertexList = new Vertex[MAX_VERTS];
adjMat = new int[MAX_VERTS][ MAX_VERTS];
nVerts = 0;
for (int j = 0; j < MAX_VERTS; j++)
for (int k = 0; k < MAX_VERTS; k++)
adjMat[j][k] = 0; // iniţializarea matricii de adiacenta cu 0
}
//-----------------------------------------------------------
public void addVertex(char lab)
{
vertexList[nVerts++] = new Vertex(lab);
}
//-----------------------------------------------------------
public void addEdge(int start,int end)
{
adjMat[start][end] = 1;
adjMat[end][start] = 1; // lipseşte dacă graful e orientat
}
//----------------------------------------------------------
public void displayVertex (int v)
{
System.out.println(vertexList[v].label);
58
Cândea I. Ovidiu-Radu Algoritmi de căutare
}
//-----------------------------------------------------------
}// sfârşitul clasei Graph
Parcurgerea grafurilor
Prin parcurgerea unui graf se înţelege examinarea sistematică a vârfurilor sale, plecând
dintr-un vârf dat i, astfel încât fiecare vârf accesibil din i să fie atins o singură dată (aici este
vorba de existenţa unei muchii între vârful i şi un altul). Trecerea de la un vârf y la un altul se
face prin explorarea într-o anumită ordine, a vecinilor lui y, adică a vârfurilor cu care nodul y
curent este adiacent. Aceasta acţiune numită şi vizitare a vârfurilor are scopul de a prelucra
informaţia asociată vârfurilor. În cazul unei căutari este necesară o simplă parcurgere în
graful pe care se lucrează facându-se la fiecare pas o comparaţie între cheia căutată şi
informaţia memorată în nodul curent.
Orice metodă de parcurgere a grafurilor am folosi va trebui în fiecare moment să ştim
dacă vârful curent mai are vârfuri adiacente nevizitate. Această operaţie este implementata
prin metoda getAdjUnvisitedVertex():
// întoarce un vârf nevizitat, adiacent cu v sau dacă acesta nu există întoarce -1
59
Cândea I. Ovidiu-Radu Algoritmi de căutare
7 10
4 6
8
3 5 9
Fig. 3
{
int v = getAdjUnvisitedVertex(theStack.peek());
if (v = = -1) theStack.pop(); // dacă nu există un astfel de vârf se extrage unul din stivă
else
{
vertexList[v].wasVisited=true;
displayVertex(v);
theStack.push(v);
}
}//sfârşit while
for (int j=0;j<nVerts;j++) vertexList[j].wasVisited=false; // restabilim indicatorii ca la început
}
60
Cândea I. Ovidiu-Radu Algoritmi de căutare
lăţime, pe de altă parte, algoritmul va rămâne cât mai aproape de punctul de pornire.
Algoritmul vizitează toate vârfurile adiacente cu acesta şi numai după aceea se avântă cu un
pas mai departe. Acest algoritm este implementat cu ajutorul unei cozi în locul stivei.
Pentru simplitate în momentul parcurgerii în lătime vom urma regulile:
Regula 1: Vizităm următorul vârf nevizitat (dacă există un astfel de vârf ) adiacent cu cel
curent, îl marcăm şi-l introducem în coadă
Regula 2: dacă nu putem aplica regula 1, din cauză că nu există vârfuri nevizitate,
ştergem un vârf din coadă (dacă este posibil), nodul şters devenind nodul curent
Regula 3: Atunci când nu putem aplica niciuna din regulile anterioare parcurgerea s-a
terminat
Pentru exemplul din Fig. 3 în urma unei parcurgeri în lăţime ordinea de parcurgere a
nodurior va fi: 1, 2, 7, 10, 3, 4, 6, 5, 8, 9.
Codul metode de parcurgere a grafului în lăţime este:
61
Cândea I. Ovidiu-Radu Algoritmi de căutare
căutată. Aşadar ordinul de complexitate a unei căutari în vârfurile dintr-un graf va fi acelaşi
cu ordinul de complexitate al algoritmului de parcurgere folosit.
CAPITOLUL 3
CĂUTAREA PE INTERNET
Bazat pe hipertext, spaţiul WWW încearcă oferirea unui mecanism facil de a stoca şi
de a pune, ulterior, la dispoziţie informaţii, într-o manieră intuitivă, nesecvenţială.
Studiile recente (efectuate la mijlocul anului 2000) estimează volumul Web-ului ca
fiind de 1-2 miliarde de pagini, utilizatorii care petrec mai mult de cinci ore pe Internet
alocând 70% din timp pentru căutarea de informaţii. Între 85% şi 90% dintre utilizatori se
bazează pe motoarele de căutare pentru a localiza resursele dorite.
Astfel, importanţa acestora se dovedeşte de necontestat, în prezent existând o
multitudine de căutătoare şi meta-căutătoare, generale sau specializate.
Motoarele de căutare pot oferi servicii de căutare pe bază de indecşi (i.e. Altavista) sau
pe baza unor ierarhii de termeni – aşa-numitele servicii director (Yahoo). În ultima perioadă,
aceste servicii au devenit hibride (primul care a adoptat tehnicile mixte fiind Excite).
Regăsirea informaţiilor
62
Cândea I. Ovidiu-Radu Algoritmi de căutare
menţinerea legăturilor
Legăturile fiind stocate în interiorul documentelor, nu există posibilitatea de a adăuga
adnotări sau de a modifica legăturile dintr-o pagină fără a fi proprietarul acesteia. Menţinerea
integrităţii legăturilor pentru site-uri Web conţinând un număr foarte mare de documente se
dovedeşte dificilă. De cele mai multe ori se utilizează programe de verificare a disponibilităţii
63
Cândea I. Ovidiu-Radu Algoritmi de căutare
legăturilor şi de construire a ierarhiilor de legături între paginile unui site Web (e.g. aplicaţia
ICE scrisă în Perl).
În principiu, un motor de căutare este constituit din trei componente de bază:
robot Web
index
ranking mechanism
Robot Web = o aplicaţie numită robot Web (spider, crawler), care parcurge spaţiul
WWW şi vizitează anumite pagini, extragând informaţii despre ele care vor fi păstrate pe
serverul sau serverele motorului de căutare, într-o bază de date sau index.
De cele mai multe ori, fiecare motor de căutare are propriul robot Web care respectă
standardul de excludere a roboţilor.
1.Se accesează un fişier text numit robots.txt stocat în directorul de rădăcină a unui
server WWW de către robotul de explorare, acest fişier specificând ce părţi vor fi evitate de la
parcurgerea automată, acest lucru făcând posibilă excluderea roboţilor din zone Web lipsite de
relevanţă.
Un exemplu de astfel de fişier este:
În vederea evitării indexării conţinutului unei pagini Web se poate scrie în antetul ei
(între etichetele <head> şi </head>):
<meta name = “robots” content =“noindex”>
Anumiţi roboţi pot utilizează tag-uri specifice (ascunse) care să dicteze un anumit
comportament pentru aceea pagină (aşa cum se întâmplă în cadrul programului de oglindire
Teleport)
2. Prin traversarea unui număr mare de hiperlegaturi, roboţii necesită o lărgime bună de
bandă, deoarece ei pot opera continuu perioade lungi de timp (săptămâni sau chiar luni).
64
Cândea I. Ovidiu-Radu Algoritmi de căutare
Pentru a accelera aceste operaţii mulţi roboţi au implementate tehnici de extragere paralelă a
datelor, metoda denumită operare în foc rapid (rapid fire), rezultând un trafic considerabil (o
încetinire temporară a traficului de date). Mai mult, serverele Web pot fi supraîncărcate de
cereri multiple de accesare venite din partea roboţilor în detrimentul cererilor agenţilor-
utilizator. Aşadar implementarea roboţilor permiţând foc rapid trebuie evitată.
3. Implementări eronate pot determina roboţii să intre în arii infinite numite găuri negre
(de exemplu atunci când un document are o legătură care se referă la el însuşi, iar programul,
robotul, nu realizează lucrul acesta).
4. Un alt aspect care trebuie luat în considerare este timpul de actualizare a bazelor de
date ale motoarelor de căutare folosind pentru descoperirea resurselor roboţii Web. Roboţii
de căutare a informaţiilor vor trebui aşadar să decidă care informaţii sunt importante a fi
transmise programelor de indexare.
5. Roboţii nu trebuie să acceseze tipuri de date fără relevanţă, având dimensiuni
considerabile cum ar fi arhive, fişiere executabile, fişiere multimedia etc.
Ca exemple de roboţi de căutare aş putea menţiona pe cei de la Excite (Inktomi), Go
(Infoseek) şi Lycos care însă nu pot indexa paginile conţinând cadre, iar cei de la FAST şi
Google au probleme cu hărţile de imagini senzitive (care pot îngloba legături spre alte
documente)
Majoritatea roboţilor de căutare pentru a accelera procesul de traversare a legăturilor
posedă o tabelă internă de perechi (adresa simbolică, adresa IP) evitând interogările DNS
(Domain Name System) care sunt prea lente.
Căutarea se realizează folosind algoritmi de parcurgere locală de tip DFS sau BFS sau
prin parcurgerea într-o ordine inteligentă a legăturilor spre alte documente.
multe ori acestea sunt comprimate (Google foloseşte biblioteca de compresie bzip) utilizându-
se clustere pentru memorarea lor.
Fiecare pagină va primi un identificator docID stabilit pe baza adresei sale URL.
Aceste adrese vor fi normalizate conform următorului algoritm:
Indecşii pot cuprinde indecşi de text obişnuit, dar şi indecşi ai metadatelor extrase
(construiţi pe baza docID-urilor şi a altor informaţii ). În mod uzual se folosesc tehnici bazate
pe tabele de dispersie multiple şi pe sortarea eficientă a indecşilor.
66
Cândea I. Ovidiu-Radu Algoritmi de căutare
Exemplu:
< meta name = “description” content = “Sportivii români de la Olimpiada de la Atena”> // descrierea succintă
< meta name = “keywords” content = “Badea, tricolori, Szekely, campion ”> // cuvinte cheie
< meta name = “author” content = “Cândea Radu” > // autorul
< meta name = “owner” content = “Cândea Radu”> // proprietarul
acces direct (random acces) = se realizează pe baza identificatorului asociat fiecărei pagini
acces bazat pe interogări (querry-based acces) = în acest caz se va formula o interogare
care să se refere la diverse atribute a metadatelor cum ar fi autor, locaţie, titlu, etc., iar în urma
acestei interogări vor fi furnizate toate documentele care satisfac cererea formulată.
acces flux de date (streaming acces) = este folosit atunci când din depozitul de date se
extrage un grup de pagini pentru a fi trimis ca flux de date spre o anumită aplicaţie.
Pentru asigurarea scalabilităţii, depozitul de date poate fi distribuit, constituindu-se o
colecţie de noduri de stocare interconectate, controlul realizându-se prin intermediul unui
server de management al nodurilor. Acest server menţine o tabelă cu starea fiecărui nod de
stocare: capacitatea totală de memorare, spaţiul liber, nivelul fragmentării datelor, numărul şi
tipul de accesări, modul de operare a nodului etc.
67
Cândea I. Ovidiu-Radu Algoritmi de căutare
paginilor plus alte informaţii sunt returnate clientului-utilizator care a formulat cererea.
Utilizatorul va decide care pagină sau grup de pagini este conform cu preferinţele sale.
Nu toate paginile vor avea aceeaşi importanţă pentru robot, aici intervenind mai
mulţi factori fiecare dintre aceştia având o anumită pondere. Se pot lua în calcul:
similaritatea cu posibilele cereri ale utilizatorilor, numărul legăturilor spre
pagina analizată, relevanţa paginii (conţinutul ei), numărul legăturilor pe care îl
are pagina respectivă precum şi metrica locaţiei (o pagină din domeniul “.com”
se consideră a fi mai importantă decât una a domeniului “.ro”)
Paginile vor fi ordonate conform acestor criterii şi vor fi indexate doar primele,
numărul acestora depinzând de capacităţile indexului.
În cadrul depozitului de date este de dorit să se memoreze cea mai recentă versiune a
paginilor traversate de roboţii Web. Trebuiesc avute în vedere aspecte importante precum
consistenţa indecşilor şi eliminarea vechilor pagini care nu mai există pe Web. Astfel pentru
fiecare pagină pot fi ataşate două valori numerice una pentru a specifica timpul de viaţă
permis (diferit de la un motor de căutare la altul) şi alta în care se va contoriza efectiv trecerea
timpului. Timpul de viaţa permis reprezintă perioada de stocare a unui document fără a
necesita ştergerea sau reactualizarea sa din depozit în timp ce contorul este decrementat
periodic până când devine nul, moment în care robotul Web va trebui să aducă noi informaţii
despre acea pagină.
Mai mult, serverul controlează cererile pentru accesări de tip flux de date şi maniera
de stocare a paginilor noi colectate de roboţi. Distribuţia în cadrul nodurilor se face uniform
sau printr-o metodă de tip hash iar organizarea se va realiza tot printr-o metodă de tip hash,
prin fişiere de jurnalizare sau printr-o metodă mixtă. În cazul actualizării se iau în considerare
scheme bazate pe actualizări secvenţiale (batch) ori incrementale. La actualizarea secvenţială
nodurile se partiţionează în două categorii: noduri de actualizare (nu vor mai fi folosite pentru
cereri de acces la pagini) şi noduri de citire. Astfel, se evită apariţia conflictelor între
operaţiunile executate asupra depozitului de date. La actualizarea incrementală nu se mai face
distincţie între noduri, fiecare nod fiind permanent operaţional, cu penalizări de performanţă
şi modificare dinamică a indecşilor locali.
68
Cândea I. Ovidiu-Radu Algoritmi de căutare
Pentru sporirea eficienţei, nodurile pot conţine pagini grupate pe diverse criterii cum ar
fi: tematică, autor, localizare, cuvinte-cheie etc.
Orice motor de căutare are o interfaţă de căutare (denumită frecvent motor de căutare)
care reprezintă o componentă importantă a sistemului, oferind în funcţie de motor posibilităţi
de formulare a cererilor prin intermediul diverşilor operatori logici, în limbaj natural,
explorând ierarhii de domenii catalogate (directoare Web), alegând localizarea paginilor etc.
Dacă vom dori ca paginile să conţină măcar unul din doi termeni vom avea: “termen1
OR termen2”
În sfârşit dacă vom dori ca paginile să conţină cei doi termeni localizaţi la mică
depărtare unul de celălalt (vecinătate de 20-2000 de cuvinte, în funcţie de motorul de căutare )
vom folosi următoarea structura: “termen1 NEAR termen2”.
69
Cândea I. Ovidiu-Radu Algoritmi de căutare
Meta-căutătoare
Prin apelarea unui meta-căutător se pot formula interogări în urma cărora se vor primi
rezultate de la o multitudine de motoare de căutare cărora li se va transmite respectiva cerere.
Funcţia principală a metacăutătorului este aceea de a compila toate listele de pagini rezultate
de la motoarele obişnuite (care sunt interogate în paralel) şi de a prezenta utilizatorului cele
mai relevante documente găsite.
De cele mai multe ori un meta-căutător are implementat propriul sistem de evaluare a
relevanţei paginilor, în funcţie de anumite criterii (ex: Mamma). De asemenea, un meta-
căutător poate oferi o vedere ierarhizată, de genul directoarelor Web, a paginilor găsite.
Nu toate meta-căutătoarele elimină duplicatele (ex: Dogpile). O serie de meta-
căutătoare sunt direcţionate spre domenii specifice, pentru căutarea de:
fişiere – în general aplicaţii şi documentaţii (ex: Filez, FTPSearch)
adrese e-mail şi numere de telefon (ex: Four11)
informaţii în cadrul grupurilor de discuţii (ex: DejaNews)
fişiere audio (ex: MP3Search)
imagini (ex: MetaSEEk)
70
Cândea I. Ovidiu-Radu Algoritmi de căutare
Deoarece fiecare motor în parte va returna o listă de pagini într-un format propriu,
meta-motorul de căutare va realiza şi o convertire la un format comun care în final va fi
prezentat clientului.
71
Cândea I. Ovidiu-Radu Algoritmi de căutare
Acum când am ajuns în sec XXI, Internet-ul reprezintă una din cele mai mari resurse
pentru informaţii iar găsirea informaţiilor necesare într-un domeniu atât de vast cum este
Internetul poate deveni o adevărată problemă. O problemă care însă se poate rezolva cu
ajutorul motoarelor de căutare, acestea reprezentând cea mai uşoară cale de găsire a
informaţiilor pe Web constituindu-se totodata ca niste importante instrumente de acces rapid
la informaţie.
CAPITOLUL 4
PREZENTAREA APLICAŢIEI
O direcţie de studiu convergentă cu sortarea este cea a căutarii după cheie, cu alte
cuvinte a regăsirii informatiilor (engl. information retrieval).
Pentru căutarea unei înregistrari într-o structură de date (vector, mulţime, listă, arbore,
tabel de dispersie, graf, sau chiar bază de date) trebuie ca unul dintre câmpurile înregistrării
să reprezinte o cheie. Se va căuta astfel o înregistrare cu o cheie bine precizată. Într-un
program complex, orice câmp poate fi ales să reprezinte o cheie. Vom observa astfel că pentru
fiecare structură de date în particular vor exista ceva diferenţe pentru executarea unor căutări
după cheie, mai ales privind parcurgerea acelei structuri.
72
Cândea I. Ovidiu-Radu Algoritmi de căutare
Prima problemă care s-a pus odată cu începerea lucrului la acest program a fost
găsirea celui mai eficient mod de a popula structurile cu date precum tipul datelor ce aveau să
fiefolosite.
Deoarece datele de tip numeric sunt cele mai uşor de manevrat, iar cele de tip String
sunt cel mai des folosite în programare am ales o soluţie de mijloc şi anume am folosit pentru
popularea structurilor nişte obiecte care conţin în structura lor atât date de tip int cât şi String.
Inserarea datelor de la tastatură în timpul rulării programului mi s-a părut a fi cea mai
proastă idee de aceea a rămas de ales între a prelua informaţiile pentru popularea bazei de date
dintr-un fişier extern sau de ce nu dintr-o bază de date. Ultima variantă mi s-a părut cea mai
interesantă aşa că am creat în Access o bază de date simplă având un singur tabel cu
câmpurile ID, Societate, Localitate, Strada, Nr, Domeniu în care am introdus datele
specificate prin numele câmpurilor pentru aproximativ 600 de firme din judeţul Sibiu.
La crearea structurii de date pentru testarea algoritmilor de căutare are loc un proces
aleator de generare a 20 de numere într-un anumit interval (20 de înregistrări mi s-au părut
relativ suficiente pentru operaţiile ce mi le-am propus pe structurile respective de date, dar
acest număr nu constituie o limitare a programului, el putând fi oricând înlocuit cu o altă
valoare mai mare). Intervalul acesta în care poate lua valori numerele aleatoare este de la 0 la
numărul de înregistrări a bazei de date folosite. Folosind interfaţa ODBC pentru lucrul cu
bazele de date, printr-o instrucţiune SQL de tip SELECT se extrag din baza de date 20 de
înregistrări, fiecare înregistrare fiind memorată într-o instanţă a clasei Link.
Această instrucţiune SQL returnează toate câmpurile înregistrării din tabelul “Tabel”
care respectă condiţia ca în dreptul câmpului ID să fie o valoare egală cu numărul aleator
“nraleator” generat mai devreme.
73
Cândea I. Ovidiu-Radu Algoritmi de căutare
Dacă cele 20 de numere generate aleator sunt diferite între ele atunci şi cele 20 de
înregistrări întoarse de către instrucţiunea SQL de mai sus vor fi diferite pentru că ID
repreyintă de fapt cheia primară a tabelului “Tabel”.
Aşadar fiecare din structurile de date vor avea câte o metodă insert() în care se
realizează legătura cu baza de date Societăţi aflate pe hardisk, se scot date pe baza unei
interogări SQL apoi, înregistrările aleator alese aşa cum am descris mai sus sunt mai apoi
utilizate la crearea unor obiecte de tip Link care vor popula structurile de date. Iată care este
structura acestei metode:
String url="jdbc:odbc:Societati";
String user="dba";
String password="sql"; // sunt folosite la crearea conexiunii cu baza de date Societăţi
Vector sir =new Vector();
int[] aleat=new int[20]; // array în care se vor înregistra numerele aleatoare
for (int i=0;i<MAX_ELEM;i++) // MAX_ELEM este o constantă egală cu 20
{
int n=(int)(java.lang.Math.random()*806);// baya de date are 806 înregistrări
aleat [i]=n;
} // în acest for aleat este umplut cu numere întregi din intervalul [ 1, 806] generate aleator
try{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); // iniţializarea driverului
}catch(ClassNotFoundException e)
{
e.printStackTrace();
System.out.println("Eroare incarcare driver!\n" + e);
}
try{
Connection c=DriverManager.getConnection(url, user, password); // cerearea conexiunii
Statement s= c.createStatement(); // crearea unui "flux" spre baya de date Societăţi
for (int i=0;i<MAX_ELEM;i++)
{
ResultSet r=s.executeQuery("SELECT * FROM Table1 WHERE ID="+aleat[i]+" ORDER BY ID");
/* în r se pune rezultatul interogării pe tabelul Table1 având criteriul de selecţie ID="+aleat[i]+" adică ID-ul (cheia
primară)să fie egal cu al i-ilea număr aleator generat din array-ul aleat */
while (r..next())
{
sir.add(r.getString("ID"));
sir.add(r.getString("Societate"));
sir.add(r.getString("Localitate"));
sir.add(r.getString("Strada"));
sir.add(r.getString("Nr"));
sir.add(r.getString("Domeniu"));
sir.add("\n");
}
sir.copyInto(b);
/* se copiază vectorul şir în arrayul b, a cărui elemente vor putea apoi fi folosite la crearea unui obiect de tip Integer în
grupul de instrucţiuni de mai jos */
try
74
Cândea I. Ovidiu-Radu Algoritmi de căutare
{
in1=new Integer(Integer. parseInt(b[0], 10));
in2=new Integer(Integer. parseInt(b[4], 10));
}catch(NumberFormatException v)
{
v.printStackTrace();
System.out.println("Eroare de transformare de tip parseInt()");
}
int id=in1.intValue();
societate=b[1];
localitate=b[2];
strada=b[3];
int nr=in2.intValue();
domeniu=b[5];
Link link = new Link(id,societate,localitate,strada,nr,domeniu);
// crearea unui obiect de tip Link
/*
Aici, nodul nou creat link va fi inserat în structura de date. Fiind inclus într-un ciclu for cu 20 de paşi structura va
avea 20 de noduri, conţinând obiecte de tip Link.
La fişier nu se vor mai crea obiectele de tip Link ci se vor insera pur şi simplu elementele vectorului b care sunt
de tip String în fişierul nou creat
La baza de date această întreagă metodă va lipsi deoarece baza de date în care se va efectua căutatrea este deja
creată şi populată în Microsoft Access
*/
System.out.println(i+".este nodul la care s-a ajuns");
sir.clear(); // vectorul sir este golit deoarece informaţia conţinută in el a fost deja folosită
}
s.close(); // se închide "fluxul"
}catch(SQLException e) {e.printStackTrace();}
catch (NullPointerException ex) {ex.printStackTrace();};
Aşa cum se poate constata din codul metodei insert(), structurile de date sunt populate
cu instanţe ale clasei Link aflată în fişierul Link.java a cărei cod este:
class Link
{
public Link right, left;
private int id, nr;
private String societate, localitate, strada, domeniu;
public boolean wasVisited;
75
Cândea I. Ovidiu-Radu Algoritmi de căutare
După cum se observă clasa Link conţine două atribute numerice (id şi nr)şi patru
atribute de tip String (societate, localitate, strada, domeniu). Celelalte atribute de tip Link (left
şi right)sunt folosite numai în cadrul listelor înlănţuite, iar cele de tip boolean
(wasVisited)sunt folosite doar la grafuri, pentru marcarea nodurilor prin care s-a trecut la o
parcurgere.
Există şi nişte metode publice (pot fi accesate şi din exteriorul clasei)care returnează
diferitele atribute ale obiectului precum şi o metodă care returnează întreg conţinutul
obiectului sub forma unui String care mai apoi poate fi citit pe o supafaţă de afişare (în cazul
programului “Căutări în structuri de date”, într-un textarea).
În ceea ce priveşte structura acestui program, avem de-a face cu un proiect Java
(Project.kpx)care conţine mai multe fişiere.java şi anume: Arbore_Binar.java, Array.java,
Baza_De_Date.java, Fişier.java, Graf.java, Lista_Înlanţuită.java, Tabela_De_Dispersie.java,
Fereastră.java, Link.java, Avertizare.java, Căutare.java şi Pseudocod.java.
În primele dintre fişiere (având numele unor structuri de date)se face implementarea
structurilor respective cu toate operaţiile caracteristice, inclusiv căutarea care constituie
subiectul acestei lucrări, precum şi construcţia a câte unei ferestre din cadrul căreia se va
putea lucra asupra structurii de date (interfaţa şi structura sunt implementate în clase diferite
aflate însă în acelaşi fişier.java).
Aşadar primele şapte fişiere.java menţionate conţin cel puţin două clase, una pentru
creareea interfeţei grafice iar cealaltă pentru lucrul cu structura de date (datorită unei mai mari
simplităţi la secţiunile Baza de Date şi Fişier, deci la structurile memorate extern structura a
fost implementată în cadrul clasei responsabilă cu interfaţa). Bineînţeles că vor exista unele
fişiere cum ar fi Graf.java care au implementate mai multe clase (pentru parcurgerea
grafurilor avem nevoie de o coadă sau o stivă, care fiecare vor avea clasa ei separată).
76
Cândea I. Ovidiu-Radu Algoritmi de căutare
Metoda efectivă de căutare este apelată prin apăsarea butonului ”Caută”. Această
metodă este membră a clasei pentru lucrul cu structura de date şi de obicei returnează către
clasa interfeţei un String care conţine rezultatele căutării. Dacă Stringul returnat este vid
atunci se creează o instanţă a clasei Avertizare (în clasa ferestrei)prin care ni se va comunica
că a avut loc o căutare nereuşită. Cheia după care caută este un şir de caractere introdus de la
77
Cândea I. Ovidiu-Radu Algoritmi de căutare
tastatură în cadrul textfieldului de unde este preluat în primă instanţă sub forma unui String
(aşadar toate metodele de căutare, indiferent de structura de date folosită primesc ca
parametru un String pe care eu l-am denumit searchkey). Apoi printr-o metodă care va fi
discutată ulterior acest String va putea fi transformat în int în cazul în care vor fi introduse
valori numerice.
Clasa Link aflată în fişierul Link.java a fost deja discutată şi prezentată ca şi cod.
Fişierul Avertizare.java are o singură clasă în care are loc construcţia unei mici
ferestre de dialog (de tipul celei care semnalizează o anumită eroare în Windows)având un
buton “OK” pentru închidere. Această fereastră dialog dependentă deci de fereastra părinte
apare atunci când are loc spre exemplu o căutare nereuşită, nu s-a introdus cheia de căutare
etc. Un exemplu de instanţă a acestei clase preluată din rularea programului “Căutare în
structuri de date”, mai precis în timpul în care era vizibilă fereastra din Lista_Înlanţuită.java
(fereastra părinte)este prezentată în continuare.
78
Cândea I. Ovidiu-Radu Algoritmi de căutare
79
Cândea I. Ovidiu-Radu Algoritmi de căutare
Ultimul fişier numit Pseudocod.java conţine tot o singură clasă Pseudocod în care se
creează o fereastră de dialog modală (va trebui închisă dacă se doreşte să se lucreze în
fereastra părinte)conţinând explicaţii despre algoritmii de căutare folosiţi în structura din a
cărei fereastră se apelează această fereastră dialog. Apelarea acestei ferestre se face prin
apăsarea butonului "Informaţii" aflat în partea din dreapta-jos a fiecăreia dintre ferestrele
structurilor chiar lângă butonul "Şterge" care curăţă fereastra de afişaj (aceste două butoane
sunt moştenite şi ele din clasa Fereastră).
Fereastra clasei Pseudocod.java conţine după cum se vede în imaginea de mai jos
(captură din timpul rulării programului în cadrul ferestrei arborelui binar de căutare)un buton
pentru închiderea ferestrei şi un JEditorPane numit tArea care este un obiect special în cadrul
căruia se vor puta încărca pagini html. În constructorul prin care este creată o instanţă a
acestei clase Pseudocod este transmis un parametru (opt)care face posibilă identificarea unui
fişier html aflat în directorul Fişiere. Trebuie făcută precizarea că există căte un fişier html în
care sunt explicaţii despre algoritmii de căutare pentru fiecare structură de date în parte. După
identificarea fişierului corespunzător acesta este încărcat în cadrul obiectului JEditorPane prin
comanda:
tArea.setPage(bookURL)
care primeşte ca parametru calea spre pagina html dorită (sau URL acestei pagini în cazul în
care aceasta se află pe Internet).
80
Cândea I. Ovidiu-Radu Algoritmi de căutare
getContentPane().setLayout(new BorderLayout());
// fixarea gestionarului de poziţie pentru elementele grafice ale ferestrei dialog
JEditorPane tArea = new JEditorPane();
// declararea şi iniţializarea suprafeţei de afişare pentru fisierele html cu explicaţii
JScrollPane areaScrollPane = new JScrollPane(tArea);
tArea.setEditable(false); // tArea devine needitabilă
areaScrollPane.setBorder(
BorderFactory.createCompoundBorder(
BorderFactory.createCompoundBorder(
BorderFactory.createTitledBorder("Info program"),
BorderFactory.createEmptyBorder(9,9,9,9)),
areaScrollPane.getBorder()));
/* datorită acestui JscrollPane atunci când textul depăşeşte dimensiunea suprafeţei de afişare vor apărea automat benzi pentru
derularea textului */
Dimension minimumSize = new Dimension(100,50);
areaScrollPane.setMinimumSize(minimumSize);
ok = new JButton("OK");
ok.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {dispose(); });
// adaptor pentru acţiunea butonului "OK"
String prefix = "file:" + System.getProperty("user.dir")+System.getProperty("file.separator");
try {
bookURL = new URL(prefix + fisier);
System.out.println(prefix + fisier);
}catch (java.net.MalformedURLException exc)
{
System.err.println("Incercare de a citi un URL gresit: " + bookURL);
bookURL = null;
}
try {
tArea.setPage(bookURL);
} catch (IOException e)
{
System.err.println("Incercare de a citi un Url Rau: " + bookURL);
}
81
Cândea I. Ovidiu-Radu Algoritmi de căutare
Până acum s-a dat codul integral al claselor Link, Avertizare şi Pseudocod. Din
pricina faptului că clasele ce ne interesează au un cod destul de lung, mai departe va fi redat
doar codul metodelor de căutare (nu se va insista asupra modului de construcţie a ferestrelor
cu ajutorul cărora se vor face operaţiile de creare, citire şi căutare etc.).
Fişierul Baza_De_Date.java
Baza de date fiind deja creată nu mai este necesară apăsarea butonului "Încarcă
structura". Se poate vizualiza conţinutul bazei de date prin apăsarea butonului "Afişează
informaţia". Fereastra fişierului pentru probarea căutării într-o bază de date este prezentată
mai jos.
Căutarea se face tot prin interogări SQL. Pentru căutare se introduce în textField
cuvântul sau numărul căutat, se ara în vedere ca butonul radio dorit să fie selectat şi apoi se
apăsa butonul "Caută".
82
Cândea I. Ovidiu-Radu Algoritmi de căutare
ResultSet r=s.executeQuery(query);
while (r.next())
{
sir.add(r.getString("ID")); sir.add(" ");
sir.add(r.getString("Societate")); sir.add(" ");
sir.add(r.getString("Localitate")); sir.add(" ");
sir.add(r.getString("Strada")); sir.add(" ");
sir.add(r.getString("Nr")); sir.add(" ");
sir.add(r.getString("Domeniu")); sir.add("\n");
}
s.close();
}catch(SQLException e)
{
e.printStackTrace();
System.out.println("EROARE la interogarea SQL");
}
}
Se poate observa că interogarea SQL folosită diferă în funcţie de butonul radio
selectat. Pentru că aşa cum s-a precizat şirul de caractere introdus de la tastatură în interiorul
textfieldului este memorat întotdeauna ca String atunci când avem nevoie de un număr
83
Cândea I. Ovidiu-Radu Algoritmi de căutare
(întreg)vom putea transforma Stringul în întreg prin crearea şi apoi folosirea metodei
intValue() a unui obiect de tip Integer. Acesta se construieşte prin instrucţiunea:
Integer in=new Integer(Integer.parseInt(cheie.getText(), 10)),
Aici cheie.getText() este o metodă care returnează Stringul din cadrul textFieldului
cheie,iar această metodă de transformare a unui String în int este folosită şi în cadrul celorlalte
fişiere.java, deci nu se va mai insista mai departe pe reexplicarea acestei chestiuni.
Fişierul Fisier.java
Odată cu apăsarea butonului "Încarcă structura" se apeleză metoda insert (). Datele
returnate de interogarea SQL sub formă de Stringuri sunt scrise prin intermediul unui flux de
scriere într-un fişier de pe hardisk denumit “out.txt”. Înainte de a se încerca o căutare se poate
vizualiza conţinutul fişierului prin apăsarea butonului "Afişează informaţia". Fereastra
fişierului pentru probarea căutării într-un fişier este următoarea:
Metoda find(), cea de căutare în fişierul “out.txt” spre deosebire de cea de la baza de
date unde era de tip void, va întoarce de această dată un String care mai apoi va fi preluat de
metoda clasei ce crează interfaţa şi afişat în textArea. Codul metodei find() este:
84
Cândea I. Ovidiu-Radu Algoritmi de căutare
try{
FileInputStream fis = new FileInputStream("Fisiere/out.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
StreamTokenizer st = new StreamTokenizer(br);
// flux care desface fişierul analizat în atomi lexicali (tokene)
BufferedReader stdin = new BufferedReader(new FileReader("Fisiere/out.txt"),128);
// flux pt citirea din fişier rând cu rând (se doreşte afişarea întregii informaţii despre o anumită cheie găsită )
String input = stdin.readLine();
st.eolIsSignificant(true); // face simbolul eol signifiant
break;
}
case StreamTokenizer.TT_NUMBER:// dacă tokenul este numar
{
if (st.nval==in.intValue()) s=s+input+"\n";
/* dacă numărul reprezentat de token este egal cu numărul căutat întreaga linie în care acesta este scris în fişier va fi
concatenată Stringului s */
break;
}
}
tip = st.nextToken(); // trecere la următorul token pt while-ul de la rând
}
input = stdin.readLine();
tip = st.nextToken(); // trecere la următorul token pt while-ul de la întregul fişier
}
stdin.close();
}catch (IOException e)
{
System.err.println("Eroare de intrare/iesire!");
}
return s; // returnează liniile în care s-a găsit şirul de caractere căutat
}
un număr
un şir de caractere
85
Cândea I. Ovidiu-Radu Algoritmi de căutare
un comentariu
un separator
Atomii lexicali sunt despărţiţi între ei de separatori. Implicit aceşti separatori sunt cei
obişnuiţi (spaţiu, tab, virgulă, punct şi virgulă), însă pot fi schimbaţi prin diverse metode ale
clasei.
Fişierul Array.java
În cadrul acestui fişier se realizează atât o metodă de căutare liniară cât şi una binară.
Pentru aceasta, fereastra pentru lucrul cu tablouri va avea două butoane pentru căutare numite,
"Cauta liniar" respectiv "Cauta binar". Deoarece o căutare binară se poate face doar pe un
tablou ordonat a mai fost introdus un buton separat numit "Sortare". Sortarea tabloului se
poate face după oricare din câmpurile obiectelor de tip Link cu care va fi populat tabloul
(”ID”, ”Societate”, ”Localitate”, ”Strada”, ”Nr”, ”Domeniu”).
86
Cândea I. Ovidiu-Radu Algoritmi de căutare
{
Integer in=new Integer(Integer.parseInt(searchkey, 10));
}catch(NumberFormatException v)
{
v.printStackTrace();
System.out.println("Eroare de transformare de tip parseInt()");
} // se transformă Stringul primit în întreg dacă este posibil
for(j=0;j<nElems;j++)
{
switch (optiune)
{
case 1:
{
if(a[j].getID()==in.intValue())
s=s+a[j].displayLink();
break;
}
case 2:
{
if(a[j].getSoc().equalsIgnoreCase(searchkey))
s=s+a[j].displayLink();
break;
}
case 3:
{
if(a[j].getLoc().equalsIgnoreCase(searchkey))
s=s+a[j].displayLink();
break;
}
case 4:
{
if(a[j].getStr().equalsIgnoreCase(searchkey))
s=s+a[j].displayLink();
break;
}
case 5:
{
if(a[j].getNr()==in.intValue())
s=s+a[j].displayLink();
break;
}
case 6:
{
if(a[j].getDom().equalsIgnoreCase(searchkey))
s=s+a[j].displayLink();
break;
}
}//end switch
}//end for
return s;
}//end find()
87
Cândea I. Ovidiu-Radu Algoritmi de căutare
String s1="";
String s2="";
int st=0;
int dr=nElems-1;
int mij;
try
{
Integer ine=new Integer(Integer.parseInt(searchkey, 10));
}catch(NumberFormatException v)
{
v.printStackTrace();
System.out.println("Eroare de transformare de tip parseInt()");
}
while (st<=dr)
{
mij=(st+dr)/2;
switch (optiune)
{
case 1:
{
if (ine.intValue()>a[mij].getID()) st=mij+1;
else if (ine.intValue()<a[mij].getID()) dr=mij-1;
else
{
s1=s1.concat(a[mij].displayLink());
return s1;
}
break;
}
case 2:
{
if (searchkey.compareToIgnoreCase(a[mij].getSoc())>0) st=mij+1;
else if (searchkey.compareToIgnoreCase(a[mij].getSoc())<0) dr=mij-1;
else
{
s1=s1.concat(a[mij].displayLink());
return s1;
}
break;
}
case 3:
{
if (searchkey.compareToIgnoreCase(a[mij].getLoc())>0) st=mij+1;
else if (searchkey.compareToIgnoreCase(a[mij].getLoc())<0) dr=mij-1;
else
{
s1=s1.concat(a[mij].displayLink());
return s1;
}
break;
}
case 4:
{
if (searchkey.compareToIgnoreCase(a[mij].getStr())>0) st=mij+1;
else if (searchkey.compareToIgnoreCase(a[mij].getStr())<0) dr=mij-1;
else
{
s1=s1.concat(a[mij].displayLink());
return s1;
}
break;
}
case 5:
{
if (ine.intValue()>a[mij].getNr()) st=mij+1;
else if (ine.intValue()<a[mij].getNr()) dr=mij-1;
else
88
Cândea I. Ovidiu-Radu Algoritmi de căutare
{
s1=s1.concat(a[mij].displayLink());
return s1;
}
break;
}
case 6:
{
if (searchkey.compareToIgnoreCase(a[mij].getDom())>0) st=mij+1;
else if (searchkey.compareToIgnoreCase(a[mij].getDom())<0) dr=mij-1;
else
{
s1=s1.concat(a[mij].displayLink());
return s1;
}
break;
}
}//end switch
}//end while
return s2;
}
Fişierul Lista_Inlantuita.java
O captură din timpul rulării programului în fereastra acestei structuri de date a fost
dată în momentul prezentării clasei Avertizare. De data aceasta din cadrul choice-ului alege
se va putea alege tipul de listă înlănţuită (simplu sau dubluînlănţuita)pentru construire şi apoi
probarea corectitudinii funcţionării următoarei metode de căutare:
89
Cândea I. Ovidiu-Radu Algoritmi de căutare
}
case 2:
{
if(current.getSoc().equalsIgnoreCase(searchkey))
s=s+current.displayLink();
current=current.right;
break;
}
case 3:
{
if(current.getLoc().equalsIgnoreCase(searchkey))
s=s+current.displayLink();
current=current.right;
break;
}
case 4:
{
if(current.getStr().equalsIgnoreCase(searchkey))
s=s+current.displayLink();
current=current.right;
break;
}
case 5:
{
if(current.getNr()==in.intValue())
s=s+current.displayLink();
current=current.right;
break;
}
case 6:
{
if(current.getDom().equalsIgnoreCase(searchkey))
s=s+current.displayLink();
current=current.right;
break;
}
}//end switch
}//end while
return s;
}//end find
Indiferent dacă lista construită este simplu sau dubluînlanţuită aceasta poate fi
parcursă de la stânga la dreapta. Pentru a se vedea cu care din câmpurile obiectului Link
curent să se facă comparaţie s-a transmis parametrul opţiune.
La fel ca şi la tablou, dacă se gaseşte cheia căutată, întreaga informaţie din nodul
respectiv (vezi displayLink())se concatenează Stringului s care mai apoi va fi returnat.
Fişierul Arbore_Binar.java
Deoarece tema prezentei lucrări este “căutarea” la capitolul arbori m-am oprit la
implementarea strict a arborelui de căutare.
Aspectul ferestrei arborelui binar poate fi văzută în captura luată la prezentarea clasei
Pseudocod. Ce nu se poate vedea în acea imagine este ce anume conţine choice-ul alege şi
90
Cândea I. Ovidiu-Radu Algoritmi de căutare
anume modul în care se va parcurge arborele binar de căutare la afişare (inordine, preordine
sau postordine) odată creat. Se poate verifica uşor că modul de implementare a arborelui este
corect deoarece la afişarea componentelor arborelui în inordine acestea apar ordonate după
câmpul după care a fost construit arborele (acest câmp va fi câmpul indicat de butonul radio
selectat la momentul apăsării butonului “Încarcă structura”).
Deoarece arborele binar de căutare este construit după un anumit câmp al obiectelor
Link ce vor fi conţinute de acesta, o căutare în arbore se poate face doar după o valoare a
acelui câmp. Acesta este motivul pentru care o dată cu crearea arborelui butoanele radio cu
care deja ne-am obişnuit să ne alegem câmpul de căutare nu vor mai fi active. Instrucţiunea
Java pentru această acţiune este buton_radio.setEnabled(false).
Dacă se doreşte ca butoanele radio să devină active, se apăsa butonul “Alt arbore”. În
acel moment butonul “Încarcă structura” va deveni şi el activ şi se va putea construi un alt
arbore binar de căutare după oricare dintre câmpurile specificate prin butoanele radio.
Pentru căutare am făcut şase metode de căutare, câte una pentru fiecare din câmpuri.
Metoda de căutare pentru cazul când câmpul de construcţie a arborelui binar de căutare este
de tip String (Societate, Localitate, Stradă sau Domeniu)este:
Dacă este înlocuit în metoda de mai sus (căutare după Societate)getSoc() cu getLoc(),
getStr() sau getDom() se obţin şi celelalte metode de căutare după Localitate, Stradă şi
Domeniu.
Pentru cazul în care câmpul de construcţie a arborelui este de tip int (ID sau Nr)
metoda este prezentată în continuare:
91
Cândea I. Ovidiu-Radu Algoritmi de căutare
Link current=root;
try
{
Integer in=new Integer(Integer.parseInt(searchkey, 10));
// se creează obiectul Integer primind ca parametru Stringul searchkey transformat într-un nr în baza 10
}catch(NumberFormatException v)
{
v.printStackTrace();
System.out.println("Eroare de transformare de tip parseInt()");
}
while (current.getID()!=in.intValue())
// intValue returnează valoarea întreag ă a obiectului Integer creat cu ajutorul Stringului searchkey
{
if (in1.intValue()<current.getID()) current=current.left;
else
current=current.right;
if (current==null) return s;
}
s=s+current.displayLink();
return s;
}
La fel ca la metoda pentru căutarea unui String dacă se înlocuieşte în metoda de mai
sus (căutare după ID) getID() cu getNr() se obţine metoda de căutare după Nr. Deosebirea
dintre aceste două metode prezentate este că în cadrul celei de-a doua este nevoie să se
convertească Stringul de numere într-un int prin intermediul metodelor clasei Integer().
Fişierul Tabela_De_Dispersie.java
92
Cândea I. Ovidiu-Radu Algoritmi de căutare
Deoarece obiectele memorate în tabelă sunt de tip Link, implicit vor avea atât câmpuri
de tip int cât şi de tip String, deci vor exista două metode de dispersie, una pentru Stringuri şi
alta pentru întregi. Cele două metode sunt:
93
Cândea I. Ovidiu-Radu Algoritmi de căutare
try
{
if ((k==1)||(k==5))
{
Integer in=new Integer(Integer.parseInt(searchkey, 10));
hashVal=hashFunc1(in.intValue());
}
else hashVal=hashFunc2(searchkey);
}catch(NumberFormatException v)
{
v.printStackTrace();
System.out.println("Eroare de transformare de tip parseInt()");
}
int key=in2.intValue();
while (hashArray_open[hashVal]!=null)
{
switch (k)
{
case 1:
{
if(hashArray_open[hashVal].getID()==key)
s=s+hashArray_open[hashVal].displayLink();
break;
}
case 2:
{
if(hashArray_open[hashVal].getSoc().equalsIgnoreCase(searchkey))
s=s+hashArray_open[hashVal].displayLink();
break;
}
case 3:
{
if(hashArray_open[hashVal].getLoc().equalsIgnoreCase(searchkey))
s=s+hashArray_open[hashVal].displayLink();
break;
}
case 4:
{
if(hashArray_open[hashVal].getStr().equalsIgnoreCase(searchkey))
s=s+hashArray_open[hashVal].displayLink();
break;
}
case 5:
{
if(hashArray_open[hashVal].getNr()==key)
s=s+hashArray_open[hashVal].displayLink();
break;
}
case 6:
{
if(hashArray_open[hashVal].getDom().equalsIgnoreCase(searchkey))
s=s+hashArray_open[hashVal].displayLink();
break;
94
Cândea I. Ovidiu-Radu Algoritmi de căutare
}
}//sfarsit switch
++hashVal;
hashVal%=arraySize;
}//sfarsit while
//cauta in lista curenta cheia searchkey dupa valoarea k
return s;
}
}
Ştim că o tabelă de dispersie cu înlănţuire separată este de fapt un tablou de liste (sau
tablouri în unele cazuri), astfel în metoda de căutare folosită în acest caz se va parcurge pur şi
simplu lista de obiecte Link memorată în cadrul locaţiei tabloului obţinută prin trecerea
valorilor searchkey sau key prin metodele de dispersie hashFunc1() sau hashFunc2(),
făcându-se la fiecare pas comparaţia cu searchkey sau key, după caz. Parcurgerea listelor se
face prin metodele unui clase separate specializate pe lucrul cu liste înlănţuite.
95
Cândea I. Ovidiu-Radu Algoritmi de căutare
Mai jos este prezentată metoda prin care se face căutarea în cadrul listelor care pentru
obţinerea unor timpi mai buni la căutare vor fi ordonate (în cazul unor căutări nereuşite nu va
fi necesară parcurgerea integrală a listei respective).
case 2:
{
while ((current != null)&&(searchkey.compareToIgnoreCase(current.getSoc())>=0))
{
if (current.getSoc().equalsIgnoreCase(searchkey)) s=s+current.displayLink();
current=current.right;
}
break;
}
case 3:
{
while ((current != null)&&(searchkey.compareToIgnoreCase(current.getLoc())>=0))
{
if (current.getLoc().equalsIgnoreCase(searchkey)) s=s+current.displayLink();
current=current.right;
}
break;
}
case 4:
{
while ((current != null)&&(searchkey.compareToIgnoreCase(current.getStr())>=0))
{
if (current.getStr().equalsIgnoreCase(searchkey)) s=s+current.displayLink();
current=current.right;
}
break;
}
case 5:
{
while ((current != null)&&(key1>=current.getNr()))
96
Cândea I. Ovidiu-Radu Algoritmi de căutare
{
if (current.getNr()==key1) s=s+current.displayLink();
current=current.right;
}
break;
}
case 6:
{
while ((current != null)&&(key1>=current.getNr()))
{
if (current.getNr()==key1) s=s+current.displayLink();
current=current.right;
}
break;
}
}// end_switch
return s;
Fişierul Graf.java
Cea mai mare deosebire faţă de celelalte fişiere discutate, în ceea ce priveşte structura,
este faptul că clasa corespunzătoare structurii de date, respectiv grafului conţine şi o fereastă
grafică construită ca fereastră dialog a ferestrei mari (cea care derivă din clasa Fereastra).
Această fereastră dialog reprezintă un mod grafic de a completa matricea de adiacenţă a
grafului pe care se lucrează şi are următorul aspect:
97
Cândea I. Ovidiu-Radu Algoritmi de căutare
independent de muchii (se va parcurge pur şi simplu tabloul în care este memorat graful)
în lăţime (se foloseşte o coadă implmentată static într-o clasă separată numită Queue)
în adâncime (se foloseşte o stivă implmentată static într-o clasă separată numită Stackx)
Pentru a afla dacă există un câmp din elementul Link (cel selectat prin butoanele radio
aflate în stânga-jos)care să conţină cheia searchkey (cea introdusă în textfield)în graf se va
face pur şi simplu o traversare a grafului în lăţime sau adâncime făcându-se câte o comparaţie
la fiecare pas.
98
Cândea I. Ovidiu-Radu Algoritmi de căutare
}
case 2:
{
if(vertexList[0].getSoc().equalsIgnoreCase(searchkey))
s=s+displayLink(0);
break;
}
case 3:
{
if(vertexList[0].getLoc().equalsIgnoreCase(searchkey))
s=s+displayLink(0);
break;
}
case 4:
{
if(vertexList[0].getStr().equalsIgnoreCase(searchkey))
s=s+displayLink(0);
break;
}
case 5:
{
if(vertexList[0].getNr()==ine.intValue())
s=s+displayLink(0);
break;
}
case 6:
{
if(vertexList[0].getDom().equalsIgnoreCase(searchkey))
s=s+displayLink(0);
break;
}
}
while (!theStack.isEmpty())//cat timp exista stiva nu e goala
{
int v=getAdjUnvisitedLink(theStack.peek());
// se găseşte primul vecin nevizitat al nodului aflat în vârful stivei
if (v==-1) theStack.pop(); // dacă nu există un astfel de nod se extrage un nod din stivă
else
{
vertexList[v].wasVisited=true;
switch (k)
{
case 1:
{
if (vertexList[v].getID()==ine.intValue())
s=s+displayLink(v);
break;
}
case 2:
{
if (vertexList[v].getSoc().equalsIgnoreCase(searchkey))
s=s+displayLink(v);
break;
}
case 3:
{
if(vertexList[v].getLoc().equalsIgnoreCase(searchkey))
s=s+displayLink(v);
break;
}
case 4:
{
if(vertexList[v].getStr().equalsIgnoreCase(searchkey))
s=s+displayLink(v);
break;
}
case 5:
{
99
Cândea I. Ovidiu-Radu Algoritmi de căutare
if(vertexList[v].getNr()==ine.intValue())
s=s+displayLink(v);
break;
}
case 6:
{
if(vertexList[v].getDom().equalsIgnoreCase(searchkey))
s=s+displayLink(v);
break;
}
}
theStack.push(v); se introduce nodul v în stivă
}
}//sfarsit while
for (int j=0;j<nVerts;j++) vertexList[j].wasVisited=false; // restabilim indicatorii
return s;
}
BIBLIOGRAFIE
100
Cândea I. Ovidiu-Radu Algoritmi de căutare
Sibiu, 2001
Bucureşti 2001
2003
Bucureşti 1997
[10] A.V. Aho, J.E. Hopcraft, J.D. Ullman , The Design And
Massachutes, 1973
101