Série 10 :
Pointeurs.

Buts

Cette série a pour but de vous faire pratiquer les pointeurs et références.

Préliminaires :

Avant d'effectuer les manipulations décrites dans cette série, créez le répertoire ~/Desktop/myfiles/cpp/serie10 (c.-à-d. créez le sous-répertoire serie10 dans le répertoire ~/Desktop/myfiles/cpp).

Exercice 0 : listes chaînées (pointeurs, niveau 0)

Cet exercice est disponible en page 48 de l'ouvrage C++ par la pratique.

Le but de cet exercice est de travailler les pointeurs (et les structures) sur une notion très connue en informatique pour organiser les données : les listes chaînées.

Cliquez ici si vous souhaitez faire cet exercice.


Exercice 1 : référence (structures, références, niveau 1)

Exercice n°26 (pages 64 et 231) de l'ouvrage C++ par la pratique.
Exercice n°22 du MOOC (semaine 7)

Un petit exercice simple pour manipuler les références (C++) comme référencement d'objets (cas d'utilisation numéro 1 du cours).

1.1 Références

Créez une structure Maison contenant simplement une adresse (chaîne de caractères).

Créez ensuite une structure Personne contenant un nom (chaîne de caractères) et une référence sur une Maison.
Les différents habitants d'une maison n'ont en effet pas une copie de leur maison avec eux, mais simplement un moyen d'y référer (mémoire de l'adresse)...
C'est un cas typique d'illustration des références.

Créez ensuite deux maisons différentes dans le main(), puis créez des personnes différentes habitant dans des maisons différentes (plusieurs habitants par maison).
La principale difficulté réside dans le fait qu'une référence doit toujours référencer quelque chose : il faut donc affecter les maisons dès l'initialisation.

Pour finir cette partie : créez une fonction affiche qui affiche une personne en indiquant son nom et son adresse, par exemple :

Pierre habite 12 rue du Château

Affichez toutes les personnes de votre programme.

1.2 Limites des références (niveau 2)

Le cadre choisi ici illustre aussi les limites des références : avec cette implémentation, il est impossible à une personne de déménager : on ne peut en effet pas «changer de référence».

C'est parfait si c'est vraiment ce que l'on veut dans le programme (=on est sûr que personne ne déménagera).
Mais si l'on voulait pouvoir faire déménager les personnes, il faudrait faire autrement... à vous de trouver comment.

En clair : implémenter une seconde version du programme dans laquelle vous faites déménager au moins une personne.
Assurez-vous que les autres personnes à la même (ancienne) adresse n'ont pas elles aussi déménagé (ce qui serait le cas avec des références :-( ).


Exercice 2 : généricité (pointeurs, niveau 1)

Exercice n°23 (pages 61 et 228) de l'ouvrage C++ par la pratique.
Exercice n°21 du MOOC (semaine 7)

Un petit exercice simple pour manipuler les pointeurs comme sélecteurs d'objets.

Créez un nouveau fichier appelé selecteur.cc.
Dans le main(), créez trois variables valeur1 valeur2 et valeur3, de type double que vous initialisez à des valeurs de votre choix.

Déclarez un pointeur choix, pointeur sur des double.

Demandez ensuite un nombre entre 1 et 3 à l'utilisateur (cf exercice 2 série 5) et, en fonction de son choix, faites pointer choix sur valeur1, valeur2 ou valeur3.

Pour finir affichez Vous avez choisi et la valeur choisie en utilisant le pointeur choix.

Remarque : il est évident que le but de cet exercice n'est que de manipuler les pointeurs. Hors de ces contraintes, la bonne solution pour faire la même chose serait bien sûr d'utiliser un tableau.


Exercice 3 : Tranche maximale (structures, tableaux, pointeurs, algorithmique, niveau 2)

Positionnement du problème

On s'intéresse ici au problème suivant :
étant donnée une liste (finie) de nombres entiers, positifs et négatifs, trouver la sous-liste (contiguë) de somme maximale.

Par exemple si la liste est :

 11, 13, -4, 3, -26, 7, -13, 25, -2, 17, 5, -8, 1
la sous-liste 3, -26, 7, -13, 25 a pour somme -4,
et la sous-liste de somme maximale est
     25, -2, 17, 5
