Apuntes DAM
Volver al inicio

Árbol binario de búsqueda

AlgoritmosEstructuras de datosNivel intermedioTambién: ABB, BST

Un árbol donde cada nodo tiene a la izquierda los valores menores y a la derecha los mayores: buscar, insertar y borrar en O(log n) si está equilibrado, y recorrerlo en orden da los datos ordenados.

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Árbol binario de búsqueda

Escribe los valores que se insertan, en orden, y los que se buscan después.

insertar(50)

  • nodo nuevo

Paso 1

El árbol está vacío: el 50 es la raíz.

1static Nodo insertar(Nodo n, int x) {
2    if (n == null) return new Nodo(x);  // x = 50, nodos = 1
3    if (x < n.valor) n.izq = insertar(n.izq, x);
4    else if (x > n.valor) n.der = insertar(n.der, x);
5    return n;
6}
7
8static boolean buscar(Nodo n, int x) {
9    while (n != null) {
10        if (x == n.valor) return true;
11        n = x < n.valor ? n.izq : n.der;
12    }
13    return false;
14}

Variables

x
50
nodos
1

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Un árbol binario está hecho de nodos, y cada nodo tiene como mucho dos hijos: el izquierdo y el derecho. En un árbol binario de búsqueda (ABB) se cumple además una regla en todos los nodos: lo que hay en su subárbol izquierdo es menor que él, y lo que hay en el derecho, mayor.

Con esa regla, buscar es como la búsqueda binaria: se empieza en la raíz y en cada nodo se baja a la izquierda o a la derecha según el valor buscado sea menor o mayor. Insertar es buscar hasta encontrar un hueco vacío y colgar ahí el nodo nuevo.

Recorrer el árbol en inorden (subárbol izquierdo, nodo, subárbol derecho) visita los valores de menor a mayor: el árbol mantiene los datos ordenados aunque se inserten en cualquier orden.

El coste depende de la altura. Si el árbol está equilibrado (las ramas tienen alturas parecidas), la altura es unas log₂ n y todo es rapidísimo. Pero si se insertan los datos ya ordenados, cada nodo nuevo cuelga del anterior y el árbol degenera en una lista: altura n. Por eso los árboles de las bibliotecas se reequilibran solos (AVL, rojo-negro).

Cuándo usarlo

  • Hay que buscar, insertar y borrar datos con frecuencia y además recorrerlos ordenados (un HashSet es más rápido buscando, pero no mantiene el orden).
  • Hacen falta consultas por orden: el mínimo, el máximo, el siguiente mayor que x, todos los valores entre a y b.
  • Para entender estructuras que se usan a diario: TreeMap, TreeSet y los índices de las bases de datos.

Cuándo no

  • Si solo hace falta saber si un valor está: un HashSet lo hace en O(1) de media.
  • Si los datos llegan ordenados y el árbol no se reequilibra: degenera en una lista. En Java, usa TreeMap o TreeSet, que siempre están equilibrados.

Paso a paso

  1. Buscar. Desde la raíz: si el valor es el del nodo, encontrado; si es menor, se sigue por la izquierda; si es mayor, por la derecha; si se llega a null, no está.
  2. Insertar. Se busca el valor; donde la búsqueda acaba en null se crea el nodo nuevo como hijo del último nodo visitado.
  3. Recorrer. Inorden (izquierda, nodo, derecha) da los valores ordenados; preorden (nodo primero) sirve para copiar el árbol; postorden (nodo al final), para borrarlo o calcular cosas que dependen de los hijos.
  4. Borrar. Tres casos: un nodo sin hijos se quita; con un hijo, el hijo ocupa su lugar; con dos, se sustituye su valor por el de su sucesor (el menor del subárbol derecho) y se borra el sucesor.

El código

Insertar, buscar, recorrer y altura

Un nodo es una clase con su valor y dos referencias. Los métodos recursivos reciben un subárbol y devuelven el resultado; insertar devuelve la raíz del subárbol para poder enganchar el nodo nuevo a su padre.

