Exercices S04 – Solutions possibles

Ce qui suit donne quelques solutions possibles aux exercices proposés à la séance S04. Vous aurez peut-être même trouvé mieux, qui sait?

Problème de trouver_consecutifs(debut,fin,n)

Consigne :

Solution possible : une solution « manuelle » possible à ce problème serait ce qui suit.

template <class It>
   It trouver_consecutifs(It debut, It fin, int n) {
      if (debut == fin || n <= 0) return fin;
      It base = debut;
      int m = 1;
      for (++debut; debut != fin; ++debut) {
         ++m;
         if (*debut != *base) {
            base = debut;
            m = 1;
         } else if (m == n) {
            return base;
         }
      }
      return m != n? fin : base;
   }

Solution possible : une solution possible à ce problème, ayant recours à des algorithmes standards serait ce qui suit.

#include <algorithm>
#include <iterator>
template <class It>
   It trouver_consecutifs(It debut, It fin, int n) {
      using namespace std;
      if (debut == fin || n <= 0) return fin;
      auto base = debut;
      auto term = find_if(next(base), fin, [=](auto val) {
         return *base != val;
      });
      while (term != fin && distance(base, term) < n) {
         base = term;
         term = find_if(next(base), fin, [=](auto val) {
            return *base != val;
         });
      }
      return distance(base, term) < n? fin : base;
   }

J'ai utilisé deux λ identiques dans la même fonction. Il aurait été tentant alléger l'écriture (et probablement le code généré) en déposant la λ dans une variable temporaire avant de l'utiliser, mais ceci aurait posé problème ici du fait de la capture par copie de base à la construction de la λ qui doit prendre une valeur distincte à chaque construction.

Notez que la version manuelle, en s'arrêtant dès que n éléments ont été comptés, sera peut-être un peu plus rapide en pratique que celle utilisant des algorithmes standards (implémentée de manière un peu naïve, il faut le dire).

Problème de plus_longue_sequence(debut,fin)

Consigne :

Solution possible : une solution « manuelle » possible à ce problème serait ce qui suit.

#include <utility>
template <class It>
   auto plus_longue_sequence(It debut, It fin) {
      using namespace std;
      auto res = pair{ fin, fin };
      if (debut == fin) return res;
      auto pos = debut;
      int meilleur = 0, n = 1;
      for (++debut; debut != fin; ++debut)
         if (*pos == *debut) {
            ++n;
         } else {
            if (n > meilleur) {
               meilleur = n;
               res = { pos, debut };
            }
            n = 1;
            pos = debut;
         }
      return n > meilleur ? pair{ pos, fin } : res;
   }

Solution possible : une solution possible à ce problème, ayant recours à des algorithmes standards serait ce qui suit.

#include <utility>
#include <algorithm>
template <class It>
   auto plus_longue_sequence(It debut, It fin) {
      using namespace std;
      auto res = pair{ fin, fin };
      while(debut != fin) {
         auto pos = find_if(next(debut), fin, [=](auto val) {
            return val != *debut;
         });
         if (distance(debut, pos) > distance(res.first, res.second))
            res = { debut, pos };
         debut = pos;
      }
      return res;
   }

Vous remarquerez sans doute que cette solution est significativement plus simple que la solution « manuelle », et elle pourrait encore être allégée.

Problème de plus_longue_sequence(debut,fin,pred)

Consigne :

Solution possible : une solution « manuelle » possible à ce problème serait ce qui suit.

#include <algorithm>
#include <iterator>
#include <utility>
template <class It, class Pred>
   auto plus_longue_sequence(It debut, It fin, Pred pred) {
      using namespace std;
      auto res = pair{ fin, fin };
      if (debut == fin) return res;
      for (; debut != fin && !pred(*debut); ++debut)
         ;
      while (debut != fin) {
         auto pos = debut;
         for (; debut != fin && pred(*debut); ++debut)
            ;
         if (distance(res.first, res.second) < distance(pos, debut))
            res = { pos, debut };
         for (; debut != fin && !pred(*debut); ++debut)
            ;
      }
      return res;
   }

Cette solution n'a recours à aucun algorithme standard. La logique de cet algorithme, grossièrement, est :

La redondance de code est un indice que nous pourrions simplifier le tout. Notez que les appels répétés à std::distance() pourraient être optimisés par le recours à une variable temporaire.

Solution possible : une solution possible à ce problème, ayant recours à des algorithmes standards serait ce qui suit.

#include <algorithm>
#include <iterator>
#include <utility>
template <class It, class Pred>
   auto plus_longue_sequence(It debut, It fin, Pred pred) {
      using namespace std;
      auto res = pair{ fin,fin };
      for (debut = find_if(debut, fin, pred); debut != fin; debut = find_if(debut, fin, pred)) {
         auto term = find_if_not(debut, fin, pred);
         if (distance(res.first, res.second) < distance(debut, term))
            res = { debut, term };
         debut = term;
      }
      return res;
   }

