![]() |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Récursivité : Introduction
|
Chapitre 1 | |||
Leçon : Récursivité | |||
---|---|---|---|
Retour au | Sommaire | ||
Chap. suiv. : | Algorithmes récursifs |
La récursivité c'est l'application de l'adage « Diviser pour régner » à l'algorithmique : pour résoudre un problème d'une taille donnée, on scinde ce problème en plusieurs sous-problèmes plus petits, on recommence avec chacun de ces sous-problèmes jusqu'à ce que tous les petits sous-...-sous-problèmes soient facilement résolubles.
Soyons un peu plus formels. On dit d'une définition qu'elle est récursive si elle fait appel à elle-même pour se définir. Une telle définition peut sembler se mordre la queue, ne rien pouvoir produire de concret. Il n'en sera rien si la définition est bien construite. C'est dans le domaine mathématique que l'on trouve de nombreuses définitions récursives :
Exemple : définition récursive de la factorielle |
|
Cet exemple montre les deux caractéristiques principales d'une définition récursive intéressante :
Utiliser une définition récursive est souvent considéré comme élégant, concis et lisible.
Appliquons le même schéma aux algorithmes. Si nous disposons d'un algorithme A qui d'une manière directe fait appel à lui-même, ou d'une manière indirecte appelle un autre algorithme B qui lui appelle A, alors l'algorithme A sera dit récursif. Afin d'être correct, il doit exister au moins un cas dans lequel l'appel récursif ne se fait pas, et ce cas doit se produire.
Définition : Algorithme récursif |
Un algorithme est récursif quand dans l'enchaînement des instructions qui le composent, l'une de celles-ci est un appel à ce même algorithme. |
Cela donne pour notre exemple de factorielle le pseudocode suivant
Nous retrouvons en ligne 4 la condition d'arrêt : aucun appel récursif n'est mis en œuvre, et en ligne 6 l'appel récursif.
Si nous appliquons un schéma récursif à la création de structures de données, nous créons une structure de données récursive. Ces structures deviennent nécessaires dès que l'on a besoin de les agrandir dynamiquement. L'exemple le plus simple est sans doute celui de la liste. On peut implémenter une liste d'objets avec une structure élémentaire comme un tableau. Mais la gestion dynamique d'un tableau peut-être difficile à maintenir. Une liste peut alors être considérée sous une forme récursive, une liste est composée de
Exemple |
||||||
Supposons que nous disposions de la liste de personnes suivante : Une manière de représenter cette liste serait un tableau :
Un tableau possédant une certaine longueur, il a ici été surdimensionné (2 cases sont vides) Une autre manière de représenter cette liste serait une liste simplement chaînée :
|
Structure de donnée récursive |
Une structure de donnée S est dite récursive si l'une de ses composantes est aussi une structure de donnée S ou si l'un de ses composantes est une structure de donnée faisant appel à S. |
Chapitre 2 | |||
Leçon : Récursivité | |||
---|---|---|---|
Chap. préc. : | Introduction | ||
Chap. suiv. : | Structure de données récursives |
On peut distinguer plusieurs catégories d'algorithmes récursifs en considérant
Sommaire[masquer] |
Comme nous l'avons déjà vu dans l'introduction, un algorithme récursif qui dans son corps s'appelle lui-même est dit direct. L'exemple de la factorielle est trivial. Un algorithme récursif est dit indirect s'il est défini par un ensemble de fonctions qui s'appellent en chaîne.
Algorithme récursif indirect pour déterminer la parité d'un entier |
Pour construire une définition récursive de la parité, remarquons tout d'abord que 0 est un entier pair, et qu'il n'est pas impair. Ensuite remarquons que un entier n est pair (resp. impair) ssi l'entier n-1 est impair (resp. pair). Si l'on trace Pair(3) on obtiendra |
L'arité d'un algorithme est le nombre de paramètres d'entrée. Un algorithme récursif peut effectuer des appels récursifs en modifiant un nombre quelconque non nul de ses paramètres. Dans l'exemple de la fonction factorielle, l'algorithme prend un paramètre d'entrée et le modifie lors des appels récursifs. Dans le cas de la puissance entière (proposé en exercice), l'algorithme prend deux entrées, la base et l'exposant. Dans les appels récursifs seul l'exposant est modifié. Voici un exemple de récursion sur deux paramètres.
Algorithme récursif de calcul du PGCD de deux entiers |
Jusqu'à présent, tous les algorithmes récursifs évoqués ne font qu'un appel récursif à la fois, c'est-à-dire que pour déterminer la valeur d'un appel, on n'a besoin que d'un appel récursif. Ces algorithmes sont dits d'ordre 1. Certains algorithmes font plusieurs appels récursifs, comme la version naïve du calcul de la suite de Fibonnaci. Celle-ci doit faire deux appels récursifs, une fois avec le paramètre n-1 et une seconde fois avec le paramètre n-2 pour calculer une valeur de retour. C'est un algorithme récursif d'ordre 2.
Algorithme récursif pour calculer le nième terme de la suite de Fibbonacci |
Pour déterminer si un algorithme est terminal, il faut regarder comment il génère les valeurs de retour. Si ce sont soit des constantes, soit des valeurs directement issue d'un appel récursif, sans aucune autre modification, alors l'algorithme est dit terminal. Dans le cas contraire, il sera dit non-terminal. Cette notion est importante si l'on veut déterminer une version itérative de l'algorithme concerné. Dans les exemples précédents, factorielle est non terminal car, bien que le cas d'arrêt renvoie une constante, celui de l'appel récursif quant à lui modifie la valeur renvoyée en la multipliant par n avant de lui-même renvoyer une valeur.
Chapitre 3 | |||
Leçon : Récursivité | |||
---|---|---|---|
Chap. préc. : | Algorithmes récursifs | ||
Chap. suiv. : | Algorithmes récursifs avancés |
La structure de donnée récursive est monnaie courante dans l'informatique de tous les jours; elle apparaît dès que l'on parle de liste, file, arbre, graphe, ... Peu de langages de programmation n'en permettent pas l'utilisation, que ce soit explicitement par l'intermédiaire de pointeurs, ou implicitement par des références. Nous nous bornerons ici à en décrire succinctement le principe, et à en présenter les plus courantes.
Sommaire[masquer] |
La plupart des langages permettent de créer de nouveaux types de données qui sont la réunion de plusieurs autres types de données (struct en C/C++, record en Pascal, classes pour les langages objets, ...). Rendre un type auto référant, consiste simplement à créer un type T et à le munir d'au moins un membre dont le type sera T. Évidemment, tout comme pour les algorithmes, le cas où un type T contient un membre de type S, et que ce dernier contient un membre de type T est englobé par le définition. La représentation classique d'une instance de ce type est formalisée par
La partie données est l'information maintenue par ce type, la case blanche représente une référence vers ce même type et la flèche indique que cette référence est utilisée et pointe vers l'instance concernée. Si la référence ne pointe pas vers une instance alors cela est formalisé ainsi :
Une telle instance indique une terminaison (fin de la liste, feuille d'un arbre)
Représente une instance d'un type agrégeant une chaîne de caratères, trois références. Cette instance contient contient la chaîne de caractères Chat. Les deux premières références ne sont pas utilisées, on dit que leur valeur est nulle, elles ne pointent nulle part. La dernière référence pointe vers une instance du même type contenant la chaîne de caractère Chien. L'ordre des références est en général important.
Les listes simplement chaînées permettent de représenter des listes d'éléments quelconques.
Une instance particulière appelée tête de liste est une référence sur le premier élément de la liste. La liste d'entiers (5, 2, 17, pourrait être représentée ainsi :
On dispose entre autres des fonctions d'accès suivantes pour écrire nos algorithmes :
Si dans la définition du nœud on ajoute une référence precedent, avec la contrainte que pour un nœud N qui n'est ni la tête ni la queue suivant(precedent(N))=precedent(suivant(N))=N, alors on obtient une liste doublement chaînée. Elle peut se parcourir aussi bien de la tête vers la queue que dans l'autre sens.
Les piles et les files sont simplement des listes simplement ou doublement chaînées, seules leurs fonctions d'accès diffèrent. En ce qui concerne la pile, on peut uniquement empiler (ajouter en tête), et dépiler (récupération de la valeur de tête, destruction de celle-ci). Quant à la file, on peut seulement enfiler (ajouter en tête) et défiler (récupération de la valeur de queue, destruction de celui-ci). Parmi les implémentations particulières de listes, on pourra citer, les ensembles (chaque valeur est présente une et une seule fois dans la liste), les listes de hashage, les matrices creuses.
Les listes apportent un comportement dynamique qui fait parfois défaut aux tableaux. Les tableaux de dimension supérieure à 1 peuvent avantageusement être remplacés par des listes de listes.
Un arbre binaire représente une structure hiérarchique, comme un arbre généalogique. Le nœud de l'arbre est composé de deux références (tout comme un nœud de liste doublement chaînée), on les nomme souvent FG pour fils gauche et FD pour fils droit. Il faut voir ces liens comme un chemin que l'on emprunte à partir de la racine jusqu'à un nœud. La seule contrainte à propos de ce chemin est qu'il ne doit en exister qu'un à partir de la racine vers un nœud quelconque.
On définit des propriétés telles que le degré d'un nœud (nombre de reférences utilisées, i.e. n'ayant pas une valeur nulle), la hauteur d'un arbre (nombre maximum de descendants), le niveau d'un nœud (distance le séparant de la racine). Un nœud de degré 0 est appelé feuille, un nœud de degré supérieur à 0 est appelé nœud interne.
Arbre binaire |
qui le plus souvent est simplifié en |
Un arbre qui est composé de nœuds pouvant avoir au maximum n références est dit arbre n-aire. On le retrouve dans l'arborescence du disque dur, par exemple. Un arbre unaire est une liste, on dit qu'il a dégénéré en liste. Tout arbre n-aire peut être tansformé en arbre binaire.
Si l'on construit une structure avec un nombre quelconque de références, sans contraintes particulières (cycle, connexité, ...) alors on obtient un graphe. Un exemple courant de graphe est une carte routière où les villes sont les nœuds et les routes les références. Les graphes sont des outils d'une puissance extraordinaire, mais leur maîtrise est complexe (mais pas compliqué).
Exercice 1 | |||
Leçon : Récursivité | |||
---|---|---|---|
Chapitre du cours : | Introduction | ||
Cet exercice est de niveau 0. |
La fonction puissance entière peut être définie ainsi :
Exercice 1 | |||
Leçon : Récursivité | |||
---|---|---|---|
Chapitre du cours : | Algorithmes récursifs | ||
Cet exercice est de niveau 0. |
Nous avons vu un algorithme (dit naïf) qui calcule le nième termde la suite de Fibbonacci :
Algorithme récursif pour calculer le nième terme de la suite de Fibbonacci |
Algorithme récursif pour calculer le nième terme de la suite de Fibbonacci (version 2) |
Exercice 3 | |||
Leçon : Récursivité | |||
---|---|---|---|
Chapitre du cours : | Structure de données récursives | ||
Cet exercice est de niveau 0. |
Sommaire[masquer] |
![]() |
À lire absolument, avant de commencer l'exercice |