Archives de catégorie : Structures de données

Structures de données : les graphes

copie de l’article :

https://info-mounier.fr/terminale_nsi/structures_donnees/graphes

merci

Table des matières

  • Introduction
  • Définitions et vocabulaire
    • Vocabulaire des graphes non orientés
    • Vocabulaire des graphes orientés
    • Graphes valués
  • Représentations d’un graphe
    • Représentation par matrice d’adjacence
    • Représentation par listes des successeurs
    • Efficacité des représentations
  • Implémentations
    • Par une matrice d’adjacence
    • Par une liste de successeurs
    • Passage d’une représentation à l’autre
  • Bilan

Introduction

Les graphes sont une structure de données très riche permettant de modéliser des situations variées de relations entre un ensemble d’entités :

  • entre les ordinateurs du réseau internet ;

réseau Internet

Source : Wikipedia, Mro, licence CC BY-SA 3.0

  • entre des personnes sur un réseau social ;

réseau Internet

Source : Pixabay

  • entre les villes dans un réseau routier ou de distribution ;

réseau Internet

Source : Wikipedia, HB, licence CC BY-SA 3.0

  • entre les atomes d’une molécule ;

réseau Internet

Source : Wikipedia, William Crochot, licence CC BY-SA 4.0

  • entre les ressources du Web (les relations sont les liens hypertextes) ;
  • entre deux situations dans un jeu ;
  • etc.

Les relations peuvent être orientées ou non.

Définitions et vocabulaire

Un graphe est constitué d’un ensemble de sommets et dans le cas orienté d’un ensemble d’arcs reliant chacun un sommet à un autre, dans le cas non orienté d’un ensemble d’arêtes entre deux sommets.

Mathématiquement, un graphe G est donc un couple formé de deux ensembles : X = { x 1 , x 2 , … , x n }

dont les éléments sont appelés les sommets et un ensemble A = { a 1 , a 2 , … , a m } dont les éléments sont appelés les arêtes dans le cas non orienté ou les arcs dans le cas orienté. Une arête (ou un arc) a i

est un couple de deux sommets, par exemple : a i = ( x 2 , x 5 )

symbolise le lien (arête ou arc) entre les sommets x 2 et x 5

. On peut noter G = ( X , A )

.

Vocabulaire des graphes non orientés

Dans le cas des graphes non orientés, les relations entre deux sommets se font dans les deux sens. On appelle ses relations des arêtes (edges en anglais), et on a les définitions suivantes.

  • Sommets adjacents : deux sommets sont adjacents s’ils sont reliés entre eux par une arête. On dit que l’arête est incidente aux deux sommets.
  • Voisins d’un sommet x : ce sont tous les sommets reliés à x par une arête .
  • Degré d’un sommet x : nombre d’arêtes incidentes au sommet , on le note d ( x ).
  • Chaîne : séquence ordonnée d’arêtes telle que chaque arête a une extrémité en commun avec l’arête suivante.
  • Cycle : dans un graphe non orienté, un cycle est une suite d’arêtes consécutives (chaîne) dont les deux sommets extrémités sont identiques.
  • Boucle : il peut exister des arêtes entre un sommet x et lui-même. Elles sont appelés boucles.

Exemple

graphe non orienté
  • Ce graphe non orienté est donné par :
    • un ensemble de sommets : { A , B , C , D , E , F , G }
    • un ensemble d’arêtes : { ( A , B ) , ( A , F ) , ( A , G ) , ( B , C ) , ( B , F ) , … }

  • Les sommets A et B sont adjacents mais B et D ne le sont pas.
  • Les voisins du sommet A sont B, F et G .
  • Le degré du sommet A est égal à 3 (d ( A ) = 3).
  • A , B , C , D est une chaîne, A , B , F aussi.
  • A , F , G , A est un cycle.
  • Il y a une boucle ( E , E ) . Le degré de E est égal à 4.

Vocabulaire des graphes orientés

Dans le cas des graphes orientés, les arêtes ont un sens et elles sont appelées arcs. Par exemple, l’arête a = ( x , y ) indique qu’il y a un arc d’origine x et d’extrémité finale y. De plus, on a les définitions suivantes.

  • Successeurs et prédécesseurs d’un sommet x : dans un graphe orienté on ne parle plus de voisins d’un sommet mais de ses successeurs et de ses prédécesseurs : le successeurs de x sont tous les sommets tels qu’il existe un arc ( x , y ) (de x vers y ) et les prédécesseurs de x sont tous les sommets w tels qu’il existe un arc ( w , x ) (de w vers x).
  • Chemin : séquence ordonnée d’arcs consécutifs (on parlait de chaîne dans un graphe non orienté).
  • Circuit : dans un graphe orienté, un circuit est une suite d’arcs consécutifs (chemin) dont les deux sommets extrémités sont identiques.
  • Degré d’un sommet x : cette notion existe aussi dans le cas des graphes orientés. On distingue le degré entrant d’un sommet x (noté d − ( x )= nombre de prédécesseurs de x ) et le degré sortant d’un sommet x (noté d + ( x ) = nombre de successeurs de x ). Le degré d’un sommet x vaut d ( x ) = d + ( x ) + d − ( x )
  • Boucle : ce sont les arcs entre un sommet et lui-même.