Ce code va à l'essentiel, en utilisant les algorithmes std::find_if() et std::find_if_not() qui font tout le travail de recherche d'un début et d'une fin de séquence pour nous.

Problème de inverser_mots(s)

Consigne :

Solution laborieuse : une solution laborieuse, parce qu'un peu manuelle, mais qui fonctionnerait bien serait la suivante.

#include <algorithm>
#include <iostream>
#include <vector>
#include <cassert>
#include <string>
#include <locale>
using namespace std;
string inverser_mots(const string &s) {
   if (s.empty()) return s;
   vector<string> blancs, mots;
   auto loc = locale{ "" };
   bool debute_par_blanc = isspace(s.front(), loc);
   auto debut = find_if(begin(s), end(s), [&loc](char c) { return !isspace(c, loc); });
   if (debut != begin(s))
      blancs.emplace_back(begin(s), debut);
   while (debut != end(s)) {
      auto fin = find_if(debut, end(s), [&loc](char c) { return isspace(c, loc); });
      mots.emplace_back(debut, fin);
      debut = find_if(fin, end(s), [&loc](char c) { return !isspace(c, loc); });
      if (debut != fin)
         blancs.emplace_back(fin, debut);
   }
   reverse(begin(mots), end(mots));
   if (!debute_par_blanc)
      blancs.swap(mots);
   string resultat;
   auto q = begin(mots);
   for (auto p = begin(blancs); p != end(blancs); ++p) {
      resultat += *p;
      if (q != end(mots)) {
         resultat += *q;
         ++q;
      }
   }
   return resultat;
}

Cette solution ressemble, sur le plan structurel, à plus_longue_sequence(debut,fin,pred), du fait qu'elle alterne entre les sous-séquences qui respectent un critère (les blancs consécutifs) et celles qui respectent un autre critère (les non-blancs consécutifs). Elle en diffère toutefois du fait qu'elle conserve les sous-séquences dans deux conteneurs (nommés blancs et mots, mais ce sont des noms discutables), pour éventuellement inverser l'ordre des mots mais pas l'ordre des séquences de blancs.

Pour fusionner les sous-séquences en fin de parcours, l'algorithme retient si le texte dans s débutait par un blanc, puis permute les séquences de manière à ce que celle qui se nomme blancs contienne en fait la sorte de séquence qui débutait s initialement. Ensuite, la construction de la chaîne resultat se fait en alternant des sous-séquences de chacun des deux conteneurs.

Raffinement : remarquez que nous utilisons un même algorithme, std::find_if(), et deux λ pour identifier les blancs et les non-blancs. Ceci nous amène à écrire du code semblable mais un peu redondant. En associant l'une des λ à une variable (ici : est_blanc) et en alternant entre les algorithmes std::find_if() et std::find_if_not(), nous pouvons clarifier quelque peu notre démarche, et alléger l'écriture.

#include <algorithm>
#include <iostream>
#include <vector>
#include <cassert>
#include <string>
#include <locale>
using namespace std;
string inverser_mots(const string &s) {
   if (s.empty()) return s;
   vector<string> blancs, mots;
   auto loc = locale{ "" };
   auto est_blanc = [&loc](char c) { return isspace(c, loc); };
   bool debute_par_blanc = est_blanc(s.front());
   auto debut = find_if_not(begin(s), end(s), est_blanc);
   if (debut != begin(s))
      blancs.emplace_back(begin(s), debut);
   while (debut != end(s)) {
      auto fin = find_if(debut, end(s), est_blanc);
      mots.emplace_back(debut, fin);
      debut = find_if_not(fin, end(s), est_blanc);
      if (debut != fin)
         blancs.emplace_back(fin, debut);
   }
   reverse(begin(mots), end(mots));
   if (!debute_par_blanc)
      blancs.swap(mots);
   string resultat;
   auto q = begin(mots);
   for (auto p = begin(blancs); p != end(blancs); ++p) {
      resultat += *p;
      if (q != end(mots)) {
         resultat += *q;
         ++q;
      }
   }
   return resultat;
}

Raffinement : bien que cela ne semble pas simplifier le code à première vue, il est possible de séparer le code de fusion des deux vecteurs et de le placer dans une fonction à part entiere, ou encore dans une λ comme c'est le cas ici.

#include <algorithm>
#include <iostream>
#include <vector>
#include <cassert>
#include <string>
#include <locale>
using namespace std;
string inverser_mots(const string &s) {
   if (s.empty()) return s;
   vector<string> blancs, mots;
   auto loc = locale{ "" };
   auto est_blanc = [&loc](char c) { return isspace(c, loc); };
   bool debute_par_blanc = est_blanc(s.front());
   auto debut = find_if_not(begin(s), end(s), est_blanc);
   if (debut != begin(s))
      blancs.emplace_back(begin(s), debut);
   while (debut != end(s)) {
      auto fin = find_if(debut, end(s), est_blanc);
      mots.emplace_back(debut, fin);
      debut = find_if_not(fin, end(s), est_blanc);
      if (debut != fin)
         blancs.emplace_back(fin, debut);
   }
   reverse(begin(mots), end(mots));
   auto fusion = [](const vector<string> &v0, const vector<string> &v1) {
      string resultat;
      auto q = begin(v1);
      for (auto &str : v0) {
         resultat += str;
         if (q != end(v1)) {
            resultat += *q;
            ++q;
         }
      }
      return resultat;
   };
   return debute_par_blanc? fusion(blancs, mots) : fusion(mots, blancs);
}

