Estructuras de Datos y Algoritmos en Java

Los desarrolladores utilizan los arrays y las variantes de listas enlazadas para construir una gran variedad de estructuras de datos complejas. Este p�gina explora dos de esas estructuras: las Pilas, las Colas . Cuando presentemos los algoritmos lo haremos �ncamente en c�digo Java por motivos de brevedad.

.�Pilas que "Recuerdan"

La Pila es una estrucutra de datos donde las inserciones y recuperaciones/borrados de datos se hacen en uno de los finales, que es conocido como el top de la pila. Como el �ltimo elemento insertado es el primero en recuperarse/borrarse, los desarrolladores se refieren a estas pilas como pilas LIFO (last-in, first-out).

Los datos se push (insertan) dentro y se pop (recuperan/borran) de la parte superior de la pila. La siguiente figura ilustra una pila con tres String cada uno insertado en la parte superior de la pila:

Como muestra la figura anterior, las pilas se construyen en memoria. Por cada dato insertado, el it�m superior anterior y todos los datos inferiores se mueven hacia abajo. Cuando llega el momento de sacar un �tem de la pila, se recpupera y se borra de la pila el �tem superior (que en la figura anterior se revela como "third").

Las pilas son muy �tiles en varios escenarios de programaci�n. Dos de los m�s comunes son:

  • Pilas que contienen direcciones de retorno:
    Cuando el c�digo llama a un m�todo, la direcci�n de la primera instrucci�n que sigue a la llamada se inserta en la parte superior de la pila de llamadas de m�todos del thread actual. Cuando el m�todo llamado ejecuta la instrucci�n return, se saca la direcci�n de la parte superior de la pila y la ejecuci�n contin�a en sa direcci�n. Si un m�todo llama a otro m�todo, el comportamiento LIFO de la pila asegura que la instrucci�n return del segundo m�todo transfiere la ejecuci�n al primer m�todo, y la del primer m�todo transfiere la ejecuci�n al c�digo que sigue al c�digo que llam� al primer m�todo. Como resultado una pila "recuerda" las direcciones de retorno de los m�todos llamados.
  • Pilas que contienen todos los par�metros del m�todo llamado y las variables locales:
    Cuando se llama a un m�todo, la JVM reserva memoria cerca de la direcci�n de retorno y almacena todos los par�metros del m�todo llamado y las variables locales de ese m�todo. Si el m�todo es un m�todo de ejemplar, uno de los par�metros que almacena en la pila es la referencia this del objeto actual.

Es muy com�n implementar una pila utilizando un array uni-dimensional o una lista de enlace simple. En el escenario del array uni-dimensional, una variable entera, t�picamente llamada top, contiene el �ndice de la parte superior de la pila. De forma similar, una variable de referencia, tambi�n nombrada noramlmente como top, referencia el nodo superior del escenario de la lista de enlace simple.

He modelado mis implementaciones de pilas despu�s de encontrar la arquitectura del API Collections de Java. Mis implementaciones constan de un interface Stack para una m�xima flexibilidad, las clases de implementaci�n ArrayStack y LinkedListStack, y una clase de soporte FullStackException. Para facilitar su distribuci�n, he empaquetado estas clases en un paquete llamado com.javajeff.cds, donde cds viene de estructura de datos complejas. El siguiente listado presenta el interface Stack:

// Stack.java 

package com.javajeff.cds; 

public interface Stack  { 
     boolean isEmpty (); 
     Object peek (); 
     void push (Object o); 
     Object pop (); 
}

Sus cuatro m�todos determinan si la pila est� vac�a, recuperan el elemento superior sin borrarlo de la pia, situan un elemento en la parte superior de la pila y el �ltimo recuera/borra el elemento superior. Aparte de un constructor espec�fico de la implementaci�n, su programa �nicamente necesita llamar a estos m�todos.

El siguiente listado presenta una implementaci�n de un Stack basado en un array uni-dimensional:

// ArrayStack.java 

package com.javajeff.cds; 

public class ArrayStack implements Stack  { 
     private int top = -1; 
     private Object [] stack; 

     public ArrayStack (int maxElements) { 
            stack = new Object [maxElements]; 
     } 