Exemple

graphe non orienté
  • Ce graphe orienté est donné par :
    • un ensemble de sommets : { A , B , C , D , E , F , G },
    • un ensemble d’arcs : { ( A , B ) , ( A , F ) , ( B , C ) , ( C , F ) , ( C , D ) , … }
  • Les successeurs de A sont les sommets B et F , le seul prédécesseur de A est G . Le degré du sommet A est égal à 3 : d ( A ) = d + ( A ) + d − ( A ) = 2 + 1 = 3 .
  • A , B , C , D est un chemin mais A , B , F n’en est pas un car il n’y a pas d’arc ( B , F ) (de B vers F ).
  • A , F , G , A est un circuit.
  • Il n’y a pas de boucle ici.

Graphes valués

Certains graphes (orientés ou non) sont dits valués : on ajoute un coût (ou valuation, ou poids) à chaque arête/arc. Dans le cas d’un graphe représentant un réseau routier, le coût sur chaque arête pourrait, par exemple, être la distance entre deux villes.

graphe valué

Représentations d’un graphe

Représentation par matrice d’adjacence

Une matrice M est un tableau de nombres, qui peut être représenté en machine par un tableau de tableaux (ou une liste de listes) noté matrice. Chaque nombre de cette matrice est repéré par son numéro de ligne i et son numéro de colonne j. On note ce nombre M i , j et on peut y accéder par l’instruction matrice[i][j].

Un graphe à sommets peut être représentée par une matrice d’adjacence de taille n x n , où la valeur du coefficient d’indice i , j

dépend de l’existence d’une arête ou d’un arc reliant les sommets i et j.

Exemple (graphe non orienté) : Si les sommets A , B , C , …du graphe

graphe non orienté

sont respectivement numérotés 0, 1 , 2, etc. alors sa matrice d’adjacence est

Par exemple, le sommet C correspond à la troisième ligne. Celle-ci contient dans cet ordre les nombres 0, 1, 0, 1, 0, 1, 0 donc cela signifie qu’il y a des arêtes ( C , B ) , ( C , D ) et ( C , F ) (les 1) mais pas entre C et les sommets A, C, E et G (les 0).

Cette matrice peut être mémorisée en machine par le tableau de tableaux suivant.

matrice = [
    [0, 1, 0, 0, 0, 1, 1],
    [1, 0, 1, 0, 0, 1, 0],
    [0, 1, 0, 1, 0, 1, 0],
    [0, 0, 1, 0, 1, 0, 0],
    [0, 0, 0, 1, 1, 1, 0],
    [1, 1, 1, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 1, 0]
]

Dans le cas d’un graphe non orienté, la matrice d’adjacence est nécessairement symétrique par rapport à sa diagonale : on a M i , j = M j , i

.

Exemple (graphe orienté) :

C’est le même principe en faisant attention au sens des arcs : M i , j = 1 s’il y a un arc d’origine i et d’extrémité j et M i , j = 0 sinon. Ainsi, le graphe

graphe non orienté

a pour matrice d’adjacence

Par exemple, le sommet C correspond à la troisième ligne. Celle-ci contient dans cet ordre les nombres 0, 0, 0, 1, 0, 1, 0 donc cela signifie qu’il y a des arcs (C,D) et (C,F) (les 1) mais pas entre C et les autres sommets (les 0).

Comme les arcs ont un sens, la matrice d’adjacence d’un graphe orienté n’est généralement pas symétrique.

Représentation par listes des successeurs

Une autre façon de représenter un graphe est d’associer à chaque sommet la liste des sommets auxquels il est relié. Dans le cas d’un graphe orienté, on parle de liste de successeurs, alors que dans le cas d’un graphe non orienté on parle de liste de voisins.

Une façon simple et efficace est d’utiliser un dictionnaire où chaque sommet est associé à la liste de ses successeurs/voisins.

Exemples :

  • Le graphe non orienté graphe non orienté peut être représenté par le dictionnaire suivant, où les clés sont les sommets et les valeurs sont les listes de voisins.
dico1 = {
    "A": ["B", "F", "G"],
    "B": ["A", "C", "F"],
    "C": ["B", "D", "F"],
    "D": ["C", "E"],
    "E": ["D", "E", "F"],
    "F": ["A", "B", "C", "E", "G"],
    "G": ["A", "F"]
}

  • Le graphe orienté graphe non orienté peut être représenté par le dictionnaire suivant, où les clés sont les sommets et les valeurs sont les listes de successeurs.
dico2 = {
    "A": ["B", "F"],
    "B": ["C"],
    "C": ["D", "F"],
    "D": ["C"],
    "E": ["D"],
    "F": ["B", "E", "G"],
    "G": ["A"]
}

Efficacité des représentations

