CoddyRun
Apprentissage

Conception algorithmique

Penser le programme avant de l’écrire

Avant d’être du code, un programme est un raisonnement. Cette page rassemble les méthodes qui permettent de passer d’un énoncé en français à un algorithme juste : spécifier, découper, choisir ses structures, construire une boucle sûre et vérifier son travail. Tous les exemples sont écrits en CoddyRun et se copient dans l’éditeur.

1

Qu’est-ce qu’un algorithme ?

Un algorithme, c’est la description précise d’une méthode pour passer de ce que l’on a à ce que l’on veut. Rien de plus. Il tient en trois éléments, et savoir les nommer est déjà la moitié du travail :

  • Les données. Ce dont on dispose au départ : trois notes, une durée en secondes, une liste de prénoms.
  • Le traitement. La suite d’opérations qui transforme les données. C’est le cœur du raisonnement.
  • Le résultat attendu. Ce que l’on veut obtenir, décrit assez précisément pour savoir si on l’a obtenu.

Une recette de cuisine, un itinéraire, une notice de montage sont des algorithmes : des ingrédients, des étapes, un plat. La différence avec l’informatique tient en un mot : la machine ne devine rien. Une recette peut dire salez à votre goût ; un algorithme, non.

Les trois éléments dans un programme

Prenons la moyenne de trois notes. Les commentaires marquent les trois parties :

// DONNÉES : trois notes sur 20
note1 est un nombre vaut 12
note2 est un nombre vaut 15.5
note3 est un nombre vaut 9

// TRAITEMENT : la règle de calcul
total est un nombre vaut note1 + note2 + note3
resultat est un nombre vaut total / 3

// RÉSULTAT ATTENDU : la moyenne, arrondie au dixième
affiche 'Moyenne : ' + decimal(resultat, 1)

Ce découpage se retrouve dans presque tous les programmes. Quand vous bloquez devant un énoncé, commencez par le relire en vous demandant : qu’est-ce que j’ai ? qu’est-ce que je veux ? Le milieu vient souvent tout seul.

Le piège du débutant

Se jeter sur le clavier. Tant que vous ne savez pas dire en français ce que doit faire le programme, aucun langage ne vous aidera. Écrire du code est la dernière étape, pas la première.

À vous

Pour chacun de ces énoncés, écrivez sur une feuille les données, le résultat attendu, et les grandes étapes du traitement. Sans écrire une ligne de code.

  • Un distributeur rend la monnaie sur un billet de 10 €.
  • Un programme dit si une année est bissextile.
  • Un jeu tire un nombre au hasard et fait deviner le joueur.
Voir une réponse pour le premier
  • Données : le prix à payer, la somme donnée (10 €), la liste des pièces et billets disponibles.
  • Résultat attendu : la liste des pièces à rendre, ou un message si la somme donnée est insuffisante.
  • Traitement : calculer la différence ; si elle est négative, s’arrêter ; sinon, prendre la plus grosse valeur possible, la retirer de ce qui reste à rendre, et recommencer tant qu’il reste quelque chose.
2

Spécifier avant d’écrire

Spécifier, c’est écrire ce que le programme doit faire, jamais comment il le fera. C’est un contrat avec vous-même : si le programme respecte la spécification, il est juste ; sinon, il est faux. Sans ce contrat, la question est-ce que ça marche ? n’a pas de réponse.

Une bonne spécification tient en quatre lignes :

  • Entrées : leur nature et leur domaine. Dire un nombre ne suffit pas : un entier ? positif ? jusqu’à combien ?
  • Sortie : sa nature et sa forme. Un texte ? un nombre arrondi à combien de décimales ?
  • Préconditions : ce que l’on suppose vrai au départ. Par exemple le tableau n’est pas vide, ou le diviseur n’est pas nul.
  • Cas limites : zéro, le vide, le négatif, le plus grand, le plus petit, l’égalité parfaite.

Un exemple de contrat

Convertir une durée en secondes en heures, minutes et secondes.

  • Entrée : un nombre entier de secondes, positif ou nul.
  • Sortie : un texte de la forme 1 h 2 min 5 s.
  • Précondition : aucune, la fonction doit se débrouiller même avec un nombre négatif.
  • Cas limites : 0 seconde, moins d’une minute, un nombre négatif.

Le contrat écrit, le code devient presque mécanique, et chaque cas limite devient une ligne de test :