Si nous faisons de fusion un algorithme générique à part entière, il devient possible de l'exprimer ainsi.

template<class T, class ItA, class ItB>
   T fusion(ItA da, ItA fa, ItB db, ItB fb, T resultat) { // fusionne à la fin de resultat
      for(; da != fa; ++da) {
         resultat += *da;
         if (db != fb) {
            resultat += *db;
            ++db;
         }
      }
      return resultat;
   }

J'ai utilisé ItA et ItB pour le type des itérateurs (on pourrait vouloir fusionner des éléments d'un vector<string> avec ceux d'une list<string> après tout), da et fa pour « début de la séquence A » et « fin de la séquence A » respectivement (idem pour la séquence B), et le type T pour permettre au code client de suppléer un type de retour.

J'utilise le paramètre resultat comme valeur initiale pour cumuler les éléments à fusionner (dans notre cas, ce devrait être une string vide).

 Notez que je triche un peu en présumant que l'ajout à la fin d'une séquence puisse se faire avec l'opérateur += de resultat; ce tronçon mériterait d'être raffiné, mais fonctionne bien si T est string. Il existe une solution pas si mal à ce problème, mais je vous laisse y réfléchir.

Avec ce changement, notre algorithme devient comme suit.

#include <algorithm>
#include <iostream>
#include <vector>
#include <cassert>
#include <string>
#include <locale>
using namespace std;
string inverser_mots(const string &s) {
   if (s.empty()) return s;
   vector<string> blancs, mots;
   auto loc = locale{ "" };
   auto est_blanc = [&loc](char c) { return isspace(c, loc); };
   bool debute_par_blanc = est_blanc(s.front());
   auto debut = find_if_not(begin(s), end(s), est_blanc);
   if (debut != begin(s))
      blancs.emplace_back(begin(s), debut);
   while (debut != end(s)) {
      auto fin = find_if(debut, end(s), est_blanc);
      mots.emplace_back(debut, fin);
      debut = find_if_not(fin, end(s), est_blanc);
      if (debut != fin)
         blancs.emplace_back(fin, debut);
   }
   reverse(begin(mots), end(mots));
   return debute_par_blanc?
      fusion(begin(blancs), end(blancs), begin(mots), end(mots), string{}) :
      fusion(begin(mots), end(mots), begin(blancs), end(blancs), string{});
}

Raffinement : on peut simplifier encore plus le tout en constatant que la distinction que nous faisons ici entre mots et blancs est artificielle, à ceci près que l'on doit se souvenir si s débutait ou non par un blanc pour s'assurer de n'inverser que l'ordre des mots (pas celui des blancs). Je vais tricher un peu ici en faisant quelques constats simplificateurs mais qui demandent des explications :

Une implémentation possible serait alors la suivante.

#include <algorithm>
#include <iostream>
#include <vector>
#include <cassert>
#include <string>
#include <locale>
using namespace std;
string inverser_mots(const string &s) {
   if (s.empty()) return s;
   vector<string> v[2];
   int cur = 0;
   auto loc = locale{ "" };
   auto est_blanc = [&loc](char c) { return isspace(c, loc); };
   bool debute_par_blanc = est_blanc(s.front());
   for (auto debut = begin(s); debut != end(s);) {
      auto fin = est_blanc(*debut) ? find_if_not(debut, end(s), est_blanc) : find_if(debut, end(s), est_blanc);
      v[cur].emplace_back(debut, fin);
      cur = cur? 0 : 1;
      debut = fin;
   }
   cur = static_cast<int>(debute_par_blanc);
   reverse(begin(v[cur]), end(v[cur]));
   return fusion(begin(v[0]), end(v[0]), begin(v[1]), end(v[1]), string{});
}

Notre acceptation du fait que l'on puisse faire abstraction du concept de « est un mot » et le remplacer par « est de même nature que le premier caractère de la séquence » nous permet une économie significative d'effort et de complexité.

Raffinement : remarquez enfin que l'on n'a pas vraiment besoin de séparer la recherche de la fin de la séquence en l'utilisation de deux algorithmes distincts. Il est en effet possible de ramener le tout à « chercher le premier élément de la séquence qui n'ait pas la même caractéristique que le premier d'entre eux ». On aurait alors ce qui suit.