     public boolean isEmpty () { 
            return top == -1; 
     } 
 
     public Object peek () { 
            if (top < 0) 
                    throw new java.util.EmptyStackException (); 
            return stack [top]; 
     } 

     public void push (Object o) { 
            if (top == stack.length - 1) 
                    throw new FullStackException (); 
            stack [++top] = o; 
     } 

     public Object pop () { 
            if (top < 0) 
                    throw new java.util.EmptyStackException (); 
            return stack [top--]; 
     } 
} 

ArrayStack revela una pila como una combinaci�n de un �ndice entero privado top y variables de referencia de un array uni-dimensional stack. top identifica el elemento superior de la pila y lo inicializa a -1 para indica que la pila est� vac�a. Cuando se crea un objeto ArrayStack llama a public ArrayStack(int maxElements) con un valor entero que representa el n�mero m�ximo de elementos. Cualquier intento de sacar un elemento de una pila vac�a mediante pop() resulta en el lanzamiento de una java.util.EmptyStackException. De forma similar, cualquier intento de poner m�s elementos de maxElements dentro de la pila utilizando push(Object o) lanzar� una FullStackException, cuyo c�digo aparece en el siguiente listado:

// FullStackException.java 

package com.javajeff.cds; 

public class FullStackException extends RuntimeException { 
} 

Por simetr�a con EmptyStackException, FullStackException extiende RuntimeException. Como resultado no se necesita a�adir FullStackException a la clausula throws del m�todo.

El siguiente listado presenta una implementaci�n de Stack utilizando una lista de enlace simple:

// LinkedListStack.java 

package com.javajeff.cds; 

public class LinkedListStack implements Stack  { 
     private static class Node { 
            Object o; 
            Node next; 
     } 

     private Node top = null;  

     public boolean isEmpty () { 
            return top == null; 
     } 

     public Object peek () { 
            if (top == null) 
                    throw new java.util.EmptyStackException (); 
            return top.o; 
     } 

     public void push (Object o) { 
            Node temp = new Node (); 
            temp.o = o; 
            temp.next = top; 
            top = temp; 
     } 

     public Object pop () { 
            if (top == null) 
                    throw new java.util.EmptyStackException (); 
            Object o = top.o; 
            top = top.next; 
            return o; 
     } 
} 

LinkedListStack revela una pila como una combinaci�n de una clase anidada privada de alto nivel llamada Node y una variable de referencia privada top que se inicialia a null para indicar una pila vac�a. Al contrario que su contrapartida del array uni-dimensional, LinkedListStack no necesita un constructor ya que se expande din�micamente cuando se ponen los �tems en la pila. As�, void push(Object o) no necesita lanzar una FullStackException. Sin embargo, Object pop() si debe chequear si la pila est� vac�a, lo que podr�a resultar en el lanzamiento de una EmptyStackException.

Ahora que ya hemos visto el interface y las tres clases que generan mis implementaciones de las pilas, juguemos un poco. El siguiente listado muestra casi todo el soporte de pilas de mi paquete com.javajeff.cds:

// StackDemo.java 

import com.javajeff.cds.*; 

class StackDemo { 
     public static void main (String [] args)  { 
            System.out.println ("ArrayStack Demo"); 
            System.out.println ("---------------"); 
            stackDemo (new ArrayStack (5)); 
            System.out.println ("LinkedListStack Demo"); 
            System.out.println ("--------------------"); 
            stackDemo (new LinkedListStack ()); 
     } 

     static void stackDemo (Stack s) { 
            System.out.println ("Pushing \"Hello\""); 
            s.push ("Hello"); 

            System.out.println ("Pushing \"World\""); 
            s.push ("World"); 

            System.out.println ("Pushing StackDemo object"); 
            s.push (new StackDemo ()); 

            System.out.println ("Pushing Character object"); 
            s.push (new Character ('C')); 

            System.out.println ("Pushing Thread object"); 
            s.push (new Thread ("A")); 

            try { 
                    System.out.println ("Pushing \"One last item\""); 
                    s.push ("One last item"); 
            } 
            catch (FullStackException e)  { 
                    System.out.println ("One push too many"); 
            } 

            System.out.println (); 
            
            while (!s.isEmpty ()) 
                 System.out.println (s.pop ()); 
            try { 
                    s.pop (); 
            } 
            catch (java.util.EmptyStackException e) { 
                    System.out.println ("One pop too many"); 
            } 
            System.out.println (); 
     } 
} 