La matrice d’adjacence est simple à mettre en œuvre mais nécessite un espace mémoire proportionnel à n x n (où n est le nombre de sommets). Ainsi, un graphe de 1000 sommets nécessite une matrice d’un million de nombres même si le graphe contient peu d’arêtes/arcs. Pour le même graphe contenant peu d’arêtes/arcs, le dictionnaire ne mémoriserait pour chaque sommet que les voisins/successeurs (les 1) sans avoir à mémoriser les autres (les 0). En revanche, pour un graphe contenant beaucoup d’arêtes/arcs, la dictionnaire occuperait plus d’espace mémoire que la matrice d’adjacence.

Cela implique en outre que l’accès aux voisins/successeurs d’un sommet est plus rapide avec le dictionnaire car il n’est pas nécessaire de parcourir toute la ligne de la matrice ( n valeurs) alors même que celle-ci peut ne contenir que très peu de 1.

De plus, l’utilisation d’un dictionnaire permet de nommer les sommets sans ambiguité et ne les limite pas à des entiers comme c’est le cas pour la matrice d’adjacence (même si on peut associer chacun de ces entiers au sommet correspondant, ce que nous avons fait précédemment).

Enfin, au lieu d’utiliser le type liste (list de Python ici) pour mémoriser les voisins/successeurs, on peut avantageusement utiliser le type ensemble (type prédéfini set de Python) qui est une structure de données permettant un accès plus efficace aux éléments (l’implémentation se fait par des tables de hachage, hors programme de NSI).

✏️ A faire : tous les exercices du notebook d’activités !

Implémentations

La fin de ce cours résume une partie de ce qui a été fait en exercices, notamment deux implémentations du type GrapheNonOriente défini par l’interface suivante :

  • faire_graphe(sommets) pour construire un graphe (sans les arêtes) à partir de la liste sommets de ses sommets.
  • ajouter_arete(G, x, y) pour ajouter une arête entre les sommets x et y du graphe G.
  • sommets(G) pour accéder à la liste des sommets du graphe G.
  • voisins(G, x) pour accéder à la liste des voisins du sommet x du graphe G.

La première implémentation se fait par une classe GrapheNoMa s’appuyant sur la représentation par une matrice d’adjacence et la seconde par une classe GrapheNoLs s’appuyant sur les listes de successeurs (qui sont les voisins dans le cas d’un graphe non orienté).

On termine en présentant comment passer d’une représentation à l’autre.

Par une matrice d’adjacence

Voici la classe GrapheNoMa s’appuyant sur une matrice d’adjacence.

class GrapheNoMa:
    def __init__ (self, sommets):
        self.som = sommets
        self.dimension = len(sommets)
        self.adjacence = [[0 for i in range(self.dimension)] for j in range(self.dimension)]

    def ajouter_arete(self, x, y):
        i = self.som.index(x)
        j = self.som.index(y)
        self.adjacence[i][j] = 1
        self.adjacence[j][i] = 1

    def sommets(self):
        return self.som

    def voisins(self, x):
        i = self.som.index(x)
        return [self.som[j] for j in range(self.dimension) if self.adjacence[i][j] == 1]

Par une liste de successeurs

Voici la classe GrapheNoLs s’appuyant sur un dictionnaire contenant les listes de successeurs de chaque sommet.

class GrapheNoLs:
    def __init__ (self, sommets):
        self.som = sommets
        self.dic = {sommet: [] for sommet in self.som} # création par compréhension

    def ajouter_arete(self, x, y):
        if y not in self.dic[x]:
            self.dic[x].append(y)
        if x not in self.dic[y]:
            self.dic[y].append(x)

    def sommets(self):
        return self.som

    def voisins(self, x):
        return self.dic[x]

Python

On peut alors créer des graphes comme objets de ces deux classes et leur ajouter des arrêtes.

>>> g1 = GrapheNoMa(["a", "b", "c", "d"])  # implémentation par matrice d'adjacence
>>> g1.ajouter_arete("a", "b")
>>> g1.ajouter_arete("a", "c")
>>> g1.ajouter_arete("c", "d")

Python

>>> g2 = GrapheNoLs(["a", "b", "c", "d"])  # implémentation par liste de successeurs
>>> g2.ajouter_arete("a", "b")
>>> g2.ajouter_arete("a", "c")
>>> g2.ajouter_arete("c", "d")

On peut accéder aux graphes à travers les fonctions de l’interface du type abstrait de manière totalement identique.

>>> g1.sommets()
['a', 'b', 'c', 'd']
>>> g1.voisins("c")
['a', 'd']

>>> g2.sommets()
['a', 'b', 'c', 'd']
>>> g2.voisins("c")
['a', 'd']

En Python, un utilisateur malin pourra observer la façon dont sont mémorisées les graphes dans les deux cas :

>>> g1.adjacence
[[0, 1, 1, 0], [1, 0, 0, 0], [1, 0, 0, 1], [0, 0, 1, 0]]
>>> g2.dic
{'a': ['b', 'c'], 'b': ['a'], 'c': ['a', 'd'], 'd': ['c']}

