!wget --no-cache -O init.py -q https://raw.githubusercontent.com/rramosp/20192.L3/master/init.py
import init; init.init(force_download=False); init.get_weblink()

LAB 07 Árboles de búsqueda

import init
from local.lib.rlxmoocapi import submit, session
import inspect
student = session.Session(init.endpoint).login( course_id=init.course_id, 
                                                session_id="UDEA", 
                                                lab_id="lab_07" )
import numpy as np

class BTNode(object):

        def __init__(self, value, left=None, right=None):
            
            self.value = value
            self.left  = None
            self.right = None
            self.parent = None
            if left is not None:
                self.add_left(left)
            if right is not None:
                self.add_right(right)

        def add_left(self, value):
            assert self.left is None, "node already has left child"
            self.left  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.left.parent = self
            return self

        def add_right(self, value):
            assert self.right is None, "node already has right child"
            self.right  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.right.parent = self
            return self

        def swap_children(self):
            tmp = self.left
            self.left = self.right
            self.right = self.left
            return self

        def insert_ordered(self, new_value):
            if new_value < self.value:
                if self.left is None:
                    self.add_left(new_value)
                    return self.left
                else:
                    return self.left.insert_ordered(new_value)
            else:
                if self.right is None:
                    self.add_right(new_value)
                    return self.right
                else:
                    return self.right.insert_ordered(new_value)

        def ird(self):
            if self.value==None:
                return []
            s1 = self.left.ird() if self.left is not None else []
            s2 = self.right.ird() if self.right is not None else []
            return s1+[self.value]+s2    

        def to_indented_string(self, level, prefix=""):
            s = (" "*2*level + prefix + str(self.value) + "\n") if self.value is not None else ""
            s += self.left.to_indented_string(level+1, prefix="L: ") if self.left is not None else ""
            s += self.right.to_indented_string(level+1, prefix="R: ") if self.right is not None else ""
            return s       

        def __repr__(self):
            return self.to_indented_string(0, prefix="root: ")

        def clone(self):
            r = self.__class__(self.value)

            if self.left is not None:
                r.left = self.left.clone()
                r.left.parent = r
            if self.right is not None:
                r.right = self.right.clone()
                r.right.parent = r

            return r

        @classmethod
        def from_list(cls, a_list):
            r = cls(a_list[0])
            for i in a_list[1:]:
                r.insert_ordered(i)
            return r

        @classmethod
        def sort_list(cls, a_list):
            r = cls.from_list(a_list)
            return np.r_[r.ird()]
k = BTNode.from_list([10,2,12,1,4])
k.ird()
k.clone().ird()

Árboles AVL

Un árbol binario de búsqueda se cumple que para cualquier nodo x, todos los datos de los nodos a la izquierda de x son menores que el dato de x y todos los datos de los nodos a la derecha de x son mayores que el dato de x.

Esto nos permite realizar búsquedas binarias sobre el árbol de manera eficiente. Si el árbol no está balanceado, la búsqueda se vuelve menos eficiente, con lo que nos interesa que, según insertamos nuevos nodos, el árbol se mantenga binario de búsqueda y balanceado.

P.ej. el siguiente árbol no es balanceado porque tiene todos los nodos colgando siempre de la rama derecha.

k = BTNode.from_list([10,12,14,18,20])
print(k)
k.ird()

en cambio el siguiente tiene los nodos más balencados y produce el mismo IRD

k = BTNode.from_list([14,10,20,18,12])
print(k)
k.ird()

dado un nodo cualquiera, el factor de balance se define como la diferencia de la altura de su hijo izquierda y la de su hijo derecho.

Un árbol AVL (por los soviéticos Adelsson-Velski y Landis, que lo inventaron) es un árbol en el que el factor de balance de cualquier nodo es estrictamente menor que 2.

En los siguientes ejercicios desarrollarás métodos que ayudan a convertir un árbol binario de búsqueda en un árbol AVL, rebalanceando los nodos, sin perder sus propiedades para la búsqueda binaria.

Ejercicio 1

completa el método height para que devuelva la altura de un nodo y el método balance_factor para que calcule el factor de balanceo de cada nodo del árbol.

Ha de retornar un árbol nuevo con la misma estructura cuyos valores es el factor de balanceo de cada nodo correspondiente del árbol original.

