14. gyakorlat: generikus algoritmusok

Függvényekre mutató pointerek. A gyakorlat első felében a generikus gondolkodást bemutató feladatok vannak – magas szinten. Az utolsó feladat, a generikus rendezés megvalósítása a típus nélküli pointerek kezelését mutatja be – alacsony szinten.

1 Karakterek számlálása típusok szerint

Írjunk C függvényt, amelyik egy sztringben meg képes számolni, hogy hány bizonyos tulajdonsággal rendelkező karakter van (pl. kisbetűk vagy számjegyek, esetleg írásjelek). Hogy mi ez a tulajdonság, az is a függvény paramétere legyen.

Megoldás

A sztringen végiglépkedés, a karakterek számlálásának módja független attól, hogy konkrétan milyen karaktert keresünk. Az „e” betűs függvény a karaktert int-ként veszi át, hogy kompatibilis legyen az islower() és hasonló függvényekkel.

#include <stdio.h>
#include <ctype.h>

int e_betu_e(int c) {
    return c=='E' || c=='e';
}

/* A feladat megoldasa ez a fuggveny. */
/* A "feltetel" parameter: egy darab int parameteru (c), int
   visszateresi ertekkel rendelkezo fuggvenyre mutato pointer. */
int karakter_szamol(char *str, int (*feltetel)(int c)) {
    int darab, i;

    darab=0;
    for (i=0; str[i]!=0; ++i)
        if (feltetel(str[i]))
            darab++;
    return darab;
}

int main() {
    char szoveg[]="Ernoke 4 eves (jovore 5 lesz).";

    printf("%d e vagy E betű\n", karakter_szamol(szoveg, e_betu_e));
    printf("%d space\n", karakter_szamol(szoveg, isspace));
    printf("%d kisbetű\n", karakter_szamol(szoveg, islower));
    printf("%d számjegy\n", karakter_szamol(szoveg, isdigit));

    return 0;
}

2 Numerikus integrálás

Integráljunk egy függvényt egy korlátos intervallumon numerikusan! Vagyis számoljuk ki az alatta lévő területet. Közelítsük ezt téglalapokkal! Írjunk generikus algoritmust, amely bármely függvényen képes dolgozni!

Megoldás

  • Az x tengelyen dx lépésközzel haladunk.
  • Mindenhol kiszámítjuk f(x) értékét.
  • Így meghatározható a rajzon látható téglalapok területe.
  • Az integrál közelítő értéke ezek összege: ∑f(x)*dx.

Az integrálandó f(x) függvényt paraméterként vesszük át. Mivel ez a példában egy egyváltozós, valós függvény, C-ben egy olyan függvényként jelenik meg, amelynek egy double paramétere, és double visszatérési értéke van. A paraméter típusa tehát:

double (*f)(double)

Az ezt megvalósító függvény pedig az alábbi. (A dx-szel való szorzást kiemelhettük a szummán kívülre, hogy gyorsabb legyen a program.)

#include <math.h>

double integral(double (*f)(double), double ettol, double eddig, double dx) {
   double osszeg=0.0, x;

   for (x=ettol; x<eddig; x+=dx)
      osszeg+=f(x);               // ∑f(x)

   return dx*osszeg;              // dx * ∑f(x)
}

double sajatf(double x) {
   return 5*x*x + 2*x - 24.3;
}

int main() {
   double t;

   t = integral(sajatf, 14.3, 29.2, 0.1);
   printf("sajatf -> %g\n", t);
   t = integral(sin, 0, 3.1415927, 0.05);
   printf("sin -> %g\n", t);

   return 0;
}

Természetesen akár a könyvtári sin() függvényt is integrálhatjuk, hiszen annak is double sin(double) a típusa.

3 Összeg, szorzat: akkumuláció

Írjunk függvényt, amely paraméterként kap két egész számot (a és b), és összegzi a két intervallum közötti számokat (beleértve a-t és b-t is).

Megoldás

int osszeg(int a, int b) {
   int szum, i;
   
   szum=0;
   for (i=a; i<=b; i+=1)
      szum=szum+i;
   
   return szum;
}

Alakítsuk át ezt úgy, hogy ne csak összegezni, hanem szorozni is lehessen vele. Ehhez az összeadást ki kell cserélnünk egy akkumuláló műveletre, és természetesen a kezdeti értéket is ki kell cserélnünk egy paraméterre (hiszen 0-nál a szorzat mindig 0 lenne).

Megoldás

int akkumulal(int a, int b, int (*akkum)(int, int), int kiindul) {
   int akk, i;
   
   akk=kiindul;
   for (i=a; i<=b; i+=1)
      akk=akkum(akk, i);
   
   return akk;
}

Gondoljunk bele: így olyan összegzések is megvalósíthatóak (pl. kivonás, osztás, négyzetösszeg, stb.), amikre az összegzés függvény megírója eredendően nem is gondolt. A függvény tovább általánosítható lenne, ha az i+=1 lépést is egy függvényre cserélnénk le.

Számoljuk ki ezzel a függvénnyel az 1…6 számok összegét és az 1…6 számok szorzatát (faktoriális)!

Megoldás

int osszegzo(int a, int b) {
   return a+b;
}

int szorzo(int a, int b) {
   return a*b;
}

int main() {
   printf("1-tol 6-ig osszeg: %d\n", akkumulal(1, 6, osszegzo, 0));
   printf("1-tol 6-ig szorzat: %d\n", akkumulal(1, 6, szorzo, 1));
}

4 double-öket rendező függvény

