Index over opgaver i denne lektion   Alfabetisk indeks   Kursets hjemmeside   

Opgaver og løsninger
Rekursion


11.1   En Fibonacci funktion med huskeværk  

Den rekursive fib funktion, som kan programmeres således

long fib(int n){
  long result;

  if (n == 0)
    result = 0;
  else if (n == 1)
    result = 1;
  else
    result = fib(n-1) + fib(n-2);

  return result;
}

lider af et alvorligt effektivitetsproblem. Hvert kald af fib(n) for n > 1 leder til to andre kald af fib. Mange af disse kald har allerede været udført en eller flere gange tidligere. Prøv f.eks. at danne dig et overblik over hvor mange gange fib(2) bliver kaldt når vi beregner fib(5).

Ideen i denne opgave er at huske på (lagre) værdien af fib(n), når den er beregnet første gang. Hvis vi efterfølgende kalder fib(n) igen, trækker vi værdien ud af lageret snarere end at genberegne fib(n) rekursivt. Denne teknik kaldes for memoisering. Her er det pseudoprogram vi har studeret:

long fib(long n){     /* working program: fib-memo.c - an exercise*/
  long result;
  
  Erklaer huskevaerk;

  if (n == 0)
    result = 0;
  else if (n == 1)
    result = 1;
  else if (Vi allerede har beregnet vaerdien en gang)
    result = den huskede vaerdi;
  else {
    result = fib(n-1) + fib(n-2);
    Husk paa vaerdien;
  }

  return result;
}

int main(void) {
  long i;

  for(i = 0; i < 100; i++)
    printf("Fib(%li) = %li\n", i, fib(i));
  
  return 0;
}

Modificer ovenstående versioner af fib, således at den lagrer værdien af fib(n) når udtrykket er beregnet. Og således at den udtrækker og returnerer en lagret værdi, i stedet for at gå i gang med en genberegning. Det er oplagt at allokere et array af element-typen long int til formålet. Overvej en anvendelse af en statisk lokal variabel!

Kald den oprindelige af fib og din nye variant af fib funktionen med alle heltal mellem n og 45, og vurder dermed effekten af memoiseringen.

Løsning

Her er min memoiserede fib funktion:

#include <stdio.h>

long fib(long n);

int main(void) {
  long i;

  for(i = 0; i < 100; i++)
    printf("Fib(%li) = %li\n", i, fib(i));
  
  return 0;
}

long fib(long n){
  long result;
  static long memo[100];   /* elements pre-initialized to 0 */

  if (n == 0)
    result = 0;
  else if (n == 1)
    result = 1;
  else if (memo[n] != 0)
    result = memo[n];
  else {
    result = fib(n-1) + fib(n-2);
    memo[n] = result;
  }

  return result;
}


11.2   Palindromer  

Et palindrom er en tekststreng, som læses ens både forfra og bagfra. Eksempelvis er strengen "regninger" et palindrom. Vi kan også sige at et palindrom er en tekststreng som er lig med sin egen strengomvending.

Skriv først en iterativ funktion int is_palindrome_iter(char *str), der afgør om str er et palindrom. En iterativ funktion er en funktion, som programmeres ved brug af en while-løkke eller en for-løkke. Funktionen skal returnere 1 hvis str er et palindrom, og 0 hvis str ikke er et palindrom.

Skriv dernæst en tilsvarende rekursiv udgave int is_palindrome_rec(char *str).

Den rekursive funktion skal løse nøjagtig det samme problem som den iterative funktion; Altså om parameteren str er et palindrom. Den skal gøre dette ved at undersøge om en bestemt (lidt mindre) delstreng af strengen er et palindrom. Hvis det hjælper dig, kan du overveje at indføre en hjælpefunktion til is_palindrome_rec, for at gøre dette.

Målet med denne opgave er at opnå viden og færdigheder i at arbejde med såvel rekursive som iterativt programmerede funktioner. Målet er endvidere at opnå yderligere viden og færdigheder i simpel programmering med tekststrenge.

Løsning

Du kan vælge at se en video hvor jeg diskuterer den iterative og rekursive løsning. Her er en løsning, som minder ganske meget om den løsning på palindrom problemet, der udvikles i videoen:

#include <stdio.h>
#include <string.h>

int is_palindrome_rec(char *str);
int is_palindrome_rec_len(char *str, int lgt);
int is_palindrome_iter(char *str);

