Vous êtes sur la page 1sur 27

1.

Problèmes et instances
1.2 Exemple
1.2.1 Planarité d’un graphe
Une instance du problème : Imaginons que dans le cadre de l’aménagement du territoire,
on doive choisir le lieu d’implantation de trois usines et le lieu des sources de distribution du
gaz, d’électricité et d’eau. Le problème consiste à établir s’il est possible que le câblage et la
tuyauterie allant des sources aux entreprises soit fait au même niveau du sous-sol et dans
l’affirmative de produire un plan comprenant les usines, les sources et les liens. La figure 1.1
décrit un plan qui conduit à un croisement et ne répond pas aux exigences énoncées.

Figure 1 Un plan de câblage

Le problème. On se donne n lieux à placer sur le plan et m couples de lieux qui doivent être
liés. Le problème à résoudre consiste à déterminer, s’il est possible de placer les lieux et les
liens sur le plan de telle sorte qu’il n’y ait aucun croisement et dans l’affirmative de produire
un tel plan.
1.2.2 Programmation linéaire
Une instance du problème. Supposons qu’une personne doive suivre un régime nutritif en fruits
qui lui garantit un apport quotidien suffisant en vitamines A, B et C.
Cette personne ne mange que trois fruits : des oranges, des bananes et des pommes. Pour chaque
fruit, elle connaît son prix et la quantité de vitamines par kilo comme indiqué dans le tableau 1

Tableau 1 : Informations nécessaires au régime

Le problème à résoudre consiste à déterminer une quantité (éventuellement fractionnaire) de


chaque fruit de telle sorte que l’apport requis en vitamines soit atteint et que le prix du régime
soit minimal. De manière plus formelle, il s’agit de trouver parmi les triplet (xo, xb, xp) qui
vérifient :
30 · xo + 40 · xb + 120 · xp ≥ 15
14 · xo + 14 · xb + 10 · xp ≥ 10
250 · xo + 8 · xb + 44 · xp ≥ 75

celui qui minimise 4 · xo + 2 · xb + 4.5 · xp.


Le problème. On se donne n éléments composés {comp1, . . . , compn} et m éléments de base
{base1, . . . , basem}. La composition d’un élément compi est donnée par {aj,i}1≤j≤m et son coût
est donné par ci. La quantité d’élément de base basej à se procurer est bj . Le problème s’énonce
ainsi. Trouver parmi les tuples (x1, . . . , xn) qui vérifient :

celui qui minimise


1.2.3 Recherche de seuils
Une instance du problème. Supposons que le département de ressources humaines d’une
entreprise maintienne un fichier des employés (déclaré à la CNIL). Ce fichier contient entre
autres le salaire de chaque employé. Le département souhaite établir quel est le seuil
correspondant au 10% des employés les mieux payés. Si cette entreprise a 500 employés, cela
revient à trouver quel est le 50ème plus gros salaire.
Le problème. Soit T un tableau de valeurs numériques (avec répétitions éventuelles) et k un
entier inférieur ou égal à la taille de T. Le problème consiste à déterminer la kième plus grande
valeur du tableau et un indice du tableau contenant cette valeur.
1.2.4 Déduction automatique
Une instance du problème. Supposons connues les affirmations suivantes :
- Tous les chats sont blancs ou noirs.
- Platon est un chat.
- Platon n’est pas noir.
- Aristote est blanc.
On souhaiterait savoir si chacune des deux phrases suivantes est une conséquence des
affirmations précédentes.
- Platon est blanc.
- Aristote est un chat.
Le problème. Etant donnée une logique (e.g., logique propositionnelle ou logique du premier
ordre), un ensemble de formules {ϕi}1 ≤ i ≤ n de cette logique et une formule ϕ, le problème
consiste à déterminer si ϕ se déduit (à l’aide des axiomes et des règles de déduction de cette
logique) de {ϕi}1 ≤ i ≤ n.
1.2.5 Terminaison d’un programme
Une instance du problème. Supposons que nous ayons écrit un programme correspondant à
l’algorithme 1 et qu’avant de l’exécuter, nous souhaitions savoir si son exécution se terminera.