fonction enHeures(secondes)
    si secondes < 0 alors
        retourne 'durée impossible'
    fin si
    h est un nombre vaut arrondi_inferieur(secondes / 3600)
    reste est un nombre vaut secondes - h * 3600
    m est un nombre vaut arrondi_inferieur(reste / 60)
    s est un nombre vaut reste - m * 60
    retourne h + ' h ' + m + ' min ' + s + ' s'
fin fonction

affiche enHeures(3725)     // 1 h 2 min 5 s
affiche enHeures(59)       // 0 h 0 min 59 s
affiche enHeures(0)        // 0 h 0 min 0 s
affiche enHeures(-10)      // durée impossible

Remarquez la première ligne de la fonction : elle traite le cas limite avant le calcul. On appelle cela un garde-fou. Écrire la spécification d’abord, c’est ce qui permet d’y penser à temps.

Le piège

Croire que ça marche parce que l’exemple de l’énoncé donne le bon résultat. Un programme juste sur un seul cas n’est pas un programme juste : ce sont les cas limites qui révèlent les erreurs, et ils ne viennent pas à l’esprit pendant qu’on code, seulement pendant qu’on spécifie.

À vous

Écrivez la spécification complète (entrées, sortie, préconditions, cas limites) de : calculer le prix d’un billet de train selon l’âge du voyageur. Gratuit avant 4 ans, demi-tarif avant 12 ans, plein tarif ensuite, et 20 % de réduction à partir de 60 ans.

Voir une réponse
  • Entrées : l’âge (entier, de 0 à 120) et le plein tarif (nombre strictement positif, en euros).
  • Sortie : un prix en euros, arrondi au centime, jamais négatif.
  • Préconditions : aucune ; un âge hors domaine doit donner un message d’erreur plutôt qu’un prix faux.
  • Cas limites : exactement 4 ans, exactement 12 ans, exactement 60 ans (les bornes sont-elles incluses ?), un tarif de 0 €, un âge négatif.

Les trois bornes sont le vrai sujet : l’énoncé dit avant 4 ans, donc 4 ans est déjà payant. C’est exactement le genre de détail qu’une spécification oblige à trancher, et qu’un programme écrit trop vite tranche au hasard.

3

Décomposer le problème

Un problème qui résiste est presque toujours un problème qu’on n’a pas assez découpé. La méthode est simple : tant qu’une étape vous semble difficile, c’est qu’elle contient plusieurs étapes plus simples. Continuez à couper jusqu’à ce que chaque morceau tienne en une phrase.

Afficher le bulletin d’un élève est vague. Découpons :

  1. Calculer la moyenne d’une liste de notes.
  2. Trouver la mention qui correspond à une moyenne.
  3. Afficher le nom, la moyenne et la mention.

Chaque morceau devient une fonction qui porte son nom. Et le programme principal, lui, se lit comme un résumé du raisonnement :

// Trois sous-problèmes, trois fonctions

fonction moyenneDe(liste)
    si longueur(liste) = 0 alors
        retourne 0
    fin si
    total est un nombre vaut 0
    pour i de 0 à longueur(liste) - 1
        total ajoute liste[i]
    fin pour
    retourne total / longueur(liste)
fin fonction

fonction mentionDe(moy)
    si moy >= 16 alors
        retourne 'Très bien'
    fin si
    si moy >= 14 alors
        retourne 'Bien'
    fin si
    si moy >= 12 alors
        retourne 'Assez bien'
    fin si
    si moy >= 10 alors
        retourne 'Admis'
    fin si
    retourne 'Refusé'
fin fonction

procédure bulletin(eleve, liste)
    moy est un nombre vaut moyenneDe(liste)
    affiche eleve + ' : ' + decimal(moy, 2) + ' -> ' + mentionDe(moy)
fin procédure

// Le programme principal se lit comme un résumé
appelle bulletin('Léa', [12, 15.5, 9])
appelle bulletin('Tom', [17, 16, 18])
appelle bulletin('Inès', [8, 11, 9.5])

Où couper ?

Trois signaux ne trompent pas :

  • Vous écrivez deux fois la même chose : c’est une fonction qui demande à naître.
  • Vous ne savez pas nommer un morceau : c’est qu’il en fait plusieurs à la fois.
  • Un bloc dépasse vingt lignes : il cache au moins deux idées.