Mais nous avons vu qu’il est possible de palier à ce problème en définissant une méthode de représentation identique dans chacune des deux classes pour masquer cette différence d’implémentation, qui importe peu à l’utilisateur de la classe.

Passage d’une représentation à l’autre

Les deux implémentations sont totalement équivalentes et on peut passer de l’une à l’autre simplement en énumérant les sommets et les voisins depuis une représentation tout en construisant l’autre représentation.

Par exemple, la fonction suivante permet de passer d’une matrice d’adjacence à une liste de successeurs (la fonction de traduction réciproque est similaire).

def ma_to_ls(gma):
    gls = GrapheNoLs(gma.sommets())
    for x in gma.sommets():
        for y in gma.voisins(x):
            gls.ajouter_arete(x,y)
    return gls

>>> g3 = ma_to_ls(g1)
>>> g1.adjacence  # représentation de départ
[[0, 1, 1, 0], [1, 0, 0, 0], [1, 0, 0, 1], [0, 0, 1, 0]]
>>> g3.dic  # traduction
{'a': ['b', 'c'], 'b': ['a'], 'c': ['a', 'd'], 'd': ['c']}

On peut implémenter le type abstrait GrapheOriente de façon quasiment similaire (fait en exercices).

Bilan

  • Le graphe est une structure de données permettant de modéliser de nombreuses situations de la vie courante.
  • Il est définit par un ensemble de sommets et de liaisons. Ces liaisons peuvent être orientées ou non et on les appelle alors respectivement des arcs ou des arêtes.
  • On peut représenter en machine un graphe par une matrice d’adjacence ou par un dictionnaire contenant les listes de successeurs (de chaque sommet).
  • La programmation par des classes de deux implémentations d’un graphe non orienté (resp. orienté) a prouvé que celles-ci étaient indépendantes de l’interface du type abstrait GrapheNonOriente (resp. GrapheOriente) et qu’elles sont équivalentes, on peut notamment passer de l’une à l’autre facilement.
  • Nous verrons dans un prochaine chapitre différents algorithmes sur les graphes (parcours en profondeur d’abord, en largeur d’abord, repérer des cycles, recherche de chemin dans un graphe).

Références :

  • Equipe pédagoqique DIU EIL, Université de Nantes.

Germain BECKER, Lycée Mounier, ANGERS

Exercices Piles

Exercice 1 (pile)

On considère deux jeux de 54 cartes à jouer. Pour les mélanger, on créer un troisième paquet en empilant les cartes aléatoirement piochées dans un des deux jeux. Quand l’un des deux jeux est vide, on empile le jeu restant sur le troisième paquet. On modélisera les deux jeux de 54 cartes par deux listes contenant respectivement les entiers de 101 à 154 et de 201 à 254. Écrire une fonction python qui réalise le mélange.

Exercice 2 (Bataille, pile)

On considère un jeu de 52 cartes à jouer (pas de joker) que l’on modélise par une liste contenant quatre fois les entiers de 1 à 13. La valeur d’une carte est égale à son numéro. On distribue ensuite aléatoirement 13 cartes à quatre joueurs qui les empilent consciencieusement devant eux. A chaque tour de jeu, les quatre joueurs posent la carte au sommet de leur pile au milieu. Celui qui a la carte la plus forte remporte le pli, c’est à dire les quatre cartes du tour. S’il y a égalité, on tire au sort le joueur qui gagne rafle le pli. Le joueur qui a la fin a le plus de points gagne la partie. Écrire un programme Python qui simule ce jeu.

Exercice 3 (La bonne syntaxe,pile)

