# Structures de données/Version imprimable

* Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
* Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
* Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.
Une version à jour et éditable de ce livre est disponible sur Wikilivres, une bibliothèque de livres pédagogiques, à l'URL : https://fr.wikibooks.org/wiki/Structures_de_donn%C3%A9es

# Introduction
## Introduction

L'étude des structures de données fait suite à celle de l'algorithmique impérative. Une bonne connaissance stable de l'algorithmique impérative est une base essentielle à l'étude menée ici. L'étude de l'algorithmique fonctionnelle est d'une aide non-négligeable.
Nous n'avons donc utilisé jusqu'ici qu'une structure de données, le tableau. Cette structure de données pose un problème : elle occupe la mémoire en fonction de sa déclaration. Ce qui signifie que si on ignore combien l'utilisateur va nécessiter d'indice, il va falloir réserver le tableau le plus grand possible afin d'être sûr qu'il puisse contenir autant d'élément que l'utilisateur souhaitera. Il serait plus judicieux de réserver l'espace au fur et à mesure de la demande. C'est ce que l'on appelle la gestion dynamique de la mémoire (par opposition à statique).
Nous avons également vu que les tableaux sont génériques : c'est à dire qu'au moment de sa déclaration, nous pouvons dire que tous ses éléments sont d'un type donné et choisir ce type.
Nous avons vu que les tableaux sont homogènes : c'est à dire qu'un tableau donné ne peut contenir qu'un seul type d'élément (celui précisé dans la déclaration du tableau). Nous pourrions avoir besoin d'un tableau dont certain élément serait des entiers, d'autres des chaînes, d'autres encore des booléens. Il s'agit là de la problématique de l'hétérogénéité.
Note : attention à ne pas confondre la notion de généricité avec celle d'hétérogénéité. La première désigne la capacité à contenir des éléments d'un même type que l'on peut choisir en le fixant au moment de la déclaration. La seconde désigne la capacité à contenir des éléments de types différents.
Supposons maintenant que nous devions utiliser des formes d'information qui ne font pas partie de type de bases : les listes chaînées, les matrices, rationnels, les arbres, de l'génome, les couleurs, etc. Comment créer les éléments nécessaires au stockage de tel information quand un simple tableau ou un simple enregistrement ne suffisent plus ?
Dynamicité, généricité, hétérogénéité et la création de nouveau types non-élémentaires sont autant de problèmes traités dans ce wikilivre.

## Problématique

* Comment stocker des données en mémoire en prenant de la mémoire de façon dynamique, en fonction du besoin. Ceci afin d'éviter les dépassements de mémoire et de ne pas mobiliser des ressources machines (parfois précieuses) inutilement.
* Comment stocker en mémoire une donnée si aucun type n'est intégré dans le langage ? Les langages ne peuvent intégrer toutes les structures de données possibles. Il faut parfois les implémenter soi-même.
* Comment, au sein d'une structure, gérer sa généricité, son hétérogénéité.

## Pré-requis