Le problème. Etant donné le texte d’un programme écrit par exemple en JAVA, le problème
consiste à déterminer si ce programme se termine. Plusieurs variantes sont possibles. Ainsi si
le programme comprend une entrée, le problème pourrait consister à déterminer si le
programme se termine pour toutes les entrées possibles.
1.3 Taille d’une instance
L’algorithmique consiste à résoudre des problèmes de manière efficace. Il est donc nécessaire
de définir une mesure de cette efficacité. Du point de vue de l’utilisateur, un algorithme est
efficace si :
1. il met peu de temps à s’exécuter ;
2. il occupe peu de place en mémoire principale.
Cependant ces mesures dépendent de la taille de l’instance du problème à traiter. Il convient
donc de définir la taille d’une instance. Plusieurs définitions sont possibles dans la mesure où
une même instance peut s’énoncer de différentes manières. En toute rigueur, l’efficacité d’un
algorithme devrait prendre en compte non pas l’instance mais sa représentation fournie en
entrée de l’algorithme. Cependant la plupart des représentations raisonnables d’une instance
conduisent à des tailles similaires. Plutôt que de formaliser cette notion, nous la préciserons
pour chaque problème traité.
Si on prend comme unité de mesure le bit, nous commençons par faire quelques hypothèses.
- Le nombre de bits nécessaires pour représenter un caractère est constant (en réalité, il
dépend de la taille de l’alphabet mais celle-ci ne change pas de manière significative).
On le notera Bc.
- Le nombre de bits nécessaires pour représenter un entier est constant. Cette hypothèse
n’est valable que si, d’une part on connaît a priori une borne supérieure de la taille d’un
entier intervenant dans une instance et si, d’autre part les opérations effectuées sur les
entiers par l’algorithme ne conduisent pas à un dépassement de cette borne1 On le notera
Be.
- Dans la plupart des systèmes d’information, les informations stockées dans des fichiers
sont accessibles via des identifiants qui peuvent être des valeurs numériques (e.g. le n˚
de sécurité sociale) ou des chaînes de caractères (e.g. la concaténation du nom et du
prénom). On supposera aussi que le nombre de bits nécessaires pour représenter un
identifiant est constant et on le notera Bi.
Illustrons maintenant la taille d’une instance à l’aide des exemples précédents.
Planarité d’un graphe. Il suffit de représenter les liaisons, i.e. des paires d’identifiants. On
obtient donc 2m · Bi. Cette représentation appelle deux remarques.
D’un point de vue technique, le programme implémentant l’algorithme doit savoir où se termine
la représentation : soit par la valeur m précédant la liste des paires soit par un identifiant spécial
(différent des identifiants possibles) qui suit la liste. Dans les deux cas, on a ajouté un nombre
constant de bits.
D’un point de vue conceptuel, un lieu qui n’apparaît dans aucune liaison, est absent de la
représentation. Vis à vis du problème traité, cela n’est pas significatif car si le plan a pu être
établi, il suffit d’ajouter ces lieux hors de l’espace occupé par le plan.
Programmation linéaire. On précise d’abord m et n, puis les éléments ai,j (ligne par ligne ou
colonne par colonne), les éléments bj et finalement les éléments ci. Ces nombres ne sont pas
nécessairement des entiers. En faisant l’hypothèse que ce sont des rationnels on peut les
représenter sous forme de fractions d’entier, i.e. par deux entiers. On obtient comme taille du
problème (2 + 2m + 2n + 2m · n) · Be.
Recherche de seuils. On précise d’abord n la taille du tableau et k le seuil puis les cellules du
tableau, ce qui nous donne (en supposant que les salaires soient des entiers) (2 + n) · Be.
Déduction automatique. On indique le nombre d’hypothèses puis les hypothèses suivi de la
conclusion. A priori, les formules peuvent être de taille quelconque. En notant |ϕ| le nombre de
caractères d’une formule φ, la taille du problème est alors Be + (Pni=1 |ϕi| + |ϕ|) · Bc.
Terminaison d’un programme. La taille de l’instance est n · Bc où n est le nombre de
caractères du texte du programme.
Remarque importante. Nous nous intéressons au comportement des algorithmes sur des
instances de grande taille. De manière encore plus précise, nous souhaitons estimer la nature de
la variation du temps d’exécution de l’algorithme lorsque la taille de l’instance augmente. Aussi
il est raisonnable de :
- conserver le terme prédominant de la taille d’une instance ;
- dans ce terme, remplacer les constantes multiplicatives par 1.
Ainsi pour le cas de la programmation linéaire, le terme prédominant est 2m · n · Be. Par
conséquent, en oubliant les constantes, on prendra pour la taille d’une instance m · n.
1.4 Algorithmes
Sauf mention du contraire, on considèrera que l’algorithme est défini par une fonction dont les
entrées sont indiquées par le mot-clef Input et les sorties (il peut y en avoir plusieurs) sont
indiqués par le mot-clef Output.
Les déclarations des variables locales de la fonction sont indiquées par le mot-clef Data dans
le corps de la fonction après les entrées et les sorties et avant le bloc d’instructions de la
fonction. L’opérateur d’affectation est noté ← à ne pas confondre avec le test d’égalité = utilisé
pour construire des expressions booléennes. On n’utilisera le « ; » que pour concaténer des
instructions placées sur la même ligne. Les opérateurs de comparaison sont notés =, ≠, <, >, ≤,
≥, les opérateurs arithmétiques sont notés +, ∗ et les opérateurs logiques sont notés and, or, not.
Lorsque nous introduirons un opérateur spécifique à un nouveau type de donnée, nous en
préciserons la signification. Pour l’instant, nous travaillerons avec des tableaux dont la
dimension sera précisée lors de leur définition. Pour accéder à un élément du tableau, on utilise
la notation Tableau[indice].
Les constructeurs de programmes (avec leur interprétation usuelle) sont :

if condition then
instructions
else if condition then (optionnel)
...
else (optionnel)
instructions
end
– while condition do
instructions
end
– repeat
instructions
until condition
– for indice ← valeur initiale to valeur finale do
instructions
end

1.5 Complexité d’un algorithme