Java
1import java.util.*;
2
3public class Main {
4    static class Nodo {
5        int valor;
6        Nodo izq, der;
7        Nodo(int valor) { this.valor = valor; }
8    }
9
10    static Nodo raiz;
11
12    /** Inserta x y devuelve la raíz del subárbol (así se engancha el nodo nuevo a su padre). */
13    static Nodo insertar(Nodo n, int x) {
14        if (n == null) return new Nodo(x);                 // hueco libre: aquí va
15        if (x < n.valor) n.izq = insertar(n.izq, x);       // menores a la izquierda
16        else if (x > n.valor) n.der = insertar(n.der, x);  // mayores a la derecha
17        return n;                                          // (los repetidos no se insertan)
18    }
19
20    static boolean contiene(Nodo n, int x) {
21        while (n != null) {
22            if (x == n.valor) return true;
23            n = x < n.valor ? n.izq : n.der;               // como en la búsqueda binaria
24        }
25        return false;
26    }
27
28    static void inorden(Nodo n, List<Integer> l) {          // izquierda, nodo, derecha
29        if (n == null) return;
30        inorden(n.izq, l);
31        l.add(n.valor);
32        inorden(n.der, l);
33    }
34
35    static void preorden(Nodo n, List<Integer> l) {         // nodo, izquierda, derecha
36        if (n == null) return;
37        l.add(n.valor);
38        preorden(n.izq, l);
39        preorden(n.der, l);
40    }
41
42    static int altura(Nodo n) {
43        return n == null ? 0 : 1 + Math.max(altura(n.izq), altura(n.der));
44    }
45
46    public static void main(String[] args) {
47        for (int x : new int[] {50, 30, 70, 20, 40, 60, 80, 45}) raiz = insertar(raiz, x);
48        List<Integer> in = new ArrayList<>(), pre = new ArrayList<>();
49        inorden(raiz, in);
50        preorden(raiz, pre);
51        System.out.println("Inorden (ordenado): " + in);
52        System.out.println("Preorden: " + pre);
53        System.out.println("Altura: " + altura(raiz));
54        System.out.println("¿Contiene 45? " + (contiene(raiz, 45) ? "sí" : "no") + " · ¿Contiene 65? " + (contiene(raiz, 65) ? "sí" : "no"));
55    }
56}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Inorden (ordenado): [20, 30, 40, 45, 50, 60, 70, 80]
Preorden: [50, 30, 20, 40, 45, 70, 60, 80]
Altura: 4
¿Contiene 45? sí · ¿Contiene 65? no

El árbol del ejemplo

text
150
2          /    \
3        30      70
4       /  \    /  \
5     20   40  60   80
6            \
7             45

Borrar un nodo

El caso difícil es el nodo con dos hijos: no se puede quitar sin romper el árbol, así que se copia en él el valor de su sucesor (que tiene como mucho un hijo) y se borra el sucesor, que es un caso fácil.

Java
1/** Borra x del subárbol n y devuelve la nueva raíz de ese subárbol. */
2static Nodo borrar(Nodo n, int x) {
3    if (n == null) return null;                       // no estaba
4    if (x < n.valor) n.izq = borrar(n.izq, x);
5    else if (x > n.valor) n.der = borrar(n.der, x);
6    else {
7        if (n.izq == null) return n.der;              // casos 1 y 2: sin hijos o con uno,
8        if (n.der == null) return n.izq;              // el hijo ocupa su lugar
9        Nodo sucesor = n.der;                         // caso 3: dos hijos
10        while (sucesor.izq != null) sucesor = sucesor.izq;   // el menor de la derecha
11        n.valor = sucesor.valor;                      // copia su valor aquí
12        n.der = borrar(n.der, sucesor.valor);         // y lo borra de la derecha
13    }
14    return n;
15}

Traza: insertar 45 en el árbol de 50, 30, 70, 20, 40, 60, 80

Nodo visitadoComparaciónDecisión
5045 < 50bajar por la izquierda
3045 > 30bajar por la derecha
4045 > 40la derecha de 40 está vacía: 45 se cuelga ahí

Tres comparaciones con siete nodos en el árbol: tantas como niveles hay que bajar.

Complejidad

OperaciónÁrbol equilibradoÁrbol degenerado (datos insertados en orden)
BuscarO(log n)O(n)
InsertarO(log n)O(n)
BorrarO(log n)O(n)
RecorrerO(n)O(n)
Mínimo y máximoO(log n)O(n)

La memoria es O(n), un nodo por valor. Las versiones recursivas usan además una pila tan profunda como la altura: en un árbol degenerado de 100.000 nodos, un StackOverflowError.

0102030405015101520tamaño de la entrada (n)operacionesO(n!)O(2ⁿ)O(n²)O(n log n)O(1)O(log n)O(n)
  • Mejor caso: O(1)
  • Caso medio: O(log n)
  • Peor caso: O(n)

