Uitwerking van opgave 8 van paragraaf 6.18 uit het boek "De programmeertaal C", 4de editie van Kelley en Pohl.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft.

Gegeven is een polynoom van graad n: f(x) = a0 + a1 x + a2 x2 + a3 x3 + ... + an xn waarbij de coëfficiënten a0, a1, ... , an reële getallen voorstellen en an ongelijk aan 0 is. In een C programma kunnen we de coëfficiënten opslaan in een array met (bijvoorbeeld) de naam p. Element p[0] bevat dan de coëfficiënt a0 en element p[1] bevat coëfficiënt a1, enz.

Gevraagd wordt om een functie genaamd eval te schrijven die de waarde van een polynoom (met graad n, waarvan de coëfficiënten opgeslagen zijn in een array p) in het punt x berekent. Het prototype van de functie (de functiedeclaratie) is gegeven:

double eval(double p[], double x, int n);

We beginnen met het schrijven van een testprogramma:

#include <stdio.h>

/* © 2012 Harry Broeders 
   Uitwerking van opgave 8 van paragraaf 6.18 uit het boek "De programmeertaal C", 4de editie van Kelley en Pohl */

double eval(double p[], double x, int n) {
    /* dummy */
    return 0;
}

void printErrorAlsOngelijk(double x, double y) {
    /* if (x != y) werkt niet vanwege afrondingsfouten ! */
    double verschil = x - y;
    double absoluutVerschil;
    if (verschil > 0) {
        absoluutVerschil = verschil;
    }
    else {
        absoluutVerschil = -verschil;
    }
    if (absoluutVerschil >= 1E-10) {
        printf("Error: verwachte waarde = %.10f maar werkelijke waarde is %.10f\n", y, x);
    }
}

int main(void) {
    double p0[] = {-3};                     /* f(x) = -3                                         */
    double p1[] = {4, -4};                  /* f(x) =  4 +   -4*x                                */
    double p2[] = {-2, -1, 1};              /* f(x) = -2 +   -1*x +    1*x^2                     */
    double p3[] = {-2, -1.5, 0.75, 0.25};   /* f(x) = -2 + -1.5*x + 0.75*x^2 +  0.25*x^3         */
    double p4[] = {3, 0.3, -0.5, 6.125, 2}; /* f(x) =  3 +  0.3*x + -0.5*x^2 + 6.125*x^3 + 2*x^4 */
    double y;

    y = eval(p0, 3.2, 0);
    printf("Als f(x) = -3 dan geldt f(3.2) = %f\n", y);
    printErrorAlsOngelijk(y, -3);
    y = eval(p1, 3.2, 1);
    printf("Als f(x) = 4 - 4*x dan geldt f(3.2) = %f\n", y);
    printErrorAlsOngelijk(y, -8.8);
    y = eval(p2, 3.2, 2);
    printf("Als f(x) = -2 - 1*x + x^2 dan geldt f(3.2) = %f\n", y);
    printErrorAlsOngelijk(y, 5.04);
    y = eval(p3, 3.2, 3);
    printf("Als f(x) = -2 - 1.5*x + 0.75*x^2 + 0.25*x^3 dan geldt f(3.2) = %f\n", y);
    printErrorAlsOngelijk(y, 9.072);
    y = eval(p4, 3.2, 4);
    printf("Als f(x) = 3 + 0.3*x - 0.5*x^2 + 6.125*x^3 + 2*x^4 dan geldt f(3.2) = %f\n", y);
    printErrorAlsOngelijk(y, 409.2592);

    fflush(stdin);
    getchar();
    return 0;
}

Om te kijken of het antwoord correct is moeten we de berekende waarde vergelijken met de verwachte waarde (die we op een rekenmachine hebben berekend). Omdat beide getallen van het type double zijn kunnen we beide getallen niet met behulp van de operator != vergelijken. Als er namelijk in de berekening een heel klein afrondfoutje is opgetreden (en dat zal zeker gebeuren) dan zal de operator != aangeven dat het antwoord niet correct is terwijl het in feite wel correct is (op een klein, niet te vermijden, afrondfoutje na). Dit probleem wordt opgelost door de berekende waarde goed te rekenen als:
| berekende waarde - verwachte waarde | < 10-10.

Het bovenstaande programma geeft de volgende uitvoer:

Als f(x) = -3 dan geldt f(3.2) = 0.000000
Error: verwachte waarde = -3.0000000000 maar werkelijke waarde is 0.0000000000
Als f(x) = 4 - 4*x dan geldt f(3.2) = 0.000000
Error: verwachte waarde = -8.8000000000 maar werkelijke waarde is 0.0000000000
Als f(x) = -2 - 1*x + x^2 dan geldt f(3.2) = 0.000000
Error: verwachte waarde = 5.0400000000 maar werkelijke waarde is 0.0000000000
Als f(x) = -2 - 1.5*x + 0.75*x^2 + 0.25*x^3 dan geldt f(3.2) = 0.000000
Error: verwachte waarde = 9.0720000000 maar werkelijke waarde is 0.0000000000
Als f(x) = 3 + 0.3*x - 0.5*x^2 + 6.125*x^3 + 2*x^4 dan geldt f(3.2) = 0.000000
Error: verwachte waarde = 409.2592000000 maar werkelijke waarde is 0.0000000000

