Algoritmos y Estructuras de Datos Herramientas Lenguaje de programación
!Prog C/C++ Rust
Linux Matemáticas
Mates Discretas
Programación Orientada a Objetos Sistemas Operativos

Árboles

[date: 16-01-2024 22:05] [last modification: 26-01-2024 23:46]
[words: 3678] [reading time: 18min] [size: 64001 bytes]

Las estructuras de datos no lineales organizadas de forma jerárquica, o árboles, son muy útiles en computación porque suelen tener complejidad logarítmica, a diferencia de las estructuras lineales. En este artículo se muestran algunos tipos de árboles junto a sus características.

Conceptos de árboles

Árbol
Estructura de datos no lineal con una organización jerárquica y elementos homéneos. Cada elemento tiene un único antecesor y puede tener varios sucesores.
    %%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
graph
    classDef normal stroke:#f05,fill:#f05,color:black;
    classDef raiz stroke:#0ef,fill:#0ef,color:black;
    classDef hoja stroke:#0f5,fill:#0f5,color:black;

    A1((A)):::normal
    B1((B)):::normal
    C1((C)):::normal
    D1((D)):::normal
    E1((E)):::normal
    F1((F)):::normal
    G1((G)):::normal
    H1((H)):::normal

    A2((Raíz)):::raiz
    B2((Interno)):::normal
    C2((Interno)):::normal
    D2((Hoja)):::hoja
    E2((Hoja)):::hoja
    F2((Interno)):::normal
    G2((Hoja)):::hoja
    H2((Hoja)):::hoja

    subgraph Ascendientes
        A1 --- B1
    end

    subgraph nodo
        B1 --- C1
    end

    B1 --- D1

    subgraph D1escendientes
        C1 --- E1
        C1 --- F1
        F1 --- G1
        F1 --- H1
    end

    A2 --- B2
    B2 --- C2
    B2 --- D2
    C2 --- E2
    C2 --- F2
    F2 --- G2
    F2 --- H2
NodoCada componente/elemento del árbol
Nodo ascendiente / antecesorNodo en un nivel superior de la jerarquía, accesible por ascenso
Nodo descendiente / sucesorNodo en un nivel inferior de la jerarquía, accesible por descenso
Nodo raízNodo inicial de la jerarquía. No tiene antecesores.
Nodo hojaNodo terminal de la jerarquía. No tiene sucesores, por tanto se encuentran en el nivel más bajo, aunque no todos tienen porqué estar en el mismo nivel.
Nodo internoTodo nodo que no es raíz ni hoja, es decir, tiene sucesores y antecesores.
    %%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
graph
    classDef normal stroke:#f05,fill:#f05,color:black;
    classDef padre stroke:#0ef,fill:#0ef,color:black;
    classDef hermano stroke:#fe0,fill:#fe0,color:black;
    classDef hijo stroke:#0f5,fill:#0f5,color:black;

    1(Padre):::padre
    2(Hermano):::hermano
    3(Hijo):::hijo

    A((A)):::normal
    B((B)):::padre
    C((C)):::hermano
    D((D)):::hermano
    E((E)):::hijo
    F((F)):::hijo
    G((G)):::normal
    H((H)):::normal

    A ~~~ 1
    B ~~~ 2
    C ~~~ 3
    A ~~~ D

    A --- B
    B --- C
    B --- D
    C --- E
    C --- F
    F --- G
    F --- H
SubárbolSubconjunto de nodos de un árbol que a su vez tiene estructura de árbol.
Nodo padreEs el nodo ascendente directo de todos los subárboles de un nodo concreto.
Nodo hijoEl el nodo descendente directo de otro nodo.
Nodos hermanosDos nodos son hermanos si tienen el mismo padre.
CaminoSecuencia de nodos donde cualquier par de nodos consecutivos son padre e hijo.
Longitud del caminoNúmero de nodos de un camino menos 1 (número de enlaces).
RamaCualquier camino desde la raíz hasta una hoja.
Nivel / Profundidad de un nodoNúmero de nodos que tiene el camino desde la raíz hasta dicho nodo.
Grado
  • De un nodo: número de descendientes directos de ese nodo.
  • De un árbol: el máximo grado de sus nodos.
PesoNúmero de nodos hoja.
AlturaNúmero de nodos que tiene la rama más larga del árbol.

Definición de árbol binario

Árbol binario
Árbol donde cada nodo tiene como máximo grado 2, es decir, no puede tener más de 2 nodos descendientes.
Árbol binario: definición recursiva

Un árbol binario es una de estas dos cosas:

  • Un árbol vacío
  • Un nodo raíz con un subárbol binario a la derecha y un subárbol binario a la izquierda
