Un tas est un tableau que l'on se représente comme un arbre binaire complet.
Il respecte la condition de tas
Par exemple, le tableau suivant est un tas
T = [ 16, 10, 14, 8, 2, 12, 6, 4]
h.afficher_tas(T)
Pour un tas dont les indices sont numérotés de $0$ à $n-1$, les relations de parenté s'écrivent
def parent(i):
return (i-1) // 2
def enfants(i):
return 2*i+1, 2*i+2
I0 = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
h.afficher_tas(I0)
L'insertion d'un élément dans le tas s'effectue en deux étapes
T.append(13); print(T); h.afficher_tas(T)
# 13 ne respecte pas la condition de tas
[16, 10, 14, 8, 2, 12, 6, 4, 13]
Pour rétablir la condition de tas, il faut faire remonter l'élément dans le tas en l'échangeant avec son parent jusqu'à ce qu'il
def remonter(T,i):
p = parent(i)
while i > 0 and T[p] < T[i]:
print("swap({},{})".format(T[i],T[p]))
T[p],T[i] = T[i],T[p]
i = p; p = parent(i)
print(T)
remonter(T,len(T)-1)
print(T)
h.afficher_tas(T)
[16, 10, 14, 8, 2, 12, 6, 4, 13] swap(13,8) swap(13,10) [16, 13, 14, 10, 2, 12, 6, 4, 8]
L'insertion d'un élément dans le tas s'écrit donc
def inserer_dans_tas(T,val):
T.append(val)
remonter(T,len(T)-1)
La complexité de l'opération est
T.append(val)
remonter(T,n)
Elle est donc logarithmique
Dans un tas, le seul élément que l'on puisse supprimer est
Mais un tas est un tableau, et la seule position d'où l'on peut supprimer efficacement un élément est en queue. On commence donc par échanger tête et queue.
print(T); T[0],T[len(T)-1] = T[len(T)-1],T[0]
print(T); T.pop()
print(T); h.afficher_tas(T)
[16, 13, 14, 10, 2, 12, 6, 4, 8] [8, 13, 14, 10, 2, 12, 6, 4, 16] [8, 13, 14, 10, 2, 12, 6, 4]
Mais l'élément au sommet ne respecte pas la condition de tas. Il faut rétablir la condition de tas en le descendant.
Pour faire descendre un élément, on l'échange avec le plus grand de ses enfants jusqu'à ce qu'il respecte la condition de tas ou qu'il atteigne le fond.
def plus_grand_enfant(T,i):
pge, e2 = enfants(i)
if e2 < len(T) and T[e2] > T[pge]:
pge = e2
return pge
def descendre(T,i):
e = plus_grand_enfant(T,i)
while e < len(T) and T[i] < T[e]:
print("swap({},{})".format(T[i],T[e]))
T[i],T[e] = T[e],T[i]
i = e
e = plus_grand_enfant(T,i)
print(T); descendre(T,0)
print(T); h.afficher_tas(T)
[8, 13, 14, 10, 2, 12, 6, 4] swap(8,14) swap(8,12) [14, 13, 12, 10, 2, 8, 6, 4]
La suppression du sommet de l'élément au sommet du tas s'écrit donc
def supprimer_sommet(T):
T[0],T[len(T)] = T[len(T)],T[0]
T.pop()
descendre(T,0)
La complexité de l'opération est
T.pop()
descendre(T,0)
Elle est donc logarithmique
La manière la plus simple de transformer un tableau en tas est d'en insérer tous les éléments un par un.
Effectuée en place, cette opération s'écrit
def creer_tas_naif(T):
for i in range(1,len(T)):
remonter(T,i)
T = [ 1, 2, 5, 7, 9, 3, 4, 6, 8 ]
creer_tas_naif(T); h.afficher_tas(T)
swap(2,1) swap(5,2) swap(7,1) swap(7,5) swap(9,5) swap(9,7) swap(3,2) swap(4,3) swap(6,1) swap(8,6) swap(8,7)
Cette approche a une complexité $\Theta(n \log n)$.
Il est possible de faire mieux.
plus exactement, on commence par le dernier élément à avoir des enfants, i.e. le parent du dernier élément
def creer_tas(T):
for i in range(parent(len(T)-1),-1,-1):
descendre(T,i)
Visualisons l'algorithme
T = [ 1, 2, 5, 7, 9, 3, 4, 6, 8 ]
h.afficher_tas(T)
descendre(T,3)
swap(7,8)
print(T); h.afficher_tas(T)
[1, 2, 5, 8, 9, 3, 4, 6, 7]
descendre(T,2)
print(T); h.afficher_tas(T)
[1, 2, 5, 8, 9, 3, 4, 6, 7]
descendre(T,1)
swap(2,9)
print(T); h.afficher_tas(T)
[1, 9, 5, 8, 2, 3, 4, 6, 7]
descendre(T,0)
swap(1,9) swap(1,8) swap(1,7)
print(T); h.afficher_tas(T)
[9, 8, 5, 7, 2, 3, 4, 6, 1]
Comparons les deux approches
T = [ 1, 2, 3, 4, 5, 6, 7 ];
creer_tas(T); h.afficher_tas(T)
swap(3,7) swap(2,5) swap(1,7) swap(1,6)
T = [ 1, 2, 3, 4, 5, 6, 7 ];
creer_tas_naif(T); h.afficher_tas(T)
swap(2,1) swap(3,2) swap(4,1) swap(4,3) swap(5,3) swap(5,4) swap(6,2) swap(6,5) swap(7,5) swap(7,6)
La complexité de l'algorithme de création de tas par descente est linéaire à la taille du tas.
En effet, considérons le nombre maximum d'échanges pour la descente de l'élément d'indice i
, en approximant les intervalles pour simplifier
i
$\in$ [n/2,n[
, 0 échangei
$\in$ [n/4,n/2[
, 1 échangei
$\in$ [n/8,n/4[
, 2 échangesi
$\in$ [n/16,n/8[
, 3 échangesi == 0
, $\log n$ échangesLa somme de tous ces termes est $\Theta(n)$
La structure de tas nous permet d'améliorer l'efficacité du tri par sélection. Pour rappel, celui-ci était
def tri_par_selection(T):
N = len(T)
for i in range(0,N-1):
jMin = i
for j in range(i+1,N):
if T[j] < T[jMin]:
jMin = j
T[i],T[jMin] = T[jMin],T[i]
la boucle interne sur j
cherche le minimum de T[i:N]
avec une complexité linéaire, ce qui entraine une complexité globale quadratique pour le tri.
Le structure de tas nous permet de trouver le maximum en temps constant et de le supprimer en temps logarithmique.
On réécrit le tri par sélection à l'envers pour pouvoir en profiter.
def element_max(T,n):
el_max = 0
for j in range(1,n):
if T[j] > T[el_max]: el_max = j
return el_max
def tri_par_selection_max(T):
for i in range(len(T)-1,0,-1):
m = element_max(T,i+1)
T[i],T[m] = T[m],T[i]
print(T[:i],T[i:])
T = [ 4, 2, 7, 5, 9, 1, 3, 8, 6 ]
tri_par_selection_max(T)
[4, 2, 7, 5, 6, 1, 3, 8] [9] [4, 2, 7, 5, 6, 1, 3] [8, 9] [4, 2, 3, 5, 6, 1] [7, 8, 9] [4, 2, 3, 5, 1] [6, 7, 8, 9] [4, 2, 3, 1] [5, 6, 7, 8, 9] [1, 2, 3] [4, 5, 6, 7, 8, 9] [1, 2] [3, 4, 5, 6, 7, 8, 9] [1] [2, 3, 4, 5, 6, 7, 8, 9]
Pour améliorer cet algorithme, on va maintenir T[:i]
, la partie non encore triée, sous forme de tas.
Il faut pouvoir traiter comme un tas les $n$ premiers éléments d'un tableau plutôt que le tableau complet. On ré-écrit donc les fonctions
def plus_grand_enfant(T,i,n):
pge, e2 = enfants(i)
if e2 < n and T[e2] > T[pge]:
pge = e2
return pge
def descendre(T,i,n = None):
if n == None: n = len(T)
e = plus_grand_enfant(T,i,n)
while e < n and T[i] < T[e]:
T[i],T[e] = T[e],T[i]
i = e
e = plus_grand_enfant(T,i,n)
Cela nous permet d'écrire l'algorithme de tri par tas, de complexité linéarithmique $\Theta(n \log n)$
def tri_par_tas(T):
creer_tas(T)
print(T)
for i in range(len(T)-1,0,-1):
T[i],T[0] = T[0],T[i]
descendre(T,0,i)
print(T[:i],T[i:])
T = [ 1, 2, 3, 4, 5, 6, 7, 8]
tri_par_tas(T)
# T[:i] est un tas, T[i:] est trié
[8, 5, 7, 4, 1, 6, 3, 2] [7, 5, 6, 4, 1, 2, 3] [8] [6, 5, 3, 4, 1, 2] [7, 8] [5, 4, 3, 2, 1] [6, 7, 8] [4, 2, 3, 1] [5, 6, 7, 8] [3, 2, 1] [4, 5, 6, 7, 8] [2, 1] [3, 4, 5, 6, 7, 8] [1] [2, 3, 4, 5, 6, 7, 8]
Suivons l'exécution pas à pas
T = [ 1, 2, 3, 4, 5, 6, 7, 8 ]; creer_tas(T);
print(T); h.afficher_tas(T)
[8, 5, 7, 4, 1, 6, 3, 2]
i = 7; T[i],T[0] = T[0],T[i]; descendre(T,0,i)
print(T[:i],T[i:]); h.afficher_tas(T[:i])
[7, 5, 6, 4, 1, 2, 3] [8]
i = 6; T[i],T[0] = T[0],T[i]; descendre(T,0,i)
print(T[:i],T[i:]); h.afficher_tas(T[:i])
[6, 5, 3, 4, 1, 2] [7, 8]
i = 5; T[i],T[0] = T[0],T[i]; descendre(T,0,i)
print(T[:i],T[i:]); h.afficher_tas(T[:i])
[5, 4, 3, 2, 1] [6, 7, 8]
i = 4; T[i],T[0] = T[0],T[i]; descendre(T,0,i)
print(T[:i],T[i:]); h.afficher_tas(T[:i])
[4, 2, 3, 1] [5, 6, 7, 8]
i = 3; T[i],T[0] = T[0],T[i]; descendre(T,0,i)
print(T[:i],T[i:]); h.afficher_tas(T[:i])
[3, 2, 1] [4, 5, 6, 7, 8]
i = 2; T[i],T[0] = T[0],T[i]; descendre(T,0,i)
print(T[:i],T[i:]); h.afficher_tas(T[:i])
[2, 1] [3, 4, 5, 6, 7, 8]
i = 1; T[i],T[0] = T[0],T[i]; descendre(T,0,i)
print(T[:i],T[i:]); h.afficher_tas(T[:i])
[1] [2, 3, 4, 5, 6, 7, 8]
Un tas est un tableau de $n$ éléments organisé de manière à respecter la condition de tas. On peut le concevoir mentalement comme un arbre binaire.
L'insertion a une complexité $\Theta(\log n)$.
Le sommet du tas est à l'indice $0$. On le trouve en $\Theta(1)$.
Supprimer le sommet du tas a une complexité $\Theta(\log n)$.
Créer un tas à partir d'un tableau a une complexité $\Theta(n)$.
Le tri par tas améliore le tri par sélection en utilisant un tas pour trouver le maximum. Il a une complexité $\Theta(n \log n)$.
© Olivier Cuisenaire, 2018 |