À l’inverse, une fonction qui ne sert qu’une fois et tient en une ligne n’apporte souvent rien. Le but n’est pas de découper le plus possible, mais de donner un nom à chaque idée.

Le piège

Une fonction qui calcule et affiche. moyenneDe renvoie un nombre et n’affiche rien : c’est ce qui permet de la réutiliser pour un classement, une moyenne de classe ou un graphique. Une fonction qui affiche au milieu d’un calcul ne sert qu’une fois.

À vous

Découpez en sous-problèmes : afficher le podium d’une course à partir d’une liste de coureurs et de leurs temps. Donnez un nom à chaque morceau et dites ce qu’il reçoit et ce qu’il renvoie, sans écrire le code.

Voir une réponse
  • enSecondes(texte) : reçoit '1:23', renvoie 83. Pour pouvoir comparer des temps.
  • ordreDesArrivees(temps) : reçoit les temps, renvoie les places dans l’ordre croissant.
  • enMinutes(secondes) : l’inverse du premier, pour réafficher proprement.
  • podium(noms, temps) : assemble les trois et affiche les trois premiers.

Les deux fonctions de conversion sont le cœur du raisonnement : elles séparent la forme des temps (du texte, pour l’affichage) de leur valeur (des nombres, pour le calcul). Sans elles, on essaie de comparer des textes, et '1:23' passe avant '59'.

4

Choisir la bonne structure

Toute la programmation impérative tient en trois structures. Trois. Le reste n’est que combinaison.

La séquence

Des instructions l’une après l’autre, dans l’ordre. C’est la structure par défaut, et l’ordre compte : on ne peut pas utiliser une valeur avant de l’avoir calculée.

L’alternative

si … alors … sinon. Le programme choisit un chemin selon une condition. Les chemins s’excluent : un seul est pris.

La répétition

pour, tant que, répète. Les mêmes instructions sont rejouées plusieurs fois, avec des valeurs différentes.

Une seule question à se poser

Pour une répétition, la question est toujours la même : est-ce que je connais le nombre de tours avant de commencer ?

  • Oui : pour chacune des 12 cases, de 1 à 100. C’est une boucle pour.
  • Non : tant que le joueur n’a pas trouvé, jusqu’à ce que le reste soit nul. C’est tant que.
// Nombre de tours connu d'avance : la boucle pour
pour jour de 1 à 7
    affiche 'Jour ' + jour
fin pour

// Nombre de tours inconnu : la boucle tant que
reste est un nombre vaut 100
etapes est un nombre vaut 0
tant que reste > 1
    reste vaut reste / 2
    etapes ajoute 1
fin tant que
affiche 'Divisé par deux ' + etapes + ' fois'    // 7 fois

// Un choix parmi plusieurs : les conditions en cascade
age est un nombre vaut 15
si age < 6 alors
    affiche 'Maternelle'
sinon si age < 11 alors
    affiche 'Primaire'
sinon si age < 15 alors
    affiche 'Collège'
sinon
    affiche 'Lycée'
fin si

Le deuxième exemple est typique : impossible de savoir d’avance combien de fois on peut diviser 100 par deux avant de descendre sous 1. C’est la boucle elle-même qui le découvre.

Le piège

La cascade de conditions écrite dans le désordre. Avec si age < 15 placé en premier, un enfant de 3 ans serait rangé au collège : la première condition vraie l’emporte. Dans une cascade, les conditions se lisent dans l’ordre : rangez-les de la plus restrictive à la plus large.

À vous

Pour chacun de ces besoins, dites quelle structure convient, et pourquoi :

  • Afficher les 10 premiers multiples de 7.
  • Demander un mot de passe jusqu’à ce qu’il soit correct.
  • Afficher majeur ou mineur selon un âge.
  • Chercher un prénom dans une liste et s’arrêter dès qu’on le trouve.
Voir une réponse
  • Boucle pour : dix tours, connus d’avance.
  • Boucle tant que : on ignore combien d’essais il faudra. Ajoutez un compteur pour limiter à trois tentatives.
  • Alternative : aucune répétition, un simple si … sinon.
  • Les deux se défendent. Une boucle pour sur tout le tableau avec arrête dès qu’on trouve ; ou une boucle tant que qui continue tant qu’on n’a pas trouvé et qu’il reste des cases. La première est plus courte et plus lisible.
5

Construire une boucle juste

