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: — un bucle de .
- f2: — bucles anidados, ambos .
- f3: — divide a la mitad cada paso (búsqueda binaria sin acceso a array).
- f4: —
sortedusa Timsort.
1.2 — Comparar algoritmos (intermedio)
Tenés un dataset de 1.000.000 elementos. Algoritmo A es con constante 1. Algoritmo B es con constante 100. ¿Cuál corre más rápido para esa ?
✅ Solución
A: ops. B: ops.
B es ~500× más rápido. La constante alta no compensa la diferencia asintótica para grande.
Lección: Big-O domina para grandes; las constantes solo importan cuando 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 tiempo y 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 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 cuando salida se vacía, pero cada elemento se mueve una sola vez entre pilas. Amortizado: .
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))
tiempo, espacio en pila de recursión, donde es la altura. Un árbol balanceado tiene .
Cap. 4 — Hashing
4.1 — Two Sum (entrevista clásica)
Dado un array nums y un target , devolvé los índices de dos elementos que suman . Solución óptima en .
💡 Pista
Para cada número , ¿lo viste antes a ? 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 []
tiempo, espacio. La versión brute force con dos bucles es .
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 : sorted(a) == sorted(b). La de Counter es .
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
tiempo, 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
. 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. .
5.3 — Orden topológico (avanzado)
Dadas materias y una lista de prerrequisitos [(a, b)] (significa "para tomar hay que tomar "), 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. . LeetCode 207/210.
Reto integrador
R.1 — Sistema de cache LRU
Implementá una clase LRUCache(capacity) con dos operaciones, ambas :
get(key): devuelve el valor o-1si no existe. Marca la clave como recién usada.put(key, value): agrega/actualiza. Si supera capacidad, descarta la menos recientemente usada.
💡 Pista
Hash + lista doblemente enlazada. La hash da por key; la doubly-linked list permite mover nodos a la "cabeza" en .
✅ 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.