Recursividad: práctica 1

Índice

Función sumar recursiva

Archivo de código: SumarRec.java.

Se desea implementar una función recursiva que sume todos los elementos de un arreglo de enteros:

static int sumar(int arr[]);

Como referencia, esta sería la implementación iterativa, con complejidad \(\mathcal{O}(n)\).

// Versión iterativa de sumar().
static int sumarIter(int arr[]) {
    int suma = 0;

    for (int i=0; i < arr.length; i++)
        suma = suma + arr[i];

    return suma;
}

Caso base y caso recursivo

Para pensar la función recursiva, hay que determinar el caso base y el caso recursivo.

La pregunta para descubrir el caso base es:

Caso base: ¿cuál es el tamaño mínimo del problema (esto es, el tamaño mínimo del arreglo) para el cual se puede dar una solución inmediata?

Y la pregunta para descubrir el caso recursivo:

Caso recursivo: si la función ya supiera sumar arreglos de longitud \(N\) ¿qué debería hacer para sumar un arreglo de longitud \(N+1\)?

Las respuestas para el caso de la suma son:

  • Caso base: se puede dar una solución inmediata si el arreglo es de tamaño 1, ya que: la suma de un arreglo con un solo elemento es, directamente, ese elemento:

    suma([7]) = 7
    suma([3]) = 3
    suma([0]) = 0
    suma([-1]) = -1
    
  • Caso recursivo: si sumar un arreglo de, por ejemplo, tres elementos (\(N = 3\)) ya es un caso resuelto, se puede dar una solución a un arreglo de \(N + 1 = 4\) agregando al primer elemento la suma de los tres restantes:

    suma([7, 3, 9, 5])  =  7 + suma([3, 9, 5])
    

Recursividad con arreglo por copia

El código del caso base de la suma es trivial:

// Caso base solamente.
static int sumarRec(int arr[]) {
    if (arr.length == 1) {
        return arr[0];
    }
}

Para el caso recursivo se debe implementar el “achicamiento” del problema. En otras palabras, implementar:

// Pseudo-código del caso recursivo
if arreglo.length > 1:
    return arreglo[0] + sumarRec("mismo arreglo sin el primer elemento");

Por ejemplo:

// Pseudo-código con un arreglo de ejemplo
arreglo = [7, 3, 9, 5];
if arreglo.length > 1:
    return arreglo[0] + sumarRec([3, 9, 5]);

En Java, para pasar de [7, 3, 9, 5] a [3, 9, 5] es necesario realizar una copia del arreglo, pues no se pueden eliminar elementos.

Se podría implementar la siguiente función auxiliar:

// Devuelve una copia del arreglo sin su primer elemento.
static int[] borrarPrimero(int arreglo[]) {
    return null;
}

Con esta función, el pseudo-código quedaría tal que:

// Pseudo-código usando borrarPrimero().
arreglo = [7, 3, 9, 5];
if arreglo.length > 1:
    return arreglo[0] + sumarRec(borrarPrimero(arreglo));
                                 ^^^^^^^^^^^^^^^^^^^^^^

Función sumarRec()

La versión final de sumarRec() sería:

static int sumarRec(int arr[]) {
    if (arr.length == 1) {
        return arr[0];
    }
    if (arr.length > 1) {
      return arr[0] + sumarRec(borrarPrimero(arr));
    }
    return 0;  // En caso de que nos pasen un arreglo vacío.
}

Una traza de su ejecución sería:

sumarRec([7, 3, 9, 5])
7 + sumarRec([3, 9, 5])
7 + 3 + sumarRec([9, 5])
7 + 3 + 9 + sumarRec([5])
7 + 3 + 9 + 5
7 + 3 + 14
7 + 17
24

Ejercicio 1: implementar borrarPrimero().

Recursividad con arreglo por índice

La complejidad de copiar un arreglo es \(\mathcal{O}(n)\) y no cambia aunque se descarte el primer elemento. El problema de realizar una copia en cada paso de la recursión es que la complejidad total de sumarRec se dispara a \(\mathcal{O}(n^2)\).

Para mantener la complejidad de la solución recursiva en \(\mathcal{O}(n)\) se debe evitar realizar copia alguna del arreglo.

El patrón recursivo para solucionar este problema es sustituir la copia del arreglo por un parámetro posición que indica cuál es la siguiente posición a sumar.

Un ejemplo sería:

// Pseudo-código para la recursión por índice:

sumarRec1([7, 3, 9, 5]) =

  arr[0] + sumarRec1("mismo arreglo, pero a partir del índice 1")
                                          ^^^^^^^^^^^^^^^^^^^^^

sumarRec1([7, 3, 9, 5], "a partir del índice 1") =

  arr[1] + sumarRec1("mismo arreglo a partir del índice 2")
                                    ^^^^^^^^^^^^^^^^^^^^^

sumarRec1([7, 3, 9, 5], "a partir del índice 2") =

  arr[2] + sumarRec1("mismo arreglo a partir del índice 3")
                                    ^^^^^^^^^^^^^^^^^^^^^

