A listákat és a fákat általában dinamikus adatszerkezeteknek nevezzük, mert dinamikus foglalással hozzuk létre az egyes csomópontjaikat. A dinamikus memóriakezelés azonban nem szükségszerű. A csomópontokból építhetünk tömböt is, hiszen az egyes csomópontokban tárolt pointerek nem feltétlenül kell dinamikus memóriaterületre mutassanak, mutathatnak egy tömb elemeire is. (Sőt ilyenkor akár a pointerek helyett indexeket is használhatnánk: -1 jelentené a lista végét vagy az üres fát, mert az nem lehet tömbindex.)
Például egy láncolt lista, amely az 5, 6, 7 számokat tartalmazza, és a tömb 0, 2, 1. elemeiből épül fel:
struct Listaelem {
int adat;
ListaElem *kov;
};
ListaElem elemek[4] = {
{ 5, &elemek[2] },
{ 7, NULL },
{ 6, &elemek[1] },
};
ListaElem *eleje = &elemek[0];
A tömb 3. eleme üres, ha a listát bővíteni szeretnénk, ott még van egy hely.
Mindez adhat egy ötletet a morzekód dekódoláshoz. Tudjuk, hogy a morzekódot egy dekódoló fával lehet leggyorsabban dekódolni. És azt is, hogy a morzekód előre adott, nem változik - ezért a dekódoló fát legjobb lenne már a forráskódba beépíteni, nem pedig futási időben létrehozni. A forráskód a fentihez hasonló lehetne.
Ezt azonban kézzel megírni elég időrabló dolog, jobb lenne egy másik programmal generálni. A feladat tehát a következő: ki kell indulnod egy olyan forráskódból, amelyik tartalmazza a morzeábécét:
Morse morse_data[] = {
{ 'A', ".-" },
{ 'B', "-..." },
{ 'C', "-.-." },
/* ... */
{ 'Z', "--.." },
{ '0', "-----" },
{ '1', ".----" },
/* ... */
{ '9', "----." },
{0}
};
És ezt kell kiegészítened egy olyan teljes programmá, amelyik a fenti tömb felhasználásával előállítja a morzekód dekódolására használható bináris fát a szabványos kimenetén:
MorseTree root[] = {
{ 0, root + 1, root + 3 },
{ 'E', root + 9, root + 2 },
{ 'A', root + 18, root + 16 },
{ 'T', root + 4, root + 12 },
/* ... */
};
Ennek a fának a gyökere a tömb 0. eleme. Onnan eggyel balra, az 1. indexen az 'E' betű van (ti), eggyel jobbra, a 3. indexen pedig a 'T' betű (tá). (Választásod szerint a tömbbön belüli pointerek helyett lehetnek akár tömbindexek is.)
Mutasd is meg, hogy működik az így megépített fa, dekódold a morzekóddal leírt nevedet a segítségével!
Tetszőlegesen hosszú szöveget beolvasni: ez nem könnyű feladat. Amíg nem láttuk az egész szöveget, nem tudjuk, mennyi memóriát kell foglalni. Amíg nem foglaltuk le a memóriaterületet a sztring számára, addig viszont nem olvashatjuk a szöveget, mert nem tudjuk eltárolni a karaktereket.
Vagy mégis? Tudjuk, hogy a függvények lokális változóiból minden függvényhíváskor létrejön egy új példány. Így ha egy rekurzív függvénynek egy karakter a lokális változója, akkor egy rekurzív hívással létrehozunk egy újabb karaktert, meg még egyet és még egyet, amíg a szöveg végére nem értünk. Ha ez megtörtént, akkor pedig lefoglalhatjuk a memóriaterületet, mert addigra látjuk, hány karakter volt.
A feladatod egy ilyen függvényt írni:
/* A függvény beolvas egy teljes sort (enterig) a szabványos bemenetről,
* és visszaadja egy dinamikusan foglalt sztringben. A sztring
* nullával van lezárva, az entert nem tartalmazza. A hívó felelőssége
* a free()-t meghívni a kapott pointerre. */
char* sort_beolvas(void);
A szöveg beolvasását a fenti módszerrel kell megvalósítanod. Amit használhatsz:
- tetszőleges saját segédfüggvény,
- egyetlen egy malloc hívás.
Amit pedig nem:
- statikus élettartamú változó (azaz globális és függvénybeli statikus),
- tömb átméretezés, bármilyen egyéb tároló.
Figyelem: ennek a feladatnak elméleti jelentősége van csak! A gyakorlatban az okos átméretezés (pl. a méret duplázása) célravezetőbb, hatékonyabb.
A trie fa (trie tree, radix tree, prefix tree) egy olyan adatszerkezet, amelyben sztringek tárolhatók, és azok prefix alapján könnyen kereshetők.
Jobb oldalt látható egy ilyen fa, amely a tea
, ten
, in
és inn
szavakat tartalmazza. A t
, te
és i
sztringek nincsenek a fában; csak az
előbbi szavakhoz vezető út miatt kellett létrehozni őket.
Írj programot, amely ilyen szótárat épít az STL eszközeivel. Lehessen egy egyszerű felületen hozzáadni a szavakat, és ellenőrizni, benne vannak-e a szótárban!
> add tea added tea. > add ten added ten. > find tea tea in dict. > find te te not in dict.
Írj függvényt, amely paraméterként egy egész számot tartozó fát kap, visszatérési értékében pedig azt jelzi, hogy ez rendelkezik-e keresőfa tulajdonsággal, vagy nem! Vagyis balra mindig a gyökértől kisebb, jobbra mindig a gyökértől nagyobb számok vannak, mindenhol.
Vizsgáld meg az algoritmusod futási idejét! Hogyan függ az a fa csúcspontjainak számától!
Ha nem O(n) időben fut az algoritmusod, oldd meg azt O(n) időben is!
Megoldás
Nem elég azt vizsgálni, hogy a teljes fa gyökeréhez képest csak balra kisebbek, jobbra nagyobbak vannak-e. Az alábbi fában ez teljesül (a 2 és a 3 kisebbek a gyökérben lévő 5-nél, illetve a 6, 9 és 8 nagyobbak nála), mégsem keresőfa; a jelölt elemek elrontják azt.
5 / \ 3 9 / / \ 2 6 8
Azt sem elég vizsgálni, hogy minden csomópont gyerekei balra kisebbek, jobbra nagyobbak-e. Az alábbi fában ez minden közvetlen kapcsolatban lévő elemre teljesül, mégsem keresőfáról van szó: a 6-os elem rossz helyen van, bár a 3-nál nagyobb, de az egész fát tekintve a gyökérben lévő 5-től jobbra keresnénk.
5
/ \
3 8
/ \ \
2 6 9
A két tulajdonságnak együtt kell teljesülnie.
struct BinFa {
int szam;
BinFa *bal, *jobb;
};
/* maximumot keres a fában. a teljes fát vizsgálja, nem feltételez keresőfa
* tulajdonságot. nem hívható üres fára. */
int max_fa(BinFa *gy) {
int max = gy->szam;
if (gy->bal != NULL) {
int bal_max = max_fa(gy->bal);
max = bal_max > max ? bal_max : max;
}
if (gy->jobb != NULL) {
int jobb_max = max_fa(gy->jobb);
max = jobb_max > max ? jobb_max : max;
}
return max;
}
int min_fa(BinFa *gy) {
return /* ... mint fent ... */;
}
bool keresofa_e(BinFa *gy) {
if (gy == NULL) /* üres fa = keresőfa, mert nem hibás */
return true;
/* ha balra bárhol túl nagy elem van, vagy jobbra bárhol túl kicsi */
if (max_fa(gy->bal) > gy->szam)
return false;
if (min_fa(gy->jobb) < gy->szam)
return false;
/* ha balra vagy jobbra nem keresőfa van */
if (!keresofa_e(gy->bal) || !keresofa_e(gy->jobb))
return false;
/* egyébként az */
return true;
}
Ez nem hatékony; minden részfát annyiszor bejár (sőt duplán annyiszor), amilyen magas részfában van.
Hatékonyabb megoldáshoz érdemes egy bejárásból kiindulni. Tudjuk, hogy a keresőfát inorder bejárva
növekvő számsort kapunk. Tehát nem kell mást tenni, mint elvégezni a bejárást, és ha bárhol olyan
számot találunk, amelyik egyenlő az előbb látottal vagy kisebb annál, akkor a fát hibásnak jelölhetjük.
Figyelni kell arra, hogy az első elemet még nincs mivel összehasonlítani a keresésben. Erre jó lehet
egy std::optional
objektum. Érdemes megállítani a keresést, ha hibát találunk a fában;
ez pedig úgy oldható meg legyegyszerűbben, ha kivételt dobunk a rekurzió mélyéről.
#include <iostream>
#include <optional>
struct BinFa {
int szam;
BinFa *bal, *jobb;
};
class NemKeresofa : public std::exception {};
void keresofa_hibat_keres(BinFa *gy, std::optional<int> &elozo) {
if (gy == NULL)
return;
keresofa_hibat_keres(gy->bal, elozo);
if (elozo && *elozo >= gy->szam)
throw NemKeresofa();
elozo = gy->szam;
keresofa_hibat_keres(gy->jobb, elozo);
}
bool keresofa_e(BinFa *gy) {
try {
std::optional<int> max;
keresofa_hibat_keres(gy, max);
} catch (NemKeresofa nk) {
return false;
}
return true;
}
int main() {
BinFa *fa =
new BinFa{5,
new BinFa{3,
new BinFa{2, nullptr, nullptr},
new BinFa{4, nullptr, nullptr}},
new BinFa{7, nullptr, nullptr}};
std::cout << keresofa_e(fa);
}
Másik megoldási lehetőség: figyelembe vehetjük azt is, hogy a hierarchia milyen intervallumba szorítja be a részfák csomópontjaiban található értékeket. Ehhez vegyük példának az alábbi fát:
5 / \ 3 9 / \ x 10
Ebben a fában az x-szel jelölt helyen (akár egy csomópont van ott, akár egy nagyobb részfa) csak 5-nél nagyobb, és 9-nél kisebb szám lehet. 5-nél nagyobb azért, mert a gyökérben lévő 5-től jobbra van (ha kisebb lenne, akkor az 5-től balra kellene legyen). 9-nél kisebb pedig azért, mert egy attól balra lévő részfáról van szó.
A megoldás így:
bool keresofa_e_belso(BinFa *gy, int *min, int *max) {
if (gy == nullptr)
return true;
if ((min != nullptr && gy->szam <= *min)
|| (max != nullptr && gy->szam >= *max))
return false;
return keresofa_e_belso(gy->bal, min, &gy->szam)
&& keresofa_e_belso(gy->jobb, &gy->szam, max);
}
bool keresofa_e(BinFa *gy) {
return keresofa_e_belso(gy, nullptr, nullptr);
}
A függvény a fa gyökere mellett átvesz egy minimumot és egy maximumot is – ezek között kellene legyen az adott részfa összes eleme. Ha ez nem teljesül az adott csomópontnál, hamissal tér vissza, amúgy pedig vizsgálja a részfákat. Amikor balra halad, akkor a gyökér határozza meg a maximumot (annál nagyobb elem a gyökértől balra nem lehet), amikor jobbra, akkor pedig a gyökér a minimumot adja (annál jobbra kisebb elem nem lehet). A keresést úgy kell elindítani, hogy mindkét pointer nullptr értékű; a gyökér esetében még nincs sem alsó, sem felső határ.
A sakk tisztjei az alábbi módokon tudnak mozogni:
- A bástya azonos soron vagy oszlopon léphet.
- A futó azonos átlón léphet.
- A vezér bástyaként és futóként is tud lépni.
- Végül pedig a huszár az egyetlen, amely el tud menekülni a vezér elől, mert L alakban (2-t erre, 1-et arra) lép.




