I. Introduction et définitions de base▲
I-A. Algorithme▲
Un algorithme est une suite finie et non ambiguë d'opérations visant la résolution d'un problème. On le formule généralement comme la transformation d'un ensemble de valeurs d'entrée en un ensemble de valeurs de sortie.
Un algorithme est dit totalement correct lorsque, pour chaque instance d'un problème, il se termine en produisant la bonne sortie. Il sera dit partiellement correct si, quand il se termine, il donne la bonne sortie, sans assurer qu'il se termine. Il sera approximatif s'il ne donne qu'une approximation de la solution (comme les algorithmes numériques).
Pour tout algorithme, on se pose finalement trois questions : sa correction, sa vitesse d'exécution et la possibilité de faire mieux.
I-B. Structure de données▲
Une structure de données est une méthode de stockage et d'organisation des données pour en faciliter l'accès et la modification. Elle regroupe des données à gérer et un ensemble d'opérations qu'on peut leur appliquer. En général, il existe plusieurs manières de représenter ces données et plusieurs implémentations de leur manipulation.
L'interface sera définie dans un type de données abstrait (TDA). Il spécifie précisément la nature et les propriétés des données à stocker et les modalités des opérations.
I-C. Récursivité▲
Un algorithme est dit récursif s'il s'invoque lui-même (directement ou non - récursif multiple). Cela permet de simplifier l'expression de certains algorithmes.
Une procédure sera récursive terminale si elle n'effectue plus aucune opération après s'être invoquée récursivement.
II. Outils d'analyse des algorithmes▲
II-A. Analyse de la correction▲
La correction d'un algorithme s'étudie par rapport à un problème donné. Un algorithme sera correct pour une instance d'un problème s'il produit une solution correcte pour cette instance. Il sera correct pour un problème s'il est correct pour toute instance de ce problème.
II-A-1. Types d'analyse▲
On peut vérifier cette correction de manière empirique, en testant concrètement l'algorithme (il faut alors l'implémenter et trouver suffisamment d'instances du problème), ou de manière formelle, mathématique. On se focalisera sur cette deuxième approche.
II-A-2. Analyse formelle▲
Une assertion est une relation entre les variables qui est vraie à un moment dans l'exécution. Notamment, on compte les pré- et postconditions (conditions que doivent remplir les données d'entrée et de sortie) kitxmlcodeinlinelatexdvp{P}finkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvp{Q}finkitxmlcodeinlinelatexdvp.
Ainsi, un code sera correct si le triplet de Hoare kitxmlcodeinlinelatexdvp{P} code {Q}finkitxmlcodeinlinelatexdvp est vrai. Pour vérifier cette propriété, on insère des assertions kitxmlcodeinlinelatexdvpP_{n}finkitxmlcodeinlinelatexdvp décrivant l'état des variables à chaque étape du programme et on vérifiera que cette suite de triplet est correcte :
kitxmlcodelatexdvp\{P\}\quad S_{1}\quad\{P_{1}\}finkitxmlcodelatexdvp kitxmlcodelatexdvp\{P_{1}\}\quad S_{2}\quad\{P_{3}\}finkitxmlcodelatexdvp kitxmlcodelatexdvp\dotsfinkitxmlcodelatexdvp kitxmlcodelatexdvp\{P_{n-1}\}\quad S_{n}\quad\{Q\}finkitxmlcodelatexdvpPour les boucles, on mettra en évidence une assertion particulière, kitxmlcodeinlinelatexdvp{I}finkitxmlcodeinlinelatexdvp , l'invariant de boucle, qui décrit l'état du programme pendant l'exécution. Cet invariant est vrai après l'initialisation de la boucle, après chaque itération (kitxmlcodeinlinelatexdvp\text{{I et B} code {I}}finkitxmlcodeinlinelatexdvp est vrai) et à la sortie de la boucle (kitxmlcodeinlinelatexdvp\text{{I et non B} fin {I}}finkitxmlcodeinlinelatexdvp est vrai). En général, l'algorithme découle de l'invariant, de l'idée de base à la conception.
II-A-3. Terminaison des boucles▲
Une fois qu'on a prouvé que la boucle était correcte, il faut s'assurer qu'elle se termine. On cherchera donc une fonction de terminaison kitxmlcodeinlinelatexdvpffinkitxmlcodeinlinelatexdvp :
- définie sur base des variables de l'algorithme ;
- à valeur naturelle ;
- décroissante suite à l'exécution du corps de la boucle ;
- à valeur positive tant que le gardien de la boucle est vrai.
Ainsi, quand kitxmlcodeinlinelatexdvpffinkitxmlcodeinlinelatexdvp atteindra zéro, on aura infirmé le gardien et la boucle sera terminée.
II-A-4. Preuve par induction et algorithmes récursifs▲
La preuve par induction se passe en deux étapes :
- cas de base : on montre explicitement que la propriété est vraie pour une série d'instances ;
- cas inductif : on suppose que la propriété est vraie pour les instances précédentes du problème et on montre qu'elle l'est aussi pour l'instance suivante.
Cette manière de procéder est correcte, car le corps des naturels kitxmlcodeinlinelatexdvp\mathbb{N}finkitxmlcodeinlinelatexdvp est bien ordonné par la relation kitxmlcodeinlinelatexdvp\leqfinkitxmlcodeinlinelatexdvp (dans tout sous-ensemble de kitxmlcodeinlinelatexdvp\mathbb{N}finkitxmlcodeinlinelatexdvp, il y a un plus petit élément).
II-B. Analyse de la complexité en temps▲
On peut la mesurer de manière expérimentale en implémentant l'algorithme et en le lançant sur des instances du problème. Cependant, les temps de calcul vont largement dépendre de l'implémentation, de la machine, du compilateur, des données, etc.
On préférera donc une manière plus théorique de procéder, sur base d'un modèle de calcul abstrait, la RAM (Random Access Machine). Les types de base sont limités aux entiers et nombres flottants avec une taille maximale. Une série d'opérations de base est définie ; chacune prend un temps constant et une série d'instructions est toujours exécutée de manière sérielle.
On distingue plusieurs types de calcul de complexité : le cas le plus défavorable (on considère le temps maximal pour toute instance d'une certaine taille), le plus favorable (on considère le temps minimal pour toute instance d'une certaine taille) et moyen (on considère le temps pour toute instance d'une certaine taille pondéré par la probabilité de cette instance).
II-C. Analyse asymptotique▲
On se contentera d'une analyse asymptotique, en considérant des instances très grandes. Cela simplifiera les calculs : on ne garde que le terme dominant en en ignorant le coefficient. En effet, même en augmentant considérablement la puissance de calcul, cela ne fera pas varier de manière significative le temps d'exécution de l'algorithme.
On utilisera trois notations :
- kitxmlcodeinlinelatexdvpO(g)=\left\{ f\,\Big|\,\exists c>0,\, n_{0}\geq1\,:\,0\leq f(n)\leq c\times g(n),\,\forall n\geq n_{0}\right\}finkitxmlcodeinlinelatexdvp, on définit une borne supérieure asymptotique à la fonction ;
- kitxmlcodeinlinelatexdvp\Omega(g)=\left\{ f\,\Big|\,\exists c>0,\, n_{0}\geq1\,:\,0\leq c\times g(n)\leq f(n),\,\forall n\geq n_{0}\right\}finkitxmlcodeinlinelatexdvp, on définit une borne inférieure asymptotique à la fonction ;
- kitxmlcodeinlinelatexdvp\Theta(g)=\left\{ f\,\Big|\,\exists c_{1}>0,\, c_{2}>0,\, n_{0}\geq1\,:\,0\leq c_{1}\times g(n)\leq f(n)\leq c_{2}\times g(n),\,\forall n\geq n_{0}\right\}finkitxmlcodeinlinelatexdvp, on définit une borne serrée asymptotique à la fonction.
II-D. Analyse asymptotique serrée▲
On peut utiliser une analyse asymptotique plus précise que les notations introduites précédemment : par exemple, kitxmlcodeinlinelatexdvp2\, n=O\left(n^{2}\right)finkitxmlcodeinlinelatexdvp, alors que, asymptotiquement, la différence entre les deux fonctions est énorme. Avec la notation kitxmlcodeinlinelatexdvpOfinkitxmlcodeinlinelatexdvp, seules quelques constantes kitxmlcodeinlinelatexdvpcfinkitxmlcodeinlinelatexdvp peuvent convenir pour satisfaire kitxmlcodeinlinelatexdvp0\leq f\left(n\right)\leq c\, g\left(n\right)finkitxmlcodeinlinelatexdvp. En notation kitxmlcodeinlinelatexdvpofinkitxmlcodeinlinelatexdvp, cette relation est valable pour toute constante kitxmlcodeinlinelatexdvpc>0finkitxmlcodeinlinelatexdvp.
On introduit donc les notations suivantes.
- kitxmlcodeinlinelatexdvpo\left(g\right)=\left\{ f\,\Big|\,\forall c>0,\exists n_{0}>0:0\leq f\left(n\right)<c\, g\left(n\right),\forall n\geq n_{0}\right\} finkitxmlcodeinlinelatexdvp ; notamment, cela implique que kitxmlcodeinlinelatexdvp\lim\limits _{n\to\infty}\dfrac{f\left(n\right)}{g\left(n\right)}=0finkitxmlcodeinlinelatexdvp.
- kitxmlcodeinlinelatexdvp\omega\left(g\right)=\left\{ f\,\Big|\,\forall c>0,\exists n_{0}>0:0\leq c\, g\left(n\right)<f\left(n\right),\forall n\geq n_{0}\right\} finkitxmlcodeinlinelatexdvp ; notamment, cela implique que kitxmlcodeinlinelatexdvp\lim\limits _{n\to\infty}\dfrac{f\left(n\right)}{g\left(n\right)}=\inftyfinkitxmlcodeinlinelatexdvp.
III. Algorithmes de tri▲
III-A. Types de tri▲
On peut assigner une série d'adjectifs aux algorithmes de tri en fonction de leurs caractéristiques :
- interne : en mémoire centrale, par opposition à externe (où les données ne sont pas accessibles aussi rapidement) ;
- de tableau : qui trie une structure de données offrant un accès constant à ses éléments ;
- générique : qui peut trier n'importe quel type d'objets comparables, sans hypothèse sur leur valeur ;
- comparatif : basé sur la comparaison des clés des éléments ;
- itératif : basé sur un ou plusieurs parcours itératifs du tableau, par opposition à récursif ;
- en place : qui modifie directement la structure en cours de tri sans la dupliquer ;
- stable : qui conserve l'ordre relatif des éléments égaux.
III-B. Tri par insertion▲
L'invariant : on considère la partie A[1..j-1] du tableau triée. À chaque itération, on va insérer la clé A[j] à sa bonne place en décalant d'un rang vers la droite tous les éléments plus grands que cette clé. Une fois qu'on trouve un élément égal ou plus petit, on l'insère juste après.
Cet algorithme a une complexité kitxmlcodeinlinelatexdvp\Theta(n^{2})finkitxmlcodeinlinelatexdvp dans les pire et moyen cas ; dans le meilleur, il suffit de comparer tous les éléments et la complexité est de kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp.
for j = 2 to A:length
key = A[j]
// Insert A[j] into the sorted sequence A[1..j-1].
i = j - 1
while i > 0 and A[i] > key
A[i + 1] = A[i]
i = i - 1
A[i + 1] = key
III-C. Tri par fusion▲
On sépare le tableau en deux sous-tableaux de même taille, que l'on trie récursivement ; ensuite, on fusionne les deux sous-tableaux en gardant l'ordre (application du principe de diviser pour régner). Le cas de base correspond au tri d'un seul élément, qui ne nécessite aucune opération.
Pour la fusion des tableaux, on prend trois paramètres tels que kitxmlcodeinlinelatexdvpp\leq q<rfinkitxmlcodeinlinelatexdvp, en sachant que les deux sous-tableaux à fusionner (A[p..q] et A[q+1..r]) sont déjà triés.
if p < r
q = round down (p+r)/2
MergeSort(A, p, q)
MergeSort(A, q + 1, r)
Merge(A, p, q, r)
n1 = q - p + 1
n2 = r - q
Let L[1..n1 + 1] and R[1..n2 + 1] be two new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
L[n1 + 1] = 1
R[n2 + 1] = 1
i = 1
j = 1
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
III-D. Tri rapide▲
Il s'agit d'un tri en place et non stable appliquant le principe du diviser pour régner. Sa complexité est kitxmlcodeinlinelatexdvpO(n^{2})finkitxmlcodeinlinelatexdvp dans le pire cas, bien que le cas moyen soit de la complexité optimale kitxmlcodeinlinelatexdvpO(n\,\log n)finkitxmlcodeinlinelatexdvp, le pire cas étant rare.
Le principe : pour trier un certain tableau A[p..r], on choisit un pivot (généralement le dernier élément, A[r]). Ensuite, on partitionne le tableau de telle sorte que tout élément de A[p..q-1] soit inférieur ou égal au pivot, déplacé en A[q], ainsi qu'à tout élément de A[q+1..r]. On applique ensuite récursivement l'algorithme sur chacun des deux demi-tableaux. Pour partitionner, on parcourt le tableau de gauche à droite ; on initialise la position future du pivot q à p, le début du sous-tableau à trier ; si l'élément courant est plus petit que le pivot, on le met à la position q et on incrémente q. Au final, on échange le dernier élément traité avec celui à la position q.
On peut en imaginer une série de variantes, notamment sur le choix du pivot : un élément au hasard, la médiane de trois éléments, etc. Cela diminue la chance de tomber dans le pire cas sans modifier la complexité tant que le choix s'effectue en temps constant.
On peut aussi remarquer que l'algorithme est lourd pour de petits tableaux (une vingtaine d'éléments), pour lesquels on pourra privilégier un algorithme plus naïf (par insertion, par exemple).
if begin < end
q = Partition(A, begin, end)
QuickSort(A, begin, q - 1)
QuickSort(A, q + 1, end)
x = A[r]
i = p - 1
for j = p to r - 1
if A[j] <= x
i = i + 1
swap(A[i];A[j])
swap(A[i + 1], A[r])
return i + 1
III-E. Tri par tas▲
III-E-1. Arbre▲
Un arbre est un graphe dirigé constitué d'un ensemble de nœuds reliés par des arcs. Ce graphe est connexe et acyclique. S'il est non vide, alors il possède un nœud distingué et unique, la racine. Un chemin est une séquence de nœuds dont toute paire consécutive est un arc de l'arbre. La hauteur d'un arbre est la distance entre la feuille la plus éloignée et la racine.
Un arbre ordonné est un arbre dans lequel l'ensemble des fils de chacun de ses nœuds est ordonné.
Un arbre est dit binaire si chacun de ses nœuds possède au plus deux fils (un gauche et un droit, le gauche précédent le droit).
Un arbre est dit parfait ou complet si toutes ses feuilles sont à la même distance de la racine.
Un arbre binaire entier ou propre (full ou proper) est un arbre binaire dans lequel tous les nœuds internes possèdent exactement deux fils. Pour ces arbres, on dispose de quelques propriétés :
- le nombre de nœuds externes est égal au nombre de nœuds internes plus une unité ;
- le nombre de nœuds internes est égal à kitxmlcodeinlinelatexdvp\frac{n-1}{2}finkitxmlcodeinlinelatexdvp, où kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est le nombre de nœuds ;
- le nombre de nœuds à la profondeur (niveau) kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp est inférieur ou égal à kitxmlcodeinlinelatexdvp2^{i}finkitxmlcodeinlinelatexdvp ;
- la hauteur de l'arbre est inférieure ou égale au nombre de ses nœuds internes.
On peut lier la hauteur kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp et le nombre de nœuds kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp pour un arbre binaire entier par
kitxmlcodelatexdvpn\in\Omega(h)finkitxmlcodelatexdvp kitxmlcodelatexdvpn\in O(2^{h})finkitxmlcodelatexdvpDe manière équivalente,
kitxmlcodelatexdvph\in O(n)finkitxmlcodelatexdvp kitxmlcodelatexdvph\in\Omega(\log n)finkitxmlcodelatexdvpIII-E-2. Tas▲
Un arbre binaire complet (aussi dit tas) est un arbre binaire tel que, notant kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp la hauteur de l'arbre :
- pour tout kitxmlcodeinlinelatexdvpi\in[0\,;\, h-1]finkitxmlcodeinlinelatexdvp, il y a exactement kitxmlcodeinlinelatexdvp2^{i}finkitxmlcodeinlinelatexdvp nœuds à la profondeur kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp ;
- une feuille a une profondeur kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp ou kitxmlcodeinlinelatexdvph-1finkitxmlcodeinlinelatexdvp ;
- les feuilles de profondeur maximale sont tassées sur la gauche.
Un tas binaire est un arbre binaire complet tel que :
- à chaque nœud est associée une clé ;
- la clé de chaque nœud est supérieure ou égale à celle de ses fils (la propriété d'ordre du tas).
Un tas (un arbre binaire complet kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp de kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp entrées et de hauteur kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp) dispose des propriétés suivantes :
- kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est supérieur ou égal à la taille de l'arbre complet de hauteur kitxmlcodeinlinelatexdvph-1finkitxmlcodeinlinelatexdvp plus une unité, soit kitxmlcodeinlinelatexdvp2^{h}finkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est inférieur ou égal à la taille de l'arbre complet de hauteur kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp, soit kitxmlcodeinlinelatexdvp2^{h+1}-1finkitxmlcodeinlinelatexdvp.
On peut également écrire
kitxmlcodelatexdvp2^{h}\leq n\leq2^{h+1}-1finkitxmlcodelatexdvp kitxmlcodelatexdvph=\left\lfloor \log_{2}n\right\rfloorfinkitxmlcodelatexdvpIII-E-3. Interface▲
Pour un tas kitxmlcodeinlinelatexdvpHfinkitxmlcodeinlinelatexdvp, on définit les opérations suivantes :
- kitxmlcodeinlinelatexdvpH.heapSizefinkitxmlcodeinlinelatexdvp : le nombre de clés de kitxmlcodeinlinelatexdvpHfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpBuildMaxHeap(A)finkitxmlcodeinlinelatexdvp : construire un tas à partir du tableau kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpHeapInsert(H,\, key)finkitxmlcodeinlinelatexdvp : insérer la valeur kitxmlcodeinlinelatexdvpkeyfinkitxmlcodeinlinelatexdvp dans le tas kitxmlcodeinlinelatexdvpHfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpHeapExtractMax(H)finkitxmlcodeinlinelatexdvp : récupérer la clé maximale.
III-E-4. Implémentation par un tableau▲
On peut représenter un tas de manière compacte par un tableau A : la racine de l'arbre est le premier élément, on peut accéder relativement aux autres éléments par ces quelques fonctions (elles renvoient l'indice à consulter).
- kitxmlcodeinlinelatexdvpParent(i)=\left\lfloor \frac{i}{2}\right\rfloor finkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpLeft(i)=2\times ifinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpRight(i)=2\times i+1finkitxmlcodeinlinelatexdvp.
La propriété d'ordre du tas impose ainsi kitxmlcodeinlinelatexdvp\forall i,\, A[Parent(i)]\geq A[i]finkitxmlcodeinlinelatexdvp.
On peut alors implémenter les quelques méthodes du tas.
kitxmlcodeinlinelatexdvpMaxHeapifyfinkitxmlcodeinlinelatexdvp considère que les deux sous-arbres du nœud kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp sont des tas et réarrange l'arbre pour qu'il soit un tas (il fait descendre l'élément kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp jusqu'à ce que l'arbre respecte la propriété d'ordre). Ensuite, on appelle récursivement l'algorithme du côté de l'élément qui a été déplacé (le cas échéant). Vu le lien entre la hauteur de l'arbre et le nombre de nœuds, on a une complexité de kitxmlcodeinlinelatexdvp\Theta(h)=\Theta(\log n)finkitxmlcodeinlinelatexdvp.
l = Left(i)
r = Right(i)
if l <= A.heapSize and A[l] > A[i]
largest = l
else
largest = i
if r <= A.heapSize and A[r] > A[largest]
largest = r
if largest 6= i
swap(A[i];A[largest])
MaxHeapify(A; largest)
kitxmlcodeinlinelatexdvpBuildMaxHeapfinkitxmlcodeinlinelatexdvp construit un tas à partir d'un tableau : à partir du premier nœud qui a un successeur (gauche, s'il n'y en a qu'un), on réorganise le tas et on remonte jusqu'à la racine. On effectue kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp appels à kitxmlcodeinlinelatexdvpMaxHeapifyfinkitxmlcodeinlinelatexdvp, d'où une complexité de kitxmlcodeinlinelatexdvpO(n\,\log n)finkitxmlcodeinlinelatexdvp ; une analyse plus fine sur un arbre binaire parfait atteint kitxmlcodeinlinelatexdvpO\left(\log n\right)finkitxmlcodeinlinelatexdvp.
A.heapSize = A.length
for i = (round down A.length/2) downto 1
MaxHeapify(A, largest)
Pour l'insertion dans un tas, on insère l'élément à la fin (soit avec une clé infiniment négative), puis on le déplace jusqu'à la clé voulue.
A.heapSize = A.heapSize + 1
A[A.heapSize] = - infinity
HeapIncreaseKey(A, A.heapSize, key)
if key < A[i]
error new key is smaller than current key
A[i] = key
while i > 1 and A[Parent(i)] < A[i]
swap(A[i], A[Parent(i)])
i = Parent(i)
On peut également récupérer le maximum et l'éliminer du tas assez facilement (kitxmlcodeinlinelatexdvpO\left(1\right)finkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpO\left(n\right)finkitxmlcodeinlinelatexdvp, respectivement : lecture et réorganisation).
return A[1]
if A:heapSize < 1
error \heap under
max = A[1]
A[1] = A[A.heapSize]
A:heapSize = A.heapSize - 1
MaxHeapify(A, 1) // rebuild the heap
return max
III-E-5. Tri par tas▲
À partir d'un tas, on récupère la racine, l'élément le plus élevé de tout le tas, on la remplace par le dernier élément. On place cette racine à la fin du tableau et on ne considère plus cette case comme partie du tas. On réordonne donc le reste du tableau comme un tas puis on recommence. Tout comme la construction d'un tas, cet algorithme a une complexité de kitxmlcodeinlinelatexdvp\Theta(n\,\log n)finkitxmlcodeinlinelatexdvp.
BuildMaxHeap(A)
for i = A.length downto 2
swap(A[i], A[1])
A.heapSize = A.heapSize - 1
MaxHeapify(A, 1)
III-F. Résumé▲
Algorithme | Complexité au pire cas | Complexité moyenne | Complexité au meilleur cas | Tri en place ? |
---|---|---|---|---|
Tri par insertion | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n\right)finkitxmlcodeinlinelatexdvp | oui |
Tri par sélection | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | oui |
Tri à bulles | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | oui |
Tri par fusion | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | non |
Tri rapide | kitxmlcodeinlinelatexdvp\Theta\left(n^2\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | oui |
Tri par tas | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta\left(n\,\log n\right)finkitxmlcodeinlinelatexdvp | oui |
IV. Structures de données élémentaires▲
Une structure de données est une manière d'organiser et de stocker l'information pour en faciliter certaines opérations comme l'accès. Elle dispose d'une interface, un ensemble de procédures d'ajout, retrait, réorganisation, etc. des données. Elle conserve des données et des métadonnées (utilisées pour améliorer l'efficacité du traitement).
Un type de données abstrait est une définition des propriétés et de l'interface de la structure. Il admet généralement plusieurs implémentations, qui feront varier la complexité des opérations.
On distingue généralement deux types d'opérations : l'accès (en ce compris la recherche : minimum, élément suivant, etc.) et la modification (ajout, suppression, etc.).
IV-A. Pile▲
En anglais, stack.
Une pile est un ensemble dynamique d'objets accessibles selon une discipline LIFO. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpisEmpty(S)finkitxmlcodeinlinelatexdvp : renvoie vrai si et seulement si la pile kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp est vide ;
- kitxmlcodeinlinelatexdvppush(S,\, x)finkitxmlcodeinlinelatexdvp : pousse la valeur kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp sur la pile kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvppop(S)finkitxmlcodeinlinelatexdvp : extrait et renvoie la valeur au sommet de la pile kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp.
On l'implémente généralement :
- avec un tableau (bien que sa taille soit, a priori, de taille fixée). Complexité en temps des procédures : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp ; en espace : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp. Cela signifie que la pile ne pourra pas grandir ; ici, ce n'est pas un avantage ;
- avec une liste liée. Complexité en temps des procédures : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp ; en espace : kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp.
IV-B. File▲
En anglais, queue.
Une file est un ensemble dynamique d'objets accessibles selon une discipline FIFO. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpenqueue(Q,\, s)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp à la file kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpdequeue(Q)finkitxmlcodeinlinelatexdvp : retire l'élément à la tête de la file kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp.
On l'implémente généralement avec :
- un tableau circulaire. Complexité en temps des procédures : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp ; en espace : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp ;
- une liste liée. Complexité en temps des procédures : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp ; en espace : kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp.
IV-C. File double▲
En anglais, double-ended queue ou deque.
Une file double est une collection ordonnée d'objets offrant la possibilité d'insérer ou d'extraire un élément à la tête ou à la queue. Il s'agit d'une généralisation des piles et files. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpinsertFirst(Q,\, x)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp au début de la file double kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpinsertLast(Q,\, x)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp à la fin de la file double kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpremoveFirst(Q)finkitxmlcodeinlinelatexdvp : extrait l'élément situé au début de la file double kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpremoveLast(Q)finkitxmlcodeinlinelatexdvp : extrait l'élément situé à la fin de la file double kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp.
On l'implémente généralement avec une liste liée. Complexité en temps des procédures : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp ; en espace : kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp.
IV-D. Liste▲
Une liste est un ensemble dynamique d'objets ordonnés accessibles relativement les uns aux autres, sur base de leur position. Il s'agit d'une généralisation des piles, files et files doubles. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpinsertFirst(L,\, x)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp au début de la liste kitxmlcodeinlinelatexdvpLfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpinsertLast(L,\, x)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp à la fin de la liste kitxmlcodeinlinelatexdvpLfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpinsertAfter(L,\, p,\, x)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp après kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp dans la liste kitxmlcodeinlinelatexdvpLfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpinsertBefore(L,\, p,\, x)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp avant kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp dans la liste kitxmlcodeinlinelatexdvpLfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpremoveFirst(L)finkitxmlcodeinlinelatexdvp : extrait l'élément situé au début de la liste kitxmlcodeinlinelatexdvpLfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpremoveLast(L)finkitxmlcodeinlinelatexdvp : extrait l'élément situé à la fin de la liste kitxmlcodeinlinelatexdvpLfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpremove(L,\, p)finkitxmlcodeinlinelatexdvp : extrait l'élément situé à la position kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp de la liste kitxmlcodeinlinelatexdvpLfinkitxmlcodeinlinelatexdvp.
On l'implémente d'une manière similaire à la file double à l'aide d'une liste doublement liée et des sentinelles. Complexité en temps des procédures : kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp ; en espace : kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp.
IV-E. Vecteur▲
Un vecteur est un ensemble dynamique d'objets occupant des rangs entiers successifs permettant la consultation, le remplacement, l'insertion et la suppression d'éléments à des rangs arbitraires. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpat(V,\, r)finkitxmlcodeinlinelatexdvp : retourne l'élément situé au rang kitxmlcodeinlinelatexdvprfinkitxmlcodeinlinelatexdvp dans le vecteur kitxmlcodeinlinelatexdvpVfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpreplace(V,\, r,\, x)finkitxmlcodeinlinelatexdvp : remplace l'élément situé au rang kitxmlcodeinlinelatexdvprfinkitxmlcodeinlinelatexdvp dans le vecteur kitxmlcodeinlinelatexdvpVfinkitxmlcodeinlinelatexdvp par kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et retourne l'élément remplacé ;
- kitxmlcodeinlinelatexdvpinsert(V,\, r,\, x)finkitxmlcodeinlinelatexdvp : insère l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp au rang kitxmlcodeinlinelatexdvprfinkitxmlcodeinlinelatexdvp dans le vecteur kitxmlcodeinlinelatexdvpVfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpremove(V,\, r)finkitxmlcodeinlinelatexdvp : extrait l'élément situé au rang kitxmlcodeinlinelatexdvprfinkitxmlcodeinlinelatexdvp du vecteur kitxmlcodeinlinelatexdvpVfinkitxmlcodeinlinelatexdvp, en diminuant le rang des objets suivants ;
- kitxmlcodeinlinelatexdvpsize(V)finkitxmlcodeinlinelatexdvp : renvoie la taille du vecteur.
On peut l'implémenter à l'aide d'une liste liée ou d'un tableau extensible (généralement préféré pour des temps d'accès constants ; lors de la réallocation du tableau, il faudra veiller à doubler la taille pour amortir ce coût).
IV-F. File à priorité▲
Une file à priorité est un ensemble dynamique d'objets classés par ordre de priorité, permettant d'extraire un objet ayant la plus grande priorité. Cela suppose un ordre total défini sur les clés. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpinsert(S,x)finkitxmlcodeinlinelatexdvp : insérer l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp dans la file kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpmaximum(S)finkitxmlcodeinlinelatexdvp : récupérer l'élément de kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp ayant la plus grande clé ;
- kitxmlcodeinlinelatexdvpextractMaximum(S)finkitxmlcodeinlinelatexdvp : supprimer et renvoyer l'élément de kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp ayant la plus grande clé.
On peut l'implémenter à l'aide d'un tableau classique (insertion en kitxmlcodeinlinelatexdvpO(n)finkitxmlcodeinlinelatexdvp, vu qu'il faut garder le tableau trié ; extraction en kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp ; complexité en espace : kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp), d'une liste liée (mêmes complexités en temps ; en espace : kitxmlcodeinlinelatexdvpO(n)finkitxmlcodeinlinelatexdvp) ou d'un tas (renvoi du premier élément en kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp, il s'agit de récupérer l'élément maximal ; extraction du premier élément en kitxmlcodeinlinelatexdvpO(\log n)finkitxmlcodeinlinelatexdvp, vu qu'il faut restaurer la propriété de tas ; changement de clé d'un élément en kitxmlcodeinlinelatexdvpO(\log n)finkitxmlcodeinlinelatexdvp ; insertion en kitxmlcodeinlinelatexdvpO(\log n)finkitxmlcodeinlinelatexdvp ; complexité en espace : kitxmlcodeinlinelatexdvpO(n)finkitxmlcodeinlinelatexdvp).
V. Dictionnaires▲
V-A. Type de données abstrait▲
Un dictionnaire est un ensemble dynamique d'objets possédant des clés comparables, on supposera un ordre total sur ces clés. L'objectif est de minimiser le coût pour l'insertion et l'accès ainsi que pour le stockage. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpsearch(S,k)finkitxmlcodeinlinelatexdvp : retourne un pointeur kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp vers un élément de kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp tel que kitxmlcodeinlinelatexdvpx.key\,=\, kfinkitxmlcodeinlinelatexdvp ou kitxmlcodeinlinelatexdvpnilfinkitxmlcodeinlinelatexdvp s'il n'y a pas de tel objet ;
- kitxmlcodeinlinelatexdvpinsert(S,x)finkitxmlcodeinlinelatexdvp : ajoute l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp dans le dictionnaire kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpdelete(S,x)finkitxmlcodeinlinelatexdvp : retire l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp du dictionnaire kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp.
V-A-1. Implémentations naïves▲
V-A-1-a. Liste liée▲
On stocke les paires clé-valeur dans une liste liée. Les complexités seront linéaires pour l'insertion, la recherche et la suppression : à chaque fois, il faut parcourir la liste pour trouver si un élément correspond (pour insertion juste après, renvoi ou suppression).
V-A-1-b. Vecteur trié▲
Si on a un ordre total sur les clés, on peut stocker les données dans un vecteur maintenu trié. Insertion et suppression requièrent une recherche et un décalage de tous les éléments, leur complexité reste linéaire. La recherche peut être optimisée avec une recherche dichotomique (kitxmlcodeinlinelatexdvpO(\log n)finkitxmlcodeinlinelatexdvp).
V-B. Arbre▲
Les arbres binaires de recherches permettront d'implémenter une insertion et une recherche efficaces dans un dictionnaire (logarithmique dans le cas moyen, mais toujours linéaire dans le pire cas tant qu'on n'équilibre pas).
V-B-1. Type de données abstrait▲
Un arbre est constitué de nœuds, accessibles les uns par rapport aux autres selon leur position ; les données sont associées à ces nœuds. Son interface est constituée de :
- kitxmlcodeinlinelatexdvpparent(T,n)finkitxmlcodeinlinelatexdvp : renvoie le parent de l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp dans l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp, sauf si kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est la racine ;
- kitxmlcodeinlinelatexdvpchildren(T,n)finkitxmlcodeinlinelatexdvp : renvoie une structure de données des fils de l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp dans l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpleft(T,n)finkitxmlcodeinlinelatexdvp : renvoie le fils gauche de l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp dans l'arbre binaire kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpright(T,n)finkitxmlcodeinlinelatexdvp : renvoie le fils droit de l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp dans l'arbre binaire kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvproot(T)finkitxmlcodeinlinelatexdvp : renvoie la racine de l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpisRoot(T,n)finkitxmlcodeinlinelatexdvp : renvoie vrai si de l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est la racine de l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpisInternal(T,n)finkitxmlcodeinlinelatexdvp : renvoie vrai si de l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est un nœud interne de l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpisExternal(T,n)finkitxmlcodeinlinelatexdvp : renvoie vrai si de l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp est un nœud externe de l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpgetData(T,n)finkitxmlcodeinlinelatexdvp : renvoie les données associées à l'élément kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp dans l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ;
- kitxmlcodeinlinelatexdvpsize(T)finkitxmlcodeinlinelatexdvp : renvoie le nombre de nœuds de l'arbre kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp.
V-B-2. Implémentation d'un arbre binaire▲
V-B-2-a. Vecteur▲
Tout l'arbre est représenté par un vecteur, où chaque position est indiquée par l'index : la racine est à la position kitxmlcodeinlinelatexdvp1finkitxmlcodeinlinelatexdvp, le successeur gauche d'un nœud de rang kitxmlcodeinlinelatexdvprfinkitxmlcodeinlinelatexdvp sera à l'index kitxmlcodeinlinelatexdvp2\, rfinkitxmlcodeinlinelatexdvp, le successeur droit au rang kitxmlcodeinlinelatexdvp2\, r+1finkitxmlcodeinlinelatexdvp.
Cependant, si l'arbre binaire n'est pas complet, on aura des trous : la complexité en espace pour le stockage de kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp nœuds sera de kitxmlcodeinlinelatexdvpO(2^{n})finkitxmlcodeinlinelatexdvp, au lieu de kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp pour un arbre binaire complet. Les opérations auront une complexité en temps de kitxmlcodeinlinelatexdvpO\text{(1)}finkitxmlcodeinlinelatexdvp.
V-B-2-b. Structure liée▲
Pour chaque nœud kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp de l'arbre binaire, on retient un champ de données (kitxmlcodeinlinelatexdvpn.datafinkitxmlcodeinlinelatexdvp), un pointeur vers le nœud parent (kitxmlcodeinlinelatexdvpn.parentfinkitxmlcodeinlinelatexdvp) et un pointeur pour chaque fils (kitxmlcodeinlinelatexdvpn.leftfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpn.rightfinkitxmlcodeinlinelatexdvp). Cette manière de procéder peut s'extrapoler facilement à des arbres non binaires ; on utilisera un champ kitxmlcodeinlinelatexdvpn.keyfinkitxmlcodeinlinelatexdvp pour retenir la clé de l'élément. On aura une complexité en espace de kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp, vu qu'on ne crée que les éléments nécessaires ; les opérations auront une complexité en temps de kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp.
V-B-3. Parcours en profondeur▲
On va descendre dans l'arbre avant d'aller visiter les nœuds voisins.
V-B-3-a. Parcours infixe (en ordre)▲
Chaque nœud est visité après son fils gauche et avant son fils droit.
if x != nil
InorderTreeWalk(x.left)
walk(x.key)
InorderTreeWalk(x.left)
V-B-3-b. Parcours préfixe (en préordre)▲
Chaque nœud est visité avant ses fils.
if x != nil
walk(x.key)
InorderTreeWalk(x.left)
InorderTreeWalk(x.left)
V-B-3-c. Parcours postfixe (en postordre)▲
Chaque nœud est visité après ses fils.
if x != nil
InorderTreeWalk(x.left)
InorderTreeWalk(x.left)
walk(x.key)
V-B-3-d. Complexité▲
Tous les parcours en profondeur ont une complexité en temps de kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp. En effet, on doit parcourir au moins une fois chaque nœud :
kitxmlcodelatexdvpT(n)=\Omega(n)\,;finkitxmlcodelatexdvpkitxmlcodeinlinelatexdvpn_{L}finkitxmlcodeinlinelatexdvp étant le nombre de nœuds à gauche et kitxmlcodeinlinelatexdvpdfinkitxmlcodeinlinelatexdvp une constante, on a la récursion
kitxmlcodelatexdvpT(n)\leq T(n_{L})+T(n-n_{L}-1)+d.finkitxmlcodelatexdvpPar induction, on prouve alors que
kitxmlcodelatexdvpT(n)<(c+d)\, n,finkitxmlcodelatexdvpavec
kitxmlcodelatexdvpc=T(0).finkitxmlcodelatexdvpOn a donc prouvé
kitxmlcodelatexdvpT(n)=O(n),finkitxmlcodelatexdvpsoit, vu la borne inférieure,
kitxmlcodelatexdvpT(n)=\Theta(n).finkitxmlcodelatexdvpV-B-4. Parcours en largeur▲
On visite le nœud le plus proche de la racine qui n'a pas été visité, soit d'abord la racine, puis ses enfants, les nœuds du deuxième niveau et ainsi de suite. On peut l'implémenter à l'aide d'une file avec une complexité en temps de kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp :
Q = new empty queue
Enqueue(Q, x)
while not QueueEmpty(Q)
y = Dequeue(Q)
walk(y.key)
if y.left != NIL
Enqueue(Q, y.left)
if y.right != NIL
Enqueue(Q, y.right)
V-C. Arbre binaire de recherche▲
Un arbre binaire de recherche est une structure d'arbre binaire implémentant un dictionnaire dont les opérations sont en kitxmlcodeinlinelatexdvpO(h)finkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp étant la hauteur de l'arbre. La propriété d'arbre binaire de recherche est la suivante, exprimée pour deux nœuds kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp :
- si kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp est dans le sous-arbre de gauche de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp, alors kitxmlcodeinlinelatexdvpy.key<x.keyfinkitxmlcodeinlinelatexdvp ;
- si kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp est dans le sous-arbre de droite de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp, alors kitxmlcodeinlinelatexdvpy.key\geq x.keyfinkitxmlcodeinlinelatexdvp.
Un parcours infixe d'un tel arbre donnera les clés par ordre croissant.
Toutes les opérations auront une complexité dans le pire cas de kitxmlcodeinlinelatexdvpO(h)finkitxmlcodeinlinelatexdvp. En supposant que les éléments ont été insérés en ordre aléatoire, on peut montrer que kitxmlcodeinlinelatexdvph\in O(\log n)finkitxmlcodeinlinelatexdvp.
V-C-1. Recherche▲
Une recherche (récursive terminale ou itérative) dans un tel arbre aura une complexité kitxmlcodeinlinelatexdvpO(h)finkitxmlcodeinlinelatexdvp.
if x == NIL or k == x.key
return x
if k < x.key
return TreeSearch(x.left, k)
else
return TreeSearch(x.right, k)
x = T.root
while x != NIL and k != x:key
if k < x.key
x = x.left
else
x = x.right
return x
V-C-2. Maximum et minimum▲
Au vu de la propriété d'arbre binaire de recherche, la clé minimale est stockée dans le nœud le plus à gauche et la clé maximale dans le nœud le plus à droite.
while x.left != NIL
x = x.left
return x
while x.right != NIL
x = x.right
return x
V-C-3. Successeur▲
On veut, à partir d'un nœud kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp, trouver le nœud contenant la valeur de clé suivante, selon un ordre arithmétique. Si le sous-arbre de droite du nœud existe, alors le successeur en est le minimum. Sinon, il s'agit du premier ancêtre kitxmlcodeinlinelatexdvpafinkitxmlcodeinlinelatexdvp de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp tel que kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp est dans le sous-arbre de gauche de kitxmlcodeinlinelatexdvpafinkitxmlcodeinlinelatexdvp.
if x.right != NIL
return TreeMinimum(x.right)
y = x.parent
while y != NIL and x == y.right
x = y
y = y.parent
return y
V-C-4. Insertion▲
Pour insérer un élément, on recherche sa clé dans l'arbre et on l'insère là où la recherche s'est arrêtée dans le cas où elle n'existe pas encore.
y = NIL
x = T.root
while x != nil
y = x
if z.key < x.key
x = x.left
else
x = x.right
z.parent = y
if y == NIL
// Tree T was empty
T.root = z
elseif z.key < y.key
y. left = z
else
y.right = z
V-C-5. Suppression▲
Trois cas particuliers sont à considérer : un seul fils ou deux fils. S'il n'y a pas de fils, il suffit de supprimer toute référence au nœud. Le but est toujours de garder la propriété d'arbre binaire de recherche. Pour supprimer le nœud kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp :
-
s'il n'a qu'un seul fils : le remplacer par celui-ci ;
-
s'il a deux fils : rechercher le successeur kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp :
- si kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp est le fils droit de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp, remplacer kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp par kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp et conserver le fils droit de kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp,
- sinon, kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp est dans le sous-arbre droit de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp sans en être la racine ; on remplace kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp par son propre fils droit et on remplace kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp par kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp.
if u.parent == NIL
T.root = v
elseif u == u.parent.left
u.parent.left = v
else
u.parent.right = v
if v != NIL
v.parent = u.parent
if z.left == NIL
Transplant(T, z, z.right)
elseif z.right == NIL
Transplant(T, z, z.left)
else // z has two children
y = TreeSuccessor(z)
if y.parent != z
Transplant(T, y, y.right)
y.right = z.right
y.right.parent = y
// Replace z by y
Transplant(T, z, y)
y.left = z.left
y.left.parent = y
V-C-6. Tri▲
Un tri dans l'ordre croissant correspond à un parcours infixe de l'arbre binaire de recherche. Sa complexité en temps est identique à celle du tri rapide, bien que sa complexité en espace soit plus importante, étant donné la structure sous-jacente.
T = new empty binary search tree
for i = 1 to n
TreeInsert(T;A[i])
InorderTreeWalk(T:root)
V-D. Arbre équilibré▲
Pour obtenir une complexité moindre, il faut maintenir un invariant sur la structure de l'arbre et prouver qu'il garantit une hauteur kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp.
V-D-1. Arbre H-équilibré▲
On étudiera ici les arbres AVL (Adelson-Velskii-Landis), qui ont la propriété d'être H-équilibrés. Leur invariant est, pour tout sous-arbre kitxmlcodeinlinelatexdvpT'finkitxmlcodeinlinelatexdvp de kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp,
kitxmlcodelatexdvp\Big|h(g(T'))-h(d(T'))\Big|\leq1.finkitxmlcodelatexdvpLes hauteurs des deux sous-arbres d'un nœud diffèrent au plus d'une unité. Un tel arbre aura une hauteur bornée par
kitxmlcodelatexdvph\in\Theta(\log n).finkitxmlcodelatexdvpPrécisément, on peut prouver que
kitxmlcodelatexdvp\log(n+1)\leq h+1\leq1,44\,\,\log(n+2).finkitxmlcodelatexdvpV-D-2. Arbre AVL▲
Un arbre AVL (Adelson-Velskii-Landis) est un arbre binaire de recherche H-équilibré. En tant qu'arbre binaire de recherche, on peut utiliser l'algorithme des arbres binaires de recherche pour la recherche ; il aura une complexité de kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp, vu la propriété.
Pour l'insertion, on procèdera comme pour un arbre binaire classique) ; ensuite, on rétablira l'invariant au besoin à l'aide de rotations.
V-D-2-a. Rotations▲
Ces rotations sont implémentées à l'aide des algorithmes suivants.
r = x.right
x.right = r.left
r.left = x
return r
r = x.left
x.left = r.right
r.right = x
return r
V-D-2-b. Équilibrage▲
On prend kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp, le nœud le plus bas violant l'invariant après insertion. Tous ses sous-arbres sont H-équilibrés, il y a cependant une différence de deux niveaux entre ses sous-arbres gauche et droit. Pour rétablir l'équilibre (en cas de déséquilibre à gauche ou à droite), on devra effectuer une ou deux rotations. Ainsi, l'insertion aura une complexité logarithmique par rapport au nombre de nœuds.
V-D-2-c. Tri▲
On peut utiliser un arbre AVL pour le tri : on insère successivement les éléments dans l'arbre et on effectue un parcours en ordre. On aura une complexité temporelle optimale (kitxmlcodeinlinelatexdvp\Theta(n\,\log n)finkitxmlcodeinlinelatexdvp), mais une complexité en espace linéaire (on doit allouer un arbre).
V-E. Table de hachage▲
V-E-1. Tableau à accès direct▲
On suppose que chaque élément est associé à une clé unique tirée d'un univers pas trop grand, kitxmlcodeinlinelatexdvpU=\{1\ldots m-1\}finkitxmlcodeinlinelatexdvp. Le dictionnaire est alors implémenté par le tableau kitxmlcodeinlinelatexdvpT[0\ldots m-1]finkitxmlcodeinlinelatexdvp, chaque position correspondant à une clé de l'univers. Si le dictionnaire contient un élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp avec la clé kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp, alors kitxmlcodeinlinelatexdvpT[k]finkitxmlcodeinlinelatexdvp contient un pointeur vers kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp. Sinon, kitxmlcodeinlinelatexdvpT[k]finkitxmlcodeinlinelatexdvp est un pointeur nul.
On a ainsi un dictionnaire à accès en temps constant, ainsi que la suppression et la recherche. Sa taille sera constante (kitxmlcodeinlinelatexdvp\Theta(\#U)finkitxmlcodeinlinelatexdvp), il ne pourra pas retenir plusieurs éléments avec la même clé. Si l'ensemble des clés utilisées est très inférieur à l'univers, alors on gaspille beaucoup de place.
V-E-2. Table de hachage▲
On utilise un tableau kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp de taille largement inférieure à la taille de l'univers. On placera l'élément kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp de clé kitxmlcodeinlinelatexdvpx.keyfinkitxmlcodeinlinelatexdvp à la position kitxmlcodeinlinelatexdvph(x.key)finkitxmlcodeinlinelatexdvp, où kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp est une fonction de hachage qui établit un lien entre l'univers kitxmlcodeinlinelatexdvpUfinkitxmlcodeinlinelatexdvp et l'ensemble des clés de kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp.
Cependant, on a un risque de collision : deux clés distinctes ont la même image par la fonction de hachage. Par le principe des tiroirs, cela se produit toujours lorsqu'on a plus d'éléments à stocker que la taille du tableau. Selon le paradoxe des anniversaires, c'est même très probable, même si la fonction de hachage répartit les clés de manière uniforme.
Deux solutions pour éviter les collisions : une fonction de hachage qui répartit de manière uniforme les clés ou un tableau de taille suffisante. Dans chaque cas, en cas de remplissage suffisant, la probabilité de collision restera élevée. Deux approches résolvent définitivement ce problème : l'adressage ouvert et l'adressage fermé.
V-E-3. Fonction de hachage▲
Idéalement, une fonction de hachage devrait être facile à calculer (en temps constant) et satisfaire l'hypothèse de hachage uniforme, ce qui n'est pas forcément facile à obtenir (on ne connaît généralement pas la distribution des clés et elles ne sont pas forcément indépendantes). Une telle fonction suppose que la clé est un nombre naturel (sinon, il faut la coder : par exemple, pour une chaîne de caractères, prendre la valeur ASCII de chaque caractère, la pondérer par kitxmlcodeinlinelatexdvp128^{i}finkitxmlcodeinlinelatexdvp et sommer).
V-E-3-a. Méthode de division▲
La fonction de hachage calcule le reste de la division entière de la clé par la taille de la table :
kitxmlcodelatexdvph(k)=k\mod\, m.finkitxmlcodelatexdvpCette manière de procéder est très rapide, mais dépend beaucoup du choix de kitxmlcodeinlinelatexdvpmfinkitxmlcodeinlinelatexdvp : si kitxmlcodeinlinelatexdvpm=2^{p}finkitxmlcodeinlinelatexdvp, alors le résultat ne dépend que des kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp bits les moins significatifs ; si kitxmlcodeinlinelatexdvpm=2^{p}-1finkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp est une chaîne de caractères codée en base kitxmlcodeinlinelatexdvp2^{p}finkitxmlcodeinlinelatexdvp, alors permuter les caractères de kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp n'influence pas le résultat ; si les clés sont périodiques et kitxmlcodeinlinelatexdvpmfinkitxmlcodeinlinelatexdvp est non premier, on n'exploite qu'une partie des valeurs possibles (pour kitxmlcodeinlinelatexdvpm=100finkitxmlcodeinlinelatexdvp, l'image de kitxmlcodeinlinelatexdvp\{206,211,216,221\}finkitxmlcodeinlinelatexdvp sera kitxmlcodeinlinelatexdvp\{6,11,16,21\}finkitxmlcodeinlinelatexdvp).
V-E-3-b. Méthode de multiplication▲
Avec une constante kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp telle que kitxmlcodeinlinelatexdvp0<A<1finkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvpk\, A\mod1finkitxmlcodeinlinelatexdvp est la partie fractionnaire de kitxmlcodeinlinelatexdvpk\, Afinkitxmlcodeinlinelatexdvp, on utilise la fonction
kitxmlcodelatexdvph(k)=\bigg\lfloor m\cdot(k\, A\mod1)\bigg\rfloorfinkitxmlcodelatexdvpLa valeur de kitxmlcodeinlinelatexdvpmfinkitxmlcodeinlinelatexdvp n'est plus critique, mais certaines valeurs de kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp fonctionnent mieux.
V-E-4. Chaînage (adressage fermé)▲
Simplement, on stocke les éléments qui ont le même hash dans une liste (doublement) liée. On aura une insertion (en début de liste) en temps constant, une suppression linéaire pour une liste liée ou constante pour une liste doublement liée, une recherche linéaire du nombre d'éléments dans la liste, du moins dans le pire cas.
Afin d'étudier la complexité dans le cas moyen, on définit le facteur de charge
kitxmlcodelatexdvp\alpha=\frac{n}{m}finkitxmlcodelatexdvpoù kitxmlcodeinlinelatexdvpmfinkitxmlcodeinlinelatexdvp est la taille du tableau et kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp le nombre d'éléments stockés. On supposera le hachage parfaitement uniforme : pour toute clé kitxmlcodeinlinelatexdvpk\in Ufinkitxmlcodeinlinelatexdvp, on a
kitxmlcodelatexdvpP\bigg(h(k)=i\bigg)=\frac{1}{m},\forall i\in\{0\ldots m-1\}finkitxmlcodelatexdvpOn considérera également que la fonction de hachage kitxmlcodeinlinelatexdvphfinkitxmlcodeinlinelatexdvp a une complexité temporelle kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp et que l'on insère les éléments en début de liste.
Ainsi, dans le cas moyen, que la recherche soit positive ou non, on aura une complexité temporelle de kitxmlcodeinlinelatexdvp\Theta(1+\alpha)finkitxmlcodeinlinelatexdvp (kitxmlcodeinlinelatexdvp\alphafinkitxmlcodeinlinelatexdvp pouvant être aussi bien inférieur à l'unité ou très grand, en fonction du remplissage). Si on a
kitxmlcodelatexdvpn\in O(m),finkitxmlcodelatexdvpsoit si kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp croît au moins linéairement avec la taille du tableau, on aura
kitxmlcodelatexdvp\alpha=\frac{O(m)}{m}=O(1).finkitxmlcodelatexdvpToutes les opérations seront kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp en moyenne.
V-E-4-a. Critique▲
On peut gérer un nombre illimité d'éléments et de collisions, les performances restant stables. On a cependant un surcoût lié à la gestion et au stockage des listes liées.
V-E-5. Sondage (adressage ouvert)▲
On stocke tous les éléments dans le tableau : une fois celui-ci rempli, on ne peut plus ajouter d'élément. Pour insérer une clé kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp, on sonde toutes les autres cases à partir de kitxmlcodeinlinelatexdvph(k,0)finkitxmlcodeinlinelatexdvp en suivant une fonction de sondage kitxmlcodeinlinelatexdvph(k,i)finkitxmlcodeinlinelatexdvp.
La fonction de hachage est ainsi définie sur kitxmlcodeinlinelatexdvpU\times[0\,;m-1]finkitxmlcodeinlinelatexdvp et telle que
kitxmlcodelatexdvp<h(k,0)\ldots h(k,m-1)>finkitxmlcodelatexdvpest une permutation de kitxmlcodeinlinelatexdvp[0\,;m-1]finkitxmlcodeinlinelatexdvp.
Pour supprimer, on ne peut pas simplement mettre un pointeur nul à l'endroit qui vient d'être libéré, on ne saurait jamais si on doit poursuivre la recherche. On utilise donc une valeur spéciale : lors de la recherche, on sait qu'on devra continuer ; lors d'une insertion, la case est considérée vide. Ainsi, le temps de recherche ne dépend plus du facteur de charge, mais de la charge maximale de la table.
Une stratégie de sondage efficace donnera un hachage uniforme : chacune des permutations de kitxmlcodeinlinelatexdvp[0\,;1]finkitxmlcodeinlinelatexdvp a la même probabilité de correspondre à une clé. Cela est cependant dur à garantir, on se satisfera généralement du fait que l'on a une permutation.
-
Sondage linéaire : facile à implémenter, on se limite à aller voir la case suivante ; on crée ainsi un fort effet de grappe, pas d'uniformité ; on a la fonction
kitxmlcodelatexdvph(k,i)=\bigg(h(k)+i\bigg)\mod m.finkitxmlcodelatexdvp -
Sondage quadratique : à l'aide de deux constantes bien choisies (pour garder la propriété de permutation), on effectue un parcours quadratique de la table ; l'effet de grappe est toujours présent, bien que moins marqué ; on a la fonction
kitxmlcodelatexdvph(k,i)=\bigg(h(k)+c_{1}\, i+c_{2}\, i^{2}\bigg)\mod m.finkitxmlcodelatexdvp -
Double hachage : on utilise deux fonctions de hachage différentes, à bien choisir pour garder la permutation ; le résultat sera très proche de l'uniformité, bien meilleur que les techniques précédentes ; on aura la fonction
kitxmlcodelatexdvph(k,i)=\bigg(h(k)+i\, h'(k)\bigg)\mod m.finkitxmlcodelatexdvp
V-E-5-a. Critique▲
Limitée par la taille du tableau, cette méthode est rapide et peu gourmande en mémoire, bien que plus ardue à implémenter pour éviter les grappes. Les suppressions sont problématiques et les performances dégringolent dès que la table est bien remplie (on peut envisager de créer une table plus grande pour stocker les éléments, mais cela empêche d'utiliser la table pendant un certain temps et il faut sélectionner une nouvelle fonction de hachage et réinsérer tous les éléments ; on garde cependant le même coût asymptotique si on double la taille, par amortissement).
V-F. Résumé▲
Complexité en recherche | Complexité en insertion | Complexité en suppression | ||||
---|---|---|---|---|---|---|
Implémentation | Pire cas | Cas moyen | Pire cas | Cas moyen | Pire cas | Cas moyen |
Liste | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp |
Vecteur trié | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp |
Arbre binaire de recherche | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp |
Arbre équilibré | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(\log n)finkitxmlcodeinlinelatexdvp |
Table de hachage | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(n)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvp\Theta(1)finkitxmlcodeinlinelatexdvp |
V-F-1. Arbres de recherche ou table de hachage ?▲
Une table de hachage est facile à implémenter, la seule solution disponible pour des clés non ordonnées, a des recherche et insertion très rapides en moyenne, mais gaspille de l'espace en faible facteur de charge ; dans le pire cas, les performances ne sont pas garanties.
Un arbre binaire de recherche (équilibré ou non ; ABR ou AVL) aura des performances garanties, ne gaspillera pas de mémoire, supportera plus d'opérations car les clés sont ordonnées, bien que l'accès et l'insertion soient plus lents en moyenne.
VI. Résolution de problèmes▲
VI-A. Force brute▲
Cette approche résout directement le problème à partir de sa définition ou d'une recherche exhaustive.
En appliquant la définition, elle mène aux tris à bulle et par sélection, de complexité quadratique ; par recherche exhaustive, on générera tous les tableaux possibles par permutation, on sélectionnera le premier à être ordonné : cette approche a une complexité temporelle de kitxmlcodeinlinelatexdvpO(n\cdot n!)finkitxmlcodeinlinelatexdvp.
Bien que parfois seule solution possible (problème du voyageur de commerce), il existe généralement de meilleures solutions. Cette technique est très simple et d'application très large, il s'agit donc d'un bon point de départ pour trouver d'autres algorithmes plus performants (faire mieux n'a pas toujours d'intérêt), plus élégants et créatifs.
VI-B. Diviser pour régner▲
On divise le problème en sous-problèmes plus faciles à résoudre (jusqu'au cas de base) que l'on fusionne pour obtenir la solution du problème original. On trouve donc trois étapes : diviser (diviser le problème), régner (résoudre récursivement les sous-problèmes jusqu'au cas de base, trivial à résoudre) et fusionner (à partir des solutions des sous-problèmes, trouver la solution du problème original).
Cette approche mène aux tris par fusion et rapide, deux algorithmes de complexité optimale (au lieu de quadratique par force brute).
Elle mène à des algorithmes efficaces, cette méthode est donc très utile, bien qu'elle ne soit pas toujours applicable.
VI-C. Programmation dynamique▲
On cherche à obtenir la solution optimale en combinant des solutions optimales de sous-problèmes se chevauchant. Il s'agit d'une généralisation de l'approche précédente : les sous-problèmes peuvent se chevaucher, dépendre l'un de l'autre. Notamment, quand le même sous-problème apparaît plusieurs fois, on s'arrange pour ne le résoudre qu'une seule en sauvegardant les solutions dans une table (on échange du temps de calcul contre de la mémoire). En général, on peut transformer un problème de complexité exponentielle en une classe polynomiale.
On en distingue deux cas : la programmation dynamique descendante avec mémoization (on résout le problème en partant des plus grandes instances de manière récursive en sauvegardant les résultats intermédiaires dans un tableau qui est passé à chaque appel) et la programmation dynamique ascendante (on résout d'abord les instances de petite taille jusqu'à la taille demandée, puis on retourne la valeur obtenue, sans récursivité).
Il est nécessaire de disposer de la propriété de sous-structure optimale : une solution optimale est composée de solutions optimales à des sous-problèmes.
La procédure générale consiste à définir de manière récursive la valeur à optimiser de la meilleure solution, puis à calculer ces valeurs pour reconstruire (à partir du tableau généré) une solution optimale (approche bottom-up).
On ne peut cependant pas utiliser la récursivité pour implémenter ces algorithmes de manière efficace : alors que l'approche diviser pour régner divise les problèmes en instances significativement plus petites (kitxmlcodeinlinelatexdvp\frac{n}{2}finkitxmlcodeinlinelatexdvp) et totalement indépendantes, la programmation dynamique n'en réduira la taille que de très peu (kitxmlcodeinlinelatexdvpn-1finkitxmlcodeinlinelatexdvp) tout en gardant une certaine dépendance.
VI-D. Approche gloutonne▲
On construit incrémentalement la solution en optimisant localement pour obtenir un optimum global. On doit donc effectuer à chaque fois un choix, alors que la programmation dynamique explore tous les choix possibles. En fonction des problèmes, des entrées et des paramètres, on pourrait n'obtenir qu'une bonne solution pas forcément optimale (on doit explorer tous les choix pour s'en assurer : programmation dynamique). Dans tous les cas, on ne récupérera qu'une solution optimale au problème.
Quand on a un choix local à faire, on effectue un choix glouton qui semble le meilleur immédiatement (sans jamais le remettre en question). Pour que l'algorithme soit correct, il faut donc prouver la propriété des choix gloutons optimaux (en effectuant ce choix de manière locale, on arrive toujours à une solution optimale). Pour que l'approche fonctionne, il faut aussi prouver la propriété de sous-structure optimale : une solution optimale du problème est composée de solutions optimales à des sous-problèmes. Si ces propriétés ne sont pas vérifiées, l'approche ne donnera pas forcément un optimal mais bien une approximation (on peut, dans certains cas, en caractériser la distance à l'optimal).
Quand un tel algorithme existe, il est très efficace et simple à implémenter. Cependant, quand il existe, sa correction peut être assez difficile à établir.
Par rapport à la programmation dynamique, on doit prouver la propriété des choix gloutons optimaux. Grâce à celle-ci, on n'aura pas besoin de résoudre plusieurs sous-problèmes : il est effectué avant de résoudre un sous-problème. Il n'y a pas besoin de stocker les résultats intermédiaires, la solution obtenue ne doit pas être recomposée.
VII. Graphes▲
VII-A. Définitions▲
Un graphe est un couple kitxmlcodeinlinelatexdvp(V,E)finkitxmlcodeinlinelatexdvp, où kitxmlcodeinlinelatexdvpVfinkitxmlcodeinlinelatexdvp est un ensemble de nœuds (sommets) et kitxmlcodeinlinelatexdvpE\subseteq V\times Vfinkitxmlcodeinlinelatexdvp un ensemble d'arcs (ou arêtes). Une arête est un ensemble de deux sommets kitxmlcodeinlinelatexdvp(u,v)finkitxmlcodeinlinelatexdvp. Un graphe non dirigé (ou non orienté) est caractérisé par une relation symétrique entre les sommets.
Deux nœuds sont adjacents s'ils sont liés par une même arête ; cette arête sera incidente à ces deux nœuds. Le degré d'un nœud est le nombre de ses arêtes incidentes. Le degré d'un graphe est le nombre maximal d'arêtes incidentes à un sommet.
Un graphe sera connexe s'il existe un chemin de tout sommet à tout autre (en suivant le sens des arêtes, le cas échéant). Une composante connexe d'un graphe non orienté est un sous-ensemble connexe maximal de ce graphe (si on ajoute un nœud du graphe étudié, il ne sera plus connexe).
Dans un graphe dirigé, une arête kitxmlcodeinlinelatexdvp(u,v)finkitxmlcodeinlinelatexdvp possède l'origine kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp et la destination kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp. Cette arête sera sortante pour kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp et entrante pour kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp. On définit ainsi les degrés entrant et sortant. Un graphe acyclique n'a aucun cycle : il n'est pas possible de suivre les arêtes du graphe à partir d'un sommet et de revenir à ce même sommet.
Un graphe simple ne possède pas de boucle composée d'une seule arête : kitxmlcodeinlinelatexdvp\forall v\in V,\,(v,v)\not\in Efinkitxmlcodeinlinelatexdvp. Un arbre est un graphe acyclique connexe. Un multigraphe est une généralisation des graphes pour laquelle il est permis de définir plus d'une arête liant un sommet à l'autre. Un graphe pondéré aura des arêtes annotées par des poids.
Un sommet kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp est accessible à partir de kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp si kitxmlcodeinlinelatexdvpu=vfinkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp est adjacent à kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp ou si kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp est adjacent à un sommet de kitxmlcodeinlinelatexdvpwfinkitxmlcodeinlinelatexdvp accessible à partir de kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp.
VII-B. Représentations▲
De manière très naïve, on pourrait représenter un graphe comme une liste de nœuds et d'arêtes, mais cette solution n'est pas la plus efficace.
On peut utiliser, pour chaque nœud, une liste d'adjacence : des listes liées pour chaque nœud indiquant les autres extrémités des arêtes. Si on associe un poids à chaque élément de la liste, on peut représenter un graphe pondéré ; on peut représenter indifféremment un graphe dirigé ou non (on insérera chaque arête deux fois). Pour un graphe kitxmlcodeinlinelatexdvpGfinkitxmlcodeinlinelatexdvp, on aura donc la liste de nœuds kitxmlcodeinlinelatexdvpG.Vfinkitxmlcodeinlinelatexdvp (kitxmlcodeinlinelatexdvp|V|finkitxmlcodeinlinelatexdvp éléments) et un tableau kitxmlcodeinlinelatexdvpG.Adjfinkitxmlcodeinlinelatexdvp de listes d'adjacence, tel que chaque sommet est représenté par un élément de ce tableau.
On peut aussi utiliser une matrice d'adjacence, le graphe étant alors entièrement représenté par une matrice kitxmlcodeinlinelatexdvpG.Afinkitxmlcodeinlinelatexdvp de dimension kitxmlcodeinlinelatexdvp|V|\times|V|finkitxmlcodeinlinelatexdvp. Cette matrice est telle que
kitxmlcodelatexdvp a_{ij}=\begin{cases} 1 & \text{ si }(i,j)\in E\\ 0 & \text{ sinon} \end{cases}. finkitxmlcodelatexdvpSi le graphe est non dirigé, elle sera symétrique. On peut également représenter des graphes pondérés en mettant le poids à la place du booléen.
Complexité en espace | Complexité en temps d'accès à un sommet | Complexité en temps du parcours des sommets | Complexité en temps du parcours des arêtes | Complexité en temps de la vérification de l'existence d'une arête | |
---|---|---|---|---|---|
Liste d'adjacence | kitxmlcodeinlinelatexdvpO(|V|+|E|)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(|V|)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(|V|+|E|)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(|V|)finkitxmlcodeinlinelatexdvp |
Matrice d'adjacence | kitxmlcodeinlinelatexdvpO(|V|^2)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(|V|)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(|V|^2)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp |
Optimal | kitxmlcodeinlinelatexdvpO(|V|+|E|)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(|V|)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(|E|)finkitxmlcodeinlinelatexdvp | kitxmlcodeinlinelatexdvpO(1)finkitxmlcodeinlinelatexdvp |
On préférera les listes d'adjacence pour des graphes creux (kitxmlcodeinlinelatexdvp|E|\ll|V|^{2}finkitxmlcodeinlinelatexdvp) ou de degré faible, alors qu'on utilisera des matrices d'adjacence pour des graphes denses (kitxmlcodeinlinelatexdvp|E|\approx|V|^{2}finkitxmlcodeinlinelatexdvp) ou pour des algorithmes désirant accéder aléatoirement aux arêtes.
VII-C. Parcours de sommets▲
On cherche à parcourir tous les sommets accessibles à partir d'un sommet donné. Dans un graphe non dirigé, seule la composante connexe contenant kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp est visitée ; s'il est dirigé, seule une partie de cette composante sera visitée.
VII-C-1. En largeur▲
On parcourt les sommets par ordre croissant de distance par rapport au sommet argument kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp : on visite kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp, puis ses voisins, puis les voisins des voisins, etc., jusqu'à avoir parcouru tous les sommets accessibles.
On devra retenir les sommets déjà visités pour éviter de partir en boucle infinie. De même, on doit retenir les sommets visités dont on n'a pas encore visité les voisins. Aussi, on devra retenir la distance d'un sommet kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp : elle sera finie si le sommet kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp a été visité. La file (structure LIFO) contiendra les sommets qui ont été visités, mais pas leurs voisins.
Chaque sommet sera visité au plus une fois (sa distance à kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp sera alors finie et il ne sera plus parcouru). La boucle principale s'exécutera kitxmlcodeinlinelatexdvp|V|finkitxmlcodeinlinelatexdvp fois, la boucle interne kitxmlcodeinlinelatexdvp|E|finkitxmlcodeinlinelatexdvp fois en tout. La complexité totale sera de kitxmlcodeinlinelatexdvpO(|V|+|E|)finkitxmlcodeinlinelatexdvp.
for each vertex u in G.V n fsg
u.d = infinity
s.d = 0
Q = new create empty Queue
Enqueue(Q, s)
while not QueueEmpty(Q)
u = Dequeue(Q)
for each v in G.Adj[u]
if v.d = 1
v.d = u.d + 1
Enqueue(Q, v)
VII-C-2. En profondeur▲
On part d'un sommet et on suit toutes les arêtes incidentes sans les mettre en attente ; on revient en arrière dès que le sommet visité n'a plus de sommet adjacent.
On retient les sommets visités ; chacun sera empilé au plus une fois.
La boucle principale s'exécutera kitxmlcodeinlinelatexdvp|V|finkitxmlcodeinlinelatexdvp fois, la boucle interne kitxmlcodeinlinelatexdvp|E|finkitxmlcodeinlinelatexdvp fois en tout. La complexité totale sera de kitxmlcodeinlinelatexdvpO(|V|+|E|)finkitxmlcodeinlinelatexdvp.
for each vertex u in G.V n fsg
u.visited = False
S = new create empty stack
Push(S, s)
while not StackEmpty(Q)
u = Pop(S)
if u.visited == False
u.visited = True
for each v in G.Adj[u]
if v.visited == False
Push(S, v)
for each vertex u in G.V
u.visited = False
DFSRec(G, s)
s.visited = True
for each v in G.Adj[s]
if v.visited == False
DFSRec(G, v)
VII-D. Plus court chemin▲
Avec le graphe dirigé kitxmlcodeinlinelatexdvpG=\left(V,E\right)finkitxmlcodeinlinelatexdvp et la fonction de poids associée kitxmlcodeinlinelatexdvpw:E\to\mathbb{R}finkitxmlcodeinlinelatexdvp (le poids peut être négatif, sauf pour certains algorithmes), on définit un chemin du sommet kitxmlcodeinlinelatexdvpv_{1}finkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpv_{k}finkitxmlcodeinlinelatexdvp comme une séquence de nœuds kitxmlcodeinlinelatexdvpp=\left\{ v_{1},v_{2}\dots v_{k}\right\}finkitxmlcodeinlinelatexdvp telle que chaque paire consécutive de ces nœuds forme une arête du graphe. Le poids associé à un chemin kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp sera la somme des poids des arêtes qui le composent :
kitxmlcodelatexdvpw\left(p\right)=\sum\limits _{i=1}^{k-1}w\left(v_{i},v_{i+1}\right).finkitxmlcodelatexdvpAinsi, un plus court chemin entre deux sommets sera le chemin de poids le plus faible. On note kitxmlcodeinlinelatexdvp\delta\left(u,v\right)finkitxmlcodeinlinelatexdvp le poids associé au plus court chemin entre kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp ; s'il n'existe pas de tel chemin, ce poids sera infini ; on peut définir ce symbole comme
kitxmlcodelatexdvp\delta\left(u,v\right)=\min\left(\left\{ w\left(p\right)\,\Big|\, p\text{ chemin de }u\text{ à }v\right\} \cup\{\infty\}\right).finkitxmlcodelatexdvpOn recense divers variantes du problème de recherche de plus court chemin :
- origine unique : trouver tous les chemins les plus courts d'un sommet à tous les autres (algorithmes de Dijkstra et de Bellman-Ford ; pour un graphe dirigé acyclique : relâchement selon un ordre topologique, kitxmlcodeinlinelatexdvp\Theta\left(\left|V\right|+\left|E\right|\right)finkitxmlcodeinlinelatexdvp ; graphe dirigé et poids positifs : Dijkstra et kitxmlcodeinlinelatexdvp\Theta\left(\left|E\right|\log\left|V\right|\right)finkitxmlcodeinlinelatexdvp ; graphe dirigé et poids quelconques : Bellman-Ford et kitxmlcodeinlinelatexdvp\Theta\left(\left|V\right|\cdot\left|E\right|\right)finkitxmlcodeinlinelatexdvp) ;
- destination unique : trouver tous les chemins les plus courts de tous les sommets vers un sommet donné ;
- paire unique : trouver le plus court chemin entre deux sommets (tous les algorithmes connus ont la même complexité asymptotique dans le pire cas que le problème d'origine unique) ;
- toutes les paires : trouver tous les chemins les plus courts entre toutes les paires de sommets du graphe (algorithme de Floyd-Warshall, kitxmlcodeinlinelatexdvp\Theta\left(\left|V\right|^{3}\right)finkitxmlcodeinlinelatexdvp).
VII-D-1. Propriétés▲
VII-D-1-a. Propriété de sous-structure optimale▲
Tout sous-chemin d'un chemin le plus court est un chemin le plus court entre ses extrémités.
On peut le prouver par l'absurde : on prend un plus court chemin kitxmlcodeinlinelatexdvpp_{uv}finkitxmlcodeinlinelatexdvp entre kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp et un sous-chemin kitxmlcodeinlinelatexdvpp_{xy}finkitxmlcodeinlinelatexdvp défini entre ses extrémités kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp. S'il existe un chemin plus court que kitxmlcodeinlinelatexdvpp_{xy}finkitxmlcodeinlinelatexdvp entre kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp, on pourrait remplacer la portion kitxmlcodeinlinelatexdvpp_{xy}finkitxmlcodeinlinelatexdvp de kitxmlcodeinlinelatexdvpp_{uv}finkitxmlcodeinlinelatexdvp par ce chemin plus court et ainsi obtenir un chemin plus court que kitxmlcodeinlinelatexdvpp_{uv}finkitxmlcodeinlinelatexdvp entre kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp.
VII-D-1-b. Cycles▲
En cas de cycle de poids négatif (on parle également de cycle absorbant), on peut réduire autant que l'on veut le poids d'un chemin en y passant aussi souvent que nécessaire. Ainsi, si un chemin de kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp passe par un cycle négatif, on posera
kitxmlcodelatexdvp\delta\left(u,v\right)=-\infty.finkitxmlcodelatexdvpUn chemin le plus court ne peut pas contenir de cycle. S'il a un poids négatif, on ne définit pas de chemin le plus court. S'il a un poids nul, il n'y a pas de raison d'utiliser ces cycles (et on ne les utilisera pas). S'il a un poids positif, on peut obtenir un chemin plus court en les supprimant.
VII-D-1-c. Inégalité triangulaire▲
Pour tous kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp dans kitxmlcodeinlinelatexdvpVfinkitxmlcodeinlinelatexdvp, on a
kitxmlcodelatexdvp\delta\left(u,v\right)\leq\delta\left(u,w\right)+\delta\left(w,v\right),\forall w\in V.finkitxmlcodelatexdvpEn effet, si on a un plus court chemin entre kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp, passer par un nouveau nœud supplémentaire ne peut être qu'indifférent ou négatif.
On dispose aussi d'un corollaire : l'origine kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp étant fixée, pour tout kitxmlcodeinlinelatexdvpu\in Vfinkitxmlcodeinlinelatexdvp,
kitxmlcodelatexdvp\delta\left(s,v\right)\leq\delta\left(s,u\right)+w\left(u,v\right),\forall v\in V.finkitxmlcodelatexdvpVII-D-2. Problème d'origine unique▲
D'un graphe dirigé kitxmlcodeinlinelatexdvpGfinkitxmlcodeinlinelatexdvp et pondéré par la fonction de poids kitxmlcodeinlinelatexdvpwfinkitxmlcodeinlinelatexdvp, d'un sommet kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp (l'origine), on veut imposer à chaque sommet deux attributs : la plus grande distance de kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp à ce nœud (kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp) et le prédécesseur dans un plus court chemin (kitxmlcodeinlinelatexdvpv.\pifinkitxmlcodeinlinelatexdvp), pour former l'arbre des plus courts chemins partant de kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp et reconstruire la solution optimale (par la propriété de sous-structure optimale, cette information est suffisante : on connaît un nœud, on y ajoute le chemin le plus court pour le nœud précédent et on a le chemin total ; par application récursive, on trouve le chemin optimal).
Par force brute, la résolution n'est pas forcément aisée : si on essaie tous les chemins possibles dans un graphe, on risque de tomber sur un cycle ; or, dans ce cas, le nombre de chemins est infini ! De même, on peut avoir un nombre de chemins exponentiel par rapport au nombre de sommets et d'arêtes (si on a kitxmlcodeinlinelatexdvpO\left(n\right)finkitxmlcodeinlinelatexdvp nœuds et que presque tous ont un degré deux, on aura kitxmlcodeinlinelatexdvpO\left(2^{n}\right)finkitxmlcodeinlinelatexdvp chemins, d'où la complexité de l'algorithme dans ce cas précis ; dans le pire cas, on peut monter à une complexité de kitxmlcodeinlinelatexdvpO\left(n^{n}\right)finkitxmlcodeinlinelatexdvp, si chaque nœud a un degré kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp, soit pour un graphe hyperconnexe).
VII-D-2-a. Schéma général d'un algorithme▲
On cherche d'abord à calculer les distances par rapport à un nœud. On se basera sur l'inégalité triangulaire. Après initialisation de toutes les distances à kitxmlcodeinlinelatexdvp\inftyfinkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp contiendra à chaque fois une estimation d'un plus court chemin de kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp ; on maintiendra l'invariant
kitxmlcodelatexdvpv.d\geq\delta\left(s,v\right).finkitxmlcodelatexdvpChaque itération tentera d'améliorer l'estimation kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp pour converger vers la longueur du chemin le plus court.
Pour améliorer une arête, on utilisera une routine de relâchement (terme hérité de la programmation linéaire), afin de mettre à jour l'approximation si on se rapproche de l'optimum. L'algorithme verra sa complexité varier fortement en fonction du choix de l'arête : notamment, à la première itération, tout choix de nœud qui n'est pas directement accessible depuis l'itération conduira à ne rien faire (la longueur depuis l'origine étant infinie par initialisation).
La fonction de poids kitxmlcodeinlinelatexdvpwfinkitxmlcodeinlinelatexdvp est intégrée dans la structure de graphe.
// Initialisation
for each v in G.V
v.d = infinity
v.p = NIL
s.d = 0
while there is one edge (u, v) such that v.d > u.d + w(u; v)
Pick one edge (u,v)
// Relaxation
if v.d > u.d + G.w(u; v)
v.d = u.d + w(u; v)
v.p = u
Grâce aux propriétés ci-après, pour montrer qu'un algorithme de plus court chemin basé sur ce squelette est correct, il suffira de montrer que le choix des arêtes mènera bien à
kitxmlcodelatexdvpv.d=\delta\left(s,v\right),\forall v\in V.finkitxmlcodelatexdvpOn note que, si le gardien de la boucle autorisait l'égalité, l'algorithme partirait en boucle infinie, vu qu'il est attendu que kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp atteigne kitxmlcodeinlinelatexdvp\delta\left(s,v\right)finkitxmlcodeinlinelatexdvp sans descendre plus bas.
L'algorithme général maintient toujours l'invariant.
On peut le prouver par induction sur le nombre d'itérations. Après l'initialisation, l'invariant est respecté ; ensuite, pour chaque itération, avant le relâchement, on considère l'invariant respecté. Par l'inégalité triangulaire, on a alors
kitxmlcodelatexdvp\delta\left(s,v\right)\leq\delta\left(s,u\right)+\delta\left(u,v\right)\leq u.d+w\left(u,v\right).finkitxmlcodelatexdvpAprès l'assignation, si elle a lieu,
kitxmlcodelatexdvpv.d=u.d+w\left(u,v\right),finkitxmlcodelatexdvpl'invariant est toujours respecté, car
kitxmlcodelatexdvpv.d\geq\delta\left(s,v\right).finkitxmlcodelatexdvpL'algorithme général ne modifie plus kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp une fois l'optimum atteint.
On a toujours
kitxmlcodelatexdvpv.d\geq\delta\left(s,v\right)finkitxmlcodelatexdvpet la relaxation ne peut que diminuer la valeur de kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp.
L'algorithme général s'arrête à des chemins d'au plus kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp arêtes après la kitxmlcodeinlinelatexdvpi^{\text{e}}finkitxmlcodeinlinelatexdvp itération.
En d'autres termes,
kitxmlcodelatexdvpv.d\leq\min\left\{ w\left(p\right)\,\Big|\,\left|p\right|\leq i\right\}.finkitxmlcodelatexdvpOn doit imposer un graphe sans cycle de poids négatif.
Avant la première itération,
kitxmlcodelatexdvpv.d=\infty.finkitxmlcodelatexdvpEnsuite, on suppose qu'on a, avant l'itération kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp,
kitxmlcodelatexdvpv.d\leq\min\left\{ w\left(p\right)\,\Big|\,\left|p\right|\leq i-1\right\}.finkitxmlcodelatexdvpCette propriété reste vraie pendant toute l'itération, puisque le relâchement ne peut que faire diminuer kitxmlcodeinlinelatexdvpv.dfinkitxmlcodeinlinelatexdvp ; cette itération considère tous les chemins avec kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp arêtes ou moins en relaxant les arêtes entrantes en kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp, les poids ayant été calculés pour kitxmlcodeinlinelatexdvpi-1finkitxmlcodeinlinelatexdvp arêtes.
VII-D-2-b. Algorithme de Bellman-Ford▲
L'algorithme de Bellman-Ford se base sur la relaxation pour effectuer son travail et est l'algorithme le plus général pour résoudre ce problème. On considère les arêtes kitxmlcodeinlinelatexdvpe_{1},e_{2}\dots\, e_{m}finkitxmlcodeinlinelatexdvp dans un ordre quelconque et on effectue kitxmlcodeinlinelatexdvp\left|V\right|-1finkitxmlcodeinlinelatexdvp relaxations dans cet ordre précis. On aura donc le pseudocode suivant.
// Initialisation
for each v in G.V
v.d = infinity
v.p = NIL
s.d = 0
for i=1 to |G:V| - 1
for each edge (u, v) in G.E
// Relaxation
if v.d > u.d + G.w(u, v)
v.d = u.d + w(u, v)
v.p = u
En supposant qu'on puisse parcourir les arêtes en kitxmlcodeinlinelatexdvpO\left(\left|E\right|\right)finkitxmlcodeinlinelatexdvp, puisque la boucle interne est exécutée exactement kitxmlcodeinlinelatexdvp\left|V\right|-1finkitxmlcodeinlinelatexdvp fois, l'algorithme aura une complexité kitxmlcodeinlinelatexdvp\Theta\left(\left|V\right|\left|E\right|\right)finkitxmlcodeinlinelatexdvp.
Cet algorithme implémente une approche par programmation dynamique de manière itérative. Si on note kitxmlcodeinlinelatexdvpv.d\left[i\right]finkitxmlcodeinlinelatexdvp la longueur du plus court chemin de kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp utilisant au plus kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp arêtes, on a la formule récursive
kitxmlcodelatexdvp v.d\left[i\right]=\begin{cases} 0 & \text{si }v=s\text{ et }i=0\\ \infty & \text{si }v\neq s\text{ et }i=0\\ \min_{\left(u,v\right)\in E}\left\{ u.d\left[i-1\right]+w\left(u,v\right)\right\} & \text{sinon} \end{cases} finkitxmlcodelatexdvpVII-D-2-b-i. Correction▲
Si le graphe ne contient pas de cycle négatif, l'algorithme de Bellman-Ford est correct. En d'autres termes, on a à la fin
kitxmlcodelatexdvpv.d=\delta\left(s,v\right),\forall v\in V.finkitxmlcodelatexdvpLe graphe n'ayant pas de cycle de poids négatif, tout plus court chemin doit être simple (acyclique). Un tel chemin a au plus kitxmlcodeinlinelatexdvp\left|V\right|finkitxmlcodeinlinelatexdvp sommets, donc kitxmlcodeinlinelatexdvp\left|V\right|-1finkitxmlcodeinlinelatexdvp arêtes. Après kitxmlcodeinlinelatexdvp\left|V\right|-1finkitxmlcodeinlinelatexdvp itérations, on a
kitxmlcodelatexdvpv.d\leq\delta\left(s,v\right).finkitxmlcodelatexdvpOr, par l'invariant, on a
kitxmlcodelatexdvpv.d\geq\delta\left(s,v\right),finkitxmlcodelatexdvpd'où
kitxmlcodelatexdvpv.d=\delta\left(s,v\right).finkitxmlcodelatexdvpVII-D-2-b-ii. Détection des cycles négatifs▲
On peut implémenter une recherche de cycles de poids négatif à la fin de l'algorithme ; il renverra une valeur booléenne vraie en cas de cycle négatif accessible depuis kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp.
// Initialisation
for each v in G.V
v.d = infinity
v.p = NIL
s.d = 0
for i=1 to |G:V| - 1
for each edge (u, v) in G.E
// Relaxation
if v.d > u.d + G.w(u, v)
v.d = u.d + w(u, v)
v.p = u
for each edge (u, v) in G.E
if v.d > u.d + w(u, v)
return False
return true
VII-D-2-b-iii. Graphe dirigé acyclique▲
On peut rendre l'algorithme plus efficace dans le cas d'un graphe dirigé acyclique.
// Initialisation
L = TopSort(G)
for each v in G.V
v.d = infinity
v.p = NIL
s.d = 0
for each vertex u, taken in their order in L
for each vertex v in G.Adj[u]
// Relaxation
if v.d > u.d + G.w(u, v)
v.d = u.d + w(u, v)
v.p = u
VII-D-2-c. Parcours en largeur▲
Si les poids sont tous égaux à l'unité, un parcours en largeur suffit pour déterminer le plus court chemin en kitxmlcodeinlinelatexdvpO\left(\left|V\right|+\left|E\right|\right)finkitxmlcodeinlinelatexdvp. Pour simuler des chemins plus longs, on peut ajouter des nœuds intermédiaires ; si les poids sont des entiers dans l'intervalle kitxmlcodeinlinelatexdvp\left[1;W\right]finkitxmlcodeinlinelatexdvp, la complexité sera alors de kitxmlcodeinlinelatexdvpO\left(\left|V\right|+W\,\left|E\right|\right)finkitxmlcodeinlinelatexdvp, soit déjà un grand gain par rapport à l'algorithme de Bellman-Ford. On ira ainsi des nœuds adjacents à l'origine aux nœuds les plus excentrés.
for each vertex u in G.V\{s}
u.d = 1
s.d = 0
Q = new empty Queue
Enqueue(Q, s)
while not QueueEmpty(Q)
u = Dequeue(Q)
for each v in G:Adj[u]
if v.d = 1
v.d = u.d + 1
Enqueue(Q, v)
VII-D-2-d. Algorithme de Dijkstra▲
Partant de la structure d'un parcours en largeur, on peut utiliser une approche gloutonne en généralisant au passage à des poids positifs non entiers. On maintient un ensemble kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp de sommets dont le poids d'un plus court chemin à partir de kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp est connu (utilité surtout théorique) ; à chaque étape, on ajoute un sommet kitxmlcodeinlinelatexdvpv\in V\setminus Sfinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp dont la distance à kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp est minimale. On met alors à jour les distances estimées des sommets adjacents à kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp, en maintenant une file à priorité minimale kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp pour déterminer l'ordre des éléments à traiter.
// Initialisation
for each v in G.V
v.d = infinity
v.p = NIL
s.d = 0
S = empty set
Q = new empty min priority queue from G.V
while not QueueEmpty(Q)
u = QueueExtractMin(Q)
S = union of S and {u}
for each v 2 G:Adj[u]
if v.d > u.d + G.w(u, v)
v.d = u.d + w(u, v)
v.p = u
QueueSetPriority(v, v.d)
Si la file à priorité est implémentée par un tas, l'extraction et l'ajustement de la clé sont réalisés en kitxmlcodeinlinelatexdvpO\left(\log n\right)finkitxmlcodeinlinelatexdvp. Chaque sommet est extrait de la file à priorité une et une seule fois, d'où une boucle principale en kitxmlcodeinlinelatexdvpO\left(\left|V\right|\log\left|V\right|\right)finkitxmlcodeinlinelatexdvp, sans compter le relâchement. Chaque arête est parcourue une et une seule fois, ce qui entraîne au plus un ajustement de clé, d'où une boucle interne en kitxmlcodeinlinelatexdvpO\left(\left|E\right|\log\left|V\right|\right)finkitxmlcodeinlinelatexdvp. En tout, on aura un algorithme de complexité kitxmlcodeinlinelatexdvpO\left(\left(\left|V\right|+\left|E\right|\right)\log\left|V\right|\right)finkitxmlcodeinlinelatexdvp ; si on considère le graphe connexe, on peut la simplifier en kitxmlcodeinlinelatexdvpO\left(\left|E\right|\log\left|V\right|\right)finkitxmlcodeinlinelatexdvp.
VII-D-2-d-i. Correction▲
On cherche à prouver que l'algorithme de Dijkstra se termine avec
kitxmlcodelatexdvpv.d=\delta\left(s,v\right).finkitxmlcodelatexdvpOn remarque tout d'abord que, lorsqu'un nœud kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp est extrait de la file kitxmlcodeinlinelatexdvpQfinkitxmlcodeinlinelatexdvp, son champ kitxmlcodeinlinelatexdvpdfinkitxmlcodeinlinelatexdvp n'est plus modifié. On montre donc que la propriété ci-dessus est respectée quand on extrait un nœud de la file. L'invariant de l'algorithme général est toujours
kitxmlcodelatexdvpv.d\geq\delta\left(s,v\right).finkitxmlcodelatexdvpOn procède par l'absurde en supposant qu'il existe un nœud kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp tel que
kitxmlcodelatexdvpu.d>\delta\left(s,u\right)finkitxmlcodelatexdvplors de son extraction et qu'il s'agisse du premier nœud satisfaisant cette propriété. On considère alors kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp, le premier nœud d'un plus court chemin de kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp se trouvant dans la file avant l'extraction de kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp. kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp en est le prédécesseur. kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp étant le premier nœud violant l'invariant, on doit avoir
kitxmlcodelatexdvpx.d=\delta\left(s,x\right).finkitxmlcodelatexdvpPar la propriété de sous-structure optimale, le sous-chemin de kitxmlcodeinlinelatexdvpsfinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp est un plus court chemin, kitxmlcodeinlinelatexdvpy.dfinkitxmlcodeinlinelatexdvp a été assigné, lors de l'extraction de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp, à
kitxmlcodelatexdvp \begin{eqnarray} y.d & = & x.d+w\left(x,y\right)\\ & = & \delta\left(s,x\right)+w\left(x,y\right)\\ & = & \delta\left(s,y\right)\\ & \leq & \delta\left(s,u\right)\\ & \leq & u.d. \end{eqnarray} finkitxmlcodelatexdvpOr, on doit avoir, puisqu'on s'apprête à extraire kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp de la file,
kitxmlcodelatexdvpy.d\geq u.d.finkitxmlcodelatexdvpOn doit donc avoir
kitxmlcodelatexdvpy.d=\mathbf{u.d}=\delta\left(s,y\right)=\boldsymbol{\delta\left(s,u\right)},finkitxmlcodelatexdvpce qui contredit l'hypothèse.
VII-D-3. Problème de recherche pour toutes les paires▲
D'un graphe dirigé kitxmlcodeinlinelatexdvpGfinkitxmlcodeinlinelatexdvp et de sa fonction de poids kitxmlcodeinlinelatexdvpwfinkitxmlcodeinlinelatexdvp, on cherche à produire une matrice kitxmlcodeinlinelatexdvpDfinkitxmlcodeinlinelatexdvp de taille kitxmlcodeinlinelatexdvpn\times nfinkitxmlcodeinlinelatexdvp, où
kitxmlcodelatexdvpd_{ij}=\delta\left(i,j\right).finkitxmlcodelatexdvpVII-D-3-a. Réutilisation des algorithmes de Bellman-Ford ou Dijkstra▲
Dans le cas général, on peut appliquer l'algorithme de Bellman-Ford sur chaque sommet pour obtenir une complexité de kitxmlcodeinlinelatexdvpO\left(\text{\left|V\right|}^{2}\left|E\right|\right)finkitxmlcodeinlinelatexdvp (si le graphe est dense, kitxmlcodeinlinelatexdvp\left|E\right|=\Theta\left(\left|V\right|^{2}\right)finkitxmlcodeinlinelatexdvp et on a la complexité kitxmlcodeinlinelatexdvpO\left(\left|V\right|^{4}\right)finkitxmlcodeinlinelatexdvp.
Si on n'a pas de poids négatif, on peut utiliser l'algorithme de Dijkstra et améliorer la complexité en kitxmlcodeinlinelatexdvpO\left(\left|V\right|\left|E\right|\log\left|V\right|\right)finkitxmlcodeinlinelatexdvp (ou, pour un graphe dense, kitxmlcodeinlinelatexdvpO\left(\left|V\right|^{3}\log\left|V\right|\right)finkitxmlcodeinlinelatexdvp).
VII-D-3-b. Algorithme de Floyd-Marshall▲
En appliquant les principes de la programmation dynamique, on peut réduire la complexité à kitxmlcodeinlinelatexdvpO\left(\left|V\right|^{3}\right)finkitxmlcodeinlinelatexdvp par l'algorithme de Floyd-Marshall.
Pour un chemin kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp, un sommet intermédiaire sera un sommet qui n'est ni origine ni extrémité de ce chemin. On notera kitxmlcodeinlinelatexdvpd_{ij}^{k}finkitxmlcodeinlinelatexdvp le poids d'un plus court chemin en kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpjfinkitxmlcodeinlinelatexdvp n'utilisant qu'un sous-ensemble des kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp premiers nœuds comme sommets intermédiaires.
L'algorithme reviendra à implémenter le fait que, si kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp n'est pas un sommet intermédiaire de kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp, alors tous les sommets intermédiaires de kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp sont dans kitxmlcodeinlinelatexdvp\left[1;k-1\right]finkitxmlcodeinlinelatexdvp, sinon, tous les sommets intermédiaires des sous-chemins entre kitxmlcodeinlinelatexdvpifinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp et entre kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpjfinkitxmlcodeinlinelatexdvp appartiennent à kitxmlcodeinlinelatexdvp\left[1;k-1\right]finkitxmlcodeinlinelatexdvp. De manière récursive,
kitxmlcodelatexdvp d_{ij}^{k}=\begin{cases} w\left(i,j\right) & \text{si }k=0\\ \min\left\{ d_{ij}^{k-1},d_{ik}^{k-1}+d_{kj}^{k-1}\right\} & \text{si }k>0 \end{cases}. finkitxmlcodelatexdvpCi-dessous, une implémentation ascendante basée sur la matrice d'adjacence pondérée (kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp si l'arête n'est pas dans le graphe, son poids sinon).
D[0] = W
for k = 1 to n
let D[k] = (d[k][i][j]) be a new n * n matrix
for i = 1 to n
for j = 1 to n
d[k][i][j] = min{d[k-1][i][j], d[k-1][i][k] + d[k-1][k][j]}
return D[n]
VII-E. Arbre couvrant▲
Un arbre couvrant pour un graphe connexe non dirigé kitxmlcodeinlinelatexdvpG=\left(V,E\right)finkitxmlcodeinlinelatexdvp est un arbre (graphe acyclique) kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp tel que
- l'ensemble des nœuds de kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp est kitxmlcodeinlinelatexdvpVfinkitxmlcodeinlinelatexdvp ;
- l'ensemble des arcs de kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp est un sous-ensemble de kitxmlcodeinlinelatexdvpEfinkitxmlcodeinlinelatexdvp.
Il sera dit de poids minimal pour un graphe pondéré par la fonction kitxmlcodeinlinelatexdvpwfinkitxmlcodeinlinelatexdvp si, de plus, on a la valeur minimale de
kitxmlcodelatexdvp\sum\limits _{e\in E}w\left(e\right).finkitxmlcodelatexdvpVII-E-2. Approche générique▲
Un arbre couvant minimal est un sous-ensemble d'arêtes du graphe initial ; ainsi, on part avec un ensemble kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp vide auquel on ajoute des arêtes en respectant l'invariant qu'il existe un arbre couvrant minimal qui contienne ces arêtes. À cette fin, on définit la notion d'arête sûre : une arête kitxmlcodeinlinelatexdvp\left(u,v\right)finkitxmlcodeinlinelatexdvp sera sûre pour kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp si et seulement s'il existe un arbre couvrant minimal qui contient les arêtes de kitxmlcodeinlinelatexdvpA\cup\left\{ \left(u,v\right)\right\}finkitxmlcodeinlinelatexdvp.
On se retrouve donc avec l'algorithme générique suivant, il faudra encore caractériser plus avant les arêtes sûres.
A = empty set
while A is not a spanning tree
find an edge (u, v) that is safe for A
A = union of A and {(u, v)}
return A
VII-E-3. Caractérisation des arêtes sûres▲
Avec kitxmlcodeinlinelatexdvpS\subsetneq Afinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpA\subseteq Efinkitxmlcodeinlinelatexdvp, on définit la coupure kitxmlcodeinlinelatexdvp\left(S,V\setminus S\right)finkitxmlcodeinlinelatexdvp comme une partition des sommets en deux ensembles disjoints kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpV\setminus Sfinkitxmlcodeinlinelatexdvp, l'arête traverse d'une coupure kitxmlcodeinlinelatexdvp\left(S,V\setminus S\right)finkitxmlcodeinlinelatexdvp comme une arête dont une extrémité est dans kitxmlcodeinlinelatexdvpSfinkitxmlcodeinlinelatexdvp et l'autre dans kitxmlcodeinlinelatexdvpV\setminus Sfinkitxmlcodeinlinelatexdvp. Une coupure respecte kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp si et seulement s'il n'y a pas d'arête dans kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp qui traverse la coupure.
VII-E-3-a. Propriété des choix gloutons optimaux▲
Soit kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp, un sous-ensemble d'un arbre couvrant minimal ; kitxmlcodeinlinelatexdvp\left(S,V\setminus S\right)finkitxmlcodeinlinelatexdvp, une coupure qui respecte kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp ; kitxmlcodeinlinelatexdvp\left(u,v\right)finkitxmlcodeinlinelatexdvp, une arête de poids minimal qui traverse la coupure kitxmlcodeinlinelatexdvp\left(S,V\setminus S\right)finkitxmlcodeinlinelatexdvp ; ainsi, kitxmlcodeinlinelatexdvp\left(u,v\right)finkitxmlcodeinlinelatexdvp est une coupure sûre pour kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp.
On considère kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp, un arbre couvrant minimal qui inclut kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp. On suppose que kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp ne contient pas kitxmlcodeinlinelatexdvp\left\{ \left(u,v\right)\right\}finkitxmlcodeinlinelatexdvp et on montre qu'il est possible de construire un arbre kitxmlcodeinlinelatexdvpT'finkitxmlcodeinlinelatexdvp qui inclut kitxmlcodeinlinelatexdvpA\cup\left\{ \left(u,v\right)\right\}finkitxmlcodeinlinelatexdvp.
Puisque kitxmlcodeinlinelatexdvpTfinkitxmlcodeinlinelatexdvp est un arbre, le chemin kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp de kitxmlcodeinlinelatexdvpufinkitxmlcodeinlinelatexdvp à kitxmlcodeinlinelatexdvpvfinkitxmlcodeinlinelatexdvp est unique, il traverse la coupure kitxmlcodeinlinelatexdvp\left(S,V\setminus S\right)finkitxmlcodeinlinelatexdvp. Soit kitxmlcodeinlinelatexdvp\left(x,y\right)finkitxmlcodeinlinelatexdvp, une arête de kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp qui traverse cette coupure. Puisque kitxmlcodeinlinelatexdvp\left(u,v\right)finkitxmlcodeinlinelatexdvp est une arête de poids minimum qui traverse la coupure,
kitxmlcodelatexdvpw\left(u,v\right)\leq w\left(x,y\right).finkitxmlcodelatexdvpPuisque la coupure respecte kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp, l'arête kitxmlcodeinlinelatexdvp\left(x,y\right)finkitxmlcodeinlinelatexdvp ne fait pas partie de kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp.
On définit alors kitxmlcodeinlinelatexdvpT'finkitxmlcodeinlinelatexdvp comme
kitxmlcodelatexdvpT'=\left(T\setminus\left\{ \left(x,y\right)\right\} \right)\cup\left\{ \left(u,v\right)\right\} .finkitxmlcodelatexdvpkitxmlcodeinlinelatexdvpT'finkitxmlcodeinlinelatexdvp est un arbre couvrant et
kitxmlcodelatexdvpw\left(T'\right)=w\left(T\right)-w\left(x,y\right)+w\left(u,v\right)\leq w\left(T\right)\text{, puisque }w\left(u,v\right)\leq w\left(x,y\right).finkitxmlcodelatexdvpkitxmlcodeinlinelatexdvpT'finkitxmlcodeinlinelatexdvp est donc bien un arbre couvrant minimal tel que kitxmlcodeinlinelatexdvpA\cup\left\{ \left(u,v\right)\right\} \subseteq T'finkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvp\left(u,v\right)finkitxmlcodeinlinelatexdvp est bien une arête sûre pour kitxmlcodeinlinelatexdvpAfinkitxmlcodeinlinelatexdvp.
VII-E-4. Algorithme de Kruskal▲
On construit une forêt, en ajoutant progressivement des arêtes à un graphe dépourvu d'arcs initialement. On maintient en permanence une partition du graphe en cours de construction en ses composantes connexes. Pour relier des composantes connexes, on choisit à chaque fois l'arête de poids minimal qui les connecte. On s'arrête dès qu'il ne reste plus qu'une composante connexe.
Cet algorithme est correct : puisqu'on connecte à chaque fois deux composantes connexes disjointes, on forme un arbre ; à la fin, on forme un arbre de recouvrement. Puisqu'on sélectionne une arête de poids minimal à chaque étape, la propriété des choix gloutons optimaux garantit que le résultat sera bien un arbre de recouvrement minimal.
On peut choisir les composantes connexes à fusionner de manière totalement arbitraire.
A = empty set
P = empty set
for each vertex v 2 G:V
P = union of P and {{v}}
for each (u, v) in G.E taken into nondecreasing order of weight
P1 = subset in P containing u
P2 = subset in P containing v
if P1 != P2
A = union of A and {(u, v)}
P = P1 merged with P2
return A
VIII. Conclusion▲
Au bout de ce cheminement relativement ardu, vous voilà armés pour partir dans vos développements en connaissance de cause : vous aurez le bagage théorique pour comprendre pourquoi votre algorithme est lent, pour prouver à quiconque que cette partie du programme ne peut être que fausse et que l'erreur doit venir des paramètres donnés. Ces algorithmes sont aussi utiles en eux-mêmes : qui n'a jamais rêvé de programmer son propre jeu ? Parmi les algorithmes que l'on rencontre le plus souvent, on retrouve bien évidemment le problème de plus court chemin à origine unique, sous un nom plus onirique : la recherche de chemin (ou path finding). On utilisera cependant plus généralement un algorithme plus rapide, mais qui ne donne pas toujours la solution optimale : A*, une extension de l'algorithme de Dijkstra.
IX. Références et remerciements▲
Ce cours se base essentiellement sur le cours oral de structures de données et algorithmes de Pierre Geurts (INFO0902), donné à l'ULg pendant l'année académique 2011-2012. Les livres Introduction to Algorithms (troisième édition) et Structures de données avancées avec la STL, première édition, de Philippe Gabrini ont été utilisés comme compléments occasionnels.
Merci à Kevin Vanaertenryck pour ses remarques sur le contenu et à Claude Leloup pour sa relecture orthographique !