#include <algorithm>
#include <iostream>
#include <vector>
#include <cassert>
#include <string>
#include <locale>
using namespace std;
string inverser_mots(const string &s) {
   if (s.empty()) return s;
   vector<string> v[2];
   int cur = 0;
   auto loc = locale{ "" };
   auto est_blanc = [&loc](char c) { return isspace(c, loc); };
   bool debute_par_blanc = est_blanc(s.front());
   for (auto debut = begin(s); debut != end(s);) {
      const bool etalon = est_blanc(*debut);
      auto fin = find_if(next(debut), end(s), [=](char c) { return est_blanc(c) != etalon; });
      v[cur].emplace_back(debut, fin);
      cur = cur? 0 : 1;
      debut = fin;
   }
   cur = static_cast<int>(debute_par_blanc);
   reverse(begin(v[cur]), end(v[cur]));
   return fusion(begin(v[0]), end(v[0]), begin(v[1]), end(v[1]), string{});
}

Pouvez-vous faire encore mieux?

Problème de inverser_lettres(s)

Consigne :

Solution possible : ce problème, pourtant semblable au précédent, admet une solution naïve relativement simple.

#include <string>
#include <locale>
#include <algorithm>
std::string inverser_lettres(std::string s) {
   using namespace std;
   auto loc = locale{ "" };
   auto debut = find_if(begin(s), end(s), [&loc](char c) { return !isspace(c, loc); });
   while (debut != end(s)) {
      auto fin = find_if(debut, end(s), [&loc](char c) { return isspace(c, loc); });
      reverse(debut, fin);
      debut = find_if(fin, end(s), [&loc](char c) { return !isspace(c, loc); });
   }
   return s;
}

Il est en effet facile ici de passer la chaîne reçue en paramètre par copie, et de travailler à l'intérieur de celle-ci plutôt que de travailler avec des conteneurs intermédiaires. Une fois les extrémités d'un mot trouvés, on peut simplement inverser la sous-chaîne correspondante.

Raffinement : on peut bien sûr se limiter à une seule λ et alterner entre std::find_if() et std::find_if_not(), comme précédemment. En remplaçant la répétitive while par un for pour contraindre la portée de debut, on en arrive à ce qui suit.

#include <string>
#include <locale>
#include <algorithm>
std::string inverser_lettres(std::string s) {
   using namespace std;
   auto loc = locale{ "" };
   auto est_blanc = [&loc](char c) { return isspace(c, loc); };
   for (auto debut = find_if_not(begin(s), end(s), est_blanc); debut != end(s);) {
      auto fin = find_if(debut, end(s), est_blanc);
      reverse(debut, fin);
      debut = find_if_not(fin, end(s), est_blanc);
   }
   return s;
}

C'est déjà pas mal, n'est-ce pas?

Problème de ltrim(s)

Consigne :

Solution possible : une solution possible à ce problème serait la suivante.

