Jazmin Barrionuevo

Jazmin Barrionuevo

Algorithms

2023

2-minute read

Python

This project is dedicated to implementing various algorithms using Python that I have developed during my Associate's Degree in Artificial Intelligence. The project features a range of algorithms, including some sorting methods, binary tree structures, and pathfinding techniques.

Apart from what I learned at the university, I also enjoy reading. Here are some of the books that have helped me learn and improve my understanding of algorithms: Learning Algorithms: A Programmer’s Guide to Writing Better Code by O'Reilly and Data Structures & Algorithms in Python by Michael T. Goodrich.

I wont be showing all the algorithms but you can find the complete code here.

Sorting methods

  1. Stack
  2. Last-In-First-Out data structure

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 %%file test_stack.py class Stack: def __init__(self): self.items = [] def push(self, item): self.items.append(item) def pop(self): return self.items.pop(-1) def isEmpty(self): return self.items == [] def __str__(self): return str(self.items)
  3. Queue
  4. First-In-First-Out data structure

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 %%file test_supermarket.py from test_stack import Stack class Queue: def __init__(self): self.items = [] def insert(self, element): self.items.append(element) def remove(self): return self.items.pop(0) def isEmpty(self): return self.items == [] class Cinta: def __init__(self, carritos): self.carritos = carritos def historial(self): productos = [] while not self.carritos.isEmpty(): # Queue carrito = self.carritos.remove() while not carrito.isEmpty(): # Stack producto = carrito.pop() productos.append(str(producto)) return productos class Producto: def __init__(self, nombre): self.nombre = nombre def __str__(self): return str(self.nombre) def carrito(*nombres): carrito = Stack() for nombre in nombres: carrito.push(nombre) return carrito def test_historial_de_supermercado(): carrito1 = carrito("Leche", "Huevos", "Mermelada") carrito2 = carrito("Harina", "Tomate", "Manteca") carrito3 = carrito("Vino", "Pescado", "Aceitunas") cola = Queue() cola.insert(carrito1) cola.insert(carrito2) cola.insert(carrito3) cinta = Cinta(cola) assert cinta.historial() == [ "Mermelada", "Huevos", "Leche", "Manteca", "Tomate", "Harina", "Aceitunas", "Pescado", "Vino" ]
  5. Priority Queue
  6. Here the elements have associated priorities

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 %%file test_priority_queue.py class PriorityQueue: def __init__(self): self.queue = [] def is_empty(self): return len(self.queue) == 0 def insert(self, item, priority): element = (priority, item) if self.is_empty(): self.queue.append(element) else: inserted = False for i in range(len(self.queue)): if self.queue[i][0] > priority: self.queue.insert(i, element) inserted = True break if not inserted: self.queue.append(element) def remove(self): if self.is_empty(): return None return self.queue.pop(0)[1] def peek(self): if self.is_empty(): return None return self.queue[0][1] def size(self): return len(self.queue) def __str__(self): return str([item for _, item in self.queue])

Binary Trees

  1. Binary Tree
  2. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 %%file test_tree.py class Tree: def __init__(self, value, left=None, right=None): self.value = value self.left = left self.right = right def nodos(self): totales = 1 totales += self.left.nodos() if self.left is not None else 0 totales += self.right.nodos() if self.right is not None else 0 return totales def menor_mayor(self): minimo = maximo = self.value if self.left is not None: (lminimo, lmaximo) = self.left.menor_mayor() minimo = min(minimo, lminimo) maximo = max(maximo, lmaximo) if self.right is not None: (rminimo, rmaximo) = self.right.menor_mayor() minimo = min(minimo, rminimo) maximo = max(maximo, rmaximo) return (minimo, maximo) def buscar(self, element): if self.value == element: return True if self.left is not None and self.left.buscar(element): return True if self.right is not None and self.right.buscar(element): return True return False def altura(self): altural = 0 if self.left is None else self.left.altura() alturar = 0 if self.right is None else self.right.altura() return 1 + max(altural, alturar)
  3. Binary Search Tree
  4. This algorithm is a special type of binary tree that focus on efficient searching, insertion, and deletion operations. And like George Heineman says, it is 'the godfather of all recursive data structures'.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 %%file test_binary_search_tree.py from test_tree import Tree class BSTree(Tree): def menor_mayor(self): current = self while current.left is not None: current = current.left menor = current.value current = self while current.right is not None: current = current.right mayor = current.value return (menor, mayor) def buscar(self, val): if self.value == val: return True if self.left is not None and val < self.value: return self.left.buscar(val) if self.right is not None and val > self.value: return self.right.buscar(val) return False def insertar(self, val): if val <= self.value: if self.left is None: self.left = BSTree(val) else: self.left.insertar(val) else: if self.right is None: self.right = BSTree(val) else: self.right.insertar(val)