class BTNode(object):

        def __init__(self, value, left=None, right=None):
            
            self.value = value
            self.left  = None
            self.right = None
            self.parent = None
            if left is not None:
                self.add_left(left)
            if right is not None:
                self.add_right(right)

        def add_left(self, value):
            assert self.left is None, "node already has left child"
            self.left  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.left.parent = self
            return self

        def add_right(self, value):
            assert self.right is None, "node already has right child"
            self.right  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.right.parent = self
            return self

        def swap_children(self):
            tmp = self.left
            self.left = self.right
            self.right = self.left
            return self

        def insert_ordered(self, new_value):
            if new_value < self.value:
                if self.left is None:
                    self.add_left(new_value)
                    return self.left
                else:
                    return self.left.insert_ordered(new_value)
            else:
                if self.right is None:
                    self.add_right(new_value)
                    return self.right
                else:
                    return self.right.insert_ordered(new_value)

        def ird(self):
            if self.value==None:
                return []
            s1 = self.left.ird() if self.left is not None else []
            s2 = self.right.ird() if self.right is not None else []
            return s1+[self.value]+s2    

        def to_indented_string(self, level, prefix=""):
            s = (" "*2*level + prefix + str(self.value) + "\n") if self.value is not None else ""
            s += self.left.to_indented_string(level+1, prefix="L: ") if self.left is not None else ""
            s += self.right.to_indented_string(level+1, prefix="R: ") if self.right is not None else ""
            return s       

        def __repr__(self):
            return self.to_indented_string(0, prefix="root: ")

        def clone(self):
            r = self.__class__(self.value)

            if self.left is not None:
                r.left = self.left.clone()
                r.left.parent = r
            if self.right is not None:
                r.right = self.right.clone()
                r.right.parent = r

            return r

        @classmethod
        def from_list(cls, a_list):
            r = cls(a_list[0])
            for i in a_list[1:]:
                r.insert_ordered(i)
            return r

        @classmethod
        def sort_list(cls, a_list):
            r = cls.from_list(a_list)
            return np.r_[r.ird()]

def BTNode1(*args,**kwargs):
    
    import numpy as np
    
    class BTNode1_class(BTNode):

        def height(self):

            result = # TU CODIGO AQUI

            return result

        def balance_factor(self):
            
            result = # TU CODIGO AQUI        
            return result

        def balance_factor_tree(self):
            r = self.__class__(self.balance_factor(), \
                               left=self.left.balance_factor_tree() if self.left is not None else None,
                               right=self.right.balance_factor_tree() if self.right is not None else None,
                              )
            return r
    if "return_class" in kwargs.keys() and kwargs["return_class"]:
        return BTNode1_class
    else:
        return BTNode1_class(*args,**kwargs)
x = BTNode1(value="x")
z = BTNode1(value="z", left=x)
y = BTNode1(value="y", left=z)
y

por ejemplo, para el siguiente árbol

k = BTNode1.from_list([2,1,12,-11,4,3,6,5,1])

---
2
  L: 1
    L: -11
    R: 1
  R: 12
    L: 4
      L: 3
      R: 6
        L: 5    
  • su altura es 4

  • su factor de balanceo (el del nodo raíz) es -2

  • el árbol de factores de balanceo de todos los nodos es

    root: -2 L: 0 L: 0 R: 0 R: 3 L: -1 L: 0 R: 1 L: 0

k = BTNode1(return_class=True).from_list([2,1,12,-11,4,3,6,5,1])
k
k.height()
k.balance_factor()
k.balance_factor_tree()

y para el siguiente árbol

  • su factor de balance es 2

  • su altura es 2

registra tu solución en línea

student.submit_task(globals(), task_id="task_01");

Ejercicio 2

Una operación de balanceo consiste en reacomodar los registros de un árbol binario de tal forma que los factores de balance de todos los registros sean -1, 0, ó +1 y que el recorrido INORDEN sea el mismo que antes del reacomodo.

Operaciones de Rebalanceo:

  1. Una rotación a la derecha.

  2. Una rotación a la izquierda.

  3. Doble rotación a la derecha.

  4. Doble rotación a la izquierda.

Para explicar lo referente a las rotaciones asumamos la siguiente convención:

Sea Fb(P) el factor de balance de un nodo.

Sea P un nodo con factor de balance no permitido.(p.ej. Fb(P)=+2 ó Fb(P)=-2).