1.5.1 Temps d’exécution des instructions
Le temps d’exécution d’un programme est la quantité de temps qui s'est écoulée entre le
démarrage du programme et la fin de son exécution. C’est la même définition pour les
instructions d’un programme.
On considèrera qu’une instruction d’affectation est exécutée en une unité de temps à condition
que l’expression à évaluer ne comporte pas d’appel de fonctions et que la variable à affecter
soit d’un type élémentaire. Dans le cas d’un appel de fonction, il faut ajouter le temps
d’exécution des appels de fonction.
Dans le cas d’un type complexe, il faut tenir compte du type. Ainsi si on affecte un tableau de
taille t, le temps d’exécution sera t.
On considère aussi que l’évaluation d’un test intervenant dans un constructeur s’effectue aussi
en une unité de temps avec les mêmes restrictions que pour l’affectation.
Enfin le temps d’exécution du renvoi du résultat d’une fonction est aussi une unité de temps.
On déduit de ce qui précède les règles suivantes :

- Chaque instruction basique consomme une unité de temps (affectation d’une variable,
comparaison, …)
- Chaque itération d’une boucle rajoute le nombre d’unités de temps consommées dans
le corps de cette boucle.
- Chaque appel de fonction rajoute le nombre d’unités de temps consommées dans cette
fonction
- Pour avoir le nombre d’unité d’opérations effectuées par l’algorithme, on additionne le
tout.

Exemple : Temps de calcul d’une fonction factorielle


n ! = n × (n-1) x (n-2) x … x 1 avec 0 ! = 1
int factorielle(n)
fact = 1 ; initialization(1)
i = 2 ; affectation(1)
while (i <= n) comparaison(1)
fact = fact * i ; affectation + multiplication (2)
i = i + 1 ; affectation + addition (2)
return fact retour (1)

temps de calcul = 1+1+(2+2+1) x (n-1) +1 =5n-2

1.5.2 Problèmes dans la détermination du temps


La déterminer un temps d’exécution pour un algorithme est complexe. Ce temps varie en
fonction de différents paramètres.
L’unité de temps abstrait :
- Dépend des données.
- Dépend de la nature des données : on ne sait pas toujours combien de fois exactement
on va exécuter une boucle.
- De même lors d’un branchement conditionnel, le nombre de comparaisons
effectués n’est pas toujours le même.
Le temps exacte :
- Dépend de la puissance de la machine.
- Dépend de la nature des données (variables): si on change les données, le temps change
Pour résoudre ces problèmes, on étudie le comportement asymptotique du temps d’exécution.
1.5.3 Estimation asymptotique du temps d’exécution
L’Estimation asymptotique du temps d’exécution est un paramètre de la complexité d’un
algorithme. Dans ce cas on veut voir comment évolution du temps d’exécution lorsque n devient
grand. La complexité algorithmique permet de mesurer les performances d’un algorithme et de
le comparer avec d’autres algorithmes réalisant les même fonctionnalités. C’est un concept
fondamental pour tout informaticien, elle permet de déterminer si un algorithme A est meilleur
qu’un algorithme B et s’il est optimal ou s’il ne doit pas être utilisé.

La complexité d’un algorithme est la mesure du nombre d’opérations fondamentales qu’il


effectue sur un jeu de données. Elle est exprimée comme une fonction de la taille du jeu de
données.

Définition la notation O
 Soit T(n) une fonction qui désigne le temps de calcul d’un algorithme A.
 On dit que T(n) est en grand O de f(n) : T(n) =O(f(n)) ssi il existe (no , c) telle que
T(n)< = c * f(n) pour tout n> =no
Exemple
Prouvons que f(n) = 5n + 37 est en O(n)
le but est de trouver une constante c de l’ensemble R et no de l’ensemble N à partir duquel
|f(no)| <= c|no|
On déduit que c=6 fonctionne à partir du no = 37
Dans cet exemple, il est très facile de borner le temps d’exécution afin de trouver la complexité
O. Mais en générale il est difficile de trouver le seuil afin de borner le temps d’exécution. Pour
cela on applique les règles suivante pour simplifier l’expression du temps d’exécution :
- On remplace les constantes multiplicatives par 1
- On annule les constantes additives
- On ne retient que les termes dominants
Exemple
g(n)=4n3 – 5n2 + 2n + 3 = O(n3)
1.5.4 Différente cas de complexité d’un algorithme
Nous notons Dn l’ensemble des données de taille n et T(d) le coût de l’algorithme sur la donnée
d.
Complexité au meilleur : Tmin(n) = mind∈Dn C(d). C’est le plus petit nombre d’opérations
qu’aura à exécuter l’algorithme sur un jeu de données de taille fixée, ici à n. C’est une borne
inférieure de la complexité de l’algorithme sur un jeu de données de taille n.
Complexité au pire : Tmax(n) = maxd∈Dn C(d). C’est le plus grand nombre d’opérations qu’aura
à exécuter l’algorithme sur un jeu de données de taille fixée, ici à n. Par exemple lorsqu’on
parcours toute une liste de taille n pour chercher un élément qui n’y est pas ou qui est le
dernier.
Avantage : il s’agit d’un maximum, et l’algorithme finira donc toujours avant d’avoir effectué
Tmax(n) opérations.
Inconvénient : cette complexité peut ne pas refléter le comportement « usuel » de l’algorithme,
le pire cas pouvant ne se produire que très rarement, mais il n’est pas rare que le cas moyen soit
aussi mauvais que le pire cas

Complexité en moyenne :
C’est la moyenne des complexités de l’algorithme sur des jeux de données de taille n (en toute
rigueur, il faut bien évidemment tenir compte de la probabilité d’apparition de chacun des jeux
de données).

