Briques de base

Le but d'un pseudo-code est d'être lu et compris par un humain. Il se doit d'être sans ambiguïté sans être lourd. Il est constitué d'instructions dont le but est soit :

Vous trouverez autant de types de pseudo-code différents que d'informaticiens. Je vous donne ici "mon" pseudo-code. Son but est d'être assez explicite pour décrire sans ambiguïté les algorithmes de ce cours. Ne soyez donc pas étonnés si en lisant d'autres pseudo-codes ils ne suivent pas mes notations : ayez l'esprit ouvert.

Nous allons montrer ici une version de pseudo-code sans accès direct à la mémoire (ie. sans pointeur). Ce modèle est tout à fait fonctionnel et est utilisé dans les langages de programmation comme python ou javascript par exemple. Nous ajouterons l'accès direct à la mémoire (bien) plus tard dans ce cours. Ce modèle est plus simple à comprendre et à implémenter que le modèle avec pointeur et ne change rien algorithmiquement. Il va cependant nécessiter des opérations élémentaires supplémentaires (on appelle ceci des overheads) pour trouver la valeur de la variable en mémoire par rapport à l'utilisation directe de pointeurs.

Commentaires

Comme en python, on considérera que tout ce qui suit le caractère # est considéré comme un commentaire dont le but est d'éclairer le lecteur.

# un commentaire

Objets et opérations

Commençons par décrire les objets que l'on peut manipuler en pseudo-code et les moyens d'y accéder.

Objets

Les objets sont ce que l'on manipule en algorithmie. On a vu qu'a priori on peut n'utiliser que des suites finies de 0 et de 1 (ou de façon équivalente des entiers), mais cela nuirait grandement à la lisibilité. En pseudo code on se permet d'utiliser d'autres formes d'objets appartenant à 6 types fondamentaux.

Objets basiques

Les objets que nous aurons directement à notre disposition sans avoir besoin de les définir sont appelés objets basiques et correspondent aux six types suivants :

Définition

Un objet basique peut être d'un des cinq types suivants :

  • le type bool (booléen) qui contient deux objets : vrai et faux
  • le type bit qui contient les 2 entiers : 0 et 1
  • le type int (entier) qui contient tous les entiers relatifs
  • le type float (réel) qui contient un ensemble dénombrable d'approximations de réels (un nombre fini de décimales)
  • le type char (caractère) qui contient l'ensemble des glyphes UNICODE : "a", "b", ...

Un type peut être vu comme un ensemble d'objets : le type entier contient tous les entiers possibles.

Chaîne de caractères

Le type str (chaîne de caractères) représente contient l'ensemble des mots dont l'alphabet est l'ensemble des caractères. Ce n'est pas un type basique en tant que tel mais est tellement utilisé qu'il fait parti des types toujours présent.

Le vide

En algorithmie on a également coutume de se doter d'un élément vide ∅ (nommé None en python, null en javascript ou encore void en C) qui sera à la fois considéré comme un type ou un objet :

Définition
  • le type vide ∅ ne contient aucun objet. On l'utilise pour des fonctions ne rendant aucun objet par exemple
  • l'objet vide ∅ est de tous les types (le ∅ int, bit, ...). Utilisé pour simuler un souci ou un cas particulier : une fonction division pouvant rendre soit un réel soit le vide si on divise par 0 par exemple.

Taille et stockage des objets

La plupart des objets basiques sont de taille fixe :

D'autres n'ont pas de limite fixée :

Les objets que l'on manipule doivent pouvoir être conservés pour que l'on puisse les réutiliser tout au long du programme. Cet espace de stockage, que l'on nomme une mémoire, est identifié, d'un point de vue algorithmique, à une gigantesque suite de cases adjacentes à laquelle l'algorithme peut accéder en 1 instruction et pouvant contenir un objet basique. En algorithmie, on ne se préoccupe pas vraiment de ce qu'est la mémoire : cela peut être celle de l'informaticien lecteur ou sur un ordinateur : peu importe.

