Vous êtes sur la page 1sur 53

Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Informatique
Table des matières
1 Structures linéaires, opérations, complexité 1
1.1 Premiers éléments sur la complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Structures linéaires en informatique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Structures linéaires en Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.4 Conversions de types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

2 Effets de bord 10

3 Terminaison d’un algorithme 11


3.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2 Terminaison d’une boucle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

4 Correction d’un algorithme 15


4.1 Généralités sur la correction d’un algorithme . . . . . . . . . . . . . . . . . . . . . . . . 15
4.2 Invariant de boucle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

5 Représentation des entiers en informatique 20


5.1 Représentation des entiers naturels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5.2 Représentation des entiers relatifs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.3 Représentation des entiers sur Python 3.x . . . . . . . . . . . . . . . . . . . . . . . . . . 25

6 Représentation des nombres réels 26


6.1 Représentation de la partie fractionnaire d’un réel . . . . . . . . . . . . . . . . . . . . . 26
6.2 Structure des nombres flottants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

7 Complexité algorithmique 30
7.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
7.2 Classification de la complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
7.3 Exemples de calculs de complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

8 Recursivité 34
8.1 Généralités sur la récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
8.2 Différents types de récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
8.3 Exemple emblématique : Les tours de Hanoï . . . . . . . . . . . . . . . . . . . . . . . . . 38

9 Graphes 41
9.1 Introduction et vocabulaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
9.2 Matrice d’adjacence et liste d’adjacence d’un graphe . . . . . . . . . . . . . . . . . . . . 43
9.3 Parcours d’un graphe et plus court chemin . . . . . . . . . . . . . . . . . . . . . . . . . . 46
9.4 Deux applications au parcours d’un graphe . . . . . . . . . . . . . . . . . . . . . . . . . 50
9.5 Parcours d’un graphe pondéré et plus court chemin . . . . . . . . . . . . . . . . . . . . . 51

Vidal AGNIEL
Aidé par Jean-Christophe FAUVEAU
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Structures linéaires, opérations, complexité


Table des matières du chapitre
1.1 Premiers éléments sur la complexité . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Structures linéaires en informatique . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Structures linéaires en Python . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3.1 Les listes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3.2 Les n-uplets (tuple) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.3 Les Chaîne de Caractères . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.4 Les dictionnaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.5 Les tableaux (array) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3.6 Opérations communes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.7 Piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.8 Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.4 Conversions de types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

1.1 Premiers éléments sur la complexité


Pour une opération sur des données, la complexité doit permettre d’évaluer les ressources nécessaires à
son exécution.
On veut pouvoir estimer le nombre d’opérations à prévoir, la quantité de mémoire requise, le temps
d’exécution.
Parmi les opérations usuelles sur une liste, les plus élémentaires sont :
1. l’ajout d’un élément en fin de liste ;
2. la suppression d’un élément de la liste ;
3. l’ajout d’un élément en un point précis d’une liste.
Définition 1. Soient f et g deux fonctions positives définies sur N.

On note f (n) = O(g(n)) s’il existe α > 0 tel que f (n) ⩽ αg(n) pour tout n ≥ 0.
On dit alors que f est un "grand O" de g, ou que f est dominée par g.
Par exemple, on a 5n2 + 200.n = O(n2 ).

Temps d’exécution selon la complexité


Prenons un système informatique qui réalise 109 opérations par seconde.
On récapitule dans ce tableau le temps nécessaire pour que ce système exécute un algrithme de com-
plexité f (n) (en abscisse), sur des données de taille n (en ordonnée).

On considère que les complexités “raisonnables” pour un algorithme sont les suivantes :
1. complexité logarithmique en O(log(n)) ;
2. complexité linéaire en O(n) ;
3. complexité semi-linéaire en O(n log(n)) ;
4. complexité quadratique en O(n2 )
Au delà, les temps de calcul aumetent trop vite en fonction de n.

1
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

1.2 Structures linéaires en informatique


Généralités sur les structures de données
Pour qu’un système informatique traite une information, il faut que cette information ait une certaine
structure compréhensible par le système. Cette structure doit au moins décrire la manière d’attribuer
de l’information à l’intérieur, la place en mémoire requise pour cela, et la façon d’accéder aux données
à l’intérieur.
Définition 2.
1. Une structure de données est dite statique lorsque la quantité de mémoire allouée est fixée au
moment de la création de celle-ci.
2. Lorsque l’attribution de la mémoire peut varier au cours du temps, la structure de données est
dite dynamique.
3. Lorsque le contenu d’une structure de données est modifiable, la structure est dite mutable.
Par exemple, la classe list de Python est dynamique et mutable.
Nous ne nous intéresserons dans ce cours qu’aux structures dites linéaires, comme les listes, tableaux,
dictionnaires, . . .. D’autres structures comme les arbres ou les base de données ne sont pas des structures
linéaires.

Les tableaux
Un tableau (array) est une suite de variables de même type associées à des emplacements consécutifs
de la mémoire :

1. Un tableau est une structure de donnée statique (sa taille ne change pas).
2. Un tableau est une structure de donnée mutable (on peut changer son contenu).
3. Les éléments du tableau sont accessibles en lecture et en écriture en temps constant O(1).

Les listes
Une liste contient un nombre fini de données, qui sont rangées dans un certain ordre. Ces données ne
sont pas stockées directement dans la liste, elles se trouvent à divers emplacements dans la mémoire du
système. Pour chaque case de la liste, la liste associe un pointeur indiquant la localisation (l’adresse)
dans la mémoire de la donnée concernée.

1. Une liste est une structure de donnée dynamique et mutable (on peut modifier sa taille et son
contenu).
2. Le n-ième élément d’une liste est accessible avec une complexité en O(n).

D’autres types de listes


Il existe d’autres types de listes où les pointeurs sont organisés de façon différente pour répondre à
certaines conditions (accéder facilement à la dernière valeur, la dernière case pointe vers la première,
. . .).

Un certain nombre de structures linéaires s’écrit et s’utilise en Python de façon très similaire à une liste
(piles et files par exemple). La machine fait par contre très bien la différence, grâce au type de chaque
objet. Il faut ainsi faire attention dans ses algorithmes à choisir le bon type d’objet selon les opérations
que l’on veut faire/informations que l’on veut stocker.

2
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Les n-uplets (tuple)


Définition 3 (Tuple).
Un tuple, ou n-uplet, est un objet qui contient n éléments de types éventuellement différents.
On ne peut pas modifier sa taille ni la valeur de ses éléments : il est statique, et non-mutable.
Un tuple est une structure similaire à celle des tableaux, où l’accès à chaque élément est très rapide (en
O(1)). On peut y stocker des objets de types différents, mais on ne peut par contre pas les modifier.

Les dictionnaires
Un dictionnaire est très semblable à une liste. Il stocke un nombre fini de données, qui sont rangées.
Pour chaque donnée il associe un pointeur indiquant la localisation (l’adresse) dans la mémoire de la
donnée.
Cependant, au lieu de référencer chacune de ses données avec un nombre entre 0 et n − 1, un diction-
naire utilise des clés. Les clés sont des mots qui servent à indexer les éléments, comme les mots d’un
dictionnaire.

Les piles
Il s’agit d’une structure de données qui repose sur le principe :

Dernier Entré, Premier Sorti : Last In, First Out (ou LIFO)

Un peu comme une pile d’assiettes, c’est la dernière assiette de la pile qui est utilisée en premier.

Les files
Le principe de cette structure de donnée abstraite est le suivant :

Premier Entré, Premier Sorti : Fist In, First Out (ou FIFO)

Un peu comme une file d’attente devant le bus, c’est la première personne de la file qui monte en premier.
Les piles et les files sont des sortes de listes pour lesquelles certaines opérations spécifiques sont optimisées
(suppression du premier/dernier élément).

1.3 Structures linéaires en Python


1.3.1 Les listes
La liste est la structure de base "complexe" de Python :
• Une liste est un objet Python dynamique et mutable.
Son type est list. Une liste contient des objets de types divers (entiers, caractères, listes,. . .).
Exemple : L=[2,3.3,"a",[1]].
• Pour accéder à un élément de la liste L, on utilise la syntaxe L[k].

3
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

• Le premier élément d’une liste est le numéro 0, et le n-ème le numéro n − 1. Le premier terme
d’une liste L de n élément est appelé par L[0], et le dernier par L[n-1].
• La commande L1 = L[i:j] crée une nouvelle liste (appelée "tranche") composée des éléments
[L[i],...,L[j-1]] (de i à j − 1). C’est le slicing.
• Ajout d’éléments : L.append(x) (ajoute x à la fin de la liste), L.insert(i,x) (ajoute x à la
position i).
• Concaténation de deux listes : L.extend(L1), L + L1.
Il y a une différence subtile entre cex deux commandes.
• Indexation : L.index(’a’) renvoie l’indice de la première apparition de ’a’, sinon une exception
est déclenchée.
• Pour tester l’existence d’un élément : x in L (renvoie True ou False).
• Supprimer un élément : L.pop() retire le dernier élément de la liste.
L.remove(x) retire la première occurence de x rencontrée dans L.

Représentation d’une liste sur Python


Une liste L est créée dans Python sous forme d’un tableau (cases mémoires contiguës à accès direct).

Puisqu’il s’agit d’un tableau, chaque modification de valeur s’exécute en O(1).

Les espaces libres sont alloués au fur et à mesure que la liste s’agrandit, en O(1) tant qu’il reste de
l’espace.

. . . S’il n’y a plus d’espace libre, la liste est redimensionnée en lui allouant un espace plus grand (selon
une formule explicite).

Un nouveau pointeur est ensuite créé et les pointeurs existants modifiés, ce qui est réalisé en O(n).

4
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

La suppression en queue de liste par la méthode pop() est réalisée en O(1).

La suppression d’un élément quelconque de la liste est réalisée en O(n) (il faut modifier les pointeurs).

Finalement, lorsque la liste devient trop petite on libère une partie de l’espace alloué.

Pour résumer ce qui précède, on peut établir les coûts suivants :

La classe list de Python tente de tirer le meilleur parti de la mémoire en utilisant des tableaux et en
modifiant la taille des tableaux lorsque cela devient nécessaire.

Fausse copie d’une liste


Une liste fonctionne avec des pointeurs. Les valeurs de la liste sont stockées à d’autres emplacements
dans la mémoire.
Si vous écrivez liste1 = [1,2,3]; liste2 = liste1, les listes liste1 et liste2 utiliseront les mêmes
valeurs dans la mémoire.
Autrement dit, si vous changez une valeur de liste2, par exemple liste1[2] = 4, cela changera aussi
la valeur correspondante pour liste1.
Quand vous voulez faire une copie d’une liste, vous voulez au contraire avoir deux listes totalement
indépendantes.
Pour réaliser une vraie copie de liste, on peut utiliser le slicing : liste3 = liste1[:]

5
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Exercice 1. Ecrire une fonction total_index prenant en entrée une liste L, un élément x, et renvoyant
la liste (éventuellement vide) des positions de x dans L.

1.3.2 Les n-uplets (tuple)


Sur Python, un tuple s’écrit comme une liste, à la différence que l’on utilise des parenthèses au lieu de
crochets (t=(1,3,"chat") définit un tuple).
Une fois créé, un tuple ne peut en aucune manière être modifié. Son type est tuple.
Exemple 4.
> > > t = (3, "b", "tuple", "z", "exemple")
> > > t[0]; t[4]
3
’exemple’

Remarque 5.
• Les éléments d’un tuple ont un ordre défini, tout comme ceux d’une liste.
• Le slicing fonctionne aussi. Lorsque vous découpez un tuple, vous obtenez un nouveau tuple.
• Vous ne pouvez pas modifier un élément d’un tuple.
Tester : a=1; t=(a,1,a); t puis a=2; t;
• Vous ne pouvez pas ajouter d’élément à un tuple. Les tuples n’ont pas de méthode append ou
extend.
• Vous ne pouvez pas enlever d’éléments d’un tuple. Ils n’ont pas de méthode remove ou pop.
• Vous pouvez toutefois utiliser in pour vérifier l’existence d’un élément dans un tuple.

Remarque 6.
• Les tuple sont plus rapides d’accès que les listes, car leurs données sont statiques.
• Votre code est plus sûr si vous "protégez en écriture" les données qui n’ont pas besoin d’être
modifiées.
Tester avec la suite d’instructions :
> > > a=[1,2]; t=(2,"toto",a); t; a[1]=0; t
• On peut obtenir un tuple t à partir d’une liste L avec t=tuple(L). On peut obtenir une liste L
à partir d’un tuple t avec L=list(t).

1.3.3 Les Chaîne de Caractères


Définition 7 (Chaîne de caractères).
Une chaîne de caractères (string) est une suite de caractères alphanumériques.
On la définit en Python avec des guillemets ou des apostrophes ”.
Leur type est str.

Exemple 8.
> > > C="Chaine f(x)=25y."
> > > D="avec des ’ ’, ca marche !"
> > > print(D)
avec des ’ ’, ca marche !
Les chaînes de caractères servent principalement pour l’affichage de texte (résultat, boîte de dialogue,
sélection de choix,. . .).
Elles peuvent aussi gérer les caractères spéciaux (saut de ligne \n, indentation,. . .), qui prennent effet
avec la fonction print. Il faut pour cela utiliser des triple guillements
Exemple 9.
> > > Ch="""C’est facile les chaînes de caractère\nil suffit de bien choisir la méthode...""".
Comparer > > > Ch et > > > print(Ch)

1.3.4 Les dictionnaires


En Python, la structure d’un dictionnaire est D={c1: v1,c2: v2,...,cn: vn} où c1, . . . , cn sont les
clés et v1, . . . , vn les données.
Les clés sont en général des chaînes de caractères, et les données sont du type que l’on veut (entier,

6
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

chaîne de caractère, liste,. . .).


Le type d’un dictionnaire est dict.
Le dictionnaire vide s’écrit {}.
Exemple 10.
D={"Maths": "matrice", "Physique": "statique", "S.I.": "schema-bloc", "chat": [2,2,2]}
type(D)
D["S.I."]
D["chat"]
len(D)
remove D["Maths"]
print(D)

1.3.5 Les tableaux (array)


Un tableau Python, ou array, est une liste indexée de données numériques uniquement (entiers, flot-
tants, complexes).
Les tableaux sont accessibles à partir de la librairie numpy (import numpy as np).
Exemple 11.
> > > import numpy as np
> > > T = np.array([1, 2.7182818, 3.14, 4, 5, 1j, 1+3j])
> > > type(T)
<type ’numpy.ndarray’>
Exemple 12 (Tableau de valeurs uniformément espacées).
> > > np.linspace(0, 1, 6)
array([0. , 0.2, 0.4, 0.6, 0.8, 1.])
Tout comme les listes, les éléments d’un tableau sont indexés à partir de 0, jusqu’à n − 1. Pour obtenir
l’élément d’incide k, on utilise T[k].
Les opérations sur les tableaux sont les opérations algébriques (à identifier avec des vecteurs ou des
matrices).
Exemple 13.
>>> a
np.array([ 1., 2., 3.])
>>> b
np.array([ 2., 3., 4.])
>>> a + b # Addition éléments par éléments
np.array([ 3., 5., 7.])
>>> a * b # Multiplication éléments par éléments
np.array([ 2., 6., 12.])
Remarque 14 (Comparaison listes-tableaux).
1. Les listes sont plus souples : tous les types sont acceptés.
Aussi, le type list est de base dans Python alors que le type ndarray est importé.
2. L’accès aux éléments d’un tableau est un accès direct (O(1)). Pour une liste, il est nécessaire de
parcourir la liste, ce qui peut être long (O(n)).
3. Lorsque vous avez à mener des calculs sur une structure algébrique (vecteur, matrice), utilisez
les tableaux numpy.
Vous pourrez facilement utiliser des opérations usuelles +, -, *, /, transposition, déterminant,. . .
Et, l’exécution sera plus rapide qu’avec des listes.
Tout comme on peut faire des listes de listes, on peut faire des tableaux de tableaux. Pour manipuler
des matrices (et notamment des opérations algébriques), la structure de tableau est plus adaptée.
Exemple 15.
> > > import numpy as np
> > > m = np.array([[1,2],[3,4]])
>>> m
array([[1, 2], [3, 4]])
> > > m.T
array([[1, 3], [2, 4]])

7
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

1.3.6 Opérations communes


Pour tous les objets prenant la forme de suites (liste, tuple, chaîne de caractères,. . .), on dispose de
certaines opérations communes :
• s+t concaténation des deux séquences
• s*n création d’une séquence où s est copiée n fois
• s[i] élément en position i
• s[i:j] tranche entre i et j − 1
• s[i:j:k] tranche entre i et j − 1, avec un pas de k
• len(s) nombre d’éléments de s
• max(s), min(s) maximum et minimum de s.
• s in t test d’appartenance
• s not in t test de non-appartenance
• s.index(x,i,j) position de x dans s entre i et j.
Variantes : s.index(x) (dans s tout entier), s.index(x,i) (après i).
• s.count(x) nombre d’occurences de x dans s.
Les opérations de concaténation/tranches crééent à chaque fois un nouvel objet, ce n’est pas une modi-
fication de l’objet actuel (pas comme L.pop() ou L.append(x)).
Ces fonctions existent en Python, mais nous aurons l’occasion d’en recoder certaines en tant qu’exercices
d’algorithmique.

