TreeSet: El Maniático del Orden
Introducción: De la Fiesta al Archivo
¡Hola, equipo de desarrollo! 👓
Si el HashSet era una fiesta descontrolada donde el portero solo chequeaba que no te colaras dos veces, el TreeSet es... bueno, es como Monica Geller de Friends organizando su armario. O como ese bibliotecario severo que no soporta ver un libro fuera de su sitio.
Imaginad un Ranking de Videojuegos. No solo queréis que los jugadores sean únicos (no puede haber dos "xX_Slayer_Xx"), sino que además queréis saber siempre quién va primero, quién va último y quién está justo por encima de ti para intentar superarle.
El TreeSet hace el trabajo sucio por nosotros: mantiene los datos siempre ordenados automáticamente. ¿El precio a pagar? Es un poco más lento que el HashSet (porque tiene que pensar dónde colocar cada cosa), pero nos da superpoderes de navegación que no tienen precio.
¡Orden en la sala, que empieza la clase! 👨⚖️
Conceptos Fundamentales
Definición: TreeSet
Es una implementación de la interfaz NavigableSet (que a su vez extiende de SortedSet).
* Estructura: Internamente usa un Red-Black Tree (Árbol Rojo-Negro), un tipo de árbol binario que se auto-equilibra.
* Orden: Los elementos se almacenan ordenados según su "Orden Natural" (Comparable) o mediante un "Árbitro externo" (Comparator) que le pasemos al constructor.
* Sin Duplicados: Como buen Set, no admite repes.
* Sin Nulos: Por lo general, NO admite null, ya que no se puede comparar null con otros objetos para saber si es mayor o menor.
Interfaces Clave
Al implementar NavigableSet, el TreeSet gana métodos para búsquedas de proximidad:
"dame el número más cercano a 100 pero que no se pase". ¡Magia pura!
Mapa Mental: El Árbol Genealógico
Desarrollo y Ejemplos Prácticos
Vamos a dividir el arsenal de métodos del TreeSet en bloques lógicos. Preparad el IDE.
1. Gestión Básica (Añadir y Ordenar)
Aquí vemos cómo el TreeSet ordena solo nada más insertar.
Métodos:
* add(E e): Añade y ordena. Ignora si ya existe.
* addAll(Collection c): Añade un montón de golpe.
* remove(Object o): Borra si existe.
* contains(Object o): Busca (usando el árbol, no hash).
* size(), isEmpty(), clear().
* clone(): Copia superficial.
Ejemplo: La Lista de Invitados VIP (Ordenada)
import java.util.TreeSet;
import java.util.Comparator;
public class OrdenAlfabetico {
public static void main(String[] args) {
// 1. Orden Natural (Strings se ordenan alfabéticamente)
TreeSet<String> invitados = new TreeSet<>();
invitados.add("Zendaya");
invitados.add("Brad Pitt");
invitados.add("Ana de Armas");
invitados.add("Brad Pitt"); // Duplicado: Se ignora
// ¡Magia! Salen ordenados sin hacer nada más
System.out.println("Lista Oficial: " + invitados);
// Salida: [Ana de Armas, Brad Pitt, Zendaya]
// 2. Orden Personalizado (Comparator)
// Queremos ordenar por longitud del nombre (de corto a largo)
TreeSet<String> porLongitud = new TreeSet<>(Comparator.comparingInt(String::length));
porLongitud.addAll(invitados);
System.out.println("Por longitud: " + porLongitud);
// Salida: [Zendaya, Brad Pitt, Ana de Armas] (Depende de longitudes)
// 3. Comprobaciones básicas
if (invitados.contains("Zendaya")) {
System.out.println("Zendaya está invitada.");
}
System.out.println("Total invitados: " + invitados.size());
// 4. Clonado
TreeSet<String> copia = (TreeSet<String>) invitados.clone();
invitados.clear(); // Borramos el original
System.out.println("Copia sobrevive: " + copia);
}
}
¡Cuidado con ClassCastException!
Si intentas crear un TreeSet de objetos propios (ej: new TreeSet<Alumno>()) y la clase Alumno no implementa la interfaz Comparable, el programa lanzará una excepción en tiempo de ejecución al intentar añadir el primer elemento. ¡Java necesita saber cómo ordenar tus alumnos!
2. Navegación Extrema (First, Last, Poll)
Estos métodos aprovechan que el árbol sabe dónde está el mínimo y el máximo. Son rapidísimos.
Métodos:
* first() / last(): Devuelven el primero (mínimo) y el último (máximo). Lanzan excepción si está vacío.
* pollFirst() / pollLast(): Recuperan y eliminan el primero o el último. Devuelven null si está vacío (más seguros).
Ejemplo: Sistema de Puntuaciones (Leaderboard)
import java.util.TreeSet;
public class HighScores {
public static void main(String[] args) {
TreeSet<Integer> puntuaciones = new TreeSet<>();
puntuaciones.add(500);
puntuaciones.add(100); // Mínimo
puntuaciones.add(9000); // Máximo
puntuaciones.add(2500);
try {
System.out.println("Puntuación más baja (Noob): " + puntuaciones.first());
System.out.println("Puntuación más alta (Pro): " + puntuaciones.last());
} catch (Exception e) {
System.out.println("Lista vacía");
}
// Eliminamos al peor y al mejor
System.out.println("Eliminando al peor: " + puntuaciones.pollFirst()); // 100
System.out.println("Eliminando al mejor: " + puntuaciones.pollLast()); // 9000
System.out.println("Quedan en medio: " + puntuaciones); // [500, 2500]
}
}
3. Búsqueda de Proximidad (Ceiling, Floor...)
Aquí es donde TreeSet justifica su sueldo. Imaginad buscar precios de vuelos.
Métodos:
* ceiling(E e) ("Techo"): Devuelve el menor elemento que sea mayor o igual (>=) al dado.
* floor(E e) ("Suelo"): Devuelve el mayor elemento que sea menor o igual (<=) al dado.
* higher(E e) ("Estrictamente Mayor"): Devuelve el menor elemento mayor estricto (>) al dado.
* lower(E e) ("Estrictamente Menor"): Devuelve el mayor elemento menor estricto (<) al dado.
Recuerda: Ceiling/Floor admiten empate. Higher/Lower no.
Ejemplo: Buscador de Tarifas de Vuelo
import java.util.TreeSet;
public class BuscadorVuelos {
public static void main(String[] args) {
TreeSet<Integer> precios = new TreeSet<>();
// Precios disponibles
precios.add(50);
precios.add(100);
precios.add(150);
precios.add(300);
precios.add(500);
int miPresupuesto = 150;
System.out.println("Mi presupuesto exacto: " + miPresupuesto);
// floor: Quiero gastar 150 o menos (lo más cercano posible)
System.out.println("Floor (<= 150): " + precios.floor(miPresupuesto)); // 150
// lower: Quiero gastar MENOS de 150 obligatoriamente
System.out.println("Lower (< 150): " + precios.lower(miPresupuesto)); // 100
// ceiling: Si no hay de 150, ¿cuál es el siguiente más barato?
System.out.println("Ceiling (>= 150): " + precios.ceiling(miPresupuesto)); // 150
// higher: Quiero algo mejor (más caro) que 150
System.out.println("Higher (> 150): " + precios.higher(miPresupuesto)); // 300
// Caso null: Buscamos algo menor que 10 euros
System.out.println("¿Vuelo por 10€? " + precios.floor(10)); // null
}
}
4. Vistas de Rango (SubSet, HeadSet, TailSet)
Estos métodos no crean copias, sino ventanas hacia el conjunto original. Si borras algo en la vista, ¡se borra en el original!
Métodos:
* subSet(from, to): Rango [desde, hasta). El "hasta" es exclusivo por defecto.
* headSet(to): Desde el principio hasta el elemento (exclusivo).
* tailSet(from): Desde el elemento (inclusivo) hasta el final.
import java.util.TreeSet;
import java.util.SortedSet;
public class FiltroRango {
public static void main(String[] args) {
TreeSet<Character> alfabeto = new TreeSet<>();
for(char c = 'a'; c <= 'z'; c++) {
alfabeto.add(c);
}
// 1. subSet: De la 'a' (incluida) a la 'd' (excluida)
SortedSet<Character> rango = alfabeto.subSet('a', 'd');
System.out.println("Primeras letras: " + rango); // [a, b, c]
// 2. tailSet: De la 'x' al final
System.out.println("Últimas letras: " + alfabeto.tailSet('x')); // [x, y, z]
// 3. Demostración de vista conectada
// ¡OJO! Si borro la 'a' del subSet, desaparece del conjunto principal
rango.remove('a');
System.out.println("Alfabeto original tras borrar en la vista: " + alfabeto);
// La 'a' ha desaparecido.
}
}
5. Iteración y Utilidades Avanzadas
Métodos:
* iterator(): Recorre en orden ascendente (menor a mayor).
* descendingIterator(): Recorre en orden descendente (mayor a menor).
* descendingSet(): Devuelve una vista completa del conjunto al revés.
* comparator(): Devuelve el comparador usado, o null si usa orden natural.
* spliterator(): Para paralelismo (avanzado, Java 8+).
import java.util.TreeSet;
import java.util.Iterator;
public class IteracionAvanzada {
public static void main(String[] args) {
TreeSet<String> palabras = new TreeSet<>();
palabras.add("Banana");
palabras.add("Avión");
palabras.add("Cereza");
// Orden natural
System.out.println("Orden normal: " + palabras); // [Avión, Banana, Cereza]
// Iterador inverso
System.out.print("Inverso: ");
Iterator<String> it = palabras.descendingIterator();
while(it.hasNext()) {
System.out.print(it.next() + " ");
}
// Salida: Cereza Banana Avión
System.out.println();
// Vista inversa completa
System.out.println("Set Invertido: " + palabras.descendingSet());
// Verificamos el comparador
System.out.println("¿Tiene comparador manual? " + palabras.comparator()); // null
// Spliterator (Para Streams)
palabras.spliterator().forEachRemaining(System.out::println);
}
}
Diagrama Mermaid: Árbol Rojo-Negro Simplificado
Así es como TreeSet organiza los números internamente para encontrar todo tan rápido.
graph TD
Root((20))
L1((10))
R1((30))
L2((5))
R2((15))
R3((25))
R4((40))
Root -->|Menor| L1
Root -->|Mayor| R1
L1 -->|Menor| L2
L1 -->|Mayor| R2
R1 -->|Menor| R3
R1 -->|Mayor| R4
style Root fill:#f96,stroke:#333
style L1 fill:#9cf,stroke:#333
style R1 fill:#9cf,stroke:#333
Tabla Comparativa: TreeSet vs HashSet
| Característica | HashSet (La Fiesta) | TreeSet (La Biblioteca) |
|---|---|---|
| Orden | Ninguno (Caótico) | Ordenado (Natural o Comparator) |
| Velocidad (Add/Remove) | O(1) - Flash ⚡ | O(log n) - Rápido 🏎️ |
Nulos (null) |
Permite 1 | ❌ Prohibidos (generalmente) |
| Navegación | No existe | higher, lower, subSet... |
| Memoria | Menos consumo | Más consumo (punteros del árbol) |
Ejercicios Reflexivos
- Búsqueda vs Orden: Si solo necesitas saber si un elemento existe (
contains), ¿usarías TreeSet o HashSet? ¿Por qué? - El caso del objeto mutable: Si insertas un objeto en un TreeSet ordenado por un atributo "edad", y luego modificas esa edad fuera del Set... ¿qué crees que le pasa al árbol? (Pista: El árbol no se entera automáticamente y se "rompe" el orden).
- Subconjuntos: ¿Para qué podría servirte
subSeten una aplicación de calendario con eventos fechados?
Aplicación en el Mundo Real
-
Bases de Datos (Índices): Aunque las BD usan estructuras más complejas (B-Trees), el concepto es idéntico al
TreeSet. Mantener los índices ordenados permite hacer consultas de rangos (WHERE fecha BETWEEN X AND Y) de forma eficiente sin recorrer toda la tabla. -
Planificador de Tareas (Schedulers): Imagina un sistema que tiene que ejecutar tareas programadas. Usas un
TreeSetde tareas ordenadas por "hora de ejecución".first()te da siempre la siguiente tarea a ejecutar inmediatamente.- No tienes que buscar entre todas las tareas, solo miras la primera.
-
Corrector Ortográfico (Sugerencias): Si tienes un diccionario cargado en un
TreeSet<String>, cuando alguien escribe "progra", puedes usartailSet("progra")para encontrar rápidamente todas las palabras que empiezan por ese prefijo y sugerir autocompletado.
Para Saber Más
-
Documentación Oficial de TreeSet: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/TreeSet.html
-
GeeksForGeeks - TreeSet with Examples: Explicación detallada y más casos de uso. https://www.geeksforgeeks.org/java/treeset-in-java-with-examples/
-
Visualización de Árboles (USF): Juega insertando números para ver cómo el árbol se balancea solo. (Selecciona "Red-Black Tree"). https://www.cs.usfca.edu/~galles/visualization/RedBlack.html