NoequilibradoEquilibrado
NollenoLleno

TAD Árbol Binario

Los valores del TAD Árbol Binario (ABin) son árboles binarios donde cada nodo contiene un dato del tipo elem. Es un TAD mutable, dado que insizq/insder, supizq/supder y modificar; insertan, eliminan y modifican respecticamente los elementos del árbol.

//// Lectura ////
*crear() -> abin           Crea un abin vacío
es_vacio(A: abin) -> bool  true si A está vacío
leer(A: abin) -> elem      Contenido del nodo raíz de A.
izq(A: abin) -> abin
der(A: abin) -> abin       Subárbol izquierdo/derecho de A.
                           Si no existe, NULL.

//// Modificación ////
insizq(A: abin, E: elem)
insder(A: abin, E: elem)
    Requisitos: es_vacio(A) = false
                es_vacio(izq/der(A)) = true
    Inserta un nodo con contenido E a la izquierda/derecha

supizq(A: abin)
supder(A: abin)
    Requisitos: es_vacio(A) = false
    Elimina el subárbol izquierdo/derecho, incluyendo descendientes

modificar(A: abin, E: elem)
    Sobreescribe el nodo raíz de A con E

Recorridos

Anchura

Se recorre el árbol por niveles, de izquierda a derecha:

    flowchart
    classDef node stroke:#f05,fill:#f05,color:black;
    1((1)) --- 2((2))
    1 --- 3((3))
    2 --- 4((4))
    2 --- 5((5))
    3 --- 6((6))

Para realizarlo, se necesita una cola como estructura auxiliar:

  1. Añadir la raíz a la cola.
  2. Tomar el primer elemento de la cola, mostrarlo en la secuencia, y meter sus hijos izquierdo y derecho (en ese orden) en la cola.
  3. Repetir el paso 2 hasta que la cola se vacíe.

Profundidad

Se recorre el árbol según sus ramas, descendiendo o subiendo niveles. Hay varios tipos dependiendo qué dato se muestra primero:

    flowchart
    classDef inorden stroke:#f05,fill:#f05,color:black;
    classDef preorden stroke:#0ef,fill:#0ef,color:black;
    classDef postorden stroke:#0f5,fill:#0f5,color:black;

    A(Inorden):::inorden ~~~ A5((5)):::inorden
    A5 --- A2((2)):::inorden
    A5 --- A7((7)):::inorden
    A2 --- A1((1)):::inorden
    A2 --- A4((4)):::inorden
    A4 --- A3((3)):::inorden
    A7 --- A6((6)):::inorden

    B(Preorden):::preorden ~~~ B1((1)):::preorden
    B1 --- B2((2)):::preorden
    B2 --- B3((3)):::preorden
    B2 --- B4((4)):::preorden
    B4 --- B5((5)):::preorden
    B1 --- B6((6)):::preorden
    B6 --- B7((7)):::preorden

    C(Postorden):::postorden ~~~ C7((7)):::postorden
    C7 --- C4((4)):::postorden
    C4 --- C1((1)):::postorden
    C4 --- C3((3)):::postorden
    C3 --- C2((2)):::postorden
    C7 --- C6((6)):::postorden
    C6 --- C5((5)):::postorden

Árboles de expresión

La expresión (A+B)*C genera el siguiente árbol de expresión:

    flowchart
    classDef node stroke:#f05,fill:#f05,color:black;

    M((*)) --- S((+))
    S --- A((A))
    S --- B((B))
    M --- C((C))

Y se se realizan recorridos sobre dicho árbol:

Árboles para búsqueda

Importante
Ahora, cada nodo debe tener una clave que permita ordenar el tipo elem, clave.

Árbol binario de búsqueda (ABB)

Árbol binario de búsqueda (ABB)

Dado un nodo de un Árbol binario,

  • todo subárbol izquierdo debe almacenar valores menores .
  • todo subárbol derecho debe almacenar valores mayores .
//// Lectura ////
*crear() -> abin
leer(A: abin) -> elem
izq(A: abin) -> abin
der(A: abin) -> abin
es_vacio(A: abin) -> bool
es_miembro(A: abin, E: elem) --> bool
buscar(A: abin, K: clave) --> elem

//// Modificación ////
insertar(A: abin, K: clave, E: elem) -> bool
    Inserta un nodo con contenido E
    Si el nodo ya existe, no hace nada
    true si se modificó A

suprimir(A: abin, K: elem) -> bool
    Requisitos: es_vacio(A) = false
    Elimina el nodo de clave K
    Si el nodo no existe, no hace nada
    true si se modificó A