Cuando se ejecuta StackDemo, produce la siguiente salida:

ArrayStack Demo 
--------------- 
Pushing "Hello" 
Pushing "World" 
Pushing StackDemo object 
Pushing Character object 
Pushing Thread object 
Pushing "One last item" 
One push too many 

Thread[A,5,main] 
C 
StackDemo@7182c1 
World 
Hello 
One pop too many 

LinkedListStack Demo 
-------------------- 
Pushing "Hello" 
Pushing "World" 
Pushing StackDemo object 
Pushing Character object 
Pushing Thread object 
Pushing "One last item" 

One last item 
Thread[A,5,main] 
C 
StackDemo@cac268 
World 
Hello 
One pop too many 

.�Priorizar con Colas

La Cola es una estructura de datos donde la inserci�n de �tem se hace en un final (el fin de la cola) y la recuperaci�n/borrado de elementos se hace en el otro final (el inicio de la cola). Como el primer elemento insertado es el primero en ser recuperado, los desarrolladores se refieren a estas colas como estructuras FIFO (first-in, first-out).

Normalmente los desarrolladores tabajan con dos tipos de colas: lineal y circular. En ambas colas, la inserci�n de datos se realiza en el fin de la cola, se mueven hacia adelante y se recuperan/borran del incio de la cola. La siguiente figura ilustra las colas lineal y circular:

La cola lineal de la figura anterior almacena cuatro enteros, con el entero 1 en primer lugar. Esa cola est� llena y no puede almacenar m�s datos adicionales porque rear identifica la parte final de la cola. La raz�n de la posici�n vac�a, que identifica front, implica el comportamiento lineal de la cola. Inicialmente, front y rear identifican la posici�n m�s a la izquierda, lo que indica que la cola est� vac�a. Para almacenar el entero 1, rear avanza una posici�n hacia la derecha y almacena 1 en esa posici�n. Para recuperar/borrar el entero 1, front avanza una posici�n hacia la derecha.

Nota:
Para se�alar que la cola lineal est� vac�a, no necesita gastar una posici�n, aunque esta aproximaci�n algunas veces es muy conneniente. En su lugar asigne el mismo valor que indique una posici�n no existente a front y a rear. Por ejemplo, asumiendo una implementaci�n basada en un array uni-dimensional, front y rear podr�an contener -1. El �ndice 0 indica entonces la posici�n m�s a la izquierda, y los datos se insertar�n empezando en este �ndice.

Cuando rear identifique la posici�n m�s a la derecha, la cola lineal podr�a no estar llena porque front podr�a haber avanzado almenos una posici�n para recuperar/borrar un dato. En este esceario, considere mover todos los �tems de datos hacia la izquierda y ajuste la posici�n de front y rear de la forma apropiada para crear m�s espacio. Sin embargo, demasiado movimiento de datos puede afectar al rendimiento, por eso debe pensar cuidadosamente en los costes de rendimiento si necesita crear m�s espacio.

La cola circular de la figura anterior tiene siete datos enteros, con el entero 1 primero. Esta cola est� llena y no puede almacenar m�s datos hasta que front avance una posici�n en sentido horario (para recuperar el entero 1) y rear avance una posici�n en la misma direci�n (para identificar la posici�n que contendr� el nuevo entero). Al igual que con la cola lineal, la razon de la posici�n vac�a, que identifica front, implica el comportamiento circular de la cola. Inicialmente, front y rear identifican la misma posici�n, lo que indica una cola vac�a. Entonces rear avanza una posici�n por cada nueva inserci�n. De forma similar, front avanza una posici�n por cada recuperaci�n/borrado.

