Fichier et Stream dEntreSortie IFT 1025 Programmation 2

  • Slides: 50
Download presentation
Fichier et Stream d’Entrée-Sortie IFT 1025, Programmation 2 Jian-Yun Nie

Fichier et Stream d’Entrée-Sortie IFT 1025, Programmation 2 Jian-Yun Nie

Concepts • Notion de fichier et de stream • Opérations: ouverture, lecture/écriture, fermeture •

Concepts • Notion de fichier et de stream • Opérations: ouverture, lecture/écriture, fermeture • Format texte vs. binaire • Accès séquentiel vs. aléatoire • Organisation des indexes

Fichier • Unité de stockage des données, sur disque dur, • stockage permanent (vs.

Fichier • Unité de stockage des données, sur disque dur, • stockage permanent (vs. en mémoire vive) • Un fichier contient un ensemble d’enregistrements CPU • Traitement fichier Mémoire vive tampon

Fichier en Java • Stream: une suite de données (octets ou caractères)

Fichier en Java • Stream: une suite de données (octets ou caractères)

Opérations typiques • Lecture: Établir un canal de communication – Ouvrir un stream –

Opérations typiques • Lecture: Établir un canal de communication – Ouvrir un stream – Lire tant qu’il y a des données Relâcher les – Fermer le stream • Écriture ressources allouées – Ouvrir un stream (ou créer un fichier) – Écrire des données tant qu’il y en a Écrire ce qu’il est dans le – Fermer le stream tampon, et Relâcher les ressources allouées

Exemple: TP 1 public static void main(String[] args) { public static void traiter_fichier() {

Exemple: TP 1 public static void main(String[] args) { public static void traiter_fichier() { String ligne; ouvrir_fichier("liste_mots"); traiter_fichier(); try { // catch EOFException ligne = input. read. Line(); while (ligne != null) { System. out. println(ligne); ligne = input. read. Line(); } fermer_fichier(); } public static void ouvrir_fichier(String nom) { try { input = new Buffered. Reader( new File. Reader(nom)); } catch (IOException e) { System. err. println("Impossible d'ouvrir le fichier d'entree. n" + e. to. String()); System. exit(1); } } public static void fermer_fichier() { try { input. close(); System. exit(0); } catch (IOException e) { System. err. println("Impossible de fermer les fichiers. n" + e. to. String()); } }

Un autre exemple public void read. File() { File. Reader file. Reader = null;

Un autre exemple public void read. File() { File. Reader file. Reader = null; try { file. Reader = new File. Reader("input. txt"); int c = file. Reader. read(); while (c != -1) { char d = ((char)c); c = file. Reader. read(); } } catch (File. Not. Found. Exception e) { System. out. println("File was not found"); } catch (IOException e) { System. out. println("Error reading from file"); } if (file. Reader != null) { try { file. Reader. close(); } catch (IOException e) { /* ignore */ } } }

Deux unité de base • Caractère (2 octets=16 bits) ou octet (8 bits) •

Deux unité de base • Caractère (2 octets=16 bits) ou octet (8 bits) • Deux hiérarchies de classe similaires (mais en parallèle)

Hiérarchies • En haut des hiérarchies pour stream de caractères: 2 classes abstraites •

Hiérarchies • En haut des hiérarchies pour stream de caractères: 2 classes abstraites • Reader java. lang. Object java. io. Reader • Writer java. lang. Object java. io. Writer • Implantent une partie des méthodes pour lire et écrire des caractère de 16 bits (2 octets)

Hiérarchie de Stream de caractère • Les sous-classe de Reader simple pré-traitement • Chaque

Hiérarchie de Stream de caractère • Les sous-classe de Reader simple pré-traitement • Chaque sous-classe ajoute des méthodes

Hiérarchie de Stream de caractère • Les sous-classe de Writer

Hiérarchie de Stream de caractère • Les sous-classe de Writer

Hiérarchies Byte Stream System. in

Hiérarchies Byte Stream System. in

Hiérarchie de Byte Stream System. out System. err

Hiérarchie de Byte Stream System. out System. err

Différence: caractère vs byte • Reader: – int read() – int read(char cbuf[], int

Différence: caractère vs byte • Reader: – int read() – int read(char cbuf[], int offset, int length) • Input. Stream: – int read() – int read(byte cbuf[], int offset, int length)

Utilisation de différentes classes • En mémoire: – Char. Array. Reader, Char. Array. Writer

Utilisation de différentes classes • En mémoire: – Char. Array. Reader, Char. Array. Writer – Byte. Array. Input. Stream, Byte. Array. Output. Stream – Lire/écrire dans un tableau de bytes – String. Reader, String. Writer, String. Buffer. Input. Stream – Lire/écrire dans un String • Pipe: – Piped. Reader, Piped. Writer – Piped. Input. Stream, Piped. Output. Stream – Relier la sortie d’un Stream comme une entrée d’un autre stream (pipeline)

Utilisation • Fichier (*) – File. Reader, File. Writer – File. Input. Stream, File.

Utilisation • Fichier (*) – File. Reader, File. Writer – File. Input. Stream, File. Output. Stream – Lire/écrire dans un fichier • Sérialisation – Object. Input. Stream, Object. Output. Stream • Conversion de données (*) – Data. Input. Stream, Data. Output. Stream – Reconnaître les types de données primitifs • Impression (*) – Print. Writer, Print. Stream – Méthodes conviviales pour l’impression

Utilisation • Buffer (Tampon) – Buffered. Reader, Buffered. Writer – Buffered. Input. Stream, Buffered.

Utilisation • Buffer (Tampon) – Buffered. Reader, Buffered. Writer – Buffered. Input. Stream, Buffered. Output. Stream – Créer un tampon: accès plus efficace • Filtering – Filter. Reader, Filter. Writer – Filter. Input. Stream, Filter. Output. Stream – Accepte un Stream, le filtre et ensuite passer à un autre Stream • Convertir entre byte et caractère – Input. Stream. Reader, Output. Stream. Writer – Lire des bytes en caractère, ou écrire des caractère en byte

Exemple • • Utiliser File. Reader et File. Writer Méthodes simples disponible: – int

Exemple • • Utiliser File. Reader et File. Writer Méthodes simples disponible: – int read(), int read(Char. Buffer []), write(int), . . – Exemple: copier un fichier caractère par caractère (comme un int) import java. io. *; public class Copy { public static void main(String[] args) throws IOException { File input. File = new File("farrago. txt"); File output. File = new File("outagain. txt"); File. Reader in = new File. Reader(input. File); File. Writer out = new File. Writer(output. File); int c; while ((c = in. read()) != -1) out. write(c); in. close(); out. close(); } } – Méthodes limitées Fin de fichier: -1

Augmenter les possibilités: wrap • Créer un stream en se basant sur un autre:

Augmenter les possibilités: wrap • Créer un stream en se basant sur un autre: File. Reader in = new File. Reader(new File("farrago. txt")); • Avantage: – Obtenir plus de méthodes • Dans File: les méthodes pour gérer les fichier (delete(), get. Path(), …) mais pas de méthode pour la lecture • Dans File. Reader: les méthodes de base pour la lecture – Un autre exemple: Data. Output. Stream out = new Data. Output. Stream( new File. Output. Stream("invoice 1. txt")); • File. Outptu. Stream: écrire des bytes • Data. Output. Stream: les méthodes pour les types de données de base: write(int), write. Boolean(boolean), write. Char(int), write. Double(double), write. Float(float), …

Pourquoi faire du wrapping? • Les streams enrichis ont besoin d’un stream plus primitif

Pourquoi faire du wrapping? • Les streams enrichis ont besoin d’un stream plus primitif dans son constructeur: – E. g. Data. Input. Stream(Input. Stream in) Data. Output. Stream(Output. Stream out) – Impossible de créer un stream directement d’un nom de fichier comme new Data. Output. Stream(“sortie”); – Besoin de faire du wrapping: • New Data. Output. Stream(new File. Output. Stream("sortie")) • Les streams de base s’occupe des E/S de base, et les streams enrichies ajoutent d’autres possibilités • Wrapping: réutiliser les méthodes de base, et ajouter d’autre méthodes

Hiérarchies

Hiérarchies

Sink stream Sink Type Character Streams Byte Streams Memory Char. Array. Reader, Char. Array.

Sink stream Sink Type Character Streams Byte Streams Memory Char. Array. Reader, Char. Array. Writer Byte. Array. Input. Stream, Byte. Array. Output. Stream String. Reader, String. Writer String. Buffer. Input. Stream Piped. Reader, Piped. Writer Piped. Input. Stream, Piped. Output. Stream File. Reader, File. Writer File. Input. Stream, File. Output. Stream

Wrap dans quelle classe ? • Dépend des méthodes dont on a besoin •

Wrap dans quelle classe ? • Dépend des méthodes dont on a besoin • Quelques exemples – Lire ligne par ligne: • Besoin de String read. Line() (null à la fin du fichier) • Buffered. Reader (sous classe de Reader) • Traitement: – Lire une ligne (String) – Traiter cette ligne avec un Parsing (comme TP 1) • Constructeur: Buffered. Reader(Reader) • new Buffered. Reader(new File. Reader("fichier")) (File. Reader est sous-classe de Reader – classe abstraite) – Écrire ligne par ligne • • Buffered. Writer (sous-classe de Writer) write(String): écrire un String new. Line(): insérer un changement de ligne Traitement: – Organiser une ligne comme String – Écrire la ligne

Wrap dans quelle classe ? • Besoin: utiliser les méthodes comme dans System. out

Wrap dans quelle classe ? • Besoin: utiliser les méthodes comme dans System. out – write(int), … – print(…), println(…) • Stream à utiliser: Print. Writer • Constructeurs – – Print. Writer(Output. Stream out) Print. Writer(Writer out) new Print. Writer(new File. Output. Stream("fichier")) new Print. Writer(new File. Writer("fichier"))

Wrap dans quelle classe ? • Besoin: écrire les données des types primitifs –

Wrap dans quelle classe ? • Besoin: écrire les données des types primitifs – write. Char(Char), write. Float(float), … • Stream à utiliser: Data. Output. Stream • Constructeur: – Data. Output. Stream(Output. Stream out) – new Data. Output. Stream(new File. Output. Stream("fichier"))

Interface: JFile. Chooser chooser = new JFile. Chooser(); File. Reader in = null; if

Interface: JFile. Chooser chooser = new JFile. Chooser(); File. Reader in = null; if (chooser. show. Open. Dialog(null) == JFile. Chooser. APPROVE_OPTION) { File selected. File = chooser. get. Selected. File(); reader = new File. Reader(selected. File); . . . }

Attention au format • Stocker les données dans un fichier • Mais aussi pouvoir

Attention au format • Stocker les données dans un fichier • Mais aussi pouvoir les ressortir • E. g. stocker le nom et la date de naissance: – Gérald. Tremblay 19500101 Susanne. Roy 19800406… • Au moment de stocker, déterminer un format pour pouvoir les relire Façon simple: Gérald Tremblay 19500101 Susanne Roy 19800406… – Lire prénom (jusqu’à l’espace), nom, date de naissance (il faut ensuite la décomposer)

Format • Stocker en binaire ou en caractère? – Caractère: lisible (e. g. avec

Format • Stocker en binaire ou en caractère? – Caractère: lisible (e. g. avec more, vi, etc. ) – Binaire: transformer en code binaire: • • int: 4 octets Long: 8 octets char: 2 octet … – Valeur 12345 (entier) • • En caractère: 12345 (10 octets) En binaire: 0 0 48 57 (4 octets) = 48*256+57 Binaire plus économique en espace Binaire = espace fixe (facilite la lecture)

Exemple en binaire • Pour un compte bancaire: – No. de compte: entier –

Exemple en binaire • Pour un compte bancaire: – No. de compte: entier – Montant: double • Pour écrire un enregistrement (pour un compte) – write. Int(int) – write. Double(double) – Classe: Data. Output. Stream • Pour lire un enregistrement – read. Int() – read. Double() – Classe: Data. Input. Stream • Penser aux écritures et aux lectures en même temps

Data. Input. Stream et Data. Output. Stream • Lire et écrire des données des

Data. Input. Stream et Data. Output. Stream • Lire et écrire des données des types de base – read. Boolean(), read. Int(), read. Float, read. Char(), read. Line(); read. UTF(), … – write. Boolean(boolean), write. Int(int), write. Float(float), write. Char(char), write. Chars(String), write. UTF(String), … – read. UTF et write. UTF: • Entier en binaire correspondant à la longueur du String + String • Exemple: liste_mots ^@&sur prep sing sur rel^@(laquelle pron_rel fem sing ^@&systeme n masc sing outil ^@ non adv non rel^@$echeance n fem sing temps^@^^entite … Références: http: //java. sun. com/j 2 se/1. 5. 0/docs/api/java/io/Data. Output. Stream. html http: //java. sun. com/j 2 se/1. 5. 0/docs/api/java/io/Data. Input. Stream. html

Sérialiser • Convertir un objet (avec une structure) en une suite de données dans

Sérialiser • Convertir un objet (avec une structure) en une suite de données dans un fichier • Reconvertir du fichier en un objet • Utilisation: avec Object. Output. Stream Employee[] staff = new Employee[3]; Object. Output. Stream out = new Object. Output. Stream(new File. Output. Stream("test 2. dat")); out. write. Object(staff); out. close();

Sérialiser • Utilité de sérialisation – Stocker un objet dans un fichier – Créer

Sérialiser • Utilité de sérialisation – Stocker un objet dans un fichier – Créer une copie d’objet en mémoire – Transmettre un objet à distance • Devient une transmission de String

Sérialiser • Pour pouvoir sérialiser un objet: – sa classe doit implanter l’interface Serializable

Sérialiser • Pour pouvoir sérialiser un objet: – sa classe doit implanter l’interface Serializable – Interface Serializable: • Pas de méthode exigée • Mais on peut réimplanter read. Object() et write. Object() si on ne se contente pas de la version par défaut: default. Read. Object(), default. Write. Object() • private void write. Object(java. io. Object. Output. Stream out) throws IOException • private void read. Object(java. io. Object. Input. Stream in) throws IOException, Class. Not. Found. Exception – Beaucoup de classes existantes sont sérialisables (e. g. Array. List)

Sérialiser: une classe sérialisable import java. io. *; import java. util. logging. *; try{

Sérialiser: une classe sérialisable import java. io. *; import java. util. logging. *; try{ Output. Stream file = new File. Output. Stream( "quarks. ser" ); Output. Stream buffer = new Buffered. Output. Stream( file ); output = new Object. Output. Stream( buffer ); output. write. Object(quarks); } catch(IOException ex){ public static void main(String[] a. Arguments) { f. Logger. log(Level. SEVERE, "Cannot perform output. ", ex); //create a Serializable List } List quarks = new Array. List(); finally{ quarks. add("up"); try { quarks. add("down"); if (output != null) { quarks. add("strange"); //flush and close "output" and its underlying streams quarks. add("charm"); output. close(); quarks. add("top"); } quarks. add("bottom"); } catch (IOException ex ){ f. Logger. log(Level. SEVERE, "Cannot close output stream. ", ex); } } Array. List est sérialisable public class Exercise. Serializable {

Définir une clase sérialisable class Employee implements Serializable { public Employee(String n, double s,

Définir une clase sérialisable class Employee implements Serializable { public Employee(String n, double s, Date d) { name = n; salary = s; hire. Date = d; } class Manager extends Employee { public Manager(String n, double s, Date d, Employee e) { super(n, s, d); secretary = e; } public Employee() {} public Manager() {} public void raise. Salary(double by. Percent) { salary *= 1 + by. Percent / 100; } public void raise. Salary(double by. Percent) { // add 1/2% bonus for every year of service Date today = new Date(); double bonus = 0. 5 * (today. get. Year() - hire. Year()); super. raise. Salary(by. Percent + bonus); } public int hire. Year() { return hire. Date. get. Year(); } public void print() { System. out. println(name + " " + salary + " " + hire. Year()); } private double salary; private String name; private Date hire. Date; } public void print() { super. print(); System. out. print("Secretary: "); if (secretary != null) secretary. print(); } private Employee secretary; }

Utiliser une classe sérialisable import java. io. *; import java. util. *; class Object.

Utiliser une classe sérialisable import java. io. *; import java. util. *; class Object. Ref. Test { public static void main(String[] args) { try { Employee[] staff = new Employee[3]; Employee harry = new Employee ("Harry Hacker", 35000, new Date(1989, 10, 1)); staff[0] = harry; staff[1] = new Manager("Carl Cracker", 75000, new Date(1987, 12, 15), harry); staff[2] = new Manager("Tony Tester", 38000, new Date(1990, 3, 15), harry); Object. Output. Stream out = new Object. Output. Stream(new File. Output. Stream("test 2. dat")); out. write. Object(staff); out. close(); Object. Input. Stream in = new Object. Input. Stream( new File. Input. Stream ("test 2. dat")); Employee[] new. Staff = (Employee[])in. read. Object(); for (int i = 0; i < new. Staff. length; i++) new. Staff[i]. raise. Salary(100); for (int i = 0; i < new. Staff. length; i++) new. Staff[i]. print(); } catch(Exception e) { e. print. Stack. Trace(); System. exit(1); } } }

Sortie de sérialisation vi test 2. dat: ¬í^@^Eur^@^K[LEmployee; ü¿ 6^QÅ<91>^QÇ^B^@^@xp^@^@^@^Csr^@ ^HEmployee~BÅ<89>^V<99>q=^B^@^CD^@^Fsalary. L^@^Hhire. Datet^@^ PLjava/util/Date;

Sortie de sérialisation vi test 2. dat: ¬í^@^Eur^@^K[LEmployee; ü¿ 6^QÅ<91>^QÇ^B^@^@xp^@^@^@^Csr^@ ^HEmployee~BÅ<89>^V<99>q=^B^@^CD^@^Fsalary. L^@^Hhire. Datet^@^ PLjava/util/Date; L^@^Dnamet^@^RLjava/lang/String; xp@á^W^@^@ ^@sr^@^Njava. util. Datehj<81>^AKYt^Y^C^@^@xpw^H^@^@7^Y×<8 c>4< 80>xt^@^LHarry Hackersr^@^GManager^U<9 d><93>þ<8 f>Íq^[^B^@^AL ^@secretaryt^@LEmployee; xq^@~^@^B@òO<80>^@^@sq^@~^@ ^Fw^H^@^@7^L¥@t<80>xt^@^LCarl Crackerq^@~^@^Esq^@~^@@â <8 e>^@^@^@sq^@~^@^Fw^H^@^@7^])^N<92>^@xt^@^KTony Testerq^@~^@ ^E Lisible par désérialisation (read. Object()): Harry Hacker 70000. 0 1989 Carl Cracker -555750. 0 1988 Secretary: Harry Hacker 70000. 0 1989 Tony Tester -281960. 0 1990 Secretary: Harry Hacker 70000. 0 1989

Accès séquentiel vs. aléatoire • Séquentiel: Première donnée, suivante, … – Approprié pour traiter

Accès séquentiel vs. aléatoire • Séquentiel: Première donnée, suivante, … – Approprié pour traiter toutes les données • Aléatoire (random): positionner à un endroit, lire les données à partir de cette position – Approprié pour sélectionner certaines données à traiter – Question importante: • Comment déterminer la position correcte ?

Random. Access. File • • Un Stream en format binaire Écrire et lire (selon

Random. Access. File • • Un Stream en format binaire Écrire et lire (selon l’ouverture) Possibilité de positionner avec seek(long) Exemple: file = new Random. Access. File(filename, "rw"); file. seek(100); int account. Number = file. read. Int(); double balance = file. read. Double(); Référence: http: //java. sun. com/j 2 se/1. 4. 2/docs/api/java/io/Random. Access. File. html

Random. Access. File • Ouverture: file = new Random. Access. File(filename, "rw"); Modes: r:

Random. Access. File • Ouverture: file = new Random. Access. File(filename, "rw"); Modes: r: lecture seulement rw: lecture et ecriture Mode

Random. Access. File • Lecture ou écriture – Sans seek: • Position au début

Random. Access. File • Lecture ou écriture – Sans seek: • Position au début du fichier • Écrire et lire à partir de cette position comme un accès séquentiel – seek(long) • Positionner à la position (no. de bytes à partir du début) • Lire ou écrire à partir de cette position

Position • Comment déterminer la bonne position? – Solution simple: • Chaque enregistrement =

Position • Comment déterminer la bonne position? – Solution simple: • Chaque enregistrement = taille fixe • Pour un enregistrement: déterminer son numéro • seek(taille*no) – Solution plus complexe: • Organiser un indexe • Utiliser une table de hashage

Taille fixe pour un enregistrement 008: public class Bank. Data 009: { 024: public

Taille fixe pour un enregistrement 008: public class Bank. Data 009: { 024: public void open(String filename) 025: throws IOException 026: { 027: if (file != null) file. close(); 028: file = new Random. Access. File(filename, "rw"); 029: } 035: public int size() 036: throws IOException 037: { 038: return (int) (file. length() / RECORD_SIZE); 039: } 071: public int find(int account. Number) 072: throws IOException 073: { 074: for (int i = 0; i < size(); i++) 075: { 076: file. seek(i * RECORD_SIZE); 077: int a = file. read. Int(); 078: if (a == account. Number) // Found a match 079: return i; 080: } 081: return -1; // No match in the entire file 082: } 056: public Bank. Account read(int n) 057: throws IOException 058: { 059: file. seek(n * RECORD_SIZE); 060: int account. Number = file. read. Int(); 061: double balance = file. read. Double(); 062: return new Bank. Account(account. Number, balance); 063: } 089: public void write(int n, Bank. Account account) 090: throws IOException 091: { 092: file. seek(n * RECORD_SIZE); 093: file. write. Int(account. get. Account. Number()); 094: file. write. Double(account. get. Balance()); 095: } 096: 097: private Random. Access. File file; 098: 099: public static final int INT_SIZE = 4; 100: public static final int DOUBLE_SIZE = 8; 101: public static final int RECORD_SIZE 102: = INT_SIZE + DOUBLE_SIZE; 103: }

Accès dans Random. Access. File • Exploiter seek pour déterminer la position pour lire

Accès dans Random. Access. File • Exploiter seek pour déterminer la position pour lire ou écrire un enregistrement – Séquentiel: lire chaque enregistrement à partir du début – Direct: déterminer une position selon une clé, et lire/écrire l’enregistrement • Position = no. de compte * taille: – clé = no. de compte • Position est déterminer selon une conversion avec une clé (e. g. code permanent GILB 76022304) – Non numérique – Non compact: valeur non continue

Cas 1: Recherche binaire • Les enregistrements sont stockés dans l’ordre croissant des clés

Cas 1: Recherche binaire • Les enregistrements sont stockés dans l’ordre croissant des clés • Accès binaire (O(log(n)): – Chercher(clé, début, fin): • Lire le milieu • Si clé_milieu == clé, trouvé • Si clé_milieu < clé, – Si (milieu – début) < 2, introuvable; – Sinon cherche(clé, début, milieur-1) • Si clé_milieu > clé, – Si (fin – milieu) < 2, introuvable; – Sinon cherche(clé, milieu+1, fin)

Recherche binaire Fichier 1 3 6 8 10 16 20

Recherche binaire Fichier 1 3 6 8 10 16 20

Cas 2: table de hashage • Table de hashage: déterminer une position pour chaque

Cas 2: table de hashage • Table de hashage: déterminer une position pour chaque clé • Fonction de hashage: clé entier • Contraintes: – Le plus compacte possible (pas beaucoup de positions vides) – Le moins de conflit possible (2 clés – même position)

Exemple simple • Transformer un code permanent en un entier: – VALB 23027502 code

Exemple simple • Transformer un code permanent en un entier: – VALB 23027502 code – 4 premiers caractères: A-Z: (1 -26)4 – 2 chiffres: 01 -31: 1 -31 – 2 chiffres: 01 -12, 5 -62: 1 -24 – 2 chiffres: 50 -40 (1950 -2040): 0 -90 – 2 chiffres: 01 -99: 1 -99 • Fonction 1(clé) = concatener les codes – Compacte ? 00*** et 27***, **00*** et **32*** non utilisés • Fonction 2(clé) = code(L 1)*…*code(L 4)*code(jour)* code(mois)*code(année)*code(derniers_ch) – Conflit? VALB 23027502 = VALB 23017504

Approche générale • Valeur non conflictuelle (mais relativement compact) • Déterminer la taille approximative

Approche générale • Valeur non conflictuelle (mais relativement compact) • Déterminer la taille approximative du fichier souhaitée (taille) • Valeur % primaire – Primaire est un nombre proche de la taille – E. g. <10 000 enregistrements: 10007, 10009, 10037, … 12007, … – Prévoir plus large – Prévoir un mécanisme pour résoudre le conflit • Aller à la suivante • Rehashage: appliquer une autre fonction de hashage • …

Cas 3: indexe • Maintenir une structure d’indexe (clé= lettre + nombre) 0 Clé=

Cas 3: indexe • Maintenir une structure d’indexe (clé= lettre + nombre) 0 Clé= a 21 a b c … … 20 21 pos 0 10 20 … 90 z 90 pos