590 likes | 718 Views
Structure de données: Liste. IFT1025, Programmation 2 Jian-Yun Nie. Points importants. Utilité d’une structure de données Comment utiliser une structure de données de liste existante Comment programmer pour gérer soi-même une liste
E N D
Structure de données: Liste IFT1025, Programmation 2 Jian-Yun Nie
Points importants • Utilité d’une structure de données • Comment utiliser une structure de données de liste existante • Comment programmer pour gérer soi-même une liste • D’autres structures souvent utilisées en informatique (arbre, pile, …)
Pourquoi une structure de données? • Structure de données: • Une organisation des informations • Pour regrouper une série de données de nature similaire (tableau, liste, …) • Pour mieux regrouper les informations pour le même concept (classe, …) • Traditionnellement • Structure de données • Traitement • Maintenant • Classe: structure de données et traitements
Structures de données déjà vues • Tableau (Array): pour organiser une séquence de données • int [ ] a = new int [10]; • Similairement: String • Classe: pour regrouper les attributs du même concept public class C { int a; String name; } C ref = new C(); a ref int a: String name: 0 null
Liste • Utilité: pour organiser une séquence de données • Une structure plus flexible que tableau
Tête A B C D Illustration • Entête (Tête): réfère au premier noeud • noeuds enchaînés • Chaque noeud enchaîné: • Valeur stockée (A, B, …) • Référence (lien, pointeur) vers le prochain nœud
Structure de données: Liste • Deux parties: • Structure pour l’entête • Référence vers le premier nœud • D’autres attributs optionnels • Référence vers le dernier nœud • Nombre de nœuds • … • Structure pour nœud • Structure (ou classe) pour le stockage d’une valeur • Référence vers le prochain nœud • Optionnel: • Référence vers le nœud précédent (liste doublement chaînée)
Définition d’une liste simplement chaînée • Entête contenant une référence public class Liste { Noeud premier; // méthodes pour traiter une liste } • Nœud contenant un int public class Noeud { int valeur; Noeud prochain; public Noeud (int v, Noeud p) { valeur = v; prochain = p; } // méthodes pour traiter un nœud }
Création • Créer une entête: Liste l = new Liste(); • Créer des nœuds: l.premier = new Noeud(1, new Noeud(2, new Noeud(3,null))); null null
Traverser une liste (tout se fait dans Liste) public class Liste { … public void print() { Noeud n = premier; while (n!=null) { System.out.print(n.valeur + "->"); n = n.prochain; } System.out.println("null"); } … } 1->2->3->null
Trouver un élément public class Liste { … public Noeud trouver(int v) { Noeud n = premier; while (n != null && n.valeur != v) n = n.prochain; return n; } … }
Déterminer la longueur public class Liste { … public int longueur() { Noeud n = premier; int nb=0; if (premier == null) return 0; while (n != null) { nb++; n = n.prochain; } return nb; } … }
Ajouter un élément au début public class Liste { … public void ajoutDebut(int v) { premier = new Noeud(v, premier); } … } null
Ajouter un élément à la fin public class Liste { … public void ajoutFin(int v) { Noeud n = premier; // cas 1: aucun autre élément ajouté if (premier == null) premier = new Noeud(v,null); // cas 2: il y a déjà des éléments else { while (n.prochain != null) n = n.prochain; n.prochain = new Noeud(v,null); } } … } • Attention: tester sur le prochain • Pas de limite de taille null n
Enlever un élément • Besoin de garder la référence sur le précédent élément • Tester sur le prochain public class Liste { … public void enlever(int v) { Noeud n = premier; // cas 1: Si le premier est null if (premier == null) return; // cas 2: Si le premier est à enlever if (premier.valeur == v) { premier = premier.prochain; return; // On pourrait aussi vouloir retourner le neoud enlevé } // cas 3: sinon, tester sur les autres éléments while (n.prochain != null && n.prochain.valeur != v) n = n.prochain; if (n.prochain != null) n.prochain = n.prochain.prochain; } … } • Pas besoin de déplacer les autres éléments pour laisser une place (comme tableau) Attention à l’ordre des tests
Concatenation de 2 listes public class Liste { … public void concat(Liste l) { if (premier == null) { premier = l.premier; return; } Noeud n = premier; while (n.prochain != null) n=n.prochain; n.prochain = l.premier; } … } // Aller à la fin
Traitement récursif • Itératif: Liste = suite d’éléments • Traitement typique: parcourir la liste avec while • Récursif: Liste = un élément + reste (une plus petite liste) • Traitement récursif • Traiter un élément • Traiter le reste par un appel récursif • Décomposition générale • Cas 1: liste vide ou un seul élément (cas d’arrêt de récursion) • Cas 2: liste non vide • L’élément courant • Appel récursif pour la suite
Déterminer la longueur(Deux niveaux de traitement) • Longueur = • 1, si la liste contient un seul élément • 1 + longueur du reste, sinon. public class Liste { Noeud premier; public int longueur() { if (premier == null) return 0; else return premier.longueur(); } } public class Nœud // Premier option: utiliser la récursion sur les noeuds { public int longueur() { // cas 1: pas de récursion if (prochain==null) return 1; // cas 2: récursion else return 1+ prochain.longueur(); } } Cet appel est possible seulement quand premier!=null
Structure générale • Class Liste: • public int longueur() { appel à la méthode de Nœud; } • Class Nœud: • public int longueur() { • Cas simple: retourner 1 • Cas complexe: 1 + appel récursif }
Déterminer la longueur (bis) public class Liste { Noeud premier; public int longueur() { return longueur(premier); } // Deuxième option: utiliser la récursion dans Liste, avec nœud comme paramètre public int longueur(Noeud n) { if (n==null) return 0; else return 1+ longueur(n.prochain); } } Cet appel est possible même si premier==null
Structure générale • Classe Liste: • public int longueur() { appel à une méthode avec Nœud comme paramètre; } • public int longueur(Nœud n) { déterminer la longueur à partir du nœud n }
Ajouter à la fin public class Liste { … public void ajoutFin(int v) { // cas 1: aucun autre élément ajouté if (premier == null) premier = new Noeud(v,null); // cas 2: il y a déjà des éléments else premier.ajoutFin(v); } } public class Noeud { public void ajoutFin(int v) { if (prochain == null) prochain = new Noeud(v,null); else prochain.ajoutFin(v); } … } Traiter le cas d’une liste vide Ajouter un élément dans une liste non-vide
Ajouter à la fin (bis) public class Liste { … public void ajoutFin(int v) { // cas 1: aucun autre élément ajouté if (premier == null) premier = new Noeud(v,null); // cas 2: il y a déjà des éléments else ajoutFin(premier, v); } public void ajoutFin(Noeud n, int v) { if (n.prochain == null) n.prochain = new Noeud(v,null); else ajoutFin(n.prochain,v); } … }
Ajouter à la fin (bis-bis) public class Liste { … public void ajoutFin(int v) { premier =ajoutFin(premier, v); } public Noeud ajoutFin(Noeud n, int v) { if (n == null) return new Noeud(v,null); else { n.prochain = ajoutFin(n.prochain,v); return n; } } … }
Réflexions • Les façons récursives d’implanter les autres méthodes: • Enlever un élément • Ajouter au début • Insérer dans l’ordre • Inverser la liste • Concaténer deux liste • Obtenir le i-ième noeud • …
Complexité des opérations • Longueur: O(n) • Trouver un élément: O(n) • Enlever un élément: O(n) • Ajouter au début: O(1) • Ajouter à la fin: O(n) • Améliorable ?
Liste simplement chaînée: amélioration • Ajouter une référence au dernier élément pour faciliter l’ajout à la fin public class Liste { Noeud premier; Noeud dernier; public void ajoutFin(int v) { if (premier==null) premier = dernier = new Noeud(v,null); else { dernier.prochain = new new Noeud(v,null); dernier = dernier.prochain; } } … }
Liste doublement chaînée • Référence vers le prochain nœud • Référence vers le précédent nœud (permettre de reculer) public class Noeud { int valeur; Noeud prochain; Noeud precedent; … }
Exemple: Enlever public class Liste { public void enlever(int v) { Noeud n = premier; if (premier == null) return; if (premier.valeur == v) { premier = premier.prochain; if (premier==null) dernier = null; return; } while (n != null && n.valeur != v) n = n.prochain; if (n != null) { n.precedent.prochain = n.prochain; n.prochain.precedent = n.predcedent; } } null n
Généralisation • Définir un nœud pour contenir tout Object public class Noeud { Object element; Noeud prochain; Noeud precedent; … }
Réflexion • Adapter les traitements à cette structure générale avec Object • Comment faire pour pouvoir trier une liste?
Tableau v.s. Liste • Tableau (array) • Taille fixe • Accès rapide avec index (O(1)) • Ajouter/enlever un élément: plus difficile (O(n)) • À utiliser si • Beaucoup d’accès aléatoire • Pas besoin de modifier souvent l’ordre des éléments • Nombre d’éléments à stocker déterminée (ou une limite existe) • Liste • Taille flexible • Accès plus lent (O(n)) • Ajouter/enlever un élément: plus facile (O(1)) • À utiliser si • Peu d’accès aléatoire (souvent un parcours de tous les éléments) • Nombre d’élément très variable • Éléments sont souvent re-ordonnés, ajoutés ou enlevés
Allocation de mémoire • La mémoire est allouée quand on crée un nœud • Les nœuds enlevés ne sont plus utilisés • Gabbage collector: récupère les mémoiresqui ne sont plus utilisées • Pas besoin de gérer l’allocation et désallocation de mémoire en Java • On ne peut pas contrôler quand le gabbage collector sera lancé (dépend de l’implantation de JVM) • finalize(): la méthode héritée de Object, mais qu’on peut remplacer • Définir des opérations à effectuer quand le gabbage collector va récupérer cette mémoire. • Ne permet pas d’évoquer le gabbage collector • À utiliser dans certains cas spéciaux (e.g. pour évoquer delete d’un objet en C++, utilisé en Java)
De l’implantation vers un type abstrait • Implantation de Liste pour les éléments contenant int, Object, etc. • Généralisation • Définir les opérations sur la liste pour tout type de données • Les opérations communes sur une liste: (interface List) • boolean add(Object o): Ajouter à la fin • void add(int index, Object o): Ajouter à une position • void clear(): Enlever tout • boolean contains(Object o): Contanir un élément? • boolean isEmpty(): Vide? • boolean remove(Object o): Enlever le premier o • int size(): nombre d’éléments • … • Iterator irerator(): Iterator – permet de parcourir les éléments
Implantation avec une liste chaînée • LinkedList public class LinkedList<E> extends AbstractList<E> { … private class Node { private E element; private Node next; // Create a Node containing specified element. public Node (E element) { this.element = element; this.next = null; } } // end of class Node } // end of class LinkedList
Implantation LinkedList public class LinkedList<E> extends AbstractList<E> { private int size; private Node first; // Create an empty LinkedList<E>. public LinkedList () { size = 0; first = null; } … }
Implantation LinkedList • Une méthode interne pour obtenir le i-ième élément /** * The i-th node of this LinkedList. * The LinkedList must be non-empty. * require 0 <= i && i < this.size() */ private Node getNode (int i) { Node p = first; int pos = 0; // p is pos-th Node while (pos != i) { p = p.next; pos = pos + 1; } return p; } public E get (int index) { Node p = getNode(index); return p.element; }
Implantation LinkedList • Enlever le i-ième élément public void remove (int index) { if (index == 0) first = first.next; else { Node p = getNode(index-1); p.next = p.next.next; } size = size - 1; }
Implantation avec tableau? • ArrayList: implante l’interface List avec un tableau • Possède une taille limite, mais est agrandi automatiquement de 50% au besoin • Accès rapide avec un indexe • Ajouter un élément: O(n) pour déplacer les éléments
Méthodes de ArrayList • void add(int index, Object o) • boolean add(Object o): ajouter à la fin • void clear() • boolean contains(Object o) • void ensureCapacity(int minCapacity): assurer qu’il y a des places nécessaires • boolean isEmpty() • Object remove(int index) • Object set(int index, Object o) • int size(): numbre d’éléments dans la liste • void trimtosize(): enlever les espaces non utilisé • …
Opération de tri sur une liste public class Liste { Noeud premier, dernier; public void concatener(Liste l) {…} // à réfléchir public void quicksort() { if (premier == null) return; Liste l1 = new Liste(); Liste l2 = new Liste(); this.separer(premier.valeur, l1, l2); l1.quicksort(); l2.quicksort(); l1.concatener(l2); premier = l1.premier; } …
Opération de tri sur une liste private separer(int pivot, Liste l1, Liste l2) { Noeud n = premier; while (n!=null) { if (n.valeur<=pivot) l1.ajoutFin(n.valeur); else l2.ajoutFin(n.valeur); } • À réfléchir: généralisation - trier une liste contenant des éléments Comparable
Iterator • Permet de parcourir sur les éléments d’une liste (ou de n’importe quel ensemble) • Référence courant: Position courante (initialisée au premier) • Utilisation: LinkedList list; … Iterator iter = list.iterator(); while (iter.hasNext()) { Object obj = iter.next(); // do something with obj }
Interface Interator • boolean hasNext() • Object next(): retourner l’élément courant, et avancer la référence courante au prochain • void remove(): enlever l’élément courant, la référence courante réfère au prochain élément
Implanter Iterator pour Liste (ex.) public class Liste { Noeud premier, dernier, courant; public ListeIterator iterator() { returnnew ListeIterator(this); } } publicclass ListeIterator implements Iterator { private Liste liste; private Noeud courant; public ListeIterator(Liste liste) { this.liste= liste; courant=liste.premier; } publicboolean hasNext() { return courant != null} public Object next() { return courant; if (hasNext()) courant = courant.prochain; } publicvoid remove() { thrownew UnsupportedOperationException(); } } Liste liste = ...; for (Iterator i= liste.iterator(); i.hasNext(); ) { Object obj= i.next(); // do something with obj }
D’autres types de données • Pile (stack): premier entré, dernier sorti • Empiler un élément • Dépiler un élément • Test isEmpty() • Queue: premier entré, premier sorti • Mettre dans la queue (enqueue) • Enlever de la queue (dequeue) • Test isEmpty() • Implantations (à réfléchir) • Avec tableau (circulaire) • Avec liste
Arbre de recherche binaire • Arbre: • Racine • Enfants • Chaque nœud a un parent au plus (0 parent pour racine) • Arbre binaire: • Un parent peut avoir 2 enfants au plus
Arbre de recherche binaire • Arbre de recherche binaire: • Organiser les nœuds selon leurs valeurs • Branche gauche: valeurs <= valeur du parent • Branche gauche: valeurs > valeur du parent 7 5 9 3 7 10
Pourquoi arbre de recherche binaire? • Recherche dans une liste: O(n) • Recherche dans un arbre de recherche binaire, selon une valeur v: • Valeur de racine r = v? • Sinon: • Si v < r, chercher à gauche; • Sinon, chercher à droite • Si l’arbre est balancé: O(log n)
Implantation en Java • class Noeud { int valeur; Noeud gauche, droite; public Noeud(int v, Noeud g, Noeud d) { valeur = v; gauche = g; droire = d; } } • class Arbre { Noeud racine; public void add(int v) { … } }