À retenir

On considérera toujours que l'on possède assez de mémoire pour pouvoir exécuter nos programmes.

Implémentation

En réalité, les objets sont stockés dans une partie de la mémoire de l'ordinateur nommée tas. Chaque case du tas peut contenir 1 byte qui correspond à 8 bits.

tas

Notez que le tas peut contenir des "trous", c'est-à-dire des endroits sans objets. Enfin, dans cette implémentation réelle un entier sera stocké sur plusieurs bytes et donc plusieurs cases mémoire.

On verra d'autres modèles de pseudo-code plus tard qui gèrent la mémoire de façon plus réaliste mais pour celui que nous présentons maintenant on considérera que :

À retenir
  • la taille en mémoire des objets de type basique (bool (booléen), bit, int, float et char) est de 1
  • la taille en mémoire d'une chaîne de caractère est le nombre de caractères qui la compose.

Opérations

Les opérations que peuvent effectuer les pseudo-codes sont liées aux objets. On doit pouvoir :

Créer des objets

La seule façon de créer un objet à partir de rien est d'écrire sa valeur. Par exemple :

42

Crée un objet entier de valeur 42.

On peut aussi demander à l'utilisateur de créer un objet d'un type donné (on suppose que l'utilisateur ne se trompe pas) :

donne int

Opérations

Définition

Une opération est une fonction dont l'espace de départ est un produit cartésien de types et l'espace d'arrivée un type donné. Elle prend en paramètre des objets (de l'espace de départ) et produit un objet du type de l'espace d'arrivée.

Les opérations correspondent au second moyen de créer des objets. Par exemple le booléen Vrai est créé comme résultat de l'opération 40 > 2. Les seules opérations définies par défaut dans tout pseudo-code sont peu nombreuses :

40 + 2  # rendra l'objet entier 42

Toutes les autres opérations devront être définies soit dans le pseudo-code (avec des fonctions, comme on va le voir) soit dans un texte avant celui-ci.

Affichage

Enfin, la dernière opération autorisée pour les objets est l'affichage :

affiche 42

L'affichage est destiné, comme le commentaire, au lecteur du pseudo-code. Son but est de lui montrer des résultats intermédiaires intéressants lors de l'exécution du pseudo-code. Ce qui est affiché sort du contrôle du pseudo-code : dans l'exemple précédent l'entier 42 est affiché, le pseudo-code n'en a pas conscience.

On peut afficher plusieurs objets à la suite, par exemple :

affiche 40 " plus " 2 " égal " 42

Qui va afficher à la suite les cinq objets : 40, " plus ", 2, " égal " et 42

Variables

Une variable permet de retrouver un objet stocké en mémoire pour sa réutilisation :

Définition

Une variable est une référence contenant l'endroit en mémoire où est stocké un objet d'un type donné.

Les variables nous permettent de manipuler les objets. Conceptuellement parlant, ce sont des liens vers les objets qu'elles référencent.

En algorithmie, tout comme pour les objets on ne se préoccupe pas vraiment de l'endroit où sont stockées les variables. On sait qu'elles existent : elles sont rangées dans une partie de la mémoire différente de celle des objets (les variables ne sont pas considérées comme des objets).

Implémentation

En python, elles sont stockées dans une partie de la mémoire nommée pile et contiennent l'indice de la mémoire du tas où commence l'objet qu'elles référencent. Chaque variable est de taille fixe, 64bit pour les ordinateurs actuels. Ceci limite la taille maximale de mémoire que peut posséder un ordinateur : $2^{64}$byte = 18446744073709551616B = 16777216 terabytes.

pile

Les variables sont stockées de façon consécutive dans la pile : toutes les variables sont créées au début de l'algorithme et sont toutes supprimées en même temps à la fin de l'algorithme.

Concernant la taille en mémoire d'une variable, on considérera pour notre modèle de pseudo-code que :