* Algorithmique impérative : indispensable
* Algorithmique fonctionnelle : facultatif mais fortement conseillé (notamment pour l'implémentation de structures dynamiques)
* Architecture des ordinateurs : au moins les notions élémentaires sur la mémoire : mémoire statique/dynamique, représentation de valeurs en mémoire, codage sur un (entiers, caractères) ou plusieurs octets (chaînes de caractères), adressage (sur n bits)...

# Enregistrements

Cette page est considérée comme une ébauche à compléter . Si vous possédez quelques connaissances sur le sujet, vous pouvez les partager en éditant dès à présent cette page (en cliquant sur le lien « »).
Ressources suggérées : Aucune (vous pouvez indiquer les ressources que vous suggérez qui pourraient aider d'autres personnes à compléter cette page dans le paramètre « ressources » du modèle engendrant ce cadre)
Vous devez déjà être familier avec la notion de type enregistrement qui a du être abordée lors de l'étude de l'algorithmique impérative. Voici néanmoins un rappel :

## Première approche

L'enregistrement est une façon de créer un nouveau type, ou encore un méta-type c'est-à-dire un type qui contient d'autres variables (appelées champs) de types déjà définis.
Toute variable déclarée avec ce nouveau type utilise alors une zone mémoire suffisante pour y stocker tous les champs de l'enregistrement.

## Spécification

On déclare un nouveau type dans la partie Types comme suit :
on peut ensuite déclarer les variables de ce nouveau type enregistrement normalement :
Pour accéder à un champ de la variable enregistrement, nous utiliserons l'expression :
Cette expression est du type de identifiant_champ soit type_champ.

## Exemple

Supposons que nous voulons un nouveau type couple (de deux entiers). Nous allons déclarer le type couple comme suit :
Supposons maintenant que nous avons le lexique suivant :
Si nous assignons :

## Un exemple d'implémentation : les rationnels

Nous souhaitons implémenter le type rationnel. Pour cela, nous allons utiliser un type enregistrement composé de deux champs de types entier : numérateur et dénominateur.
Ensuite, nous réaliserons :
* un constructeur, permettant de créer un rationnel à partir de son numérateur et de son dénominateur,
* deux "accesseurs" c'est à dire des fonctions retournant la valeur des champs d'un enregistrement. Elles renvoient le numérateur ou le dénominateur d'un rationnel passé en paramètre,
* prédicat de nullité d'un rationnel, d'égalité de deux rationnels,
* somme, différence, produit et quotient de deux rationnels,
* réduction renvoyant sous sa forme réduite un rationnel donné.

# Pointeurs
## Retour sur la gestion des variables en mémoire

Pour aborder la notion de pointeur, il convient de revenir sur la notion de variable. Nous devons regarder de plus près ce qu'il se passe au niveau de la machine quand on déclare une variable ou qu'on l'assigne.
Faisons donc un retour sur l'architecture des ordinateurs. Les variables sont stockées dans une mémoire statique : on peut représenter cette mémoire par un tableau. Chaque ligne de ce tableau est une "case" mémoire c'est à dire une zone où l'on peut stocker une donnée atomique (par exemple un entier). Dans la machine, toutes ces cases sont numérotées sur n bits (dépend de l'architecture de la machine en question). Ce numéro, attribué de façon unique à chaque case identifie la case : c'est l'adresse mémoire de la case.
Pour la suite nous utiliserons une machine 8 bits, bien que les machines PC grand public (x86) sont aujourd'hui (2006) des machines 32 bits, 64 bits pour les plus récentes.
Nous avons donc une mémoire dont l'adresse de chaque octet est codée sur un octet, ce qui nous fait 256 cases numérotées de 0 à 255, 00 à FF en hexadécimal (qu'on utilisera pour numéroter les cases mémoires)
Que se passe-t-il quand on exécute un programme P ? Et bien le système d'exploitation qui exécute P va allouer au programme autant de place que nécessaire dans cette mémoire statique pour que toutes les variables du lexique de P puisse y être stockées. Les cases ainsi réservées seront représentées sur fond gris.

### Exemple

Avec le programme suivant : il s'agit de l'algorithme inversion de deux variables étudié en algorithmique impérative) :
Voici à quoi ressemble notre mémoire statique si notre machine code les entiers sur un octet (une case mémoire). Schéma de la mémoire avec deux cases a et b réservées :
À chaque assignation d'une variable, on affecte le contenu de la case mémoire dont l'adresse est contenu dans la variable. L'identifiant de la variable est en fait une abstraction, derrière l'identifiant d'une variable se cache l'adresse mémoire de son contenu.

## La mémoire dynamique

En plus d'allouer à chaque programme une mémoire statique, le système d'exploitation alloue au programme P une mémoire dynamique. Dans notre exemple avec l'inversion de deux variables notre programme n'utilise que la mémoire statique. Nous aurions pu omettre de représenter sur nos schémas cette mémoire.
La mémoire dynamique a les mêmes caractéristiques que le mémoire statique telle que décrite précédemment. Les cases sont numérotées (nous supposerons là encore sur 8 bits : de 00 à FF)
Nous allons apprendre comment utiliser cette mémoire dynamique. Et ce, avec cet outil que sont les pointeurs.

## Les pointeurs typés

Le pointeur est un nouveau type au même titres que les entiers, les caractères, chaînes de caractères, booléens et autres abordés en algorithmique impérative.
Il convient cependant de distinguer les pointeurs typés des pointeurs non-typés (également appelés génériques). Nous allons d'abord nous pencher sur les premiers et réserver les seconds pour plus tard.
Un pointeur typé indique le type de la donnée qu'il pointe. Il y a donc des pointeurs vers des entiers, des pointeurs vers des caractères, etc. Il convient d'affecter à des variables de types pointeur vers T uniquement des expressions de types pointeur vers T (tout comme on doit assigner à une variable de type entier une expression entière).

### Spécification

