Vous trouverez ici quelques documents et quelques liens pouvant, je l'espère,
vous être utiles.
Ce qui suit détaille le contenu des séances du cours.
Séance |
Jour |
Date |
Contenu |
S00 |
LUN
|
29
août
|
Au menu :
- Présentation du plan de cours
- Présentation de l'enseignant et de son horaire quelque peu
acrobatique
- Présentation de la forme particulière du cours
- Présentation du matériel de support
- Vous devrez aller vous procurer les notes de cours du cours
Structures de données – IFT339 à la
coopérative où elles vous attendent avec impatience
- Première approche avec ce mécanisme d'abstraction qu'est la classe
(mot-clé class ou mot-clé
struct en
C++)
- Nous utilisons les notes de cours en soutien, en travaillant à
partir du chapitre 4 (voir les
notes sur ce chapitre, plus bas)
- Nous essaierons, tout au long de notre cheminement, de distinguer
les concepts généraux de la
POO de leur
réification dans le langage
C++
- Sur le plan des modalités, nous avons fait un hybride entre
présentation des concepts et leur réification dans ce langage, puisque
la plupart d'entre vous ne sont pas familières ou familiers avec
C++
- Nous nous sommes arrêtés au début de la section sur le
polymorphisme à la page 33. Nous
poursuivrons à la séance S01
|
S01
|
MER
|
31
août
|
Au menu :
- Poursuite de l'examen de ce mécanisme d'abstraction qu'est la classe
(mot-clé class ou mot-clé
struct en
C++)
- Nous utilisons les notes de cours en soutien, en travaillant à
partir du chapitre 4 (voir les
notes sur ce chapitre, plus bas)
- Nous essaierons, tout au long de notre cheminement, de distinguer
les concepts généraux de la
POO de leur
réification dans le langage
C++
- Sur le plan des modalités, nous avons fait un hybride entre
présentation des concepts et leur réification dans ce langage, puisque
la plupart d'entre vous ne sont pas familières ou familiers avec
C++
- Nous reprenons au début de la section sur le
polymorphisme à la page 33
- Nous avons fait plusieurs digressions, entre autres sur les notions
de pointeur et
de référence
qui peuvent vous être étrangères si vous avez utilisé des langages sans
accès direct aux objets par le passé
- Nous avons discuté de la
Sainte-Trinité
- Nous avons discuté de sémantique de passage de paramètres
- Pour certains aspects, nous avons utilisé le
Compiler Explorer, aussi appelé
godbolt du nom de son (très
sympathique) auteur
- Nous nous sommes arrêtés au début de la section sur les « fonctions
de base du type abstrait » à la page 36. Nous poursuivrons à la
séance S02
|
S02
|
VEN
|
2 sept.
|
Au menu :
- Poursuite de l'examen de ce mécanisme d'abstraction qu'est la classe
(mot-clé class ou mot-clé
struct en
C++)
- Nous utilisons les notes de cours en soutien, en travaillant à
partir du chapitre 4 (voir les
notes sur ce chapitre, plus bas)
- Nous essaierons, tout au long de notre cheminement, de distinguer
les concepts généraux de la
POO de leur
réification dans le langage
C++
- Sur le plan des modalités, nous avons fait un hybride entre
présentation des concepts et leur réification dans ce langage, puisque
la plupart d'entre vous ne sont pas familières ou familiers avec
C++
- Nous reprenons au début de la section sur les « fonctions de base du
type abstrait » à la page 36
- Plusieurs des aspects des sections aujourd'hui étaient des rappels
d'idées explorées lors des cours précédents
- Nous avons discuté de surcharge d'opérateurs
- Nous avons aussi commencé à discuter de programmation générique
- Nous nous sommes arrêtés à la fin du chapitre 4
Pour quelques exercices, voir
Exercices--serie-00.html
|
S03
|
MER
|
7 sept.
|
Au menu :
|
s/o
|
VEN
|
9 sept.
|
Pas de cours cette semaine; je serai hors du pays pour donner des cours
et une conférence à CppCon. Pour suivre
mes péripéties :
../../Sujets/Orthogonal/cppcon2022.html (je suis en
déplacement dans cette direction aujourd'hui)
|
s/o
|
MER
|
14 sept.
|
Pas de cours cette semaine; je serai hors du pays pour donner des cours
et une conférence à CppCon. Pour suivre
mes péripéties :
../../Sujets/Orthogonal/cppcon2022.html
|
s/o
|
VEN
|
16 sept.
|
Pas de cours cette semaine; je serai hors du pays pour donner des cours
et une conférence à CppCon. Pour suivre
mes péripéties :
../../Sujets/Orthogonal/cppcon2022.html
|
S04
|
LUN
|
19 sept.
|
Au menu :
- Retour sur la série
00 d'exercices
- Pas besoin de tout avoir fait, ne vous en faites pas, mais l'idée
est de voir avec vous ce qui demande plus d'explications et ce qui se
passe bien pour vous
- Questions sur le devoir 1
- Idée d'itérateur
- Écrire un itérateur pour un conteneur
- Exemple avec une liste simplement chaînée
Le code auquel nous en sommes arrivés fut :
// sliste.h
#ifndef SLISTE_H
#define SLISTE_H
#include <cstddef> // std::size_t
#include <utility> // std::swap
#include <iostream>
template <class T>
class sliste {
// p-ê mouvement (ctor, affectation)
struct noeud {
T valeur;
noeud *prochain = nullptr;
noeud(const T &valeur) : valeur{ valeur } {
}
};
noeud *tete = nullptr;
noeud *queue = nullptr;
std::size_t nelems = 0;
public:
class iterator {
noeud *cur = nullptr;
public:
iterator() = default;
iterator(noeud *p) : cur{ p } {
}
T &operator*() {
return cur->valeur;
}
const T &operator*() const {
return cur->valeur;
}
iterator &operator++() { // version préfixe : ++i
cur = cur->prochain;
return *this;
}
iterator operator++(int) { // version suffixe : i++
iterator temp = *this;
operator++();
return temp;
}
bool operator==(const iterator &autre) const {
return cur == autre.cur;
}
bool operator!=(const iterator &autre) const {
return !(*this == autre);
}
};
// complexité : O(1) donc constant
bool empty() const {
return tete == nullptr;
}
sliste() = default;
// complexité : O(n)
sliste(const sliste &autre) {
//for (noeud *p = autre.tete; p != nullptr; p = p->prochain)
// push_back(p->valeur);
for (const T &val : autre)
push_back(val);
}
// complexité : O(1) donc constant
void swap(sliste &autre) {
using std::swap;
swap(tete, autre.tete);
swap(queue, autre.queue);
swap(nelems, autre.nelems);
}
// complexité : O(ctor de copie)... donc linéaire
sliste& operator=(const sliste &autre) {
sliste{ autre }.swap(*this);
return *this;
}
// complexité : O(n) donc linéaire
void clear() {
while (!empty())
pop_front();
}
// complexité : O(n) donc linéaire
~sliste() {
clear();
}
// précondition : !empty()
// complexité : O(1)
void pop_front() {
noeud *p = tete->prochain;
delete tete;
--nelems;
tete = p;
if (tete == nullptr)
queue = nullptr;
}
// complexité : O(1) donc constant
void push_front(const T &val) {
noeud *p = new noeud{ val };
p->prochain = tete;
tete = p;
++nelems;
if (queue == nullptr)
queue = p;
}
// complexité : O(1) donc linéaire
void push_back(const T &val) {
if (empty())
push_front(val);
else {
noeud *p = new noeud{ val };
queue->prochain = p;
queue = p;
++nelems;
}
}
// précondition : !empty()
// complexité : O(1) donc constant
T& front() {
return tete->valeur;
}
// précondition : !empty()
// complexité : O(1) donc constant
const T& front() const {
return tete->valeur;
}
// précondition : !empty()
// complexité : O(1) donc constant
T &back() {
return queue->valeur;
}
// précondition : !empty()
// complexité : O(1) donc constant
const T &back() const {
return queue->valeur;
}
// complexité : O(1)
std::size_t size() const {
return nelems;
}
//// NOTE : ceci est une patch temporaire, le temps qu'on
//// ait une meilleure interface... avec des itérateurs
//void afficher() {
// for (noeud *p = tete; p != nullptr; p = p->prochain)
// std::cout << p->valeur << ' ';
//}
//
iterator begin() {
return { tete };
}
iterator end() {
return {};
}
bool operator==(const sliste &autre) const {
if (size() != autre.size()) return false;
for (iterator p = begin(), q = autre.begin();
p != end(); ++p, ++q)
if (*p != *q)
return false;
return true;
}
};
#endif
// principal.cpp
#include <string>
int main() {
using namespace std;
cout << "Taille en bytes d'un sliste<int> : " << sizeof(sliste<int>) << endl;
int tab[]{ 2,3,5,7,11 };
sliste<int> lst;
for (int n : tab)
lst.push_back(n);
// lst.afficher();
for (int n : lst)
cout << n << ' ';
std::cout << '\n';
sliste<int> ze_copie = lst;
ze_copie.pop_front();
for (int n : lst)
cout << n << ' ';
std::cout << '\n';
for (int n : ze_copie)
cout << n << ' ';
std::cout << '\n';
sliste<string> slst;
slst.push_front("prof");
slst.push_front("mon");
slst.push_front("j'aime");
for (const string &s : slst)
cout << s << ' ';
cout << '\n';
}
|
S05
|
MER
|
21 sept.
|
Au menu :
- Survol des conteneurs standards de C++
- Complexité algorithmique des opérations clés
- Faire un choix éclairé
- S'il reste du temps :
- Implémenter un équivalent (naïf; pour un véritable équivalent,
faudra venir me voir en IFT611 🙂) de
std::vector<T>
- Attention : std::vector<T> et le
Array<T,N> de votre devoir 1 sont
des classes semblables, mais différentes à plusieurs égards (en
particuler, un Array<T,N> a une capacité
fixe alors que la capacité d'un std::vector<T>
est dynamique)
|
S06
|
VEN
|
23 sept.
|
Au menu :
- Journée atypique car plusieurs d'entre vous ont fait le choix
(légitime) de participer aux manifestations entourant les événements
pour la justice climatique
- Les cours n'étant pas suspendus, et notre groupe étant incomplet,
j'ai préféré offrir une séance informelle de soutient au travail
pratique... ce qui fut, je pense, utile
- Au passage, j'ai glissé quelques bribes d'information pour divertir
les gens présents. Entre autres, j'ai fait remarquer que ceci :
int main() {
enum { N = 10'000 };
int tab[N * N]{}; // initialisé avec des zéros... oups!
}
-
... ne fonctionnera pas en pratique même si la taille du tableau
tab est connue à la compilation, du fait que
N*N*sizeof(int) est approximativement 4 Mo
alors que la pile d'exécution d'un programme est typiquement de
1 Mo ou de 2 Mo (ça varie selon les
systèmes d'exploitation), du moins pas défaut.
-
Cela explique d'ailleurs l'intérêt d'une classe comme le
Array<T,N> de votre devoir our prendre en charge l'allocation et
la libération dynamique des ressources. En effet, si nous le faisions
« manuellement », il nous faudrait écrire quelque chose
comme :
int main() {
enum { N = 10'000 };
int * tab = new int[N * N]{}; // initialisé avec des zéros...
// ICI : ... le programme s'exécute et il se passe "des choses" ...
delete [] tab;
}
-
... or ce serait une approche fragilisante, car si une instruction dans
le code remplacé ici par le commentaire débutant par
ICI devait lever une exception, nous risquerions de laisser fuir
les ressources associées à tab
-
Aussi, j'ai laissé planer que le code suivant :
int main() {
enum { N = 10'000 };
int * tab = new int[N * N]{}; // initialisé avec des zéros
// remplir le tableau avec des valeurs impaires
// question : que pensez-vous de ce code?
for (int i = 0; i != N * N; ++i)
tab[i] = 2 * i + 1;
// ...
delete []tab;
}
-
... bien que fonctionnel, manque un peu d'élégance. J'ai alors soulevé
que si nous remplacions le code de génération de valeurs par un appel de
fonction...
int generer_impair(int n) {
return 2 * n + 1;
}
int main() {
enum { N = 10'000 };
int * tab = new int[N * N]{}; // initialisé avec des zéros
// question : que pensez-vous de ce code?
for (int i = 0; i != N * N; ++i)
tab[i] = generer_impair(i);
// ...
delete []tab;
}
-
... pour clarifier le propos, nous gagnerions beaucoup au change.
Ensuite, si nous essayons d'élever le niveau d'abstraction de notre
boucle pour remplacer le recours à un tableau et un indice par le
recours à un simple pointeur, nous arriverions à une boucle moins
élégante, mais plus féconde...
int generer_impair(int n) {
return 2 * n + 1;
}
int main() {
enum { N = 10'000 };
int * tab = new int[N * N]{}; // initialisé avec des zéros
// question : que pensez-vous de ce code?
int i = 0; // le i est un artéfact du code pour générer des impairs, et n'est
// pas essentiel au propos ici
for (int *p = tab; p != tab + N * N; ++p)
*p = generer_impair(i++);
// ...
delete []tab;
}
-
... car elle serait exprimée en termes d'une entité qui s'apparente à un
itérateur. Sachant cela, nous pourrions abstraire le concept que
modélise cette boucle (remplir une séquence de valeurs à partir
d'expressions dépendant de la position des valeurs dans la séquence) en des termes plus généraux
(mais avec du code qui ne compilera pas pour le moment) :
int generer_impair(int n) {
return 2 * n + 1;
}
void remplir(debut, fin, f) {
int n = 0;
for (; debut != fin; ++debut)
*debut = f(n++);
}
int main() {
enum { N = 10'000 };
int * tab = new int[N * N]{}; // initialisé avec des zéros
// question : que pensez-vous de ce code?
remplir(tab, tab + N, generer_impair);
// ...
delete []tab;
}
-
... ce qui devient, si nous utilisons la syntaxe du code générique de
C++, ceci (qui compile et fonctionne!) :
int generer_impair(int n) {
return 2 * n + 1;
}
template <class It, class F> // It pour itérateur, F pour fonction
void remplir(debut, fin, f) {
int n = 0;
for (; debut != fin; ++debut)
*debut = f(n++);
}
int main() {
enum { N = 10'000 };
int * tab = new int[N * N]{}; // initialisé avec des zéros
// question : que pensez-vous de ce code?
remplir(tab, tab + N, generer_impair);
// ...
delete []tab;
}
Que pensez-vous du code client (de l'appel de la fonction remplir
dans le programme principal)? Il me semble que, sur le plan de
l'utilisabilité, nous avons fait un gain net... Notez aussi que la fonction
remplir est exprimée non pas en termes de pointeurs mais bien en termes des
opérations d'un itérateur (comme vector<T>::iterator
ou... Array<T,N>::iterator par exemple), et serait
donc applicable à tout conteneur, pas seulement à un tableau brut.
|
S07
|
LUN
|
26 sept.
|
N'oubliez pas que, de manière exceptionnelle, notre séance
d'aujourd'hui se tiendra une heure plus tard qu'à l'habitude, soit de 14 h
30 à 16 h 20
car votre chic prof a des obligations administratives...
Au menu :
- Implémenter un équivalent (naïf; pour un véritable équivalent,
faudra venir me voir en IFT611 🙂) de
std::vector<T>
- Attention : std::vector<T> et le
Array<T,N> de votre devoir 1 sont
des classes semblables, mais différentes à plusieurs égards (en
particuler, un Array<T,N> a une capacité
fixe alors que la capacité d'un std::vector<T>
est dynamique)
- Modéliser un tableau 2D,
3D, ..., ND
- Examen sommaire de l'équivalent (contextuel) des itérateurs dans
d'autres langages
- Interfaces IEnumerable<T> et
IEnumerator<T> de
C#
- Interfacesset
Iterator<T> de
Java
- Intervalles à demi ouverts selon les langages ([debut,fin) en
C++, (debut,fin] en
Java et en
C#)
- Réécriture des boucles sur des intervalles dans ces langages
- En fonction du temps qu'il reste :
- Implémenter un équivalent (naïf) de
std::deque<T>
Le code vu en classe était (note : il y a beaucoup d'allocations là-dedans pour le pauvre
petit coeur de votre chic prof...). Je l'ai présenté en classe par étapes,
et j'ai essayé de respecter la même forme ici :
#include <cstddef> // std::size_t
#include <utility> // std::swap
template <class T>
class deque {
T **debut{};
size_t nelems{}, cap{}, zero{};
// ...
// ...
bool full() const {
return size() == capacity();
}
public:
std::size_t size() const {
return nelems;
}
std::size_t capacity() const {
return cap;
}
bool empty() const {
return !size();
}
// ...
// ...
class iterator {
std::size_t cur;
deque &source;
public:
iterator(deque &source, std::size_t pos)
: source{ source }, cur{ pos } {
}
iterator &operator++() {
++cur;
return *this;
}
iterator operator++(int) {
auto temp = *this;
operator++();
return temp;
}
T &operator*() {
return source[cur];
}
const T &operator*() const {
return source[cur];
}
bool operator==(const iterator &autre) const {
return cur == autre.cur;
}
bool operator!=(const iterator &autre) const {
return !(*this == autre);
}
};
// ...
// ...
class const_iterator {
std::size_t cur;
const deque &source;
public:
const_iterator(const deque &source, std::size_t pos)
: source{ source }, cur{ pos } {
}
const_iterator &operator++() {
++cur;
return *this;
}
const_iterator operator++(int) {
auto temp = *this;
operator++();
return temp;
}
const T &operator*() const {
return source[cur];
}
bool operator==(const iterator &autre) const {
return cur == autre.cur;
}
bool operator!=(const iterator &autre) const {
return !(*this == autre);
}
};
// ...
// ...
iterator begin() {
return { *this, 0 };
}
iterator end() {
return { *this, size() };
}
const_iterator begin() const {
return { *this, 0 };
}
const_iterator end() const {
return { *this, size() };
}
// ...
// ...
deque() = default;
deque(std::size_t dim)
: nelems{ dim }, cap{ dim }, zero{} {
if (dim > 0)
debut = new T *[capacity];
else
debut = nullptr;
for (std::size_t i = 0; i < size(); ++i)
debut[i] = new T{};
}
// ...
// ...
~deque() {
clear();
}
// ...
// ...
deque(const deque &autre) : deque{ autre.size() } {
for (std::size_t i = 0; i != size(); ++i)
*debut[i] = *autre.debut[(i + autre.zero) % autre.cap];
}
// ...
// ...
void swap(deque &autre) {
using std::swap;
swap(debut, autre.debut);
swap(cap, autre.cap);
swap(nelems, autre.nelems);
swap(zero, autre.zero);
}
deque &operator=(const deque &autre) {
deque{ autre }.swap(*this);
return *this;
}
// ...
// ...
void resize(std::size_t nouv_nelems) {
// ...
}
// ...
// ...
void reserve(std::size_t nouv_cap) {
// ...
}
// ...
// ...
void clear() {
for (std::size_t i = 0; i != size(); ++i)
delete debut[(i + zero) % capacity()];
delete[] debut;
debut = nullptr;
zero = 0;
cap = nelems = 0;
}
// ...
// ...
T &operator[](std::size_t n) {
return *debut[(n + zero) % capacity()];
}
const T &operator[](std::size_t n) const {
return *debut[(n + zero) % capacity()];
}
// ...
// ...
void push_back(const T &val) {
// ...
}
void pop_back() {
// ...
}
void push_front(const T &val) {
// ...
}
void pop_front() {
// ...
}
};
// ...
// ...
#include <string>
#include <iostream>
int main() {
using namespace std;
deque<int> v;
for (int i : { 2, 3, 5, 7, 11 })
v.push_back(i);
for (int n : v)
cout << n << ' ';
cout << endl;
while (!v.empty()) {
v.pop_front();
for (int n : v)
cout << n << ' ';
cout << endl;
}
}
|
S08
|
MER
|
28 sept.
|
Au menu :
- On joue avec les listes
- Listes simplement chaînées (on en a déjà fait une à la séance
S04)
- Liste doublement chaînée
- Sans noeuds sentinelles
- Impact sur les opérations
- Avec noeuds sentinelles
- Impact sur les opérations
- Inversion de l'ordre des éléments
- Splicing
- Insertion
- Suppression
|
s/o
|
VEN
|
30 sept.
|
Journée nationale de la vérité et de la réconciliation (levée des
cours)
|
S09
|
MER
|
5 oct.
|
Au menu :
- On joue avec les listes (suite)
- Listes simplement chaînées (on en a déjà fait une à la séance
S04)
- Liste doublement chaînée
- Sans noeuds sentinelles
- Impact sur les opérations
- Avec noeuds sentinelles
- Impact sur les opérations
- Inversion de l'ordre des éléments
- Splicing
- Insertion
- Suppression
|
S10
|
VEN
|
7 oct.
|
Au menu :
- Support pour le devoir 2
- Trucs amusants qui me passeront pas la tête 🙂
Puisque certains ont fini le devoir 2 et d'autres ont besoin de
soutien, j'ai laissé un petit défi pour vous occuper :
« Codez (pour le plaisir!) la classe
FileCirculaire<T,N> ayant :
- Une capacité fixe (de N)
- Un push() (en pratique, un
push_back()) de complexité
- Un pop() (en pratique, un
pop_front()) de complexité
- Un empty() de complexité
- Un full() de complexité
- Un size() de complexité
- ... et aucune allocation dynamique de mémoire! »
Chose amusante : le défi ici est d'écrire...
full() et empty()
|
S11
|
MER
|
12 oct.
|
Au menu :
- Bref retour sur le devoir 1
- En particulier, sur les signatures de fusion
et de subset
- Retour sur la question de divertissement de la séance
S10. Exemple de solution de type Owning :
template <class T, int N>
class file_circulaire {
static_assert(N > 1); // bof
using buf_type = std::array<T,N>;
buf_type buf {};
int ins_pt = 0;
int ext_pt = 0;
static int next(int n) {
return ++n, n == N ? 0 : n;
}
static int prev(int n) {
return n == 0 ? N - 1 : n - 1;
}
bool full() const noexcept {
return next(ins_pt) == ext_pt;
}
bool empty() const noexcept {
return ins_pt == ext_pt;
}
public:
void push(const T &obj) {
if (full()) throw file_pleine{};
buf[ins_pt] = obj;
ins_pt = next(ins_pt);
}
void pop() {
if (empty()) throw file_vide{};
ext_pt = next(ext_pt);
}
T top() {
if (empty()) throw file_vide{};
return buf[ext_pt];
}
bool try_push(const T &obj) {
if (full()) return false;
buf[ins_pt] = obj;
ins_pt = next(ins_pt);
return true;
}
bool try_pop(T &obj) {
if (empty()) return false;
obj = buf[ext_pt];
ext_pt = next(ext_pt);
return true;
}
};
- ... et exemple de solution de type Non-Owning :
template <class T, int N>
class file_circulaire {
static_assert(N > 1); // bof
T(&buf)[N];
int ins_pt = 0;
int ext_pt = 0;
static int next(int n) {
return ++n, n == N ? 0 : n;
}
static int prev(int n) {
return n == 0 ? N - 1 : n - 1;
}
bool full() const noexcept {
return next(ins_pt) == ext_pt;
}
bool empty() const noexcept {
return ins_pt == ext_pt;
}
public:
file_circulaire(T (&buf)[N]) : buf{ buf } {
}
void push(const T &obj) {
if (full()) throw file_pleine{};
buf[ins_pt] = obj;
ins_pt = next(ins_pt);
}
void pop() {
if (empty()) throw file_vide{};
ext_pt = next(ext_pt);
}
T top() {
if (empty()) throw file_vide{};
return buf[ext_pt];
}
bool try_push(const T &obj) {
if (full()) return false;
buf[ins_pt] = obj;
ins_pt = next(ins_pt);
return true;
}
bool try_pop(T &obj) {
if (empty()) return false;
obj = buf[ext_pt];
ext_pt = next(ext_pt);
return true;
}
};
- Discussion d'une structure de données atypique (mais utile) : la
SkipList
- Notez que, bien qu'utile, cette structure de données ne fera pas
l'objet de questions à l'examen de mi-session de
S13
|
S12
|
VEN
|
14 oct.
|
Au menu :
- Quelques exercices de révision en vue de l'examen prévu pour
S13
|
S13
|
MER
|
19 oct.
|
Examens périodiques (levée des cours). Le nôtre se tiendra au local
6680 de 13 h 30 à 15 h 20 (16 h si vous avez le statut de EBP)
|
s/o
|
MER
|
26 oct.
|
Relâche (levée des cours)
|
s/o |
VEN
|
28 oct.
|
Relâche (levée des cours)
|
S14
|
LUN
|
31 oct.
|
Au menu :
- Petit retour sur l'examen de mi-session
- Introduction aux arbres (p. 93 des notes de cours)
- Quelques éléments de vocabulaire :
- Racine
- Feuille
- Niveau
- Noeud intérieur
- Arc
- Noeud parent
- Noeuds enfants
- Noeuds frères ou soeurs (siblings)
- Longueur d'un chemin
- Hauteur de l'arbre
- Parcours possibles :
- inordre (gauche → parent → autres)
- préordre (parent → enfants)
- postordre (enfants → parent)
- largeur (racine → enfants de la racine → enfants de chaque
enfant, etc.)
|
S15
|
MER
|
2 nov.
|
Au menu :
- On essaie de construire un petit arbre n-aire, capable de représenter
l'arbre présenté à la page 93 de vos notes de cours
Rappel (tiré de vos notes de cours) :
- Parcours inordre : on parcourt d'abord le premier sous arbre, en inordre, puis on visite
la racine, puis chacun des autres sous arbres, en inordre
- Parcours préordre : on visite d’abord la racine, puis on parcourt chacun des sous-arbres,
en préordre. Ce type de parcours est aussi appelé « en profondeur », ou depth-first
- Parcours postordre : on parcourt d'abord tous les sous-arbres, en postordre, puis on visite
la racine
- Je vous propose le cahier de charges suivant :
- Une classe noeud<T> exposant :
- Une méthode valeur()
retournant la valeur du noeud
- Un constructeur paramétrique acceptant une valeur en
paramètre et l'entreposant dans le noeud
- Une méthode ajouter_enfant()
acceptant une valeur en paramètre et ajoutant un noeud enfant
de cette valeur au noeud *this
- Une méthode enfant() acceptant
en paramètre une valeur et retournant une référence sur
l'enfant (ne considérez que les enfants directs) du noeud
contenant cette valeur. Si aucun noeud de cette valeur n'est
un enfant de *this, levez une
exception
- Une méthode vider() qui
élimine les noeuds enfants directs de
*this
- Un destructeur
- (pour fins de simplicité, faites en sorte que les noeuds
soient incopiables)
- Une méthode est_feuille()
retournant true seulement si le noeud est une feuille
- Une méthode pour réaliser une visite inordre à
partir du noeud *this
- Une méthode pour réaliser une visite préordre à
partir du noeud *this
- Une méthode pour réaliser une visite postordre à
partir du noeud *this
- (On n'implémentera pas une méthode pour réaliser une
visite en largeur à partir
du noeud *this)
- Une classe arbre<T> exposant :
- Un constructeur par défaut créant un arbre vide
- Une méthode empty() pour
tester si l'arbre est vide
- Une méthode vider() qui
élimine les noeuds enfants directs de
*this
- Un destructeur
- (pour fins de simplicité, faites en sorte que l'arbre soit
incopiable)
- Une méthode racine()
retournant une référence sur le noeud racine. Si l'arbre est
vide, levez une exception
- Une méthode creer_racine()
créant un noeud racine d'une valeur donnée. Si l'arbre n'est
pas vide au moment de l'appel, levez une exception
- Une méthode pour réaliser une visite inordre de
l'arbre
- Une méthode pour réaliser une visite préordre de
l'arbre
- Une méthode pour réaliser une visite postordre de
l'arbre
- (On n'implémentera pas une méthode pour réaliser une visite en largeur de l'arbre)
- Si vous en avez le temps, implémentez une fonction
hauteur() qui retournera (surprise!) la hauteur de l'arbre.
Calculez cette hauteur sur demande
À titre d'exemple, le programme suivant devrait construire l'arbre
n-aire de la page 93 :
#include <iostream>
int main() {
using namespace std;
arbre<char> a;
a.creer_racine('A')
.ajouter_enfant('B')
.ajouter_enfant('E')
.ajouter_enfant('J')
.ajouter_enfant('L');
a.get_racine().enfant('B').ajouter_enfant('F');
a.get_racine().ajouter_enfant('C');
a.get_racine().ajouter_enfant('D')
.ajouter_enfant('G');
a.get_racine().enfant('D')
.ajouter_enfant('H');
a.get_racine().enfant('D')
.ajouter_enfant('I')
.ajouter_enfant('K');
// visite inordre de a devrait afficher : LJEBFACGDHKI
// visite preordre de a devrait afficher : ABEJLFCDGHIK
// visite postordre de a devrait afficher : LJEFBCGHKIDA
}
|
S16
|
VEN
|
4 nov.
|
Au menu : séance suspendue (votre chic prof a les mains pleines
dans le local voisin)
|
S17
|
MER
|
9 nov.
|
Au menu :
- Retour sur l'arbre n-aire sur lequel nous avons oeuvré à la séance
S15
- Examen d'un arbre de recherche binaire (Binary Search Tree,
BST) et de quelques opérations clés sur ce type
Implémentation naïve d'un BST :
|
S18
|
VEN
|
11 nov.
|
Au menu :
- Poursuite du regard sur les
BST
- Morceaux de robot formels à celui d'entre vous qui avait noté un
cas incorrect avec la solution de
S17 (merci!)
- Dans vos notes de cours, ces arbres sont nommés
ABR et sont
aux pages 97-100
- Examen des arbres
AVL (Adel'son-Vel'skii et Landis)
- Dans vos notes de cours, ces arbres sont aux pages
100-108
|
S19
|
MER
|
16 nov.
|
Au menu : on sort du cadre et on fait des boîtes imbriquées
|
S20
|
VEN
|
18 nov.
|
Au menu :
- Parcours d'un
BST (fait à partir d'un arbre
AVL)
- Implémentation naïve (complexité par progrès, donc pour un parcours complet
- Implémentation raffinée (complexité amortie par progrès,
donc en moyenne pour un parcours complet)
- Idée de couture pour un arbre
Version .pdf du petit document préparé
avec amour :
Arbres-AVL-et-rotations.pdf
Implémentation naïve (et différente de celle des notes de cours) d'un arbre AVL :
#include <utility>
#include <string>
//
// Ce qui caractérise un arbre binaire de recherche, c'est
// l'ordre des valeurs des noeuds. Dans un tel arbre, tous
// les noeuds du sous arbre de gauche sont inférieurs à la
// racine, et tous ceux du sous arbre de droite sont
// supérieurs à la racine
//
// Un arbre AVL est un arbre binaire de recherche tel que
// la différence entre les hauteurs des sous-arbres est au
// plus 1 (ceci évite des cas dégénérés qui ralentiraient
// la recherche en déséquilibrant l'arbre)
//
template <class T>
class avl_tree {
struct node {
T valeur;
node *parent = nullptr; // ICI
node *gauche = nullptr;
node *droite = nullptr;
node(const T &valeur) : valeur{ valeur } {
}
bool est_feuille() const noexcept {
return nb_enfants() == 0;
}
int nb_enfants() const noexcept {
return static_cast<int>(gauche != nullptr) +
static_cast<int>(droite != nullptr);
}
};
node *racine = nullptr;
public:
bool empty() const noexcept {
return !racine;
}
avl_tree() = default;
// fonctions spéciales (copie supprimée pour simplifier le propos)
avl_tree(const avl_tree &) = delete;
avl_tree &operator=(const avl_tree &) = delete;
~avl_tree() {
while (!empty())
supprimer(racine->valeur);
}
bool contient(const T &valeur) const {
if (empty()) return false;
return contient(racine, valeur);
}
void balancer(node *p) {
if (!p) return;
if (int balance = hauteur(p->gauche) - hauteur(p->droite);
balance == -2) {
// débalancement à droite
if (int balance_droite = hauteur(p->droite->droite) - hauteur(p->droite->gauche);
balance_droite == -1) { // rotation droite-gauche
rotation_droite(p->droite);
}
rotation_gauche(p);
} else if (balance == 2) {
// débalancement à gauche
if (int balance_gauche = hauteur(p->gauche->droite) - hauteur(p->gauche->gauche);
balance_gauche == 1) { // Cas de rotation gauche-droite.
rotation_gauche(p->gauche);
}
rotation_droite(p);
}
balancer(p->parent);
}
void rotation_gauche(node *p) {
if (!p) return;
node *droite = p->droite;
// attacher le parent de p à son enfant de droite
if (p->parent) {
if (p->parent->droite == p)
p->parent->droite = droite;
else
p->parent->gauche = droite;
}
droite->parent = p->parent;
// lier p et le sous-arbre gauche de l'enfant de droite
if (droite->gauche)
droite->gauche->parent = p;
p->droite = droite->gauche;
// placer droite au sommet de ce sous-arbre
p->parent = droite;
droite->gauche = p;
if (p == racine)
racine = droite;
}
void rotation_droite(node *p) {
if (!p) return;
node *gauche = p->gauche;
// attacher le parent de p à son enfant de gauche
if (p->parent) {
if (p->parent->gauche == p)
p->parent->gauche = gauche;
else
p->parent->droite = gauche;
}
gauche->parent = p->parent;
// lier p et le sous-arbre droite de l'enfant de gauche
if (gauche->droite)
gauche->droite->parent = p;
p->gauche = gauche->droite;
// placer gauche au sommet de ce sous-arbre
p->parent = gauche;
gauche->droite = p;
if (p == racine)
racine = gauche;
}
bool ajouter(const T &valeur) {
node *p = racine,
*q = nullptr;
while (p) {
q = p;
if (valeur < p->valeur)
p = p->gauche;
else
p = p->droite;
}
node *r = new node{ valeur };
if (!q)
racine = r;
else {
if (valeur < q->valeur)
q->gauche = r;
else
q->droite = r;
r->parent = q; // ICI
}
balancer(trouver(r->parent, valeur)); // ICI
return true;
}
private:
bool contient(node *p, const T &valeur) const {
if (!p) return false;
if (valeur == p->valeur) return true;
if (valeur < p->valeur)
return p->gauche ? contient(p->gauche, valeur) : false;
return p->droite ? contient(p->droite, valeur) : false;
}
node *trouver(node *p, const T &valeur) const {
if (!p || valeur == p->valeur) return p;
if (valeur < p->valeur)
return trouver(p->gauche, valeur);
return trouver(p->droite, valeur);
}
public:
template <class F>
void visiter_preordre(F fct) { // depth-first
visiter_preordre(fct, racine, std::string{});
}
private:
template <class F>
void visiter_preordre(F fct, node *p, const std::string &prefixe) { // depth-first
using namespace std;
fct.entrer();
if (!p) return;
fct(p->valeur, prefixe + "[" + to_string(hauteur(p->gauche) - hauteur(p->droite)) + "] : "s);
visiter_preordre(fct, p->gauche, "g"s);
visiter_preordre(fct, p->droite, "d"s);
fct.sortir();
}
public:
void supprimer(const T &valeur) {
if (empty()) return;
supprimer(racine, valeur);
}
private:
// précondition : p != nullptr
bool supprimer_gauche(node * &p, const T &valeur) {
supprimer(p->gauche, valeur);
if(p)
balancer(p);
return true;
}
// précondition : p != nullptr
bool supprimer_droite(node * &p, const T &valeur) {
supprimer(p->droite, valeur);
if (p)
balancer(p);
return true;
}
bool supprimer(node *&p, const T &valeur) {
if (!p)
return false;
if (valeur < p->valeur)
return supprimer_gauche(p, valeur);
if (p->valeur < valeur)
return supprimer_droite(p, valeur);
if (p->gauche) {
p->valeur = trouver_maximal(p->gauche)->valeur;
return supprimer_gauche(p, p->valeur);
}
node *victime = p;
p = p->droite;
if (p) p->parent = victime->parent;
delete victime;
return true;
}
node *trouver_minimal(node *p) {
return !p ? p : p->gauche ? trouver_minimal(p->gauche) : p;
}
node *trouver_maximal(node *p) {
return !p ? p : p->droite ? trouver_maximal(p->droite) : p;
}
public:
T minimum() const {
if (empty()) throw 3;
node *p = racine;
for (; p->gauche; p = p->gauche)
;
return p->valeur;
}
T maximum() const {
if (empty()) throw 3;
node *p = racine;
for (; p->droite; p = p->droite)
;
return p->valeur;
}
int hauteur() const {
return hauteur(racine);
}
private:
int hauteur(node *p) const {
if (!p) return 0;
int hauteur_gauche = hauteur(p->gauche);
int hauteur_droite = hauteur(p->droite);
return 1 + (hauteur_gauche < hauteur_droite ?
hauteur_droite : hauteur_gauche);
}
friend class iterator_naif;
friend class iterator_raffine;
public:
class iterator_naif {
friend class avl_tree<T>;
node *cur;
avl_tree &src;
iterator_naif(node *init, avl_tree &src)
: cur{ init }, src{ src } {
}
iterator_naif(avl_tree &src)
: cur{ nullptr }, src{ src } {
}
// ICI :
// vos notes de cours, p.109
// note : O(log n) par appel, donc O(n log n) pour un parcours
// complet... aussi long qu'un tri!
node *suivant(node *p) {
node *q = src.racine;
node *sq = p->droite;
if (sq)
while (sq->gauche)
sq = sq->gauche;
else
while (q != p)
if (p->valeur < q->valeur)
q = (sq = q)->gauche;
else
q = q->droite;
return sq;
}
public:
iterator_naif &operator++() {
cur = suivant(cur);
return *this;
}
iterator_naif operator++(int) {
auto temp = *this;
operator++();
return temp;
}
bool operator==(const iterator_naif &autre) const {
return cur == autre.cur;
}
bool operator!=(const iterator_naif &autre) const {
return !(*this == autre);
}
T operator*() const { return cur->valeur; }
};
//
// ICI
//
iterator_naif begin_naif() {
return { trouver_minimal(racine), *this };
}
iterator_naif end_naif() {
return { *this };
}
//
// ICI
//
class iterator_raffine {
friend class avl_tree<T>;
node *cur;
avl_tree &src;
iterator_raffine(node *init, avl_tree &src)
: cur{ init }, src{ src } {
}
iterator_raffine(avl_tree &src)
: cur{ nullptr }, src{ src } {
}
// ICI :
// vos notes de cours, p.110
// note : O(1) amorti par appel, donc O(n) en moyenne pour
// un parcours complet... bien mieux!
node *suivant(node *p) {
node *sq;
if (!p->droite)
for (sq = p->parent;
sq && sq->gauche != p;
p = sq, sq = p->parent)
;
else
for (sq = p->droite; sq->gauche; sq = sq->gauche)
;
return sq;
}
public:
iterator_raffine &operator++() {
cur = suivant(cur);
return *this;
}
iterator_raffine operator++(int) {
auto temp = *this;
operator++();
return temp;
}
bool operator==(const iterator_raffine &autre) const {
return cur == autre.cur;
}
bool operator!=(const iterator_raffine &autre) const {
return !(*this == autre);
}
T operator*() const { return cur->valeur; }
};
//
// ICI
//
iterator_raffine begin_raffine() {
return { trouver_minimal(racine), *this };
}
iterator_raffine end_raffine() {
return { *this };
}
};
#include <iostream>
#include <string>
using namespace std;
struct afficheur {
int indent = -1;
void entrer() {
++indent;
}
void sortir() {
++indent;
}
void operator()(int n) const {
cout << string(2 * indent, ' ') << n << endl;
}
};
struct afficheur_annote {
int indent = -1;
void entrer() {
++indent;
}
void sortir() {
++indent;
}
template <class T>
void operator()(const T &val, const string &prefixe = {}) const {
cout << string(2 * indent, ' ') << prefixe << val << endl;
}
};
enum class operation { inserer, supprimer, quitter };
template <class T>
void mise_a_jour(avl_tree<T> &avlt, const T &val, operation op) {
if (op == operation::inserer)
cout << "Insertion de " << val << '\n';
else if (op == operation::supprimer)
cout << "Suppression de " << val << '\n';
else
return;
cout << "Avant :\n";
avlt.visiter_preordre(afficheur_annote{});
if (op == operation::inserer)
avlt.ajouter(val);
else if (op == operation::supprimer)
avlt.supprimer(val);
cout << "Apres :\n";
avlt.visiter_preordre(afficheur_annote{});
cout << string(70, '-') << '\n';
}
#include <utility>
pair<operation, int> lire_choix() {
auto est_valide = [](char c) {
return c == 'a' || c == 's' || c == 'q';
};
cout << "Operation : [a]jouter, [s]upprimer, [q]uitter ? ";
char c;
while (cin.get(c) && !est_valide(c))
;
if (c != 'q') {
cout << "quelle valeur? ";
if(int valeur; cin >> valeur)
return c == 'a'? pair{ operation::inserer, valeur } :
pair{ operation::supprimer, valeur };
}
return { operation::quitter, 0 };
}
#include <vector>
#include <random>
#include <algorithm>
int main() {
avl_tree<int> avl;
int vals[]{ 25, 35, 30, 20, 15, 5, 28, 50, 45, 48, 43, 1, 3, 7, 2 };
for (auto n : vals)
mise_a_jour(avl, n, operation::inserer);
// source d'entropie
random_device rd;
mt19937 prng{ rd() };
vector<int> v{ begin(vals), end(vals) };
shuffle(begin(v), end(v), prng);
while (!v.empty()) { // O(n)
mise_a_jour(avl, v.back(), operation::supprimer);
v.pop_back(); // O(1)
}
//for (auto [choix, val] = lire_choix();
// choix != operation::quitter;
// tie(choix, val) = lire_choix()) {
// mise_a_jour(avl, val, choix);
//}
cout << '\n';
if (!avl.empty()) {
cout << "Noeud de valeur minimale : "
<< avl.minimum() << '\n';
cout << "Noeud de valeur maximale : "
<< avl.maximum() << '\n';
}
cout << "hauteur : " << avl.hauteur() << endl;
//
// ICI
//
cout << "Parcours sequentiel (naif) :\n\t";
for (auto p = avl.begin_naif(); p != avl.end_naif(); ++p)
cout << *p << ' ';
cout << '\n';
cout << "Parcours sequentiel (raffine) :\n\t";
for (auto p = avl.begin_raffine(); p != avl.end_raffine(); ++p)
cout << *p << ' ';
cout << '\n';
}
|
S21
|
MER
|
23 nov.
|
Au menu :
- Survol (sommaire) des arbres équilibrés en poids
- Survol (sommaire) des tables de hachage
- On fait de la botanique : exercices de rotations
AVL
|
S22
|
VEN
|
25 nov.
|
Au menu :
- Soutien au devoir 5
- On fait de la botanique : exercices de rotation sur des arbres
équilibrés en poids
- Survol (sommaire) des arbres auto-équilibrants, ou Splay Trees,
pp. 116-118 des notes de cours
|
S23
|
MER
|
30 nov.
|
Au menu :
- On fait de la botanique : exercices de rotation sur des arbres
équilibrés en poids (bis.)
- Ah, les erreurs absolument bêtes...
- Examen des B-Trees
- On discute de hachage
|
S24
|
VEN
|
2 déc.
|
Au menu :
Implémentation naïve (et différente de celle des notes de cours) d'un arbre équilibré en poids :
#include <utility>
#include <string>
#include <iostream>
//
// Ce qui caractérise un arbre binaire de recherche, c'est
// l'ordre des valeurs des noeuds. Dans un tel arbre, tous
// les noeuds du sous arbre de gauche sont inférieurs à la
// racine, et tous ceux du sous arbre de droite sont
// supérieurs à la racine
//
// Un arbre AVL est un arbre binaire de recherche tel que
// la différence entre les hauteurs des sous-arbres est au
// plus 1 (ceci évite des cas dégénérés qui ralentiraient
// la recherche en déséquilibrant l'arbre)
//
// Un arbre équilibré en poids est comme un arbre AVL, à ceci
// près qu'on ne rebalance un sous-arbre que si le débalancement
// entre ses branches de gauche et de droite dépasse un certain
// seuil (p.ex.: 3 ou 2.15). Avec un facteur de 3, par exemple,
// il faut que le nombre de noeuds d'un des deux sous-arbres
// soit au moins trois fois le nombre de noeuds de l'autre avant
// de procéder
//
template <class T>
class arbre_equil_poids {
struct node {
T valeur;
int poids = 1; // ICI
node *parent = nullptr;
node *gauche = nullptr;
node *droite = nullptr;
node(const T &valeur) : valeur{ valeur } {
}
bool est_feuille() const noexcept {
return nb_enfants() == 0;
}
int nb_enfants() const noexcept {
return static_cast<int>(gauche != nullptr) +
static_cast<int>(droite != nullptr);
}
};
node *racine = nullptr;
double facteur_desequilibre = 3.0; // gamma
// ICI
int calculer_poids(node *p) {
return !p ? 0 : 1 + calculer_poids(p->gauche) + calculer_poids(p->droite);
}
public:
bool empty() const noexcept {
return !racine;
}
arbre_equil_poids() = default;
// ICI : faudrait valider, évidemment, mais bon...
arbre_equil_poids(double gamma) : facteur_desequilibre{ gamma } {
}
// fonctions spéciales (copie supprimée pour simplifier le propos)
arbre_equil_poids(const arbre_equil_poids &) = delete;
arbre_equil_poids &operator=(const arbre_equil_poids &) = delete;
~arbre_equil_poids() {
while (!empty())
supprimer(racine->valeur);
}
bool contient(const T &valeur) const {
if (empty()) return false;
return contient(racine, valeur);
}
enum class debalancement {
aucun, gauche, droite
};
debalancement evaluer_debalancement(node *p) const {
if (!p) return debalancement::aucun;
// AVL
//if (int balance = hauteur(p->gauche) - hauteur(p->droite);
// balance == -2)
// return debalancement::droite;
//else if (balance == 2)
// return debalancement::gauche;
// patch temporaire
// AVL
if (int balance = hauteur(p->gauche) - hauteur(p->droite);
std::abs(balance) < 2)
return debalancement::aucun;
// WBT
const int poids_gauche = evaluer_poids(p->gauche),
poids_droite = evaluer_poids(p->droite);
if (poids_gauche * facteur_desequilibre < poids_droite) {
std::cout << "Desequilibre vers la droite a partir de "
<< p->valeur
<< " (" << poids_gauche << " < "
<< facteur_desequilibre << " * " << poids_droite << ")\n";
return debalancement::droite;
}
if (poids_droite * facteur_desequilibre < poids_gauche) {
std::cout << "Desequilibre vers la gauche a partir de "
<< p->valeur
<< " (" << poids_gauche << " vs " << poids_droite << ")\n";
return debalancement::gauche;
}
return debalancement::aucun;
}
void balancer(node *p) {
if (!p) return;
// ICI
node *parent = p->parent;
// ICI
if (debalancement balance = evaluer_debalancement(p);
balance == debalancement::droite) {
if (hauteur(p->droite->droite) < hauteur(p->droite->gauche)) {
// rotation droite-gauche
rotation_droite(p->droite);
}
rotation_gauche(p);
} else if (balance == debalancement::gauche) {
if (hauteur(p->gauche->droite) > hauteur(p->gauche->gauche)) {
// rotation gauche-droite
rotation_gauche(p->gauche);
}
rotation_droite(p);
}
// ICI
balancer(p->parent);
}
// ICI : pour éviter la répétition des calculs
int evaluer_poids(node *p) const {
return p ? p->poids : 0;
}
void rotation_gauche(node *p) {
if (!p) return;
// ICI
if (!p->droite && !p->gauche) return;
node *droite = p->droite;
// attacher le parent de p à son enfant de droite
if (p->parent) {
if (p->parent->droite == p)
p->parent->droite = droite;
else
p->parent->gauche = droite;
}
droite->parent = p->parent;
// lier p et le sous-arbre gauche de l'enfant de droite
if (droite->gauche)
droite->gauche->parent = p;
p->droite = droite->gauche;
// placer droite au sommet de ce sous-arbre
p->parent = droite;
droite->gauche = p;
if (p == racine)
racine = droite;
// ICI : recalculer les poids des noeuds qui ont été
// affectés par la rotation
p->poids = 1 + evaluer_poids(p->gauche) + evaluer_poids(p->droite);
droite->poids = 1 + evaluer_poids(droite->gauche) +
evaluer_poids(droite->droite);
if (node * parent = droite->parent; parent)
parent->poids = 1 + evaluer_poids(parent->gauche) +
evaluer_poids(parent->droite);
}
void rotation_droite(node *p) {
if (!p) return;
// ICI
if (!p->droite && !p->gauche) return;
node *gauche = p->gauche;
// attacher le parent de p à son enfant de gauche
if (p->parent) {
if (p->parent->gauche == p)
p->parent->gauche = gauche;
else
p->parent->droite = gauche;
}
gauche->parent = p->parent;
// lier p et le sous-arbre droite de l'enfant de gauche
if (gauche->droite)
gauche->droite->parent = p;
p->gauche = gauche->droite;
// placer gauche au sommet de ce sous-arbre
p->parent = gauche;
gauche->droite = p;
if (p == racine)
racine = gauche;
// ICI : recalculer les poids des noeuds qui ont été
// affectés par la rotation
p->poids = 1 + evaluer_poids(p->gauche) + evaluer_poids(p->droite);
gauche->poids = 1 + evaluer_poids(gauche->gauche) +
evaluer_poids(gauche->droite);
if(node * parent = gauche->parent; parent)
parent->poids = 1 + evaluer_poids(parent->gauche) +
evaluer_poids(parent->droite);
}
bool ajouter(const T &valeur) {
node *p = racine,
*q = nullptr;
while (p) {
q = p;
if (valeur < p->valeur)
p = p->gauche;
else
p = p->droite;
}
node *r = new node{ valeur };
if (!q)
racine = r;
else {
if (valeur < q->valeur)
q->gauche = r;
else
q->droite = r;
for (auto par = q; par; par = par->parent) // ICI
++par->poids;
r->parent = q;
}
balancer(trouver(r->parent, valeur)); // ICI
return true;
}
private:
bool contient(node *p, const T &valeur) const {
if (!p) return false;
if (valeur == p->valeur) return true;
if (valeur < p->valeur)
return p->gauche ? contient(p->gauche, valeur) : false;
return p->droite ? contient(p->droite, valeur) : false;
}
node *trouver(node *p, const T &valeur) const {
if (!p || valeur == p->valeur) return p;
if (valeur < p->valeur)
return trouver(p->gauche, valeur);
return trouver(p->droite, valeur);
}
public:
template <class F>
void visiter_preordre(F fct) { // depth-first
visiter_preordre(fct, racine, std::string{});
}
private:
template <class F>
void visiter_preordre(F fct, node *p, const std::string &prefixe) { // depth-first
using namespace std;
fct.entrer();
if (!p) return;
fct(p->valeur, prefixe + "[h: "s +
to_string(hauteur(p->gauche) - hauteur(p->droite)) +
"; poids: "s + to_string(p->poids) + "] : "s);
visiter_preordre(fct, p->gauche, "g"s);
visiter_preordre(fct, p->droite, "d"s);
fct.sortir();
}
public:
void supprimer(const T &valeur) {
if (empty()) return;
supprimer(racine, valeur);
}
private:
// précondition : p != nullptr
bool supprimer_gauche(node *&p, const T &valeur) {
supprimer(p->gauche, valeur);
if (p)
balancer(p);
return true;
}
// précondition : p != nullptr
bool supprimer_droite(node *&p, const T &valeur) {
supprimer(p->droite, valeur);
if (p)
balancer(p);
return true;
}
bool supprimer(node *&p, const T &valeur) {
if (!p)
return false;
if (valeur < p->valeur)
return supprimer_gauche(p, valeur);
if (p->valeur < valeur)
return supprimer_droite(p, valeur);
if (p->gauche) {
p->valeur = trouver_maximal(p->gauche)->valeur;
return supprimer_gauche(p, p->valeur);
}
// ICI : p va mourir...
if (p) {
// ICI
for (node *par = p->parent; par; par = par->parent)
--par->poids;
}
node *victime = p;
p = p->droite;
if (p && p->parent == victime)
p->parent = victime->parent;
delete victime;
return true;
}
node *trouver_minimal(node *p) {
return !p ? p : p->gauche ? trouver_minimal(p->gauche) : p;
}
node *trouver_maximal(node *p) {
return !p ? p : p->droite ? trouver_maximal(p->droite) : p;
}
public:
T minimum() const {
if (empty()) throw 3;
node *p = racine;
for (; p->gauche; p = p->gauche)
;
return p->valeur;
}
T maximum() const {
if (empty()) throw 3;
node *p = racine;
for (; p->droite; p = p->droite)
;
return p->valeur;
}
int hauteur() const {
return hauteur(racine);
}
private:
int hauteur(node *p) const {
if (!p) return 0;
int hauteur_gauche = hauteur(p->gauche);
int hauteur_droite = hauteur(p->droite);
return 1 + (hauteur_gauche < hauteur_droite ?
hauteur_droite : hauteur_gauche);
}
friend class iterator_naif;
friend class iterator_raffine;
public:
class iterator_naif {
friend class arbre_equil_poids<T>;
node *cur;
arbre_equil_poids &src;
iterator_naif(node *init, arbre_equil_poids &src)
: cur{ init }, src{ src } {
}
iterator_naif(arbre_equil_poids &src)
: cur{ nullptr }, src{ src } {
}
// ICI :
// vos notes de cours, p.109
// note : O(log n) par appel, donc O(n log n) pour un parcours
// complet... aussi long qu'un tri!
node *suivant(node *p) {
node *q = src.racine;
node *sq = p->droite;
if (sq)
while (sq->gauche)
sq = sq->gauche;
else
while (q != p)
if (p->valeur < q->valeur)
q = (sq = q)->gauche;
else
q = q->droite;
return sq;
}
public:
iterator_naif &operator++() {
cur = suivant(cur);
return *this;
}
iterator_naif operator++(int) {
auto temp = *this;
operator++();
return temp;
}
bool operator==(const iterator_naif &autre) const {
return cur == autre.cur;
}
bool operator!=(const iterator_naif &autre) const {
return !(*this == autre);
}
T operator*() const { return cur->valeur; }
};
//
// ICI
//
iterator_naif begin_naif() {
return { trouver_minimal(racine), *this };
}
iterator_naif end_naif() {
return { *this };
}
//
// ICI
//
class iterator_raffine {
friend class arbre_equil_poids<T>;
node *cur;
arbre_equil_poids &src;
iterator_raffine(node *init, arbre_equil_poids &src)
: cur{ init }, src{ src } {
}
iterator_raffine(arbre_equil_poids &src)
: cur{ nullptr }, src{ src } {
}
// ICI :
// vos notes de cours, p.110
// note : O(1) amorti par appel, donc O(n) en moyenne pour
// un parcours complet... bien mieux!
node *suivant(node *p) {
node *sq;
if (!p->droite)
for (sq = p->parent;
sq && sq->gauche != p;
p = sq, sq = p->parent)
;
else
for (sq = p->droite; sq->gauche; sq = sq->gauche)
;
return sq;
}
public:
iterator_raffine &operator++() {
cur = suivant(cur);
return *this;
}
iterator_raffine operator++(int) {
auto temp = *this;
operator++();
return temp;
}
bool operator==(const iterator_raffine &autre) const {
return cur == autre.cur;
}
bool operator!=(const iterator_raffine &autre) const {
return !(*this == autre);
}
T operator*() const { return cur->valeur; }
};
//
// ICI
//
iterator_raffine begin_raffine() {
return { trouver_minimal(racine), *this };
}
iterator_raffine end_raffine() {
return { *this };
}
};
#include <iostream>
#include <string>
using namespace std;
struct afficheur {
int indent = -1;
void entrer() {
++indent;
}
void sortir() {
++indent;
}
void operator()(int n) const {
std::cout << string(2 * indent, ' ') << n << std::endl;
}
};
struct afficheur_annote {
int indent = -1;
void entrer() {
++indent;
}
void sortir() {
++indent;
}
template <class T>
void operator()(const T &val, const string &prefixe = {}) const {
cout << string(2 * indent, ' ') << prefixe << val << endl;
}
};
enum class operation { inserer, supprimer, quitter };
template <class T>
void mise_a_jour(arbre_equil_poids<T> &avlt, const T &val, operation op) {
if (op == operation::inserer)
cout << "Insertion de " << val << '\n';
else if (op == operation::supprimer)
cout << "Suppression de " << val << '\n';
else
return;
cout << "Avant :\n";
avlt.visiter_preordre(afficheur_annote{});
if (op == operation::inserer)
avlt.ajouter(val);
else if (op == operation::supprimer)
avlt.supprimer(val);
cout << "Apres :\n";
avlt.visiter_preordre(afficheur_annote{});
cout << string(70, '-') << '\n';
}
#include <utility>
pair<operation, int> lire_choix() {
auto est_valide = [](char c) {
return c == 'a' || c == 's' || c == 'q';
};
cout << "Operation : [a]jouter, [s]upprimer, [q]uitter ? ";
char c;
while (cin.get(c) && !est_valide(c))
;
if (c != 'q') {
cout << "quelle valeur? ";
if (int valeur; cin >> valeur)
return c == 'a' ? pair{ operation::inserer, valeur } :
pair{ operation::supprimer, valeur };
}
return { operation::quitter, 0 };
}
#include <algorithm>
#include <random>
template <class It>
vector<int> brasser(It debut, It fin) {
mt19937 prng{ random_device{}() };
vector<int> v(debut, fin);
shuffle(v.begin(), v.end(), prng);
return v;
}
int main() {
arbre_equil_poids<int> a_eq_p{ 3.0 };
int vals[]{ 25, 35, 30, 20, 15, 5, 28, 50, 45, 48, 43, 1, 3, 7, 2 };
//const string tirets = string(70, '-');
//cout << tirets << '\n'
// << "Debut des insertions:\n"
// << tirets << '\n';
//for (auto n : vals) {
// mise_a_jour(a_eq_p, n, operation::inserer);
//}
//cout << '\n';
//cout << tirets << '\n'
// << "Quelques statistiques:\n"
// << tirets << '\n';
//if (!a_eq_p.empty()) {
// cout << "Noeud de valeur minimale : "
// << a_eq_p.minimum() << '\n';
// cout << "Noeud de valeur maximale : "
// << a_eq_p.maximum() << '\n';
//}
//cout << "hauteur : " << a_eq_p.hauteur() << endl;
//// ICI
//cout << "Parcours sequentiel (naif) :\n\t";
//for (auto p = a_eq_p.begin_naif(); p != a_eq_p.end_naif(); ++p)
// cout << *p << ' ';
//cout << '\n';
//cout << "Parcours sequentiel (raffine) :\n\t";
//for (auto p = a_eq_p.begin_raffine(); p != a_eq_p.end_raffine(); ++p)
// cout << *p << ' ';
//cout << '\n';
//cout << tirets << '\n'
// << "Debut des suppressions (pseudoaleatoires):\n"
// << tirets << '\n';
//for (auto n : brasser(begin(vals), end(vals))) {
// mise_a_jour(a_eq_p, n, operation::supprimer);
//}
for (auto [choix, val] = lire_choix();
choix != operation::quitter;
tie(choix, val) = lire_choix()) {
mise_a_jour(a_eq_p, val, choix);
}
}
|
S25
|
MER
|
7 déc.
|
Au menu :
Implémentation naïve d'un Quad-tree :
|
S26
|
VEN
|
9 déc.
|
Au menu :
- Monceaux
- Pourquoi se limiter à un seul comparateur, typiquement
operator<(const T&,const T&), pour les structures de données
faites pour faciliter les recherches?
Implémentation naïve d'un monceau :
#include <iostream>
#include <vector>
#include <initializer_list>
#include <algorithm>
#include <iterator>
#include <functional>
#include <utility>
#include <cassert>
using namespace std;
//
// implémentation "maison" d'un monceau de T, avec
// un std::vector<T> comme substrat
//
namespace maison {
template <class T, class Comp = std::less<>>
class monceau {
std::vector<T> substrat;
// Complexité : O(n log n)
template <class It>
monceau creer_monceau(It b, It e) {
monceau m;
// opportunité d'optimisation ici
m.substrat.reserve(std::distance(b, e)); // O(1) ou O(n)
for (; b != e; ++b)
m.ajouter(*b);
return m;
}
public:
using size_type = typename std::vector<T>::size_type;
auto begin() { return std::begin(substrat); }
auto begin() const { return std::begin(substrat); }
auto end() { return std::end(substrat); }
auto end() const { return std::end(substrat); }
bool empty() const {
return std::empty(substrat);
}
size_type size() const {
return std::size(substrat);
}
monceau() = default;
// Complexité : O(n log n)
monceau(std::initializer_list<T> src) {
// opportunité d'optimisation ici
substrat.reserve(src.size());
for (auto val : src) {
ajouter(val);
}
}
// Complexité : O(n log n)
template <class It>
monceau(It debut, It fin) {
// opportunité d'optimisation ici
substrat.reserve(std::distance(debut, fin));
for (; debut != fin; ++debut) {
ajouter(*debut);
}
}
// Complexité : O(log n)
void ajouter(const T &val) {
using std::swap;
Comp comp;
substrat.push_back(val); // O(1) amorti
size_type pos = size() - 1;
// O(log n)
while (pos > 0 && static_cast<int>(parent(pos)) >= 0 &&
comp(substrat[parent(pos)], substrat[pos])) {
swap(substrat[parent(pos)], substrat[pos]);
pos = parent(pos);
}
}
// Complexité :
// précondition : !empty()
T extraire() {
assert(!empty());
auto ret = substrat.front();
if (size() > 1) {
auto m = creer_monceau(begin() + 1, end()); // O(n log n)
// opportunité d'optimisation ici
substrat.swap(m.substrat); // O(1)
//for (size_type i = 0; i != m.size(); ++i) // O(n)
// substrat[i] = m.substrat[i];
} else {
substrat.pop_back(); // à tester
}
return ret;
}
// Complexité : O(n) mais... dans des cas dégénérés
bool contient(const T &val) const {
return empty() ? false : contient_impl(val, 0);
}
private:
// Complexité : O(n) mais... dans des cas dégénérés
bool contient_impl(const T &val, size_type n) const {
if (!(n < size()))
return false;
if (Comp comp;
!comp(val, substrat[n]) && !comp(substrat[n], val)) {
return true;
}
return contient_impl(val, gauche(n)) || contient_impl(val, droite(n));
}
// précondition : n != 0
static size_type parent(size_type n) {
assert(n != 0);
return (n - 1) / 2;
}
static size_type gauche(size_type n) {
return 2 * n + 1;
}
static size_type droite(size_type n) {
return 2 * (n + 1);
}
};
}
//
// implémentation "standard" d'un monceau de T, avec
// un std::vector<T> comme substrat
//
namespace standard {
template <class T, class Comp = std::less<>>
class monceau {
std::vector<T> substrat;
public:
using size_type = typename std::vector<T>::size_type;
auto begin() { return std::begin(substrat); }
auto begin() const { return std::begin(substrat); }
auto end() { return std::end(substrat); }
auto end() const { return std::end(substrat); }
bool empty() const {
return std::empty(substrat);
}
size_type size() const {
return std::size(substrat);
}
monceau() = default;
// Complexité : O(n log n)
monceau(std::initializer_list<T> src) : substrat{ src } {
std::make_heap(std::begin(substrat), std::end(substrat), Comp{});
}
// Complexité : O(n log n)
template <class It>
monceau(It debut, It fin) : substrat{ debut, fin } {
std::make_heap(std::begin(substrat), std::end(substrat), Comp{});
}
// Complexité : O(log n)
void ajouter(const T &val) {
substrat.push_back(val); // O(1) amorti
std::push_heap(std::begin(substrat), std::end(substrat), Comp{});
}
// Complexité : O(log n)
// précondition : !empty()
T extraire() {
assert(!empty());
auto ret = substrat.front();
// O(log n)
std::pop_heap(std::begin(substrat), std::end(substrat), Comp{});
substrat.pop_back();
return ret;
}
// Complexité : O(n) dans le pire cas
bool contient(const T &val) const {
return empty()? false : contient_impl(val, 0);
}
private:
// Complexité : O(n) dans le pire cas
bool contient_impl(const T &val, size_type n) const {
if (!(n < size()))
return false;
if (Comp comp;
!comp(val, substrat[n]) && !comp(substrat[n], val)) {
return true;
}
return contient_impl(val, gauche(n)) || contient_impl(val, droite(n));
}
// précondition : n != 0
static size_type parent(size_type n) {
assert(n != 0);
return (n - 1) / 2;
}
static size_type gauche(size_type n) {
return 2 * n + 1;
}
static size_type droite(size_type n) {
return 2 * (n + 1);
}
};
}
#include <string>
template <class M, int N, class It>
void test(It b, It e, const std::string &nom) {
M m;
cout << "Test avec " << nom << " :\n";
for (; b != e; ++b) {
m.ajouter(*b);
for (auto n : m)
cout << n << ' ';
cout << '\n';
}
// -1 et N+1 ne doivent pas être trouvés
for (int i = -1; i != N + 1; ++i)
if (!m.contient(i)) {
cout << "Element " << i << " introuvable\n";
}
cout << "Ordre d'apparition dans le substrat :\n";
for (auto n : m)
cout << n << ' ';
cout << '\n';
cout << "Ordre conceptuel dans le monceau :\n";
while (!m.empty()) {
cout << m.extraire() << ' ';
}
cout << '\n';
}
//
// applications des monceaux
//
//
// application classique : file prioritaire
//
template <class T,
template <class, class> class S = maison::monceau,
class Comp = std::less<>>
class file_prioritaire {
S<T,Comp> substrat;
public:
file_prioritaire() = default;
file_prioritaire(std::initializer_list<T> src)
: substrat{ src } {
}
// API fragile ici
T extraire() {
return substrat.extraire();
}
bool empty() const {
return substrat.empty();
}
};
//
// application classique : heap sort
//
//
// version naïve
//
template <class T>
std::vector<T> trier(std::vector<T> v) {
using maison::monceau;
monceau<T, std::greater<>> m{ std::begin(v), std::end(v) };
v.clear();
while (!m.empty()) {
v.push_back(m.extraire());
}
return v;
}
//
// version "in place"
//
template <class T>
std::vector<T> trier_sur_place(std::vector<T> v) {
using namespace std;
if (v.empty()) return v;
make_heap( begin(v), end(v) );
for(auto e = end(v); begin(v) != e; e = prev(e)) {
pop_heap(begin(v), e);
}
return v;
}
#include <random>
int main() {
using namespace std;
// using standard::monceau;
using maison::monceau;
monceau<int> m;
// données sources
enum { N = 10 };
vector<int> v;
for (int i = 0; i != N; ++i)
v.push_back(i);
shuffle(begin(v), end(v), mt19937{ random_device{}() });
test<standard::monceau<int>, N>(begin(v), end(v), "standard::monceau");
test<maison::monceau<int>, N>(begin(v), end(v), "maison::monceau");
file_prioritaire<string, maison::monceau> file{
"J'aime", "mon", "prof", "et", "il", "me", "le", "rend", "bien"
};
while (!file.empty()) {
cout << file.extraire() << ' ';
}
cout << '\n';
cout << "Avant le tri :\n\t";
for (auto n : v) cout << n << ' ';
cout << '\n';
cout << "Apres le tri :\n\t";
for (auto n : trier(v)) cout << n << ' ';
cout << '\n';
cout << "Avant le tri :\n\t";
for (auto n : v) cout << n << ' ';
cout << '\n';
cout << "Apres le tri sur place :\n\t";
for (auto n : trier_sur_place(v)) cout << n << ' ';
cout << '\n';
}
|
S27
|
JEU
|
15 déc.
|
Chic examen final, de 9 h à 12 h
|
Nous avons vu, jusqu'ici...
Les consignes des devoirs et des laboratoires suivront.
Quelques notes pour accompagner votre lecture des notes de cours.
... par cela :
... qui néglige entre autre de valider que les lectures aient été
réussies, préférez :