Université de Sherbrooke, Structures de données (SDO)

Vous trouverez ici quelques documents et quelques liens pouvant, je l'espère, vous être utiles.

Les documents qui vous sont fournis ici le sont pour vous rendre service.

Je travaille très fort sur chacun de mes cours. Veuillez ne pas vendre (ou donner) les documents que je vous offre ici à qui que ce soit sans mon consentement. Si des abus surviennent, je vais cesser de rendre ce matériel disponible à toutes et à tous.

Si ces documents vous rendent service, faites-le moi savoir. Mon adresse de courriel est disponible à travers la page où on trouve mon horaire.

Vous trouverez sur ce site :

Documents sous forme électronique

Cliquez sur cette cible pour le plan de cours, sous forme électronique.

Contenu des séances

Ce qui suit détaille le contenu des séances du cours.

Index des séances
S00 S01 S02 S03 S04 S05 S06 S07 S08 S09
S10 S11 S12 S13 S14 S15 S16 S17 S18 S19
S20 S21 S22 S23 S24 S25 S26 S27

Notez que pour la session en 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.

N'oubliez pas de remettre le devoir 1 au plus tard ce soir à 23 h 59 sur https://turnin.dinf.usherbrooke.ca

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()

N'oubliez pas de remettre le devoir 2 au plus tard ce soir à 23 h 59 sur https://turnin.dinf.usherbrooke.ca

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 :

xxx

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 :

xxx

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

Vue aérienne des conteneurs examinés en classe

Nous avons vu, jusqu'ici...

  Opérations clés
Conteneur push_back() pop_back() push_front() pop_front() obj[i] insert(it) erase(it)

Tableau de capacité fixe, p. ex. :

  • T[]
  • std::array<T,N>
  • votre Array<T,N> (devoir 1)
  • etc.

s/o

s/o

s/o

s/o

s/o

s/o

Tableau de capacité dynamique, p. ex. :

  • std::vector<T>

amorti

s/o

s/o

Liste simplement chaînée, p. ex. :

  • std::forward_list<T>
  • notre sliste<T> (séance S04)

ou s/o selon l'implémentation

ou s/o selon l'implémentation

s/o

Liste doublement chaînée, p. ex. :

  • std::list<T>
  • notre liste<T> (séance S08)

s/o

deque (séance S07, devoir 2)

Consignes des devoirs et laboratoires

Les consignes des devoirs et des laboratoires suivront.

Notes à propos des notes de cours

Quelques notes pour accompagner votre lecture des notes de cours.

Chapitre 1

À venir

 

Chapitre 2

À venir

 

Chapitre 3

À venir

 

Chapitre 4