À retenir

Toutes les variables ont la même taille : 1 case mémoire.

La valeur d'une variables sera :

  • soit ∅ (correspond à une variable non associée à un objet),
  • soit l'indice dans le tableau de la mémoire où commence l'objet qui lui est associé.

Définition d'une variable

Avant de pouvoir être utilisée, une variable doit être définie. La ligne suivante définit une variable de nom $a$ pouvant référencer un objet de type entier :

ref a int

La ligne précédente crée une nouvelle variable nommée a pouvant référencer des objets de type entier. Dans tout le reste du pseudo-code, on sera sûr que a contient une valeur entière.

Définition

On utilise l'opérateur de définition ref pour créer une variable. Le format général d'une définition de variable est :

ref <nom de la variable> <type d'objet qu'elle peut référencer>

Après sa définition, la valeur de la variable est ∅ (elle ne référence aucun objet).

En pseudo-code, comme le principal souci est la non-ambiguïté, une variable ne peut référencer que des objets du type spécifié lors de sa définition.

Ce mode de fonctionnement n'est pas utilisé en python où une variable peut être associée à des objets de types différents, mais est suivi par la plupart des langages compilé comme java, rust ou go.

Affectation

Une fois la variable créée, sa valeur est ∅ : elle est non affectée. Pour l'utiliser dans un programme il faut lui affecter un objet, par exemple pour notre variable a créée précédemment :

a ← 3

La ligne précédente associe à la variable a un objet entier valant 3.

Définition

On utilise l'opérateur d'affectation ← pour affecter une variable. Le format général de l'affectation d'un objet à une variable est :

<nom de la variable> ← <objet qu'elle va référencer>

On n'utilisera pas le signe = en pseudo-code pour l'affectation car cette opération n'est pas symétrique : à gauche une variable, à droite un objet. Mais comme le symbole ← n'est pas présent sur un clavier, de nombreux langages de programmation utilisent cependant = pour une affectation.

L'affectation d'une variable est une opération répétable, on peut la réaffecter à un autre objet au cours du pseudo-code :

ref a int

# ... des instructions

a ← 4

# ... des instructions

a ← 2

Après la troisième ligne, le code précédent associe la variable a à un entier valant 4 et à un entier valant 2 après la cinquième ligne. Il est important de noter que :

À retenir

Une variable n'est pas un objet, c'est un lien vers un objet qui pourra changer au cours du temps.

Après la définition d'une variable, elle n'est encore liée à aucun objet, sa valeur est vide. Il ne faut pas l'utiliser avant sa première affectation.

Utilisation

Utiliser une variable consiste à la remplacer dans l'instruction par l'objet qu'elle référence. Par exemple :

ref a int
a ← 42
affiche a

Le code précédent affiche l'objet référencé par $a$. Il est équivalent à : affiche 42.

Définition

Utiliser une variable dans un code revient à la remplacer par l'objet qu'elle référence. Ce remplacement se fait avant l'exécution de l'instruction.

Regardons ceci avec quelques exemples :

ref a int
a ← 42
ref b int
b ← a

La ligne 4, une instruction d'affectation, s'exécute de la façon suivante :

  1. on commence par retrouver l'objet à droite de l'opérateur ←. C'est une variable : on récupère son objet, un entier valant 42
  2. on affecte cet objet à la variable à gauche de l'opérateur ←, la variable b

Autre exemple :

ref a int
a ← 41
ref b int
b ← a + 1

La ligne 4, une instruction composée d'une opération puis d'une affectation, s'exécute de la façon suivante :

  1. on commence par retrouver l'objet à droite de l'opérateur ←. C'est le résultat d'une opération :
    1. pour effectuer l'opération, il faut commencer par retrouver l'objet associé à a : un entier valant 41
    2. on peut maintenant effectuer l'opération d'addition qui rend un objet de type entier valant 42
  2. on affecte cet objet à la variable à gauche de l'opérateur ←, la variable b