Sea Q la dirección del hijo izquierdo o del hijo derecho de P, dependiendo de si Fb(P)=+2 ó Fb(P)= - 2, es decir, si factor de balance de P es +2 entonces Q es el hijo izquierdo de P y si factor de balance de P es -2 entonces Q es el hijo derecho de P.

Rotación a la derecha

Se realiza cuando Fb(P)=+2 y Fb(Q)=+1, y consiste en girar, en sentido de las manecillas del reloj, el registro P alrededor del registro Q. Con lo que:

  • P pasará a ser el nuevo hijo derecho de Q.

  • El anterior hijo derecho de Q será el nuevo hijo izquierdo de P.

  • Q será la nueva raíz del árbol balanceado.

  • Los nuevos factores de balance de P y Q serán cero.

  • La altura del árbol balanceado disminuye en uno.

La siguiente figura ilustra esto

from IPython.display import Image
Image(filename='local/imgs/AVL-rotright.jpg') 

completa el método rotate_right para que el nodo realice una rotación a la derecha con si hizo izquierdo.

realiza un assert al principio del método para verificar la condición Fb(P)=+2 y Fb(Q)=+1

ATENCIÓN: opera sobre una copia del propio árbol (usa clone), y así no lo modificarás.

class BTNode(object):

        def __init__(self, value, left=None, right=None):
            
            self.value = value
            self.left  = None
            self.right = None
            self.parent = None
            if left is not None:
                self.add_left(left)
            if right is not None:
                self.add_right(right)

        def add_left(self, value):
            assert self.left is None, "node already has left child"
            self.left  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.left.parent = self
            return self

        def add_right(self, value):
            assert self.right is None, "node already has right child"
            self.right  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.right.parent = self
            return self

        def swap_children(self):
            tmp = self.left
            self.left = self.right
            self.right = self.left
            return self

        def insert_ordered(self, new_value):
            if new_value < self.value:
                if self.left is None:
                    self.add_left(new_value)
                    return self.left
                else:
                    return self.left.insert_ordered(new_value)
            else:
                if self.right is None:
                    self.add_right(new_value)
                    return self.right
                else:
                    return self.right.insert_ordered(new_value)

        def ird(self):
            if self.value==None:
                return []
            s1 = self.left.ird() if self.left is not None else []
            s2 = self.right.ird() if self.right is not None else []
            return s1+[self.value]+s2    

        def to_indented_string(self, level, prefix=""):
            s = (" "*2*level + prefix + str(self.value) + "\n") if self.value is not None else ""
            s += self.left.to_indented_string(level+1, prefix="L: ") if self.left is not None else ""
            s += self.right.to_indented_string(level+1, prefix="R: ") if self.right is not None else ""
            return s       

        def __repr__(self):
            return self.to_indented_string(0, prefix="root: ")

        def clone(self):
            r = self.__class__(self.value)

            if self.left is not None:
                r.left = self.left.clone()
                r.left.parent = r
            if self.right is not None:
                r.right = self.right.clone()
                r.right.parent = r

            return r

        @classmethod
        def from_list(cls, a_list):
            r = cls(a_list[0])
            for i in a_list[1:]:
                r.insert_ordered(i)
            return r

        @classmethod
        def sort_list(cls, a_list):
            r = cls.from_list(a_list)
            return np.r_[r.ird()]

def BTNode2(*args,**kwargs):
    
    
    
    class BTNode2_class(BTNode):

        def height(self):

            result = # TU CODIGO AQUI

            return result

        def balance_factor(self):
            height_r = 0 if self.right is None else self.right.height()+1
            height_l = 0 if self.left is None else self.left.height()+1
            return height_l - height_r

        def balance_factor_tree(self):
            r = self.__class__(self.balance_factor(), \
                               left=self.left.balance_factor_tree() if self.left is not None else None,
                               right=self.right.balance_factor_tree() if self.right is not None else None,
                              )
            return r

        def rotate_right(self):
            assert # TU CODIGO AQUI
                
            result = # TU CODIGO AQUI

            return result
        
    if "return_class" in kwargs.keys() and kwargs["return_class"]:
        return BTNode2_class
    else:
        return BTNode2_class(*args,**kwargs)

prueba tu código. una rotación derecha con el siguiente nodo

k = BTNode2("c", left=BTNode2("b", left=BTNode2("a")))
k

debería de dar

b
  L: a
  R: c