La boucle est l’endroit où l’on se trompe le plus. Pas parce qu’elle est compliquée, mais parce qu’on l’écrit à l’instinct. Trois questions suffisent à la rendre sûre, et elles se posent avant d’écrire la première ligne.

  1. Qu’est-ce qui est vrai avant le premier tour ? C’est l’initialisation. Le plus souvent, c’est le cas je n’ai encore rien traité ou je n’ai traité que le premier.
  2. Qu’est-ce qui reste vrai à chaque tour ? C’est l’invariant : une phrase qui décrit l’état du calcul après n’importe quel tour. C’est la clé de voûte.
  3. Qu’est-ce qui avance, et quand s’arrête-t-on ? Quelque chose doit se rapprocher de la fin à chaque tour, sans quoi la boucle tourne sans fin.

L’invariant, en pratique

Cherchons la plus grande valeur d’un tableau. L’invariant s’écrit en une phrase : après le tour numéro i, record contient la plus grande des i premières valeurs. Cette phrase décide de tout le reste :

fonction plusGrand(liste)
    si longueur(liste) = 0 alors
        retourne 0
    fin si
    // Au départ : le plus grand d'une seule valeur, c'est elle-même
    record est un nombre vaut liste[0]
    pour i de 1 à longueur(liste) - 1
        // Ici, record est le plus grand des i premières valeurs
        si liste[i] > record alors
            record vaut liste[i]
        fin si
        // Et ici, le plus grand des i + 1 premières
    fin pour
    // Après le dernier tour : le plus grand de toutes
    retourne record
fin fonction

temperatures est un tableau vaut [-4, -9, -2, -7]
affiche plusGrand(temperatures)    // -2

Pourquoi partir de liste[0] et non de 0 ? Parce que l’invariant l’impose : avant le premier tour, on n’a regardé qu’une valeur, et la plus grande d’une seule valeur, c’est elle-même. Avec un record initialisé à 0, le programme répondrait 0 sur un tableau de températures négatives, une erreur classique, et invisible tant qu’on ne teste qu’avec des nombres positifs.

Ce qui avance

Avec pour, l’arrêt est garanti : le compteur avance tout seul. Avec tant que, c’est à vous de garantir que la condition finira par devenir fausse :

// Ce qui avance : reste diminue à chaque tour, donc la boucle s'arrête
reste est un nombre vaut 37
chiffres1 est un nombre vaut 0
tant que reste > 0
    reste vaut arrondi_inferieur(reste / 10)
    chiffres1 ajoute 1
fin tant que
affiche '37 s’écrit avec ' + chiffres1 + ' chiffres'    // 2

reste diminue strictement à chaque tour et ne peut pas descendre sous 0 : la boucle s’arrête forcément. Si vous ne savez pas dire ce qui avance, votre boucle est peut-être infinie.

Le piège

L’erreur d’un cran, la plus fréquente de toute la programmation. Les cases d’un tableau de 5 éléments vont de 0 à 4, donc la boucle s’écrit pour i de 0 à longueur(liste) - 1. Un tour de trop, et le programme lit une case qui n’existe pas.

À vous

Écrivez l’invariant de ces trois boucles, sous la forme d’une phrase en français, pas de code :

  • Une boucle qui additionne les valeurs d’un tableau.
  • Une boucle qui compte combien de voyelles contient un texte.
  • Une boucle qui cherche si une valeur est présente dans un tableau.
Voir une réponse
  • Après le tour i, total contient la somme des i premières valeurs. D’où l’initialisation à 0 : la somme de zéro valeur vaut 0.
  • Après le tour i, compteur contient le nombre de voyelles parmi les i premières lettres.
  • Après le tour i, trouve est vrai si et seulement si la valeur figure parmi les i premières cases. D’où l’initialisation à faux : dans zéro case, elle n’y est pas.

Dans les trois cas, l’invariant donne l’initialisation : c’est l’état du calcul quand on n’a encore rien traité.

6

Récursivité ou itération ?

Certains problèmes se définissent à partir d’eux-mêmes. La factorielle de 5, c’est 5 × la factorielle de 4. Un dossier contient des dossiers. Un ancêtre a lui-même des ancêtres. Pour ceux-là, la récursivité, une fonction qui s’appelle elle-même, dit exactement ce qu’on pense.