Avantage : reflète le comportement « général » de l’algorithme si les cas extrêmes sont rares
ou si la complexité varie peu en fonction des données.
Inconvénient : la complexité en pratique sur un jeu de données particulier peut être nettement
plus importante que la complexité en moyenne, dans ce cas la complexité en moyenne ne
donnera pas une bonne indication du comportement de l’algorithme.
1.5.5 Classe de complexités classiques
On compare la complexité de n+1 à n. C-à-d on incrémente le paramètre n et regarde le nombre
d’instruction élémentaire qu’il faut rajouter à la complexité de n. soit C le coût d’un algorithme.
- Si C(n+1) = C(n) alors O(1) => complexité constante.
- Si C(n+1) = C(n) + 1 alors O(n) => complexité linéaire
- Si C(n+1) = C(n) + e difficile à conclure mais en continuant l’incrémentation jusqu’à
avoir 2n. C(2n) = C(n) + 1 alors O(log2(n)) => complexité logaritmique
- Si C(n+1) = C(n) + n alors O(n2) => complexité quadratique
- Si C(n+1) = 2 * C(n) alors O(2n) => complexité exponentielle
O(1) correspond à la situation où on doit retourner les k premiers éléments d’une liste.
O(n) => lorsqu’on doit chercher un élément dans une liste non ordonnée (On doit parcourir
toute la liste)
O(log2(n)) => lorsqu’on doit chercher un élément dans une liste ordonnée d’élément
O(n2) traitement sur toutes les paires dans une liste. Pour chaque élément dans la liste, il faut
reparcourir toute la liste et refaire le traitement avec toutes les paires.
O(2n) => nombre de sous ensemble dans un ensemble. C’est ce qui se passe lorsqu’on parcours
les arbres binaires.
1.5.6 Illustration cas du trie par insertion
a- Problématique du trie
Entrée : une séquence de n nombres, a1, ..., an.
Sortie : une permutation, a’1, … , a’n , de la séquence d’entrée, telle que a’1 ≤ a’2 ≤ … ≤ a’n

b- Principe du tri par insertion


De manière répétée, on retire un nombre de la séquence d’entrée et on l’insère à la bonne place
dans la séquence des nombres déjà triés (ce principe est le même que celui utilisé pour trier une
poignée de cartes).

c- Algorithme

d- Exemple
Soit le tableau suivant : [5 ; 2 ; 4 ; 6 ; 1 ; 3]
e- Complexité
Nous passons en revue les différentes étapes de notre algorithme afin d’évaluer son temps
d’exécution. Pour ce faire, nous attribuons un coût en temps à chaque instruction, et nous
comptons le nombre d’exécutions de chacune des instructions. Pour chaque valeur de j ∈ [2,n],
nous notons tj le nombre d’exécutions de la boucle tant que pour cette valeur de j. Il est à noter
que la valeur de tj dépend des données
Complexité au meilleur : le cas le plus favorable pour l’algorithme TRI-INSERTION est
quand le tableau est déjà trié, comme le montre le cas j = 4 du tableau ci-dessus. Dans ce cas
tj = 1 pour tout j.

T(n) peut ici être écrit sous la forme T(n) = an + b, a et b étant des constantes indépendantes
des entrées, et T(n) est donc une fonction linéaire de n.
Complexité au pire : le cas le plus défavorable pour l’algorithme TRI-INSERTION est quand
le tableau est déjà trié dans l’ordre inverse, comme le montre le cas j = 5 tableau ci-dessus.
Dans ce cas tj = j pour tout j.

T(n) peut ici être écrit sous la forme T(n) = an2 + bn + c, a, b et c étant des constantes, et T(n)
est donc une fonction quadratique de n.
f- Ordre de grandeur
Ce qui nous intéresse vraiment, c’est l’ordre de grandeur du temps d’exécution. Seul le terme
dominant de la formule exprimant la complexité nous importe, les termes d’ordres inférieurs
n’étant pas significatifs quand n devient grand. On ignore également le coefficient
multiplicateur constant du terme dominant. On écrira donc, à propos de la complexité du tri par
insertion :
 meilleur cas : Θ(n).
 pire cas : Θ(n2).
 en moyenne : Θ(n2).

En général, on considère qu’un algorithme est plus efficace qu’un autre si sa complexité
dans le pire cas a un ordre de grandeur inférieu
2. Algorithmes récursifs
Un algorithme est dit récursif lorsqu’il est défini en fonction de lui-même.
2.2 Principe
- Une fonction est dite récursive si elle comporte, dans son corps, au moins un appel à
elle-même. Exemple pour calculer la somme des entiers de 1 à n, on calcul la somme
jusqu’à n-1 et on ajoute n ; (n + somme(n-1)).
- Une fonction peut-être d’une récursivité terminale ou non terminale. Exemple

Récursivité terminale
Fonction Bonjour(n)
Début
Si (n==0)
Alors Ecrire (‘’Bonjour’’)
Sinon Bonjour(n-1)
Fin

Récursivité non terminale


Fonction Bonjour(n)
Début
Si (n>0)
Alors Bonjour(n-1)
Ecrire (‘’Bonjour’’)
Finsi
Fin
- Lorsque f s’appelle elle-même on parle de récursivité directe
- Lorsque f appelle g qui appelle f on parle de récursivité indirecte