Buscar: log n si el árbol está equilibrado, n si ha degenerado en una lista. Las curvas grises son las demás clases, para comparar.

En la práctica

  • TreeMap y TreeSet son árboles rojo-negro: árboles binarios de búsqueda que se reequilibran al insertar y borrar, así que siempre son O(log n). Dan firstKey, ceilingKey, subMap…
  • Los índices de las bases de datos son árboles B+: la misma idea con cientos de hijos por nodo, para leer pocas páginas del disco.
  • Los árboles sintácticos de un compilador o de una calculadora, y el DOM de una página web, son árboles (no de búsqueda) que se recorren con las mismas técnicas.

Errores típicos

  • En la inserción recursiva, llamar a insertar(n.izq, x) sin asignar el resultado (n.izq = insertar(n.izq, x)): el nodo nuevo se crea y se pierde.
  • Olvidar raiz = insertar(raiz, x) en el primer nodo: el árbol se queda vacío.
  • No decidir qué hacer con los repetidos (ignorarlos, contarlos o ponerlos a un lado): acaban duplicados o perdidos.
  • Borrar un nodo con dos hijos quitándolo sin más: se pierde uno de los dos subárboles.
  • Dar por hecho que siempre es O(log n): insertando datos ordenados se obtiene una lista con forma de árbol.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Un árbol con órdenes

Programa las operaciones básicas de un árbol binario de búsqueda de enteros y úsalas con las órdenes de la entrada. El main, que lee las órdenes y escribe los resultados, ya está: completa los métodos. Los valores repetidos no se insertan.

  • insertar x → insertado x o x ya estaba. buscar x → x está (profundidad d) (la raíz es 1) o x no está.
  • inorden, preorden, postorden → Inorden: 20 30 40 ((vacío) si no hay nodos). altura → Altura: h (0 vacío, 1 con solo la raíz). nodos → Nodos: n.
  • minimo y maximo → Mínimo: x, Máximo: x o El árbol está vacío. Otra orden: Orden no válida: línea.
☕JavaUn árbol con órdenesMedio

Ejemplo

Entrada (lo que se escribe por teclado)
insertar 50
insertar 30
insertar 70
insertar 20
insertar 40
insertar 60
insertar 80
insertar 30
buscar 40
buscar 65
inorden
preorden
postorden
altura
nodos
minimo
maximo
Salida esperada
insertado 50
insertado 30
insertado 70
insertado 20
insertado 40
insertado 60
insertado 80
30 ya estaba
40 está (profundidad 3)
65 no está
Inorden: 20 30 40 50 60 70 80
Preorden: 50 30 20 40 70 60 80
Postorden: 20 40 30 60 80 70 50
Altura: 3
Nodos: 7
Mínimo: 20
Máximo: 80
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    static class Nodo {
5        int valor;
6        Nodo izq, der;
7        Nodo(int valor) { this.valor = valor; }
8    }
9
10    static Nodo raiz;
11
12    static String lista(List<Integer> l) {
13        return l.isEmpty() ? "(vacío)" : String.join(" ", l.stream().map(String::valueOf).toList());
14    }
15
16    static Nodo insertar(Nodo n, int x) {
17        if (n == null) return new Nodo(x);
18        if (x < n.valor) n.izq = insertar(n.izq, x);
19        else if (x > n.valor) n.der = insertar(n.der, x);
20        return n;
21    }
22
23    /** Profundidad del nodo con x (la raíz es 1), o 0 si no está. */
24    static int profundidad(int x) {
25        int d = 1;
26        for (Nodo n = raiz; n != null; d++) {
27            if (x == n.valor) return d;
28            n = x < n.valor ? n.izq : n.der;
29        }
30        return 0;
31    }
32
33    static void recorrer(Nodo n, String orden, List<Integer> l) {
34        if (n == null) return;
35        if (orden.equals("preorden")) l.add(n.valor);
36        recorrer(n.izq, orden, l);
37        if (orden.equals("inorden")) l.add(n.valor);
38        recorrer(n.der, orden, l);
39        if (orden.equals("postorden")) l.add(n.valor);
40    }
41
42    static int altura(Nodo n) {
43        return n == null ? 0 : 1 + Math.max(altura(n.izq), altura(n.der));
44    }
45
46    static int nodos(Nodo n) {
47        return n == null ? 0 : 1 + nodos(n.izq) + nodos(n.der);
48    }
49
50    static Integer extremo(boolean minimo) {
51        if (raiz == null) return null;
52        Nodo n = raiz;
53        while ((minimo ? n.izq : n.der) != null) n = minimo ? n.izq : n.der;
54        return n.valor;
55    }
56
57    public static void main(String[] args) {
58        Scanner sc = new Scanner(System.in);
59        while (sc.hasNextLine()) {
60            String linea = sc.nextLine().trim();
61            if (linea.isEmpty()) continue;
62            String[] p = linea.split("\\s+");
63            try {
64                switch (p[0]) {
65                    case "insertar" -> {
66                        int x = Integer.parseInt(p[1]);
67                        boolean estaba = profundidad(x) > 0;
68                        raiz = insertar(raiz, x);
69                        System.out.println(estaba ? x + " ya estaba" : "insertado " + x);
70                    }
71                    case "buscar" -> {
72                        int x = Integer.parseInt(p[1]);
73                        int d = profundidad(x);
74                        System.out.println(d > 0 ? x + " está (profundidad " + d + ")" : x + " no está");
75                    }
76                    case "inorden", "preorden", "postorden" -> {
77                        List<Integer> l = new ArrayList<>();
78                        recorrer(raiz, p[0], l);
79                        System.out.println(Character.toUpperCase(p[0].charAt(0)) + p[0].substring(1) + ": " + lista(l));
80                    }
81                    case "altura" -> System.out.println("Altura: " + altura(raiz));
82                    case "nodos" -> System.out.println("Nodos: " + nodos(raiz));
83                    case "minimo", "maximo" -> {
84                        Integer v = extremo(p[0].equals("minimo"));
85                        System.out.println(v == null ? "El árbol está vacío" : (p[0].equals("minimo") ? "Mínimo: " : "Máximo: ") + v);
86                    }
87                    default -> System.out.println("Orden no válida: " + linea);
88                }
89            } catch (RuntimeException e) {
90                System.out.println("Orden no válida: " + linea);
91            }
92        }
93    }
94}