On déclare un pointeur vers T (T : un type) comme suit :
On accède à la valeur pointée comme suit :
Cette expression est donc du type pointé : T
Ecrire(identifiant_du_pointeur) afficherait l'adresse mémoire ou est stockée la donné pointée (souvent en sous la forme hexadécimale) : dans notre cours ce n'est d'aucune utilité.

### Utilisation

Pour stocker des données dans notre mémoire dynamique, il faut réserver l'espace nécessaire.
p est une variable de type pointeur (vers T, un type quelconque : entier...) :
Au lancement du programme, il sera réservé dans la mémoire statique l'espace permettant de stocker ce pointeur. Voici la représentation de la mémoire à cet instant :
On réserve l'espace nécessaire pour stocker une variable de type T en faisait appel à l'instruction :
Voici l'effet produit par new sur la mémoire.
Deux choses se sont produites :
* De la mémoire a été réservée pour stocker une donnée de type T (la case pointée est grisée).
* La variable p contient l'adresse mémoire de cet espace réservé (représenté par une flèche).
p contient alors l'adresse de l'espace qui nous pourront utilisé : on peut donc stocker le contenu de t en mémoire dynamique.
Pour utiliser l'espace pointé par p, nous utiliserons l'expression p^
L'espace pointé contient alors la valeur de l'expression qu'on a assigné :
On peut libérer l'espace : c'est à dire indiquer au système d'exploitation que cette partie de la mémoire dynamique n'est plus utilisée et qu'il peut désormais en faire ce qu'il veut. Pour cela on utilise la procédure « réciproque » de new :
On remarquera qu'après un delete la donnée stockée ainsi que le pointeur restent en mémoire : la seule chose qui change est que l'espace en mémoire dynamique n'est plus réservé. La donnée pointée peut donc être écrasée. La valeur de p^ restera en mémoire tant que le système d'exploitation n'aura pas décidé d'attribuer cet espace. Remarquez que demander au processeur de supprimer les contenus qui ont été stockés serait une perte de temps puisque si cette partie de la mémoire venait à être utilisé, les données serait écrasées de toute façon.
Remarque : l'utilité de ce qui est expliqué dans cette partie « utilisation » peut échapper au lecteur. En effet, cette partie sert seulement à donner les procédures new et delete. Stocker la valeur de t dans la mémoire dynamique n'a aucun intérêt en soi puisque la valeur est déjà dans t. L'important est de comprendre comment (par new et delete) on peut stocker et lire des informations en mémoire dynamique.

### Exemple avec des entiers

Remarque : encore une fois ce programme n'est d'aucune utilité. On pourrait en faire un équivalent sans utiliser de pointeurs. Le but est d'expliquer les mécanismes décrits précédemment.

## Les pointeurs génériques

Un pointeur générique est un type de pointeur particulier puisqu'il ne donne pas d'information sur ce qu'il pointe. C'est simplement une adresse vers une zone mémoire.

### Spécification

Il suffit de déclarer un pointeur comme suit

### Utilisation

Les pointeurs typés sont également des adresses mémoires donc on peut assigner les uns aux autres.
Ces opérations sont possibles :
Cependant, on ne peut utiliser les procédures new et delete en leur passant un pointeur générique en paramètre. En effet, le type de la donnée pointée étant inconnu le système d'exploitation de peut savoir combien d'espace réservé.

### Exemple avec des entiers
## Les effets de bord

Que se passe-t-il ?

# Ressources
## Wikilivres

* Structures de données en C

## Bibliographie

* Algorithmes et structures de données : cours et exercices corrigés en langage C / Michel Divay. - Paris : Dunod, 1999. - 328 p. : ill. n. et bl. ; 24 cm. - (Sciences sup. Informatique). La couv. porte en plus : "2e cycle, écoles d'ingénieurs". Bibliogr. p. 321-322. Index. ISBN 2-10-004277-7 (Br.)
* Structures de données en C++ / John Hubbard ; trad. de l'américain Virginie Maréchal. - Paris : Dunod, 2003. - X-405 p. : ill. n. et bl. ; 27 cm. - (Schaum's). ISBN 2-10-006938-1 (Br.)
* "Structures de données en Java, C++ et Ada95" C. Carrez, Masson 1997

## Liens externes

* Sophie Gire (Université de Bretagne Occidentale) polycopié du cours « Algorithmique et de Structures de données » pour IUP Ingéniérie Informatique 1ère année (1999-2000)