1.3.7 Piles
Sur Python, il n’y a pas besoin de définir un nouvel objet pour manipuler des piles : les listes Python
(list) possèdent déjà toutes les opérations fondamentales nécessaires : l.append(x) (ajouter un nouvel
élément) et l.pop() (supprimer le dernier élément).
Le temps de calcul de ces deux opérations est déjà très optimisé, ce qui est ce que l’on recherche quand
on veut manipuler une pile.
Exemple 16.
L=[1,3,5]
type(L)
L.pop()
L.append("chat")
L

1.3.8 Files
Sur Python, on peut utiliser une liste comme une file, avec l.append(x) (ajouter un nouvel élément)
et l.remove(L[0]) (retirer le premier élément).
Mais la fonction remove nécessite trop de temps de calcul (sa complexité est en O(n)). Ce n’est donc
pas efficace.

Les files (queue) sur Python sont utilisables avec le module queue. (import queue as *).
Leurs opérations sont L.append(x) (ajouter un nouvel élément) et L.popleft() (retirer le premier
élément) sont optimisées (complexité en O(1)).
Cependant, on prefère importer les files à double-extrémité (double-ended queues), qui sont encore
plus optimisées.
Cet objet peut servir à la fois de pile et de file (from collections import deque) avec les opérations
L.pop(), L.popleft(), L.append(), L.appendleft(). L’ajout et le retrait d’un élément au début
ou à la fin d’un deque ont une complexité en O(1).
Exemple 17.
from collections import deque
L=deque([1,2,"mer"])
type(L)
L.popleft()
L.appendleft(-2)
L.pop()
L.append("chat")
L

8
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

1.4 Conversions de types


Certaines structures de données sont très similaires les unes des autres. Leurs différences reposent beau-
coup dans la façon dont les données sont stockées. Python est alors en mesure de convertir un objet
d’un certain type en un objet d’un autre type (str, list, int, float, tuple, array,. . .).
Il suffit en général d’une syntaxe sous la forme type(données).

Pour les listes/piles/files/tuple, la conversion fait sens. Pour les nombres, les nombres entiers peuvent
être convertis en nombre flottants (réels). La conversion flottant → entier est possible, mais pas toujours
exacte.
Une chaîne de caractères qui ne contient que des chiffres peut être convertie en nombres (sinon non).

Exemple 18.
> > > chaine=’123’
> > > int(chaine)+3
126
> > > float(chaine)+3
126.0
> > > str(23+12)
’35’
Exemple 19.
>>> K=([1,2],"chat",5)
>>> L=list(K)
>>> L
[[1, 2], ’chat’, 5]

9
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Effets de bord
Définition 20.
Une opération informatique est à effet de bord si elle modifie le contenu des données en entrée.
Un algorithme est à effet de bord si elle modifie le contenu des données en entrée.
En mathématiques, avec une fonction f et un nombre x, on conserve toujours une distinction entre la
valeur en entrée x et son image f (x). On garde alors l’information de ces deux éléments.
En informatique, si une opération f a une effet de bord sur une variable x, lorsque l’on exécute f la
variable x change. La machine ne conservera pas en mémoire l’ancienne valeur de x, il n’y a pas de
retour en arrière possible (sauf si on anticipe cela en amont).
Les effets de bord sont intéressants en informatique quant on cherche à modifier x pour l’adapter à une
situation donnée.
Si l’on veut au contraire conserver la valeur initiale de x dans un algorithme, il faut faire attention à ne
pas introduire d’effet de bord.
De par leur structure, les listes, tableaux, piles, files, . . . sont souvent sensibles aux effets de bord. Cela
est moins le cas pour les nombres (entiers, flottants).
Exemple 21.
• Pour une liste L, l’opération L.pop() (supprimer le dernier élément) est à effet de bord. La
machine modifie la liste L et renvoie ce résultat.
Une fois le dernier élément supprimé, il n’y a pas d’opération pour revenir en arrière.
• L’opération L.append(x) est aussi à effet de bord.
Comme on ajoute une nouvelle information, il est possible de revenir en arrière avec L.pop().
• Une opération de slicing L’=L[i:j] (tranche entre i et j − 1) crée une nouvelle liste. Elle n’est
pas à effet de bord.
• Pour un algorithme de tri, on cherche à avoir un effet de bord : modifier la liste L de façon à ce
qu’elle soit plus pratique à utiliser. Il n’y a pas d’intérêt à conserver la liste non-triée de départ.
Si l’on ne veut pas avoir d’effet de bord dans un algorithme, une méthode efficace est de créer une copie
des données initiales. On peut alors modifier les copies tout en conservant les valeurs initiales.
Exemple 22. Pour créer une copie d’un nombre n en Python, il suffit de définir une nouvelle variable
n1 et d’indiquer qu’elle prend la même valeur que n.
>>> n=2
>>> n1=n
>>> n1=3
>>> n
2
Pour créer une copie d’une liste L en Python, il faut utiliser le slicing (L2=L[:]).
>>> L=[1,1,2]
>>> L1=L
>>> L2=L[:]
>>> L1[0]=3
>>> L
[3, 1, 2]
>>> L2
[1, 1, 2]

10
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Terminaison d’un algorithme


Table des matières du chapitre
3.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.1.1 Contexte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2 Terminaison d’une boucle . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

3.1 Définition
3.1.1 Contexte
Avant de tester un algorithme, sur des données éventuellement de grande taille et avec éventuellement
beaucoup d’opérations, on veut s’assurer que cet algorithme s’arrêtera en un nombre fini d’étapes.
Définition 23. La terminaison est le fait de prouver théoriquement qu’un algorithme donné s’arrêtera
toujours.
Cet élément est important car il existe des algorithmes pour lesquels on n’a toujours pas la réponse,
comme par exemple l’algorithme associé à la "convergence" dans le calcul de la suite de Syracuse.
Algorithme 1 : fonction syracuse(n)
Entrées : un entier n
Syr ← n;
tant que Syr ̸= 1 faire
si Syr est pair alors
Syr ← Syr/2
sinon
Syr ← 3 ∗ Syr + 1
fin
fin

Remarque 24. Pour les instructions informatiques, les affectations/copies/tests logiques terminent
tous. Les boucles for aussi.
Le problème de la terminaison réside dans les boucles (boucles while et appels récursifs), qui peuvent
éventuellement ne jamais se terminer.

3.2 Terminaison d’une boucle


Pour cela, nous allons utiliser le résultat suivant :
Proposition 25. Soit f une fonction à valeurs entières, positive, et strictement décroissante.
Alors la fonction f ne peut prendre qu’un nombre fini de valeurs.
Pour montrer qu’une boucle à l’intérieur d’un algorithme se terminera toujours en un nombre fini
d’étapes, on utilise une fonction de terminaison.

Définition 26. Une fonction de terminaison (ou variant de boucle) est une fonction f , qui dépend
des variables dans la boucle, à valeurs entières positives, qui décroît strictement à chaque passage dans
la boucle (ou à chaque appel récursif).
Comme cette fonction f ne peut prendre qu’un nombre fini de valeurs, la boucle ne peut pas être
exécutée autant de fois que l’on veut.
Plus précisément, si n0 est le plus grand entier atteint par la fonction f , l’image de la fonction f contient
au plus n0 + 1 valeurs, et donc la boucle ne peut pas être exécutée plus de n0 + 1 fois.
Méthode 27 (Démontrer la terminaison d’une boucle).
Pour une boucle (while ou appel récursif) donnée à l’intérieur d’un algorithme, on cherche f une fonction
de terminaison de la boucle, en fonction des quantités qui varient à chaque passage de la boucle.
Une fois que l’on a exhibé f , on montre qu’elle est positive et strictement décroissante à chaque passage

11
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

de la boucle.
On conclut ensuite que la boucle se terminera toujours en un nombre fini d’étapes. La terminaison est
vérifiée.
La fonction de terminaison dépend toujours des variables qui apparaissent dans la condition while
(. . .):, ou dans les paramètres de l’appel récursif (nous y reviendrons plus tard).
Exemple 28 (Recherche par dichotomie dans une liste triée).
Soit L une liste triée à n éléments, et a ∈ R. On veut savoir si a est dans la liste L ou non.
Algorithme 2 : fonction RechDicho(L,a)
Entrées : L[0..n − 1] une liste triée et a un nombre
Sorties : un booléen
1 g ← 0 ; d ← n − 1;
2 tant que d − g > 0 faire
3 m ← Ent((d + g)/2);
4 si L[m] < a alors
5 g ←m+1
6 sinon
7 d←m
8 fin
9 fin
10 si L[g]==a alors
11 retourner Vrai
12 sinon
13 retourner Faux
14 fin

Pour que la boucle while se termine, il faut que la condition d − g > 0 (ligne 2) soit contredite.
Étudions les variations de la quantité d − g
En fonction du test (L[m] < a), soit g devient m + 1, soit d devient m.
Notons (gn , dn , mn ) les valeurs de g et de d après n passages dans la boucle.
On a donc dn+1 − gn+1 = dn − (mn + 1) ou dn+1 − gn+1 =n m − gn .
La partie entière nous donne l’encadrement dn +g 2
n
≤ mn < dn +g2
n
+ 1.
dn −gn
Cela donne les encadrements : 2 − 1 < dn − (mn + 1) ≤ 2 et dn −g dn −gn
2
n
− 1 ≤ mn − gn < dn −g
2
n
.
dn −gn
Dans tous les cas, on a la majoration : dn+1 − gn+1 ≤ 2 .
Comme on a dn − gn > 0, on en déduit que dn+1 − gn+1 ≤ dn −g 2
n
< dn − gn .
Ainsi, on pose la fonction f (g, d) = g − d.
Cette fonction est à valeurs entières.
Cette fonction est positive lors de chaque passage dans la boucle while (elle devient négative quand la
boucle se termine).
A chaque passage dans la boucle, cette fonction est strictement décroissante.
La fonction f est donc une fonction de terminaison de cette boucle while. Cette boucle se termine donc
après un nombre fini d’exécutions.

Exercice 2. Déterminer une fonction de terminaison pour l’algorithme :


Algorithme 3 : fonction algo1(a,b)
Entrées : deux entiers a et b avec a strictement positif
Sorties : l’entier p
1 p ← 0;
2 x ← a;
3 tant que x>0 faire
4 p ← p + b;
5 x ← x − 1;
6 fin
7 retourner p

Exercice 3. Déterminer une fonction de terminaison pour l’algorithme :

12
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Algorithme 4 : fonction algo2(a,b)


Entrées : a et b deux entiers strictement positifs
Sorties : un entier u
1 u ← a;
2 v ← b;
3 tant que u <> v faire
4 si u > v alors
5 u←u−v
6 sinon
7 v ←v−u
8 fin
9 fin
10 retourner u

Exemple 29. On considère l’algorithme suivant :

def mystere(n) :
L=[]
while n >= 0 :
L.append(n % 10)
n = n//10
return L

1. Cet algorithme ne se termine pas toujours. Pourquoi ? Le rectifier.


2. Prouver que l’algorithme rectifié se termine toujours. On explicitera une fonction de terminaison
(ou variant de boucle).
3. Que fait ce programme ? Sauriez vous comment démontrer votre hypothèse ?
4. Pour un nombre entier n, compter le nombre de passages dans la boucle while. Quel est l’intérét
de ce calcul ?

Exemples de terminaison d’algorithmes récursifs


Exemple 30 (Algorithme récursif pour n!).

def factoriel(n) :
if n == 0 :
return 1
else :
return n*factoriel(n-1)

Cet algorithme utilise les propriéés 0! = 1 et n! = n(n − 1)! pour calculer n! de façon récursive.
A chaque appel récursif, le paramètre n change. Après k appels récursifs, notons nk la valeur de la
variable n. On a alors la relation nk+1 = nk − 1.
On pose la fonction f (n) = n. Il n’est pas nécessaire ici de montrer que f est une fonction de terminai-
son, on a mieux.
La suite (nk )k est une suite arithmétique de raison 1 avec n0 = n. Elle s’écrit donc nk = n − k.
Au bout de n appels récursifs, quand k = n, on a nn = n − n = 0.
Or, quand l’entier en entrée vaut 0, l’algorithme facoriel se termine.
Donc, il ne peut pas y avoir plus de n appels récursifs.
Ainsi, l’algorithme factoriel termine toujours en un nombre fini d’étapes.

Exercice 4. On considère l’algorithme de MacCarthy :

def M91(n) :
assert (type(n)==int) and (n>=0)
if n>100 :
return n-10
else :

13
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

return M91(M91(n+11))

1. Quelle est l’utilité de l’instruction assert ?


2. Que vaut M91(99) ? et M91(88) ?
3. Quelles sont les valeurs prises par cette fonction pour n ≥ 101 ?
4. Cette fonction termine-t-elle toujours ?

14
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Correction d’un algorithme


Table des matières du chapitre
4.1 Généralités sur la correction d’un algorithme . . . . . . . . . . . . . . . . 15
4.2 Invariant de boucle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

4.1 Généralités sur la correction d’un algorithme


Définition 31. Un algorithme est correct s’il fait ce pour quoi on l’a écrit. La correction d’un algo-
rithme est le fait de démontrer théoriquement que cet algorithme est correct.
Tout comme la terminaison, au lieu de tester l’algorithme dans différentes situations et sur des données
de taille éventuellement importante, on prouve que les opérations effectuées donnent le résultat que l’on
voulait.
Exemple 32.
Algorithme 5 : fonction minimum(L)
Entrées : Une liste L
Sorties : le plus petit élément de la liste L
mini ← 0;
pour i de 0 à n − 1 faire
si L[i] < mini alors
mini ← L[i]
fin
fin
retourner mini
Cet algorithme n’est pas correct.
Le fait de choisir mini → 0 initialement contraint le problème. Si la liste L ne contient que des valeurs
strictement positives, on obtiendra 0 en sortie, ce qui n’est pas le minimum de L.
Un changement d’initialisation par mini ← L[0] suffit à résoudre le problème.
Remarque 33. Le principe de la preuve de correction est de suivre l’état de la mémoire de l’ordinateur
au fur et à mesure des lignes de l’algorithme. Il faut que le résultat de l’algorithme coïncide avec le
résultat voulu pour toutes les valeurs en entrée possibles.
S’il y a des valeurs en entrée qui sont possibles mais pour lesquelles on n’obtient pas le bon résultat,
l’algorithme n’est pas correct (on a trouvé un contre-exemple).
Remarque 34. Les problèmes viennent souvent :
• des affectations ;
• des conditions (if. . . then . . . else. . .) ;
• des boucles (for et while).
La correction n’inclut pas le fait que l’algorithme ait une syntaxe correcte. Les erreurs de syntaxe sont
d’un autre domaine (et vous devez savoir écrire des algorithmes sans erreurs).
Il faut faire une étude pas à pas de l’algorithme pour détecter d’éventuelles erreurs dans les affectations
ou les conditions.

Algorithme 6 : fonction absolue(x)


Entrées : un réel x
Sorties : la valeur absolue de x
si x < 0 alors
Exemple 35. absx ← −x
sinon
absx ← x
fin
retourner absx
Montrons que cet algorithme est correct. Soit x ∈ R un réel.
Pour vérifier une condition if ... then ... else, on fait une disjonction de cas.

15
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Si x ≥ 0, on a |x| = x. Quand x ≥ 0, l’algorithme renvoie x. On a bien |x| = x.


Si x < 0, on a |x| = −x. Quand x < 0, l’algorithme renvoie −x. On a bien |x| = −x.
Cet algorithme est donc correct : Pour tout x ∈ R on a absolue(x)=|x|.
Pour montrer qu’une boucle (for, while, ou même une boucle récursive) est correcte, on utilise un
invariant de boucle.

4.2 Invariant de boucle


Définition 36. Soit P une proposition (une phrase mathématique).
On dit que la proposition P est un invariant pour la boucle "Tant Que C Faire I" si : Lorsque C
et P sont vraies, si on applique I, alors la proposition P reste vraie.
En pratique, P , I et C dépendent de paramètres (a1 , . . . , ap ) qui varient tous selon le nombre de fois k
que la boucle a été exécuté.
Pour la k-ème exécution de la boucle, on a (a1 (k − 1), . . . , ap (k − 1)) → (a1 (k), . . . , ap (k)), et P , I et C
passent des valeurs (P k−1 , I k−1 , C k−1 ) aux valeurs (P k , I k , C k ).
Prouver un énoncé mathématique qui dépend d’une variable entière positive k se fait en général d’une
seule façon : par récurrence.
Méthode 37 (Prouver la correction d’un algorithme contenant une boucle). Prouver la correction d’une
boucle (que le résultat final est correct) repose sur la recherche d’un invariant de boucle. La recherche
d’un invariant de boucle repose sur la recherche de relations de récurrence. Démontrer que la proposition
P est un invariant de boucle se fait par récurrence sur k le nombre d’exécutions de la boucle.

Exemples d’invariants
Exemple 38.
def pow(x,n):
z = 1
m = n
y = x
while m > 0:
if m % 2 == 1:
z = z * y
m = m // 2
y := y * y
return(z)
• Montrons que la proposition (xn = ym *z) est un invariant de boucle.

