8. gyakorlat: állapotgépek

Állapotgépek. A feladatok megoldásának lényege az, hogy az állapotátmeneti táblát és a tekékenyságtáblát megalkotjuk: az állapotgépet megtervezzük. Ha ez megvan, akkor onnantól a kódolás már mechanikus. Egy olyan feladatnál, ahol több állapotátmenet is van, nem lehet csak úgy nekiugrani a kódolásnak! Ha a táblázat megalkotása után fejből írunk valami kódot, nem a tábla alapján, annak sincs semmi értelme. Az is értelmetlen, ha a kettőt fordított sorrendben csináljuk meg…

A tervezés közben érdemes az állapottáblát használni a gráf helyett, hiszen áttekinthetőbb. A kódolás során pedig kifejezetten jobb abből dolgozni.

1 „Hejesírásreform”

Írjunk programot, amelyik megreformálja a helyesírást: cseréljen minden ly karakterpárt j betűre, minden lly karakterhármast jj párra! Kis/nagybetűkkel most ne foglalkozzunk; illetve tekintsünk el attól az esettől, amikor egy összetett szó közepén az első tag l-lel végződik, a második y-nal vagy ly-nal kezdődik.

Megoldás

lyegyéb
alap→lbetűki: cki: c
lbetű→hosszú_llki: "j"
→alap
ki: "l", c
→alap
hosszú_llki: "l"ki: "jj"
→alap
ki: "ll", c
→alap

A kiírásokat itt jól meg kell gondolni. Alapállapotban mindent kiírunk, kivétel az l betűt, mert az lehet egy későbbi ly része. lbetű állapotban bejövő y esetén kiírjuk a j-t; viszont bejövő egyéb karakter esetén az előző l-t is ki kell írni, és a mostanit is (ilyen szó: „előző”). hosszú_ll esetén pedig, ha bármi más jön, akkor az előző ll-t is ki kell írni (ilyen szó: „hallgat”).

#include <stdio.h>

int main()
{
    enum allapot {
        alap,
        lbetu,
        hosszu_ll
    } all;
    int c;          /* ennek kell intnek lennie! */

    all=alap;
    while ((c=getchar())!=EOF) {
        switch (all) {
            case alap:
                if (c=='l')
                    all=lbetu;
                else
                    putchar(c);
                break;
            case lbetu:
                switch (c) {
                    case 'l':
                        all=hosszu_ll;
                        break;
                    case 'y':
                        putchar('j');
                        all=alap;
                        break;
                    default:
                        printf("l%c", c);
                        all=alap;
                        break;
                }
                break;
            case hosszu_ll:
                switch (c) {
                    case 'l':
                        putchar('l');
                        break;
                    case 'y':
                        printf("jj");
                        all=alap;
                        break;
                    default:
                        printf("ll%c", c);
                        all=alap;
                        break;
                }
                break;
        }
    }

    return 0;
}

A szövegfájloknál bevett szokás az, hogy a fájl legutolsó karaktere mindig egy újsor (\n) karakter. Ezt azonban sajnos nem mindenhol tartják be (és nem minden szövegszerkesztő tesz így).

Ha a fájl utolsó szava „hell”, és utána még van egy újsor karakter, akkor minden kiíródik helyesen. Ha azonban az az újsor karakter hiányzik, akkor a fenti program sem működik tökéletesen (az amúgy tulajdonképp hibásnak tekinthető (!) fájlra). Ezt úgy tudjuk kezelni, ha az állapotgép ciklusa után megvizsgáljuk az állapotváltozót. Az lbetu és a hosszu_ll állapotok azt jelentik, hogy a fájl l/ll karakter(ek)re végződött, lezáró újsor nélkül. Ezeket kiírhatjuk az állapotgép ciklusából kilépés után:

/* lehet, hogy vannak "beragadt" l betuk */
switch (all) {
   case alap: break;
   case lbetu: printf("l"); break;
   case hosszu_ll: printf("ll"); break;
}

2 Rövidítések

Írjunk állapotgépes mondatot, amely a soronkért beírt tulajdonnevekből rövidítéseket csinál. Írja ez ki minden szó első betűjét. Pl.

Budapesti Muszaki Egyetem
BME
Elektronikus Eszkozok Tanszeke
EET

Megoldás

A tervezés két állapotot ad: azt, amikor várunk a szó első betűjére (mert azt kell kiírni), és azt, amikor egy szó belsejében vagyunk.

szóköz\negyéb
szóra vár--ki: c
→szóban
szóban→szóra várki: \n
→szóra vár
-
#include <stdio.h>
#include <ctype.h>

int main()
{
    enum Allapot { szora_var, szoban };
    
    int c;
    enum Allapot all;
    
    all=szora_var;
    while ((c=getchar()) != EOF) {
        switch (all) {
            case szora_var:
                if (!isspace(c)) {
                    putchar(c);
                    all=szoban;
                }
                break;
            case szoban:
                if (c=='\n')
                    putchar(c);
                if (isspace(c))
                    all=szora_var;
                break;
        }
    }
    
    return 0;
}

3 Mondat második szava

Tervezz állapotgépes programot, amelyik a szabványos bemeneten olvasott szövegből minden mondat második szavát írja csak ki! Jelenjenek meg ezek a szavak külön sorban!

Megoldás