Todas las operaciones siguen la regla del árbol: comparar y bajar por un lado. Por eso buscar, insertar, el mínimo y el máximo cuestan lo que la altura, y los recorridos y contar los nodos, lo que el número de nodos.

Un solo método recorrer con el orden como parámetro muestra que los tres recorridos se diferencian solo en dónde se visita el nodo.

2. Borrar, recorrer por niveles y comprobar el equilibrio

Completa el árbol con tres operaciones: borrar un valor (con los tres casos, y el sucesor inorden cuando el nodo tiene dos hijos), el recorrido por niveles (de arriba abajo y de izquierda a derecha, con una cola) y la comprobación de si está equilibrado (en todos sus nodos las alturas de los dos subárboles se diferencian en 1 como mucho). Insertar y la lectura de órdenes ya están hechos.

  • insertar x y z… inserta los valores en ese orden (sin escribir nada). borrar x → borrado x o x no está.
  • niveles → una línea Nivel N: valores por nivel, o (árbol vacío). equilibrado → Equilibrado: sí (altura h) o Equilibrado: no (altura h).
  • Otra orden o un número mal escrito: Orden no válida: línea.
☕JavaBorrar, recorrer por niveles y comprobar el equilibrioDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
insertar 50 30 70 20 40 60 80 45
niveles
borrar 20
borrar 40
borrar 50
niveles
borrar 99
equilibrado
Salida esperada
Nivel 1: 50
Nivel 2: 30 70
Nivel 3: 20 40 60 80
Nivel 4: 45
borrado 20
borrado 40
borrado 50
Nivel 1: 60
Nivel 2: 30 70
Nivel 3: 45 80
99 no está
Equilibrado: sí (altura 3)
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    static class Nodo {
5        int valor;
6        Nodo izq, der;
7        Nodo(int valor) { this.valor = valor; }
8    }
9
10    static Nodo raiz;
11
12    static Nodo insertar(Nodo n, int x) {
13        if (n == null) return new Nodo(x);
14        if (x < n.valor) n.izq = insertar(n.izq, x);
15        else if (x > n.valor) n.der = insertar(n.der, x);
16        return n;
17    }
18
19    static boolean contiene(Nodo n, int x) {
20        while (n != null && n.valor != x) n = x < n.valor ? n.izq : n.der;
21        return n != null;
22    }
23
24    static int altura(Nodo n) {
25        return n == null ? 0 : 1 + Math.max(altura(n.izq), altura(n.der));
26    }
27
28    /** Borra x del subárbol n; con dos hijos, lo sustituye por su sucesor (el menor de la derecha). */
29    static Nodo borrar(Nodo n, int x) {
30        if (n == null) return null;
31        if (x < n.valor) n.izq = borrar(n.izq, x);
32        else if (x > n.valor) n.der = borrar(n.der, x);
33        else {
34            if (n.izq == null) return n.der;
35            if (n.der == null) return n.izq;
36            Nodo s = n.der;
37            while (s.izq != null) s = s.izq;
38            n.valor = s.valor;
39            n.der = borrar(n.der, s.valor);
40        }
41        return n;
42    }
43
44    /** Un recorrido por niveles: una cola con los nodos del nivel actual. */
45    static List<List<Integer>> niveles() {
46        List<List<Integer>> r = new ArrayList<>();
47        Deque<Nodo> cola = new ArrayDeque<>();
48        if (raiz != null) cola.add(raiz);
49        while (!cola.isEmpty()) {
50            List<Integer> nivel = new ArrayList<>();
51            for (int k = cola.size(); k > 0; k--) {
52                Nodo n = cola.poll();
53                nivel.add(n.valor);
54                if (n.izq != null) cola.add(n.izq);
55                if (n.der != null) cola.add(n.der);
56            }
57            r.add(nivel);
58        }
59        return r;
60    }
61
62    /** En todos los nodos, las alturas de sus dos subárboles se diferencian en 1 como mucho. */
63    static boolean equilibrado(Nodo n) {
64        if (n == null) return true;
65        return Math.abs(altura(n.izq) - altura(n.der)) <= 1 && equilibrado(n.izq) && equilibrado(n.der);
66    }
67
68    public static void main(String[] args) {
69        Scanner sc = new Scanner(System.in);
70        while (sc.hasNextLine()) {
71            String linea = sc.nextLine().trim();
72            if (linea.isEmpty()) continue;
73            String[] p = linea.split("\\s+");
74            try {
75                switch (p[0]) {
76                    case "insertar" -> {
77                        if (p.length < 2) throw new IllegalArgumentException();
78                        int[] xs = new int[p.length - 1];
79                        for (int i = 1; i < p.length; i++) xs[i - 1] = Integer.parseInt(p[i]);   // todos válidos antes de insertar
80                        for (int x : xs) raiz = insertar(raiz, x);
81                    }
82                    case "borrar" -> {
83                        int x = Integer.parseInt(p[1]);
84                        if (!contiene(raiz, x)) System.out.println(x + " no está");
85                        else {
86                            raiz = borrar(raiz, x);
87                            System.out.println("borrado " + x);
88                        }
89                    }
90                    case "niveles" -> {
91                        List<List<Integer>> n = niveles();
92                        if (n.isEmpty()) System.out.println("(árbol vacío)");
93                        for (int i = 0; i < n.size(); i++) System.out.println("Nivel " + (i + 1) + ": " + String.join(" ", n.get(i).stream().map(String::valueOf).toList()));
94                    }
95                    case "equilibrado" -> System.out.println("Equilibrado: " + (equilibrado(raiz) ? "sí" : "no") + " (altura " + altura(raiz) + ")");
96                    default -> System.out.println("Orden no válida: " + linea);
97                }
98            } catch (RuntimeException e) {
99                System.out.println("Orden no válida: " + linea);
100            }
101        }
102    }
103}

Borrar con el sucesor inorden mantiene la regla del árbol: el sucesor es mayor que todo el subárbol izquierdo y menor que el resto del derecho, así que puede ocupar el sitio del nodo borrado.

El recorrido por niveles usa una cola en lugar de recursividad: es una búsqueda en anchura. La comprobación de equilibrio, tal como está, recalcula alturas y es O(n²) en el peor caso; calculando la altura y el equilibrio en la misma pasada sería O(n).

Test

Test: Árbol binario de búsqueda

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.En un árbol binario de búsqueda, ¿qué recorrido da los valores de menor a mayor?

  2. 2.Insertas 1, 2, 3, 4 y 5 en ese orden en un ABB sin reequilibrio. ¿Qué altura tiene?

  3. 3.Al borrar un nodo con dos hijos, ¿por qué valor se sustituye?

  4. 4.¿Qué estructura de la biblioteca de Java es un árbol binario de búsqueda equilibrado?

  5. 5.¿Cuánto cuesta buscar en un ABB equilibrado de n nodos?

Relacionado