(de somme 45).

Un des objectifs de cet exercice est de travailler les pointeurs (qui ne sont pas strictement nécessaire pour résoudre ce problème).

Un autre objectif est de revenir sur la complexité des algorithmes et comparer différentes versions de solutions.

Structures de données

Pour résoudre informatiquement ce problème définissez les types suivants :

Pour tester, commencer de suite à déclarer dans le main() un tableau de listes, p.ex. :

  vector seq({
     { 11, 13, -4, 3, -26, 7, -13, 25, -2, 17, 5, -8, 1 },
     {}, 
     { -3, -4, -1, -2, -3 },
     { -1, -4, -3, -2, -3 },
     { 3, 4, 1, 2, -3 },
     { 3, 4, 1, 2, 3 },
     { 3, -1, -1, -1, 5 },
     { -5, -4, 1, 1, 1 }
  });

Algorithmes (et complexité)

Algorithme naïf

L'algorithme le plus simple qui vient à l'esprit est de chercher parmi toutes les sous-listes, celle de somme maximale. « Chercher parmi toutes les sous-listes » signifie, pour tous les débuts possibles, et pour toutes les fins possible pour un tel début.

D'où l'algorithme suivant :

    Entree : liste de N nombres
    Sortie : la sous-liste (debut, fin, somme) de somme maximale

    sousliste = (0, 0, -infini)
    Pour tout debut de 1 à N
        Pour tout fin de debut à N
            somme = 0
            Pour tout position de debut à fin
                somme = somme + liste[position]
                Si somme > sousliste.somme
                    sousliste = (debut, fin, somme)
    Sortir : sousliste

QUESTION : quelle est la complexité de cet algorithme ?

Implémentez cet algorithme (en C++) dans une fonction tranche_max_1 (de votre programme tranches.cc).

Complétez la fonction main() pour rechercher la sous-liste (ou « tranche ») de somme maximale dans vos listes de test.

Algorithme un peu moins naïf

La complexité de l'algorithme précédent vient de ses boucles imbriquées.
Serait-il possible d'en enlever une ?

En regardant de plus prêt, on s'appercoit vite que l'on refait beaucoup de calculs plusieurs fois :
le calcul de la somme d'une sous-liste utilise souvent les calculs d'une sous-liste précédente.

En d'autres termes, la boucle la plus intérieure est inutile car pour calculer la somme à « position+1 », il suffit d'ajouter liste[position+1] à l'ancienne somme.

Voici alors un nouvel algorithme :

    Entree : liste de N nombres
    Sortie : la sous-liste (debut, fin, somme) de somme maximale

    sousliste = (0, 0, -infini)
    Pour tout debut de 1 à N
        somme = 0
        Pour tout fin de debut à N
            somme = somme + liste[fin]
            Si somme > sousliste.somme
                sousliste = (debut, fin, somme)
    Sortir : sousliste

QUESTION : quelle est la complexité de cet algorithme ?

Implémentez cet algorithme dans une fonction tranche_max_2 et vérifiez qu'il fournit les bons résultats.

Algorithme linéaire

Peut-on aller plus loin ? Peut-on encore enlever une boucle ?

La réponse est oui, mais la solution est peut-être moins triviale.

L'idée reste cependant la même : supprimer des calculs inutiles. Mais elle utilise ici le fait que l'on cherche un maximum.

Si donc on trouve une sous-liste initiale de somme inférieure (ou égale) à 0, on peut supprimer cette sous-liste initiale car elle apporte moins à la somme totale que de commencer juste après elle.

Par exemple dans la liste 4,-5,3,... il vaut mieux commencer au 3 (avec une somme initiale qui vaut 0) que commencer au 4 (qui nous donne une somme de -1 arrivé au 3).

D'où l'idée de l'algorithme suivant :

    Entree : liste de N nombres
    Sortie : la sous-liste (debut, fin, somme) de somme maximale

    sousliste = (0, 0, -infini)
    debut = 1
    somme = 0
    Pour tout fin de 1 à N
        somme = somme + liste[fin]
        Si somme > sousliste.somme
            sousliste = (debut, fin, somme)
        Si somme <= 0
            debut = fin + 1
            somme = 0
    Sortir : sousliste

