1
1. Structuri de date. Clasificarea și caracteristica generală
Structura de date este un set de date care pot fi considerate un lucru sau o entitate noua. Structuri de date (interne din memoria
calc) divizate in 2.
1) Structuri liniare – au proprietatea ca fiecare element component poate avea nu mai mult decit 2 vecini:
a) tablouri (unidimensionale sau siruri de caractere)
b) structuri inregistrate, record
c) liste inlantuite liniare (nu sunt obligatoriu sa fie 2 vecini)
Liste simple liniare, unidirectionale – cind fiecare element legat numai cu urmatorul, are si precedent si urmator, dar poate
fi procedat numai intr-o directie, incepind cu 1 element si pina la sfirsit;
Stiva (stack) – se permite de a sterge de la sfirsit numai. LIFO (LAST INPUT FIRST OUTPUT);
Coada (quene) – 2 capete, de la o parte adaugam, din alta parte extragem. (FIFO)
Liste dublu inlantuite – fiecare element are 2 legaturi cu urmatorul si precedentul, ce permite parcurgerea listei in ambele
directii.
2) Structuri neliniare(data are mai mult de 2 vecini):
a) Structuri lantuite neliniare sau multiinlantuite;
b) Grafuri – arbori;
Binari – fiecare nod(parinte) este legat 2 feciori(copii);
a) arbori binari de cautare;
b) Heap – independenta care valori sunt stocate in noduri;
De grad n>2 – are mai mult de 2 copiii ... 3 4
Dupa acces se divizeaza:
Structuri cu acces direct;
Structuri cu acces secvential.
Structuri omogene – elementele de acelasi tip, aceeasi structura, tablou (omogena), structuri (omogena sau neomogena), Liste
(omogene sau neomogene - complicat), arbore (omogen sau neomogen).
Structuri recursive – cind structura, orice parte a structurii de date are aceeasi structura, lista inlantuita, structura arbore binar:
structuri nerecursive.
2. Structura de date, tipul de date și tipul de date abstract
Limbajul C ofera programatorului posibilitatea de a grupa datele, date de tipuri diferite putind fi prelucrate atit individual cit si
impreuna. Daca tabloul contine numai elemente de acelasi tip, o structura este formata din mai multe elemente de tipuri diferite. La
rindul lor, structurile definite pot canstitui elemente componente pentru formarea de noi tipuri de date (tablouri, structuri, uniuni).
Elementele componente ale unei structuri se numesc membri sau cimpuri.
Sintaxa declararii unei structuri este urmatoarea:
Struct [nume_tip_structura]
{
tip_1 lista_cimpuri_tip_1;
tip_2 lista_cimpuri_tip_2;
tip_n lista_cimpuri_tip_n;
}[lista_variabile_structura];
In declaratie poate lipsi precizarea numelui pentru tipul structurii sau a listei de variabile, insa nu ambele simultan. In cazul in care
lipseste numele tipului, spunem ca variabilele au un tip anonim. Cimpurile componente ale unei structuri pot fi oricare din tipurile:
Predefinite (intreg, caracter, real);
Definite de utilizator: scalar (cimpuri de biti, enumerare) sau compuse (tablouri, structuri, uniuni).
Exemplul 1:
Definirea datelor de baza pentru o persoana fiind specificate numele si virsta:
Struct persoana
{
char nume[35];
int virsta; sau int an;
};
S-a definit o structura de tip persoana fara a fiind folosita nici o variabila de tipul definit. Topurile de date este une set de date si
operatiuni asupra lor. Tipul de date abstract este un element matematic. Tipuri de date se divizeaza in 2:
1) predefinite in limbaj;
2) create de programator;
a) create de programator utilizind specificator predefinit care este structura, enumerare;
b) definite de programator utilizind conceptia tipului de date abstract.
================================================================================================
3. Etape de implementare a tipului de date abstract în limbajul C
Exemplu: numar complex:
Implementarea tipului abstract de date, numarul complex:
Etapele:
1) de scris fisierul cu extensia ’h’, unde:
a) se determina proprietatile datele obiectului sau descrierea proprietatilor obiectului prin declararea datelor;
b) se declara functiile pentru prelucrarea datelor sau descrierea operatiunilor asupra obiectelor utilizind prototipurile functiilor;
2) de scris fisierul cu extensia ’c’ sau ’cpp’ cu codurile functiilor din punctul 1.
Primul fisier se numeste interfata tipului de date abstract, dar al II-lea se numeste implementarea functiilor tipurilor de date
abstract.
2
// fiserul antet complex.h
Typedef struct complex
{
float re; float im; }complex;
//tipurile functiilor care asigura operatiuni
complex add(cimplex c1, complex c2);
complex scadere(cimplex c1, complex c2);
complex add(cimplex c1, complex c2);
complex multime(cimplex c1, complex c2);
complex divizare(cimplex c1, complex c2);
float real(complex c); float imaginar(complex c);
complex conjugare(complex c);
float module(complex c);
void readcomplex(complex *c); // read schimba continutul
void writecomplex(complex c); // write nu schimba continutul
3
5. Liste înlanțuite. Clasificarea și caracteristica generală
Avem urmatoarele clasificari:
1) Simplu inlantuite;
2) Lista circulara simplu inlantuita;
3) Liste dublu inlantuite.
1) O lista simplu inlantuita are nodul format din doua componente principale: o parte statica, ce contine informatia utila si o parte
dinamica, ce contine adresa nodului urmator din lista. Ultimul nod al listei are in partea dinamica valoarea NULL, deoarece dupa
acesta nu mai este inlantuit nici un alt nod.
2) Lista circulara simplu inlantuita este aproape similara cu lista liniara simplu inlantuita.Diferentele sunt: nodul tail (ultim) este
legat cu nodul head (prim), in acest caz lista nu mai are inceput sau sfirsit.
3) Listele dublu inlantuite sunt structuri de date dinamice omogene. Ele au aceleasi caracteristici de baza ca si listele simplu
inlantuite. Diferenta fata de acestea consta in faptul ca, pentru fiecare nod, se retine si adresa elementului anterior, ceea ce permite
traversarea listei in ambele directii.
Structura unui nod este următoarea:
typedef struct tip_nod {
int cheie; /* câmp neobligatoriu */
alte câmpuri de date utile;
struct tip_nod urm; /* legătura spre următorul nod */
} TIP_NOD;
Organigrama fiind modelul grafic al listei simplu înlănţuite este prezentat în fig. 2.1.
Fig. 2.1. Model de listă simplu înlănţuită
================================================================================================
6. Lista simplu înlanțuită. Implementarea tipului de date abstract „Lista simplu înlanțuită” în limbajul C
Lista simplu inlantuita este o structura de date care consta din elementele legate intre ele cu ajutorul adreselor elementelor in
memorie. Fiecare element al listei simple inlantuite are o singura adresa in memorie, unde se afla elementul urmator al listei.
Primul element reprezinta capul listei. Daca ultimul element este egal cu primul, lista e circulara. Pentru lista necirculara in ultimul
element trebuie de pus NULL.
#include<conio.h>
#include<alloc.h>
#define NEW (NOD*)malloc(sizeof(NOD));
struct lista { int info; lista *next; };
typedef struct lista NOD;
NOD* creare() { NOD *prim,*ultim,*q; int nr;
printf(" primul nr="); scanf("%i",&nr); // se citeşte primul număr
prim=(NOD*)malloc(sizeof(NOD)); prim->info=nr; // se creează primul nod
prim->next=NULL; ultim=prim; // se creează ultimul nod
printf("\n urmatorul nr sau tastati CTRL+Z pentru iesire) = "); scanf("%i",&nr); // se citeşte al doilea număr
while(!feof(stdin)) // atâta timp cât nu introducem CTRL+Z
{ q=(NOD*)malloc(sizeof(NOD)); q->info=nr; // se creează nodul următor
q->next=NULL; ultim->next=q; ultim=q; // se creează ultimul nod
printf("\n urmatorul nr sau tastati CTRL+Z pentru iesire)= "); scanf("%i",&nr); // se citeşte numărul următor
} return prim; }
void afis(NOD* prim) { printf("\n"); printf("\n Lista este");
while(prim!=NULL) // atâta timp cât p este diferit de NULL
{ printf("\t \n%i",prim->info); // se afişează nodul curent
prim=prim->next; // se trece la următorul nod
} }
void stergere (NOD* prim) { NOD *p;
while(prim) // atâta timp cât prim este diferit de NULL
{ p=prim; // p devine primul nod
prim=prim->next; // prim devine următorul nod
free(p); // se eliberează nodul curent
} }
void main() { NOD *prim; int a,b,c,d,e,f,g; clrscr();
// Creare
prim=creare(); afis(prim);
// Eliberare memorie
stergere(prim); getch();
}
================================================================================================
4
7. Lista dublu înlanțuită. Implementarea tipului de date abstract „Lista dublu înlanțuită” în limbajul C
O componenta a unei liste dublu inlantuite se declara ca o data structurala de tip inregistrare, formata din trei cimpuri:
informatia propriu-zisa (care poate fi orice tip: numric, caracter, pointer, tablou, inregistrare) si informatiile de legatura (adresa la care e
momorata urmatoarea componenta si adresa la care e memorata precedenta componenta). Ultima componenta va avea informatia de
legatura corespunzatoare urmatoarei adresa NULL (sau 0), cu semnificatia ca dupa ea nu mai urmeaza nimic (retine adresa ”nici o
adresa” a urmatoarei componente). La fel si in cazul primei componente pentru cimpul adresa precedenta.
Lista dublu lănţuită este o listă, în care fiecare element al listei conţine doi pointeri: unul la precedentul element, altul – la
succesorul element din listă. Lista dublu lănţuită în program se poate determina cu ajutorul următoarelor descrieri:
typedef struct ndd
{ float val; /* valoarea informaţională a componentei */
struct ndd * succesor; /* pointer la succesorul element al listei n*/
struct ndd *precedent; /* pointer la precedentul element al listei m*/
} NDD;
NDD * prim, * p, * r;
Interpretarea grafică a listei F=< 2,5,7,1 > ca listă dublu lănţuită este următoarea:
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
typedef struct lista //definim tipul de date LISTA care este o lista dublu lantuita
{ char x; //fiecare element contine cate un element de tip char
struct lista *next,*anti; // si legaturile la urmatorul si la precedentul elemnte
}LISTA; //tipul de date care este creat acuma
LISTA *r,*p; // doi pointeri care ne vor ajuta la crearea si parcurgerea listei
int nr; LISTA *prim; // pointer la primul element al listei
void add(char y) { if(!prim) //daca nu exista primul element
{ prim=new LISTA; //atunci el se creaza
if(!prim)printf("\a\n Memorie insuficienta !!!"); // daca nu s-a putut crea atunci mesaj de eroare
prim->x=y; //se initializeaza campul x din Lista cu valoarea variabilei y transmisa ca parametru
prim->next=NULL; // nu exista urmatrul elemnt
prim->anti=NULL; // nu exista elemntul anterior
} else { r=new LISTA; // daca exista elemente in lista atunci se creaza altul, r
if(!r)printf("\a\n Memorie insuficienta !!!"); // daca nu s-a alocat memorie atunci va fi afisat un mesaj de erare
r->x=y; // se initializeaza campul x din pointerul r, creat recent
p=prim; // de la inceputul listei
while(p->next) p=p->next; // parcurgem pana la ultimul
r->next=NULL; // de la elementul creat acuma, r nu exista elemente dupa el
r->anti=p; // se face legatura dintre elementul r cu ultimul element
p->next=r; // se realizeaza legatura inversa, dintre ultimul element cu r, cel care va deveni ultimul
} }
void print_invers() // functie care afiseaza lista in forma inversa
{ p=prim; // de la primul element
printf("\n\nLista contine:\n"); while(p) // cat timp nu s-a ajuns la ultimul element
{r=p; // se duce la
p=p->next; // ultimul element
} while(r!=NULL) // si parcurgand in sens invers, spre primul element
{ if(r) printf("%c",r->x); // si afiseaza caracterele in forma inversa
r=r->anti; // se deplaseaza spre inceputul listei
} getch(); // se asteapta introducerea unul caracter de la tastatura
}
void main() { int i; char c; // caracterul citit de la tatstatura si clrscr(); // se curata ecranul
clrscr(); printf("\nIntroduceti sirul de caractere terminandu-se cu \'$\' :\n");
do{ c=getche(); // se preia un caracter de la tastatura si se afiseaza, si apoi se atribuie lui c
if(c=='$') break; // daca c este egal cu $ atunci se iese din ciclu
else add(c); // daca nu se iese din ciclu atunci se adauga in lista
}while(c!='$'); // se citeste cate un caracter pana cand c este diferit de $
print(); // se afiseaza lista in ordinea introdusa
print_invers(); // se afiseaza in ordinea inversa
getch(); // se asteapta introducerea unul caracter de la tastatura
}
================================================================================================
5
8. Coada. Implementarea tipului de date abstract "Coada" în limbajul C
Aceasta structura de date este un caz particular de lista care functioneaza pe principal FIFO (first in – first out, primul
intrat – este primul servit, iesit). Prin urmare principalele prelucrari care se refera la aceasta structura de date vor fi:
- crearea cozii;
- listare coada(parcurgere);
- adaugare la sfirsit;
- stergere prim element;
- prelucrare prim element.
Specific acestei structure de date este faptul ca adaugarea se va face intotdeauna la sfirsit, in timp ce prelucrarea (stergerea) se
va face la celalalt capat. Pentru a prelucra o coada vor fi necesari doi pointeri: unul il vom numi virful cozii (primul nod creat),
in timp ce la capatul opus ne vom referi la ultimul element.
Functia pune() – creaza coada cind aceasta este vida sau adauga un nou element la sfirsit in caz contrar.
Functia scoate() – elimina elemental din virful cozii.
Cozi Coada este o listă simplu înlănţuită, bazată pe algoritmul FIFO (First In First Out), adică primul element introdus este primul
scos. Modelul cozii care va fi avut în vedere în consideraţiile
următoare, este prezentat în fig.1.7.1.
Fig.1.7.1. Model de coadă
Se introduce
6
9. Stiva. Implementarea tipului de date abstract "Stiva" în limbajul C
STIVA este o structura de date cu proprietatea ca ultimul element inserat – este primul element extras. Inserarile si extragerile
se fac doar prin virful stivei.
Operatii premise sunt:
adaugarea unui element in stiva; (PUSH)
eliminarea sau extragerea unui element din stiva; (POP)
Definirea tipului abstract stiva. În funcţie de metoda de acces la elementele listei liniare pot fi cercetate următoarele tipuri de liste
liniare: stive, cozi şi cozi de tip vagon. Stiva este o consecutivitate de elemente de acelaşi tip – variabile scalare, tablouri, structuri sau
uniuni. Stiva reprezintă o structură dinamică, numărul de elemente a căreia variază. Dacă stiva n-are elemente, ea este vidă. Stiva este
un tip special de lista în care toate insertiile si suprimarile de noduri au loc la un singur capat. Acest capat se numeste vârful stivei.
Tipul abstract stiva pe care îl definim contine urmatorii operatori:
Initializarea stivei.
Verificarea faptului ca stiva e plina.
Verificarea faptului ca stiva e goala.
Introducerea unui element în vârful stivei.
Eliminarea elementului din vârful stivei.
Furnizarea elementului din vârful stivei fara a-l elimina.
Stiva este o listă simplu înlănţuită bazată pe algoritmul LIFO (Last In First Out), adică ultimul nod introdus este primul scos.
Modelul stivei, care va fi avut în vedere în continuare, este prezentat în fig.1.6.1.
Fiind o structură particulară a unei liste simplu înlănţuite, operaţiile principale asupra unei stive sunt:
- push - pune un element pe stivă; funcţia se realizează conform paragrafului 2.3.a., adică prin inserarea unui nod înaintea primului;
- pop - scoate elementul din vârful stivei; funcţia se realizează conform paragrafului 2.4.a., adică prin ştergerea primului
nod;
- clear - ştergerea stivei; funcţia se realizează conform paragrafului 2.5.
În concluzie, accesul la o stivă se face numai pe la un capăt al său.
//prelucrari stiva : (LIFO)
#include<iostream.h>
#include<conio.h>
struct nod{int info;
nod *back;};
nod *varf;
void push(nod* &v,int x)
{nod *c;
if(!v)
{v=new nod;
v->info=x;
v->back=0;}
else
{c=new nod;
c->back=v;
c->info=x;
v=c;}
}
void afisare(nod *v)
{nod *c;
c=v;
while(c) {cout<<c->info<<" "; c=c->back;}
}
void main()
{int n,a;
cout<<"numarul initial de noduri ";
cin>>n;
for(int i=1;i<=n;i++)
{cout<<"valoarea de adaugat in stiva ";
cin>>a; push(varf,a);
}
cout<<endl; afisare(varf);
}
=============================================================================
7
10. Arbori. Clasificarea și caracteristica generală
Un arbore binar e o multime de n>=0 noduri, care daca nu este vida, contine un nod numit radacina, restul nodurilor formind
doi arbori disjunctivi numiti subarbore sting si subarbore drept.
Arbori binari
Arbori cu mai multe noduri caracteristici nr de noduri, inaltimea, adincimea si modul de parcurgere.
Structura arborescentă este o structură de date de tip arbore datele fiind înmagazinate în noduri, nodul de baza e radacina. Un
arbore reprezinta un "graf" particular, în care substructurile legate la orice nod sunt disjuncte şi în care exista un nod numit radacina
din care e accesibil orice nod prin traversarea unui numar finit de arce. În teoria grafurilor, un arbore este un graf neorientat, conex şi
fără cicluri. Arborii reprezintă grafurile cele mai simple ca structură din clasa grafurilor conexe, ei fiind cel mai frecvent utilizaţi în
practică. Daca nodul A conţine o referinţă la un nod B, atunci A e ascendentul lui B sau B e descendentul lui A. Considerând
nodul radacina situat la nivelul 0 descendenţii nodului radacina vor fi situaţi pe nivelul 1, iar descendenţii acestora pe nivelul 2
ş.a.m.d. Numărul de arce transversat de la nodul radacină până la un nod K defineşte lungimea căii acelui nod. Ca structură de date
recursivă un arbore se defineşte recursiv astfel: el poate fi vid sau poate consta dintr-un nod conţinând referinţe la arborele disadjuncti.
ARBORI BINARI. Un arbore binar e un arbore orientat, în care fiecare vârf are maximum 2 descendenţi. Un arbore binar în
care orice vârf are 1 sau 2 descendenţi se numeşte arbore binar complet.
Arbore binar echilibrat e acel pentru fiecare nod a căruia numărul de noduri din subarborele stâng diferă de cel din subarborele
drept cu cel mult 1. Ca exemplu poate servi arborele genealogic.
Un mod de reprezentare a arborilor e acel a expresiilor cu paranteze incluse, ce se construieste astfel: expresia începe cu
radacina arborelui, iar fiecare vârf, ce are descendenti e urmat de expresiile atasate subarborilor, ce au ca radacini descendentii
vârfurilor, despartite între ele prin virgula şi cuprinse în paranteze.
A(B(C,D(H)),K(L(N),M(E)))
.A
/\
B./ \
/\.C \.K
./ \ /\
/\ / \.M
H./ L./ \
/ \.E
N./
Exista 3 metode principale de parcurgere a unui arbore şi anume:
1) preordine ;
2) inordine ;
3) postordine.
Transversarea în preordine e acea, în care mai întii se prelucreaza informatia din nod şi apoi se parcurge prin arbore.
Transversarea în inordine e acea, în care secventa actiunilor e urmatoarea: parcurgerea subarborelui stâng, vizitarea nodului
radacina şi apoi parcurgerea subarborelui drept.
Transversarea în postordine cu parcurgerea subarborelui stâng, subarborelui drept şi apoi vizitînd radacina.
Structura arborescenta tree - structura arborescenta de date utilizata frecvent în sistemele, ce implica memorarea unei mari
cantitati de date sortate pe baza unor chei compuse în vederea unor operatii de cautare.
11. Diferite forme de reprezentare a unui arbore în memoria calculatorului
Graful, asemenea arborelui, este o structura în care relatia dintre nodul parinte si nodul fiu este una ierarhica, dar care este
mai putin restrictiva în sensul ca un nod are mai multi succesori, dar si mai multi predecesori. El este definit ca o colectie de date
reunite în doua multimi: multimea N = { N1, N2, …, Nn | n – numarul de noduri al grafului}ce contine toate nodurile grafului si
multimea A = { ( Ni, Nj ) = Aij | Ni, Nj N si i,j = 1,n cu i j}care contine arcele dintre doua noduri vecine.
Reprezentarea in memoria calculatorului a grafurilor presupune memorarea numarului de noduri si a legaturilor dintre acestea. In
practica cele mai folosite moduri de reprezentare presupun utilizarea variabilelor de tip tablou sau a structurilor de date dinamice.
a) Reprezentarea grafurilor folosind variabile de tip tablou:
1. Reprezentarea tata-fii:
Se utilizeaza un vector V care pentru oricare nod i ce apartine grafului pastreaza toate nodurile j cu care acesta este adiacent
(daca graful este neorientat atunci trebuie respectata conditia j > i, deoarece se repeta muchii ). Prin conventie se separa
nodurile tata si adiacentii lor cu valoarea 0.
,0, Nodul i, fiu1i, fiu 2i, fiu 3i,, 0,
c). Matricea de adiacenta:
Fie G un graf cu n noduri. Pentru memorarea acestuia in forma numerica se poate folosi o matrice A (n x n) elemente unde
A(i, j)=
Exemplu pentru graful din figura 1.3.a) matricea de adiacenta este:
1 2 3 4 5
1 0 1 1 1 1
2 0 0 1 0 1
A: 3 0 0 0 1 0
4 0 0 1 0 0
5 1 0 1 1 0
In cazul grafurilor neorientate matricea de adiacenta este simetrica si deci se va folosi efectiv, doar jumatate din spatiul
matricei. De aceea in unele situatii este recomandat ca in cealalta jumatate sa fie memorate alte informatii.
Matricea de adiacenta se va folosi in general pentru grafuri cu numar mic de noduri (aproximativ 250 de noduri).
8
c).Matricea de incidenta a muchiilor:
Fie un graf cu n noduri si muchii. Pentru memorarea lui se foloseste un tablou bidimensional cu (n x m) elemente unde orice
element din matrice poate lua urmatoarele valori:
a(i , j)=
d) Matricea costurilor:
In practica exista situatii cand se cunoaste costul legaturii a doua noduri din graf(exemplu: costul cablului telefonic necesar
conectarii a doua posturi telefonice, costul asfaltarii unei portiuni de sosea dintre doua puncte, etc.) si in aceste situatii graful se
reprezinta in memoria calculatorului folosind matricea costurilor. Matricea costurilor se noteaza cu C, este o matrice de dimensiune n *
n, unde n reprezinta numarul de noduri, iar fiecare element al ei poate lua urmatoarele valori:
C(i, j)=
e) Matricea muchiilor:
Atunci cand sunt cunoscute costurile muchiilor pentru memorarea unui graf se mai poate utiliza si un alt mod de reprezentare
interna care foloseste o matrice cu trei linii si un numar de coloane egal cu numarul de muchii (numarul maxim de muchii pentru un
graf neorientat este unde n reprezinta numarul de noduri). O linie din aceasta matrice are forma:
Linia 1: i
Linia 2: j
Linia 3: costul muchiei (i , j)
unde:
i, j sunt doua noduri adiacente din graf.
Exemplu: Pentru graful ponderat reprezentat in fig. 1.6 matricea muchiilor este:
M=
================================================================================================
12. Arbore binar. Implementarea tipului de date abstract "Arbore binar" în limbajul C
Arborii binari sunt o structura de date menita sa ocupe un spatiu de memorie cit mai mic posibil. Cele mai multe reprezentari
concise si complexe ale arborilor binary pot indica nu numai spatial ocupar de acestia ci si operatiile directe ce se pot aplica acestora
atita timp cit acestia sunt pastrati intr-o forma concisa.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <conio.h>
struct node // declararea unei structuri
{
char data[15]; // cimp de tip char
int num; // cimp de tip int
struct node *left, *right;
// declarare 2 pointeri sting si drept
};
int repet=1; // variabila de tip int
void insert(struct node *r, struct node *p)
// adaugare unui nod in arbore
{
if((r->right==NULL)&&(strcmp(p->data, r->data)>0))
r->right=p;
if((r->right!=NULL)&&(strcmp(p->data, r->data)>0))
insert(r->right, p);
if((r->left==NULL)&&(strcmp(p->data, r->data)< 0))
r->left=p;
if((r->left!=NULL)&&(strcmp(p->data, r->data)< 0))
insert(r->left, p);
p->num=repet;
if(strcmp(p->data, r->data)==0)
r->num++;
}
11
14. Arbore binar de căutare. Implementarea tipului de date abstract"Arbore binar de căutare" în limb. C
Arborele binar de cautare (BST). Arborele binar de cautare reprezinta o solutie eficienta de implementare a structurii
de date numite "dictionar". Vom considera o multime "atomi". Pentru fiecare element din aceasta multime avem: a atomi, este
definita o functie numita cheie de cautare: key(a) k cu proprietatea ca doi atomi distincti au chei diferite de cautare: a1 a2
key(a1) key(a2).
Exemplu: (abreviere, definitie) ("BST","Binary Search Tree")
("LIFO","Last In First Out") key(a) = a.abreviere
Un dictionar este o colectie S de atomi pentru care se definesc operatiile:
insert(S,a) insereaza atomul a în S daca nu exista deja;
delete(S,k) sterge atomul cu cheia k din S daca exista;
search(S,k) cauta atomul cu cheia k în S si-l returneaza sau determina daca nu este.
================================================================================================
15. Crearea arborelui binar de căutare. Algoritm pentru inserarea unui nod
Structura unui nod al unui arbore binar de căutare este următoarea:
typedef struct tip_nod { tip cheie;
informaţii_utile;
struct tip_nod *stg, *dr;
} TIP_NOD;
În cele ce urmează, rădăcina arborelui se consideră ca o variabilă globală:
TIP_NOD *rad;
Inserarea într-un arbore binar de căutare.
Construcţia unui arbore binar de căutare se face prin inserarea a câte unui nod de cheie key. Algoritmul de inserare este următorul:
a) Dacă arborele este vid, se creează un nou nod care este rădăcina, cheia având valoarea key, iar subarborii stâng şi drept fiind
vizi.
b) Dacă cheia rădăcinii este egală cu key atunci inserarea nu se poate face întrucât există deja un nod cu această cheie.
c) Dacă cheia key este mai mică decât cheia rădăcinii, se reia algoritmul pentru subarborele stâng (pasul a).
d) Dacă cheia key este mai mare decât cheia rădăcinii, se reia algoritmul pentru subarborele drept (pasul a).
Funcţia nerecursivă de inserare va avea următorul algoritm:
void inserare_nerecursivă (int key)
{TIP_NOD *p, *q;
int n;
/* construcţie nod p*/
n=sizeof (TIP_NOD);
p=(TIP_NOD*)malloc(n);
p->cheie=key;
/* introducere informaţie utilă în nodul p */
p->stg=0; p->dr=0; /* este nod funză */
if (rad==0) { /* arborele este vid */
rad=p;
return;
}
/* arborele nefiind vid se caută nodul tată pentru nodul p */
q=rad; /* rad este rădăcina arborelui variabilă globală */
for ( ; ; ) { if (key<q->cheie)
{ /* căutarea se face în subarborele stâng */
if (q->stg==0) { /* inserare */
q->stg=p;
return;
}
else q=q->stg;
}
else if (key>q->cheie)
{ /* căutarea se face în subarborele drept */
if (q->dr==0) { /* inserare */
q->dr=p;
return;
}
else q=q->dr;
}
else { /* cheie dublă */
/* scriere mesaj */
free (p);
return;
} } }
================================================================================================
12
16. Arbore binar de căutare. Algoritmi pentru căutarea unui nod și pentru determinarea nodului max/min
Căutarea unui nod de cheie dată key într-un arbore binar de căutare.
Căutarea într-un arbore binar de căutare a unui nod de cheie dată se face după un algoritm asemănător cu cel de inserare.
Numărul de căutări optim ar fi dacă arborele de căutare ar fi total echilibrat (numărul de comparaţii maxim ar fi log 2 n – unde n
este numărul total de noduri).
Cazul cel mai defavorabil în ceea ce priveşte căutarea este atunci când inserarea se face pentru nodurile având cheile ordonate
crescător sau descrescător. În acest caz, arborele degenerează într-o listă.
Algoritmul de căutare este redat prin funcţia următoare:
TIP_NOD *cautare (TIP_NOD *rad, int key)
/* funcţia returnează adresa nodului în caz de găsire sau 0 în caz că nu există un nod de cheia key */
{ TIP_NOD *p;
if (rad==0) return 0; /* arborele este vid */
/* dacă arborele nu este vid, căutarea începe din rădăcina rad */
p=rad;
while (p!=0)
{ if (p->cheie==key) return p; /* s-a găsit */
else if (key<p->cheie) p=p->stg; /*căutarea se face în subarb.stâng */
else p=p->dr; /* căutarea se face în subarborele drept */
}
return 0; /* nu există nod de cheie key */
}
Apelul de căutare este:
p=cautare (rad, key);
rad fiind pointerul spre rădăcina arborelui.
Valoarea maximă dintr-un arbore binar de căutare se află în nodul din extremitatea dreaptă şi se determină prin coborârea pe
subarborele drept, iar valoarea minimă se află în nodul din extremitatea stângă.
Minimul şi maximul
Determinarea elementului având cheia minimă dintr-un arbore binar de căutare se realizează întotdeauna
urmând pointerii fiu stânga începând cu rădăcina şi terminând când se întâlneşte NULL.
Procedura următoare întoarce un pointer la elementul minim din subarborele a cărui rădăcină este nodul x:
ARBORE-MINIM (x) ARBORE-MAXIM (x)
1. cât timp stânga[x] ≠ NULL execută 1. cât timp dreapta[x] ≠ NULL execută
2. x <- stânga[x] 2. x <- dreapta[x]
3. returnează x 3. returnează x
Pseudocodul pentru procedura ARBORE-MAXIM este simetric.
================================================================================================
17. Arbore binar de căutare. Algoritm pentru ștergerea unui nod
În cazul ştergerii unui nod, arborele trebuie să-şi păstreze structura de arbore de căutare.
La ştergerea unui nod de cheie dată intervin următoarele cazuri:
a) Nodul de şters este un nod frunză. În acest caz, în nodul tată, adresa nodului
fiu de şters (stâng sau drept) devine zero.
b) Nodul de şters este un nod cu un singur descendent. În acest caz, în nodul tată,
adresa nodului fiu de şters se înlocuieşte cu adresa descendentului nodului fiu de şters.
c) Nodul de şters este un nod cu doi descendenţi. În acest caz, nodul de şters se înlocuieşte cu nodul cel mai din stânga al
subarborelui drept sau cu nodul cel mai din dreapta al subarborelui stâng.
Algoritmul de ştergere a unui nod conţine următoarele etape:
căutarea nodului de cheie key şi a nodului tată corespunzător;
determinarea cazului în care se situează nodul de şters.
Exemplu de stergere a unui cod:
void sterg(nod *&c,int k)
{nod *aux,*f;
if(c)
if(c->nr_o==k)
if(c->st==0&&c->dr==0) //daca e nod terminal
{delete c;
c=0;
}
else
if(c->st==0) //are numai subordonat drept
{aux=c->dr;
delete c;
c=aux;}
else
if(c->dr==0) //are numai subordonat drept
{aux=c->st;
13
delete c;
c=aux;}
else
cmd(c,c->st); //are ambii subordonati
else
if(c->nr_o<k)
sterg(c->dr,k);
else
sterg(c->st,k);
else
cout<<"valoarea de sters nu se gaseste in arbore ";
}
================================================================================================
18. Algoritmi de căutare secvențială și binară pentru tabloul unidimensional
Enuntarea problemei de cautare
Se cauta o valoare (citita de exemplu de la tastatura), intr-un tablou dimensional (citit anterior).
Cautarea secventiala
Presupune parcurgerea secventiala a unui tablou unidimansional si compararea fiecarui element din tablou cu elemental cautat pina cind
acesta este gasit sau ajunge la sfirsitul tabloului.
Cautarea binara
Presupune cautarea unei valori intr-un tablou, al carui elemente sunt ordonate. Valoarea cautata este comparata cu cea a elementului din
mijlocul listei. Daca e egala cu cea a acelui element, algoritmul se termina. Daca e mai mare decit acea valoare, algoritmul se reia, de la
mijlocul listei pina la sfirsit, iar daca e mai mica, algoritmulse reia pentru elementele de la inceput pina la mijloc.
Algoritm:
Consideram un tablou v cu n elemente deja sortat, si trei variabile: i=inceput, s=sfirsit, m=mijloc. Metoda verifica de mai
multe ori daca mijlocul vectorului este egal cu elemental cautat:
in cazul in care este egala, variabila m reprezinta pozitia elementului in vector.
Daca nu se indeplineste conditia de egalitate se trece la verificarea pozitiei elementului cautat in vector astfel: daca
elemental cautat este mai mic decit elemental din mijlocul vectorului, variabila s ia valoarea lui m , iar daca nu variabila i ia
valoarea lui m.
Totul se repeat cit timp i este mai mic decit s.
Exemplu:
m s
i 0 1 2 3 4 5 6 7 8 9
v 3 5 7 8 10 13 19 29 30 55
i=0; s=9; m=[(i+j)/2]=4 13> v[4] =10 deci se cauta in a doua jumatate a tabloului v
i 5 6 7 8 9
v 13 19 29 30 55
i=5; s=9; m=[(i+j)/2]=7 13<v[7]=29 deci se cauta in prima jumatate a tabloului v
i 5 6
v 13 19
i=5; s=6; m=[(i+j)/2]=5 13=v[5]
15
Algoritmii de sortare prin distribuire presupunand ca se cunosc informatii privind distributia acestor elemente (informatii suplimentare
despre elementele). Utilizam aceste informatii pentru a distribui elementele secventei de sortat in “pachete” ce se vor sorta in acelasi
mod prin alta metoda, iar apoi le combinam obtinand lista finala sortata.
Algoritmi de sortare a cuvintelor (RadixSort)
Sa presupunem ca avem n fise, iar fiecare contine un nume ce identifica in mod unic fisa.Sa sortam manual fisele.Le impartim in
pachete,fiecare pachet cuprizand toate fisele ce incep cu aceeasi litera.Apoi sortam fiecare pachet dupa a doua litera.. s.a.m.d. Dupa
sortarea pachetelor le recombinam obtinand o lista liniara sortata. Aceasta metoda poate fi formalizata intr-un algoritm de sortare a
sirurilor de cuvinte (caractere). Presupunem ca elementele secventei de sortat reprezinta sirul de lungime fixata m ce se definesc peste
un alfabet de k litere. Putem presupune ca elementele de sortat reprezinta nmere scrise in baza k. De aceea sortarea cuvintelor este
numita in limba engleza “radix sort”(radix=baze).
C.Algoritmul de sortare prin numarare
Consta in a numara pentru fiecare element a(i) cate elemenete ,strict mai mici decat el, exista. Numerele obtinute sunt memorate intr-un
vector k.Pe baza lui vom rearanja elementele lui a intr-un alt vector b.
D. Sortarea topologica
Consideram ca relatia de ordine este partiala. De ex a2<a1 ,a2<a3<a4. Problema consta in crearea unei liste liniare compatibila cu
relatia de ordine: daca Ai<Aj atunci Ai precede pe Aj in lista finala.In exemplul anterior lista finala poate fi (a2,a1,a3,a4) sau
(a2,a3,a1,a4) sau (a2,a3,a4,a1).
E.Sortare untilizand metoda “Divide et impera”
1.Sortarea rapida (Quick Sort) reprezentata in 1962. Pentru rezolvare este necesara o procedura care traseaza o portiune de vector
cuprinsa intre indicii dati de p si q. Rolul acestei proceduri este de a pozitiona prima componenta a[p] pe o pozitie k suprinsa intre p si q
astfel incat toate componentele vectorului curinse intre p si k-1 sa fie mai mici sau egale decat a[k] si toate componentele vectorului
cuprinse intre k+1 si q sa fie mai mari sau egale decat a[k].
2.Mergesor. Un alt algoritmcare se bazeaza pe metoda “Divide et Impera” este algoritmul de interclasare a doi vectori sortati.El produce
ca rezultat tot un vector sortat, care contine elementele celor doi vectori care se interclaseaza.DEci pentru a sorta elementele vectorului
a , il vom partitiona in doua vom sorta sirurile a[1]…a[m],a[m+1]…a[n] apoi vom interclasa subsirurile sortate.
Exemplu
Fie tabloul A = (5, 0, 8, 7, 3).
Pas Tabloul A Element minimPoziţia minimuluiNoul tablou A
i=1 (5, 0, 8, 7, 3) 0 2 (0, 5, 8, 7, 3)
i=2 (0, 5, 8, 7, 3) 3 5 (0, 3, 8, 7, 5)
i=3 (0, 3, 8, 7, 5) 5 5 (0, 3, 5, 7, 8)
i=4 (0, 3, 5, 7, 8) 7 4
Algoritmul se poate scrie şi prin determinarea valorilor maxime şi mutarea lor în tablou de la dreapta la stânga, astfel rezultând, de
asemenea, un şir ordonat crescător. Un astfel de algoritm este des utilizat pentru că este mai simplu de reţinut, în forma următoare:
Subalgoritm Selecţie(n,a)
1: pentru i=1,n-1 execută:
2: pentru j=i+1,n execută:
3: dacă a[i] > a[j] atunci
4: aux = a[i]
5: a[i] = a[j]
6: a[j] = aux sfd 3: sfp 2: sfp 1:
16
element. Comparãm elementele, dacã douã elemente adiacente nu sunt în ordine crescãtoare, le interschimbãm. Astfel elementul cu
cheia cea mai mare ajunge primul la locul potrivit, iar elementul cu cheia cea mai micã, urcã tot timpul spre vârf .
Descrierea algoritmului
Fiind dat o tabelã A cu N înregistrãri (Ri). Pentru sortarea tabelei este nevoie de N-1 pasuri. Parcurgem tabela: K1 cheia primului
element, comparãm cu K2 cheia celui de al doilea element si dacã nu sunt în ordine crescãtoare, R1 primul element interschimbãm cu
R2. Procesul se repetã pentru R2, R3,…..Dupã primul pas înregistrarea cu cheia cea mai mare va ajunge pe ultima pozitie N . Dupã
fiecare pas succesiv elementele cu chei mari vor ajunge pe pozitiile N-1, N-2, …, 2 rezultând o tabelã sortatã în ordine crescãtoare.
Pasii bubble sortului
În primul pas comparãm 6 cu 4, cu 5 si interschimbãm, apoi comparãm cu 10 nu interschimbãm; acum comparãm 10 cu 3, cu 1, cu 8, cu
9, cu 7, cu 2 si interschimbãm pe rând cu fiecare, astfel 10 ajunge pe ultima pozitie. Dupã pasul al doilea 9 va ajunge pe pozitia potrivitã
s.a.m.d. În pasul 8 comparãm 1 cu 2 si 2 cu 3. Dupã pasul 8 tabela este sortatã dar dacã nu avem o variabilã "flag" care sã opreascã
procesarea, algoritmul va mai face un pas în care comparã 1 cu 2 si numai dupã accea se opreste.
Algoritmul general:
1. Repetã de N-1 -ori pânã la pasul 4
2. Repetã pasul 3 pentru elementele nesortate din tabelã
3. Dacã cheia elmentului curent > cheia urmãtorului element atunci interschimbã
4. Dacã numai sunt necesare interschimbãri atunci stop
Altfel reduce mãrimea vectorului nesortat cu unu
Se parcurge vectorul (operand interschimbarii de elemente ,,vecine”)atat timp cat mai exista o pareche (a[i],a[a+i] ) cu a[i]>a[a+i].
repeta
modificat=fals
pentru i=1,N-1 executa
daca a[i]>a[i+1] atunci
schimba a[i],a[i+1]
modificat adevarat
sf daca
sf pentru
pana cand modificat=fals
Acest algoritm se mai numeste si “sortarea prin selectie si interschimbare” , “sortarea prin propagare” sau “metoda lenta de sortare”
datorita numarului mare de operatii care trebuie effectuate.Succesul algoritmului este asigurat de trecerea succesiva prin tablou,pana
cand acesta este sortat,cu specificatia ca , la fiecare trecere , elementele successive I si i+1 pentru care a[i]>a[i+1], vor fi interschimbate.
Metoda poate fi imbunatatita daca ,dupa fiecare trecere , se va retine ultima pozitie din tablou in care a avut loc o interschimbare, iar
trecerea urmatoare se va efectua doar pana la acea pozitie. In cazul in care la o trecere nu a avut oc nicio interschimbare algoritmul se
va incheia.
Pentru o si mai buna optimizare se poate inlocui trecerea prin tablou intr-un sens cu trecerea in dublu sens .In acest caz , daca la doua
treceri successive, intre doua elemente I si i+1 nu a avut loc o interschimbare atunci la trecerile urmatoare nu se vor inregistra
interschimbari.
Cu toate optimizarile aduse acestei sortari, ea se dovedeste a fi cu mult mai lenta decat sortarea prin insertie,fiind insa preferata de unii
datorita simplitatii, ce nu ridica problem de implementare. Pentru valori suficient de mari ale dimensiunii vectorului ce urmeaza a fi
sortat se recomanda utilizarea unor algoritmi ce au o complexitate mai redusa si, prin urmare, ofera un timp de calcul mai mic.
#include<stdio.h>
main( ){
int n,i,aux,terminat,a[50];
printf(”Introduceti dimensiunea vectorului : “);
scnf(“%d”, &n);
for(i=0;i<=n-1;i++){
printf(”a[%d]=”,i+1);
scanf(“%d”, &a[i]);
}
terminat=0;
while(!terminat){
terminat=1;
for(i=0;i<n-1;i++)
if(a[i]>a[i+1]){
aux-a[i];
a[i]=a[i+1];
a[i+1]=aux;
terminat=0;}
}
printf(”Vectorul ordonat este este : “);
for(i=0;i<+n-1;i++)
printf(”%d ”,a[i]);
}
================================================================================================
17
21. Algoritmi de sortare ”bubble” și prin inserție
21.1) Metoda bulelor (bubble-sort)
Descrierea generalã a metodei
Ideea de bazã constã în faptul cã, imaginãm o tabelã tinutã vertical care contine elemente (înregistrãri) nesortate. Înregistrãrile cu chei
mici sunt usoare si asemenea bulelor de aer urcã spre vârful tabelei. Sortarea bulelor se bazeazã pe cãutarea celui mai mare (mic)
element. Comparãm elementele, dacã douã elemente adiacente nu sunt în ordine crescãtoare, le interschimbãm. Astfel elementul cu
cheia cea mai mare ajunge primul la locul potrivit, iar elementul cu cheia cea mai micã, urcã tot timpul spre vârf .
Descrierea algoritmului
Fiind dat o tabelã A cu N înregistrãri (Ri). Pentru sortarea tabelei este nevoie de N-1 pasuri. Parcurgem tabela: K1 cheia primului
element, comparãm cu K2 cheia celui de al doilea element si dacã nu sunt în ordine crescãtoare, R1 primul element interschimbãm cu
R2. Procesul se repetã pentru R2, R3,…..Dupã primul pas înregistrarea cu cheia cea mai mare va ajunge pe ultima pozitie N . Dupã
fiecare pas succesiv elementele cu chei mari vor ajunge pe pozitiile N-1, N-2, …, 2 rezultând o tabelã sortatã în ordine crescãtoare.
Pasii bubble sortului
În primul pas comparãm 6 cu 4, cu 5 si interschimbãm, apoi comparãm cu 10 nu interschimbãm; acum comparãm 10 cu 3, cu 1, cu 8, cu
9, cu 7, cu 2 si interschimbãm pe rând cu fiecare, astfel 10 ajunge pe ultima pozitie. Dupã pasul al doilea 9 va ajunge pe pozitia potrivitã
s.a.m.d. În pasul 8 comparãm 1 cu 2 si 2 cu 3. Dupã pasul 8 tabela este sortatã dar dacã nu avem o variabilã "flag" care sã opreascã
procesarea, algoritmul va mai face un pas în care comparã 1 cu 2 si numai dupã accea se opreste.
Algoritmul general:
1. Repetã de N-1 -ori pânã la pasul 4
2. Repetã pasul 3 pentru elementele nesortate din tabelã
3. Dacã cheia elmentului curent > cheia urmãtorului element atunci interschimbã
4. Dacã numai sunt necesare interschimbãri atunci stop
Altfel reduce mãrimea vectorului nesortat cu unu
Se parcurge vectorul (operand interschimbarii de elemente ,,vecine”)atat timp cat mai exista o pareche (a[i],a[a+i] ) cu a[i]>a[a+i].
repeta
modificat=fals
pentru i=1,N-1 executa
daca a[i]>a[i+1] atunci
schimba a[i],a[i+1]
modificat adevarat
sf daca
sf pentru
pana cand modificat=fals
Acest algoritm se mai numeste si “sortarea prin selectie si interschimbare” , “sortarea prin propagare” sau “metoda lenta de sortare”
datorita numarului mare de operatii care trebuie effectuate.Succesul algoritmului este asigurat de trecerea succesiva prin tablou,pana
cand acesta este sortat,cu specificatia ca , la fiecare trecere , elementele successive I si i+1 pentru care a[i]>a[i+1], vor fi interschimbate.
Metoda poate fi imbunatatita daca ,dupa fiecare trecere , se va retine ultima pozitie din tablou in care a avut loc o interschimbare, iar
trecerea urmatoare se va efectua doar pana la acea pozitie. In cazul in care la o trecere nu a avut oc nicio interschimbare algoritmul se
va incheia.
Pentru o si mai buna optimizare se poate inlocui trecerea prin tablou intr-un sens cu trecerea in dublu sens .In acest caz , daca la doua
treceri successive, intre doua elemente I si i+1 nu a avut loc o interschimbare atunci la trecerile urmatoare nu se vor inregistra
interschimbari.
Cu toate optimizarile aduse acestei sortari, ea se dovedeste a fi cu mult mai lenta decat sortarea prin insertie,fiind insa preferata de unii
datorita simplitatii, ce nu ridica problem de implementare. Pentru valori suficient de mari ale dimensiunii vectorului ce urmeaza a fi
sortat se recomanda utilizarea unor algoritmi ce au o complexitate mai redusa si, prin urmare, ofera un timp de calcul mai mic.
#include<stdio.h>
main( ){
int n,i,aux,terminat,a[50];
printf(”Introduceti dimensiunea vectorului : “);
scnf(“%d”, &n);
for(i=0;i<=n-1;i++){
printf(”a[%d]=”,i+1);
scanf(“%d”, &a[i]);
}
terminat=0;
while(!terminat){
terminat=1;
for(i=0;i<n-1;i++)
if(a[i]>a[i+1]){
aux-a[i];
a[i]=a[i+1];
a[i+1]=aux;
terminat=0;
} }
printf(”Vectorul ordonat este este : “);
for(i=0;i<+n-1;i++) printf(”%d ”,a[i]); }
18
21.2) Metoda insertiei
Sortarea prin insertie se bazeaza pe aceleasi principii ca si cele aplicate de majoritatea jucatorilor de carti,adica dupa ridicarea uncei
carti de pe masa,aceasta se aseaza in pachetul din mana la locul potrivit.Cu alte cuvinte, consideram ca avem vectorul sortat a, iar la
ivirea unui nou element care se va adauga vectorului,el va fi pus pe locul potrivit printr-o insertie in interiorul vectorului.
Insertie directa.Este cea mai simpla implementare a algoritmului si se face in felul urmator: Se considera ca primele i elemente al
vectorului sunt deja sortate.Pentru elementul al (i+1)-lea,din tabloul initial,se va gasi pozitia in care trebuie inserat printre primele i
elemente.Toate elementele tabloului de la acasta pozitie si pana la i vor fi deplasate cu o pozitie mai la dreapta iar pozitia eliberata va fi
ocupata de elemntul i+1.
pentru j=1 ,N-1 executa
x=a[j]; i=j-1;
cat timp ((i>=0) && (x<a[i]))
a[i+1]=a[i]
i=i-1;
sf cat timp
a[i+1]=x
sf pentru
Inserare rapida.Deoarece vectorul destinatie este un vector ordonat crescator, cautarea pozitiei in care va fi inserat a[i] se poate
face nu secvential ( ca in cazul inserarii directe) ci prin algoritmul de cautare binara. Subvectorul destinatiei este impartit in doi
subvectori,se examineaza relatia de ordine dintre elementul de la mijlocul subvectorului si elementul a[j] si se stabileste daca elemntul
a[i] va fi inserat in prima jumatate sau in a doua jumatate.Operatia de divizare a subvectorului continua pana se gaseste pozitia in care
urmeaza sa fie inserat a[i].
#include<stdio.h>
main( )
{
int i,j,n,aux,a[50];
printf(“Iintroduceti dimensiunea sirului: ");
scanf(“%d”,&n);
printf("Dati elementele sirului:\n");
for(i=0;i<n;i++) {
printf("a[%d]=”,i+1); scanf(“%d”,&a[i]);
}
for(j=1;j<n;j++)
{ aux=a[j];
i=j-1;
while (aux<a[i] && i>=0)
{ a[i+1]=a[i];
i=i-1;
}
a[i+1]=aux;
}
printf("Sirul ordonat este:");
for(i=0;i<n;i++)
printf(“%d ”,a[i]);
}
================================================================================================
22. Algoritmi de sortare ”shaker” și Shell
22.1) Metoda de sortare “Shaker”
Sortare prin amestecare (Shaker Sort)La prima parcuregere cheia de valoare imediata inferioara celei maxime migreazaspre penultima
pozitie.La fiecare parcurgere a tabloului se memoreaza indicele ultimei interschimbari facute astfel incat la urmatoarea parcurgere un
capat al subtabloului va fi marcat de acest indice. Se schimba alternativ sensul de parcurgere al subtablourilor pentru doua treceri
succesive. Datorita acestor migrari,metoda a fost denumita astfel, deoarece elementele de valori mari se ridica la suprafata asemenea
unor bule.
Principiu: reprezinta o varianta a metodei bubblesort, avind urmatoarele imbunatatiri:
-la fiecare parcurgere a subtabloului a[i],...,a[N] se memoreaza indicele k al ultimei interschimbari efectuate, astfel incit la urmatoarea
trecere un capat al subtabloului va fi marcat de k (intre 1 si k tabloul este ordonat);
-se schimba alternativ sensul de parcurgere al subtablourilor pentru doua treceri consecutive.
Implementarea algoritmului in Pascal:
procedure ShakerSort;
VAR j,k,l,r : TipIndex; x : TipElement;
begin
l:=2; r:=N; k:=N;
repeat
for j:=r downto l do
if a[j-1].cheie>a[j].cheie then begin
x:=a[j-1];
a[j-1]:=a[j];
a[j]:=x;
19
k:=j
end;
l:=k+1;
for j:=l to r do
if a[j-1].cheie>a[j].cheie then begin
x:=a[j-1];
a[j-1]:=a[j];
a[j]:=x;
k:=j
end;
r:=k-1
until l > r
end; {ShakerSort}
La sortarile bubblesort si shakersort, M si C sint proportionali cu N*N, metodele fiind mai putin performante decit cele anterioare.
Implementare :
Code:
void shell(long a[],long n)
{
long i, j, k, h, v;
long cols[] = {1391376, 463792, 198768, 86961, 33936, 13776, 4592,
1968, 861, 336, 112, 48, 21, 7, 3, 1};
for (k = 0; k < 16; k++) //parcurgem fiecare limita
{
h = cols[k];
for (i = h; i < n; i++)//insertion sort
{
v = a[i];
j = i;
while (j >= h && a[j-h] > v) //crescator
{
a[j] = a[j-h];
j = j - h;
}
a[j] = v;
}
}
}
================================================================================================
23. Algoritm de sortare prin interclasare (mergesort)
Mulţi algoritmi utili au o structura recursiva: pentru a rezolva o problema data, aceştia sunt apelaţi de către ei înşişi o data sau de mai
multe ori pentru a rezolva subprobleme apropiate. Aceşti algoritmi folosesc de obicei o abordare de tipul divide şi stăpâneşte: ei rup
problema de rezolvat în mai multe probleme similare problemei iniţiale, dar de dimensiune mai mica, le rezolva în mod recursiv şi apoi
le combina pentru a crea o soluţie a problemei iniţiale. Pentru metoda de sortare prin interclasare principiul divide şi stăpâneşte poate fi
privit astfel:
Divide: Împarte şirul de n elemente care urmează a fi sortat în doua subşiruri de câte n/2 elemente.
Stăpâneşte: Sortează recursiv cele doua subşiruri utilizând sortarea prin interclasare.
Combina: Interclasează cele două subşiruri sortate pentru a produce rezultatul final.
20
Figura 1.12 Modul de operare al sortării prin interclasare asupra vectorului A = {5, 2, 4, 6, 1, 3, 2, 6}.Lungimile şirurilor sortate, în curs
de interclasare, cresc pe măsură ce algoritmul avansează de jos în sus. Să observăm că recursivitatea se opreşte când şirul de sortat are
lungimea 1, caz în care nu mai avem nimic de făcut, deoarece orice sir de lungime 1 este deja sortat. Operaţia principala a algoritmului
de sortare prin interclasare este interclasarea a două şiruri sortate, în pasul denumit mai sus “Combina”. Pentru aceasta vom utiliza o
procedura auxiliara, Interclaseaza(A, p, q, r), unde A este un vector şi p, q şi r sunt indici ai vectorului, astfel încât p ≤ q < r. Procedura
presupune că subvectorii A[p..q] şi A[q + 1..r] sunt sortaţi. Ea îi Interclasează pentru a forma un subvector sortat care înlocuieşte
subvectorul curent A[p..r]. O procedura de tip Interclaseaza are un de execuţie de ordinul Θ(n), în care n = r - p + 1 este numărul
elementelor interclasate. Revenind la exemplul nostru cu cărţile de joc, să presupunem că avem doua pachete de cărţi de joc aşezate pe
masa cu faţa în sus. Fiecare din cele doua pachete este sortat, cartea cu valoarea cea mai mica fiind deasupra. Dorim să amestecam cele
doua pachete într-un singur pachet sortat, care să rămână aşezat pe masa cu faţa în jos. Pasul principal este acela de a selecta cartea cu
valoarea cea mai mica dintre cele doua aflate deasupra pachetelor (fapt care va face ca o noua carte să fie deasupra pachetului respectiv)
şi de a o pune cu faţa în jos pe locul în care se va forma pachetul sortat final. Repetam acest procedeu până când unul din pachete este
epuizat. În aceasta faza, este suficient să luam pachetul rămas şi să-l punem peste pachetul deja sortat întorcând toate cărţile cu faţa în
jos.
1: daca p < r atunci
2: q ← [(p + r)/2]
3: Sorteaza-Prin-Interclasare(A, p, q)
4: Sorteaza-Prin-Interclasare(A, q + 1, r)
5: Interclaseaza(A, p, q, r)
i ← p;
j ← q + 1;
k←1;
cat timp i ≤ q şi j ≤ r executa
daca A[i] < A[j] atunci
C[k] ← A[i]
k←k+1
i←i+1
altfel
C[k] ← A[j]
k←k+1
j←j+1
cat timp i ≤ q executa
C[k] ← A[i]
k←k+1
i←i+1
cat timp j ≤ r executa
C[k] ← A[j]
k←k+1
j←j+1
k←1;
pentru i ← p, r executa
A[i] ← C[k]
k←k+1
Utilizam procedura Interclaseaza ca subrutina pentru algoritmul de sortare prin interclasare. Procedura Sorteaza-Prin-
Interclasare(A, p, r) sortează elementele dinsubvectorul A[p..r]. Daca p ¸ r, subvectorul are cel mult un element şi este, prin urmare, deja
sortat. Altfel, pasul de divizare este prezent aici prin simplul calcul al unui indice q care împarte A[p..r] în doi subvectori, A[p..q]
conţinând [n/2]+1elemente şi A[q + 1..r] conţinând [n/2]elemente.4 Pentru a sorta întregul sir A = {A[1],A[2], …A[n]}, vom apela
procedura Sorteaza-Prin-Interclasare sub forma Sorteaza-Prin-Interclasare(A, 1, lungime[A]) unde, din nou, lungime[A] = n. Daca
analizam modul de operare al procedurii, de jos în sus, când n este o putere a lui 2, algoritmul consta din interclasarea perechilor de
şiruri de lungime 1, pentru a forma şiruri sortate de lungime 2, interclasarea acestora în şiruri sortate de lungime 4, şi aşa mai departe,
până când doua şiruri sortate de lungime n/2 sunt interclasate pentru a forma şirul sortat final de dimensiune n. Figura 1.12 ilustrează
acest proces.
Deşi algoritmul Sorteaza-Prin-Interclasare funcţionează corect când numărul elementelor nu este par, analiza bazata pe recurenţa se
simplifica daca presupunem că dimensiunea problemei originale este o putere a lui 2. Fiecare pas de împărţire generează deci doua
subşiruri având dimensiunea exact n=2.
21
================================================================================================
24. Algoritm de sortare rapidă (quicksort)
Descriere
Metoda Divide et Impera este utilizată în sortarea rapidă.
Ideea algoritmului:
1. Se alege o valoare pivot. Se ia valoarea elementului din mijloc ca valoare pivot, dar poate fi oricare altă valoare, care este în
intervalul valorilor sortate, chiar dacă nu este prezentă în tablou.
2. Partiţionare. Se rearanjează elementele în aşa fel încât, toate elementele care sunt mai mari decât pivotul merg în partea dreaptă a
tabloului. Valorile egale cu pivotul pot sta în orice parte a tabloului. În plus, tabloul poate fi împărţit în părţi care nu au aceeaşi
dimensiune (nu sunt egale).
3. Se sortează amândouă părţile.se aplică recursiv algoritmul de sortare rapidă în partea stângă şi în partea dreaptă.
Algoritmul de partiţie în detaliu.
Există 2 indici i şi j, şi la începutul algoritmului de partiţionare i indică primul element al tabloului iar j indică ultimul element din
tablou. La pasul următor algoritmul mută i înainte, pâna când un element cu o valoare mai mare sau egală cu pivotul este găsită.
Indicele j este mutat înapoi, pâna când un element cu valoare mai mică sau egală cu pivotul este găsită. Dacă i<=j atunci i merge pe
poziţia i+1 iar j merge pe poziţia j-1. Algoritmul se opreşte, când i > j
22
Tabloul H[1..n] formează un max-heap dacă
H[i] H[i div 2], i {2,3,...,n}.
Tabloul H[1..n] formează un min-heap dacă
H[i] H[i div 2], i {2,3,...,n}.
De exemplu, arborele binar complet din figura este un max-heap.
şi va fi reprezentat implicit :
1 2 3 4 5
9 7 5 5 7
Crearea unui heap
I. O primă soluţie este de a insera succesiv elementele în heap, plecând iniţial de la heap-ul format dintr-un singur
element:
void CreareHeap()
//organizează vectorul global H, de dimensiune n, ca un heap inserând succesiv elemente;
{ for i = 1;i<=n; i++)
InsertHeap(i-1, H[i]);
}
Inserarea unui nod într-un heap
Fie H[1..n] un heap cu n elemente şi x valoarea ce trebuie inserată. Valoarea x va fi memorată în heap pe poziţia n+1, apoi
se restaurează eventual heap-ul, “promovând” valoarea x spre rădăcină, până când proprietatea de heap este restabilită.
De exemplu,să inserăm valoarea 10 în heap-ul din figura:
Valoarea inserată va ocupa poziţia 6. Pentru a conserva proprietatea de heap, valoarea 10 trebuie promovată până în rădăcină, valorile
de pe drumul parcurs spre rădăcină fiind retrogradate succesiv, cu un nivel.
Rezultă arborele:
23
25.2) Coada de prioritate
O coadă de priorități este o structură de date în care vom putea stoca informații alocate dinamic ca în orice altă structură de date, dar în
plus față de o coadă obișnuită, inserția datelor se face în funcție de un anumit număr ce reprezintă prioritatea acelei informații. Astfel,
atunci când scoatem informații din coadă, vom primi mereu informația cu cea mai mare prioritate la momentul respectiv.
Tipul de dată al fiecărui nod al cozii va trebui să fie o structură care va conține informația care se vrea ținută în coadă, o variabilă de tip
întreg (pozitiv) care va conține prioritatea informației și un pointer către următorul nod. Pentru ca să existe mai multă claritate la nivel
de cod ar fi bine să creem încă o structură care va conține un pointer către primul nod din coadă și o variabilă de tip întreg care va
conține numărul de noduri aflate la un moment dat în stivă. Ne interesează să știm dimensiunea pentru a putea limita dimenziunea
maximă a cozii.
//Definirea tipului nodurilor din coada
typedef struct coada{
TYPE val;
unsigned int priority;
struct coada *next;
}coada;
Inițializarea cozii
Înainte de fiecare utilizare, coada va trebui să fie inițializată. Astfel, câmpul first din structura de tip coadă va trebui inițializat cu NULL
iar câmpul size va trebui inițializat cu 0.
void initpcoada(pcoada **coad)
{
//Alocam memorie pentru tipul coada de prioritati
//si punem valorile initiale in campurile structurii.
(*coad) = (pcoada *) malloc(sizeof(pcoada));
(*coad)->first = NULL;
(*coad)->size = 0;
return;
}
Adăugarea în coadă
Fucția pentru adăugarea unui element va trebui să știe în ce coadă să adauge informația, ce informație să adauge și cu ce prioritate.
Dacă câmpul size din coadă a ajuns deja la dimensiunea maximă permisă (cea definită în header), nici nu are rost să mergem mai
departe, afișăm un mesaj de eroare și ieșim din funcție.
Este bine să alocăm memoria pentru elementul pe care vrem să-l introducem dinainte să ne dăm seama unde trebuie să-l plasăm, altfel
ar trebui să scriem mult cod pentru fiecare caz în parte. Știind că informația are o prioritate, elementul cu prioritatea cea mai mare va fi
mereu la început. Dacă coada este goală, evident, indiferent de prioritate, elementul se va adăuga la început. Altfel, trebuie să
parcurgem coada până întâlnim un element care are o prioritate mai mică decât prioritatea elementului pe care vrem să-l adăugăm și să-l
plasăm înaintea lui. Evident, nu putem reface legăturile dacă verificăm prioritatea în funcție de elementul curent, din acest motiv,
comparăm de fiecare dată cu elementul imediat următor. Dacă am ajuns la sfârșit și nu s-a adaugat încă elementul în coadă, îl vom
adăuga la sfârșit. Funcția de adăugare o puteți vizualiza în codul sursă aflat la sfârșitul articolului.
Preluarea elementelor din coadă
Funcția care preia elementele va returna efectiv informația primului element din coadă (adică cel de prioritatea cea mai mare), dar
inainte de asta, îl va elimina din coadă, transformând primul element de după el în primul element din coadă.
Funcția va afișa mesaj de eroare când se va încerca preluarea elementului când coada este goală.
TYPE getpcoada(pcoada **coad)
{
TYPE returnValue;
//Luam elemente din coada atata timp cat ea nu este goala.
if((*coad)->size>0)
{
returnValue = (*coad)->first->val;
(*coad)->first = (*coad)->first->next;
(*coad)->size--;
}
else
{
//Daca coada este goala afisam mesaj de eroare.
//Functia va returna totusi ce se gaseste in memorie la momentul
//in care se defineste variabila returnValue.
printf("\nQueue is empty.\n");
}
return returnValue; }
24
26. Algoritmi de sortare utilizînd coada de prioritate și arbore binar heap
26.1) Algoritmul de sortare coada de prioritate
Aplicatiile pricipale ale arborilor heap sint:
- implementarea tipului de date abstract "coada cu prioritate".
- agoritmul de sortare HeapSort.
O coada cu prioritate este o structura de date de tip "container" pentru care se definesc urmatoarele functii de acces:
INSERT(C, X) - pune in coada C atomul X;
REMOVE(C) - scoate din coada C si returneaza atomul cu valoarea cea mai mare a cheii. Arborii heap pot fi reprezentati explicit
(reprezentarea standard a arborilor binari) sau implicit. Reprezentarea implicita a arborilor heap este cea mai folosita, si ea va face
obiectul celor prezentate in continuare. Vom realiza in cotinuare operatiile INSERT si REMOVE pentru arbori heap in reprezentare
implicita. Arborii heap la care ne referim sint formati din k nivele complete iar pe nivelul k+1 nodurile sint grupate in partea stinga:
Operatia INSERT
Se adauga X la sfirsitul vectorului si apoi se promoveaza spre radacina pina ajunge in pozitia in care structura de heap a arborelui in
reprezentare implicita este respectata.
De exemplu, se insereaza valoarea 35 in heapul de mai sus:
Pentru a pastra structura arborelui vloarea 35 trebuie promovata pana pe nivelul 2. Valorile de pe portiunea din ramura parcursa vor fi
retrogradate cu un nivel. Rezulta arborele:
In algoritmul urmator promovarea valorii inserate se face "din aproape in aproape", aceasta urcind in arbore din nivel in nivel.
25
Operatia REMOVE
Operatia REMOVE sterge din heap valoarea cea mai mare, valoare care se afla intotdeauna in radacina arborelui heap, deci in prima
pozitie a heap-ului. Pentru a reface structura de heap se aduce in prima pozitie ultimul element din vector si apoi se retrogradeaza pina
se reface structura de heap. La retrogradarea cu un nivel valoarea din radacina este schimbata cu descendentul care are valoarea
maxima.
Iata algoritmul:
REMOVE(A,N) // N trebuie sa fie pasat prin referinta
{
IF N=0 THEN eroare
ELSE │ret_val = A[1]; //Valoarea de returnat
│A[1] = A[N]; //Aduce ultimul element in radacina
│N = N-1; //Micsoreaza heapul
│parinte = 1;
│fiu = 2;
│WHILE fiu<=N DO //parcurge o ramura in sens descendent
│ │IF (fiu+1<=N) and (A[fiu]<A[fiu+1]) THEN
│ │ fiu = fiu+1; // este ales fiu cel mai mare
│ │IF A[fiu]>A[parinte] THEN // daca parinte<max(fii)
│ │ │SCHIMBA(A[parinte],A[fiu]); // retrogradeaza
│ │ │parinte := fiu; │
│ │ │fiu := fiu*2; │ // coboara
│ │ELSE fiu=N+1; // pentru parasirea buclei
}
Elementul adaugat, A[i], va fi retrogradat la fel cu modul de retrogradare folosit in procedura REMOVE.
Construirea heap-ului prin aceasta metoda este un algoritm de complexitate O(n*log n).
Heapsort este o metodă de sortare care se bazează pe proprietatea de heap.
================================================================================================
27. Funcţii standard qsort ( ) şi bsearch ( ) în limbajul C
27.1) Functii standart qsort()
27.2) Functii standart bsearch()
27
28. Tehnici de programare. Clasificarea şi caracteristica generala
O clasificare a cunoasterii importanta pentru programare în general se refera la continutul acesteia, existând urmatoarele trei
categorii: cunoastere procedurala, declarativa sitacita. Cunoasterea procedurala se refera la a sti cum sa faci ceva, adica a sti mijloacele
pentru a ajunge la un anume rezultat . Cunoasterea declarativa se refera la a sti daca un enunt este adevarat sau fals (o asemenea
cunoastere ar trebui sa se foloseasca în justitie!). Cunoasterea tacita (denumita uneori si neconditionata, datorata inconstientului) este
cunoasterea ce nu poate fi exprimata prin limbaj (un exemplu de acest fel este cunoasterea privind mersul pe bicicleta).
Tehnici de programare:
Clasa limbajelor procedurale se bazeaza pe notiunea de algoritm. Acesta este o metoda de a rezolva o problema într-un numar finit de
pasi, folosind o multime finita de operatii (instructiuni) cunoscute, care executate într-o ordine bine stabilita, pornind de la un set de
valori (intrarile), produc în timp finit un alt set de valori (iesirile). Limbajele imperative sau orientate pe instructiune (în engleza
”statement-oriented”) au ca o caracteristica esentiala aceea ca programul este un sir de instructiuni - comenzi, precizând calculatorului
operatiile pe care trebuie sa le execute la momente succesive de timp. Aceste limbaje au fost dezvoltate pentru a elibera programatorul
de lucrul în cod masina si în acest sens au fost
prevazute cu facilitati privind manipularea variabilelor, operatiile de atribuire si asigurarea repetarii unor secvente.
Programarea functionala este centrata pe notiunea de functie. Aici programul este vazut ca o functie, care se obtine din compunerea mai
multor functii simple, fie functii sistem, fie definite de utilizator.
O alta trasatura pe care au dorit sa o realizeze limbajele functionale este asa numita transparenta referentiala. Aceasta este o
caracteristica a limbajului folosit în matematica si care nu a fost respectata în limbajele imperative. Ea se refera la faptul ca întelesul
unui întreg este complet determinat de întelesul partilor, neputând apare efecte parazite datorita combinarii partilor sau unor relatii de
ordine dintre ele.
================================================================================================
29. Tehnica de programare „Forța brută” și tehnica de programare „ Divide et empera”. Exemple
29.1) Tehnica de programare „Forța brută”
Programarea Dinamică este cea mai flexibilă tehnica din programare. Cel mai ușor mod de a o înțelege presupune parcurgerea cât mai
multor exemple.
O problema clasică de Programare Dinamică este determinarea celui mai lung subșir strict crescător dintr-un șir de numere. Un subșir al
unui șir este format din caractere (nu neapărat consecutive) ale șirului respectiv, în ordinea în care acestea apar în șir.
Exemplu: pentru șirul 24 12 15 15 8 19 răspunsul este șirul 12 15 19
Se observa că dacă încercăm o abordare greedy nu putem stabili nici măcar elementul de început într-un mod corect. Totuși, problema
se poate rezolva ”muncitorește” (forța brută) folosind un algoritm care alege toate combinațiile de numere din șir, validează că șirul
obținut este strict crescător şi îl reține pe cel de lungime maximă, dar aceasta abordare are complexitatea temporală O(N!). Cu
optimizări, este posibil sa se ajungă la O(2N).
O metoda de rezolvare mai eficientă folosește Programarea Dinamică. Începem prin a stabili pentru fiecare element lungimea celui mai
lung subșir strict crescător care începe cu primul element și se termină în elementul respectiv. Numim aceasta valoare best i și aplicăm
formula recursivă besti = 1 + max(bestj), cu j<i și elemj < elemi.
Aplicând acest algoritm obținem:
elem 24 12 15 15 8 19 best 1 1 2 2 1 3
Pentru 24 sau 12 nu există nici un alt element în stânga lor strict mai mici decât ele, de aceea au best egal cu 1. Pentru elementele 15 se
poate găsi în stânga lor 12 strict mai mic decât ele. Pentru 19 se găsește elementul 15 strict mai mic decât el. Cum 15 deja este capăt
pentru un subșir soluție de 2 elemente, putem spune că 19 este capătul pentru un subșir soluție de 3 elemente.
Cum pentru fiecare element din mulțime trebuie să găsim un element mai mic decât el şi cu best maxim, avem o complexitate O(N)
pentru fiecare element. În total rezultă o complexitate O(N 2). Se pot obține şi rezolvări cu o complexitate mai mica folosind structuri de
date avansate. Atât soluția în O(N2), cât si o soluție în O(N log N) poate fi găsită la [5]. Tot acolo se poate găsi și o lista de probleme
mai dificile ce folosesc tehnica Programării Dinamice, adaptată în diferite forme.
Pentru a găsi care sunt elementele ce alcătuiesc subșirul strict crescător putem să reținem și o „cale de întoarcere”. Reconstrucția astfel
obținută are complexitatea O(N). Exemplu: subproblema care se termina în elementul 19 are subșirul de lungime maximă 3 şi a fost
calculată folosind subproblema care se termină cu elementul 15 (oricare din ele). Subșirul de lungime maximă care se termină în 15 a
fost calculat folosindu-ne de elementul 12. 12 marchează sfârșitul reconstrucției fiind cel mai mic element din subșir.
#include <stdio.h>
int v[10],n;
int max(int i, int j)
{
int a, b, m;
if (i==j) return v[i];
else
{
m = (i+j)/2;
a = max(i, m);
b = max(m+1, j);
if (a>b) return a;
else return b;
}
}
int main( )
{
printf("n="); scanf(“%d”,&n);
for (int i=1; i<=n; i++)
{
printf("v[%d]=",i); scanf(“%d”,&v[i]);
}
printf("max=%d",max(1,n));
return 0; }
================================================================================================
30. Tehnica de programare Backtracking și programarea dinamică. Exemple
30.1) Tehnica de programare Backtracking
Backtracking este o metodă de parcurgere sistematică a spaţiului soluţiilor posibile al unei probleme. Este o metodă generală
de programare, şi poate fi adaptă pentru orice problemă pentru care dorim să obţinem toate soluţiile posibile, sau să selectăm o soluţie
optimă, din mulţimea soluţiilor posibile. Backtracking este însă şi cea mai costisitoare metodă din punct de vedere al timpului de
execuţie.
Forma generală a unei funcţii backtracking Implementarea recursivă a algoritmului furnizat de metoda backtracking, este mai
naturală şi deci mai uşoară. Segmentul de stivă pus la dispoziţie prin apelul funcţiei este gestionat în mod automat de sistem. Revenirea
la pasul precedent se realizează în mod natural prin închiderea nivelului de stivă.
void BK(int k) //k-poziţia din vector care se completează
{int i;
for (i=1; i<=nr_elemente_Sk; i++) //parcurge elementele mulţimii Sk
{ v[k]=i; //selectează un element din mulţime
if (validare(k)==1) //validează condiţiile de continuare ale problemei
{ if (solutie(k)==1) //verifică dacă s-a obţinut o soluţie
afisare(k); //afişează soluţia
else
BK(k+1); //reapelează functia pentru poziţia k+1
}
} //dacă nu mai există nici un element neselectat în mulţimea Sk,
} //se închide nivelul de stivă şi astfel se revine pe poziţia k-1 a
//vectorului
//execuţia funcţiei se încheie, după ce s-au închis toate nivelurile stivei, înseamnă că în vectorul v nu
mai poate fi selectat nici un elemente din multimile Sk
Exemplu - Sa consideram problema submultimilor de suma data care consta in urmatoarele: Fie A = (a1, a2, ..., an) cu ai > 0, oricare ar
fi i. Fie M inclus in R. Se cauta toate submultimile B ale lui A pentru care suma elementelor este M.
Pentru a putea realiza problema prin metoda backtracking vom reprezenta solutia sub forma x = (x1, ..., xn) unde xi = 1 daca ai inclus in
B si xi = 0 in caz contrar. Sa ne situam in ipoteza ca n = 4.
29
Castigul obtinut prin introducerea conditiilor de continuare consta in faptul ca, daca intr-un varf ele nu mai sunt verificate, se va renunta
la parcurgerea subarborelui care are ca radacina acest varf.
Acest exemplu permite prezentarea unei variante a metodei backtracking. Intr-adevar, sa consideram drept solutie posibila o valoare k
<= n impreuna cu cuplul (x1, ...,xk) unde pentru i inclus in {1, ..., k}, xi reprezinta indicele elementului pe care il introducem in B.
Evident xi este diferit de xj pentru i diferit de j. Pentru a nu se repeta solutii, vom presupune x1 < x2 < ... < xn.
Diferite variante ale metodei backtracking nu schimba esenta ei care consta in faptul ca este reprezentabila pe un arbore care este
parcurs "coborand" in arbore numai daca exista sanse de a ajunge la o solutie rezultat.
In continuare, problemele care vor fi prezentate vor urma o schema generala si anume:
- se va testa daca am obtinut o solutie, situatie in care aceasta se va retine;
- daca nu am obtinut o solutie se incearca plasarea unui nou element in vectorul solutie cu respectarea conditiilor de continuare.
- daca nu se reuseste plasarea unui nou element si spatiul valorilor posibile de plasat s-a epuizat, se revine la pozitia anterioara si se
incearca sa se plaseze pe ea un alt element.
Faptul ca dupa plasarea unui element in vectorul solutie algoritmul presupune plasarea unui element pe pozitia imediat urmatoare, adica
de fapt reluarea algoritmului, conduce posibilitatea abordarii recursive a algoritmilor de tip backtracking. Acest lucru permite o scriere
mult mai scurta si mai simpla a algoritmilor si apoi a programelor care ii implementeaza. Astfel, general, un algoritm backtracking
poate fi prezentat astfel:
In functie de problema concreta, in algoritmul descris mai sus se vor modifica doar instructiunea pentru, conditiile interne si cele de
solutie, structura algoritmului pastrandu-se.
PRODUS CARTEZIAN
Se dau n mulţimi, ca cele de mai jos:
S1={1,2,3,...,w1}
S2={1,2,3,...,w2}
.........................
Sn={1,2,3,...,wn}
Se cere să se gnereze produsul lor cartezian.
Exemplu:
pemtru n=3 şi urmăroarele mulţimi
S1={1,2} w1=2
S2={1,2,3} w2=3
S3={1,2} w3=2
produsul cartezian este:
S1 xS2 xS3 ={ (1,1,1),(1,1,2),(1,2,1),(1,2,2),(1,3,1),(1,3,2),
(2,2,1),(2,1,2),(2,2,1),(2,2,2),(2,3,1),(2,3,2)}
Prin urmare o soluţie este un şir de n elemente, fiecare element iєSi, cu i=1,n
Să analizăm exemplul de mai sus:
1. La pasul k selectăm un element din mulţimea Sk ={1,2,3,...,wk}.
2. Elementele unei soluţii a produsului cartezian, pot să se repete, pot fi în orice ordine, iar valori străine nu pot apare, deoarece le
selectăm doar dintre elementele mulţimii Sk . Prin urmare nu trebuie să impunem
condiţii de continuare.
3. Obţinem o soluţie în momentul în care am completat n elemente în vectorul v.Vom memora numărul de elemente al fiecăerei mulţimi
Sk , într-un vector w. Soluţiile le vom construi pe rând în vectorul v.
31