L’analyse syntaxique est une phase indispensable de la compilation des programmes. Les piles sont particulièrement bien adaptées à ce genre de traitements. On va se limiter à la reconnaissance des expressions bien parenthésés. Nous devons écrire un programme qui :—accepte les expressions comme (a), (a b)((c d e)) ou (((a)(b c d))(e)) où a, b, etc. sont des expressions quelconques sans parenthèses—rejette les expressions comme a )(, (a b)((c) ou (((a b)(c d e f))))On remarque d’abord que les expressions a, b, c .. qui apparaissent n’ont aucune importance pour savoir si l’expression est bien parenthésée. On va lire l’expression sous la forme d’une chaine de caractères et utiliser une pile. Lors de la lecture dela chaine, on empile les ”(”. Pour les ”)”, on vérifie d’abord si le sommet de la pile est ”(”, dans ce cas, on dépile. Sinon, on empile le ”(”. A la fin de l’algorithme, si l’expression est bien parenthésée, la pile est vide.Écrire un algorithme utilisant une pile et ses primitives.

Exercice 4 (Évaluation d’une expression arithmétique, pile)

Nous allons voir ici une utilisation des piles pour évaluer des expressions arithmétiques.Par exemple, on souhaite évaluer(3×(5+2×3)+4)×2On se contentera d’évaluer des expressions correctement écrites avec seulement des nombres à un seul chiffre et uniquement des sommes et des produits.

1. On commence par écrire l’expression en notation ”postfixé” ou inverse polonaise Dans cette notation, on écrit les opérations après les nombres plutôt qu’avant.Par exemple,2+3s’écrira2,3,+
Ecrire l’expression(3×(5+2×3)+4)×2 en postfixé.
2. En lisant de gauche à droite l’expression postfixé, on construit une Pile suivant les règles suivantes :—Si on lit un nombre, on l’empile—Si on lit une opération, on dépile deux nombres et on effectue l’opération entre ces deux nombres et on l’empile.
3. Ecrire en pseudo code, une séquence qui permet d’évaluer l’expression.
4. Implémenter l’algorithme en python.

Exercice 5 (file)

Dans un supermarché il y a 5 caisses et une file d’attente commune. Dès qu’une caisse est libre, le client en tête de file y est envoyé. Le temps de passage en caisse est aléatoirement compris entre 3 et 10 minutes. Il y a dix clients dans la file d’attente.

1. Réaliser une simulation de leurs passages en caisses et afficher en combien de minutes tous les clients sont passés.
2. Refaire quelques essais avec plus de clients.
  • Pour la file d’attente , vous utiliserez une file avec l’objet deque sous python, voir description objet deque ici.

Par exemple voici la liste d’attente:

from collections import deque #import container deque
file_attente=['1','2','3','4','5','6','7','8','9','10'] #la liste d'attente commune avec 10 clients
d = deque(file_attente) #création file client

Résultat:

deque([‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ’10’])

On peut ajouter:

append(x)

Ajoute x à l’extrémité droite de la deque.

appendleft(x)

Ajoute x à l’extrémité gauche de la deque.

On peut retirer et renvoyer :

pop()

Retire et renvoie un élément de l’extrémité droite de la deque. S’il n’y a aucun élément, lève une exception IndexError.

popleft()

Retire et renvoie un élément de l’extrémité gauche de la deque. S’il n’y a aucun élément, lève une exception IndexError.

  • Pour la liste des caisses, vous utiliserez une liste [0,0,0,0,0], premier élément temps caisse 1, ici à 0, puis temps caisse 2 ou un dictionnaire {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}, clé 1 = caisse 1 avec sa valeur = temps ici à 0.

Exercice 6

Un poste de contrôle est alimenté en pièces toutes les 7 minutes. Ces pièces sont de quatre types qui nécessitent respectivement

4, 6, 8 et 9 minutes pour être contrôlés. Les fréquences des quatre types sont identiques.

1. A l’aide d’une file, simuler le fonctionnement du poste de contrôle pendant 1 heure, puis 80 heures.
2. Calculer le nombre de pièces en attente de contrôle
3. Déterminer la durée de séjour des pièces dans le poste de contrôle. Afficher celle de la dernière
4. Le contrôleur fait une pause de 15 minutes toutes les 4 heures. Déterminer le nombre d’heures effectives de travail.

Exercice 7

Un restaurant de 50 tables à 2 couverts ouvre ses portes pendant 4 heures chaque soir. L’intervalle de temps entre l’arrivée de deux groupes de clients est d’environ 2 minutes. Les groupes sont constitués aléatoirement de 2 à 6 convives avec des fréquences identiques.

Les tables peuvent être rassemblées pour les groupes de plus de 2 personnes. Lorsqu’un groupe arrive, il attend que des tables en nombre suffisant soient libres.

Une fois assis, le groupe reste pendant une durée aléatoirement comprise entre 60 et 120 minutes.

Simuler le fonctionnement du restaurant pendant une soirée.

Afficher le nombre de clients servis lors d’un service et nombre moyen de clients par table.

Exercice 2 du sujet 10 de l’épreuve pratique de 2021

Faire l’exercice 2 du Sujet n°10 de l’épreuve pratique de 2021

Structures de données : les arbres

Beaucoup de tournoi sportif sont organisés avec une structure qui représente les matchs joués ou à jouer dans le tournoi. Le principe de base est que chaque branche rassemble deux joueurs (ou deux équipes). Le gagnant avance et le perdant est éliminé. C’est en tout cas le fonctionnement avec élimination Directe.

Ce graphique si on commence par le vainqueur ressemble à un arbre.

Nous avons ci-dessous ce que l’on appelle une structure en arbre. On peut aussi retrouver cette même structure dans un arbre généalogique :

arbre_généa

Dernier exemple, les systèmes de fichiers dans les systèmes de type UNIX ont aussi une structure en arbre.sys_unixsystème de fichiers de type UNIX

Les arbres sont des types abstraits très utilisés en informatique. On les utilise notamment quand on a besoin d’une structure hiérarchique des données : dans l’exemple ci-dessous le fichier grub.cfg ne se trouve pas au même niveau que le fichier rapport.odt (le fichier grub.cfg se trouve « plus proche » de la racine / que le fichier rapport.odt). On ne pourrait pas avec une simple liste qui contiendrait les noms des fichiers et des répertoires, rendre compte de cette hiérarchie (plus ou moins « proche » de la racine). On trouve souvent dans cette hiérarchie une notion de temps (les quarts de finale sont avant les demi-finales ou encore votre grand-mère paternelle est née avant votre père), mais ce n’est pas une obligation (voir l’arborescence du système de fichiers).

Les arbres binaires sont des cas particuliers d’arbre : l’arbre du tournoi de rugby et l’arbre « père, mère… » sont des arbres binaires, en revanche, l’arbre représentant la structure du système de fichier n’est pas un arbre binaire. Dans un arbre binaire, on a au maximum 2 branches qui partent d’un élément (pour le système de fichiers, on a 7 branches qui partent de la racine : ce n’est donc pas un arbre binaire). Dans la suite nous allons uniquement travailler sur les arbres binaires.

Soit l’arbre binaire suivant :

Un peu de vocabulaire :

  • chaque élément de l’arbre est appelé noeud (par exemple : A, B, C, D,…,P et Q sont des noeuds)
  • le noeud initial (A) est appelé noeud racine ou plus simplement racine
  • On dira que le noeud E et le noeud D sont les fils du noeud B. On dira que le noeud B est le père des noeuds E et D
  • Dans un arbre binaire, un noeud possède au plus 2 fils
  • Un noeud n’ayant aucun fils est appelé feuille (exemples : D, H, N, O, J, K, L, P et Q sont des feuilles)
  • À partir d’un noeud (qui n’est pas une feuille), on peut définir un sous-arbre gauche et un sous-arbre droite (exemple : à partir de C on va trouver un sous-arbre gauche composé des noeuds F, J et K et un sous-arbre droit composé des noeuds G, L, M, P et Q)
  • On appelle arête le segment qui relie 2 noeuds.
  • On appelle profondeur d’un nœud ou d’une feuille dans un arbre binaire le nombre de nœuds du chemin qui va de la racine à ce nœud. La racine d’un arbre est à une profondeur 1, et la profondeur d’un nœud est égale à la profondeur de son prédécesseur plus 1. Si un noeud est à une profondeur p, tous ses successeurs sont à une profondeur p+1. Exemples : profondeur de B = 2 ; profondeur de I = 4 ; profondeur de P = 5
    ATTENTION : on trouve aussi dans certains livres la profondeur de la racine égale à 0 (on trouve alors : profondeur de B = 1 ; profondeur de I = 3 ; profondeur de P = 4). Les 2 définitions sont valables, il faut juste préciser si vous considérez que la profondeur de la racine est de 1 ou de 0.
  • On appelle hauteur d’un arbre la profondeur maximale des nœuds de l’arbre. Exemple : la profondeur de P = 5, c’est un des noeuds les plus profond, donc la hauteur de l’arbre est de 5.
    ATTENTION : comme on trouve 2 définitions pour la profondeur, on peut trouver 2 résultats différents pour la hauteur : si on considère la profondeur de la racine égale à 1, on aura bien une hauteur de 5, mais si l’on considère que la profondeur de la racine est de 0, on aura alors une hauteur de 4.

Il est aussi important de bien noter que l’on peut aussi voir les arbres comme des structures récursives : les fils d’un noeud sont des arbres (sous-arbre gauche et un sous-arbre droite dans le cas d’un arbre binaire), ces arbres sont eux mêmes constitués d’arbres…

À faire vous-même 1

Trouvez un autre exemple de données qui peuvent être représentées par un arbre binaire (dans le domaine de votre choix). Dessinez au moins une partie de cet arbre binaire. Déterminez la hauteur de l’arbre que vous aurez dessiné.


Python ne propose pas de façon native l’implémentation des arbres binaires. Mais nous aurons, plus tard dans l’année, l’occasion d’implémenter des arbres binaires en Python en utilisant un peu de programmation orientée objet.

Nous aurons aussi très prochainement l’occasion d’étudier des algorithmes permettant de travailler sur les arbres binaires.

Définition des parcours sur les arbres


FICHE REVISION

Merci à l’auteur : David Roche

les listes, les piles et les files

De nombreux algorithmes « classiques » manipulent des structures de données plus complexes que des simples nombres (nous aurons l’occasion d’en voir plusieurs cette année). Nous allons ici voir quelques-unes de ces structures de données. Nous allons commencer par des types de structures relativement simples : les listes, les piles et les files. Ces trois types de structures sont qualifiés de linéaires.

Les listes

Une liste est une structure de données permettant de regrouper des données. Une liste L est composée de 2 parties : sa tête (souvent noté car), qui correspond au dernier élément ajouté à la liste, et sa queue (souvent noté cdr) qui correspond au reste de la liste.

Le langage de programmation Lisp (inventé par John McCarthy en 1958) a été un des premiers langages de programmation à introduire cette notion de liste (Lisp signifie « list processing »). Voici les opérations qui peuvent être effectuées sur une liste :

  • créer une liste vide (L=vide() on a créé une liste L vide)
  • tester si une liste est vide (estVide(L) renvoie vrai si la liste L est vide)
  • ajouter un élément en tête de liste (ajouteEnTete (x,L) avec L une liste et x l’élément à ajouter)
  • supprimer la tête x d’une liste L et renvoyer cette tête x (supprEnTete(L))
  • Compter le nombre d’éléments présents dans une liste (compte(L) renvoie le nombre d’éléments présents dans la liste L)

La fonction cons permet d’obtenir une nouvelle liste à partir d’une liste et d’un élément (L1 = cons(x,L)). Il est possible « d’enchaîner » les cons et d’obtenir ce genre de structure : cons(x, cons(y, cons(z,L)))

Exemples :

Voici une série d’instructions (les instructions ci-dessous s’enchaînent):

  • L=vide() => on a créé une liste vide
  • estVide(L) => renvoie vrai
  • ajoutEnTete(3,L) => La liste L contient maintenant l’élément 3
  • estVide(L) => renvoie faux
  • ajoutEnTete(5,L) => la tête de la liste L correspond à 5, la queue contient l’élément 3
  • ajoutEnTete(8,L) => la tête de la liste L correspond à 8, la queue contient les éléments 3 et 5
  • t = supprEnTete(L) => la variable t vaut 8, la tête de L correspond à 5 et la queue contient l’élément 3
  • L1 = vide()
  • L2 = cons(8, cons(5, cons(3, L1))) => La tête de L2 correspond à 8 et la queue contient les éléments 3 et 5

À faire vous-même 1

Voici une série d’instructions (les instructions ci-dessous s’enchaînent), expliquez ce qui se passe à chacune des étapes :

  • L = vide()
  • ajoutEnTete(10,L)
  • ajoutEnTete(9,L)
  • ajoutEnTete(7,L)
  • L1 = vide()
  • L2 = cons(5, cons(4, cons(3, cons (2, cons(1, cons(0,L1))))))

Les piles

On retrouve dans les piles une partie des propriétés vues sur les listes. Dans les piles, il est uniquement possible de manipuler le dernier élément introduit dans la pile. On prend souvent l’analogie avec une pile d’assiettes : dans une pile d’assiettes la seule assiette directement accessible et la dernière assiette qui a été déposée sur la pile.

Les piles sont basées sur le principe LIFO (Last In First Out : le dernier rentré sera le premier à sortir). On retrouve souvent ce principe LIFO en informatique.

Voici les opérations que l’on peut réaliser sur une pile :

  • on peut créer une pile
  • on peut savoir si une pile est vide (pile_vide?)
  • on peut empiler un nouvel élément sur la pile (push)
  • on peut récupérer l’élément au sommet de la pile tout en le supprimant. On dit que l’on dépile (pop)
  • on peut accéder à l’élément situé au sommet de la pile sans le supprimer de la pile (sommet)
  • on peut connaitre le nombre d’éléments présents dans la pile (taille)

Exemples :

Soit une pile P composée des éléments suivants : 12, 14, 8, 7, 19 et 22 (le sommet de la pile est 22) Pour chaque exemple ci-dessous on repart de la pile d’origine :

  • pop(P) renvoie 22 et la pile P est maintenant composée des éléments suivants : 12, 14, 8, 7 et 19 (le sommet de la pile est 19)
  • push(P,42) la pile P est maintenant composée des éléments suivants : 12, 14, 8, 7, 19, 22 et 42
  • sommet(P) renvoie 22, la pile P n’est pas modifiée
  • si on applique pop(P) 6 fois de suite, pile_vide?(P) renvoie vrai
  • Après avoir appliqué pop(P) une fois, taille(P) renvoie 5

À faire vous-même 2

Soit une pile P composée des éléments suivants : 15, 11, 32, 45 et 67 (le sommet de la pile est 67). Quel est l’effet de l’instruction pop(P)


Les files

Comme les piles, les files ont des points communs avec les listes. Différences majeures : dans une file on ajoute des éléments à une extrémité de la file et on supprime des éléments à l’autre extrémité. On prend souvent l’analogie de la file d’attente devant un magasin pour décrire une file de données.

Les files sont basées sur le principe FIFO (First In First Out : le premier qui est rentré sera le premier à sortir. Ici aussi, on retrouve souvent ce principe FIFO en informatique.

Voici les opérations que l’on peut réaliser sur une file :

  • on peut savoir si une file est vide (file_vide?)
  • on peut ajouter un nouvel élément à la file (ajout)
  • on peut récupérer l’élément situé en bout de file tout en le supprimant (retire)
  • on peut accéder à l’élément situé en bout de file sans le supprimer de la file (premier)
  • on peut connaitre le nombre d’éléments présents dans la file (taille)

Exemples :

Soit une file F composée des éléments suivants : 12, 14, 8, 7, 19 et 22 (le premier élément rentré dans la file est 22 ; le dernier élément rentré dans la file est 12). Pour chaque exemple ci-dessous on repart de la file d’origine :

  • ajout(F,42) la file F est maintenant composée des éléments suivants : 42, 12, 14, 8, 7, 19 et 22 (le premier élément rentré dans la file est 22 ; le dernier élément rentré dans la file est 42)
  • retire(F) la file F est maintenant composée des éléments suivants : 12, 14, 8, 7, et 19 (le premier élément rentré dans la file est 19 ; le dernier élément rentré dans la file est 12)
  • premier(F) renvoie 22, la file F n’est pas modifiée
  • si on applique retire(F) 6 fois de suite, file_vide?(F) renvoie vrai
  • Après avoir appliqué retire(F) une fois, taille(F) renvoie 5.

À faire vous-même 3

Soit une file F composée des éléments suivants : 1, 12, 24, 17, 21 et 72 (le premier élément rentré dans la file est 72 ; le dernier élément rentré dans la file est 1). Quel est l’effet de l’instruction ajout(F,25)


Types abstraits et représentation concrète des données

Nous avons évoqué ci-dessus la manipulation des types de données (liste, pile et file) par des algorithmes, mais, au-delà de la beauté intellectuelle de réfléchir sur ces algorithmes, le but de l’opération est souvent, à un moment ou un autre, de « traduire » ces algorithmes dans un langage compréhensible pour un ordinateur (Python, Java, C,…). On dit alors que l’on implémente un algorithme. Il est donc aussi nécessaire d’implémenter les types de données comme les listes, les piles ou les files afin qu’ils soient utilisables par les ordinateurs. Les listes, les piles ou les files sont des « vues de l’esprit » présent uniquement dans la tête des informaticiens, on dit que ce sont des types abstraits de données (ou plus simplement des types abstraits). L’implémentation de ces types abstrait, afin qu’ils soient utilisables par une machine, est loin d’être une chose triviale. L’implémentation d’un type de données dépend du langage de programmation. Il faut, quel que soit le langage utilisé, que le programmeur retrouve les fonctions qui ont été définies pour le type abstrait (pour les listes, les piles et les files cela correspond aux fonctions définies ci-dessus). Certains types abstraits ne sont pas forcément implémentés dans un langage donné, si le programmeur veut utiliser ce type abstrait, il faudra qu’il le programme par lui-même en utilisant les « outils » fournis par son langage de programmation.

Pour implémenter les listes (ou les piles et les files), beaucoup de langages de programmation utilisent 2 structures : les tableaux et les listes chaînées.

Un tableau est une suite contiguë de cases mémoires (les adresses des cases mémoire se suivent) :

File

Le système réserve une plage d’adresse mémoire afin de stocker des éléments.

File

La taille d’un tableau est fixe : une fois que l’on a défini le nombre d’éléments que le tableau peut accueillir, il n’est pas possible modifier sa taille. Si l’on veut insérer une donnée, on doit créer un nouveau tableau plus grand et déplacer les éléments du premier tableau vers le second tout en ajoutant la donnée au bon endroit !

Dans certains langages de programmation, on trouve une version « évoluée » des tableaux : les tableaux dynamiques. Les tableaux dynamiques ont une taille qui peut varier. Il est donc relativement simple d’insérer des éléments dans le tableau. Ce type de tableaux permet d’implémenter facilement le type abstrait liste (de même pour les piles et les files)

À noter que les « listes Python » (listes Python) sont des tableaux dynamiques. Attention de ne pas confondre avec le type abstrait liste défini ci-dessus, ce sont de « faux amis ».Filetableau dynamique

Autre type de structure que l’on rencontre souvent et qui permet d’implémenter les listes, les piles et les files : les listes chaînées.

Dans une liste chaînée, à chaque élément de la liste on associe 2 cases mémoire : la première case contient l’élément et la deuxième contient l’adresse mémoire de l’élément suivant.

Il est relativement facile d’insérer un élément dans une liste chaînée :

Il est aussi possible d’implémenter les types abstraits en utilisant des structures plus complexes que les tableaux et les listes chaînées. Par exemple, en Python, il est possible d’utiliser les tuples pour implémenter le type abstrait liste :

À faire vous-même 4

Étudiez attentivement les fonctions suivantes :


def vide():
    return None
def cons(x,L):
    return(x,L)
def ajouteEnTete(L,x):
    return cons(x,L)
def supprEnTete(L):
    return (L[0],L[1])
def estVide(L):
    return L is None
def compte(L):
    if estVide(L):
        return 0
    return 1 + compte(L[1])
		

À faire vous-même 5

Après avoir saisi et exécuté le programme étudié au « À faire vous-même 4 », tapez successivement les commandes suivantes dans une console Python :

  • L = vide()
  • estVide(L)
  • L = cons(5, cons(4, cons(3, cons(2, cons(1, cons(0,L))))))
  • estVide(L)
  • compte(L)
  • L = ajouteEnTete(L,6)
  • compte(L)
  • x, L=supprEnTete(L)
  • x
  • compte(L)
  • x, L=supprEnTete(L)
  • x
  • compte(L)

À faire vous-même 6

Étudiez l’implémentation des piles et des files en Python en vous aidant de la documentation officielle (5.1.1 et 5.1.2).

Exercices sur les piles et les files


FICHE REVISION

Auteur : David Roche