Path Finding

  1. Breadth First Search
  2. Guarantees the shortest path in unweighted graphs

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from ..models.grid import Grid from ..models.frontier import QueueFrontier from ..models.solution import NoSolution, Solution from ..models.node import Node class BreadthFirstSearch: @staticmethod def search(grid: Grid) -> Solution: node = Node("", state = grid.start, cost = 0) explored = {} explored[node.state] = True if grid.end == node.state: return Solution(node,explored) frontier = QueueFrontier() frontier.add(node) while True: if frontier.is_empty(): return NoSolution(explored) node = frontier.remove() explored[node.state] = True neighbours = grid.get_neighbours(node.state) for accion in neighbours.keys(): new_state = neighbours[accion] new_node = Node("", new_state, node.cost + grid.get_cost(new_state)) new_node.parent = node new_node.action = accion if grid.end == new_node.state: return Solution(node,explored) if new_node.state not in explored.keys(): explored[new_node.state] = True frontier.add(new_node)
  3. Depth First Search
  4. This one uses less memory than BFS for wide graphs and is very useful for decision tree problems

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from ..models.grid import Grid from ..models.frontier import StackFrontier from ..models.solution import NoSolution, Solution from ..models.node import Node class DepthFirstSearch: @staticmethod def search(grid: Grid) -> Solution: node = Node("", grid.start, 0) frontier = StackFrontier() frontier.add(node) explored = {} #explored[node.state] = True while True: if frontier.is_empty(): return NoSolution(explored) node = frontier.remove() if grid.end == node.state: return Solution(node,explored) if node.state not in explored.keys(): explored[node.state] = True neighbours = grid.get_neighbours(node.state) for accion in neighbours.keys(): new_state = neighbours[accion] new_node = Node("", new_state, node.cost + grid.get_cost(new_state)) new_node.parent = node new_node.action = accion if new_node not in explored: frontier.add(new_node) return NoSolution(explored)
  5. Greedy Best-First Search
  6. GBFS can find a solution without exploring the entire graph and it is more informed than BFS or DFS due to the use of a heuristic.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 from ..models.grid import Grid from ..models.frontier import PriorityQueueFrontier from ..models.solution import NoSolution, Solution from ..models.node import Node def manjaran(node, objetivo): discol = abs(node.state[0] - objetivo[0]) disfil = abs(node.state[1] - objetivo[1]) dist = discol**2 + disfil**2 return dist**(1/2) class GreedyBestFirstSearch: @staticmethod def search(grid: Grid) -> Solution: node = Node("", grid.start, 0) explored = {} explored[node.state] = node if node.state == grid.end: return Solution(node, explored) frontier = PriorityQueueFrontier() frontier.add(node, manjaran(node,grid.end)) while True: if frontier.is_empty(): return NoSolution(explored) node = frontier.pop() if grid.end == node.state: return Solution(node,explored) neighbours = grid.get_neighbours(node.state) for accion in neighbours.keys(): new_state = neighbours[accion] new_node = Node("", new_state, node.cost + grid.get_cost(new_state)) new_node.parent = node new_node.action = accion if new_node.state not in explored or new_node.cost < explored[new_node.state].cost: explored[new_node.state] = new_node frontier.add(new_node, manjaran(new_node, grid.end))
  7. A*
  8. This one will always find a solution if one exists and it is generally faster.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from ..models.grid import Grid from ..models.frontier import PriorityQueueFrontier from ..models.solution import NoSolution, Solution from ..models.node import Node def manjaran(node, objetivo): discol = abs(node.state[0] - objetivo[0]) disfil = abs(node.state[1] - objetivo[1]) return discol + disfil class AStarSearch: @staticmethod def search(grid: Grid) -> Solution: node = Node("", grid.start, 0) explored = {node.state: node} # explored[node.state] = True frontier = PriorityQueueFrontier() frontier.add(node, node.cost + manjaran(node, grid.end)) while True: if frontier.is_empty(): return NoSolution(explored) node = frontier.pop() if grid.end == node.state: return Solution(node, explored) neighbours = grid.get_neighbours(node.state) for action in neighbours.keys(): new_state = neighbours[action] new_node = Node("", new_state, node.cost + grid.get_cost(new_state)) new_node.parent = node new_node.action = action if new_node.state not in explored.keys() or new_node.cost < explored[new_node.state].cost: estimated_distance = new_node.cost + manjaran(new_node, grid.end) explored[new_node.state] = new_node frontier.add(new_node, estimated_distance)
  9. Uniform Cost Search
  10. The UCS algorithm guarantees the optimal solution but can be slower.

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from ..models.grid import Grid from ..models.frontier import PriorityQueueFrontier from ..models.solution import NoSolution, Solution from ..models.node import Node class UniformCostSearch: @staticmethod def search(grid: Grid) -> Solution: node = Node("", grid.start, 0) explored = {node.state:node} #explored[node.state] = True frontier = PriorityQueueFrontier() frontier.add(node,node.cost) while True: if frontier.is_empty(): return NoSolution(explored) node = frontier.pop() if grid.end == node.state: return Solution(node,explored) neighbours = grid.get_neighbours(node.state) for accion in neighbours.keys(): new_state = neighbours[accion] new_node = Node("", new_state, node.cost + grid.get_cost(new_state)) new_node.parent = node new_node.action = accion if new_node.state not in explored.keys() or new_node.cost < explored[new_node.state].cost: explored[new_node.state] = new_node frontier.add(new_node,new_node.cost) return NoSolution(explored)

You can find the complete code of Path Finding Algorithms in this repository.