Attention cependant :

À retenir

On ne peut utiliser une variable qu'après l'avoir affectée. Utiliser une variable qui n'a été que définie est interdit en algorithmie.

Type d'un objet

On aura parfois besoin de connaître le type d'un objet pour définir une variable du même type. On suppose qu'il existera toujours une opération unaire spéciale nommée type qui rend le type de l'objet passé en paramètre, et que l'on pourra utiliser dans les déclarations de variables. Par exemple type 42 va rendre int. Ceci permet d'écrire :

ref a int
ref b (type a)

Tableaux

Définition

Un tableau est un conteneur nommé pouvant contenir $n$ variables de même type, où $n$ est la longueur ou la taille du tableau.

La longueur d'un tableau est déterminée à sa création et ne peut être modifiée. Chaque variable du tableau peut être accédée via son indice, qui est un entier entre $0$ et $n-1$.

Si le tableau est nommé $T$ :

  • $T.\mbox{longueur}$ sera égal à sa taille.
  • $T[i]$ est sa variable d'indice $i$ si $0 \leq i < n$
  • $T[-i]$ vaut $T[n-i]$ si $0 < i \leq n$

Le type d'un tableau est défini par le type T des objets qu'il contient précédé de crochets :

[]T

Un tableau d'entiers est ainsi de type []int.

Créons un objet tableau pouvant contenir 13 variables de type entier :

ref T []int  # création de la variable pouvant accueillir un tableau d'entiers

T ← [13]int  # création d'un tableau pouvant contenir 13 variables entières
Définition
  • le format général de la création d'une variable tableau : ref nom_du_tableau []T
  • création d'un tableau de longueur $n$ : [n]T

Une fois le tableau créé, chaque case du tableau vaut ∅. Comme pour les variables, il faut les initialiser avant de les utiliser.

Un tableau est un mix entre variables et objets : c'est un objet contenant des variables de même type.

Lors de la définition d'un tableau on crée à la fois la variable contenant le tableau et le tableau lui-même qui est une suite de $n$ variables valant initialement ∅.

La remarque ci-dessus est cruciale à comprendre car cela peut entraîner tout un tas d'effets de bord incompréhensibles sinon. Pour comprendre ce qui se passe, voyez comment python gère les tableaux :

Implémentation en python

Une façon d'implémenter cette structure en pratique est de stocker les différentes références des variables du tableau de façon contiguë en mémoire pour pouvoir y accéder rapidement pour y être lues ou modifiées :

tableau

L'implémentation en python est simple et générale mais nécessite une indirection : avant d'accéder à un objet on passe d'abord par son adresse.

On utilise un tableau comme une collection de variables, par exemple :

ref T []int
T ← [3]int

T[0] ← 41
T[1] ← 1
T[2] ← T[0] + T[1]

La seule opération spécifique à un tableau est sa création qui est un mix entre création d'objet et création de variable, puis on affecte ses variables avant de pouvoir les utiliser. Toutes les autres opérations sur les tableaux sont faites grâce aux opérations des objets basiques qui les composent. Il n'y a pas d'opérations spécifiques à ceux-ci :

À retenir

Les opérations sur les tableaux seront toujours des opérations composées d'une suite d'opérations effectuées sur les objets basiques les constituant.

Ainsi, on ne peut pas affecter un tableau à un autre. Il faut créer un nouveau tableau puis y recopier tous les éléments de l'ancien.

On considère que :

  • créer un tableau prend autant de temps que de créer une variable (1 instruction) fois sa longueur, puisqu'il faut affecter chaque case du tableau à ∅,
  • la taille en mémoire d'un tableau est proportionnelle à la taille d'une variable fois sa longueur,
  • accéder à une variable d'indice donné d'un tableau ne prend pas plus de temps que d'accéder à l'objet d'une simple variable (1 instruction) car sa position en mémoire est connue.