int main(void) {
  char str[100];

  do{
    printf("Enter string (the string exit will stop the program): ");
    scanf("%s", str);

    if (strcmp(str,"exit") != 0){
      if (is_palindrome_iter(str))
        printf("Iterative %s is a palindrome\n", str);
      else
        printf("Iterative: %s is NOT a palindrome\n", str);
    }

    if (strcmp(str,"exit") != 0){
      if (is_palindrome_rec(str))
        printf("Recursive: %s is a palindrome\n", str);
      else
        printf("Recursive: %s is NOT a palindrome\n", str);
    }

  }
  while (strcmp(str,"exit") != 0);

  return 0;
}

int is_palindrome_rec(char *str){
  int lgt = strlen(str);
  return is_palindrome_rec_len(str, lgt);
}

int is_palindrome_rec_len(char *str, int lgt){
  if (lgt < 2)
    return 1;
  else
    return str[0] == str[lgt-1] && is_palindrome_rec_len(str+1, lgt-2);
}

int is_palindrome_iter(char *str){
  int i, j;
  for(i = 0, j = strlen(str)-1; i < j; i++, j--)
    if (str[i] != str[j]) 
      return 0;
  return 1;     
}


11.3   Heltalsdivision og restuddragning med rekursive funktioner  

Programmer en funktion

  int quotient(int dividend, int divisor)

som beregner dividend / divisor ved brug af rekursion (ved gentagen subtraktion).

Skriv ligeledes en rekursiv funktion

  int modulus(int dividend, int divisor)

som beregner resten, dividend % divisor.

Løsning

Her er min udgave af de to funktioner

#include <stdio.h>
int quotient(int dividend, int divisor);
int modulus(int dividend, int divisor);

int main(void) {
  int a, b;
  printf("Enter dividend and divisor: ");
  scanf(" %d %d", &a, &b);

  printf("%d / %d = %d\n", a, b, quotient(a, b));
  printf("%d %% %d = %d\n", a, b, modulus(a, b));
  
  return 0;
}

/* Calculate dividend / divisor - integer division 
   Precondition: divisor != 0 and both dividend and divisors are non-negative.
*/
int quotient(int dividend, int divisor){
  if (dividend < divisor)
     return 0;
  else 
     return  1 + quotient(dividend - divisor, divisor);
}

/* Calculate dividend % divisor - integer remainder.
   Precondition: divisor != 0 and both dividend and divisors are non-negative.
 */
int modulus(int dividend, int divisor){
  if (dividend < divisor)
     return dividend;
  else 
     return modulus(dividend - divisor, divisor);
}



11.4   Opgave 1 side 587 i PSPD(8ed) - blob_count  

Dette er hints til opgave 1 side 587 i Problem Solving and Program Design in C, 8ed (svarende til opgave 1 side 583 i 7ed).

Der findes en video som giver et oplæg til problemløsningen. Jeg anbefaler at I ser denne video forud for løsningen af opgaven.

Jeg synes det er mere naturligt at kalde funktionen blob_count i stedet for bogens forslag, blob_check.

Bogens grid, vist nederst side 587 (8ed), kan laves på denne måde:

  int grid[5][5] = {
                     {1, 1, 0, 0, 0},
                     {0, 1, 1, 0, 0},
                     {0, 0, 1, 0, 1},
                     {1, 0, 0, 0, 1},
                     {0, 1, 0, 1, 1},
                    };

Med denne opskrivning er det mest naturligt at x 'er lodret' (betegner rækker) og at 'y er vandret' (betegner søjler). Dette syn er altså lidt anderledes end bogens.

Bemærk at grid[0][0] er 1 (øverste venstre hjørne), grid[0][4] er 0 (øverste højre hjørne), grid[4][0] er 0 (nederste venstre hjørne), og at grid[4][4] er 1 (nederste højre hjørne).

Overvej nøje, hvordan du vil håndtere felter som falder uden for griddet (f.eks grid[-1][5]). Hint: Lad blob_count på sådanne felter returnere 0.

Som en centalt punkt i opgaven skal du besøge alle 8 naboer til et punkt i tabellen. Det kan være træls at gøre dette manuelt, nabo for nabo. Overvej hvordan dette kan gøres med (to nestede) for-løkker.

Vær sikker på at funktionen blob_count bliver rekursiv. Altså at funktionen kaldes rekursivt på alle 8 naboer af en celle. Ideen er at tælle blob størrelsen af eksempelvis celle (x+1,y+1) efter nøjagtig samme opskrift som blev anvendt til at tælle blob størrelsen af celle (x,y).

Som beskrevet i bogen er det nødvendigt at markere at en celle er talt. Dette gøres lettest ved at ændre 1-tallet til 0 i cellen. Dette ødelægger griddet. Jeg foreslår derfor at det givne grid kopieres over i et andet grid før kaldet af blob_count. På denne måde er det en kopi, som ødelægges, ikke originalen. Skriv gerne en copy_grid funktion, som udfører dette.

