Capı́tulo 3 Listas 3.1 Introducción Las listas las tenemos en muchos casos en la vida real: lista de compras, lista de útiles escolares, lista de invitados, lista de pendientes, lista de tareas, lista de discos, etc. Pero, ¿qué es una lista? Una lista es una estructura de datos con la propiedad que se pueden agregar y suprimir datos en cualquier lugar. El tipo de datos Lista debe tener las siguientes propiedades: • Determinar si está vacı́a la lista. • Limpiar la lista. • Agregar un elemento. • Encontrar un elemento. (por valor). • Actualizar un elemento. • Eliminar un elemento. Como caracterı́sticas de las listas se tiene que de antemano se desconoce cuántos elementos tendrá ésta. Es irrelevante el orden de los elementos y el manejo de ellos, es decir las inserciones y supresiones pueden ser de cualquier dato no por posición. La interfaz para la clase de las listas es: 1 3.2. IMPLEMENTACIÓN 2 interface Listable { public boolean estáVacı́a(); public void limpiar(); public Object primerElemento(); public void agregar(Object x); //Inserta al inicio de la lista public boolean contiene(Object x); public void eliminar(Object x); //Borra el primer nodo que tenga valor x public void sustituir(Object orig, Object nuevo); public java.util.Iterator elementos(); } Propiedades que se esperan de una lista: • estáVacı́a. Devuelve true si la lista está vacı́a y false en otro caso. • limpiar. Elimina todos los elementos de la lista, es decir el tamaño de la lista después de esta operación es cero.. • primerElemento. Devuelve el valor almacenado en el primer elemento de la lista. El estado de la lista no se ve alterado. • agregar. Agrega al inicio de la lista el objeto especificado como el parámetro. El tamaño de la lista crece en una unidad. • contiene. Regresa true si el elemento está en la lista y false en otro caso. Este método no cambia el estado de la lista. • eliminar. Si el elemento existe en la lista lo elimina, en caso contrario dispara la excepción NoSuchElementException. En caso de que la operación sea exitosa se reduce el tamaño de la lista en una unidad. • sustituir. Si el elemento a sustituir se encuentra en la lista lo sustituye por el segundo parámetro, en caso contrario dispara la excepción NoSuchElementException. Esta operación no altera el tamaño de la lista. 3.2 Implementación Las listas se pueden implementar con arreglos pero para evitar el costo de recorridos durante la inserción o supresión, se puede usar el concepto de lista 3.2. IMPLEMENTACIÓN 3 ligada (Ver figura) la cual consta de un conjunto de nodos, no necesariamente almacenados en forma adyacente. Cada nodo contiene el elemento y una liga o enlace a su sucesor. e1 e2 e3 e4 e5 l Figura 3.1: Implementación usando dos clases: la de la lista ligada (Lista)y la de nodos (Nodo). La clase de los nodos tinen un elemento que es el dato a guardar en la lista, y una referencia al siguiente nodo, que es la liga. Puede resultar curioso que se tenga una referencia a un objeto del tipo que está definiendo pero es válido hacer esto. /* * Clase para manejar los nodos de la lista */ class Nodo { Object elemento; Nodo sgte; /** * Crea un nodo con elemento igual a valor y apuntando al vacı́o. * @param valor el Objeto que es el valor de nodo */ Nodo(Object valor) { elemento = valor; sgte = null; } /** * Crea un nodo después del indicado, con elemento igual a valor. * @param valor el Objeto que es el valor de nodo * @param n el nodo anterior al recién creado */ Nodo(Object valor, Nodo n) { elemento = valor; 3.2. IMPLEMENTACIÓN 4 sgte = n; } /* * Devuelve el valor de un nodo * @return Object el valor del nodo */ public Object daElemento () { return elemento; } /** * Devuelve la referencia del siguiente nodo * @return NOdo el siguietne nodo */ public Nodo daSgte() { return sgte;} } A continuación la implementación de la interfaz, creando la lista ligada con un nodo centinela al inicio de la misma. /** * Listada ligada usando un nodo centinela */ public class Lista implements Listable{ private Nodo inicio; /** Construye la lista */ public Lista() { inicio = null; } /** Prueba que la lista esté vacı́a. * @return true si está vacı́a y false en otro caso. */ public boolean estaVacı́a() { return inicio == null; } /** Crea una lista vacı́a. */ public void limpiarLista() { inicio = null; } 3.2. IMPLEMENTACIÓN 5 /** Devuelve el primer nodo de la lista **/ public Nodo primerElemento() { return inicio; } /** * Devuelve la posición del nodo que contiene el dato buscado. * @param dato el dato a buscar. * @return un nodo; null si el dato no se encuentra. */ public Nodo buscar(Object dato) { Nodo pos = inicio; while(pos != null && !pos.elemento.equals(dato)) pos = pos.sgte; return pos; } public void sustituir(Object orig, Object nuevo) Nodo n = buscar(orig); if (n != null) n.elemento = nuevo; } /** * Inserta el primer elemento de la lista. * @param dato el dato a agregar. */ public void agregar(Object dato) { inicio = new Nodo(dato,inicio); /** * Elimina la primera ocurrencia de un dato. * @param dato el dato a eliminar. */ public void eliminar(Object dato) { Nodo pos = inicio, anterior = null; { while(pos != null && !pos.elemento.equals(dato)) { anterior = pos; pos = pos.sgte; } 3.3. LISTAS CON DOS CENTINELAS 6 if (pos == null) return; // No lo encontró if(pos == inicio) // Es el inicio de la lista inicio = inicio.sgte else anterior.sgte = pos.sgte; } public java.util.Iterator elementos() { return new MiIterador(); } private class MiIterador implements java.util.Iterator { private Nodo posicion = inicio; public boolean hasNext() { return posicion != null;} public Object next() throws java.util.NoSuchElementException { if (hasNext()) { Object o = posicion.elemento; posicion = posicion.sgte; return o; } throw new java.util.NoSuchElementException(); } public void remove() { throw new IllegalStateException(); } } } 3.3 Listas con dos centinelas Con la implementación antes vista se tiene que la operación de agregar es de O(1) lo cual es inmejorable. Sin embargo, si se presentara la situación que la operación más común es la agregar al final de la lista, o suprimir el final de la lista, esta serı́a una operación de orden n porque es necesario recorrer toda la lista antes de llegar al final. 3.3. LISTAS CON DOS CENTINELAS 7 public void agregarFinal(Object dato) { if (estaVacı́a()) agregar(dato); else { while(Nodo p = inicio; p.sgte != null; p = p.sgte) ; p.sgte = new Nodo(dato); } Para que este método también sea de orden constante es necesario incluir una nueva referencia que esté siempre apuntando al final de la lista. Es decir, se puede hacer una implementación usando dos centinelas: el del inicio y y otro al final de la lista. como se presenta aquı́: /** * Lista ligada usando dos centinelas: uno al inicio y otro al final. */ public class ListaF implements Listable { private Nodo inicio; private Nodo fin; private final Comparator prueba; Con lo cual la implementación de algunos métodos cambia y se incluyen nuevos métodos. /** Construye la lista */ public ListaF() { inicio = null; fin = null; prueba = new DefaultComparator(); } /** Devuelve el último elemento de la lista **/ public Object últimoElemento() { return (estáVacı́a()) ? null : fin.elemento; } /** * Inserta el primer elemento de la lista. * @param dato el dato a agregar. */ 3.4. LISTAS DOBLEMENTE LIGADAS 8 public void agregar(Object dato) { if (estáVacı́a()) inicio = fin = new Nodo(dato); else inicio = new Nodo(dato,inicio); } /** * Inserta el último elemento de la lista. * @param dato el dato a agregar. */ public void agregarÚltimo(Object dato) { if (estáVacı́a()) inicio = fin = new Nodo(dato); else fin = fin.sgte = new Nodo(dato); } /** * Elimina el primer nodo de la lista. */ public void eliminarPrimero() { inicio = inicio.sgte; } } 3.4 Listas doblemente ligadas En ocasiones resulta conveniente poder moverse en ambas direcciones dentro de la lista. En estos casos es conveniente que cada nodo cuente con un apuntador al nodo que le sigue y otro al nodo que le precede. Es decir se requiere tener una lista con doble liga. class NodoD { Object elemento; NodoD sgte; NodoD anterior; /* Constructor de un nodo con valor dado y apuntadores a null. * @param valor - Objeto que será el valor del nodo. 3.4. LISTAS DOBLEMENTE LIGADAS 9 */ NodoD(Object valor) { elemento = valor; sgte = null; anterior = null; } /* Constructor de un nodo con valor dado y apuntador siguiente al nodo dado * El apuntador al nodo anterior es null. * @param valor - Objeto que será el valor del nodo. * @param sig - valor de la referencia siguiente */ NodoD(Object valor, NodoD sig) { elemento = valor; sgte = sig; anterior = null; } /* Constructor de un nodo con valor dado y apuntador al nodo siguiente y * al anterior proporcionados. * @param valor - Objeto que será el valor del nodo. * @param ant - valor de la referencia anterior * @param sig - valor de la referencia siguiente */ NodoD(Object valor, NodoD ant, NodoD sig) { elemento = valor; anterior = ant; sgte = sig; } } Con estos nodos la implementación de la clase para listas ligadas quedarı́a como sigue: import java.util.Comparator; /** * Lista doblemente ligada con apuntador al inicio y al final de la misma. * @version Octubre 2006 */ public class ListaDoble implements Listable{ 3.4. LISTAS DOBLEMENTE LIGADAS 10 private NodoD inicio; private NodoD fin; private final Comparator prueba; /** Construye la lista vacı́a y utiliza el comparador por omisión */ public ListaDoble() { inicio = fin = null; prueba = new DefaultComparator(); } /** Construye la lista con el comparador especificado */ public ListaDoble(Comparator c) { inicio = fin = null; prueba = c; } /** Prueba que la lista esté vacı́a. * @return true si está vacı́a y false en otro caso. */ public boolean estáVacı́a() { return inicio == null; } /** Crea una lista vacı́a. */ public void limpiar() { inicio = fin = null; } /** Devuelve el primer elemento de la lista * @return Object - primer elemento de la lista */ public Object primerElemento() { return (estáVacı́a()) ? null : inicio.elemento; } /** Devuelve el último elemento de la lista **/ public Object últimoElemento() { return (estáVacı́a()) ? null : fin.elemento; } /** Inserta el primer elemento de la lista * @param dato - Objeto a agregar al inicio de la lista */ public void agregar(Object dato) { 3.4. LISTAS DOBLEMENTE LIGADAS 11 if (estáVacı́a()) inicio = fin = new NodoD(dato); else inicio = inicio.anterior = new NodoD(dato, inicio); } /** Inserta el último elemento de la lista * @param x -- Objeto que se agregará al final de la lista */ public void agregarÚltimo(Object x) { if (estáVacı́a()) agregar(x); else { NodoD n = new NodoD (x); fin.sgte = n; n.anterior = fin; fin = fin.sgte; } } /** * Elimina la primera ocurrencia de un dato. * @param dato el dato a eliminar. */ public void eliminar(Object dato) { NodoD p = inicio; while(p != null && prueba.compare(p.elemento,dato) != 0) p = p.sgte; if (p != null) return; // No encontró el dato a eliminar if (p == inicio) eliminarPrimero(); else if (p == fin) eliminarÚltimo(); else if(p.sgte != null) { p.anterior.sgte = p.sgte; p.sgte.anterior = p.anterior; } } 3.4. LISTAS DOBLEMENTE LIGADAS 12 /** Método para eliminar el primer elemento de una lista */ public void eliminarPrimero() { if (!estáVacı́a()){ inicio = inicio.sgte; if (inicio == null) fin = null; else inicio.anterior = null; } } /** Método para eliminar el último elemento de una lista */ public void eliminarÚltimo() { if (!estáVacı́a()){ fin = fin.anterior; if (fin == null) inicio = null; else fin.sgte = null; } } public MiIterador elementos() { return new MiIterador(); } ... } Faltarı́a hacer el iterador que incluya métodos para recorrer la lista en sentido inverso.