Estructuras de Datos y Algoritmos en Java
Estructuras de Datos y Algoritmos en Java
Estructuras de Datos y Algoritmos en Java
Juan A. Garcia
2005
Introducción
Estructuras de Datos y Algoritmos Básicos
o ¿Qué es una Estructura de Datos?
o ¿Qué es un Algoritmo?
o ¿Cómo se Representa un Algoritmo?
Flowchart
Pseudo código
Arrays
o Arrays de Una Dimensión
Utilizar sólo un Inicializador
Utilizar sólo la Palabra Clave "new"
Utilizar la palabra clave "new" y un Inicializador
o Trabajar con un array uni-dimensional
o Algoritmos de búsqueda-lineal, búsqueda-binaria y ordenación de
burbuja
Búsqueda Lineal
Búsqueda Binaria
Ordenación de Burbuja
o Arrays de Dos Dimensiones
Utilizar sólo un Inicializador
Utilizar sólo la palabra clave "new"
Utilizar la palabra clave "new" y un inicializador
o trabajar con Arrays bi-dimensionales
o Algoritmo de Multiplicación de Matrices
o Arrays Desiguales
o Los Arrays Java son Objetos
Listas Enlazadas
o Lista de Enlace Simple
o Los Algoritmos de Concatenación e Inversión
o Lista Doblemente Enlazada
Algoritmo de Inserción-Ordenada
o Lista de Enlace Circular
o Listas Enlazadas frente a Arrays
Pilas y Colas
o Pilas que "Recuerdan"
o Priorizar con Colas
Árboles
o Organización Jerárquica con Árboles
o Recursión
Introducción
La ciencia informática enfatiza dos tópicos importantes: las estructuras de datos y los
algoritmos. Estos tópicos son importantes porque las elecciones que usted haga para las
estructuras de datos y los algoritmos de un programa afectarán al uso de la memoria (las
estructuras de datos) y al tiempo del procesador (los algoritmos que interactúan con esas
estructuras de datos). Cuando utiliza una estructura de datos o un algoritmo algunas
veces descubre una relación inversa entre la utilización de memoria y el tiempo de CPU:
cuanta menos memoria utiliza una estructura de datos, más tiempo de CPU necesitan los
algoritmos asociados para procesar los ítems de datos de la estructura, que son valores
de tipos primitivos u objetos, mediante referencias. De igual forma, cuanta más
memoria utilice una estructura de datos, menor tiempo de CPU necesitan los algoritmos
asociados y el procesamiento de los ítems de datos es mucho más rápido. En la siguiente
figura aparece está relación inversa.
Las estructuras de datos nos han estado rodeando desde la era de la programación
estructurada. Una definición de esa era: una estructura de datos es un conjunto de tipos,
un tipo diseñado partiendo de ese conjunto de tipos, un conjunto de funciones, y un
conjunto de axiomas. Esta definición implica que una estructura de datos es un tipo con
implementación. En nuestra era de la programación orientada a objetos, tipo con
implementación significa clase.
La definición una estructura de datos es una clase es demasiado amplia porque supone
que Empleado, Vehículo, Cuenta, y otras muchas clases específicas de entidades del
mundo real son estructuras de datos. Aunque esas clases estructuran varios ítems de
datos, describen entidades del mundo real (en la forma de objetos) en lugar de describir
contenedores de objetos para otras entidades objetos (y posiblemente otro contenedor).
Esta idea de contenido da una definición más apropiada para una estructura de datos:
una estructura de datos es una clase contenedora que proporciona almacenamiento
para ítems de datos, y capacidades para almacenar y recuperar estos datos. Algunos
ejemplos de estructuras de datos son los arrays, las listas enlazadas, las pilas y las colas.
¿Qué es un Algoritmo?
La representación más obvia: código fuente Java. Sin embargo escribir código fuente
antes de entender completamente un algoritmo normalmente acaba con bugs difíciles de
encontrar. Una técnica para evitar estos bus es utilizar un flowchart (diagrama de flujo).
Flowchart
Pseudo código
DECLARE CHARACTER ch
DECLARE INTEGER count = 0
DO
READ ch
IF ch IS '0' THROUGH '9' THEN
count++
END IF
UNTIL ch IS '\n'
PRINT count
END
Nota:
En este tutorial se utiliza pseudocódigo para representar algoritmos.
Arrays
El array es una de las estructuras de datos más ampliamente utilizada por su flexibilidad
para derivar en complejas estructuras de datos y su simplicidad. Empezaremos con una
definición: un array es una secuencia de elementos, donde cada elemento (un grupo de
bytes de memoria que almacenan un único ítem de datos) se asocia con al menos un
índice (entero no-negativo). Esta definición lanza cuatro puntos interesantes:
Cada elemento ocupa el mismo número de bytes; el número exacto depende del
tipo de datos del elemento.
Todos los elementos son del mismo tipo.
Tendemos a pensar que los elementos de un array ocupan localizaciones de
memoria consecutivas. Cuando veamos los arrays bi-dimensionales descubrirá
que no es siempre así.
El número de índices asociados con cada elemento es la dimensión del array
Nota:
Esta sección se enfoca exclusivamente en arrays de una y dos dimensiones porque los
arrays de más dimensiones no se utilizan de forma tan frecuente.
El tipo de array más simple tiene una dimensión: cada elemento se asocia con un único
índice. Java proporciona tres técnicas para crear un array de una dimensión: usar sólo un
inicializador, usar sólo la palabra clave new, y utilizar la palabra clave new con un
inicializador.
Utilizando la palabra clave new se puede utilizar cualquiera de estas dos sintaxis:
type variable_name '[' ']' '=' 'new' type '[' integer_expression ']'
';'
type '[' ']' variable_name '=' 'new' type '[' integer_expression ']'
';'
Truco:
Los desarrolladores Java normalmente sitúan los corchetes cuadrados después del tipo
(int [] test_scores) en vez de después del nombre de la variable (int test_scores
[]) cuando declaran una variable array. Mantener toda la información del tipo en un
único lugar mejora la lectura del código.
El siguiente fragmento de código utiliza sólo la palabra clave new para crear un array
uni-dimensional que almacena datos de un tipo primitivo:
Cuidado:
Cuando se crea un array uni-dimensional basado en un tipo primitivo, el compilador
requiere que aparezca la palabra clave que indica el tipo primitivo en los dos lados del
operador igual-a. De otro modo, el compilador lanzará un error. Por ejemplo, int []
test_scores = new long [20]; es ilegal porque las palabras claves int y long
representan tipos primitivos incompatibles.
Los arrays uni-dimensionales de tipos primitivos almacenan datos que son valores
primitivos. Por el contrario, los arrays uni-dimensiones del tipo referencia almacenan
datos que son referencias a objetos. El siguiente fragmento de código utiliza la palabra
clave new para crear una pareja de arrays uni-dimensionales que almacenan datos
basados en tipo referencia:
Clock [] c1 = new Clock [3]; declara una variable array uni-dimensional, (c1) del
tipo Clock [], asigna memoria para un array uni-dimensional Clock que consta de tres
elementos consecutivos, y asigna la referencia del array Clock a c1. Cada elemento
debe contener una referencia a un objeto Clock (asumiendo que Clock es una clase
concreta) o un objeto creado desde una subclase de Clock y lo inicializa a null.
Utilizar la palabra clave new con un inicializador requiere la utilización de alguna de las
siguientes sintaxis:
type variable_name> '[' ']' '=' 'new' type '[' ']' initializer ';'
type '[' ']' variable_name '=' 'new' type '[' ']' initializer ';'
Nota:
Un array uni-dimensional (o de más dimensiones) creado con la palabra clave new con
un inicializador algunas veces es conocido como un array anónimo.
El siguiente fragmento de código utiliza la palabra clave new con un inicializador para
crear un array uni-dimensional con datos basados en tipos primitivos:
Cuidado:
No especifique una expresión entera entre los corchetes cuadrados del lado derecho de la
igualdad. De lo contrario, el compilador lanzará un error. Por ejemplo, new int [3]
{ 70, 80, 20, 30 } hace que el compilador lance un error porque puede determinar el
número de elementos partiendo del inicializador. Además, la discrepancia está entre el
número 3 que hay en los corchetes y las cuatro entradas que hay en el inicializador.
Clock [] c1 = new Clock [3]; declara una variable de array uni-dimensional (c1)
del tipo Clock [], asigna memoria para un array Clock que consta de un sólo
elemento, crea un objeto Clock y asigna su referencia a este elemento, y asigna la
referencia del array Clock a c1. El código Clock [] c2 = new AlarmClock [3]; se
parece a la declaración anterior, excepto en que crea un array uni-dimensional de un
sólo elemento AlarmClock que inicializa un objeto del tipo AlarmClock.
Después de crear un array uni-dimensional, hay que almacenar y recuperar datos de sus
elementos. Con la siguiente sintaxis se realiza esta tarea:
Clock [] c = ac;
c [0] = new Clock ();
El compilador no dará ningún error porque todas las líneas son legales. Sin embargo,
durante la ejecución, c [0] = new Clock (); resulta en una ArrayStoreException.
Esta excepción ocurre porque podríamos intentar acceder a un miembro específico de
AlarmClock mediante una referencia a un objeto Clock. Por ejemplo, supongamos que
AlarmClock contiene un método public void soundAlarm(), Clock no lo tiene, y el
fragmento de código anterior se ejecuta sin lanzar una ArrayStoreException. Un
intento de ejecutar ac [0].soundAlarm (); bloquea la JVM Máquina Virtual Java
porque estamos intentando ejecutar este método en el contexto de un objeto Clock (que
no incorpora un método soundAlarm()).
Cuidado:
Tenga cuidado cuando acceda a los elementos de un array porque podría recibir una
ArrayIndexOutOfBoundsException o una ArrayStoreException.
Los desarrolladores normalmente escriben código para buscar datos en un array y para
ordenar ese array. Hay tres algoritmos muy comunes que se utilizan para realizar estas
tareas.
Búsqueda Lineal
// LSearchDemo.java
class LSearchDemo {
Found 72
END
// BSearchDemo.java
class BSearchDemo {
72 found
Ordenación de Burbuja
class BSortDemo {
-5
12
15
20
30
72
456
Truco:
Otro algoritmo muy utilizado para arrays uni-dimensionales copia los elementos de un
array fuente en otro array de destino. En vez de escribir su propio código para realizar
esta tarea puede utilizar el método public static void arraycopy(Object src,
int srcindex, Object dst, int dstindex, int length) de la clase
java.lang.System, que es la forma más rápida de realizar la copia.
Un array de dos dimensiones, también conocido como tabla o matriz, donde cada
elemento se asocia con una pareja de índices, es otro array simple. Conceptualizamos un
array bi-dimensional como una cuadrícula rectangular de elementos divididos en filas y
columnas, y utilizamos la notación (fila, columna) para identificar un elemento
específico.
type '[' ']' '[' ']' variable_name '=' '{' [ rowInitializer [ ',' ...
] ] '}' ';'
El siguiente código usa sólo un inicializador para crear un array bi-dimensional que
almacena datos basados en un tipo primitivo:
type '[' ']' '[' ']' variable_name '=' 'new' type '['
integer_expression ']' '[' ']' ';'
En ambas sintaxis:
El siguiente fragmento de código usa sólo la palabra clave new para crear un array bi-
dimensional que almacena datos basados en un tipo primitivo:
temperatures [0] = new double [3]; // Allocate three columns for row 0
temperatures [1] = new double [3]; // Allocate three columns for row 1
El siguiente fragmento de código usa la palabra clave new y un inicializador para crear
un array bi-dimensional que almacena datos basados en un tipo primitivo:
Multiplicar una matriz por otra es una operación común en el trabajo con gráficos, con
datos económicos, o con datos industriales. Los desarrolladores normalmente utilizan el
algoritmo de multiplicación de matrices para completar esa multiplicación. ¿Cómo
funciona ese algoritmo? Dejemos que 'A' represente una matriz con 'm' filas y 'n'
columnas. De forma similar, 'B' representa un matriz con 'p' filas y 'n' columnas.
Multiplicar A por B produce una matriz C obtenida de multiplicar todas las entradas de
A por su correspondencia en B. La siguiente figura ilustra estas operaciones.
Cuidado:
La multiplicación de matrices requiere que el número de columnas de la matriz de la
izquierda (A) sea igual al de la matriz de la derecha (B). Por ejemplo, para multiplicar
una matriz A de cuatro columnas por fila por una matriz B (como en A x B), B debe
contener exactamente cinco filas.
// MatMultDemo.java
class MatMultDemo {
10 30
20 40
5
7
260
380
¿A qué ciudades debería enviar los semitrailers para obtener el máximo ingreso? Para
resolver este problema, primero consideremos la tabla de la imagen anterior como una
matriz de precios de cuatro filas por tres columnas. Luego construimos una matriz de
tres filas por una columna con las cantidades, que aparece abajo:
== ==
| 1250 |
| |
| 400 |
| |
| 250 |
== ==
Ahora que tenemos las dos matrices, simplemente multiplicamos la matriz de precios
por la matriz de cantidades para producir una matriz de ingresos:
== == == ==
| 10.00 8.00 12.00 | == == | 18700.00 | New York
| | | 1250 | | |
| 11.00 8.50 11.55 | | | | 20037.50 | Los Angeles
| |X | 400 | = | |
| 8.75 6.90 10.00 | | | | 16197.50 | Miami
| | | 250 | | |
| 10.50 8.25 11.75 | == == | 19362.50 | Chicago
== == == ==
Enviar los dos semitrailers a Los Angles produce el mayor ingreso. Pero cuando se
consideren la distancia y el consumo de gasoil, quizás New York sea la mejor apuesta
Arrays Desiguales
Suponga que su código fuente contiene la siguiente declaración de matriz: int [][] x
= new int [5][];. Esto declara una matriz de enteros que contiene cinco filas, y
x.length devuelve ese número de filas. Normalmente, completa la creación de la
matriz especificando el mismo número de columnas para cada fila. Por ejemplo,
especificando 10 columnas para cada fila utilizando el siguiente código:
Después de ejecutar este código, usted tendrá una matriz degenerada conocida como un
array desigual. La siguiente imagen ilustra este tipo de arrays:
Los arrays desiguales son estructuras de datos útiles debido a su capacidad de ahorro de
memoria. Por ejemplo, considere una hoja de cálculo con el potencial de 100.000 filas
por 20.000 columnas. Si intentamos utilizar una matriz que contenga toda la hoja de
cálculo, requeriremos una enorme cantidad de memoria. Pero supongamos que la
mayoría de las celdas contienen valores por defecto, como un 0 para valores numéricos
y null para celdas no numéricas. Si utilizamos un array desigual en lugar de una matriz,
almacenaremos sólo las celdas que contienen datos numéricos. (Por supuesto,
necesitamos algún tipo de mecanismo de mapeo que mapee las coordenadas de la hoja
de cálculo [filas, columnas] a las coordenadas del array desigual [filas], [columnas]).
// ArrayIsObject.java
class ArrayIsObject {
ArrayIsObject crea las referencias a los arrays a- y b con la misma precisión y los
mismos contenidos y la misma longitud. Para el array a, a.getClass () devuelve
class [D, donde [D es el nombre de la clase oculta. A pesar de que ambos arrays tienen
los mismos contenidos, a.equals (b) devuelve false porque equals() compara
referencias (no contenidos), y a y b contienen diferentes referencias. La referencia de b
se asigna a c, y b.equals (c) devuelve true porque b y c referencian al mismo array.
c.clone() crea un clon de c, y una referencia de ese array se asigna a d. Para probar
que la referencia d contiene los mismos contenidos que la referencia del array c, el
bucle for itera sobre todos los elementos e imprime su contenido. El bucle lee los
contenidos y el campo de sólo lectura length para determinar sobre cuantos elementos
iterar.
Truco:
En el código fuente, especifique siempre .length (como d.length) en vez de la
longitud real del array. De esta forma, eliminará el riesgo de introducir bugs relacionados
con la longitud, si después decide modificar la longitud del array en su código de
creación.
Listas Enlazadas
Además de los arrays, otra de las estructuras de datos muy utilizada es la lista enlazada.
Esta estructura implica cuatro conceptos: clase auto-referenciada, nodo, campo de
enlace y enlace.
Los cuatro conceptos de arriba nos llevan a la siguiente definición: una lista enlazada
es una secuencia de nodos que se interconectan mediante sus campos de enlace. En
ciencia de la computación se utiliza una notación especial para ilustrar las listas
enlazadas. En la siguiente imagen aparece una variante de esta notación que utilizaré a
lo largo de esta sección:
Aunque se pueden crear muchos tipos de listas enlazadas, las tres variantes más
populares son la lista de enlace simple, la lista doblemente enlazada y la lista enlazada
circular. Exploremos esas variantes, empezando con la lista enlazada.
Lista de Enlace Simple
Una lista de enlace simple es una lista enlazada de nodos, donde cada nodo tiene un
único campo de enlace. Una variable de referencia contiene una referencia al primer
nodo, cada nodo (excepto el último) enlaza con el nodo siguiente, y el enlace del último
nodo contiene null para indicar el final de la lista. Aunque normalmente a la variable
de referencia se la suele llamar top, usted puede elegir el nombre que quiera. La
siguiente figura presenta una lista de enlace simple de tres nodos, donde top referencia
al nodo A, A conecta con B y B conecta con C y C es el nodo final:
Este pseudocódigo declara una clase auto-referenciada llamada Node con un campo no
de enlace llamado name y un campo de enlace llamado next. También declara una
variable de referencia top (del tipo Node) que contiene una referencia al primer Node de
una lista de enlace simple. Como la lista todavía no existe, el valor inicial de top es
NULL. Cada uno de los siguientes cuatro casos asume las declaraciones de Node y top:
En la siguiente imagen se puede ver la lista de enlace simple que emerge del
pseudocódigo anterior:
// SLLInsDemo.java
class SLLInsDemo {
static class Node {
String name;
Node next;
}
Node temp;
Node temp2;
temp2 = top;
temp2.next = temp;
temp2 = top;
temp.next = temp2.next;
temp2.next = temp;
El método static void dump(String msg, Node topNode) itera sobre la lista e
imprime su contenido. Cuando se ejecuta SLLInsDemo, las repetidas llamadas a este
método dan como resultado la siguiente salida, lo que coincide con las imagénes
anteriores:
Case 1 A
Case 2 B A
Case 3 B A C
Case 4 B A D C
Nota:
SLLInsDemo y los ejemplos de pseudocódigo anteriores empleaban un algoritmo de
búsqueda lineal orientado a listas enlazadas para encontrar un Node específico.
Indudablemente usted utilizará este otro algoritmo en sus propios programas:
Búsqueda del últimoNode:
// Assume top references a singly linked list of at least one
Node.
Node temp = top // We use temp and not top. If top were used, we
// couldn't access the singly linked list after
// the search finished because top would refer
// to the final Node.
WHILE temp.next IS NOT NULL
temp = temp.next
END WHILE
// temp now references the last Node.
Búsqueda de un Node específico:
// Assume top references a singly linked list of at least one
Node.
Node temp = top
WHILE temp IS NOT NULL AND temp.name IS NOT "A" // Search for
"A".
temp = temp.next
END WHILE
// temp either references Node A or contains NULL if Node A not
found.
La siguiente imagen presenta las vistas anterior y posterior de una lista donde se
ha borrado el primer nodo. en esta figura, el nodo B desaparece y el nodo A se
convierte en el primer nodo.
La siguiente figura presenta las vistas anterior y posterior de una lista donde se
ha borrado un nodo intermedio. En esa figura el nodo D desaparece.
// SLLDelDemo.java
class SLLDelDemo {
static class Node {
String name;
Node next;
}
top = top.next;
dump ("After first node deletion", top);
// Put back B
temp = top;
temp.next = temp.next.next;
Después de estudiar SLLDelDemo, podría preguntarse qué sucede si asigna null al nodo
referenciado por top: ¿el recolector de basura recogerá toda la lista? Para responder a
esta cuestión, compile y ejecute el código del siguiente listado:
// GCDemo.java
class GCDemo {
static class Node {
String name;
Node next;
top = null;
temp = null;
GCDemo crea la misma lista de cuatro nodos que SLLDelDemo. Después de volcar los
nodos a la salida estándar, GCDemo asigna null a top y a temp. Luego, GCDemo ejecuta
System.gc (); hasta 100 veces. ¿Qué sucede después? Mire la salida (que he
observado en mi plataforma Windows):
La salida revela que todos los nodos de la lista de enlace simple han sido finalizados (y
recolectados). Como resultado, no tiene que preocuparse de poner a null todos los
enlaces de una lista de enlace simple cuando se quiera deshacer de ella. (Podría
necesitar tener que incrementar el número de ejecuciones de System.gc (); si su
salida no incluye los mensajes de finalización.)
Existen muchos algoritmos útiles para listas de enlace simple. Uno de ellos es la
concatenación, que implica que puede añadir una lista de enlace simple al final de otra
lista.
Otro algoritmo útil es la inversión. Este algoritmo invierte los enlaces de una lista de
enlace simple permitiendo atravesar los nodos en dirección opuesta. El siguiente código
extiende la clase anterior para invertir los enlaces de la lista referenciada por top1:
>
// CIDemojava
class CIDemo {
liMaster.last.next = liWorking.top;
dump ("New master list =", liMaster.top);
invert (liMaster);
dump ("Inverted new master list =", liMaster.top);
}
li.last = li.top;
li.last = li.last.next;
}
li.last.next = null;
}
while (p != null) {
r = q;
q = p;
p = p.next;
q.next = r;
}
li.top = q;
}
}
CIDemo declara un DictEntry anidado en la clase de más alto nivel cuyos objetos
contienen palabras y significados. (Para mentener el programa lo más sencillo posible,
he evitado los significados. Usted puede añadirlos si lo desea). CIDemo también declara
ListInfo para seguir las referencias el primero y último DictEntry de una lista de
enlace simple.
El thread principal ejecuta el método public static void main(String [] args)
de CIDemo. Este thread llama dos veces al método static void buildList
(ListInfo li, String [] words) para crear dos listas de enlace simple: una lista
maestra (cuyos nodos se rellenan con palabras del array wordsMaster), y una lista de
trabajo (cuyos nodos se rellenan con palabras del array wordsWorking). Antes de cada
llamada al método buildList (ListInfo li, String [] words), el thread principal
crea y pasa un objeto ListInfo. este objeto devuelve las referencias al primero y último
nodo. (Una llamada a método devuelve directamente un sólo dato). Después de
construir una lista de enlace simple, el thread principal llama a static void dump
(String msg, DictEntry topEntry) para volcar un mensaje y las palabras de los
nodos de una lista en el dispositivo de salida estándar.
Se podría estar preguntando sobre la necesidad del campo last de ListInfo. Este
campo sirve a un doble propósito: primero, simplifica la creación de cada lista, donde se
añaden los nodos. Segundo, este campo simplifica la concatenación, que se queda sólo
en la ejecución de la siguiente línea de código: liMaster.last.next =
liWorking.top;. Una vez que se completa la concatenación, y el thread principal
vuelva los resultados de la lista maestra en la salida estándar, el thread llama al método
static void invert (ListInfo li) para invertir la lista maestra y luego muestra la
lista maestra invertida por la salida estándar.
Las listas de enlace simple restringen el movimiento por lo nodos a una sola dirección:
no puede atravesar una lista de enlace simple en dirección opuesta a menos que primero
utilice el algoritmo de inversión para invertir los enlaces de los nodos, lo que lleva
tiempo. Después de atravesarlos en dirección opuesta, probablemente necesitará repetir
la inversión para restaurar el orden original, lo que lleva aún más tiempo. Un segundo
problema implica el borrado de nodos: no puede borrar un nodo arbitrario sin acceder al
predecesor del nodo. Estos problemas desaparecen cuando se utiliza una lista
doblemente enlazada.
Una lista doblemente enlazada es una lista enlazada de nodos, donde cada nodo tiene un
par de campos de enlace. Un campo de enlace permite atravesar la lista hacia adelante,
mientras que el otro permite atravesar la lista hacia atrás. Para la dirección hacia
adelante, una variable de referencia contiene una referencia al primer nodo. Cada nodo
se enlaza con el siguiente mediante el campo de enlace next, excepto el último nodo,
cuyo campo de enlace next contiene null para indicar el final de la lista (en dirección
hacia adelante). De forma similar, para la dirección contraria, una variable de referencia
contiene una referencia al último nodo de la dirección normal (hacia adelante), lo que se
interpreta como el primer nodo. Cada nodo se enlaza con el anterior mediante el campo
de enlace previous, y el primer nodo de la dirección hacia adelante, contiene null en
su campo previous para indicar el fin de la lista. La siguiente figura representa una
lista doblemente enlazada de tres nodos, donde topForward referencia el primer nodo
en la dirección hacia adelante, y topBackward referencia el primero nodo la dirección
inversa.
Truco:
Piense en una lista doblemente enlazada como una pareja de listas de enlace simple que
interconectan los mismos nodos.
El siguiente listado muestra la inserción de nodos para crear la lista de la figura anterior,
el borrado de nodos ya que elimina el nodo B de la lista, y el movimiento por la lista en
ambas direcciones:
// DLLDemo.java
class DLLDemo {
static class Node {
String name;
Node next;
Node prev;
}
topForward.next = temp;
temp.next = topBackward;
topBackward.next = null;
topBackward.prev = temp;
temp.prev = topForward;
topForward.prev = null;
temp = topForward;
while (temp != null){
System.out.print (temp.name);
temp = temp.next;
}
System.out.println ();
temp = topBackward;
while (temp != null){
System.out.print (temp.name);
temp = temp.prev;
}
System.out.println ();
// Reference node B
temp = topForward.next;
// Delete node B
temp.prev.next = temp.next;
temp.next.prev = temp.prev;
temp = topForward;
while (temp != null){
System.out.print (temp.name);
temp = temp.next;
}
System.out.println ();
temp = topBackward;
while (temp != null){
System.out.print (temp.name);
temp = temp.prev;
}
System.out.println ();
}
}
Algoritmo de Inserción-Ordenada
Algunas veces querrá crear una lista doblemente enlazada que organice el orden de sus
nodos basándose en un campo no de enlace. Atravesar la lista doblemente enlazada en
una dirección presenta esos nodos en orden ascendente, y atravsarla en en dirección
contraria los presenta ordenados descedentemente. El algoritmo de ordenación de
burbuja es inapropiado en este caso porque requiere índices de array. Por el contrario,
inserción-ordenada construye una lista de enlace simple o una lista doblemente enlzada
ordenadas por un campo no de enlace para identificar el punto de inserción de cada
nuevo nodo. El siguiente litado demuestra el algoritmo de inserción-ordenada:
// InsSortDemo.java
class InsSortDemo {
// Note: To keep Employee simple, I've omitted various constructor
and
// nonconstructor methods. In practice, such methods would be
present.
Employee next;
Employee prev;
}
if (temp == null) {
}
else{
if (temp.prev == null) {
e.next = topForward; // Insert new Employee node at
topForward = e; // head of forward singly
linked
// list.
temp.prev = e; // Update backward singly
linked
// list as well.
}
else {
e.next = temp.prev.next; // Insert new Employee node
temp.prev.next = e; // after last Employee node
// whose empno is smaller
in
// forward singly linked
list.
e.prev = temp.prev; // Update backward
temp.prev = e; //singly linked list as
well.
}
}
}
System.out.println ();
temp = topBackward;
while (temp != null) {
System.out.println ("[" + temp.empno + ", " + temp.name + "]
");
temp = temp.prev;
}
System.out.println ();
}
}
Ascending order:
[100, George]
[234, Alice]
[325, Joan]
[567, Jack]
[654, Sam]
[687, April]
[987, Brian]
Descending order:
[987, Brian]
[687, April]
[654, Sam]
[567, Jack]
[325, Joan]
[234, Alice]
[100, George]
El campo de enlace del último nodo de una lista de enlace simple contiene un enlace
nulo, ocurre lo mismo en los campos de enlace del primer y último elemento en ambas
direcciones en las listas doblemente enlazadas. Supongamos que en vez de esto los
últimos nodos contiene un enlace a los primeros nodos. En esta situacion, usted
terminará con una lista de enlace circular, como se ve en la siguiente figura:
Las listas de enlace circular se utilizan con frecuencia en procesamiento repetitivo de
nodos en un orden específico. Dichos nodos podrían representar conexiones de servidor,
procesadores esperando una sección crítica, etc. Esta estructura de datos también sirve
como base para una variante de una estructura de datos más compleja: la cola (que
veremos más adeltante).
Las listas enlazadas tienen las siguiente ventajas sobre los arrays:
En contraste, los arrays ofrecen las siguiente ventajas sobre las listas enlazadas:
Los elementos de los arrays ocupan menos memoria que los nodos porque no
requieren campos de enlace.
Los arrays ofrecen un aceso más rápido a los datos, medante índices basados en
enteros.
Las listas enlazadas son más apropiadas cuando se trabaja con datos dinámicos. En otras
palabras, inserciones y borrados con frecuencia. Por el contrario, los arrays son más
apropiados cuando los datos son estáticos (las inserciones y borrados son raras). De
todas formas, no olvide que si se queda sin espacio cuando añade ítems a un array, debe
crear un array más grande, copiar los datos del array original el nuevo array mayor y
elimiar el original. Esto cuesta tiempo, lo que afecta especialmente al rendimiento si se
hace repetidamente.
Mezclando una lista de enlace simple con un array uni-dimensional para acceder a los
nodos mediante los índices del array no se consigue nada. Gastará más memoria, porque
necesitará los elementos del array más los nodos, y tiempo, porque necesitará mover los
ítems del array siempre que inserte o borre un nodo. Sin embargo, si es posible integrar
el array con una lista enlazada para crear una estructura de datos útil (por ejemplo, las
tablas hash).
Pilas y Colas
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.
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:
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.
// Stack.java
package com.javajeff.cds;
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.
// ArrayStack.java
package com.javajeff.cds;
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;
El siguiente listado presenta una implementación de Stack utilizando una lista de enlace
simple:
// LinkedListStack.java
package com.javajeff.cds;
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 ());
}
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 ();
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"
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.
// Queue.java
package com.javajeff.cds;
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;
// FullQueueException.java
package com.javajeff.cds;
// EmptyQueueException.java
package com.javajeff.cds;
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;
// 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
}
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 ();
try {
q.remove ();
}
catch (EmptyQueueException e) {
System.out.println ("One remove too many");
}
System.out.println ();
}
}
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
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
Un árbol es un grupo finito de nodos, donde uno de esos nodos sirve como raíz y el
resto de los nodos se organizan debajo de la raíz de una forma jerárquica. Un nodo que
referencia un nodo debajo suyo es un nodo padre. De forma similar, un nodo
referenciado por un nodo encima de él, es un nodo hijo. Los nodos sin hijos, son nodos
hoja. Un nodo podría ser un padre e hijo, o un nodo hijo y un nodo hoja.
Un nodo padre podría referenciar tantos hijos como sea necesario. En muchas
situaciones, los nodos padre sólo referencian un máximo de dos nodos hijos. Los
árboles basados en dichos nodos son conocidos como arboles binarios. La siguiente
figura representa un árbol binario que almacena siete palabras en orden alfabético.
Insertar nodos, borrar nodos, y atravesar los nodos en árboles binarios o de otros tipos
se realiza mediante la recursión (vea el capítulo siguiente). Por brevedad, no entraremos
en los algoritmos recursisvos de inserción, borrado y movimiento por los nodos. En su
lugar, presentaré el código fuente de una aplicación de contaje de palabras para
demostrar la inserción y el movimiento por los nodos. Este código utiliza inserción de
nodos para crear un árbol binario, donde cada nodo contiene una palabra y un contador
de ocurrencias de esa palabra, y muestra estas palabras y contadores en orden alfabético
mediante una variante del algoritmo de movimiento por árboles move-left-examine-
node-move-right:
// WC.java
import java.io.*;
class TreeNode {
String word; // Word being stored.
int count = 1; // Count of words seen in text.
TreeNode left; // Left subtree reference.
TreeNode right; // Right subtree reference.
if (right == null)
right = new TreeNode (word);
else
right.insert (word);
}
else
this.count++;
}
}
class WC {
public static void main (String [] args) throws IOException {
int ch;
Para entender la recursión, consideremos un método que suma todos los enteros desde 1
hasta algún límite superior:
Este método es correcto porque consigue el objetivo. Después de crear una variable
local total e inicializarla a cero, el método usa un bucle for para sumar repetidamente
enteros a total desde 1 hasta el valor del parámetro limit. Cuando la suma se
completa, sum(int limit) devuelve el total, mediante return total;, a su llamador.
La recursión hace posible realizar está suma haciendo que sum(int limit) se llame
repetidamente a sí mismo, como demuestra el siguiente fragmento de código:
Cuidado:
Asegurese siempre que un método recursivo tiene una condición de parada (como if
(limit == 1) return 1;). Por el contrario, la recursión continuará hasta que se
sobrecargue la pila de llamadas a métodos.