Olyan programot kell írnod, amelyben feladat megmondani egy kiválasztott mezőn álló figuráról, hogy támadja-e valamelyik másik figurát a táblán. A színekkel (világos, sötét) most nem kell foglalkoznod.
Procedurálisan
Készíts ehhez procedurális modellt! Ebben egy mezőn álló figura típusát egy felsorolt típusú értékkel enum
-mal
tárolhatod, amelynek része kell legyen az üres mező is. A tábla ilyen enum
-ok tömbje. Írd meg azt a függvényt,
amely egy pozíciót kap, és igaz/hamis értékkel tér vissza attól függően, hogy van-e ott figura ÉS támad-e az egy másik figurát!
Objektumorientáltan
Az objektumorientált modellben a tábla olyan objektumokból áll, amelyek mind egy figura ősosztály leszármazottjai. A tábla így egy heterogén kollekció, amely 8×8 pointerből áll; az üres mező lehet null pointer is. A lépést pedig valamilyen virtuális függvény adja meg. Írd meg így is a programot!
Végül hasonlítsd össze a két programot olyan szempontból, hogy mennyire különülnek el a programban az egyes önálló tevékenységek függvényei!
Megoldás
#include <iostream>
class Table;
class Piece {
public:
/* igazzal tér vissza, ha a megadott mezőre tud mozogni a figura */
virtual bool can_move(int x, int y) const = 0;
/* igazzal tér vissza, ha bármit épp támad */
bool attacks_something() const;
/* táblán elhelyezés */
void set_pos(Table *table, int x, int y) {
table_ = table;
x_ = x;
y_ = y;
}
protected:
int x_, y_;
Table *table_;
};
class Table {
public:
Table() {
for (int y = 0; y < 8; ++y)
for (int x = 0; x < 8; ++x)
table_[y][x] = nullptr;
}
void set(int x, int y, Piece *p) {
table_[y][x] = p;
p->set_pos(this, x, y);
}
Piece *get(int x, int y) const {
return table_[y][x];
}
private:
Piece *table_[8][8];
};
bool Piece::attacks_something() const {
/* mit támad? */
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 8; ++x) {
/* saját magát nem */
if (x == x_ && y == y_)
continue;
/* ha van ott valami, és én léphetek oda, akkor támadom. */
/* az algoritmus független a típustól! */
if (table_->get(x, y) && this->can_move(x, y))
return true;
}
}
return false;
}
class Rook : public Piece {
public:
virtual bool can_move(int x, int y) const {
return x == x_ || y == y_;
}
};
class Bishop : public Piece {
public:
virtual bool can_move(int x, int y) const {
return abs(x - x_) == abs(y - y_);
}
};
class Queen : public Piece {
public:
virtual bool can_move(int x, int y) const {
return x == x_ || y == y_ || abs(x - x_) == abs(y - y_);
}
};
class Knight : public Piece {
public:
virtual bool can_move(int x, int y) const {
int dx = abs(x - x_);
int dy = abs(y - y_);
return (dx == 2 && dy == 1) || (dx == 1 && dy == 2);
}
};
int main() {
Table t;
Rook r1;
Rook r2;
t.set(5, 5, &r1);
t.set(6, 6, &r2);
std::cout << r1.attacks_something();
}
Első rész
Adott a következő szótárprogram: szotar.c. Ez C-ben íródott; egy max. 200 szópárból álló angol–magyar szótárat képes tárolni, fájlba írni és onnan visszaolvasni. A szavak maximum 50 betűsek lehetnek.
Írd át ezt a programot C++-ba! Használd az STL eszközeit a szótár tárolásához, szüntesd meg a max. 50 betű, illetve max. 200 szópár korlátozásokat! OOP-sítsd a szótárat, szünetsd meg a C stílusú hibakezelést, és végül írd át C++-osra a fájlkezelést is!
Második rész
Végezd el a következő módosításokat!
- Hozz létre indexelő tömböket, amelyek segítségével magyar és angol nyelven egyszerre is rendezett lehet a szótár.
Pl. ha egy indexelő tömb tartalma
{3, 4, 2, 0, 1}
, az azt jelentheti, hogy a szótár elemeit ilyen sorrendben olvasva magyar ábécérendben kapjuk a szavakat. A másik indexelő tömb pl.{2, 4, 3, 1, 0}
lehet, az meg az angol sorrendre. - Az indexelő tömböt fájlba ne mentsd, ugyanakkor figyelj arra, hogy betöltéskor újra kell azt generálni.
- Új szó felvételekor szintén az indexelő tömböket kezelni kell.
- Lehessen listázni kétféleképpen: magyar nyelvű sorrend szerint és angol nyelvű sorrend szerint is!
- A keresést írd át úgy, hogy bináris keresést alkalmazz benne. Ügyelj arra, hogy a funkcionalitás megmaradjon: magyar és angol kifejezés alapján is kell tudni keresni.
Próbáld minél inkább elkerülni a kódduplikációt!
Ha végül már csak nyelv szerinti esetszétválasztásaid maradnak a programban (if nyelv == magyar ... else ...
jellegű
kódrészletek), akkor azokat is próbáld megszüntetni! Ez nehezebb feladat, komolyabb átalakítást igényelhet.
Megoldás
A probléma oda vezethető vissza, hogy egy osztály két adattagja közül kell választani mindig. Hol eszerint keresünk, hol eszerint rendezünk stb. Valahogy így:
if (nyelv == Magyar)
return tomb[i].magyar;
else
return tomb[i].angol;
Több megoldás is kínálkozik:
- Át kell alakítani az adatszerkezetet. A nyelvek nem osztály adattagjaiként jelennek meg, hanem egymástól
független tömbökként, valahogy így:
A párhuzamos tömbök nem szépek, de az indexelő tömbök eleve párhuzamosan kell fussanak (pl. megegyező méretben) a szavak tömbjeivel, így ez elfogadható ebben a feladatban. Esetleg egy nyelv szavaihoz és indexeihez osztályt lehet létrehozni:class Szotar { std::vector<std::string> magyar_szavak; std::vector<int> magyar_indexek; std::vector<std::string> angol_szavak; std::vector<int> angol_indexek; };
A lényeg így is az lesz, hogy egy összetartozó szótömb–indextömb páros már adatként kezelhető; pl. a kereső függvény referenciaként kaphatja, hogy melyik szótömbben, melyik indextömbbel kell keresni.class Szotar { struct Nyelv { std::vector<std::string> szavak; std::vector<int> indexek; }; Nyelv magyar, angol; };
- Lehet C++ adattag mutatókat (member pointer) is használni:
Így is elérhető, hogy ez az információ paraméterként adható át a függvényeknek.struct Szo { std::string magyar, angol; }; std::string Szo::* p; p = &Szo::magyar; std::cout << szavak[i].*p; // a magyar szó p = &Szo::angol; std::cout << szavak[i].*p; // az angol szó
- Lehet függvényeket is írni is, amelyek egy szópár angol vagy magyar szavát adják vissza. Ezek kompatibilis függvények lesznek (szópár → sztring fejléccel), és átadhatók paraméterként. Így a keresés és a rendezés is generikus lehet.