• Aucun résultat trouvé

Allocation m´ emoire d’un tableau

2.4 R´ esolution d’´ equations de r´ ecurrence

3.1.1 Allocation m´ emoire d’un tableau

Il existe deux fa¸cons d’allouer de la m´emoire `a un tableau.

– la plus simple permet de faire de l’allocation statique. Par exemple int tab[100]; qui va allouer un tableau de 100 entiers pour tab. De mˆeme int tab2[4][4]; va allouer un tableau `a deux dimensions de taille 4× 4. En m´emoire ce tableau bidimensionnel peut ressemblera `a ce qu’on voit dans la Figure 3.2. Attention, un tableau allou´e statiquement ne se trouve pas dans la mˆeme zone m´emoire qu’un tableau allou´e avec l’une des m´ethodes d’allocation dynamique : ici il se trouve dans la mˆeme zone que toutes les variables de type int . On appelle cela de l’allocation statique car on ne peut pas modifier la taille du tableau en cours d’ex´ecution.

– la deuxi`eme fa¸con utilise soit la commande new (syntaxe C++), soit la commande malloc (syntaxe C) et permet une allocation dynamique (dont la taille d´epend des entr´ees par exemple). L’allocation du tableau s’´ecrit alors int* tab = new int [100]; ou int* tab = (int* ) malloc(100*sizeof(int ));. En revanche cette technique ne permet pas d’allouer directement un tableau `a deux dimensions. Il faut pour cela effec- tuer une boucle qui s’´ecrit alors :

1 int i; 2 int** tab2;

3 tab2 = (int** ) malloc(4*sizeof(int* ));

4 for (i=0; i<4; i++) {

5 tab2[i] = (int* ) malloc(4*sizeof(int ));

6 }

7 /* ou en utilisant new */

8 tab2 = new int* [4];

9 for (i=0; i<4; i++) {

10 tab2[i] = new int [4];

Mémoire

tab tab[0] tab[1] tab[2] tab[3]

tab[0][0] tab[0][1] tab[0][2] tab[0][3]

tab[2][0] tab[2][1] tab[2][2] tab[2][3] tab[3][0] tab[3][1] tab[3][2] tab[3][3]

tab[1][0] tab[1][1] tab[1][2] tab[1][3]

Figure 3.2 – Repr´esentation en m´emoire d’un tableau `a deux dimensions.

L’utilisation de malloc (ou new en C++) est donc beaucoup plus lourde, d’autant plus que la m´emoire allou´ee ne sera pas lib´er´ee d’elle mˆeme et l’utilisation de la commande free (ou delete en C++) sera n´ecessaire, mais pr´esente deux avantages :

– le code est beaucoup plus proche de ce qui se passe r´eellement en m´emoire (surtout dans le cas `a deux dimensions) ce qui permet de mieux se rendre compte des op´erations r´eellement effectu´ees. En particulier, une commande qui paraˆıt simple comme int tab[1000][1000][1000]; demande en r´ealit´e d’allouer un million de fois 1000 entiers, ce qui est tr`es long et occupe 4Go de m´emoire.

– cela laisse beaucoup plus de souplesse pour les allocations en deux dimensions (ou plus) : contrairement `a la notation simplifi´ee, rien n’oblige `a avoir des tableaux carr´es ! Pour les cas simples, la notations [100] est donc suffisante, mais d`es que cela devient compliqu´e, l’utilisation de malloc devient n´ecessaire.

Exemple d’allocation non carr´ee. Le programme suivant permet de calculer tous les coefficients binomiaux de fa¸con r´ecursive en utilisant la construction du triangle de Pascal. La m´ethode r´ecursive simple vue au chapitre pr´ec´edent (cf. page 30) est tr`es inefficace car elle recalcule un grand nombre de fois les mˆeme coefficients. Pour l’am´eliorer, on utilise un tableau bidimensionnel qui va servir de cache : chaque fois qu’un coefficient est calcul´e on le met dans le tableau, et chaque fois que l’on a besoin d’un coefficient on regarde d’abord dans le tableau avant de la calculer. C’est ce que l’on appelle la programmation

dynamique. Le point int´eressant est que le tableau est triangulaire, et l’utilisation de malloc (ou new) permet de ne pas allouer plus de m´emoire que n´ecessaire (on gagne un facteur 2 sur l’occupation m´emoire ici).

1 int** tab; 2

3 int binomial(int n, int p) {

§ 3.1 Tableaux ENSTA – cours IN101

5 if ((p==0) || (n==p) {

6 tab[n][p] = 1;

7 } else {

8 tab[n][p] = binomial(n-1,p) + binomial(n-1,p-1);

9 }

10 }

11 return tab[n][p];

12 }

13

14 int main(int argc, char** argv) {

15 int i;

16 tab = (int** ) malloc(33*sizeof(int* ));

17 for (i=0; i<34; i++) {

18 tab[i] = (int* ) calloc((i+1),sizeof(int ));

19 }

20 for (i=0; i<34; i++) {

21 binomial(33,i);

22 }

23

24 /* ins´erer ici les instructions qui

25 utilisent la table de binomiaux */

26

27 for (i=0; i<33; i++) {

28 free(tab[i]);

29 }

30 free(tab);

31 }

On utilise donc la variable globale tab comme table de cache et la fonction binomiale est exactement la mˆeme qu’avant, mis `a part qu’elle v´erifie d’abord dans le cache si la valeur a d´ej`a ´et´e calcul´ee, et qu’elle stocke la valeur avant de la retourner. On arrˆete ici le calcul de binomiaux `a n = 33 car au-del`a les coefficients binomiaux sont trop grands pour tenir dans un int de 32 bits. Notez ici l’utilisation de calloc qui alloue la m´emoire et l’initialise `a 0, contrairement `a malloc qui laisse la m´emoire non initialis´ee. La fonction calloc est donc plus lente, mais l’initialisation est n´ecessaire pour pouvoir utiliser la technique de mise en cache. Cette technique de programmation dynamique est tr`es utilis´ee quand l’on veut programmer vite et efficacement un algorithme qui se d´ecrit mieux de fa¸con r´ecursive qu’it´erative. Cela sera souvent le cas dans des probl`emes combinatoires ou de d´enombrement.

Lib´eration de la M´emoire Allou´ee

Notez la pr´esence des free `a la fin de la fonction main : ces free ne sont pas n´ecessaire si le programme est termin´e `a cet endroit l`a car la m´emoire est de toute fa¸con lib´er´ee par le syst`eme d’exploitation quand le processus s’arrˆete, mais si d’autres instructions doivent suivre, la m´emoire allou´ee sera d´ej`a lib´er´ee. De plus, il est important de prendre l’habitude de toujours lib´erer de la m´emoire allou´ee (`a chaque malloc doit correspondre un free) car cela permet de localiser plus facilement une fuite de m´emoire (de la

m´emoire allou´ee mais non lib´er´ee, typiquement dans une boucle) lors du debuggage.

