• Aucun résultat trouvé

Un tri qui n’est pas un tri par comparaisons : le tri par comptage

Dans cette section, on montre un algorithme de tri qui n’est pas un tri par comparaisons : l’algorithme fait l’hypothèse que les éléments de la liste à trier sont des entiers naturels, bornés par une certaine constante k > 0. Sous cette hypothèse, il atteint un temps d’exécution en O(n + k) pour trier une liste de taille n, ce qui est meilleur que les algorithmes de ce chapitre si k = o(n2) et même meilleur que les tris efficaces (à suivre) si k = o(n log n).

Idée de l’algorithme. On suppose que la liste à trier est constituée d’entiers de l’intervalle [[0, k[[. L’algorithme fonctionne suivant un principe simple : il suffit de parcourir une fois la liste et de compter le nombre d’éléments de la liste égaux à 0, 1, ..., k − 1. Pour ce faire on utilise une liste de taille k. On peut alors facilement procéder à une réécriture de la liste initiale, de sorte qu’en sortie elle soit constituée des mêmes éléments, mais triés dans l’ordre croissant.

Code Python. L’algorithme prend en entrée la liste L à trier, ainsi qu’un entier k tel que tous les éléments de la liste soient des entiers de l’intervalle [[0, k[[. On procède en deux étapes : d’abord compter les éléments de chaque type, ensuite réécrire la listeL.

def tri_comptage(L,k): C=[0]*k for i in range(len(L)): C[L[i]]=C[L[i]]+1 p=0 for i in range(k): for j in range(C[i]): L[p]=i p+=1

Terminaison et correction. L’algorithme termine car il est constitué de boucles for. Si la liste à trier est bien constituée d’éléments de [[0, k[[, la première bouclefor ne produit pas d’erreur, et après cette boucle la somme des

10.6. UN TRI QUI N’EST PAS UN TRI PAR COMPARAISONS : LE TRI PAR COMPTAGE Lycée Masséna

éléments deC vaut exactement la taille n de la liste L. Ainsi, les deux boucles for imbriquées suivantes ne produisent pas non plus d’erreurs car on écrit n fois dans L, aux indices 0, 1, . . . , n − 1. La correction est alors évidente : après la première bouclefor, un élément i ∈ [[0, k[[ apparaît exactement C[i] fois dans la liste L, et on écrit dans L chaque entier i exactementC[i] fois avec les deux boucles imbriquées.

Complexité. Remarquons que la complexité en espace de l’algorithme est en O(k) : on utilise en effet la listeC de taille k pour trierL. La création de la liste C se fait en temps O(k). Le remplissage de C se fait avec la première boucle for en temps O(n). La boucle for j interne a une complexité O(1+C[i]) : en effet, même si C[i] est nul, il faut quand même incrémenter i. Ainsi, les deux bouclesfor imbriquées ont une complexité totale O(Pk−1i=0(1 +C[i])) = O(n+k). On a bien une complexité totale O(n + k).

Remarques. • Une « erreur » classique d’implémentation de l’algorithme consiste à parcourir k fois la listeL pour remplir la listeC (pour i ∈ [[0, k[[, on teste au i-ème parcours si chacun des éléments de L est égal à i). Ceci mène à une complexité O(nk), ce qui est maladroit.

• Ce tri n’est pas un tri par comparaisons, puisqu’on ne compare pas les éléments entre eux.

• Il existe bien d’autres tris qui ne sont pas des tris par comparaisons : le tri par base trie lui aussi des entiers naturels bornés par une certaine constante B, mais n’utilise qu’un espace de taille O(log B) pour une complexité temporelle O(n log B). On utilise en général la base 2, l’algorithme peut alors être vu comme l’application successive de O(log2B) algorithmes de tri par comptage avec k = 2.

• Un autre tri classique est le tri par baquets : il est efficace pour trier des réels supposés bien répartis dans un intervalle semi-ouvert [a, b[. Voir la feuille d’exercices !

Chapitre 11

Structures de données linéaires : piles (et

files)

Introduction

Ce chapitre décrit pour la première fois une structure de donnée abstraite : la pile. C’est une des structure les plus élémentaires mais aussi l’une des plus utiles. Donnons tout de suite une métaphore : de très lourdes assiettes sont empilées les unes sur les autres. Les opérations que l’on peut réaliser sont les suivantes :

— enlever une assiette de la pile (celle du haut), si bien sûr il reste des assiettes dans la pile ; — ajouter une assiette en haut de la pile.

Et c’est tout ! On suppose qu’une assiette ailleurs qu’au sommet n’est pas accessible, à moins de dépiler toutes les assiettes situées au dessus, comme montré en figure 11.1.

base de la pile ... ... ...

sommet : le seul élément accessible

Figure 11.1 – Une pile

Outre les opérations ci-dessus, on voudrait aussi savoir si notre pile est vide, créer éventuellement une nouvelle pile, et savoir si notre pile est pleine (dans le cas où elle a une capacité finie), ce genre de choses. Donnons tout de suite quelques exemples d’application des piles :

— lorsqu’en programmation, on appelle une fonction, cet appel (avec ces paramètres) est empilé sur la pile d’appel. Peu importe qui se situe ailleurs qu’au sommet dans la pile d’appel : seul le dernier appel est traité et doit être résolu avant de revenir à la fonction précédente. On verra qu’en cas d’appels récursifs, ces appels sont empilés successivement sur la pile d’appel, jusqu’à arriver à un cas terminal puis sont dépilés ensuite.

— dans votre navigateur internet, vous avez deux boutons « page suivante » et « page précédente ». Chacun utilise une pile !

— si vous avez une vieille calculatrice qui utilise la notation « polonaise inverse » (par exemple, l’opération (3 + 4) × (5 − 7) se note 3 4 + 5 7 − ×, les parenthèses sont inutiles !), celle-ci est basée sur une pile. On détaillera cet exemple en fin de chapitre.

11.1

Les Piles : une classe abstraite

Commençons par donner la définition d’une classe abstraite. Définition 11.1. Une classe abstraite est un type, muni d’opérations.

11.1. LES PILES : UNE CLASSE ABSTRAITE Lycée Masséna

Cette définition est un peu vague, mais elle a le mérite de fixer les idées : pour définir une pile, on a essentiellement besoin de définir les opérations que l’on veut effectuer. L’implémentation effective d’une pile est indépendante de sa définition en tant que classe abstraite.

Définition 11.2. Le type pile est une structure de données abstraite, munie des opérations suivantes : — creer_pile(c) : construire une pile vide, de capacité c : la pile peut contenir c éléments. — est_vide(P) : renvoie un booléen suivant si la pile P est vide ou non.

— sommet(P) : renvoie le sommet de la pile P si celle-ci est non vide.

— depiler(P) : enlève l’élément au sommet de la pile P et le renvoie. Si la pile est vide, renvoie une erreur. — est_pleine(P) : suivant la réalisation de la pile, renvoie un booléen suivant si la pile P est pleine ou non. Si

elle est pleine, on ne peut plus ajouter d’éléments.

— empiler(P,x) : ajoute l’élément x à la pile P si la pile n’est pas pleine, sinon, renvoie une erreur.

Maintenant que l’on a défini la classe abstraite pile, on peut déja décrire des algorithmes qui vont utiliser la structure, avant même d’avoir proposé une réalisation concrète de la classe.

Donnons tout de suite un exemple très classique mais aussi très instructif : déterminer si un mot est bien parenthésé et indiquer (dans une liste, par exemple) les couples de positions des parenthèses ouvrantes et fermantes.

Définition 11.3. Un mot parenthésé est une chaîne de caractères constituée uniquement des caractères'(' et ')'. Le mot est bien parenthésé si il contient autant de parenthèses ouvrantes que fermantes et si tout préfixe du mot contient au moins autant de parenthèses ouvrantes que fermantes.

Voici quelques exemples :

— Le mot vide'' est bien parenthésé.

— Le mot'(()' n’est pas bien parenthésé car il possède plus de parenthèses ouvrantes que fermantes. — Le mot'(())()' est bien parenthésé : ses préfixes sont au nombre de 7 :

'' '(' '((' '(()' '(())' '(())(' '(())()' et contiennent chacun plus de parenthèses ouvrantes que fermantes, avec égalité éventuelle.

— Le mot'(()()))(()' contient autant de parenthèses ouvrantes que fermantes, mais n’est pas bien parenthésé : il contient en particulier le préfixe'(()()))' qui possède 4 parenthèses fermantes mais seulement 3 ouvrantes. Proposition 11.4. Soit m un mot bien parenthésé. Alors soit m est le mot vide, soit il se décompose de façon unique comme m = (u)v où u et v sont deux mots bien parenthésés, éventuellement vides.

Démonstration. Supposons m non vide. On peut considérer le plus petit préfixe non vide p de m ayant autant de parenthèses ouvrantes que fermantes. Celui-ci existe car l’ensemble des préfixes ayant cette propriété est non vide : m convient. Alors p commence par une parenthèse ouvrante (sinon m n’est pas bien parenthésé), et termine par une parenthèse fermante (même chose), et p s’écrit p = (u) et m = pv = (u)v, avec u et v deux mots. Par minimalité de p, u est un mot bien parenthésé, et puisque m est bien parenthésé, v aussi. D’où l’existence de la décomposition. Montrons maintenant l’unicité : si m = (u0)v0 avec u0 et v0 deux mots bien parenthésés, alors u0 a pour préfixe u (par minimalité de u), mais ne peut contenir le caractère suivant (une parenthèse fermante) sinon m aurait un préfixe contenant plus de parenthèses fermantes qu’ouvrantes. Donc u = u0, et v = v0.

Sur l’exemple du mot vide '', notre algorithme devrait renvoyer une liste vide car il n’y a pas de parenthèses. Sur le mot'(())()', notre algorithme devrait renvoyer la liste d’indices [(1,2),(0,3),(4,5)] (on verra que l’ordre croissant suivant la deuxième composante est naturel).

L’idée de l’algorithme est assez simple : on crée une pile (initialement vide), et on parcourt la chaîne de caractères passée en entrée de gauche à droite. Lorsqu’on examine une parenthèse ouvrante, on empile la position (l’indice dans la chaîne) de cette parenthèse. Lorsqu’on examine une parenthèse fermante, deux cas peuvent se produire :

— la pile est vide : alors on a trouvé un préfixe qui contient plus de parenthèses fermantes qu’ouvrantes, ce qui signifie que le mot n’est pas bien parenthésé.

— la pile est non vide : on dépile alors l’élément en haut de la pile (qui est l’indice d’une parenthèse ouvrante), et on ajoute le couple (indice ouvrant, indice fermant) à la liste des couples en construction.

À la fin du traitement de la chaîne, on vérifie que la pile est vide. Si ce n’est pas le cas, le mot possède plus de parenthèses ouvrantes que fermantes et n’est donc pas bien parenthésé.

Le code Python correspondant est donc le suivant :

Traitement d’une expression parenthésée def parentheses(s):

""" Prend une chaîne de caractères et détermine si l'expression est bien parenthésée. s: chaîne composée de '(' et ')'

Renvoie une liste donnant les couples de positions des parenthèses ouvrantes fermantes qui correspondent, ou affiche une erreur si le mot n'est pas bien parenthésé."""

P=creer_pile(len(s)) #on pourrait optimiser car théoriquement, len(s)/2 éléments suffisent. L=[]

for i in range(len(s)): c=s[i]

assert c=='(' or c==')', "Le mot n'est pas uniquement constitué de parenthèses." if c=='(':

empiler(P,i) else:

if est_vide(P):

print("Le mot n'est pas bien parenthésé.") return

else:

x=depiler(P) L.append((x,i)) if not est_vide(P):

print("Le mot n'est pas bien parenthésé.") else:

return L

Donnons tout de suite quelques exemples d’exécution de la fonction

Exemple d’exécution >>> parentheses('(())()') [(1, 2), (0, 3), (4, 5)] >>> parentheses('') [] >>> parentheses('(()')

Le mot n'est pas bien parenthésé >>> parentheses('(()()))(()') Le mot n'est pas bien parenthésé

Évidemment, il a bien fallu implémenter le type Pile pour faire fonctionner cet exemple. C’est le but de la section suivante.