Une fonction récursive tient toujours en deux morceaux, et il en faut impérativement deux :

  • Le cas de base. Le cas le plus simple, celui qu’on sait résoudre sans rien appeler. Sans lui, la fonction ne s’arrête jamais.
  • La réduction. Le cas général, exprimé avec un cas strictement plus petit. Sans cette diminution, on n’atteint jamais le cas de base.
// Version récursive : la définition mathématique, telle quelle
fonction factorielleR(n)
    si n <= 1 alors
        retourne 1
    fin si
    retourne n * factorielleR(n - 1)
fin fonction

// Version itérative : le même calcul, avec une boucle
fonction factorielleI(n)
    produit est un nombre vaut 1
    pour i de 2 à n
        produit vaut produit * i
    fin pour
    retourne produit
fin fonction

affiche factorielleR(5)    // 120
affiche factorielleI(5)    // 120

Les deux fonctions donnent le même résultat. La récursive ressemble à la définition mathématique ; l’itérative dit comment la machine va s’y prendre. Aucune n’est meilleure dans l’absolu.

Laquelle choisir ?

La récursivité

Imbattable quand le problème est naturellement récursif : parcourir une arborescence, les tours de Hanoï, un dessin qui se répète à l’intérieur de lui-même. Le code est court et se lit comme l’énoncé.

L’itération

Préférable pour tout ce qui est un parcours : additionner, compter, chercher. Pas d’empilement d’appels, donc pas de limite de profondeur, et c’est souvent plus rapide.

Chaque appel en attente occupe une place en mémoire, dans ce qu’on appelle la pile. CoddyRun autorise 5 000 appels imbriqués : largement de quoi travailler, mais une récursivité sans cas de base atteint cette limite en un clin d’œil et arrête le programme.

Un exemple à connaître : le plus grand commun diviseur, par divisions successives. Il se définit récursivement, mais s’écrit très bien avec une boucle, ce qui évite d’empiler des appels :

// Le plus grand commun diviseur, par soustractions successives
fonction diviseurCommun(a1, b1)
    grand est un nombre vaut maximum(a1, b1)
    petit est un nombre vaut minimum(a1, b1)
    reste est un nombre
    tant que petit > 0
        reste vaut grand mod petit
        grand vaut petit
        petit vaut reste
    fin tant que
    retourne grand
fin fonction

affiche diviseurCommun(84, 36)    // 12
affiche diviseurCommun(17, 5)     // 1
affiche diviseurCommun(42, 0)     // 42

// CoddyRun possède déjà pgcd(a, b) : le réécrire est un exercice
affiche pgcd(84, 36)              // 12
Le piège

Un cas de base qui n’est jamais atteint. factorielleR(-1) avec le test si n = 1 au lieu de si n <= 1 descendrait indéfiniment. Posez-vous toujours la question : est-ce que chaque appel se rapproche vraiment du cas de base, pour toutes les entrées possibles ?

À vous

Écrivez, de tête, le cas de base et la réduction de : compter le nombre de chiffres d’un entier positif. Puis dites laquelle des deux versions vous écririez en vrai.

Voir une réponse
  • Cas de base : un nombre inférieur à 10 s’écrit avec 1 chiffre.
  • Réduction : sinon, c’est 1 + le nombre de chiffres de n / 10 arrondi à l’inférieur. Le nombre diminue strictement, donc on finit par descendre sous 10.
  • En vrai : la version itérative, avec la boucle tant que de la partie précédente. C’est un simple parcours, et la récursivité n’apporte ici aucune clarté supplémentaire.
7

Choisir ses données

Le choix de la structure de données n’est pas un détail technique qu’on règle après coup : c’est une décision de raisonnement. La bonne structure rend l’algorithme évident ; la mauvaise le rend inextricable. Voici comment choisir.

  • Une variable quand il n’y a qu’une valeur à retenir : un total, un compteur, un record.
  • Un tableau quand les valeurs sont de même nature et que leur ordre ou leur position compte : des notes, les cases d’un plateau, l’historique d’une partie.
  • Un dictionnaire quand on cherche une valeur par un nom plutôt que par un rang : un stock par produit, un score par joueur, un nombre d’apparitions par mot.
  • Des tableaux parallèles quand chaque élément porte plusieurs informations : le tableau des noms et celui des temps, où la case i parle toujours du même coureur.

Quand le dictionnaire change tout

