Documente Academic
Documente Profesional
Documente Cultură
1
#include <stdio.h>
#include <conio.h>
#define MaxS10
typedef int ElTip; // ElTip - definire tip element din stiva
typedef struct {
ElTip elem [MaxS];// spatiu pentru elem. stivei
int sp;
/* index/ referinta la varful stivei (stack pointer) */
} TStiva, *AStiva;
void eroare (char *meserr){
printf("%s\n", meserr);
}
void InitStiva (AStiva rs){
/* initializare stiva, sp nu refera un element (stiva goala) */
rs->sp=-1;
}
int StivaGoala (AStiva rs){
return (rs->sp == -1);
}
int StivaPlina (AStiva rs){
/* stiva este plina, index=MaxS-1, indice maxim,
nu se mai pot introduce elemente in stiva */
return (rs->sp == MaxS-1);
}
void Push (AStiva rs, ElTip el){
/* introduce element in stiva, daca nu este plina */
if (StivaPlina (rs))
eroare ("Stiva Plina !!!");
else {rs->sp=rs->sp+1;
rs->elem[rs->sp]=el;
}
}
ElTip Pop (AStiva rs){
/* extrage element din stiva, daca nu este goala */
ElTip e;
if (StivaGoala (rs))
{eroare ("Stiva Goala !!!");
return 0;
}
else
{e=rs->elem[rs->sp];
rs->sp=rs->sp-1;
return e;
}
}
ElTip CapStiva (AStiva rs){
if (StivaGoala (rs))
{eroare ("Stiva Goala !!!");
2
return 0;
}
else
return rs->elem[rs->sp];
}
void AfisareStiva (AStiva rs){
/* se creaza o copie a stivei curente, pentru a utiliza functiile
definite pentru operare stiva (Pop, CapStiva, StivaGoala) */
TStiva s=*rs;
AStiva pt=&s;
int nvl=0; /* Numar de valori afisate pe linie */
if (pt->sp == -1)
eroare("Stiva Goala !!!\n");
else
while (!StivaGoala (pt))
{printf ("%d -> ", CapStiva (pt));
Pop (pt);
if (++nvl % 10 == 0)
printf("\n");
}
printf("NULL\n");
}
void Afisare_Poz_sp (AStiva rs){
printf("Referinta (indexul), sp: %d\n", rs->sp);
if (!rs->sp)
printf("\tSTIVA GOALA!!!\n");
else if (rs->sp == MaxS-1)
printf("\tSTIVA PLINA !!! ELIMINATI ...\n");
}
void main (void){
TStiva s, *rs;
char optiune[20];
ElTip n;
clrscr();
printf("Exemplu de stiva realizata utilizand un tablou.\n");
printf("Stiva este alcatuita din numere intregi.\n");
InitStiva (rs);
printf("\nOptiuni Introduc, Extrag, Afis stiva, Cap stiva, Poz sp, Stop: ");
gets(optiune);
while (optiune[0] != '\0' && optiune[0] != 's' && optiune[0] != 'S'){
switch(optiune[0]){
case 'i':
case 'I':if (StivaPlina(rs)){
eroare("\n\tSTIVA PLINA !!! ELIMINATI...\n");
break;}
printf("Introd. nr. (CTRL-Z, Enter-Stop Introd): ");
while (scanf("%d", &n) == 1 && !StivaPlina(rs)){
Push (rs, n);
3
if (StivaPlina (rs)){
eroare("Stiva PLINA !!! ELIMINATI.....\n");
break;
}
printf("Introd. nr. (CTRL-Z, Enter-Stop Introd): ");
}
fflush(stdin);
break;
case 'e':
case 'E': optiune[0]='D';
while ( !StivaGoala (rs) &&
(optiune[0] == 'D' || optiune[0] == 'd') )
{printf ("Se extrage primul element din stiva: %d\n",
CapStiva (rs));
Pop (rs);
if (!StivaGoala (rs)){
printf("Continuati extragerea (Da/Nu): ");
gets(optiune);
fflush(stdin);
}
}
if (StivaGoala (rs))
printf("Stiva Goala !!! INTRODUCETI ELEMENTE \n");
break;
case 'c':
case 'C': if (!StivaGoala (rs))
printf("Afisare element din capul stivei: %d\n",
CapStiva (rs));
else
eroare("Stiva Goala !!!\n");
break;
case 'a':
case 'A':AfisareStiva (rs);
break;
case 'p':
case 'P': Afisare_Poz_sp (rs);
break;
}
printf("\nOptiuni Int, Ext, Afis stiva, Cap stiva, Poz sp, Stop: ");
gets(optiune);
}
}
O coadă este o listă liniară în care inserările se fac doar în capul listei (prim), iar extragerile
doar din coada listei (ultim). Cozile se numesc şi liste FIFO (First In First Out). O reprezentare
secvenţială pentru o coadă se obţine prin utilizarea unui tablou C[n], pe care îl tratăm ca şi cum ar fi
circular: după locatia C[n-1] urmează locaţia C[0]. Dacă cei doi indecşi (ultim şi prim) referă cele două
locaţii, sau două locaţii în această ordine (k-1, respectiv k) coada este goală; dacă cei doi indecşi sunt
4
egali (k), atunci coada conţine un singur element, iar dacă ultim=k-2, iar prim=k, atunci coada este
plină.
5
ElTip e;
if (CoadaGoala (pc))
{eroare ("Coada Goala !!!");
return 0;
}
else
{e=pc->elem[pc->prim];
pc->prim=IncIndexC(pc->prim);
return e;
}
}
ElTip CapCoada (ACoada pc){
if (CoadaGoala (pc))
{eroare ("Coada Goala !!!");
return 0;
}
else
return pc->elem[pc->prim];
}
void AfisareCoada (ACoada pc){
/* se creaza o copie a cozii curente, pentru a utiliza functiile
definite pentru operare coada (ScotElC, CapCoada, CoadaGoala) */
TCoada c=*pc;
ACoada pt=&c;
int nvl=0; /* Numar de valori afisate pe linie */
if (CoadaGoala(pt))
eroare("Coada Goala !!!\n");
else
while (!CoadaGoala (pt))
{printf ("%d -> ", CapCoada (pt));
ScotElC (pt);
if (++nvl % 10 == 0)
printf("\n");
}
printf("NULL\n");
}
void Afisare_Poz_Prim_Ultim (ACoada pc){
printf("Cei doi indecsi (referinte), prim si ultim: %d(p), %d(u)\n",
pc->prim, pc->ultim);
}
void main (void){
TCoada c, *pc;
char optiune[20];
ElTip n;
clrscr();
printf("Exemplu de coada realizata utilizand un tablou.\n");
printf("Coada este alcatuita din numere intregi.\n");
InitCoada (pc);
6
printf("\nOptiuni Int, Ext, Afis coada, Cap coada, Poz p/u, Stop: ");
gets(optiune);
while (optiune[0] != '\0' && optiune[0] != 's' && optiune[0] != 'S'){
switch(optiune[0]){
case 'i':
case 'I':if (CoadaPlina(pc)){
eroare("\n\tCOADA PLINA !!! ELIMINATI...\n");
break;}
printf("Introd. nr. (CTRL-Z, Enter-Stop Introd): ");
while (scanf("%d", &n) == 1){
AdaugElC (pc, n);
if (CoadaPlina (pc)){
eroare("Coada PLINA !!! ELIMINATI.....\n");
break;
}
printf("Introd. nr. (CTRL-Z, Enter-Stop Introd): ");
}
fflush(stdin);
break;
case 'e':
case 'E': optiune[0]='D';
while ( !CoadaGoala (pc) &&
(optiune[0] == 'D' || optiune[0] == 'd') )
{printf ("Se extrage primul element din coada: %d\n",
CapCoada (pc));
ScotElC (pc);
if (!CoadaGoala (pc)){
printf("Continuati extragerea (Da/Nu): ");
gets(optiune);
fflush(stdin);
}
}
if (CoadaGoala (pc))
printf("Coada Goala !!! INTRODUCETI ELEMENTE \n");
break;
case 'c':
case 'C': if (!CoadaGoala (pc))
printf("Afisare element din capul cozii: %d\n",
CapCoada (pc));
else
eroare("Coada Goala !!!\n");
break;
case 'a':
case 'A':AfisareCoada (pc);
break;
case 'p':
case 'P': Afisare_Poz_Prim_Ultim (pc);
break;
7
}
printf("\nOptiuni Int, Ext, Afis coada, Cap coada, Poz p/u, Stop: ");
gets(optiune);
}
}
Structurile de date utilizate anterior (tablouri, sau structuri, uniuni) nu sunt structuri de date
dinamice, deoarece ramân fixe în ce priveşte dimensiunea lor pe toată durata lor de viaţă, adică pe
durata execuţiei funcţiei respective. Astfel de exemple sunt tablourile sau structurile, ale căror
dimensiuni pot fi cunoscute de la declararea lor.
Sunt însă situaţii în care structurile de date trebuie să se modifice ca formă sau ca mărime pe
parcursul programului; astfel de structuri sunt listele, arborii, grafurile şi mulţimile disjuncte. Deoarece
pentru astfel de structuri numărul de componente şi legăturile dintre acestea nu sunt dinainte fixate, ele
se crează dinamic, pe durata execuţiei programului.
Variabilele statice sunt toate variabilele declarate în secţiunile declarative ale unei funcţii sau
ale unui bloc, având aceeaşi durată de viaţă cu cea a funcţiei sau blocului respectiv în care au fost
declarate.
Variabilele dinamice se generează şi se distrug dinamic pe parcursul execuţiei programului.
Spre deosebire de variabilele statice, variabilele dinamice nu au un nume (identificator), referirea la ele
făcându-se în mod indirect prin intermediul variabilelor pointer (referinţă).
O variabilă referinţă (pointer) are ca valoare o adresă a unei variabile dinamice. Variabilele
pointer se folosesc pentru a creea variabile dinamice (a aloca memorie pentru acestea) şi pentru a le
distruge când nu mai sunt necesare (deci pentru a elibera memoria alocată).
O structură de date dinamice este constituită dintr-un număr de elemente referite prin pointeri.
Se pot introduce noi elemente sau suprima cele vechi, în timpul execuţiei programului. Adresele reale
ale elementelor nu au importanţă, deoarece legăturiile logice dintre elemente sunt stabilite de pointeri şi
nu prin poziţia lor relativă din memorie, ca în cazul tablourilor.
Fiecare element al unei structuri dinamice poate conţine unul sau mai multe câmpuri cu referire
(pointeri) la alte elemente, care pot fi de orice tip (simplu, tablou, structură sau pointer). De exemplu,
componentele unei listei sunt structuri cu două câmpuri: o valoare de tip întreg şi un pointer la
următoarea componentă:
struct intrare
{
int valoare ;
struct intrare *urmator ;
};
Crearea efectivă a variabilei dinamice, deci alocarea de memorie, se face utilizând una dintre
funcţiile malloc(), calloc() sau new (C++). Distrugerea unei variabile dinamice, deci eliberarea
spaţiului de memorie ocupat de aceasta, se realizează cu una dintre funcţiile free() sau delete (C++).
Aceste funcţii sunt definite după cum urmează.
void *malloc(size_t dimensiune), alocă un spaţiu continuu de ‘dimensiune’ octeţi, returnând un pointer
de tipul void la începutul blocului alocat, dacă s-a realizat alocarea cu succes, sau pointerul NULL dacă
nu se poate aloca memorie;
void *calloc (size_t n , size_t dim), alocă spaţiu continuu pentru n elemente de date, fiecare necesitând
‘dim’ octeţi. Spaţiul alocat este iniţializat cu zero. Dacă s-a alocat, returnează un pointer la începutul
spaţiului alocat, altfel returnează pointerul NULL.
Când se termină operarea cu memoria ce a fost alocată dinamic, cu funcţiile malloc() sau calloc(),
spaţiul de memorie poate fi eliberat (cedat sistemului) prin aplicarea funcţiei free():
8
void *free (void *pointer), eliberează blocul de memorie referit de pointer, ce a fost alocat de una dintre
funcţiile calloc( ) sau malloc( ).
Aceste funcţii se află în fişierul antet standard <stdlib.h> (şi <alloc.h>).
În C++ pentru operaţii de alocare / eliberare de memorie se utilizează operatorii (funcţiile) new
şi delete.
Operatorul new încearcă să creeze un obiect de tipul specificat (nume) prin alocarea, dacă e
posibil, de sizeof (nume) octeţi în memoria disponibilă (denumită “heap”). Durata de alocare e până la
eliberare, cu operatorul delete sau până la sfârşitul programului.
Dacă alocarea se face cu succes, new returnează un pointer către această zonă, altfel un pointer
nul. Trebuie testat pointerul returnat, la fel ca la funcţia malloc. Dar, spre deosebire de aceasta, new
calculează dimensiunea spaţiului de memorie necesar (sizeof(nume)) fără a fi nevoie de utilizarea
explicită a operatorului. De asemenea pointerul returnat este de tipul corect, pointer la “nume”, fără să
fie necesară prezenţa operatorului de conversie: (nume *)malloc( sizeof( nume)). Sintaxa sa de utilizare
este: ptr_nume = new nume;
Respectiv delete ptr_nume;
Utilizarea variabilelor dinamice asigură o administrare eficientă a spaţiului de memorie.
‘Y’
‘X’
‘Z’
care porneşte de la o singură variabilă, pointerul primul şi trei elemente de tip st_litera. Într-o lista de
tip stivă elementele se pot extrage în ordinea inversă introducerii în listă. Să vedem cum se poate creea
prin program o astfel de structură. În primul rând declaraţiile pentru o astfel de structură vor fi:
struct st_litera
{
char car;
struct litera *urmator;
};
struct st_litera *vr, *primul;
Iniţial lista este vidă, deci putem scrie:
primul = NULL;
În continuare este nevoie să inserăm un element în această listă. În primul rând trebuie creat
acest element:
vr = (struct st_litera *) malloc ( sizeof ( struct st_litera));
primul vr
9
După aceasta trebuie actualizate câmpurile noului element:
vr -> car = ‘Z’;
primul vr
‘Z’
vr -> urmator = primul;
primul vr
NULL
‘Z’
Valoarea curentă pentru primul este NULL şi efectul acestei instrucţiuni este de a pune NULL la
vr -> urmator. Pentru ca variabila primul să refere primul element din listă vom realiza atribuirea:
primul = vr;/* acum şi variabila primul va referi adresa referită de vr */
vr
NULL
‘Z’
primul
Pentru a insera un nou element (caracterul ‘Y’) în listă vom proceda în aceeaşi manieră:
vr = (struct st_lista *) malloc ( sizeof ( struct st_lista ));
vr -> car = ‘Y’;
vr -> urmator = primul;
şi se obţine:
primul vr
NULL
‘Y’ ‘Z’
primul vr
NULL
‘Y’ ‘Z’
În mod asemănător se introduce în lista, de tip stivă, şi elementul următor pentru a obţine lista
iniţială, prezentată la începutul paragrafului.
10
Să utlizăm, în continuare, această tehnică de inserare pentru a crea o listă înlănţuită cu o
structură de tip stivă. Vom scrie programul pentru citirea unui şir de caractere, memorarea lor într-o
astfel de listă şi tipărirea lor în ordine inversă.
11
între caracterele introduse în listă până în acel moment) şi va returna 0 (fals) dacă caracterul este găsit
în listă.
12
int gasit;
ptr=prim; /* initializare pointer curent cu primul din lista */
gasit=0; /* presupunem ca nu l-am gasit inca */
while (ptr != NULL && !gasit) /* cat timp nu s-a terminat lista si nu
l-am gasit */
{ /* se continua parcurgerea listei */
gasit=(ptr->car == ch);
ptr=ptr->urmator;
}
return (!gasit);
}
Să considerăm acum un alt tip de listă înlănţuită, şi anume una de tip FIFO (First In First Out),
adică primul intrat în listă este şi primul care poate fi extras. O astfel de structură mai este denumită şi
coadă sau coadă (şir) de aşteptare, întrucât elementele se introduc pe la un capăt şi sunt accesibile pe la
celălalt capăt, deci la un moment dat este accesibilă numai prima componentă introdusă în listă.
Vom considera de această dată problema construirii unei liste înlănţuită formată din cuvinte şi
apoi tipărirea acestor cuvinte în ordinea în care au fost introduse. Vom defini două funcţii: una pentru
ataşarea unui element (cuvânt) la sfârşitul listei, iar cea de-a doua pentru extragerea, adică afişarea
cuvintelor din lista de tip coadă, cu eliberarea spaţiului de memorie alocat dinamic pentru această
structură.
13
}
void extragere ( struct st_cuvant * primu)
{
struct st_cuvant *ptr;
while (primu)
{ptr=primu; /* memorat pentru a elibera spatiul referit de el */
printf (" %s -> ", primu->cuvant);
ncl++;
if (ncl%5==0)
printf("\n");
primu=primu->urmator;
free(ptr); /* eliberarea spatiului alocat pointerului */
}
printf ("NULL\n");/* sfarsit lista sau lista vida daca primu == NULL */
}
void main()
{
struct st_cuvant *primul, *ultimul;
char cuv[DIMCV];
clrscr();
printf("Programul construieste o lista de tip coada cu cuvinte de la"
" tastatura\n");
printf("Programul ia sfarsit cu un cuvant vid, adica RETURN,"
" la inceput de linie.\n");
printf("Cuvant de introdus in lista:");
gets(cuv);
primul=(struct st_cuvant *) malloc(sizeof(struct st_cuvant));
primul->cuvant=strdup(cuv);
primul->urmator=NULL;
ultimul=primul;
printf("Cuvant de introdus in lista:");
gets(cuv);
while (strcmp(cuv, SIR_VID))
{
if(!(ultimul=atasare_coada (ultimul, cuv)))
break;
printf("Cuvant de introdus in lista:");
gets(cuv);
}
extragere (primul);
}
O altă operaţie necesară în aplicaţii cu liste înlănţuite este introducerea unui element în orice
punct din listă, astfel încât elementele listei să respecte o anumită ordine. Vom defini o funcţie care
inserează un nou element, marcat prin referinţa nou, după un element din listă, marcat prin variabila
pointer curent.
void insereaza_dupa (struct st_orice_tip *curent; struct st_orice_tip *nou)
{nou -> urmator = curent -> urmator;
14
curent -> urmator = nou;
}
Această funcţie se utilizează când avem o variabilă pointer (cum este pointerul curent) după
care se inserează noul (nou) element al listei. Inserarea se poate face şi înaintea unui element din listă,
dar este mai dificilă deoarece lista este unidirecţională şi nu ne putem referi la predecesorul unui
element, ci numai la succesorul său. Această problemă se poate rezolva în două moduri: fie prin
parcurgerea listei de la început şi determinarea poziţiei unde se face inserarea, fie prin inserarea după
elementul respectiv (curent), urmată de interschimbarea conţinuturilor celor două variabile, mai puţin a
legăturilor (referinţelor).
În primul caz funcţia pentru inseare va fi următoarea:
void insereaza_inainte (struct st_orice_tip *curent, struct st_orice_tip *nou,
struct st_orice_tip *primul)
{ /* se insereaza elementul ‘nou’ in fata celui ‘curent’ */
struct st_orice_tip aici; /* va reprezenta locul unde se insereaza ‘nou’ */
if ( curent == primul ) /* testeaza daca trebuie inserat in fata primului element */
{ /* daca da, se pune primul in lista */
nou -> urmator = curent; /* deci ‘curent’ devine ‘primul’ */
primul = curent;
}
else
{/* daca nu, i se determina pozitia de inserare in lista */
aici = primul; /* care va fi intre ‘aici’ si ‘curent’ */
while ( aici -> urmator != curent )
aici = aici -> urmator; /* ‘aici’ va fi predecesorul lui ‘curent’*/
aici -> urmator = nou; /* si se insereaza intre cei doi pointeri */
nou -> urmator = curent;
}
}
Dacă lista este foarte mare, cea de-a doua funcţie este mai bună, deoarece nu parcurge lista,
ceea ce ar lua destul timp. Dacă, însă, lista este scurtă, dar fiecare element are asociată multă
informaţie, adică fiecare componentă este destul de mare, este preferabilă prima funcţie.
Cea de-a doua metodă nu poate fi folosită dacă structurile asociate componentelor conţin şi alte
variabile referinţă (pointeri).
15
Operaţia inversă celei de inserare este cea de eliminare din listă a unui element. Eliminarea
elementului “curent”, chiar dacă este primul din listă, se face astfel:
void eliminare_elem_curent (struct st_orice_tip *curent, struct st_orice_tip *primul)
{
struct st_orice_tip *elimin; /* pointer local la elementul de eliminat (curent)*/
if ( curent == primul ) /* daca trebuie eliminat chiar 'primul' */
primul = curent -> urmator; /* devine 'primul' campul 'urmator' */
else
{ /* altfel trebuie determinata pozitia elementului de eliminat */
elimin = primul;
while ( elimin -> urmator != curent )
elimin = elimin -> urmator;
elimin -> urmator = curent -> urmator;/* elimnarea propriu-zisa */
}
free (curent);
}
Eliminarea unui element situat după un element marcat prin variabila referinţă ‘curent’ se face
astfel:
void eliminare_elem_dupa (struct st_orice_tip *curent)
{struct st_orice_tip *elimin; /* elementul de eliminat */
elimin = curent -> urmator; /* salvarea referintei la urmatorul celui ce va fi eliminat */
curent -> urmator = elimin -> urmator;/* refacerea legaturii ocolind elementul eliminat */
free (elimin); /* eliberare spatiu de memorie ocpat de element */
}
16
struct st_numar *prim, *vr;
int nr;
clrscr();
printf("Programul construieste o lista ordonata, de numere distincte.\n");
printf("Introducerea numerelor se termina cu o valoare nenumerica.\n");
printf("Numar de inserat in lista: ");
prim=(struct st_numar *) malloc(sizeof (struct st_numar));
scanf("%d", &prim->numar); /* citire numar de introdus in lista */
prim->next=NULL;
printf("Numar de inserat in lista: ");
while (scanf("%d", &nr)==1){
prim=inserare (prim, nr);
printf("Numar de inserat in lista: ");
}
/* tiparirea listei introduse cu eliberarea memoriei alocate */
printf("\nLista de numere ordonata este:\n");
while (prim)
{vr=prim; /* variabila auxiliara pentru a apela functia free() */
printf("%d -> ", vr->numar);
nvl++;
if (nvl%8==0) /* daca s-au tiparit 8 valori pe linie */
printf("\n"); /* se trece pe o linie noua */
prim=prim->next;
free(vr); /* eliberare memorie alocata pentru vr */
}
printf("NULL\n"); /* tiparire sfarsit de lista */
}
struct st_numar *inserare (struct st_numar *primul, int n){
/* daca nu se mai poate aloca memorie se rutrneaza poiterul NULL,
altfel se returneaza pointerul la primul numar din lista,
care se poate modifica daca se insereaza un numar mai mic
decat primul din lista */
struct st_numar *anterior, *curent, *vr;
vr=(struct st_numar *) malloc(sizeof(struct st_numar));
if (vr == NULL){
printf("\nNu mai introduceti numere!!!\nMEMORIE PLINA!!!\n");
return (primul);
}
else
{
vr->numar=n;
anterior=NULL; /* initializare pointeri 'anterior' si 'curent' */
curent=primul; /* pentru det. pozitie element de inserat, vr */
while (curent != NULL && vr->numar > curent->numar)
{
anterior=curent;
curent=curent->next;
} /* vr trebuie inserat intre anterior si curent */
17
if(vr->numar == curent->numar) /* daca numarul exista in lista */
{free(vr); /* elibereaza spatiul alocat variabilei 'vr' */
return (primul); /* nu mai insereaza si termina inserarea */
}
vr->next=curent;
if (anterior == NULL) /* se insereaza in fata primului din lista */
primul=vr; /* este pus primul in lista */
else
anterior->next=vr; /* este pus dupa 'anterior' */
return (primul);
}
}
18
vr->urmator=(struct st_cuvant *)malloc(sizeof(struct st_cuvant));
if (vr->urmator==NULL)
return (0);
else
{
gets(cuv);
if (!strcmp(cuv,SIR_VID))
{vr->urmator=NULL;
return(0);
}
else
{if(!(pc=strdup(cuv)))
{vr->urmator=NULL;
return(0);
}
else
{
vr=vr->urmator;
vr->urmator = NULL;
vr->cuvant=pc;
return (1);
}
}
}
}
else /* daca s-a putut crea un pointer se continua atasarea */
return(atasare (vr->urmator));
}
void afisare (struct st_cuvant * curent)
{
if (curent)
{
printf ("%s -> ", curent -> cuvant);
nvl++;
if(nvl%5==0)
printf("\n");
afisare (curent -> urmator);
}
}
void main()
{
struct st_cuvant *primul;
char c[DIMC];
clrscr();
printf("Programul construieste o lista de tip coada cu cuvinte de la"
" tastatura\n");
printf("Programul ia sfarsit cu un cuvant vid, adica RETURN,"
" la inceput de linie.\n");
19
gets(c);
primul->cuvant=strdup(c);
primul->urmator=NULL;
while (atasare(primul));
afisare (primul);
printf("NULL\n");
}
Alte operaţii uzuale asupra unei liste simplu înlănţuite, de tip coadă sau stivă, sunt descrise în
continuare.
Determinarea lungimii unei liste.
typedef struct st_tip_info TINF;
typedef struct st_lista
{ TINF info;
struct st_lista* urm;
} TEl, * TLista, ** ALista; /* tipurile: element lista, lista si adresa lista*/
Căutarea unui element în listă, care îndeplineşte o anumită condiţie descrisă de funcţia de
prelucrare (f, de tipul TFPrelEl) care returnează o anumită valoare transmisă ca parametru (Gasit).
TLista PrimEl (TLista L, TFPrelEl f, int Gasit)
/* intoarce adresa primului gasit sau NULL */
{ for (; L != NULL; L = L->urm)
if ( f (L->info) == Gasit )
return L;
return NULL;
}
TLista UltimEl (TLista L, TFPrelEL f, int Gasit)
/* intoarce adresa ultimului gasit sau NULL */
{ TLista ultim = NULL;
for (; L != NULL; L = L->urm)
if ( f (L->info) == Gasit )
ultim = L;
return u;
}
20
Prototipul funcţiei transmisă ca parametru (pointer la funcţie) este următorul:
typedef int (* TFPrelEl)(const void *p,...); /* pointer la functie de prelucrare element
lista - p - adresa element -*/
Pentru parcurgerea eficientă, în oricare din cele două sensuri, este necesar accesul la cele două
extremităţi ale listei (primul şi ultimul). Pentru o astfel de listă putem defini un element (o celulă) care
să conţină adresele primului şi ultimului element din listă (în câmpurile respective, anterior şi următor),
pentru a permite accesul la cele două capete ale listei. În acest caz, elementul de acces la listă conţine
cele două adrese (primul şi ultimul) iar câmpul informaţie nu este folosit; ultimul element din listă va
conţine în câmpul urmator pointerul NULL, după cum primul element ca conţine în câmpul anterior
pointerul NULL.
Se observă că în această abordare există diferenţe de tratare în cazul operaţiilor asupra listei
(inserare / eliminare) la extremităţi, deoarece acestea afectează fie câmpul primul, fie câmpul ultimul.
Pentru o tratare uniformă se poate adopta soluţia de organizare ca listă circulară (cu santinelă).
Câmpul info al celulei santinelă (de început, start, a listei circulare) poate fi utilizat pentru
păstrarea adresei sau valorii unor informaţii cu caracter general, cum ar fi referinţa la funcţia de
verificare a relaţiei de ordine în cazul în care elementele din listă sunt ordonate, după un anumit criteriu
(funcţie).
Dacă, prin construcţie, se leagă între ele, ultima componentă cu prima se obţine o listă dublu
înlănţuită circulară. Lista circulară vidă se consideră formată dintr-un element legat la el însuşi, fără a
conţine informaţie.
21
Pentru această structură funcţiile de inserare a unui element nou, înainte sau după elementul
curent, devin:
void inserare_dupa (struct st_tip *curent; struct st_tip *nou)
{/* inserare element ‘nou’ dupa elementul ‘curent’ */
nou -> urmator = curent -> urmator;
nou -> anterior = curent;
curent -> urmator -> anterior = nou;
curent -> urmator = nou;
}
Iar procedura de eliminare a unui element, curent, dintr-o astfel de listă dublu înlănţuită va fi:
void eliminare (struct st_tip *curent)
{
curent -> anterior -> urmator = curent -> urmator;
curent -> urmator -> anterior = curent -> anterior;
}
Parcurgerea unei liste circulare nu este la fel de simplă ca în cazul parcurgerii unei liste
înlănţuite în care există referinţa NULL şi care reprezintă o condiţie de oprire a parcurgerii listei (de
terminare a ei). Parcurgerea listei circulare presupune memorarea punctului de plecare (santinela,
denumită în continuare start), printr-un pointer, şi compararea pointerului curent din listă cu pointerul
de start. Parcurgerea ia sfârşit când poziţia curentă ajunge în punctul de plecare.
Parcurgerea listei de la început:
for ( x = start->urmator; x != start; x = x->urmator )
{. . . .}
Parcurgerea listei de la sfârşit:
for ( x = start->anterior; x != start; x = x->anterior )
{. . . .}
De exemplu, să definim o funcţie care determină de câte ori se repetă, într-o listă circulară ce
conţine caractere, deja creată, caracterul aflat în câmpul respectiv al variabilei referinţă (pointer) start.
Funcţia va primi caracterul căutat în santinelă (start->car) şi va returna caracterul căutat (referinţa
acestuia) şi numărul său de apariţii în lista circulară (numar_aparitii_car).
struct st_litera
{
char car;
struct st_litera *urmator;
};
int numar_aparitii_car (struct st_litera *start, char *pcar)
{
int numar = 0;
22
struct st_litera *curent; /* pointerul cu care se parcurge lista */
*pcar=start->car; /* se transmite prin adresa primita, caracterul cautat*/
curent=start->urm;
while (curent != start) /* cat timp referinta la urmatorul din lista */
{ /* nu a ajuns la santinela/ start */
if (start->car == curent->car) /* se numara daca este caracterul */
numar++;
curent=curent->urm;
}
return (numar);
}
Se poate constata că listele circulare dublu înlănţuite sunt mai uşor de prelucrat (operaţii de
eliminare, inserare etc.) decât listele simplu înlănţuite, deoarece pentru operaţii simple nu este necesară
o parcurgere prealabilă. În schimb listele circulare sunt mai voluminoase (deoarece fiecare eleement
conţine o referinţă în plus) şi gestionarea lor este mai lentă deoarece ea trebuie să actualizeze mai multe
referinţe (pointeri).
1.3. Arbori
23
A
D
H
G
C
B
E
FJI
Un arbore multicăi (denumit şi arbore generalizat) este un arbore în care nodurile pot avea mai
mult de 2 subarbori; orice arbore multicăi poate fi transformat în arbore binar.
Un caz particular al arborilor îl constituie arborele binar, caracterizat prin faptul că fiecare nod
are cel mult doi descendenţi, deci are referinţe la cel mult doi subarbori. Exemple de arbori binari sunt:
- arborele genealogic, având ca nod rădăcină om persoană, iar ca noduri descendente cei doi părinţi,
nodurile descendente ale acestora cei patru bunici etc.
- rezultatele unui turneu eliminator se reprezintă tot printr-un arbore binar, având ca nod rădăcină pe
câştigător, nodurile descendente ale acestuia pe cei doi finalişti, următoarele noduri descendente
sunt cei patru semifinalişti etc.
Într-un arbore binar fiecare nod se reprezintă printr-o structură având minim trei câmpuri: un câmp ce
conţine informaţia din nod şi cele două referinţe la cei doi subarbori, notaţi stâng (st) şi drept (dr).
struct st_arbore
{ tip_informatie info;
struct st_arbore *st, *dr;
}TNod, *AArb;
Parcurgerea unui arbore binar poate fi definită recursiv pentru fiecare nod al acestuia:
- prelucrarea informaţiei din nod;
- parcurgerea subarborele stâng;
- parcurgerea subarborele drept;
Modificând ordinea acestor operaţii se pot defini 6 moduri diferite de parcurgere a unui arbore. Dacă
însă impunem ca parcurgerea să înceapă întotdeauna cu subarborele stâng înaintea celui drept rămân
numai 3 moduri de parcurgere (în adâncime):
- parcurgere în preordine, denumită şi rădăcină-stângă-dreapta RSD, care prelucrează mai întâi
informaţia din nod şi apoi parcurge subarborii (bineînţeles în ordinea stânga-dreapta);
- parcurgere în inordine, sau SRD, în care ordinea este: parcurgere subarbore stâng, prelucrează
nodul, parcuregere subarbore stâng;
- parcurgere în postordine, sau SDR, în care ordinea este: parcurgere subarbori dreapta-stânga,
prelucrează informaţia din nod.
Să considerăm următoarea structură de arbore binar, pentru a exemplifica cele trei moduri de
parcurgere (în adâncime) a unui astfel de arbore.
24
D
A
G
H
B
C
E
FI
+
db*eca/
- preordine (RSD): + ( * ( a , + ( b , c ) ) , / ( d , e ) )
- inordine (SRD): ( ( a * ( b + c ) ) + ( d / e ) )
- postordine (SDR): ( ( a , ( b , c ) + ) * , ( d , e ) / ) +
25
Expresiile aritmetice pot fi reprezentate utilizând arbori binari, respectând următoarele reguli:
- fiecare operaţie aritmetică corespunde unui nod neterminal, care conţine operaţia respectivă, iar
subarborii săi reprezintă operanzii săi, în ordinea, primul operand este cel din stânga şi apoi cel din
dreapta;
- fiecare nod terminal conţine o variabilă sau o constantă;
- rădăcina conţine ultima operaţie ce se efectuează.
Parcurgerea arborelui în postordine va furniza forma poloneză asociată unei expresii aritmetice,
ca în exemplul prezentat anterior.
Se pot descrie funcţii recursive de parcurgere a unui arbore binar pentru oricare dintre cele trei
moduri. Iată, de exemplu, funcţia pentru pacurgerea în preordine (RSD).
void preordine (AArb arbore)
{
if (arbore != NULL)
{
prelucreaza ( arbore);
preordine (arbore -> st);
preordine (arbore -> dr)
}
}
Modificând ordinea operaţiilor se pot obţine şi celelate două modri de parcugere. Vom descrie
în continuare alte funcţii des utilizate în aceste aplicaţii. Prima este o funcţie de inserare a unei valori
printre valorile unui arbore binar ordonat (sau de căutare, cum mai este denumit); informaţiile asociate
nodurilor pot fi numere, scalari, şiruri de carctere ordonate alfabetic sau alte structuri ordonate după o
anumită cheie. Inserarea unei noi valori presupune adăugarea unui nod frunză la arbore. Locul de
inserare depinde de poziţia relativă a informaţiei acestui nod faţă de informaţiile deja existente în
arbore. Dacă arborele este NULL înseamnă că am găsit poziţia corespunzătoare pentru noul nod, altfel
se compară informaţia de inserat cu informaţiile referite de arbore: dacă sunt mai mici se continuă
căutarea în subarborele stâng, dacă sunt mai mari se continuă căutarea în subarborele dreapta.
int inserare (AArb arbore; tip_informatie val)
{
if ( val < arbore -> info )
if (arbore->st) /* exista aceasta ramura ? */
inserare ( arbore -> st , val ); /* exista, se continua cautarea pe ramura stanga */
else
{ /* se insereaza aici, intrucat ramura nu exista; se creaza nodul */
arbore->st = ( struct st_arbore ) malloc ( struct st_arbore);
if ( arbore->st == NULL) /* nu mai este memorie disponibila */
return ( 0 );
else
{arbore=arbore->st; /* daca nu este NULL se asociaza valoarea */
arbore -> st = NULL; /* si se initializeaza referintele */
arbore -> dr = NULL;/* stanga si dreapta cu NULL */
arbore -> info = val;
return ( 1 );
}
}
26
else if ( val > arbore -> info )
if (arbore->dr) /* exista aceasta ramura ? */
inserare ( arbore -> dr , val );/* exista, continua cautarea pe ramura dreapta */
else
{ /* se insereaza aici, intrucat ramura nu exista; se creaza nodul */
arbore->dr = ( struct st_arbore ) malloc ( struct st_arbore);
if ( arbore->dr == NULL) /* nu mai este memorie disponibila */
return ( 0 );
else
{arbore=arbore->dr; /* daca nu este NULL se asociaza valoarea */
arbore -> st = NULL; /* se initializeaza referintele subarbori */
arbore -> dr = NULL;/* stanga si dreapta cu NULL */
arbore -> info = val;
return ( 1 );
}
}
else
return ( 1 ); /* nodul exista deja; se poate adauga un camp care sa
contorizez numarul de aparitii a valorii respective */
}
O altă operaţie des utilizată este cea de căutare a unei informaţii (val) într-un arbore. Vom
descrie o funcţie care caută, cauta, şi care returnează referinţa către un nod al arborelui ce conţine
informţia căutată, sau returnează NULL dacă un astfel de nodnu se află în arbore.
AArb cauta ( AArb arbore, tip_informatie val)
{int gata = 0;
do {
if ( arbore == NULL)
gata = 1;
else
{
if ( val < arbore -> info)
arbore = arbore -> st;
else if ( val > arbore -> info)
arbore = arbore -> dr;
else
gata = 1;
}
}while ( ! gata );
return ( arbore );
}
27
if ( val < arbore -> info)
return ( cauta( arbore -> st) );
else if ( val > arbore -> info)
return ( cauta( arbore -> dr) );
else
return ( arbore );
}
Operaţia de eliminare a unui nod, care nu este nod frunză, dintr-un arbore binar este mai
dificilă, deoarece trebuie să ne asigurăm că arborele rămâne tot binar.
Un arbore binar de căutare (ordonat) este o structură de date mult mai bună pentru memorarea şi
căutarea datelor. Cu un astfel de arbore, cu N noduri, timpul necesar pentru inserarea sau eliminarea
unui nod este proporţional cu log2N, în timp ce durata necesară aceloraşi operaţii într-un tablou,
efectuând o căutare liniară este prporţională cu N.
Să considerăm un alt exemplu, şi anume un program care citeşte un text şi afişează lista de
cuvinte din text precum şi numărul de apariţii pentru fiecare cuvânt din text. Vom rezolva această
problemă, utilizând pentru memorarea cuvintelor un arbore binar de căutare, în locul unui tablou.
Căutarea într-un astfel de arbore este mult mai rapidă şi în plus va tipări cuvintele în ordine alfabetică
fără să le mai sortăm, întrucât arborele este astfel construit încât ele sunt în ordine alfabetică. În
schimb, această metodă va necesita mai mult spaţiu de memorie, deoarece fiecare nod va conţine, pe
lângă cuvântul respectiv şi numărul său de apariţii şi două referinţe.
28
else {
arb->st=(struct st_arbore *) malloc (sizeof(struct st_arbore)) ;
arb=arb->st;
arb->cuvant=cuv;
arb->nrap=1;
arb->st=NULL;
arb->dr=NULL;
}
else if (strcmp(cuv, arb->cuvant) >0 )
if(arb->dr)
actualizare (arb->dr, cuv);
else {
arb->dr=(struct st_arbore *) malloc (sizeof(struct st_arbore)) ;
arb=arb->dr;
arb->cuvant=cuv;
arb->nrap=1;
arb->st=NULL;
arb->dr=NULL;
}
else
arb->nrap=arb->nrap + 1;
}
void tiparire_arbore (struct st_arbore *arbo)
/* functia afiseaza arborele de cautare ce contine cuvinte si
numarul lor de aparitii */
{
if (arbo!=NULL)
{
tiparire_arbore (arbo->st) ; /* tiparire arbore stanga */
printf ("%s (ap: %d)\n", arbo -> cuvant, arbo -> nrap ) ;
nle++;
if (nle%24==0){
printf("Apasati o tasta pentru a continua afisarea!\n");
getch();
}
tiparire_arbore (arbo->dr) ; /* tiparire arbore dreapta */
}
}
void main ()
{struct st_arbore *arb;
char cuvant[LUNGMAX] ;
clrscr();
printf("Introduceti cuvinte, care vor fi apoi tiparite in ordine"
" alfabetica:\n");
gets(cuvant) ;
arb=(struct st_arbore *) malloc (sizeof(struct st_arbore)) ;
/* am presupus ca nu se returneaza de catre malloc
valoarea NULL */
29
arb->cuvant=strdup(cuvant);
arb->nrap=1;
arb->st=NULL;
arb->dr=NULL;
gets(cuvant) ;
while (strcmp(cuvant, ""))
{actualizare (arb, strdup(cuvant));
gets(cuvant);
}
printf("Lista ordonata a cuvintelor (numar aparitii):\n");
tiparire_arbore (arb);
}
30
- ordinul (gradul) unui nod, reprezintă numărul de subarbori ataşaţi nodului;
- noduri interne, sunt nodurile care au subarbori;
- noduri externe (noduri terminale sau frunze), sunt nodurile fără subarbori (cu ordin 0);
- nivelul unui nod este distanşa la care se află faţă de rădăcină (rădăcina are nivelul 0);
- înălţimea unui arbore este nivelul maxim din arbore.
- arbore complet de ordin k – toate frunzele se află pe acelaşi nivel şi toate nodurile interne au
ordinul k; un astfel de arbore, de înălţime h, are (k(h+1)-1)/(k-1) noduri, dintre care (kh-1)/(k-1)
noduri interne si kh frunze;
- strămoş al unui nod x – orice nod aflat pe calea (unică) de la rădăcină până la nodul x (rădăcina este
strămoşul tuturor celorlalte noduri);
- descendent al unui nod x – orice nod aflat în arborele cu rădăcina x;
- tată / fiu – noduri aflate la distanţa 1 (numite şi stramoş / descendent direct);
- fraţi – fiii aceluiaşi nod.
31
return NNN(r->st, n-1) + NNN(r->dr, n-1);
}
O problemă (operaţie) puţin mai delicată, decât cele prezentate anteror, este cea a eliminării
unui nod dintr-un arbore binar de căutare. Problema constă în a determina poziţia în arbore a unei chei,
x, ceea ce este simplu într-un arbore de căutare, iar apoi să se elimine nodul cu cheia respectivă. În
principiu avem două situaţii, după cum nodul respectiv are ambii succesori (subarbori), sau numai pe
unul dintre ei. Mai există şi cazul când nu are succesori, dar aici eliminarea nu este o problemă, doar se
eliberează spaţiul de memorie alocat nodului respectiv şi se şterge referinţa din nodul precedent.
În situaţia în care nodul de eliminat nu are decât subarborele drept (deci cel stâng este vid), dacă
a este adresa nodului cu cheia x (cel de eliminat), atunci se atribuie lui a adresa descendentului drept,
ceea ce permite păstrarea proprietăţii arborelui (cea de căutare). Se procedează similar şi pentru cazul
în care nodul de eliminat nu are decât subarborele stâng (deci cel drept este vid), adică se atribuie lui a
adresa descendentului stâng. Cele două situaţii sunt
9426371 prezentate în figura următoare.
32
Din arborele iniţial (cel din stânga) a fost eliminat nodul cu cheia 6, şi se obţine arborele din
dreapta.
Să considerăm aceeaşi problemă pentru eliminarea nodului ce conţine valoarea 4, din arborele
din figura următoare (în partea stângă înainte de eliminare, şi dreapta după eliminare).
94263715
Arborele s-a obţinut prin mutarea valorii maxime din subarborele stâng valorii 4, adică a valorii
3 în locul valorii 4, şi eliminarea frunzei pe care a fost valoarea 3. Mai există şi varianta în care
valoarea minimă de pe subarborele drept al nodului ce conţine valoarea 4, adică valoarea 5 se mută în
locul valorii 4, şi se elimină nodul în care a fost valoarea 5. Ambele variante vor furniza acelaşi arbore
SRD, la listare.
Dacă nodul de eliminat, a, cel ce conţine cheia x, are ambii subarbori nevizi, atunci procedeul
de eliminare decurge după cum urmează. Se determină cea mai mare valoare din subarborele stâng,
adică nodul (frunza) cel mai din dreapta din subarborele stâng, ce conţine valoarea maximă din acest
subarbore, imediat mai mică decât cheia x, din arbore. Se va pune această valoare în locul lui x, iar
frunza respectivă se şterge. Sau se poate proceda similar pentru subarborele stâng, adică să determinăm
cea mai mică valoare din acesta, care va fi valoarea imediat mai mare decât x, din arbore. Întrucât
algoritmul este recursiv vom defini o funcţie recursivă pentru eliminare. Întregul program este
prezentat în continuare.
/* arbelnod.cpp - programul construieste recursiv si tipareste un arbore
de numere intregi, utilizand functiile recursive: 'inserare' si 'listare'.
Arborele contine pe langa valorile intregi ordonate (SRD)
si numarul de aparitii pentru valorile ce se repeta.
Programul permite elimiarea unei anumite valori (chei), adica
un nod din arbore ce contine cheia, pastrand arborele de cautare (SRD). */
#include <stdio.h>
33
#include <stdlib.h>
#include <conio.h>
typedef struct tnod
{int nr;
int ap;
struct tnod *pstg;
struct tnod *pdr;
} ARB;
int nel = 0;
ARB *e;
void inserare (ARB * &v, int val){
ARB *pc;
int gasit;
if (!v) {v = new ARB; v->nr=val; v->ap=1;
v->pstg=NULL; v->pdr=NULL;}
else if (v->nr > val)
inserare(v->pstg, val);
else if (v->nr < val)
inserare(v->pdr, val);
else v->ap++;
}
void inlocuire_nod(ARB * &n){ // n - nodul de eliminat
// se inlocuieste cu maximul de pe subarborele stang (acum maximul din n),
// care este pe ramura dreapta a lui 'n',
// sau cu minimul de pe subarborele drept (adica minimul din arborele n),
// rezultatul final fiind acelasi, adica un arbore de cautare
if (n->pdr)
inlocuire_nod(n->pdr);
else {e->nr=n->nr; // e - nodul care se elimina (continutul sau)
e->ap=n->ap; // se actualizeaza valorile lui 'e' cu maximul din
e=n; // arborele nodului 'n' , ramura dreapta
n=n->pstg; // pastrez legatura la ramura stanga a nodului
free(e); // pentru care elimin ramura dreapta, a carei
} // valori s-au mutat in nodul 'eliminat' (inlocuit)
}
void elimina_nod (ARB * &a, int x){// a-arbore, x-valoarea de eliminat
if (!a) printf("\nCheia cautata %d nu este in arbore!\n", x);
else if (x < a->nr) // se cauta pozitia valorii 'x'
elimina_nod (a->pstg, x);
else if (x > a->nr)
elimina_nod (a->pdr, x);
else // s-a gasit pozitia lui 'x'
{e=a;
if (!a->pstg){ // are subarborele stang vid
a=a->pdr; // se conecteaza subarborele drept
free(e); // si eliberez spatiul alocat
} // nodului eliminat
else if (!a->pdr){ // are subarborele drept vid
34
a=a->pstg; // se conecteaza subarborele stang
free(e); // si eliberez spatiul alocat
} // nodului eliminat
else
inlocuire_nod(a->pstg); // sau cu parametru 'a->pdr'
} // cu modificarile respective in functia de inlocuire
}
void listare_SRD (ARB *v){
if (v) {if (v->pstg)
listare_SRD (v->pstg);
printf("%3d(%d) -> ", v->nr, v->ap);
nel++;
if (nel%7==0)
printf("\n");
if (v->pdr)
listare_SRD(v->pdr);
}
}
main(){
ARB *pl;
int valoare;
pl = NULL;
clrscr();
printf("Se construieste un arbore de cautare si permite elimarea unui nod.\n");
printf ("\nIntroduceti un sir de valori, terminat cu EOF.\n");
printf ("Valoare:");
while ( scanf ("%d", &valoare) != EOF) {
inserare (pl,valoare);
printf ("Valoare:");
}
/* afisare arbore */
printf("\nListare arbore SRD:\n");
listare_SRD (pl);
printf ("NULL\n");
printf("\nValoare de eliminat: ");
scanf("%d", &valoare);
elimina_nod(pl, valoare);
/* afisare arbore dupa eliminare */
printf("\nListare arbore SRD, dupa eliminarea valorii %d:\n", valoare);
nel=0; // initializare contor afisare pe ecran
listare_SRD (pl);
printf ("NULL\n");
printf("Terminare dupa apasarea unei taste!\n");
getch();
}
35
Arbori generalizaţi
Să considerăm acum cazul arborilor generalizaţi, adică arbori în care fiecare nod poate conţine
referinţe către mai mult de doi subarbori, fără ca numărul lor să fie constant. Dacă numărul subarborilor
poate creşte cu o valoare finită, este comod de utilizat un tablou de referinţe care se referă către aceşti
subarbori. Dacă însă fixăm numărul de noduri la o valoare prea mare, numeroase referinţe nu vor
utiliza spaţiul de memorie rezervat. O soluţie a acestei probleme este de a ataşa fiecărui nod al arborelui
o listă înlănţuită a descendenţilor săi. Fiecare nod conţine două referinţe, una către “fiul” său iar
cealaltă către “fratele” său cel mai apropriat. Una din aceste referinţe poate fi NULL. Vom reprezenta,
în acest mod, primul arbore, prezentat la începulul paragrafului; legăturile de descendenţă sunt
reprezentate (desenate) vertical, iar cele de fraternitate, pe orizontală. Practic, în acest mod, s-a
transformat un arbore generalizat într-unul binar:
A
D
H
G
C
B
E
FJI
36