A tervezést az „új mondat” állapotnál kell kezdeni. Ide kell kerülnie egy bejövő pont, felkiáltójel vagy kérdőjel karakter hatására a gépnek – de az induláskor is. Ettől meg kell különböztetni az „első szó” állapotot, amelybe akkor kerülünk, amikor az első, szó betűjeként értelmezhető karaktert megkapjuk. Innen a „második szóra vár” állapotba egy újabb szóköz (vagy szóelválasztó) hatására kerülünk. Ez még nem a „második szó” állapot, ahol majd ki kell írni a beolvasott betűket (és ahonnan tovább ugrunk egy újabb szóelválasztó hatására), hiszen itt még sok szóelválasztó jöhet (pl. szóköz), ami még nem jelenti a második szó elkezdését. Ha betűt kapunk, az viszont már igen, és az a második szó első betűje kell legyen, ezért ki is írjuk. Azután egy újabb szóelválasztó hatására onnantól „mondat vége” állapotban működik tovább a gép, és már nem figyel semmire, csak várja a következő mondatelválasztó karaktert, amely hatására minden elölről kezdődik.

. ! ?szóköz \n , : ;egyéb
új mondat--→első szó
első szó→új mondat→második szó-
második szóra vár→új mondat-kiír: c
→második szó
második szó→új mondat→mondat vége
kiír: \n
kiír: c
mondat vége→új mondat--

Érdemes a mondatelválasztó és a szóelválasztó karakter felismeréséhez segédfüggvényeket írni.

#include <stdio.h>
 
/* Igazzal tér vissza, ha a karakter mondat végét jelzi. */
int mondatvege_jel(int c)
{
    return c=='!' || c=='.' || c=='?';
}
 
/* Igaz, ha a karakter szó végét jelzi. Nem csak szóköz lehet,
 * hanem pl. vessző, pontosvessző is. */
int szovege_jel(int c)
{
    return c==' ' || c=='\n' || c==':' || c==',' || c==';';
}
 
int main()
{
    enum Allapot { ujmondat, elsoszo, masodikszoravar, masodikszo, mondatvege };
 
    int c;
    enum Allapot all=ujmondat;
    
    while ((c=getchar())!=EOF) {
        switch (all) {
            case ujmondat:
                if (!mondatvege_jel(c) && !szovege_jel(c))
                    all=elsoszo;
                break;
            case elsoszo:
                if (mondatvege_jel(c))
                    all=ujmondat;
                else if (szovege_jel(c))
                    all=masodikszoravar;
                break;
            case masodikszoravar:
                if (mondatvege_jel(c))
                    all=ujmondat;
                else if (!szovege_jel(c)) {
                    putchar(c);
                    all=masodikszo;
                }
                break;
            case masodikszo:
                if (mondatvege_jel(c))
                    all=ujmondat;
                else if (szovege_jel(c)) {
                    putchar('\n');
                    all=mondatvege;
                }
                else
                    putchar(c);
                break;
            case mondatvege:
                if (mondatvege_jel(c))
                    all=ujmondat;
                break;
        }
    }
    
    return 0;
}

4 Mondatok nagybetűsítője

Írjunk állapotgépes programot, amely a beírt, csupa kisbetűkből álló szöveget úgy javítja ki, hogy minden mondat elején álló első betűt nagybetűre cseréli!

Megoldás

Mondat végét jelző írásjel után a következő betű nagybetű, de csak akkor, ha szóköz is jött. Figyelni kell, hogy az utána lévő szóköztől nem váltunk kisbetűs módra még! A gép alapállapota a nagybetűsítés, hiszen a bejövő szöveg legeslegelső karaktere biztosan mondat elején van.

. ! ?szóköz, \negyéb
nagybetűki: cki: cki: toupper(c)
→kisbetű
kisbetűki: c
→mondatvége?
ki: cki: c
mondatvége?ki: cki: c
→nagybetű
ki: c
→kisbetű
#include <stdio.h>
#include <ctype.h>

int mondatvege_jel(int c)
{
    return c=='!' || c=='.' || c=='?';
}

int main()
{
    enum allapot {
        nagybetu,
        kisbetu,
        mondatvege
    } all;
    int c;

    all=nagybetu;       /* kiindulo allapot */

    while ((c=getchar())!=EOF) {
        switch (all) {
            case nagybetu:
                putchar(toupper(c));
                if (!mondatvege_jel(c) && !isspace(c))
                    all=kisbetu;
                break;
            case kisbetu:
                putchar(c);
                if (mondatvege_jel(c))
                    all=mondatvege;
                break;
            case mondatvege:
                putchar(c);
                if (isspace(c))
                    all=nagybetu;
                else
                    if (!mondatvege_jel(c))
                        all=kisbetu;
                break;
        }
    }

    return 0;
}

5 Zárójeles szöveg

Feladat: írjunk állapotgépes programot, amelyik egy adott szövegből eltávolítja a (zárójelezett részeket). Előfordulhat, hogy a zárójelek egymásba vannak ágyazva (például így, mint ez a (zárójeles) szó).

Megoldás

Megoldás: nem lehet megoldani véges számú állapotot tartalmazó állapotgéppel. Minden bezáró zárójel ) esetén tudni kellene, hogy hány be nem zárt zárójel van még; ez annyiféle ( utáni állapotot feltételez.

6 További állapotgépek

C++ komment szűrő

C++ kommenteket eltávolító állapotgép. C++-ban a komment //-rel kezdődik, sor végéig tart.

Megoldás

/\negyéb
alap→perki: cki: c
perkommentki: /, \n
→alap
ki: /, c
→alap
komment-ki: \n
→alap
-

C++ komment → C komment

A //-es C++ kommenteket /* */ C kommentté alakítja.

Megoldás

A megtalált komment elejénél kiír egy /* karaktersorozatot. A komment belsejében minden karaktert kiír (ahogy máskor is). A komment végén, az újsor a karakternél pedig kiírja a bezáró */-ot, és persze az újsort is (hogy a forráskód tördelése ne változzon).

/\negyéb
alap→perki: cki: c
perki: /*
→komment
ki: /, \n
→alap
ki: /, c
→alap
kommentki: cki: */\n
→alap
ki: c