3.1.2

Compl´ement : allocation dynamique de tableau

Nous avons vu que la structure de tableau a une taille fix´ee avant l’utilisation : il faut toujours allouer la m´emoire en premier. Cela rend cette structure relativement peu adapt´ee aux algorithmes qui ajoutent dynamiquement des donn´ees sans que l’on puisse borner `

a l’avance la quantit´e qu’il faudra ajouter. Pourtant, les tableaux pr´esentent l’avantage d’avoir un acc`es instantan´e `a la i`eme case, ce qui peut ˆetre tr`es utile dans certains cas. On a alors envie d’avoir des tableaux de taille variable. C’est ce qu’impl´emente par exemple la classe Vector en java (sauf que la classe Vector le fait mal...).

L’id´ee de base est assez simple : on veut deux fonctions insert et get(i) qui permettent d’ajouter un ´el´ement `a la fin du tableau et de lire le i`eme ´el´ement en temps constant (en

O(1) en moyenne). Il suffit donc de garder en plus du tableau tab deux entiers qui indiquent

le nombre d’´el´ements dans le tableau, et la taille totale du tableau (l’espace allou´e). Lire le

i`eme´el´ement peut se faire directement avec tab[i] (on peut aussi impl´ementer une fonction get(i) qui v´erifie en plus que l’on ne va pas lire au-del`a de la fin du tableau). En revanche, ajouter un ´el´ement est plus compliqu´e :

– soit le nombre d’´el´ements dans le tableau est strictement plus petit que la taille totale et il suffit d’incr´ementer le nombre d’´el´ements et d’ins´erer le nouvel ´el´ement,

– soit le tableau est d´ej`a rempli et il faut r´eallouer de la m´emoire. Si on veut conserver un acc`es en temps constant il est n´ecessaire de conserver un tableau d’un seul bloc, il faut donc allouer un nouvel espace m´emoire, plus grand que le pr´ec´edent, y recopier le contenu de tab, lib´erer la m´emoire occup´ee par tab, mettre `a jour tab pour pointer vers le nouvel espace m´emoire et on est alors ramen´e au cas simple vu pr´ec´edemment. Cette technique marche bien, mais pose un probl`eme : recopier le contenu de tab est une op´eration longue et son coˆut d´epend de la taille totale de tab. Recopier un tableau de taille n a une complexit´e de Θ(n). Heureusement, cette op´eration n’est pas effectu´ee `a chaque fois, et comme nous allons le voir, il est donc possible de conserver une complexit´e en moyenne de O(1).

La classe Vector de java permet `a l’initialisation de choisir le nombre d’´el´ements `a ajouter au tableau `a chaque fois qu’il faut le faire grandir. Cr´eer un Vector de taille n en augmentant de t `a chaque fois va donc n´ecessiter de recopier le contenu du tableau une

§ 3.2 Listes chaˆın´ees ENSTA – cours IN101

Documents relatifs