Structures de données IFT-2000 Abder Alikacem Semaine 11 Gestion des arbres binaires de tri et de recherche. Les arbres cousus. Les arbres n-aires Département dinformatique et de génie logiciel Édition Septembre 2009
Gestion des arbres binaires Algorithmes de gestion dun arbre binaire de tri Les arbres de recherche, arbres AVL Algorithme de balancement dun arbre AVL Les arbres cousus Les arbres n-aires Notez bien. Les exemples de code qui seront montrés dans le cours se trouvent dans la section exemple de la semaine 11 Classe Arbre: une implémentation d'un arbre de tri.
Implantation dun arbre binaire par chaînage template class Arbre { public: //.. private: // classe Noeud class Noeud { public: E data; Noeud *gauche; Noeud *droite; int card; int hauteur; Noeud( const E&d ): gauche(0),data( d ),droite(0),hauteur(0) { } }; // Les membres données Noeud * racine;//racine de l'arbre long cpt;// Nombre de noeuds dans l'arbre // Les membres fonctions privés //... };... data Modèle dimplantation par chaînage
Implantation dun arbre binaire par chaînage template class Arbre { public: //Constructeurs Arbre(){racine = 0; cpt=0;} Arbre(const Arbre& source) { _auxCopier(source.racine,racine);} //Destructeur ~Arbre() { _auxDetruire(racine);} //Les membres méthodes bool estVide(){ return cpt==0;} long taille() {return cpt;} int hauteur() throw(logic_error); E max() const throw(logic_error); E min()const throw(logic_error); int nbFeuilles()const; int nbNoeuds()const; E parent(const E&) throw(logic_error); E successeur(const E& ) throw(logic_error); bool appartient(const E &); void lister(vector &) const; //.. Linterface publique
template E Arbre ::max()const throw (logic_error) { if (cpt==0) throw logic_error("Max: l'arbre est vide!\n"); if (racine->droite == 0) { return racine->data; } Noeud * temp = racine->droite; while (temp->droite!=0) temp = temp->droite; return temp->data; }
template E Arbre ::min()const throw (logic_error) { if (cpt==0) throw logic_error("Min: l'arbre est vide!\n"); if (racine->gauche == 0) { return racine->data; } Noeud * temp = racine->gauche; while (temp->gauche!=0) temp = temp->gauche; return temp->data; }
template int Arbre :: nbNoeuds() const { return _nbNoeuds(racine); } template int Arbre :: _nbNoeuds(Noeud* arb) const { if (arb==0) return 0; return _nbNoeuds(arb->gauche) + _nbNoeuds(arb->droite) + 1; }
template int Arbre ::nbFeuilles() const { return _nbFeuilles(racine); } template int Arbre ::_nbFeuilles(Noeud*arb) const { int nbG (0), nbD(0); if (arb != 0) { if (arb->gauche == 0 && arb->droite == 0) return 1; else { if (arb->gauche != 0) nbG = _nbFeuilles(arb->gauche); if (arb->droite != 0) nbD = _nbFeuilles(arb->droite); } return nbG + nbD; }
template int Arbre ::hauteur() throw (logic_error) { if (cpt==0) throw logic_error("Hauteur: l'arbre est vide!\n"); return _hauteurParcours(racine); } template int Arbre ::_hauteurParcours(Noeud * arb) { if (arb==0) return -1; return 1 + _maximum(_hauteur(arb->gauche), _hauteur(arb->droite)); }
template bool Arbre :: appartient(const E &data) { return _auxAppartient(racine, data)!=0; } template typename Arbre :: Noeud* Arbre :: _auxAppartient(Noeud* & arbre, const E &data) { if (arbre == 0) return 0; if ( arbre->data == data ) return arbre; if ( arbre->data > data ) return _auxAppartient(arbre->gauche, data); else return _auxAppartient(arbre->droite, data); }
template E Arbre :: parent(const E& el) throw(logic_error) { Noeud* noeudDeEl = _auxAppartient(racine, el); Noeud* parentDeEl = _parent(racine, noeudDeEl); return parentDeEl->data; }
template typename Arbre :: Noeud* Arbre :: _parent(Noeud* arb, Noeud* sArb) throw(logic_error) { if (arb == 0) throw logic_error("Parent: l'arbre est vide!\n"); if (sArb == 0) throw logic_error("Parent: l'element n'existe pas!\n"); if (sArb == arb) throw logic_error("Parent: Le parent de la racine d'existe pas!\n"); if ( sArb->data data ) { if (arb->gauche == sArb)return arb; elsereturn _parent(arb->gauche, sArb); } else { if (arb->droite == sArb)return arb; elsereturn _parent(arb->droite, sArb); }
template E Arbre :: successeur(const E& info) throw(logic_error) { return _successeur(racine, info); } template E Arbre :: _successeur(Noeud* arb, const E& info) throw (logic_error) { if (cpt == 0) throw logic_error("Successeur: l'arbre est vide!\n"); Noeud* sArb = _auxAppartient(racine, info); if (sArb == 0) throw logic_error("Successeur: l'element n'existe pas!\n"); if ( info == _max(arb)) throw logic_error("Successeur: l'element est le max dans l'arbre!\n"); if (sArb->droite != 0) return _min(sArb->droite); else { Noeud * pere = _parent(arb, sArb); while (pere->data data ) pere = _parent(arb,pere); return pere->data; }
Implantation dun arbre binaire par chaînage template class Arbre { public: //.. void insererAVL(const E &data) throw(bad_alloc);//..//.. void enleverAVL( const E&) throw(logic_error);//…//… void parcourirPreOrdre(void (* traitement)(E &iteme)) const; //…//… void parcourirEnOrdre(void (* traitement)(E &iteme)) const; void parcourirPostOrdre(void (* traitement)(E &iteme)) const; void parcourirParNiveau(void (* traitement)(E &iteme)) const; //…//… //surcharge d'opérateurs void operator = (const Arbre & a) {…; _auxCopier(a.racine,racine);} bool operator == (const Arbre &a) { return( _auxArbresEgaux(racine,a.racine));} //… private: // classe Nœud // Attributs internes de Arbre // Méthodes privées
Ajout déléments dans un arbre de tri ,3,11,2,1,6,10,9,12,4,5,14
,3,11,2,1,6,10,9,12,4,5,14 Ajout déléments dans un arbre de tri
,3,11,2,1,6,10,9,12,4,5,14 Ajout déléments dans un arbre de tri
,3,11,2,1,6,10,9,12,4,5,14 Ajout déléments dans un arbre de tri
,3,11,2,1,6,10,9,12,4,5,14 Ajout déléments dans un arbre de tri
,3,11,2,1,6,10,9,12,4,5,14 Ajout déléments dans un arbre de tri
,3,11,2,1,6,10,9,12,4,5,14 Ajout déléments dans un arbre de tri
1 re séquence dinsertions ,3,11,2,1,6,10,9,12,4,5,14 O(log n)
2 e séquence dinsertions 1 2 1,2,3,4,5,6,8,9,10,11,12, O(n/2)
template void Arbre ::inserer(const E &data) throw(bad_alloc) { _auxInserer(racine, data); } template void Arbre ::_auxInserer(Noeud *&arbre, const E &data) { if (arbre == 0) { arbre = new Noeud(data); cpt++; } else if(arbre->data > data ) _auxInserer(arbre->gauche, data); else _auxInserer(arbre->droite, data); } Insertion sans balancement
Arbres binaires équilibrés (AVL) Rappel Cest un arbre de recherche binaire tel que pour chaque noeud, les hauteurs des ses sous-arbres gauche et droite sont différentes dau plus k, k étant le critère déquilibre (on attribue comme hauteur la valeur -1 pour un sous-arbre vide). K =1 dans le cas des arbres AVL. Avec cette condition, on est assuré de toujours avoir un arbre dont la profondeur est proportionnelle à log (n). arbres AVL = HB[1] (arbres Adelson-Velski et Landis) arbres HB[k] « Un arbre T est HB[k] si T et tous ses sous-arbres ont la propriété HB[k] qui est : les sous-arbres gauche et droit diffèrent en hauteur dau plus k. »
Arbres équilibrés (AVL) Bien équilibré selon la règle AVL Mal équilibré selon la règle AVL 0
Équilibration : HB[1] 2 cas (~gauche) S1 A B S2 S3 h + 1 h + 2 h + 3 h + 4 N S1 A B S2 S3 h + 1 h + 2 h + 3 h + 4 N
Rotation simple S1 A B 2 S3 h + 1 h + 2 h + 3 h + 4 N S1 A B 2 S3 h + 1 h + 2 h + 3 h + 4 N avantaprès S2
Rotation simple S1 A B 2 S3 h + 1 h + 2 h + 3 h + 4 N S1 A B 2 S3 h + 1 h + 2 h + 3 h + 4 N avantaprès S2
Rotation simple S1 A B 2 S3 h + 1 h + 2 h + 3 h + 4 N S1 A B 2 S3 h + 1 h + 2 h + 3 h + 4 N avantaprès S2
S1 A B 2 S3 h + 1 h + 2 h + 3 h + 4 N Rotation simple hauteur initiale vs hauteur finale S1 A B S2 S3 h + 1 h + 2 h + 3 h + 4 N avantaprès S2
Rotation double S1 A B S2 S3 h + 1 h + 2 h + 3 h + 4 N S1 A B S2.1 S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C
Rotation double S1 A B S2.1 S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C S1 A B S2.1 S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C avantaprès
Rotation double S1 A B S2.1 S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C S1 B S2.1 S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C A avantaprès
Rotation double S1 B S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C S1 B S3 h + 1 h + 2 h + 3 h + 4 N C A A S2.1 S2.2 avantaprès
Rotation double S1 B S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C S1 A B S2.1 S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C hauteur initiale vs hauteur finale A S2.1 avantaprès
1 re rotation B S3 h + 1 h + 2 h + 3 h + 4 N S2.2 S1 A B S2.1 S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C S2.1 S1 C A avantaprès
2 e rotation B S3 h + 1 h + 2 h + 3 h + 4 N S2.2 S2.1 S1 C A S1 B S3 h + 1 h + 2 h + 3 h + 4 N S2.2 C A S2.1 avantaprès
Nœud critique Mais ce nest pas toujours le cas Ici, le nœud critique du déséquilibre est la racine 1 Quand il y a un déséquilibre, le nœud le plus bas à partir duquel il y a une différence de 2 ou plus est appelé le nœud critique.
Nœud critique Il peut parfois y avoir plusieurs nœuds débalancés. Dans ce cas, on soccupe dabord du plus bas de tous, cest le nœud critique.
Maintien de léquilibre Quand on implémente un arbre AVL, il faut maintenir son équilibre. Les déséquilibres surviennent soit lors dun ajout, soit lors dune suppression. Le ou les nœuds critiques engendrés, sil y a lieu, sont toujours sur le chemin de lajout ou de la suppression. Nouveau noeud
Rééquilibrer un arbre déséquilibré Quand un déséquilibre apparaît, il faut remodeler la partie de larbre dont la racine est le nœud critique. Nouveau noeud
Trouver le cas de déséquilibre Il faut dabord identifier le genre de déséquilibre auquel on a affaire. Dabord, il faut voir de quel côté larbre penche à partir du nœud critique. Dans le cas de lexemple ci-bas, cest vers la gauche. Nouveau noeud
Trouver le cas de déséquilibre Puis il faut regarder de quel côté penche larbre à partir du nœud sous- critique: il penche vers la droite: deux rotations sont nécessaires. Nous appelons ça un zig-zag. Nœud critique Nœud critique Nœud sous-critique Nœud sous-critique Nouveau noeud
Les rotations Nœud critique Nœud critique Nœud sous-critique Nœud sous-critique Nouveau noeud Première rotation (préparatoire à la deuxième).
Les rotations Première rotation (préparatoire à la deuxième). Nœud critique Nœud critique Nouveau noeud
Les rotations Première rotation (préparatoire à la deuxième). Nœud critique Nœud critique Nouveau noeud
Les rotations Première rotation (préparatoire à la deuxième). Nœud critique Nœud critique Ancien nœud sous-critique Nouveau noeud Nœud nouvellement sous-critique Nœud nouvellement sous-critique
Les rotations Deuxième rotation. Nœud critique Nœud critique Nouveau noeud
Les rotations Deuxième rotation. Nouveau noeud
Les rotations Deuxième rotation. Nouveau noeud
Les rotations Deuxième rotation. Nouveau noeud
Les rotations Les deux rotations sont terminées, nous avons maintenant un arbre AVL équilibré. Nouveau noeud
Implémentation : remarques indice de débalancement : tag = hauteur(gauche) - hauteur(droit) valeurs : 2, 1, 0, -1, -2 calcul de hauteur(gauche) - hauteur(droit) au parcours dinsertion + stockage de la hauteur mise à jour un seul rebalancement requis la racine de larbre rebalancé a changé
Modèle dimplantation arbre AVL template class Arbre { public: //.. private: // classe Noeud class Noeud { public: E data; Noeud *gauche; Noeud *droite; int card; int hauteur; Noeud( const E&d ): gauche(0),data( d ),droite(0),hauteur(0) { } }; // Les membres données Noeud * racine;//racine de l'arbre //... };... data Modèle dimplantation par chaînage
template int Arbre :: _hauteur(Noeud *arb) { if (arb == 0) return -1; return arb->hauteur; } template int Arbre :: _maximum(int ent1, int ent2) { if (ent1 <= ent2) return ent2; else return ent1; } deux méthodes utiles.. Insertion dans un arbre AVL
Algorithme insereAvl (Nœud* & T, int x) Début Si T = NULL, alors Début Allocation de mémoire à l'adresse T T.element = x T. filsG = NULL et T. filsD = NULL T.hauteur = 0 Fin Sinon Début Si x < T.element, alors Appel insereAvl(T.filsG, x) Si Hauteur (T.filsG)-Hauteur (T.filsD) = 2 alors Si x < T. filsG.element, alors Appel ZigZigGauche (T) Sinon Appel ZigZagGauche (T) Sinon T.hauteur =Max(Hauteur (T.filsG),Hauteur (T.filsD))+1 Sinon Début /* Cas symétrique pour le sous-arbre droit */ Fin
algorithme ZigZigGauche (Nœud* &K2) Début K1 = K2.filsG K2.filsG = K1.filsD K1.filsD = K2 K2.hauteur = Max (Hauteur (K2.filsG), Hauteur (K2.filsD)) +1 K1.hauteur = Max (Hauteur (K1.filsG), K2.hauteur) +1 K2 = K1 Fin + k2 k1 k2 k1
algorithme ZigZigDroit (Nœud* &K2) Début K1 = K2.filsD K2.filsD = K1.filsG K1.filsG = K2 K2.hauteur = Max (Hauteur (K2.filsD), Hauteur (K2.filsG)) +1 K1.hauteur = Max (Hauteur (K1.filsD), K2.hauteur) +1 K2 = K1 Fin simple rotation, déséquilibre vers la gauche k2 k1
algorithme ZigZagGauche (Nœud * &K3) Début ZigZigDroit (K3.filsG) ZigZigGauche (K3) Fin + k3
algorithme ZigZagDroit (Nœud * &K3) Début ZigZigGauche (K3.filsD) ZigZigDroit (K3) Fin + k3
template void Arbre :: _zigZigDroit(Noeud * &K2) { Noeud *K1; K1 = K2->droite; K2->droite = K1->gauche; K1->gauche = K2; K2->hauteur = 1 + _maximum(_hauteur(K2->droite), _hauteur(K2->gauche)); K1->hauteur = 1 + _maximum(_hauteur(K1->droite), K2->hauteur); K2 = K1; } Implémentation dune rotation
Analyse insertion balancée : trouver le point dinsertion : O(log n) insertion dune feuille : O(1) + vérification et rebalancement: on remonte (suite aux appels récursifs) : O(log n) on vérifie le rebalancement possible : O(1) on rebalance au besoin : O(1) total : O(log n)
Enlèvement dans un arbre AVL Pour supprimer un nœud dans un arbre AVL, il y a deux cas simples et un cas compliqué: Premier cas simple: le nœud à supprimer est une feuille. Dans ce cas, il suffit de le supprimer directement. Deuxième cas simple: le nœud à supprimer possède un seul enfant. Dans ce cas, il suffit de le supprimer et de le remplacer par son seul enfant. Cas compliqué: le nœud à supprimer a deux enfants. Dans ce cas, il faut dabord échanger ce nœud avec son successeur, puis le supprimer à son nouvel endroit, ce qui nous mènera nécessairement à lun des deux cas simples. Bien entendu, il faut aussi vérifier les déséquilibres en remontant jusquà la racine. Lalgorithme fonctionne aussi bien si on prend le prédécesseur plutôt que le successeur. Étant donné que la nécessité de retrouver le successeur ne survient que dans le cas où le nœud a deux enfants, alors nous sommes nécessairement toujours en présence du cas simple de recherche du successeur! Ainsi, une simple boucle suffit.
Enlèvement dans un arbre AVL une feuille : trivial 48 Analyse : l algorithme de suppression d un nœud présente donc 3 cas : O 45 gauche 48 droit O 45 29
Enlèvement dans un arbre AVL un nœud simple : on le remplace par son unique fils 50 Deuxième cas de nœud à supprimer O 45 gauche 48 droit
Enlèvement dans un arbre AVL 23 un nœud double : on lui donne la valeur minimale de son sous-arbre droit (ex: 29), et on supprime le nœud qui a cette valeur Troisième cas de nœud à supprimer O 45 gauche 48 droit O
68 Exemple denlèvement AVL Supprimons le nœud 36 de cet arbre-ci. Il faut dabord le repérer avec une recherche conventionnelle à partir de la racine
69 Puis, nous établissons quil sagit dun cas compliqué car le nœud à supprimer a deux enfants Exemple denlèvement AVL
70 Il faut donc dabord retrouver son successeur à laide dune boucle simple (une fois à droite, plein de fois à gauche) Exemple denlèvement AVL
71 Puis on léchange avec Exemple denlèvement AVL
72 Puis on léchange avec Exemple denlèvement AVL
73 Puis on léchange avec Exemple denlèvement AVL
74 Puis on léchange avec Exemple denlèvement AVL
75 Puis on léchange avec Exemple denlèvement AVL
76 Remarquez que la règle dordonnancement darbre binaire de recherche est temporairement enfreinte Exemple denlèvement AVL
77 Ensuite, on continue à descendre récursivement pour supprimer 36, comme si rien nétait Exemple denlèvement AVL
78 Puis lorsquon retombe sur 36, on arrive nécessairement à lun des deux cas simple Exemple denlèvement AVL
79 Dans ce cas-ci, il sagit du cas avec un seul enfant. 36 sera donc remplacé par Exemple denlèvement AVL
80 Dans ce cas-ci, il sagit du cas avec un seul enfant. 36 sera donc remplacé par Exemple denlèvement AVL
81 Dans ce cas-ci, il sagit du cas avec un seul enfant. 36 sera donc remplacé par Exemple denlèvement AVL
82 Puis le nœud 36 est détruit avec « delete ». Ensuite, il faut remonter jusquà la racine pour vérifier léquilibre de larbre Exemple denlèvement AVL
83 Puis le nœud 36 est détruit avec « delete ». Ensuite, il faut remonter jusquà la racine pour vérifier léquilibre de larbre Exemple denlèvement AVL
84 Puis le nœud 36 est détruit avec « delete». Ensuite, il faut remonter jusquà la racine pour vérifier léquilibre de larbre Exemple denlèvement AVL
85 Puis le nœud 36 est détruit avec « delete ». Ensuite, il faut remonter jusquà la racine pour vérifier léquilibre de larbre Exemple denlèvement AVL
86 Puis le nœud 36 est détruit avec « delete ». Ensuite, il faut remonter jusquà la racine pour vérifier léquilibre de larbre Exemple denlèvement AVL
template void Arbre ::enlever(const E& data) throw(logic_error) { if( racine == 0 ) throw logic_error("Enlever: l'arbre est vide\n"); if( _auxAppartient(racine, data) == 0 ) throw logic_error("Enlever: l'element nest pas dans l'arbre\n"); _auxEnlever(racine, data); //data est certain dans l'arbre } Enlèvement dans un arbre de tri Enlèvement sans balancement
template void Arbre :: _auxEnlever(Noeud * & t, const E & valeur) throw(logic_error) { if( t->data > valeur) _auxEnlever( t->gauche, valeur); else if( t->data < valeur ) _auxEnlever( t->droite, valeur); else if( t->gauche != 0 && t->droite != 0 ) {//Troisième cas //chercher le noeud qui contient la valeur minimale dans le sous-arbre droit Noeud * temp = t->droite; while ( temp->gauche != 0) temp = temp->gauche; t->data = temp->data; _auxRetireMin( t->droite ); // Retirer minimum dans le sous-arbre droit } else { //Premier ou deuxième cas // le noeud n'a aucun enfant ou qu'un seul enfant, il suffit donc de retirer // ce noeud et pointer sur l'éventuel enfant Noeud * vieuxNoeud = t; t = ( t->gauche != 0 ) ? t->gauche : t->droite; delete vieuxNoeud; }
template void Arbre :: _auxRetireMin( Noeud* & t) const throw(logic_error) { if (t == 0) throw logic_error("_auxRetireMin: pointeur NULL\n"); else if (t->gauche != 0) _auxRetireMin( t->gauche ); else { Noeud * tmp = t; t = t->droite; delete tmp; }
Analyse enlèvement balancé : plusieurs rebalancements possiblement requis la racine de tout arbre rebalancé a changé trouver le nœud à enlever (déjà vu) : O(log n) + vérification et rebalancement: on remonte (suite aux appels récursifs) : O(log n) on vérifie le rebalancement possible : O(1) on rebalance au besoin : O(1) total : O(log n) (mais plus coûteux que linsertion)
problèmes : remonter vers les parents : fonction parent à partir dun nœud retour par les appels récursifs espace à gérer : accès par pile : quelle est la taille de la pile ? combien de piles a-t-on besoin ? pour de nombreux usagers = trop despace ! Parcours avec pile
Peut-on éliminer les piles ? ajouter un pointeur vers le parent problèmes : ne jamais perdre le parent lors de lajout beaucoup despace perdu complique les rebalancements Parcours avec pile
Arbres cousus Peut-on éliminer les piles ? ajouter un pointeur vers le parent ajouter un pointeur vers le succ./préd. problèmes : - complique les ajouts - complique les rebalancements - il faut éviter les cycles
Arbres cousus class Noeud { public: E data; /* élément */ Noeud *gauche; /* ptr sur SAG ou prédécesseur */ Noeud *droite; /* ptr sur SAD ou successeur */ bool filGauche;/* indique si gauche est un fil ou non */ bool filDroit;/* indique si droite est un fil ou non */ //… }; Comment reconnaître les cycles ? utiliser un «tag» avec chaque pointeur
Arbres n-aires (pour n fixe) critère de branchement multiple exemples ? arbres-B analyse lexicale : 1 re lettre, 2 e lettre, etc. …
Arbres n-aires (pour n fixe) template class Arbre { public: //.. private: // classe Noeud class Noeud { public: E data; Noeud ** fils; int card; Noeud(const E&d ) { …} }; // Les membres données Noeud * racine;//racine de l'arbre //... };... data
Arbres n-aires (pour n variable) nombre de branchements inconnu au départ nombre de branchements très variable exemples ? structure organisationnelle analyse lexicale autres ?
Arbres n-aires variables modèles dimplantation ? tableau dynamique ou liste de pointeurs
Arbres n-aires variables modèles dimplantation ? vector ou liste de pointeurs
Arbres n-aires variables template class Arbre { public: //.. private: // classe Noeud class Noeud { public: E data; vector fils; int card; Noeud(const E&d ) { …} }; // Les membres données Noeud * racine;//racine de l'arbre //... };... data
Arbres n-aires variables modèles dimplantation ? tableau dynamique ou liste de pointeurs 2 types de pointeurs : 1 er fils, frère cadet
Arbres n-aires variables modèles dimplantation ? tableau dynamique ou liste de pointeurs 2 types de pointeurs : 1 er fils, frère cadet
Arbres n-aires variables modèles dimplantation ? tableau dynamique ou liste de pointeurs 2 types de pointeurs : 1 er fils, frère cadet
Arbres n-aires variables modèles dimplantation ? tableau dynamique ou liste de pointeurs 2 types de pointeurs : 1er fils, frère cadet