2.3 Implémentation
Pour implémenter une fonction récursive, il faut non seulement veiller à ce que la fonction
s’appelle elle-même, mais surtout identifier une condition d’arrêt. Exemple
Fonction Bonjour(n)
Début
Si (n==0)
Alors Ecrire (‘’Bonjour’’)
Sinon Bonjour(n-1) Fin
Dans l’algorithme ci-dessus lorsque n=0, on s’arrête. Arrive-t-on à n=0 ? la condition
terminale ne sert à rien si elle ne devient jamais vraie. Exemple Bonjour(-2) avec l’algo ci-
dessus, il y aura une infinité d’exécution. Pour éviter cela, il faut ajouter une condition dans la
boucle :
Fonction Bonjour(n)
Début
Si (n < 0)
Alors Ecrire(‘’erreur d’exécution’’)
Sinon
Si (n==0)
Alors Ecrire (‘’Bonjour’’)
Sinon Bonjour(n-1)
FinSi
FinSi
Fin
Théorème de Gödel : Il n’existe pas de moyen automatique pour savoir si un programme
termine ou pas. Si aucune méthode n’est générale, le principe de récurrence aide souvent.
2.4 Exemple d’algorithme récursif : les tours de Hanoï
Le problème
Le jeu est constitué d’une plaquette de bois où sont plantées trois tiges. Sur ces tiges sont enfilés
des disques de diamètres tous différents. Les seules règles du jeu sont que l’on ne peut déplacer
qu’un seul disque à la fois, et qu’il est interdit de poser un disque sur un disque plus petit. Au
début tous les disques sont sur la tige de gauche, et à la fin sur celle de droite.
Algorithme

HANOÏ (n, départ, intermédiaire, destination)


Si n = 1 alors déplacer le disque supérieur de la tige départ vers la tige destination
sinon HANOÏ (n-1, départ, destination, intermédiaire)
déplacer le disque supérieur de la tige départ vers la tige destination
HANOÏ (n-1, intermédiaire, départ, destination)

Complexité
On compte le nombre de déplacements de disques effectués par l’algorithme HANOÏ invoqué
sur n disques.

d’où l’on en déduit que C(n) = 2n-1. On a donc ici un algorithme de complexité exponentielle.
2.5 Diviser pour régner
La méthode de diviser pour régner est une m´méthode qui permet, parfois de trouver des
solutions efficaces à des problèmes algorithmiques. L’idée est de découper le problème initial,
de taille n, en plusieurs sous-problèmes de taille sensiblement inférieure, puis de recombiner
les solutions partielles.
a- Exemple Algorithme de trie par fusion
L’exemple typique est l’algorithme de tri fusion : pour trier un tableau de taille n, on le découpe
en deux tableaux taille n/2 et l’étape de fusion permet de recombiner les deux solutions en n −
1 opérations. On peut l’écrire ainsi :
b- Complexité
On compte le nombre T(n) de comparaison effectuées par l’algorithme

c’est sensiblment égale car il y a des partie entière à


considérer pour être plus rigoureux

2.6 Forme générale et théorème Maître


La forme générale considérée dans ce cours vaêtre :
- Diviser : on d´découpe le problème en a sous-problèmes de tailles n/b, qui sont de
même nature, avec a ≥ 1 et b > 1.
- Régner : les sous-problèmes sont résolus récursivement.
- Recombiner : on utilise les solutions aux sous-problèmes pour reconstruire la solution
au problème initial en temps O(nd), avec d ≥ 0.
L’´équation qu’on aura à résoudre quand on traduit le programme en équation sur la
complexité est :

Ce type d’équation est résolue par le théorème maître énoncé comme suit
Théorème Maître :

On considère l’équation soit


 Par exemple, pour le tri fusion, on a a = 2, b = 2, λ = d = 1 et donc une complexité de
O(n logn).
 En pratique, seuls les cas 1. et 2. peuvent mener à des solutions algorithmiques
intéressantes. Dans le cas 3., tout le coût est concentré dans la phase “recombiner”, ce
qui signifie souvent qu’il y a des solutions plus efficaces.

a- Exemples : Recherche par dichotomie


Si T est un tableau trié de taille n, on s’intéresse à l’algorithme qui recherche si x ∈ T au moyen
d’une dichotomie. Pour l’algorithme récursif, on spécifie un indice de début d et de fin f, et on
recherche si x est dans T entre les positions d et f. L’appel initial se fait avec d = 0 et f = n − 1.

On identifie les paramètres : a = 1 car on appelle soit à gauche, soit à droite (ou on a fini, mais
on se place dans le pire des cas), b = 2 car les sous-problèmes sont de taille n/2 et d = 0 car on
se contente de renvoyer la solution, donc en temps constant. La complexité de la dichotomie
est donc O(log n).
b- Exponentielle rapide
En informatique, l'exponentiation rapide est un algorithme utilisé pour calculer rapidement de
grandes puissances entières. Il s’agit de calculer xn pour x et n donnés, en calculant la
complexité par rapport à n. La méthode naïve (multiplier n fois 1 par x) donne une complexité
linéaire. On peut faire mieux en utilisant le fait que

On peut directement traduire cette constatation en algorithme.


