Ejercicios — Estructuras de Datos y Algoritmos

Estos son ejercicios de entrevista técnica: la materia más útil para conseguir trabajo. Resolvelos en papel + en código.


Cap. 1 — Complejidad

1.1 — Identificar Big-O (básico)

Para cada función, indicá la complejidad temporal:

def f1(n):
    s = 0
    for i in range(n):
        s += i
    return s

def f2(n):
    s = 0
    for i in range(n):
        for j in range(n):
            s += i * j
    return s

def f3(n):
    if n <= 1:
        return 1
    return f3(n // 2) + 1

def f4(arr):
    return sorted(arr)
✅ Solución
  • f1: O(n)O(n) — un bucle de nn.
  • f2: O(n2)O(n^2) — bucles anidados, ambos nn.
  • f3: O(logn)O(\log n) — divide nn a la mitad cada paso (búsqueda binaria sin acceso a array).
  • f4: O(nlogn)O(n \log n)sorted usa Timsort.

1.2 — Comparar algoritmos (intermedio)

Tenés un dataset de 1.000.000 elementos. Algoritmo A es O(n2)O(n^2) con constante 1. Algoritmo B es O(nlogn)O(n \log n) con constante 100. ¿Cuál corre más rápido para esa nn?

✅ Solución

A: 110121 \cdot 10^{12} ops. B: 100106log2(106)10010620=2109100 \cdot 10^6 \cdot \log_2(10^6) \approx 100 \cdot 10^6 \cdot 20 = 2 \cdot 10^9 ops.

B es ~500× más rápido. La constante alta no compensa la diferencia asintótica para nn grande.

Lección: Big-O domina para nn grandes; las constantes solo importan cuando nn es chico.


Cap. 2 — Listas, pilas y colas

2.1 — Invertir lista enlazada (entrevista clásica)

Dada una lista enlazada simple, invertila in-place en O(n)O(n) tiempo y O(1)O(1) espacio.

💡 Pista

Manejá tres punteros: prev, actual, siguiente. En cada paso, redirigí actual.next hacia prev.

✅ Solución
def invertir(cabeza):
    prev = None
    actual = cabeza
    while actual:
        siguiente = actual.next
        actual.next = prev
        prev = actual
        actual = siguiente
    return prev   # nueva cabeza

Es la pregunta más clásica. Si no la podés escribir en 5 minutos, practicala 10 veces.

2.2 — Validar paréntesis balanceados (intermedio)

Dada una cadena con (), [], {}, decidí si los paréntesis están balanceados.

✅ Solución
def balanceados(s: str) -> bool:
    pila = []
    cierre = {')': '(', ']': '[', '}': '{'}
    for c in s:
        if c in '([{':
            pila.append(c)
        elif c in ')]}':
            if not pila or pila[-1] != cierre[c]:
                return False
            pila.pop()
    return not pila

Casos críticos: cadena vacía (True), cierre sin apertura (")"), apertura sin cierre ("("), tipos mezclados ("(]").

2.3 — Cola con dos pilas (avanzado)

Implementá una cola (FIFO) usando solo dos pilas (LIFO). Operaciones enqueue y dequeue deben ser O(1)O(1) amortizado.

✅ Solución
class ColaConPilas:
    def __init__(self):
        self.entrada = []
        self.salida = []

    def enqueue(self, x):
        self.entrada.append(x)

    def dequeue(self):
        if not self.salida:
            while self.entrada:
                self.salida.append(self.entrada.pop())
        return self.salida.pop() if self.salida else None

Análisis: dequeue puede tardar O(n)O(n) cuando salida se vacía, pero cada elemento se mueve una sola vez entre pilas. Amortizado: O(1)O(1).


Cap. 3 — Árboles

3.1 — Recorridos (básico)

Para el árbol:

        4
       / \
      2   6
     / \ / \
    1  3 5  7

Listá los nodos en preorden, inorden y postorden.

✅ Solución
  • Preorden (raíz, izq, der): 4, 2, 1, 3, 6, 5, 7.
  • Inorden (izq, raíz, der): 1, 2, 3, 4, 5, 6, 7. (En BST, inorden = ordenado.)
  • Postorden (izq, der, raíz): 1, 3, 2, 5, 7, 6, 4.

3.2 — Validar BST (intermedio)

¿Cómo verificarías que un árbol binario es un Binary Search Tree?

💡 Pista

Un nodo no solo debe ser mayor que su hijo izquierdo: debe ser mayor que todo el subárbol izquierdo. Pasá un rango (min, max).

✅ Solución
def es_bst(nodo, lo=float('-inf'), hi=float('inf')):
    if nodo is None:
        return True
    if not (lo < nodo.valor < hi):
        return False
    return (es_bst(nodo.izq, lo, nodo.valor) and
            es_bst(nodo.der, nodo.valor, hi))

Trampa común: comparar solo nodo vs. hijos directos falla con árboles como [5, 3, 8, 2, 6] donde el 6 está en el subárbol izquierdo del 5 pero su padre es 3.

3.3 — Profundidad máxima (básico)

Devolvé la profundidad de un árbol binario.

✅ Solución
def profundidad(nodo):
    if nodo is None:
        return 0
    return 1 + max(profundidad(nodo.izq), profundidad(nodo.der))

O(n)O(n) tiempo, O(h)O(h) espacio en pila de recursión, donde hh es la altura. Un árbol balanceado tiene h=O(logn)h = O(\log n).


Cap. 4 — Hashing

4.1 — Two Sum (entrevista clásica)

Dado un array nums y un target TT, devolvé los índices de dos elementos que suman TT. Solución óptima en O(n)O(n).

💡 Pista

Para cada número xx, ¿lo viste antes a TxT - x? Hash table es perfecto para "lo viste antes".

✅ Solución
def two_sum(nums, target):
    visto = {}                          # valor → índice
    for i, x in enumerate(nums):
        complemento = target - x
        if complemento in visto:
            return [visto[complemento], i]
        visto[x] = i
    return []

O(n)O(n) tiempo, O(n)O(n) espacio. La versión brute force con dos bucles es O(n2)O(n^2).

4.2 — Anagrama (básico)

¿"caracoles" y "reclamos" son anagramas? Escribí una función general.

✅ Solución
from collections import Counter

def son_anagrama(a: str, b: str) -> bool:
    return Counter(a) == Counter(b)

Para esos strings: ambos tienen 9 letras y la misma cuenta. Ejemplo: Counter("caracoles") == Counter("escolarac").

Alternativa O(nlogn)O(n \log n): sorted(a) == sorted(b). La de Counter es O(n)O(n).

4.3 — Longest substring without repeating (avanzado)

Dada una string, devolvé la longitud del substring contiguo más largo sin caracteres repetidos.

"abcabcbb"  → 3 ("abc")
"bbbbb"     → 1
"pwwkew"    → 3 ("wke")
💡 Pista

Sliding window con un set: expandí el window por la derecha mientras no haya repetidos; cuando aparezca uno, contraé por la izquierda hasta que no.

✅ Solución
def longitud_max(s: str) -> int:
    visto = {}
    inicio = 0
    mejor = 0
    for fin, c in enumerate(s):
        if c in visto and visto[c] >= inicio:
            inicio = visto[c] + 1
        visto[c] = fin
        mejor = max(mejor, fin - inicio + 1)
    return mejor

O(n)O(n) tiempo, O(min(n,Σ))O(\min(n, |\Sigma|)) espacio (alfabeto).


Cap. 5 — Grafos

5.1 — Número de islas (BFS/DFS)

Dado un grid 2D de 0s y 1s, contá las "islas" (componentes conexos de 1s, conectados por 4 direcciones).

✅ Solución
def islas(grid):
    if not grid: return 0
    rows, cols = len(grid), len(grid[0])
    visit = [[False] * cols for _ in range(rows)]
    count = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols: return
        if visit[r][c] or grid[r][c] == 0: return
        visit[r][c] = True
        for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
            dfs(r+dr, c+dc)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 1 and not visit[r][c]:
                count += 1
                dfs(r, c)
    return count

O(RC)O(R \cdot C). LeetCode 200, muy clásico.

5.2 — Camino más corto en laberinto (intermedio)

Dado un grid con obstáculos (1 = pared, 0 = libre), encontrá el camino más corto desde (0,0) hasta (R-1,C-1) con BFS.

✅ Solución
from collections import deque

def camino_corto(grid):
    if not grid or grid[0][0] or grid[-1][-1]: return -1
    R, C = len(grid), len(grid[0])
    cola = deque([(0, 0, 1)])              # (r, c, distancia)
    visit = {(0, 0)}
    while cola:
        r, c, d = cola.popleft()
        if (r, c) == (R-1, C-1):
            return d
        for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
            nr, nc = r+dr, c+dc
            if (0 <= nr < R and 0 <= nc < C
                and grid[nr][nc] == 0 and (nr,nc) not in visit):
                visit.add((nr, nc))
                cola.append((nr, nc, d+1))
    return -1

BFS encuentra el camino mínimo en aristas porque expande nivel a nivel. O(RC)O(R \cdot C).

5.3 — Orden topológico (avanzado)

Dadas nn materias y una lista de prerrequisitos [(a, b)] (significa "para tomar aa hay que tomar bb"), devolvé un orden válido para cursarlas. Si hay ciclo, devolvé [].

✅ Solución
from collections import defaultdict, deque

def orden_materias(n, prereqs):
    graph = defaultdict(list)
    indeg = [0] * n
    for a, b in prereqs:
        graph[b].append(a)            # b → a
        indeg[a] += 1

    cola = deque(i for i in range(n) if indeg[i] == 0)
    orden = []
    while cola:
        v = cola.popleft()
        orden.append(v)
        for w in graph[v]:
            indeg[w] -= 1
            if indeg[w] == 0:
                cola.append(w)

    return orden if len(orden) == n else []

Algoritmo de Kahn. O(V+E)O(V + E). LeetCode 207/210.


Reto integrador

R.1 — Sistema de cache LRU

Implementá una clase LRUCache(capacity) con dos operaciones, ambas O(1)O(1):

💡 Pista

Hash + lista doblemente enlazada. La hash da O(1)O(1) por key; la doubly-linked list permite mover nodos a la "cabeza" en O(1)O(1).

✅ Solución
class Node:
    def __init__(self, k, v):
        self.k, self.v = k, v
        self.prev = self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.map = {}                  # k → Node
        self.head, self.tail = Node(0,0), Node(0,0)   # sentinelas
        self.head.next, self.tail.prev = self.tail, self.head

    def _remove(self, node):
        node.prev.next, node.next.prev = node.next, node.prev

    def _add_front(self, node):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    def get(self, k):
        if k not in self.map: return -1
        node = self.map[k]
        self._remove(node); self._add_front(node)
        return node.v

    def put(self, k, v):
        if k in self.map:
            self._remove(self.map[k])
        elif len(self.map) >= self.cap:
            lru = self.tail.prev
            self._remove(lru)
            del self.map[lru.k]
        node = Node(k, v)
        self._add_front(node)
        self.map[k] = node

LeetCode 146, casi obligatorio en entrevistas FAANG.


Más ejercicios: LeetCode (etiqueta cada estructura), HackerRank, Codeforces.