class BinaryTreeTuple: # Sólo para tenerlo en cuenta, un binary tree está compuesto de una raíz (root), ramas (branches) y hojas (leafs); # otra manera de llamarle a las hojas es nodos (nodes). He aquí algunas observaciones sobre binary trees: # 1. La profundidad de un árbol se refiere a todos los niveles que tiene el árbol. # 2. La cardinalidad de un árbol es el número de nodos que este tiene. # 3. Un nodo podría no tener hijos y a lo mucho 2. # 4. Un nodo siempre tiene un padre (y sólo uno), excepto root. # 5. El descenso en árbol se hace a través de las ramas del mismo. # 6. Evaluar un nodo, después de ser encontrado durante el descenso, también se le dice "visitar". # # En esta implementación de binary tree usaremos la estrategia "bottom to top", lo que quiere decir que para obtener # la respuesta partimos de lo general y procesamos hasta llegar a lo particular; una vez que llegamos al caso # particular se construye la respuesta a partir de ese caso hasta regresar a lo general. En el caso de un binary # tree usualmente se dice que bajamos hasta lo más profundo de un árbol (lo cual se hace a través de sus ramas) y # encontrado o no lo que se busca, se construye hacía arriba la respuesta. # # Esta implementación del árbol binario usará el tipo de dato "tuple" como la hoja o nodo. El constructor # inicializa la raíz del árbol ("root") a None porque al momento no hay nodos en el árbol. Por conveniencia, # también habrá un stack que nos servirá para buscar números en el árbol, el cual servirá para llevar memoria # de los nodos visitados durante la búsqueda; este stack nos servirá para agregar un nuevo nodo al árbol, en caso # de ser necesario. def __init__(self): self.root = None self.stack = Stack() # Método "clásico" para saber si hay elementos en el árbol. def tree_empty(self): return self.root is None if True else False # Método para buscar un número en el árbol. Recibe un número como parámetro. Si encuentra el número devolverá # True, de lo contrario False. def number_in_tree(self, number: int) -> bool: # El resultado por default del método. En caso de que el número se encuentre, tomará el valor True. number_found = False # Para buscar el número, creamos un árbol temporal, que será igual al árbol existente. search_root = self.root # Con esta declaración reseteamos el stack de los resultados de la búsqueda. self.stack = None print('Buscar número "%i" en árbol.' % number) # Mientras el árbol de búsqueda no esté vacío o no se haya encontrado el número, la búsqueda continuará. # La condición del while se podría también escribir así: search_root is not None and not number_found. ¿Por qué # la escribí diferente? En este caso "not" es un factor común de ambas evaluaciones, así que lo podemos sacar # para afectar ambas variables, en otras palabras, negar toda la expresión, en vez de hacerlo individualmente; # ahora, como estamos negando toda la expresión, el "and" debe cambiar a "or", es decir, la operación contraria. while not (search_root is None or number_found): # El tipo de dato tuple por si mismo puede descomponerse en sus partes, sin necesidad de hacer nada más. # En este caso, cada elemento del árbol es una tupla compuesta de 3 valores: el número, una rama # izquierda y una rama por la derecha. Python descompone (o explota) la tupla y asigna cada valor # a las variables de la asignación, lo cual hace muy conveniente. En este caso la tupla tiene 3 valores (en # este orden): nùmero, rama izquierda y rama derecha. tuple_num, left, right = search_root # Validar si el número que se busca es igual el que se obtuvo de la tupla. if not number == tuple_num: # Si el número no es igual al de la tupla, etonces guardamos la tupla para usarla después, si es que # agregaremos el número al árbol. self.stack.push(search_root) # Si el número que se busca es mayor al que se obtuvo de la tupla, entonces la búsqueda del # número continuará por la rama derecha, de lo contrario la búsqueda se irá por la izquierda. if number > tuple_num: # El árbol de búsqueda será igual a la rama derecha de la tupla que validamos. search_root = right else: # De lo contario, el árbol de búsqueda será igual a la rama de la deracha de la tupla. search_root = left else: # En caso de que el número se encuentre, entonces se devolverá True. print('El número %i ya existe en árbol.' % number) number_found = True # Devolver el resultado de la búsqueda. return number_found # Este método agregará al árbol el número que se pasa como parámetro, si es que este no existe ya en el árbol. def add_number(self, number: int): # Si el número no existe en el árbol, entonces agregarlo al árbol. if not self.number_in_tree(number): print('Add Number "%i".' % number) # Se crea una tupla para el número que se pasó como parámetro. new_tree = tuple([number, None, None]) # El método "number_in_tree" crea lo que llamaremos el stack de la respuesta. Con ese stack podemos # construir el árbol que incluye el número que obtuvimos como parámentro. while not self.stack.stack_empty(): # Se obtine la tupla y se descompone en sus valores atómicos, es decir, el número, rama izquierda y # rama derecha. node_value, node_left, node_right = self.stack.pop() # Si el número que se pasó como parámetro es mayor al valor de la tupla, entonces la nueva tupla # se asigna como la rama derecha de la tupla que se obtuvo del stack, de lo contrario se asignará # a la rama izquierda. if number > node_value: node_right = new_tree else: node_left = new_tree # Ahora el árbol nuevo será igual a la rama recién creada. new_tree = tuple([node_value, node_left, node_right]) # Finalmente, una vez reconstruido el árbol con el número que recibimos, se asigna a la ráiz del árbol. self.root = new_tree # Mostrar el árbol final. print('Árbol: %s' % str(self.root))
class UnaCalculadoraMuySimple(Queue): # Esta es una implementación MUY simple de una calculadora... Y a pesar de ello, como sea quedó un código # medio largo, pero espero que no sea dificil de comprender. No estoy validando la precedencia de operadores, # tampoco si hay parentesis. Los dígitos y operadores se evaluaran en el orden que estén en la cadena y el # resultado de la operación anterior se acumulará para ser utilizado en la siguiente operación. # Método constructor que podría recibir una expresión para inicializar el objeto. def __init__(self, calculation=None): # Inicialización de la clase Queue con la cadena que se pase como parámetro. # Tal como pasa con la declaración de Queue (donde inicializamos la clase "list" de la que se hereda) # hacemos lo mismo en esta clase al inicializar Queue. Queue.__init__(self, calculation) # Declaración de stacks para llevar control de las operaciones y dígitos. self.opers = Stack() self.numbers = Stack() # Método para pasar una expresión al objeto. def set_calculation(self, calculation): self.__init__(calculation) # Muestra la cadena que se pasó para evaluar. def show_calculation(self): self.show_queue() # Evaluar y resolver expresión. def sort_calculation(self): rslt = None # Con esta variable se devolverá el mensaje de error, en caso de haber alguno. calculation = 0 # Variable con la que se "acumulará" el resultado de las operaciones. # Primero revisar que se haya pasado alguna cadena para evaluarse. if not self.queue_empty(): numero = '' # Dado que la cadena no está vacía, vamos a leerla mientras haya valores. while not self.queue_empty(): # Obtenemos el siguiente elemento de la cadena. element = self.dequeue() # Si el elemento es un espacio, entonces ignorarlo. if not element == ' ': # Si es una operación, meterla al stack de operaciones. if element in ('+', '-', '*', '/'): # Cuando se encuentra una operación quiere decir que no hay más números a concatenar, así que # el número completo se mete al stack de números, luego se reinicia la variable. if not numero == '': self.numbers.push(float(numero)) numero = '' else: # En caso de haber una operación, pero no un número, entonces la expresión es inválida. break self.opers.push(element) elif element.isdigit() or (element == '.' and not numero == ''): # Dado que la expresión se evalua elemento por elemento, se obtendrá dígito por dígito, # entonces según aparezcan se irán concatenado para formar el número completo. En caso de que # el número tenga decimales, concatenar el punto al número completo. numero = element + numero else: # En caso de que haya un elemento que no sea una operación o dígito, devolver error. rslt = 'La cadena del cálculo es inválida.' break # Este comando es para terminar (o salir) el ciclo. if not numero == '': print('Número completo: %s.' % numero) self.numbers.push(float(numero)) # Si no hay un mensaje de error, hay operandores que procesar y al menos 2 digitos, # se procede a evaluar la expresión. if rslt is None and not (self.opers.stack_empty() or self.numbers.stack_size() < 2): # Tomamos el primer valor a evaluar. Esto podría interprestarse como el primer # resultado de evaluar la expresión. calculation = self.numbers.pop() # Ciclarse mientras haya dígitos en el stack de dígitos. while not self.numbers.stack_empty(): # Proceder sólo si hay operaciones disponible. if not self.opers.stack_empty(): # Sacar la siguiente operación disponible. oper = self.opers.pop() # Validar que haya otro dígito a considerar. if not self.numbers.stack_empty(): # Tomar el siguiente dígito a evaluar. digit = self.numbers.pop() # Este es un mensaje de seguimiento para ver qué sucede durante la evaluación # de la expresión. print('Operación: %f %s %f' % (calculation, oper, digit)) # Ejecutar operación con el valor acumulado y el último dígito obtenido. # El resultado de la operación se acumula en la variable "calculation". if oper == '+': calculation += digit elif oper == '-': calculation -= digit elif oper == '*': calculation *= digit elif oper == '/': # Esto es para evitar una excepción por dividir entre 0. if not digit == 0: calculation /= digit else: # En caso de que el dígito sea un cero, el valor acumulado al momento será cero. calculation = 0 else: # Si falta un dígito para continuar con la evaluación de la expresión, devolver error. rslt = 'Falta al menos un dígito para la operación %s.' % oper break # Este comando es para terminar el ciclo. else: # En caso que falté algún operador para seguir evaluando la expresión, devolver error. rslt = 'Falta operando.' break # Este comando es para terminar el ciclo. else: # En caso de que haya un error previo, no haya operaciones o al menos 2 dígitos, devolver error. rslt = 'La cadena del cálculo es inválida.' else: # En caso de que no haya una expresión a evaluar, devolver error. rslt = 'No hay una expresión a procesar.' # Si no hubo algún error durante la evaluación de la expresión, mostrar el resultado... if rslt is None: print('El resultado del cálculo es: %s.' % str(calculation)) else: # ... De lo contrario mostrar el mensaje de error. print(rslt)
class BinaryTreeNode: # En función esta clase funciona igual que "BinaryTreeTuple", con la diferencia que usamos el objeto Node en vez # de tuple para almacenar el número y las ramas que salen de cada nodo. De ahí que no hondaré en explicaciones # porque los métodos hacen exactamente lo mismo que en "BinaryTreeTuple". def __init__(self): self.root = None self.stack = Stack() # Este método busca un número en el binary tree. Durante la búsqueda guardará los nodos que visita en caso de # este número se vaya a agregar al árbol. def is_num_tree(self, number: int) -> bool: # El valor default de la respuesta; dado que una posibilidad es que árbol esté vacío, entonces inmediatamente # brincará al return del método. node_found = False # Primero validar que haya nodos en el árbol. print(' Search %i in the tree.' % number) if self.root is not None: # Si no hay nodos en el árbol obtenemos la raíz del árbol; recordar que esta raíz podría tener más nodos # abajo de ella. print(' * Root is not empty, then proceed with the search.') root = self.root # Con esta declaración reseteamos el stack de búsqueda. print(' * Reset search stack.') self.stack = Stack() while not (root is None or node_found): print(' * Is %i equal to %i?' % (number, root.get_value())) if not number == root.get_value(): print(' * No, then add current node to the stack.') self.stack.push(root) print(' * Is %i less than %i?' % (number, root.get_value())) if number < root.get_value(): print(' --> Yes, then search for it in the LEFT branch.') root = root.get_left_node() else: print(' --> No, then search for it in the RIGHT branch.') root = root.get_right_node() else: print(' --> Yes, then the number exists in the tree.') node_found = True if not node_found: print(' --> The number does ** NOT ** exists in the tree.') return node_found # Tal como lo hicimos en la clase "BinaryTreeTuple" este método agregará el número al binary tree. def add_number(self, number: int): # Buscamos el número en el árbol para saber si existe. Tal como en "BinaryTreeTuple" el stack que se genere # durante la búsqueda nos servirá para armar el nuevo árbol con el número que entra como parámetro. print('* First, check if %i exists in the tree.' % number) if not self.is_num_tree(number): # De entrada este nodo será el que tenga el valor que queremos agregar al árbol, pero reutilizaremos # este objeto para construir el árbol que incluya el nodo nuevo. print('* Add %i to the tree.' % number) new_tree = Node(number) # Ahora vamos a traer los nodos que se guardaron durante la búsqueda para hacerlos parte del nuevo # árbol que incluye el nodo nuevo. while not self.stack.stack_empty(): stack_node = self.stack.pop() # Vemos en qué rama debe ir el árbol con la respuesta. if new_tree.get_value() > stack_node.get_value(): stack_node.set_right_node(new_tree) else: stack_node.set_left_node(new_tree) # Asignamos el árbol recién armado al nodo que trae el resultado de agregar el nuevo nodo. new_tree = stack_node # Una vez que todos los nodos han sido considerados, el árbol final que quedó en "new_tree" es el árbol # anterior, pero incluyendo el nodo nuevo, así que "new_tree" se convierte en root. self.root = new_tree print('--> Number added to the tree.')