Et on retrouve les mêmes paramètres pour le théorème maître que dans le cas de la dichotomie.
La complexité de l’exponentiation rapide est donc en O(n log n). (O(n))
3. Les arbres
Un arbre est une structure de données composée de nœuds reliés entre eux par des branches,
selon une organisation hiérarchique, à partir d’un nœud racine.

a- Degré d’un nœud


Par définition le degré d'un nœud est égal au nombre de ses descendants (enfants). Soient les
deux exemples ci-dessous

Par définition c'est le nombre de nœuds du chemin le plus long dans l'arbre. La hauteur h d'un
arbre correspond donc au nombre de niveau maximum
b- Degré d’un arbre
Le degré d'un arbre est égal au plus grand des degrés de ses nœuds :

c- Taille d’un arbre


On appelle taille d'un arbre le nombre total de noeuds de cet arbre :

3.2 Parcours d’un arbre


a- Parcours en profondeur
Les parcours en profondeur se définissent de manière récursive sur les arbres. Le parcours d'un
arbre consiste à traiter la racine de l'arbre et à parcourir récursivement les sous-arbres gauche
et droit de la racine. Les parcours préfixe, infixe et suffixe se distinguent par l'ordre dans lequel
sont faits ces traitements.
- Parcours préfixe
Dans le parcours préfixe, la racine est traitée avant les appels récursifs sur les sous-arbres
gauche et droit (faits dans cet ordre). Le parcours préfixe de l'arbre ci-dessus parcourt les nœuds
dans l'ordre [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].
- Parcours infixe
Dans le parcours infixe, le traitement de la racine est fait entre les appels sur les sous-arbres
gauche et droit. Le parcours infixe de l'arbre ci-dessus parcourt les nœuds dans l'ordre
[3,2,1,5,4,6,7,0,9,11,10,12,8,14,13,15].

- Parcours suffixe
Dans le parcours suffixe, la racine est traitée après les appels récursifs sur les sous-arbres
gauche et droit (faits dans cet ordre). Le parcours suffixe de l'arbre ci-dessus parcourt les nœuds
dans l'ordre [3,2,5,7,6,4,1,11,12,10,9,14,15,13,8,0].
b- Parcours en largeur
Le parcours en largeur consiste à parcourir l'arbre niveau par niveau. Les nœuds de niveau 0
sont sont d'abord parcourus puis les nœuds de niveau 1 et ainsi de suite. Dans chaque niveau,
les nœuds sont parcourus de la gauche vers la droite. Le parcours en largeur de l'arbre ci-dessus
parcours les nœuds dans l'ordre [0,1,8,2,4,9,13,3,5,6,10,14,15,7,11,12].
3.3 Algorithme de parcours d’un arbre
a- préfixe
Procédure Préfixe(A: arbre)
Début
Si (NON est_arbre_vide (A)) alors
Ecrire (Racine (A))
Préfixe (FilsGauche (A))
Préfixe (FilsDroite(A))
Finsi
Fin

b- infixe
Procédure Infixe(A: arbre)
Début
Si (NON est_arbre_vide (A)) alors
Infixe (FilsGauche (A))
Ecrire (Racine (A))
Infixe (FilsDroite(A))
Finsi
Fin

c- suffixe

Procédure suffixe(A: arbre)


Début
Si (NON est_arbre_vide (A)) alors
suffixe (FilsGauche (A))
suffixe (FilsDroite(A))
Ecrire (Racine (A))
Finsi
Fin

d- parcours en largeur
l’algorithme de parcours en largeur se fera dans la section où sera vue la file.
3.4 Arbre binaire de recherche
Un arbre binaire de recherche (ABR) est un arbre binaire ordonné talque pour tout nœud n:
- Toutes les valeurs du sous arbre gauche de n sont inférieures ou égales à la valeur de n
- Toutes les valeurs du sous arbre droit de n sont supérieures ou égales à la valeur de n

NA: Généralement, les valeurs dans un ABR sont uniques; on n’admet pas de répétition de
valeur pour éviter les confusion . Mais si jamais ceci arrive : par exemple si un arbre contient
deux fois la valeur 4, par convention, la deuxième valeur est stockée dans le sous-arbre droit
ayant pour racine 4
3.5 Opération sur arbre binaire de recherche (ABR)
a- Recherche d’un élément dans un arbre (ABR)
La recherche est dichotomique, à chaque étape, un sous arbre est éliminé.

Fonction Recherche_rec (A: arbre, x: entier) : booleen


Début
si (A = Nil) alors
retourner (faux)
sinon
si (A-> val = x) alors
retourner (vrai)
sinon
si (A-> val < x)
retourner (Recherche_rec (A-> FD, x))
sinon
retourner(Recherche_rec (A-> FG, x))
Finsi
Finsi
Finsi
Fin
Cette fonction permet de rechercher un élément x dans un ABR et retourner un booléen. Pour
les ABR, les éléments sont stockés de façon à respecter une relation d’ordre, cela rend la
recherche plus beaucoup plus efficace que pour les arbre binaires quelconques.
Version itérative
Dans un ABR, on sait de façon précise s’il faut continuer la recherche à gauche ou à droite pour
chaque nœud exploré, alors il est possible d’écrire une version itérativede recherche dans un
ABR.