I main bør du indlæse et (x,y) punkt og kalde blob_count(x, y). Gør gerne dette i en løkke, så du kan undersøge flere (x,y) punkter i samme kørsel af programmet.

Løsning

Herunder finder du en udgave af programmet. Der findes en video, som viser hvordan jeg har udviklet løsningen. Pt. er løsningen som udvikles i videoen en anelse anderledes end programmet herunder.

#include <stdio.h>

#define MAX_X 5
#define MAX_Y 5

int blob_count(int x, int y, int grid[MAX_X][MAX_Y]);
void copy_grid(int from_grid[5][5], int to_grid[MAX_X][MAX_Y]);

int main(void) {
  int x, y;
  int grid_original[MAX_X][MAX_Y] = {
                             {1, 1, 0, 0, 0},
                             {0, 1, 1, 0, 0},
                             {0, 0, 1, 0, 1},
                             {1, 0, 0, 0, 1},
                             {0, 1, 0, 1, 1},
                            },
      grid[MAX_X][MAX_Y];

  do{
    printf("Enter x and y (a negative x or y terminates): ");
    scanf("%d %d", &x, &y);

    copy_grid(grid_original, grid);

    if (x >= 0 && y >= 0)
      printf("blob check af (%d,%d): %d\n", x, y, blob_count(x,y,grid));
  }  while (x >= 0 && y >= 0);
   
  return 0;
}

int blob_count(int x, int y, int grid[MAX_X][MAX_Y]){
  if (x < 0 || y < 0 || x > MAX_X - 1 || y > MAX_Y - 1)
     return 0;
  else if (grid[x][y] == 1){
     int sum, dx, dy;
     
     grid[x][y] = 0;    // Reset cell, in order not to count again
     sum = 1;           // Now counted.

     for(dx = -1; dx <= 1; dx++)
       for(dy = -1; dy <= 1; dy++)
         sum += blob_count(x + dx, y + dy, grid);

     return sum;
  }
  else //  grid[x][y] == 0
     return 0;
}

void copy_grid(int from_grid[MAX_X][MAX_Y], int to_grid[MAX_X][MAX_Y]){
 int x, y;

 for(x = 0; x < MAX_X; x++)
    for(y = 0; y < MAX_Y; y++)
      to_grid[x][y] = from_grid[x][y];
}



11.5   Rekursive udgaver af Euclids algoritme  

I denne opgave vil vi programmere iterative beregninger med brug af rekursion. Funktionerne, som skal programmeres i denne opgave, skal altså følge det mønster vi har set i funktionen iterative_factorial (et program i lektionen om rekursion).

Vi har set iterative udgaver af Euclids algoritme i lektionen om gentagelse og løkker og vi har set en gcd funktion i lektionen om funktioner.

Programmer først en rekursiv udgave af den version af Euclids algoritme, der er baseret på gentagen restberegning.

Programmer dernæst en rekursiv udgave af den version af Euclids algoritme, der er baseret på gentagen subtraktion.

Løsning

Her er en rekursiv udgave af Euclids algoritme, der er baseret på gentagen restberegning:

#include <stdio.h>

int gcd(int a, int b);

int main(void) {
  int i, j, result;
 
  printf("Enter two non-negative integers: ");
  scanf("%d %d", &i, &j);

  result = gcd(i, j);

  printf("GCD of %d and %d is %d\n\n", i, j, result);
  
  return 0;
}   

/* If a < b, then the first recursive call will be gcd(b, a).
   This call will exchange the parameters such that, subsequently, a >= b */
int gcd(int a, int b){
  if (b == 0)
    return a;
  else 
    return gcd(b, a % b);
}

Og her er, tilsvarende, er rekursiv udgave baseret på gentagen subtraktion:

#include <stdio.h>

int gcd(int a, int b);

int main(void) {
  int i, j, result;
 
  printf("Enter two non-negative integers: ");
  scanf("%d %d", &i, &j);

  result = gcd(i, j);

  printf("GCD of %d and %d is %d\n\n", i, j, result);
  
  return 0;
}   

int gcd(int a, int b){
  if (a == b)
    return a;
  else if (a > b)
    return gcd(a - b, b);
  else 
    return gcd(a, b - a);
}


Bemærk at begge de viste rekursive funktioner er halerekursive. Det er er bestemt ikke en tilfældighed. Iterative beregningsprocesser - programmeret med løkker - kan systematisk transformeres til halerekursive funktioner, hvor beregningens tilstand bliver parametre i funktionen.


Genereret: Onsdag 17. november 2021, 12:59:02