Les tableaux peuvent être simples comme une suite finie d'entiers ou des types plus complexes comme une matrice à 2 dimensions où chaque élément du tableau est un autre tableau (on y reviendra).

Tranches

On utilisera parfois, comme en python par exemple, des sous-tableaux via des tranches (slices en anglais) :

Tout comme pour les tableaux, on ne peut pas affecter une tranche de tableau. Il faut créer un nouveau tableau puis y recopier tous les éléments de l'ancien.

Chaînes de caractères et tableaux

Les chaînes de caractères, une fois créées, peuvent être utilisées comme un tableau composé de caractères. On peut :

On ne peut cependant pas : modifier un caractère de la chaîne. Elle est créée une fois pour toutes.

Instructions de contrôle

Si un des deux buts d'une instruction est de créer des objets à partir d'autres (ce que l'on vient de voir), le second but est de contrôler le flux d'instructions à exécuter. Ces instructions sont de deux types :

Ces formes d'instruction nécessitent de grouper les instructions en blocs.

Blocs

Lier les instructions en blocs. Ce formalisme a été popularisé par python et permet de voir du premier coup d'œil un bloc et ses instructions :

Définition

Un bloc d'instructions est une suite d'instructions décalées.

<type de bloc>:
    instruction 1
    instruction 2
    ...
    instruction n

Les blocs vont être utiles dans les très nombreuses situations où l'on veut, par exemple, répéter une suite d'instructions ou encore exécuter des instructions conditionnellement.

Exécution conditionnelle d’instructions

On veut pouvoir exécuter des instructions si une condition logique est VRAIE :

Définition
si <condition logique>:
    instruction 1

    ...
    instruction n

Par exemple :

si (age ≥ 18):
    affiche "Tu peux voter !"

Cette instruction basique suffit pour gérer toutes les exécutions de bloc conditionnelles, par exemple :

si (age ≥ 18):
    affiche "Tu peux voter !"
si (age < 18):
    affiche "Tu ne peux pas jouer au Loto."

Mais pour plus de lisibilité on admettra plein de variantes comme la variante "si-sinon" :

si <condition logique>:
    instruction 1
    ...
    instruction n
sinon:                      # <condition logique> est fausse
    instruction 1
    ...
    instruction n'

ou encore "si-sinon-si" :

si <condition logique>:
    instruction 1
    ...
    instruction n
sinon si <autre condition logique>:
    instruction 1
    ...
    instruction n'

Ou tout mix de tout ça (comme "si-sinon si-sinon"), du moment que c'est clair :

si <condition logique A>:
    # instructions A
sinon si <condition logique B>:
    # instructions B
sinon:                            # <condition logique A> et <condition logique B> sont fausses
    # instructions C

Écrire la variante si, sinon si, sinon précédente en utilisant uniquement la forme initiale si.

corrigé

si <condition logique A>:
    # instructions A
si ((NON <condition logique A>) ET (<condition logique B>)):
    # instructions B
si ((NON <condition logique A>) ET (NON <condition logique B>)):
    # instructions C
À retenir

Les différentes formes d'instruction conditionnelle peuvent toutes s'écrire uniquement avec la forme simple si.

Répétitions

On doit pouvoir répéter un bloc tant qu'une condition logique est vérifiée. Les répétitions en pseudo-code vont prendre deux formes dont l'une est un cas particulier de l'autre.

Boucles tant que

Définition
tant que <condition logique>:
    instruction 1
    ...
    instruction n

Le bloc précédent est exécuté tant que la condition logique est vraie.

Boucles pour chaque

Tout comme l'exécution conditionnelle, la forme précédente suffit pour exprimer toutes les formes de répétition possibles, mais on admet la variation suivante, très utile :

pour chaque <variable> de <objet pouvant être énuméré>:
    instruction 1
    ...
    instruction n

On exécutera alors le bloc autant de fois qu'il y a d'éléments dans l'<objet pouvant être énuméré> et à chaque itération du bloc, la variable (de type de celui des objets stockés dans le tableau) vaudra un autre élément de l'objet.

Par exemple :

pour chaque x de [1 .. 10]:
    affiche x

Si la variable de l'itération n'est pas déclarée avant la boucle, elle sera déclarée et utilisable uniquement pendant l'exécution de la boucle.

Typiquement l'<objet pouvant être énuméré> est un tableau ou une chaîne de caractères que l'on parcourt par ordre d'indice croissant.

Notez que le type de la variable utilisée pour la boucle est sous-entendu mais clairement défini puisque le type de l'objet sur lequel on itère est connu.

Les deux codes suivants sont équivalents. Le second, moins élégant, explicite le numéro de l'itération courante d'un tableau de chaînes.

Code pour chaque :

pour chaque x de T:
    affiche x

Code tant que :

ref x int

ref i int
i ← 0
tant que i < T.longueur:
    x ← T[i]
    affiche x
    i ← i + 1
À retenir

La boucle pour chaque peut s'écrire avec la boucle tant que.

Comme on va souvent faire des itérations sur des intervalles d'entiers, on utilise les notations suivantes :

On peut alors écrire, par exemple :

ref x int
x ← 0

pour chaque i de [0 .. 10000[:
    x ← i + x

Sortir d'une boucle avant la fin

On a parfois besoin de sortir d'un bloc de boucle avant la fin de sa condition. On utilise pour cela deux instructions spéciales de sortie de boucle :

Ces instructions sont utiles si elles permettent de rendre le pseudo-code plus lisible, en séparant le cas général (la condition de la boucle) des cas particuliers (traités avec un break ou un continue)

break

Par exemple le code suivant qui trouve le pgcd (plus grand commun diviseur dont on a déjà vu l'algorithme) entre deux nombres :

ref x int
ref y int

x ← donne int
y ← donne int

ref d int
d ← min(x, y)

tant que (d > 0):
    si (x mod d == 0) ET (y mod d == 0):
        break
    d ← d - 1

affiche "le pgcd entre " x " et " y " vaut " d

Qui est équivalent au code suivant, moins lisible :

ref x int
x ← donne int

ref y int
y ← donne int

ref d int
d ← min(x, y)

ref sortir bool
sortir ← Faux

tant que (sortir == Faux) ET (d > 0):
    si (x mod d == 0) ET (y mod d == 0):
        sortir ← Vrai
    sinon:
        d ← d - 1

affiche "le pgcd entre " x " et " y " vaut " d

Et qui donne en python :

x = int(input("Donne un entier : "))
y = int(input("Donne un entier : "))

d = min(x, y)

while (d > 0):
    if (x % d == 0) and (y % d == 0):
        break
    d -= 1

print("le pgcd entre", x, "et", y, "vaut", d)
continue

Par exemple le code suivant qui va afficher tous les diviseurs de x. Le continue permet d'expliciter les cas rejetés :

ref x int
x ← donne int

pour chaque d de [1 .. x]:
    si (x mod d != 0):
        continue

    affiche d " divise " x

Qui est équivalent au code suivant, un peu moins lisible :

ref x int
x ← donne int

pour chaque d de [1 .. x]:
    si (x mod d == 0):
        affiche d " divise " x

Et qui donne en python :

x = int(input("Donne un entier : "))

for d in range(1, x + 1):
    if (x % d != 0):
        continue
    
    print(d, "divise", x)
Boucles imbriquées

Les instructions break et continue n'agissent que dans le bloc dans lequel elles sont définies, elles n'affectent pas les boucles extérieures.

Par exemple :

pour chaque x de [1 .. 3]:
    pour chaque y de [0 .. 3]:
        si x * y > 2:
            break
        affiche "x = " x "; y = " y

code python

for x in range(1, 4):
    for y in range(4):
        if x * y > 2:
            break

        print("x =", x, "; y =", y)

Va afficher :

L'instruction break ne sort que de la boucle la plus imbriquée.