Soit k le nombre de passages dans la boucle, et yk , zk , mk les valeurs de y, z, m lors du k-ème pas-
sage.
Pour k = 0 (entrée de la boucle), on a y0 = x, m0 = n, z0 = 1. On obtient bien y0m0 .z0 = xn .
Soit k ≥ 0. On regarde yk , zk , mk .
2mk
m
Si mk est pair, on a zk+1 = zk , mk+1 = m2k et yk+1 = yk2 . Cela donne alors yk+1
k+1
.zk+1 = yk 2 zk =
mk
yk zk .
mk+1
Si mk est impair, on a zk+1 = zk .yk , mk+1 = mk2−1 et yk+1 = yk2 . Cela donne alors yk+1 .zk+1 =
mk −1
2
yk 2 zk .yk = ykmk zk .
mk+1
Ainsi, dans tous les cas, on a la relation de récurrence yk+1 .zk+1 = ykmk zk .
mk mk
Ainsi, la suite (yk .zk )k est constante. On a donc yk .zk = y0m0 .z0 = xn pour tout k ≥ 0.
Cela démontre que la proposition (xn = ym *z) est un invariant de boucle.

• Montrons que cet algorithme est correct.

La boucle "while m>0" se termine lorsque m ≤ 0. On a initialement m = n, qui est un entier po-
sitif. L’opération m=m//2 fourit encore un entier positif, comme quotient de division euclidienne.
Ainsi, cette boucle se termine quand m = 0. (La condition (m ≥ 0) est aussi un invariant de boucle)
A la fin de la boucle, l’invariant de boucle nous donne : xn = y m .z = y 0 .z = z.
On obtient ainsi que la valeur finale de la variable z est z = xn . Cette valeur est celle attendue.
L’algorithme pow est donc correct.

16
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Exemple 39.
Algorithme 7 : Algorithme Somme Arithmétique
Entrées : un entier n
Sorties : la somme des n premiers entiers
i ← 0;
somme ← 0;
tant que i < n faire
i ← i + 1;
somme ← somme + i;
fin
retourner somme
Dans cet algorithme, on regarde les opérations effectuées dans la boucle while. Soit k le nombre de pas-
sages dans la boucle. On peut remarquer que la variable somme (qui dépend de k) vérifie : (sommek =
1 + · · · + k).
Cette condition est un invariant de boucle. Montrons-le par récurrence sur k ≥ 0.
Initialisation : A l’entrée de la boucle on a somme0 = 0.
Hérédité : Supposons la proposition vraie pour un entier k ≥ 0. On a alors sommek+1 = sommek +ik+1
et ik+1 = ik + 1.
La suite (ik )k est une suite arithmétique de raison 1, de premier terme i0 = 0. On a donc ik = k.
On a donc sommek+1 = sommek + (k + 1) = 1 + . . . + k + (k + 1).
Cela termine la récurrence. Cela démontre que (sommek = 1 + · · · + k) est un invariant de boucle.

De plus, la boucle while se termine au bout de n étapes exactement, avec in = n et car ik = k < n
quand k < n.
Pn
Ainsi, quand en sortie de boucle, on a somme = 1 + . . . + n = k=1 k = n(n+1)
2 .
Cela démontre que cet algorithme est correct.
Exemple 40. Comment montrer rapidement que 13574 est divisible par 9 ?
Soit un nombre x écrit sous forme décimale x = an an−1 . . . a0 (10) .
Alors x est divisible par 9 ssi la somme de ses chiffres est un multiple de 9.
Exercice : Démontrer ce résultat.
On construit alors les algorithmes suivants : def mod9(n):
a=str(abs(n))
r=int(a[0])
for k in range(1,len(a)):
r=r+int(a[k])
return(abs(r))
Le premier algorithme récupère les chiffres de n sous forme de chaîne de caractères, et les ajoute.
def preuve9(n):
m=abs(n)
while m > 9:
m=mod9(m)
if m==0:
return("le nombre est divisible par 9")
else:
return("le nombre n’est pas divisible par 9")
Le second algorithme applique l’algorithme mod9 à m tant que m est strictement supérieur à 9.

Premièrement, l’algorithme mod9 se termine bien, car il ne contient qu’une boucle for.
Pk
Un invariant de la boucle de mod9 est la proposition (rk = i=0 a[i]), où k est le nombre de passages
dans la boucle.
On a en effet r0 = a0 et rk+1 = rk + ak . Ces propriétés permettent de démontrer par récurrence sur k
Pk
que (rk = i=0 a[i]) est vraie tout au long de la boucle.
Plen(a)
Lorsque l’on sort de la boucle on a k = len(a). On obtient ainsi rk = i=0 a[i], qui est la somme de
tous les chiffres de n.
L’algorithme mod9 est donc correct.

17
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Pour la boucle while de l’algorithme preuve9, une fonction de terminaison est f (m) = m. Posons k
le nombre de pasages dans la boucle. En effet, mk est toujours un entier positif car l’algorithme mod9
renvoie un entier positif. On a la relation mk+1 = mod9(mk ).
De plus, si m > 9 on a m ≥ 10.
Pour m = a0 + 10.a1 + 102 .a2 + . . . + 10r .ar , avec ar ̸= 0, on a mod9(m) = a0 + a1 + a2 + . . . + ar < m.
On obtient donc que tant que m > 9, on a mk+1 < mk . La fonction f est ainsi strictement décroissante
à chaque exécution de la boucle.
Cette fonction est bien une fonction de terminaison de la boucle. Cette boucle se termine.
Un invariant de la boucle de preuve9 est la divisibilité de mk par 9 (la proposition "9 | n ⇔ 9 | mk ").
En effet, on a 9|mod9(mk ) ⇐⇒ 9|mk .
Cela permet de montrer par récurrence sur k que 9 | n ⇔ 9 | m0 ⇔ 9 | mk .
Enfin, lorsque la boucle se termine, au bout de r étapes, on a mr < 9. Comme mr est un entier positif,
il est compris entre 0 et 8.
Le seul entier dans {0, . . . , 8} qui est divisible par 9 est 0.
On a donc que 9 | n ⇔ 9 | mr ⇔ mr = 0.
Cela démontre que l’algorithme preuve9 est correct.
Dans les faits, il faudra surtout trouver une fonction de terminaison f , un invariant de boucle P , ainsi
que les éléments (valeurs initiales, relations de récurrence) qui permettent de prouver que f est bien un
invariant de boucle et que P est bien une fonction de terminaison.
On ne vous demandera pas de rédiger des récurrences en permanence.
Il n’y a pas de technique générique de recherche d’invariant. Il faut regarder ce qu’il se passe lors des
premiers passages de la boucle pour chaque variable et il faut chercher une quantité qui reste fixe (ou
une égalité qui reste vraie) par rapport aux opérations effectuées/au résultat voulu. Ensuite, on corrige
éventuelement la proposition P pour obtenir un invariant correct qu’il sera possible de démontrer avec
une relation de récurrence.
En général, un invariant de boucle consiste en la description exacte et précise des liens entre les arguments
présents dans la boucle.

Quelques exemples de preuves d’algorithmes


Exemple 41 (Algorithme de division euclidienne).
Algorithme 8 : fonction division(X,Y )
Entrées : X et Y deux entiers positifs.
Sorties : R et Q le reste et le quotient de la division euclidienne de X par Y .
R ← X;
Q ← 0;
tant que Y ≤ R faire
R←R−Y;
Q ← Q + 1;
fin
retourner (R, Q)

L’algorithme est associé au théorème :


Théorème 42. Soient X et Y deux entiers strictement positifs, alors il existe un unique couple d’entiers
positifs (R, Q) tels que 0 ≤ R < Y et X = Q × Y + R.
Un invariant de la boucle est P = ((X = Q ∗ Y + R) et (R ≥ 0)).
Une fonction de terminaison de la boucle est f (Y, R, Q) = R − Y .
Exemple 43 (Algorithme de recherche d’un minimum).
Algorithme 9 : fonction minimum(L)
Entrées : une liste L[0..n − 1] non vide.
Sorties : variable mini contenant l’élément minimum de L
mini ← L[0];
i ← 2;
tant que i ≤ n − 1 faire
si mini > L[i] alors
mini ← L[i]
fin
i←i+1
fin
retourner mini

18
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Un invariant de boucle pour cet algorithme est {minii = min(L[0], . . . , L[i])


Exercice 5 (Dichotomie). On continue avec un algorithme de dichotomie en Python.
Ici, on recherche un zéro d’une fonction continue f , sur un intervalle [a, b]. On suppose que f (a) et f (b)
sont de signes opposés (cela revient à f (a)f (b) ≤ 0).
Les valeurs proposées par l’algorithme de base convergent vers un zéro de f .
Pour un nombre fini d’étapes, il faut se fixer une précision ϵ > 0 sur le résultat obtenu.
def dichot2(f,a,b,eps):
m = min(a,b)
M = max(a,b)
while (M-m)/2 > eps:
c = (M+m)/2
if f(M)*f(c)<0:
M = c
else:
m = c
return [(M+m)/2]
Montrer que la correction de l’algorithme.
Exemple 44 (Pgcd).
def pgcditer(a,b):
c,d = max(a,b),min(a,b)
while d != 0:
c,d = d,c%d
return c
Un invariant de boucle est : pgcd(c, d) = pgcd(a, b).
Exemple 45 (Pgcd 2).
def pgcditer(a,b):
a,b = max(a,b),min(a,b)
while b != 0:
a,b = b,a%b
return a
Dans ce deuxième exemple, les entrées a et b sont modifiées à chaque étape de la boucle while.
Ainsi, un invariant de boucle est : pgcd(a, b) = pgcd(a, b).
Dans cette écriture on n’a pas précisé l’état des variables que l’on considère (à gauche les a, b après
chaque étape de la boucle while, à droite les a, b en entrée).
Pour être plus précis il faut écrire pgcd(ak , bk ) = pgcd(a, b), où k représente le nombre de passages dans
la boucle while.
Exemple 46 (Pgcd récursif).
Pour a, b ∈ N avec a ≥ b, on pose :
def pgcdrec(a,b):
a,b = max(a,b),min(a,b)
if b == 0:
return a
else:
return(pgcdrec(b,a%b))
La correction de pgcdrec(a,b) se fait par récurrence forte sur a.
Pour tout a ∈ N on pose HRa : "Pour tout b ∈ {0, . . . , a}, pgcdrec(a,b) renvoie pgcd(a, b)".
Initialisation : Pour a = 0, pgcdrec(0,b) renvoie b, et b = pgcd(0, b).
Hérédité : Soit a > 0. Supposons HRk vraie pour tout k ∈ {0, . . . , a − 1}.
Si b = 0 on a pgcdrec(a,0)= a = pgcd(a, 0).
Si b = a on a pgcdrec(a,a)=pgcdrec(a,0)= a = pgcd(a, a).
Si 0 < b < a on a pgcdrec(a,b)=pgcdrec(b,a%b). Comme on a b < a, d’après HRb on sait que
pgcdrec(b,a%b)= pgcd(b, a%b). Et on a pgcd(b, a%b) = pgcd(b, a) = pgcd(a, b).
Cela termine la récurrence.

19
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Représentation des entiers en informatique


Table des matières du chapitre
5.1 Représentation des entiers naturels . . . . . . . . . . . . . . . . . . . . . . 20
5.1.1 La numérotation à position. Base de numération . . . . . . . . . . . . . . . . 20
5.1.2 Addition en binaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.1.3 Implantation informatique de l’addition . . . . . . . . . . . . . . . . . . . . . 22
5.2 Représentation des entiers relatifs . . . . . . . . . . . . . . . . . . . . . . . 23
5.2.1 Représentation par complément à 2 . . . . . . . . . . . . . . . . . . . . . . . 23
5.2.2 Représentation binaire d’un entier et de son opposé . . . . . . . . . . . . . . 24
5.2.3 Représentation binaire de la somme de deux entiers . . . . . . . . . . . . . . 24
5.2.4 Dépassement de capacité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5.3 Représentation des entiers sur Python 3.x . . . . . . . . . . . . . . . . . . 25

• Toutes les données que l’on stocke en informatique sont sous forme booléenne (un assemblage
d’éléments à deux états : 0 ou 1). On parle de représentation binaire.
• Ces données sont de nature multiple : nombres, instructions, textes, images, sons, mais elles sont
toujours représentées en binaire.
• Pour traiter cette information, on utilise des transistors.
Un transistor fonctionne avec une logique à deux états, grâce à un courant électrique : 0 le courant
ne passe pas, 1 le courant passe.
• Les systèmes informatiques actuels sont composés circuits intégrés rassemblant des centaines de
millions de transistors.
Pour traiter de l’information en binaire, on va souvent chercher à la découper en blocs de taille fixée.
Les 3 tailles les plus utilisées sont :
• Le bit. C’est une information qui vaut 0 ou 1 (contraction de Binary Digit).
• L’octet (byte). Un octet (ou byte) est constitué de 8 bits.
• Le mot (word).
Suivant le type de processeur, les mots peuvent avoir 16, 32, 64, 128 bits ou plus. (toujours une
puissance de 2).
Remarque 47. Attention à ne pas confondre en anglais byte et bit.
On peut facilement se tromper sur les capacités de stockage ou sur les vitesses de transfert. Par exemple
entre Mégabyte (Mégaoctet) et Mégabit (Mégabit) ou entre kilobyte/s (kilooctet/s) et kilobit/s (kilobit/s).
Un mégabyte c’est un mégaoctet, soit 1000 octets, donc 8000 bits.

5.1 Représentation des entiers naturels


Pour représenter des nombres entiers en informatique, il faut transformer leur écriture. Nous écrivons
les nombres en base 10, mais 10 n’est pas une puissance de 2.
On utilise alors une écriture en base 2 (binaire) ou en base 16 (hexadécimal).

5.1.1 La numérotation à position. Base de numération


Proposition 48 (Numération en base B). Soit B ∈ N \ {0, 1}. Soit n ∈ N∗ .
Alors il existe k ∈ N et a0 , . . . , ak ∈ {0, . . . , B}, avec ak ̸= 0, tels que

n = ak B k + ak−1 B k−1 + . . . + a1 B + a0 .

De plus, l’entier k et les nombres a0 , . . . , ak sont uniques.


On définit alors l’écriture de n en base B par :

n = (ak ak−1 . . . a1 a0 )B
| {z }
k+1 chif f res

Le membre de droite représente n dans la numérotation à position en base B. Chaque ai est un chiffre
de n dans son écriture en base B.
Pour la base 16, on prend comme chiffres ai ∈ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F }.

20
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Remarque 49. Pour la numérotation en base 2, on écrit parfois : (an . . . a0 )2 = an . . . a0 .


Par exemple : 1710 = 16 + 1 = 100012 = 10001.
Pour la base 16, on écrit parfois 0 × (an . . . a0 ) = (an . . . a0 )16 .
Par exemple : 2110 = 16 + 5 = (15)16 = 0 × 15.

Proposition 50 (Décomposition en base B). Pour décomposer un entier positif n en base B, on utilise
des divisions euclidiennes successives par B.
On divise les diviseurs, et les restes fournissent les chiffres de l’écriture en base B.
Algorithme 10 : Représentation en base B
Entrées : n et B deux entiers strictement positifs, avec B > 1
Sorties : la représentation (ai . . . a1 a0 )B
i←0
tant que n ̸= 0 faire
ai ← resteDivEucl(n, B);
n ← quoDivEucl(n, B);
i ← i + 1;
fin
retourner (ai . . . a0 )B

Exemple 51. Conversion de 25 en base 2.


25 = 2.12 + 1
12 = 2.6 + 0
6 = 2.3 + 0
3 = 2.1 + 1
1 = 2.0 + 1
Ainsi, 2510 = 110012 .
Exercice 6. Ecrire une fonction Python correspondant à cet algorithme.

La conversion retour vers la base 10 se fait grâce à la définition de l’écriture de n.


Exemple 52. Si n = (11011)2 , on a alors n = 20 + 21 + 0 + 23 + 24 = 1 + 2 + 8 + 16 = 3 + 24 = 27.
Si n = (13A)16 , on a alors n = 10.160 + 3.161 + 1.162 = 10 + 48 + 256 = 10 + 304 = 314.
Remarque 53 (Méthode de Horner). On a :

ak B k + ak−1 B k−1 + . . . a1 B + a0 = ((. . . (ak B + ak−1 )B + . . .)B + a1 )B + a0

Si l’on connaît les chiffres de n en base B, on peut retrouver la valeur de n en base 10 à l’aide de k
multiplications et d’au plus k additions.
Cela évite aussi de stocker en mémoire les valeurs de B 2 , B 3 , B 4 , . . . , B k , qu’on utilise lorsque l’on fait
un calcul en base 10 à la main.
Exercice 7. Ecrire une fonction Python correspondant à cet algorithme. Entrée : Une liste (ak , . . . , a0 )
d’entiers et B > 0. Sortie : Valeur de (ak . . . a0 )B en base 10.
Les nombres écrits en binaire sont très difficilement lisibles pour nous, car ils ont trop peu de chiffres.
L’écriture hexadécimale apporte dans certaines situations davantage de lisibilité. On cherche souvent à
représenter des quantités (couleurs RGB, pointeurs, adresses) sous forme hexadécimale.
Le fait que 16 = 24 permet une conversion assez pratique.
Remarque 54 (Conversion binaire - hexadécimal).
Pour convertir une écriture en binaire en hexadécimal, on regroupe les chiffres par paquet de 4 (ou moins
pour le dernier paquet), et on convertit chaque paquet en hexadécimal.
Par exemple, 1100110112 = 1 1001 00112 = 19B16 . (car 10012 = 9 et 10112 = 11 = B16 )
Les nombres écrits sur 4 bits en binaire correspondent aux entiers entre 0 et 15, donc aux chiffres 0 à
F en hexadécimal.
Exercice 8.
• Déterminer la représentation en base 10 de 101010102 .
• Déterminer la représentation en base 2 de 1, 3, 7, 15, 31. Quel est le prochain nombre de la
série ? Pourquoi ?
• Pour n ∈ N∗ fixé, combien d’entiers naturels peut-on représenter avec un mot de n bits ?
• Réciproquement, combien de bits faut il, au minimum, pour représenter tous les entiers dont
l’écriture décimale contient m chiffres ?

