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
l | y | egyéb | |
---|---|---|---|
alap | →lbetű | ki: c | ki: c |
lbetű | →hosszú_ll | ki: "j" →alap | ki: "l", c →alap |
hosszú_ll | ki: "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 ahosszu_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 | \n | egyéb | |
---|---|---|---|
szóra vár | - | - | ki: c →szóban |
szóban | →szóra vár | ki: \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, \n | egyéb | |
---|---|---|---|
nagybetű | ki: c | ki: c | ki: toupper(c) →kisbetű |
kisbetű | ki: c →mondatvége? | ki: c | ki: c |
mondatvége? | ki: c | ki: 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
/ | \n | egyéb | |
---|---|---|---|
alap | →per | ki: c | ki: c |
per | komment | ki: /, \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).
/ | \n | egyéb | |
---|---|---|---|
alap | →per | ki: c | ki: c |
per | ki: /* →komment | ki: /, \n →alap | ki: /, c →alap |
komment | ki: c | ki: */\n →alap | ki: c |