Las colas son muy �tiles en varios escenarios de programaci�n, entre los que se encuentran:

  • Temporizaci�n de Threads:
    Una JVM o un sistema operativo subyacente podr�an establecer varias colas para coincidir con diferentes prioridades de los threads. La informaci�n del thread se bloquea porque todos los threads con una prioridad dada se almacenan en una cola asociada.
  • Trabajos de impresi�n:
    Como una impresora normalmente es m�s lenta que un ordenador, un sistema operativo maneja los trabajos de impresi�n en un subsistema de impresi�n, que inserta esos trabajos de impresi�n en una cola. El primer trabajo en esa cola se imprime primero, y as� sucesivamente.

Los desarrolladores normalmente utilizan una array uni-dimensional para implementar una cola. Sin embargo, si tienen que co-existir m�ltiple colas o las inserciones en las colas deben ocurrir en posiciones distintas a la �ltima por motivos de prioridades, los desarrolladores suelen cambiar a la lista doblemente enlazada. Con un array uni-dimensional dos variables enteras (normalmente llamadas front y rear) contienen los �ndices del primer y �ltimo elemento de la cola, respectivamente. Mis implementaciones de colas lineales y circulares usan un array uni-dimensional y empiezan con el interface Queue que puede ver en el siguiente listado:

// Queue.java 

package com.javajeff.cds; 

public interface Queue { 
     void insert (Object o); 
     boolean isEmpty (); 
     boolean isFull (); 
     Object remove (); 
} 

Queue declara cuatro m�todos para almacenar un datos, determinar si la cola est� vac�a, determinar si la cola est� llena y recuperar/borrar un dato de la cola. Llame a estos m�todos (y a un constructor) para trabajar con cualquier implementaci�n de Queue.

El siguiente listado presenta una a implementaci�n de Queue de una cola lineal basada en un array uni-dimensional:

// ArrayLinearQueue.java 

package com.javajeff.cds; 

public class ArrayLinearQueue implements Queue { 
     private int front = -1, rear = -1; 
     private Object [] queue; 

     public ArrayLinearQueue (int maxElements) { 
            queue = new Object [maxElements]; 
     } 

     public void insert (Object o) { 
            if (rear == queue.length - 1) 
                    throw new FullQueueException (); 
            queue [++rear] = o; 
     } 

     public boolean isEmpty () { 
            return front == rear; 
     } 

     public boolean isFull () { 
            return rear == queue.length - 1; 
     } 

     public Object remove () { 
            if (front == rear) 
                    throw new EmptyQueueException (); 
            return queue [++front]; 
     } 
} 

ArrayLinearQueue revela que una cola es una combinaci�n de variables privadas front, rear, y queue. front y rear se inicializan a -1 para indicar una cola vac�a. Igual que el constructor de ArrayStack llama a public ArrayLinearQueue(int maxElements) con un valor entero que especifique el n�mero m�ximo de elementos durante la construcci�n de un objeto ArrayLinearQueue.

El m�todo insert(Object o) de ArrayLinearQueue lanza una FullQueueException cuando rear identifica el elemento final del array uni-dimensional. El c�digo de FullQueueException aparece en el siguiente listado:

// FullQueueException.java 

package com.javajeff.cds; 

public class FullQueueException extends RuntimeException { 
} 

El m�todo remove() de ArrayLinearQueue lanza una EmptyQueueException cuando los objetos front y rear son iguales. El siguiente listado presenta el c�digo de esta clase:

// EmptyQueueException.java 

package com.javajeff.cds; 

public class EmptyQueueException extends RuntimeException { 
} 

El siguiente listado presenta una implementaci�n de Queue para una cola circular basada en un array uni-dimensional:

// ArrayCircularQueue.java 

package com.javajeff.cds; 

public class ArrayCircularQueue implements Queue { 
     private int front = 0, rear = 0; 
     private Object [] queue; 

     public ArrayCircularQueue (int maxElements) { 
            queue = new Object [maxElements]; 
     } 

     public void insert (Object o) { 
            int temp = rear; 
            rear = (rear + 1) % queue.length; 
            if (front == rear) { 
                    rear = temp; 
                    throw new FullQueueException (); 
            } 
            queue [rear] = o; 
     } 

