Skip to content

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

  1. Búsqueda vs Orden: Si solo necesitas saber si un elemento existe (contains), ¿usarías TreeSet o HashSet? ¿Por qué?
  2. 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).
  3. Subconjuntos: ¿Para qué podría servirte subSet en una aplicación de calendario con eventos fechados?

Aplicación en el Mundo Real

  1. 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.

  2. Planificador de Tareas (Schedulers): Imagina un sistema que tiene que ejecutar tareas programadas. Usas un TreeSet de 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.
  3. Corrector Ortográfico (Sugerencias): Si tienes un diccionario cargado en un TreeSet<String>, cuando alguien escribe "progra", puedes usar tailSet("progra") para encontrar rápidamente todas las palabras que empiezan por ese prefijo y sugerir autocompletado.

Para Saber Más