Compter combien de fois chaque mot apparaît. Avec un tableau seul, il faudrait parcourir la liste des mots déjà vus à chaque mot : deux boucles imbriquées, et un code pénible. Avec un dictionnaire, le mot est la clé :

// Compter les occurrences : un dictionnaire, pas un tableau
mots est un tableau vaut ['chat', 'chien', 'chat', 'souris', 'chat', 'chien']
decompte est un dictionnaire
mot est un texte

pour i de 0 à longueur(mots) - 1
    mot vaut mots[i]
    si contient(decompte, mot) alors
        decompte[mot] vaut decompte[mot] + 1
    sinon
        decompte[mot] vaut 1
    fin si
fin pour

noms est un tableau vaut cles(decompte)
trier(noms)
pour i de 0 à longueur(noms) - 1
    affiche noms[i] + ' : ' + decompte[noms[i]]
fin pour

Le même raisonnement s’applique au stock d’une boutique, aux voix d’un vote ou aux lettres d’un texte : dès que la question commence par combien de fois chaque…, pensez dictionnaire.

Quand deux tableaux valent mieux qu’un

// Deux tableaux qui avancent ensemble : la case i parle du même coureur
coureurs est un tableau vaut ['Léa', 'Tom', 'Inès']
temps est un tableau vaut [83, 79, 91]

meilleur est un nombre vaut 0
pour i de 1 à longueur(temps) - 1
    si temps[i] < temps[meilleur] alors
        meilleur vaut i
    fin si
fin pour
affiche 'Vainqueur : ' + coureurs[meilleur] + ' en ' + temps[meilleur] + ' s'

Notez l’astuce : on ne retient pas le meilleur temps, mais la place du meilleur temps. C’est elle qui permet de retrouver le nom. Retenir l’indice plutôt que la valeur est un réflexe qui sert constamment.

Le piège

Lire une clé qui n’existe pas dans un dictionnaire : le programme s’arrête. On vérifie toujours avec contient(dictionnaire, cle) avant de lire : c’est précisément ce que fait le si de l’exemple ci-dessus.

À vous

Quelle structure pour chacun de ces besoins ?

  • Le nombre de points de chaque joueur d’une partie.
  • Les 20 dernières valeurs mesurées par un capteur.
  • Une grille de morpion.
  • Le prix de chaque article d’un magasin, et son stock.
Voir une réponse
  • Dictionnaire : on cherche les points de Léa, pas les points du troisième.
  • Tableau : l’ordre est l’information même, puisque ce sont des mesures successives.
  • Tableau de tableaux : grille1[ligne1][colonne], car une case se repère par deux nombres.
  • Deux dictionnaires avec les mêmes clés, un pour les prix et un pour les stocks, ou une classe produit si chaque article porte beaucoup d’informations.
8

Vérifier sans exécuter

Exécuter un programme vous dit qu’il est faux. Il ne vous dit jamais qu’il est juste : il vous dit seulement qu’il a donné le bon résultat pour ce que vous avez essayé. La relecture, elle, s’attaque au raisonnement. Deux techniques, et elles se pratiquent avec un papier et un crayon.

1. Dérouler à la main

On fabrique un tableau avec une colonne par variable et une ligne par tour de boucle, et on joue le rôle de la machine. Voici un programme qui se trompe :

// Ce programme doit afficher la moyenne. Il affiche 11,25 au lieu de 15.
notes est un tableau vaut [12, 18, 15]
total est un nombre vaut 0
pour i de 0 à longueur(notes)
    total ajoute notes[i]
fin pour
affiche decimal(total / 4, 2)

Déroulons les trois premiers tours, en notant ce que vaut chaque variable à la fin du tour :

Tourinotes[i]total
avant--0
101212
211830
321545
43case inexistante?

Le tableau révèle deux erreurs d’un coup. La boucle fait quatre tours au lieu de trois : longueur(notes) vaut 3, et aller de 0 à 3 fait bien quatre tours. Et la division se fait par 4, une valeur écrite en dur qui ne correspond à rien. La version juste n’écrit aucun nombre en dur :

notes est un tableau vaut [12, 18, 15]
total est un nombre vaut 0
pour i de 0 à longueur(notes) - 1
    total ajoute notes[i]
fin pour
affiche decimal(total / longueur(notes), 2)    // 15,00

2. Le jeu d’essai

