Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Ejercicios: Recursividad y Divide y Vencerás

Universidad Nacional de Rio Negro - Sede Andina

Ejercicios para dominar la recursividad y el paradigma de divide y vencerás, desde conceptos fundamentales hasta algoritmos avanzados de ordenamiento y búsqueda. Estos ejercicios refuerzan el pensamiento recursivo y el análisis de complejidad.

1: Fundamentos de Recursividad

1.1: Factorial

Implementar la función factorial de forma recursiva siguiendo la definición matemática.

long int factorial(int n);

Definición recursiva:

n!={1si n=0n×(n1)!si n>0n! = \begin{cases} 1 & \text{si } n = 0 \\ n \times (n-1)! & \text{si } n > 0 \end{cases}

Casos de prueba:

1.2: Suma de Enteros

Implementar suma de dos enteros positivos usando solo recursividad (sin operador + en el paso recursivo).

int suma_recursiva(int a, int b);

Estrategia: Decrementar b e incrementar a hasta que b sea 0.

suma(a,b)={asi b=0suma(a+1,b1)si b>0suma(a, b) = \begin{cases} a & \text{si } b = 0 \\ suma(a + 1, b - 1) & \text{si } b > 0 \end{cases}

1.3: Producto por Sumas Recursivas

Implementar multiplicación usando solo sumas recursivas.

int producto_recursivo(int a, int b);
a×b={0si b=0a+producto(a,b1)si b>0a \times b = \begin{cases} 0 & \text{si } b = 0 \\ a + producto(a, b - 1) & \text{si } b > 0 \end{cases}

Complejidad: O(b)O(b) en tiempo.

1.4: Potencia

Implementar baseexponentebase^{exponente} de forma recursiva.

long int potencia(int base, int exponente);

Versión básica: O(n)O(n) en tiempo.

baseexp={1si exp=0base×potencia(base,exp1)si exp>0base^{exp} = \begin{cases} 1 & \text{si } exp = 0 \\ base \times potencia(base, exp - 1) & \text{si } exp > 0 \end{cases}

Desafío: Implementar versión optimizada usando exponenciación rápida (divide y vencerás) con complejidad O(logn)O(\log n).

baseexp={1si exp=0(baseexp/2)2si exp es parbase×(base(exp1)/2)2si exp es imparbase^{exp} = \begin{cases} 1 & \text{si } exp = 0 \\ \left(base^{exp/2}\right)^2 & \text{si } exp \text{ es par} \\ base \times \left(base^{(exp-1)/2}\right)^2 & \text{si } exp \text{ es impar} \end{cases}

2: Series Numéricas Recursivas

2.1: Fibonacci Básico

Implementar la secuencia de Fibonacci recursivamente.

long int fibonacci(int n);
fib(n)={0si n=01si n=1fib(n1)+fib(n2)si n>1fib(n) = \begin{cases} 0 & \text{si } n = 0 \\ 1 & \text{si } n = 1 \\ fib(n-1) + fib(n-2) & \text{si } n > 1 \end{cases}

Advertencia: Esta implementación tiene complejidad exponencial O(2n)O(2^n) debido a recálculos redundantes.

Secuencia: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...

2.2: Fibonacci con Memoización

Optimizar Fibonacci usando un arreglo para almacenar resultados ya calculados (programación dinámica).

long int fibonacci_memo(int n, long int memo[]);

Algoritmo:

  1. Si memo[n] ya está calculado, retornarlo

  2. Si no, calcular recursivamente y guardar en memo[n]

Complejidad mejorada: O(n)O(n) en tiempo, O(n)O(n) en espacio.

2.3: Tribonacci

Generalizar Fibonacci: cada término es la suma de los tres anteriores.

long int tribonacci(int n);
trib(n)={0si n=01si n=11si n=2trib(n1)+trib(n2)+trib(n3)si n>2trib(n) = \begin{cases} 0 & \text{si } n = 0 \\ 1 & \text{si } n = 1 \\ 1 & \text{si } n = 2 \\ trib(n-1) + trib(n-2) + trib(n-3) & \text{si } n > 2 \end{cases}

Secuencia: 0, 1, 1, 2, 4, 7, 13, 24, 44, 81...

2.4: Suma de Dígitos

Calcular la suma de los dígitos de un número usando recursividad.

int suma_digitos(int n);

Estrategia:

Ejemplos:

3: Operaciones sobre Arreglos

3.1: Suma de Elementos

Implementar suma de elementos de un arreglo recursivamente.

int suma_arreglo(int arr[], int n);

Estrategia:

Alternativamente, usar índices:

int suma_arreglo_idx(int arr[], int inicio, int fin);

3.2: Encontrar Máximo

Encontrar el elemento máximo de un arreglo recursivamente.

int maximo_arreglo(int arr[], int n);

Algoritmo:

3.3: Búsqueda Lineal Recursiva

Buscar un elemento en un arreglo no ordenado.

int buscar_recursivo(int arr[], int n, int objetivo);

Retorno: índice del elemento o -1 si no se encuentra.

Algoritmo:

3.4: Invertir Arreglo

Invertir un arreglo in-place usando recursividad.

void invertir_arreglo(int arr[], int inicio, int fin);

Algoritmo:

4: Operaciones sobre Cadenas

4.1: Longitud de Cadena

Calcular la longitud de una cadena sin usar strlen.

size_t longitud_recursiva(const char* str);

Algoritmo:

4.2: Verificar Palíndromo

Determinar si una cadena es un palíndromo.

bool es_palindromo(const char* str, int inicio, int fin);

Algoritmo:

Ejemplos:

4.3: Contar Vocales

Contar el número de vocales en una cadena.

int contar_vocales(const char* str);

Algoritmo:

4.4: Invertir Cadena

Invertir una cadena in-place recursivamente.

void invertir_cadena(char* str, int inicio, int fin);

Similar al algoritmo de invertir arreglo.

5: Búsqueda Binaria

Implementar búsqueda binaria recursiva en un arreglo ordenado.

int busqueda_binaria(int arr[], int inicio, int fin, int objetivo);

Algoritmo:

  1. Caso base: inicio > fin retorna -1 (no encontrado)

  2. Calcular punto medio: medio = inicio + (fin - inicio) / 2

  3. Si arr[medio] == objetivo, retornar medio

  4. Si arr[medio] > objetivo, buscar en mitad izquierda: busqueda_binaria(arr, inicio, medio - 1, objetivo)

  5. Sino, buscar en mitad derecha: busqueda_binaria(arr, medio + 1, fin, objetivo)

Complejidad: O(logn)O(\log n)

5.1: Encontrar Primera Ocurrencia

Modificar búsqueda binaria para encontrar la primera ocurrencia de un elemento (puede haber duplicados).

int primera_ocurrencia(int arr[], int inicio, int fin, int objetivo);

Estrategia: Cuando se encuentra el elemento, continuar buscando en la mitad izquierda para ver si hay ocurrencias anteriores.

5.2: Encontrar Última Ocurrencia

Similar al anterior, pero continuar buscando en la mitad derecha.

int ultima_ocurrencia(int arr[], int inicio, int fin, int objetivo);

6: Ordenamiento por Fusión (Merge Sort)

Implementar Merge Sort, el algoritmo clásico de divide y vencerás.

6.1: Función Merge

Implementar la función que fusiona dos sub-arreglos ordenados.

void merge(int arr[], int inicio, int medio, int fin);

Algoritmo:

  1. Crear arreglos temporales para las dos mitades

  2. Copiar datos a los arreglos temporales

  3. Fusionar comparando elementos y copiando el menor

  4. Copiar elementos restantes si los hay

6.2: Función Merge Sort

Implementar la función recursiva principal.

void merge_sort(int arr[], int inicio, int fin);

Algoritmo:

  1. Caso base: inicio >= fin (un solo elemento o vacío)

  2. Calcular punto medio: medio = inicio + (fin - inicio) / 2

  3. Ordenar mitad izquierda: merge_sort(arr, inicio, medio)

  4. Ordenar mitad derecha: merge_sort(arr, medio + 1, fin)

  5. Fusionar las mitades: merge(arr, inicio, medio, fin)

Complejidad: O(nlogn)O(n \log n) en tiempo, O(n)O(n) en espacio.

Relación de recurrencia: T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)

6.3: Contar Inversiones

Modificar Merge Sort para contar el número de inversiones en un arreglo (pares de índices i < j donde arr[i] > arr[j]).

long int contar_inversiones(int arr[], int inicio, int fin);

Aplicación: Medir cuán desordenado está un arreglo.

7: Quick Sort

Implementar Quick Sort, otro algoritmo de divide y vencerás.

7.1: Función Partition

Implementar la función que particiona el arreglo alrededor de un pivote.

int partition(int arr[], int bajo, int alto);

Algoritmo (Lomuto):

  1. Elegir arr[alto] como pivote

  2. Mantener índice i de elementos menores al pivote

  3. Recorrer de bajo a alto - 1:

    • Si arr[j] < pivote, intercambiar arr[i] con arr[j] e incrementar i

  4. Colocar pivote en su posición final: intercambiar arr[i] con arr[alto]

  5. Retornar i (posición final del pivote)

7.2: Función Quick Sort

Implementar la función recursiva principal.

void quick_sort(int arr[], int bajo, int alto);

Algoritmo:

  1. Caso base: bajo >= alto

  2. Particionar: pivote = partition(arr, bajo, alto)

  3. Ordenar recursivamente la parte izquierda: quick_sort(arr, bajo, pivote - 1)

  4. Ordenar recursivamente la parte derecha: quick_sort(arr, pivote + 1, alto)

Complejidad:

7.3: Quick Sort con Pivote Aleatorio

Mejorar Quick Sort eligiendo el pivote aleatoriamente para evitar el peor caso.

int partition_aleatorio(int arr[], int bajo, int alto);

Estrategia: Elegir índice aleatorio entre bajo y alto, intercambiarlo con arr[alto], y luego aplicar partition normal.

8: Torres de Hanoi

Resolver el problema clásico de las Torres de Hanoi.

void hanoi(int n, char origen, char destino, char auxiliar);

Reglas:

  1. Mover un solo disco a la vez

  2. Un disco más grande nunca puede estar sobre uno más pequeño

  3. Solo se puede mover el disco superior de cada torre

Algoritmo recursivo:

  1. Caso base: n == 1, mover disco de origen a destino

  2. Mover n-1 discos de origen a auxiliar (usando destino como auxiliar)

  3. Mover disco n de origen a destino

  4. Mover n-1 discos de auxiliar a destino (usando origen como auxiliar)

Complejidad: O(2n)O(2^n) movimientos.

Número de movimientos: 2n12^n - 1

8.1: Contar Movimientos

Modificar la función para que retorne el número de movimientos realizados.

int hanoi_contar(int n, char origen, char destino, char auxiliar);

9: Generación de Combinaciones

9.1: Subconjuntos (Power Set)

Generar todos los subconjuntos de un conjunto usando recursividad.

void generar_subconjuntos(int conjunto[], int n, int subconjunto[], int tam_sub, int indice);

Algoritmo:

Número de subconjuntos: 2n2^n

Ejemplo con {1, 2, 3}:

9.2: Permutaciones

Generar todas las permutaciones de un arreglo.

void generar_permutaciones(int arr[], int inicio, int fin);

Algoritmo (backtracking):

  1. Caso base: inicio == fin, imprimir permutación

  2. Para cada i de inicio a fin:

    • Intercambiar arr[inicio] con arr[i]

    • Generar permutaciones del resto: generar_permutaciones(arr, inicio + 1, fin)

    • Deshacer intercambio (backtrack)

Número de permutaciones: n!n!

9.3: Combinaciones de Tamaño k

Generar todas las combinaciones de tamaño k de un conjunto de n elementos.

void combinaciones(int arr[], int n, int k, int combinacion[], int indice, int inicio);

Número de combinaciones: (nk)=n!k!(nk)!\binom{n}{k} = \frac{n!}{k!(n-k)!}

10: Backtracking - N-Reinas

Resolver el problema de las N-Reinas: colocar N reinas en un tablero de NxN sin que se ataquen.

bool resolver_n_reinas(int tablero[][MAX], int n, int col);
bool es_seguro(int tablero[][MAX], int n, int fila, int col);

Algoritmo (backtracking):

  1. Caso base: col >= n, se colocaron todas las reinas

  2. Para cada fila de 0 a n-1:

    • Si es seguro colocar reina en (fila, col):

      • Colocar reina

      • Recursivamente intentar colocar en col + 1

      • Si tiene éxito, retornar true

      • Sino, quitar reina (backtrack)

  3. Si ninguna fila funciona, retornar false

Función auxiliar es_seguro: Verificar que no haya reinas en la misma fila, columna o diagonales.

10.1: Contar Soluciones

Modificar para contar todas las soluciones posibles en lugar de encontrar solo una.

int contar_soluciones_n_reinas(int tablero[][MAX], int n, int col);

11: Máximo Común Divisor (MCD)

Implementar el algoritmo de Euclides recursivamente.

int mcd(int a, int b);

Algoritmo de Euclides:

mcd(a,b)={asi b=0mcd(b,amodb)si b0mcd(a, b) = \begin{cases} a & \text{si } b = 0 \\ mcd(b, a \mod b) & \text{si } b \neq 0 \end{cases}

Ejemplos:

Complejidad: O(logmin(a,b))O(\log \min(a, b))

11.1: Mínimo Común Múltiplo

Usar el MCD para calcular el MCM.

int mcm(int a, int b);

Relación: mcm(a,b)=a×bmcd(a,b)mcm(a, b) = \frac{a \times b}{mcd(a, b)}

12: Recursividad en Estructuras de Datos

12.1: Búsqueda en Lista Enlazada

Buscar un elemento en una lista enlazada recursivamente.

typedef struct nodo {
    int dato;
    struct nodo* siguiente;
} nodo_t;

bool buscar_lista(nodo_t* nodo, int objetivo);

Algoritmo:

12.2: Longitud de Lista Enlazada

Calcular la longitud de una lista enlazada recursivamente.

size_t longitud_lista(nodo_t* nodo);

12.3: Imprimir Lista al Revés

Imprimir los elementos de una lista enlazada en orden inverso sin modificar la lista.

void imprimir_reversa(nodo_t* nodo);

Estrategia: Primero hacer la llamada recursiva, luego imprimir el dato.

12.4: Suma de Árbol Binario

Calcular la suma de todos los nodos de un árbol binario.

typedef struct nodo_arbol {
    int dato;
    struct nodo_arbol* izq;
    struct nodo_arbol* der;
} nodo_arbol_t;

int suma_arbol(nodo_arbol_t* raiz);

Algoritmo:

13: Altura de Árbol Binario

Calcular la altura (máxima profundidad) de un árbol binario.

int altura_arbol(nodo_arbol_t* raiz);

Algoritmo:

13.1: Árbol Balanceado

Determinar si un árbol binario está balanceado (la diferencia de altura entre subárboles izquierdo y derecho no excede 1 para cada nodo).

bool es_balanceado(nodo_arbol_t* raiz);

13.2: Recorridos de Árbol

Implementar los tres recorridos estándar recursivamente:

void preorden(nodo_arbol_t* raiz);   // Raíz, Izq, Der
void inorden(nodo_arbol_t* raiz);    // Izq, Raíz, Der
void postorden(nodo_arbol_t* raiz);  // Izq, Der, Raíz

Aplicación de inorden: En un árbol de búsqueda binaria, imprime los elementos ordenados.

14: Operaciones Avanzadas sobre Cadenas

14.1: Eliminar Espacios

Eliminar todos los espacios de una cadena recursivamente.

void eliminar_espacios(char* str, int indice);

Estrategia: Si encuentra espacio, desplazar todos los caracteres siguientes una posición a la izquierda.

14.2: Conversión a Mayúsculas/Minúsculas

Convertir una cadena a mayúsculas o minúsculas recursivamente.

void a_mayusculas_rec(char* str);
void a_minusculas_rec(char* str);

14.3: Subcadenas Palindrómicas

Encontrar todas las subcadenas que son palíndromos.

void encontrar_palindromos(const char* str, int inicio, int fin);

15: Problema del Cambio de Monedas

Calcular el número de formas de dar cambio para una cantidad usando un conjunto de denominaciones.

int contar_formas_cambio(int cantidad, int denominaciones[], int n);

Algoritmo:

Ejemplo: Para 10 con monedas {1, 2, 5}:

15.1: Cambio con Mínimas Monedas

Variante: encontrar el número mínimo de monedas para dar el cambio.

int min_monedas(int cantidad, int denominaciones[], int n);

16: Laberinto (Pathfinding)

Encontrar un camino en un laberinto usando backtracking.

bool resolver_laberinto(int laberinto[][MAX], int x, int y, int solucion[][MAX], int n);

Representación:

Algoritmo (backtracking):

  1. Caso base: si (x, y) es destino, retornar true

  2. Si (x, y) es válido (dentro de límites, no pared, no visitado):

    • Marcar como parte de la solución

    • Intentar mover en las 4 direcciones: derecha, abajo, izquierda, arriba

    • Si alguno tiene éxito, retornar true

    • Sino, desmarcar (backtrack) y retornar false

16.1: Contar Todos los Caminos

Modificar para contar todos los caminos posibles del origen al destino.

int contar_caminos(int laberinto[][MAX], int x, int y, bool visitado[][MAX], int n);

17: Problema de la Mochila (0/1 Knapsack)

Dado un conjunto de objetos con peso y valor, determinar el máximo valor que se puede llevar en una mochila de capacidad limitada.

int mochila_01(int pesos[], int valores[], int n, int capacidad);

Algoritmo recursivo:

Complejidad sin optimización: O(2n)O(2^n)

Optimización: Usar memoización para reducir a O(n×capacidad)O(n \times capacidad).

18: Parentización Óptima de Matrices

Determinar el orden óptimo para multiplicar una cadena de matrices minimizando operaciones.

int parentizacion_matrices(int dimensiones[], int i, int j);

Problema: Dadas matrices A1,A2,,AnA_1, A_2, \ldots, A_n con dimensiones (d0×d1),(d1×d2),,(dn1×dn)(d_0 \times d_1), (d_1 \times d_2), \ldots, (d_{n-1} \times d_n), encontrar el orden de multiplicación que minimice operaciones escalares.

Relación de recurrencia:

m[i,j]={0si i=jminik<j{m[i,k]+m[k+1,j]+di1dkdj}si i<jm[i,j] = \begin{cases} 0 & \text{si } i = j \\ \min_{i \le k < j} \{m[i,k] + m[k+1,j] + d_{i-1} \cdot d_k \cdot d_j\} & \text{si } i < j \end{cases}

Aplicación: Optimización de consultas en bases de datos.

19: Subset Sum Problem

Determinar si existe un subconjunto de un conjunto de enteros que sume exactamente un valor objetivo.

bool existe_suma(int conjunto[], int n, int suma_objetivo);

Algoritmo:

Ejemplo: {3, 34, 4, 12, 5, 2} y objetivo 9true (usando {4, 5})

19.1: Imprimir Subconjunto Solución

Modificar para imprimir el subconjunto que suma el objetivo.

bool imprimir_subconjunto_suma(int conjunto[], int n, int suma_objetivo, int solucion[], int tam_sol);

20: Análisis de Complejidad Recursiva

20.1: Ecuaciones de Recurrencia

Analizar la complejidad temporal de las siguientes funciones recursivas usando el Teorema Maestro o expansión:

a) Factorial:

b) Fibonacci naive:

c) Búsqueda Binaria:

d) Merge Sort:

e) Torres de Hanoi:

20.2: Conversión Iterativa

Para cada algoritmo recursivo, analizar:

  1. ¿Es posible convertirlo a iterativo?

  2. ¿Cuál sería más eficiente en espacio?

Ejemplos:

20.3: Recursión de Cola (Tail Recursion)

Identificar cuáles funciones tienen recursión de cola (la llamada recursiva es la última operación) y pueden ser optimizadas por el compilador.

Recursión de cola:

int factorial_tail(int n, int acumulador) {
    if (n == 0) return acumulador;
    return factorial_tail(n - 1, n * acumulador);  // Última operación
}

No es recursión de cola:

int factorial(int n) {
    if (n == 0) return 1;
    return n * factorial(n - 1);  // Multiplicación después de la recursión
}

Ventaja: El compilador puede optimizar recursión de cola a un lazo, eliminando el overhead de la pila.