21
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

5.1.2 Addition en binaire


L’addition en binaire (ou en base B en général) est absolument identique à l’addition en base 10.
Cependant, la retenue apparaît dès que l’on atteint 2.


 0 2 + 02 = 02
1 2 + 02 = 12


 0 2 + 12 = 12
12 + 12 = 102

5.1.3 Implantation informatique de l’addition


Portes logiques
Il existe des dispositifs électroniques (transistors) réalisant les opérations suivantes :

La porte NOT La porte AND

Sortie = 1 ssi entrée = 0 Sortie = 1 ssi entrée = (1,1)

La porte OR La porte XOR

Sortie = 1 ssi au moins une entrée vaut 1 Sortie = 1 ssi une unique entrée vaut 1

La porte NAND La porte NOR

Sortie = 1 ssi au moins une entrée vaut 0 Sortie = 1 ssi les deux entrées valent 0

Additionneur
Ce montage permet d’additionner deux bits x et y, en renvoyant un bit de somme et un bit de retenue :

Additionneur 2-bits
On peut alors concevoir un motage pour additionner des nombres à 2 bits x1 x0 et y1 y0 , en donnant le
résultat sous forme d’un nombre à deux bits z1 z0 , et d’une retenue r1 .

22
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

On peut alors connecter des additionneurs en série sur le même principe de sorte à obtenir un addition-
neur entre deux nombres de n bits.
Remarque 55. On obtiendra en bout de chaîne un bit de retenue, qui influe sur le résultat.
1. Si le résultat ne reste pas dans l’intervalle d’entiers représentables (donné par le nombre de bits),
la retenue vaudra 1 et la représentation sera erronée. On parle alors d’overflow (dépassement par
excès).
Les nombres 10 = 10102 et 15 = 11112 s’écrivent avec 4 bits en binaire, mais 10 + 15 = 25 =
110012 nécessite 5 bits.
2. En revanche, si la retenue vaut 0, c’est qu’il n’y a pas de dépassement de capacité et la représen-
tation est correcte.
Exercice 9. L’opérateur xor ("ou exclusif", noté aussi ∧) compare deux nombres bit par bit.
Il renvoie 1 chaque fois que les deux bits correspondants sont différents et écrit 0 sinon.
Par exemple 4 xor 3 vaut 7 et 7 xor 2 vaut 5.
Calculer 10 xor 15 et 2A16 xor 4B16 .
Que valent a xor (b xor b) et (a xor b) xor b ?

5.2 Représentation des entiers relatifs


5.2.1 Représentation par complément à 2
Avec n bits on représente 2n nombres. Si l’on veut représenter des entiers positifs et des négatifs, il faut
couper la poire en deux. Voici la méthode standard.
Définition 56 (Complément à 2).
• On représente les entiers compris entre −2n−1 et 2n−1 − 1.
• Si 0 ⩽ x ⩽ 2n−1 − 1, x est représenté par sa représentation binaire sur n bits.
• Si −2n−1 ⩽ x < 0, x est représenté par la représentation binaire de x + 2n sur n bits.
Exemples : pour n = 16, on peut coder les entiers entre −32768 = −215 et 32767 = 215 − 1.

entier représentation
0 0000 0000 0000 0000
1 0000 0000 0000 0001
62 0000 0000 0011 1110
32767 0111 1111 1111 1111
-1 1111 1111 1111 1111
-62 1111 1111 1100 0010
-32768 1000 0000 0000 0000

Exercice 10.
1. Pour n = 8 bits, en complément à 2, écrire 0 et −128.
2. Trouver les entiers relatifs dont les représentations en complément à 2 sur n = 8 bits sont 0001
0111 et 1000 1100.

23
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Remarque 57. Dans l’écriture en complément à 2, certains nombres ont des écritures facilement
identifiables, peu importe le nombre n de bits :
• 0 = (0000 . . . 0000)2
• 1 = (0000 . . . 0001)2
• −1 = (1111 . . . 1111)2
• 2n−1 − 1 = (0111 . . . 1111)2
• −2n−1 = (1000 . . . 0000)2
Et, les nombres négatifs sont ceux dont le bit le plus à gauche vaut 1. Ce bit est appelé le bit de signe.
Ainsi, l’entier signé (1111 0100)2 (sur 8 bits) est négatif.

5.2.2 Représentation binaire d’un entier et de son opposé


Proposition 58. Soit n ∈ N∗ .
• Soit 0 ⩽ x ⩽ 2n−1 − 1. Si l’on connaît p la représentation en binaire de x, alors −x est représenté
en binaire par p′ = 2n − p.
• En pratique, on utilise la relation p′ = p̄ + 1, où p̄ est le nombre binaire symétrique de p (on
échange tous les bits de p, les 1 deviennent des 0 et vice-versa (bit flip)).
• Cette relation est aussi vraie dans l’autre sens : p = p̄′ + 1.
Exemple 59. On a 11 = (0000 1011)2 . Le symétrique de (0000 1011)2 est (1111 0100)2 .
On ajoute 1 : (1111 0100)2 + (1)2 = (1111 0101)2 .
Donc, en complément à 2 sur 8 bits, −11 s’écrit (1111 0101)2 .
Vérification : 28 − 11 = 256 − 11 = 245, et 245 = 128 + 64 + 32 + 12 + 4 + 1 = (1111 0101)2 .
Dans l’autre sens, on a (1111 0101)2 + 12 = (0000 1010)2 + 12 = (0000 1011)2 = 11.
Exemple 60. L’exemple de référence est −1.
En écriture binaire sur n bits, on a a 1 = (0000 . . . 0001)2 . Le symétrique de (0000 . . . 0001)2 est
(1111 . . . 1110)2 . Si l’on ajoute (1)2 , on obtient (1111 . . . 1111)2 .
On retrouve bien la valeur donnée précédemment.

5.2.3 Représentation binaire de la somme de deux entiers


Proposition 61. Avec l’écriture de complément à deux, pour deux entiers relatifs x, y représentés en
binaire par p et q, leur somme x + y est représentée par p + q.
Sauf si x + y sort de l’ensemble {−2n−1 , . . . , 2n−1 − 1} (dépassement).
Exemple 62. Prenons n = 4 bits, en complément à 2.
Pour x = 4 et y = 3, on a 01002 + 00112 = 01112 = 7.
Pour x = 7 et y = −3, on a 01112 + 11012 = 01002 = 4.
Pour x = −2 et y = −3, on a 11102 + 11012 = 10112 = −5.
Pour x = 7 et y = 1, on a 01112 + 00012 = 10002 = −8. (problème de dépassement)

5.2.4 Dépassement de capacité


Le résultat de l’addition de deux nombres codés sur n bits n’est pas toujours possible car il peut nécessiter
un bit supplémentaire.

Exemple 63. Avec 8 bits, calculons 247(10) + 53(10) :

247(10) + 53(10) = 300(10) = 1 00101100


| {z }
octets retenus

Le résultat retenu est 0010111002 = 44(10) au lieu de 300(10) .


On parle alors de dépassement de capacité (overflow en anglais). Selon les machines et les logiciels, ce
phénomène est traité de diverses façons (message d’erreur, perte d’un bit,. . .).

Le résultat obtenu n’est pas n’importe quel nombre : il diffère du résultat par un multiple de 2n (on dit
qu’il est congru au résultat modulo 2n ).
Dans l’exemple, on a 310 = 44 + 256 = 44 + 28 , donc on a bien 44 ≡ 310[256].
Le dépassement n’est ainsi pas un problème si l’on veut étudier des résultats à un multiple de 2n près
(ou des restes de division euclidienne par 2n ). Ce domaine s’appelle le calcul modulaire.

24
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

5.3 Représentation des entiers sur Python 3.x


La représentation en complément à 2 est un cas "simple". Les représentations d’entiers en pratique sont
un peu plus élaborées pour apporter d’autres avantages.
Python, depuis les versions 3.x, a un mécanisme de gestion exact de l’arithmétique. La limite de la
taille d’un nombre à gérer est la mémoire mise à disposition de Python par la machine. Les problèmes
liés au dépassement de capacité mémoire n’existent (presque) plus en Python 3.x.
Définition 64. Pour une machine avec des registres de 64 bits (standard actuel des PC) : Soit x un
entier relatif, et soit p la représentation binaire de |x|.
• On coupe la représentation p en paquets de 30 bits.
• On stocke chaque entier correspondant dans un tableau d’entiers naturel sur 32 bits.
• On associe à ce tableau un entier taille qui précise la longueur du tableau, et dont le signe est
le signe de x.

Exemple 65. Soit x = 123456789123345678912.


Avec la fonction bin, on convertit en binaire : x = (110 1011 0001 0100 1110 1001 1111 1001 1011 0000 0111
0101 0100 0000 0010 0100 0000)2 , qui contient 67 bits. On découpe en paquets de 30 bits :
x = (1101011 000101001110100111111001101100 000111010101000000001001000000)2 . Ainsi, sur Py-
thon, x est représenté par le tableau [122946112, 87719532, 107] auquel on associe le nombre taille =
3.
Et −x = −123456789123345678912 est représenté par le tableau [122946112, 87719532, 107] avec taille
= −3.

25
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Représentation des nombres réels


Table des matières du chapitre
6.1 Représentation de la partie fractionnaire d’un réel . . . . . . . . . . . . . 26
6.2 Structure des nombres flottants . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.2.1 La norme IEEE 754 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.2.2 Convertir un réel sous forme de nombre flottant . . . . . . . . . . . . . . . . . 28
6.2.3 Procédure de conversion flottant vers réel . . . . . . . . . . . . . . . . . . . . 28

6.1 Représentation de la partie fractionnaire d’un réel


On sait représenter un nombre entier en binaire. Pour tous les nombres non-entiers, une difficulté
s’ajoute : sa partie fractionnaire.
La partie fractionnaire de x est x − ⌊x⌋. C’est le nombre constitué des décimales de x.
En informatique, on utilise les nombres flottants (floats) pour représenter les nombres réels.
Les nombres flottants sont l’analogue en base 2 des nombres décimaux. Ils utilisent à la fois les puissances
positives de 2 et les puissances négatives de 2.
La notation est la suivante :
Définition 66 (Nombres décimaux en base B).
Soit B ∈ N \ {0, 1}. Soient k ∈ N∗ et b1 , . . . , bk ∈ {0, . . . , B − 1}.
Pk
On définit alors : (0, b1 b2 . . . bk )B = b1 .B −1 + b2 .B −2 + . . . + bk .B −k = r=1 br B −r .

0, 01012 = 0.2−1 + 1.2−2 + 0.2−3 + 1.2−4


Exemple 67. Ainsi, pour B = 2, on a : = 0 + 0,25 + 0 + 0,0625 On a aussi
= 0, 3125(10)
(11, 11)2 = (20 + 21 ) + (2−1 + 2−2 ) = 3 + 1
2 + 1
4 = 3 + 34 = (3, 75)10 .

Un nombre décimal est en fait un nombre de la forme 10km avec k ∈ Z et m ∈ N. De même, un nombre
"décimal" en base 2 est en fait de la forme 2rs avec r ∈ Z et s ∈ N.
Ainsi, la majorité des nombres réels ne sont pas décimaux, ni "décimaux" en base 2, ( 32 par ex.). De
plus, des nombres décimaux comme 0, 2 = 15 ne sont pas "décimaux" en base 2.
On a par exemple : 32 = 0, 1010101010101010P . . .2
n
L’égalité est vraie au sens des limites : limn ( k=0, k impair ( 12 )k ) = 23 .
En ce sens, on a aussi : 0, 11111111 . . .2 = 1. (Pour x = (0, 11 . . .)2 on a 2x = 1 + x, donc x = 1. Preuve
identique à celle de 0, 99 . . . = 1 en base 10.)