k.rotate_right()
k = BTNode2("c", left=BTNode2("b", left=BTNode2("a")))
print(k.ird())
print(k.rotate_right().ird())

y con el siguiente nodo

k = BTNode2("e", left=BTNode2("c", left=BTNode2("b", left="a"), right="d"), right="f")
k

debería de dar

    c
      L: b
        L: a
      R: e
        L: d
        R: f
print(k)
print(k.ird())
print(k.rotate_right())
print(k.rotate_right().ird())

registra tu solución en línea

student.submit_task(globals(), task_id="task_02");

Ejercicio 3

Haremos ahora una rotación a la izquierda. Se realiza cuando Fb(P)=-2 y Fb(Q)=-1 y, por tanto, Q es el hijo derecho de P

  • P será el nuevo hijo izquierdo de Q.

  • El nuevo hijo derecho de P será el anterior hijo izquierdo Q.

  • Q será la nueva raíz del árbol balanceado.

  • Los factores de balance P y Q quedarán en cero.

  • La altura del árbol balanceado disminuye en uno (1).

class BTNode(object):

        def __init__(self, value, left=None, right=None):
            
            self.value = value
            self.left  = None
            self.right = None
            self.parent = None
            if left is not None:
                self.add_left(left)
            if right is not None:
                self.add_right(right)

        def add_left(self, value):
            assert self.left is None, "node already has left child"
            self.left  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.left.parent = self
            return self

        def add_right(self, value):
            assert self.right is None, "node already has right child"
            self.right  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.right.parent = self
            return self

        def swap_children(self):
            tmp = self.left
            self.left = self.right
            self.right = self.left
            return self

        def insert_ordered(self, new_value):
            if new_value < self.value:
                if self.left is None:
                    self.add_left(new_value)
                    return self.left
                else:
                    return self.left.insert_ordered(new_value)
            else:
                if self.right is None:
                    self.add_right(new_value)
                    return self.right
                else:
                    return self.right.insert_ordered(new_value)

        def ird(self):
            if self.value==None:
                return []
            s1 = self.left.ird() if self.left is not None else []
            s2 = self.right.ird() if self.right is not None else []
            return s1+[self.value]+s2    

        def to_indented_string(self, level, prefix=""):
            s = (" "*2*level + prefix + str(self.value) + "\n") if self.value is not None else ""
            s += self.left.to_indented_string(level+1, prefix="L: ") if self.left is not None else ""
            s += self.right.to_indented_string(level+1, prefix="R: ") if self.right is not None else ""
            return s       

        def __repr__(self):
            return self.to_indented_string(0, prefix="root: ")

        def clone(self):
            r = self.__class__(self.value)

            if self.left is not None:
                r.left = self.left.clone()
                r.left.parent = r
            if self.right is not None:
                r.right = self.right.clone()
                r.right.parent = r

            return r

        @classmethod
        def from_list(cls, a_list):
            r = cls(a_list[0])
            for i in a_list[1:]:
                r.insert_ordered(i)
            return r

        @classmethod
        def sort_list(cls, a_list):
            r = cls.from_list(a_list)
            return np.r_[r.ird()]

def BTNode3(*args,**kwargs):
    
    class BTNode3_class(BTNode):

        def height(self):

            result = # TU CODIGO AQUI
        
            return result

        def balance_factor(self):
            result = # TU CODIGO AQUI
        
            return result

        def balance_factor_tree(self):
            r = self.__class__(self.balance_factor(), \
                               left=self.left.balance_factor_tree() if self.left is not None else None,
                               right=self.right.balance_factor_tree() if self.right is not None else None,
                              )
            return r


        def rotate_left(self):
            assert # TU CODIGO AQUI
                
            result = # TU CODIGO AQUI

            return result
        
    if "return_class" in kwargs.keys() and kwargs["return_class"]:
        return BTNode3_class
    else:
        return BTNode3_class(*args,**kwargs)

prueba tu código

k = BTNode3("b", right=BTNode3("d", right=BTNode3("e", right="f"), left="c"), left="a")
print(k)
print(k.ird())
print(k.rotate_left())
print(k.rotate_left().ird())

registra tu solución en línea

student.submit_task(globals(), task_id="task_03");

Ejercicio 4

Haremos ahora una doble rotación a la derecha. Se realiza cuando Fb(P)=+2 y Fb(Q)=-1 y, por tanto, Q es el hijo derecho de P