     public boolean isEmpty () { 
            return front == rear; 
     } 

     public boolean isFull () { 
            return ((rear + 1) % queue.length) == front; 
     } 

     public Object remove () { 
            if (front == rear) 
                    throw new EmptyQueueException (); 
            front = (front + 1) % queue.length; 
            return queue [front]; 
     } 
} 

ArrayCircularQueue revela una implementaci�n, en terminos de variables privadas y un constructor, muy similar a ArrayLinearQueue. El m�todo insert(Object o) es interesante porque guarda el valor actual de rear antes de hacer que esa variable apunte a la siguiente posici�n. Si la cola circular est� llena, rear restaura su valor original antes de lanzar una FullQueueException. La restauraci�n de rear es necesaria porque front es igual a rear (en ese punto), y una subsecuente llamada a remove() resulta en la lanzamiento de una EmptyQueueException (incluso aunque la cola circular no est� vac�a).

Despu�s de estudiar el c�digo del interface y de varias clases que lo implementan bas�ndose en arrays uni-dimensionales, consideremos en el siguiente listado una aplicaci�n que demuestra las colas lineales y circulares:

// QueueDemo.java 

import com.javajeff.cds.*; 

class QueueDemo { 
     public static void main (String [] args) { 
            System.out.println ("ArrayLinearQueue Demo"); 
            System.out.println ("---------------------"); 
            queueDemo (new ArrayLinearQueue (5)); 
            System.out.println ("ArrayCircularQueue Demo"); 
            System.out.println ("---------------------"); 
            queueDemo (new ArrayCircularQueue (6)); // Need one more slot because 
                                                    // of empty slot in circular 
                                                    // implementation 
     } 

     static void queueDemo (Queue q) { 
            System.out.println ("Is empty = " + q.isEmpty ()); 
            System.out.println ("Is full = " + q.isFull ()); 

            System.out.println ("Inserting \"This\""); 
            q.insert ("This"); 

            System.out.println ("Inserting \"is\""); 
            q.insert ("is"); 

            System.out.println ("Inserting \"a\""); 
            q.insert ("a"); 

            System.out.println ("Inserting \"sentence\""); 
            q.insert ("sentence"); 

            System.out.println ("Inserting \".\""); 
            q.insert ("."); 

            try  { 
                    System.out.println ("Inserting \"One last item\""); 
                    q.insert ("One last item"); 
            } 
            catch (FullQueueException e) { 
                    System.out.println ("One insert too many"); 
                    System.out.println ("Is empty = " + q.isEmpty ()); 
                    System.out.println ("Is full = " + q.isFull ()); 
            } 

            System.out.println (); 

            while (!q.isEmpty ()) 
               System.out.println (q.remove () + " [Is empty = " + q.isEmpty () + 
                   ", Is full = " + q.isFull () + "]"); 

            try { 
                    q.remove (); 
            } 
            catch (EmptyQueueException e) { 
                    System.out.println ("One remove too many"); 
            } 
            System.out.println (); 
     } 
} 

Cuando se ejecuta QueueDemo, se produce la siguiente salida:

ArrayLinearQueue Demo 
--------------------- 
Is empty = true 
Is full = false 
Inserting "This" 
Inserting "is" 
Inserting "a" 
Inserting "sentence" 
Inserting "." 
Inserting "One last item" 
One insert too many 
Is empty = false 
Is full = true 

This [Is empty = false, Is full = true] 
is [Is empty = false, Is full = true] 
a [Is empty = false, Is full = true] 
sentence [Is empty = false, Is full = true] 
. [Is empty = true, Is full = true] 
One remove too many 
 
ArrayCircularQueue Demo 
--------------------- 
Is empty = true 
Is full = false 
Inserting "This" 
Inserting "is" 
Inserting "a" 
Inserting "sentence" 
Inserting "." 
Inserting "One last item" 
One insert too many 
Is empty = false 
Is full = true 

This [Is empty = false, Is full = false] 
is [Is empty = false, Is full = false] 
a [Is empty = false, Is full = false] 
sentence [Is empty = false, Is full = false] 
. [Is empty = true, Is full = false] 
One remove too many

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP