Compromissos entre Recursão e Iteração
Converta fatorial e Fibonacci recursivos em laços iterativos e explique quando o limite de recursão e o tamanho da pilha do Python tornam a iteração preferível.
Compromissos entre Recursão e Iteração é uma aula grátis de Coding Interview Prep no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Coding Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Coding Interview Prep inclui 4 aulas no total.
A dualidade entre recursão e iteração
Todo algoritmo que pode ser escrito recursivamente também pode ser escrito iterativamente, e vice-versa. A versão recursiva costuma refletir mais de perto a definição matemática do problema, enquanto a versão iterativa oferece controle explícito sobre a memória e evita riscos de estouro da pilha. Escolher entre elas é uma decisão pragmática baseada na legibilidade, nos limites de profundidade e nos requisitos de desempenho.
Em entrevistas, ser capaz de apresentar as duas versões e explicar os compromissos envolvidos é um forte sinal de domínio.
factorial: recursivo versus iterativo
factorial é o exemplo clássico. A versão recursiva codifica diretamente a definição matemática n! = n × (n-1)!. Ela usa espaço O(n) na pilha devido aos n valores de retorno pendentes. A versão iterativa percorre um laço de 1 até n, usando espaço O(1). Para n = 1000, a versão recursiva atinge o limite padrão do Python; a versão iterativa lida com n arbitrariamente grande.
def factorial_rec(n):
if n == 0:
return 1
return n * factorial_rec(n - 1) # O(n) stack
def factorial_iter(n):
result = 1
for i in range(2, n + 1):
result *= i # O(1) stack
return result
print(factorial_rec(10)) # 3628800
print(factorial_iter(10)) # 3628800
# Large n: iterative works, recursive may overflow
print(factorial_iter(1000) > 0) # True (Python handles big ints)Fibonacci: exponencial versus linear
O Fibonacci recursivo ingênuo tem complexidade O(2^n) em time — é desastrosamente lento para valores grandes de n. A versão iterativa tem complexidade O(n) em time e espaço O(1). A recursão com memoização (na próxima lição) também tem complexidade O(n) em time, mas usa espaço O(n) devido ao dicionário de memoização e à pilha O(n). Para Fibonacci, a abordagem iterativa é ideal em todos os aspectos. Para n = 50, a recursão ingênua leva segundos; a versão iterativa leva microssegundos.
import time
def fib_rec(n):
if n <= 1: return n
return fib_rec(n-1) + fib_rec(n-2) # O(2^n)
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a # O(n) time, O(1) space
# Timing comparison for n=35
start = time.time()
fib_rec(35)
print(f'Recursive n=35: {time.time()-start:.3f}s')
start = time.time()
fib_iter(35)
print(f'Iterative n=35: {time.time()-start:.6f}s')
print(fib_iter(100)) # handles large nPercurso de Árvores: Recursivo versus Iterativo
O percurso recursivo de uma árvore é naturalmente limpo, pois a estrutura da árvore reflete a recursão. Porém, em uma árvore profundamente desbalanceada (essencialmente uma lista encadeada), a profundidade da recursão é igual à altura da árvore = O(n), o que pode causar um estouro de pilha. A versão iterativa, que usa uma pilha explícita, não tem limite de profundidade e permite que o tamanho da pilha cresça na memória dinâmica, em vez de na pilha de chamadas.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val; self.left = left; self.right = right
def preorder_rec(root, result=None):
if result is None: result = []
if root:
result.append(root.val)
preorder_rec(root.left, result)
preorder_rec(root.right, result)
return result
def preorder_iter(root):
if not root: return []
result, stack = [], [root]
while stack:
node = stack.pop()
result.append(node.val)
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
return result
root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
print(preorder_rec(root)) # [1, 2, 4, 5, 3]
print(preorder_iter(root)) # [1, 2, 4, 5, 3]Ordenação por Mesclagem: Recursiva versus Iterativa (de Baixo para Cima)
A ordenação por mesclagem é naturalmente recursiva (dividir, aplicar a recursão, mesclar). A ordenação por mesclagem iterativa de baixo para cima evita completamente a recursão: começa com subvetores de tamanho 1, mescla pares adjacentes em subvetores de tamanho 2, depois de tamanho 4 e assim por diante, dobrando o tamanho do subvetor a cada passagem. A ordenação por mesclagem de baixo para cima tem tempo O(n log n), espaço O(n) (para o buffer de mesclagem) e espaço O(1) na pilha.
def merge_sort_iterative(arr):
n = len(arr)
size = 1
while size < n:
for start in range(0, n, 2 * size):
mid = min(start + size, n)
end = min(start + 2 * size, n)
left = arr[start:mid]
right = arr[mid:end]
# Merge
i = j = 0
for k in range(start, end):
if i < len(left) and (j >= len(right) or left[i] <= right[j]):
arr[k] = left[i]; i += 1
else:
arr[k] = right[j]; j += 1
size *= 2
return arr
print(merge_sort_iterative([5, 2, 4, 6, 1, 3])) # [1,2,3,4,5,6]Quando a Recursão é Claramente Melhor
A recursão se destaca quando o problema tem uma estrutura semelhante a uma árvore que corresponde diretamente ao grafo de chamadas, quando os casos-base são naturais e quando a profundidade é limitada (O(log n) para árvores balanceadas e divisão e conquista). Exemplos: análise de JSON, percurso de diretórios, árvores de jogos e problemas de retrocesso. Nesses casos, o código recursivo é mais curto, mais claro e mais fácil de ter sua correção demonstrada do que a versão iterativa equivalente.
# Recursion is clearest for JSON-like nested structures
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item)) # recurse on sub-list
else:
result.append(item)
return result
print(flatten([1, [2, [3, 4], 5], 6])) # [1, 2, 3, 4, 5, 6]
print(flatten([])) # []
print(flatten([[1, [2]], [3, [4, [5]]]])) # [1, 2, 3, 4, 5]Quando a Iteração é Claramente Melhor
A iteração é a escolha certa quando: a profundidade é O(n) e n é grande (mais de aproximadamente 500 em um código Python seguro), as versões recursiva e iterativa são igualmente legíveis (Fibonacci, fatorial) ou o problema é fundamentalmente sequencial, sem uma decomposição natural em subproblemas. Laços simples que processam vetores da esquerda para a direita — somas acumuladas, janelas deslizantes e dois ponteiros — devem sempre ser iterativos.
# Iterative is clearest for sequential array processing
def running_max(nums):
result = []
curr_max = float('-inf')
for n in nums:
curr_max = max(curr_max, n)
result.append(curr_max)
return result
print(running_max([3, 1, 4, 1, 5, 9, 2, 6])) # [3,3,4,4,5,9,9,9]
# No natural recursion here — iteration is the only sensible choiceConvertendo a Recursão de DFS em Iteração
Uma abordagem sistemática: toda DFS recursiva pode se tornar iterativa ao colocar os argumentos recursivos em uma pilha explícita. A ideia principal é que a chamada recursiva f(args) equivale a colocar args na pilha e executar um laço. Para o processamento em pós-ordem (quando são necessários os resultados dos filhos antes do pai), pode ser necessária uma abordagem em duas passagens ou um sinalizador de visitado.
# Post-order iterative using two stacks
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val=val; self.left=left; self.right=right
def postorder_iter(root):
if not root: return []
s1, s2 = [root], []
while s1:
node = s1.pop()
s2.append(node.val)
if node.left: s1.append(node.left)
if node.right: s1.append(node.right)
return s2[::-1] # reverse gives post-order
root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
print(postorder_iter(root)) # [4, 5, 2, 3, 1]Sobrecarga de Desempenho da Recursão
Cada chamada recursiva em Python tem uma sobrecarga significativa: um novo quadro é criado (alocando memória na memória dinâmica), as variáveis locais são inicializadas e um ponteiro para o endereço de retorno é armazenado. Testes de desempenho mostram que a sobrecarga de uma chamada de função em Python é de aproximadamente 100–200 nanossegundos por chamada. Para uma profundidade de recursão de 10^6, isso totaliza de 0,1 a 0,2 segundo de sobrecarga pura, independentemente do trabalho do algoritmo. Laços iterativos evitam completamente essa sobrecarga.
import time
def rec_sum(n):
if n == 0: return 0
return n + rec_sum(n - 1)
def iter_sum(n):
total = 0
for i in range(n + 1):
total += i
return total
import sys; sys.setrecursionlimit(10000)
n = 5000
start = time.time()
for _ in range(100): rec_sum(n)
print(f'Recursive sum({n}) x100: {(time.time()-start)*1000:.2f}ms')
start = time.time()
for _ in range(100): iter_sum(n)
print(f'Iterative sum({n}) x100: {(time.time()-start)*1000:.2f}ms')Tomando uma Decisão em uma Entrevista
Em uma entrevista de programação, se você tiver escolha, pergunte: 'A profundidade da recursão é limitada por O(log n)?' Se sim, a recursão é adequada. 'A profundidade da recursão é O(n)?' — prefira a iteração ou mencione que a converteria para uma versão iterativa em produção. 'O problema tem naturalmente a forma de uma árvore ou envolve divisão e conquista?' — opte pela recursão. 'O problema é uma varredura sequencial?' — use a iteração.
Sempre explique seu raciocínio: 'Usarei a recursão aqui porque a profundidade é O(log n) para uma BST balanceada, portanto o espaço de pilha O(log n) é aceitável.'
Resumo: Tabela de Compromissos
Resumindo os compromissos: o código recursivo costuma ser mais curto e refletir a estrutura do problema, mas custa O(profundidade) de espaço na pilha e tem sobrecarga de chamadas de função. O código iterativo é mais longo, mas usa espaço O(1) na pilha e evita os limites da recursão. A recursão com memoização (na próxima lição) é um meio-termo: preserva a clareza da recursão e elimina o recálculo redundante. Sempre seja explícito sobre a complexidade espacial, incluindo o espaço da pilha de chamadas, ao analisar sua solução.
rows = [
('Factorial', 'O(n) / O(1)', 'O(n) / O(1)', 'Same time; iter wins on space'),
('Fibonacci', 'O(2^n) / O(n)', 'O(n) / O(1)', 'Iter massively wins'),
('Binary search','O(log n) / O(log n)', 'O(log n) / O(1)', 'Iter wins on space'),
('Tree DFS', 'O(n) / O(h)', 'O(n) / O(h)', 'Equal; rec cleaner'),
('Merge sort', 'O(n log n) / O(log n)', 'O(n log n) / O(1)', 'BU-iter wins on stack'),
]
for name, rec, it, note in rows:
print(f'{name:<15} rec={rec:<22} iter={it:<22} {note}')Verificação Rápida
Verifique sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação desta lição.
Recapitulação da Lição
Nesta lição, você aprendeu: a recursão é preferível quando a profundidade é O(log n) ou quando o problema tem naturalmente a forma de uma árvore; a iteração é preferível quando a profundidade é O(n) ou quando o problema é sequencial, o Fibonacci recursivo ingênuo tem complexidade O(2^n) — a versão iterativa tem tempo O(n) e espaço O(1) e qualquer DFS recursiva pode ser convertida em iterativa ao gerenciar uma pilha explícita na memória dinâmica. A seguir, aplicaremos a memoização para eliminar chamadas recursivas redundantes.
Perguntas Frequentes
A aula “Compromissos entre Recursão e Iteração” é grátis?
Sim — o texto completo de “Compromissos entre Recursão e Iteração” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Coding Interview Prep, atualize para CoddyKit PRO. O curso de Coding Interview Prep inclui 4 aulas no total.
O que vou aprender em “Compromissos entre Recursão e Iteração”?
Converta fatorial e Fibonacci recursivos em laços iterativos e explique quando o limite de recursão e o tamanho da pilha do Python tornam a iteração preferível. Você pratica Coding Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Coding Interview Prep?
Nenhuma experiência prévia é necessária. Coding Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.
Quanto tempo leva a aula “Compromissos entre Recursão e Iteração”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Coding Interview Prep?
Sim. Cada aula de Coding Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Estrutura da Recursão: Caso Base, Confiança, Construção
- Visualizando a Pilha de Chamadas
- Compromissos entre Recursão e Iteração
- Memoização: Armazenando Resultados Recursivos em Cache