Remarque 68. Pour stocker un nombre réel sous forme de flottant (sous forme décimale en base 2)
dans Python, on doit faire en général une approximation de ce nombre, et c’est cette approximation qui
est stockée.
Avec n bits, on ne peut stocker que 2n valeurs différentes. Pour une taille de stockage donnée il y a ainsi
une limite en taille pour les nombres flottants (à la fois pour de grands nombres et pour de très petits
nombres).
Définition 69 (Codage d’une partie fractionnaire en base 2). Soit x ∈ R, et y = x − ⌊x⌋ la partie
fractionnaire de x (y ∈ [0, 1[). Soit n ∈ N∗ . Pour approcher y en base 2 avec n bits :
1. On multiplie y par 2.
2. On regarde la partie entière de 2y, qui vaut 0 ou 1. On stocke cette valeur pour b1 .
3. On regarde la partie fractionnaire de 2y, càd y1 = 2y − ⌊2y⌋.
4. On regarde la partie entière de 2y1 , qui vaut 0 ou 1. On stocke cette valeur pour b2 .
5. On répète le procédé jusqu’à ce qu’il n’y ait plus de partie fractionnaire restante, ou que l’on ait
atteint un mot de n bits (0, b1 b2 . . . bn )2 .
Dans le premier cas, on a une valeur exacte de y. Dans le second on obtient une valeur approchée
à 2n près de y.

Reprenons les exemples précédents :

26
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Exemple 70. 0, 3125 en base 2 :


0,3125 x 2 = 0,625 = 0 + 0,625
0,6250 x 2 = 1,250 = 1 + 0,250
0,2500 x 2 = 0,500 = 0 + 0,500
0,5000 x 2 = 1,000 = 1 + 0,000
On obtient ainsi que 0, 3125 = (0, 0101)2 .
Pour n = 5bits, convertissons 32 en base 2 :
0,667 x 2 = 1,333 = 1 + 0,333
0,333 x 2 = 0,667 = 0 + 0,667
0,667 x 2 = 1,333 = 1 + 0,333
0,333 x 2 = 0,667 = 0 + 0,667
0,667 x 2 = 1,333 = 1 + 0,333
On obtient ainsi que 23 ≃ (0, 10101)2 . Il reste une partie fractionnaire non-nulle à la fin, donc il n’y a
pas égalité entre ces deux nombres. Le second nombre est 12 + 81 + 32 1
= 16+4+1
32 = 21
32 = 0, 65625. La
2 1 1
différence entre ce nombre et 3 est inférieure à 25 = 64 .

Exercice 11. Soit n ≥ 1.


Convertir 0, 4(10) en binaire avec n bits de précision.

6.2 Structure des nombres flottants


6.2.1 La norme IEEE 754
Les nombres flottants suivent le principe de l’écriture scientifique. Prenons un exemple :
2
|{z}
−34, 254 = − 0, 34254 .10 3
|{z} | {z }
1 2

Trois éléments apparaissent :


1. Le signe, −1.
2. La mantisse, 34254.
3. L’exposant, 2.
C’est cette méthode que l’on adapte pour coder les nombres flottants.
Définition 71 (Norme IEEE 754 - Institute of Electrical and Electronics Engineers). Pour x ∈ R, on
code x comme nombre flottant sous la forme :

x = (signe ).(1,mantisse). (.2exposant )

Le signe est un bit (0 ou 1). La mantisse est une partie fractionnaire en base 2 à n bits. L’exposant est
un entier relatif en base 2 à m bits.
On le stocke sous la forme :

Répartition des bits selon le type de précision :


Signe Exposant Mantisse
Simple précision - 32 bits 1 8 23
Double précision - 64 bits 1 11 52
Précision étendue - 80 bits 1 15 64
Remarque 72. L’exposant d’un nombre flottant est un entier relatif, mais il n’est pas stocké en suivant
le complément à 2. Il est stocké sous forme décalée.
Avec m bits, les exposants possibles sont les entiers k entre −2m−1 + 1 et 2m−1 .
Pour stocker k, on lui ajoute 2m−1 − 1, et on le convertit en binaire. Cela donne l’exposant décalé.
Comme k + 2m−1 − 1 est un entier entre 0 et 2m − 1, il n’y a pas de problème.
Par exemple, pour m = 8 bits, et pour l’exposant k = 27, l’exposant décalé associé est 127 + 27 = 154 =
(1001 1010)2 .
On stocke ainsi (1001 1010)2 comme exposant décalé.

27
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Pour retrouver k, on convertit (1001 1010)2 en décimal et on soustrait 127.


Avec un exposant décalé et m = 8 bits, on stocke les entiers de −127 à 128. Le plus petit entier est codé
par (00 . . . 00)2 , et le plus grand est codé par (11 . . . 11)2 .
Avec le complément à 2 on stocke les entiers de −128 à 127. Le plus petit entier est codé par (100 . . . 00)2
et le plus grand est codé par (011 . . . 11)2 .

6.2.2 Convertir un réel sous forme de nombre flottant


Proposition 73 (Conversion réel vers flottant). Soit x ∈ R un réel.
1. Convertir en binaire la partie entière et la partie fractionnaire de |x| (sans tenir compte du signe
de x). Utiliser au plus n + m + 1 bits au total.
2. Dans l’écriture binaire, décaler la virgule vers la gauche ou la droite en factorisant par une
puissance de 2 pour la mettre sous la forme normalisée (IEEE 754).
3. Construire le nombre flottant avec les conventions suivantes :
• Signe : 0 si x positif, 1 si x négatif.
• Convertir l’exposant de la forme normalisée sous sa forme décalée, en lui ajoutant 2m−1 − 1
(m est le nombre de bits de l’exposant).
• Les décimales de la forme normalisée constituent mantisse. On la complète à droite avec des
zéros pour obtenir n bits.
Exemple 74. Convertissons -243,25 en nombre flottant, avec 32 bits (format IEEE 754 simple préci-
sion). On aura m = 8 bits pour l’exposant, et n = 23 bits pour la mantisse.
On a : 24310 = 111100112 et 0, 2510 = 0, 012 .
Donc, 243, 25(10) = 11110011, 01(2) = (1, 111001101(2) ).27 .
Le nombre −234, 25 est négatif. On stocke donc 1 pour le signe.
L’exposant est donc 7. L’exposant décalé est 7 + 127 = 134(10) = (10000110)(2) .
La mantisse est 111001101. On rajoute des 0 à droite pour la compléter.

On obtient ainsi le mot de 32 bits (ou 8 caractères hexadécimaux) :

Exercice 12. Exercice : Convertir +16, 5 en nombre flottant à 32 bits (simple précision).
On donnera le résultat final en binaire (32 bits) et en hexadécimal (8 caractères).

6.2.3 Procédure de conversion flottant vers réel


Exemple 75. Retrouvons la valeur du nombre flottant 44 F3 E0 00. On commence par écrire ce nombre
en binaire, et par positionner signe, exposant (8 bits), mantisse (23 bits).

On obtient :
• Signe : 0 (positif)
• Exposant décalé : 10001001(2) = 137(10) . Donc l’exposant est 137 − (27 − 1) = 137 − 127 = 10.
• Mantisse : 11100111110000000000000.
• Finalement, on obtient le nombre x = (1, 11100111110000000000000)2 .210 =
(1110011111, 0000000000000)2 = ( 11110011111
| {z } , 0000000000000
| {z } )2 = 1951(10) .
partie entiere 1951 partie fractionnaire

Exemple 76. Convertir le nombre flottant (4024000000000000)(16) en un réel.


C’est un codage sur 16 caractères hexadécimaux, donc en 64 bits.
Sa représentation binaire est 01000000001001000000000 . . . 00.
Son signe est 0.
Son exposant décalé (11 bits) est 10000000010 = 102610 .
L’exposant initial est alors 1026 − (210 − 1) = 1026 − 1023 = 3. Sa mantisse est 010000 . . . 00.
Le nombre associé est donc : x = (1, 01)2 .23 = (1010)2 = 10(10) .
Remarque 77.

28
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

• Le nombre flottant le plus grand que l’on peut obtenir est celui associé à (0 11 . . . 11 11 . . . 111),
n−1 n−1
c’est-à-dire +(1, 11 . . . 111)2 .22 ≃ 2.22 .
• Le nombre flottant le plus proche de 0 que l’on peut obtenir est celui associé à (0 00 . . . 00 00 . . . 000),
n−1
c’est-à-dire +(1, 00 . . . 00)2 .2−2 +1 = 22n−1 1
+1
.
• Avec les nombres flottants, on peut ainsi stocker à la fois des nombres relativement grands (de
n
l’ordre de 22 ) et des nombres relativements petits (de l’ordre de 221n ).
• Par convention les nombres (x 00 . . . 00 xx . . . xx) sont réservés pour 0 (exposant minimal), ceux
de la forme (0 11 . . . 11 xx . . . xx) sont réservés pour +∞ (positifs d’exposant maximal), et les
(1 11 . . . 11 xx . . . xx) sont réservés pour −∞ (négatifs d’exposant maximal).

Nombres flottants sur Python


Sur Python, comme dans la plupart des langages de programmation, un nombre flottant x est représenté
sur 64 bits selon la norme IEEE 754.
Exemple 78. On a : >>> f = 5.25
>>> f.hex()
’0×1.5000000000000p+2’
Le résultat indique que 5.25 est représenté par : (1, 5000000000000)16 × 2+2 .
On a (1, 5)16 × 22 = (1, 0101)2 .22 = (101, 01)2 = 5 + 41 = 5, 2510 .
Un autre exemple :
>>> f = 17.18
>>> f.hex()
’0×1.12e147ae147aep+4’
Ainsi, on a 17, 18 ≃ (1, 12e147ae147ae)16 .24 = (1 + 1.16−1 + 2.16−2 + 14.16−3 + 1.16−4 + . . .).16.

29
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Complexité algorithmique
Table des matières du chapitre
7.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
7.1.1 Lien avec la taille des données . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
7.1.2 Différents types de complexité . . . . . . . . . . . . . . . . . . . . . . . . . . 31
7.1.3 Un exemple : la multiplication par exponentiation binaire . . . . . . . . . . . 32
7.2 Classification de la complexité . . . . . . . . . . . . . . . . . . . . . . . . . . 32
7.3 Exemples de calculs de complexité . . . . . . . . . . . . . . . . . . . . . . . 33

7.1 Généralités
La complexité d’un algorithme représente “nombre d’opérations” nécessaire pour qu’il effectue son tra-
vail. Cette complexité donne une idée du temps nécessaire à l’algorithme pour qu’il soit exécuté par la
machine.
Cependant, toutes les opérations "élémentaires" ne prennent pas le même temps d’exécution.
Il y a plusieurs angles de vues concernant la complexité (nombre de tâches demandées à l’algorithme,
nombre d’opérations "élémentaires" effectuées, nombre d’opérations binaires effectuées par l’ordinateur).
Nous nous contenterons d’évaluer le nombre d’opérations élémentaires d’un certain type, qui caracté-
risent assez bien le temps d’exécution d’un algorithme.
Définition 79 (Opérations "élémentaires").
• Opérations arithmétiques : +, -, x, /, quotient //, reste % sur les entiers et flottants, . . .
• Comparaisons : <, <=, ==, !=, . . .
• Opérations logiques : not, and, or, xor.
• Affectation et lecture de variables : définir ou modifier une variable, append sur les listes,. . .
• Appel d’une fonction : appel à un autre algorithme, appel récursif.
Sur Python, ces opérations élémentaires sont codées de façon à optimiser au maximum le nombre
d’opérations binaires nécessaires (le temps de calcul). A moins d’étudier de l’informatique très théorique,
on fait confiance aux informaticiens qui ont passé du temps à optimiser les commandes les plus simples.
Dans un algorithme, on peut aussi s’intéresser à certaines opérations en particulier :
• Dans un algorithme de tri comparatif, une opération significative sera une comparaison.
• Dans un algorithme de manipulation de données, ce pourrait être le nombre d’accès à une donnée
générique de la structure.
• Dans une multiplication de polynômes à coefficients réels, ce sera le nombre de produits de réels.
• Dans le cas d’une fonction récursive, on cherchera à calculer le nombre d’appels récursifs.
• Dans un algorithme de traitement d’image, le nombre d’accès à un pixel et/ou le nombre d’opé-
rations d’affectation seront révélateurs du temps de calcul.

7.1.1 Lien avec la taille des données


Pour x une donnée en entrée et f (x) une quantité voulue, la complexité d’un algorithme calculant f (x)
dépend de x. Cette dépendance peut parfois être très complexe : pensez à un algorithme de primalité,
par exemple.

La complexité dépend souvent majoritairement de la taille de x. Pour le tri d’une liste, plus une liste
est grande et plus il y aura d’opérations à faire pour la trier.
Mais pas que : si la liste x est déjà presque triée, il faudra peu d’opérations pour terminer le tri. Pour
x une donnée de taille fixée, il se peut que la forme de x nécessite bien plus/bien moins d’opérations
qu’en moyenne.

La façon de stocker les données importe aussi. A part quelques cas particuliers (graphes) on ne cherchera
pas à étudier la complexité par rapport à la façon dont sont codées les données.

Remarque 80 (Quelques grandeurs pour les tailles de données). Pour les ordres de grandeurs, tout
comme pour le O(g(n)), on s’intéresse aux fonctions à une constante multiplicative près.

30
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

• Un entier Python borné est une donnée de taille 1.


En effet, si tous les entiers considérés s’écrivent sur n bits avec n fixé et raisonnable (32, 64, 128),
à une constante près ces données sont de taille 1.
On travaillera majoritairement avec des entiers de taille bornée, donc de taille 1.
• Un entier N quelconque stocké en binaire nécessite O(ln(N )) bits. (environ ln(N )
ln(2) )
• Un tableau de n entiers est de taille n.
• Une matrice m × n d’entiers est de taille m × n.
• Un polynôme de degré N à coefficients entiers est de taille N . (N + 1 coefficients)
Remarque 81 (Cas des graphes). Un graphe G possédant M sommets et N arêtes est de taille M + N ,
si l’on choisit de le coder par un tableau dont chaque case représente les M sommets de G, et si les
éléments du tableau sont des listes formées des sommets adjacents au sommet regardé (2N sommets
adjacents au total).

Mais le graphe sera de taille M 2 si on choisit de coder G par une matrice A de taille M × M , dont le
coefficient ai,j vaut 1 s’il y a une arête du sommet i vers le sommet j, et 0 sinon.
Etudier un graphe via un tableau de listes demandera bien moins d’opérations que via une matrice,
même si la matrice en question contient beaucoup de zéros.
On cherchera d’abord à manipuler correctement les graphes avant de s’intéresser à ces notions plus
difficiles.

7.1.2 Différents types de complexité


Un calcul exact de la complexité s’avère souvent difficile. On s’oriente en général vers les calculs suivants :
Définition 82 (Différentes complexités).
• La complexité dans le pire des cas consiste à calculer la quantité C(N ) égale au maximum
des complexités pour toutes les données de taille N .
• La complexité dans le meilleur des cas consiste à calculer la quantité C(N ) égale au mini-
mum des complexités pour toutes les données de taille N .
• La complexité en moyenne (HP) consiste à associer aux données X de taille N une proba-
bilité, et à considérer la moyenne C(N ) de la complexité de l’algorithme sur toutes ces données.

Remarque 83. Les calculs de complexité sont généralement effectués en ordre de grandeur, parfois
seulement de façon plus exacte.
2
Un résultat du type C(n) = O(n ln n) sera aussi utile que C(n) = (n + 23 ) ln( 3n 2n+4
+7n−1
).
Pour A, B deux matrices de Mn (R), effectuer le produit A × B revient à déterminer n2 nouveaux coef-
ficients. Pn
Avec la relation (AB)ij = k=1 ai,k bk,j , chaque coefficient nécessite n produits et n − 1 sommes.
On a donc au total n3 produits et n3 − n2 sommes à effectuer, soit 2n3 − n2 opérations. La complexité de
ce produit de matrices est de l’ordre de 2n3 . Il faut donc O(n3 ) opérations élémentaires pour un produit
de matrices.

Exercice 13 (Complexités naïve dans des cas usuels).


Déterminer la complexité (nombre d’opérations élémentaires) des algorithmes suivant.
1. Déterminer si x appartient à une liste de nombres.
2. Déterminer si x appartient à une liste de nombres triée par ordre croissant, en faisant une
recherche par dichotomie.
3. Calcul du produit AB, pour A ∈ Mn,p (R) et B ∈ Mp,q (R).
4. Calcul de A−1 pour A ∈ Mn (R) est inversible, avec la méthode du Pivot.

31
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

7.1.3 Un exemple : la multiplication par exponentiation binaire


Revenons sur l’algorithme calculant rapidement xn à l’aide de l’écriture en binaire de n.
def pow(x,n):
z = 1
m = n
y = x
while m != 0:
if m % 2 == 1:
z = z * y
m = m // 2
y = y * y
return(z)
La grandeur importante que l’on choisit pour évaluer la complexité de cet algorithme est le nombre de
multiplications, que l’on regarde en fonction de n. Notons-le T (n).
En effet, les opérations d’affectation, de comparaison, et de division euclidienne sur un entier (de taille
bornée) prennent moins de temps à la machine que la multiplication de deux réels sous forme de nombres
flottants.
On a 1 ou 2 multiplications de réels dans chaque passage de la boucle while.
L’entier m est celle qui détermine le nombre de passages dans la boucle. m commence en prenant la
valeur n, et à chaque passage m est divisé par 2 (quotient de division euclidienne).
Pour 2k ≤ n < 2k+1 , il faudra k + 1 divisions par 2 pour que m arrive à 0.
L’entier k qui donne l’encadrement est k = ⌊ log(n)
log(2) ⌋.
La boucle while est donc parcourue k + 1 fois, pour un nombre total de multiplications compris entre
k + 1 et 2k + 2.
ln(n)
Ainsi, T (n) est approximativement compris entre ln(2) et 2 ln(n)
ln(2) . On a donc T (n) = O(ln(n)).

7.2 Classification de la complexité


On distingue en général 7 grandes classes de complexité, en fonction de n la taille des données en
argument.
1 - Le temps constant, noté O(1) : peu importe l’entrée, le nombre d’opérations est majoré par une
constante.
C’est le cas de L.append(1), quelque soit la longueur de L.
2 - Le temps sous-linéaire, noté O(log(n)) : le nombre d’opérations dépend logarithmiquement de n.
C’est le cas de l’exponentiation binaire, ou de la conversion décimal -> binaire.
3 - Le temps linéaire, noté O(n) : le nombre d’opérations est proportionnel à n.
C’est le cas de la recherche du minimum ou du maximum dans un tableau, ou de L = L+[1].
4 - Le temps quasi-linéaire, noté O(nlog(n)).
C’est le cas du crible d’Ératosthène ou des tris efficaces.
5 - Le temps quadratique, noté O(n2 ) : c’est le cas des algorithmes de tri simples.
6 - Le temps polynômial, noté O(nk ) : le nombre d’opérations possède une dépendance polynômiale
par rapport à n.
Par exemple la multiplication de deux matices n × n demande de l’ordre de n3 opérations.
7 - Le temps exponentiel, noté O(cn ) où c > 1 est une constante.
Ces algorithmes sont très rapidement inutilisables. Par exemple, les algorithmes liés au problème
du voyageur de commerce.
Un algorithme qui nécessite au plus a.n. ln(n) opérations en nécessite au plus a.n2 . (un O(nlog(n)) est
aussi un O(n2 )). Lorsque l’on parle de complexité, on essaye aussi de trouver une domination optimale.
En s’appuyant sur une base de 109 opérations par secondes, le tableau qui suit donne le temps d’exécution
des différentes complexités citées ci-dessus :

32
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

n 5 10 20 50 250 1 000 10 000 1 000 000


1 10 ns 10 ns 10 ns 10 ns 10 ns 10 ns 10 ns 10 ns
log(n) 10 ns 10 ns 10 ns 20 ns 30 ns 30 ns 40 ns 60 ns
O(n) 50 ns 100 ns 200 ns 500 ns 2.5 µs 10 µs 100 µs 10 ms
n log(n) 50 ns 100 ns 200 ns 501 ns 2.5 µs 10 µs 100,5 µs 10 050 µs
n2 250 ns 1 µs 4 µs 25 µs 625 µs 10 ms 1s 2.8 heures
n3 1.25 µs 10 µs 80 ms 1.25 ms 156 ms 10 s 2.7 heures 316 ans
2n 320 ns 10 µs 10 ms 130 jours 1059 ans ... ... ...
n! 1.2 µs 36 ms 770 ans 1048 ans ... ... ... ...

Remarque 84. On peut remarquer qu’il faut éviter les complexités supérieures à O(n2 ).
• Lorsque l’algorithme s’y prête, on contourne parfois la contrainte grâce à un parallélisme massif.
Le MilkyWay-2, accueille 3 120 000 coeurs pour une puissance développée de 33,86 petaflop/s.
Un PC puissant atteint aujourd’hui 40 gigaflops, soit la puissance de calcul des meilleurs super-
calculateurs en 1987.
• Les ordinateurs quantiques ont théoriquement une puissance de calcul multipliée par 2 par l’ajout
de 1 q-bit. . . Une machine à 300 q-bits pourrait rendre compte de l’évolution de l’univers depuis
le Big Bang (très discutable sur le plan réaliste).

7.3 Exemples de calculs de complexité


Exemple 85 (Recherche d’un extremum dans un tableau). Soit L une liste de longueur n. On cherche
le minimum de L.

def mini(L):
m = L[0]
for i in range(len(L)):
if m > L[i]:
m = L[i]
return(m)

Les opérations élémentaires importantes pour la complexité de cet algorithme sont le test et l’affectation.
Le nombre de tests est constant et vaut n = len(L).
Dans cet algorithme on a entre 0 et n affectations. Le nombre total d’opérations élémentaires est donc
de n dans le meilleur cas, et de 2n dans le pire cas. La complexité, en nombre d’opérations, de cet
algorithme est donc en O(n).

Exemple 86 (Le tri à bulles). Soit L1 un tableau à n éléments, que l’on veut trier.

def tribulle(L1):
L = L1[:]
for i in range(len(L)-1,-1,-1):
for j in range(i):
if L[j] > L[j+1]:
L[j],L[j+1] = L[j+1],L[j]
return(L)

Les opérations élémentaires que l’on considère pour la complexité de cet algorithme sont d’une part le
nombre de tests et d’autre part le nombre d’échanges.
Pn−1 Pi
Dans tous les cas, le nombre de tests vaut exactement i=1 j=1 1 = n(n−1) 2 .
Le nombre d’échanges dans le meilleur des cas est 0. Dans le pire cas, on a autant d’échanges que de
n(n − 1)
tests , soit .
2
Ainsi, la complexité de cet algorithme varie entre n(n−1)
2 et n(n − 1) = n2 − n.
Dans tous les cas, cette complexité est de l’ordre de n , c’est un O(n2 ).
2

33
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Recursivité
Table des matières du chapitre
8.1 Généralités sur la récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . 34
8.2 Différents types de récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . 36
8.2.1 La récursivité simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
8.2.2 La récursivité multiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
8.2.3 La récursivité imbriquée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
8.2.4 La récursivité mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
8.3 Exemple emblématique : Les tours de Hanoï . . . . . . . . . . . . . . . . . 38

To understand what recursion is,

you must first understand recursion.

(Mais tu peux aussi commencer par apprendre à apprendre !)

8.1 Généralités sur la récursivité


Définition 87 (Algorithme récursif). Soit f un algorithme.
On dit que f est récursif quand, dans ses instructions, il s’appelle lui-même avec des paramètres “plus
petits”.
Remarque 88 (Terminaison).
Pour qu’un algorithme récursif se termine, il faut qu’il vérifie deux conditions :
1. Avoir un ou plusieurs cas de base où l’algorithme se termine directement ;
2. Être certain que l’algorithme n’exécutera qu’un nombre fini d’appels récursifs avant d’arriver à
un cas de base.
La vérification de cette seconde condition se fait avec une fonction de terminaison.
Définition 89 (Récursivité sur Python).
Pour gérer les appels récursifs, Python utilise une pile d’exécution.
Quand l’algorithme f d’entrée x1 fait un appel récursif (il appelle f avec x2 en entrée), le premier
algorithme est "mis en pause" et ses informations sont stockées dans la pile d’exécution (arguments de
la fonction, point du code où retourner à la fin de son exécution, etc.). Python exécute alors f d’entrée
x2 . Si cet algorithme fait encore un appel récursif (appel de f avec x3 en entrée), il est mis en pause,
ses informations sont stockées dans la pile, et Python exécute f de paramètre x3 .
Toutes ces exécutions sont ainsi empilées. Une fois que Python est arrivé à un algorithme qui n’a plus
d’appel récursif (f avec xk en entrée), il reprend l’exécution du dernier algorithme dans la pile (f avec
xk−1 en entrée), puis avec l’avant-dernier, etc, jusqu’à remonter finalement à l’algorithme initial (f
avec x1 en entrée).
Cette structure est celle d’une pile : Le dernier algorithme stocké est le premier à être exécuté.
Pour terminer l’exécution du tout premier algorithme, il faut avoir vidé toute la pile, donc avoir terminé
l’exécution de tous les autres algorithmes appelés récursivement.
Pour construire un algorithme récursif, on a besoin d’une (ou plusieurs) relation de récurrence, ainsi que
de valeurs initiales.
Exemple 90 (La fonction factorielle).
Pour la fonction factorielle, on a : 0! = 1 et n! = n.(n − 1)! pour tout n ≥ 1.
On construit alors l’algorithme récursif :

def factorielle(n) :
if n == 0 :
return 1
else :
return n*factorielle(n-1)

Pour n = 4, voici les appels récursifs effectués ainsi que la gestion de la pile d’exécution par Python :

34
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Il y a eu ainsi 4 appels récursifs, et il a fallu stocker 4 valeurs intermédiaires pour calculer 4!.
En général, cet algorithme est de complexité O(n) (n appels récursifs, n multiplications, n stockages de
valeurs intermédiaires).
Remarque 91. Les algorithmes récursifs ont des avantages :
1. Leurs expressions sont proches de la pensée mathématique.
2. Les algorithmes sont compacts et rapides à construire. Il faut savoir traiter des cas initiaux, et
pouvoir résoudre un cas général à partir d’une version de taille plus petite (avoir une sorte de
relation de récurrence).
Mais ils ont aussi des inconvénients.
1. Ils nécessitent davantage de calculs (complexié moins bonne) face à une version non-récursive.
2. Ils demandent davantage d’espace en mémoire pour stocker toutes les données en attente de
traitement (pile d’exécution, valeurs intermédiaires).
3. Il y a des limites sur le nombre d’appels récursifs, qui ne sont pas de très grands nombres. Par
exemple, Python limite à 1000 la profondeur d’appels d’une fonction récursive.
Cette limite d’appels récursifs peut être toutefois modifiée :
import sys
sys.setrecursionlimit (2000)

En général, la programmation explicite (impérative) est généralement plus efficace que la programmation
récursive. La programmation récursive n’est à envisager que si le problème s’y prête fortement. Par
exemple pour les tours de Hanoï.
Exemple 92 (Algorithme d’Euclide récursif).
Pour 0 < b < a, deux entiers, Euclide remarque que :

pgcd(a, b) = pgcd((a − b), b) et pgcd(b, 0) = b.

En itérant le processus, si a = qb + r, il vient que pgcd(a, b) = pgcd(b, r). Ceci fournit des valeurs
initiales et une relation de récurrence.
On peut alors constuire récursivement l’algorithme d’Euclide pour le calcul du pgcd.

def pgcd_explicite(a,b) :
while b != 0 :
a,b = b,a%b
return a

def pgcd_recursif(a,b) :
if b == 0 :
return a

35
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

return pgcd_recursif(b,a%b)

La version récursive fait apparaître clairement le fait que pgcd(a, b) = pgcd(b, r), ce qui n’est pas le cas
de la version explicite qui elle montre clairement les étapes de calcul de l’algorithme d’Euclide.
Il est intéressant de visualiser ces deux algorithmes sur des exemples.
Il existe différents types de récursivité, selon le nombre d’appels récursif et la façon dont on utilise
l’appel

8.2 Différents types de récursivité


8.2.1 La récursivité simple
Définition 93. Un algorithme récursif est dit de récursivité simple si quand on l’exécute il ne fait
qu’un seul appel récursif à lui-même.
Les algorithmes récursifs pour la fonction factorielle et pour l’algorithme d’Euclide sont à récursivité
simple. Si nous utilisons de la complexité, nous nous limiterons à de la complexité simple.

Définition 94 (Récursivité terminale). On dit qu’un algorithme récursif est à récursivité terminale
s’il fait un seul appel récursif et que cet appel récursif ne nécessite pas de stocker des informations
intermédiaires.
Le résultat de la fonction appelée est immédiatement renvoyé par la fonction appelante.
L’algorithme récursif pour la fonction factorielle vu précédemment n’est pas à récursivité terminale car
il nécessite de stocker des informations intermédiaires avec les appels récursifs pour obtenir le résultat
final.
L’algorithme d’Euclide récursif est à récursivité terminale. Une fois que l’on obtient la valeur de pgcd(a, b)
dans le tout dernier appel récursif, il n’y a plus d’autres opérations à effectuer, la pile d’exécution se
vide et la valeur est simplement retournée comme résultat.
C’est pour cela que l’on parle de récursivité terminale.

Exemple 95 (Recherche dichotomique récursive). On considère un tableau t de taille n qui contient


des nombres réels rangés dans l’ordre croissant. Pour x ∈ R on cherche à vérifier si x est contenu dans
t, et à quelle position, à l’aide d’une dichotomie.

def dicho(x, t) :
if len(t) == 0 :
return False
m = len(t) // 2
if x == t[m] :
return m
if x < t[m] :
return dicho(x, t[ :m])
else :
return dicho(x, t[m+1 :])

Cet algorithme est récursif. Chaque exécution de l’algorithme ne fait qu’un seul appel récursif au maxi-
mum (soit aucun, soit dicho(x, t[:m]), soit dicho(x, t[m+1:]).
De plus, les appels récursifs ne demandent aucune opération supplémentaire à part renvoyer la valeur
obtenue (qui sera la valeur obtenue par le dernier appel récursif).
Cet algorithme est donc à récursivité terminale.

Regardons la complexité de cet algorithme.


A chaque appel récursif, on a une division euclidienne, 1 ou 2 comparaisons, et 1 extraction de sous-
tableau. A chaque appel récursif on exécute l’algorithme avec la même valeur x mais avec un tableau de
taille divisée par 2 (division euclidienne). Comptons donc le nombre d’appels récursifs.
Cet algorithme se termine soit si l’une des valeurs t[m] vaut x, soit si le tableau de l’un des appels
récursifs est de taille 0 (tableau vide).
Ainsi, dans le pire cas, le nombre maximal d’appel récursifs est égal au nombre de fois que l’on peut

36
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

diviser n = len(t) par 2 (div eucl) jusqu’à arriver à 0.


Nous avons vu cela dans l’algorithme d’exponentiation binaire, ce nombre de divisions par 2 est égal à
⌊ ln(n)
ln(2) ⌋ + 1.
Cet algorithme nécessite ainsi O(ln(n)) opérations élémentaires.
On peut transformer certains algorithmes récursifs en algorithmes à récursivité terminale.
Exemple 96 (Factorielle en récursivité terminale).

def factorielleTerm (n,accu = 1) :


if n == 0 :
return accu
else :
return factorielleTerm (n-1,n*accu )

Cet algorithme se distingue du premier par l’usage d’un paramètre supplémentaire (accu). Avec ce para-
mètre, il n’y a plus besoin de conserver en mémoire les résultats de chaque appel récursif. Cela optimise
ainsi l’utilisation de la pile d’exécution.

La trace de FactorielleTerm(4) :

Remarque 97. La transformation d’un algorithme récursif en un algorithme récursif terminal n’est pas
évidente.
De plus, il est toujours possible de transformer un algorithme récursif terminal en une version explicite
(impérative), ce qui évite totalement l’emploi d’une pile d’une pile d’exécution.
En général, on évite souvent d’utiliser tout court des appels récursifs.

8.2.2 La récursivité multiple


Un algorithme récursif est à récursivité multiple si il y a plusieurs appels récursifs a lui-même lors d’une
seule exécution.
Exemple 98 (Triangle de Pascal récursif). 
Pour les coefficients binomiaux, on sait que n0 = 1, nn = 1, et que n n−1 n−1
   
k = k−1 + k .
On peut alors calculer nk avec l’algorithme récursif :


def binom(k,n) :
assert 0 <= k and k <= n
if k == 0 or k == n :
return 1
return binom(k,n-1)+binom(k-1,n-1)

4

Le nombre d’appels récursifs pour calculer binom(k,n) est assez important. Pour calculer 3 = 6 on a
besoin de 8 appels récursifs.
Ces algorithmes sont associés à des relations de récurrence qui font intervenir trois termes ou plus. Deux
autres exemples sont :
1. La suite de Fibonacci (et autres suites récurrentes linéaires d’ordre 2 avec un = aun−1 + bun−2 ).
2. Le tri fusion.

37
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

8.2.3 La récursivité imbriquée


Un algorithme récursif est dit à récursivité imbriquée si l’un des appels est une composée d’appels
récursifs.
Exemple 99 (La fonction A d’Ackermann).

def A(m,n) :
assert m==int(m) and n==int(n) and m>=0 and n>=0
if m==0 :
return n+1
elif n==0 :
return A(m-1,1)
else :
return A(m-1,A(m,n-1))

Méme pour des petites valeurs de m et n, l’évaluation de A(m,n) excède les capacités de la machine à
cause du nombre d’appels récursifs nécessaires.
Par exemple, A(4,3).

8.2.4 La récursivité mutuelle


Deux algorithmes F 1, F 2 sont à récursivité mutuelle si F 1 appelle F 2 et F 2 appelle F 1.
Exemple 100 (La parité d’un entier naturel n).

vrai si n = 0
pair(n) =
 impair(n-1) sinon
faux si n = 0
impair(n) =
pair(n-1) sinon

Exercice 14.
1) Écrire sur Python le code de pair(n) et de impair(n) ;
2) Afficher la trace d’exécution pour calculer pair(6) ;
3) Afficher la trace d’exécution pour calculer impair(8).
Exercice 15. Décrire ce que fait cet algorithme récursif :