modificar(A: abin, K: clave, E: elem) -> bool
    Sobreescribe el nodo de clave K con E
    Si el nodo no existe, no hace nada
    true si se modificó A

La ventaja principal es que el tiempo de ejecución promedio es $O(\log{(n)})$, siempre y cuando el árbol esté balanceado, de lo contrario, será como una lista, $O(n)$.

    flowchart
    classDef node stroke:#f05,fill:#f05,color:black;
    classDef hidden stroke:none,fill:none,color:none

    A((55)) --- B((30))
    B --- C((4))
    B --- D((41))
    A --- E((75))
    E ~~~ H1( ):::hidden
    E --- F((85))

    G((55)) --- I((41))
    G ~~~ H2( ):::hidden
    I --- J((30))
    I ~~~ H3( ):::hidden
    J --- K((4))
    J ~~~ H4( ):::hidden

El recorrido en inorden devuelve los datos ordenados: 4 30 41 55 75 85.

Montículo binario (Heap)

Montículo binario (heap)

Árbol binario completo parcialmente ordenado:

  • Completo: en el nivel más bajo pueden faltar nodos, pero deben estar a la derecha.
  • Condición de montículo:
    • minheap: $\text{clave}(\text{nodo}) \le \text{clave}(\text{hijos})$
    • maxheap: $\text{clave}(\text{nodo}) \ge \text{clave}(\text{hijos})$

Propiedades

  • Orden parcial es más débil que el total ==> Más eficiente de mantener
  • Orden parcial más eficiente que el aleatorio ==> Conocemos el mayor/menor (maxheap/minheap) elemento: la raíz.

Aplicaciones

  • Soporte eficiente a la cola de prioridad
  • Algoritmos voraces

Implementación mediante arrays

La implementación más natural de un montículo binario es el árbol binario, pero se puede aprovechar su propiedad de completo:

Comparativa
Ventajas
  • Se obtiene en tiempo constante cualquier nodo, incluyendo hijos y padre.
  • Si el árbol es completo, ocupa menos menoria que la implementación enlazada, dado que no hay que almacenar punteros.
Desventajas
  • Se requiere conocer a priori el número máximo de elementos.
  • Si el árbol no es completo, se desperdicia memoria por tener huecos en el array.
  • Tampoco es tan flexible a la hora de hacer modificaciones arbitrarias.

Cola de prioridad con montículos binarios

void insertar(ColaPrio C, Elem E, Prio P)
void suprimir(ColaPrio C)

Estas operaciones se pueden completar en $O(\log{(n)})$, dado que la altura de un árbol binario completo de $n$ nodos es $\log{(n + 1)}$.

Monticulización / Heapify

Dado un array de datos, se construye un montículo:

  1. Se crea un árbol a partir del array
  2. Si el árbol tiene $L$ niveles, desde el nivel $L-1$ hasta 1 repetir:
    • Comprobar la condición de montículo, si no la cumple, intercambiar con el menor/mayor de sus hijos
    • Propagar ese cambio desde el nivel actual hasta $L-1$

Árbol equilibrado (AVL)

Se ha comentado que la eficiencia de un Árbol Binario de Búsqueda depende de su estructura, lo que viene dado por el orden de inserción de los datos.

Árbol equilibrado (AVL)

Es un Árbol Binario en el que las alturas de los dos subárboles de cada nodo no difieren en más de una unidad.

A cada nodo se le asigna un factor de equilibrio, que indica la diferencia entre alturas del subárbol izquierdo y derecho:

$$ F_e = H_{RD} - H_{RI} $$

Para que un árbol equilibrado sea válido debe tener un factor de equilibrio en $\set{-1, 0, 1}$

Rotaciones

Para mantener el árbol equilibrado tras una actualización (inserción o eliminación), se realizan rotaciones que transforman la estructura del árbol sin cambiar su significado.

  • Rotaciones simples: II, DD
  • Rotaciones compuestas: ID, DI

Dependiendo de la combinación del factor de equilibrio de cada nodo, se podrá determinar la rotación adecuada para equilibrar el árbol.

Eficiencia

Las operaciones siguen siendo $O(\log{(n)})$, pero cada paso necesita más operaciones: se necesita una pasada desde la raíz para buscar el nodo sobre el que operar, y luego otra hacia la raíz para recalcular los factores de equilibrio y realizar las rotaciones.