std::string ltrim(const std::string &s) {
   using namespace std;
   return { find_if(begin(s), end(s), [loc = locale{ "" }](char c) { return !isspace(c, loc); }, end(s) };
}

Problème de rtrim(s)

Consigne :

Solution possible : une solution possible à ce problème serait la suivante.

std::string rtrim(const std::string &s) {
   using namespace std;
   auto pos = find_if(rbegin(s), rend(s), [loc = locale{ "" }](char c) { return !isspace(c, loc); });
   return { begin(s) + distance(rbegin(s), pos), end(s) };
}

Problème de trim(s)

Consigne :

Solution possible : une solution possible à ce problème serait la suivante.

std::string trim(const std::string &s) {
   return ltrim(rtrim(s));
}

Problème de elaguer_blancs(s)

Consigne :

Solution possible : une solution possible à ce problème serait la suivante.

//
// Votre code va ici, ou dans un .h que vous inclurez
//
#include <cassert>
int main() {
   assert(elaguer_blancs(" j'aime   mon prof  ") == " j'aime mon prof ");
   assert(elaguer_blancs("   ") == " ");
   assert(elaguer_blancs(" ") == " ");
   assert(elaguer_blancs("") == "");
}

Autre angle d'approche

Pour un autre angle d'approche, on pourrait pousser un peu l'analyse de ces quelques problèmes et constater qu'ils ont certains points en commun. Je vous invite donc à relire brièvement les consignes qui vous ont été données lors de la séance S04...

...en regardant tout cela de plus près, il me semble apparent que certaines opérations réapparaîtront à plusieurs reprises dans le code :

Dans ma démarche de solution personnelle, j'y suis allé de manière graduelle :

Dans certains cas, je pourrais aller plus loin, mais je ne suis pas certain que cela aiderait à la lisibilité.

Programme principal

Mon programme principal consiste en une série de tests affichant des résultats à l'écran. C'est une manière parmi plusieurs de procéder; j'aurais aussi pu faire un programme interactif (je le fais rarement, pour être honnête, car les tests deviennent vite fastidieux), ou encore un programme validant les tests par des assertions dynamiques.

Les tests proposés ne sont pas exhaustifs; j'ai demandé plus de vous dans ma liste d'exigences (votre fonction devra fonctionner avec...) mais je triche un peu du fait que je connais bien les pièges qui nous guettent ici. Ne soyez pas complaisantes ou complaisants, et testez votre code avec rigueur.

Les fonctions de test utilisées suivent, mais ne sont pas le coeur du propos.

#include <algorithm>
#include <utility>
#include <ostream>
#include <string>
#include <vector>
#include <locale>
#include <tuple>
#include <numeric>
//
// outils divers...
//
// fonction à écrire...
//
// code de test...
//
#include <iostream>
int main() {
   using namespace std;
   int vals[] { 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 5, 6, 6 };
   for (int i = 0; i < 6; ++i)
      tester_trouver_consecutifs(begin(vals), end(vals), i, cout);
   tester_plus_longue_sequence(begin(vals), end(vals), cout);
   tester_plus_longue_sequence(
      begin(vals), end(vals), [](int n) {
         return n % 2 != 0;
      }, "entiers impairs", cout
   );
   string tests[] {
      "   j'aime mon   prof! ", "j'aime mon prof!", "",
      j'aime mon   prof! ", "   j'aime mon   prof!", "x"
   };
   tester_inverser_mots(begin(tests), end(tests), cout);
   tester_inverser_lettres(begin(tests), end(tests), cout);
}

Fonctions de tests

Comme le montre le programme principal, plus haut, j'ai écrit des fonctions de test pour chacun des algorithmes à tester. À noter :

  • J'ai passé un flux en sortie à chacune d'elles, pour qu'il soit plus facile d'envoyer les résultats dans un fichier si je le souhaite
    • Pour mes tests, j'ai utilisé la sortie standard (modélisée par std::cout) mais ce n'est qu'une option parmi plusieurs
  • Je me trouvais à afficher les éléments d'une séquence à quelques reprises, ce que j'ai refactorisé en un algorithme d'affichage sur un flux (fonction afficher_sequence())
  • Ce que les fonctions de tests font est appeler les fonctions à écrire et afficher des résultats. Ce n'est pas le rôle des algorithmes que nous avions à développer que d'écrire à la console, après tout
template <class It>
   void tester_trouver_consecutifs(It debut, It fin, int n, std::ostream &os) {
      using namespace std;
      afficher_sequence(debut, fin, os);
      os << '\n';
      if (auto pos = trouver_consecutifs(debut, fin, n); pos == fin)
         os << "Pas de sous-sequence de longueur " << n
            << " dans cette sequence\n" << endl;
      else
         os << "La 1re sous-sequence de longueur " << n
            << " dans cette sequence debute a la position "
            << distance(debut, pos) << '\n' << endl;
   }

template <class It>
   void tester_plus_longue_sequence(It debut, It fin, std::ostream &os) {
      using namespace std;
      if (auto [d,f] = plus_longue_sequence(debut, fin); d == f)
         os << "La sequence est vide\n";
      else {
         os << "La plus longue sequence est de longueur "
            << distance(d, f) << " et contient ";
         afficher_sequence(res.first, res.second, os);
         os << '\n';
      }
      os << endl;
   }

template <class It, class Pred>
   void tester_plus_longue_sequence
      (It debut, It fin, Pred pred, const std::string &nom_predicat, std::ostream &os) {
      using namespace std;
      if (auto [d,f] = plus_longue_sequence(debut, fin, pred); d == f)
         os << "La sequence est vide\n";
      else {
         os << "La plus longue sequence de " << nom_predicat
            << " est de longueur " << distance(d, f)
            << " et contient ";
         afficher_sequence(d, f, os);
         os << '\n';
      }
      os << endl;
   }

template <class It>
   void tester_inverser_mots(It debut, It fin, std::ostream &os) {
      using namespace std;
      for_each(debut, fin, [&os](const string &s) {
         os << "Avant : \"" << s << "\"\nApres : \"" << inverser_mots(s) << "\"\n" << endl;
      });
   }

template <class It>
   void tester_inverser_lettres(It debut, It fin, std::ostream &os) {
      using namespace std;
      for_each(debut, fin, [&os](const string &s) {
         os << "Avant : \"" << s << "\"\nApres : \"" << inverser_lettres(s) << "\"\n" << endl;
      });
   }

Algorithmes à écrire

L'approche que j'ai pris pour chacun des algorithmes qui devaient être écrits va comme suit.

Pour la fonction trouver_consecutifs(debut,fin,n), ma vision des choses était :

  • Prenons chacune des sous-séquences de la séquence , dans l'ordre
  • Évaluons sa taille avec la fonction distance()
  • Si la taille est au moins n, alors nous avons trouvé
  • Si nous n'en trouvons aucune, ou encore si n est déraisonnable (selon ma lecture, cela signifie ), alors l'algorithme retourne fin, signifiant que rien de pertinent n'a été trouvé

Mon implémentation repose sur l'outil consecutifs(debut,fin), qui retourne une paire représentant la sous-séquence d'éléments de valeur consécutive dans

template <class It>
   It trouver_consecutifs(It debut, It fin, int n) {
      using namespace std;
      if (debut == fin || n <= 0) return fin;
      for (auto p = consecutifs(debut, fin); p.second != fin; debut = p.second, p = consecutifs(debut, fin))
         if (distance(p.first, p.second) >= n)
            return p.first;
      return fin;
   }

Pour la fonction plus_longue_sequence(debut,fin), ma vision des choses était :

  • Prenons chacune des sous-séquences d'éléments de même valeur de la séquence , dans l'ordre
  • Évaluons sa taille avec la fonction distance()
  • Conservons les extrémités de cette sous-séquence s'il s'agit de la plus longue trouvée jusqu'ici

Mon implémentation repose sur l'outil consecutifs(debut,fin), qui retourne une paire représentant la sous-séquence d'éléments consécutifs de même valeur dans

Il est clair que plus_longue_sequence(debut,fin) sera en moyenne plus lente que trouver_consecutives(debut,fin,n) puisque cette dernière pourra s'arrêter dès qu'une sous-séquence convenable aura été trouvée.

Il serait possible d'optimiser plus_longue_sequence() dans le cas des itérateurs autres que ceux de catégorie Random Access en conservant la longueur de la plus longue sous-séquence trouvée dans une variable temporaire.

Notez que j'ai utilisé un mécanisme de C++ 17 nommé les Compile-Time Argument Deduction pour déduire pair<It,It> à partir de pair(fin,fin). J'aurais aussi pu utiliser make_pair(fin,fin) avec un compilateur plus âgé.

template <class It>
   auto plus_longue_sequence(It debut, It fin) {
      using namespace std;
      auto res = pair{ fin, fin };
      for (auto p = consecutifs(debut, fin); p.second != fin; debut = p.second, p = consecutifs(debut, fin))
         if (distance(p.first, p.second) >= distance(res.first, res.second))
            res = p;
      return res;
   }

Pour la fonction plus_longue_sequence(debut,fin,pred), ma vision des choses était :

  • Prenons chacune des sous-séquences d'éléments satisfaisant le prédicat pred de la séquence , dans l'ordre
  • Évaluons sa taille avec la fonction distance()
  • Conservons les extrémités de cette sous-séquence s'il s'agit de la plus longue trouvée jusqu'ici

Mon implémentation repose sur l'outil consecutifs(debut,fin,pred), qui retourne une paire représentant la sous-séquence d'éléments consécutifs satisfaisant le prédicat pred dans

Pour que l'algorithme fonctionne, il faut comprendre que par définition, une séquence sera faite d'une alternance de sous-séquences satisfaisant, puis ne satisfaisant pas, le prédicat. C'est pourquoi j'ai défini l'outil negation_logique(pred) qui retourne un prédicat qui ne sera vrai pour un certain paramètre arg que si pred(arg) est faux; ceci facilite l'implémentation de cette alternance.

Le test suivant la répétitive couvre le cas où la plus longue sous-séquence terminerait la séquence originale.

J'ai utilisé ici encore un mécanisme de C++ 17 nommé les Compile-Time Argument Deduction pour déduire pair<It,It> à partir de pair(fin,fin). J'aurais aussi pu utiliser make_pair(fin,fin) avec un compilateur plus âgé.

template <class It, class Pred>
   auto plus_longue_sequence(It debut, It fin, Pred pred) {
      using namespace std;
      auto res = pair{ fin, fin };
      auto p = consecutifs(debut, fin, pred);
      for (; debut = p.second, p = consecutifs(debut, fin, negation_logique(pred)), p.second != fin;
           debut = p.second, p = consecutifs(debut, fin, pred))
         if (distance(p.first, p.second) >= distance(res.first, res.second))
            res = p;
      if (distance(p.first, p.second) >= distance(res.first, res.second))
         res = p;
      return res;
   }

J'ai groupé inverser_mots() et inverser_lettres() dans un même bloc, puisque l'essentiel des deux algorithmes est identique. En effet, il s'agit de :

  • Découper la séquence originale en groupes de blancs et de non-blancs. Pour des raisons de simplicité, j'ai choisi de prendre des paires d'itérateurs dans chaque cas pour retenir le début et la fin de chacune des sous-séquences, mais on aurait aussi pu copier les données, donc le texte des sous-séquences
    • j'ai écrit la fonction separer_sous_sequences(debut,fin,pred) pour ce faire, puisque la logique était la même dans chaque cas
    De démarer la séquence de destination par des blancs seulement si la séquence source débutait par un blanc. J'ai abstrait ceci en « démarrer la séquence en notant si le prédicat s'avère pour son premier élément »
  • Construire deux séquences de sous-séquences, soit les blancs et les mots (les « non-blancs »)
  • Transformer certaines sous-séquences. Ceci varie selon les algorithmes :
    • dans le cas de la fonction inverser_mots(), on parle d'inverser l'ordre des mots
    • dans le cas de la fonction inverser_lettres(), on parle d'inverser l'ordre des lettres dans chaque mot
  • Ensuite, fusionner les sous-séquences en alternance, commençant par la séquence de sous-séquences la plus opportune, et
  • Retourner la concaténation de toutes ces sous-séquences. J'ai utilisé std::accumulate() pour ce faire

Il y aurait lieu d'optimiser encore l'une et l'autre, mais j'ai choisi d'arrêter ici par souci de lisibilité.

J'ai utilisé dans separer_sous_sequences() des Structured Bindings de C++ 17 pour alléger l'écriture. Ce n'est évidemment pas nécessaire (j'aurais pu utiliser les paires en tant que paires, tout simplement). J'ai fait de même dans inverser_mots() et dans inverser_lettres() pour récupérer les deux vecteurs retournées par separer_sous_sequences()

 