Avant même d’écrire le code, écrivez la liste des cas à essayer, et le résultat attendu pour chacun. Un bon jeu d’essai contient toujours ces quatre familles :

  • Le cas normal, celui de l’énoncé. Il doit marcher, évidemment.
  • Les cas limites : zéro, le tableau vide, une seule valeur, la borne exacte d’une condition.
  • Les cas impossibles : un âge négatif, une division par zéro, un texte là où on attend un nombre.
  • Les cas où toutes les valeurs se ressemblent : toutes égales, toutes négatives, déjà triées, triées à l’envers.

Cette dernière famille est la plus négligée, et la plus rentable : c’est elle qui attrape le record initialisé à 0 de la partie précédente.

Le piège

Tester uniquement avec les valeurs de l’énoncé. Si l’exemple donne [12, 18, 15], le programme fautif ci-dessus aurait très bien pu afficher un résultat plausible et passer inaperçu. Choisissez vos cas de test contre votre programme, pas pour lui faire plaisir.

À vous

Un programme doit dire si un mot est un palindrome. Écrivez son jeu d’essai : au moins six cas, avec le résultat attendu pour chacun.

Voir une réponse
  • 'kayak' → vrai (cas normal, longueur impaire).
  • 'ressasser' → vrai (longueur impaire, plus long).
  • 'bateau' → faux (cas normal négatif).
  • 'ici' → vrai, 'ab' → faux (les mots très courts).
  • 'a' → vrai (une seule lettre : il faut trancher, et le dire dans la spécification).
  • '' → le texte vide : à vous de décider, mais le programme ne doit pas s’arrêter en erreur.
  • 'Kayak' → selon la spécification : si les majuscules comptent, c’est faux. Encore une décision à écrire noir sur blanc.
9

Mesurer le coût

Deux programmes peuvent donner le même résultat, l’un en une seconde et l’autre en une heure. Ce qui les sépare n’est presque jamais la vitesse de la machine : c’est le nombre d’opérations que l’algorithme demande. Et ce nombre se compte, sur le papier, avant d’écrire quoi que ce soit.

La bonne question n’est pas combien de temps ça prend ? mais qu’est-ce qui se passe si les données sont dix fois plus nombreuses ? Trois réponses possibles, et elles suffisent à couvrir presque tous les cas :

Coût constant

Dix fois plus de données, autant de travail. Lire une case par son indice, ajouter une valeur à la fin d’un tableau.

Coût proportionnel

Dix fois plus de données, dix fois plus de travail. Une boucle qui parcourt tout : somme, maximum, recherche simple.

Coût au carré

Dix fois plus de données, cent fois plus de travail. Deux boucles imbriquées : comparer toutes les paires, tri à bulles.

À part, et précieux : les algorithmes qui coupent en deux à chaque étape. Doubler les données ne coûte qu’une étape de plus. C’est la recherche par dichotomie, qui exige un tableau trié.

// Combien de comparaisons pour trouver une valeur ?

fonction coutLineaire(liste, cherche)
    compteur est un nombre vaut 0
    pour i de 0 à longueur(liste) - 1
        compteur ajoute 1
        si liste[i] = cherche alors
            retourne compteur
        fin si
    fin pour
    retourne compteur
fin fonction

fonction coutDichotomie(liste, cherche)
    compteur est un nombre vaut 0
    bas est un nombre vaut 0
    haut est un nombre vaut longueur(liste) - 1
    milieu est un nombre
    tant que bas <= haut
        milieu vaut arrondi_inferieur((bas + haut) / 2)
        compteur ajoute 1
        si liste[milieu] = cherche alors
            retourne compteur
        sinon si liste[milieu] < cherche alors
            bas vaut milieu + 1
        sinon
            haut vaut milieu - 1
        fin si
    fin tant que
    retourne compteur
fin fonction

nombres est un tableau vaut intervalle(1, 1000)
affiche 'Une à une : ' + coutLineaire(nombres, 1000)      // 1000
affiche 'En coupant en deux : ' + coutDichotomie(nombres, 1000)    // 10

Mille comparaisons contre dix. Et l’écart se creuse : sur un million de valeurs, la recherche simple en demanderait un million, la dichotomie vingt. Pas vingt mille : vingt.

Le bon réflexe

Avec dix valeurs, tout va vite et le choix de l’algorithme n’a aucune importance. C’est quand les données grossissent que la différence apparaît, et en général, on ne s’en rend compte qu’en production. Prenez donc l’habitude de compter les boucles imbriquées : chaque niveau d’imbrication multiplie le coût par la taille des données.