La page 33 donne un exemple qui présente une classe entierRelatif comme un cas pertinent de dérivé d'une classe entier, mais notez qu'il y a des nuances à apporter ici (en particulier, si entierRelatif est un dérivé public de la classe entier, il y a alors bris de ce qu'on nomme le principe de Liskov, sur lequel nous reviendrons). Le texte est bon en général, mais cet exemple précis mérite qu'on le revisite éventuellement

La note en bas de page numéro 23, page 33, parle d'encapsulation en termes de regroupement; le regroupement est effectivement une caractéristique importante de l'approche OO, mais l'encapsulation trouve sa réelle importance dans la responsabilisation de l'objet quant à la garantie du respect de ses invariants. Nous y reviendrons, car cet aspect a une incidence importante sur plusieurs choix de représentation des objets

À la page 34, le littéral "Bonjour" n'est pas techniquement un const char* mais est par contre implicitement convertible en un const char* (il y a une subtilité ici). Ceci revient aussi ailleurs dans le texte

La page 35 et plusieurs autres utilisent des majuscules pour des noms d'attributs (en termes du langage C++, on utilisera aussi « variables membres » car le mot anglais attribute a un autre sens technique), mais cet usage privilégié dans le document n'est pas recommandé (en pratique, il causerait beaucoup de dégâts car les majuscules, en C++ comme en C, indiquent normalement des macros) alors nous l'éviterons dans notre cours

Le discours au bas de la page 35 demande quelques nuances, que nous ferons en classe

Le texte utilise copieur pour le constructeur de copie et affectateur pour l'opérateur d'affectation. Ce sont des choix raisonnables

La page 36 affirme « Si [une] classe a absolument besoin d'un destructeur, alors on doit absolument aussi coder le copieur et l’affectateur », mais je vous propose de nuancer sous cet angle : « Si [une] classe a absolument besoin d'un destructeur, alors mieux vaut considérer coder le copieur et l’affectateur ». Le mot « absolument » est trop fort ici, mais le signal est qu'il faut une bonne raison pour ne pas compléter ce trio d'opérations. Notez aussi que l'enjeu n'est pas limité à l'allocation dynamique (de mémoire), mais bien de manière plus large à la gestion des ressources sous la responsabilité de l'objet. Pour en savoir plus, voir ../../Sujets/Divers--cplusplus/Sainte-Trinite.html

La page 37 contient un passage qui demande un peu de nuances et qui indique « Pour les objets définis par des classes, le constructeur sans paramètre est appelé. Pour les objets primitifs, il n'y a pas de tel appel ». En réalité, la réalité décrite à cet endroit s'applique aussi à plusieurs types plus complexes que des primitifs, et ces types (les agrégats) sont très utiles

L'exemple de la page 37 pour présenter un constructeur de copie est correct, mais dans le cas de la classe point le constructeur de copie implicitement généré par le compilateur aurait été préférable

À la page 38, le passage suivant est incorrect : « On ne pourrait pas non plus déclarer de vector<point> car ce conteneur a besoin d’initialiser des points avec ce constructeur », car vector<T> pour un certain type T ne créera pas de T par défaut si on ne le lui demande pas. Démonstration : https://wandbox.org/permlink/Y5GEcdBdG1R7GuI7

À la page 38, notez que dans plusieurs cas, retourner un objet par valeur d'une fonction ne provoquera pas une construction par copie (en fait, cela en provoquera rarement). Il y a des subtilités ici...

À la page 39, on aurait pu remplacer ceci :

flot>>i>>j;
if(flot)... // si vrai, il y a des données

... par cela :

if (flot>>i>>j)... // si les lectures de i et de j ont réussi

La page 40 (une page parmi plusieurs) utilise le vocable « référence constante » pour l'idée de « référence-vers-const » mais notez que ce n'est pas vraiment la référence qui est const, c'est le référé

Lisez la section sur les mutateurs (pages 40 et 41) avec attention : fuyez les automatismes! Cette section est excellente. Les mutateurs à la « set » devraient être rares dans du code sérieux, et l'argumentaire à ce sujet dans le texte est limpide

Page 43, notez qu'une fonction friend participe à l'interface du type dont elle est amie. Notez que le premier exemple de la page est légèrement trompeur (c'est subtil) dans son recours aux parenthèses, car l'ordre d'évaluation des sous-expressions

Page 43, les opérateurs d'entrée / sortie sur des flux sont perfectibles. Plutôt que ceci :

istream& operator>>(istream &entree,point &p){
   if(!entree)return entree;
   char skip;
   double X,Y;
   entree >> skip >> X >> skip >> Y >> skip;
   p=point(X,Y);
   return entree;
}
ostream& operator<<(ostream &sortie,const point &p){
   sortie << "(" << p.abscisse() << "," << p.ordonnee() << ")";
   return sortie;
}

... qui néglige entre autre de valider que les lectures aient été réussies, préférez :

istream& operator>>(istream &entree,point &p){
   if(!entree)return entree;
   char skip;
   double x, y;
   if (entree >> skip >> x >> skip >>  y>> skip)
      p = { x,y };
   return entree;
}
ostream& operator<<(ostream &sortie,const point &p){
   return sortie << '(' << p.abscisse() << ',' << p.ordonnee() << ')';
}

Page 44, le passage « Ces objets sont tous initialisés au départ du programme, avant même le début de l’exécution de la fonction main() » demande un peu de nuances. Notez aussi que int2 n'est pas initialisé deux fois en C++ (il existe des langages qui font une double initialisation, mais C++ n'en est pas), du moins si l'initialisation de ses attributs est faite de manière idiomatique

Page 45, section « L'agrégation », deux remarques : (a) l'agrégation au sens OO n'est pas exactement ce qu'indique cette section (on parle plus de composition, règle générale, avec ce descriptif), et (b) le texte est un peu trompeur en escamotant l'enjeu contemporain de la sémantique de mouvement, dont nous discuterons

Page 45, section « La surdéfinition », notez que C++ n'est pas pleinement compatible avec C (il existe des programmes C qui ne sont pas des programmes C++ corrects). Notez aussi que map<double,double>, bien que légal, est un type périlleux à utiliser... Voyez-vous pourquoi? Démonstration : https://wandbox.org/permlink/iC4c6SBYSkFX9oiw

Page 47, le passage « Cela allège beaucoup le code, mais dans ce cas, il faut faire très attention à l’ordre des définitions des fonctions dans la classe, car celles-ci ne peuvent utiliser une fonction qui n’a pas encore été déclarée » est incorrect (c'est tout à fait légal en C++, et ce depuis des décennies). Par contre, il faut que les types aient été déclarés avant d'être utilisés. Démonstration : https://wandbox.org/permlink/3lfKZpYqbJOzIdwi (si vous commentez Y::f() et commentez l'autre version, définie après l'introduction du type Y::type, tout fonctionne : https://wandbox.org/permlink/YhydoA64nhiHzQGM pour le voir)

Chapitre 5

À venir

 

 


Valid XHTML 1.0 Transitional

CSS Valide !