template <class It, class Pred>
   auto separer_sous_sequences(It debut, It fin, Pred pred) {
      using namespace std;
      vector<pair<It, It>> oui, non;
      for (bool use_neg = pred(*debut); debut != fin; use_neg = !use_neg)
         if (use_neg) {
            auto [d,f] = consecutifs(debut, fin, negation_logique(pred));
            oui.emplace_back(debut, f);
            debut = f;
         } else {
            auto [d,f] = consecutifs(debut, fin, pred);
            non.emplace_back(debut, f);
            debut = f;
         }
      return pair{ oui, non };
   }

std::string inverser_mots(const std::string &s) {
   using namespace std;
   using It = string::iterator;
   if (s.empty()) return s;
   const auto &loc = locale{ "" };
   auto [blancs, mots] = separer_sous_sequences(begin(s), end(s), est_blanc(loc));
   reverse(begin(mots), end(mots));
   auto v = isspace(s.front(), loc) ?
      fusionner_alternance(std::move(blancs), std::move(mots)) :
      fusionner_alternance(std::move(mots), std::move(blancs));
   return accumulate(begin(v), end(v), string{}, [](const string &s, auto &&p) {
      return s + string(p.first, p.second);
   });
}

std::string inverser_lettres(std::string s) {
   using namespace std;
   using It = string::iterator;
   if (s.empty()) return s;
   const auto &loc = locale{ "" };
   auto [blancs, mots] = separer_sous_sequences(begin(s), end(s), est_blanc(loc));
   for(auto & [d,f] : mots)
      reverse(d, f);
   auto v = isspace(s.front(), loc) ?
      fusionner_alternance(std::move(blancs), std::move(mots)) :
      fusionner_alternance(std::move(mots), std::move(blancs));
   return accumulate(begin(v), end(v), string{}, [](const string &s, auto &&p) {
      return s + string(p.first, p.second);
   });
}