sumarRec1([7, 3, 9, 5], "a partir del índice 3") =

  → 3 es el úlimo índice, no hay llamada recursiva
    ^^^^^^^^^^^^^^^^^^^^

  return arr[3];

Ahora, por tanto, la función recursiva toma dos parámetros:

static int sumarRec1(int arr[], int pos);
  • Caso base: si pos es el último índice válido, se devuelve directamente arr[pos].
  • Caso recursivo: si no, se achica el problema incrementando en 1 el índice.

El código sería:

static int sumarRec1(int arr[], int pos) {
    // Caso base: pos es el último índice del arreglo.
    if (pos == arr.length - 1) {
        return arr[pos];
    }
    // Caso recursivo: incrementar índice en 1.
    return arr[pos] + sumarRec1(arr, pos + 1);
}

Función sumarRec1()

La firma de la función original que se pidió es:

static int sumar(int arr[]);

Sin embargo, la función sumarRec1 no cumple con esta firma, pues toma un parámetro adicional.

La solución es mover el código recursivo a una función auxiliar privada:

private static int sumarAux1(int arr[], int pos) {
    // Caso base: pos es el último índice del arreglo.
    if (pos == arr.length - 1) {
        return arr[pos];
    }
    // Caso recursivo: incrementar índice en 1.
    return arr[pos] + sumarAux1(arr, pos + 1);
}

Y dejar a sumarRec1 llamar a la función auxiliar, comenzando a sumar por el primer elemento (índice 0):

static int sumarRec1(int arr[]) {
    // Manejar primero el caso de un arreglo vacío.
    if (arr.length == 0) {
        return 0;
    }
    // Llamada a la función auxiliar recursiva.
    return sumarAux1(arr, 0);
}

Una traza de su ejecución sería:

sumarRec1([7, 3, 9, 5])
sumarAux1([7, 3, 9, 5], 0)
                        ⬆
7 + sumarAux1([7, 3, 9, 5], 1)
                            ⬆
7 + 3 + sumarAux1([7, 3, 9, 5], 2)
                                ⬆
7 + 3 + 9 sumarRec([7, 3, 9, 5], 3)
                                 ⬆
7 + 3 + 9 + 5
7 + 3 + 14
7 + 17
24

Función mínimo recursiva

Archivo de código: MinimoRec.java.

Se desea implementar una función recursiva que encuentre el mínimo de un arreglo. Si el arreglo está vacío, debe devolver -1.

static int minimo(int arr[]);

La versión iterativa se puede escribir usando el operador comparacion < o usando la función de Java Math.min(), que devuelve el menor de dos números.

Es importante darse cuenta que ambas son equivalentes. Por brevedad, en general en las versiones recursivas se usará exclusivamente Math.min().

// Versión iterativa con comparación directa.
static int minimoIter1(int arr[]) {
    if (arr.length == 0) {
        return -1;
    }
    int minimo = arr[0];

    for (int i=1; i < arr.length; i++)
        if (arr[i] < minimo)
            minimo = arr[i];

    return minimo;
}

En la segunda versión, el if se sustituye por una llamada a Math.min():

// Versión iterativa con Math.min().
static int minimoIter2(int arr[]) {
    if (arr.length == 0) {
        return -1;
    }
    int minimoParcial = arr[0];

    for (int i=1; i < arr.length; i++)
        minimoParcial = Math.min(arr[1], minimoParcial);

    return minimoParcial;  // Resultado final.
}

Recursividad por índice

[Pendiente de explicar.]

Función minimoRec1()

Siguiendo el mismo patrón que en sumarRec1:

static int minimoRec1(int arr[]) {
    // Manejar primero el caso de un arreglo vacío.
    if (arr.length == 0) {
        return -1;
    }
    // Llamada a la función auxiliar recursiva.
    return minimoAux1(arr, 0);
}

private static int minimoAux1(int arr[], int pos) {
    // Caso base: pos es el último índice del arreglo.
    if (pos == arr.length - 1) {
      return arr[pos];
    }
    // Caso recursivo: incrementar índice en 1.
    return Math.min(arr[pos], minimoAux1(arr, pos + 1));
}

Ejercicio 2: implementar una versión de minimoAux1 que use un if en lugar de una llamada a Math.min().

Recursividad con acumulador parcial

[Pendiente de explicar.]

Función minimoRec2()

private static int minimoAux2(int arr[], int pos, int minimoParcial) {
    if (pos == arr.length - 1) {
      return Math.min(arr[pos], minimoParcial);
    }
    return minimoAux2(arr, pos + 1,
                      Math.min(arr[pos], nuevoParcial));
}

Extrayendo a una variable local:

private static int minimoAux2(int arr[], int pos, int minimoParcial)
{
    int nuevoParcial = Math.min(arr[pos], minimoParcial);

    if (pos == arr.length - 1) {
      return nuevoParcial;  // Es el final.
    }
    return minimoAux2(arr, pos + 1, nuevoParcial);
}
static int minimoRec2(int arr[]) {
    if (arr.length == 0) {
        return -1;
    }
    // El primer "minimoParcial" es el primer elemento.
    return minimoAux2(arr, 1, arr[0]);
}