Fonction Recherche_iter (A: arbre, x: entier) : booleen


Début
Tant que (A != Nil) et (x != Racine (A)) faire
si (A-> val < x) alors
A ← A-> FD
sinon
A ← A-> FG
Finsi
Fintq
si (A = Nil) alors
retourner (faux)
sinon
retourner (vrai)
Finsi
Fin

b- Opération élémentaire sur ABR


 Insertion d’un élément dans un ABR
Pour insérer un nouvel élément dans un ABR il faut d’abord repérer sa place dans l’arbre, il
faut donc le comparer aux éléments déjà existants dans l’ABR. Enfin l’insérer comme fils du
dernier nœud visité.
Procédure Inserer_rec (var A: arbre, e: entier) Var P: arbre
Début
si (A = Nil) alors
allouer (A)
A-> val ← e
A-> FG ← Nil
A-> FD ← Nil
sinon
si (A-> val > e) alors
Inserer_rec ( A-> FG, e)
sinon
Inserer_rec ( A-> FD, e)
Finsi
Finsi
Fin

 suppression d’un élément d’un ABR


 La suppression dans un ABR est assez compliqué, c’est pour cela que nous allons
détailler tous les cas possibles.
 Pour supprimer un nœud dans un ABR, plusieurs cas de figure peuvent se présenter. Il
est toutefois nécessaire d’obtenir un ABR à l’issue de la suppression.
 D’abord il faut chercher l’élément à supprimer, une fois trouvé on se trouve dans l’une
des situations suivantes, soit « i » le nœud à supprimer:
 1er cas : i est une feuille : on la supprime et on la remplace par Nil.
 2eme cas : i est un nœud qui a un seul fils : on supprime i et on le remplace par
ce fils.
 3eme cas : i est un nœud qui a deux fils : on supprime i et on le remplace par
l’élément minimum se trouvant dans son sous arbre droit (le nœud le plus à
gauche du sous arbre droit)ou par l’élément maximum se trouvant dans son sous
arbre gauche (le nœud le plus à droite du sous arbre gauche).
4. Une pile
Structure en LIFO (Last in First Out)
4.2 Créer une pile
4.3 Initialiser une pile
4.4 Vérifier si une pile est vide
4.5 Empiler
4.6 Désempiler
4.7 Exercide d’application
a- Exo 1
b- Exo 2
c- Exo 3
5. Une file
5.2 Création d’une file
5.3 Initialiser une file
5.4 Vérifier si une file est vide
5.5 Enfiler
5.6 Défiler
5.7 Exercice d’application
a- Exo 1
6. Parcours d’un arbre en largeur en utilisant une file

7. Algorithme du plus court chemin : Algorithme de Dijkstra


7.2 Principe
L’algorithme de Dijkstra (prononcé Dextra) est un algorithme de recherche de chemin le plus
court dans un graphe pondéré, c’est-à-dire un réseau de nœuds connectés par des arêtes ayant
des poids ou des coûts associés. L’algorithme détermine le chemin le plus court entre un nœud
de départ et tous les autres nœuds du graphe.
Cet algorithme utilise une approche gloutonne pour trouver le chemin optimal. Il attribue
initialement une distance infinie à tous les nœuds, sauf au nœud de départ qui a une distance de
0. Ensuite, à chaque itération, l’algorithme sélectionne le nœud non visité avec la distance la
plus faible, appelé le « nœud courant ». Il met à jour les distances des nœuds voisins en les
comparant à la distance actuelle du nœud courant, plus le poids de l’arête les reliant. Si la
nouvelle distance est plus petite, elle est mise à jour. L’algorithme répète ce processus jusqu’à
ce que tous les nœuds aient été visités ou que la distance minimale vers le nœud d’arrivée soit
trouvée. Il est largement utilisé dans les applications de routage dans les réseaux de
télécommunications, la planification de trajets dans les systèmes de transport et d’autres
domaines où la recherche de chemins optimaux est nécessaire.
7.3 Tableau de Dijkstra
Le tableau se présente comme suite selon qu’on choisisse d’aller de la gauche vers la droite ou
de la droite vers la gauche. En colonne se trouve les sommet du graphe et en ligne se trouve les
étapes.
La distance du nœud de départ est de 0 ;
 La distance des autres nœuds est initialement infinie ou une valeur très élevée pour
représenter l’infini ;
 Le nœud de départ n’a pas de nœud précédent.
Ensuite, il faut marquer tous les nœuds comme non visités. Puis, répéter les étapes suivantes
jusqu’à ce que tous les nœuds aient été visités :
 Sélectionner le nœud non visité avec la plus petite distance. Cela peut être fait en
parcourant le tableau et en recherchant le nœud avec la plus petite distance non visitée;
 Marquer ce nœud comme visité ;
 Mettre à jour les distances des nœuds voisins non visités. Ici, il faut calculer la distance