def triangle1(n) :
if n = = 0 :
return
else :
print("x"*n)
triangle1(n-1)

A-t-on le même résultat si l’on échange les deux dernières lignes ?

def triangle2(n) :
if n > 0 :
print("x"*n)
triangle2(n-1)

8.3 Exemple emblématique : Les tours de Hanoï


Dans ce jeux de réflexion, on prend une tour constituée d’un empilement de n anneaux (de diamètres
décroissants), et 3 poteaux. L’objectif est de déplacer toute la tour du poteau de gauche au poteau de
droite, en utilisant un poteau intermédiaire, et en respectant les régles suivantes :
1. On ne peut déplacer qu’un anneau à la fois ;

38
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

2. On ne peut déplacer qu’un anneau qui n’a pas d’anneau au dessus de lui ;
3. Les anneaux doivent toujours être empilés par diamètres décroissants.

Remarque 101. Soit n ≥ 2. On considère une tour de Hanï à n anneaux.


Si on sait résoudre le problème pour n − 1 anneaux, alors on sait résoudre le problème pour n anneaux
avec les étapes suivantes :
1. Déplacer n − 1 anneaux du premier poteau vers le poteau du milieu.
2. Déplacer le plus grand anneau du premier poteau vers le dernier poteau.
3. Déplacer les n − 1 autres anneaux du poteau du milieu vers le dernier poteau.
Ces règles représentent une relation de récurrence (Hanoï(n) se résout à partir de Hanoï(n − 1)). Si l’on
sait gérer le cas initial (pour n = 1), on pourra alors résoudre le cas général avec un algorithme récursif.
Cet algorithme tiendra compte du nombre d’anneaux n et aussi de l’ordre d’utilisation des 3 poteaux.
Proposition 102. Soit n ≥ 1. Notons a, b, c les 3 poteaux. Voici le pseudo code qui permet de résoudre
le problème pour n anneaux :

Hanoï(a,b,c,n)
Si n = 1, déplacer l’anneau le plus haut en a, vers b
Sinon
appeler Hanoï(a,c,b,n-1)
déplacer l’anneau le plus haut en a, vers b
appeler Hanoï(c,b,a,n-1)

Cet algorithme fait deux appels récursifs chaque fois (sauf pour n = 1), c’est un algorithme à récursivité
multiple.
Sur Python, on obtient :

Tours de Hanoï
def Hanoi(n,source,auxiliaire,destination) :
if n==1 :
print("Déplacer l’anneau 1 depuis le poteau",source,
"vers le poteau",destination)
return
Hanoi(n-1,source,destination,auxiliaire)
print("Déplacer le disque",n,"depuis le poteau",source,
"vers le poteau",destination)
Hanoi(n-1,auxiliaire,source,destination)

Exemple 103. Exécuter Hanoi(4,’A’,’B’,’C’).


On peut alors produire un code plus compact :

Tours de Hanoï version 2


def hanoi(n, i, j, k) :
if n == 0 :
return
hanoi(n-1, i, k, j)
print("Déplacer l’anneau {} du poteau {} vers le poteau {}."

39
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

.format(n, i, k))
hanoi(n-1, j, i, k)

Exemple 104. Exécuter hanoi(4,’A’,’B’,’C’).


Concernant la terminaison de cet algorithme, à chaque appel récursif la première variable n décroît
strictement, et cette variable est un entier strictement positif. La fonction f (x) = x appliquée à la
première variable permet donc de démontrer que cet algorithme se termine bien en un nombre fini
d’appels récursifs.
Pour la complexité de cet algorithme, chaque exécution de l’algorithme engendre deux appels récursifs
(sauf pour n = 1 anneau), et chaque exécution contient un message à écrire (print).
Pour C(n) le nombre d’appels récursifs en fonction de n, on obtient C(1) = 0, C(2) = 2, et la relation
de récurrence C(n) = 2 + 2C(n − 1) pour tout n ≥ 2.
Ce nombre d’appels vérifie une relation de suite arithmético-géométrique. On résout x = 2 + 2x, de
solution x = −2.
Alors, la suite (C(n) − x)n est géométrique de raison 2. On a donc C(n) − x = (C(1) − x).2n−1 , c’est-
à-dire C(n) = (C(1) − x).2n−1 + x = 2.2n−1 − 2 = 2n − 2.
On a ainsi 2n − 2 appels récursifs et 2n − 2 + 1 messages de déplacements à écrire (exécution initiale à
ajouter).
La complexité de cet algorithme est donc en O(2n ) = O(en ln(2) ) opérations élémentaires. C’est une
complexité exponentielle.

40
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Graphes
Table des matières du chapitre
9.1 Introduction et vocabulaire . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
9.1.1 Graphe orienté, graphe non-orienté . . . . . . . . . . . . . . . . . . . . . . . . 41
9.1.2 Connexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
9.2 Matrice d’adjacence et liste d’adjacence d’un graphe . . . . . . . . . . . . 43
9.2.1 Matrice d’adjacence d’un graphe . . . . . . . . . . . . . . . . . . . . . . . . . 43
9.2.2 Liste d’adjacence d’un graphe . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
9.2.3 Pondération d’un graphe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
9.3 Parcours d’un graphe et plus court chemin . . . . . . . . . . . . . . . . . . 46
9.3.1 Algorithmes de parcours . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
9.3.2 Parcours en profondeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
9.3.3 Parcours en largeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
9.4 Deux applications au parcours d’un graphe . . . . . . . . . . . . . . . . . . 50
9.4.1 Détection des parties connexes dans un graphe non-orienté . . . . . . . . . . 50
9.4.2 Détection de cycles dans un graphe orienté . . . . . . . . . . . . . . . . . . . 50
9.5 Parcours d’un graphe pondéré et plus court chemin . . . . . . . . . . . . 51
9.5.1 Algorithme de Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

9.1 Introduction et vocabulaire