Írjunk függvényt, amely paraméterként kap egy double elemekből álló tömböt, és rendezi azt! A rendezés szempontja (növekvő, csökkenő, abszolútérték szerint növekvő stb.) is legyen a függvény paramétere!

Megoldás

#include <stdio.h>
#include <math.h>

/* rendezi a tombot a megadott szempont szerint */
void double_rendez(double tomb[], int meret, int (*kisebb_e)(double a, double b)) {
   int i;
   
   for (i=0; i<meret-1; ++i) {
      int lk=i;
      int j;
      double temp;
      
      for (j=i+1; j<meret; ++j)
         if (kisebb_e(tomb[j], tomb[lk]))
            lk=j;
      
      temp=tomb[lk];
      tomb[lk]=tomb[i];
      tomb[i]=temp;
   }
}

/* igaz, ha a<b */
int kisebb(double a, double b) {
   return a<b;
}

/* igaz, ha |a|<|b| */
int abszolut_kisebb(double a, double b) {
   return fabs(a)<fabs(b);
}

int main() {
   int i;
   double tomb[10]={1.2, 5.6, 9, -1.4, -6, 5, 9.1, 11, 0, -12};
   
   double_rendez(tomb, 10, kisebb);
   for (i=0; i<10; ++i) 
      printf("%7.2f", tomb[i]);
   printf("\n");

   double_rendez(tomb, 10, abszolut_kisebb);
   for (i=0; i<10; ++i) 
      printf("%7.2f", tomb[i]);
   printf("\n");

   return 0;
}

A fenti függvények nem követik az strcmp() konvenciót: az összehasonlító függvények nem kompatibilisek azzal. Azonban nincs is erre szükség itt még, hiszen a rendezendő adatok típusa adott.

Gondoljuk meg: szétválasztottuk a rendezés algoritmusát az összehasonlítás menetétől. Ennek több előnye van:

  • Külön tudjuk megvalósítani őket.
  • Az összehasonlítás algoritmusa cserélhető anélkül, hogy a rendező algoritmust módosítani kellene.
  • A rendező függvény használója olyan rendezési szempontot is megadhat, amelyre a rendező függvény írója „álmában sem gondolt”.

5 A qsort() használata

Írjunk összehasonlító függvényeket a qsort()-hoz, amelyekkel:

Megoldás

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

int integer_osszehasonlito(const void *pelso, const void *pmasodik) {
    const int *elso=(const int *)pelso;
    const int *masodik=(const int *)pmasodik;

    if (*elso < *masodik)
        return -1;
    if (*elso > *masodik)
        return 1;
    return 0;
}

int sztring_csokkeno(const void *pelso, const void *pmasodik) {
    /* -1-gyel megszorzom, es akkor csokkenobe lesz rendezve!
     * a parameterek megforditasa is jo lenne:
     *    return strcmp(pmasodik, pelso); */
    return -1*strcmp((const char *) pelso, (const char *) pmasodik);
}

int main() {
    enum { MERET=15, SZAVAKMERET=5 };
    /* 5 db, max 19 betus szo */
    char szavak[SZAVAKMERET][20]={"mango", "pomelo", "papaja", "grapefruit", "lime"};
    int intek[MERET];
    int i;

    /* veletlen elemek */
    srand(time(0));
    for (i=0; i<MERET; i++)
        intek[i]=rand()%100;

    /* rendezes es kiiras */
    /* itt az int-et batran sizeofolhatom */
    qsort(intek, MERET, sizeof(intek[0]), integer_osszehasonlito);
    for (i=0; i<MERET; i++)
        printf("%d ", intek[i]);
    printf("\n");

    /* a sztringeket azert sizeofolom, mert a karaktertomb
       meretere vagyok kivancsi, nem pedig a sztring hosszara! */
    qsort(szavak, SZAVAKMERET, sizeof(szavak[0]), sztring_csokkeno);
    for (i=0; i<SZAVAKMERET; i++)
        printf("%s ", szavak[i]);
    printf("\n");

    return 0;
}

6 Saját generikus rendező függvény

Alakítsuk át az előbbi rendezőfüggvényünket úgy (bármelyik algoritmust is választottuk), hogy ne double, hanem tetszőleges elemekkel dolgozzon!

Megoldás

Ehhez a függvénynek void* pointerrel kell átvennie a tömböt (mivel a típusait nem ismeri), és ugyancsak paraméter kell legyen az egyes elemek mérete is. Az elemek cseréje nem végezhető el értékadással, de segítségül hívhatjuk a memcpy függvényt, amely adott méretű memóriaterületet másol. Az „ideiglenes változó” helyébe – amely a háromlépéses cseréhez kell – is egy külön, típus nélküli memóriaterület lép. Mivel ennek mérete függvényparaméter, dinamikusan foglaljuk. A hasonlító függvény is void*-okat kell várjon, nem pedig double-öket.

void rendez(void *tomb,
            int meret, size_t elemmeret,
            int (*kisebb_e)(void const *pa, void const *pb)) {
   int i;
   void *temp;
   
   temp=malloc(elemmeret);
   
   for (i=0; i<meret-1; ++i) {
      void *pi=(char*)tomb + i*elemmeret;
      void *plk=pi;
      int j;
      
      for (j=i+1; j<meret; ++j) {
         void *pj=(char*)tomb + j*elemmeret;
         if (kisebb_e(pj, plk))
            plk=pj;
      }

      memcpy(temp, plk, elemmeret); /* temp=lk */
      memcpy(plk, pi, elemmeret);   /* lk=i */
      memcpy(pi, temp, elemmeret);  /* i=temp */
   }
   
   free(temp);
}

A teljes program letölthető innen: genrend.c.