De functie eval kan als volgt geïmplementeerd worden:

double eval(double p[], double x, int n) {
    int i, j;
    double res = p[0];
    for (i = 1 ; i <= n; i = i + 1) {
        /* bereken x^i */
        double xTotDeMachti = x;
        for (j = 1; j < i; j = j + 1) {
            xTotDeMachti = xTotDeMachti * x;
        }
        res = res + p[i] * xTotDeMachti;
    }
    return res;
}

In het boek wordt dit de "naïeve" aanpak genoemd. De functie eval kan echter efficiënter geïmplementeerd worden door gebruik te maken van de regel van Horner. Volgens deze regel kunnen we f(x) = a0 + a1 x + a2 x2 + a3 x3 + ... + an xn ook schrijven als f(x) = a0 + x ( a1 + x (a2 + x ( a3 ... + x (an) ... ))). Merk op dat we in dit geval de coëfficiënten van hoog naar laag gebruiken, omdat we de haakjes van binnen naar buiten moeten uitwerken. De functie evalHorner kan als volgt geïmplementeerd worden:

double evalHorner(double p[], double x, int n) {
    int i;
    double res = p[n];
    for (i = n - 1 ; i >= 0; i = i - 1) {
        res = p[i] + x * res;
    }
    return res;
}

Het hele programma kun je hier vinden: 6.18 opgave 8.c.

Tot slot wordt in het boek de vraag gesteld: Hoeveel optellingen en vermenigvuldigingen komen er voor in elk van de twee versies van de functie (eval en evalHorner).

In de functie eval wordt f(x) berekend met de formule: f(x) = a0 + a1 x + a2 x2 + a3 x3 + ... + an xn. Dit kunnen we ook schrijven (door de machten uit te schrijven) als f(x) = a0 + a1 x + a2 x x + a3 x x x + ... + an x x x ... x. We zien dat het aantal optellingen bij de naieve methode gelijk is aan de graad van de polynoom (n). Het aatal vermenigvuldigingen bij de naieve methode is gelijk aan 0 + 1 + 2 + 3 + ... + n (waarbij n de graad van de polynoom is). Deze reeks is gelijk aan 0.5 * n * (n + 1). Voor een 4de graads polynoom is het aantal vermenigvuldigingen dus 0.5 * 4 * 5 = 10.

In de functie evalHorner wordt f(x) berekend met de formule: f(x) = a0 + x ( a1 + x (a2 + x ( a3 ... + x (an) ... ))). We zien dat het aantal optellingen bij de methode van Horner gelijk is aan de graad van de polynoom (n). Het aatal vermenigvuldigingen bij de methode van Horner is ook gelijk aan de graad van de polynoom (n). Voor een 4de graads polynoom is het aantal vermenigvuldigingen dus 4.

In het programma 6.18 opgave 8b.czijn instructies toegevoegd om het aantal optellingen en vermenigvuldigingen te tellen. De uitvoer van dit programma is zoals verwacht:

Naief
graad = 0 : aantalOptellingen = 0, aantalVermenigvuldigingen = 0
graad = 1 : aantalOptellingen = 1, aantalVermenigvuldigingen = 1
graad = 2 : aantalOptellingen = 2, aantalVermenigvuldigingen = 3
graad = 3 : aantalOptellingen = 3, aantalVermenigvuldigingen = 6
graad = 4 : aantalOptellingen = 4, aantalVermenigvuldigingen = 10
Horner
graad = 0 : aantalOptellingen = 0, aantalVermenigvuldigingen = 0
graad = 1 : aantalOptellingen = 1, aantalVermenigvuldigingen = 1
graad = 2 : aantalOptellingen = 2, aantalVermenigvuldigingen = 2
graad = 3 : aantalOptellingen = 3, aantalVermenigvuldigingen = 3
graad = 4 : aantalOptellingen = 4, aantalVermenigvuldigingen = 4

De implementatie met behulp van de regel van Horner is dus sneller omdat er (veel) minder vermenigvuldigingen nodig zijn. Dit zal vooral bij grote waarden van n opvallen. Voor n = 1000000 geld dat bij de naïeve implementatie 500000500000 vermenigvuldigingen nodig zijn terwijl bij de implementatie die gebruik maakt van de regel van Horner slechts 1000000 vermenigvuldigingen nodig zijn. Een PC met een rekenkracht van 20 GFlops doet ongeveer 25 seconden over de vermenigvuldigingen in de naïeve implementatie en 0,00005 seconden over de vermenigvuldigingen in de Horner implementatie.