Estructuras de datos en Java
Una estructura de datos es básicamente una forma de almacenar y organizar los datos en una memoria, ya sea en la RAM o en el disco duro. Cada estructura de datos tiene su propio conjunto de reglas y restricciones que determinan cómo los datos pueden ser almacenados y accedidos.
En Java, existen varias clases predefinidas que se pueden
utilizar para implementar diferentes estructuras de datos. Algunas de las
estructuras de datos más comunes incluyen arrays, listas, pilas, colas, árboles
y grafos.
Cada estructura de datos tiene sus propias ventajas y
desventajas en términos de rendimiento y facilidad de uso, por lo que es
importante entender cuál estructura de datos es la más adecuada para una tarea
específica.
En este post presentaré algunas de las estructuras de datos
más comunes en Java, y cómo podemos utilizarlas para mejorar nuestro código y
hacerlo más eficiente. Cubriré temas como el rendimiento de las diferentes
estructuras de datos, como implementarlas en Java, y proporcionaré ejemplos de
código para ayudar a ilustrar cómo se utilizan.
¡Así que vamos a empezar! A continuación, te presento el
orden que seguiremos para las principales estructuras de datos:
1.
ArrayList y LinkedList
1.1.
¿Qué son?
1.2.
Comparación de rendimiento
1.3.
Ejemplo con gráficos
1.4.
Ejemplo de código
2.
Stack y Queue
2.1.
¿Qué es Queue?
2.2.
¿Qué es Stack?
2.3.
Ejemplo de código comparando Queue y Stack
3. Árboles de búsqueda binarios
3.1. ¿Qué son?
3.2. Implementación en Java
3.2. Ventajas y desventajas
3.
HashMap y TreeMap
3.1.
¿Qué son?
3.2.
Comparación de rendimiento
3.3.
Ejemplo de código
ArrayList y LinkedList
¿Qué son?
ArrayList y
LinkedList son estructuras de datos de tipo lista. Una lista es una colección
ordenada de elementos en la que cada elemento se identifica mediante un índice
numérico. Es decir, podemos acceder a cualquier elemento de la lista a través
de su índice.
Un ArrayList es una
lista dinámica que puede aumentar o disminuir su tamaño automáticamente según
sea necesario. Se implementa utilizando un array subyacente que se reemplaza
por uno nuevo cuando el tamaño de la lista supera su capacidad. Esto significa
que agregar o eliminar elementos de un ArrayList puede ser más rápido que en
otras estructuras de datos, como un LinkedList.
Por otro lado, un
LinkedList es una lista enlazada que se implementa mediante una serie de nodos
enlazados, donde cada nodo contiene el valor del elemento y una referencia al
siguiente nodo en la lista. Agregar o eliminar elementos de un LinkedList puede
ser más lento que un ArrayList, pero la inserción o eliminación de elementos en
el medio de la lista es mucho más eficiente.
Comparación y rendimiento
En términos de rendimiento, ambos ArrayList y LinkedList
tienen sus propias ventajas y desventajas.
En general, si necesitas una lista en la que agregas o
eliminas elementos al final de la lista con mucha frecuencia, entonces un ArrayList
puede ser más eficiente si se conoce la cantidad de elementos final que tendrá
la lista. También es más eficiente emplear un ArrayList, en lugar de un
LinkedList, si se desea agregar elementos en una posición intermedia siempre y
cuando no se exceda el tamaño del ArrayList, pese a que se deban organizar los
elementos nuevamente. Esto se debe a que se conoce la posición en memoria de
cada elemento y se llega directamente al mismo cuando se necesita. En cambio,
en un LinkedList, se deben iterar la lista hasta llegar al elemento buscado y
en algunos casos es más costoso y en otros no, según la cantidad de elementos
que posea la lista.
Por otro lado, si necesitas agregar o eliminar elementos en
el medio de la lista con frecuencia, entonces un LinkedList puede ser más
eficiente. Agregar o eliminar elementos en el medio de una lista es mucho más
lento en un ArrayList, ya que se necesita desplazar todos los elementos
posteriores hacia la derecha o izquierda para crear espacio para un nuevo
elemento. En un LinkedList, solo se necesita actualizar las referencias de los
nodos adyacentes. Cabe resaltar, que la eficiencia depende del caso, pero a grandes
rasgos lo anterior se cumple.
Una forma adicional de comprender la diferencia entre el uso
de ArrayList y LinkedList es a través de la complejidad de los algoritmos, la
cual se expresa mediante la notación “O(n)”. Sin embargo, este tema se abordará
en profundidad en un futuro post. Para aclarar cualquier duda que pueda surgir
en el lector, debido a que al principio puede resultar complicado entender el
caso de uso de cada lista, se presentará un ejemplo gráfico en el siguiente
punto.
Ejemplo con gráficos
Supongamos que instanciamos un ArrayList con una capacidad inicial
de 8 elementos. Después de agregar los elementos necesarios para llenar la
lista, se desea agregar un noveno elemento. En este caso, se debe crear automáticamente
un nuevo ArrayList con una longitud de 12 elementos (1.5 veces la anterior).
Luego, copiar los 8 elementos anteriores en el nuevo ArrayList y agregar sin
problemas el noveno elemento. Claro está, que este proceso sucede en tiempo de
ejecución y no podemos observarlo directamente, pero es posible darse cuenta de
esto por otro método que no se verá en este post.
Puedes imaginar, el coste de recursos si se agregan
elementos continuamente en la posición final del ArrayList, una vez llena la
lista. Constantemente se crean nuevas listas, mayores en tamaño a las
anteriores, en las que se copian los elementos anteriores y se agregan nuevos.
Para solucionar esto, se puede calcular de manera específica la cantidad de
elementos que tendrá el ArrayList y no superarlo. En algunos casos no puede
hacerse esto, por lo que, es necesario usar otras estructuras de datos.
Si bien es cierto, si sucediera lo anterior en el proceso de
desarrollo y despliegue de un proyecto grande esto no sería un problema porque
se cuenta con servidores equipados con la cantidad necesaria de memoria RAM y espacio
de almacenamiento sólido. Pero, para los fines del post, se tomarán los
ejemplos anteriores como principales para mostrar las diferencias en cuanto a
rendimiento.
Para el caso de la estructura LinkedList, tomemos como ejemplo el caso anterior. Se instancia un LinkedList y se agregan los mismos 7 elementos. El espacio que ocupa cada elemento en la lista se le denomina nodo y se agregan dos referencias para cada uno: referencia para el nodo anterior y una para el nodo posterior.
Debido a esto, es más eficiente usar un LinkedList para agregar
un elemento al final y al inicio de esta lista, ya que para la primera
situación se ubica el nodo con la referencia al nodo siguiente como nula y se
agregan las referencias necesarias. Para la segunda situación, se ubica el nodo
que tenga la referencia al nodo anterior como nula y se agregan las referencias
necesarias. Esto también se aplica al insertar o eliminar elementos en
posiciones arbitrarias de la lista. Sin embargo, la lista se debe iterar desde
el inicio o el final hasta encontrar la posición o la referencia buscada.
De lo explicado anteriormente, puedes suponer los casos más óptimos
para cada estructura de datos. Sin embargo, es mejor probar lo explicado con un
ejemplo en código.
Ejemplo de código
A continuación, se muestra un ejemplo de cómo utilizar
ArrayList y LinkedList en Java. Si has seguido los posts anteriores o ya tienes
conocimiento del lenguaje, no tendrás inconvenientes al seguir a partir de
aquí. De lo contrario, te recomiendo que revises los post de POO (Programación
Orientada a Objetos) y luego vuelvas.
Primero, debemos crear un nuevo proyecto llamado “Estructuras
de datos” y una clase en la carpeta “src” llamada “App”. Es importante tener en
cuenta que la clase debe incluir el método “main()” para el punto de inicio de
la ejecución. Para hacer esto, debemos marcar la casilla correspondiente, como
se muestra en una de las imágenes siguientes.
Ahora instanciaremos la clase ArrayList y agregaremos un
bucle “for” de la siguiente manera:
Ahora, analizaremos el código por cada línea como sigue:
-
· En la línea 8 se instancia la estructura ArrayList, con nombre “lista” y dentro del primer operador diamante “<>” se coloca el tipo de dato que guardará en la lista. Es importante mencionar, que no se ha pasado un valor al constructor del ArrayList, por lo que la lista se creará con una capacidad de 10. Si deseas variar esto, agrega la capacidad inicial, por ejemplo 20” en el paréntesis: ArrayList<>(20).
Otro aspecto a resaltar en esta línea, es que el segundo operador diamante se ha dejado en blanco, esto no afecta al proceso de instanciación, ya que, su función es simplificar la instancia de clases genéricas, algo que se verá en otros posts.
-
En la línea 10 se inicia el bucle “for”. Si no has estudiado el funcionamiento de los bucles aún ten en cuenta lo siguiente:
- La palabra “int” es el tipo de dato utilizado para declarar la variable “i”, cuyo valor inicial es 0. Esto se escribe como “int i = 0”. Luego, se utiliza el símbolo “;” para separar la siguiente instrucción, que establece la condición para el bucle “for”, en este caso “i < 17”. Esta condición indica que el bucle se ejecutará siempre y cuando el valor de “i” sea menor que 17. Finalmente, se utiliza nuevamente “;” para separar la instrucción: “i++”, que indica que el valor de “i” se incrementará en 1 después de cada iteración del bucle. En otras palabras, el valor de “i” aumentará en 1 cada vez que se ejecute el código dentro de las llaves del bucle.
- En resumen, el bucle “for” declarado en este ejemplo tiene una variable “i” con valor inicial de “0”, y por cada ejecución del código dentro de las llaves del bucle, la variable “i” aumentará su valor en 1 debido a la instrucción “i++”. El bucle quedará sin efecto, una vez que la condición “i < 17” sea falsa.
En la línea 11 se declara la variable “start” como un tipo de dato “long” y se le asigna el valor devuelto por la llamada “System. nanoTime()”. La clase “System” se ha utilizado previamente en posts anteriores para imprimir en consola, pero esta clase también cuenta con otros métodos útiles, como “nanoTime()”, que devuelve el tiempo de ejecución en nanosegundos hasta el momento de su llamada. En este ejemplo, el tiempo devuelto se asigna a la variable “start”.
-
Hasta este punto, la lista declarada en la línea 8 no tiene ningún valor. Por lo tanto, se llama al método “add” que tienen las listas, y se le pasa como parámetro el valor que se desea guardar en la lista. Para nuestro ejemplo, el valor es la palabra “Hola”, seguida de un espacio y un símbolo “+” para concatenar el valor de la variable “i”. No te preocupes por mezclar un texto con un número entero; por defecto, el número entero se convertirá a tipo de dato de texto y se imprimirá sin problemas. Esto ocurre cada vez que se concatena un texto con un número entero utilizando el signo “+”. Sin embargo, debes tener cuidado en otros lenguajes de programación, como javascript, donde “+ +” no es lo mismo que “+”. A primera vista, la concatenación de un espacio entre dos símbolos de suma (“+”) puede parecer sin sentido, pero el conocer su función puede sumarte puntos en una entrevista técnica.
-
En la línea 13 se declara una nueva variable de tipo “long” llamada “end”, a la cual se le asigna el resultado de la ejecución de la instrucción “System.nanoTime()”, la cual devuelve el tiempo actual en nanosegundos. A este valor se le resta el valor de la variable “start”, que contiene el tiempo en nanosegundos de la ejecución hasta la línea 11.
En la línea 12 se agrega un nuevo elemento al ArrayList declarado anteriormente, lo que puede tomar cierto tiempo de ejecución dependiendo del tamaño de la lista y la complejidad del elemento que se está agregando
En resumen, la variable “end” almacena el tiempo en nanosegundos que tardó la ejecución del código desde la línea 11 hasta la línea 13, es decir, el tiempo que tardó en agregar el nuevo elemento a la lista. En la línea 14, se imprime el valor de la variable “end” por medio de la clase “System”. De esta forma tendremos en consola el tiempo que tarda en agregarse cada elemento en el ArrayList.
En la línea 15 se cierra el cuerpo del bucle “for” con una llave “}”.
En la línea 17 se imprime la lista completa por medio de la clase “System”.
Luego se ejecuta el código y se analizan los resultados en consola.
En este ejemplo se puede observar que el elemento 11 tarda
más en agregarse a la lista que el elemento 10, y que el elemento 16 tarda más que
el elemento 17. Esto también puede ocurrir con algunos elementos intermedios,
como el elemento 13, debido a otros procesos en el equipo o en el código.
Para entender mejor este comportamiento, podemos crear un
objeto y agregarlo como elemento a la lista del ejemplo anterior, realizando las
modificaciones necesarias. Este proceso se detalla en la imagen siguiente.
Ahora cambiemos la estructura de datos a LinkedList para el
caso anterior y ejecutemos el código.
Como análisis y aclaración final de este punto, es
importante destacar que en la consola mostrada en la imagen anterior se puede
observar que los tiempos que tardó en agregarse cada elemento son considerablemente
menores en comparación con el uso de la estructura de datos ArrayList.
No obstante, es necesario tener en cuenta que,
tal como se mencionó al inicio, una estructura de datos puede ser más eficiente
que otra dependiendo del uso. En este sentido, como tarea, se sugiere modificar
el código y probar distintas funcionalidades, como buscar un elemento en un
punto aleatorio de la lista, para determinar cuál es la estructura de datos más
eficiente para esta tarea en particular. Además, se puede hacer uso de cada
método de la estructura de datos instanciada, llamándolo y probando su
funcionalidad. Por ejemplo, la lista instanciada con la clase LinkedList tiene los
métodos “getFirst()” y “getLast()”, para obtener el primer elemento y último
elemento respectivamente.
Stack y Queue
¿Qué es Queue?
La expresión “Queue” es una clase de Java que hace referencia
a “Cola”. Una cola es una estructura de datos lineal que se utiliza para
almacenar elementos en orden de llegada, y que permite acceder a ellos en el
mismo orden en el que fueron agregados. La operación principal de una cola es ‘enqueue()’,
que agrega un elemento al final de la cola, y “dequeue()”, que elimina el primer
elemento de la cola y devuelve su valor. En otras palabras, esta estructura de
datos se usa como FiFO ( First In First Out) o, parafraseando la frase en español,
el primero en entrar es el primero en salir.
En Java, se puede implementar una cola utilizando la interfaz “Queue” que proporciona la biblioteca estándar de Java. Algunas de las implementaciones de “Queue” disponibles son:
- LinkedList: Implementación mencionada anteriormente de una lista doble que también implementa la interfaz “Deque”.
- ArrayDeque: Implementación de una cola doble-ended
utilizando un array (lista en inglés).
- PriorityQueue: Implementación de una cola de prioridad, donde los elementos se ordenan según un comparador o su orden natural.
Veremos una por una para comprender mejor el uso de cada
implementación. En primer lugar, la clase LinkedList implementa una interfaz llamada “Deque”, que a su vez implementa “Queue”.
Si posicionas el cursor sobre LinkedList y, con la tecla “Control”
presionada, haces clic abrirás la clase que has seleccionado. Ahí podrás ver que
la clase LinkedList implementa la interfaz “Deque”. Si repites este proceso
para abrir la interfaz Deque, verás que se abrirá otra interfaz llamada “Queue”.
Puede parecer confuso si no has estudiado antes el uso de interfaces y herencia.
Sin embargo, ten presente que con este proceso solo verificamos la relación que
hay entre LinkedList y Queue.
En las siguientes dos imágenes verás la clase LinkedList y las dos interfaces que he comentado anteriormente.
En la línea 33 del siguiente código se muestra como instanciar
la clase “LinkedList” y usarla como cola recibiendo el resultado como tipo de
dato Queue. Si se desea definir el tipo de dato que guardará nuestra lista se
usa el operador diamante y se agrega el tipo de dato. Para nuestro caso, los
elementos que recibe la lista en la línea 35 son de tipo “DtoSaludo”, la clase
creada en ejemplos anteriores.
Si luego de agregar varios elementos mediante el método “add()”
que posee la lista, eliminamos un elemento con el método “poll()” verás
que se eliminará el primer elemento que se agregó a la lista en un inicio. Esto
demuestra su funcionalidad FIFO.
La restricción que tiene el código anterior es que la lista2 y lista3 son de tipo Queue. Esto quiere decir, que no se podrán usar métodos como “getFirst” o “getLast” en cada lista, lo que es correcto por tener su funcionalidad principal de: el primer elemento que entra es el que sale. Sin embargo, se puede solucionar esto obteniendo la lista directamente como tipo Deque, con el costo de tener la funcionalidad de FIFO y LIFO al mismo tiempo.
Es importante mencionar que la interfaz Deque (Double-ended
queue) se usa como FIFO para las colas y además como LIFO (Last In First Out),
o parafraseando en español, el último en entrar es el primero en salir. Las
pilas, algo que se verá más adelante, tienen la función de LIFO.
La clase ArrayDeque se puede usar tanto como una cola o una
pila, por lo que se detallará más adelante y por ahora veremos las características
de PriorityQueue. Esta estructura de datos permite que los elementos sean
procesados de acuerdo a un orden específico, pese a tener la funcionalidad o
algoritmo FIFO.
Cuando instanciamos la clase PriorityQueue, los elementos
que agreguemos se ordenarán por medio del “Orden Natural”. Esto quiere decir,
que tendrán un orden ascendente en números y de igual forma para textos. Sin
embargo, podemos agregar en el constructor una forma de comparar los elementos y
ordenarlos en consecuencia.
Cabe resaltar que, el ordenamiento no es visible. El orden de los elementos puede ser percibido cuando se elimina uno o varios elementos. Bajo esa circunstancia, los elementos se eliminarán según el orden establecido. Sin embargo, al imprimir en consola la lista, se podrán observar los elementos en el orden en que se agregaron u otro orden.
Para comprender esto, analicemos el siguiente ejemplo:
En la imagen se puede observar que los elementos de la lista
impresa en la consola tienen un orden diferente al que fueron agregados. Sin
embargo, después de eliminar un elemento con el método “poll()” e imprimir la
lista nuevamente, se observa que el elemento eliminado es el menor. Se repite
el proceso de eliminación de elementos por tres veces más y se puede observar
que los elementos se eliminan de acuerdo a un orden específico. Este es el beneficio
del PriorityQueue.
El uso de esta estructura de datos trae consigo un método muy
útil que permite obtener el elemento siguiente en la lista según el orden. En nuestro
ejemplo, al eliminar por primera vez un elemento, se eliminó el número 1. Si usamos
el método “peek()” en este punto, se devolvería el número 2, ya que es el que
sigue según el orden natural de la lista, no el visto al imprimir la lista.
Como último aporte a las estructuras de datos revisadas
hasta el momento, tanto el método “poll()”, “pop()” y “peek()” devuelven
valores. Es decir podemos almacenar los valores en una variable y usarlos luego.
La expresión “Stack” es una clase de Java que hace referencia
a “Pila”. Una pila es una estructura de datos lineal que se utiliza para almacenar
elementos en un orden específico (el último elemento agregado es el primero en ser eliminado), y que
permite acceder a ellos en ese mismo orden. Las operaciones operaciones de la
pila son “push()”, que agrega un elemento a la parte superior de la pila, y “pop()”,
que elimina y devuelve el elemento en la parte superior de la pila, o en otras palabras,
el último elemento agregado. Por lo tanto, una pila tiene funcionalidad LIFO, el
último elemento agregado es el primero en ser eliminado.
En Java, se puede utilizar una pila utilizando la clase Stack que proporciona la biblioteca estándar de Java, aunque, pese a no ser muy usadas, se puede utilizar una lista enlazada o un array.
Ejemplo de código comparando Stack y Queue
En el ejemplo presentado, se puede apreciar que se agregaron
los mismos elementos a la lista 4 y 5 utilizando el método “add()” y “push()”
respectivamente, en un bucle “for”. Luego, se le eliminó un elemento de cada
lista mediante el método correspondiente. Como resultado, se observa que la
lista 4, al ser una cola, eliminó el primer elemento con valor “Número 1”,
mientras que la lista 5, al ser una pila, eliminó el último elemento con valor “Número
4”.
Es importante mencionar que, la clase Stack en Java hereda
sus métodos de la clase Vector, lo que significa que tiene acceso tanto al
método “add()” como al método “push()”. Ambos métodos tienen el mismo propósito:
agregar un elemento a la lista.
El método “push()” es específico de la clase Stack y está
diseñado para agregar elementos en la parte superior de la pila, como una
operación de apilamiento típica. Por otro lado, el método “add()” es un método
genérico de la clase Vector que agrega un elemento en cualquier posición de la
lista. En el caso de Stack, el método “add()” también agrega elementos a la
parte superior de la pila, y se comporta de manera equivalente al método “push()”.
En resumen, aunque la clase Stack tiene acceso a ambos métodos, la recomendación es utilizar “push()” para agregar elementos a la pila, ya que es un método específico de esta clase y hace que el código sea más legible y fácil de entender. Además, “push()” sigue el comportamiento esperado de una estructura de datos de tipo pila.
Árboles de búsqueda binarios
¿Qué son?
Los árboles de búsqueda binarios son una estructura de datos
utilizada para almacenar elementos de forma ordenada y permitir la búsqueda
eficiente de elementos. Cada nodo del árbol tiene un valor y dos hijos, uno que
contiene valores menores y otro que contiene valores mayores.
No es lo mismo un árbol
binario que un árbol de búsqueda binario. En la siguiente imagen se destaca la diferencia
entre ambos.
Implementación en Java
En Java, se puede implementar un árbol de búsqueda binario utilizando
la clase “TreeMap”. Esta clase implementa la interfaz “NavigableMap”. Lo que
significa que se puede acceder a los elementos del mapa de forma ordenada
utilizando los métodos “higherEntry()”, “lowerEntry()”, “ceilingEntry()”, “floorEntry()”,
“navigableKeySet()” y “hedMap()”, entre otros no muy utilizados. Además, la
clase “TreeMap” también proporciona otros métodos para insertar y eliminar
elementos, así como para realizar búsquedas.
Ventajas y desventajas
Las ventajas de utilizar árboles de búsqueda binarios incluyen:
- La búsqueda de elementos en el árbol tiene una complejidad temporal de O(log n), lo que hace que sea muy eficiente incluso para árboles grandes.
- Los elementos se almacenan de forma ordenada, lo que permite realizar búsquedas por rangos utilizando los métodos “higherEntry()”, “lowerEntry()”, “ceilingEntry()” y “floorEntry()”.
Las desventajas de utilizar árboles de búsqueda binarios incluyen:
- La inserción y eliminación de elementos pueden ser costosas si se debe mantener el árbol balanceado.
- Si el árbol no está balanceado, la búsqueda de
elementos puede degradarse a una complejidad temporal de O(n). Es decir, toma
más tiempo de cómputo.
HashMap y TreeMap
¿Qué son?
HashMap y TreeMap son dos estructuras de datos utilizadas
para almacenar y acceder a elementos de manera eficiente en base a una clave.
Ambas implementan la interfaz Map en Java y proporcionan métodos para insertar,
buscar, actualizar y eliminar elementos en la colección.
HashMap utiliza una tabla “hash” para almacenar los
elementos, lo que proporciona un acceso muy rápido a los elementos en promedio.
Cada elemento se almacena en una posición calculada a partir de su clave, lo
que permite una búsqueda directa y eficiente de los elementos en la tabla hash.
Por otro lado, TreeMap utiliza un árbol binario de búsqueda
para almacenar los elementos, lo que proporciona un acceso rápido y ordenado a
los elementos en la colección. Cada elemento se inserta en el árbol según su
clave, lo que garantiza que los elementos estén ordenados según su clave.
Comparación de rendimiento
La principal diferencia de rendimiento entre HashMap y
TreeMap radica en el tiempo de búsqueda de elementos en la colección.
En HashMap, el tiempo de búsqueda es constante en promedio
(O(1)), ya que utiliza una tabla hash para almacenar los elementos y la
búsqueda se realiza directamente en la posición calculada a partir de la clave
del elemento. Sin embargo, en el peor de los casos, el tiempo de búsqueda puede
ser lineal (O(n)), lo que ocurre cuando se produce una colisión de claves en la
tabla hash y varios elementos se almacenan en la misma posición.
En cambio, en TreeMap, el tiempo de búsqueda es logarítmico
(O(log n)), ya que utiliza un árbol binario de búsqueda para almacenar los elementos
y la búsqueda se realiza siguiendo el camino del árbol en función de la clave
del elemento. Este tiempo de búsqueda garantiza que los elementos estén
ordenados en la colección.
En resumen, si se necesita un acceso rápido a los elementos
en la colección y no se requiere un orden específico, se debe utilizar HashMap.
Por otro lado, si se necesita un acceso rápido y ordenado a los elementos, se
debe utilizar TreeMap.
Ejemplo de código
A continuación, se presenta un ejemplo de código que ilustra
cómo utilizar HashMap y TreeMap en Java:
En la imagen anterior se pueden observar los elementos que
fueron agregados a la lista con las claves sin orden ascendente. Sin embargo,
al imprimir los valores en consola, los elementos están ordenados según su clave.
Esto puede confundir al lector, pero sirve de ejercicio para entender que, por
más que se impriman las claves en este orden, no siempre sucede así al usar la
clase HashMap, el orden de los elementos es aleatorio. Más adelante, se verá un
caso más específico mediante el uso de un bucle “for”.
Como se comentó anteriormente, se puede acceder a los
elementos de manera eficiente usando el método “get()” y pasando como parámetro
la clave del elemento requerido. Además, pueden ser usados métodos que podrían
validar ciertas situaciones como “containsKey()”, que devuelve un valor de tipo
booleano si existe o no el elemento. En la siguiente imagen se utilizan algunos
métodos disponibles.
Para el caso de TreeMap,
podemos cambiar la instancia al ejemplo anterior y observar que los elementos
también se imprimen ordenados según su clave. Sin embargo, en este caso los
elementos realmente serán ordenados para cualquier situación.
Para comprender la diferencia entre HashMap y TreeMap,
veamos otro ejemplo desarrollado en código en las siguientes dos imágenes donde
tanto la clave como el valor de cada elemento tienen el mismo tipo de dato “String”.
Para recorrer la lista, se utiliza un bucle “for”. Es posible que esta forma de
usar el “for” no la hayas estudiado aún, pero ten presente que se imprimirá
elemento por elemento según el orden que se han guardado en la lista. No te
preocupes por esta forma de escribir el “for”, ya se abordará en un futuro post.
Con este sencillo ejemplo no habrá lugar a dudas. La estructura
HashMap no guarda las claves en el orden natural, en este caso alfabético si son
de tipo “String”. Esto también se cumple para los elementos con claves de tipo
integer. Por el contrario, la estructura TreeMap sí las guarda según el orden
alfabético si son de tipo “String”, o en orden ascendente si son de tipo “Integer”.
Cabe resaltar, que ambas estructuras poseen el método “remove()”
que permite eliminar un elemento, pasando como parámetro solo la clave del
elemento que se pretende eliminar o la clave y el valor juntos. Además, se recomienda
realizar pruebas de eficiencia en ambas estructuras, de manera similar a como
se hizo para las estructuras ArrayList y LinkedList. Se sugiere al lector que
realice estas pruebas por su cuenta.
Conclusión
En este artículo hemos explorado algunas de las estructuras
de datos más comunes en Java, incluyendo ArrayList, LinkedList, HashMap TreeMap,
Queue y Stack. Aunque hay muchas otras estructuras de datos disponibles, estas
son algunas de las más utilizadas y pueden ser un buen punto de partida para
cualquier desarrollador que quiera mejorar su conocimiento en estructuras de
datos en Java. Es importante recordar que cada estructura de datos tiene sus
ventajas y desventajas, y que la elección de la estructura adecuada dependerá
del problema específico que se esté tratando de resolver.
Comentarios
Publicar un comentario