QUESTION : quelle est la complexité de cet algorithme ?

Implémentez cet algorithme dans une fonction tranche_max_3 et vérifiez qu'il fournit les bons résultats.

Améliorations (FACULTATIF)

Vous pouvez aussi :

  1. vous aider d'une fonction affiche() pour afficher les résultats ;

  2. (semaine prochaine) lire des listes depuis un fichier.


Exercice 4 : dérivation formelle (pointeurs, niveau 3)

Le but est ici de faire un programme capable de dériver formellement des expressions (arithmétiques).

On utilisera pour cela une structure d'arbre binaire. Un arbre binaire est une structure de donnée définie de façon récursive par :
un arbre est

Par exemple, si l'on note un arbre entre parenthèses, en premier son sous-arbre droit, puis sa valeur et enfin son sous-arbre gauche, alors

(((a) + (b)) * ((c) - (d)))

est un arbre binaire : la valeur est * et les deux sous-arbres ((a) + (b)) et ((c) - (d)) :

De même (a) est un arbre réduit à une valeur (c.-à-d. avec 2 sous-arbres vides).

  1. Dans un fichier derivation.cc, définissez, en vous inspirant de l'exercice des listes chaînées une structure de données pour représenter les arbres binaires.

    Dans cette structure de données, la valeur sera représentée par un char.

    Assurez-vous que l'arbre puisse partager un ou plusieurs sous-arbres. Par exemple, l'arbre ((x) + (x)) peut être construit à partir d'un seul arbre (x). Pour ce faire, vous utiliserez un compteur de références c.-à-d. un nombre qui compte combiens d'arbres utilisent un sous-arbre donné (les plus avancés d'entre vous pourront utiliser ici un shared_ptr, hors du programme du cours).

  2. Écrire la fonction permettant de créer un arbre binaire à partir d'une valeur et de deux (pointeur sur des) sous-arbres. Vous pourrez également créer une fonction qui créée ce que l'on appelle «une feuille» c'est-à-dire un arbre réduit à sa seule valeur, dont les 2 sous-arbres sont vides.
  3. Tester que son code fonctionne correctement.
  4. Passons maintenant à la représentation des expressions arithmétiques.

    On utilisera pour cela les arbres binaires (nous ne considérerons ici que les opérateurs binaires + - / * et ^ (puissance)) :
    par exemple a + b sera représenté par l'arbre
    ((a) + (b))
    où '+' est la valeur du premier arbre et "(a)" et "(b)" ses sous-arbres.
    De même, l'expression arithmétique
    (a + b) * (c + d)
    sera représentée par l'arbre
    (((a) + (b)) * ((c) + (d)))

    Construire les arbres représentant les expressions suivantes (4 expressions) :
    x + a
    (x + a) * (x + b)
    ((x * x) * x) + (a * x)
    (x ^ a) / ((x * x) + (b * x))

    Indication : commencez par créer les arbres binaires représentant les expressions a, b et x (seuls), puis ceux représentant les expressions x+a, x+b et x*x.

  5. Ecrire une fonction derive qui prend un arbre binaire en argument, supposé être une représentation d'une expression arithmétique valide et un caractère représentant la variable par rapport à laquelle il faut dériver, et qui retourne l'arbre binaire correspondant à l'expression arithmétique dérivée.

    On procédera pour cela de façon récursive en se rappelant les règles usuelles de dérivation :

    da/dx = 0 où a est une constante (ici : caractère différent de x)
    
    dx/dx = 1
    
    d(f+g)/dx = df/dx + dg/dx
    
    d(f-g)/dx = df/dx - dg/dx
    
    d(f*g)/dx = (df/dx * g) + (f * dg/dx)
    
    d(f/g)/dx = ((df/dx * g) - (f * dg/dx) ) / (g * g)
    
    d(f^a)/dx = a * df/dx * (f ^ (a - 1))
    
    (où a ne dépend pas de x, ce que l'on supposera ici)
    
    Note importante : on ne veut pas l'arbre minimal de dérivation ! En clair, on ne cherchera pas à simplifier les expressions obtenues.

Dernière mise à jour le 25 novembre 2021
Last modified: Thu Nov 25, 2021