9.1.1 Graphe orienté, graphe non-orienté
Définition 105 (Graphe, graphe orienté). Un graphe orienté est défini par une paire (S, A), avec :
1. S un ensemble fini. Les éléments x1 , . . . , xn de S sont appelés les sommets (ou noeuds) de G.
2. A un sous-ensemble de S × S. Les éléments de A, de la forme (xi , xj ), sont appelés les arrêtes
de G.
3. Pour (xi , xj ) ∈ A, on dit que l’arrête part de xi et va vers xj . Les arrêtes sont orientées.
Le graphe G comporte Card(S) sommets, et Card(A) arrêtes.
Exemple 106.

Le problème des 7 ponts de Königsberg


La ville de Königsberg était divisée en quatre régions reliées entre elles par 7 ponts. Le problème que se
posait aux habitants était :
Est-il possible de trouver un chemin dans la ville qui passe par chaque pont exactement une fois ? Euler
a simplifié le problème en réduisant le schéma de la ville aux informations importantes. Ces informations
sont les quatre régions, et les sept ponts les reliant, c’est-à-dire à l’aide d’un graphe.

41
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Ce dessin de graphe ne représente pas un graphe orienté (on peut emprunter chaque pont dans un sens
ou dans l’autre, et certains sommets sont reliés plusieurs fois), mais un autre type de graphe (un graphe
non-orienté, avec un degré associé à chaque arrête).
Avec ce modèle, Euler a pu énoncer une proposition mathématique portant sur des graphes de la même
nature, et démontrer que dans ce cas particulier il n’y a pas de solution.