Con este pequeño precio, se consigue un árbol equilibrado en todos los casos, por lo que nunca una operación será $O(n)$ como puede suceder en un árbol binario convencional.

    flowchart
    classDef node stroke:#0f5,fill:#0f5,color:black;
    classDef hidden stroke:none,fill:none,color:none

    T(Equilibrado) ~~~ A((#45;1))
    A --- B((1))
    A --- C((1))
    B --- D((0))
    B --- E((#45;1))
    C ~~~ H1( ):::hidden
    C --- F((0))
    E --- G((0))
    E ~~~ H2( ):::hidden

    classDef other stroke:#f05,fill:#f05,color:black;

    T2(No equilibrado):::other ~~~ A2((#45;3)):::other
    A2 --- B2((#45;3)):::other
    A2 --- C2((0)):::other
    B2 --- D2((2)):::other
    B2 ~~~ H3( ):::hidden
    D2 ~~~ H4( ):::hidden
    D2 --- E2((#45;1)):::other
    E2 --- F2((0)):::other
    E2 ~~~ H5( ):::hidden

Árbol B

Árbol B

Un Árbol B es una generalización de un Árbol Equilibrado para almacenar y recuperar información de medios externos, por lo que es necesario operar con muchos más datos.

Los Árboles B utilizan páginas como nodos, que contienen una secuencia de datos ordenados según sus respectivas claves y ramas a otras páginas. Todas las páginas hoja deben estár al mismo nivel, por tanto, es un árbol equilibrado.

Entonces, para representar una página de un Árbol B se necesita:

  • Vector de claves y datos
  • Vector de ramas
  • Número de elementos ocupados

Aplicaciones

  • Bases de datos: es una forma de crear un índice en una BD Relacional
  • Gestión del sistema de archivos por el SO: aumenta la eficacia en la búsqueda de archivos por subdirectorios.
  • Sistemas de compresión de datos
  • Búsquedas externas en discos: se minimiza el número de accesos

==> Si el volumen de datos es grande y se almacenan de forma externa, se deben usar Árboles B.

Tipos de páginas en un Árbol B de orden $m$
Página única$0 \ldots m-1$ claves
$0$ ramas
Raíz$1 \ldots m-1$ claves
$2 \ldots m$ ramas
Página interna$\lfloor m/2 \rfloor \ldots m-1$ claves
$\lceil m/2 \rceil \ldots m$ ramas
Hoja$\lfloor m/2 \rfloor \ldots m-1$ claves
$0$ ramas

También se cumple que:

$$ \text{ramas} \not = 0 \implies \text{claves} = \text{ramas} - 1 $$

Árbol B+

Árbol B+

Se trata de una mejora de los Árboles B.

  • Todas las claves están en hojas y se duplican las necesarias en el resto de páginas para definir los caminos de búsqueda. Las claves internas solo se usan como índices.
  • Aunque hay redundancia de claves, se evita la operación de reorganización.
  • Se pueden vincular las hojas entre sí para un recorrido secuencial más rápido.
  • Todos los caminos desde la raíz hasta cualquier hoja tienen la misma longitud.

Características

Al igual que un Árbol B, un Árbol B+ de orden $m$ tiene:

  • Cada página (salvo la raíz) tiene $n$ elementos, siendo $\lfloor m/2 \rfloor \le n \le m-1$
  • Cada página (salvo la raíz) tiene $r$ ramas, siendo $\lceil m/2 \rceil \le r \le m$
  • La raíz tiene $1 \ldots m-1$ elementos y 2 descendientes como mínimo
  • Las páginas hoja están al mismo nivel
  • Todos los índices indican donde está la información y se almacenan en la raíz y las páginas interiores.
  • Los elementos se insertan en las hojas, y luego los índices crecen «hacia arriba».
typedef struct {
    elem nodos[MAX];
    int cuenta;
} PaginaHoja;

typedef struct {
    clave claves[MAX];
    ArbolBmas ramas[MAX+1];
    int cuenta;
} PaginaInterna;

Resumen

ABBÁrbol binario ordenado.
En promedio opera en $O(\log(n))$, pero en el peor de los casos es $O(n)$ (según el orden de inserción).
HeapÁrbol binario completo parcialmente ordenado.
Se puede implementar mediante arrays y da un soporte eficiente a las colas de prioridad.
AVLÁrbol binario ordenado equilibrado.
Mejora ABB implementando un factor de equilibrio y rotaciones para garantizar $O(\log(n))$.
Árbol BÁrbol ordenado equilibrado de páginas.
Permite almacenar mucha más información con un acceso $O(\log(n))$. Se usa para medios externos cuyo acceso suele ser costoso.
Árbol B+Modificación del árbol B.
Evita la operación de reorganización a coste de almacenar claves duplicadas. Permiten un recorrido secuencial rápido.
Volver a Algoritmos y Estructuras de Datos Siguiente: Grafos