En este caso consideramos R el nodo que representa el hijo izquierdo o el hijo derecho de Q dependiendo de si el factor de balance de Q es +1 ó -1. Es decir, si el factor de balance del registro Q es +1, el registro R es el hijo izquierdo del registro Q, y si el factor de balance de Q es -1, R es el hijo derecho del registro Q.

Esta rotación consiste en una rotación a la izquierda de Q alrededor de R seguida de una rotación a la derecha de P alrededor de R, de forma que:

  • P será el nuevo hijo izquierdo de Q.

  • El nuevo hijo derecho de P será el anterior hijo izquierdo Q.

  • Q será la nueva raíz del árbol balanceado.

  • Los factores de balance P y Q quedarán en cero.

  • La altura del árbol balanceado disminuye en uno (1).

class BTNode(object):

        def __init__(self, value, left=None, right=None):
            
            self.value = value
            self.left  = None
            self.right = None
            self.parent = None
            if left is not None:
                self.add_left(left)
            if right is not None:
                self.add_right(right)

        def add_left(self, value):
            assert self.left is None, "node already has left child"
            self.left  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.left.parent = self
            return self

        def add_right(self, value):
            assert self.right is None, "node already has right child"
            self.right  = self.__class__(value) if value.__class__.__name__!=self.__class__.__name__ else value
            self.right.parent = self
            return self

        def swap_children(self):
            tmp = self.left
            self.left = self.right
            self.right = self.left
            return self

        def insert_ordered(self, new_value):
            if new_value < self.value:
                if self.left is None:
                    self.add_left(new_value)
                    return self.left
                else:
                    return self.left.insert_ordered(new_value)
            else:
                if self.right is None:
                    self.add_right(new_value)
                    return self.right
                else:
                    return self.right.insert_ordered(new_value)

        def ird(self):
            if self.value==None:
                return []
            s1 = self.left.ird() if self.left is not None else []
            s2 = self.right.ird() if self.right is not None else []
            return s1+[self.value]+s2    

        def to_indented_string(self, level, prefix=""):
            s = (" "*2*level + prefix + str(self.value) + "\n") if self.value is not None else ""
            s += self.left.to_indented_string(level+1, prefix="L: ") if self.left is not None else ""
            s += self.right.to_indented_string(level+1, prefix="R: ") if self.right is not None else ""
            return s       

        def __repr__(self):
            return self.to_indented_string(0, prefix="root: ")

        def clone(self):
            r = self.__class__(self.value)

            if self.left is not None:
                r.left = self.left.clone()
                r.left.parent = r
            if self.right is not None:
                r.right = self.right.clone()
                r.right.parent = r

            return r

        @classmethod
        def from_list(cls, a_list):
            r = cls(a_list[0])
            for i in a_list[1:]:
                r.insert_ordered(i)
            return r

        @classmethod
        def sort_list(cls, a_list):
            r = cls.from_list(a_list)
            return np.r_[r.ird()]

def BTNode4(*args,**kwargs):
    
    class BTNode4_class(BTNode):

        def height(self):

            result = # TU CODIGO AQUI
        
            return result

        def balance_factor(self):
            result = # TU CODIGO AQUI
        
            return result

        def balance_factor_tree(self):
            r = self.__class__(self.balance_factor(), \
                               left=self.left.balance_factor_tree() if self.left is not None else None,
                               right=self.right.balance_factor_tree() if self.right is not None else None,
                              )
            return r

        def double_rotate_right(self):
            assert # TU CODIGO AQUI

            result = # TU CODIGO AQUI

            return result
        
        
        
    if "return_class" in kwargs.keys() and kwargs["return_class"]:
        return BTNode4_class
    else:
        return BTNode4_class(*args,**kwargs)
k = BTNode4("c", left=BTNode4("a", right="b"))
print(k)
print(k.ird())
print(k.double_rotate_right())
print(k.double_rotate_right().ird())
k = BTNode4("e", left=BTNode4("b", left="a", right=BTNode4("d", left="c")), right="f")
print(k)
print(k.ird())
print(k.double_rotate_right())
print(k.double_rotate_right().ird())
k = BTNode4("e", left=BTNode4("b", left="c", right=BTNode4("a", right="d")), right="f")
print(k)
print(k.ird())
print(k.double_rotate_right())
print(k.double_rotate_right().ird())

registra tu solución en línea

student.submit_task(globals(), task_id="task_04");