Remarque 107. Les graphes sont la formalisation d’un ensemble d’objets ayant des relations entre
eux : on gère donc, en plus de l’ensemble des objets, l’ensemble des relations qui peuvent lier deux
objets.
Ces objets peuvent être de n’importe quelle nature (nombres, fonctions, surfaces, personnes,. . .).
Exemples de problèmes modélisables :
1. Organisation d’un réseau informatique ou de transport (routier, aérien, maritime).
2. Connexions à un réseau électrique/d’eau/d’égoûts.
3. Liens dans un réseau social.
4. File d’attente (à l’OPT, à l’aéroport).
5. Parcours dans un labyrinthe.
Sur chacun de ces problèmes, on peut ensuite se poser beaucoup de questions (chemin le plus court/le
moins coûteux entre deux sommets, chemin le plus court qui passe par tous les sommets, nombre moyen
de connexions entre les sommets, distance moyenne entre deux sommets, sommets isolés ou ayant peu
de connexions au reste des sommets,. . .
Définition 108 (Degré dans un graphe orienté).
Soit G = (S, A) un graphe orienté. Soit s un sommet de G.
1. On note d+ (s) le degré sortant de s, comme le nombre d’arrêtes partant de s.
2. On note d− (s) le degré rentrant de s, comme le nombre d’arrêtes arrivant à s.
3. Les voisins de s sont les sommets s′ tels que (s, s′ ) ∈ A. Le sommet s a d+ (s) voisins.
Remarque 109. Pour G = (S, A) et s ∈ S un sommet de G, on peut tout à fait avoir (s, s) ∈ A.
C’est-à-dire l’existence d’une arrête qui relie s à s.
Cette arrête est particulière puisque son point de départ est aussi son point d’arrivée. On la représente
souvent sous forme de boucle (elle autorise de se déplacer dans le graphe G tout en restant au point s).
Définition 110 (Graphe non-orienté). Soit G = (S, A) un graphe.
Si l’ensemble A est symétrique ((s, s′ ) ∈ A ⇔ (s′ , s) ∈ A), on dit alors que G est un graphe non-orienté.
Dans un graphe non-orienté, si deux sommets s, s′ sont reliés par une arrête, on peut se déplacer de s
vers s′ ou de s′ vers s.
Exemple 111.

Définition 112 (Degré dans un graphe non orienté).


Soit G = (S, A) un graphe non-orienté. Soit s ∈ S un sommet de G.
On note d(s) le nombre d’arrêtes partant de s.
Les voisins de s sont les sommets s′ tels que (s, s′ ) ∈ A. Le sommet s possède d(s) voisins.

Cette définition est en fait identique à la précédente (d(s) = d+ (s)), seule la quantité d− (s) disparaît
(elle n’est plus nécessaire).

42
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Remarque 113. En informatique, on considère en général qu’un graphe non-orienté n’a pas de boucles
(pas d’arrêtes qui vont d’un sommet s vers lui-même).
Dans ce chapitre, le fait d’avoir des boucles ne pose pas de problèmes. Mais pour d’autres notions sur
les graphes, être sans boucles est le "cas général" et avoir des boucles est le "cas particulier" (les résultats
sont plus compliqués à écrire quand il y a des boucles).
X
Exercice 16. Pour les graphes non-orientés précédents, calculer Card(A) et d(s).
s∈S

9.1.2 Connexité
Définition 114. Soit G = (S, A) un graphe. Soient a, b ∈ S.
1. Un chemin de a vers b est une suite finie d’arrêtes de la forme
(a, a1 ), (a1 , a2 ), (a2 , a3 ), . . . , (ak−1 , b)
On le note aussi a → a1 → a2 → . . . → ak−1 → b ou c = (a, a1 , a2 , a3 , . . . , ak−1 , b).
2. On dit qu’un chemin de a vers b est élémentaire si tous les sommets parcourus sont distincts.
3. La longueur l(c) d’un chemin est le nombre d’arrêtes qui le composent.
4. Un circuit est un chemin de a vers a (quand a = b).
5. Un cycle est un circuit élémentaire : Il part de a, revient en a, et tous les sommets intermédiaires
sont deux à deux distincts. 1
Définition 115 (Connexité dans un graphe). Soit G = (S, A) un graphe. Soient a, b ∈ S.
On dit que a et b sont reliés s’il existe un chemin de a vers b.
On dit que a et b sont équivalents s’il existe un chemin de a vers b et un chemin de b vers a.
1. On appelle composante connexe du graphe l’ensemble C(s) de tous les points équivalents à s.
Le graphe G se retrouve alors partitionné (découpé) en un nombre fini de composantes connexes.
Certaines composantes connexes sont reliées entre elles (à sens unique), d’autres sont isolées.
2. Si le graphe G n’a qu’une seule composante connexe, on dit qu’il est connexe. 2
Exercice 17. Représenter toutes les composantes connexes de ces graphes. Sont-ils connexes ?

9.2 Matrice d’adjacence et liste d’adjacence d’un graphe


9.2.1 Matrice d’adjacence d’un graphe
Définition 116. Soit G = (S, A) un graphe avec n sommets. On pose S = {x1 , . . . , xn }.
On définit la matrice d’adjacence du graphe G comme la matrice M = (mij ) ∈ Mn (R) telle que :

2 1 si (xi , xj ) ∈ A
∀(i, j) ∈ {1, . . . , n} , mi,j =
0 si (xi , xj ) ̸∈ A
On peut noter qu’un graphe est non orienté si et seulement si la matrice d’adjacence est symétrique.
Exemple 117 (Graphe non-orienté et sa matrice d’adjacence).
1. NB : Pour un graphe non orienté, on ne considère que les cycles de longueur supérieure à 3.
2. NB : le programme se limite à la connexité pour les graphes non orientés. Dans ce cas, les composantes connexes
sont isolées les unes des autres.

43
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

 
0 2 0 1 1 0 0
1 0 1 1 0
 
1 1 0 1 1
4  
0 1 1 0 1
0 0 1 1 0
1 3

Exemple 118 (Graphe orienté et sa matrice d’adjacence).


 
0 1 2 0 1 0 1 0 0
0 0 0 0 1 0
 
0 0 0 0 1 1
 .
0 1 0 0 0 0
 
0 0 0 1 0 0
3 4 5 0 0 0 0 0 1

Remarque 119.
• Un graphe G est non-orienté si et seulement si sa matrice d’adjacence est symétrique.
• Les coefficients diagonaux de la matrice symétrique représentent les arrêtes qui relient un sommet
à lui-même.
• Dans un graphe non-orienté, les sommets ne sont pas forcément reliés à eux-mêmes.
• La matrice d’adjacence de G dépend de l’ordre de ses sommets. Si l’on change l’ordre des sommets
(des éléments de S), cela change l’écriture de la matrice d’adjacence. Cependant, cela conserve
toutes ses propriétés, donc ce n’est pas un problème.
Remarque 120. La matrice d’adjacence d’un graphe G contient toutes ses informations (nombre de
sommets et arrêtes).
On peut ainsi représenter le graphe G sur Python avec sa matrice d’adjacence M .
Avec ce point de vue, les manipulations sur G seront des manipulations sur la matrice M (produit de
matrices, échange de lignes/colonnes, extraire un coefficient).
Exercice 18. Soit G un graphe dont la matrice d’adjacence est M=[[1,0,0,1],[1,0,1,0],[1,0,1,1],[0,0,1,0]].
Représenter sur un dessin le graphe G (sommets, arrêtes). Est-il orienté ou non-orienté ?

9.2.2 Liste d’adjacence d’un graphe


En représentant un graphe à n sommets avec une matrice d’adjacence, on obtient un objet informatique
de taille n2 .
Cette représentation est largement surdimensionnée quand le graphe a beaucoup de sommets et peu
d’arrêtes. On préfère alors une autre représentation, avec une liste.

Définition 121 (Liste d’adjacence d’un graphe).


Soit G = (S, A) un graphe à n sommets. On pose S = {x0 , . . . , xn−1 }.
On définit L la liste d’adjacence du graphe G par une liste de n listes, telles que pour tout i ∈
{0, . . . , n − 1}, la liste L[i] contient tous les sommets adjacents à xi .
L est une liste de listes, et L[i] est une liste de sommets.
On peut aussi utiliser à la place un tableau de tableaux, ou un dictionnaire de listes.

Exemple 122.

0 2

1 3

La liste d’adjacence de ce graphe est L = [[1,2],[0,2,3],[0,1,3,4],[1,2,4],[2,3]].


Le graphe G est non-orienté. On peut le lire dans la liste d’adjacence (0 relié à 1, 1 relié à 0,. . .) mais
ce n’est pas immédiatement visible.
Exemple 123. Voici un graphe orienté dont les sommets sont nommés.

44
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Maths Phys SI

Philo LV1 Info

On peut associer un numéro à chaque sommer et ainsi construire la liste d’adjacence du graphe. Mais,
si l’on veut conserver le nom initial des sommets, le dictionnaire d’adjacence est plus adapté.
Sur Python, cela donne :

Adj = {"Maths": ["Phys","Philo"], "Phys": ["LV1"], "SI": ["LV1","Info"],


"Philo": ["Phys"], "LV1": ["Philo"], "Info": ["Info"]}
Au lieu d’avoir les listes d’arrêtes associées à des nombres entiers, elles sont ici associées au nom des
sommets, ce qui rend leur utilisation plus claire.

Remarque 124. Sur Python, beaucoup d’opérations élémentaires pour les listes sont identiques à celles
des dictionnaires (ou des tableaux) : taille, données contenues, concaténer deux objets, lire une donnée,
boucle for sur toutes les donnée, boucle "tant que X est non-vide", . . .
D’autres opérations s’écrivent légèrement différemment (ajouter un nouvel élément, retirer un élément
spécifique, créer la liste/le dictionnaire vide).
Ainsi, les algorithmes utilisant une liste d’adjacence ou un dictionnaire d’adjacence sont quasiment
identiques.
Exemple 125. Soit G = (S, A) un graphe, codé sous forme de liste d’adjacence L. Voici un programme
Python qui prend en entrée L et qui retourne une liste contenant toutes les arrêtes de G (sous forme de
2-uplets).

def arcsListe(L):
arretes = []
for i in range len(L):
for j in L[1]:
arretes.append((i,j))
return(arretes)

Exercice 19. Pour G un graphe de matrice d’adjacence M , écrire une fonction arcsMatrice en Python
qui prend en entrée M et qui retourne une liste contenant toutes les arrêtes de G.

9.2.3 Pondération d’un graphe


Dans beaucoup de contextes, il est utile d’ajouter des informations sur le graphe G. L’inormation la
plus utilisée est la pondération des arêtes (distance entre des lieux, coût de déplacement, durée de vol).

Définition 126 (Graphe pondéré).


Soit G = (S, A) un graphe.
On dit que G est un graphe pondéré si l’on associe à chaque arrête (s, s′ ) ∈ A un nombre x(s,s′ ) ∈ R.
Le nombre x(s,s′ ) est appelé le poids de l’arrête (s, s′ ).

Définition 127. Sur Python, on représente un graphe pondéré G avec une matrice d’adjacence pondérée
M : mi,j = s(i,j) s’il y a une arrête reliant i et j, et mi,j = 0 sinon.

45
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Autrement dit, on remplace les 1 de la matrice d’adjacence par le poids de chaque arrête.
Exemple 128. On pose
M = [[2,3,0],[1,0,1],[-1,2,0]]

Remarque 129. On peut aussi représenter un graphe pondéré par une liste d’adjacence ou un diction-
naire d’adjacence. Il faudra cependant utiliser des listes de listes de 2-uplets (tuple) pour avoir à la fois
l’information sur les arrêtes et sur leurs poids.
Pour le graphe de l’exemple précédent, une liste d’adjacence est :
L=[[(0,2),(1,3)],[(0,1),(2,1)],[(0,-1),(1,2)]]

L[i] contient toutes les arrêtes pondérées partant de i. La première coordonnée de L[i][j], L[i][j][1], est
le numéro du sommet d’arrivée. La seconde, L[i][j][2], est le poids affecté à l’arrête.
Avec un dictionnaire d’adjacence, cela donne :
Adj = {"s1": [("s1",2),(("s",3)], "s2": [("s1",1),("s3",1)], "s3": [("s1",-1),("s2",2)], }

Avoir un nom pour chaque sommet aide à la lecture.


Avec un graphe ou un graphe pondéré, l’une des questions essentielles est : Quel est le plus court chemin
de a vers b ?
L’enjeu est de savoir comment parcourir un graphe pour obtenir la réponse, de façon si possible efficace.

9.3 Parcours d’un graphe et plus court chemin


9.3.1 Algorithmes de parcours
Pour G = (S, A) un graphe et a, b deux sommets de G existe-t-il déjà un chemin reliant a à b ?
Sur cet exemple (graphe non-orienté), les sommets A et D sont visuellement bien reliés. Mais comment
vérifier cela à partir de la matrice d’adjacence ou de la liste d’adjacence de G ?

Il faut pour cela construire des algorithmes qui vont explorer le graphe, à la recherche d’un chemin entre
a et b.
Explorer le graphe veut dire que l’on ne se déplace que suivant les arrêtes, afin de trouver une suite
d’arrêtes (a, a1 ), (a1 , a2 ), . . . (an , b) qui relient a à b.

Remarque 130. Un algorithme de parcours efficace doit au moins garder en mémoire :


• Le sommet de départ et le sommet d’arrivée ;
• La liste des sommets déjà explorés ;
• La liste des sommets accessibles (via une arrête) à partir de ceux déjà explorés ;
• La liste des sommets restants (non explorés et non accessibles pour le moment) ;
Nous utiliserons cette convention pour distinguer ces trois ensembles de sommets à chaque étape de
l’algorithme :

46
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Un algorithme de parcours aura ainsi la structure suivante en pseudo-code :

Parcours avec mémoire


On dispose d’un sommet initial Si , et un sommet cible Sc .
Colorier Si en gris (à explorer), et les autres sommets en blanc
Tant qu’il reste des sommets gris :
Choisir un sommet gris S (à préciser !)
Si S = Sc : l’algorithme se termine par un succés.
Sinon : le colorier en noir (exploré)
Colorier tous ses voisins blancs en gris (à explorer)
L’algorithme se termine par un échec (Si n’est pas relié à Sc ).

Remarque 131.
• Cet algorithme va visiter, un à un, tous les sommets que l’on peut atteindre à partir de Si . Il se
termine bien car le graphe a un nombre fini de sommets. Si Si et Sc sont reliés, cela sera toujours
détecté.
• Si G a n sommets, la boucle "Tant que" sera exécutée au plus n fois.
• Il reste à décider de la façon de choisir qui sera le prochain sommet visité parmi les sommets en
gris (le premier dans la liste ? le dernier dans la liste ? au hasard ?).
• Cet algorithme ne fournit pas de chemin explicite qui relie Si à Sc .
Pour obtenir en réponse un chemin explicite qui relie Si à Sc , l’algorithme de parcours doit mémoriser
une information supplémentaire : Pour chaque sommet s′ que l’algorithme visite, il se souvenir du
sommet s déjà visité qui est relié à s′ (celui qui a permis d’aller vers s′ ).
Pour chaque sommet visité, on stocke l’information du "sommet précédent". On utilisera pour cela une
liste, que l’on appelle la liste des pères des sommets visités.

Recherche d’un chemin avec mémoire


On dispose d’un sommet initial Si , et d’un sommet cible Sc .
Colorier Si en gris (à explorer), et les autres sommets en blanc
Tant qu’il reste des sommets gris :
Choisir un sommet gris S (à préciser)
Si S = Sc : renvoyer la liste des pères, et fournir un chemin.
Sinon : colorier S en noir.
Pour chaque voisin V de S qui est blanc :
Colorier V en gris
père(V ) ← S
L’algorithme se termine par un échec (Si n’est pas relié à Sc ).

47
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Remarque 132. Pour choisir quel prochain sommet visiter dans un algorithme de parcours, il existe
deux grandes méthodes :
1. En profondeur : on choisit comme nouveau sommet le dernier sommet colorié en gris.
2. En largeur : on choisit comme nouveau sommet le premier sommet colorié en gris.

9.3.2 Parcours en profondeur


Le nouveau sommet à explorer est le dernier colorié en gris. Ainsi, la liste des sommets gris est à gérer
comme une pile (dernier arrivé, premier sorti).
Sur Python, la structure de liste convient parfaitement pour un fonctionnement de pile.
Exemple 133. Dessiner un graphe à 6 sommets, et représenter à la main un parcours en profondeur
partant du sommet 1, et cherchant à trouver le sommet 6.
A la main et sur des vidéos, l’algorithme est assez rapide, mais le chemin obtenu n’est pas du tout le
plus cours.
Voici un algorithme en Python qui réalise un parcours en profondeur dans le graphe G, en utilisant sa
liste d’adjacence L. On part de si et on veut rejoindre sc.
N est la liste des sommets coloriés en noir. G est la liste des sommets coloriés en gris. Il n’y a pas besoin
de liste pour les sommes coloriés en blanc, ce sont exactement les sommets qui ne sont ni blancs ni gris.

def profondeur(L,si,sc) :
N = [si] # On colore si en noir.
G = L[si] # On colore en gris les voisins de si.
while G != [] : # si G = [], exploration terminée
s = G.pop() # On prend le dernier élément de G, et on le retire de G.
N.append(s) # On colore s en noir.
for v in L[s] : # Pour tout v voisin de s
if v == sc :
N.append(v)
return N # Le sommet sc est atteint.
elif (v not in N) and (v not in G) :
G.append(v) # Si v est blanc, on le colorie en gris.
return False # Le sommet si n’est pas relié au sommet sc.

Remarque 134. La liste N des sommets visités semble former un chemin de si vers sc, mais ce n’est
pas toujours le cas (si l’algorithme explore une impasse, il doit revenir en arrière et prendre une autre
direction).
Pour obtenir un chemin de si vers sc, il faut ajouter une liste des pères P .
La liste des pères est une liste qui a autant d’éléments que G a de sommets.
Pour un sommet s, P [s] indique le "père" de s, ou None si s n’a pas encore été exploré.
Avec la liste des pères P , on créée le chemin C en remontant le trajet (partir de sc et revenir jusqu’à si
de père en père).

def profondeur(L,si,sc) :
P = [None]*len(L) # Création de la liste des pères.
C = [] # Création du chemin de si vers sc.
N = [si] # On colore si en noir.
G = L[si] # On colore en gris les voisins de si.
while G != [] : # si G = [], exploration terminée
s = G.pop() # On prend le dernier élément de G, et on le retire de G.
N.append(s) # On colore s en noir.
for v in L[s] : # Pour tout v voisin de s
if v == sc : # Le sommet sc est atteint.
N.append(v)
C=[sc]+C # construction du chemin C en revenant en arrière.
while s != si :
s = P[s] # On remonte au père du sommet s.
C = [s]+C # On ajoute le père au chemin C, jusqu’à revenir au sommet si.
return C # On renvoie le chemin C.

48
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

elif (v not in N) and (v not in G) :


G.append(v) # Si v est blanc, on le colorie en gris.
P[v] = s # Le père (le sommet d’origine) de v est s.
return False # Le sommet si n’est pas relié au sommet sc.

9.3.3 Parcours en largeur


Le nouveau sommet à explorer est le premier colorié en gris. Ainsi, la liste des sommets gris est à gérer
comme une file (premier arrivé, premier sorti).
Sur Python, la structure de liste peut être utilisée pour faire des files, mais cela n’est pas très adapté.
Avec l’objet file, le retrait du premier élément (popleft) a une complexité en O(1), alors que pour une
liste le retrait du premier élément (L.remove(L[0])) a une complexité en O(n).
Exemple 135. Dessiner un graphe à 6 sommets, et représenter à la main un parcours en profondeur
partant du sommet 1, et cherchant à trouver le sommet 6.
Remarque 136. Sur des exemples et des vidéos, on peut constater que :
• Le parcours en largeur est moins rapide que celui en profondeur ;
• Le parcours en largeur va explorer tous les voisins du sommet si (les sommets à distance 1), puis
tous les voisins des voisins (sommets à distance 2),. . ..
• Le chemin reliant si à sc que l’on obtient est le plus court.
Dans un parcours en profondeur, on explore un voisin a1 , puis a2 un voisin de a1 , puis a3 un voisin de
a2 ,. . ., jusqu’à atteintre sc ou une impasse. En cas d’impasse on rebrousse chemin pour explorer avec
un autre voisin colorié en gris.
Dans un parcours en largeur, on explore d’abord tous les voisins de si atteignables en 1 déplacement,
puis tous ceux atteignables en 2 déplacements, puis tous ceux atteignables en 3 déplacements,. . .. Dans
ce cas, lorsque l’on atteint l’arrivée sc, on peut savoir combien de déplacements sont nécessaires pour
aller de si à sc (on obtiendra la longueur du plus court chemin).
Voyons un algorithme Python de parcours en largeur. Ce premier algorithme utilise des listes (moins
optimisé que les files), et indique seulemnt si si et sc sont reliés (pas de liste des pères).

def profondeur(L,si,sc) :
N = [si] # On colore si en noir.
G = L[si] # On colore en gris les voisins de si.
while G != [] : # si G = [], exploration terminée
s = G[0] # On prend s le premier élément de G.
G.remove(s) # On retire le premier élément de G.
N.append(s) # On colore s en noir.
for v in L[s] : # Pour tout v voisin de s
if v == sc :
N.append(v)
return N # Le sommet sc est atteint.
elif (v not in N) and (v not in G) :
G.append(v) # Si v est blanc, on le colorie en gris.
return False # Le sommet si n’est pas relié au sommet sc.

Voyons maintenant un second algorithme utilisant une file, qui fournit un chemin entre si et sc (qui
s’avère être un chemin de longueur minimale).
Pour utiliser une file (queue) sur Python, on peut importer le module queue (import queue as *).
Cependant, on prefère importer les double-ended queues (files à double-extrémité) qui fournissent un
objet qui peut servir à la fois de pile et de file (from collections import deque). L’ajout et le retrait
d’un élément sur un deque a une complexité en O(1) dans les deux sens.

from collections import deque


def profondeur(L,si,sc) :
P = [None]*len(L) # Création de la liste des pères.
C = [] # Création du chemin de si vers sc.
N = [si] # On colore si en noir.

49
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

G = deque() # On définit la file des sommets gris par la file vide.


for j in L[si] : # On colore en gris les voisins de si.
G.append(L[si][j]) # L[si] est une liste, G un deque. On remplit G sommet par sommet.
while G != deque() : # si G = deque(), exploration terminée
s = G.popleft() # On prend le premier élément de G, et on le retire de G.
N.append(s) # On colore s en noir.
for v in L[s] : # Pour tout v voisin de s
if v == sc : # Si le sommet sc est atteint.
N.append(v)
C=[sc]+C # construction du chemin C en revenant en arrière.
while s != si :
s = P[s] # On remonte au père du sommet s.
C = [s]+C # On ajoute le père au chemin C, jusqu’à revenir au sommet si.
return C # On renvoie le chemin C.
elif (v not in N) and (v not in G) :
G.append(v) # Si v est blanc, on le colorie en gris.
P[v] = s # Le père de v est s.
return False # Le sommet si n’est pas relié au sommet sc.

Dans le pire des cas, il faut parcourir un graphe G en entier pour trouver un chemin entre si et sc (si
le graphe est en forme de ligne droite par exemple). La complexité dans le pire des cas d’un algorithme
de parcours en longueur/largeur est donc en O(n), où n est le nombre de sommets du graphe.

9.4 Deux applications au parcours d’un graphe


9.4.1 Détection des parties connexes dans un graphe non-orienté
Pour G un graphe non-orienté, les algorithmes de parcours permettent facilement de déterminer la
composante connexe d’un sommet si (la liste de tous les sommets reliés à s).
On utilise pour cela un algorithme de parcours en largeur à partir du sommet si, mais sans donner de
sommet d’arrivée sc.
L’algorithme va ainsi explorer tous les voisins de si, jusqu’à ce que tous les voisins aient été explorés.
La liste des sommets en Noir donne alors la composante connexe du sommet si.
Cet algorithme peut ensuite être utilisé pour déterminer toutes les composantes connexes de G.
Pour cela, il faut ensuite prendre un nouveau sommet initial (parmi ceux non visités) pour obtenir une
autre composante connexe.
En ajoutant une boucle supplémentaire on peut ainsi construire une liste de composantes connexes de
G.
Exercice 20. Soit G un graphe non-orienté, de liste d’adjacence L. Soit si un sommet de G.
1. Rédiger un algorithme CompoConnexe(L,si) qui renvoie la composante connexe du sommet si
2. On pose L = [[0, 1, 3], [0, 2], [1, 3], [0, 2, 3], [ ]] et si = 0.
Exécuter CompoConnexe(L,si).
3. Rédiger un algorithme CompoConnexe2(L) qui renvoie la liste des composantes connexes de G.
Cet algorithme appellera l’algorithme CompoConnexe pour obtenir chaque composante connexe.
On pose L = [[0, 1, 3], [0, 2], [1, 3], [0, 2, 3], [ ]] et si = 0.
Exécuter CompoConnexe2(L).

9.4.2 Détection de cycles dans un graphe orienté


En appliquant un algorithme de parcours en profondeur, si l’algorithme explore un sommet s dont l’un
des voisins s′ est déjà colorié en noir, cela veut dire que l’on a un cycle (on peut aller de s à s′ , puis de
s′ à s en ne repassant jamais par le même sommet).
Exemple 137. Soit G un graphe de liste d’adjacence L = [[1, 5], [2], [3], [4], [0], [ ]].
Dessiner G. Appliquer à la main un algorithme de parcours en profondeur à G, en parant de 0.
Quel cycle détecte-t-on ?
Pour certaines tâches, on souhaite parcourir un graphe G "sans jamais revenir en arrière" (ou un nombre
minimal de fois). Si le graphe G n’a pas de cycles, peu importe la façon dont on le parcours on sait que

50
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

l’on ne visitera pas deux fois le même sommet.


Si G a des cycles, on peut alors chercher à savoir combien de cycles "différents" sont présents dans G
afin d’estimer combien de fois l’on risque de repasser sur un sommet déjà visité.
Il faut donc avoir des algorithmes capables de détecter les cycles de G, et les algorithmes de parcours le
permettent.

Toute la théorie derrière la simplification d’un graphe G en transformant chaque cycle en un seul sommet
(construire un second graphe qui n’a plus de cycles et qui "correspond" à G) est hors programme.

9.5 Parcours d’un graphe pondéré et plus court chemin


Dans un graphe pondéré, on peut regarder pour chaque chemin entre s et s′ la somme des poids le long
du parcours.
On cherchera alors à trouver un chemin entre s et s′ tel que le poids total est minimal.
Exemple : Recherche du chemin qui consomme le moins de carburant/qui coûte le moins cher.
Un graphe qui n’est pas pondéré peut être comme un graphe où chaque arrête est de poids 1. Le chemin
entre s et s′ qui passe par le plus petit nombre de sommets est ainsi celui dont le poids total est minimal.
Dans un graphe pondéré, le chemin le plus direct n’est pas forcément celui dont le poids total est minimal.

9.5.1 Algorithme de Dijkstra


Un algorithme très efficace existe pour répondre à cette question : l’algorithme de Dijkstra.
Cet algorithme repose sur le résultat suivant :
Proposition 138 (Principe de Bellman). Soit G = (S, A) un graphe pondéré. Soient s, t deux sommets
de G. Soit C un chemin optimal entre s et t.
Pour tout sommet s′ dans le chemin C, le sous-chemin entre s et s′ et celui entre s′ et t sont optimaux.
Ainsi, au lieu de chercher parmi tous les chemins entre s et t lequel est de poids optimal, ce qui est
difficile, on construit ce chemin par un algorithme de parcours en largeur qui va à chaque étape à
considérer les arrêtes/sous-chemins dont le poids est minimal.
Le fait d’avoir un poids à chaque arrête permet d’ordonner les voisins que l’on va explorer.
Remarque 139. Soit G = (S, A) un graphe pondéré. On prend Si , Sc deux sommets de G.
Chaque instant, lors de l’exécution de l’algorithme de Dijkstra :
• Les sommets en noir ont leur distance minimale à Si fixée et définitive.
• Les sommets en gris sont dans une file de priorité, selon leur distance par rapport à Si . Cette
distance peut être mise à jour si un père plus proche est trouvé.
La distance à Si de chaque sommet en gris ne pourra que diminuer.
• Les sommets en blanc ne sont pas atteints depuis de Si . Leur distance à Si est fixée à +∞.
En effet, sii le sommet n’est pas atteignable depuis Si , sa distance à Si doit être "infinie".
Définition 140. Pour deux sommets S, S ′ reliés par une arrête, on note l(S, S ′ ) le poids de l’arrête
(S, S ′ ). Pour un chemin reliant Si au sommet S, on notera d(S) la distance parcourue de Si vers S.

• Créer une liste des pères.


• Colorier Si en noir. Lui affecter le poids 0 (distance de Si à Si ).
Pour chaque voisin de Si :
• Colorier le voisin en gris.
• Affecter à chaque voisin un poids (la distance à Si ) et trier la file des sommets gris selon les poids (le plus
léger en premier, le plus lourd en dernier).
Colorier tous les autres sommets en blanc (non explorés). Leur affecter le poids +∞.
Tant qu’il reste des sommets gris :
• Prendre le sommet gris S de poids minimal.
• Colorier S en noir.
Si S = Sc :
• Renvoyer d(S) (la distance de Si à S = Sc ), et le chemin de Si à S. L’algorithme se termine.
Pour chaque voisin S ′ de S :
Si S ′ est blanc :
• Colorier S ′ en gris, l’ajouter à la file des sommets gris avec le poids d(S ′ ) = d(S) + l(S, S ′ ).
• Poser père(S’)=S. # On a trouvé un chemin de Si à S ′ passant par S.

Si S est gris :

51
Lycée du Diadème - Te Tara o Mai’ao PTSI, Année 2023-2024

Si d(S) + l(S, S ′ ) < d(S ′ ) :


• Affecter d(S ′ ) ← d(S) + l(S, S ′ ), et père(S’)=S.
# On avait déjà trouvé un chemin de Si à S’, mais celui passant par S est plus rapide.
L’algorithme se termine par un échec. (Sc n’a pas été trouvé)

Remarque 141. Si l’on retire la condition S == Sc , l’algorithme de Dijkstra calculera les distances
minimales entre Si et n’importe quel point de G relié à Si (ce sont les poids des points coloriés en noir).
La liste des pères fournit quand à elle un chemin de poids minimal entre Si et n’importe quel point de
G (on part du point final et on remonte à Si par les pères). Le chemin entre Si et Sc de poids minimal
n’est qu’une fraction des informations que calcule cet algorithme.
Si le graphe G a n sommets, et si v est le maximum du nombre de voisins pour chaque sommet,
l’algorithme de Dijkstra a une complexité en O(vn ln(n)).

Algorithme Best First


Dans le cas d’un graphe pour lequel les sommets sont des points du plan ou de l’espace (un réseau routier
entre des villes par exemple) et où les poids des arrêtes correspondent plus ou moins à des distances, on
peut utiliser une autre approche pour construire un chemin reliant Si à Sc . Cette approche est celle de
l’algorithme Best First.

Il s’agit d’un algorithme de parcours en profondeur. Pour chaque sommet gris on calcule ∥s − Sc ∥, et le
prochain sommet gris exploré est celui dont la distance à Sc est la plus petite. On regarde ici la distance
entre des points du plan/de l’espace (la distance "à vol d’oiseau", rapide à calculer).
Cet algorithme fournit rapidement un chemin reliant Si et Sc . Ce chemin n’est pas celui de distance
minimale, mais il en est proche.

Algorithme A∗
L’algorithme de Dijkstra détermine le nouveau sommet gris S à explorer en prenant celui dont la dis-
tance à Si (distance selon les arrêtes) est la plus petite. Il fournit toujours la meilleure solution mais
stocke et renouvelle beaucoup d’informations.
L’algorithme Best-First détermine le nouveau point gris S à explorer en prenant celui dont la distance
à Sc (distance dans le plan/l’espace) est la plus petite. Il ne fournit pas la meilleure solution, mais il
demande moins d’étapes et ne renouvelle pas d’informations sur les points gris.

Ces deux approches peuvent être mélangées : c’est l’algorithme A∗ . Cet algorithme est un algorithme
de parcours en profondeur, de structure identique à celle de Dijkstra.
Pour chaque sommet S on stocke deux informations :
• La distance du sommet Si à S selon les poids des arrêtes, d(S).
Si le sommet est blanc on a d(S) = +∞.
Si le sommet est gris, cette distance peut être mise à jour si l’on trouve S ′ un parent de S tel
que d(S ′ ) + l(S ′ , S) < d(S).
Si le sommet est noir, cette distance est optimale.
• La distance euclidienne entre S et Sc : d(S, Sc ) = ∥S − Sc ∥.
Si le sommet est blanc on pose d(S, Sc ) = +∞.
Si le sommet est gris, on calcule ∥S − Sc ∥.
Avec ces deux quantités, on détermine le nouveau sommet gris S à explorer en prenant celui dont la
quantité d(S) + αd(S, Sc ) est la plus petite.
Le paramètre α ≥ 0 sert de pondération entre Dijkstra et Best-First.
Ce type de mélange est souvent utilisé pour combiner les propriétés intéressantes de plusieurs algo-
rithmes traitant d’un même type de problème. Le résultat est bien plus efficace/qualitatif que les deux
algorithmes pris séparément.

52

Vous aimerez peut-être aussi