Le piège

Optimiser avant de mesurer. Un algorithme juste et lent vaut mille fois mieux qu’un algorithme rapide et faux. Écrivez d’abord la version simple, vérifiez-la, et ne l’améliorez que si elle est vraiment trop lente, et là, comptez les opérations avant de toucher au code.

À vous

Dans quelle famille ranger chacun de ces traitements, sur un tableau de n valeurs ?

  • Afficher la première valeur.
  • Chercher si deux valeurs du tableau sont identiques.
  • Calculer la moyenne.
  • Chercher une valeur dans un tableau déjà trié.
Voir une réponse
  • Constant : une seule opération, quelle que soit la taille.
  • Au carré : pour chaque valeur, on la compare à toutes les suivantes. Sur 1 000 valeurs, cela fait près de 500 000 comparaisons.
  • Proportionnel : un seul parcours complet.
  • En coupant en deux : une dizaine d’étapes sur mille valeurs, une vingtaine sur un million.

Le deuxième cas a une bien meilleure solution : ranger les valeurs déjà vues dans un dictionnaire et vérifier au passage si la valeur s’y trouve déjà. Le coût redevient proportionnel. Changer de structure de données change la famille : c’est tout l’intérêt de la partie précédente.

10

Du papier au programme

Le raisonnement est fait, la spécification est écrite, le jeu d’essai est prêt. La traduction en CoddyRun devient presque de la copie. Dans l’ordre :

  1. Écrivez la spécification en commentaire, en tête du programme. Elle sert de rappel pendant que vous codez, et de documentation après.
  2. Posez les fonctions, vides. Une par sous-problème, avec son nom définitif. Le programme ne tourne pas encore, et ce n’est pas grave.
  3. Remplissez-les une par une, en commençant par la plus simple. Testez chacune séparément avant de passer à la suivante.
  4. Déroulez votre jeu d’essai, cas limites compris. C’est maintenant que les erreurs se voient.
// Spécification
//   Entrée  : un tableau de notes sur 20, éventuellement vide
//   Sortie  : un texte moyenne (mention), ou bien pas de note
//   Limites : tableau vide, une seule note, notes toutes égales

fonction moyenneDe(liste)
    total est un nombre vaut 0
    pour i de 0 à longueur(liste) - 1
        total ajoute liste[i]
    fin pour
    retourne total / longueur(liste)
fin fonction

fonction mentionDe(moy)
    si moy >= 16 alors
        retourne 'Très bien'
    fin si
    si moy >= 14 alors
        retourne 'Bien'
    fin si
    si moy >= 10 alors
        retourne 'Admis'
    fin si
    retourne 'Refusé'
fin fonction

fonction bilan(liste)
    si longueur(liste) = 0 alors
        retourne 'pas de note'
    fin si
    moy est un nombre vaut moyenneDe(liste)
    retourne decimal(moy, 2) + ' (' + mentionDe(moy) + ')'
fin fonction

// Le jeu d'essai : un cas normal, puis chaque cas limite
vide1 est un tableau
affiche bilan([12, 18, 15])    // 15,00 (Bien)
affiche bilan([9])             // 9,00 (Refusé)
affiche bilan([10, 10])        // 10,00 (Admis)
affiche bilan(vide1)           // pas de note

Les outils de CoddyRun qui aident à raisonner

  • L’exécution pas à pas (bouton dédié, ou F7 et F8) déroule le programme ligne par ligne en montrant la valeur de chaque variable. C’est le tableau de déroulement de la partie 8, fait pour vous.
  • Analyser la syntaxe repère les blocs mal fermés et les fautes de frappe sans rien exécuter.
  • Formater le code (Ctrl+Maj+F) remet l’indentation d’aplomb. Un code mal indenté cache les erreurs de structure.
  • Les commentaires avec // : écrivez-y l’invariant de vos boucles. Votre vous-même de la semaine prochaine vous remerciera.

Continuer sur le site

Une dernière chose. Le plus difficile n’est pas d’apprendre la syntaxe d’un langage : cela prend quelques heures. C’est d’apprendre à découper un problème, à écrire une boucle qui a un sens et à se méfier de son propre code. Ça, c’est l’affaire de toute une vie de programmeur, et c’est aussi ce qui rend le métier intéressant.