Structure de données: Liste

publicité
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 soimême une liste
• D’autres structures souvent utilisées en
informatique
Pourquoi une structure de
données?
• Structure de données:
– Une organisation des informations
– Pour faciliter le traitement efficace (tableau,
liste, …)
– Pour mieux regrouper les informations pour le
même concept (classe, …)
Structures de données déjà vues
• Tableau: pour organiser une séquence de données
– int [ ] a = new int [10];
a
• Classe: pour regrouper les attributs du même concept
public class C
{
int a;
String name;
}
C ref = new C();
ref
int a:
String name:
0
null
Liste
• Utilité: pour organiser une séquence de données
• Une structure plus flexible que tableau
Tableau
Taille fixe
Ordre entre éléments
Insertion et enlèvement difficile
Liste
Taille variable
Ordre
Plus facile
Accès rapide
Plus lent
Illustration
• Entête (node)
• noeuds enchaînés
node
A
B
C
• Chaque noeud enchaîné:
– Valeur stockée (A, B, …)
– Référence vers le prochain nœud
D
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
• Facultatif:
– 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;
}
}
Création
• Créer une entête:
Liste l = new Liste();
l
null
• Créer des nœuds:
l.premier = new Noeud(1,
new Noeud(2,
new Noeud(3,null)));
l
1
2
3
null
Traverser une 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");
}
…
Noeud premier;
}
Résultat: 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;
}
…
Noeud premier;
}
Déterminer la longueur
public class Liste
{ …
public int longueur()
{
Noeud n = premier;
int nb=0;
if (premier == null) return 0; // cette ligne peut être enlevée
while (n != null)
{
nb++;
n = n.prochain;
}
return nb;
}
…
Noeud premier;
}
Ajouter un élément au début
public class Liste
{…
public void ajoutDebut(int v)
{
premier = new Noeud(v, premier);
}
…
}
l
1
5
2
3
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
l
1
2
null
3
n
5
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;
Attention
return;
}
// 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;
}
…
}
•
à l’ordre des tests
Pas besoin de déplacer les autres éléments pour laisser une place (comme
tableau)
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
Et si?
• Si on a une réf. vers le dernier élément?
public class Liste
{
Noeud premier, dernier;
// méthodes pour traiter une liste?
}
• Exemple:
public void ajoutFin(int v)
{
Noeud n = premier;
// cas 1: aucun autre élément ajouté
if (premier == null) premier = dernier = new Noeud(v,null);
// cas 2: il y a déjà des éléments
else
{
dernier.prochain = new Noeud(v,null);
dernier = dernier.prochain;
}
}
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 (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
•
Longueur =
– 0, si la liste est vide
– 1 + longueur du reste, sinon.
public class Liste
{ Noeud premier;
public int longueur()
{
if (premier == null) return 0;
Cet appel est possible seulement
else return premier.longueur();
quand premier!=null
}
}
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();
}
}
Illustration
l
1
premier.longueur()
longueur()=
=
3
1 + ?2
2
longueur()=
1 + ?1
public class Nœud
{ public int longueur()
{
// cas 1: pas de récursion
if (prochain==null) return 1;
// cas 2: récursion
else return 1+ prochain.longueur();
}
}
3
longueur()=
1
null
Déterminer la longueur (bis)
public class Liste
{ Noeud premier;
public int longueur()
{
return longueur(premier);
}
Cet appel est possible même si
premier==null
// 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);
}
}
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 nonvide
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)
//même nom: ça fonctionne,
//mais déconseillé
{
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
–…
Notion de complexité
asymptotique
• Ne pas mesurer le traitement en temps réel sur un
ordinateur (dépendant de l’ordinateur)
• Déterminer une expression de complexité en fonction du
nombre d’éléments à traiter (n)
• Notation grand O:
– Une approximation
– Ignore les constantes:
• O(n+3) = O(n)
• O(4n) = O(n), O(10) = O(1)
– Déterminer par le plus grand facteur
• O(n+n2) = O(n2) (car n2 > n)
• O(n2+n log n) = O(n2) (car n2 > n log n)
Complexité des opérations
• Longueur: O(n)
– n éléments déjà dans la liste
• Trouver un élément: O(n)
– En moyenne O(n/2)=O(n)
• Enlever un élément: O(n)
– En moyenne O(n/2)
• Ajouter au début: O(1)
– Quelques opérations – nombre constante
• Ajouter à la fin: O(n)
– Aller à la fin, et ajouter
– Améliorable ?
Liste simplement chaînée:
amélioration
• Ajouter une référence au dernier élément
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;
}
}
…
}
O(n)  O(1)
Liste doublement chaînée
• Référence vers le prochain nœud
• Référence vers le précédent nœud
public class Noeud
{
int valeur;
Noeud prochain;
Noeud precedent;
…
}
• Permet d’avancer et reculer dans la liste
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;
if (n.prochain != null) n.prochain.precedent = n.predcedent;
}
}
1
2
n
3
null
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
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émoires qui
ne sont plus utilisées
– Pas besoin de gérer l’allocation et
désallocation de mémoire 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)
•
•
•
•
•
•
•
•
•
Ajouter à la fin: boolean add(Object o)
Ajouter à une position: void add(int index, Object o)
Enlever tout: void clear()
Contanir un élément? Boolean contains(Object o)
Vide?: boolean empty()
Enlever le premier o: boolean remove(Object o)
Taille: int size()
…
Iterator: Iterator irerator()
Implantation avec une liste chaînée
• LinkedList
public class LinkedList<E> extends
AbstractList<E> {
…
private class Node {
private E element;
private Node next;
// an inner class only used in LinkedList
// 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 (cont.)
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 (cont.)
• 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;
}
Dans LinkedList:
abstract Object get(int index)
Implantation LinkedList
• Enlever le i-ième élément
public void remove (int index) {
if (index == 0)
if (first!==null) {first = first.getNext(); size--;}
else {
Node p = getNode(index-1);
if (p!==null) {p.setNext(p.getNext().getNext());
size--;}
}
}
Implantation avec tableau?
• À réflechir
• Exemple:
– Création: créer un tableau de 10 éléments (à agrandir
si nécessaire)
– Chercher le i-ième élément (si i<longueur, retourner
tab[i], sinon null)
– Ajouter à la position courante
– Ajouter à la fin (ajouter à tab[longueur], longueur++)
– Insérer à la i-ième place (déplacer i:fin, ajouter à i)
–…
Exemple
public class ArrayList
{
public ArrayList()
{
tab = new Object[10];
position = 0;
size = 0; //nombre d’éléments
capacity = 10; //taille de tableau
}
public void add(Object o)
{
if (position >= capacity) { …} // doubler la taille
tab[position] = o;
position++;
size++;
}
…
private int size, capacity;
private Object[] tab;
private int position;
}
Téléchargement