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.



 (Haz clic en las imágenes si no son muy claras)

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.



Al ejecutar el código nuevo, en la consola se observa mejor la diferencia de tiempo en los elementos 11 y 16.


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. 


Si tienes alguna duda hasta el momento, revisa nuevamente las definiciones y ejemplos anteriores hasta comprender lo máximo posible. Si no estás haciendo los ejemplos por tu cuenta mientras avanzas en este texto, te invito a revisar rápidamente los dos primeros posts. De este modo, tendrás las herramientas necesarias que te permitirán escribir código en lenguaje Java sin problemas.

¿Qué es Stack?
 

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

Entradas populares de este blog

Instalación del IDE Eclipse para Java y JShell

POO (Programación Orientada a Objetos), public, private y clases en Java - 1