Outils construits en cours de route

Ce qui suit montre des outils construits au fur et à mesure de ma progression à travers les exercices proposés.

La fonction consecutifs(debut,fin,pred) retourne une paire d'éléments pour lesquels s'avère. Il se peut que debut==f, signifiant une séquence vide, dans le cas où !pred(*debut)

template <class It, class Pred>
   std::pair<It, It> consecutifs(It debut, It fin, Pred pred) {
      return { debut, std::find_if(debut, fin, pred) };
   }

La fonction consecutifs(debut,fin) retourne une paire d'éléments pour lesquels *debut==e. Il se peut que debut==f, signifiant une séquence vide, dans le cas où !pred(*debut).

J'ai utilisé une syntaxe compacte de C++ 14 pour la capture de ma λ, soit [x = *debut] signifiant « fait une copie de *debut à la construction et nomme-la», mais cette manoeuvre n'est pas essentielle à la fonction (on aurait pu faire la même chose avec une variable locale).

template <class It>
   std::pair<It, It> consecutifs(It debut, It fin) {
      return debut == fin?
                std::pair{ debut,fin } :
                consecutifs(debut, fin, [x = *debut](auto val) { return val != x; });
   }

La fonction negation_logique(pred) retourne un foncteur qui prend un paramètre arg quelconque dont l'opérateur () retourne la négation de pred(arg). On aurait pu écrire, avc C++ 11 ou avant, ceci :

template <class Pred>
   class negation_logique_ {
      Pred pred;
   public:
      negation_logique_(Pred pred) : pred{ pred } {
      }
      template <class T>
         bool operator()(const T &arg) {
            return !pred(arg);
         }
   };
template <class Pred>
   negation_logique_<Pred> negation_logique(Pred pred) {
      return { pred };
   }

... et obtenir du code machine pleinement équivalent, mais ça aurait été plus laborieux.

template<class Pred>
   auto negation_logique(Pred pred) {
      return [=](const auto &arg) { return !pred(arg);  };
   }

La fonction est_blanc(loc) retourne un prédicat foncteur qui retournera true pour un caractère c seulement si c est un blanc au sens de loc.

auto est_blanc(const std::locale &loc) {
   return [&loc](char c) { return std::isspace(c, loc); };
}

