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 |