temporaire en ajoutant la distance actuelle du nœud sélectionné à la distance de l’arête
qui relie ce nœud à un nœud voisin. Si la distance temporaire est plus petite que la
distance actuelle du nœud voisin, mettez à jour sa distance avec la distance temporaire
et mettez le nœud sélectionné comme son nœud précédent.
Avec le tableau de Dijkstra rempli, il est désormais possible de déterminer le chemin le plus
court en suivant les nœuds précédents à partir du nœud d’arrivée jusqu’au nœud de départ.
Application à faire avec moi en présentielle
8. Une Liste chaînée
a- Définition
Une liste chaînée est un ensemble d’éléments contenus chacun dans une cellule (ou nœud ou
maillon). Chaque cellule contient, en plus de l’élément, l’adresse de l’élément suivant appelée
pointeur.
Contrairement au tableau, :
- la liste peut stocker plusieurs valeurs de même type de taille variable.
- Les éléments d'une liste chaînée ne sont pas placés côte à côte dans la mémoire. Chaque
case pointe vers une autre case en mémoire, qui n'est pas nécessairement stockée juste
à côté.
Une liste chaînée est un moyen d'organiser une série de données en mémoire. Cela consiste à
assembler des structures en les liant entre elles à l'aide de pointeurs.

Il existe trois types de listes:


 Les listes simplement chaînées: Dans ce type de listes, les éléments sont chaînés à l’aide
d’un seul pointeur afin de parcourir la liste du premier au dernier élément.

 Les listes doublement chaînées: Dans ces listes, les éléments sont chaînées à l’aide de
deux pointeurs, et non un seul, ce qui permet de parcourir la liste dans les deux sens.
 Les listes circulaires: dans ces listes, le dernier élément pointe sur le premier.
8.2 Les opérations
Les code seronts en C
a- Création d’une liste

typedef struct Element Element;


struct Element
{
int nombre;
Element *suivant;
};

Description de la structure
 Une donnée, ici un nombre de type int: on pourrait remplacer cela par n'importe quelle
autre donnée (un double, un tableau…). Cela correspond à ce que vous voulez stocker,
c'est à vous de l'adapter en fonction des besoins de votre programme.

D’une manière générique, l'idéal est de faire un pointeur sur void :void*. Cela permet
de faire pointer vers n'importe quel type de données.

 Un pointeur vers un élément du même type appelé suivant. C'est ce qui permet de lier
les éléments les uns aux autres : chaque élément sait où se trouve l'élément suivant en
mémoire.

En revanche, il ne sait pas quel est l'élément précédent, il est donc impossible de
revenir en arrière à partir d'un élément avec ce type de liste

b- Créez également la structure de contrôle

typedef struct Liste Liste;


struct Liste
{
Element *premier;
};
Cette structure Liste contient un pointeur vers le premier élément de la liste.
c- Initialiser une liste
La fonction d'initialisation est la toute première que l'on doit appeler. Elle crée la structure de
contrôle et le premier élément de la liste :

Liste *initialisation()
{
Liste *liste = malloc(sizeof(*liste));
Element *element = malloc(sizeof(*element));

if (liste == NULL || element == NULL)


{
exit(EXIT_FAILURE);
}

element->nombre = 0;
element->suivant = NULL;
liste->premier = element;

return liste;
}

On commence par créer la structure de contrôle liste.


Description
Le type de données est Liste et la variable s'appelle liste. La majuscule permet de les
différencier.
malloc : alloue dynamique de la structure de contrôle
sizeof(*liste) : calcule automatiquement la taille à allouer. L'ordinateur saura qu'il doit allouer
l'espace nécessaire au stockage de la structure Liste.
On alloue ensuite de la même manière la mémoire nécessaire au stockage du premier élément.
On vérifie si les allocations dynamiques ont fonctionné. En cas d'erreur, on arrête
immédiatement le programme en faisant appel à exit().
Si tout va bien, on obtient
d- Ajout d’un nouveau élément
On peut ajouter un nouveau élément au début et à la fin de l’initialisation. Nous allons choisir
d’ajouter au début parce que c’est plus simple à comprendre

void insertion(Liste *liste, int nvNombre)


{
/* Création du nouvel élément */
Element *nouveau = malloc(sizeof(*nouveau));
if (liste == NULL || nouveau == NULL)
{
exit(EXIT_FAILURE);
}
nouveau->nombre = nvNombre;

/* Insertion de l'élément au début de la liste */


nouveau->suivant = liste->premier;
liste->premier = nouveau;
}

La fonction insertion() prend en paramètres :


 l'élément de contrôle liste(qui contient l'adresse du premier élément) ;
 et le nombre à stocker dans le nouvel élément que l'on va créer.
Dans un premier temps, on alloue l'espace nécessaire au stockage du nouvel élément et on y
place le nouveau nombre nvNombre. Il reste alors une étape délicate : l'insertion du nouvel
élément dans la liste chaînée. Nous avons ici choisi pour simplifier d'insérer l'élément en début
de liste. Pour mettre à jour correctement les pointeurs, nous devons procéder dans cet ordre
précis :
 Faire pointer notre nouvel élément vers son futur successeur, qui est l'actuel premier
élément de la liste.
 Faire pointer le pointeur premier vers notre nouvel élément.
e- Supprimez un élément
La suppression ne pose pas de difficulté supplémentaire. Il faut cependant adapter les pointeurs
de la liste dans le bon ordre pour ne perdre aucune information :

void suppression(Liste *liste)


{
if (liste == NULL)
{
exit(EXIT_FAILURE);
}

if (liste->premier != NULL)
{
Element *aSupprimer = liste->premier;
liste->premier = liste->premier->suivant;
free(aSupprimer);
}
}

Vous aimerez peut-être aussi