La fonction afficher_sequence(debut,fin,pos) affichera les éléments de sur os, intercalant un espace entre chaque paire d'éléments.

template <class It>
   void afficher_sequence(It debut, It fin, std::ostream &os) {
      using namespace std;
      if (debut == fin) return;
      os << *debut;
      for_each(next(debut), fin, [&os](auto val) { os << ' ' << val; });
   }

La fonction fusionner_alternance(premier,second) est particulière au sens où elle est destinée à un usage « interne ». Elle suppose que premier est au moins aussi grand (en termes de nombre d'éléments) que second, responsabilité qui incombe au code client (c'est une précondition de la fonction) et qui n'est pas validée.

Son rôle est de construire et de retourner un conteneur contenant des éléments de premier et de second en alternance, commençant par premier. Le conteneur construit puis retourné sera du même type que celui des conteneurs premier et second.

J'ai utilisé res.insert(end(res),*q) pour l'insertion de manière à être plus général que push_back(), qui n'est pas supporté par tous les types de conteneurs.

//
// precondition: premier.size() >= second.size()
//
template <class T, class R = T>
   R fusionner_alternance(T premier, T second) {
      using namespace std;
      R res;
      for (auto p = begin(premier), q = begin(second); p != end(premier); ++p) {
         res.insert(end(res), *p);
         if (q != end(second)) {
            res.insert(end(res), *q);
            ++q;
         }
      }
      return res;
   }

À propos de inverser_mots()

À plusieurs parmi vous, j'ai dit à la blague que « inverser_mots(), c'est simple au fond : on sépare la chaîne en blancs et en non-blancs, on inverse les non-blancs, puis on fusionne ». Plusieurs m'ont regardé d'un air perplexe en entendant cela.

Voici une démonstration :

Quelques inclusions d'en-tête sont requises, bien sûr.

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <iterator>
#include <utility>
#include <locale>
#include <algorithm>
using namespace std;

Pour inverser les éléments d'un vecteur de string (ou d'un vecteur d'un autre type contenent des éléments; j'ai fait de cette fonction une fonction générique), suffit d'appliquer reverse() sur chacun d'entre eux.

template <class T>
   void inverser_elements(vector<T> &v) {
      for (T &elem : v)
         reverse(begin(elem), end(elem));
   }

Pour fusionner deux vector<string>, je prends les éléments de chacun d'eux, un à un, et je les cumule dans une même string. Si le premier est plus gros que le second (ce qui se produira si la string originale commence et finit par des blancs, ou encore si elle commence et finit par des non-blancs), je traite l'élément de plus du premier vecteur comme un cas dégénéré, après la boucle.

string fusionner(vector<string> v0, vector<string> v1) {
   string res;
   for (auto i0 = begin(v0), i1 = begin(v1);
        i0 != end(v0) && i1 != end(v1); ++i0, ++i1)
      res += *i0 + *i1;
   if (v0.size() > v1.size()) res += v0.back();
   return res;
}

J'ai fait de la fonction qui sépare la chaîne originale en blancs et en non-blancs une fonction générique sur la base d'un prédicat quelconque (plus flexible, pas plus compliqué), qui retournera deux vecteurs (en gros, un pour les blocs qui respectent le prédicat et un pour les blocs qui ne le respectent pas).

Cette fonction pourrait être un peu simplifiée, si ça vous amuse (on n'a ps vraiment besoin de n, entre autres).

template <class Pred>
   auto separer(string s, Pred pred) {
      vector<string> v[2]; // oui, non
      if (s.empty()) return pair{ v[0], v[1] };
      bool oui = pred(s.front());
      int n = 0;
      for (auto p = begin(s); p != end(s); oui = !oui) {
         auto q = oui ? find_if_not(p, end(s), pred) : find_if(p, end(s), pred);
         v[n].emplace_back(p, q);
         p = q;
         n = n ? 0 : 1;
      }
      return pair{ v[0], v[1] };
   }

Muni de ces outils, inverser_mots() est exactement tel que je le décrivais. Le recours à la sémantique de mouvement est une optimisation; le code fonctionne sans elle.

string inverser_mots(string s) {
   auto est_blanc = [loc = locale{ "" }](char c){ return isspace(c, loc); };
   auto [v0, v1] = separer(std::move(s), est_blanc);
   inverser_elements(!v0.empty() && !est_blanc(v0.front()[0])? v0 : v1);
   return fusionner(std::move(v0), std::move(v1));
}

Reste plus qu'à tester le tout!

int main() {
   cout << '\"' << inverser_mots("") << '\"' << endl;
   cout << '\"' << inverser_mots(" j'aime mon prof ") << '\"' << endl;
   cout << '\"' << inverser_mots("j'aime mon prof ") << '\"' << endl;
   cout << '\"' << inverser_mots(" j'aime mon prof") << '\"' << endl;
   cout << '\"' << inverser_mots("j'aime mon prof") << '\"' << endl;
}

Valid XHTML 1.0 Transitional

CSS Valide !