kezdokonyv.az.algoritmusokrol.2006.ebook digit
TRANSCRIPT
Simon Harris - James Ross
Kezdőkönyv az
algoritmusokról
Simon Harris - James Ross
Kezdőkenyv az
algoritmusokról
200 6
Kezdőkönyv az algoritmusokról
Eeginning algorithms, Simon Harris-James Ross
Copyright © 2005 by Wiley Publishing, Inc., Indianapolis, Indiana All rights reserved. This translation published by license. Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/ or i ts affiliates, in the United States and other countries, and may not be used without written permission, SQL Server is a trademark of Microsoft Corporation in the United States and/ or other countries. All other trademarks are the property of their respective owners, Wiley Publishing, Inc., is not associated with any product or vendor mentioned in this book. The Wrox Brand trade dress is a trademark of Wiley Publishing, Inc. in the United States and/ or other countries. Used by permission.
Minden jog fenntartva. A fordítás a Wiley Publishing, Inc. engedélyével jelent meg. Védjegyek: Wiley, a Wiley embléma, Wrox, a Wrox embléma, a Programmer to Programmer, és a hozzá kapcsolódó arculat a John Wiley & Sons, Inc. és/vagy partnerei védjegye vagy bejegyzett védjegye az Amerikai Egyesült Államokban és más országokban, és nem használható fel írásbeli engedély nélkül. SQL Server a Microsoft Corporation védjegye az Egyesült Államokban és más országokban. Minden további védjegy a megfelelő védjegybirtokos tulajdona. A könyvben említett cégekkel és termékekkel sem a Wiley Publishing, Inc., sem pedig a SZAK Kiadó nem áll függőségi viszonyban. A Wrox Brand arculat a Wiley Publishing, Inc. védjegye az Amerikai Egyesült Államokban és más országokban. Felhasználva a Wiley Publishing, Inc. engedélyével.
Magyar fordítás (Hungarian translation) © SZAK Kiadó 2006. Fordította a SZAK Kiadó fordítócsoportja: Baksáné Varga Erika, Barát Éva, Csapó Ádám, Csomay Dávid, Egenhoffer Norbert, Gyimesi Csaba, Herczeg Géza, Lucza Mónika, Tiber Melinda Terminológiai előkészítés: Kis Balázs Lektor: dr. Csink László
ISBN 963 9131 89 X
A könyv fordítása a Kilgray Kft. MemoQ (http://www.memoqtrn.com) programjával készült, a szöveg helyességét és az elválasztásokat pedig a MorphoLogic Helyesek nevű programjával ellenőriztük
Minden jog fenntartva. Jelen könyvet, illetve annak részeit a kiadó engedélye nélkül tilos reprodukálni, adatrögzítő rendszerben tárolni, bármilyen formában vagy eszközzel elektronikus úton vagy más módon közölni.
SZAK Kiadó Kft. • Az 1795-ben alapított Magyar Könyvkiadók és Könyvte*sztők Egyesülésének a tagja • 2060 Bicske, Diófa u. 3. •
Tel.: 36-22-350-209 • Fax: 36-22-565-311 • www.szak.hu • e-mail: [email protected] • Kiadóvezető: Kis Ádám, e-mail: [email protected] Főszerkesztő: Kis Balázs MCSE, MCT, e-mail: [email protected]
Tartalomjegyzék
Köszönetnyi Ivánitás
Bevezetés
Kinek szól a könyv? Elvárt előismeretek A könyv témája A könyv használata A megközelités alapelvei
Törekedjünk az egyszerűségre! Ne optimalizáljunk előre! . Felhasználói interfészek Tesztelni, tesztelnil Legyünk alaposak!
Mire van szükség a könyv használatához? A könyvben használt jelölések Forráskód Hibajegyzék p2p.wrox.com
1. Az alapok
Az algoritmusok definíciója Az algoritmusok bonyolultsága A nagy O jelölés
Konstans idő: 0(1) Lineáris idő: O(N) Kvadratikus idő: O(NZ) Logaritnúkus idő: O (log N) és O (N log N) Faktoriális idő: O(N!)
Egységtesztelés Mi az egységtesztelés? Miért fontos az egységtesztelés? )Unit-bevezető Tesztelésen alapuló programozás
Összefoglalás
x i
xiii
xiii
xiii
xiv xiv
XV
xvi
xvi
XV11
xvii
xviii
xix XX
xxi
xxi
xxii
1
1 4 5 7 7 8 9 9
10 11 13 13 17 18
Tartalomjegyzék
2. Iteráció és rekurzió 19
Számítások végrehajtása 20 Tömbök feldolgozása 22
Iterátorak használata tömbalapú problémák megoldására 23
Rekurzió 42 Rekurzív könyvtárfa-nyomtatási példa 44
A rekurzív algoritmus működése 47
Összefoglalás 48 Gyakorlatok 49
3. Listák 51
A listákról 51 A listák tesztelése 55 Listák megvalósítása 68
A tömblista 69
Láncolt lista 77
Összefoglalás 87 Gyakorlatok 87
4. Várakozási sorok 89
Asorok 89 Sarműveletek 90
A sorinterfész 91
AFIFO-sor 92
A FIFO-sor megvalósítása 96
Blokkolósorok 97 Példa: telefonos ügyfélszolgálat szimulátora 102
Az alkalmazás futtatása 112
Összefoglalás 114 Gyakorlatok 114
5. Vermek 115
Vermek 115 A tesztek 118 Megvalósítás 121 Példa: az undo/redo parancs megvalósítása 124
Az undo/ red o parancs tesztelése 125
Összefoglalás 134
v i
6.
7.
8.
Alapvető rendezés
A rendezés fontossága Rendezési alapismeretek Az összehasonlítókról
Összehasonlító műveletek Az összehasonlító interfész Néhány szabványos összehasonlító A buborékrendezésről A ListSorter interfész Az AbstractListSorter tesztelése
A kiválasztásos rendezés alkalmazása A beszúrásos rendezésről A stabilitásról Az alapvető rendezési algoritmusok összehasonlítása
CallCountingListComparator ListSorterCallCountingTest Az algoritmus-összehasonlításról
Összefoglalás Gyakorlatok
Fejlettebb rendezés
A Shell-rendezési algoritmus alapjai A gyorsrendezésről Az összetett összehasonlítóról és a stabilitásról Az összefésüléses rendezési algoritmusról
Összefésülés Az összefésüléses rendezési algoritmus
A fejlettebb rendezési algoritmusok összehasonlításáról Összefoglalás Gyakorlatok
Prioritásos sorok
A prioritásos sorok áttekintése Egyszerű példa prioritásos sorra Prioritásos sorok kezelése Rendezetlen listás prioritásos sor áttekintése Rendezetlen listás prioritásos sor megvalósítása Halmon alapuló prioritásos sorok működése
Prioritásos sorok megvalósításainak összehasonlítása Összefoglalás Gyakorlatok
Tartalomjegyzék
135
135
136
137
137 138 138 143 146 146 151
156
160
161
162 163 166 167
168
169
169
175
182
186
186 187 194
198
198
199
199
200 203 206 208 210 219
222
223
vii
Tartalomjegyzék
9. Bináris keresés és beszúrás 225
A bináris keresés működése 225
A bináris keresés megközelítései 228
Listabeli kereső 228
Iteratív bináris kereső 236
A listabeli kereső teljesítményének vizsgálata 238
Bináris beszúrás működése 245
Listabeszúró 246
Teljesítmény vizsgálata 250
Összefoglalás 254
10. Bináris keresőfák 257
A bináris keresőfákról 257
Minimum 258
Maximum 259
A következő csomópont 259
A megelőző csomópont 260
Keresés 260
Beszúrás 262
Törlés 264
Inorder bejárás 266
Preorder bejárás 267
Posztorder bejárás 267
Kiegyensúlyozás 268
A bináris keresőfa tesztelése és megvalósítása 270
A bináris keresőfa teljesítményének megállapítása 295
Összefoglalás 299
Gyakorlatok 299
11. Hasitás 301
A hasítás megértése 301
Munka a hasítással 309
Lineáris vizsgálat 312
Vödrös módszer 319
A teljesítmény megállapítása 324
Összefoglalás 331'
Gyakorlatok 331 ..
12. Halmazok 333
A halmazokról 333
Halmazmegvalósítások tesztelése 337
Listahalmaz 344
viii
Tartalomjegyzék
Hasítóhalmaz 346 Fahalmaz 350 Összefoglalás 357 Gyakorlatok 358
13. Leképezések 359
A leképezésekró1 359 Leképezésmegvalósítások vizsgálata 364 Listaleképezés 373 Hasítóleképezés 377 F aleképezés 381 Összefoglalás 388 Gyakorlatok 389
14. Hármas keresőfák 391
Hármas keresőfák 391 Szó keresése 392 Szó beszúrása 396 Prefix keresés 398 Mintaillesztés 399
A hármas keresőfák gyakorlati alkalmazása 403 Keresztrejtvény megoldását segítő példa 417 Összefoglalás 422 Gyakorlat 422
15. B-fák 423
A B-fákról 423 B-fák a gyakorlatban 429 Összefoglalás 443 Gyakorlatok 443
16. Sztri ngkeresés 445
Általános sztringkereső interfész 445 Általános tesztcsomag 447 Letámadásos algoritmus 451 A Boyer-Moore-algoritmus 454
A tesztek létrehozása 456 Az algoritmus megvalósítása 457
Sztringillesztő iterátor 461 A teljesítmény összehasonlítása 462
A teljesítmény mérése 463 Az összehasonlítás eredménye 467
Összefoglalás 468
i x
Tartalomjegyzék
17. Sztri ngillesztés
A Soundex algoritmus A Levenshtein-szótávolság Összefoglalás
18. Számítógépes geometria
Rövid geometriai ismédés Koordináták és pontok Egyenes szakaszok Háromszögek Két egyenes szakasz metszéspontjának meghatározása Meredekség Az y tengely metszése
A metszéspont meghatározása A legközelebbi pontpár meghatározása Összefoglalás Gyakorlatok
19. Pragmatikus optimalizálás
Az optimalizálás szerepe A profilírozásról A FileSortingHelper példaprogram
Profilirozás a hprof modullal Profilírozás a J ava Memory Profiler programmal
Az optimalizálásról Optimalizálás a gyakorlatban Összefoglalás
"A" függelék: Ajánlott irodalom
"B" függelék: Internetes források
"C" függelék: Bibliográfia
"D" függelék: A gyakorlatok megoldásai
Tárgymutató
A szerzőkről
x
471
471
483
494
495
495
495
497
497
498
499
500
501
517
529
529
531
531
533
534
538
541
543
544
552
553
555
557
559
609
621
Köszönetnyi lvánitás
Simon Harris
Először is hatalmas köszönet illeti J on Eavest, aki biztosította számunkra ezt a lehetőséget, és Jamest, akinek tudása és professzionalizmusa mind a mai napig lenyűgöz. A könyvet egyikük segítsége nélkül sem tudtam volna befejezni.
Köszönettel tartozom azoknak is, akik elolvasták és véleményezték a kéziratot: Andrew Harris, Andy Trigg, Peter Barry, Michael Melia és Darrell Deboer (egészen biztos, hogy valakit kihagytam). Remélem, hogy a végeredmény méltónak bizonyul az erőfeszítéseikhez.
Szeretnék köszönetet mondani testvéremnek, Timnek, aki elviselte folyamatos locsogásomat, Kerri Rusnaknak és családjának, akik elláttak teával és rágcsálnivalóval, és nem utolsósorban aikido tanitványaimnak, akik távollétemben is szargalmasan edzettek.
És végül szeretnék őszinte köszönetet mondani mindazoknak a Wiley kiadóná!, akik a munka során végig segítségemre voltak, valarnint barátaimnak és családomnak, akik akkor is mögöttem álltak és bátoritottak, arnikor már azt hittem, hogy az egész világ összedől. Igen fontos tapasztalat volt.
James Ross
Először szeretnék köszönetet mondani Simonnak, arniért megengedte, hogy első könyvének társszerzője legyek. Remek alkalom volt arra, hogy életemben először komolyan írjak, ráadásul Simonnal dolgozni örömteli és tanulságos. Gyakran hallani olyan történeteket, amelyekben a szerzők barátságát tönkretette a közös munka, örülök, hogy nekünk sikerült ezt a csapdát kikerülni.
Szeretném megköszönni a Wiley összes munkatársának, hogy ilyen megértőek voltak két újonc szerzővel, és tévedhetetlenül terelgettek bennünket a cél felé - külön köszönet Ami Sullivannek és Carol Longnak Segítségüket nagyra becsüljük.
Köszönöm a szuperguruk segítségét is a ThoughtWorksnél, akik az elmúlt néhány évben szakmai életemet csodálatossá varázsolták, különösen Andy Triggét, aki azóta igen nagy programozó cimborám, arnióta megírtuk az első közös egységtesztjeinket, és aki lankadatlan figyelemmel és gondossággal olvasta át a fejezeteimet, valamint Jon Eavesét, a könyv szakmai szerkesztőjéét, aki mindig megnevettet, és új dolgokra tanít. Simon Stewart az első kéziratok véleményezésével járult hozzá a könyvhöz, és Gregor Hohpe, valamint Martin Fowler biztosította az energiát és az inspirációt a hosszú éjszakákon át húzódó, lázas gépeléshez.
Köszönetnyilvánítás
Ha már a hosszú éjszakákról esett szó, oszmtén meg kell vallanom, hogy a
könyv Oegalábbis az én részem) nem készülhetett volna el az életemben fontos sze
repet betöltő hölgyek szeretete és megértése nélkül: ők Catherine, a mi külön nap
rendszerünk középpontj a, Jessica, Ruby és a kis Ella, aki hat hónapos volt, amikor a
könyv írásába belekezdtem, és a munka során minden egyes éjjel legalább 12 órát
aludt. Lehet, hogy soha nem olvasod el ezt a könyvet, kicsim, de ha én a kezembe
veszem, mindig te jutsz eszembe!
xii
Bevezetés
A Kezdókiitryv az algoritmusokróllépésenkénti bevezetőt nyújt a szárrútástechnikai algo
ritmusok életszerű használatának világába.
A fejleszták mindennapi munkájuk során algoritmusokkal és adatstruktúrákkal
dolgoznak. Az algoritmusok alapos ismerete és annak felismerése, hogy mikor kell
alkalmazni őket, nélkülözhetetlen a szoftverek készítése során, hogy azok nemcsak
helyesen, hanem megfelelő teljesítménnyel is működjenek.
A könyv célja, hogy a napról napra haladó szaftverfejlesztés során leggyakrab
ban előforduló algoritmusokat és adatstruktúrákat bemutassa, ugyanakkor maradjon
gyakorlatias, pontos, lényegre törő, és igyekezzen nem eltérni az alapszintű témakö
röktől és példáktóL
Kinek szól a könyv?
A könyv azoknak szál, akik alkalmazásokat fejlesztenek, vagy éppen fejlesztésbe
fognak, és szeretnék megérteni az algoritmusokat és az adatstruktúrákat. A célkö
zönség a programozók, fejlesztők, szoftvermérnök-hallgatók, információrendszer
hallgatók és informatikushallgatók népes tábora.
A könyv szerzői feltételezik, hogy a számítógépes programozás általános ismere
tei a birtokukban vannak, és remélik, hogy a kötet a kód kihagyásával - még ha nagy
részt fogalmi szinten is - olvasható és követhető az első oldaltól az utolsóig. Ebből
kifolyólag csoportvezetők, építészek és üzleti elemzők is haszonnal forgathatják.
Elvárt előismeretek
Mivel a példaprogramok mindegyike a Java programozási nyelv felhasználásával ké
szült, használható Java-tudásra, valamint a szabványos Java-könyvtárak- különösen
a j ava. l an g csomag - ismeretére szükség lehet. A tömb ökkel, ciklusokkal és egyéb
programozási technikákkal sem árt tisztában lenni, és természetesen a J ava-osztályok
létrehozásának és fordításának mikéntje is lényeges.
Az itt említett előismereteken kívül más követelmény nem szükséges a kötet
adatstruktúrákra vagy algoritmusokra vonatkozó ismeretanyagának elsajátításához.
Bevezetés
A könyv témája
A kötetben részletes magyarázatokat, néhány megvalósítást, a mindennapi használat
ra vonatkozó példákat és gyakorlatokat találunk, amelyek mindegyikének célja, hogy
olyan tudás birtokába jussunk, amellyel új ismereteinket az életben is kamataztami
tudjuk. A könyvben található példák ritkán elméleti természetűek. Az egyes fejezetek
kódjait különös gonddal válogattuk össze, és azokat a legtöbb esetben akár azonnal
is használhatjuk életszerű alkalmazásokban.
Próbáltunk ragaszkodni a legáltalánosabban elfogadott szaftverfejlesztési gya
korlatokhoz. Ezek közé tartozik a tervezési minták [GoF - Gang of Four, Design
Patterns], a kódolási konvenciók, a minóség-ellenórzések és a teljesen automatizált
egységtesztek használata. Remélhetőleg az algoritmusok és az algoritmusok problé
mamegoldásban betöltött rendkívül fontos szerepének megértésén kívül megtanul
juk, hogy a robusztus, bővíthető és természetesen működó szoftverek építése tiszte
letet érdemlő tevékenység.
A Java-nyelvben járatos olvasók felfedezhetnek némi átfedést a könyvben is
mertetett osztályok és a j ava. uti l csomag osztályai között. A könyv nem foglalko
zik a Java-könyvtárakban található specifikus megvalósításokkal. Ehelyett inkább
bepillantást enged abba, miért tartották fontosnak a Java-nyelv tervezői bizonyos al
goritmusok és adatstruktúrák megvalósításainak beépítését csakúgy, mint azok mű
ködését és használatát is.
A kötet nem a számítógépes programozás alapjait tanítja meg, sem általában,
sem a Java-programozás tekintetében. Nem ismerteti a szabványos Java-könyvtárak
használatának szabályait sem: nem ez célja. Noha a példaprogramok használják a
j ava. l an g osztályait és néhány esetben a j ava. i o csomagokat, az összes többi J ava
csomag túlmutat a könyv témáján. Ehelyett az összes szükséges osztályt kézzel épít
jük meg, ezáltal tapasztalha�uk az algoritmusok felfedezésének örömét.
Noha az egységtesztelés minden fejezetben kiemeit figyelmet kap, a kötet nem
egységtesztdési kézikönyv vagy útmutató. Inkább az egységtesztek kódolásának be
mutatásával próbálja meg elsajátíttatni az alapszintű egységtesztelés alapismereteit.
A könyv használata
A könyvet az elejétól a végéig érdemes elolvasni. Rendezési, keresési és egyéb meg
határozott algoritmusok segítségével a kötet az algoritmusok, adatstruktúrák és telje
sítménykarakterisztikák alapjain vezeti végig az olvasót. A könyv négy fő részból áll.
xiv
A megközelítés alapelvei
• Az első öt fejezet az algoritmusok alapjait, például az iterációt, a rekurziót
ismerteti, mielőtt bevezetné az olvasót az alapvető adatstruktúrák, a listák, a
vermek és a sorok világába.
• A 6-10. fejezet különböző rendezési algoritmusokkal foglalkozik, valamint
olyan nélkülözhetetlen témákkal, mint a kulcsok és a sorrend kérdése.
• A 7-15. fejezet a tárolás és keresés hatékony módszereivel foglalkozik hasí
tótáblák, fák, halmazok és leképezések segítségéveL
• A 16-19. fejezet speciális és bonyolultabb témaköröket érint, emellett részle
tezi az általános teljesítménybeli buktatókat és az optimalizálási módszereket.
Minden fejezetben újabb, az előző fejezetek témaköreire épülő fogalmakkal találko
zunk, amelyek megalapozzák a következő fejezetek ismeretanyagát. Tehát a könyvet
bármelyik fejezetnél felüthetjük, és néhány fejezet átlapozásával megfelelő képet
kaphatunk a témáról. Mindenesetre tanácsos minden fejezetben elvégezni a példabeli
megvalósításokat, példaprogramokat és gyakorlatokat, hogy a tárgyalt fogalmak és
elvek teljesen letisztulhassanak. A könyv végén lévő függelékekben megtaláljuk a to
vábbi ajánlott olvasmányok listáját, a felhasznált weboldalak listáját és a bibliográfiát.
A megközelítés alapelvei
A kód megértésének többnyire az a legnehezebb része, hogy átlássuk a döntéshoza
tali folyamatot befolyásoló, gyakran íratlan feltételezéseket és elveket. Ezért tartjuk
fontosnak, hogy részleteiben megvilágítsuk a könyvben alkalmazott megközelítést.
Betekintést engedünk a logikai alapokba, amelyeket alapvető fejlesztési gyakorlatnak
tekintettünk a könyv megírása során. A könyv elolvasása után remélhetőleg az olva
só is méltányolja majd, hogy miért hiszünk a következő elvekben.
• Az egyszerűség jobb kódot eredményez.
• Ne optimalizáljunk idejekorán!
• Az interfészek hozzájárulnak a tervezés rugalmasságához.
• A kódot automatikus egység- és funkcionális tesztelésnek kell alávetni.
• Az assertion technika a fejlesztő legjobb barátja.
XV
Bevezetés
Törekedjünk az egyszerűségre!
Milyen gyakran halljuk ezt a megjegyzést: "Ó, ez túl bonyolult! Úgysem értené meg." Vagy:
"A kódunk túl nehezen tesztelhető." A szoftvermérnökség lényege a
bonyolultság kezelése. Ha sikerült a célnak megfelelő rendszert építenünk, de a rendszer ismertetése vagy
tesztelése túl bonyolult, akkor a rendszer csak véletlenül működik. Gondolha�uk azt, hogy a megoldást szándékosan valósítottuk meg az adott módon, de a tény, hogy a rendszer működése inkább a valószínűségtől és nem a tiszta determinizmustól függ.
Ha túl összetettnek tűnik, bontsuk le a feladatot kisebb, könnyebben kezelhető részekre. Kezdjük a kisebb problémák megoldásával. Majd a közös kód, a közös megoldások alapján kezdjük el átszervezni és absztrahálni a problémákat. liy módon a nagy rendszerek kisebb feladatok összetett elrendezésévé alakulnak.
Az "EHMM - egyszeruen, hogy rnindenki megértse" jelszóhoz ragaszkodva a könyv összes példája a lehető legegyszerubb. Mivel a könyv célja, hogy gyakorlati segédletet biztosítson az algoritmusokhoz, a példaprogramokat az életszerű alkalmazásokhoz a lehető legközelebb igazítottuk Bizonyos esetekben azonban a metódusokat kissé hosszabbra kellett hagynunk, rnint szerettük volna, de végül is oktató célzattal készült könyvről van szó, és nem a lehető legtömörebb kód megírásáról.
Ne optimalizáljunk előre!
Csábító lehet rögtön a kezdetektől fogva a kód gyorsaságára törekedni. Az optimalizálás és a teljesítmény érdekessége, hogy a szűk keresztmetszetek sohasem ott vannak, ahol várnánk, és nem is olyan természetűek, rnint amilyeneket várnánk. Az ilyen kényes pontok előzetes találgatása költséges gyakorlat. Sokkal jobban járunk, ha jól megtervezzük a kódot, és külön kezeljük a teljesítményjavítás feladatát, amihez a 19.
fejezetben ismertetett speciális ismeretekre lesz szükség. Ha a könyvben komprornisszumot kellett kötni a teljesítmény és az érthetőség
között, igyekeztünk az érhetáségre törekedni. Sokkal fontosabb, hogy megértsük a kód elvét és célját, rninthogy milliszekundumokat lefaragjunk a futásidőbőL
A jó tervet sokkal könnyebb profilirozni és optimalizálni, rnint az "
okos" kódolással előállitott spagettikódot, és tapasztalataink szerint az egyszerű terv eredményeként készített kód kis optimalizálás mellett is remekül teljesít.
xvi
A megközelítés alapelvei
Felhasználói interfészek
A:z adatstruktúrák és algoritmusok nagy része ugyanazt a külsó működést muta�a, még
akkor is, ha a mögöttes megvalósítás eléggé eltérő. A:z életszerű alkalmazásokban a kü
lönbözó megvalósítások között gyakran feldolgozási vagy memóriamegszorítások mi
att kell választanunk. A:z esetek zömében ezek a megszorítások előre nem ismertek.
Az interfészek lehetóvé teszik, hogy mögöttes megvalásításra való tekintet nél
kül meghatározzuk a megállapodást. Ebből kifolyólag a tervezést a megvalósítás be
köthetóségének támogatásával teszik rugalmassá. Ezért van szükség arra, hogy minél
inkább az interfészeknek megfelelóen kódoljunk, és így lehetóvé tegyük a különbözó
megvalósítások helyettesítését.
A könyv minden példabeli megvalósítása a meghatározott· működés interfész
műveletekre való fordításával kezdődik. A legtöbb esetben ezek a műveletek a kö
vetkező két csoport valamelyikébe sorolhatóak: alapszintű vagy elhagyható.
Az alapszintű műveletek biztosítják az adott interfészhez szükséges alapműkö
dést. A megvalósítások általában az első elvekból származnak, és ezért szarosan ösz
szefüggnek egymással.
Az elhagyható műveleteket ezzel szemben az alapszintű műveletekre alapozva
valósíthatjuk meg, és rendszerint a fejlesztő kényeimét szalgálják Szükség szerint
magunk is könnyűszerrel megvalósítha�uk őket saját alkalmazáskódunkban. Mivel a
gyakorlatban sokan használják őket, ezeket a műveleteket az alapszintű API részé
nek tekinthetjük, és egy adott témakör tárgyalását addig nem fejezzük be, amíg
mindegyiket részleteiben meg nem valósítottuk
Tesztelni, tesztelnil
A korszeru fejlesztési gyakorlat megköveteli, hogy a szaftver szigorúan egyesített és
funkcionálisan tesztelt legyen a kód integritásának biztosítása érdekében. A megkö
zelítést követve az interfész definiálása után, de még bármilyen konkrét megvalósítás
definiálása előtt funkcionális követelményeinket tesztesetre fordítjuk annak ellenőr
zésére, hogy minden feltétellel foglalkoztunk, és megerősítettünk őket.
A tesztek a J Unit segítségével készültek, amely a J ava tényleges szabványos tesz
tdési keretrendszere, és a tesztek a megvalósítás minden funkcionális szempontját
ellenőrzik.
A tesztek a meghatározott interfészek alapján, nem pedig bármilyen konkrét meg
valósítás alapján készültek. Ez lehetóvé teszi, hogy az összes megvalósítás esetén
ugyanazokat a teszteket alkalmazzuk, és így biztosítsuk az egységes minóséget. Ezen
kívül a különbözó teljesítménykarakterisztikákat is bemuta�ák, ami akkor fontos, ha az
alkalmazásban a használni kívánt különbözó megvalósítások között válogatunk.
xvii
Bevezetés
A tesztelés puristái kifogásolják, hogy a tesztek az ó ízlésük szerint túl hosszúak,
és egy metódusban túl sok dolgot tesztelnek Hajlamosak lennénk egyetérteni velük,
de a megértés támogatása érdekében leegyszerűsí�ük a dolgokat, és alkalmanként
úgy gondoltuk, hogy vehe�ük magunknak a bátorságot, és néhány helyzetet össze
vonhatunk egyetlen tesztmetódusban.
A lényeg, hogy mielótt bármilyen megvalósírási kódot elkészítenénk, először írjuk
meg a teszteket. Ez a megközelítés, a tejifelésen alapuló programozás (test-driven development, IDD) az osztályok megállapodására, azaz a közzétett viselkedésre összpontosít, nem a
megvalósításra. Lehetóvé teszi, hogy a teszteseteket majdhogynem a kód követelmé
nyeiként vagy használatának eseteiként kezeljük; a tapasztalatok szerint ez is egyszerű
síti az osztályok terveit. Mint a példákban látni fogjuk, azáltal, hogy az interfészekhez
kódoljuk tesz� einket, gyerekjáték lesz a tesztelésen alapuló programozás.
Legyünk alaposak!
A tesztelés szigorúsága miatt önelégültté válhatunk, és azt hihe�ük, hogy a kódunkat tel
jes alapossággal teszteltük, ezért az hibamentes. A baj csak az, hogy a tesztek nem feltét
lenül bizonyí�ák, hogy a szoftver azt a feladatot haj�a végre, amit kell. Ehelyett csupán
azt igazolják, hogy a szoftver az adott helyzetekben és feltételekkel működik, de ezek
nem mindig fedik le a valóságot. Lehet, hogy a világ legnagyszerűbb, legátfogóbb teszt
csomagjával rendelkezünk, de ha rossz dolgokat tesztelünk, semmit sem ér az egész.
A gyors hibázás elve alapján ajánlott a defenzív programozás: ellenőrizzük a null
mutatókat, győződjünk meg róla, hogy az objektumok a metódus elején megfelelő ál
lapotban vannak, és így tovább. A gyakorlat bebizonyította, hogy ezzel a programo
zási móddal hamarabb megtalálha�uk az összes különös programhibát, és nem kell a
Nu ll Po i n terException kivételre várnunk.
Mielőtt bármilyen objektum állapotáról vagy paraméter típusáról bármit is felté
teleznénk, a kód vizsgálatával ellenőrizzük a feltevést. Ha bármikor azt gondoljuk,
hogy valami sohasem fordulhat elő, ezért nem is kell aggódnunk miatta, végezzünk
kódszintű érvényességvizsgálatot!
Képzeljük el például, hogy az adatbázisban van egy pénzügyi mezó, amelyról
"tudjuk", hogy "soha" nem tartalmaz majd negatív értéket. Ha a vizsgálatokat ki
kapcsoljuk, valamikor, valahogyan egy negatív érték egészen biztosan megjelenik a
mezőben. Lehet, hogy napok, hónapok vagy évek telnek el, mire észrevesszük ennek
következményeit. Előfordulhat, hogy a rendszer más részeiben is befolyásolja más
számítások működését. Ha az összeg -0,01 cent volt, a különbség alig észrevehető.
Mire felfedezzük a problémát, már nem tudjuk az összes káros mellékhatást megha
tározni, nem is beszélve arról, hogy ki is kellene javítani őket.
xviii
Mire van szükség a könyv használatához?
Ha engedélyeztük volna a kódszintű érvényességvizsgálatot, a szoftver teljesen
megjósolható módon hibát jelzett volna abban a pillanatban, hogy a baj előállt, és
valószínűleg a probléma diagnosztizálásához szükséges összes információ is a ren
delkezésünkre állt volna. Ehelyett jóvátehetetlenül megsérültek a rendszer adatai.
Az éles kód vizsgálata lehetővé teszi, hogy a kód hibái megjósolható módon áll
janak elő, ami lehetővé teszi a probléma okának és természetének könnyű és gyors
azonosítását, és elhanyagolható segédszámítási költségekkel jár. Egyetlen pillanatig
se gondoljuk, hogy a vizsgálatok hátrányosan befolyásolják a rendszer teljesítményét.
Jó esélyünk van arra, hogy a kód összes vizsgálatához szükséges idő nem összemér
hető egy távoli eljáráshívásban vagy adatbázis-lekérdezésben töltött idővel. Ajánlatos
az éles kódban bekapcsolt állapotban hagyni a vizsgálatokat.
Mire van szükség a könyv használatához 7
A felépítés és futtatás nem is lehetne könnyebb. Ha kezdeti előnnyel szetetnénk in
dulni, a teljesen működőképes projektet forráskóddal, tesztekkel együtt, valamint az
automatizált parancssori verziót letölthetjük a Wrox webhelyéről Oásd a "Forrás
kód" cím ű részt).
Ha a "csináld magad" megközelítés hívei vagyunk, szerencsénk van, mert így mi
nimalizálha�uk a függőségek számát. Kiindulásként a következőkre van szükségünk:
• Java Development kit QDK) 1.4 vagy újabb verziója, amely tartalmazza a
kód fordításához és futtatásához szükséges összes komponenst;
• ]Unit-könyvtár, amely egyetlen jar fájlból áll, és ha egységteszteket szetet
nénk fordítani és futtatni, a classpath környezeti változónak tartalmaznia
kell a fájlt;
• szövegszerkesztő vagy integrált fejlesztői környezet (Integrated Development
Environment- IDE) a kódoláshoz.
Az első két tétel (a JDK és a ]Unit) ingyenesen letölthető az internetről Oásd a B
függeléket). Az utolsó követelmény tekintetében nem szetetnénk vitát kirobbantani,
ezért a választást az olvasóra bízzuk. Egészen biztosan van kedvencünk, ragaszkod
junk hozzá! Ha nincsen olyan program, amellyel kódolhatnánk, kérdezzük meg bará
tainkat, hallgatótársainkat, előadóinkat vagy kollégáinkat. Egészen biztosan szívesen
megosztják velünk véleményüket.
xix
Bevezetés
Mivel a Javáról van szó, a példaprogramokat bármely operációs rendszeren le�
fordíthatjuk és futtathatjuk. A könyvet Apple Macintosh és Windows alapú számító�
gépeken írtuk és fejlesztettük Egyetlen kód sem különösebben processzorintenzív,
tehát a szaftverfejlesztéshez használt hardverünk biztosan megfelel majd.
A könyvben használt jelölések
Annak érdekében, hogy a legtöbb új ismeret birtokába juthassunk, és nyomon tud�
juk követni, mi történik, a könyvben az alábbi jelöléseket alkalmaztuk:
Gyakorlófeladat
A Gyakorlófeladat elnevezésű részben érdemes végigcsinálni a feladatot a könyv utasí�
tásait követve.
1. A Gyakorlófeladat rendszerint több kódolt lépést tartalmaz.
2. A lépések nem mindig számozottak, néhányuk nagyon rövid, míg mások a
nagyobb, végső célhoz vezető, kis lépések sorozatából állnak.
A megvalósitás müködése
Minden Gyakorlófeladat után A megvalósítás működése című részben találjuk a kódblokkok
működésének részletes magyarázatát. A könyv témája, az algoritmusok kérdése nem
igazán felel meg számozott feladatoksarok elvégzésének, sokkal inkább a gyakorlati
példáknak, tehát észre fogjuk venni, hogy a Gyakorlófeladat és A megvalósítás műkodése
megfelelően módosult. Az alapelv az, hogy alkalmazzuk a megszerzett tudást.
Az ilyen dobozokban a közvetlenül a dobozt körülvev6 szövegre vonatkozó
fontos információkat találunk, ameJ:yela6J. nem 82:abad elfeledkeznünk.
Az aktuális témára vonatkozó tippek, ö"tletek, trükko·k dőlt betűve� kissé be!Jebb húz
va szerepelnek.
A szövegben megjelenő betűtípusokkal kapcsolatban:
XX
• A fontos szavakat bevezetésük során kiemeljük.
• A billentyűleütések a következő formában jelennek meg: Ctrl+ A.
Forráskód .
• A fájlnevek, az URL-ek és a kódok a következőképpen szerepelnek a szö
vegben: persistence.properties.
• A kódokat kétféle változatban láthatjuk:
A példaprogramokban azrú"f-ésfo-ntos kódot szürkeháttérrel , emeljük ki.
A szürke háttér nem jelenik meg az aktuális témában kevésbé fontos vagy már korábban bemutatott kód mögött.
Forráskód
A könyv példáinak elvégzésekor mi magunk is begépelhetjük kézzel a kódot, vagy
használha�uk a könyvhöz.
tartozó forráskódfájlokat is. A könyvben használt példák
forráskódja letölthető a h t tp: l lwww. w r ox. com címrőL Ha már ezen a címen járunk,
keressük meg a könyvet (a Search doboz vagy az egyik címlista segítségéve!), majd
kattintsunk a könyvet részletező oldal Download Code hivatkozására, és töltsük le a
könyv összes forráskódját!
Mivel tó'bb, hasonló dmű könyv található az oldalon, keressünk az ISBN-szám segítsé
géve�· az eredeti kó'f!Yv ISBN száma: 0-7645-9674-8 (a 200 7 januá1jában bevezetésre
kerülő 4), 13jegyű ISBN-számozás szerint ez a szám 978-0-7645-9674-2 lesi).
A kód letöltése után tomöntőeszközünk segítségével csomagoljuk ki a kódot. A másik
lehetőség, ha a Wrox-kód letöltési oldalára, a h t tp: l lwww. w ro x. comldynami clbooksl
download. aspx címre lépünk, és megkeressük a könyv és más Wrox könyvek kód jait .
. Hibajegyzék
Mindent elkövettünk annak érdekében, hogy a könyv szövege és a kódok ne tartal
mazzanak hibákat. De senki sem tökéletes, és hibák előfordulhatnak. A könyvben
talált hibákkal �apcsolatos visszajelzésekért hálásak vagyunk. Hibajegyzékek bekül
désével egy másik olvasó számára megtakaríthatunk többórányi bosszankodást,
ugyanakkor segíthetünk, hogy a könyv még jobb információkat biztosítson.
A könyv hibajegyzékoldalát a h t tp: l lwww. w ro x. com címen találj uk, ha a Search
doboz vagy az egyik címlista segítségével megkeressük a könyvet. A könyvet részle
tező oldalon kattintsunk a Book Errata hivatkozásral Az oldalon megtaláljuk a
xxi
Bevezetés
könyvvel kapcsolatban már bejelentett hibákat, amelyeket a Wrox szerkesztői küld
tek el. A teljes könyvlista, amely az egyes könyvek hibajegyzékeit tartalmazza, a
www . w ro x. com/mi sc-pages/bookl i st. shtml címen található.
Ha nem találjuk "saját'' hibánkat a hibajegyzékoldalon, a www. w r ox. com/ contact/
techsupport. shtml oldalon töltsük ki az űrlapot, és küldjük el a felfedezett hiba leírá
sát. A Wrox szerkesztői ellenőrzik az információkat, és ha szükséges, a könyv hibajegy
zékében üzenet jelenik meg a hibáról, a könyv következő kiadásaiban pedig kijavi�uk.
p2p.wrox.com
A szerzőkkel és az olvasókkal a p 2 p. wrox. com címen a P2P vitafórumokhoz csatlakozva
beszélgethetünk A fórum olyan webes rendszer, amelyben a Wrox-könyvekre és azok
kal kapcsólatos technológiákra vonatkozó üzeneteket küldhetünk; és a többi olvasóval,
illetve a technológia felhasználójával folytathatunk beszélgetéseket. A fórumok előfize
tési funkciót biztosítanak, amelynek segítségével a számunkra érdekes témakörökhöz va
ló új hozzászólás érkezésekor e-mailben értesítést kapunk. A Wrox szerzői, szerkesztői,
számítástechnikai szakértői és olvasói küldenek üzeneteket ezekbe a fórumokba.
A http: l l p 2 p. w r ox. com címen több fórumot is találunk, amelyek nemcsak a
könyv olvasását, de saját alkalmazásaink fejlesztését is segítik. Ha szetetnénk csatla
kozni a fórumokhoz, kövessük az alábbi lépéseket:
1. Lépjünk a p2p. wrox. com címre, és kattintsunk a Register hivatkozásral
2. Olvassuk el a felhasználás feltételeit, majd kattintsunk az Agree gombral
3. Töltsük ki a csatlakozáshoz szükséges és az egyéb információkat, amelyeket
szetetnénk megadni, majd kattiusunk a Submit gombral
4. Ezután e-mailben kapunk értesítést arról, hogyan tudjuk ellenőrizni a fió
kunkat, és befejezni a csatlakozási folyamatot. _/
A P2P-hez való csatlakozás nélkül is olvashatjuk a fórum üzeneteit, de ha saját üze
netet szetetnénk küldeni, akkor csatlakoznunk kell.
Ha csatlakoztunk, új üzeneteket küldhetünk, illetve válaszolhatunk más felhasz
nálók üzeneteire. Az üzeneteket bármikor elolvasha�uk az interneten. Ha adott fó
rum új üzeneteit szetetnénk e-maiben megkapni, a fórumok listájában kattintsunk a
fórum neve mellett a Subscribe to this Forum ikonra.
A Wrox P2P használatáról további információkat a P2P gyakran ismétlődő kér
dések listáiban találunk, ahol a fórumszerver működésére, a P2P-re és a Wrox köny
vekre vonatkozó kérdéseinkre is választ kaphatunk. A gyakran ismétlődő kérdéseket
a GYIK-hivatkozásta kattintva bármely P2P oldalon elolvashatjuk.
xxii
ELSŐ FEJEZET
Az alapok
Az algoritmusok világába induló utazás előkészületekkel és háttér-információkkal
kezdődik. Tudnunk kell néhány dolgot, mielőtt a könyvben található algoritmusok
kal és adatstruktúrákkal megismerkedhetnénk. Bizonyára ég bennünk a vágy, hogy
mielőbb belemerüljünk, de ennek a fejezetnek az elolvasása a könyv többi részét te
szi érthetőbb� mert olyan fontos alapfogalmakat tartalmaz, amelyek elengedhetetle
nek a kódok és az algoritmusok elemzésének megértéséhez.
A fejezetben a következő témaköröket tárgyaljuk
• mi az algoritmus,
• az algoritmusok szerepe a szaftverekben és mindennapi életünkben,
• mit jelent az algoritmus bonyolultsága,
• az algoritmusbonyolultság széles osztályai, amelyek segítségével gyorsan meg
különböztethe�ük ugyanannak a problémának a különböző megoldásait,
• a "nagy O" j�lölés,
• mi az egységtesztelés, és miért fontos,
• hogyan kell a ]Unit segítségével egységteszteket írni.
Az algoritmusok definiciója
Az talán már tudjuk, hogy az algoritmusok a számitástechnika fontos részét képezik, de
egészen pontosan mik is azok? Mire használhatók? Kell egyáltalán törődnünk velük?
Az algoritmusok valójában nem korlátozódnak a számitástechnika világára;
mindennapi életünkben is alkalmazunk algoritmusokat. Egyszerű meghatározással az
algoritmus valarnilyen feladat végrehajtásához szükséges, jól meghatározott lépések
sora. Ha tortát sütünk, és a recept utasításait köve�ük, tulajdonképpen egy algorit
must használunk.
Az algoritmusok segítségével egy rendszert lehetőség szerint közbenső, átmeneti
állapotok sorozatán keresztül adott állapotból egy másikba vihetünk. Az életből vett
másik példa az egyszerű egész szorzás művelete. Noha általános iskolában mindany
nyian bemagoltuk a szorzótáblákat, a szorzási folyamatot összeadások sorozatának is
Az alapok
tekinthe�ük. Például az 5 X 2 kifejezés a 2 + 2 + 2 + 2 + 2 (illetve az 5 + 5) kifeje
zés gyorsírásos változata. Vagyis bármely két egész szám, például A és B esetén el
mondhatjuk, hogy az A X B annyit jelent, hogy B-t A-szor önmagához adjuk. Ezt az
alábbi lépések soraként fejezhetjük ki:
1. Inicializáljunk egy harmadik egész számot, C-t O-ra.
2. Ha A nulla, akkor készen vagyunk, és az eredményt C tartalmazza. Ha nem
ez a helyzet, haladjunk tovább a 3. lépéshez.
3. Adjuk B értékét C-hez.
4. Csökkentsük A értékét.
5. Lépjünk a 2. lépéshez.
Vegyük észre, hogy a torta receptjével ellentétben az összeadással szorzó algoritmus
az 5. lépésben visszakanyarodik a 2. lépéshez. A legtöbb algoritmusban felfedezhe
tünk valarnilyen ciklikusságot, amelynek a segítségével számításokat vagy egyéb mű
veleteket ismétlünk Az iteráció! és a rekuqjót- a ciklusok két fő típusát - a követke
ző fejezet részletesen ismerteti.
Az algoritmusokat gyakran pszeudokódként emlegetjük, amely még nem prog
ramozó személyek számára is könnyen érthető, kitalált programozási nyelv. Az aláb
bi kód a Mul ti pl y függvényt mutatja be, amely két egész szám- A és B- szorzatát,
A X B-t adja vissza, és csak összeadást használ. A pszeudokód a két egész szám ösz
szeadással való szorzásának műveletét muta�a be:
Function Multiply(Integer A, Integer B) Integer c = O
While A is greater than O C = C + B A = A - l
End
Return c End
A szorzás az algoritmusok nagyon egyszerű példája. A legtöbb alkalmazásban ennél
jóval bonyolultabb algoritmusokkal találkozhatunk. A bonyolult algoritmusok megér
tése nehezebb, és ezért azok nagyobb valószínűséggel tartalmaznak hibákat. (A számí
tógép-tudomány nagy része tulajdonképpen azt próbálja igazolni, hogy bizonyos algo
ritmusok helyesen működnek.)
Nem minden helyzetben alkalmazhatunk algoritmusokat. Előfordulhat, hogy
adott problémát több algoritmussal is megoldhatunk Néhány megoldás egyszerű,
mások bonyolultabbak, és egyik megoldás hatékonyabb lehet a többinéL Nem min-
2
Az algoritmusok definíciója
dig a legegyszerűbb megoldás a legnyilvánvalóbb. Bár a szigorú, tudományos elemzés mindig jó kiindulópont, gyakran találjuk magunkat az elemzési paralí'{js helyzetében. Néha a jól bevált régimódi kreativitásra van szükség. Próbáljunk ki több megközelítést, és járjunk utána megérzéseinknek Vizsgáljuk meg, miért működnek bizonyos esetekben az aktuális megoldási kísérletek, más esetekben pedig nem. Nem véletlen, hogy a szárrútógép-tudomány és a szoftvermérnökség egyik alapművének címe A számítógép-programozás múvészete (írta Donald E. Knuth). A könyvben ismertetett algoritmusok nagy része determinis'{!ikus- azaz az algoritmus eredménye a bemenetek alapján pontosan meghatározható. Előfordul azonban, hogy a probléma olyan bonyolult, hogy az idő és az erőforrások tekintetében a pontos megoldás megkeresése túlságosan nagy ráforditást igényel. Ilyen esetekben a heurisztikus megközelítés lehet a hasznosabb. A tökéletes megoldás keresése helyett a heurisztikus megközelítés a probléma jól ismert tulajdonságai alapján állit elő egy közelítő megoldást. A heurisztikák segítségével kiválogatha�uk az adatokat, eltávolíthatunk vagy figyelmen kívül hagyhatunk lényegtelen értékeket, hogy az algoritmus számítási szempontból költségesebb részeinek kisebb adathalmazon kelljen múködniük.
A heurisztika egyik hétköznapi példája az utca egyik oldaláról a másikra való átkelés a világ különböző országaiban. Észak-Amerikában és Európa nagy részén a járművek az út jobboldalán haladnak. Ha életünk nagy részét eddig Észak-Amerikában töltöttük, akkor az úttesten való átkelés előtt kétségtelenül előbb balra, majd jobbra nézünk. Ha Ausztráliában balra néznénk, azt látnánk, hogy szabad az út, majd lelépnénk a járdáró� és nagy meglepetés érhetne bennünket, mert Ausztráliában az Egyesült Királysághoz, Japánhoz, illetve több más országhoz hasonlóan az út bal oldalán haladnak a járművek.
A járművek menetirányát országtól függetlenül igen könnyen megállapíthatjuk, ha egy pillantást vetünk a parkoló járművekre, és megfigyeljük, merre néznek. Ha az autók balról jobbra sorakoznak egymás után, akkor nagy valószínűséggel az úton való átkelés előtt előbb balra, majd jobbra kell figyelnünk Ha ellenben a parkoló autók jobbról balra sorakoznak, akkor először jobbra, aztán balra kell néznünk az átkelés előtt. Ez az egyszerű heurisztika az esetek túltryomó részében beválik. Sajnálatos módon azonban vannak helyzetek, amikor a heurisztika kudarcot vall: nem látunk parkoló autót, az autók összevissza parkolnak (ez Londonban elég gyakran megesik), vagy az autók az út bármelyik oldalán haladhatnak, mint Bangalore-ban.
Tehát a heurisztika használatának nagy hátulütője, hogy nem tudjuk minden helyzetben meghatározni, hogyan viselkedik - mint azt az előző példában láttuk. Ez az algoritmus bizonytalansági szin�éhez vezet, amely az alkalmazástól függően vagy elviselhető, vagy nem.
Végeredményben bármilyen problémát próbálunk megoldani, valamilyen algoritmusra kétségtelenül szükségünk lesz; minél egyszerűbb, pontosabb és érthetőbb az algoritmus, annál könnyebben meghatározhatjuk, hogy megfelelően múködik-e, és a teljesítménye is elfogadható-e.
3
Az alapok
Az algoritmusok bonyolultsága
Miután megalkottuk, hogyan tudjuk meghatározni egy új, korszakalkotó algoritmus hatékonyságát? Nyilvánvaló elvárás, hogy szetetnénk kódunkat a lehető leghatékonyabbnak tudni, tehát be kell bizonyítanunk, hogy a hozzá fűzött reményeknek megfelelőerr működik. De pontosan mit értünk hatékonyság alatt? Processzoridőt, memóriafelhasználást, lemez bemenet-kimenetet? És hogyan mérhetjük az algoritmusok hatékonyságát?
Az algoritmusok hatékonyságának vizsgálata során a leggyakrabban elkövetett hibák egyike, hogy a te!Jesítmétryt (a processzoridő/memória/lemezterület-foglalás mennyiségét) összekeverik a bof!Jolu/tsággal (az algoritmus mérhetőségéveD. A tény, hogy az algoritmus 30 milliszekundum alatt 1 OOO rekordot dolgoz fel, nem az algoritmus hatékonyságának fokmérője. Noha igaz, hogy végeredményben az erőforrásfogyasztás is fontos, az olyan tényezőket, mint a processzoridő a kódon kivül a mögöttes hardver - amelyen a kód fut - hatékonysága és teljesítménye, valamint a gépi kód generálásához használt fordító is erősen befolyásolja. Sokkal fontosabb annak megállapítása, hogyan viselkedik az adott algoritmus a probléma méretének növekedéséveL Ha például a feldolgozni kivánt rekordok száma megkétszereződik, annak milyen hatása van a feldolgozási időre? Eredeti példánkhoz visszatérve, ha egy algoritmus 1000 rekordot 30 milliszekundum alatt dolgoz fel, rníg egy másik algoritmus 40 milliszekundum alatt, akkor az első algoritmust tekinthe�ük "jobbnak". Ha azonban az első algoritmus 300 milliszekundum alatt 10 OOO rekordot (tízszer annyit) dolgoz fel, de a második algoritmus 80 milliszekundum alatt ugyanennyit, akkor választásunkat felül kell vizsgálni.
Általánosságban elmondha�uk, hogy a bonyolultság az adott funkció végrehajtásához szükséges meghatározott erőforrás-roennyiség fokmérője. Lehetséges - és gyakran hasznos - a bonyolultságot lemez bemenet-kimenet, memóriafelhasználás tekintetében mérni, de a könyvben a bonyolultság processzoridőre gyakorolt hatását vizsgáljuk. A bonyolultság fogalmát tovább finomí�uk az adott funkció végrehajtásához szükséges számítások vagy műveletek számának mértékére.
Érdekes módon a múveletek pontos számát rendszerint nem szükséges mérnünk. Sokkal fontosabb az, hogyan változik a végrehajtott múveletek száma a probléma méretével. Mint az előző példában: ha a probléma mérete egy nagyságrenddel nő, ez hogyan befolyásolja az egyszerű funkció végrehajtásához szükséges múveletek számát? Ugyanannyi múveletre lesz szükség? Vagy kétszer annyira? A szám a probléma méretével lineárisan nő? Vagy exponenciálisan? Ezt kell az algoritmusbonyolultság alatt értenünk. Az algoritmus bonyolultságának mérésével a teljesítményét próbáljuk megjósolni: a bonyolultság kihat a teljesítményre, de ez fordítva nem igaz.
4
A nagy O jelölés
A könyvben az algoritmusok és adatstruktúrák bemutatása során a bonyolultsá
gukat is elemezni fogjuk. Az elemzések megértéséhez nem lesz szükség matematikai
doktorátusra. Az egyszerű elméleti bonyolultságelemzést minden esetben könnyen
követhető empirikus eredmények követik tesztesetek formájában, amelyeket rni ma
gunk is kipróbálhatunk, és kísérletezhetünk a bemenet módosításával, hogy kitapasz
talhassuk a szóban forgó algoritmus hatékonyságát. A legtöbb esetben az ádagos
bonyolultság adott - a kód elvárt tipikus esetbeli futási sebessége. Számos esetben a
legrosszabb esetbeli és a legjobb esethez tartozó idő is adott. Az, hogy a legjobb, a
legrosszabb és az ádagos esetek közül melyik a meghatározó, részben az algoritmus
tól függ, de a legtöbbször attól az adattípustól, amelyet az algoritmus használ.. Min
denesetre fontos megjegyeznünk, hogy a bonyolultság nem az elvárt teljesítmény
pontos mértékét biztosítja, hanem az elérhető teljesítményt szorítja bizonyos hatá
rok vagy korlátok közé.
A nagy O jelölés
Mint már korábban említettük, a műveletek pontos száma valójában nem fontos. Az
algoritmus bonyolultságát a funkció végrehajtásához szükséges műveletek számának
nagJságrencfjével definiálhatjuk a nagy o jelöléssei - order of (nagyságrend) - innen a
nagy O. Az O mögötti kifejezés a probléma méretét jelölő N-hez képest a relatív nö
vekedést jelenti. Az alábbi lista néhány gyakran alkalmazott nagyságrendet mutat be,
a későbbiekben mindegyikre részletesen visszatérünk.
• 0(1): az "ordó l" konstans futási idejű függvényt jelent
• O(N): az "ordó N" lineáris futási idejű függvényt jelent
• O(N2): az "ordó N négyzet"
kvadratikus futási idejű függvényt jelent
• o(log N): az "ordó logaritmus N" logaritmikus futási idejű függvényt jelent
• O(N log N): az "ordó N logaritmus N" a probléma méretével és a logarit
mikus idővel arányos futási idejű függvényt jelent
• O(N! ): az "ordó N faktoriális"
faktoriilis futási idejű függvényt jelent
Természetesen a fenti lista elemein kivül is van még néhány hasznos bonyolultság,
de ezek elegendőek lesznek a könyvben bemutatott algoritmusok bonyolultságának
leírására.
5
Az alapok
Az 1.1. ábrán látjuk, hogy a különböző bonyolultság-nagyságrendek hogyan vi
szonyulnak egymáshoz. A vízszintes tengely a probléma méretét jelenti - például a
keresési algoritmussal feldolgozandó rekordok számát. A függőleges . tengely az
egyes osztályok algoritmusainak számitásigényét jelenti. Az ábra nem jelzi a futásidőt
vagy a szükséges processzorciklusokat; pusztán annyit mutat, hogy a számitógépes
erőforrásigény a megoldani kívánt probléma méretével együtt nő.
1. 1. ábra. A bof!Yolultság küliinbiizó nagyságremijeinek ilsszehasonlítás a
Az előző listában talán feltűnt, hogy a nagyságrendek egyike sem tartalmaz kons
tanst. Azaz, ha az algoritmus várt futásidejű teljesítménye az N, 2xN, 3xN vagy akár
lOOxN értékekkel arányban áll, a bonyolultság minden esetben o (N). Első pillantásra
kicsit furcsának tűnhet- természetes, hogy a 2xN jobb, mint a lOOxN - , de mint már
korábban említettük, nem az a célunk, hogy megállapítsuk a műveletek pontos szá
mát, hanem hogy a különböző algoritmusok relatív hatékonyságát összehasonlítsuk
Más szóval az O(N) idő alatt befejezett algoritmus túlszárnyal egy másik, O(N2) ideig
futó algoritmust. Továbbá, ha N nagy értékeivel akad dolgunk, a kanstansok nem
sokat változtatnak a helyzeten: a teljes méret arányát tekintve az 1 OOO OOO OOO és a
20 OOO OOO OOO közötti különbség majdnem elhanyagolható, még akkor is, ha az
egyik a másiknak a hússzorosa.
Természetesen szeretnénk összehasonlítani a különböző algoritmusok tényleges
teljesítményét, különösen akkor, ha az egyik 20 perc alatt befejeződik, mig a másik
csak 3 óra alatt, és mindkét algoritmus nagyságrendje O(N). Azt kell megjegyezünk,
hogy sokkal könnyebb megfelezill egy O(N) bonyolultságú algoritmus idejét, mint
módosítani egy olyan algoritmust, amely az O(N) nagyságrendhez képest O(N2) nagy
ságrenddel bír.
6
A nagy O jelölés
Konstans idő: O( 1)
Megbocsátható az a feltételezés, hogy az 0(1) bonyolultság azt jelenti, hogy az algoritmus egyeden művelet segítségével végrehajtja a funkciót. Noha ez valóban lehetséges, az 0(1) tulajdonképpen valójában annyit jelent, hogy az algoritmus konstans ideig fut; vagyis a teljesítményt nem befolyásolja a probléma mérete. Valószínűleg nem tévedünk, ha úgy véljük, ez túl szép ahhoz, hogy igaz legyen.
Az egyszerű funkciók futása garantáltan 0(1) ideig tart. A konstans időbeli teljesítmény legegyszerűbb példája a számítógép operatív memóriáját címezi, és kiterjesztésként tömbbeli keresést hajt végre. A tömb egy elemének keresése a mérettől függedenill általában ugyanannyi ideig tart.
Bonyolultabb problémák esetén azonban nagyon nehéz konstans ideig futó algoritmust találni: a "Listák" című fejezet (3.) és a "Hasítás" című fejezet (11.) bevezeti az 0(1) időbeli bonyolultsággal rendelkező adatstruktúrákat és algoritmusokat.
A konstans időbeli bonyolultsággal kapcsolatban még azt kell megjegyeznünk, hogy ez még mindig nem garantálja az algoritmus gyorsaságát, csak azt, hogy a végrehajtásához szükséges idő mindig ugyanannyi lesz: az algoritmus, amely egy hónapig fut, még mindig 0(1) algoritmus, még akkor is, ha ez a futásidő teljességgel elfogadhatatlan.
Lineáris idő: O(N)
Az algoritmus akkor fut O(N) nagyságrenddel, ha a funkció végrehajtásához szükséges műveletek száma egyenesen arányos a feldolgozni kívánt elemek számával. Az 1.1. ábrára pillantva látha�uk, hogy az O(N) vonala felfelé folytatódik, a meredeksége változadan.
ilyen algoritmus például az áruházi pénztárnál való várakozás. A vásárlókat átlagosan ugyanannyi idő alatt lehet kiszolgálni: ha egy vásárló kosarát két perc alatt fel lehet dolgozni, körülbelül 2x10 = 20 perc kell tíz vásárló kiszolgálásához, és 2x40 = 80
perc 40 vásárlóhoz. A lényeg, hogy nem fontos, hány vásárló áll a sorban, az egyes vásárlók kiszolgálásához szükséges idő nagyjából ugyanannyi marad. Elmondha�uk, hogy a kiszolgálás ideje egyenesen arányos a vásárlók számával, tehát az idő O(N).
Érdekes módon, ha bármikor megkétszerezzük vagy akár megháromszorozzuk a műveletben a regiszterek számát, a feldolgozási idő továbbra is O(N) marad. Ne feledjük, hogy a nagy O jelölés mindig minden konstans t figyelmen kívül hagy.
Az O(N) futási idejű algoritmusok rendszerint elfogadhatóak Legalább olyan hatékonynak tekinthetők, mint az 0(1) futásidejű algoritmusok, de ahogy már említettük, igen nehéz konstans idejű algoritmust találni. Ha sikerül lineáris idővel futó algoritmust találnunk, mint azt a "Sztringkeresés" című fejezetben (16.) látni fogjuk, kis elemzéssel- és zseniális ötletekkel- még hatékonyabbá tehetjük.
7
Az alapok
Kvadratikus idő: O(N2)
Képzeljünk el egy csoportot, amelynek tagjai most találkoznak egymással először, és az illemszabályoknak megfelelően mindannyian kézfogással üdvözlik a csoport öszszes többi tagját. Ha a csoportban hatan vannak, akkor ez az 1.2. ábrán látható módon összesen 5+4+3+2+1 = 15 kézfogást jelent.
1.2. ábra. A csoport minden tagja iidvo'ifi a csoport osszes to'bbi tagját
Mi történne, ha a csoport hét főből állna? Az üdvözlés összesen 6+5+4+3+2+1 = 21 kézfogásba kerülne. És ha nyolcból? Ez 7+6+ ... +2+1 = 28 kézfogást jelentene. És, ha kilencen lennének a csoportban? Már nagyjából láthatjuk a lényeget: Ahányszor a csoport mérete egy fővel növekszik, egy további embernek kell kezet ráznia az öszszes többivel.
Az N méretű csoportban a kézfogások száma (NLN) /2 lesz. Mivel a nagy O
minden konstanst figyelmen kívül hagy - ebben az esetben a 2-t -, a kézfogások száma NLN. Mint azt az 1.1. táblázatban látjuk, ahogy N egyre nő, N kivonása NLből a végeredményre egyre elhanyagolhatóbb hatással van, tehát bátran elhagyhatjuk a kivonást, ezáltal az algoritmus bonyolultsága O(N2) lesz.
Különbség
1 1 o 100,00%
10 100 90 10,00%
100 10 OOO 9 900 1,00%
8
A nagy O jelölés
ÍN N 2 N2-N Kutönbség
1 OOO 1 OOO OOO 999 OOO 0,10%
10 OOO 100 OOO OOO 99 990 OOO 0,01%
1.1. táblázat. Az N kivonása N-ból az N növekedése me/lett
A kvadratikus idővel futó algoritmusok a programozók legvadabb rémálmaiban je
lennek meg; bármely O(N2) bonyolultságú algoritmus kizárólag a legjelentéktelenebb
problémák megoldására alkalmas. A keresést tárgyaló 6. és 7. fejezetben további ér
dekes példákat találunk.
Logaritmikus idő: O(log N) és O(N log N)
Az 1.1. ábrán látható, hogy az O(l og N) jobb, mint az O(N), de nem olyan jó, mint
az 0(1).
A logaritmikus algoritmus futásideje a probléma méretének - rendszerint 2-es
alapú - logaritmusával együtt növekszik. Ez annyit jelent, hogy ha a beviteli adat
halmaz mérete milliós szorzóval növekszik, a futásidő csak log (1000000) = 20
szorzóval növekszik. Az egész számok 2-es alapú logaritmusát egyszerűen kiszárnit
hatjuk, ha megkeressük, hogy a szám tárolásához hány bináris számjegy szükséges.
Például, a 300 2-es ala pú logari tm usa 9, mivel a decimális 300 megj elemtéséhez 9 bináris számjegy szükséges (a bináris ábrázolás 100101100).
A logaritmikus futásidők megvalósításához az algoritmusnak a beviteli adathalmaz ·
nagy részét rendszerint figyelmen kívül kell hagynia. Ennek eredményeként a legtöbb
algoritmus - amely így viselkedik - valamilyen keresést foglal magában. A "Bináris ke
resés és beszúrás" cím ű fejezet (9.), és a "Bináris keresőfák" című fejezet (10.) o(l og N)
algoritmusokat mutat be.
Ha ismét megtekin�ük az 1.1. ábrát, látha�uk, hogy az O(N log N) jobb, mint
az O(N2), de nem olyan jó, mint az O(N). A 6. és a 7. fejezetben O(N log N) algorit
musokkal találkozhatunk.
Faktoriális idő: O(N!)
Lehet, hogy nem gondolnánk, de néhány algoritmus még az O(N2)-nél is rosszabbul tel
jesít- az 1.1. ábrán hasonlítsuk össze az O(N2) és O(N!) bonyolultságokat. (Valójában
vannak ennél sokkal rosszabb algoritmusok is, de ezeket a könyvben nem tárgyaljuk.)
9
Az alapok
Elég ritkán találkozhatunk ilyen funkciókkal, különösen, ha olyan példákat kere
sünk, amelyek nem a kódolásról szólnak. Ha tehát elfelejtettük volna, hogy mi a fak
toriilis - vagy még nem is találkoztunk vele soha -, íme egy gyors ismédés:
A faktoriilis az egész szám és az azt megelőző természetes számok szorzata.
Például a 6! ("6 faktoriális'') = 6x5x4x3x2xl = 720 és a 10! = 10x9x8x7x6x5x
4x3x2xl= 3628800.
Az 1.2. táblázat az N2 és az N ! összehasonlítását tartalmazza 1 és 1 O közötti
egész számokra.
N N z Nl
1 1 1
2 4 2
3 9 6
4 16 24
5 25 120
6 36 720
7 49 5 040
8 64 40 320
9 81 362 880
10 100 3 628 800
1.2. táblázat. Az N2 és az N! összehasonlítása kis egész számok esetén
Mint lá�uk, ha az N értéke N=2 vagy annál kisebb, a faktoriális bonyolultság jobb, mint
a kvadratikus, de ezen a ponton a faktoriilis bonyolultság nekilendül, és katasztrófával
fenyeget. Következésképpen az O(N2) bonyolultsághoz képest még inkább remény
kednünk kell abban, hogy algoritmusunk bonyolultsága ne O(N!) legyen.
Egységtesztelés
Mielőtt folytatnánk utazásunkat az algoritmusok világában, kicsit elkalandozva beszél
nünk kell egy szívünknek igen kedves témáról: az egységtesztelésrőL A2 elmúlt évek
ben az egységtesztelés nagyon népszerűvé vált azoknak a fejlesztáknek a körében,
akiknek fontos az általuk készített rendszerek minősége. Közülük sokan kellemedenill
érzik magukat, ha a szoftver készítése közben nem építenek egy automatikus teszteket
10
Egységtesztelés
magában foglaló csomagot is, amely bizonyítja, hogy az elkészült szoftver az elvárásoknak megfelelően műköclik. Mi is ezt a hozzáállást képviseljük Ezért minden ismertetett algoritmus esetén bemuta�uk, hogyan működik az adott algoritmus, és egységtesztek révén azt is, hogy mit csinál. Mindenkinek ajánljuk, hogy fejlesztési munkája során váljon ez a szokásává, mivel nagyban segíti a túlórázás elkerülését.
A következő néhány részben gyors áttekintést kapunk az egységtesztelésrő� és betekintést nyerünk a ]Unit-keretrendszerbe Java-program egységteszteléséhez. A könyvben a ]Unit-rendszert alkalmazzuk, tehát érdemes megismerkednünk ezzel a programm� hogy könnyebben megértsük a könyv példáit. Ha már kemény, teszteléssei fertőzött fejlesztőnek érezzük magunkat, a könyv e részét nyugodtan átlapozha�uk. Örüljünk neki!
Mi az egységtesztelés?
Az egységteszt olyan program, amely egy másikat tesztel. Java-környezetben ez egy Java-osztályt jelent, amelynek a célja a többi osztály tesztelése. Nagyjából ennyi. Mint az életben a legtöbb dolog, ez is könnyen megtanulható, de sokat kell gyakorolni. Az egységtesztelés művészet és egyben tudomány is; rengeteg irodalmat találunk a tesztelésről, tehát itt nem merülünk el a tesztelés részleteiben. Az A függelék több könyvet is ajánl a témával kapcsolatban.
Az egységteszt alapvető működése az alábbiakat foglalja magában.
1. A teszt támogatásához szükséges objektumok, mint a példaadatok előkészítése. Ezek az úgynevezett tartozékok.
2. Az objektumok használatával futtassuk a tesztet, és győződjünk meg róla, hogy valóban az történt, amire számítottunk. Ezek a folyamatok a vizsgálatok.
3. Végül szabaduljunk meg minden felesleges dologtól. Ez a lebontás folyamata.
A könyvben az egységtesztek elnevezésénél minden esetben úgy járunk el, hogy az osztálynévvel létrehozott tesztosztály nevéhez hozzáfűzzük a Teszt szót. Ha például a Mütyür névre hallgató osztályt készülünk tesztelni, az osztály egységtesz*ként létrehozott osztály neve MütyürTeszt lesz. Erre rengeteg példát találunk majd a könyvben. A forrásfájlok elrendezése is egységes szabály szerint történik. Az egységteszteket a fő forrásfájlok csomagstruktúrájával megegyező párhuzamos forrásfán helyezzük el. Ha például a Mütyür a com·. wrox. algorithms osztályon belül létezik, a forrásfájlok elrendezése az 1.3. ábrán látható elrendezéshez lesz hasonlatos.
11
Az alapok
src l - main l l l l l l
test
- com
l - com
- wrox - algorithms
l - Mütyür
- wrox - algorithms
l - MütyürTeszt
1.3. ábra. Az egységtes:;;j forrásjeij/jai párhuiflmos csomagstruktúrába rendeződnek
Ez annyit jelent, hogy a fájlok tetején a Java-csomag utasítás pontosan ugyanaz, de
maguk a fájlok a fájlrendszer különböző könyvtáraiban helyezkednek el. A modell a
könnyebb csomagolhatóság és a fő kód terjesztésének érdekében az éles kódot elkü
löníti a tesztelés kódjától, és így biztosítja, hogy az éles kód az elkészítése folyamán
nem a tesztkócion alapul, mivel a két könyvtár fordítása közben az osztályútvonal
kissé eltérő. Tetszetős az a tény is, hogy lehetővé teszi a tesztek számára a csomagel
vű metódusok hozzáférését, ezt is érdemes megfontolni.
Az egységtesztek részletezésének befejezése előtt még meg kell jegyeznünk, hogy
a tesztelés más közös típusaival is találkozhatunk majd munkánk során. A továbbiak
ban megismerkedünk néhány definícióval, amelyek segítségével megfelelő háttér
információkhoz jutunk, és elkerülhetjük a szükségtelen félreértéseket. A könyv csak
alkalmazza az egységtesztelést, tehát a tesztelés további típusaival kapcsolatban érde
mes megtekinteni a referenciaanyagokat. A szakirodalomban az alábbi szakkifejezé
sekkel találkozhatunk:
12
• Fekete dobozos tesztelés: képzeljük el, hogy DVD-lejátszónkat teszteljük.
Csak az előlap gombjaihoz és a hátlap csatlakozóihoz férünk hozzá (haésak
nem akarunk búcsút inteni a garanciának). A DVD-lejátszó összetevőit nem
tudjuk tesztelni, mivel nem férünk hozzájuk. Kizárólag a fekete doboz külső
oldalán látható vezérlőelemekre hagyatkozhatunk. Számítástechnikai szem
pontból ez a telepített alkalmazás felhasználói felületének hozzáféréséhez ha
sonlít. Rengeteg összetevővel állunk szemben, de azokhoz nem férünk hozzá.
• Funkcionális tesztelés: a kifejezést a fekete dobozos tesztelés szinonimá
jaként használhatjuk.
• Fehér dobozos tesztelés: olyan tesztelésre utal, amely kisebb vagy na
gyobbmértékben hozzáférhet a rendszer átívelő komponensszervezéséhez,
és képes egyedi komponenseket is tesztelni, rendszerint a felhasználói felü
let igénybevétele nélkül.
Egységtesztelés
• Integrációs teszt: nagy, elosztott rendszer külön összetevőjének tesztelését
jelenti. Az ilyen tesztek célja, hogy egymástól független fejlesztésük folyamán
biztosítsák a rendszerek előzetes megállapodásoknak megfelelő működését.
Az egységtesztelés a tesztdési módszerek közill a legaprólékosabb, mivel az ilyen
tesztek során bármely más osztálytól függetlenül egyetlen osztályt tesztelünk Ez
annyit jelent, hogy az egységteszteket gyorsan futtathatjuk, és egymástól viszonylag
függetlenek.
Miért fontos az egységtesztelés?
Ha nem értjük, hogy miért olvashatunk egy alapvetően algoritmusokról szóló
könyvben ilyen sokat az egységtesztelésről, gondoljunk csak a Java-fordítóra, amely
lehetővé teszi Java programjaink futtatását. Működőképesnek vélhetünk egy megírt
kódot anélkül, hogy lefordítanánk? Valószínűleg nem! Gondoljunk úgy a fordí tóra,
mint a program egyfajta tesztelésére - biztosítja, hogy a program helyes szintaxissal
készült. Nagyjából ennyi. Nem ad visszacsatolást arról, hogy a program praktikus
vagy hasznos feladatot hajt végre: ez az a pont, ahol az egységtesztek bekerülnek a
képbe. Ha jobban érdekel bennünket az, hogy programjaink valóban valami hasznos
dolgot hajtanak-e végre, mint az, hogy helyesen írtuk-e be a Java-kódot, akkor az
egységtesztek segítségével komoly gátat emelhetünk mindenféle programhiba elé.
Az egységtesztek másik előnye, hogy megbízható dokumentációt biztosítanak a
tesztelés alatt álló osztály viselkedéséről. Ha majd látunk néhány egységtesztet mű
ködés közben, észre fogjuk venni, hogy a tesztek vizsgálatával mennyivel könnyebb
rájönni, mit csinál az osztály, mint ha magát a kódot nézegetnénk (A kódhoz akkor
kell fordulni, ha azt szeretnénk kideríteni, hogy a program hogyan csinálja azt, amit
csinál, de ez már teljesen tnás téma.)
J U nit-bevezető
Az első hely, ahová el kell látogatnunk, a JUnit-webhelye a www . j uni t. or g/ címen.
Itt nemcsak a letölthető szoftvert találjuk meg, hanem a ]Unit integrált fejlesztői
környezetünkben való használatával kapcsolatos információkat is, valamint a ]Unit
kibővítéseit és fejlesztéseit, amelyek a speciális igények kielégítésére készültek.
A szoftver letöltése után csak a junit.jar fájlt kell hozzáadnunk a classpath
környezeti változó hoz, és már készen is állunk első egységtesztünk létrehozására. Az
egységteszt létrehozásához készítsünk egy Java-osztályt, amely kibővíti a junit.
framework. Testcase alaposztályt. Az alábbi kód a ]Unit segítségével készített egy
ségteszt alapvető felépítését mutatja:
13
Az alapok
package com.wrox.algorithms.queues;
import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List; import junit.framework.Testcase;
public class RandomListQueueTest extends Testcase { private static final String VALUE...A = "A"; private static final String VALULB "B"; private static final String VALULC = "c";
private Queue _queue;
}
Ne foglalkozzunk azzal, valójában mit tesztel ez az egységteszt; erre a könyv későbbi
részében a várakozási sorok ismertetése során még visszatérünk, és ráérünk akkor
megérteni. A lényeg, hogy az egységteszt szokványos osztály, amely a ]Unit-keret
rendszer által biztosított alaposztállyai rendelkezik. A kód meghatározza az osztályt,
kibővíti az alaposztályt, majd deklarál néhány statikus tagot és egy példány tagot,
amely a tesztelni kívánt várakozási sort tárolja.
A következő lépés a setup metódus felülbírálása és az objektumok teszteléséhez
szükséges kód hozzáadása. Ebben az esetben ez annyit jelent, hogy a felülbírált
setup metódust a szuperosztályban kell meghívni, és a várakozásisor-objektumot
teszteléshez példányosítani:
FigyelJük meg a setUp metódus írásmóc!Ját! Vegyük ési[e kiizépen a nagy U betűt! A Ja
va egyik gyenge pon!} a, hogy a metódusokat puszján egybeeséssei bírálha!fuk felii4 nem ele
gendő a szándék. Ha elgépe!Jiik a metódus nevét, a kód nem az elvárásoknak megfelelően
működik mqd
protected void setup() throws Exception { super. setup() ;
_queue = new RandomListQueue();
J.
A ]Unit-keretrendszer által biztosított dolgok egyike a garancia, hogy a tesztmetódus
futtatása során (ezt is mindjárt látni fogjuk), a rendszer minden egyes tesztfuttatás
előtt meghívja a setup metódust. Hasonlóképpen minden tesztmetódus futtatása
után a tearoown metódus biztosí�a számunkra a lehetőséget, hogy a következő pél
dában bemutatott módon kitakarítsunk magunk után:
14
protecteéfVO:fcCtearoown o throws Ei<Céi:ition { super.tearoown();
_queue = null;
Egységtesztelés
Meglepődhetünk azon, hogy miért kell a példánytag mezőt null értékre állitani. Noha nem feltétlenül szükséges, az egységtesztek túlnyomó többségében, ha ezt a lépést figyelmen kívül hagyjuk, az egységtesztek a szükségesnél jóval több memóriát foglalnak; tehát érdemes hozzászokill a lépés végrehajtásához.
A tényleges egységteszt kódjának következő metódusa a várakozási sor viselkedését teszteli, ha a sor üres, és valaki megpróbál egy elemet kivenni belőle; ezt az objektum tervezője nem tette Iehetővé. Ez egy roppant érdekes eset, mivel bemutatja azt a technikát, amelynek a segítségével bebizonyíthatjuk, hogy a helytelen használat során osztályaink az elvárt módon kudarcot vallanak. Íme a kód:
l
publ i c voi d testAccessAnEmptyQUeueQ{� assertEquals(O, _queue.size()); assertTrue(_queue.isEmpty());
try { _queue.dequeue(); fail();
} catch (EmptyQueueException ll ezt várjuk
e) {
l } � - - -- 1� - -�-- • "-�� -- � - � - - - -�--�-�· --L----------
A kóddal kapcsolatban vegyük észre a következőket:
• A metódus neve test-tel kezdődik. Ez a ]Unit keretrendszer követelménye, amelynek a segítségével a tesztrnetódust meg tudja különböztetni a támogató metódustól.
• A metódus első sora az assertequals O metódust alkalmazza annak ellenőrzésére, hogy a várakozási sor hossza valóban nulla. A metódus szintaxisa assertEquals (ezt várjuk, tényleges). A metódusnak túlterhelt verziói is léteznek az összes Java-alaptípus számára, tehát a metódussal a könyvben részletesen megismerkedhetünk. Az egységtesztek világában valószínűleg ez a legáltalánosabb érvényességvizsgálat: annak ellenőrzése, hogy egy adott objektum az elvárt értékkel rendelkezik. Ha valamilyen okból kifolyólag az elvárttól eltérő értéket talál, a ]Unit-keretrendszer megszakítja a teszt végrehajtását, és hibát jelez. Ezáltal az egységteszteket tömörré és olvashatóvá tehetjük.
15
Az alapok
• A második sor egy másik nagyon általános érvényességvizsgálatot alkalmaz, az asserttrueOmetódust, amellyel ellenőrizhető, hogy a tesztfuttatás során a logikai értékek a várt állapotban vannak. Ebben az esetben arról győződhetünk meg, hogy a várakozási sor helyesen az üres állapotot jelzi.
• A try/catch blokk veszi körül a várakozásisor-objektumon a metódushívást, amely kivételt jelez, ha a várakozási sor üres. Ez a felépítés csak nagyon kevéssé tér el a Java normális kivételkezelésétől, tehát vizsgáljuk meg jó alaposan. J elen esetben az a jó, hogy kód kivételt jelez, és az a ross'\; ha nem. Ebből kifolyólag a kód a catch blokkban semmit sem csinál, hanem a try blokkban közvedenül a tesztelni kívánt metódus meghívása után meghívja a ]Unit-keretrendszer fai l O metódusát. Ha a fai l O metódus megszakítja a tesztet, és hibát jelez, azaz a metódus várt kivételt jelez, a végrehajtás keresztülhalad a metódus végén, és a teszt sikeres lesz. Ha a rendszer nem jelez kivételt, a teszt azonnal megbukik. Ha ez egy kissé zavarosnak tű
nik, olvassuk át újra a példát!
Íme egy újabb egységteszt-példametódus ugyanabban az osztályban.
public void testclear() {
_queue.enqueue(VALUE_A); _queue.enqueue(VALUE_B); _queue.enqueue(VALUE_C);
assertEquals(3, _queue.size()); assertFalse(_queue.isEmpty());
_queue.clearO;
assertEquals(O, _queue.size()); assertTrue(_queue.isEmpty());
J.
A metódus neve ismét test-tel kezdődik, hogy a ]Unit-reflexió segítségével megtalálhassa. A teszt néhány új elemet ad a várakozási sorhoz, megvizsgálja, hogy a si ze O és az i sEmpty metódusok megfelelően működnek-e, majd kiüríti a várakozási sort, és ismét ellenőrzi a metódusok működését.
Az egységteszt megírása után a következő lépés a teszt futtatása. Vegyük észre, hogy egységtesztünk nem tartalmaz ma i n metódust, tehát közvedenül nem tudjuk futtatni. A ]Unit több tesztfuttató környezetet is biztosít különböző interfészekkelegészen az egyszerű szövegalapú konzolfelülettól kezdve a gazdag grafikus felületig.
16
Egységtesztelés
A legtöbb Java-fejlesztői környezet, mint például az Belipse vagy az lntelliJ IDEA
közvetlen támogatást biztosít a ]Unit-tesztek futtatásához, de ha csak parancssor áll
rendelkezésünkre, az előző tesztet az alábbi parancs segítségével is futtatha�uk (a
classpath környezeti változónak természetesen tartalmaznia kell a j uni t. j ar fájlt):
java junit.textui.TestRunner com.wrox.algorithms.queues.Random
ListQueueTest
A grafikus változat futtatása ugyanilyen egyszerű:
java junit.swingui.TestRunner com.wrox.algorithms.queues.Random
ListQueueTest
A ]Unit rengeteg, a szaftver építésére alkalmazott eszközben-például Antben vagy
Mavenben -használható. Fejlesztői életünket nagyban megkönnyíthetjük a szaftver
minden egyes változatának jó egységtesztcsomagban való futtatásával, és a szaftver
sokkal robusztusabb is lesz, tehát további részletekért érdemes ellátogatui a ]Unit
webhelyére.
Tesztelésen alapuló programozás
A könyv összes algoritmusa és adatstruktúrája egységteszteket is tartalmaz, amelyek
biztosítják, hogy a kód az elvárásoknak megfelelően működjön. Az egységtesztek va
lójában még a tesztelni kívánt kód megírása előtt megszülettek! Ez kissé furcsának
tűnhet, de ha egységtesztekkel lesz dolgunk, tudnunk kell arról az egyre nagyobb
népszerűségnek örvendő technikáról, amelyet azok a fejleszták alkalmaznak, akiknek
fontos a megírt kód minősége. Ez a technika a tes�elésen alapuló programozás.
A tes�elésen alapuló programozás fogalma Kent Becktől, az eXtreme Programming
atyjától származik, aki több könyvet is írt az eXtreme Programming témakörében,
valarnint a tesztelésen alapuló programozásróL Az alapötlet szerint a fejlesztési pró
bálkozások felvesznek egy bizonyos ritmust, amely a tesztkód írása, az éles kód írása
és a kód a célnak való megfelelés érdekében végrehajtott tisztogatása (átszervezése)
között váltakozik. A ritmus a szaftver készítése során a folyamatos előrehaladás ér
zését kelti, miközben olyan szilárd egységtesztcsomagot építünk, amely megvéd a
kód változtatásai által okozott programhibáktóL
Ha a könyv olvasása közben úgy határozunk, hogy az egységtesztelést szeret
nénk saját kódjainkba is beépíteni, rengeteg könyv áll rendelkezésünkre ebben a té
makörben. Ajánlásaink az A függelékben találhatók.
17
Az alapok
Összefoglalás
A fejezetben a következő témakörökkel foglalkoztunk.
18
• Az algoritmusok mindennapi életünkben is jelen vannak.
• Az algoritmusok a legtöbb szárrútógéprendszer központi részét alkotják.
• Mit jelent az algoritmusbonyolultság?
• Az algoritmusokat bonyolultságuk tekintetében összehasonlíthatjuk
• A nagy O jelölést széles körben alkalmazhatjuk az algoritmusok bonyolult
ság alapján való osztályozására.
• Mi az egységtesztelés, és miért fontos?
• Hogyan lehet egységteszteket írni a ]Unit segitségével?
MÁSODIK FEJEZET
Iteráció és rekurzió
Az iteráció és a rekurzió két olyan alapvető elv, amelyek nélkül nehezen végezhet
nénk hasznos számításokat. A név szerinti rendezés, a hitelkártya-tranzakció végösz
szegének kiszámítása és a rendezett sor adatainak kinyomtatása esetén egyaránt
szükséges a kívánt eredmény eléréséhez, hogy minden rekord, minden egyes adat
pont fel legyen dolgozva.
Az iteráció egyszerűen a feldolgozási lépések ismédése. Azt, hogy mennyi ismét
lésre van szükség, számos különböző tényezővel is meghatározha�uk. Részvényport
foliónk végösszegének kiszámításához például a vagyonrészeinket iterálnánk, miköz
ben az összeget egy futó változó segítségével tárolnánk, ameddig minden egyes akti
váok fel nincs dolgozva. Ebben az esetben az ismédések számát az általunk birtokolt
vagyonrészek határoznák meg. A rekur':{jó egy másik feladatmegoldási módszer. A re
kurzió gyakran - bár nem mindig- sokkal természetesebb módon teheti lehetővé az
algoritmus kifejezését, mint az iteráció. Aki valaha is foglalkozott programozással, az
valószínűleg már tudja, mi az a rekurzió, csak lehet, hogy nincs vele tisztában.
A rekurzív algoritmus magában foglal egy metódust vagy függvényt, amely ön
magát hívja. Ezt úgy teszi meg, hogy a feladatot egyre kisebb és kisebb részekre
bon�a, amelyek nagyon hasonlóak a nagy részekhez, de azoknál "finomabb szem
cséjűek". Ez az elv bonyolult lehet ahhoz, hogy elsőre megértsük.
Rá fogunk jönni, hogy az algoritmusok természetükből adódóan tartoznak egyik
vagy másik kategóriába: legegyszerűbben iteratív vagy rekurzív módon fejezhe�ük ki
őket. Ezek után el kell mondanunk, hogy a legtöbb gyakorlati alkalmazás esetén rit
kábbak a rekurzív algoritmusok, mint az iterarivak Ebben a fejezetben feltételezzük,
hogy már tisztában vagyunk vele, miként alkossunk ciklust, metódushívást és hason
lókat, ezért inkább arra koncentrálunk, hogyan használha�uk az iterációt és a rekur
ziót problémák megoldására.
Ez a fejezet a következő területeket mutatja be:
• hogyan használjuk az iterációt számítások végrehajtására,
• hogyan dolgozzunk fel az iterációval tömböket,
• hogyan általánosítsuk az iterációt az egyszerű tömböktől az összetettebb
adatstruktúrákig,
• hogyan lehet hasonló problémákat rekurzióval megoldani.
Iteráció és rekurzió
Számítások végrehajtása
Az iterációt használha�uk számítások elvégzésére. Talán az egyik legegyszerűbb pél
da erre, amikor egy számot (az alap) egy másik (a kitevo) segítségével hatványozzuk a l apkitevö. Ez azt jelenti, hogy az alapot annyiszor szorozzuk önmagával újra és újra,
amennyit a kitevő meghatározott. Például: 32 = 3x3 = 9 és 106 = lOxlOxlOxlOx
lOxlO = l OOO OOO.
Ebben a részben egy osztályt implementálunk. Powercal c ul a tor néven, egy
ca l eu l a te nevű metódussal, amely két paramétert vár - egy alapot, amely egész
szám, és egy kitevő t-, majd a hatványozás értékét kapjuk vissza. Habár elképzelhető
negatív kitevő, e példa célja miatt feltehetjük, hogy csak nullát vagy annál nagyobb
kitevőt használhatunk.
Gyakorlófeladat: a számitás tesztelése
Az általános eset meglehetősen egyszerű, de meg kell vizsgálnunk néhány speciális
szabályt is, amelyek arra valók, hogy ellenőrizzük, vajon a végső megvalósítás úgy
működik-e, ahogy vártuk.
Kezdjük a tesztosztály létrehozásával, ami egy kicsivel több, mint a Testcase
kibővítése:
package com.wrox.algorithms.iteration;
import junit.framework.Testcase;
public class PowercalculatorTest extends Testcase {
}
Az első szabály az alap nulladik hatványával kapcsolatos. Ez minden esetben l érté
ket ad eredményül:
public void testAnythingRaisedToThePowerofzerorsone() { Powercalculator calculator = Powercalculator.INSTANCE;
}
assertEquals(l, calculator.calculate(O, 0)); assertEquals(l, calculator.calculate(l, O)); assertEquals(l, calculator.calculate(27, O)); assertEquals(l, calculator.calculate(l43, O));
A következő szabály az alap első hatványát érinti. Ebben az esetben ugyanis az
eredménynek mindig az alapot kell visszaadnia:
20
Számítások végrehajtása
putíHC'�voTiitésU.:nythi llgtiaiSe"cJToThePoWE!-roi'öílei'Sitséiföi Powercalculator calculator = Powercalculator.INSTANCE;
assertEquals(O, calculator.calculate(O, l)); assertEquals(l, calculator.calculate(l, l)); assertEquals(27, calculator.calculate(27, l)); assertEquals(l43, calculator.calculate(l43, l));
�1�- --·--- ·--------- ------ ---
Végül elértünk az általános esethez:
public void testAritrary()-{
l
Powercalculator calculator = Powercalculator.INSTANCE;
assertEquals(O, calculator.calculate(O, 2)); assertEquals(l, calculator.calculate(l, 2)); assertEquals(4, calculator.calculate(2, 2));
assertEquals(S, calculator.calculate(2, 3)); assertEquals(27, calculator.calculate(3, 3));
A megvalósitás müködése
Az első szabály biztosítja, hogy a számítás minden esetben l értéket adjon. Vegyük
észre, hogy ha nulladik hatványra emeljük, még a O is l értéket ad.
A második szabály szerinti számításokhoz változó alapérték tartozik, ezúttal
azonban az l kitevőt használjuk.
Most a számítás végeredményét leteszteljük különböző alap-kitevő kombinációkkal.
Gyakorlófeladat: a kalkulátor megvalósítása
Miután megírtuk a tesztek kódját, implementálhatjuk az aktuális kalkulátort.
pack-age com-: wroX. al go ri thms ·. herati on;
public final class Powercalculator { public static final Powercalculator INSTANCE =
private Powercalculator() { }
new Powercalculator();
public int calculate(int base, int exponent) { assert exponent >= O : "az 'expQflent' nem lehet < O";
int result = l;
21
Iteráció és rekurzió
}
for (int i = O; i < exponent; ++i) { result *= base;
}
re tu r n res u l t;
J ______ _
A megvalósitás müködése
A calculate() metódus először megvizsgálja, valóban érvényes-e a kitevő (ne fe
lejtsük el, hogy a negatív értékek nem engedélyezettek), majd az eredményt az l ér
tékre inicializálja. Ezután következik az iteráció egy for ciklus formájában. Ha a ki
tevő O lenne, akkor a ciklus véget érne szorzás nélkül, és az eredmény mindig l len
ne, mivel bármely szám nulladik hatványa egy. Ha a kitevő l lenne, a ciklus egyeden
lúvást eredményezne, megszorozva a kezdeti eredményt az alappal, majd visszajut
tatva a lúvóhoz, mivel minden szám első hatványa a szám önmaga. Ennél nagyobb
kitevők esetén a ciklus folytatódik, annyiszor szorozva az eredményt az alappal,
amennyit megadtunk
A privát konsh"uktort annak érdekében használhatjuk, hogy megakadá!Jowk az osifáfy
példá'!Jainak keletkezését az osifáfyon kivül. Ehefyett egyetlen példá'!Jt érhetiink el az
INSTANCE konstans segítségéve!. Ezjó példa a Singleton teroezési mintára [Gamma, 1995].
Tömbök feldolgozása
Az iterációt számítások végrehajtása mellett tömbök feldolgozására is használjuk.
Képzeljük el, hogy rendelések egy csoportjára árengedményt akarunk adni. A követ
kező kódrészlet egy megrendelési tömbön iterál végig, mindegyikre speciális áren
gedményt alkalmazva:
Order[] orders =
for (int i = O; i < orders.length; ++i) { orders[i].applyoiscount(percentage);
}
Először a ciklusváltozót inicializáljuk az első elem pozíciójával (i nt O), majd
növeljük (++i) addig, amíg el nem éri az utolsó elemet (i < orders .length - 1),
közben alkalmazva a százalékarányt. V együk észre, hogy mindegyik iteráció összeve
ti a ciklusváltozó értékét a tömb hosszával.
22
Tömbök feldolgozása
Vannak esetek, amikor egy tömböt fordítva szetetnénk feldolgozni. Például fordított sorrendben akarunk kinyomtatni egy névlistát. A következő kódrészlet egy ügyféltömbön visszafelé halad, és mindegyik ügyfél nevét kiírja:
customers = . • • ;
for (int i = customers.length- l; i >=O; --i) { System.out.println(customers[i].getName());
,_} ........ ��- -���-"--.
Ekkor a ciklusváltozót az utolsó elem pozíciójára inicializáljuk (i nt i = customers.
length - 1), és egységnyivel csökken�ük (--i), amíg el nem éri az elsőt (i >= O), közben minden ügyfélnevet kiírunk
Iterátorak használata tömbalapú problémák megoldására
Habár a tömbalapú iteráció jól használható egészen egyszerű adatstruktúrák esetén, általánosított algoritmusok alkotása, amelyek a tömb elemeinek egyszerű feldolgozásánál sokkal többre képesek, meglehetősen bonyolult. Például tegyük fel, hogy csupán minden második elemet szetetnénk feldolgozni; bizonyos kiválasztási feltételeknek megfelelő értékeket akarunk beemelni vagy kizárni; vagy, mint ahogy már láttuk, fordított sorrendben szetetnénk végrehajtani az elemeket. A tömbökben való rögzítettség is nehezíti az alkalmazások írását, amelyek adatbázison, vagy fájlokon dolgoznak, anélkül hogy előtte átmásolnák az adatokat a tömbbe.
Az egyszerű tömbalapú iteráció használata nem csupán az algoritmusunkat rögzíti a tömbhasználathoz, hanem szükségessé teszi, hogy az elemkiválasztás és feldolgozás sorrendjét meghatározó logika már előre ismert legyen. Sőt, ha az iterációt a kódunkban nem csak egy helyen szetetnénk felhasználni, akkor valószínűleg duplikálnunk kell a logikát. Ez egyértelműen nem egy jól bővíthető megközelítés. Nekünk arra van szükségünk, hogy különválasszuk az adatokat kiválasztó logikát a ténylegesen feldolgozó kódtóL
Egy iterátor (más néven enumerátor) úgy oldja meg ezeket a problémákat, hogy az adathalmazon létrehozott ciklusnak általános felületet biztosít, így a mögöttes adatstruktúra vagy tárolási mechanizmus - mint például egy tömb, adatbázis és így tovább -rejtve marad. Amíg egy egyszerű iteráció általában szükségessé teszi speciális kódrészlet írását, amel)' kezeli, hogy az adat mely forrásból származik, vagy milyen típusú rendezés, illetve előfeldolgozás szükséges, az iterátor lehetővé teszi egyszerűbb, általánosabb algoritmusok írását.
23
Iteráció és rekurzió
lterátorm ü ve letek
Egy iterátor bizonyos műveleteket tesz lehetővé adatok bejárására és hozzáférésére.
A 2.1. táblázatban felsorolt műveleteket megvizsgálva felfedezhe�ük, hogy az előre
haladó és forditott irányú bejárásra egyaránt létezik metódus.
Ne jelqtsük e4 hogy az iterátor egy elv, és nem egy megvalósítás. A Java iinmagában
tartalmaz egy rterator interf'ésif a Java Collections Frame1vork részeként. Az álta
lunk itt meghatározott iterátor azonban szemmel láthatóan és tudatosan küMnbö.:?fk a
szabvátryos Java-változattó4 ehe!Jett sokkal jobban igazodik a Design Patterns [Gam
ma, 199 5] által táwalt iterátorho=?;
Művelet Leirás
previous
isoone
current
A megelőző elemhez pozíciónáL Ha nincs implementálva, akkor
unsuppo rtedope ra ti onExcepti on hibaüzenetet dob.
Meghatározza, hogy az iterátor hivatkozik-e elemre. A visszatérése
true, ha elértük az utolsó elemet, ellenkező esetben fa l se, jelezve,
hogy még több elem feldolgozására van szükség.
Az aktuális elem értékét adja vissza. rteratoroutofBoundsException
hibaüzenetet dob, ha nincs aktuális elem.
2. 1. táblázat. Iterátormúveletek
A legtöbb metódus dobhat unsupportedOperati onExcepti on hibaüzenetet. Nem
minden adatstruktúra teszi lehetővé az adatok mindkét irányba történő bejárását, és
ennek nem is mindig van értelme. Ezen okból kifolyólag bármely bejárási metódus -
first(), l ast(), next(), és previous()- esetén elképzelhető, hogy unsupported
Operati onExcepti on hibaüzenetet dob, jelezve, hogy az egy hiányzó vagy nem meg
valósított funkció.
A current() meghívását definiálatlannak kell tekintenünk, mielőtt a first()
vagy l ast() metódust meg nem hívtuk. Bizonyos iterátorimplementációk az első
elemre vannak pozicionálva, míg léteznek olyanok, amelyek igénylik a first() vagy
a last() metódus előzetes meghívását. Mindenesetre, aki ebben bízik, az a véletlenre
hagyatkozva programo'(, ezt pedig el kell kerülni. Ehelyett, ha iterátorakat használunk,
akkor győződjünk meg róla, hogy a fejezet későbbi, "Iterátor idiómák" című részben
leírt idiómák egyikét köve�ük.
24
Tömbök feldolgozása
Az lterátor interfész
Az elóbb bemutatott műveletekból meg tudjuk alkotill a következő Java interfészt:
package com.wrox.algorithms.iteration;
public interface Iterater { public void first();
public void last();
public boolean isoone();
public void next();
public void previous();
public object current() throws IteratoroutOfBoundsException; }
Ahogy láttuk, igencsak szóról szára fordítottuk a műveleteket a Java-felületre, műve
letenként egy metódust.
Meg kell határoznunk a kivételt is, amelyet akkor dobhatunk, amikor aktuális
elemet próbálunk meg elérni, de már nincs feldolgozandó elem:
package com.wrox.algorithms:iteration;
public class IteratoroutOfBoundsException extends RuntimeException {
1�--------------�--�----� Mivel a határokon kívül eső iterátor elérését programozási hibának tekintjük, körül
lehet írni kóddal. Ebből adódóan jó elképzelés az IteratoroutOfBoundsExcepti on
létrehozása, amely a Run timeException-t terjeszti ki, úgynevezett ellenőrizetlen kivételt alkotva. Ez biztosítja, hogy a kliens kódnak nem kell kezelnie a kivételeket. Valójá
ban, ha ragaszkodunk a később tárgyalandó idiómához, akkor szinte sohasem fo
gunk találkozni az IteratoroutOfBoundsException hibával.
Az lterable interfész
Az Iterater interEészen kívül létre fogunk hozni egy másik interfészt is, amely álta
lános módot nyújt iterátorak elérésére, az azt támogató adatstruktúrákból:
package com.�roX7ilgorithms.iteration;
public interface Iterable { public Iterater iterator();
l
25
Iteráció és rekurzió
Az It e rab l e interfész egy metódust határoz meg- i te ra to r O -, amely a mögöttes
adatstruktúrára vonatkozó iterátort ad meg. Bár ebben a fejezetben nem használtuk,
az Iter ab l e interfész olyan kód írását teszi lehetővé, amelynek csak az adatstruktúra
tartalmán szükséges iterálni, elfedve ezáltal a konkrét megvalósítást.
lterátoridiómák
Ahogy az egyszerű tömbalapú iteráció esetén, itt is két alapvető módszerről beszél
hetünk, amikor iterátorral dolgozunk: whi l e vagy fo r ciklusróL Az eljárás mindkét
esetben hasonló: először az iterátor beáll a megfelelő kezdő vagy vég pozícióra -
vagy explicit a létrehozásakor, vagy egy metódus meghívásával. Aztán ameddig ma
rad, minden egyes elemet feldolgoz, mielőtt a következőte (vagy az előzőre) lépne.
A whi l e ciklus használata az előzőeknek nagyjából szó szerinti kóddá fordítását
teszi lehetővé:
Iterator iterator iterator.first();
. . . '
while (!iterator.isoone()) {
object object = iterator.current();
iterator.next();
}
Ez a módszer különösen jól használható, amikor az iterátor metódushívás esetén
paraméterként kerül átadásra. Ebben az esetben a metódusnak nem szükséges meg
hívnia a fi r st O vagy a l ast O eljárást, amennyiben az iterátort már a megfelelő
kezdési ponthoz pozicionáltuk.
A for ciklus használata azonban valószínűleg sokkal ismerősebb, mivel megkö
zelíti azt a módot, amellyel normális esetben egy tömb bejárásánál dolgoznánk:
Iterator iterator = . . . ;
for (iterator.first(); !iterator.isoone(); iterator.next()) {
Object object = iterator.current();
}
Vegyük észre, mennyire hasonlít ez a tömbiterációhoz: az inicializálásból fi rstO
hívás lesz; a leállási feltételnek az i sooneOellenőrzése felel meg; a léptetést pedig a
next() meghívás valósítja meg.
26
Tömbök feldolgozása
Mindkét icliómát bátran használhatjuk, és mindkettőt nagyjából ugyanolyan gyakorisággal alkalmazzák a legtöbb életszerű kódban. Akármelyik módot választjuk is,
vagy akár mindkettőt, soha ne felejtsük el meglúvni a fi r st() vagy a l ast() eljá
rást, rnielőtt bármely más metódust meglúvunk. Ellenkező esetben az eredmény
esetleg megbízhatatlan, az· iterátor megvalósításától függő lesz.
Szabványositerátorok
Az adatstruktúrák által nyújtott iterátorokon (erről a könyv későbbi részében lesz szó), illetve az általunk létrehozott iterátorokon felül számos szabványos megvalósí
tás nyújt gyakran használt funkcionalitásokat. Arnikor más iterátotokkal kombinál
juk, ezek a szabványos iterátotok lehetővé teszik igen bonyolult adatfeldolgozó algoritmusok létrehozását.
Tömbiterátor
A legkézenfekvőbb megvalósítás tömböt használ. A tömbiterátorba való beágyazá
sával elkezdhetünk alkalmazásokat írni, amelyek most a tömbön működnek, és a jö
vőben bármikor könnyen kibővíthetők más adatstruktúrákra.
Gyakorlófeladat: a tömbiterátor tesztelése
Tömbiterátorunk teszteléséhez a JUnit-tesztesetnél használatos struktúrát használ
juk, ahogy alább látható:
pickagecöiíi ':" w r ox. a l go ri thtii"s�: iterati on;
iaport junit.framework.Testcase;
public class ArrayiteratorTest extends Testcase {
} -- �
Az iterátorhasználat egyik előnye, hogy nem szükséges az elejétől bejárni a tömböt, és a végéig sem kell elmenni. Olykor a tömbünknek csak egy részére van szükségünk. Ezért az első tesztet annak biztosítására végezzük, hogy meg tudjuk-e alkotni a tömh
itetátott az elfogadható határokon belül- ebben az esetben egy kezdő pozíció és egy
elemszám megadásával. Ez lehetővé teszi, hogy ugyanazzal a konstruktorral a tömb egy részére vagy egészére iterátort hozzunk létre.
pu6lic .. voicriesüteratiormesJ>ect"ssounC:IsO -{ object[] array = new Object[] {"A", "B", "c", "o", "E", "F"}; Arrayiterator iterator = new Arrayrterator(array, l, 3);
iterator.first(); assertFalse(iterator.isDone()); assertSame(arrf!y[l],. iterator.currentQL; .----1
27
Iteráció és rekurzió
}
iterator.next(); assertFalse(iterator.isoone()); assertSame(array[2], iterator.current());
iterator.next(); assertFalse(iterator.isoone()); assertSame(array[3], iterator. current());
iterator.next(); assertTrue(iterator.isoone()); try {
iterator.current(); fai l O;
} catch (IteratorOutofsoundsException e) {
ll ezt várjuk
}
A következő tesztelendő a tömbön való visszafelé iterálás - az utolsó elemnél kezd
jük, és az első irányában haladunk:
28
public void testBackwardsiteration() {
}
object[] array = new object[] {"A", "B", "c"}; Arrayiterator iterator = new Arrayiterator(array);
iterator.last(); assertFalse(iterator.isoone()); assertsame(array[2], iterator.current());
iterator.previous(); assertFalse(iterator.isoone()); assertsame(array[l], iterator.current());
iterator.previous(); assertFalse(iterator.isoone()); assertsame(array[O], iterator.current());
iterator.previous(); assertTrue(iterator.isoone()); try {
iterator.current(); fa il();
} catch (IteratoroutOfBoundsException e) {
ll ezt várjuk
}
Tömbök feldolgozása
A megvalósitás működése
Az első tesztben az iterátor létrehozásával kezdjük, egy hat elemet tartalmazó tömbhöz. Vegyük észre azonban, hogy egy l értékű kezdő pozíciót (a második elem) és egy 3 értékű elemszámot is megadtunk Ennek alapján azt várjuk, hogy az iterátor csak a B, A c és a D értékeket adja vissza. Ennek tesztelésére az iterátort az első helyre pozicionáljuk, és megbizonyosodunk arról, hogy a várt értékről van szó-ebben az esetben ez B. Ezután meghívjuk a next eljárást minden egyes megmaradó elemhez: először a c-hez, aztán újra a D-hez, ami után az iterátor már várhatóan végez, annak ellenére, hogy a mögöttes tömbben még több elem van. A teszt uto�só része azt igazolja, hogy ha current() hívást végzünk, arnikor már nincs több elemünk, akkor az IteratoroutOfBoundsExcepti on üzenet váltódik ki.
Az utolsó tesztben, mint az előzőben is, egy tömbhöz hozunk létre iterátort. Ezúttal azonban engedélyezzük az iterátornak, hogy a tömb rninden elemét bejárja, tehát nem csak egy részét, mint ezelőtt. Ezután az utolsó elemre pozicionálunk, és visszafelé lépdelünk, meghíva a previous() eljárást, rníg az első elemhez nem érünk. Amint az iterátor jelzi, hogy készen van, ismét ellenőrizzük, hogy a current() kivételt dob-e, mint ahogyan vártuk.
Ez az! Még néhány szituációban letesztelhetjük, de nagyrészt meggyőződtünk tömbiterátorunk megfelelő viselkedéséről. Most pedig itt az ideje, hogy a gyakorlatban is kipróbáljuk, amit a következő feladatban teszünk meg.
Gyakorlófeladat: a tömbiterátor megvalósítása
A helyes tesztek birtokában most megvalósíthatjuk magát a tömbiterátort. Az iterátornak a mögöttes tömbre való hivatkozásan kívül még az Iterator interfész megvalósítására lesz szüksége.
Ha feltételezzük, hogy az iterátor mindig a tömb egészén működik, az elejétől a végéig, akkor ezenkívül az egyedüli információ, amit el kell tárolni, az aktuális pozíció. Ám gyakran csak a tömb egy részének a hozzáférését szetetnénk lehetővé tenni. Ehhez az iterátornak a tömbhatárokat - a legfelső és legalsó pozíciót - is tárolnia kell, amelyek fontosak az iterátor használójának
package-c�wrox:a 1 gorTtllm.s':"iierati on;
public class Arrayiterator implements Iterator { private final object[] _array; private final int _start; private final int _end; private int _current = -1;
public Arrayiterator(object[] array, int start, int length) { != null : "a tömb nem lehet NULL"; >=-0�--:����-�dó�ék n� l ehet .. negatí v"_;
29
Iteráció és rekurzió
}
}
assert start < array.length : "a kezdőpozíció nem letlet >
array.length (a tömb hossza)"; assert length >=O : "a length (hossz) nem lehet negatív";
_array = array; _first = start; _last = start + length - l;
assert _last < array.lengt:h "start: + length (a kezdőérték és a hossz) nem lehet: nagyobb, mint az array.lengt:h (a tömb méret:e)";
Bár most a tömb egy részén történt az iterálás, természetesen lesznek olyan esetek,
amikor az egészen szeretnénk. Kényelmi okokból érdemes lehet egy konstruktorról
is gondoskodni, amelynek egyetlen paramétere a tömb, és amely kiszámítja nekünk a
kezdő és a végső pozíciót:
public Arrayiterator(Object[] array) { assert array != null : "az array (tömb) nem lehet: NULL";
_array = array; _first = O; _last array.length - l;
Most, hogy megvan a tömbünk, és kiszámítottuk a felső és alsó határokat, már nem
is lehetne könnyebb a first() és a l ast() megvalósítása:
public void first() { _current = _first:;
}
public void last() { _current = _last;
}
Az előre és visszafelé haladó bejárás teljesen úgy történik, mint a közvetlen tömb
hozzáférés:
30
public void next() { ++_current;
}
public void previous() { --_current;
Tömbök feldolgozása
Használjuk az i sDone() metódust annak megállapítására, van-e még feldolgozandó elem. Ebben az esetben ezt úgy tehetjük meg, hogy meghatározzuk, vajon az aktuális pozíció a konstruktor által kiszámított határokon belülre esik-e:
publicbooleani soone()-{
return _current < _first l l _current> _last; l
Ha a current (aktuális) pozíció a first (első) előtt vagy a l ast (utolsó) után van, akkor nincs több elem, és az iteráció befejeződött.
Végül mégvalósítjuk a current() metódust, hogy azzal visszakaphassuk az aktuális elemet a tömbből:
publ i c ooject cu- rr.ent"()-throws tt"l!ratorOiitofifouni:lsException{ if (isoone()) {
throw new IteratoroutOfBoundsException(); } return _array[_current];
}
A megvalósitás müködése
Ahogy azt az előző példa első kódblokkjában láthattuk, a mögöttes tömb referenciáján kívül találhati.mk." még változókat, amelyek meghatározzák az aktuális, az első és az utolsó elempozíciót (O, l, 2 ... ). Továbbá van néhány ellenőrzés arról, hogy a paraméterek értékének van-e értelme. Az például érvénytelen lenne, ha a meghívó egy 10 méretű tömböt adna meg, kezdési pozícióként pedig 20-at.
Tovább haladva már ismerjük az első és utolsó elem pozícióját, így már csak megfelelően be kell állítani az aktuális pozíciót. Ha előre akarunk haladni, növeljük az aktuális pozíciót; ha hátra, akkor csökkentjük
Vegyük észre közben, hogy először az i sDone () hívás segítségével meggyőzőclünk róla, hogy valóban létezik érték, amelyet vissza fogunk adni. Ezután, feltételezve, hogy van ilyen érték, az aktuális pozíciót indexként h�sználva ugyanúgy járunk el, mintha közvedenül férnénk hozzá a tömbhöz.
A fordított iterátor
Olykor meg szeretnénk fordítani az iterátor irányát anélkül, hogy az értéket feldolgozó kódot megváltoztatnánk Képzeljük el nevek egy tömbjét, amely ábécérendbe van sorolva, és ez valahogy megjelenik a felhasználónak is. Ha a felhasználó úgy dönt, hogy a neveket visszafelé akarja megjeleníteni, akkor újra kell rendeznünk a tömböt, vagy legalábbis olyan kódot kell alkotnunk, amely a tömböt a végétől viszszafelé járja végig. Egy fordított iterátorral ugyanezt a magatartást elérhetjük újrarendezés és a kód megkettőzése nélkül is. Amikor az alkalmazás a first() eljárást hívja meg, a fordított iteráto r valójában a l ast() eljárást a mögöttes iterátorral.
31
Iteráció és rekurzió
Amikor pedig az alkalmazás a next() eljárást lúvja meg, a mogottes iterátor
previous() metódusa lúvódik meg, és így tovább. liy módon az iterátor magatartá
sát meg lehet fordítani, anélkül hogy megváltoztatnánk a kliens kódját, amely az
eredményeket megjeleníti, és anélkül hogy újra rendeznénk a tömböt, ami igazán
költséges is tud lenni, ahogy azt a könyv későbbi részeiben is látha�uk, amikor már
magunk írunk rendező algoritmusoka t.
Gyakorlófeladat: a fordított iterátor tesztelése
A fordított iterátor tesztjei egyszerűek. Két fő szituációt kell tesztelnünk: az előreha
ladó iteráció válik visszafelé haladóvá, és fordítva. Mindkét esetben használhatjuk
ugyanazokat a tesztadatokat, és csupán mindig a megfelelő irányba iterálunk Mivel
éppen az előbb teszteltünk és valósítottunk meg egy tömbiterátort, ezt használjuk a
fordított iterátor tesztelésére:
package com.wrox.algorithms.iteration;
import junit.framework.Testcase;
public class ReverseiteratorTest extends Testcase { private static final object[] ARRAY =
new object[] {"A", "B", "c"};
.}.
A tesztosztály meghatároz egy tömböt, amelyet rnindegyik esetben használhatunk.
Most teszteljük le, hogy a fordított iterátor a megfelelő sorrendben adja-e vissza a
tömb elemeit:
32
public void testForwardsiterationBecomesBackwards() { Reverseiterator iterater =
new Reverseiterator(new Arrayiterator(ARRAY));
iterator.first(); assertFalse(iterator.isoone()); assertsame(ARRAY[2], iterator.current());
iterator.next(); assertFalse(iterator.isoone()); assertsame(ARRAY[l], iterator.current());
iterator.next(); assertFalse(iterator.isoone()); assertsame(ARRAY.[Ol üerator. current());,
Tömbök feldolgozása
}
iterator. next(); assertTrue(iterator.isoone()); try {
iterator.current(); fail();
} catch (IteratoroutofsoundsException e) {
ll ezt várjuk
}
Vegyük észre, hogy bár előre iteráltunk, a tömb elejétól a végéig, a visszaadott érté
kek fordított sorrendben vannak. Ha eddig nem lett volna nyilvánvaló, remélhetőleg
most már látjuk, milyen hatékony struktúra is ez. Képzeljük el, hogy az általunk vé
gigjárandó tömb rendezett sorrendű adatok listája. Most már meg tudjuk fordítani a
rendezési so�rendet, anélkül hogy ténylegesen újrarendeznénk
pub li c void �
téStBiCkwardsrterati onBecomesForwards()-
{ Reverseiterater iterator =
}
new Reverserterator(new Arrayrterator(ARRAY));
iterator .l ast(); assertFalse(iterator.isoone()); assertsame(ARRAY[O], iterator.current());
iterator.previous(); assertFalse(iterator.isoone()); assertSame(ARRAY[l], iterator.current());
iterator;previous(); assertFalse(iterator.isoone()); assertsame(ARRAY[2], iterator.current());
iterator.previous(); assertTrue(iterator.isoone()); try {
iterator.current(); fail();
} catch (IteratoroutofsoundsException e) {
ll ezt várjuk
}
A megvalósitás müködése
Az első teszteset biztosí�a, hogy amikor a fordított iterátorral meghívjuk a first()
és a next() elemet, akkor valójában a tömb l ast (utolsó) és previ o us (előző) ele
meit kapjuk, mindegyiknek a megfelelő párját.
A második teszt azt biztosítja, hogy egy tömbön hátrafelé végzett iterálás való
jában a mögöttes iterátor elemeit adja vissza, az elejétól a végéig.
33
Iteráció és rekurzió
Az utolsó teszt szerkezetileg nagyon hasonlít az előzőhöz, de ennél a fi r st() és
a next() helyett a l ast() és a previous() elemet lúvjuk meg, és természetesen el
lenőrizzük, hogy az elejétől a végéig visszaérkezett-e minden érték.
Most már készen állunk arra, hogy a fordított iterátort a gyakorlatban is kipró
báljuk, ahogy ezt a következő gyakorlófeladatban láthatjuk.
Gyakorlófeladat: a fordított iterátor megvalósítása
A fordított iterátor megvalósítása igen egyszerű: csak megfordítjuk a bejárási metó
dus lúvási viselkedését, fi r st, l ast, next, és previ ous:
3.4
Az e!!Jszerííség végett ú!!J diintóitünk, ho!!J e!!Jszerre bemutatjuk az egész os:{fáfyt, abe
lJett bO!!) széttó"rde/nénk e!!Jedi metódusokra, abO!!J eddig tettük.
package com.wrox.algorithms.iteration;
public class Reverseiterator implements Iterator { private final Iterator _iterator;
}
public Reverseiterator(Iterator iterator) { assert iterator != null : "az iterátor nem lehet NULL";
_iterator = iterator; }
public boolean isoone() { return _iterator.isoone();
}
public object current() throws IteratorOutOfBoundsException { return _iterator.current();
}
public void first() { _iterator.last();
}
public void last() { _iterator.first();
}
public void next() { _iterator.previous();
}
public void previous() { �iterator.next();
}
Tömbök feldolgozása
A megvalósitás működése
Amellett, hogy megvalósítja az Iterator interfészt, az osztály rögzíti az iterátort, és
megfordítva a viselkedését. Ahogy láthatjuk, az i soone() és a current() közvetle
nw hívódik meg. A többi metódust- first(), l ast(), next() és previous()- át
irányítja az ellentettjéhez-l ast(), first(), next() és previous() -,az előzőek
nek megfelelően megfordítva ezáltal az iteráció irányát.
A szűrőiterátor
Az iterátorak használatának egyik legérdekesebb és legnagyobb előnye, hogy képe
sek beburkolni egy másik iterátort, ezáltal szűrve a visszaadott értékeket �ásd:
Decorator pattern [Gamma, 1995]). Ez jelentheti azt például, hogy csak minden má
sodik értéket ad vissza, vagy esetleg valami bonyolultabbat, egy adatbázis-lekérdezés
eredményeinek feldolgozását és a nem kívánt értékek eltávolítását. Képzeljünk el egy
szituációt, amelyben az adatbázis-lekérdezésben megadott feltételeken túl a kliens is
végre tud hajtani bizonyos szűréseket.
A szűrőiterátor másik iterátor beburkolásával működik, és csak bizonyos felté
telnek, a predikátumnak megfelelő értékeket ad vissza. Minden alkalommal, arnikor
a mögöttes iterátor meghívódik, a visszaadott érték a predikátumhoz kerül, amely
meghatározza, hogy az érték maradjon vagy elvetendő. Az adatok szűrését az érté
keknek ez a predikátummal történő folyamatos kiértékelése teszi lelietővé.
A predikátumosztály
Egy olyan interfész létrehozásával kezdjük, amely egy predikátumot reprezentál:
packageCoiii:WFOx":"ál go ri thms. i terati on;
public interface Predicate { public boolean evaluate(Object object);
J�
Az interfész nagyon egyszerű, csupán egy e va l ua te() metódust tartalmaz, amely
minden értékre meghívódik, és Boolean eredményt ad, jelezve, hogy az érték kielégí
ti-e a kiválasztási feltételt vagy nem. Ha az eval uate() true értékkel tér vissza, ak
kor az értéket be kell emelni, és így a szűrőiterátor visszatér vele. Ha ellenben a pre
dikátum fal se üzenetet küld, akkor ez értéket figyelmen kívül fogjuk hagyni, és úgy
kezeljük, mintha nem is létezett volna.
Bár egyszerű, a predikátuminterfész lehetővé teszi igen bonyolult szűrők létre
hozását. Predikátummal megvalósíthatunk ÉS(&&), VAGY (ll), NEM(!) és hason
ló operátorokat, lehetővé téve ezzel bármilyen összetett predikátum megalkotását.
35
Iteráció és rekurzió
Gyakorlófeladat: a predikátumosztály tesztelése
Most néhány tesztet írunk, hogy meggyőződjünk róla, helyesen működik-e szűrőite
rátorunk. Ellenőriznünk kell, hogy a szűrő visszaadja-e a mögöttes iterátorból a pre
dikátum által elfogadott értékeket. Négy tesztet hajtunk végre: az előre és hátra iterá
lás két kombinációját: egyet, amelyben a predikátum elfogadja az értékeket, és egyet,
amelyben elveti őket.
package com.wrox.algorithms.iteration;
import junit.framework.Testcase;
public class FilteriteratorTest extends Testcase { private static final object[] ARRAY = {"A", "B", "c"};
}
Azt akarjuk tudni, hogy a predikátumot egyszer hívják meg minden egyes elernhez,
amely a mögöttes iterátorból visszatért. Ezért létrehozunk egy predikátumot kifeje
zetten tesztdési céllal.
private static final class oummyPredicate implements Predicate { private final Iterator _iterator;
}
private final boolean _result;
public oummyPredicate(boolean result, Iterator iterator) { _iterator = iterator; _result = result; _iterator.first();
}
public boolean evaluate(object object) { assertsame(_iterator.current(), object); _iterator.next(); return _result;
}
Az első teszttel azt ellenőrizzük, hogy a szűrő olyan értékeket ad vissza, amelyeket a
predikátum elfogad- az ev a l ua te() true értéket ad vissza-, miközben előrehaladunk.
36
public void testForwardsiterationincludesitemswhenPredicateReturnsTrue() {
Iterator expectediterator = new Arrayiterator(ARRAY); Iterator underlyingiterator = new Arrayiterator(ARRAY);
Iterator iterator = new Filteriterator(underlyingiterator, new oummypredicate(true, exQectediterator));
·; terator. fi r st o ; . assertFalse(iterator.isoone())j assertsame(ARRAY[O], iterator.current())j
iterator. next(); assertFalse(iterator.isoone())j assertsame(ARRAY[l], iterator.current());
it:erator.next() j assertFalse(it:erator.isoone()); assertsame(ARRAY[2], iterator.current())j
iterator.next() j assertTrue(iterator.isoone())j try {
iterator.current()j fail() j
} catch (IteratoroutOfBoundsException e) { ll ezt várjuk
}
assertTrue(expectediterator.isoone())j assertTrue(underlyingiterator.isoone())j
Tömbök feldolgozása
}--�----------------------------------�--
A következő teszt sokkal egyszerűbb, mint az első. Ezúttal azt akarjuk látni, rrú törté
nik, ha a predikátum elveti az értékeket- ekkor ev a l ua te() fa l se értéket ad vissza:
public void testForwardsiterationExcludesitemswhenPredicateReturnsFalse() {
Iterator expectediterator = new Arrayiterator(ARRAY)j
}
Iterator underlyingiterator = new Arrayiterator(ARRAY)j
Iterator iterator = new Filteriterator(underlyingiterator, new oummyPredicate(false, expectediterator))j
iterator.first()j assertTrue(iterator.isoone())j try {
iterator.current(); fail() j
} catch (IteratoroutOfBoundsException e) { ll ezt várjuk
}
assertTrue(expectediterator.isoone())j assertTrue(underlyingiterator.isoone());
37
Iteráció és rekurzió
A másik két teszt nagyjából megegyezik az első kettővel, kivéve, hogy az iteráció
sorrendje megfordult:
38
public void testsackwardssiterationincludesitemswhenPredicateReturnsTrue() {
Iterator expectediterator =
}
new Reverseiterator(new Arrayiterator(ARRAY)); Iterator underlyingiterator = new Arrayiterator(ARRAY);
Iterator iterator = new Filteriterator(underlyingiterator, new oummyPredicate(true, expectediterator));
iterator.last(); assertFalse(iterator.isoone()); assertsame(ARRAY[2], iterator.current());
iterator.previous(); assertFalse(iterator.isoone()); assertsame(ARRAY[l], iterator.current());
iterator.previous(); assertFalse(iterator.isoone()); assertsame(ARRAY[O], iterator.current());
iterator.previous(); assertTrue(iterator.isoone()); try {
iterator.current(); fail();
} catch (IteratoroutOfBoundsException e) {
ll ezt várjuk }
assertTrue(expectediterator.isoone()); assertTrue(underlyingiterator.isoone());
public void testsackwardsiterationExcludesitemswhenPredicateReturnsFalse() {
Iterator expectediterator = new Reverseiterator(new Arrayiterator(ARRAY));
Iterator underlyingiterator = new Arrayiterator(ARRAY);
Iterator iterator = new Filteriterator(underlyingiterator, new oummyPredicate(false, expectediterator));
iterator.last(); assertTrue(iterator.isoone()w)�;�--------------------------------�
}
try { iterator.current(); fai l O;
} catch (IteratoroutofsoundsException e) {
ll ezt várjuk }
assertTrue(expectedrterator.isoone()); assertTrue(underlyingrterator.isoone());
A megvalósitás működése
Tömbök feldolgozása
Magukon a teszteseteken kivül, a tesztosztály többet tartalmaz, mint egyszerű teszt
adatokat. Mivel a szűrőiterátor megfelelő teszteléséhez nemcsak az iterációtól elvárt
eredményét kell igazolni, hanem azt is, hogy a predikátumot helyesen hívjuk meg.
A oummyPredi ca te belső osztály, amelyet a második kódblokkban hoztunk létre
tesztdési céllal, egy iterátort tartalmaz, amely ugyanolyan sorrendben adja vissza az
értékeket, mint amilyenben a predikátumhívásokat várjuk. Minden esetben, amikor
az evaluate() meghívódik, ellenőrizzük, hogy a helyes érték került-e átadásra. Az
érték ellenőrzésén túl az ev a l ua te() előre meghatározott eredménnyel tér vissza -
amelyet a teszteset határoz meg -, így ellenőrizni tudjuk, rni történik, ha a prediká
tum elfogad értéket, illetve ha nem.
Ezután megalko�uk az aktuális tesztet. Két iterátor létrehozásával kezdtük:
egyet azoknak az elemeknek, melyekkel a predikátum várhatóan meghívásra kerül, a
másikat pedig azoknak az elemeknek, amelyeket első helyen kiszűrünk Ezek alapján
létrehozzuk a szűrőiterátort, átadva a mögöttes iterátornak, és egy beállitott álpredi
kátumot, amely mindig elfogadja a neki kiértékelésre eljuttatott értékeket. Ezután a
szűrőiterátort az első elemhez pozicionáljuk, ellenőrizzük, hogy valóban van-e elér
hető elem, és hogy az érték az-e, amelyet vártunk. A teszt fennmaradó része egysze
rűen a next() metódust hívja újra és újra, amíg az iterátor nem végez a kimenő
eredmények leellenőrzésével. Figyeljük meg az első teszt két utolsó sorát (a harma
dik kódblokkban az előző gyakorlófeladatból), amelyek biztosí�ák, hogy mind a
mögöttes, mind a várható iterátor végzett.
A következő teszt szinte ugyanúgy kezdődik, mint az előző, csak itt a predikátu
mot előre meghatározott fa l se visszaadott értékével hozzuk létre. Miután a szűrő
iterátort az első elemhez pozicionáltuk, azt várjuk, hogy egyszerűen befejeződjön,
ugyanis a predikátum minden értéket elvet. Megint arra számítunk, hogy mindkét
iterátor végezzen; és a mögöttes iterátortól külön még azt is várjuk, hogy minden ér
tékét leellenőrizzen.
Az utolsó tesztben figyeljünk a Reverseiterater használatára; az üres iterátor
még mindig azt hiszi, hogy előreiterál, pedig már hátrafelé lépked.
39
Iteráció és rekurzió
Gyakorlófeladat: a predikátumosztály megvalósítása
A helyes teszteket közvetlenül megvalósíthatjuk Az interfészt már meghatároztuk a
predikátumokhoz, így már csak a szűrőiterátor osztályt kell létrehoznunk
package com.wrox.algorithms.iteration;
public class Filterrterator implements Iterator { private final Iterator _iterator;
}
private final Predicate _predicate;
public Filteriterator(Iterator iterator, Predicate predicate) { assert iterator != null : "az iterátor nem lehet NULL"; assert predicate != null : "a predikátum nem lehet NULL";
}
_iterator = iterator; _predicate = predicate;
public boolean isoone() {
return _iterator.isoone();
}
public object current() throws IteratorOutOfBoundsException { return _iterator.current();
}
A first() és next() esetén a hívást először a mögöttes iterátorhoz küldtük, mielőtt
az aktuális pozíciótól előrehaladva kerestük volna a szűrőt kielégítő értéket:
40
public void first() { _iterator.first(); filterForwards();
}
public void next() { _iterator.next(); filterForwards();
}
private void filterForwards() {
}
while (!_iterator.isoone() &&
!_predicate.evaluate(_iterator.current())) { _iterator.next();
}
Tömbök feldolgozása
Végül hozzáadjuk a l ast() és a previous() metódust, amelyek nem meglepő mó
don nagyon hasonlóak a first() és a next() metódushoz:
putine:·-
void la. st"O- {
_iterator.last(); filterBackwards();
}
public void previous() { _iterator.previous(); filterBackwards();
}
private void filterBackwards() {
l
while (!_iterator.isDone() &&
!_predicate.evaluate(_iterator.current())) { _iterator.previous();
}
Most már használhatjuk a Fi lteriterator metódust, hogy bármely iterátotokat tá
mogató adatstruktúrát bejárjunk. Csak a megfelelő predikátumot kell létrehozni,
hogy végrehajthassuk a kívánt szűrést.
A megvalósitás működése
A szűrőiterátor osztály természetesen megvalósítja az Iterator interfészt, valamint
tartalmazza a beágyazott iterátort és a szűréshez szükséges predikátumot. A kon
struktor először ellenőrzi, hogy egyik paraméter se legyen null, rnielőtt későbbi
használatra a példányváltozókhoz rendeli őket. A két metódusnak - i soone() és
current() -csak a mögöttes iterátor megfelelő metódusait kell delegálnia. Ez mű
ködik, rnivel a mögöttes iterátor mindig olyan állapotban van, hogy csak a prediká
tum által engedélyezett objektum lehet az aktuális objektum.
Az iterátor igazi működése akkor megy végbe, rnikor az egyik bejárásmetódus
meghívódik Bármikor, ha a first(), a next(), a l ast() vagy a previous() meghí
vódik, a predikátumot kell használni a megfelelő értékek beemelésére vagy kizárásá
ra, rniközben az iterátor szemantikáját megőrizzük:
public void first() { _iterator.first(); filterForwards();
}
public void next() { _iterator.next(); filterForwards();
}
41
Iteráció és rekurzió
private void filterForwards() {
}
whil e (!_iterator.isoone() &&
!_predicate.eval uate(_iterator.current())) {
_iterator.next();
}
Amikor a fi lterForwards meghívódik, feltételezzük, hogy az iterátor már pozicionálva van egy elemhez, ahonnan a keresést kezdeni fogjuk. A metódus ezután ciklust hoz létre, meghíva a next() értéket addig, amíg el nem fogynak az elemek, vagy egy illeszkedő elemet nem talál. Vegyük észre, hogy minden esetben közvetlenül a mögöttes iterátor metódusait hívjuk meg. Ezzel elkerüljük a szükségtelen ciklusokat, amelyek szélsőséges esetekben valószínűleg rendellenes programleállás t eredményeznének.
publ ic void last() {
_iterator.last();
filterBackwards();
}
publ ic void previous() {
_iterator.previous();
fil terBackwards();
}
private void fil terBackwards() {
}
while (!_iterator.isoone() &&
!_predicate.evaluate(_iterator.current())) { _iterator.previous();
}
Úgy, ahogy a fi r st() és a next(), a l ast O és a p rev i o us O esetében, most is a tartalmazott osztály metódusait hívjuk, rnielőtt meghívnánk a fi l terBackwards metódust a predikátumot Icielégítő elem megtalálására.
Rekurzió
,,Ahho� hogJ megérthessük a rekur':(jót, előszö·r meg kell értenünk a rekuqót."
Ismeretlen
Képzeljünk el egy fájlrendszert, olyat, arnilyen a számítógépünkön található. Közismert, hogy a fájlrendszernek van egy gyökérkönyvtára számos alkönyvtárral (és fájlokkal), amelyek további alkönyvtárakat (és fájlokat) tartalmaznak.
42
Rekurzió
Erre a könyvtárstruktúrára gyakran kö.tryvtáifa néven hivatkozunk: egy fa, amely
nek van gyökere, ágai (könyvtárak) és levelei (fájlok). A 2.1. ábrán látható egy fájl
rendszer ilyen ábrázolása. Megfigyelhető, hogy ez egy fordított fa, a gyökere van felül,
a levelei pedig alul.
2. 1. ábra. Kö.tryvtárszerkezetfa ábrázolása
2.2. ábra. A fák ágai maguk is fák
43
Iteráció és rekurzió
Az egyik érdekes dolog a "fákkal" kapcsolatban, hogy minden águk akár egy másik,
kisebb fának tekinthető. A 2.2. ábrán látható az előző fa, de ezúttal kiemeltük az
egyik ágát. Szembeötlő, hogy mennyire hasonlit a struktúrája a nagyobb fáéhoz.
Ez a jellemző, amitől néhány dolog különböző tagoltság és nagyítás esetén
ugyanúgy néz ki, jól használható problémák megoldására. Amikor a probléma ehhez
hasonlóan kisebb összetevőkre bontható, amelyek pontosan úgy néznek ki, mint a
nagy (oszd meg és uralkodj), megjelenhet a rekurzió. Bizonyos értelemben a rekur
zió egy újrafelhasználási minta: metódus, amely meglúvja önmagát.
Rekurzív könyvtárfa-nyomtatási példa
Folytassuk a fájlrendszer hasonlósággal, és írjunk egy programot, amely kiírja egy teljes
könyvtárfa tartalmát. A rekurzió bemutatására készült példákat leggyakrabban a Fibo
nacci-számok vagy prímszámok megtalálására, illetve útvesztőfeladványok megoldására
használják, ezek azonban aligha olyan dolgok, amelyekkel mindennap összefuthatunk.
Azonkívül, hogy kiírja a neveket, lehetővé teszi a kimenet formázását, így min
den fájl és alkönyvtár bekerül az elődje alá - mint a Windows Intéző vagy a Mac OS
X Pinder tesztverziója esetén. A fájlrendszer struktúrája adott, ehhez tudunk rekur
zív algoritmust készíteni, amely bejárja a könyvtárstruktúrát azzal, hogy lebontja a
problémát; a megoldás múködik egy szinten, azután meglúvja önmagát a könyvtárfa
minden egyes alsóbb szintjére.
Természetesen egy osztállyal kell kezdenünk, és mivel valószínűleg parancssor
ból szeretnénk futtatni a programunkat, szükségünk lesz egy mai n metódusra:
44
package com. w ro x. a l go ri thms. i terati on;
import java.io.File;
public final class RecursiveoirectoryTreePrinter { private static final string SPACES = " " ;
}
public static void main(String[] args) {
}
assert args != null : "az argumentumlista nem lehet NULL";
if (args.length !=l) {
}
system.err.println("Használat: RecursiveoirectoryTreePrinter <könyvtár>");
system.exit(4);
print(new File(args[O]), "");
Rekurzió
Programunknak parancssori paraméterként meg kell adnunk egyetlen könyvtár (vagy
fájl) nevét. Miután elvégzett néhány alapvető ellenőrzést, a mai n() függvény létrehoz
egy j ava. i o. Fi l e objektumot a paraméter alapján, és átadja a pr i nt() metódusnak.
V együk észre, hogy a második paraméter a metódushívásnál üres sztring. Ezt
fogjuk használni a print() metódus esetén, hogy beljebb írjuk a kimenetet, de eb
ben az esetben, mivel ez az első szintje a könyvtárfának, amelyet kiíratunk, nem sze
retnénk semmilyen behúzást, ezért kell a"". A SPACES konstanst (amely két szóköz
ként lett megadva) fogjuk használni arra, hogy megnöveljük később a behúzás t.
A p ri nt() metódus egyetlen Fi l e objektumot és a behúzás miatt használt sztrin
get fogad el.
pubtic static void pr'inüF'ile file, stri'ii'Qindent)-
{ assert file != null : "a fájl nem lehet NULL"; assert indent != null : "a behúzás nem lehet NULL";
}
system.out.print(indent); system.out.println(file.getName());
if (file.isoirectory()) { print(file.listFiles(), indent + SPACES);
}
A kód lényegre törő. Először a behúzás kerül kiírásra, azt követi a fájl neve, majd
egy új sor. Ha a fájl könyvtárat reprezentál (a Java Fi l e objektumokat használ az
egyedi fájlok és a könyvtárak esetén is), meghívunk egy eltérő p ri nt() metódust,
amely feldolgozza a könyvtárban található fájllistát.
Mivel egy szinttel lejjebb lépünk a fában, szeretnénk megnöveini a behúzás mér
tékét, vagyis szeretnénk, ha minden kiírás néhány szóközzel jobbra kerülne. Ezt úgy
érhetjük el, hogy az aktuális behúzáshoz hozzátoldjuk a SPACES konstans értékét.
Induláskor a behúzás értéke üres sztring lesz, így növekedni fog két majd négy, az
tán hat szóközre, és a kiírt kimenet is minden alkalommal jobbra fog tolódni.
Ahogy jeleztük, a l i st Fi l es O metódus tömbbel tér vissza; és mivel még nincs
olyan pr i nt O metódusunk amely ilyet elfogad, hozzunk létre egyet:
public static void print(File[] files, String indent) ·{
assert files l= null : "a fájllista nem lehet NULL";
for (int i =O; i < files.length; ++i) { print(files[i], indent);
} .}.
45
Iteráció és rekurzió
Ez a metódus a tömbön iterál végig, az eredeti pr i nt() metódust minden egyes fájl
ra meglúvja.
Látszik, hogy rekurzív? Emlékezzünk vissza, hogy az első p rí nt() metódus -
amely egy fájlt kap- meglúvja a második print() metódust, amely egy tömböt kap,
amely sorjában meglúvja az első metódust, és így tovább. Ez örökké folytatódhatna,
de valójában a második pr i nt() metódus kifogy a fájlokból-vagyis a tömb végére
ér -, és visszatér.
A következőkben látható a program futtatásának kimenete, amint a könyv kód
jait tartalmazó könyvtárfát járja be.
46
seginníng Algorithms build
classes
sr c
com wrox
algorithms i terati on
Arrayiterator.class ArrayiteratorTest.class Iterator.class IteratoroutofsoundsException.class RecursíveDirectoryTreePrinter.class Reverserterator.class ReverseiteratorTest.class Singletoniterator.class Si ngl eton ite r atorTe st.class
build.xml conf
build.properties checkstyle-header.txt checkstyle-main.xml checkstyle-test.xml checkstyle.xsl simian.xsl
l i b ant l r-2. 7. 2. j ar checkstyle-3.5.jar checkstyle-optional-3.5.jar commons-beanutils.jar commons-collections-3.1.jar getopt.jar jakarta-oro.jar jakarta-regexp.jar jamaica-tools.jar junit-3.8.1.jar simian-2.2.2.Ju·a�r-----------------------�--A-------�------�
main com
w ro x algorithms
iteration Arrayiterator.java Iter a tor .java IteratoroutofaoundsException.java RecursiveoirectoryTreePrinter.java Reverseiterator.java
'-------------=si.nglet.oniterator.java ---��
Rekurzió
Ahogy látható, a kimenet szépen formázott a megfelelő behúzással a könyvtár tar
talmának kiírása esetén. Ezzel a gyakorlati példával remélhetőleg sikerült bemutat
nunk, hogyan használható a rekurzió bizonyos fajta feladatok megoldására.
Bármely probléma, amely megoldható reku1ifv módon, iteratív módon is megoldható,
ugyanakkor az utóbbi válas'.{fása néha jóval bOf!JOlultabb és fáradságos abb, haSZJZálatá
hoz szükség lehet olyan adatstruktúrákra, amelyeket még nem mutattunk be, mint pél
dául a verem (lásd 5. fr!jezet).
A rekurzfv algoritmus müködése
A problémától függetlenül a rekurzív algoritmus általában két részre bontható: az
alapesetre és az általános esetre. Vizsgáljuk meg az előző példát, és azonosítsuk eze
ket az elemeket!
Az alapeset
A példában, amikor egy egyszerű fájllal találkozunk, a legalacsonyabb szintű prob
lémával szembesülünk az algoritmusban, ebben az esetben kiírjuk a nevét. Ezt alap
esetnek nevezzük.
Az alapeset tehát a probléma azon része, amelyet egyszerűen megoldhatunk re
kurzió nélkül. Ez egyben a leállási eset, amely megakadályozza, hogy a rekurzió
örökké folytatódjon.
A Stackove r flowException (veremtúlcsorduláschiba) reku1ifv algoritmus futtatása
kiizben gyakran jelzi!Je annak, hogy a program hiáf!Jzó vagy nem elégséges leállási jeltétel
miatt tijabb és tijabb beágyazott hívásokba kezd, végül kifut a rendelkezésre álló memó
riából. Ez természetesen a'{! is jelentheti, hogy a megoldandó feladat túl nagy a rendelke
zésre álló erőforrásokhoz képest.
47
Iteráció és rekurzió
Az általános eset
A legtöbbször fennálló általános eset az, ahol a rekurzív hívás történik. A példában az első rekurzív hívás akkor történik, arnikor egy könyvtárat reprezentáló fájllal találkozunk. Miután kiírtuk a nevét, fel szeretnénk dolgozni az összes könyvtárban található fájlt, így meghívjuk a második pr i nt() metódust.
A második p ri nt() metódus visszahívja az első p ri nt() metódust minden egyes fájira a könyvtárban.
Két metódus használatát, amelJek egymást rekur'{jv módon hívják, kolcsiinos rekurzió
nak is neveifk.
Összefoglalás
Az iteráció és a rekurzió alapvető fontosságú az algoritmusok implementálása során. A könyv többi része erősen támaszkodik erre a két fogalomra, így fontos, hogy továbblépés előtt jól megértsük őket.
48
A fejezetből a következőket tudhattuk meg:
• Az iteráció néhány probléma megoldása során adja magát, míg más esetekben a rekurzió természetesebbnek tűnhet.
• Az iteráció több gyakori probléma nagyon egyszerű, egyenes megközelítése, például számításoké és tömbök feldolgozásáé.
• Az egyszerű tömbalapú iteráció nem igazán jól skálázható életszerű alkalmazások esetén. Ezt kiküszöbölendő, bemutattuk az iterátor fogalmát, és megvizsgáltunk több különböző típusú iterátort.
• A rekurzió. "osifl meg és uralkodj" megközelítést használ, ahol a metódus ismételten egymásba ágyazva meghívja önmagát. Gyakran jobb választás egymásba ágyazott adatstruktúrák feldolgozása esetén.
• Sok probléma megoldható az iteráció vagy a rekurzió használatával.
Gyakorlatok
Gyakorlatok
Ezeknek a gyakorlatoknak (és az összes többi fejezethez tartozóknak is) a megoldásaira a D függelékben találhatunk példát.
1. Hozzunk létre iterátort, amely csak minden n-edik elem értékével tér vissza, ahol n nullánál nagyobb pozitív egész.
2. Hozzunk létre predikátumot, amely elvégzi a logikai ÉS (&&) műveletet két másik predikátumon.
3. Írjuk újra a Powerca l cuator-t iteráció helyett rekurzió t használva.
4. Cseréljük le a tömbök használatát iterátorokra a rekurzív könyvtárfanyomtatóban.
5. Hozzunk létre egy iterátort, amely egyetlen értéket tartalmaz.
6. Hozzunk létre egy üres iteráto rt, amely mindig végzett állapotban van.
49
HARMADIK FEJEZET
Listák
Most, hogy már megismerkedtünk az iterációval és az algoritmusok néhány alapele
mével, ideje rátérni az első összetett adatstruktúrára. A listák a legalapvetőbb adat
struktúrák, amelyekre sok más adatstruktúra épül, és amelyeket még több algoritmus
használ fel.
A valós életben nem nehéz példát találni a listákra: bevásárlólisták, teendők listá
ja, menetrendek, megrendelőlapok vagy éppen ez a "listák listája". A tömbökhöz ha
sonlóan a listák is jól hasznosíthaták lesznek a legtöbb alkalmazásban, amelyet írunk.
A listákkal tulajdonképpen kiválóan helyettesíthetők a tömbök; általában lehetséges
(és a legtöbb esetben kívánatos is), hogy a leginkább memóriaérzékeny/időkritikus
alkalmazások kivételével mindenütt teljesen kiváltsuk a listákkal a tömböket.
Ebben a fejezetben először áttekin�ük az alapvető listamúveleteket. Ezt követi
egy teszt, majd megvizsgáljuk a listák két megvalósítását: a tömblistát és a láncolt lis
tát. Mindkét megvalósítás ugyanarra az interEészre illeszkedik, ugyanakkor elég eltérő
tulajdonságokkal rendelkeznek. Ezek a különbségek határozzák meg, hogy mikor és
hogyan használhatjuk őket alkalmazásainkban. A fejezet végére a következőket is
merjük meg:
• mi a lista,
• hogyan néz ki a lista,
• hogyan használjuk a listákat,
• hogyan valósíthatjuk meg a listákat.
A listákról
A lista az elemek rendezett gyűjteménye, amely a tömbökhöz hasonlóan támogatja
az egyes elemekhez való véleden hozzáférést; a listából lekérdezhetjük egy tetszőle
ges elem értékét. A lista megőrzi a beszúrási sorrendet is, így egy adott lista - feltéve,
hogy semmilyen beavatkozással nem módosítjuk - ugyanarról a helyről mindig
ugyanazt az értéket fogja visszaadni. A tömbökhöz hasonlóan a listák sem töreksze
nek arra, hogy a tárolt értékek egyedi ek legyenek, vagyis a listák tartalmazhatnak két
szerezett értékeket. Ha például egy listában az "úszás", "kerékpározás" és "tánc"
Listák
szerepel, majd ismét az "úszás" elemet szetetnénk felvenni, akkor azt tapasztaljuk,
hogy a lista mérete megnő, és kétszer fogja tartalmazill az "úszás" elemet. A legfőbb
különbség a tömbök és listák között abban rejlik, hogy míg a tömbök fix méretűek,
a listák átméretezhetők: igény szerint csökkentheták vagy növelhetők.
A listák minimálisan a 3.1. táblázatban bemutatott négy alapszintű műveletet
támogatják.
jMűvelet i n se rt
delete
get
size
Letrás
Egy elemet illeszt be a listába a megadott helyre (O, 1, 2 ... ).
A lista mérete eggyel nő. IndexoutOfBoundsExcepti on kivételt
dob, ha a megadott hely a tartományon kívülre esik
(O <= index < size() ).
Egy elemet töröl a listából a megadott helyről (O, 1, 2 ... ). A lista
mérete eggyel csökken. IndexoutofBoundsExcepti on kivételt
dob, ha a megadott hely a tartományon kívülre esik
(O <= index < size() ).
Egy elemet ad vissza a listából a megadott helyről (O, 1, 2 ... ).
IndexoutofBoundsExcepti on kivételt dob, ha a megadott hely
a tartományonkívülre esik(O <=index < size() ).
A lista elemszámát adja vissza.
3. 1. táblázat. A lista alapsifntú múveletei
Ez az összes olyan művelet, amely a listák kezelésekor elengedhetetlen. Viszont ha
csak ezek a műveletek állnának rendelkezésünkre, akkor folyton-folyvást ugyanazokat
a kódrészleteket másolgatnánk és illesztgetnénk be, ahogy egyre kifinomultabb művel
teket szetetnénk végrehajtani listánkon. Nincs például célzott művelet arra, hogy egy
listaelem értékét módosítsuk (mint ahogyan a tömböknél), de ugyanezt az eredményt
érhe�ük el azzal, ha először töröljük az elemet, majd egy újat illesztünk a helyére. Az
ilyen egyszerű interfészek használatából fakadó állandó kódismételgetések elkerülése
érdekében ezeket az általános viselkedésmintákat a 3.2. táblázatban tátható néhány ké
nyelmi művelet bevezetésével beágyazha�uk például magába a listába.
jMüvelet set
52
Leirás
Egy elem értékét állítja be a listában a megadott helyen (O, l,
2 ... ). Az adott helyen lévő elem eredeti értékét adja vissza.
IndexoutOfBoundsExcepti on kivételt dob, ha a megadott hely
a tartományon kívülre esik (O <= index < size()) .
A listákról
MOvelet Leirás
add A lista végére beszúr egy elemet. A lista mérete eggyel nő.
delete Egy adott érték első előfordulását törli a listábóL A lista mérete
eggyel csökken. Visszatérési értéke true, ha az érték szerepel a
listában, és fa l se, ha nem.
contains Megállapítja, hogy az adott érték szerepel-e a listában.
indexof Egy adott értéknek listabeli első előfordulási helyét (O, l,
2 ... ) adja vissza. Visszatérési értéke -l, ha az elem nem talál
ható meg a listában. Az egyenlőséget az elem equals metódu
sának meghívásával dönti el.
isEmpty
iterator
clear
Megállapítja, hogy a lista üres-e. Visszatérési értéke true, ha a
lista üres (si ze() == O); egyébként fa l se.
A lista összes dernén végigfutó iterátort hoz létre.
A lista összes elemét törli. A lista mérete nullára lesz állítva.
3.2. táblázat. A lista kéf!Yelmi miiveletei
Ezek a műveletek mind megvalósíthaták a korábban bemutatott alapszintű művele
tekre alapozva. Viszont ha úgy döntünk, hogy a lista részeként megvalósí�uk ezeket
is, akkor sokkal gazdagabb interfészhez jutunk, így nagyban leegyszerűsödik a listá
kat használó programozók dolga.
A set() művelet könnyedén megvalósítható például a delete() és insert(),
add() és insert(), isEmpty() és size() stb. kombinációjával. Ismételten hangsú
lyozzuk, hogy a listákhoz hasonló adatstruktúrákat az alapszintű műveleteken túli
gazdagság, a közös funkciók és viselkedések beágyazása teszi hatékonnyá .
Gyakorlófeladat: listainterfész létrehozása
Most, hogy általánosságban már leírtuk a műveleteket, ideje létrehozni egy tényleges
Java interfészt, amelyet majd a fejezet egy későbbi részében megvalósítunk
pack:agecom. wrox:al'gö'rithms. l i sts;
import com.wrox.algorithms.iteration.Iterable;
public interface List extends Iterable { public void insert(int index, object value)
throws IndexoutOfBoundsException; public void add(object value); pub l i c��j_ect dED e "t:�( i nt i ndexLth ��-!ll.!lexoutof�QundsExc_�ti c>n
•
53
Listák
}
public boolean delete(object value); public void clear(); public object set(int index, Object
throws public object get(int index) throws public int indexof(Object value); public boolean contains(Object value); public int size();
value) IndexoutofsoundsException; IndexoutofsoundsException;
public boolean isEmpty();
A megvalósitás müködése
Mint látható, szó szerint fogtuk a műveleteket, és egyenként egy interfész metódusaivá alakítottuk őket, mindet a megfelelő paraméterekkel, visszatérési tipusokkal és kivételekkel. Ez semmi esetre sem tekinthető triviális interfésznek; számos metódus vár megvalósításra. Ám amint rátérünk a tényleges megvalósításra, látni fogjuk, hogy ezek az extra funkciók viszonylag egyszerűen biztosíthatók.
Megfigyeljük majd azt is, hogy a L i st interfész a 2. fejezetben bemutatott Iterable interfész kibővítése. Ez az interfész egyetlen iterator() metódussal rendelkezik, és lehetővé teszi, hogy a listát a kódban bárhol használhassuk, ahol csak egy lista elemein kell végigmennünk Ezt az interfészt észben tartva vessünk egy pillantást a következő két kódrészletre. Az első egy három értéket tároló tömböt hoz létre, majd végigmegy az elemeken, és ki is írja mindegyik értékét:
String[] anArray = . . . ;
anArray[O] anArray [l] anArray[2]
"alma"; ll banán"; "cseresznye";
for (int i O; i < anArray.length; ++i) { system.out.println(anArray[i]);
}
A második kódrészlet egy három értéket tartalmazó listát hoz létre, majd végigmegy az elemeken és közben mindegyik értékét ki is írja:
54
List aList = . . . ;
aList.add("alma"); aList.add("banán"); aList.add("cseresznye");
Iterator i = aList.iterator() for (i.first(); !i.isoone(); i.next()) {
system.out.println(aList.current()); }
A Listák tesztelése
Nincs sok különbség a kettő között; talán azt lehetne mondani, hogy a listás verzió valamivel könnyebben olvasható. Különösen az add() és az iterátor használata segít a kód szándékának megértésében.
A listák tesztelése
Bár még egyeden konkrét listát sem valósítottunk meg, végiggondolhatjuk és kódban le is írhatjuk azokat a helyzeteket, amelyekkel listánk valószínűleg találkozni fog. A különböző listamegvalósítások helyes viselkedésének biztosítása érdekében létre kell hoznunk néhány olyan tesztet, amelyeken minden megvalósításnak meg kell felelnie. Ezek a tesztek fogják kódban megvalósítani a 3.1. és a 3.2. táblázatban bemutatott követelményeket, és ezek adják a listaság deflllÍcióját. Ráadásul, ha végignézzük a teszteket, tisztán fogjuk látni a listák elvárt viselkedését, ami jelentősen megkönnyíti a munkánkat, arnikor eljön a saját megvalósításunk megírásának ideje.
Gyakorlófeladat: generikus tesztosztály létrehozása
Már tudjuk, hogy két listamegvalósításunk lesz. Általában gondolhatjuk úgy, hogy mindkettőhöz egyedi tesztcsomag létrehozása szükséges, de írhatunk egyetlen olyan tesztcsomagot is, amelyet minden egyes megvalósításunkhoz újra és újra felhasználhatunk Ehhez létrehozunk egy absztrakt osztályt, amely tartalmazza a tényleges teszteseteket és néhány horgonyt az alosztálykezeléshez. Indulásként definiáljuk az absztrakt alaposztályt, amely kiterjeszthető a listák egyes megvalósításaira jellemző konkrét tesztosztályokra.
package com. wrox. algorithms .l i sts;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutofsoundsException; import junit.framework.Testcase;
public abstract class AbstractListTestcase extends Testcase { protected static final Object VALUE....A = "A"; protected static final Object VALUE_B = "s"; protected static final object VALUE._C = "c";
protected abstract List createList();
}
55
Listák
Néhány közös tesztadattól eltekintve egy absztrakt metódust hoztunk létre, amely egy lista egy példányát adja vissza. Ezt fogja használni a tesztmetódus a tesztelendő lista beszerzésére. Ha ezután bármikor egy új listatípushoz szeretnénk tesztkészletet létrehozni, kibővíthetjük az Abst:ract:L i st:Test:Case osztályt és megvalósítha�uk a create
L i st:() metódust, hogy megkapjuk az adott listaosztály egy példányát. Így ugyanazt a tesztet alkalmazha�uk újra és újra, a tényleges megvalósítástól függetlenül.
És most térjünk át a lista működésének tesztelésére.
Gyakorlófeladat: tesztmetódus létrehozása értékek beszúrására és hozzáadására
A beszúrás talán a listák legalapvetőbb funkciója; nélküle üresek maradnának a listák. Íme a kód:
public void t:est:Insert:Int:oEmpt:yList:() { List: list: = creat:eList();
}
assertEquals(O, list.size()); assertTrue(list.isEmpty());
list.insert(O, VALUE_A);
assertEquals(l, list.size()); assertFalse(list:.isEmpty()); assert:same(VALUE_A, list:.get:(O));
Most azt tesztelj ük, mi történik, ha az értéket két másik érték kö"zé akarjuk beszúrni. Azt várjuk, hogy a beszúrási ponttól jobbra lévő elemek egy hellyel jobbra tolódjanak, helyet adva így az új értéknek
56
public void t:est:InsertBetweenElement:s() { List list = creat:eList:();
}
list:.insert:(O, VALUE_A); list:.insert(l, VALUE_B); list.insert(l, VALUE_C); assertEquals(3, list:.size());
assert:Same(VALUE_A, list:.get:(O)); assert:same(VALUE_C, list:.get:(l)); assert:same(VALUE_B, list:.get:(2));
A listák tesztelése
Ezután ellenőrizzük, be tudunk-e szúrni a lista első eleme elé egy új elemet:
pub li c void t:estrnsert:BeforeFi rst:El ement:()-{ List: list:= creat:eList:();
}
list:.insert:(O, VALUE_A); list.insert(O, VALUE_B);
assertEquals(2, list.size()); assertsame(VALUE_B, list.get(O)); assertsame(VALUE_A, list.get(l));
Ellenőrizzük le az utolsó elem mög,é történő beszúrást is. Általában ezzel a módszerrel bővítjük a listát. (Könnyen meglehet, hogy sokkal gyakrabban használjuk majd ezt, mint bármilyen egyéb beszúrást; éppen ezért nem árt jól megértenünk!)
pub li c void test:InsertAft:erLastEl ement()-
{ List list = createList:();
}
list:.insert:(O, VALUE_A); list:.insert:(l, VALUE_B);
assert:Equals(2, list:.size()); assert:same(VALUE_A, list:.get:(O)); assert:Same(VALUE_B, list:.get:(l));
Most azt fogjuk tesztelni, hogy a lista helyesen működik-e abban az esetben, ha a tartományán kívülre eső pozícióba szeretnénk elemet beilleszteni. ilyen esetekben Indexout:ofsoundsExcept:i on kivétel dobását várjuk, ami az alkalmazás programozási hibáját jelzi:
public void t:est:rnsert:out:ofsounds()-
{ List: list:= creat:eList:();
}
t:ry { list:.insert:(-1, VALUE_A); fail();
} catch (Indexout:OfBoundsExcept:ion e) { l l ezt: várjuk
}
t:ry { list:.insert:(l, VALUE_B); fa il O;
} catch (Indexout:OfBoundsExcept:ion e) { ll ezt: várjuk
}
57
Listák
És most tesztelhetjük az add() metódust. Bár elég egyszerűen lehet a lista végéhez elemet hozzáadni az insert() metódus segítségéve!, sokkal természetesebb (és kevesebb kódolással is jár), ha ezt egy speciális metódussal tesszük.
public void testAdd() { List list= createList();
}
list.add(VALUE_A); list.add(VALUE_C); list.add(VALUE_B);
assertEquals(3, list.size()); assertsame(VALUE_A, list.get(O)); assertsame(VALUE_C, list.get(l)); assertsame(VALUE_B, list.get(2));
A megvalósitás működése
A testrnsertrntoAnEmptyL i st() metódus egyszerűen csak ellenőrzi, hogy ha egy üres listába beszúrunk egy elemet, akkor a lista hossza eggyel nő, és a várt helyről visszakapjuk a beszúrt értéket.
A testrnsertBetweenEl emen ts() metódus azt ellenőrzi, mi történik akkor, ha két elem közé szeretnénk beilleszteni egy értéket. A teszt egy kételemű listából indul ki: az A és B értékek rendre a O és l pozícióban találhatók, amint a 3.1. ábrán látható.
Index: O Index: 1
3.1. ábra. A beszúrás előtti lista
Ezután beszúr közéjük egy újabb (c) értéket az 1 pozícióba. Az új értéknek az A és a B érték közé kell kerülnie, így a 3.2. ábrán látható listához jutunk.
Index: O Index: 1 Index: 2
3.2. ábra. A lista a két elem kiizé beszúrás t1fán
Mint látható, a B egy hellyel jobbra tolódott, hogy helyet adjon a c értéknek A testrnsertBeforeFi rstElement() metódus biztosítja, hogy ha az első helyre
illesztünk be elemet, akkor az összes létező elem egy hellyel jobbra tolódjon. A teszt az i n se rt() meghívásakor minden alkalommal ugyanazt a beszúrási pontot- a O pozíciót - használja és ellenőrzi, hogy az értékek a helyes sorrendben szerepeljenek: az A
elem a O pozícióból indul, majd eggyel jobbra tolódik, hogy a B értéknek helyet adjon.
58
A listák tesztelése
A testinsertAfterLastEl ement() metódus biztosí*, hogy a lista végéhez is tud
junk hozzáadni úgy, hogy az utolsó érvényes pozíciónál eggyel nagyobb szám ú helyre il
lesztünk be elemet. Ha a listában egyetlen elem van, az 1 pozícióba való beszúrás a lista
végére helyezi az új értéket. Ha a listában három elem van, a 3 pozícióba való beszúrás
a lista végére helyezi az új értéket. Más szóval a lista végéhez úgy adhatunk hozzá ele
met, hogy a lista mérete által meghatározott pozícióba illesztjük be az új értéket.
A testinsertoutOfBounds() metódus ellenőrzi, hogy a lista helyesen állapít-e
meg olyan általános programozási hibákat, mint például negatív beszúrási pont vagy
a lista méreténél nagyobb beszúrási pont használata (a lista méretével megegyező be
szúrási ponttal a lista végéhez adhatunk hozzá elemet). A kód tesztelése üres listával
indul, vagyis az első hely - a O pozíció - az egyetlen, ahová új értéket lehet beszúrni.
Minden olyan kísérletnek, amelyben negatív vagy nullánál nagyobb értéket haszná
lunk, IndexoutOfBoundsExcepti on kivételt kell eredményeznie.
Végül a testAdd O metódus ellenőrzi az add() kényelmi módszer viselkedését.
Három értéket adunk hozzá a listához, majd ellenőrizzük, hogy a helyes sorrendben
vannak-e. Mint a testAdd O metódus testinsertAfterLastElement() metódushoz
viszonyított egyszerűségéből látszik, ha külön metódust hozunk létre a lista végéhez
való hozzáadásra, a kód sokkal olvashatóbbá válik, és valarnivel kevesebb kódolása
is van szükségünk. De ami fontosabb: kevesebb fejtörést igényel, hogy jól csináljuk.
Az add() meghívása sokkal egyszerűbb, mint az insert() meghívása a si ze() érté
két mint beszúrási pontot átadva.
Gyakorlófeladat: tesztmetódus létrehozása értékek visszakeresésére és tárolására
Ha már be tudunk szúrni elemeket a listába, következő feladatunk ezek elérése.
Nagyrészt már kipróbáltuk a get() (és tulajdonképpen a si ze() és az i sEmpty()) viselkedését, arnikor az i n se rt() és add() metódusokat ellenőriztük, tehát most a
set() tesztelésével kezdjük:
pul:il icVörti-test"Set:o- { List list � createList();
.J
list.insert(O, VALUE-A); assertSame(VALUE-A, list.get(O));
assertsame(VALUE_A, list.set(O, VALUE_B)); assertsame(VALUE_B, list.get(O));
Egy másik dolog, amit még nem teszteltünk, a korlátfeltételek: rni történik, ha egy
listát az első elem előtt vagy az utolsó mögött akarunk elérni? Mint az i n se rt() ese
tében is, a lista határain kívüli elérési kísérletnek IndexoutOfBoundsExcepti on kivé
telt kell eredményeznie:
59
Listák
public void testGetoutOfBounds() { List list= createList();
}
try { list.get(-1); failO;
} catch (IndexoutOfBoundsException e) { ll ezt várjuk
}
try { list.get(O); fai l O;
} catch (IndexoutOfBoundsException e) { ll ezt várjuk
}
list.add(VALUE_A);
try { list.get(1); fai l O;
} catch (IndexOUtOfBoundsException e) { ll ezt várjuk
}
A set() meghívásakor is tesztelhetünk bizonyos peremfel tételeket:
60
public void testsetoutofsounds() { List list= createList();
try { list.set(-1, VALUE_A); failO;
} catch (IndexoutofsoundsException e) { ll ezt várjuk
}
try { list.set(O, VALUE_B); fai l O;
} catch (IndexoutOfBoundsException e) { ll ezt várjuk
}
list.insert(O, VALUE_C);
A listák tesztelése
try{ - ---�---.... .-.-------� .,.. - - -- --·
list.set(l, VALUE_C); fai l();
} catch (IndexoutofsoundsException e) { ll ezt várjuk
}
A megvalósitás müködése
A set() metódus nagyjából ugyanúgy műköclik, mint egy tömbelem értékének beál
lításakor, vagyis miután feltöltöttük a listát ismert értékekkel, a testset () lecseréli
és biztosítja, hogy az új értékeket kapjuk vissza az eredetiek helyett.
A testGetoutofBounds () metódus üres listával indul, majd egy negatív és egy
túl nagy pozícióval próbálja meg elérni a listát. Majd biztos, atni biztos, egy elemmel
bővíti, és ismét megpróbálja elérni a végpontjain kívül. Minden esetben rndexoutofsoundsExcepti on kivétel dobását várjuk.
A testsetoutOfBounds() metódus alapjában véve megegyezik a testGetOutofBounds() metódussal, csak ahelyett, hogy egy értéket akarnánk visszakapni, a set() meghívásával megpróbáljuk módosítani az értékét.
Gyakorlófeladat: tesztmetódus létrehozása értékek törlésére
Az első törléstípus, amelyet kipróbálunk, a lista egyeden elemét törli. Azt várjuk,
hogy törlés után a lista üres legyen:
putil1c void TeStoeleteonlyElement() �{ List list = createList();
}
list.add(VALUE_A);
assertEquals(l, list.size()); assertsame(VALUE_A, list.get(O));
assertsame(VALUE_A, list·.delete(O));
assertEquals(O, list.size());
Azt is szetetnénk látni, hogy mi történik egy többelemű lista első elemének törlése
kor. Minden értéknek el kell tolódnia egy hellyel balra:
pubric vofi:l testoeleteFi rstElementé){ List list = createList();
list.add(VALUE_A); list.add(VALUE_B); l i st. add(VALUE_C.�) !..; --------
61
Listák
}
assertEquals(3, list.size()); assertsame(VALUE_A, list.get(O)); assertsame(VALUE_B, list.get(l)); assertsame(VALUE_C, list.get(2));
assertsame(VALUE_A, list.delete(O));
assertEquals(2, list.size()); assertsame(VALUE_B, list.get(O)); assertsame(VALUE_C, list.get(l));
Most nézzük, mi történik egy többelemű lista utolsó elemének törlésekor:
public void testDeleteLastElement() { List list = createList();
}
list.add(VALUE_A); list.add(VALUE_B); list.add(VALUE_C);
assertEquals(3, list.size()); assertSame(VALUE_A, list.get(O)); assertsame(VALUE_B, list.get(l)); assertsame(VALUE_C, list.get(2));
assertsame(VALUE_C, list.delete(2));
assertEquals(2, list.size()); assertsame(VALUE_A, list.get(O)); assertsame(VALUE_B, list.get(l));
Ezúttal két másik elem közötti érték törlését vizsgáljuk. Minden jobbra lévő elemnek
egy hellyel balra kell tolódnia.
62
public void testoeleteMiddleElement() { List list= createList();
list.add(VALUE_A); list.add(VALUE_C); list.add(VALUE_B);
assertEquals(3, list.size()); assertsame(VALUE_A, list.gét(O)); assertsame(VALUE_C, list.get(l)); assertsame(VALUE_B l i st. g, _!:e :.!:t�(2�)t__. )u;._ _____________ _,
}
assertsame(VALUE:C; ·rnst:-:-del ete(l));
assertEquals(2, list.size()); . assertsame(VALUE_A, list.get(O)); assertsame(VALUE_B, list.get(l));
A l is ták tesztelése
Biztosítanunk kell azt is, hogy a lista határain kívül lévő elem törlési kísérlete IndexOutOfBoundsExcepti on kivételt dobjon.
�publ.i c void testoe l eteoutofsounds O-{
List list= createList();
try { list.delete(-1); fai l O;
} catch (IndexoutOfBoundsException e) { ll ezt várjuk
}
try { list.delete(O); fai l O;
} catch (IndexoutofBoundsException e) {
ll ezt várjuk }
Már megnéztük, mi történik akkor, amikor pozíció alapján törlünk, de mi a helyzet
az érték szerinti törléssel? Az érték szerinti törlés nem olyan egyértelmű, mint az in
dex szerinti - mint tudjuk, egy lista többször is tartalmazhatja ugyanazt az értéket,
így még arra is figyelnünk kell, hogy többszörös előfordulás esetén a törlés minden
egyes meghíváskor csupán az adott érték első előfordulását törölje.
pub l i c -voi d -tes to e l eteByVa l u e()"-{ List list= createList();
list.add(VALUE_A); list.add(VALUE_B); list.add(VALUE_A);
assertEquals(3, list.size()); assertsame(VALUE_A, list.get(O)); assertsame(VALUE_B, list.get(l)); assertSame(VALUE_A, list.get(2));
assertTrue Cl i st. de l �li{VALUE.,A).L.):.L;-------.�-----....1
63
Listák
}
asser.tEquáls(2; list.sizeOY :.
assertsame(VALUE_B, list.get(O)); assertsame(VALUE_A, list.get(l));
assertTrue(list.delete(VALUE_A));
assertEquals(l, list.size()); assertsame(VALUE_B, list.get(O));
assertFalse(list.delete(VALUE_C));
assertEquals(l, list.size()); assertsame(VALUE_B, list.get(O));
assertTrue(list.delete(VALUE_B));
assertEquals(O, list.size());
A megvalósitás működése
Az első négy teszt egy meghatározott érték törlésének alapfeladatát végzi. A törlés a
beszúrás inverze, így tehát feltételezhetjük, hogy egy elem törlésekor a lista mérete
eggyel csökken, és hogy a törölt elemtól jobbra található elemek egy hellyel balra to
lódnak. Az "index szerinti törlés" definíciója azt is magában foglalja, hogy az aktuá
lisan törölt elem értékével kell visszatérnie, tehát ezt is ellenőrizzük.
A testDeleteoutOfBounds() metódus- mint minden határellenőrző metódus
érvénytelen pozícióban próbálja meg elérni a listát: először egy negatív, majd egy túl nagy pozícióval próbálkozik. Minden alkalommal IndexoutofBoundsExcepti on kivé
tel dobását várjuk, amely az alkalmazás programozási hibáját jelzi.
A testDel eteByVa l u e() metódus biztosítja, hogy tudjunk törölni olyan eleme
ket, amelyeknek nem ismerjük a pontos helyét a listában. A teszt három értéket szúr
be a listába, amelyek közül kettő egymás duplikátuma. Ezután a kérszerezett értékek
egyikét eltávolítja és ellenőrzi, hogy a másik benne marad-e a listában. Ezután
ugyanezt az értéket használva a másodikat is törli a listábóL Ezután egy nem ·létező
értéket próbál meg törölni. Ez semmilyen hatással sem lehet a listára. Végül törli az
utolsó ismert megmaradt értéket, így kiürül a lista. Minden alkalommal ellenőrizzük,
hogy a deleteáltal visszaadott érték helyes-e: az ismert érték törlésének true, míg a
nem létező elem törlésének fal se értékkel kell visszatérnie.
Gyakorlófeladat: az jteráció tesztelése
A listák megvalósításának egyik legnehezebb eleme az iteráció. Emlékezzünk vissza,
hogy a L i st interfész a (2. fejezetben bemutatott) Iter ab l e interfész kibővítése,
amelyhez megfelelő megvalósítás szükséges, hogy az iterátor végigmenjen a tartalmon.
64
A listák tesztelése
Három általános helyzetet kell tesztelnünk iterátor üres listára, előre iterálás a
kezdőelemtől és visszafelé iterálás a legutolsó elem től.
Kezdjük a tesztelést az üres lista iterátorának viselkedésével:
public void test:Emptyrteration()-{ List: list ; createList();
}
It:erator iterat:or ; list:.it:erat:or();
assert:True(it:erat:or.isoone());
try { iterat:or.current:(); fai l O;
} catch (It:eratorout:OfBoundsException e) { ll ezt várjuk
}
Most a lista első elemétől induló előrehaladó iterációt teszteljük:
publ1C:Void testForwardit:erat:ion()-{ List list= createList:();
_}
list.add(VALUE_A); list.add(VALUE_B); list.add(VALUE_C);
Iterator iterator = list.iterat:or();
it�rator.first:(); assertFalse(iterator.isoone()); assertsame(VALUE_A, it:erator.current());
iterator.next(); assert:False(iterator.isoone()); assertsame(VALUE_B, iterator.current());
iterator.next(); assert:False(iterator.isoone()); assert:Same(VALUE_C, it:erat:or. current());
i terator. next(); assertTrue(iterat:or.isoone()); try {
iterat:or.current:(); fail();
} catch (Iterat:orout:OfBoundsException e) { ll ezt várjuk
}
65
Listák
Végül teszteljük a lista utolsó elemétől visszafelé haladó iterátort:
public void testReverseiteration() { List list= createList();
}
list.add(VALUE_A); list.add(VALUE_B); list.add(VALUE_C);
Iterator iterator list. iterator();
iterator.last(); assertFalse(iterator.isoone()); assertsame(VALUE_C, iterator.current());
iterator.previous(); assertFalse(iterator. i soon e()); assertsame(VALUE_B, iterator.current());
iterator.previous(); assertFalse(iterator.isoone()); assertsame(VALUE_A, iterator.current());
iterator.previous(); assertTrue(iterator.isoone()); try {
iterator.current(); fai l O;
} catch (IteratorQutofsoundsException e) { ll ezt várjuk
}
A megvalósitás müködése
Amikor az iterátort üres listára futta�uk, azt várjuk, hogy az i soon e visszatérési értéke mindig true legyen, jelezve, hogy már nincs több elem.
A testFrirwardrteration() metódus egy három értéket tartalmazó listát hoz létre, és megad egy iterátort. Ezután a first() metódus meghívásával elindul a lista első elemétő� majd egymás után meghívja a next() és a current() metódusokat annak ellenőrzésére, hogy a visszaadott értékek a várt sorrendben vannak-e. Az i soone() metódusnak csak akkor szabad true értékkel visszatérnie, ha már minden elemet bej ártunk.
A visszafelé haladó iteráció tesztelése ugyanazokat a lépéseket hajtja végre, rnint az előre haladó iteráció tesztelése, csak a lista legutolsó elemétől indul, és hátrafelé halad a previous() meghívásával a next() helyén.
Minden esetben, amikor az iterátor végzett- az i soone() true értékkel tér viszsza-, a current() meghívásával megpróbáljuk elérni az iterátort. Ennek Iterator
outOfBoundsExcepti on kivételt kell dobnia.
66
Gyakorlófeladat: tesztmetódus létrehozása értékek megtalálására
A listák tesztelése
A listák az i ndexof O és a conta i ns O metódusok segítségével teszik lehetővé az
elemek keresését.
Az indexofO metódus az érték helyét (O, l, 2 ... ) adja vissza, ha megtalálta az
elemet, illetve a -l értéket, ha nem. Abban az esetben, ha a lista kétszerezett értéke
ket tartalmaz, az i ndexofO mindig csak az első előfordulás helyét adhatja vissza.
public···
void testrndexofO List list = createList();
list.add(VALUE_A); list.add(VALUE_B); list.add(VALUE_A);
assertEquals(O, list.indexof(VALUE_A)); assertEquals(l, list.indexof(VALUE_B)); assertEquals(-1, list.indexof(VALUE_C));
}'------------------------------�--------�
A con ta i ns O metódus visszatérési értéke t ru e, ha megtalálta a keresett értéket,
egyébként fa l se:
public voia testcontains()-
{ List list= createList();
}
list.add(VALUE_A); list.add(VALUE_B); list.add(VALUE_A);
assertTrue(list.contains(VALUE_A)); assertTrue(list.contains(VALUE_B)); assertFalse(list.contains(VALUE_C));
A megvalósitás müködése
Mindkét teszt három értéket vesz fel a listába, amelyek közül az egyik duplikátum.
A testrndexofO metódus ekkor ellenőrzi, hogy a helyes pozíciókat kapjuk-e
vissza a létező A és B értékekre, illetve -l-et a nem létező c értékre. A kétszerezett
érték esetén az első előfordulás helyét kell megkapnunk
A testeonta i nsO metódus ellenőrzi, hogy a contai nsO létező értékekre true,
a nem létezőkre fa l se értékkel tér-e vissza.
67
Listák
Gyakorlófeladat: mi történik a lista elemeinek törlésekor
Végül, de nem utolsósorban nézzük, rni történik, ha a listát a cl ear() meghívásával alaphelyzetbe álli�uk. A listának üresnek kell lennie, és méretének vissza kell állnia O-ra:
public void testclear() { List list = createList();
}
list.add(VALUE_A); list.add(VALUE_B); list.add(VALUE_C);
assertFalse(list.isEmpty()); assertEquals(3, list.size());
list.clear();
assertTrue(list.isEmpty()); assertEquals(O, list.size());
A megvalósitás müködése
A testel e ar() metódus három értéket vesz fel a listába, meghívja a clear metódust, majd meggyőződik arról, hogy a listában egyetlen érték sem maradt.
Listák megvalósítása
Mostanra már teljesen tisztában kell lennünk a listafunkciókkal. Miután a tesztekkel mintegy törvénybe foglaltuk a várt működést, könnyedén ellenőrizhetjük, hogy megvalósításaink az elvárásnak megfelelően működnek-e. És most belevethe�ük magunkat a jól kiérdemelt alkotó kódolásba.
A listákat sokféleképpen megvalósíthatjuk, de a két legelterjedtebb, amelyet be is mutatunk, a tömb alapú megvalósítás és az úgynevezett láncolt lista. Mint a név is sugallja, a tömblista egy tömbben tárolja az értékeket. A láncolt lista viszont az elemek sorozata, amelyben minden elem hivatkozik (vagy kapcsolódik) a következő (és opcionálisan az előző) elemre.
A legegyszerűbb esettel fogjuk kezdeni, a tömblistával, majd áttérünk a sokkal kifinomultabb láncolt listára. Mindkettőnek vannak olyan tulajdonságai, amelyek az alkalmazásunk követelményeitől függően többé-kevésbé hasznossá teszik őket. Éppen ezért mindkettőre vonatkozóan megvizsgáljuk a speciilisan mellette és ellene szóló érveket, kódmagyarázattal együtt.
68
Listák megvalósítása
Minden esetben teszünk kikötéseket a listában tárolható adattípusokra. Először
is, nem engedélyezzük nulla értékek tárolását a listában. Azzal, hogy kizárjuk a nulla
értékeket, nagyban egyszerűsödik a kódunk; így szükségtelenné válik egy sor olyan
korlátfeltétel, amely általában a nulla értékek kezelése esetén fellép. Emiatt a meg
szorítás miatt nem kell különösebben aggódnunk, mivel a legtöbb üzleti alkalmazás
ban a listák szinte soha nem tartalmaznak nulla értékeket.
A tömblista
Mint a név is sugallja, a tömblista egy tömbben tárolja az értékeket. Ezért az a tény,
hogy a tömbök közvetlenül indexelhetók, szinte triviálissá teszik az elemekhez való
hozzáférés megvalósítását. Ez teszi a tömblistát az indexelt és szekvenciális hozzáfé
rés leggyorsabb megvalósításává is.
A tömblista hátránya az, hogy minden új elem beszúrásakor a listában eggyel
magasabb helyre kerülő elemeket fizikailag egy pozícióval jobbra kell másolnunk
Ehhez hasonlóan a meglévő elemek törlésekor a törölt elemnél magasabb pozíció
ban lévő elemeket ugyanígy egy pozícióval balra kell tolnunk, hogy betöltsük a törölt
elem hagyta hiányt.
Ráadásul, mivel a tömbök fix méretűek, ha a lista méretét növelnünk kell, min
den esetben újra le kell foglalnunk az új tömböt, és át kell másolnunk az összes ele
met. Ez egyértelműen befolyásolja a beszúrás és a törlés teljesítményét. Ugyanakkor
a legtöbb esetben a tömblista jó kiindulási pont lehet olyan esetekre, amikor az egy
szerű tömböktól el kell rugaszkodnunk a listákhoz hasonló, gazdagabb adatstruktú
rák használata felé.
Gyakorlófeladat: a tesztosztály létrehozása
Mindenekelótt definiálnunk kell azokat a teszteseteket, amelyekkel meggyóződünk
megvalósításunk helyességérőL Először hozzuk létre az ArrayListTest nevű osz
tályt, amely a korábban létrehozott Abstract L istTestcase osztály kiterjesztése:
packageC'Oiii":Wrox.algorithms.l'ists;
public class ArrayListTest extends AbstractListTestcase { protected List createList() {
return new ArrayList(); }
public void testResizeBeyondinitialcapacity() { List list = new ArrayList(l);
list.add(VALUE_A); list.add(VALUE_A);
L. li S_!:.a,9"cjj'{AL�f::_A=C,)!.J·,_ ______________ �-----'
69
Listák
}
assertEquals(3, list.size());
assertsame(VALUE_A, list.get(O)); assertSame(VALUE_A, list.get(l));
assertSame(VALUE_A, list.get(2)); }
public void testoeleteFromLastElementinArray() {
List list = new ArrayLíst(l);
líst.add(new object());
list.delete(O);
}
A megvalósitás müködése
A munka nehezét már elvégeztük, amikor létrehoztuk az Abstract:L istTestcase
osztályt. Mivel ezt az osztályt terjesztettük ki, örököltük az összes tesztet. Ezért
egyedül a createL í st() metódust kellett megvalósítanunk, amely a tesztek által
használtArrayList osztály egy példányával tér vissza. A tömblisták belső működé
séből adódóan a standard teszteken kívül még néhány másikra is szükségünk lesz.
Az első metódus, a testResizeBeyondinitíalCapacity() azért szükséges,
mert ahogyan a tömblista mérete nő, az alapul szolgáló tömböt át kell méretezni,
hogy legyen hely az új elemek számára. Ilyen esetekben biztosak akarunk lenni ab
ban, hogy a másolás megfelelően történik. A teszt egy olyan tömblista létrehozásával
kezdődik, amelynek kezdeti kapacitása egy. Ezután három elemet szúrunk be. Ez az
alapul szolgáló tömb méretét is megfelelően növeli. Eredményképpen az eredeti
tömb elemei átmásolódnak az új, nagyobb tömbbe. Ezután a teszt ellenőrzi, hogy a
méret és a tartalom helyesen lett-e átmásolva.
Mint a neve is sugallja, a második tesztmetódus, a testDel eteFromLastEl ement
InArray() ellenőrzi, rni történik a lista utolsó elemének törlésekor. Ahogy később a
kódrészletben látni fogjuk, nem megfelelő kezelés esetén ez a korlátfeltétel Array
Indexoutofsounds Ex ce pti ons kivételhez vezethet.
Gyakorlófeladat: az ArrayList osztály létrehozása
Most, hogy a tesztesetek már elkészültek, biztonságosan rátérhetünk a tömblista
megvalósításának elkészítésére. Kezdjük az ArrayList osztály létrehozásával az itt
bemutatott módon:
70
package com.wrox.algorit:hms.lists;
import com.wrox.algorithms.iteration.Arrayrterator; import com.wrox.algorithms.iteration.rterator;
public class ArrayList implements List {
Listák megvalósítása
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private final int _initialcapacity; private object[] _array; private int _size;
public ArrayList() { this(DEFAULT_INITIAL_CAPACITY);
}
public ArrayList(int initialcapacity) { assert initialcapacity > O
"az i nit: i a l capaci ty köte l ezően poz i t í v";
l
}
_initialcapacity = initialcapacity; clear();
public void clear() {
}
_array= new object[_initialcapacity]; _size = O;
A megvalósitás működése
Maga az osztály elég egyszerű. Mindössze néhány mezőre van szükség, és természetesen a L i st interfész megvalósítására. Már létrehoztuk az elemek tömbjét tároló mezőt, valamint egy külön mezőt a lista méretének tárolásira is. Ne feledjük, hogy a lista mérete nem mindig egyezik meg a tömb méretével: a tömb végén szinte mindig lesznek "tartalék" helyek, tehát a tömb hossza nem feltétlenül lesz azonos a listában tárolt elemek számával.
Két konstruktorunk is van. Az első lényegében kényelmi megoldás: a másodikat hívja meg adott alapértelmezett értékekkel. A második konstruktor egyetlen argumentuma a kezdeti tömb mérete, amely érvényesítődik és mentődik, mielőtt a cl ear()
meghívásával inicializálnánk az elemek tömb jét, és alaphelyzetbe állitanánk a lista méretét. (Technikailag megengedhető lenne a O érték, de akkor át kellene méretezni a tömböt még az első elem beszúrása előtt. Ehelyett arra kényszerítjük a meghívót, hogy legalább 1 legyen az átadott érték.)
71
Listák
Gyakorlófeladat: metódus létrehozása értékek beszúrására és hozzáadására
Az elsőként megvalósított metódus a megadott pozícióba szúr be egy értéket:
public void insert(int index, object value) throws rndexoutofsoundsException {
}
assert value != null : "az érték nem lehet NULL";
if (index < O ll index > _size) { throw new rndexoutofsoundsException();
}
ensurecapacity(_size +l); System.arraycopy(_array, index,
_array, index + l, _size - index);
_array[index] = value; ++_size;
private void ensurecapacity(int capacity) { assert capacity > O :
"a capacity (kapacitás) kötelezően pozitív";
if (_array.length < capacity) {
}
Object[] copy= new Object[capacity + capacity l 2];
System.arraycopy(_array, 0, copy, 0, _size); _array = copy;
}------------------------------------------------�
Ha már be tudunk szúrni egy értéket, a lista végére való beszúrás ebből természete
sen adódik:
public void add(Object value) { insert(_size, value);
}�------------------------------------------------
A megvalósitás müködése
Az insert() metódus a bemenet érvényesítésével indul. Először is ellenőrizni kell a
null értéket, mivel ezek explicit módon meg vannak tiltva. Másodszor, mint talán a
tesztesetekből rémlik, az insert() metódusnak IndexoutofsoundsException kivé
telt kell dobnia, ha az első elem elé vagy egynél több pozícióval az utolsó elem mögé
próbálunk meg elemet beszúrni.
72
Listák megvalósítása
Ezután, mivel a tömbök mérete rögzített, a listáké viszont nem, biztosítanunk kell, hogy az alapul szolgáló tömbben legyen elég hely az új érték tárolására. Van például egy öt hosszúságú tömbünk, amihez hozzá szeretnénk adni a hatodik elemet. Egyértelmű, hogy a tömbben nincs elég hely, de nem is fog varázsütésre kibővülni; vagyis az ensurecapacity() metódus meghívása biztosítja, hogy legyen elég hely a tömbben az új érték befogadására. Ha az ens u recapaci ty() meghívása viszszatér, akkor tudjuk, hogy már van elég helyünk, és biztonságosan eltolhatjuk a meglévő elemeket a listában egy pozícióval jobbra, hogy helyet csináljunk az új érték számára. Végül elmen�ük az értéket a megfelelő helyre, nem feledkezve meg a lista méretének növeléséről sem.
Az ensurecapacity() metódus kezeli az alapul szolgáló tömb dinamikus átméretezését is. Amikor úgy érzékeli, hogy az alaptömb túl kicsi, új tömböt foglal le, átmásolja az elemeket, és elengedi a régi tömböt, amely ezáltal szabaddá válik a szemétgyűjtés számára. Rengeteg különféle stratégia alkalmazható annak eldöntésére, hogy mikor és mekkora méretben foglaljuk le az új tömböt; ebben a konkrét példában a ténylegesen szükségesnél 50 százalékkal nagyobb értékkel növeljük a tömb méretét. Ez egyfajta védőhálót nyújt és biztosítja, hogy a listának ne kelljen folyton új tömböket lefoglalgatnia és elemeket áttnásolgatnia.
Az add() metódus egyszerűen csak delegálja a feladatot az i n se rt() metódusnak, a lista méretét mint beszúrási pontot átadva; ezáltal az új érték biztosan a lista végére lesz beszúrva ..
Gyakorlófeladat: metódus létrehozása értékek hely szerinti beszúrására és visszakeresésére
Most létrehozzuk az elemek tárolására és visszakeresésére szolgáló get() és set()
metódust. Mivel az aktuális megvalósításunk a tömbökre épül, a tárolt elemekhez való hozzáférés szinte triviális:
" "PuiiiTé obfect get(int index) throws IndéXOutöfsoundsEXC:ej:itioii -{ checkoutofsounds(index); return _array[index];
}
public object set(int index, object value)
l
throws IndexoutofsoundsException { assert value != null : "az érték nem lehet NULL"; checkoutofsounds(index); object oldvalue = _array[index]; _array[index] = value; return oldvalue;
73
Listák
private void checkoutofsounds(int index) { if (isoutofsounds(index)) {
throw new IndexoutofsoundsExcepti.on();
} }
private boolean isoutofsounds(int index) { return index < O l l index >= _size;
}
A megvalósitás működése
Miután első lépésben ellenőriztük a kért pozíció érvényességét, a get() metódus
visszaadja az adott indexhez tartozó értéket, rníg a set() lecseréli. Ezenfelül a set()
másolatot készít az adott pozícióban eredetileg tárolt értékről, mielőtt felülírná. Ez
után az eredeti értékeket átadja a lúvónak.
Mint láthatjuk, a tömblista kifejezetten jól működik indexelt hozzáférés esetén.
Tulajdonképpen rníg az indexelt lista általában 0(1) elérésűnek tekinthető, azonos
tipikus esetbeli legjobb, legrosszabb és átlagos teljesítményével a tömblista közeliti
meg a lehető legjobban ezt az értéket.
Gyakorlófeladat: értékek megtalálása
Mint a get() és a set() bemutatásában jeleztük, a listák ideálisak ismert pozíciójú
elemek tárolására. Ez teszi őket tökéletessé egyes rendezés- Q.ásd 6. és 7. fejezet) és ke
reséstípusokhoz Q.ásd 9. fejezet). Ha viszont egy rendezetlen listában akarjuk meghatá
rozni egy elem helyét, akkor be kell érnünk a viszonylag durva, de egyértelmű lineáris
keresés módszerével. Az i ndexof() metódus lehetővé teszi egy meghatározott érték
helyének megkeresését a listában. Ha megtalálta az értéket, akkor a pozíciójával tér
vissza, egyéb esetekben a -1 visszatérési érték jelzi, hogy a keresett érték nem létezik.
public int indexof(ol:iject vaíue) {
}
assert value != null : "az érték nem lehet NULL";
for (int i = O; i < _size; ++i) { if (value.equals(_array[i])) {
return i;
} }
return -1;
Most, hogy már tudunk keresni a listában az i ndexof() segítségéve!, áttérhetünk a
cont a i n s () megvalósítás ára:
74
publicboolean con ta i ns (Öoject"'Val'Ue)- { return indexof(value) != -1;
}
A megvalósitás müködése
Listák megvalósítása
A2 i ndexof () metódus lineáris keresést hajt végre a listán, hogy megtalálja a kere
sett értéket. Céljának megvalósítását a lista első elemétől kezdi, és elemről elemre ha
lad, míg el nem éri a keresett elemet vagy a lista végét.
A contai ns () metódus az i ndexof() metódust hívja meg, hogy elvégezze helyet
te a keresést, és csak akkor tér vissza true értékkel, ha megtalálta (i ndexof () >=0). Bár egyszerűen megvalósítható, a lineáris keresés nem túl szerenesés nagy listák
esetén. Képzeljünk el egy listát a következő értékekkel: macska, kutya, egér, zebra. Most tegyük fel, hogy egymás után núnden elemet meg akarunk keresni (először a
macska, majd a kutya stb.), és núnden esetben meg is számoljuk az adott érték meg
találásához szükséges összehasonlítások számát. A macska, mivel a lista első eleme,
egyeden összehasonlítást igényel. A kutya kettőt, az egér hármat, míg a zebra né
gyet. Ha kiszámoljuk az ádagos szükséges összehasonlítás-számot (1+2+3+4)/4 =
10/4 = 2, 5), láthatjuk, hogy egy N elemet tartalmazó listára az ádagos szükséges
összehasonlítás-szám N/2 vagy O (N) körül van. Ez megegyezik a legrosszabb esetbeli
idővel, tehát egyértelmű, hogy ez nem valami hatékony keresési mód.
A Bináris keresés című fejezet (9.) egy sokkal hatékonyabb keresési módszert
mutat be a listákra, de egyelőre az a letámadásos keresési mód is megteszi.
Gyakorlófeladat: értékek törtése
A l i st interfész két metódust biztosít értékek törlésére. Az elsővel pozíció alapján
törölhetjük az értékeket:
public ob-ject delete(int index)
l
throws IndexoutOfBoundsException { checkoutOfBounds (index); object value = _array[index]; int copyFromindex = index + l; if (copyFromindex < _size) {
System.arraycopy(_array, copyFromindex, _array, index,
} _array[ --_size] return value;
_size - copyFromindex);
null;
Támogatnunk kell az olyan értékek törlését is, amelyeknek nem ismerjük a pontos
pozícióját. Mint a con ta i ns () esetében, most is kihasználhatjuk azt a tényt, hogy az
i ndexof ()alkalmazásával van már egy· módszerünk egy adott érték pozíciójának
meghatározására:
75
Listák
public boolean delete(ob)éct value) { int index = indexof(value); if (index != -1) {
delete(index); return true;
} return false;
}
A megvalósitás müködése
Miután először ellenőriztük a bemenet érvényességét, az első de l e te() metódus a törlési ponttól jobbra eső összes elemet egy pozícióval balra másolja. Ezután a lista mérete is megfelelőerr csökken, és a tömb utolsó elemében tárolt érték is törlődik.
Azért kell a lista utolsó értékét törölnünk, mert ténylegesen nem helyeztük egy pozícióval balra az értékeket, csupán másoltuk őket. Ha nem törölnénk ki a korábbi utolsó elem értékét, akkor figyelmetlenségből törölt értékek példányait tartogatnánk, ezért azok nem lennének elérhetők a szemétgyűjtés számára. Ezt nevezzük memórias�vá?gámak.
Figyeljük meg a határellenőrzést, amellyel biztosítjuk, hogy a tömb utolsó elemének törlésekor ne okozzunk ArrayrndexoutofBoundsException kivételt. Tulajdonképpen ki is próbálhatj uk, mi történik, ha az if utasítás alatti teljes kódrészletet kikommentezzük, és újrafuttatjuk a tesztet. Vegyük észre azt is, hogy gondosan elmentettük a törölt pozícióban tárolt értéket, hogy vissza tudjuk adni a meghívónak
Érdemes megjegyeznünk, hogy az alaptömb mérete sohasem csökken. Ez azt jelenti, hogy ha a lista mérete nagyon nagyra nő, majd jelentősen lecsökken, akkor nagyon sok "elpazarolt" tárolónk lesz. Ezt a problémát megkerülhe�ük, ha megvalósítjuk az ensurecapaci ty() inverzét. Minden elemtörléskor összevethetjük a lista új méretét egy adott százalékos küszöbértékkel. Ha például a lista mérete a tárolókapacitás 50 százalékánál kisebbre csökken, akkor lefoglalhatunk egy kisebb tömböt, és átmásolhatjuk abba a tartalmat, így felszabadítva a használaton kívüli tárat. Csak az egyértelműség kedvéért: mi ezt nem tettük meg.
Mellékesen megjegyzem, hogy az ArrayList JDK-megvalósításának kócjja pontosan
ugyanígy viselkedik. Ez megint o!Jasmi, ami miatt tiibbt!Jire nem kell aggódnunk, vi
szont érdemes észben tartani.
A második de l e te O úgy működik, hogy az i ndexof () meghívásával meghatározza az adott érték első előfordulásának helyét, és ha megtalálja, meghívja az első de l e te O metódust. Az első de l e te() metódus teljesítménye - az értékek másolására fordított időt leszámítva - o (l), míg a második de l e te O önmagából fakadóan az indexofO teljesítményéhez van kötve, így átlagos törlési ideje O(N).
76
Listák megvalósítása
Gyakorlófeladat: az interfész befejezése
Majdnem végeztünk is a teljes L i st interfész megvalósításávaL Már csak néhány me
tódus van hátra:
public Iterator iter.itor()-{ return new Arrayiterator(_array, O, _size);
}
public int size() { return _size;
}
public boolean isEmpty() { return size() == O;
} __ _____J
A megvalósitás müködése
Az i terator() metódus nagyon egyszerű: a szükséges kódunk már megvan a 2. fe
jezetben bemutatott Arrayiterater osztály képében.
A si ze() metódus megvalósítása még ennél is egyszerűbb. Az i n se rt() és a
de l e te() metódusok tárolják a lista méretét, tehát egyszerűen csak vissza kell ad
nunk a _si ze mezőben aktuálisan tárolt értéket,
Végül az i sEmpty() visszatérési értéke csak akkor igaz, ha a lista mérete nulla
(size() == O). Bár a megvalósítása triviális, az isEmpty()- a List interfész többi
kényelmi metódusához hasonlóan - a "zaj" csökkentésével könnyebben olvashatóvá
teszi az alkalmazás kódját.
Láncolt lista
A láncolt lista nem tömböt használ az elemek tárolására, hanem a közöttük lévő
kapcsolatokkal együtt tartalmazza az egyes elemeket. Mint a 3.3. ábrán látható, a lán
colt lista minden egyes eleme tartalmaz egy hivatkozást (vagy kapcsolódást) a követ
kező és az előző elemre is, így mintegy láncot alkotva.
Index: O Index: 1 Index: 2
� Next � Next � U+- Previous � Previous � 3,3, ábra. A duplán láncolt lista elemei mindkét irátryban tartalmaznak hivatkozásokat.
77
Listák
Pontosabban ezt duplán láncolt lista (minden elemnek két kapcsolódása van) néven
emlegetjük, megkülönböztetve az egyszeresen láncolt listától, amelyben minden
elemnek csak egyetlen hivatkozása van. Ez a kettős kapcsolódás teszi lehetővé, hogy
az elemeket rnindkét irányban bejárhassuk Ezáltal a beszúrás és a törlés is sokkal
egyszerűbb lesz, mint a tömblistáknál.
Mint a tömblista bemutatásából tudjuk, a törlés és beszúrás legtöbb esetében az
alaptömb egy részét át kell másolnunk A láncolt listáknál viszont elegendő csupán
az előző és a következő elemekben rendre az előre és visszafelé hivatkozást frissíte
ni. Ezzel a szélsőséges esetek kivételével szinte elhanyagolható lesz a tényleges be
szúrás vagy törlés költsége. A különösen nagy elemszámú listáknál a bejárási idő je
lenthet teljesítményproblémát.
A duplán láncolt listákban találhatunk hivatkozást a lista első és utolsó elemére
is, amelyekre gyakran a lista fejeként és végeként (head and tail) hivatkozunk. Ezáltal
lehetőségünk nyilik a lista mindkét végpontját egyenlő teljesítménnyel elérni.
Gyakorlófeladat: tesztosztály létrehozása
Ne feledjük, hogy a tesztek nyúj�ák a legjobb módszert annak ellenőrzésére, ele
get tesz-e a megvalósításunk a fejezet elején a 3.1. és a 3.2. táblán bemutatott követel
ményeknek. Ezúttal hozzuk létre a L i n kedListTest osztályt, amely az AbstractL i st
Testcase kiterjesztése:
package com.wrox.algorithms.lists;
public class LinkedListTest extends AbstractListTestcase { protected List createList() {
return new LinkedList();
} }
A megvalósitás működése
lviint az ArrayListTest osztály esetén korábban is, az AbstractL istTestcase osztályt
bővítjük ki, hogy fel tudjuk használni az előre definiált teszteseteket. Ebben az esetben
viszont a createLi st() metódus a L i n ked L i st egy példányával tér vissza. Figyeljük
meg azt is, hogy nem hoztunk létre újab b teszteseteket sem, rnivel az AbstractL i st
Testcase osztályban definiált tesztek elegendőek lesznek.
Gyakorlófeladat: a LinkedList osztály létrehozása
Kezdjük a L i n ked L i st osztály létrehozásával, annak összes mezőjével és konstruk
torával együtt:
package com.wrox.algorithms.lists;
import com.wrox.algorithms.iteratíon.Iterator; ím ort com.wrox.algorithms.iteration.IteratoroutofsoundsExceRtíon;
78
Listák megvalósítása
public--aas5Linkedi::Jst iriiPlements L ist{
}
private final Element _headAndTail = new Element(null); private int _size;
public LinkedList() { clear();
}
A megvalósitás müködése
Mint minden más listánál, most is a L i st interfész megvalósításával kell kezdenünk. Itt is a _si ze példányváltozón keresztül követhetjük nyomon a lista méretét. (Elméletileg megszámolhatnánk az összes elemet minden alkalommal, amikor szükségünk van a lista méretére, de ez téf!Yleg nem lenne kifizetődő!)
Nem ennyire egyértelmű, miért van egyetlen, nem módosítható elemünk, a _head
AndTai l a két hivatkozás helyett, amelyről korábban szó volt. Ezt a mezőt nevezzük őrszemnek*. A:z. őrszem - amelyet gyakran null oijektummintának vagy egyszerűen csak null oljektumnak nevezünk - egy algoritmusegyszerűsítő technika, amely egy speciális elemet ad az adatstruktúra egyik vagy mindkét végére, hogy ne legyen szükség korlátfeltételeket kezelő speciális kódok írására. Őrszem használata nélkül a kódunk tele lenne szórva olyan kifejezésekkel, amelyek a fej és vég null referenciáit ellenőriznék és frissítenék Ehelyett az őrszem következő és előző mezőivel hivatkazunk a lista első és utolsó elemére. Ezenkívül az első és utolsó elem is mindig visszahivatkozhat az őrszemre, mintha az csupán a lánc egy eleme lenne. A:z. őrszem fogalma nehezen megfogható, ezért semmi ok az aggodalomra, ha első látásra egy kicsit bizarrnak tűnik. Valójában egy őrszemet használó algoritmus megalkotása általában nem igényel különleges képességeket. Ha viszont már megszaktuk a használatát, rájövünk, hogy sokkal elegánsabbá és szabatosabbá teszi algoritmusainkat - próbáljunk meg nélküle duplán láncolt listát írni: egyből látszik majd, hogy miről van szó!
Végül a konstruktor meghívja a clear() metódust. A clear() metódust később fogjuk megírni, tehát most ne nagyon foglalkozzunk a működésével; legyen elég annyi, hogy alaphelyzetbe állítja az osztály belső állapotát.
Gyakorló feladat: elemosztály létrehozása A tömblistákkal ellentétben a láncolt listáknak nincs olyan belső adatszerkeze
tük, amelyben az értékeket tárolhatnák, tehát más módon kell reprezentálnunk az elemeket. Ehhez először létrehozunk egy találóan elnevezett El emen t belső osztályt:
private staticfinal classElement -{ private Object _value; private Element _previous;
_private Element _next·
* A magyar nyelvú szakirodalomban szokás a "strázsa" elnevezés is. (A lektor)
79
Listák
}
80
-public Element(Object value) { setvalue(value);
}
public void setvalue(Object value) { _value = value;
}
public Object getvalue() { return _value;
}
public Element getPrevious() { return _previous;
}
public void setPrevious(Element previous) { assert previous != null : "a previous nem lehet NULL"; _previous = previous;
}
public Element getNext() { return _next;
}
public void setNext(Element next) { assert next != null : "a next nem lehet NULL"; _next = next;
}
public void attachBefore(Element next) {
}
assert next != null : "a next nem lehet NULL";
Element previous = next.getPrevious();
setNext(next); setPrevious(previous);
next.setPrevious(this); previous.setNext(this); }
public void detach() { _previous.setNext(_next); _next.setPrevious(_previous);
Listák megvalósítása
A megvalósitás müködése
Az El emen t belső osztály az esetek többségében elég egyértelmű. Amellett, hogy az
értéket tárolja, hivatkozást is tartalmaz a következő és az előző elemre, valamint
rendelkezik néhány egyszerű metódussal is a különböző mezők értékeinek visszake
resésére és beállítására.
Egy bizonyos ponton a kódunknak új elemet kell majd beszúrnia a listába. Ez a
logika van beágyazva az attachBefo re() metódusba.
Ahogyan a neve is sugallja, ez a metódus lehetővé teszi egy elem számára, hogy
egy másik elé illessze be magát úgy, hogy átveszi az előző és a következő elemek hi
vatkozásait, majd úgy frissíti azokat, hogy rá mutassanak.
Törölnünk is kell majd elemeket. Erre hoztuk létre a detach() metódust, amely
lyel egy elem eltávolithatja magát a listából azáltal, hogy az előző és következő ele
mek hivatkozásait úgy állítja be, hogy egymásra mutassanak.
Vegyük észre, hogy mindeközben egyetlenegyszer sem kellett null értékeket el
lenőriznünk, vagy a fejre és végre mutató hivatkozásokat frissítenünk. Ez csupán
azért lehetséges, mert őrszemet használunk. Mivel az őrszem maga is az El emen t egy
példánya, mindig lesz frissítendő következő és előző elem.
Gyakorlófeladat: metódus létrehozása értékek beszúrására és hozzáadására
A láncolt listába való beszúrás elméletileg egyszerűbb, mint a tömblistába való be
szúrás, mert nem kell hozzá átméretezés. A helyes beszúrási pont megtalálásához
azonban szükség van egy kis logikára:
public void�
insert(int index, Object value) throws IndexoutOfBoundsException { assert value != null : "az érték nem lehet NULL";
if (index < O l l index > _size) { throw new IndexoutofBoundsException();
}
Element element = new Element(value); element.attachBefore(getElement(index)); ++_size;
}
private Element getElement(int index) { Element element = _headAndTail.getNext();
}
for (int i = index; i > O; --i) { element = element.getNext();
}
return element;
81
Listák
Ami az add() metódust illeti, ismét csak delegáljuk a feladatot az i n se rt() metó
dusnak, a lista méretét mint beszúrási pontot átadva:
public void add(object value) { insert(_size, value);
J
A megvalósitás működése
Mint mindig, az i n se rt() a bemenet érvényesítésével indul. Ezután egy új elemet ho
zunk létre az adott értékkel, megkeressük a beszúrási pontot, és bekapcsoljuk a láncba,
mielőtt utolsó lépésként megnövelnénk a lista méretét, hogy tükrözze a módosítást.
A láncolt lista e megvalósításának a getElement() metódus az igazi teherhordója.
Több metódus is meghívja, és bejárja az egész listát az adott pozícióban található elem
megkeresésére. Ebből a letámadásos megközelítésből kifolyólag az insert() (és mint
később látni fogjuk, a de l e te()) átlagos és legrosszabb esetbeli futásideje o (N). A valóságban azonban javíthatunk a get El emen t() tényleges teljesítményén.
Mint ahogyan a vicc is mondja: "Mekkora egy láncolt lista hossza? A közepétől a
végéig terjedő távolság kétszerese." Emlékezzünk vissza, hogy láncoltlista-megvaló
sításunk a lista mindkét végére tárol hivatkozást, nem csak a fejre. Ha a keresett po
zíció a lista első felébe esik, akkor indulhatunk a lista első elemétől, és haladhatunk
előre. Ha viszont a lista második felébe esik, akkor a keresést kezdhetjük a lista utol
só elemétől, és visszafelé haladhatunk. Ily módon soha nem kell a lista felénél többet
bejárnunk ahhoz, hogy megtaláljuk a célt. Bár ennek a keresési idő nagyságrendjére
semmilyen hatása nincs, a tényleges átlagos futásidő lényegében a felére csökken. Az
erre vonatkozó gyakorlat a fejezet végére marad.
Gyakorlófeladat: metódus létrehozása értékek beszúrására és visszakeresésére
Az elemek beszúrása és visszakeresése a láncolt listában is majdnem ugyanúgy törté
nik, mint a tömblistában, kivéve, hogy a tömb indexelése helyett a getEl emen t()
metódust használjuk, amelyet az insert() számára vezettünk be:
82
public object get(int index) throws IndexoutofsoundsException { checkoutofsounds(index);
return getElement(index).getValue();
}
public Object set(int index, Object value) throws IndexoutofsoundsException {
assert value != null : "az érték nem lehet NULL"; checkoutOfBounds(index);
Element element = etElement(index);
}
Ob]ect oldval�lement.getvalue(); element.setvalue(value); return oldvalue;
private void checkoutOfBounds(int index) { if (isoutofsounds(index)) {
throw new IndexoutOfBoundsException(); }
}
private boolean isOutOfBounds(int index) { return index < O l l index >= _size;
}
A megvalósitás müködése
Listák megvalósítása
Mindkét esetben először ellenőrizzük a pozíció érvényességét, elérjük a megfelelő
elemet, majd megfelelően kinyerjük vagy beállitjuk az értékét.
Mivel mind a get(), mind a set() a getElement() megvalósításához van kötve,
a futásidejük is hasonlóan korlátozott. Ezáltal az indexalapú elemhozzáférés a lán
colt listákban átlagosan sokkal lassabbá válik, mint a tömblistákban.
Gyakorlófeladat: értékek megtalálása
A láncolt listában való keresés elméletben legalábbis nem különbözik a tömblisták
ban való kereséstől. Nincs más választásunk, mint az egyik végén elkezdeni, és addig
folytatni a keresést, míg meg nem találjuk a kívánt értéket, vagy egyszerűen ki nem
fogyunk az elemekből:
public int indexof(Object value) {
}
assert value != null : ''az érték nem lehet NULL";
int index = O;
for (Element e = _headAndTail.getNext(); e != _headAndTail; e = e.getNext()) {
if (value.equals(e.getvalue())) { return index;
}
++index; }
return -1;
83
Listák
A contai ns() metódus núndenben megegyezik az ArrayList osztályban találhatóval:
public boolean contains(object value) { return indexof(value) != -1;
}
A megvalósitás müködése
Az i ndexof() láncolt listabeli és tömblistabeli megvalósításai közötti különbség tulajdonképpen csak annyi, hogy hogyan lépünk az egyik elemről a következőre. Tömblista esetében ez egyszerű: csupán megnöveljük eggyel az index értékét, és közvetlenül elérjük a tömböt. Ugyanakkor a lánclisták esetében magukat a hivatkozásokat kell használnunk arra, hogy az egyik elemről a következőre lépjenek. Ha az érték létezik, a pozíciója lesz visszaadva. Ha azonban elérjük az őrszemet, akkor lecsúsztunk a lista végéről, véget ér a hurok, és a -1 érték visszaadása jelzi, hogy az érték nem létezik.
A con ta i ns () metódus meghívja az i ndexof() metódust és true értéket ad vissza.
Gyakorlófeladat: értékek törtése
A láncolt listákban a törlés szinte triviális. Tulajdonképpen a kód legnagyobb részét már megvalósítottuk az El emen t belső osztályban:
public object delete(int index)
}
throws IndexoutofsoundsException { checkoutofsounds(index); Element element = getElement(index); element.detach(); --_size; return element.getvalue();
És természetesen itt egy példa az érték szerinti törlésre is:
84
public boolean delete(Object value) {
J.
assert value != null : "az érték nem lehet NULL";
for (Element e = _headAndTail.getNext(); e != _headAndTail;
}
e = e.getNext()) { if (value.equals(e.getvalue())) {
e.detach(); --_size; return true;
}
return false;
Listák megvalósítása
A megvalósitás müködése
Miután ellenőriztük, hogy az adott pozíció érvényes, az első de l e te() metódus a
get El emen t() meglúvásával kinyeri a megfelelő elemet, leválasz�a a listáról, majd
csökkenti a lista méretét, mielőtt visszatérne az értékéveL
A második delete() metódus kódja szinte teljesen megegyezik az indexof()
kódjával, de a pozíció követése és visszaadása helyett az első illeszkedő elem megta
lálásakor azonnal töröljük az elemet, és visszaadjuk az értékét. (Ne felejtsük el csök
kenteni a lista méretét a detach () meglúvása után!)
Gyakorló feladat: iterátor létrehozása
A láncolt listák iterációja kicsit munkásabb, mint a tömblistáké. A kereséshez és a
törléshez hasonlóan azonban ez is egyszerűen csak a hivatkozások követéséből áll
(bármelyik irányban), amig végig nem érünk. Erre egy val uerterator belső osztályt
fogunk létrehozni, amelybe beágyazzuk az iterációs logikát.
private- finalcrass va lllerterator implements rterator { private Element _current= _headAndTail;
l
public void first() { _current= _headAndTail.getNext();
}
public void last() { _current= _headAndTail.getPrevious();
}
public boolean isoone() { return _current== _headAndTail;
}
public void next() { _current= _current.getNext();
}
public void previous() { _current= _current.getPrevious();
}
public object current() throws IteratoroutofsoundsException { if (i soone()) {
throw new IteratoroutofsoundsException(); } return _current.getvalue();
}
85
Listák
A belső osztály definiálása után az i terator() metódus már tud visszaadni példányt:
public Iterator iterator() { return new valueiterator();
}
A megvalósitás működése
A valuerterator osztály látszólag megegyezik a 2. fejezet Arraynerator osztályá
val, kivéve, hogy a kereséshez és a törléshez hasonlóan, most is rendre a getNext O és a get P rev i ou s () metódusokat használjuk az elemek előre és visszafelé történő
bejárásakor, amíg el nem érjük az őrszemet.
Gyakorlófeladat: az interfész befejezése
Elérkeztünk az interfész utolsó néhány metódusához: si ze(), i sEmpty() és clear():
public int size() { return _size;
}
public boolean isEmpty() { return size() == O;
}
public void clear() { _headAndTail.setPrevious(_headAndTail); _headAndTail.setNext(_headAndTail); _size = O;
}
A megvalósitás működése
Nem meglepő módon a si ze() és az i sEmpty() metódusok a tömblista-megfelelőik
pontos másolatai.
Az utolsó, a clear() metódus már majdnem, de azért mégsem annyira egysze
rű, mint a tömblista megvalósítása. Ha őrszem használata során is fenn akarjuk tar
tani a megfelelő működést, akkor a következő és előző értékeit úgy kell beállitanunk,
hogy önmagára mutassanak. Ez biztosí�a, hogy a lista első elemének beszúrásakor
annak következő és előző eleme is az őrszernre mutasson, és ami még fontosabb,
hogy az őrszem következő és előző értékei az új elemre mutassanak.
86
Összefoglalás
Összefoglalás
Ebben a fejezetben bemutattuk, hogy a legtöbb életszerű alkalmazásban a tömbök helyettesíthetők listákkal.
Láttuk, hogy a listák megőrzik a beszúrási sorrendet, és hogy nem valósul meg bennük az elemek egyediségének fogalma.
Átnéztünk elég sok kódot is, hogy megvizsgáljuk a két leggyakoribb listamegvalósítást és relatív teljesítménykarakterisztikájukat. A tömblisták és a láncolt listák hasonló keresési és iterációs időkkel rendelkeznek. Természetükből fakadóan azonban a tömblisták a láncolt listákhoz képest sokkal jobb indexalapú hozzáférést biztosítanak. Másrészről viszont a láncolt listáknál nem áll fenn a másolás és átméretezés segédszámítási költsége, mint a tömblistáknál, így általában jobb beszúrási és törlési idővel rendelkeznek, különösen a végpontokban.
Bár az itt bemutatott listák sok helyzetben nagyon hasznosak, vannak olyan esetek is, amikor kicsit más viselkedésre van szükség. A következő két fejezet sor és ve
rem néven ismert listamódosulatokat mutat be, amelyek segítenek áthidaini bizonyos speciális programozási problémákat.
Gyakorlatok
1. Írjunk egy olyan konstruktort az ArrayList osztályhoz, amely szabványos Javatömböt vár a L i st kezdeti feltöltéséhez.
2. Írjunk egy e qua ls() metódust, amely bármely L i st megvalásításra működik.
3. Írjunk egy tostri n g() metódust, amely bármely L i st megvalásításra működik, és a lista tartalmát egyetlen sorba, az értékeket szögletes zárójelek közé zárva, vesszővel elválasztva kiírja. Például "[A, B, C]" vagy
"[]" üres L i st esetén.
4. Írjunk egy iterátort, amely bármely L i st megvalásításra működik. Melyek a teljesítményt befolyásoló tényezők?
5. Frissítsük a L i n ked L i st osztályt úgy, hogy beszúrás vagy törlés esetén visszafelé keressen a listában, ha a kívánt index a lista közepén túl található.
6. Írjuk át az i ndexof() metódust úgy, hogy bármely listára működjön.
7. Hozzunk létre egy mindig üres L i st megvalósítást, amely módosítási kísérlet esetén unsupportedoperati onExcepti on kivételt dob.
87
NEGYEDIK FEJEZET
Várakozási sorok
A várakozási sorok (a továbbiakban röviden sorok) az algoritmusok fontos részei,
amelyek feldolgozni kívánt munka, események vagy üzenetek allokációját és üteme
zését kezelik. A sorokat gyakran a különböző folyamatok közötti kommunikáció
biztosítására használjuk - ugyanazon a gépen vagy különbözőeken.
A fejezetben a következő témaköröket tárgyaljuk:
• miben különböznek a sorok a listáktól,
• a FIFO- (first-in-first-out - az elsőnek betett elemet vesszük ki először)
sor tulajdonságai és megvalósítása,
• a szálbiztos sorok létrehozása,
• hogyan kell korlátos sorokat, azaz felső méretkorláttal rendelkező sort lét
rehozni,
• hogyan kell ezeket a típusokat kombinálva előkészíteni egy telefonos ügyfél
szolgálat többszálas szimulációját, hogy lássuk a sorok alkalmazásának lehe
tőségeit.
A sorok
A bankban az ügyfelek sorban állva várnak arra, hogy az alkalmazott kiszolgálja
őket, az áruházban pedig a pénztárnál sorakoznak. Egészen biztosan várakoztunk
már hosszasan arra, hogy egy telefonos ügyfélszolgálat munkatársával beszélhes
sünk. Számítástechnikai szempontból a várakozási sor adatelemek listáját jelenti,
amelyet a rendszer úgy tárol, hogy az adatokat meghatározható sorrend szerint le
hessen visszakeresni. A lista és a sor megkülönböztető jegye, hogy míg - a listán be
lüli pozíció segítségével - a lista bármely eleméhez hozzáférhetünk, a sor egyetlen
hozzáférhető eleme a lista Ji!Jinél található elem. Hogy melyik elem a fej, az a megha
tározott sormegvalósítástól függ.
Várakozási sarok
A visszakeresési sorrend nagyon gyakran megegyezik a beszúrási sorrenddel
(mint a FIFO esetén, ahol az elsőnek betett elemet vesszük ki először), de vannak
más lehetőségek is. A leggyakrabban használt sorok közé tartozik még a LIFO-sor
Oásd az 5. fejezetet) és a priorirásos sor Oásd a 8. fejezetet), amelynél a visszakeresés
az egyes elemek prioritásától függ. Akár véletlen sort is létrehozhatunk, amely hatéko
nyan "megkeveri" a tartalmat.
Ha a könyvben "sort" ernlitünk, nem feltétlenül a FIFO-sorra hivatkozunk.
A sorokat általában gyártók és fogyaszták segítségével lehet jellemezni. A gyártó bármilyen objektum, amely adatokat tárol a sorban, rnig a fogyas'{!ó bármi lehet, ami
adatokat olvas a sorbóL A 4.1. ábrán a gyártók, a fogyaszták és a sorok közötti
együttműködést láthatjuk.
4. 1. ábra. A gyártók és fogyas'{!ók együttműkodése a sorral
A sorok lehetnek korlátosak vagy nem korlátosak A korlátos sorok elemszámára vala
milyen megszorítás vonatkozik, amelyet a sornak mindenkor be kell tartania. A korláto
zások akkor bizonyulnak különösen hasznosnak, ha a rendelkezésre álló memória nagy
sága szűkös - például olyan eszközök, mint a router vagy a memóriabeli üzenetsarok
esetén. Ezzel ellentétben a nem korlátos sorok méretének csak a hardver szab határt.
Sarműveletek
A fejezet a könyvben használt több különböző sorra is kitér; ezek a sorok a vissza
keresési sorrend terén mutatnak némi eltérést. Viselkedésükre való tekintet nélkül a
különböző sorok közös interEésszel rendelkeznek. A 4.1. táblázatban az egyes sor
műveletek listáját találjuk a rövid leírásukkal együtt.
lviint látjuk, a sorinterfész sokkal egyszerűbb, mint a listák interfésze: az enqueue()
felelős az értékek tárolásáért, a dequeue() pedig az értékek visszakereséséért. A többi
metódus ugyanúgy viselkedik:, mint a listák számára definiált metódusok. Vegyük észre,
hogy iterátor segítségével nincs lehetőség a sor összes adateleméhez való egyidejű hoz
záférésre Oásd a 2. fejezetet), és ez tovább erősíti a kijelentést, amely szerint az egyetlen
hozzáférhető elem a sor fejénél található.
90
A sorok
Művelet leirás
enqueue A sorban értéket tárol. A sor mérete eggyel növekszik.
dequeue Visszakeresi a sor fején található értéket. A sor mérete eggyel csökken. Ha a sorban már nincs több elem, EmptyQueueExcepti on kivételt jelez.
clear A sor összes elemét törli. A sor méretét alaphelyzetbe, nullára (O) állítja.
Size A sor elemeinek számát adja vissza.
isEmpty Meghatározza, hogy a sor üres (si ze() O) vagy sem.
4. 1. táblázat. S orműveletek
A sorinterfész
Bármely defuúált interfészt közvetlen ül J ava interfészre fordíthatunk, és így játszi könnyedséggel létrehozhatunk beköthető megvalósításokat:
package com.wrox.algoriihms.queues;
public interface Queue {
l
public void enqueue(Object value); public object dequeue() throws EmptyQueueException; public void clear(); public int size(); public boolean isEmpty();
Minden műveletet közvetlenül az interfész egy metódusára fordítottunk. Az egyetlen, amit magunknak kell definiálnunk, a dequ eu e() metódus által jelzett EmptyQueueExcepti on kivétel:
package com.wrox.algorithms.queues;
public class EmptyQueueException extends RuntimeException {
l
Úgy döntöttünk, hogy az EmptyQueueExcepti on futásidejű kiterjesztés lesz. Ez anynyit jelent, hogy a try-catch blokkokat a dequeue() hívások körül nem kell becsomagolni. Ennek elsődleges oka az, hogy az üres sarok visszakeresési kísérleteit programozási hibának tekintjük; a dequeue() hívása előtt bármikor meghívhatjuk az i sEmpty() metódust.
91
Várakozási sorok
A FIFO-sor
A következő rész a FIFO- (flrst-in-first-out) sor megvalósítását mutatja be. Először
megismerkedhetünk a FIFO-sor tulajdonságaival, majd fejlesztünk néhány tesztet,
végül pedig megvalósítunk egy eléggé egyértelmű, listákon alapuló nem korlátos
FIFO-sort.
A sor neve mindent elárul: az első bemenő érték mindig az első kijövő érték is
egyben. A dequ eu e() metódus hívása a FIFO-soron mindig azt az elemet adja visz
sza, amely a leghosszabb ideje van a sorban.
Ha például az enqueue() metódust a Macska, a Kutya, az Alma és a Banán érté
kekkel hívnánk meg, a dequeue() metódus a következő sorrendben adja vissza az
értékeket: Macska, Kutya, A l ma, Banán.
Noha a FIFO-sor (és a többi sortípus) megvalósításának több lehetősége van, az
egyik legegyszerűbb megoldás, amelyet itt is bemutatunk, a lista alkalmazása mögöt
tes tárolási mechanizmusként Ez több szempontból is természetesnek tűnik: a sort
egyszerűsített listaként kell elképzelni, amely az elemek hozzáadása és eltávolítása te
rén rendelkezik néhány megszorítással.
Az elemek sorba állítása során, a 4.2. ábrán látható módon a rendszer az új ele
met a lista végéhez adja hozzá.
o 2 3
4.2. ábra. Az enqueue O metódus hívása az értéket a lista végéhez acfja
Ezzel szemben ha az elemet kiemeljük a sorból, akkor a 4.3. ábrán látható módon a
lista elejéról kell eltávolítani.
o 2 3
4.3. ábra. A dequeue() metódus hívása eltávolíija az értéket a lista elejéről
92
A FIFO-sor
Természetesen azt is megtehettük volna, hogy a lista elejéhez adunk hozzá elemet,
és a végéről távolitunk el egyet. Mindkét lehetőség működik, de ebben az esetben a
lista végéhez adtuk hozzá az elemet, és az elejéről távolitottuk el, mert ez jobban il
leszkedik a sor mentális modelljére.
A2. elméleti alapok után itt az idő, hogy kódoljunk egy kicsit! Szokás szerint először
csak teszteket készítünk, és ezután kezdünk hozzá a sor tényleges megvalósításához.
Gyakorlófeladat: a FfFO-sor tesztelése
No�a a FIFO-sor megvalósításának csak az egyik lehetőségét fogjuk megvizsgálni,
több másik is létezik, ezért a könyv eddigi megközelítési módjánál maradva egy
tesztcsomagot fejlesztünk, amelynek minden FIFO-sornak meg kell felelnie. Ezeket
a teszteket csatlakozási pontokkal rendelkező absztrakt osztályokkal definiáljuk,
amelyek lehetővé teszik, hogy speciálls megvalósítások teszteléséhez a későbbiekben
ki tudjuk bővíteni őket.
package com.wrox.algorithms.queues;
import junit.framework.Testcase;
public abstract class AbstractFifoQueueTestcase extends Testcase { private static final String VALUE_A = "A";
}
private static final String VALUE_B = "B"; private static final string VALUE_C = "c";
private Queue _queue;
protected void setup() throws Exception { super. setUp();
_queue = createFifoQUeue(); }
protected abstract Queue createFifoQueue();
A2. első teszt valójában a korlátok ellenőrzése. Szetetnénk meggyőződni róla, hogy
az üres lista mérete nulla, az i sEmpty() metódus true értéket ad vissza, és a sorból
való kiemelés kísérlete EmptyQueueExcepti on kivételt jelez:
public voicftestAccessMEmptyQueue() �{ assertEquals(O, _queue.size());
l asser.tTrue( queue. i sEmpty()); _,_ � _ ___.
93
Várakozási sorok
}
Úy { _queue.dequeue(); fail();
}
} catch (EmptyQueueException e) { ll ezt várjuk
A következő teszt kissé hosszabb, de eléggé egyértelmű. Ellenőrzi, hogy sikeresen
sorba állithatunk és a sorból kiemelhetünk értékeket:
public void testEnqueueoequeue() { _queue.enqueue(VALUE_B); _queue.enqueue(VALUE-A); _queue.enqueue(VALUE_C);
}
assertEquals(3, _queue.size()); assertFalse(_queue.isEmpty());
assertsame(VALUE_B, _queue.dequeue()); assertEquals(2, _queue.size()); assertFalse(_queue.isEmpty());
assertsame(VALUE_A, _queue.dequeue()); assertEquals(l, _queue.size()); assertFalse(_queue.isEmpty());
assertsame(VALUE_C, _queue.dequeue()); assertEquals(O, _queue.size()); assertTrue(_queue.isEmpty());
try { _queue.dequeue(); fail();
} catch (EmptyQueueException e) { ll ezt várjuk
}
Az utolsó teszttel meggyőződhetünk arról, hogy a cl ear() metódus hívása után a
sor az elvárásoknak megfelelően kíürül:
94
public void testclear() { _queue.enqueue(VALUE-A); _queue.enqueue(VALUE_B); _queue.enqueue(VALUE_C);
assertFalse( gueue.isEmpty(l}j�----------------- '"------------�
A FIFO-sor
}
_queue.clear();
assertEquals(O, _queue.size()); assertTrue(_queue.isEmpty());
try { _queue.dequeue(); fail();
} catch (EmptyQueueException e) { ll ezt várjuk
}
Az absztrakt tesztosztály elkészítése után létrehozhatjuk a tényleges FIFO-sor meg
valósításának konkrét tesztosztályát. A megvalósítási osztályt természetesen, még
nem definiáltuk, de ez nem tart vissza bennünket a teszteset definiálás ától:
package com. wrox:iil go ri thms. que u es;
public class ListFifoQueueTest extends Abstract:FifoQueueTestcase { protected Queue createFifoQueue() {
return new ListFifoQueue(); }
l
A megvalósitás müködése
Az új Abstract: Fi foQueueTestcase tesztosztály meghatároz néhány korlátozást,
amelyeket a tényleges tesztek során a későbbiekben alkalmazni fogunk. Definiálja a
_queue lokális változót, amely FIFO-sor példányt tárol, ezen futtatjuk majd a teszte
ket. A setup() metódus - amelyet az egyedi tesztek futtatása előtt meghívunk- biz
tosítja, hogy a lokális változó mindig rendelkezzen értékkel. Célját a createFi fo
Queue() absztrakt metódus segítségével éri el, amelynek a megvalósításával a speci
fikus FIFO-sor tesztelés alatt álló osztályának egy példányát adjuk vissza.
A második és a harmadik tesztben ellenőrizzük, hogy a sorba állitás és a sorból
való kiemelés műveleteit a sor mérete mindig híven tükrözi, és ami még fontosabb,
az értékek visszaolvasása során az értékeket pontosan a tárolás sorrendjében kapjuk
vissza. Ez a FIFO-sor definíciójának megfelelő viselkedés.
Az utolsó teszt a sor értékeinek számát tárolja, meghívja a clear() metódust, és
meggyőződik róla, hogy a sor tényleg üres.
A konkrét osztály létrehozásával a sorosztály neve L i st: Fi foQueue lesz; ez meg
felel annak a ténynek, hogy a FIFO-sorról van szó, amely lista segítségével tárolja az
adatokat. Vegyük észre, rnilyen könnyű kibővíteni az AbstractFifoQueueTest:case
tesztesetet, és megvalósítani a creat:eFi foQueue metódust, hogy a konkrét sorosz
tály egy példányát adja vissza.
95
Várakozási sorok
A FIFO-sor megvalósítása
A tesztek készen állnak, tehát nyugodtan elkezdhetjük a megvalósítási osztály kódolását: L i st Fi foQueue:
pacKage com.wrox.algoritnms.queues;
import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
public class ListFifoQueue implements Queue { private final List _list;
}
public ListFifoQueue(List list) {
}
assert list != null : "list nem lehet NULL";
_list = list;
public ListFifoQueue() { this(new LinkedList());
}
A Queue interfész megvalósításán kívül a mögöttes listát is tartalmazza, és két konstruktort definiál. Az első konstruktor egyetlen argumentuma egy lista, amely az adatokat tárolja (természetesen ellenőrizzük, hogy a mérete null) . A második- alapértelmezett konstruktor - meghivja az elsőt, és á tad egy láncoltlista-példányt.
A láncolt listák kiválóan használhaták a sorokkal, mivel a listák mindkét végén hatékonyan lehet elemeket hozzáadni és eltávolitani. Vessük ezt össze egy tömblistával, amely- ha emlékszünk rá- folyamatosan az elemek eltávolításával járó segédszámítási költségekkel küzd!
Most, hogy már létre tudunk hozni listaalapú FIFO-sort, itt az ideje, hogy a sorhoz hozzáadjunk elemeket. Ezt a célt szolgálja az enqueue() metódus:
public void enquéue(Object value) { _list.add(value);
}
Ez meglehetősen egyszerű. Mint korábban már emlitettük, az enqueue () hozzáadja az értéket a mögöttes lista végéhez.
A következőkben a dequeue() metódust valósítjuk meg, amely lehetővé teszi, hogy elemeket olvassunk vissza a sorból:
96
publicoliject aequeue() throws EmptyQueueException{
l
if (isElllpty()) { _
throw new EmptyQueueException(); }
return _list.delete(O);
Blokkolósorok
Ez sem sokkal bonyolultabb, nún t az elŐző metódus. Ne feledjük, hogy a dequ eu e()
pusztán eltávolítja és visszaadja a mögöttes lista utolsó elemét. Az egyetlen további múvelet annak ellenőrzése, hogy van eltávolítható elem. Ha nincs (mert a lista üres), a Queue int�rfészben definiált EmptyQueueExcepti on kivételt kell jelezn ünk.
Ezen a ponton e� vitathatnánk, mert a L j st interfész IndexoutofsoundsExcepti on
kivételt jeleZ; és az üres lista ellenőrzése he/yett e%Jiszerűen a kivétel kezelésével Empty
QueueExcepti on kivételt jelezhetnénk Ha azonban, mint már korábban említettük, a
kód IndexoutofsoundsExcepti on kivételt jeleZ; a� programozási hiba jelzéseként
szeretnénk kezelni, nem pedig a hívó fol hibcijaként.
A Queue interfész utolsó néhány metódusának megvalósítása még könnyebb, rnivel núndegyik metódus (nem véletlenül) a L i st interfész metódusainak megfelelő, azonos névvel rendelkező metódus:
publ; c Voi d':Clear·o ---{ _list.clear();
}
public int size() { return _list.size();
}
public boolean isEmpty() { return _list.isEmpty();
}
Mindhárom esetben csak annyit kell tennünk, hogy a hívásokat a mögöttes listának küldjük.
Blokkolósarok
A sorokat többszálas környezetben gyakran folyamatközi kommunikáció bonyolítására alkalmazzuk. A L i stFi foQueue sajnos teljesen alkalmatlan olyan helyzetek biztonságos kezelésére, arnikor egyidejűleg több fogyasztó is hozzáfér a sorhoz. Ehelyett a blokkolósarok biztosítanak szálbiztos megvalósítást, amely gondoskodik az adatok hozzáférésének megfelelő szinkronizálásáróL
97
Várakozási sorok
Az első fontos fejlett tulajdonság, amellyel a blokkoló sor rendelkezik, de a szok
ványos sor nem, hogy lehet korlátos. A fejezetben eddig csak nem korlátos sorokkal
foglalkoztunk: ezek mérete korlátozások nélkül folyamatosan növekedhet. A blokko
lósor lehetővé teszi, hogy beállitsuk a sor méretének felső korlá�át. Ha olyan sorban
próbálunk meg további elemet tárolni, amely elérte méretének felső határát, a sor -
mint már kitalálhattuk- egészen addig blokkalja a szálat, amíg egy elem eltávolitásá
val vagy a c l e ar() metódus hívásával ismét lesz szabad hely. Ezáltal garantálhatjuk,
hogy a sor mérete sohasem haladja meg az előre beállitott értékeket.
A másik jelentős tulajdonság a dequeue() metódus viselkedésével kapcsolatos.
A korábban ismertetett L i st Fi foQueue megvalósításból még talán emlékszünk rá,
hogy ha üres sorból próbálunk meg kiolvasni egy elemet, a rendszer EmptyQueue
Excepti on kivételt jelez. Ezzel ellentétben a blokkolósor addig blokkol ja az aktuális
szálat, amíg egy újabb elemet sorba nem állitunk - tökéletes megvalósítás munkaso
rok számára, amelyeknél több párhuzamos fogyasztónak kell várakoznia, amíg nem
érkezik végrehajtandó feladat.
A Queue interfészbe ágyazva megszabadí�uk a sor fogyasztóit a szálszinkronizá
lás banyadalmaitól és finomságaitóL A blokkolósor létrehozására két lehetőség kí
nálkozik: kibővíthetünk egy létező sormegvalósítást (pillanatnyilag a L i st Fi foQueue
megvalósítás áll rendelkezésünkre), vagy a viselkedést másik sor kiiré csomagolhat
juk. Az első lehetőség egy meghatározott sormegvalósítást biztosít, ezért a második
lehetőséget kell alkalmaznunk, mert ezáltal bármely sormegvalósítást (például a 8. fe
jezetben ismertetett prioritásos sorokat) könnyedén blokkolósorrá alakíthatunk.
Ami a szinkronizálást illeti, általános technika segítségével biztosítjuk, hogy a
kód többszálas környezetben is kiválóarr működjön: zárolási objektumot, illetve
szakkifejezéssel élve kolesiinas kizárást megvalósító szemafort (mutex) alkalmazunk az osz
tályban a metódusok szinkronizálási pontjaként. A mutex egyike a hibákra kevésbé
hajlamos módszereknek, amelyekkel biztosíthatjuk, hogy a mögöttes sorhoz egy
időben kizárólag egy szál fér hozzá.
Gyakorlófeladat: a 81ockingQueue használata
Rendszerint ez az a pont, ahol az "és akkor most következzen a tesztelés" gondolata
megfogalmazódik bennünk. Ebben az esetben azonban eltérünk az eddigi szokások
tól, és kihagyjuk a tesztdési részt.
Micsoda?! Semmi tesztelés? Valójában tényleg írtunk teszteket, de mível a több
szálú alkalmazások tesztelésének ismertetése a könyv témakörén kívül esik; ezért azzal
ehelyütt most mégsem foglalkozunk. De bátran elhihe�ük, hogy ezek a sorok is mű
ködnek. Természetesen futtathatunk teszteket, ha letöl�ük a könyv teljes forráskódját.
98
Ha a tb"bbszálú kódolással kapcsolatban további ismereteket szeretnénk szerezni, kezcjjük
Do ug Lea Concurrent Programming in J ava: Design Principles and Patterns
(1999) című kbiryvéveL
A B l oc kingQueue kód ismertetése az osztálydeklarációval kezdődik:
package com.wrox�lgorithms.queues;
public class 81ockingQueue implements Queue { private final object _mutex = new object(); private final Queue _queue;
l
private final int _maxsize;
public BlockingQueue(Queue queue, int maxsize) { assert queue != null : "queue nem lehet NULL"; assert maxsize > O : "size nem lehet < l";
}
_queue = queue; _maxsize = maxsize;
public BlockingQueue(Queue queue) { this(queue, Integer.MAX_VALUE);
}
Blokkolósarok
A B l oc kingQueue megvalósítja a Queue interfészt, és néhány példányváltozót is tartal
maz. Két változó magáért beszél: az első, a queue az adatok tárolására szolgáló mögöt
tes sor hivatkozását tartalmazza, a másik, a _maxsi ze a sor maximálisan megengedett
méretét. A harmadik változó, a _mut:ex a korábban ismertetett zárolási objektum.
Van továbbá két konstruktor is. Az első az adattárolásra alkalmazni kívánt sort
és a maximálisan megengedett méretet tartalmazza. Ez a konstruktor teszi lehetővé,
hogy létrehozzuk a korlátos sort. A második konstruktor fogadja el a sort, majd
meghívja az első konstruktort, és átadja a legnagyobb sorméret legnagyobb lehetsé
ges egész értékét. Noha definiáltunk korlátozást, az olyan nagy, hogy gyakorlatilag
nem korlátos sort hoztunk létre.
Itt az ideje, hogy megtekintsük a kívánt viselkedés megvalósításának lehetőségeit;
kezdjük az enqueue() metódussal. Első pillantásra kissé kísértetiesnek tűnhet, de egyáltalán nem bonyolult:
public void enqueue(Object value)-
{ synchronized (_mut:ex) {
}
while (size() == _maxsize) { wait:ForNot:ificat:ion();
}
_queue.enqueue(value); _mut:ex.not:ifyAll();
.. }�.-�.k---�--�-------
99
Várakozási sorok
private void waitForNotification() { try {
_mutex.wait();
} catch (InterruptedException e) {
ll Ignore ("nem kell figyelembe venni")
} }
A megvalósitás müködése
Az első dolog, amelyet az enqueue (illetve az összes többi metódus is) végrehajt, an
nak biztosítása, hogy ugyanabban az időpontban másik szál ne férhessen hozzá a
sorhoz. A Java programozási nyelvben a synchronized metódus segítségével lehet
zároini egy objektumot- ebben az esetben a mutexünket. Ha egy másik szál már zá
rolta az objektumot, az aktuális szál blokkolt marad mindaddig, amíg a másik szál fel
nem oldja a zárolást. Ha a szál megszerezte a zárolást, a többi szál egészen addig
nem fér hozzá asorhoz, amíg az aktuális szál ki nem kerül a szinkronizált blokkból.
Ez lehetővé teszi a mögöttes sorral való munkát úgy, hogy közben ne kelljen azért
aggódnunk, hogy egy másik szál műveleteire futunk, vagy egy másik szál váratlanul
műveleteket kezdeményez a mögöttes soron.
Kizárólagos hozzáférést szerezve a sorhoz a következő végrehajtandó feladat annak
biztosítása, hogy ne lépjük túl a sor korlátait. Ha a sor már elérte maximálisan megenge
dett méretét, akkor lehetőséget kell biztosítanunk egy másik szál számára, hogy felszaba
dítson némi területet. Ezt a wa i tForNoti fi ca ti on metódus hívásával valósí�uk meg.
A metódus a mutex wa i t O metódusát hívja, és gyakorlatilag elalta�a a szálat. A szál elai
tatásával átmenetileg feladjuk a sor zárolásának lehetőségét. A szálat kizárólag úgy lehet
felébreszteni, ha egy másik szál meghívja a mutex notifyAll O metódusát, arnikor is az
enqueue () ismét magához ragadja a vezérlést, és újra megkísérli a zárolást.
Végül elegendő hely szabadul fel, és a mögöttes sor az új értéket tárolja majd. Ezt
követően a mutex notifyAll O metódusával felébresz�ük az esetlegesen alvó szálakat.
Gyakorlófeladat: a dequeue() metódus megvalósítása
A dequ eu e O megvalósítása ehhez hasonló, de tárolás helyett ez a metódus a sorból
olvas:
100
public Object dequeue() throws EmptyQueueException { synchronized (_mutex) {
} }
while (isEmpty()) { waitForNotification();
} object value = _queue.dequeue(); _mutex.notifyAll();
return value;
Blokkolósarok
1\!fint ahogyan az az enqueue () esetén történt, a dequeue () exkluzív zárat helyez el
annak biztosítására, hogy kizárólag egy szál férjen hozzá a sorhoz. Mielőtt a dequeue()
metódust meghívná, addig vár, amíg a mögöttes soron legalább egy elem hozzáférhe
tővé válik.
A megvalósitás müködése
Ahogyan azt az enqueue() esetén tettük, ha készen vagyunk, meghívjuk a notify
A ll () metódust. Mivel a dequeue () metódus elemeket olvas, az enqueue () metódus
hívása közben (például a maximálisan megengedett méretet elért sor miatt) blokkolt
többi szálat értesítenünk kell.
Gyakorlófeladat: a clear() metódus megvalósitása
A clear() metódus még egyszerűbb:
public void clear()-{ synchronized (_mutex) {
_queue.clear(); _mutex.notifyAll();
} }'----------------------------�-----
A megvalósitás müködése
A szokott módon meg kell szereznünk a zárat, kiürítjük a mögöttes sort, és mint azt
a dequeue() metódus esetén tettük, értesítjük a méretkorlátot elért sorban elemeket
tárolni szándékozó, blokkolt szálakat.
Gyakorlófeladat: a size() és az isEmpty() metódusok megvalósitása
Végül, íme a két utolsó metódus, a si ze() és az i sEmpty() metódusok kódja:
puolic int size()_{ __
_
}
synchronized (_mutex) { return _queue.size();
}
publ i c boolean i sEmpty() { synchronized (_mutex) {
return _queue.isEmpty();
} l
101
Várakozási sarok
A megvalósitás müködése
Mindkét metódus a mögöttes sor ekvivalens metódusai köré csomagolható a szál
biztos szinkronizáló kódba. Ebben az esetben azonban nem módosítottuk a mögöt
tes sort, tehát nem szükséges meghívni a notifyAll() metódust.
Példa: telefonos ügyfélszolgálat szi m u látora
Elérkezett az idő, hogy hasznosítsuk a sorokat. Most kell az eddig tanult ismeret
anyagot gyakorlati - kissé leegyszerűsített - környezetbe helyezni. Már láttuk, ho
gyan osztják ki és állitják prioritási sorrendbe a sarok a munkát, ezért ebben a rész
ben az egyik példabeli forgatókönyvet, egy telefonos ügyfélszolgálatot ragadunk ki,
és blokkolósort alkalmazó szimulátort készítünk.
Az alapötlet igen egyszerű: olyan rendszert hozunk létre, amelyben a telefonos
ügyfélszolgálathoz véletlenszerűen beérkező hívások sorba kerülnek, és a hívást a
következő elérhető ügyfélszolgálati munkatárs fogadja. A 4.4. ábrán az elképzelés
alapötletének bemutatását látjuk.
Telefonos ügyfélszolgálat
4.4. ábra. Telefonos ü!!Jfélszolgálat s\fmufácir!Jának magas s\fntií modeflje
A telefonos ügyfélszolgálatnak küldött hívásokat a hívásgenerátor hozza létre. A te
lefonos ügyfélszolgálat a hívásokat blokkolósorban tárolja, ahol a hívások arra vár
nak, hogy a következő elérhető ügyfélszolgálati munkatársak fogadják őket. Ahogy
az egyes alkalmazottak befejezik a hívásokat, a rendszer visszatér a sorhoz, és újabb
hívást próbál kivenni belőle. Ha több feldolgozásra váró hívás is van, a rendszer
azonnal kiosztja a következőt. Ha azonban a sor üres, a következő beérkező hívásig
blokkolt marad. Ezáltal az ügyfélszolgálati munkatársnak sohasem kell törődnie a
megválaszolásra váró hívásokkal; a blokkoló sor gondját viseli ennek a logikának.
102
Példa: telefonos ügyfélszolgálat szimulátora
Vegyük észre, hogy a sor az ügyfélszolgálati munkatársakkal együtt a telefonos
ügyfélszolgálaton belül él. Annak is fel kell tűnnie, hogy több munkatárs van, akik
egyszerre dolgoznak - csakúgy, mint a való életben. A konkurens végrehajtás miatt
az egyes ügyfélszolgálati ügynökök saját szálakat futtatnak Szerencsére blokkolósor
megvalósításunk kimondottan többszálas helyzetek kezelésére készült, és mivel a sor
a száltelítődés egyetlen pontja, a példát tekintve nem kell az alkalmazás egyéb részei
nek szinkronizálása miatt aggódnunk.
A szimulátort különálló alkalmazásként készítjük el, amely a naplóüzeneteket fu
tás közben a konzolra küldi, így nyomon követhetjük, mi történik. A program lehe
tővé teszi, hogy bizonyos változók értékeitől függően különböző helyzeteket szimu
láljunk. Ezeket a változókat parancssorban határozzuk meg:
• az ügyfélszolgálati ügynökök száma,
• a hívások száma,
• a maximális hívásidő,
• a maximális hívási időköz.
Az ügyfélszolgálati munkatársak száma lehetővé teszi, hogy meghatározzuk a sorhívá
sokat fogyasztó szálainak számát. Minél több a munkatárs (szál), annál gyorsabb a hívá
sok feldolgozása. A probléma másik oldala, hogy a generált hívásszámtól függően minél
több szálunk van, annál több munkatárs vár a bejövő hívásokra, ha a sor éppen üres.
A hívásszám szabja meg, hogy összesen hány hívást kell generálni. Ez nem több
puszta biztonsági óvintézkedésnél, amely megakadályozza, hogy az alkalmazás min
dig fusson. Ha szeretnénk, állítsuk nagyon magas értékre a hívások számát, és néz
zük meg, mi történik.
A maximális hívásidő meghatározza a hívásidőtartam felső korlátját. Ez lehető
vé teszi, hogy szimuláljuk azokat a helyzeteket, amikor egy-egy hívás hosszabb vagy
rövidebb ideig tarthat.
A maximális hívási időköz meghatározza az egyes hívások generálása közötti vá
rakozási idő felső korlátját.
Maga a terv viszonylag egyértelmű - a lehetőségekhez mérten igyekeztünk le
egyszerűsíteni -, és a korábban elkészített B l oc kingQueue metódus használatán kí
vül több osztályt is magában foglal. A következőkben minden egyes osztályt részle
tesen bemutatunk.
Most, hogy már látjuk a kitűzött célt, ideje elkezdeni az alkalmazás fejlesztését.
A korábban ismertetett okokból kifolyólag lemondunk a szokásos tesztelésről, és
rögtön a kódolással kezdjük a munkát. (Ne feledjük, hogy a letölthető forráskód tar
talmaz teszteket, ámbár úgy éreztük, hogy a szövegen belüli magyarázat feleslegesen
bonyolítaná a dolgokat.)
103
Várakozási sarok
Indulásként új osztályokat hozunk létre a 4.4. ábrán látható összes fogalom szá
mára, és a végeredményként előállt egyszerű alkalmazást parancssorból futtathatj uk.
Annak érdekében, hogy a szimuláció viselkedését futtatás közben is figyelemmel
tudjuk kísérni, az egyes osztályok a konzolra írják az információkat. Az alkalmazás
futtatása közben megjelenő üzenetáradat muta�a, hogy rni történik a szimulátorban.
A rész végén néhány példakimenetet találunk, amelyek képet adnak arról, hogyan
néznek ki a diagnosztikai információk.
Gyakorlófeladat: a Call osztály létrehozása
A hívás a rendszeren belüli telefonhívást jelenti. A telefonos ügyfélszolgálat a hívá
sokat sorba állítja, majd az ügyfélszolgálati munkatársak fogadják őket (mindezekről
a későbbiekben részletesen szót ejtünk):
package com.wrox.algorithms.queues;
public class call {
}
private final int _id; private final int _duration; private final long _startTime;
public call(int id, int duration) {
}
assert duratien >= O : "a callTime (hívásid6) nem lehet negatív";
_id = id; _duration = duration; _startTime = System.currentTimeMillis();
public string tostring() { return "call " + _id;
}
A megvalósitás müködése
Minden hívás egyedi azonosítóval és hívásidőtartammal rendelkezik. Az azonosító le
hetővé teszi, hogy a rendszerben nyomon követhessük, rni történik egy hívással. A hí
vás időtartama határozza meg, hogy mennyi ideig tarthat a hívás "fogadása". Végül
rögzí�ük a hívás kezdetének időpon�át. Erre annak megállapításához van szükség,
hogy az egyes hívások mennyi időt töltenek a sorban.
A hívási osztályban az egyetlen metódus az answer(). Mint azt már kitalálhattuk,
az ügyfélszolgálati munkatársak a metódus segítségével fogadják a bejövő hívást:
104
Példa: telefonos ügyfélszolgálat szimulátora
public void-answer()-{
l
System.out.println(this + " fogadva; várakozás: " + (System.currentTimeMillis() - _startTime) + " ms");
try { Thread.sleep(_duration);
} catch (InterruptedException e) { ll Ignore ("nem kell figyelembe venni")
}
Kezdjük azzal, hogy kinyomta�uk a hívás fogadásának tényét a hívás várakozással
töltött összes idejével együtt. A metódus a hívás létrehozása során meghatározott
időre elalszik. Ezáltal a hívás felelős a hívás fogadásához szükséges idő szimulálásá
ért. Képzeljük el ezt úgy, mint egy ügyfelet, aki egészen addig nem fejez be egy hí
vást, amíg jónak nem lá�a.
Gyakorlófeladat: a CustomerServiceAgent osztály létrehozása
A következő osztály a cust:omerserviceAgent:- a 4.1. ábra fogyasztója. Az osztály
felelős a hívások sorból való kiemeléséért és fogadásáért:
package -com.wrox.algorit:hms.queues;
public class customerserviceAgent implements Runnable { ll Ezen még ne akadjunk fenn; a későbbiekben részletes ll magyarázatot fűzünk hozzá
}
public static final call GO_HOME = new call(-1, 0);
private final int _id; private final QUeue _calls;
public customerServiceAgent(int id, Queue calls) { assert calls l= null : "a calls nem lehet NULL"; _id = id; _calls .. calls;
}
public String tostring() { return "Agent " + _id;
}
105
Várakozási sorok
Csakúgy, rnint a hívások, a munkatársak is egyedi azonosítót kapnak. Ez elősegíti
annak azonosítását, hogy az egyes alkalmazottak mit csinálnak a rendszerben. Az
egyes munkatársak azon sor hivatkozását is tárolják, amelyekből a hívásokat kapják.
Vegyük észre, hogy a customerservi ceAgent a Runnable interfészt valósítja
meg. Ez lehetővé teszi, hogy rninden egyes példány külön szálként fusson, és ezáltal
egyidejűleg több munkatárs dolgozzon. A Run n ab l e egyetlen megvalósítandó metó
dust, a run metódust határozza meg; ez az a pont, ahová a sorból a hívásokat kieme
lő és fogadó kódot be kell illesztenünk:
public voia run() {
}
system.out.println(this + " bejelentkezett");
while (true) {
}
system.out.println(this + " várakozik");
call call = (call) _calls.dequeue(); system.out.println(this + " fogadás: " + call);
if (call == GO_HOME) {
break;
}
call.answer();
system.out.println(this + " hazamegy");
A megvalósitás működése
Minden alkalommal, amikor az ügyfélszolgálati munkatárs elindul, rövid üzenetben
jelzi, hogy elkezdte a munkát. Ezután egy ciklusban kiemeli és fogadja a sor hívásai t.
Az egyes hívások kiemelése során üzenet jelenik meg, és a hívás fogadásáról tájékoz
tat. A hívás befejezése után az alkalmazott újab b hívásért visszatér a sorhoz.
Talán észrevettük, hogy nincs ellenőrzés, amely a dequeue() metódus hívása
előtt meggyőződne arról, hogy a sorban van bejövő hívás. Megbocsátható a feltéte
lezés, amely szerint nem kell sok időnek eltelnie ahhoz, hogy a rendszer Empty
QueueExcepti on kivételt jelezzen; és ez az a pont, ahol a blokkoló sor működésbe
lép. Emlékezzünk rá, hogy a szálbiztos blokkoló sor üres sor esetén - ahelyett hogy
kivételt jelezne- vár.
106
A metódussal kapcsolatos másik furcsaság a következő kódrészlet:
if (call == GO_HOME) { break;
}
Példa: telefonos ügyfélszolgálat szimulátora
Az ellenőrzés nélkül az alkalmazott a végtelenségig a ciklusban ragadna, és további
bejövő lúvásokra várna. Képzeljük el, mi történik akkor, amikor a nap végén a tele
fonos ügyfélszolgálat bezár, és nem fogad több lúvást. Mint már említettük, a blok
kolósor várni fog, szegény ügyfélszolgálati munkatárs pedig ott ülhet, és az egész éj
szakát semmittevéssel töltheti!
A munkasorok tekintetében ez igen gyakran felmerül, de szerencsére létezik rá ál
talános megoldás. A lényeg, hogy létre kell hozni egy speciális értéket, amely a "fel
dolgozás leállítása" jelentéssei bír. A fenti példában konstanst definiáltunk, a GO_HOME
konstanst közvetlenül az osztálydefiníció elején. Ha a sorban ez a lúvás jelenik meg,
az ügyfélszolgálati munkatárs tudni fogja, hogy aznapra ideje befejezni a munkát.
Gyakorlófeladat: a CaliCenter osztály létrehozása
Most, hogy gondoskodtunk a lúvásokról és az ügyfélszolgálati munkatársakról, elér
kezett az idő, hogy létrehozzuk a telefonos ügyfélszolgálatot. Ez az osztály az alkal
mazottak kezeléséért - az elindításukért és a leállításukért -, valamint a lúvások sor
ba helyezéséért felelős:
package com.wrox.algoritnms.queues;
l import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List;
public class callcenter { private final Queue _calls
}
new BlockingQueue(new ListFifoQueue());
private final List _threads; private final int _numberOfAgents;
public Callcenter(int numberOfAgents) { _threads = new ArrayList(numberofAgents); _numberOfAgents = numberOfAgents;
}
Mielőtt elkezdhetnénk a lúvások feldolgozását, meg kell nyitnunk a telefonos ügyfél
szalgálatot - csakúgy, mint azt a való életben tennénk. A találóan elnevezett open()
metódus áll rendelkezésünkre:
pub l i c voi d.
open'() assert _threads.isEmpty() : "Már nyitva";
.__�sy.stem, ou.t. r> ri nt l n("telefonos üg_y,;tél szol gálaLmegnyitása");
107
Várakozási sorok
}
for (int i = O; i < _numberofAgents; ++i) { Thread thread =
}
new Thread(new customerserviceAgent(i, _calls));
th read. start O ; _threads.add(thread);
system.out.println("telefonos ügyfélszolgálat megnyitva");
Ha megnyitottuk a telefonos ügyfélszolgálatot, készen áll a hívások fogadására:
public void accept(Call call) { assert !_threads.isEmpty() : "nincs megnyitva";
_calls.enqueue(call);
system.out.println(call + " queued");
}
A nap végén be kell zárnunk a telefonos ügyfélszolgálatot, és haza kell küldenünk a
munkatársakat:
108
public void close() {
}
assert !_threads. i sEmpty() : "már bezárva";
system.out.println("telefonos ügyfélszolgálat bezárása");
for (int i = O; i < _numberOfAgents; ++i) { accept(CustomerServiceAgent.GO_HOME);
}
Iterator i = _threads.iterator(); for (i.first(); !i.isoone(); i.next()) {
waitForTermination((Thread) i.current());
}
_threads.clear();
system.out.println("telefonos ügyfélszolgálat bezárva");
private void waitForTermination(Thread thread) { try {
thread. joi n();
} catch (InterruptedException e) {
} .}.
ll Ignore ("nem kell figyelembe venni")
Példa: telefonos ügyfélszolgálat szimulátora
A megvalósitás müködése
A callcenter metódus először létrehozza a sort - egészen pontosan a Blacking
Queue egy példányát. Ezáltal probléma nélkül működtethetünk több ügyfélszolgálati munkatársat is, mindegyiket saját külön szálon, amelyek ugyanahhoz a sorhoz férnek hozzá. Vegyük észre, hogy mivel több szálat indítunk, leállitanunk is több szálat kell. Ebből kifolyólag a pillanatnyilag futó szálakról listát kell vezetnünk Végül pedig tárolnunk kell az elindítani kívánt munkatársak számát.
Az open() metódus feladata az osztály építése során meghatározott szám ú
munkatárs elindítása. Minden egyes customerservi ceAgent - az iterációs változó értékének segítségével - azonosítót és sort kap. Ha az operátor sikeresen létrejött, a rendszer saját szálán elindítja, és a listához adja.
Minden hívás, amelyet a sorba helyezünk, arra vár, hogy a "következő rendelkezésre álló operátor" fogadja, ami nem azt jelenti, hogy a hívás nem fontos számunkra, hanem csak annyit, hogy nem tudjuk az összes hív�st azonnal fogadni.
A munkatársak hazaküldéséhez először speciális hívást kell a sorba helyezni, olyat, amely tudatja az ügyfélszolgálati munkatársakkal, hogy ideje befejezni a munkát. Az összes dolgozó operátor számára a GO_HOME speciális hívást kell a sorba helyezni. Ám nem elég megmondani az ügynököknek, hogy hazamehetnek, mert előfordulhat, hogy a sorban még bejövő hívások várakoznak; barátságos telefonos ügyfélszolgálatunk nem csaphatja le a telefont, miközben ügyfeleink a hívás fogadására várnak. A GO_HOME hívás elküldésével meg kell várnunk, hogy a várakozó hívások befejeződjenek, és csak ezután kapcsolhatjuk le a villanyt, és zárhatjuk be az ajtót.
AwaitForTermination() metódus a Thread.join() segítségével altatja a rendszert, amíg a szál befejezi az aktuális hívás lebonyolítását.
Majdnem készen vagyunk, már csak két osztályt kell létrehoznunk
Gyakorlófeladat: a CallGenerator osztály létrehozása
A hívásgenerátor- ahogyan azt a neve is sugallja- a telefonhívások generálásáéit felelős.
package com.wrox.algorithms.queues;
public class callGenerator { private final callcenter _callcenter; private final int _numberofcalls; private final int _maxcallouration; private final int _maxcallinterval;
public callGenerator(callcenter callcenter, int numberofcalls, int maxcallouration, int maxcallinterval) {
assert callcenter != null : "callcenter nem lehet NULL"; assert numberofcalls > O : "numberofcalls nem lehet < l"; assert maxcallouration >O : "maxcallouration nem lehet <l"; assert maxcallinterval >O : "maxcallinterval nem lehet < l";
109
Várakozási sorok
}
}
_callcenter = callcenter; _numberofcalls = numberofcalls; _maxcallouration maxcallouration; _maxcallrnterval = maxcallrnterval;
A konstruktoron kívül csak egyetlen nyilvános metódusunk van, amelynek ténylege
sen a hívásgenerálás a feladata:
public void generatecalls() {
}
for (int i = O; i < _numberofcalls; ++i) { sleep(); _callcenter.accept(
new Call(i, (int) (Math.random() * _maxcallouration)));
}
private void sleep() { try {
}
Thread.sleep((int) (Math.random() * _maxcallrnterval));
} catch (InterruptedException e) { ll Ignore ("nem kell figyelembe venni")
}
A megvalósitás működése
A generatecall s () metódus a beállitott számnak megfelelően ciklusban generálja a
hívásokat. Az egyes hívásokat véletlen időtartammal generálja, és ezután küldi tovább
a telefonos ügyfélszolgálathoz feldolgozásra. A metódus a hívások között véletlen
időközökig vár - ezt az időköz t is az osztály létrehozása során határozzuk meg.
Gyakorlófeladat: a Cal/CenterSimulator osztály létrehozása
Az utolsó osztály maga a telefonos ügyfélszolgálat szimulátora. Ezt az apró alkalma
zást parancssorból futtathatjuk A telefonos ügyfélszolgálatot és a hívásgenerátort
kapcsolja össze. Az igazi szimuláció nagy részét a már részletesen tárgyalt osztályok
végzik. A ca ll Cente r simu l a tor osztály elsősorban a p arancssori paraméterek olva
sásával és szintaktikai elemzésével foglalkozik.
110
package com.wrox.algorithms.queues;
public final class callcentersimulator { private static final int NUMBER_OF_ARGS = 4; pr i vat e static fi na l i nt NUMBER_OF .,8GENTS_ARG 0;
Példa: telefonos ügyfélszolgálat szimulátora
priva:tesffiic finaÍ "i rit NUMB"ER_OF_CAI..LS_ARG·-;; l;
private static final int MAX_CALL_DURATION_ARG = 2;
private static final int MAX_CALL_INTERVAL_ARG = 3;
private callcentersimulator() { }
public static void main(String[] args) { assert args != null : "az argumentumlista nem lehet NULL";
if (args.length != NUMBER_OF_ARGS) { system.out. p ri htln(''Használ at: callGenerator <ágensek__száma>n
+ n <hívások__száma> <maximális_hívásidő>n + n <max_hívási_időköz>n);
System.exit(-1); }
callcenter callcenter =
new callcenter(Integer.parseint(args[NUMBEILOF_AGENTS_ARG]));
}
callGenerator generator =
new callGenerator(callcenter, Integer.parseint(args[NUMBER_OF_CALLS_ARG]), Integer.parseint(args[MAX_CALL_DURATION_ARG]), Integer.parseint(args[MAX_CALL_INTERVAL_ARG]));
callcenter.open(); try {
callGenerator.generatecalls(); } finally {
callcenter.close(); }
A megvalósitás müködése
A ma i n() metódus az alkalmazás belépési pontja, amelyet a J ava-értelmező meghív,
és átadja a parancssori paramétereket. A rendszer ellenőrzi az adatokat, és meggyő
ződik arról, hogy az összes szükséges paramétert megkapta:
• az alkalmazni kívánt ügynökök számát,
• a generálni kívánt hívások számát,
• a maximális hívásidőt,
• a generált hívások közötti maximális várakozási időt.
111
Várakozási sorok
Ha az egyik paraméter hiányzik, az alkalmazás üzenetet küld, és azonnal megszakí�a a végrehajtást. Ha az összes szükséges paraméter rendelkezésre áll, az alkalmazás felépíti a telefonos ügyfélszolgálatot és a hívásgenerátort. A telefonos ügyfélszolgálat kinyit, a generátor hívásokat generál, és végül az ügyfélszolgálati munkatársak megfelelő leállításával a telefonos ügyfélszolgálat bezár.
Az alkalmazás futtatása
A szimulátor fordítása és futtatása előtt foglaljuk össze, rnit is csinál a frissen létrehozott alkalmazás: a ca ll Generator véletlen időtartammal létrehozza a ca ll hívásokat. A hívásokat a callcenter fogadja, és a BlackingQueue sorba helyezi őket. Egy vagy több customerservi ceAgent munkatárs fogadja a hívásokat egészen addig, amíg a GO_HOME hívással le nem jár a munkaidő. Mindezeket a komponenseket a callCentersimul a tor alkalmazás fogja össze. ·
Ezt követően a telefonosügyfélszolgálat-szimulátort három ügyfélszolgálati munkatárssal futtatjuk, akik 200 hívást fogadnak. A hívásidő legfeljebb 1 másodperc (1000 milliszekundum), a hívások generálása közötti maximális időtartam pedig 100 milliszekundum. Íme, az alkalmazás kimenete (helytakarékossági szempontból egy nagyobb darabot kihagytunk):
112
telefonos ügyfélszolgálat megnyitása Agent O bejelentkezett Agent O várakozik Agent l bejelentkezett Agent l várakozik Agent 2 bejelentkezett Agent 2 várakozik telefonos ügyfélszolgálat megnyitva Agent O fogadás: call O Call O fogadva; várakozás: l milliszekundum call O queued Agent l fogadás: call l call l fogadva; várakozás: l milliszekundum ca ll l queued Agent 2 fogadás: call 2 call 2 fogadva; várakozás: l milliszekundum call 2 queued call 3 queued call 4 queued call 5 queued call 6 queued call 7 queued Agent 2 várakozik Agent 2 fogadás: call 3 call 3 fogadva; várakozás: 203 milliszekundum ca ll 8 _queued
Példa: telefonos ügyfélszolgálat szimulátora
call-
9 queuei:l call 10 queued call 11 queued Agent l várakozik Agent l fogadás: call 4 call 4 fogadva; várakozás: 388 milliszekundum
call 195 fogadva; várakozás: 22320 milliszekundum Agent l várakozik Agent l fogadás: Call 196 call 196 fogadva; várakozás: 22561 milliszekundum Agent O várakozik Agent O fogadás: call 197 call 197 fogadva; várakozás: 22510 milliszekundum Agent O várakozik Agent O fogadás: call 198 call 198 fogadva; várakozás: 22634 milliszekundum Agent l várakozik Agent l fogadás: Call 199 call 199 fogadva; várakozás: 22685 milliszekundum Agent 2 várakozik Agent 2 fogadás: call -1 Agent 2 hazamegy Agent O várakozik Agent O fogadás: call -1 Agent O hazamegy Agent l várakozik Agent l fogadás: call -1 Agent l hazamegy te lefonos üg_yf� l sz o l gá lat bezárva 1
Igaz ugyan, hogy a kimenet csak az első és az utolsó öt lúvás fogadását mutatja be, de még így is látha�uk a programot működés közben. Megfigyelhetjük a telefonos ügyfélszolgálat megnyitását, a három operátor bejelentkezését és a generált lúvások várakozását a sorban, mielőtt a következő ügyfélszolgálati munkatárs fogadná a soron következő lúvást. Figyeljük meg, hogy a várakozási idő kevesebb, mint egy másodpercről indul, és mire az utolsó lúvást is fogadják, a várakozási idő 20 másodpercre (20000 milliszekundumra) növekszik! Próbáljunk meg kísérletezni a bemenő változókkal, például a munkatársak számával, a lúvások közötti időtartammal, és vizsgáljuk meg, rnilyen hatással vannak a módosítások az eredményekre!
Megpróbáltunk viszonylag egyszerű kódot írni, és remélhetőleg most már valamelyest látszik, hogyan lehet alkalmazni a sorokat. Érdemes további statisztikákat gyűjteni, például a lúvások vagy az ügynökök ádagos várakozási idejére vonatkozóan, illetve kibővíteni a kódot, és különböző lúvásgenerátor-típusok számára biztosítani a lehetőséget, hogy ugyanazon a telefonos ügyfélszolgálaton futhassanak. Ily módon különböző lúvástípusokat, csúcsterhelési időszakokat stb. szimulálhatnánk.
113
Várakozási sarok
Összefoglalás
Ebben a fejezetben a következő témaköröket vizsgáltuk:
• A sorok hasonlitanak a listákhoz, de egyszerűbb interfésszel és meghatáro
zott visszakeresési sorrenddel rendelkeznek.
• A sorok korlátosak lehetnek, azaz korlátozható az egyidejűleg a sorba álli
tott elemek száma.
• A láncolt listák ideális adatstruktúrát biztosítanak FIFO-sor építéséhez.
• Megvalósíthatunk olyan szálbiztos csomagolót, amely bármely sormegvaló
sítással működik.
• Megvalósíthatunk korlátos, azaz felső méretkorláttal rendelkező sort.
Gyakorlatok
1. Valósítsunk meg olyan szálbiztos sort, amelyben nincs várakozás! Néhány eset
ben nem lesz másra szükségünk, mint egy többszálas környezetben működő
blokkolás nélküli sorra.
2. Valósítsunk meg egy sort, amely véletlen sorrendben olvassa vissza az értékeket!
114
A feladat egy csomag kártya lapjainak kiosztásához vagy bármely más, véletlen
kiválasztási folyamattal járó helyzethez hasonlatos.
ÖTÖDIK FEJEZET
Vermek
Most, hogy megismerkedtünk a listákkal, ideje rátérni a vermek bemutatására. Való
színűleg már találkoztunk néhány életszerű példájukkal: A tányérokat általában egy
halomba rakjuk: az elsőt a polera tesszük, és rá a többit. Ha egy tányérra van szüksé
günk, először a legfelsőt mozdítjuk el. A helyi vegyesboltban az újságak egymásra
vannak rakva, csakúgy, mint az asztalunkon elolvasásta váró könyvek.
Vermet használhatunk egy egyszerű legújabban használt (Most-Recendy-Used:
MRU) gyorsítótár megvalósítására, és gyakran alkalmazzuk a programozási nyelvek
elemzésére is.
Ez a fejezet a következő témákkal fog megismertetni minket:
• Mi az a verem?
• Milyen egy verem?
• Hogyan használjuk a vermeket?
• Hogyan valósítjuk meg a vermeket?
Az áttekintést az alapvető veremműveletekkel kezdjük, ezután tárgyaljuk a teszteket,
amelyek a veremmegvalósítás helyességének igazolásához szükségesek. Végül a ve
rem leggyakoribb, listán alapuló formáját tekintjük meg.
Vermek
A verem olyan listához hasonlít, amelynek az egyik végéhez korlátozott a hozzáfé
rés. Az 5.1. ábra egy verem grafikus ábrázolását mutatja.
T,,,i, 1 5.1. ábra. A vermet függőlegesen ábrázo!Jttk
Vermek
Észre fogjuk venni, hogy míg a listák és sorok általában balról jobbra futnak, a vermek függőlegesen vannak ábrázolva - ezért a "teteje" kifejezés a verem első és egyetlen közvetlenül elérhető elemére vonatkozik. A verem a beszúrást (verembe írás) és a törlést (veremből olvasás) egyaránt a csúcsától végzi.
A verem LIFO ("last-in-first-out": utolsónak be, elsőnek ki) várólistaként is ismert, mert biztosítja, hogy az az elem lesz először eltávolitva, amelyik a legkevesebb időt töltötte a veremben.
A veremműveleteket az 5.1. táblázat mutatja be.
Müvelet
push
pop
size
pe ek
isEmpty
clear
Leírás
Egy értéket ad a verem tetejéhez. A verem mérete eggyel nő.
Törli és visszaadja a verem tetején levő értéket. A verem mérete eggyel csökken. EmptyStackExcepti on hibaüzenetet kapunk, ha már nincs több elem.
A verem elemszámát adja vissza.
Visszaadja, de nem törli a verem tetején levő értéket. Emptystack
Excepti on hibaüzenetet kapunk, ha nincs elem a veremben.
Megállapítja, hogy a verem üres-e. A visszatérési érték true, ha a verem üres (size() == O); ellenkező esetben az érték false.
A verem összes elemét törli. A verem méretét visszaálli�a nullára.
5.1. táblázat. Veremmlíveletek
Ha egy értéket a verembe írunk, az a tetejére kerül. Az 5.2. ábra bemutatja, mi történik, ha a D értéket az 5.1. ábrán megjelenített verembe írjuk.
Teteje
Tffieje
1 5.2. ábra. Ha egy éttéket a verembe írunk, az a tetqére kerül
Ha egy értéket a veremből olvasunk, az a tetejéről lesz eltávolitva. Az 5.3. ábra bemuta�a, mi történik, ha egy értéket az 5.1. ábrán megjelenített veremből olvasunk.
Az utolsó három művelet- peek(), isEmpty() és clear()- csak a kényelmet növelő technikai segítség, mivel mindet megvalósíthatjuk az első hárommal.
116
Vermek
T•l•i• 1 • tB- T•rej•
5.3. ábra. Ha egy értéket a veremből olvasttnk, az a tetejéről lesz eltávolítva
Most pedig fogjuk a múveletdeflllÍciókat, és alakítsuk át őket J ava-interfészek és
tesztek kombinációjává:
package com.wrox.algorithms.stacks;
import com.wrox .algorithms.queues.Queue;
public interface Stack extends Queue { public void push(object value); public object pop() throws EmptyStackException;
public object peek() throws EmptystackException;
public void clear(); public int size(); public boolean isEmpty();
l
A Java interfész meglehetősen egyszerű, a múveletek viszonylag kis számának kö
szönhetően. A pop() és a pe ek() metódus egyaránt EmptyStackExcepti on hibaüze
netet dob, amennyiben olyan veremhez próbálunk hozzáférni, amelynek nincs ele
me, tehát ezt a kivételosztályt is definiálnunk kell:
package com.wrox.algorithms�stacKs;
public class EmptystackException extends RuntimeException { }
Vegyük észre, hogy a negyedik fejezetben található EmptyQueueException
kifejezéshez hasonlóan ezt is a RuntimeException kiterjesztéseként definiáltuk. Éspedig azért, mert úgy gondoljuk, hogy programozási hibára utal - az alkalmazáslogikában található hibára. Nincs rá méltányolható ok, hogy ezek közül bánnelyik létrejöhetne az alkalmazás végrehajtásának nonnális folyamata közben, igy nem akarjuk a fejleszt6t arra kényszeríteni, hogy fölöslegesen elfogja ezeket.
Végül jegyezzük meg, hogy a Stack interfész a Queue interfész kibővítése. Ez azért
van, mert ahogy már korábban is tárgyaltuk, a verem tulajdonképpen egy UPO-sor (és
szeretnénk, ha kompatibilis lenne), enqueue() (sorbaállit) és dequeue() (várakozási
sorból kiemel) eljárással, amelyek tulajdonképpen-a push() és a pop() szinonimái.
117
Vermek
A tesztek
Most pedig a helyes veremműveletek biztosításához szükséges tesztesetek létrehozá
sával folytathatjuk a munkát. Minden egyes metódushoz- push(), pop(), peek() és
clear()- külön tesztesetet határozunk meg. A size() és az isEmpty() metódu
soknak nincs saját explicit tesztelése, mert az elóbb említett metódusok részeként
teszteljük őket.
Bár ebben a fejezetben csak egy verem megvalósítását mutatjuk be, természete
sen lehetséges saját változatok létrehozása. Ezért alkotunk egy általános tesztosz
tályt, amelyet kibővíthetünk az egyes megvalósításokra specializált tesztosztályokkaL
Gyakorlófeladat: generikus tesztosztály létrehozása
package com.wrox.algorithms.stacks;
import junit.framework.Testcase;
public abstract class AbstractstackTestcase extends Testcase { protected static final string VALUE_..A = "A"; protected static final String VALUE_B = "B"; protected static final String VALUE._C = "c";
protected abstract Stack createstack();
A megvalósitás müködése
A verem interfész nagyon egyszerű, amit a tesztosztályok kis száma is tükröz. Mind
azonáltal fontos, hogy ne bízzuk el magunkat, és ne gondoljuk, hogy az egyszerűség
miatt nincs is szükség tesztelésre.
Gyakorlófeladat: a push() és a pop() metódus használata
A peek() metódus mellett, amelyet következőként tesztelünk, a verem hozzáférésé
nek egyetlen módja a push() és a pop() metódus. Ezért csaknem lehetetlen egyiket
a többi nélkül tesztelni:
118
pubÍic void testPushAndPop() { stack stack = createstack();
stack.push(VALUE._B); stack.push(VALUE._A); stack.push VALUE._C)·
}
assertEquals(3� stack�ize()); assertFalse(stack.isEmpty());
assertsame(VALUE_C, stack.pop()); assertEquals(2, stack.size()); assertFalse(stack.isEmpty());
assertsame(VALUE_A, stack.pop()); assertEquals(l, stack.size()); assertFalse(stack.isEmpty());
assertsame(VALUE_B, stack.pop()); assertEquals(O, stack.size()); assertTrue(stack.isEmpty());
A tesztek
Meg kell győződnünk arról is, hogy ha egy üreslista-eredményen próbálunk pop()
lúvást végrehajtani, akkor lúbát dob:
publ i c voi d te"stcaritPopFromAnEmptystack() ··{ Stack stack = createstack();
}
assertEquals(O, stack.size()); assertTrue(stack.isEmpty());
try { stack. pop() ; fa il();
} catch (EmptystackException e) { ll ezt várjuk
}
A megvalósitás működése
A teszt a verembe írja a három értéket: B, A és c, és utána egyenként leolvassa őket,
ellenőrizve, hogy helyes sorrendben lettek-e eltávolitva: c, majd A, végül B.
lviiután először megbizonyosadtunk róla, hogy a verem üres, megpróbálunk egy
értéket kiolvasni. Ha a pop() lúvás sikeres, akkor a teszt hibás, mert ez helytelen visel
kedés - egy üres veremből nem tudnánk olvasni. Ha ellenben EmptyStackExcepti on
hibaüzenetet kapunk, akkor a verem úgy működik, ahogy vártuk.
119
Vermek
Gyakorlófeladat: a peek() metódus tesztelése
Azonfelül, hogy értékeket tudunk írni a verembe, és olvasni onnan, a pe ek() metódus
sal "előzetes képet" kaphatunk a legfelső elemről, erről kapta a nevét (megtekintés):
public voia testPeek() { stack stack = createStack();
stack.push(VALUE_C); stack.push(VALUE_A); assertEquals(2, stack.size());.
assertSame(VALUE_A, stack.peek()); assertEquals(2, stack.size());
A peek tesztelésére két értéket írunk a verembe - c, majd A -, megbizonyosodunk
róla, hogy a peek() az utoljára beírt elemet adta vissza- esetünkben az A értéket-,
ugyanakkor semmit sem távolitott el a veremből:
public void testcantPeekrntoAnEmptystack() { stack stack = createstack();
}
assertEquals(O, stack.size()); assertTrue(stack.isEmpty());
try { stack. pe ek(); fail();
} catch (EmptyStackException e) {
ll ezt várjuk }
Végül, de nem utolsósorban leellenőrizzük, vajon a clear() úgy múködik-e, ahogy
vártuk, és a verem összes elemét eltávolítja-e:
120
public void testclear() { stack stack = createstack();
stack.push(VALUE_A); stack.push(VALUE_B); stack.push(VALUE_C);
assertFalse(stack.isEmpty()); assertEquals(3, stack.size());
}
assertTrue(stack--:-i sEmpty()); assertEquals(O, stack.size());
try { stack.pop(); fail();
} catch (EmptystackException e) {
ll ezt várjuk
}
A megvalósitás müködése
Megvalósítás
Először betöltöttünk a verembe néhány értéket, majd töröltük a tartalmát, ezután pedig megvizsgáltuk a méretét, és megpróbáltunk értéket olvasni, aminek nem szabad működnie: ha egy üres veremből akarunk olvasni, akkor EmptystackExcepti on
hibaüzenetet kell kapnunk.
Megvalósitás
Habár megvalósíthatnánk egy vermet az alapoktól, valójában nincs rá szükség. Ehelyett azt a megoldást válasz�uk, amelyet a listákról szóló fejezetben, tehát felhasználha�uk az előnyt, hogy egy lista mindennel rendelkezik, amire egy verem létrehozásához szükség van.
Láthatjuk majd, hogy egészen egyszerűen valósíthatunk meg vermet, amely a lista által már meglévő metódusokon alapszik. Így tehát minden, amit egy veremmel létre tudunk hozni, listával is megvalósítható. Amikor azonban speciális szerkezetet használunk valarnilyen cél elérésére, a lista és a verem tisztán szétválasztott elvi megközelítésével gondolkodunk. A célok szétválasztása meghatározóan fontos a szoftverek tervezésénéL
Ha a listát választottuk egy verem megvalósításához, akkor azt is el kell dönteni, hogyan érhetjük el vele a legjobbat. Út láthatunk néhány lehetséges utat: továbbfejlesztünk, bővítünk egy már létező listamegvalósítást, vagy egy teljesen új osztályt hozunk létre.
Mindegyik megoldásnak megvannak az előnyei és a hátrányai. Egy már létező megvalósítás fejlesztése vagy kibővítése könnyű lenne - egyszerűen a L i st interfészén kívül megvalósítanánk az osztállyai egy Vermet, és hozzáadnánk a metódusokat, amelyek a veremhez szükségesek. Ennek a megközelítésnek azonban van egy nagy hátulütője: tudjuk, hogy legalább kettő ismert és bizonyára még számos ismeretlen listamegvalósítás létezik, így minden különböző típusú listánál, amelyet használni akarunk, meg kell ismételni a folyamatot. Ez természetesen nem igazán elegáns megoldás.
121
Vermek
A másik lehetőség, amelyet itt tárgyalunk, hogy egy teljesen új osztályt hozunk
létre: L i stStack, amely kompozíciót használ. Vagyis új osztályunk tartalmaz és elfed
egy listapéldányt. Ennek számos előnye van, nem utolsósorban az, hogy ha okosan
valósítjuk meg, a verem képes lesz bármilyen listán működni, anélkül hogy megvál
toztatnánk a kódot.
A könyv e pontján valószínűleg már tisztában vagyunk vele, milyen fontosak a
tesztek, ezért mint mindig, szükségünk van egy konkrét tesztosztályra:
package com.wrox.algorithms.stacks;
public class ListstackTest extends AbstractStackTestcase { protected stack createStack() {
} .}.
return new Liststack();
Gyakorlófelada t: a UstStack osztály megvalósítása
A következő feladatunk a L i ststack osztály definiálása, amelynek többek között
meg kell valósítania a korábban meghatározott Stack interfészt:
package com.wrox.algorithms.stacks;
import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List;
public class ListStack implements Stack { private final List _list = new LinkedList();
}
Egy érték verembe írása úgy történik, hogy hozzáadjuk a lista végéhez.
public void push(Object value) { _list.add(value);
}
public void enqueue(Object value) { push(value);
}
A megvalósitás működése
Az osztálynak mögöttes adatstruktúraként csak egy listát kell tartalmaznia. Láncolt
listát használtunk, mivel ez nagyon hatékony az elemek hozzáadásában és eltávolítá
sában - amit egy verem végrehajt. Most, hogy ezt tudjuk, nyugodtan helyettesíthet
jük tömblistával is. A legfontosabb megértenünk, hogy egy konkrét listamegvalósítás
122
Megvalósítás
kibővítése helyett inkább egy listát "burkoló" kompozíciót használtunk Ez meggátolja, hogy a listametódusok láthatóvá tegyenek információkat, mivel ezáltal L i st
Stack osztályunk felhasználói azt hihetnék, hogy használhatják a metódusokat a lista interfészből, csakúgy, mint amelyeket a veremhez definiáltunk.
Amint láthatjuk, a push() csupán hozzáadja az értéket a mögöttes listához, miközben egyszerűen az enqueueO metódust használja.
Vegyük észre azt is, hogy itt nem vizsgáltuk a null értéket, ezt a felelősséget átháríthatjuk a mögötteslista-megvalósításra.
Gyakorlófeladat: érték olvasása veremből
Egy érték veremből olvasása majdnem ugyanilyen egyszerű. Csak az utolsó elemet kell eltávolítani a mögöttes listából:
publicooject pop() throws EmptystackException{ if (isEmpty()) {
throw new EmptystackException(); }
return _list.delete(list.size() -l); }
public Object dequeue() throws EmptyQueueException { try {
}
return pop() ; } catch (EmptyStackEXception e) {
throw new EmptyQueueException(); }
Az itt megvalósított push O és pop O teljesítményét kizárólag a mögöttes listában nekik megfelelő add O és de l e te O metódus határozza meg.
A peek() metódus lehetővé teszi a verem csúcsán levő értékhez való hozzáférést, anélkül hogy eltávolítaná azt:
publfc--ObTj ect peek�Othrows EmptystackExceptiOil{ object result =pop();
push(result); return result;
} �----�----�
Az osztály befejezésére delegálha�uk a megmaradó metódusokat a mögöttes listának, mivel a várt viselkedés azonos:
pubúcvoid.�cl earÖ�{ _list.clear();
}
123
Vermek
public int size() { return _list.size();
}
public boolean isEmpty() { return _list.isEmpty();
}
A megvalósitás működése
Ezúttal kissé elővigyázatosabbnak kell lennünk. Defenzív vizsgálat nélkül a lista de l e te() metódusa rndexoutOfBounds Ex ce pti on hibaüzenetet dobhat - a várttól eltérően. Ehelyett megvizsgáljuk a verem méretét, és a verem interfészben meghatározott EmptystackExcepti on hibaüzenetet dobunk, Inielőtt eltávolítanánk és visszaadnánk a mögöttes lista utolsó elemét.
Vegyük észre azt is, hogy bár a pop O működésének nagy részét elvégzi a dequ eu e(), az EmptystackExcepti on hibaüzenetet EmptyQueueExcepti on üzenetté kell átalakítani.
Ezután a peek() metódussal meghivjuk a pop() metódust a következő elem kikeresésére, rögzí�ük az értékét, és visszaírjuk a verembe, Inielőtt még visszaérkezne a hívóhoz. Így valójában visszajuttattuk az értéket a verem tetejére, Inielőtt még ténylegesen eltávolítottuk volna
Most már le tudjuk fordítani és futtatni a teszteket a teljes, lista alapú veremhez. Bármennyire is megnyugtató, hogy minden tesztet egyszer lefuttatunk, valószínűleg szeretnénk a vermünket valami építőbb jellegű célra is felhasználni.
Példa: az undo/redo parancs megvalósítása
Tulajdonképpen meglepően nehéz olyan példát találni a veremre, amely nem túlságosan tudományos. A szokásos példák a Hanoi-torony megoldását, a fordított lengyel jelölésű számológép megvalósítását, értékek listájának megfordítását és hasonló eseteket mutatnak be, tehát egyik sem igazán használható a jövőben számunkra felmerülő problémák esetén.
Néhány életszerű példa, amellyel nagyobb valószínűséggel találkozunk, az XML
feldolgozás, oldalbejárás irányítása (mint az előre és hátra gombok a böngészőnkben) és a visszavon/újra elvégez parancs. A következőkben ez utóbbiról lesz szó.
124
Példa: az undo/redo parancs megvalósítása
Képzeljünk el egy alkalmazást, amely egy listát tartalmaz: bevásárlólista, e-mail üzenetek listája vagy hasonló. A felhasználói felület, amelyet itt nem részletezünk, megjeleníti a listát, és lehetővé teszi a felhasználó számára, hogy elemet adjon hozzá, vagy távolítson el belőle.
Tegyük fel, hogy engedélyezni szetetnénk a felhasználónak a műveletek visszavonását. Minden alkalommal, amikor a felhasználó végrehajt egy műveletet, tárolnunk kell bizonyos információkat a lista állapotáról Q.ásd Memento [Gamma, 1995]), ·
amelyek segítségével majd vissza tudjuk vonni. Az állapotinformációt egy verembe írhatjuk Amikor pedig a felhasználó a művelet visszavonását kéri, leolvashatjuk az információt a verem tetejéről, és vissza tudjuk állítani a listát a múvelet végrehajtását megelőző állapotra.
A legkézenfekvőbb mód ennek megvalósítására, ha a lista egy másolatát minden egyes művelet előtt eltároljuk. Noha ez is múködik, nem ez az ideális megoldás. Először is minden alkalommal teljes másolatot kellene készítenünk a listáról. Ehelyett kihasználhatjuk azt a tényt, hogy a beszúrás a törlés inverze: ha egy elemet beszúrunk az 5-ös pozícióba, ezt visszavonha�uk az 5-ös pozíción levő érték törléséveL És fordítva, ha törlünk egy elemet a 3-as pozícióból, akkor az eredeti érték 3-as pozícióba történő beszúrása a törlés visszavonásával egyenértékű.
Bár ennek a témának a teljes kö'rú tárgyalása meghalaclja köf!yvünk kereteit, a bemuta
tott példát mégis kö'nf!Jen bővítheijük, támogatva az egyszerű visszavonást végző vermet
tö"bbszö'rös listákon vagy bárme/y alkalmas adatstruktúrán, mégpedig az undo foggvéf!Y
külső osztá!Jokba történő beágyazásávaL
Az undo/redo parancs tesztelése
Az általunk leírtak szemléltetésére és egyúttal egy megbízható éles kód létrehozására miért ne használnánk teszteket? V együk az előző részben bemutatott feltételeket, és képezzünk teszteseteket.
Gyakorlófeladat: tesztosztály létrehozása és futtatása
Mivel azt szeretnénk, hogy visszavonható listánk nagyjából ugyanúgy viselkedjen, mint bármely más lista, szárnos múködést kell tesztelnünk. Mindazonáltal, ha megvalósí�uk a L i st interfészt, bővíthe�ük az AbstractL istTestcase osztályt, és egyszerűen megkapha�uk az összes előredefiniált teszteseteti
paC:I<.a-ge""""CöM:wrox-.-a:rg;;; ttiltiS:-sia
cks:·�- · -- ·-·· · ····-··
i.,ort ca..wrox.alvorithms.lists.A»stractListTestcase; i�rt co..wrox.algorithas.lists.ArrayList; iMRQrt com.wrox.algorithms.lists.Lis�.�----.
-=='''1
125
Vermek
public class undoablelistTest extends. AbstractlistTestcase { protected List createlist() {
return new undoableList(new Arraylist()); }
}
Miután beszúrtunk egy értéket a listába, meg kell tudnunk hívni az undo() művele
tet, hogy azzal visszaállitsuk eredeti állapotára:
public void testundoinsert() {
}
undoablelist list= new undoablelist(new Arraylist());
assertFalse(list.canundo());
list.insert(O, VAlUE_A); assertTrue(list.canundo());
list.undo(); assertEquals(O, list.size()); assertFalse(list.canundo());
public void testundoAdd() {
J.
undoablelist list= new undoablelist(new Arraylist());
assertFalse(list.canundo());
list.add(VAlUE_A); assertTrue(list.canundo());
list.undo(); assertEquals(O, list.size()); assertFalse(list.canundo());
Az undo és canundo metódus kö"zül egyik sem része a l i st inteifésznek. Ezeket ké
sőbb fogjuk hozzáadni az undoab l el i st osifá!Jhoi;
Miután a de l e te() műveletet meghívtuk egy érték eltávolítására, meg kell tudnunk
hívni az undo() műveletet is, az érték eredeti helyre történő visszaállítására:
126
public void testundooeleteByPosition() { undoablelist list= new undoablelist(
new Arraylist(new Object[] {VAlUE_A, VAlUE_B}));
___ assertFalse(list.canundo( -�)h; ------�---------- ------ -----------
Példa: az undo/redo parancs megvalósítása
}
assertsame(VALUE_B, list.delete(l)); assertTrue(list.canundo());
list.undo(); assertEquals(2, list.size()); . assertSame(VALUE_A, list.get(O)); assertsame(VALUE_B, list.get(l)); assertFalse(list.canundo());
public void testundooeletesyvalue() { undoableList list = new undoableList(
new ArrayList(new object[] {VALUE_A, VALUE_B})); ·
assertFalse(list.canundo());
assertTrue(list.delete(VALUE_B)); assertTrue(list.canundo());
list. undo(); assertEquals(2, list.size()); assertsame(VALUE_A, list.get(O)); assertsame(VALUE_B, list.get(l)); assertFalse(list.canundo());
Bár a set() meghívása nem változtatja meg a lista méretét, módosítja a tartalmat.
Következésképpen számíthatunk rá, hogy ha egy elem értékének megváltoztatása
után meghívjukáz undo() műveletet, az visszaállítja az eredeti értéket.
public void testundoset()--{
}
undoableList list= new undoableList(new ArrayList(new object[] {VALUE_A}));
assertFalse(list.canundo());
assertsame(VALUE_A, list.set(O, VALUE_B)); assertTrue(list.canundo());
list.undo(); assertEquals(l, list.size()); assertsame(VALUE_A, list.get(O)); assertFalse(list.canundo());
A példa szándékának megfelelően úgy döntöttünk, hogy a c l e a r() műveletet némi
képp megkülönböztetjük a többitől, mert ez nem rögzít állapotot a későbbi vissza
vonáshoz. Csakis az egyszerűség kedvéért határoztunk így. Ez nem jelenti azt, hogy
ne tudnánk visszavonást megvalósítani c l e a r() es etén is, esetleg a törlés előtt a lis
táról történő teljes másolattal:
127
Vermek
public void testclearResetsundostack() {
}
undoableList list = new UndoableList(new ArrayList());
assertFalse(list.canundo());
list.add(VALUE_A); assertTrue(list.canundo());
list.clear(); assertFalse(list.canundo());
Eddig csak egyedi műveleteket, és a nekik megfelelő visszavonás magatartását tesztel
tük. Ha csak egy szintet szeretnénk visszavonni, akkor nincs szükség veremre. Valójá
ban azt szeretnénk elérni, hogy bármennyi műveletet képesek legyünk helyes sorrend
ben visszagörgetni. Mindenképpen szükség van egy tesztre, hogy lássuk, működik-e ez:
·public void testundoMultiple() { undoableList list = new undoableList(new ArrayList());
assertFalse(list.canundo());
list.add(VALUE_A); list.add(VALUE_B);
l i st. undo(); assertEquals(l, list.size()); assertsame(VALUE_A, list.get(O)); assertTrue(list.canundo());
list.delete(O);
l i st. undo(); assertEquals(l, list.size()); assertsame(VALUE-A, list.get(O)); assertTrue(list.canundo());
list.undo(); assertEquals(O, list.size()); assertFalse(list.canundo());
A megvalósitás müködése
A teszt először meggyőződik arról, hogy egy üres listánk van, és semmit sem lehet
visszavonni. Ezután beszúrunk egy értéket, vagy hozzáadjuk a listához. Mivel a
tesztosztály az Abstract L istTestCase osztályt bővíti ki, biztosak lehetünk benne,
hogy az érték beszúrása a listába rendben működik. Ezért csak azt kell ellenőriz
nünk, hogy a meghivott visszavonás eltávolí�a a beszúrt értéket.
128
Példa: az undatredo parancs megvalósítása
A visszavonás és a törlés eseteiben egyaránt viszonylag egyszerű tesztekről beszélhetünk, mivel nem kell foglalkoznunk az aktuális de l e te() metódus magatartásával -ezt már az ősosztály metódusainál letesZteltük. A lista kezdeti értékeinek először néhány előre definiált értéket állítunk be. Ezután törlünk egy értéket, és miután meghívtuk az undo() műveletet, ellenőrizzük, hogy az érték ismét megjelent�e a várt helyen.
Az utolsó teszt üres listával indul, és különböző módon hozzáadunk és eltávolítunk értékeket, közben meghívjuk az undo műveletet. A konkrét példában látni fogjuk, hogy a legelső hozzáadott elem a teszt végéig nem lesz visszavonva, még ha közben két másik műveletet is visszavontunk. Ez igazolja, hogy a verernalapú viszszavonás a vártnak megfelelőerr működik.
A tesztek helyesek, itt az ideje, hogy megvalósítsuk az undo ab l eL i st osztályt, ahogy azt a következő feladatban látha�uk.
Gyakorlófeladat: az undo művelet megvalósítása az UndoabieList osztállyai
Miután a feltételeket kódban rögzítettük, a visszavonható lista megvalósítása már viszonylag egyszerű. Az undoab l eL i st osztály megírásával kezdjük, majd a listametódusokkal folyta�uk. Vegyük észre, miként teszi lehetővé a tervezés a funkció hozzáadását minimális kódoJási munkával. A konkrét megvalósításnak nem csak beburkolnia kell a mögöttes listát, hanem valóban meg kell valósítania a L i st interfészt (lásd Decorator [Gamma, 1995]):
package com.wrox.algorithms.stacks;
import com.wrox.algorithms.iteration.rterator; import com.wrox.algorithms.lists.List;
public class undoableList implements List { private final stack _undostack = new Liststack(); private final List _list;
}
public undoableList(List list) {
}
assert list != null : "a lista nem lehet NULL"; _list = list;
private static interface undoAction { public void execute();
}
129
Vermek
Ahhoz, hogy elindulhassunk, meg kell kezdeni az állapotinformáció gyűjtését, min
den alkalommal, amikor egy értéket beszúrunk, vagy a listához hozzáadunk az
insert() hívással:
private final class undoinsertAction implements Action { private final int _index;
}
public undornsertAction(int index) { _index = index;
}
public void execute() { _list.delete(_index);
}
public void insert(int index, object value) throws IndexoutOfBoundsException {
_list.insert(index, value); _undostack.push(new undooeleteAction(index));
}
public void add(Object value) { insert(size(), value);
}
Ezután szükség lesz de l e te() lúvásra, hogy egy későbbi fázisban visszaállíthassuk a
törölt értéket:
130
private final class undooeleteAction implements Action { private final int _index;
}
private final object _value;
public undooeleteAction(int index, Object value) { _index index; _value = value;
}
public void execute() { _list.insert(_index, _value);
}
public Object delete(int index) throws rndexoutOfBoundsException { object value = _list.delete(index); _undostack.push(new undoinsertAction(index, value));
return value;
}
Példa: az undo/redo parancs megvalósítása
publicboolean deiete(Öbject value)-{ int index = indexof(value);
l
if (index == -1) { return false;
} delete(index); return true;
A metódus először egy i ndexof () lúvást végez az érték listán belüli pozíciójának
meghatározására. Ezután ha az értéket nem találja, fal se értékkel tér vissza; ellenke
ző esetben a de l e te() metódus lúvódik meg egy index paraméterrel, amely rögzíti a
szükséges állapotot egy későbbi undo művelethez. A set() meglúvása szintén módo
sí�a a lista állapotát, így szükségünk van egy eljárásra, amely visszaállítja az eredetit:
private final-class undosetAction-implements Acti�{ private final int _index;
}
private final Object _value;
public undoSetAction(int index, Object value) { _index = index; _value = value;
}
public void execute() { _list.set(_index, _value);
}
public object set(int index, object value)
}
throws IndexoutOfBoundsException { object originalvalue = _list.set(index, value); _undostack.push(new undosetAction(index, originalvalue)); return originalvalue;
Most, hogy meghatároztuk a szükséges infrastruktúrát a visszavonás-állapot rögzíté
sére, megírhatjuk az undo() metódus kódját:
public voia!Urldo()�hrows EmptystackException-{ ((Action) _undostack.pop()).execute();
}
Kényelmi okokból lehetővé tehe�ük a lúvónak, hogy meghatározza, van-e még visz
szavonandó művelet. Ez például akkor célszerű, ha szetetnénk engedélyezni, illetve
letiltani egy visszavonásgombot a felhasználói felületen:
publicoool ean canundo()-{ return !_undostack.isEmpty();
}
131
Vermek
Annak meghatározására, hogy van-e még visszavonandó művelet, egyszerűen a visz
szavonás-vermet kell megkérdeznünk: ha üres, akkor már nincs mit visszavonnunk,
és ez fordítva is igaz.
Még ha a clear() módosítja is a listát, úgy döntöttünk, hogy ebben a példában
nem rögzítünk visszavonási állapotot, és a lista visszaáll alaphelyzetbe:
public void clear() { _list.clear();
_undostack.clear();
}
Azonkívül, hogy töröljük a mögöttes listát, a visszavonásverem is törlődik, ily mó
don állítva vissza alaphelyzetbe az egész struktúrát.
Az erre az osztályra vonatkozó interfésszel kapcsolatos követelmények teljesíté
se csak formaság:
public object get(int index) throws IndexoutOfBoundsException {
return _list.get(index);
}
public int indexof(Object value) {
return _list.indexof(value);
}
public Iterator iterator() { return _list.iterator();
}
public boolean contains(Object value) { return _list.contains(value);
}
public int size() { return _list.size();
}
public boolean isEmpty() { return _list.isEmpty();
}
public String tostring() { return _list.tostring();
}
public boolean equals(Object object) { return _list.equals(object);
}
A megmaradó metódusok egyike sem módosítja a lista állapotát, ezért elegendő,
hogy egyszerűen hivatkoznak a mögöttes példány metódusaira.
132
Példa: az undo/redo parancs megvalósítása
A megvalósitás müködése
A mögöttes listán túl az osztály tartalmazza a visszavonásvermet is, amely magában
foglalja a belső interfész undoAction (szintén bemutatott) példányait, amely egyetlen
execute () metódust definiál, hogy végül lúvásra végrehajtsa a visszavonás-művelet
megvalósításának legnagyobb részét.
Az UndoAction osifá/y a parancs te17Jezési mintára (command pattem) [Gamma, 1995}
példa. Ebben az esetben a parancsminta megkiinf!Yíti a visszavonási lehetőségek beágyazá
sát, ho!!J a tevékef!YSég iinmaga le!!Je11 felelős azért, amit később végre kell hajtanunk. Haté
kof!Y, de kevésbé elegáns- és sokkal kevésbé kitujesifhető- alternatíva, ha a switch uta
sítást használjuk, és a miiveletekhez rendelt konstansokkal iráf!Yí!Juk a miikOdést.
Az undoDel eteAct i on osztály megvalósítja az undoAction interfészt, és ami termé
szetesen ennél fontosabb: az execute() metódust. A beszúrás visszavonása a törlés,
így amikor az execute () meglúvódik, a rögzített pozíciót használja az érték mögöt
tes listából való törlésére. Az insert() metódus meglúvja a mögöttes lista insert()
metódusát, és aztán a verembe ír egy visszavonás műveletet. Az add() metódus ez
után meglúvhatja az i n se rt() metódust. Létrehozhattunk volna egy speciális műve
letet a lista végén levő érték törlésére, de az i n se rt() meglúvása, a pozíció megadá
sával ugyanazt éri el, és sokkal kevesebb kóddal.
Az undoDel eteAction osztály megvalósítja az undoAct i on interfészt, és későbbi
használatra eltárolja a pozíciót és az értéket. A törlés visszavonása 'a beszúrás, így ami
kor az execute () eljárás meglúvódik, a művelet visszaírja az értéket a mögöttes listába.
Az első de l e te() művelet meglúvja a mögöttes lista de l e te() műveletét, és ki
keresi a törölt értéket, mielőtt verembe írna egy beszúrási műveletet, és visszaadná a
lúvónak. Az érték szerinti törlés valamivel bonyolultabb. Mivel nincs rá mód, hogy
megtudjuk, a lista mely részéről lett kitörölve az érték, újra meg kell valósítanunk a
pozíciótörlésen alapuló értéktörlést - nem különösebben hatékony megoldás, de
nincs más lehetőségünk.
A mögöttes lista set() műveletének meglúvása mindig a megadott pozíción tar
tózkodó eredeti értéket adja vissza, ebben az esetben tehát az undosetAction osz
tály execute() metódusa eltárolja a régi értéket a pozícióval együtt, hogy aztán vég
rehajthassa a visszavonást. Vegyük észre ismét, hogy az execute() metódus, mint
ahogy az előző két művelet esetén is, lúvást intéz a mögöttes listához, megakadá
lyozva, hogy a visszavonás egy újab b visszavonás-ruűveletet írjon a verembe.
Amint láthatjuk, nem volt szükség sok kócira az aktuális undo() metódus meg
írásához. A munka nehéz részét elvégezték az undoAction osztályok, így nekünk
már csak egyszerűen össze kell őket hozni, úgy, hogy a következő műveletet leolvas
suk a veremből, és meglúvjuk az execute () metódust.
Elkészült: most tehát van egy teljes mértékben tesztelt és megvalósított listánk,
amely támogatja a visszavonás műveletét.
133
Vermek
Összefoglalás
Bár fogalmilag n�gyon egyszerűek, a vermek alapvetők a legtöbb számítógép működésében. Ebből a fejezetből a következőket tanulhattuk meg:
• A legtöbb processzor és ezért a legtöbb programozási nyelv, beleértve a Javát is, veremalapú.
• A verem mindig a tetejéhez ad hozzá és távolít el értéket- ennélfogva gyakran nevezik LIFO-sornak.
• A verem könnyen megvalósítható listával, anélkül hogy megkötnénk a lista típusát.
• A vermeket számos lehetséges módon használhatjuk. Ez a fejezet megmutatta, milyen egyszerűen lehet megnöveilli más adatstruktúrát - ebben az esetben egy listát- a visszavonás-jellemzőveL
Miután láttunk néhány egyszerű algoritmust szövegkeresésre, és megismerkedtünk az adathasználó alapszintű adatstruktúrák, mint a listák, a sorok és a vermek irányításával, itt az idő, hogy összetettebb problémák megoldására térjünk át.
134
HATODIK FEJEZET
Alapvető rendezés
Most, hogy megismertünk a mai szaftveralkalmazásokban használt alapvető adat
sttuktúrák közill néhányat, felhasználha�uk ezeket annak a hatalmas adatmennyiség
nek a rendszerezésére, amelyet alkalmazásainknak fel kell dolgozniuk. Az adatok lo
gikus rendszerezése az elkövetkező fejezetekben tárgyalandó algoritmusok létfontos
ságú előfeltétele, és olyan lehetséges teljesítménybeli szűk keresztmetszet, amelyre ha
talmas kutatási munka irányult az elmúlt évtizedekben annak megitélésére, hogy a kü
lönböző adattípusoknak melyik a leghatékonyabb rendezési módja. Ebben a fejezet
ben három könnyen megvalósítható rendezési algoritmust mutatunk be, amelyek ki
válóan alkalmasak kisebb adathalmazok rendezésére, rnivel teljesítményük O(N2). A 7.
fejezetben bonyolultabb rendezési algoritmusokat tárgyalunk, amelyek nagyon nagy
adathalmazokra jobb teljesítménykarakterisztikával rendelkeznek.
Ez a fejezet a következőket tárgyalja:
• a rendezés fontossága,
• az összehasonlíták szerepe,
• a buborékrendezéses algoritmus működése,
• a kiválasztásos rendezési algoritmus működése,
• a beszúrásos rendezési algoritmus működése,
• a stabilitás jelentése,
• az alapvető rendezőalgoritmusok előnyei és hátrányai.
A rendezés fontossága
Már a mindennapi életből is tudha�uk, mennyire fontos a rendezés, arnikor kereső
algoritmusokkal van dolgunk. Ha egy szót akarunk megnézni a szótárban, algorit
must használunk: a szótárt azon a helyen nyitjuk ki, amely nagyjából megegyezik a
keresett szónak a könyvben található szavak rendezett listájában elfoglalt helyével.
Ezután néhány gyors szűkítő keresést hajtunk végre, míg meg nem találjuk a megfe
lelő oldalt, majd végül az oldalon átfutva megkeressük a szót. Most képzeljük azt az
esetet, hogy a szótárban nincsenek rendezve a szavak. Valószínűleg feladnánk, rnivel
a rendezetlen adatokon nem lenne kifizetődő a keresési idő, és igazunk is lenne!
Alapvető rendezés
Rendezés nélkül a keresés nagyon nagy adathalmazon nem praktikus. A nún
dennapokban sok egyéb adattípusra is alkalmazhatnánk ugyanezt az elvet: például a
telefonkönyvben tárolt nevekre vagy a könyvtár polcain sorakozó könyvekre. Ezek
kel a példákkal csak az a gond, hogy sohasem kellett (legalábbis remélem) foglalkoz
nia ezekkel az adattípusokkal azelőtt, hogy rendszerezték volna őket, tehát sohasem
kellett hatékony algoritmust készítenie az ilyen elemek rendezésére. A számítógépes
világban viszont gyakran találkozunk ilyen méretű adathalmazokkal, amelyek rende
zetlenül érkeznek programunkba, vagy más sorrendben, mint ahogyan nekünk szük
ségünk lenne rájuk. A bevált algoritmusok megértése segíthet az ilyen típusú prob
lémák kezelésében.
Rendezési alapismeretek
Az adatok valamíféle célszerű sorrendbe rendezéséhez szükségünk van egy olyan
adatstruktúrára, amely képes tartalma sorrendjének a kezelésére. Mint a 4. fejezetből
tudjuk, ez a listák egyik sajátos tulajdonsága, tehát listákat használunk majd olyan
adatstruktúraként, amelyen a rendezőalgoritmusok működhetnek.
Miután a rendezendő objektumokat eltároltuk a listában, az összes rendezőalgo
ritmus a következő két alapműveletre támaszkodik:
• az elemek összehasonlításával ellenőrizzük, hogy jó helyen vannak-e;
• az elemeket a rendezett pozícióba helyezzük.
Az egyes rendezőalgoritmusok előnyei és hátrányai attól függnek, hogy ezeket az
alapvető műveleteket hányszor kell végrehajtani, és hogy a műveletek teljesítmény
tekintetében rnilyen költségekkel járnak. Az objektumok összehasonlítása annak
megítélésére, hogy rendezett sorrendben vannak-e, nagyobb feladat, mint első ráné
zésre gondolnánk; éppen ezért foglalkoznunk kell vele az összehasonlítókról szóló
következő részben. A lista-adatstruktúra több metódust is támogat az objektumok
áthelyezésére, név szerint: get(), set(), insert() és delete(). Ezeket a művelete
ket a 3. fejezet tárgyalja részletesen.
136
Az összehasonlítékról
Az összehasonlitókról
Sok más nyelvhez hasonlóan a Javában is esihálhatunk valami hasonlót, amikor két egész értéket akarunk összehasonlítani:
int x, y;
if (X < y) {
L __
Ez nagyon jól működik primitív típusok esetén, de kicsit bonyolódik a helyzet, amikor összetett objektumokkal kell foglalkoznunk. Arnikor például a számítógépünkön egy fájllistát nézünk meg, általában név szerint rendezve látjuk a fájlokat. Előfordulhat viszont, hogy a létrehozás dátuma, az utolsó módosítás dátuma vagy éppen fájltípus szerinti sorrendben szetetnénk látni őket.
Az is fontos, hogy különböző rendezéseket támogassunk anélkül, hogy teljesen új algoritmust kellene írnunk. És itt jönnek be a képbe az összehasonlítók. Az összehasonlító felel azért, hogy adott rendbe állitsa az objektumokat, így könnyen előfordulhat, hogy a fájlok rendezésekor külön összehasonlítónk lesz a fájlnevekre, a fájltípusokra és megint másik a módosítás idejére. Ezek az összehasonlítók teszik lehetővé az egyszerű rendezési algoritmusok számára, hogy különbözőképpen rendezzék az objektumlistákat.
Ez jó példa az érdekek szétválastfása néven ismert fontos tervezési elvre. Ebben az esetben a szétválasztott érdekek a következők: hogyan hasonlítsunk össze két objektumot (összehasonlító), és hogyan rendezhetünk hatékonyan nagy objektumlistát (algoritmus). Ezáltal lehetőségünk van az algoritmus használhatóságát kiterjeszteni olyan összehasonlítók beszúrásával, amelyekre eredetileg nem is gondoltunk, és ugyanazt az összehasonlítót több algoritmusmegvalósításban is felhasználhatjuk, hogy összevethessük a teljesítményüket.
Összehasonlitó műveletek
Az összehasonlító egyeden műveletből áll, amellyel két objektum egymáshoz viszonyított sorrendje határozható meg. Visszatérési értéke negatív egész, nulla vagy pozitív egész érték lehet attól függően, hogy az első argumentum kisebb, egyenlő vagy nagyobb-e, mint a második. Ha valamelyik objektum típusa miatt az összehasonlítás nem végezhető el, cl asscastExcepti on kivételt dob.
137
Alapvető rendezés
Az összehasonlftó interfész
Az összehasonlító nagyon egyszerű: egyetlen metódusa van, amellyel megállapítható,
hogy az első objektum kisebb, egyenlő vagy nagyobb-e, mint a másoclik. A követke
ző kód a comparator interfészt muta�a be:
public interface Comparator { public int compare(Object left, object right);
}
Az összehasonlító műveletnek két argumentuma van: l e ft és ri g h t. Azért neveztük
őket így, mert ebben az összefüggésben leginkább két primitív érték összehasonlítá
sának bal és jobb oldalára hasonlitanak. Ha a compare meghívásakor a bal megelőzi
a jobbot (left < right), az eredmény nullánál kisebb egész szám (általában -1); ha a
bal a jobb után következik (left > right), az eredmény nullánál nagyobb egész szám
(általában l); ha peclig a bal és a jobb egyenlő, az összehasonlítás eredménye nulla.
Néhány szabványos összehasonlftó
A sok egyecli összehasonlító mellett, amelyeket majd létre fogunk hozni, létezik né
hány szabványos összehasonlító is, amelyek nagymértékben leegyszerűsítik alkalma
zásunk kódját. Mindegyiknek egyszerű az elmélete és a megvalósítása, mégis igen
hasznosak a könyv későbbi részében tárgyalt bonyolultabb algoritrnusokhoz.
A természetes összehasonlító alkalmazása
Sok adattípus, különösen az olyan primitívek, mint a sztringek, az egész számok stb.,
rendelkeznek természetes rendezési sorrenddel: az A megelőzi a B-t, a B megelőzi a
C-t, és így tovább. A természetes összehasonlító egyszerűen egy olyan összehasonlí
tá, amely az objektumoknak ezt a természetes rendezését támogatja. Látni fogjuk,
hogy lehetséges egyetlen olyan összehasonlító létrehozása, amellyel bármely természetes
rendezéssei rendelkező objektumot rendezhetünk egy Java-nyelvbeli konvenció alapján.
A Java-nyelvben meg van valósítva a Compar ab l e elv: ez egy olyan interfész, amelyet
bármely osztály megvalósíthat, és amely biztosí� a a természetes rendezési sorrendet.
Az összehasonlítható interfész
A compar ab l e interfész egyszerű, az itt bemutatott egyetlen metódusból áll:
public interface comparable { public int compareTo(object other);
}----�--------------------------�------------�
138
Az összehasonlítókról
A comparator-hoz hasonlóan ez is rendre negatív egész, pozitív egész vagy nulla
visszaadásával jelzi, hogy az egyik objektum a másik elé vagy mögé kerül, illetve a
két objektum egyenlő. A comparator és a compar ab l e objektumok között az a kü
lönbség, hogy a comparator két értéket hasonlít össze egymással, mig a Comparable
objektum egy másik objektumot vet össze önmagával.
Időnként előfordulhat, hogy saját osztályunkkal szeretnénk megvalósíttatni a Com
parab l e objektumot, hogy legyen természetes rendezési sorrendjük A Person osztályt
például definiálhatjuk úgy, hogy név szerint legyen rendezhető. A tény, hogy ez az elv
megjelenik a szabványos Java-nyelvben, lehetővé teszi a típusok természetes rendezési
sorrendjére alapuló általános Comparator létrehozását. Megalkothatunk olyan compa
rator összehasonlítót, amely bármely olyan osztályra működik, amelyben meg van va
lósítva a Compar ab l e. Azáltal, hogy a javaJang csomag számos általánosan használt
osztálya megvalósí* ezt az interfészt, hasznos kiindulási összehasonlítóvá válik.
Amikor a Natural comparator kívánt viselkedésére gondolunk, látha�uk, hogy há
rom lehetséges helyzetet kell kezelnünk: az összehasonlítás minden lehetséges ered
ményére egyet. Már tudjuk, hogy a Java-sztringek megvalósí�ák a compar ab l e össze
hasonlítót, vagyis a sztringeket használhatjuk tesztadatként. A következő gyakorlófel
adatban a Natural comparator összehasonlítót teszteljüK, majd valósí�uk meg.
Gyakorlófeladat: a természetes összehasonlitó tesztelése
Először azt ellenőrizzük, hogy negatív egész eredményt kapunk-e, ha rendezéskor a
bal argumentum a jobb elé esik:
pub l i c voi d testLessTh.im () .. {
assertTrue(Naturalcomparator.INSTANCE.compare("A", "B")< O); }
Ezután meghatározzuk, hogy pozitív eredményt kapunk-e, ha rendezéskor a bal ar
gumentum a jobb mögé kerül:
public void testGreaterThan() { assertTrue(Naturalcomparator.INSTANCE.compare("B", "A") > O);
}
Végül megállapítjuk, hogy nulla-e az eredmény, ha a két argumentum egyenlő.
pub li c voi a testEqualToO { assertTrue(NaturalComparator.INSTANCE.compare("A", "A")== 0);
}.
139
Alapvető rendezés
A megvalósitás működése
A teszteset a fent azonosított mindhárom esetre rendelkezik egy tesztmetódussal. Minden tesztmetódus feltételezi, hogy a Naturalcomparator egyetlen statikus példányban létezik, és nincs szükség a példányosítására. Mindhárom tesztmetódus két egyszerű karaktersztringet használ tesztadatként annak ellenőrzésére, hogy a Natu ra 1-
comparator a vártnak megfelelően viselkedik.
Gyakorlófeladat: a természetes összehasonUtá megvalósítása
Mivel a Naturalcomparator hasonlítónak nincs állapota, csak egy példányra van szükségünk belőle:
public f"inal ciass Naturalcomparator implements comparator { public static final Naturalcomparator INSTANCE
}
new Naturalcomparator();
private Naturalcomparator() { }
Ennek biztosításához a konstruktort private elemként jelöljük, így elkerüljük a példányosítást, helyette publikusan elérhető statikus változó tárolja az osztály egyetlen példányát. Ügyelnünk kell arra is, hogy az osztálynak fi na l jelölése legyen, hogy megakadályozzuk a véletlenszerű kibővítést.
Ezután megvalósí�uk a compare O metódust. Mivel ezt a Compar ab l e interfészeit valósítjuk meg, a tényleges munka legnagyobb részét maguk az argumentumok fogják végrehajtani, így a megvalósítás szinte triviálissá válik:
public int compare(Object left, object right) {
}
assert left l= null : "a 'left' (bal oldal) nem lehet NULL"; return ((Comparable) left).compareTo(right);
Miután először ellenőriztük, hogy nem NULL argumentumot kaptunk, a baloldali argumentum lesz a Camparab l e, és a jobboldali argumentumot átadva meghívjuk rá a definiált com pa re To O metódust.
Sohasem ellenőrizzük, hogy a baloldali argumentum ténylegesen a compar ab l e
egy példánya-e, mivel a comparator interfész lehetővé teszi cl asscastExcepti on
kivétel dobását, vagyis az átadást ellenőrzés nélkül is végrehajthatjuk.
140
Az összehasonlítékról
A megvalósitás müködése
A Naturalcomparator arra lett kialakítva, hogy két objektumot hasonlítson össze, amelyek a Compar ab l e interfész megvalósításai. Sok beépített Java-objektum valósítja meg ezt az interfészt, és az általunk létrehozott objektumok is szabadon megvalósíthatják A kódnak csupán a bal oldali operandus t kell Compar ab l e interfészként megadni, hogy az meg tudja hívni a compareTo() metódust, a jobb oldali operandust átadva a bal oldali operandusnak, önmagával való összehasonlításra. Az összehasonlítóknak itt tulajdonképpen nem kell semmiféle összehasonlítási logikát megvalósítaniuk, rnivel azt maguk az objektumok kezelik.
A forditott összehasonlitó alkalmazása
Gyakran szeretnénk a dolgokat fordított sorrendben rendezni. Ha például a számítógépünkön lévő fájllistát nézzük, előfordulhat, hogy � legkisebbtől a legnagyobbig vagy fordított sorrendben, a legnagyobbtól a legkisebbig rendezve szeretnénk látni a fájlokat. A korábban bemutatott Naturalcomparator fordítottjának előállítására az egyik módszer az, hogy lemásoljuk a megvalósítást, és a compare() metódust a következőképpen valósítjuk meg:
public int compare(Ob]ect-left, Object right)-{ assert right l= null : "a 'right' (jobb oldal) nem lehet NULL"; return ((Comparable) right).compareTo(left);
} ------......-..---..1
Felcseréljük a jobb és a bal oldali argumentumot: megerősí�ük, hogy a jobb oldali argumentum nem null, majd a bal oldali argumentumot átadjuk a compare() metódusnak.
Bár kiválóan működik ebben a konkrét esetben, ez az elképzelés nem igazán kiterjeszthető. Az összetett típusoknál, mint arnilyen a Person vagy a Fi l e, végül mindig két összehasonlítót írunk: egyet a növekvő és egyet a csökkenő rendezéshez. ·
Valarnivel jobb elképzelés, amelyet a következő feladatban meg is valósítunk, hogy írunk egy általános összehasonlítót, amely egy másik összehasonlítót tartalmaz, és megfordítja az eredményt. Így minden rendezendő komplex típusunkra csupán egyetlen összehasonlítót kell írnunk. Az általános Reversecomparator használható az ellentétes irányú rendezésre.
Gyakorlófeladat: a fordított összehasonlító tesztelése
A Naturalcomparator összehasonlítóhoz hasonlóan itt is három esetet kell kezelnünk az összehasonlítás lehetséges eredménytípusainak megfelelően. Ebben a tesztben a korábban definiált Natural comparator segítségével hasonlí�uk össze az egyszerű sztringértékeket.
141
Alapvető rendezés
Ha a bal oldali argumentum szokványos esetben a jobb oldali elé kerülne, akkor
a Reversecomparator esetén pont az ellenkezőjét szetetnénk elérni; azaz ha az alap
összehasonlitó negatív egésszel tér vissza, jelezvén, hogy a bal oldali argumentum ki
sebb a jobb oldalinál, akkor biztosítanunk kell, hogy a Reversecomparator által visz
szaadott eredmény pozitív egész legyen:
public void testLessThanBecomesGreaterThan() { Reversecomparator comparator =
new Reversecomparator(Naturalcomparator.INSTANCE);
assertTrue(comparator.compare("A", "B")> 0); '}.
Ha az alap összehasonlitó pozitív egésszel tér vissza, mivel a bal oldali argumentum
normál esetben a jobb oldali után következne, akkor az eredménynek negatív egész
nek kell lennie:
public void testGreaterThanBecomesLessThan() { Reversecomparator comparator =
new Reversecomparator(Naturalcomparator.INSTANCE);
assertTrue(comparator.compare("B", "A") <O); }
Ha a két argumentum egyenlő, az eredménynek nullának kell lennie:
public void testEqualsRemainsunchanged() { Reversecomparator comparator =
new Reversecomparator(Naturalcomparator.INSTANCE);
assertTrue(comparator.compare("A", "A")== 0); }
A megvalósitás müködése
Az előző kód a Reversecomparator objektumokat példányosítja, majd átadja őket a
Naturalcomparator objektumnak, amelynek az összehasonlitási logika delegálható.
Az első két tesztmetódus ezután tesz valamit, ami értelmetlen érvényességvizsgálat
nak tűnhet: tudjuk, hogy az A megelőzi a B elemet, de ebben az esetben éppen az el
lenkezője igaz; az első tesztmetódus erről meg is győződik. A második tesztmetódus
is hasonlóan visszafelé működik. A végső tesztmetódus biztosítja, hogy az egyenlő
objektumok a Reversecomparator használata során is egyenlők maradjanak.
A következő gyakorlófeladatban megvalósí�uk a Reversecomparator összeha
sonlitót.
142
Az összehasonlítékról
Gyakorlófeladat: a forditott összehasonUtá megvalósítása
Valósítsuk meg az általános Reversecomparator összehasonlítót egy néhány soros kóddal:
package com.wrox.algorithms.sorting;
public class Reversecomparator implements comparator { private final comparator _comparator;
l
public Reversecomparator(comparator comparator) {
}
assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
Természetesen a Comparator interfész megvalósításával kezdünk, és definiálunk egy olyan konstruktort, amely azt az alap comparator objektumot várja, amelynek végül a compare meghívását delegáljuk
Ezután következik a compare tényleges megvalósítása:
public int compare(Öbject lef�bject right)- { return _comparator.compare(right, left);
l
A megvalósitás müködése
Első ránézésre a kód elég ártalmatlannak tűnik, egyszerűen csak az alap összehasonlítónak delegálunk; viszont ha jobban megnézzük, látha:tjuk, hogy a két argumentumot átadás előtt megfordítottuk Ha a Reversecomparator (A, B) paraméterekkel lenne meghívva, akkor az alap összehasonlítónak a (B, A) lenne átadva, és így pontosan ellentétes eredményt kapnánk.
Mivel egyik argumentum egyetlen attribútumával sem kell foglalkoznunk, ez a megoldás teljesen magától értetődő; csupán egyszer kell megvalósítanunk, és minden esetre megoldást kapunk. És most elkezdhetjük felépíteni első rendezőalgoritmusunkat: a buborékrendezés algoritmusát.
A buborékrendezésről
A buborékrendezés algoritmusának megvalósítása előtt definiálnunk kell néhány tesztesetet, amelyeket megvalósításunknak sikerrel kell vennie. Mivel minden rendezőalgoritmusnak ugyanazon az alapvető teszten kell megfelelnie (vagyis bizonyítania kell, hogy helyesen rendezi sorba az objektumokat), az egységtesztekhez létrehozunk
143
Alapvető rendezés
egy alaposztályt, amelyet majd kiterjeszthetünk a konkrét alkalrnazásokra. Minden algoritmus egy interfészt valósít meg, hogy könnyen helyettesíthetők legyenek. Ez azt jelenti, hogy egyeden tesztesettel bármely algoritmus alapvető funkcióit bizonyítani lehet, akár még azt is, amelynek gondolata eddig még fel sem merült!
Gyakorlófeladat: a buborékrendezés végrehajtása
Képzeljük el, hogy egy családi összejövetelen vagyunk, ahol mindenkiről fényképet szeretnénk készíteni. Úgy döntünk, hogy a családtagokat kor szerint rendezzük sorba, a legfiatalabbtól kezdve a legidősebbig, de jelenleg véledenszem elrendezésben vannak, a 6.1. ábra szerint.
6.1. ábra. Véletlenszerűen elrendezett családtagok
Ahhoz, hogy a buborékrendezést alkalmazzuk erre a problémára, összpontosítsuk figyelmünket a bal szélen található két családtagra. Kérdezzük meg, melyikük az idősebb. Ha a jobb oldalon álló az idősebb, akkor nem csinálunk semmit, mert egymáshoz viszonyítva rendezve vannak. Ha a bal oldali az idősebb, kérjük meg őket, hogy cseréljenek helyet. A 6.2. ábrán a család látható, miután megtörtént az első helycsere.
6.2. ábra. Megto"rtént az első helJesere
Most egy hellyel továbbhaladunk a sorban, és a második és a harmadik embert kérdezzük meg. A másodikat már összehasonlítottuk az elsővel, és éppen most készülünk összehasonlítani a harmadikkal. Megismételjük az előző eljárást: megkérdezzük, melyikük az idősebb, és helyet cseréltetünk velük, ha nem megfelelő sorrendben vannak.
Mi történik, amikor elérünk az utolsó párhoz, és minden szükséges helycserét végrehaj tunk? A 6.3. ábra az első sorozat után mutatja a családot.
144
Az összehasonlítékról
Q g 6.3. ábra. A család az első sorozat után: a legidősebb áll a jobb szélen
A csoport még közel sem rendezett, de a legidősebb ember a sor végére, a végső, rendezett helyre került. Ez valószínűleg elég sok összehasonlításnak és helycserének tűnik
ahhoz, hogy egyetlen ember a helyére kerüljön, és így is van. A később bemutatásra kerülő algoritmusok hatékonysága sokkal jobb, de ezzel egyelőre ne foglalkozzunk.
A buborékrendezés következő sorozata pontosan . ugyanolyan, mint az előző, csák az utolsó emberre nem végzünk összehasonlítást, rnivel ő már a helyén van. Ismét a bal szélről indulunk, ugyanazt az összehasonlítás-helycsere folyamatot hajtjuk végre, míg csak a második legidősebb ember jobbról a második pozícióba nem kerül, ahogy a 6.4. ábra mutatja.
Q g 6.4. ábra. A második sorozat után a második legidősebb ember jobbról a második
poz!cióba került
Ezt folytatva szép lassan rendezzük az egyre kisebb és kisebb megmaradó csoportot, amíg az egész csoport rendezve nem lesz. És most elkészíthe�ük a fényképet (lásd a 6.5. ábrát).
Q g� 6.5. ábra. A te/jes csoport rendezye van
145
Alapvető rendezés
A ListSorter interfész
Sok más interfészhez hasonlóan a L istsorter interfész is rendkívül egyszetű, csu
pán egyetlen listarendező műveletből áll.
A Sort művelet bemenetként egy listát vár, kimenete pedig a lista rendezett vál
tozata. A megvalósítástól függően a visszaadott lista megegyezhet a bemenetként
kapott listával, vagyis egyes megvalósítások helyben rendezik a listát, mások új listát
hoznak létre.
A L istsorter interfész kódja:
public interface Listsorter { public List sort(List list);
}
Az AbstractlistSorter tesztelése
Bár még egyetlen rendezőalgoritmust sem írtunk, a következő gyakorlófeladatban
írunk egy tesztet, amely a L istsorter interfész bármely megvalósítására működik. A
példa absztrakt tesztosztályt használ, ami azt jelenti, hogy nem futtatható addig,
amíg ki nem terjesztjük a konkrét algoritrnusmegvalósításra. A teszt tényleges meg
valósítása az egyes meghatározott algoritmusokra triviális eredményként adódik.
Az AbstractL i stsorterTest a következő feladatokat haj�a végre:
• létrehozza a sztringek egy rendezetlen listáját,
• létrehozza ugyanezeknek a sztringeknek a rendezett listáját, amely a teszt el
várt eredményeként használható,
• létrehoz egy L istSorter objektumot (absztrakt metóduson keresztül),
• a L istsorter segítségével rendezi a rendezetlen listát,
• összehasonlítja a rendezett listát az elvárt eredménylistávaL
Gyakorlófeladat: az AbstractSorterTest tesztelése
A kódot kezdjük a két lista deklarálásával, és a setUp() megvalósításával töltsük fel
mindkettőt sztringekkel:
146
package com.wrox.algorithms.sorting;
import junit.framework.Testcase; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.lists.LinkedList; imRort com.wrox.algorithms.iteration.Iterator;
Az összehasonlítékról
publ ;� abstract cl ass AbstractL i stsorterTest extends Testcase { private List _unsortedList;
}
private List _sortedList;
protected void setUp() throws Exception { _unsortedList = new LinkedList();
_unsortedList.add("test"); _unsortedList.add("driven"); _unsortedList.add("development"); _unsortedList.add("is"); _unsortedList.add("one"); _unsortedList.add("small"); _unsortedList.add("step"); _unsortedList.add("for"); _unsortedList.add("a"); _unsortedList.add("programmer"); _unsortedList.add("but"); _unsortedList.add("it's"); _unsortedList.add("one"); _unsortedList.add("giant"); _unsortedList.add("leap"); _unsortedList.add("for"); _unsortedList.add("programming");
_sortedList = new LinkedList();
_sortedList.add("a"); _sortedList.add("but"); _sortedList.add("development"); _sortedList.add("driven"); _sortedList.add("for"); _sortedList.add("for"); _sortedList.add("giant"); _sortedList.add("is"); _sortedList.add("it's"); _sortedList.add("leap"); _sortedList.add("one"); _sortedList.add("one"); _sortedList.add("programmer"); _sortedList.add("programming"); _sortedList.add("small"); _sortedList.add("step"); _sortedList.add("test");
Ezután valósítsuk meg a tearoown () metódust, amely felszabadítja a két L i st ob
jektumra való hivatkozásokat:
protected vOid. tearoown(j'"' 'tllrowS Excepti őrt { _sortedList = null; _unsortedList = null;
l
147
Alapvető rendezés
Végül definiáljuk az absztrakt metódust, amely létrehozza a konkrét rendezőalgo
ritmust és magát a tesztet
protected abstract L i stsorter· createListsorter(Comparator comparator);
public void testListsortercanSortSampleList() {
}
Listsorter sorter =
createListsorter(NaturalComparator.INSTANCE); List result = sorter.sort(_unsortedList);
assertEquals(result.size(), _sortedList.size());
Iterator actual = result.iterator(); actual. first(); Iterator expected = _sortedList.iterator(); expected.first();
while (!expected .isoone()) { assertEquals(expected.current(), actual.current()); expected. next O ; actual.next();
}
A megvalósitás müködése
A tesztmetódus első két sora létrehozza a rendezőalgoritmus megvalósítását, és rende
zi vele a rendezeden listát. Természetes összehasonlítót adunk át, mert az elvárt ered
mény a sztringek természetes sorrendjében lett rendezve. A teszt nagyobbik része el
lenőrzi, megegyezik-e a rendezés eredménye az elvárt eredménylistával. Ezt úgy tesz
szük, hogy létrehozunk egy listaiterátort, és egymás után összehasonlí�uk az elemeket,
így biztosítva az elemenkénti pontos illeszkedést. Minden rendező algoritmusunknak
meg kell felelnie ezen a teszten, különben nem sok hasznát vesszük a gyakorlatban!
A következő gyakorlófeladatban létrehozunk egy tesztet konkrétan a buborék
rendezéses megvalósításunkra.
Gyakorlófeladat: a BubblesortListSorter tesztelése
Terjesszük ki az AbstractL i stSorterTest osztályt, és valósítsuk meg az absztrakt
createLi stsorter() metódust az itt bemutatott módon:
148
package com.wrox.algorithms.sorting;
public class BubblesortListsorterTest
J
extends AbstractListSorterTest { protected Listsorter createListsorter(Comparator comparator) {
return new BubblesortListSorter(comparator); }
Az összehasonlítékról
Csak ennyit kell tennünk, hogy elkészítsük a tesztet a B u bb l esortL istsorter számára. Természetesen az előbbi kód így még nem fordul le, mivel nincs Bubbl esort
L istsorter osztályunk; éppen ezt fogjuk most létrehozni. A következő gyakorlófeladatban megvalósí�uk a buborékrendezést.
A megvalósitás müködése
Annak ellenére, hogy egyeden metódust valósítottunk meg egyeden kódsorral, a lényeg az, hogy az előző kódban az Abstract L i stsorterTest osztályt terjesztettük ki. Az absztrakt osztály adja a tesztadatokat és számos tesztmetódust; csupán annyit kell tennünk, hogy megadjuk a L istsorter ezekben a tesztekben használandó megvalósítását, és mi éppen ezt tettük.
Gyakorlófeladat: a BubblesortListSorter megvalósítása
A buborékrendezéses algoritmus megvalósításának a következő tervezési feltételeket kell kielégítenie:
• valósítsa meg a L istsorter interfészt,
• egy összehasonlítót várjon, amivel meghatározhatja az objektumok sorrendjét,
• feleljen meg az előző szakaszban bemutatott egységteszten.
Miután ezeket az irányelveket helyre tettük, a konstruktorral kezdjük a megvalósítást, az itt bemutatottak szerint:
package com�rox.iilgori thms. sort i n g;
import com.wrox.algorithms.lists.List;
public class BubblesortListSorter implements ListSorter { private final comparator _comparator;
public BubblesórtListSorter(Comparator comparator) {
assert comparator != null : " a 'comparator' nem lehet NULL "; _comparator = comparator;
}
Most magát a buborékrendezés algaritmusát kell megvalósítanunk Az algoritmus leírásából tudjuk, hogy több sorozatban megyünk végig az adatokon, és minden egyes sorozatban egy elem kerül a végleges helyére. Első dolgunk azt meghatározni, hogy hány sorozatra van szükségünk. Amikor az utolsó elem kivételével már minden elem a helyére került, akkor az utolsó elemnek már nincs hova 'mennie, tehát szükségszerűen az is a helyén van; vagyis eggyel kevesebb sorozatra van szükségünk, mint amennyi az elemek száma. A következő kódban ez a kül ső ci kl us nevet kapta.
149
Alapvető rendezés
Minden sorozatban minden elempárt összehasonlítunk, és ha (az általunk megadott összehasonlító megítélése szerint) nem megfelelő sorrendben vannak, fel is cseréljük őket. Ne feledjük viszont, hogy minden sorozatban egy elem a végleges helyére kerül, és azt a következő sorozatokban már nem kell vizsgálni. Ezért minden sorozat eggyel kevesebb ele�et vizsgál, mint az előtte lévő. Ha a lista elemszáma N, akkor az első menetben (N-1) összehasonlítást kell végeznünk, a másodikban (N-2) darabot, és így tovább. Ezért szabályazza a következő kód a belső ciklusában a l e ft
< (si ze - pass) feltétellel a végrehajtott ellenőrzések számát:
public List sort(List list) {
}
assert list != null : "a 'list' nem lehet NULL";
int size= list.size();
for (int pass = l; pass < size; ++pass) { ll külső ciklus for (int left= O; left < (size - pass); ++left) {
}
}
ll belső ciklus int right = left + l;
if (_comparator.compare(list.get(left), list.get(right)) > O) {
swap(list, left, right);
}
return list;
Az előző kód a megadott összehasonlítót használja annak eldöntésére, hogy a két vizsgált elem megfelelő sorrendben van-e. Ha nem, akkor a swap() metódus meghívásával javítja a relatív elhelyezkedésüket a listán. Íme a swap () kódja:
private void swap(List list, int left, int right) { object temp = list.get(left); list.set(left, list.get(right))
.;
list.set(right, temp);
}
A teszt fordítás és futtatás után csillagos ötöst érdemel. Ha biztosra akarunk menni, elhelyezhetünk egy szándékos hibát a teszt várt eredményében, és lefuttathatjuk újra: látha�uk, hogy a következő rendezőalgoritmusunk megvalósításakor bizony leblikunk
150
A kiválasztásos rendezés alkalmazása
A kiválasztásos rendezés alkalmazása
Tegyük fel, hogy a könyvespolcunkon összevissza sorakoznak az eltérő nagyságú könyvek. Édesanyánk látogatóba érkezik, és el akarjuk kápráztatui precizitásunkkal, tehát elhatározzuk, hogy méret szerint szépen sorba rendezzük a könyveket, a legnagyobbtól a legkisebbik. A 6.6. ábra mutatja a könyvespolcot, mielőtt hozzáfognánk a munkához.
6.6. ábra. Rendezetlen kónyvespolc
Nem valószínű, hogy ebben az esetben a buborékrendezés mellett döntenénk, mivel a sok helycsere csak időpocsékolás lenne. Nagyon sokszor kellene a polcról levenni és visszarakni a könyveket, ami túl sokáig tartana. Ebben a példában az elemek mozgatásának költsége az összehasonlitáshoz mérten nagy. A kiválasztásos rendezés sokkal jobb választás ebben az esetben, és hamarosan látni is fogjuk, hogy miért.
Először keressük meg a polcon a legnagyobb könyvet. Vegyük ki, mert ennek kell majd a könyvespolc bal szélére kerülnie. Ahelyett, hogy a polcon lévő összes könyvet jobbra tolnánk, hogy ennek helyet szorítsunk, vegyük ki azt a könyvet, amelyik ennek a helyén van. Természetesen a többi könyv is egy kicsit elmozdul, mert a könyvek nem egyforma vastagok, de mivel ennek ebben a szoftvermegvalósításban nincs jelentősége, figyelmen kívül hagyjuk. (Azáltal, hogy a könyveket így cseréljük fel, és nem csúsztatjuk oldalra, a meg\ralósítás instabillá válik; ezzel a témával a fejezet egy későbbi részében foglalkozunk majd, most nem kell törődnünk vele.) A 6.7.
ábra szemlélteti az első helycserét. A legnagyobb könyvet a helyén hagyva megkeressük a polcon a második legma
gasabbat. Ha megtaláltuk, helyet cserél a legnagyobb -könyvtől jobbra lévő könyvvel. Már két könyvünk rendezve van; ezekhez többet már nem kell hozzányúlnunk A 6.8.
ábrán látható a polc jelenlegi állása.
151
Alapvető rendezés
Q /
/ /
D �
"--...._
v
�
:/
6. 7. ábra. A legnagyóbb kijnyv most a bal szélső poifcióban van
6.8. ábra. A második legnagyobb kiitryv 17'.JJst a második helJen áll
A legnagyobb könyveket a helyükön hagyva a maradékban folytatjuk a legnagyobb könyv keresését, amelyet aztán minclig felcserélünk a már rendezett könyvektől egygyel jobbra található könyvvel. Minden alkalommal, amikor végignézzük a polcot, kiválasztjuk a soron következő könyvet, és a helyére rakjuk. Ezért lett az algoritmus neve kiválasztásos rendezés. A 6.9. ábra muta�a a polcot azután, hogy minden könyvet a helyére tettünk
Néha a nem rendezett könyvek közötti legnagyobb keresése közben azt tapasztaljuk, hogy az már a megfelelő helyen van, így nincs szükség helycserére. Látha�uk, hogy minden könyv áthelyezése után a rendezett könyvek sora nő, a rendezetleneké csökken, amíg az egész polc rendezett nem lesz. Minden könyv rögton a végleges helyére kerül, ahelyett hogy kis lépésekben haladnánk a cél felé (mint a buborékrendezésnél), ami megfelelő érv arra, hogy ezt az algoritmust használjuk ebben az esetben.
A kiválasztásos rendezés teszteléséhez felhasználhatjuk a buborékrendezés algoritmusánál végzett munkánk nagy részét. A következő gyakorlófeladatban egy tesztesetet hozunk létre, megvalósítjuk magát az algoritmust, majd ellenőrizzük, hogy sikerrel veszi-e a tesztet, vagyis helyes-e a megvalósításunk.
152
A kiválasztásos rendezés alkalmazása
6.9. ábra. A kiinyvespolc azután, ho!!J minden körryvet a megfolelő poifcióba raktunk
153
Alapvető rendezés
Gyakorlófeladat: a SelectionSortUstSorter tesztelése
A se l e ct i onSortL istsorter tesztje szinte pontosan megegyezik a buborékrende
zéses megfelelőjéveL Az absztrakt tesztdési esetet kiterjesz�ük, és példányosí�uk a
kiválasztásos rendezés megvalósítását:
package com. w ro x. al go ri thms .-sort i n g;
public class selectionsortListsorterTest extends AbstractListsorterTest {
}
protected Listsorter createListsorter(Comparator comparator) { return new selectionsortListsorter(comparator);
}
A következő gyakorlófeladatban megvalósítjuk a se l e c ti onsortL istsorter osztályt.
A megvalósitás működése
Annak ellenére, hogy egyetlen metódust valósítottunk meg egyetlen kódsorral, a lé
nyeg az, hogy az előző kódban az AbstractL i stSorterTest osztályt terjesztettük ki.
Az absztrakt osztály adja a tesztadatokat és számos tesztmetódust, csupán annyit
kell tennünk, hogy megadjuk a L istsorter ezekben a tesztekben használandó meg
valósítását, és mi éppen ezt tettük.
Gyakorlófeladat: a SelectionSortUstSorter megvalósítása
Ez a megvalósítás is sokban megegyezik a buborékrendezéses megfelelőjéveL Ennek
is a L istsorter interfészt kell megvalósítania, a comparator fogadásával meghatá
roznia a rendezési sorrendet, és át kell mennie a fenti teszten. Hozzuk létre a követ
kező osztálydeklarációt és konstruktort:
154
package com.wrox.algorithms.sorting;
import com.wrox.algorithms.lists.List;
public class selectionsortListsorter implements Listsorter { private final comparator _comparator;
public SelectionsortListsorter(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
}
}
A kiválasztásos rendezés alkalmazása
A megvalósitás működése
A buborékrendezéshez hasonlóan a megvalósításnak belső és külső ciklusa is van, de vannak halvány különbségek, amelyek fölött könnyen átsiklunk, ha nem figyeljük meg alaposan a kódot. Először is a külső ciklus indextartománya nulla és (N-2) között van, nem pedig 1 és (N-1) között, mint a buborékrendezésnéL Figyeljük meg, hogy attól ez még ugyanúgy (N-1) menetet jelöl, de a kiválasztásos rendezés esetén arra tereli a figyelmet, hogy minden sorozatban egy adott "rést'' a megfelelő objektummal töltünk fel. Az első sorozatban például a cél az, hogy a megfelelő objektumot helyezzük a lista nulladik pozíciójába. A második menetben a cél az 1. pozíció betöltése, és így tovább. Itt is elegendő (N-1) sorozat, mivel az utolsó objektum a többi obj�ktum rendezésének következményeképpen természetszerűleg a megfelelő helyen köt ki.
A belső ciklusban egyeden helycsere sem történik, mint a buborékrendezésnéL Itt mindössze a legkisebb elem helyére kell emlékeznünk Amikor a belső ciklus véget ér, a legkisebb elem helyet cserél az éppen betöltendő résben található elemmel. Ez kissé eltér a könyvespolcos példától, amelyben a legnagyobbtól a legkisebbig rendeztük a könyveket, de az algoritmus éppúgy működne arra az esetre is. Tulajdonképpen csak betöl�ük a fejezet korábbi részében létrehozott Reversecomparator összehasonlítót
public List sort(Cist list)-{
}
}
assert list 1= null : "a 'list' nem lehet NULL";
int size = list.size();
for (int slot = O; slot < size - l; ++slot) { ll külsó ciklus int smallest = slot; for (int check = slot + l; check < size; ++check) {
}
ll belső ciklus if (_comparator.compare(list.get(check),
list.get(smallest)) < 0) { smallest = check;
}
swap(list, smallest, slot);
return list;
A swap () kiválasztásos rendezéses megvalósításában .is van egy kis eltérés a buborékrendezéses megvalósításhoz képest. Kibővítjük egy védőfeltételle� amely kihagyja azokat az eseteket, amikor egy rést önmagával kell felcserélni: ez az eset könnyen előfordulhat a kiválasztásos rendezés esetén, rníg buborékrendezésnél soha.
155
Alapvető rendezés
private void swap(List lEt, int if (left == right) {
return;
} Object temp = list.get(left); list.set(left, list.get(right)); list.set(right, teMp);
A beszúrásos rendezésről
A beszúrásos rendezés az az algoritmus, amelyet a kártyázók széles körben alkal
maznak a kezükben tartott kártyalapok rendezésére. Képzeljük el, hogy öt lapunk
van leosztva, arccal lefelé, amelyet a következő elvek szerint szeretnénk rendezni:
• Színek szerint különválasztjuk a lapokat a következő sorrendben: pikk, treff,
káró és kór.
• Minden színt növekvő sorrendbe rendezünk: ász, 2, 3 ... 9, 10, bubi, dáma,
király.
A 6.10. ábra mutatja a leosztott lapokat, arccal lefelé. Nincsenek rendezve, bár akár a
kívánt sorrendben is lehetnek. (Ha így van, az algoritmusnak akkor is le kell futnia.)
6.1 O. ábra. Ötiapos kostfás
Az első kártya felfordításával kezdjük. Mi sem egyszerűbb, mint egyetlen kártyalap
rendezése, tehát egyszerűen csak megfogjuk. Ebben az esetben ez egy káró hetes.
A 6.11. ábra muta�a az aktuális helyzetet: egyetlen rendezett kártya, és négy még
rendezetlenül, arccal lefelé.
Felvesszük a második kártyát. Ez ·a pikk bubi. Mivel tudjuk, hogy a pikk a káró
elé kerül, az aktuális kártyánktól balra szúrjuk be. A 6.12. ábrán látható az így kiala
kUlt helyzet.
156
o O� 6. 11. ábra. Az első kárrya önmagában is rendeif!e van
r;l7 lt.l•
6. 12. ábra. A második kárrya az első elé lett beszúrva
A beszúrásos rendezésről
Felvesszük a harmadik kártyát. Példánkban ez a treff ász. A kezünkben lévő rende
zett kártyákat nézve ennek a kettő közé kell kerülnie. A 6.13. ábra muta�a a kezünk
ben tartott lapok állását.
r--W � 1�1-J-1
6.13. ábra. A harmadik kárrya kó"zépre keriif
Q
J l A l :.. l.
• eJt
6.14. ábra. A két utolsó kárrya is be lett szúrva
157
Alapvető rendezés
A beszúrásos rendezés az adatokat két csoportra osztja: rendezett elemek és rende
zeden elemek. A rendezett elemek csoportja kezdetben üres, az összes elemet a ren
dezeden elemek csoportja tartalmazza. A rendezeden csoportból egyesével kivesz
szük az elemeket, és beszúrjuk a megfelelő pozícióba a rendezett elemek növekvő
csoportjában. A végén minden elem a rendezett csoportba kerül, és a rendezeden
csoport üres lesz. A 6.14. ábra szemlélteti, rni történik a két utolsó lap felvételekor.
A következő gyakorlófeladatban tesztesetet hozunk létre a beszúrásos rendezési
algoritmushoz. Ezután meg is valósítjuk, és ezzel végeztünk a fejezet három alapve
tő rendezési algoritmusávaL
Gyakorlófeladat: az /nsertionSortListSorter tesztelése
Ahogy a buborékrendezés és a beszúrásos rendezés esetén is tettük, az Abstract
L istsorter tesztesetet kiterjesz�ük a beszúrásos rendezési algoritmusra, a követke
zők szerint:
pacKage com.wrox.algorithms.sorting;
public class InsertionsortListSorterTest extends AbstractListSorterTest {
}
protected Listsorter createListsorter(Comparator comparator) { return new InsertionsortListsorter(comparator);
}
A megvalósitás müködése
Bár csak egyeden metódust valósítottunk meg egyeden kódsorral, a lényeg az, hogy
az előző kódban az AbstractL i stsorterTest osztályt terjesztettük ki. Az absztrakt
osztály adja a tesztadatokat és számos tesztmetódust; csupán annyit kell tennünk,
hogy megadjuk a L istsorter ezekben a tesztekben használandó megvalósítását,
amit meg is tettünk.
Gyakorlófeladat: az lnsertionSortListSorter megvalósítása
Mostanra már megismertük a rendezésialgoritmus-megvalósítások alapvető struktúrá
ját. A következő osztálydeklarációt és konstruktort alkalmazzuk az Insertionsort
L istsorter osztályra:
158
package com.wrox.algorithms.sorting;
import com.wrox.algorithms.lists.List; ifflPQrt com.wrox.algorithms.lists.LinkedList; i��rt com.wrox.algorithms.iteration.Iterator;
A beszúrásos rendezésről
puoilc class rnserfiönsortListsorter implements Lfstsorter { private final comparator _comparator;
public InsertionsortListsorter(comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
}
A megvalósitás működése
A sort() metódus megvalósítása nagyon eltér a fejezet korábbi részében bemutatott
két algoritmusétóL Ez az algoritmus nem helyben rendezi az objektumokat az adott
lista átrendezésével, hanem egy új, üres listát hoz létre, és az eredeti lista minden
egyes elemét rendezve illeszti be az eredménylistába.
Ráadásul az eredeti listát index szerinti elérés helyett iterátor segítségével dol
gozzuk fel, mivel nincs szükség az eredeti lista elemeinek közvetlen elérésére. Egy
szerűen csak egymás után feldolgozzuk őket, ami egy iterátor természetes feladata:
public Lis� sort(List lis�) t
}
assert list != null : "a 'list' nem lehet NULL";
final List result = new LinkedList();
Iterator it = list.iterator();
for (it.first(); !it.isoone(); it.next()) { int slot � result.size();
}
while (slot > 0) {
}
if (_comparator.compare(it.current(), result.get(slot - l)) >= 0) {
break; } --slot;
result.insert(slot, it.current());
return result;
Végül vegyük észre, hogy a belső ciklus whi l e ciklus, nem pedig for. A feladata, hogy
megtalálja az eredménylistában a megfelelő pozíciót a következő elem számára. Miu
tán megtalálta a megfelelő helyet (vagy lecsúszik az eredménylistáról), kilép a belső
ciklusbóL Ekkor az aktuális elemet beilleszti az eredménylistába. Az eredménylista
mindig teljesen rendezett; minden elem a már listában lévő elemekhez viszonyítva ke
rül helyére, így megmarad a teljes rendezett sorozat. Ez a példa l ánc o l t l i st át
használ az eredménylista tárolására, mert ez sokkal jobb a beszúrási művelethez.
159
Alapvető rendezés
Vegyük még észre azt is, hogy az algoritmus visszafelé keresi a listában a megfe
lelő pozíciót, nem előrehalad va. Ez nagyon nagy előny a rendezett vagy közel rende
zett objektumok esetén, mint a fejezet későbbi szakaszában, az Alapvető rendezési
algoritmusok összehasonlítása címú részben látni fogjuk. Ez az oka annak is, hogy
az algoritmus stabil; erről szól a következő szakasz.
A stabilitásról
Néhány rendezési algoritmus osztozik a stabilitás néven ismert érdekes tulajdonság
ban. Az alapelv illusztrálásához nézzük meg a 6.1. táblázatban látható, keresztnév
alapján rt::ndezett személyneveket.
Keresztnév Vezetéknév
Albert Smith
Brian Jackson
Davi d Barn es
John Smith
John Wilson
Mary smith
Tom Barnes
Vince De Marco
walter clarke
6. 1. táblázat. Keresztnév alapján rendezett lista
Most tegyük fel, hogy ugyanezeket az embereket a vezetéknevük alapján szeretnénk
rendezni. A 6.1. tábla listájában van néhány igen gyakori vezetéknév, például Smith
és Barnes. Mit várunk, rni fog történni az azonos vezetéknevú emberek sorrendjé
vel? Arra szárrúthatunk, hogy az azonos vezetéknevú személyek egymáshoz viszo
nyítva ugyanolyan sorrendben maradnak, ahogyan az eredeti listában szerepeitek -
vagyis keresztév alapján rendezve az azonos vezetéknevűek csoportjában. Ez a sta
bilitás. Ha egy rendezőalgoritmus megőrzi a közös rendezési kulccsal rendelkező
elemek relatív sorrendjét, akkor azt stabil algoritmumak nevezzük.
A 6.2. táblázat mutatja a példában szereplő személyek vezetéknév szerinti stabil
rendezését.
160
Az alapvető rendezési algoritmusok összehasonlítása
Keresztnév Vezetéknév
David Barnes
Tom Barn es
walter clarke
Vince De Marco
B ri an Jackson
Albert smith
John Smith
Mary smith
John Wilson
6.2. táblázat. A 6.1. tábláiflt stabil vezetéknév-rendezése
A három eddig bemutatott megvalósításból kettő, a buborékrendezés és a beszúrá
sos rendezés stabil. A kiválasztásos rendezés megvalósítása könnyen stabillá tehető.
A későbbi fejezetekben szereplő, kifinomultabb rendezőalgoritmusok némelyike
gyorsabb lehet, mint a három itt bemutatott, de gyakran nem őrzik meg a stabilitást,
és ezt figyelembe kell vennünk, ha fontos a konkrét alkalmazásunk számára.
Az alapvető rendezési algoritmusok összehason litása
Most, hogy már több rendezőalgoritmust is láttunk működés közben, és tudjuk, milyen
könnyen beilleszthe�ük bármelyik megvalósítást, amely támoga* a L istsorter inter
fészt, felmerülhet a kérdés, hogy mikor melyiket használjuk. Ez a szakasz nem elméleti
vagy matematikai megközelítésből, hanem gyakorlati szempontokból hasonlí�a össze
az egyes algoritmusokat. Nem célja, hogy határozott feltétellistát adjon az algoritmus
kiválasztására, inkább azt muta�a be, hogyan használha�uk fel az összehasonlító elem
zést akkor, amikor rendszerünk építése közben megvalósítási döntéseket kell hoznunk.
Emlékezzünk vissza a fejezet bevezetőjére, ahol említettük, hogy a rendezőalgo
ritmusok két alapvető lépést ismételgetnek összehasonlí�ák és átmozga�ák az ele
meket. Ez a leírás felméri a három rendezőalgoritmus viselkedését az első műveletre
nézve, és próbára teszi az algoritmusok sebességét azáltal, hogy sokkal nagyobb
adathalmazt használ, rnint rni a megvalósításukkor. Ez azért fontos, mert relatív tel
jesítményük minden eltérése sokkal élesebben kirajzolódik, ha nagyobb adathalmazt
használunk. Az is fontos, hogy minden algoritmus különböző elrendezésekben kapja
meg a bemeneti adatokat, a következők szerint:
161
Alapvető rendezés
• már rendezve (a legjobb eset),
• már rendezve, de éppen a kívánt sorrenddel ellentétes sorrendben (legrosz
szabb eset),
• véletlenszerű sorrendben (tipikus eset).
Ha minden tesztesetben ugyanazt a bemeneti adathalmazt adjuk minden algorit
musnak, akkor megalapozott döntést hozhatunk a relatív erősségeikről, életszerű szi
tuációban. Első feladatunk összegyűjteni, hogy hány összehasonlítást hajtanak végre.
CallCountinglistComparator
A rendezőalgoritmusokban minden összehasonlítást a megfelelő összehasonlíróik
hajtanak végre. Ha össze akarjuk számolni, hogy az összehasonlító compare() me
tódusa hányszor lesz meghívva, kissé módosítanunk kell az összehasonlíták kódját,
hogy emlékezzenek a hívások számára. Vagy megírhatjuk az összehasonlítókat egy
közös alaposztály kiterjesztéseként, és abban helyezhetjük el az összeszámlálást. Vi
szont hogy a már megírt kódunk nagy részét újrahasznosíthassuk, a hívásszámláló
funkciót bármely már meglévő összehasonlítóba beépíthetjük, rnint ahogyan a
Reversecomparator esetében tettük:
162
public final class cailcountingcomparator implements comparator { private final comparator _comparator;
}
private int _callcount;
public Callcountingcomparator(comparator comparator) {
}
assert comparator != null : "a 'comparator' nem lehet NULL";
_comparator = comparator; _ca ll count = O;
public int compare(Object left, object right) { ++_callcount; return _comparator.compare(left, right);
}
pub l i c i nt gé tea ll co unt O { return _callcount;
}
Az alapvető rendezési algoritmusok összehasonlítása
Éppúgy, ahogyan a Reversecomparator, a call counti ngcomparator is elfogadja a konstruktorában bármely másik comparator metódust. A ca ll counti ngcomparator
a tényleges összehasonlítást ennek az alap összehasonlítónak delegálja, miután eggyel megnövelte a lúvásszámlálót. Már csak annyi van hátra, hogy a rendezés befejezésekor a getCallCount() metódussal lekérdezzük a lúvásszámláló eredményét.
A ca ll counti ngcomparator segítségével most már megépíthetjük a programot, hogy a legjobb, a legrosszabb és a tipikus esethez tartozó tesztadatokkal futtassuk a rendezőalgoritmusokat, és összegyűjtsük az eredményt.
ListSorterCallCountingTest
Bár ez nem kifejezetten egységteszt, a programot azért írtuk, hogy az algoritmusokat ]Unit tesztesetként futtathassuk, mivel minden algoritmusra valamennyi telepítést igényel, és sok diszkrét forgatókönyv alapján kell futtatni. Először létrehozzuk a tesztosztályt, egy konstanst az adatlista méretére és példányváltozókat a legjobb, a legrosszabb és a tipikus esethez tartozó adathalmazra. Szükségünk van még egy példányváltozóra, amely az előző szakaszban létrehozott ca ll counti ngcomparator hivatkozását tárolja:
package ..
com. w r ox:- a l go ri ttiiiis . so r ti n g;
import junit.framework.Testcase; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.lists.ArrayList;
public class ListSortercallcountingTest extends Testcase { private static final int TEST_SIZE = 1000;
private final List _sortedArrayList = new ArrayList(TEST_SIZE); private final List _reverseArrayList = new ArrayList(TEST_SIZE); private final List _randomArrayList = new ArrayList(TEST_SIZE);
private callcountingcomparator _comparator;
}
Most összeállítjuk a tesztadatokat. A legjobb és a legrosszabb esetekre 1 és 1000 közé eső értékű Integer objektumokkal töltjük fel a megfelelő listát Az átlagos esetre véletlenszerűen generálunk számokat ebben a tartományban. a Natural comparator
becsomagolásával létrehozzuk a lúvásszámláló összehasonlítót is. Ez azért működik, mert a java.lang.Integer támogatja a Compara�le interfészt, mint ahogyan a korábbi példákban a sztringek is tették:
163
Alapvető rendezés
protected void setup() throws Exception { _comparator =
}
new callcountingcomparator(Naturalcomparator.INSTANCE);
for (int i = l; i < TEST_SIZE; ++i) { _sortedArrayList.add(new Integer(i));
}
for (int i = TEST_SIZE; i > 0; --i) {
_reverseArrayList.add(new Integer(i));
}
for (int i = l; i < TEST_SIZE; ++i) {
_randomArrayList.add(new Integer((int)(TEST_SIZE *
Math.random())));
}
Az algoritmusok legrosszabb esetbeli futtatásához hozzuk létre a megfelelő L i st
sorte r megvalósírást és rendezzük vele setUp() metódus által előállitott fordítva
rendezett listát. A következő kódban van egy metódus, amely ezt mindhárom algo
ritmusra megteszi. Hogyan működik? Ha a fordítva rendezett lista példányváltozó,
és először a buborékrendezés algoritmusával rendezzük, hogyan lehet még a követ
kező algoritmus induláskor is fordítva rendezett? Ez az egyik oka annak, hogy ]Unit
segítségével strukturáltuk ezt az illesztőprogramo t. A ]Unit az egyes tesztmetódusok
számára az illesztőprogram-osztály külön példányát hozza létre, így minden metó
dusnak lényegében megvan a saját fordítva rendezett listapéldánya, és a setUp() kü
lön fut le mindegyikre. Ez távol tar� a egymástól a teszteket:
public void testworstcaseBubblesort() {
}
new BubblesortListsorter(_comparator).sort(_reverseArrayList);
reportcalls(_comparator.getcallcount());
public void testworstcaseselectionsort() {
}
new SelectionsortListsorter(_comparator).sort(_reverseArrayList);
reportcalls(_comparator.getcallcount());
public void testworstcasernsertionsort() {
}
new InsertionsortListsorter(_comparator).sort(_reverseArrayList);
reportcalls(_comparator.getcallcount());
A kimenet létrehozásához minden metódus a r epo r tea ll s () metódust használja,
amelyet a fejezet későbbi részében mutatunk be. Most három hasonló metódus kö
vetkezik a legjobb eset forgatókönyvére, amelyben minden algoritmusnak a setUp()
által létrehozott rendezett listát kell rendeznie:
164
Az alapvető rendezési algoritmusok összehasonlítása
public void test8estcasefii:d)l:Jlesort0�{
}
�w aubblesortListSOrter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount());
public void testsestcaseselectionsort() { new selectionsortListsorter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount());
}
public void testBestcaseinsertionsort() { new InsertionsortListsorter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount());
}
Még három metódust hozunk létre a véletlenszerűen generált számlistát használó ti
pikus eset tesztelésére:
pubfi C voi a testAverageCaseBubol esort()-{
}
new BubblesortListsorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount());
public void testAveragecaseselectionsort() { new SelectionsortListsorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount());
}
public void testAveragecaseinsertionsort() { new InsertionsortListSorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount());
}
Végül definiáljuk a reportcalls() metódust, amely a korábban definiált forgató
könyvekre hozza létre a kimenetet:
private voidreportcalls CTnt�callcounü { system.out.println(getName() +
":
" + calleount + " hívás");
l
Ez az egyszerű kód egyetlen érdekes pontot tartalmaz. A getName() metódust al
kalmazza, amelyet a ]Unit Testcase szuperosztálya biztosít a forgatókönyv nevének
kiíratására. A program által a legrosszabb esetre létrehozott kimenet itt látható:
testworstcaseaubblesort: 499500-hívás testworstcaseselectionsort: 499500 hívás testworstcaseinserti.onsort: 499500 hív:ás
165
Alapvető rendezés
Mint látható, mindhárom algoritmus pontosan ugyanannyi összehasonlítást hajtott
végre a fordítottan rendezett ista rendezésekori Ebből azonban még ne gondoljuk,
hogy ugyanannyi időbe telik a futásuk is; itt most nem mérjük a sebességet. Mindig
vigyázzunk, nehogy túl messzemenő következtetéseket vonjunk le az ilyen egyszerű
statisztikákból! Mindent egybevetve nagyon érdekes így együtt látni a három algo
ritmus eredményét erre a forgatókönyvre.
A következő számok a legjobb esetre vonatkoznak:
testBestcaseBubbiesort: 49850i hívás testBestCaseselectionsort: 498501 hívás testBestcaseinsertionsort: 998 hívás
Ismét csak érdekes eredményeket kaptunk. A buborékrendezés és a kiválasztásos
rendezés pontosan ugyanannyi összehasonlítást végzett, a beszúrásos rendezés vi
szont sokkal kevesebbet. Talán hasznos lenne újra átnézni a beszúrásos rendezés
megvalósítását, hogy fény derüljön az okára.
A következő számok a tipikus esetre vonatkoznak:
testAverageCaseBubblesort: 498501 nívás testAveragecaseselectionsort: 498501 hívás testAveragecaseinsertionsort: 262095 hívás
A buborékrendezés és a kiválasztásos rendezés megint pontosan ugyanannyi össze
hasonlítást végzett, a beszúrásos rendezésnek ugyanakkor hozzávetőleg feleannyi
összehasonlításra volt szüksége a feladat végrehajtás ához.
Az algoritmus-összehasonlitásról
Levonhatunk néhány következtetést a most végrehajtott összehasonlító elemzésből, de
túl sokat azért nem szabad. Ahhoz, hogy igazán megértsük a viselkedésükben rejlő kü
lönbségeket, újab b forgatókönyveket is meg kellene vizsgálnunk, például az alábbiakat:
• Számszerűsítsük, hány objektumot mozgat meg a rendezés!
• Használjuk a L i nkedL i st és ArrayList megvalósításokat is a tesztadatokral
• Minden forgatókönyvnél mérjük az időt is!
Az elemzés korlátait észben tartva a következő észrevételeket tehetjük.
166
• A buborékrendezés és a kiválasztásos rendezés mindig pontosan ugyanany
nyi összehasonlítást végez.
Összefoglalás
• A buborékrendezés és a kiválasztásos rendezés esetén a szükséges összehasonlítások száma független a bemeneti adat állapotától.
• A beszúrásos rendezés esetén szükséges összehasonlítások száma nagyon érzékeny a bemeneti adat állapotára. A legrosszabb esetben ugyanannyi öszszehasonlítást igényel, mint a másik két algoritmus. A legjobb esetben a bemeneti adat elemszámánál kevesebb összehasonításra van szüksége.
Talán a legfontosabb elem az, hogy a buborékrendezés és a kiválasztásos rendezés nem érzékeny a bemeneti adat állapotára. Éppen ezért tekinthetjük őket "letámadásos" algoritmusoknak, rníg a beszúrásos rendezés alkalmazkodó, hiszen kevesebb munkát végez, ha kevesebb munkára van szükség. Ez az oka annak, hogy a gyakorlatban a beszúrásos rendezést általában jobban kedvelik, mint a másik két algoritmust.
Összefoglalás
A fejezet legfontosabb megállapításait az alábbiakban foglaljuk össze.
• Megvalósítottunk három egyszerű rendezési algoritmust (buborékrendezés, kiválasztásos rendezés és beszúrásos rendezés) és a hozzájuk tartozó egységteszteket annak bizonyítására, hogy a vártnak megfelelően működnek.
• Megismertük az összehasonlíták elvét, többet meg is valósítottunk, például a természetes összehasonlítót, a fordított összehasonlítót és a hivásszámláló összehasonlító t.
• Megnéztük a három algoritmus összehasonlító vizsgálatát, hogy megalapozott döntéseket hozhassunk mindegyikük erősségeit és gyenge pon� ait illetően.
• A stabilitás elve a sztringek viszonylatában szintén előkerült
Most, hogy a fejezet végére értünk, már tisztában kelllennünk a rendezés fontosságával és egyéb algoritmusok - mint például a keresőalgoritmusok - támogatásában betöltött szerepével. Ráadásul már tudjuk, hogy sokféleképpen megvalósíthatjuk az elemek sorba rendezésének egyszerű feladatát. A következő fejezet néhány bonyolultabb rendezőalgoritmust mutat be, amelyekkel hatalmas mennyiségű adatot tudunk bámulatosan jól rendezni.
167
Alapvető rendezés
Gyakorlatok
l. Írjunk egy tesztet, amellyel bizonyítjuk, hogy minden fenti algoritmus tudja rendezni a véletlenszerűen generált kétszerezett objektumok listáját.
2. Írjunk egy tesztet, amellyel bizonyítjuk, hogy a fejezetben bemutatott buborékrendezéses és beszúrásos rendezési algoritmus stabil.
3. Írjunk egy összehasonlitót, amely ábécérendbe tudja rendezni a sztringeket, és nem tesz különbséget a kis- és a nagybetűk között.
4. Írjunk egy illesztőprogramot annak eldöntésére, hány objektumot mozgatnak meg az egyes algoritmusok a rendezési művelet során.
168
HETEDIK FEJEZET
Fej lettebb rendezés
A 6. fejezetben háromféle rendezési algoritmussal ismerkedtünk meg, amelyek a kis-, illetve közepes méretű problémák megoldására alkalmasak. Bár ezeknek az algoritmusoknak egyszerű az alkalmazása, szükségünk van további rendezési algoritmusokra a nagyobb problémák kezelésére. A jelen fejezetben található algoritmusok megértése kissé több időt vesz igénybe, alkalmazásuk nagyobb ügyességet kíván, ugyanakkor a leghatékonyabb általános célú rendezési rutinok közé tartoznak. Az a nagyszerű ezekben az algoritmusokban, hogy oly sok éve léteznek már, és kiállták az idő próbáját. Jó eséllyel azelőtt találták ki őket, mielőtt Ön megszületett, mivel egészen az 1950-es évekig nyúlik vissza a történetük Egészen biztosan korosabbak mind a két szerzőnél! Megnyugtatásul hadd jegyezzem meg, az ezeknek az algoritmusoknak a megtanulásával töltött idő az évek során kifizetődik.
A fejezet a következőkről szól:
• a Shell rendezési algoritmusról,
• a gyorsrendezési algoritmussal való munkáról,
• az összetett összehasonlítóról és a stabilitásról,
• az összefésüléses rendezési algoritmus használatáról,
• arról, hogyan küszöbölik ki az összetett összehasonlíták az instabilitást,
• a fejlettebb rendezési algoritmusok összehasonlításáróL
A Shell-rendezési algoritmus alapjai
Az alapvető rendezési algoritmusok fő korlátai közé tartozik az a munkamennyiség, amelyre szükségük van ahhoz, hogy a végső rendezett helyzetüktől távol levő elemeket a megfelelő helyre mozgassák. A fejezet során tárgyalt fejlettebb rendezési algoritmusok lehetőséget nyújtanak a helyüktől messze levő elemek gyors mozgatására, ezért az előző fejezetben említett algoritmusoknál sokkal hatékonyabbak nagy adathalmazok kezelésére.
Fejlettebb rendezés
A Shell-rendezés ezt az eredményt nagy elemlisták kisebb részlistákra való szét
tördelésével éri el, amelyeket aztán külön-külön rendez a beszúrásos rendezés segít
ségével Oásd 6. fejezet). Míg ez igen egyszerűen hangzik, a trükk abban áll, hogy ad
dig ismételjük az eljárást mind nagyobb és nagyobb részlisták használatával, míg
végső soron az egész listát beszúrásos módszerrel rendezzük. Amint azt az előző fe
jezetben is emlitettük, a beszúrásos rendezés nagyon hatékony majdnem rendezett
adatok esetében, és éppen ez az állapot áll elő a Shell-rendezés eredményeként.
A Shell-rendezést szemiéitető alábbi példa a 7.1. ábrán látható betűket rendezi
ábécérendbe.
N N
7.1. ábra. Példa a Shell-rendezés bemutatására
A Shell-rendezés a H-rendezés alapelvére épül. Akkor mondjuk, hogy egy listát H
rendezünk, ha bármilyen kezdőpozícióból indulva minden H-adik elem rendezett
pozícióban van a többi elemhez képest. Ez az elképzelés a példa végére érthetőbbé
válik majd. A következő gyakorlófeladat során kezdetnek 4-rendezzük a 7 .1. ábrán
látható listát. Másképpen minden negyedik elemet veszünk figyelembe, és egymás-
hoz viszonyítva rendezzük őket. , A 7.2. ábrán minden negyedik elem látható, a kiinduló pozíció O.
7. 2. ábra. Minden negyedik elemet vesszük figyelembe, a kiinduló poifció O
Minden más elem figyelmen kívül hagyásával a kiemelt elemeket egymáshoz viszo
nyítva rendezzük, ami a 7.3. ábrán látható eredményre vezet. A kiemeit elemek im
már ábécésotrendben jelennek meg (B, G, H, N, O).
7.3. ábra. Minden negyedik elem egymáshoz viszotryítva rendezye, a kiinduló poifció O
Mostantól minden negyedik elemet 1-es pozíciójúnak tekintünk. A 7.4. ábrán ezek
az elemek láthatók az előtt és után, hogy egymáshoz képest elrendeztük őket.
B l E l G l l G l N l I N I H lA l L l G I N I R I T l o l M I s
B l A l G l l G l E l l N l H l M l L l G I N I N I T l o l R I s
7.4. ábra. Minden negyedik elem rendezése, az 1. pozíciótól kezdve
170
A Shell-rendezési algoritmus alapjai
Most 4-rendezünk a 2-es pozíciótól kezdve, ahogyan a 7.5. ábrán látható.
sGEJ GE l N l H l M l L l G l N l N ll l T l O l R lS l sGEJ GE l N l H lM l l G l N l N l L l rloiRisl
7.5. ábra. Minden negyedik elem rendezése, a 2. po'{jciótól kezdve
Végül minden negyedik olyan elemet veszünk figyelembe, amelyik 3. pozícióban ta
lálható. A 7.6. ábra az e lépés előtti és utáni helyzetet mutatja.
siAIGIIIGIEIIINIHIMIIIGININILirloiRis
B l A l G l G l G l E l l l l l H l M l l l N l N-� N l L l T l O l R l S 7.6. ábra. Minden negyedik elem rmdezése, a 3. po'{jciótól kezdve
Nincs már rnit tenni a példabeli lista 4-rendezése érdekében. A 4. pozícióhoz való eset
leges továbblépés ugyanazoknak az objektumoknak a figyelembevételével járna, ame
lyeket a O. pozícióban használtunk A 7.6. ábra második során látszik, hogy a lista egyál
talán nem rendezett, hanem 4-rendezett. Ezt úgy tesztelhe�ük, hogy kiválasz�uk a lista
bármely elemét, és megvizsgáljuk, hogy a tőle jobbra álló elemhez képest kisebb-e (vagy
egyenlő azzal), és hogy a tőle balra álló elemhez képest nagyobb-e (vagy egyenlő azzal).
A Shell-rendezés gyorsan mozgatja az elemeket nagy távolságokra. Sok elemből
álló listák esetében egy jó Shell-rendezés magas H-számmal kezdődik, tehát mond
juk a lista 10 OOO-rendezésével, ezáltal több tízezer pozíciónyit mozgatva egy ele
men, így azok hamar a végleges pozíciójuk közelébe kerülnek. Miután egy nagy H
értékkel megtörtént a lista H-rendezése, a Shell-rendezés kisebb értéket választ a H
helyett, és az egészet megismétli. A folyamat addig tart, amíg H értéke 1 nem lesz, és
végül az egész lista rendezetté válik. Az alábbi példában a következő lépésben 3-rendezzük az elemeket.
A 7.7. ábrán minden harmadik elem látható O. pozícióban, egymáshoz viszonyí
tott rendezésük előtt és után. Ha már itt tartunk, figyeljük meg az utolsó négy betű
elrendezését!
siAIGIGIGIEIIIIIHI�I�INININI LlrloiRis
siAIGIGIGIEI 1 l 1 IH IMI 1 INININILisloiRir 7.7. ábra. 3-rendezés O. kiinduló po'{jcióval
171
Fejlettebb rendezés
1. pozícióba mozgatunk, és véletlenül nincs mit tenni, ahogyan az a 7.8. ábrán látható.
IBIAIGIGIGI E H lM l l N l N l N l L s l o l RI T
IBIAIGIGIGI E H lM l l N l N l N l L s l o l RI T
7.8. ábra. 3-rendezés 1. kiinduló poz!cióval
Végül rendezünk minden harmadik elemet a 2-es pozíciótól kezdve, ahogyan az a
7.9. ábrán látható.
B IAIGIGIGI E IHIMI l N l N N L s l o IRI T
B IAIEIGIGIGI IHIMI l L l N l N l N l s l o IRI T
7. 9. ábra. 3-rendezés 2. kiinduló poz!cióval
Figyeljük meg, hogy a lista majdnem rendezett. A legtöbb elem egy- vagy kétpozíció
nyira található attól, ahol lennie kellene, ami megoldható egy egyszerű beszúrásos
rendezésset Egy rövid futtatás a listán, és az eredmény a 7.10. ábrán látható végső
elrendezés. Ha ezt összehasonlitjuk az előző listával, láthatjuk, hogy egy elemnek
sem kellett két pozíciónál többet mozdulnia végső pozíciójának eléréséhez.
l A l B l E l G l G l G l H l l l l l l l L l M l N l N l N l 0 l R l S l T
7.10. ábra. A végső, rendezett lista
A Shell-rendezéssei kapcsolatos legtöbb kutatás a H soron következő értékeivel fog
lalkozott. Az algoritmus feltalálója által javasolt eredeti számsor (1, 2, 4, 8, 16, 32 ... ) valószínűleg borzalmas, mivel csak páratlan pozícióban található elemeket hasonlit
össze páratlan pozícióban álló elemekkel az utolsó lépésig. A Shell-rendezés akkor
működik jól, ha minden elemet különböző elemekkel összehasonlitva rendezünk
minden lépésben. A (1, 4, 13, 40 ... ) egyszerű és hatékony sorozat, ahol minden H 3xH+l. A következő feladatban a Shell-rendezést fogjuk megvalósítani.
Gyakorlófeladat: a Shell-rendezés tesztelése
A következő részben a Shell rendezési algoritmust fogjuk megvalósítani, ugyanan
nak a tesztsornak a használatával, amelyet az előző fejezet rendezési algoritmusában
is láthattunk.
A Shell-rendezési algoritmus tesz* mostanra már biztosan ismerős látvány.
Csak kiterjeszti az Abstract l i stsorterTest használatát, és példányosítja a (még
nem megírt) Shell-rendezés megvalósítását.
172
A Shell-rendezési algoritmus alapjai
package com.wrox.algodthms.sórting;
public class shellsortListsorterTest extends AbstractListsorterTest {
protected ListSorter createListsorter(Comparator comparator) { return new shellsortListsorter(comparator);
} }
A megvalósitás müködése
Az előző teszt kiterjesztette a 6. fejezetben elkészített általános célú algoritmusren
dezési tesztet. A Shell-rendezés megvalósításának tesztjéhez ezt csupán példányosí
tani kell a createLi stsorter() metódus figyelmen kívül hagyásával.
A következő gyakorlófeladatban egy Shell-rendezést valósítunk meg.
Gyakorlófeladat: a Shell-rendezés megval_ósitása
A megvalósítás hasonló felépítésű, mint az alapvető rendezési algoritmusok eseté
ben. A L istsorter interfészt valósí�a meg, és összehasonlítót alkalmaz az elemek
rendezése során. Hozzunk létre egy megvalósítási osztályt példánymezővel, hogy az
tartalmazza a használandó összehasonlítót, és egy egész típusú tömböt a használan
dó H -értékek tárolására:
package com.wrox.algor1thms.sorting;
import com.wrox.algorithms.lists.List;
public class shellsortListsorter implements Listsorter { private final Comparator _comparator;
l
private final int[] _increments = {121, 40, 13, 4, l};
public shellsortListsorter(Comparator comparator) {
}
assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
A következőként elkészítendő so rt() metódus csupán ciklusonként végighalad az
előző tömbben definiált növekményeken, és valamennyi növekményhez meghívja a
hSort() metódust a listához. Figyeljük meg: a siker azon múlik, hogy a végső nö
vekmény l legyen. Bátran kísérletezzünk más sorrendekkel, de tartsuk észben, hogy
a végső érték l legyen, kül�nben a lista végül csak "majdnem" rendezett lesz!
173
Fejlettebb rendezés
public List sort(List list) {
}
assert list l= null : "a 'list' nem lehet NULL";
for (int i= O; i < _increments.length; i++) { int increment = _increments[i]; hsort(list, increment);
} return list;
Most hozzunk létre egy hsort() megvalósítást, gondosan figyelmen kívül hagyva a
rendezni kívánt adathoz képest túl nagy növekményeket. Nincs sok értelme 50-
rendezni egy rnindössze tíz elemből álló listát, rnivel nem lesz mihez hasonlítani az
elemeket. A növekménynek kisebbnek kell lennie, mint a lista fele. A metódus ez
után egyszerűen csak rninden pozícióhoz egyszer meghívja a sortsubL i st() metó
dust, O. kezdőpozícióval, ahogyan azt a példabeli listánál is tettük:
private void hsort(List list, int increment) { if (list.size() < (increment * 2)) {
}
return; } for (int i=O; i< increment; ++i) {
sortsublist(list, i, increment); }
Végül létrehozzuk a metódust, amely egymáshoz képest rendez minden H-adik ele
met. Ez a beszúrásos rendezés belső változata, azzal a csavarral, hogy ez valamennyi
H-adik elemet figyelembe veszi. Ha ki akarnánk cserélni a +i ncrement és a -i ncre
ment valamennyi előfordulását a következő kódban a +l és a -l jelölésekre, lényegében
a beszúrásos rendezést kapnánk. A beszúrásos rendezés részletes magyarázatáért la
pozzuk fel a 6. fejezetet.
174
private void sortsublist(List list, int startindex, int increment) {
}
for (int = startindex + increment; i < list.size();
}
Object value = list.get(i); i nt j;
i += increment) {
for (j = i; j> startrndex; j-= increment) { Object previousvalue = list.get(j - increment);
}
if (_comparator.compare(value, previousvalue) >= O) { break;
} list.set(j, previousvalue);
list.set(j, value);
A gyorsrendezésről
A megvalósitás müködése
A Shell-rendezés kódja úgy múködik, hogy egymást követően rendez olyan részlistá
kat, amelyeknek az elemei egymástól egyenlő távolságra helyezkednek el. Ezekben a
listákban kezdetben kevés elem található, és sok közöttük az üres hely. A részlisták
mérete aztán nő, míg számuk csökken, az elemek pedig egyre közelebb kerülnek
egymáshoz. A fő sort() metódus legkülső ciklusa felel a részlisták mind kisebb nö
vekményú H-rendezéséért, ahol végül a H-rendezés egy l értékű H-val történik, ami
azt jelenti, hogy a lista rendezett.
A hsort() metódus biztosítja, hogy az aktuális növekménnyel elválasztott rész
listák elemei megfelelően legyenek rendezve. A program egy ciklust futtat a részlis
tákon az aktuális növekmény felhasználásával, és meghívja a sortSub l i st metódust
a részlista rendezésére. A metódus a beszúrásos rendezési algoritmust használja
(amelyről a 6. fejezetben olvashatunk) a részlista elemeinek újrarendezéséhez, így
azok viszonylag rendezettek lesznek.
A gyorsrendezésről
A gyorsrendezés az első általunk tárgyalt olyan algoritmus, amely rekurziót használ.
Bár iteratív megvalósítással is megformálható, a gyorsrendezés természetes állapota
a rekurzió. A gyorsrendezés oszd meg és uralkodj megközelítést használ, amikor re
kurzív módon feldolgozza a lista mind kisebb elemeit. Minden szinten három rész
ből áll az algoritmus célja:
• egy elemet a végső, rendezett pozícióba helyezni;
• valamennyi, a rendezett elemnél kisebb elemet a rendezett elemtől balra el
helyezni;
• valamennyi a rendezett elemnél nagyobb elemet a rendezett elemtől jobbra
elhelyezni
Ezeknek az invariánsoknak a fenntartásával a lista minden lépésben két részre bom
lik Gegyezzük meg, hogy nem feltétlenül két egyenlő részre), amelyeket egymástól
függetlenül lehet rendezni. A következő rész a 7 .11. ábrán látható betúk listáját
használja példaként.
lalulllciKisloiRir S � EIAITIFIUIN 7. 11. ábra. Példa a gyorsrendezéshez
175
Fejlettebb rendezés
A gyorsrendezés első lépése az elválasztó elem meghatározása. Ez az az elem, amely a végső, rendezett pozícióba kerül ez után a lépés után, és a listát két részre osztja, a kisebb elemekkel a bal oldalon (véletlenszerű elrendezéssel), és a nagyobb elemekkel a jobb oldalon (szintén véletlenszerű elrendezéssel). Sokféleképpen ki lehet választani a kettéosztó elemet: az alábbi példa egy egyszerű stratégiával él, mivel az elem végtére is a listának igencsak a jobb szélén található. A 7.12. ábrán láthatjuk a kiemelt elemet. Két indexet is kijelölünk a fennmaradó elemek közül a leginkább bal oldali és a leginkább jobb oldali helyen, ahogyan azt az ábrán is látha�uk.
7. 12. ábra. Az iniciális gyorsrendezési lépés kezdópo:ifciija
Az algoritmus a bal és a jobb indexet addig közeliti egymáshoz, amíg találkoznak. A bal index akkor áll meg, amikor a kettéosztó elemnél nagyobb elemmel találkozik. A jobb index akkor áll meg, amikor a kettéosztó elemnél kisebb elem kerül az ú�ába. Az elemek ezután felcserélődnek, így mindegyik a lista megfelelő részébe kerül. Emlékezzünk vissza: a cél az, hogy balra kisebb, jobbra nagyobb elemek legyenek, bár nem feltétlenül rendezett sorrendben.
A 7.12. ábrán látható példán a bal index a Q betűre mutat. Ez az ábécében később következik, mint a kettéosztó érték (N), tehát a lista bal végén nem megfelelő helyen van. A jobb index eredetileg az U-ra mutatott, amely későbbi, mint az N, ez tehát rendben van. Ha ez egy pozícióval elmozdul balra, akkor az F betűre mutat, amely előbb jön, mint az N. Így tehát rossz helyen van a lista jobb végén. A helyzet a 7 .13. ábrán látható.
7.13. ábra. Az első két rossz helJen lévő elemet megtaláltuk
Ezeknek a rossz helyen lévő elemeknek a végső rendezett pozícióhoz közelebb mozgatásához a 7.14. ábrán látható módon felcseréljük őket.
7.14. ábra. Az első két rossz helJen lévő elemet Jelcseréltük
176
A gyorsrendezésről
A bal index most addig folytatja a mozgást jobbra, amíg olyan elemmel találkozik,
amelyik a kettéosztó elem után jön az ábécében. Mindjárt a következő pozícióban
(U) talált egyet. A jobb index folytatja a mozgást balra, és egy A betűt talál a rossz
helyen, ahogyan az a 7.15. ábrán látható.
F G t
CIKIS� T s0R EIAITialuiNI t
7 .15. ábra. Két további rossz helJen lévő elemet találtunk
Az elemek ismét helyet cserélnek, ahogyan azt a 7 .16. ábrán láthatjuk.
F l A l t
1lciKisloiRITI 1 lsiGIRIEiuiTialuiNI t
7. 16. ábra. A második pár rossz he!Jen lévő elem he !Jet cserélt
Ugyanez az eljárás folytatódik tovább, ahogyan a bal és a jobb index egymás felé
mozog. A következő pár rossz helyen lévő elem az S és az E, lásd az 7.17. ábrán.
F A c KlsloiR T s0 R 0 U
t T l Q l U l N l
t 7.17. ábra. Két további rossz he !Jen lévő elemet találtunk
Az elemeket felcseréljük, így a lista a 7.18. ábrán látható helyzetbe kerül. Ezen a szin
ten a bal indextől balra lévő minden elemnek előbb kell következnie, mint a kettéosz
tó elemnek, és a jobb indextől minden jobbra lévő elem későbbi, mint a kettéosztó
elem. A bal és a jobb index között található elemekre még gondot kell fordítani.
F IA�C Kl EloiRIT t
siGIRisluiTialuiNI t
7. 18. ábra. Az E és az S betű he !Jet cserél
A munka ugyanígy folytatódik. A következő rossz helyen lévő elemet a 7.19. ábrán
láthatjuk.
F l A CIKIEloiRIT t
siGIRisluiTialuiNI t
7. 19. ábra. Az O és a G betű van rossz he !Jen
1n
Fejlettebb rendezés
Megcseréljük őket a 7 .20. ábrán látható pozícióba.
F l A l c K E l GIRl T s l ol RI s u T l Q l u l N l t t
7 .20. ábra. Az O és a G betű helJet cserélt
Az első gyorsrendezés már majdnem kész. A 7.21. ábrán látható, hogy egy pár rossz
helyen lévő elem maradt.
l l t
u
7. 21. ábra. Az R és az I betű van rossz helJen
A lista felcserélés utáni állapotát a 7 .22. ábra szernlélteti.
c K s u
7 .22. ábra. Az R és az I betű helJet cserélt
Most kezdenek érdekessé válni a dolgok. Az algoritmus folytatódik az eddigiek sze
rint, a bal index addig halad, amíg a kettéosztó elemnél nagyobb elemet talál, jelen
esetben a T betűt. A jobb index ezután balra halad, de megáll, arnikor eléri ugyanazt
az értéket, mint a bal index. Nem jár semmilyen előnnyel az ezen a ponton való túlhaladás, rnivel az indextől balra lévő összes elem sorra került már. A lista most a
7.23. ábrán látható állapotban van, mind a bal, mind a jobb index a T betűre mutat.
7.23. ábra. A bal és a jobb index a kettéos'{jó pozícióban találkoifk
A két index találkozási pontja a kettéosztó pozíció - azaz az a hely a listában, ahová
a kettéosztó érték valójában tartozik. Ezért végrehajtunk egy végső cserét e kőzött a
pozíció között és a kettéosztó érték között a lista jobb végében, hogy a kettéosztó
értéket a végső rendezett helyére tegyük. Arnikor ez kész, az N betű található a ket
téosztó pozícióban, ahol a tőle balra található értékek mind kisebbek, és a jobbra ta
lálhatóak mind nagyobbak (lásd 7 .24. ábra).
178
A gyorsrendezésről
"N" előttiek "N" utániak
Fl A
l
l C lK l E l G l l
INIRisloiRislulrlolulr t
7.24. ábra. A kettéos'{fó elem a végső, rendezett po:ifcióban
Az eddig bemutatott lépések eredményeképpen csupán az N betű került végső pozí
ciójába. A lista még igen messze van a rendezett állapottól. Mindazonáltal felosztot
tuk két, egymástól függetlenül rendezhető részre. Egyszerűen rendezzük a lista bal,
majd a jobb felét, és az egész lista rendezett lesz. Itt kerül elő a rekurzió. Ugyanazt a
gyorsrendezési algoritmust alkalmazzuk a kettéosztó elemtől balra és jobbra találha
tó részlistára.
Két esetet kell figyelembe vennünk a rekurzív algoritmusok felépítésénél: az
alapvető és az általános esetet. A gyorsrendezés szempontjából az alapvető eset az,
amikor a rendezendő részlistában csak egy elem szerepel; ez definíció szerint rende
zett és nincs szükség további teendőkre. Az általán.os eset akkor fordul elő, amikor
több mint egy elem van, ekkor az előző algoritmust használjuk, hogy a listát kisebb
részlistákra bontsuk a kettéosztó elem végső pozícióba helyezése után.
Miután láttuk, hogyan működik a gyorsrendezési algoritmus, itt az idő kipróbál
ni a következő gyakorlófeladat segítségéve!.
Gyakorlófeladat: gyorsrendezési algoritmus tesztelése
Először létrehozunk egy tesztesetet kimondottan a gyorsrendezési algoritmus kedvéért:
package Cöm�wrox. a l gorithms -:sort" i n g;
public class QuicksortListsorterTest extends AbstractListsorterTest {
protected Listsorter createListsorter(comparator comparator) { return new QuicksortListSorter(comparator);
} }
A megvalósitás működése
A előző teszt kiterjesztette a 6. fejezetben elkészített általános célú algoritmusrende
zési tesztet. A gyorsrendezés megvalósításának tesztjéhez csupán példányosítani kell
a createLi stSorter() metódus figyelmen kívül hagyásával.
A következő gyakorlófeladatban egy gyorsrendezést valósítunk meg.
179
Fejlettebb rendezés
Gyakorlófeladat: a gyorsrendezés megvalósítása
Először is létrehozzuk a Qui cksortL istSorter metódust, amely már ismerős lesz,
mivel az alapvető struktúrája nagyon hasonlit az eddig látott többi rendező algorit
muséra. A L istsorter interfészt valósítja meg, és összehasonlítót alkalmaz, amely
létrehozza a rendezett állapotot a rendezendő objektumokon.
package com.wrox.algorithms.sorting;
import com.wrox.algorithms.lists.List;
public class QuicksortListsorter implements Listsorter { private final comparator _comparator;
public QuicksortListsorter(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
}
}
A sort() metódust használjuk a qui cksort() (gyorsrendezés) metódusba való to
vábbításhoz, továbbítva az első és az utolsó rendezendő elem indexét. Jelen esetben
ez az egés� listát jelenti. Ezt a metódust később rekurzív módon hivjuk majd meg,
kisebb részlisták továbbításával, amelyeket indexek definiálnak.
public List sort(List list) { assert list != null : "a 'list' nem lehet NULL";
quicksort(list, O, list.size() - l);
return list;
A gyorsrendezés megvalósításához a lista felosztó érték körüli felosztásához meg
adott indexeket használjuk, majd rekurzív módon meghivjuk a qui cksort() metó
dust a bal és a jobb részlistához is:
180
private void quicksort(List list, int startindex, int endindex) { if estartindex < o l l endindex >= list.size()) {
return; } if (endindex <= startindex) {
return; }
object value. = U.s.t g� e.:.t�(e=.�n.!!:d=.= I J.!n ::::de=.� xu )� �; _____________ _,
A gyorsrendezésről
}
int partition = partition(list, value, startindex, endin if (_comparator.compare(list.get(partition), value) <O)
++parti ti on; }
swap(list, partition, endindex);
quicksort(list, startlndex, partition - l); quicksort(list, partition +l, endindex);
A parti ti on () metódust használjuk annak az algoritmusnak a végrehajtására,
amelynek segítségével a rossz helyen levő elemek helyet cserélnek egymással, így a
bal oldalra kerülnek a kis elemek, a jobb oldalra pedig a nagyok:
prwate int partition(List llst, ooject val�int leftl:ndex, int rightindex) {
}
int left = leftindex; int right = rightindex;
while (left < right) {
}
if (_comparator.compare(list.get(left), value) <0) { ++left; continue;
}
if (_comparator.compare(list.get(right), value) >=O) { --right; continue;
}
swap(list, left, right); ++left;
return left;
Végül egy egyszerű swap() metódust hajtunk végre, amely biztosítja magát az ellen,
hogy egy elemet saját magával cseréljen ki:
private voiO'S'Waji(L i sCli�i"nt,-ef�int rigtí1T-{ if (left == right) {
}
return; } object temp = list.get(left); list.set(left, list.get(right)); list.set(right, temp);
181
Fejlettebb rendezés
A megvalósitás müködése
A qui cksortO metódus a két átadott index ellenőrzésével kezdődik. Ez lehetővé teszi a későbbi kód számára ennek a kétségnek a figyelmen kívül hagyását. Ezután a lista jobb végéről eléri a kettéosztó értéket. A következő lépés a kettéosztó pozíció elérése a parti ti on O metódus delegálásávaL
A parti ti on O metódus részét képezi egy teszt, amely megvizsgálja, hogy a kettéosztó helyzetben található érték kisebb-e a kettéosztó értéknéL Ez előfordulhat például abban az esetben, amikor a kettéosztó érték a teljes elemlista legnagyobb értéke. Véletlenszerű kiválasztás esetén ez könnyen előfordulhat. Ekkor a kettéosztó indexet egy pozícióval közelitjük meg. Ebben a kódban az szerepel, hogy a bal és a jobb index értéke végül mindig ugyanaz, ahogyan azt az algoritmus magyarázatánál a fejezet korábbi részében láthattuk. Az ebben az indexben található érték a metódus eredményéül kapott érték.
Az összetett összehasonUtóról és
a stabilitásról
Mielőtt belekezdenénk a harmadik fejlett rendezési algoritmus tárgyalásába, szánjunk egy kis időt az előző fejezetben már említett stabilitás megvitatásárai Most nyílik erre megfelelő alkalom, mivel a fejezetben eddig megtárgyalt mindkét algoritmus ugyanazzal a hátránnyal rendelkezik: nem stabilak. A következő említendő algoritmus - az összefésüléses rendezés - stabil, tehát itt az ideje, hogy beszéljünk a Shellrendezés és a gyorsrendezés esetében tapasztalható stabili táshiányróL
Ahogyan azt a 6. fejezetből megtudhattuk, a stabilitás egy rendezési algoritmus
azon tulajdonsága, hogy a rendezési folyamat során fenntartsa az azonos rendezési kulccsal bíró elemek egymáshoz viszonyított pozícióját. A gyorsrendezésből és a
Shell-rendezésből hiányzik a stabilitás, mivel nem fordítanak figyelmet az eredeti bemeneti listában egymáshoz közel álló elemekre. A következő rész egy olyan módszerrel foglalkozik, amely kompenzálni tudja ezt a hiányosságat ennek a két algoritmusnak a használatánál is: az összetett összehasonlítóval.
A 6. fejezetben található példában személyek listáját láthattuk, amelyet vagy vezetéknév, vagy keresztnév alapján rendeztünk. Az azonos vezetéknévvel rendelkező emberek egymáshoz viszonyított sorrendje megmarad (vagyis az azonos névcsoportba tartozók a keresztnevük alapján voltak rendezve), ha a használt algoritmus stabil. Ugyanennek a hatásnak az elérésére egy másik mód összetett kulcs használata a person (személy) objektumhoz, amely a keresztnév és a vezetéknév együtteséből áll a keresztnévvel való rendezésnél, és a vezetéknév és a keresztnév együtteséből vezetéknév szerinti rendezésnéL
182
Az összetett összehasonlítóról és a stabilitásról
Ahogyan azt a 6. fejezetben is láthattuk, gyakran van lehetőség hasznos általános
célú összehasonlitók használatára sokféle probléma megoldásánál. Ez a nézőpont,
amelyet a következő feladatban is alkalmazunk, ahol összetett összehasonlítót hasz
nálunk, amely bármennyi szabványos egyértékű összehasonlítót be tud csamagoini
egy összetett kulcson alapuló rendezett kimenet eléréséhez.
Gyakorlófeladat: az összetett összehasonlitó tesztelése
Az összetett összehasonlitó tesztjei egy próba-összehasonlitó szolgáltatásaira tá
maszkodnak, amely mindig egy ismert értéket szalgáltat a compare() metódusból.
Adjunk ennek egy egyértelmű Fixedcomparator elnevezést. Ennek kódja a követke
zőképpen fest:
package com.wrox�lgorithms.sorting;
public class Fixedcomparator implements comparator { private final int _result;
}
public Fixedcomparator(int result) { _result = result;
}
public int compare(Object left, object right) { return _result;
}
Most már elkezdhetünk teszteket írni az összetett összehasonlítóhoz. Három alap
esetet kell lefednünk: amikor a compare() metódus nullát, amikor pozitív egész
számot, illetve amikor negatív egész számot eredményez. Valamennyi fenti teszt
több rögzített összehasonlítót ad az ,összetett összehasonlitóhoz. Az első a beállítása
alapján nullát eredményez, amely jelzi, hogy az összetett összehasonlítónak az össze
tett kulcs első elemén kívül többet is kell használnia az elemek elrendezéséhez. Az
első három tesztesethez tartozó kód így néz ki:
package com.wrox.algorithms.sorting;
import junit.framework.Testcase;
public class compoundcomparatorTest extends Testcase { public void testcomparisoncontinueswhileEqual() {
Compoundcomparator comparator = new Compoundcomparator(); comparator.addcomparator(new Fixedcomparator(O)); comparator.addcomparator(new Fixedcomparator(O)); comparator.addcomparator(new Fixedcomparator(O));
assertTrue(comparator.compare("IGNORED", "IGNORED") ==O);
l
183
Fejlettebb rendezés
}
public void testcomparisonstopsWhenLessThan() { compoundcomparator comparator = new compoundcomparator(); comparator.addcomparator(new Fixedcomparator(O)); comparator.addcomparator(new Fixedcomparator(O)); comparator.addcomparator(new Fixedcomparator(-57)); comparator.addcomparator(new Fixedcomparator(91));
assertTrue(comparator.compare("IGNORED", "IGNORED") <0); }
public void testComparisonStopswhenGreaterThan() { compoundcomparator comparator = new compoundcomparator(); comparator.addcomparator(new Fixedcomparator(O)); comparator.addcomparator(new Fixedcomparator(O)); comparator.addcomparator(new Fixedcomparator(91)); comparator.addcomparator(new Fixedcomparator(-57));
assertTrue(comparator.compare("IGNORED", "IGNORED") >O);
}
A megvalósitás működése
A teszt azon a képességen alapul, hogy bármilyen számú más összehasonlítót hozzá
adhatunk az új compoundcomparator-hoz (összetett összehasonlítóhoz) a számsor
ban. Az első teszt négy összehasonlítót ad hozzá, amelyek mind nullát eredményez
nek a hozzájuk tartozó compare() metódus meghívásakor. Az elv a következő: va
lamennyi Compoundcomparator egyenként valamennyi beágyazott összehasonlítót el
lenőrzi, és akkor hoznak eredményt, amikor valamelyikük nem nulla értéket ered
ményez. Ha valamennyi beágyazott összehasonlító nullát eredményez, az összeha
sonlító folyamat megállapítja, hogy az objektumok azonosak.
A második teszt beágyazott összehasonlíták sorozatát állí* fel, míg a harmadik
negatív értéket eredményez. A compoundcomparator-nak úgy kell működnie, hogy a
beágyazott összehasonlíták közül az első nem nulla értéket hozza eredményül. A teszt
biztosítja, hogy ez a viselkedés megfelelő. Az utolsó teszt ugyanezt a munkát végzi el,
de pozitív visszaadott értéket eredményez.
A következő feladatban egy compoundcomprator-t valósítunk meg.
Gyakorlófeladat: az összetett összehasonUtá megvalósítása
Először létrehozzuk az osztályt a Comparator (összehasonlítá) interfész megvalósí
tásához, majd hozzáadunk egy privát L i st-et (listát), amely tartalmazza az ismeretlen
számú összehasonlítót valamennyi összetett rendezési kulcselemhez:
184
Az összetett összehasonlítóról és a stabilitásról
package com.wrox.algorithms.sorting;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List;
public class compoundcomparator implements Comparator { private final List _comparators = new ArrayList();
}
Biztosítjuk az addcomparator() ( összehasonlító hozzáadása) metódust, hogy az ösz
szetett összehasonlító bármennyi összehasonlítót felhasználhasson:
pulili c void addcomparator(Comparator comparator)-{
l
assert comparator != null : "a 'comparator' nem lehet NULL"; assert comparator != this : "a 'comparator'-t nem lehet hozzáadni
önmagához";
_comparators.add(comparator);
Végül valamennyi felhasznált összehasonlítónál megvalósítjuk a compare() metó
dust, amely akkor hoz eredményt, ha valamelyik nem nulla eredménnyel jár.
public int compare(öb-ject lef�oject rfght)- { int result = O;
l
Iterator i = _comparators.iterator();
for (i.first(); !i.isoone(); i.next()) {
}
result = ((comparator) i.current()).compare(left, right); if (result != O) {
break; }
return result;
A Compoundcomparator azért kiemelkedően hasznos, mert bármely létező összeha
sonlítót fel tud használni újra, hogy felülemelkedjen a stabilitás hiányán, vagy egysze
ruen egy összetett kulcs segítségével rendezést hajtson végre.
185
Fejlettebb rendezés
Az összefésü léses rendezési algoritmusról
Az összefésüléses rendezés az utolsó fejlett rendezési algoritmus, amellyel ebben a
fejezetben foglalkozunk. A gyorsrendezéshez hasonlóan az összefésüléses rendezést
is meg lehet valósítani rekurzív és iteratív módon is, a következőkben mi az utóbbit
választjuk. A gyorsrendezéssei ellentétben az összefésüléses rendezés a listát nem a
megadott helyen rendezi; ehelyett egy új kimeneti listát hoz létre, amely a bemeneti
lista objektumait tartalmazza rendezett sorrendben.
Összefésülés
Ez a rendezés az összefésülés elvére épül. Az összefésülés két (már eleve rendezett)
listát vesz, és egy új kimeneti listát hoz létre, amelyben mind a két lista elemei megta
lálhatók rendezett sorrendben. Például a 7.25. ábrán két összefésülendő bemeneti
listát láthatunk. Figyeljük meg, hogy mind a két lista eleve rendezett.
7 .25. ábra. Két eleve rendezett lista, ame!Jeket ds sze kívánunk Jésiilni
Az összefésülési folyamat elején indexeket helyezünk el az egyes listák fejénél. Ezek
egyértelműen az egyes listák legkisebb elemére mutatnak majd, ahogyan azt a 7.26.
ábrán is láthatjuk.
7 .26. ábra. Az ósszefésiilés az egyes listák frjénél kezdődik
A listák elején álló elemeket összehasonlítjuk, és a kisebbiket hozzáadjuk a kimeneti
listához. Annak a listának a következő elemét vesszük figyelembe, amelyből kimá
soltuk a legkisebb elemet. Az első elem kimeneti listába való másolása utáni helyze
tet ábrázolja a 7.27. ábra.
Az egyes listák aktuális elemeit ismét összehasonlítjuk, és a legkisebbet elhelyez
zük a kimeneti listában. Jelen esetben ez a D betű a második listábóL A 7 .28. ábra az
ez után a lépés után fennálló helyzetet mutatja.
A folyamat folytatódik; most az első listában található F betű a legkisebb elem,
ezt bemásoljuk a kimeneti listába, ahogyan azt a 7.29. ábrán láthatjuk.
186
Az összefésüléses rendezési algoritmusról
8fG KIMENET r:l
----+-�
~ 7.27. ábra. Az első elemet hozzáadtuk a kimeneti listához
8fG � �
~ 7.28. ábra. A második elemet hoz'(fÍadtuk a kimeneti listához
GEifJ � KIMENET •l A l D l F
7.29. ábra. A harmadik elemet hozzáadtuk a kimeneti listához
A folyamat addig folytatódik, amíg mind a két bemeneti lista el nem fogy, és a kime
net a két listából származó elemeket rendezett sorrendben tartalmazza. A 7.30. áb
rán látható a végállapot.
A l F GJ t
l D l G l L l t
KIMENET� A l D l F l G l L l M l 7. 30. ábra. A befqezett óss'(!fésülési fo!Jamat
Az összefésüléses rendezési algoritmus
Az összefésüléses rendezési algoritmus az összefésülés elvére épül. Ahogyan a gyors
rendezési algoritmusnál is, az összefésüléses rendezést is rekurzióval közelí�ük meg,
de míg a gyorsrendezés az "oszd meg és uralkodj" elvére épült, az összefésüléses ren
dezési algoritmus inkább egyfajta "kombináld és uralkodj" megközelítést alkalmaz.
A rekurzió felső szintjén csak akkor történik meg a rendezés, amikor az alsóbb szin
teken már befejeződött.
187
Fejlettebb rendezés
Hasonlítsuk ezt össze a gyorsrendezéssel, ahol egy elemet a végső rendezett pozícióba helyezünk a felső szinten, mielőtt a problémát lebontanánk, és minden egyes rekurzív hívás egy újabb elemet helyez a rendezett pozícióba.
A következő példa a 7 .31. ábrán látható betűk listáját használja adatként.
7.31. ábra. Példalista a rekur'{fv iisszefésüléses rendezés bemutatásához
Mint minden rendezési algoritmus, az összefésüléses rendezés is egy egyszerű, de érdekes ötletre épít. Az összefésüléses rendezés kettéosz�a a rendezendő listát, mindkét részt egymástól függetlenül rendezi, majd összefésüli őket. Ez szinte túl
egyszerűen hangzik, hogy hatékony legyen, de azért szükség van némi magyarázatra. A 7 .32. ábrán látható a példabeli lista a kettéosztás után. Míg a két fél rendezése egymástól függetlenül történik, a végső lépés az összefésülésük, amelynek leírását az előző, összefésülésről szóló részben találjuk
7.32. ábra. A kettéosztott példabe/i lista
Az összefésüléses rendezés és a gyorsrendezés közti fő különbség az, hogy az összefésüléses rendezés során a felosztott lista teljesen független a bemeneti adattóL Az összefésüléses rendezés egyszerűen megfelezi a listát, rníg a gyorsrendezés a listát egy kiválasztott érték alapján osZ!fa fel, bármely ponton, bármely lépésben fel tudja osztani a listát.
Hogyan rendezzük tehát a lista első felét? Ismét az összefésüléses rendezés segítségéve!! A fél listát is kettéosz�uk, mindkét részt egymástól függetlenül rendezzük, majd összefésüljük őket. A 7.33. ábrán látha�uk az eredeti lista felét kettéosztva.
7.33. ábra. Az első rekur'{fv hívás ilsszefésüli az eredeti lista első felét
Hogyan rendezzük az eredeti lista első felének az első felét? Természetesen még egy összefésüléses rendezésre irányuló rekurzív hívással. Mostanára már biztosan rájöttünk, hogy addig folyta�uk a rekurzív hívásokat, amíg elérünk egy egyetlen elemből álló részlistát, amely természetesen már rendezett, mint minden egyelemű lista, és amely ennek a rekurzív algoritmusnak az alapesete lesz. Már láttuk az általános esetet - azaz amikor több mint egy elem található a rendezendő listában: megfelezzük a listát, rendezzük a feleket, majd összefésüljük őket.
A 7 .34. ábrán látható a helyzet a harmadik rekurziós szinten.
188
Az összefésüléses rendezési algoritmusról
l R (� l c l u l R l 8 l IRfeJcluiRI [s IRIElei �
lvfel 0 ll l V l E l
EIRIGIEisloiRir
7.34. ábra. A harmadik rekurzjós s'{jnt az iissziflsüléses rendezés során
Mivel még mindig nem egy egyelemű részlistát kaptunk, folytatjuk a rekurziót még
egy szinten, ahogyan az a 7 .35. ábrán szerepel.
IRJeJel � �0
[M[ EIRIGIEisloiRIT E l
7 .35. ábra. A negyedik rekurzjós s'{jnt az összefésüléses rendezés során
A 7.35. ábrán látható rekurziós szinten a kételemű, R és E betűket tartalmazó rész
listát próbáljuk rendezni. Ez a részlista több mint egy elemet tartalmaz, tehát ismét
meg kell feleznünk, és a feleket a 7 .36. ábrán látható módon rendeznünk kell.
� 0
00
[MIEIRIGIEisloiRir E l
7.36. ábra. A rekurzió végső s'{jntje az összefésüléses rendezési példa során
Végül elérkeztünk egy olyan szintre, ahol két egyelemű lista található. Ez a rekurzív
algoritmus alapesete, így most összefésülhetjük a két egyelemű részlistát egy kétele
mű rendezett részlistává, ahogyan azt a 7.37. ábra mutatja.
A 7 .37. ábrán az (R, E, C) részlista két részlistája már rendezett formában látha
tó. Az egyik az éppen összefésült kételemű részlista, míg a másik egy egyelemű rész
lista, amely csak a C betűt tartalmazza. Ezt a két részlistát összefésülhetjük, így egy
háromelemű, rendezett részlistát kapunk, ahogyan az a 7 .38. ábrán is látható.
189
Fejlettebb rendezés
l R l E l C l U l R l S l l l V �-E l l M l E l R l G l E l S l O l R l T
7. 3 7. ábra. Az első osszifésülési művelet eikésifi/t
7.38. ábra. Elkészült a második osszifésülési művelet
Az (R E, C, U, R) részlista most egy rendezett és egy rendezeden részlistával ren
delkezik. A következő lépés a második (U, R) részlista rendezése. Ahogyan az várha
tó, ezt a kételemű részlista rekurzív összefésülésével érhetjük el, ahogyan az a 7.39. ábrán látható.
JR)e(cJufRisJ lvJeiiMIEIRIGIEisloiRIT
lS l l V l E l lelEIRI �
G 0 7.39. ábra. Az (U, R) résiJista reku'ifv módon való rendezése
A két egyelemű részlistát most a 7 .40. ábrán látható módon összefésülhetjük.
l Rt i: l C l U l R t S f l l V l E l l M l E l R l G l E S l O l R l T
l S l l V l E
lelEIRI � 7 .40. ábra. A két egyelemű résiJista osszifésülése
Az (R E, C, U, R) részlista mindkét részlistáját egymástól függedenill rendeztük, így a
rekurziót legombolyítha�uk a két részlista összefésülésével, ahogyan a 7.41. ábra muta*.
190
Az összefésüléses rendezési algoritmusról
IRIEicluiRislllviEIIMIEIRIGIEisloiRir lciEIRIRiullsl1lviEI
7 .41. ábra. A rekurifó legombo!Jítása a két �észjista bsszefésülésével
Az algoritmus a (S, I, V, E) részlistával folytatódik, amíg az is rendezett lesz, ahogyan az a 7 .42. ábrán látható. (Kihagytunk néhány lépést az előző és a következő ábra között, n:llvel azok nagyon hasonlitanak az első részlista rendezésének lépéseihez.)
l R l E l c l u l R ls ll l v l E l lM l E l R l G l E ls l o IR l T l lciEIRIRiuiiEIIIslvl
7 .42. ábra. Készen állunk az eredeti lista első Jelének ossi!fésülésére
Az eredeti lista első felének rendezésénél a végső lépés a két rendezett részlista öszszefésülése, amely a 7 .43. ábrán szemléltetett eredménnyel jár.
lciEIEIII�IRislulviiMIEIRIGIEisloiRir 7 .43. ábra. A lista első jele most már rendezett
Az algoritmus ugyanúgy folytatódik, amíg az eredeti lista jobb fele is rendezett lesz, ahogyan az a 7 .44. ábrán látható.
lciEIEI1IRIRislulviiEIEIGIMioiRIRislr 7 .44. ábra. Az eredeti listajobb jele rendezett
Végül összefésülhetjük az eredeti lista két rendezett felét, hogy megkapjuk a végső rendezett listát, ahogyan az a 7 .45. ábrán szerepel.
lciEIEIIIRIRislulviiEIEIGIMioiRIRislr lciEIEIEIEIGI11MioiRIRIRIRislslrlulvl
7 .45. ábra. A végső eredméf!J
Az összefésüléses rendezés viszonylag könnyen megvalósítható, elegáns algoritmus, ezt a következő gyakorlófeladat során is meg fogjuk látni.
191
Fejlettebb rendezés
Gyakorlófeladat: az összefésüléses rendezési algoritmus tesztelése
A2 összefésüléses rendezés tesztje ugyanaz, mint a fejezetben található többi teszt,
az egyetlen különbség a megfelelő megvalósítási osztály példányosítása.
package com.wrox.algoritnms.sorting;
public class MergesortListsorterTest extends AbstractListsorterTest {
protected Listsorter createListsorter(Comparator comparator) { return new MergesortListsorter(comparator);
} }
A következő gyakorlófeladatban az összefésüléses rendezést valósítjuk meg.
Gyakorlófeladat: az összefésüléses rendezés megvalósítása
A megvalósítás ismét a megszakott sémát követi: Megvalósítjuk a L istsorter inter
fészt, és összehasonlítót alkalmazunk a rendezendő elemek rendezéséhez:
package com.wrox.algorithms.sorting;
import com.wrox.algorithms.lists.List; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.iteration.Iterator;
public class MergesortListSorter implements ListSorter { private final comparator _comparator;
}
public MergesortListsorter(Comparator comparator) {
}
assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
A sort() metódust használjuk a L istsorter interfészből a mergesort() (összefésü
léses rendezés) metódus meghívásához, paraméterként a legalacsonyabb és a legma
gasabb elem indexeit megadva, így az egész lista rendezett lesz. Egymás után követ
kező rekurzív hívások olyan indextartományokat adnak meg, amelyek a rendezést ki
sebb részlistákra korlátozzák:
public List sort(List list) { assert list != null : "a 'list' nem lehet NULL";
return mergesort(list, 0, list.size() - l); }
192
Az összefésüléses rendezési algoritmusról
Hozzuk létre a mergesort metódust, amely ezek után kezeli majd azokat a helyzeteket is, melyek során egy egyelemű részlistát kell rendeznie. Ebben az esetben egy új eredménylistát hoz létre, és hozzáadja az egyetlen elemet, ezáltal befejezi a rekurziót.
Ha a rendezendő részlistában több mint egy elem található, a kód egyszerűen felosztja a lis tát, rekurzív módon rendezi mind a két felét, és összefésüli az eredményt:
private Lfst inergesort(List li�int startrndex;- int end!naex)-{
}
if estartindex == endrndex) {
}
List result = new ArrayList(); result.add(list.get(startrndex)); return result;
int splitrndex = startindex + (endrndex - startindex) l 2;
List left= mergesort(list, startindex, splitrndex); List right = mergesort(list, splitrndex + l, endindex);
return merge(left, right);
A következő merge() (összefésülés) metódus kissé bonyolultabb, mint először várnánk, főleg azért, mert olyan eseteket kell kezelnie, amelyekben valamelyik lista egy másik előtt elfogy, és. olyanokat, amelyekben bármelyik lista szolgáltatha�a a következő elemet:
pr i vate Li st merge(Cist lef�i st right)-{ List result = new ArrayList();
Iterator l = left.iterator(); Iterator r = right.iterator();
l.first(); r.first();
while (!(l.isoone() && r.isoone())) { if (l.isoone()) {
result.add(r.current()); r.next();
} else if (r.isoone()) { result.add(l.current()); l.next();
} else if (_comparator.compare(l.current(), r.current()) <= O) {
result.add(l.current()); l.next�)�·--------------------�------------------ -------
193
Fejlettebb rendezés
}
} else { result.add(r.current()); r.next();
}
return result;
A megvalósitás müködése
Mint a rekurzív algoritmusok esetében is, a megvalósítás kulcsfontosságú részét ké
pezi mind az alapeset, mind az általános eset bevezetése a rekurzív módszerbe. A fen
ti kódban a mergesort() metódus teljesen egyértelműen elválasz�a ezeket az esete
ket, azáltal, hogy először az alapesetet kezeli, és azonnal visszatér a metódusból,
amint a szóban forgó részlistában csak egy elem található. Ha több mint egy elemet
talál, megfelezi a listát, mindkét felét rekurzív módon rendezi, majd az eredményt a
me r ge() metódus segítségével összefésüli.
A merge() metódus iterátort kap minden részlistára, és csak egy egyszerű szek
venciális bejárásra van szükség az egyes részlistákon. A kódot valamivel bonyolultabbá
teszi, hogy az algoritmusnak akkor is folytatódnia kell, amikor valamelyik részlistából
kifogynak az elemek, ez esetben a másik lista összes elemét hozzá kell adni a kimeneti
lista végéhez. Amikor mind a két részlistában találhatók még elemek, amelyekkel fog
lalkozni kell, a két részlista aktuális elemeit hasonlí�a össze, és a kettő közül a kisebbik
kerül be a kimeneti listába.
Ezzel befejeztük a három fejlettebb rendezési algoritmusról szóló értekezésünket
ebben a fejezetben. A következő rész ennek a három algoritmusnak az összehasonlításá
val foglalkozik, hogy a megfelelót tudjuk kiválasztani a mindenköri probléma kezelésére.
A fej lettebb rendezési algoritmusok összehason litásáról
Ahogyan a 6. fejezetben is tettük, az ebben a fejezetben szerepló három algoritmust
is gyakorlati szempontból hasonlítjuk össze, elméleti vagy matematikai megközelítés
alkalmazása helyett. Aki az algoritmusok mögött található matematikára kíváncsi, az
lapozza fel az Algorithms in Java című kiváló kötetet (Sedgwick, 2002). Azért válasz
tottuk ezt a megközelítést, hogy inspiráljuk a kreativitást a saját vagy a mások által írt
kód értékelésekor, és hogy általában véve bátorítsuk az empirikus bizonyítékra való
támaszkodást az elméleti előnyökkel szemben.
194
A fejlettebb rendezési algoritmusok összehasonlításáról
Az előző fejezetben található algoritmusok összehasonlítására használt L i st
sorte rca ll Counti ngTest osztály részleteiért lapozzuk fel a 6. fejezetet. Itt olyan
kódot hozunk létre, amely kiterjeszti ezt az illesztőprogramot, hogy a három fejlett
algoritmust is támogassa. A legrosszabb eset tesztelésére írt kód így fest:
public void testworstcaseshellsort()-{
}
new shellsortListsorter(_comparator).sort(_reverseArrayList); reportcalls(_comparator.getcallcount());
public void testworstcaseQuicksort() {
}
new QuicksortListsorter(_comparator).sort(_reverseArrayList); reportcalls(_comparator.getcallcount());
public void testworstcaseMergesort() {
}
new MergesortListsorter(_comparator).sort(_reverseArrayList); reportcalls(_comparator.getcallcount());
A legjobb esetekre készült kód természetesen nagyon hasonló:
pulili'CV'öi d testBes tcaseshe ll sort ()-{
}
new ShellsortListsorter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount());
public void testBestCaseQuicksort() {
}
new QuicksortListsorter(_comparator).sort(_sortedArrayList); reportCalls(_comparator.getcallcount());
public void testBestcaseMergesort() {
l
new Mergesortlistsorter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount());
Végül nézzük az ádagos esetekre vonatkozó kódot:
public void testAveragecaseshellsort()--{
}
new ShellsortListSorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount());
public void testAveragecaseQuicksort() {
}
new QuicksortListsorter(_comparator).sort(_randomArrayList); reportCalls(_comparator.getcallcount());
195
Fejlettebb rendezés
public void testAveragecaseMergesort() {
}
new MergesortListsorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount());
Ez a kiértékelés csak algoritmus-végrehajtás során teljesített összehasonlítások szá
mát méri. Olyan fontos kérdésekkel nem foglalkozik, mint például a listaelem
áthelyezések száma, amelynek jelentős hatása van arra, hogy egy algoritmus megfe
lel-e egy bizonyos célnak. A vizsgálatot ki kell terjeszteni a saját erőfeszítéseinkkel is,
hogy ezeket az algoritmusokat használni tudjuk.
Vizsgáljuk meg a legrosszabb esetbeli eredményeket mind a hat rendező algo
ritmus esetében. A 6. fejezetből ide másoltuk az alapalgoritmusokra vonatkozó ered
ményeket, hogy ne kelljen visszalapozni hozzájuk:
testworstcaseBubblesort: 499500 hívás testworstcaseselectionsort: 499500 hívás testworstcasernsertionsort: 499500 hívás testworstcaseshellsort: 9894 hívás testworstcaseQuicksort: 749000 hívás testworstcaseMerg�e�s�o� r �t �:------ �4�9�3 �2�h�,�-v� �á�s�--��--�--------------
Hoppá! Mi történt a gyorsrendezéssel? 50 százalékkal több összehasonlítást végzett
el, mint akár az alapalgoritmusoki A Shell-rendezésnek és az összefésüléses rende
zésnek nyilvánvalóan kevesebb összehasonlító műveletre van szüksége, de a gyors
rendezés a legrosszabb (ez alapján az egy mérés alapján). Emlékezzünk arra, hogy a
legrosszabb eset olyan lista, amely teljesen fordított sorrendben áll, vagyis a legki
sebb elem található a lista jobb (legmagasabb indexű) oldalán, arnikor az algoritmus
elkezdődik. Azt is tudjuk, hogy a létrehozott gyorsrendezési megvalósítás mindig a
lista jobb szélén álló elemet választja, és megkísérli két részre osztani a listát, a vá
lasztott elemnél kisebb elemekkel az egyik oldalon, és a nagyobbakkal a másikon.
Ennélfogva a legrosszabb esetben a kettéosztó elem núndig a legkisebb elem, tehát
nem történik kettéosztás. V alójában a kettéosztó elem kivételével nem történik hely
csere, tehát kimerítő összehasonlításra van szükség a kettéosztó elemmel valamennyi
objektum esetében minden lépésben, igen kevés eredménnyel.
Képzelhetjük, hogy ez a pocsékoló viselkedés a kettéosztó elem megtalálására
irányuló okosabb stratégiák kiválasztására sarkallt. Egy lehetséges módszer, amely
kimondottan sokat segít ebben az egyedi helyzetben, három kettéosztó elem kivá
lasztása (egy a lista bal, egy a jobb széléről, egy pedig a közepéről), és a mediánérték
kiválasztása minden lépésnél kettéosztó elemként. Ez a fajta megközelítés jobb
eséllyel ér el valamiféle felosztást először is a legrosszabb esetben.
Alább láthatjuk a legjobb esetbeli tesztek eredményeit:
196
A fejlettebb rendezési algoritmusok összehasonlításáról
testBestcasesubblesort: · 498501.hívás testBestcaseselectionsort: 498501 hívás testBestcaseinsertionsort: 998 hívás testBestcaseshellsort: 4816 hívás testBestcaseQuicksort: 498501 hívás testBestcas�Mer9�sort: 5041 hívás - ----- �--...J
A beszúrásos rendezés kiváló eredményei nem okoznak meglepetést, és ismét csak a gyorsrendezés tűnik a kakukktojásnak a fejlett algoritmusok között. Felmerülhet a
kérdés, hogy miért kellett ezeket megmutatni, ha nem történt fejlődés az alapalgo
ritmusokhoz képest, de vegyük figyelembe, hogy a kettéosztó elem kiválasztása nagymértékben javítható. Ez az eset ismét csak meghiúsítja a gyorsrendezés törekvé
seit az adat felosztására, mivel minden lépésben a legnagyobb elemet találja meg a lista jobb szélén, ezáltal a kettéosztó elem sarokpontként való használatával rengeteg
időt veszteget el az adat felosztására.
Ahogyan az eredményekből látható, a beszúrásos rendezés teljesít a legjobban,
ami a már rendezett adatra vonatkozó összehasonlítási erőfeszítést illeti. Láthattuk,
hogy a Shell-rendezés hogyan csökken végül beszúrásos rendezéssé, mivel az adatot
először majdnem rendezett állapotba teszi. Gyakran használunk beszúrásos rende
zést végső lépésként a gyorsrendezés megvalósításakor. Például amikor a rendezen
dő részlista átlép egy bizonyos küszöböt (mondjuk öt vagy tíz elemet), a gyorsrende
zés leállíthatja a rekurziót, és használhat egy egyszerű inorder beszúr�sos rendezést a
feladat befejezéséhez.
Most lássuk az általános eset eredményeit, amelyek többnyire reálisan tükrözik a
termelési rendszerben elérhető eredményeket:
testAverageCaseBubblesort: testAveragecaseselectionsort: testAveragecaseinsertionsort: testAveragecaseshellsort: testAveragecaseQuicksort: testA�erageCaseMergeSort:
498501 hívás 498501 hívás 251096 hívás
13717 hívás 19727 hívás
896� !lív� .-
A három alapalgoritmus és a három fejlett algoritmus világosan elkülönül egymástól!
A fejezetben található három algoritmus nagyjából huszadannyi összehasonlítást vé
gez egy átlagos esetben az adatok sorba rendezésekor, mint egy alapalgoritmus. Ez
igen nagy különbség az erőfeszítés mértékét illetően nagy adathalmazok sorba ren
dezésénéL A jó hír az, hogy ahogy egyre nagyobbak lesznek az adathalmazok, az al
goritmusok között is úgy nő a szakadék - ezt magunk is megállapítha�uk, ha szem
ügyre vesszük a fejezet végén található feladatokat, és egy kicsit mélyebbre ássuk
magunkat a témában.
197
Fejlettebb rendezés
Mielőtt úgy döntenénk, hogy valamennyi probléma megoldásához összefésüléses rendezést használunk a bemutatott esetekben tanúsított kiemelkedő teljesítménye miatt, gondoljunk arra, hogy az összefésüléses rendezés minden rendezett listáról (és részlistáról) másolatot készít, így sokkal nagyobb a memória- (és valószínűleg az idő-) felhasználása, mit más algoritmusoknak. Ügyeljünk rá, hogy ne vonjunk le olyan következtetéseket, amelyeket nem tudunk bizonyítani, itt és más programozási törekvéseink során.
Összefoglalás
A fejezetben három fejlett rendezési algoritmussal foglalkoztunk. Míg ezek az algoritmusok sokkal összetettebbek és körmönfontabbak a 6. fejezetben használtaknál, sokkal valószínűbb az is, hogy hasznosnak bizonyulnak nagy, gyakorlati természetű problémák megoldásában, amelyekkel programozási pályafutásunk során találkozhatunk. Minden egyes fejlett rendezési algoritmust - a Shell-rendezést, a gyorsrendezést és az összefésüléses rendezést- kimerítően elmagyaráztunk a megvalósítás és a tesztelés előtt.
Érintettük a Shell-rendezésnél és a gyorsrendezésnél tapasztalható inherens stabilitáshiányt, valamint azt, hogy összetett összehasonlító használatával e hátrány kompenzálható. Végül az ebben és az előző fejezetben tárgyalt hat algoritmus egyszerű összehasonlítását mutattuk be, amely lehetővé teszi, hogy megértsük a számtalan választási lehetőség erősségeit és gyengéit.
A következő fejezetben kifinomult adatstruktúrákkal fogunk találkozni, amelyek a sorokról tanultakra épülnek, és néhány rendezési algoritmusokkal kapcsolatos technikát is felhasználnak
Gyakorlatok
1. Valósítsuk meg az összefésüléses rendezést iteratív módon, ahelyett hogy rekurzív módon tennénk!
2. Valósítsuk meg a gyorsrendezést rekurzív helyett iteratív módon!
3. Soroljuk fel a listakezelés módszereit (például set(), add(), insert()) a gyorsrendezés és a Shell-rendezés esetében!
4. Valósítsuk meg a beszúrásos rendezés egy belső verzióját!
5. Hozzuk létre a gyorsrendezés egy olyan verzióját, amely beszúrásos rendezést alkalmaz olyan részlistáknál, amelyek kevesebb mint öt elemből állnak!
198
NYOLCADIK FEJEZET
Prioritásos sorok
Miután az előző két fejezetben jó néhány rendezési algoritmust áttekintettünk, tér
jünk vissza az adatstruktúrák vizsgálatára! A prioritásos sor speciális sor (lásd 4. feje
zet), amely hozzáférést biztosít a legnagyobb tárolt elemhez. Amint később látni fog
juk, a prioritásos soroknak nagyon sokféle alkalmazásuk van. A bemutatásuk előtt
azért kellett részletesen tárgyalnunk a rendezési algoritmusokat, mert az összetettebb
prioritásos sorok megvalósításához elengedheteden ezek mélyreható ismerete.
A prioritásos sorok alkalmazására lehetőség nyílhat például egy szerepjátékban.
Képzeljük el, hogy ellenséges területen kell végigmennünk, az életünket veszélyezte
tő szereplők gyűrűjében. Ezek közül néhány veszélyesebb, mások pedig kevésbé ve
szélyesek lehetnek. Ilyen helyzetben célravezető túlélési stratégia lehet a legveszélye
sebb szereplők gyors felismerése. A cél elérése érdekében nem feltédenül kell tárol
nunk a veszélyforrások teljes rendezett listáját. Feltéve, hogy egyszerre csak egy el
lenséggel harcolunk, bármely pillanatban csak az éppen legkomolyabb ránk leselke
dő veszélyt kell ismernünk. Mire legyőzzük a legkeményebb ellenfelünket, több új
ellenfél is színre léphet, ezért használhatadanná válhat a kezdeti rendezés.
A fejezetben a következő témakörök szerepelnek:
• A prioritásos sorok áttekintése.
• Rendezeden listákból álló prioritásos sor létrehozása.
• Rendezett listás prioritásos sor létrehozása.
• Halmok működése.
• Prioritásos sorok halomalapú megvalósítása.
• Prioritásos sorok különböző megvalósításainak összehasonlítása.
A prioritásos sorok áttekintése
A prioritásos sor olyan adatstruktúra, amely támogatja az adatokhoz való, sorrendben
történő hozzáférést. Az egyszerű sorokkal ellentétben, amelyekből ugyanolyan egymás
utánban nyerhetők ki az adatok, mint amilyenben eltároltuk őket, illetve a UFO-típusú
vermekkel ellentétben, amelyekből a legfrissebben eltárolt adatok nyerhetők ki először,
a prioritásos sorok sokkal rugalmasabb hozzáférést tesznek lehetővé a tárolt adatokhoz.
Prioritásos sarok
A prioritásos sor lehetővé teszi az ügyfélprogram számára, hogy bármely időpilla
natban hozzáférhessen az éppen aktuális legnagyobb, pontosabban maximális elemhez.
(A legnagyobb elem kifejezés helyett legkisebb elemefis írhattunk volna, hiszen egy egyszerű
inverz összehasonlító segitségével mindenféle járulékos költség nélkül felcserélhető a
kettő hatása.) A lényeg, hogy a prioritásos sorok beépített mechanizmussal (összeha
sonlítóval) rendelkeznek az adott pillanatban legnagyobb tárolt elem meghatározására.
A prioritásos sor a mindennaposan használt first-in, first-out (FIFO) típusú so
toknál, illetve a last-in, first-out (LIFO) típusú vermeknél általánosabb forma. Köny
nyerr elképzelhetünk olyan prioritásos sort, amelynek beépített összehasonlítája ép
pen az adat eltárolása óta eltelt időt (FIFO), vagy az eltárolás időpillanatát (LIFO)
veszi alapul. Egy ilyen kialakítású prioritásos sor pontosan ugyanazt a funkcióhal
mazt biztosítaná, mint egy általános verem vagy sor.
Egyszerű példa prioritásos sorra
Tegyük fel, hogy adott egy betűket tartalmazó prioritásos sor. A képzeletbeli kliens-·
programnak egy szósorozatban megtalálható betűket kell beszúrnia a sorba. Az
egyes szavak beszúrása után a program kiolvassa az ábécérend szerinti legnagyobb
betűt a sorbóL A 8.1. ábrán láthatók a felhasználható betűk.
lriHIEIIalul1lciKIIsiRiolwiNIIFiolx J uiMIP Elollolv EIRI
l T l H l E l r--l L"""T""I -A 1.--z_,-y---.1 l 0 l 0 l G l 8.1. ábra. A példabeli prioritásos sor bemenete
lalulllciKIIsiRiolwiNIIFiolx IJiuiMIPIEiollolviEIRI riHIEIILIAizlvlloloiGI
Prioritásos sor
8.2. ábra. Az első szó betűi bekerültek a prioritásos sorba
200
A prioritásos sorok áttekintése
Először a legelső szó betűit szúrjuk be a prioritásos sorba, így a 8.2. ábrán látható
eredményt kapjuk.
A program kiemeli a prioritásos sorban tartalmazott legnagyobb betűt. Amikor az
ügyfélprogram kiveszi a legnagyobb elemet a sorból, visszaadja a hívó folyamatnak.
A példabeli prioritásos sort inkább betűk halmazaként képzeltük el, mint rendezett lis
tájukként. A legtöbb prioritásos sor valóban listaként tárolja az elemeit, de fontos látni,
hogy ez megvalósítási részlet, és nem a prioricisos sor absztrakciójának része.
A 8.3. ábrán látható a legnagyobb betű prioritásos sorból való kiemelése utáni
helyzet.
lalulllciKIIsiRiolwiNIIFiolxl IJiuiMIPIEiollolviEIRI T GE] l L l A l z lv l l D l O l G l
Prioritásos sor
KIMENET
8.3. ábra. A prioritásos sorból eltávolítjuk a legnagyobb betűt
lsiRiolwiNIIFiolx IJiuiMIPIEiollolviEIRI TIHIEIILIAizlvlloloiGI
Prioritásos sor
KIMENET
8.4. ábra. A második szót is ho�áa4Juk a sorho� ma;d kiolvassuk a legnagyobb betűt
201
Prioritásos sorok
Az ügyfélprogram ezt követően a második szóban található összes betűt hozzáadja a
prioritásos sorhoz, majd az előzőekhez hasonló módon kiolvassa a legnagyobbat. Az
eredmény a 8.4. ábrán látható.
Ahogy a 8.5. ábra muta*, a folyamat a harmadik bemeneti szónál is megismétlődik.
IJiuiMIPIEiollolviEIRI T l H l El l L l A l z l y l l D l O l G l
Prioritásos sor
_K_IM_E_N _E_T� � G B
8.5. ábra. A példában szereplő harmadik szót is jeldolgozz;tk
Fl ol x
Mostanra már érthető a prioritásos sor működésének alapelve, ezért a többi szó fel
dolgozásának lépéseit átugorva a 8.6. ábrán a végeredményt láthatjuk.
Prioritásos sor
KIMENET
8.6. ábra. A példabeli prioritásos sor végállapota
A végállapotban a prioritásos sorban található, két azonos értékű legnagyobb betű
kijelölve látható. Az ügyfélprogramnak a következő legnagyobb elem kiválasztására
vonatkozó kérését követően a két elem közül bármelyik törlődhet a sorbóL
202
A prioritásos sorok áttekintése
Prioritásos sorok kezelése
A következő néhány gyakorlópéldában három prioritásos sor megvalósítása a feladat.
Ezek közül mindegyik a 4. fejezetben található Queue interfészt valósí* meg, és bo
nyolultságuk az egyszerű megvalósításoktól egészen az összetettebb, halomstruktú
rára alapozott változatig terjed. Nincs szükség műveleteknek a Queue interfészhez
történő hozzáadására, hiszen a prioritásos sorok lényegében csak a dequeue() metó
dus szemantikáját változtatják meg oly módon, hogy a sorban található éppen legna
gyobb elemet kell visszaadnia.
Gyakorló feladat: AbstractPriorityQueue teszteset létrehozása
Először is definiáljunk egy követelményrendszert, amelyet a prioritásos sor minden
megvalósításának teljesítenie kell. Ahogy a rendezési algoritmusok esetében, most is
úgy járunk el, hogy absztrakt tesztdési eset alapján definiáljuk a teljesítendő köve
telményrendszert, ezzel létrehozva egy osztálygenerátor metódust mint helykijelölőt.
Ily módon valahányszor tesztelni szeretnénk a prioritásos soroknak egy konkrét
megvalósítását, csupán annyi a feladatunk, hogy kiterjesszük ezt az absztrakt osz
tályt, és megvalósítsuk az osztálygenerátor-metódusát, amely majd példányosítja a
konkrét megvalósítást.
Kezdetben definiáljuk a tesztesetünket néhány később felhasználandó konkrét
értékkel együtt, majd hozzunk létre egy példányt a sor tárolására:
public abstract class Abst�actPriorityQueueTestcase
}
extends Testcase { private static final String VALUE_A = "A"; private static final string VALULB = "B"; private static final String VALUE_C = "c"; private static final String VALUE_D = "o"; private static final String VALUE_E = "E";
private Queue _queue;
Ezt követően defmiáljul( a setup O és a tearoown O metódusokat. A setUp O me
tódus az ezt követő, createQueue() nevű absztrakt osztálygenerátor-metódus meg
hívásáért felelős. Ezt a metódust minden egyes teszthez tartozó osztálynak meg kell
valósítania.
protected-\ioi d·
setupO-
throws Exception{ super. setup();
_queue = createQueue(Naturalcomparator.INSTANCE);
}
203
Prioritásos sorok
protected void tearoown() throws Exception { _queue = null;
super.tearoown(); }
otected abstract Queue createQueue(comparator com arable)·
Az első teszteset az üres sor viselkedését írja le. Ez teljes mértékben megegyezik a 4.
fejezetben található, egyéb típusú sorokra vonatkozó tesztekkeL Ennek a kódnak a megismétlése a tesztesetek bonyolultabb hierarchiájának létrehozásával elkerülhető lenne, de most inkább az egyszerűséget és az áttekinthetőséget választottuk. Éles kód esetén azonban nem ajánlatos így dönteni!
public void testAccessAnEmptyQueue() { assertEquals(O, _queue.size()); assertTrue(_queue.isEmpty());
}
try { _queue.dequeue(); fa il();
} catch (EmptyQueueException e) {
ll ezt várjuk }
A következő metódus képezi a prioritasos sor viselkedését vizsgáló teszt alapját. Először is illesszünk be három elemet a sorba, majd ellenőrizzük, hogy a si ze() és i sEmpty() metódusok az elvárt módon működnek-e:
public void testEnqueueoequeue() { _queue.enqueue(VALUE_B); _queue.enqueue(VALUE_D); _queue.enqueue(VALUE-A);
assertEquals(3, _queue.size()); ----�a=ss ertFalse( gueue . isEmQ!Y. �(� )�)� ,·�------------------------------�
Ezt követően ellenőrizzük hogy a dequ eu e() metódus a beillesztett három elem közül a legnagyobbat Gelen esetben egy sztringet, o) adja-e vissza. A fenti kódban ezt az elemet másodikként szúrjuk be a sorba, ezért egy tipikus FIFO-sor vagy UPOverem esetében a sor már ezen a ponton nem felelne meg a teszt elvárásainak Ennek az elemnek a sorból való kiemelése után ismét ellenőrizhetjük, hogy a többi művelet még mindig a várt módon működik-e:
204
assertsame (VAL ULD, _queue. dequeue () )- ; assertEquals(2, _queue.size()); assertFalse(_queue.isEm[!Y.�(� )_ L) d·------------------�··�·----------�
A prioritásos sarok áttekintése
A B sztring a maradék elemek között immár a legnagyobb, ezért a dequeue() metódus következő meghívásakor ezt a karakterláncot kell visszakapnunk:
assertsame(VALUE_B, _queue.dequeue()); assertEquals(l, _queue.size()); assertFaheC..,.�l:!���!'f!l_P..'!Lilli.- ...
Adjunk hozzá még néhány elemet a sorhoz! A prioritásos sorokat használó alkalmazások esetében általánosnak mondható, hogy az enqueue() illetve a dequeue() metódusok hívásai inkább keverednek, mint hogy az alkalmazás egyszer felépítsen egy sort, majd később teljesen kiürítse:
_queue.enquewe(VALilE:EJ; _queue.enqueue(VALUE_C);
assertEquals(3, _queue.size()); ��ertF�seS_queue . isEf!!_Q!Y.())� ;------· ------------------------------�
J elenleg három sztring található a prioritásos sorban: A, E és c. Ezeknek nagyság szerinti csökkenő sorrendben kell kikerülniük a sorbóL Éppen ezért a teszt utolsó lépése, hogy mindhármat kiemeljük a sorból, miközben ügyelünk rá, hogy a size() és az isEmpty() metódusok működése konzisztens maradjon:
}
assertsameCVALlJE_E , _queue. deqúeue O); assertEquals(2, _queue.size()); assertFalse(_queue.isEmpty());
assertsame(VALUE_C, _queue.dequeue()); assertEquals(l, _queue.size()); assertFalse(_queue.isEmpty());
assertsame(VALUE_A, _queue.dequeue()); assertEquals(O, _queue.size()); assertTrue(_queue.isEmpty());
A tesztesetet egy utolsó általános sorteszttel zárjuk, amelynek célja a cl ear() metódus itt bemutatott módon történő ellenőrzése:
pulliiCVölC:i--testclearO { _queue.enqueue(VALUE_A); _queue.enqueue(VALUE_B); _queue.enqueue(VALUE_C);
assertFalse(_queue.isEmpty());
_queue.clear();
assertTrue(_queue.isEmpty()); }
205
Prioritásos sorok
Ezzel elérkeztünk az általános prioritásos sor tesz�ének végéhez. Térjünk is rá az el
ső megvalósításra, ez esetünkben egy nagyon egyszerű, listaalapú sor lesz, amely a
felhasználó kérésére képes kiválasztani a legnagyobb elemet.
Rendezetlen listás prioritásos sor áttekintése
A prioricisos sorok legegyszerűbb megvalósítása abból áll, hogy valarnilyen halmazt
építünk fel az elemekből, majd a dequeue() metódus minden meghívásakor újra és
újra kikeressüle a legnagyobb elemet. Nyilvánvaló, hogy az ilyen letámadásos algorit
musok O(N) komplexitásúak a keresés szempon�ából, de bizonyos alkalmazások ese
tében ez elfogadható is lehet: Amennyiben például tudjuk, hogy a felhasználó nem túl
gyakran hívja meg a dequeue() metódust, akkor a legegyszerűbb módszer egyben a
legjobbnak bizonyulhat. Ennek a módszernek az előnye, hogy az enqueue () metódus
O(l) komplexitású, ennél pedig nehéz jobbat elképzelni.
A következő példában a feladat egy egyszerű prioritásos sor megvalósítása,
amely listában tárolja az elraktározott elemeket.
Gyakorlófeladat: rendezetlen listát használó prioritásos sor megvalósítása
Ebben az esetben L i n ked L i st adatstruktúrát használunk, de természetesen az Array
L i st is működne.
Először is a korábban megvalósított AbstractPriori tyQueueTestcase tesztese
tet kibővítjük, és megvalósítjuk a createQueue() metódust, ezzel példányosítva a je
lenleg még nem létező megvalósítás t:
public class unsortedListPriorityQueueTest extends AbstractPriorityQueueTestcase {
protected Queue createQueue(comparator comparator) { return new unsortedListPriorityQueue(comparator);
} J.
A megvalósításnak sok kijzös vonása van a 4. fejezetben látott egyéb sor megvalósítá
sokkaL A kijmryebb átláthatóság érdekében a teljes kódot megismételjük.
Először is hozzuk létre a megvalósításhoz tartozó osztályt, és definiáljunk két példány
tagot: egy az elemek tárolását szolgáló listát, valamint egy összehasonlítót az elemek
viszonyának megállapítására. Ezenkívül hozzunk létre egy konstruktor metódust is:
206
public class unsortedListPriorityQueue implements Queue { private final List _list; J)rivate fina,l Com arator com arator·
A prioritásos sorok áttekintése
}
publ i c Unsortedl i stP ri orhyQueue(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator; _list = new LinkedList();
}
Az enqueue () metódus megvalósítása nem is lehetne egyszerűbb: egyszerűen hoz
záadunk egy elemet a lista végére:
public void enqueue,Cöl:ijectvalue)-{ _list.add(value);
}
A dequeue() metódus megvalósításában először is ellenőriznünk kell, hogy az eleme
ket tároló lista nem üres-e. Amennyiben üres listára hívjuk meg, a metódusnak kivételt
kell dobnia. Ha ellenben a lista legalább egy elemet tartalmaz, a getrndexofLargest
El emen t() metódus segítségével megkeressük a legnagyobb elemhez tartozó indexet
pul:il.iC öb]ect dequeue() throws EmptyQueueExceptiOil{ if (isEmpty()) {
throw new EmptyQueueException(); } return _list.delete(getindexOfLargestElement());
l _____ _
Ahhoz, hogy megkeressük a legnagyobb elemhez tartozó indexet, végig kell lépked
nünk a lista egyes elemein, útközben feljegyezve az addig szembejövő legnagyobb
elem indexét. Egyébként megjegyezzük, hogy a metódus megvalósítása egyszerűbb
lenne ArrayList adatstruktúra felhasználásával, mint az általunk választott L i n ked
L i st segítségéveL Vajon miért?
private int getindexOfLargestElement()--{ int result = O;
}
for (int i =l; i < _list.size(); ++i) {
}
if (_comparator.compare(_list.get(i), _list.get(result)) >O) { result =i;
}
return result;
Végezetül már csak a Queue interfész metódusainak megvalósítása van hátra. Ezek
minden egyes listaalapú sor megvalósításában ugyanúgy néznek ki:
207
Prioritásos sarok
public void clear() { _list.clear();
}
public int size() {
return _list.size();
}
public boolean isEmpty() {
return _list.isEmpty();
}
Ha ezt a kódot lefutta�uk, látni fogjuk, hogy a mégvalósítás pontosan az elvárt módon műköclik. Feladatunk végeztével térjünk rá a prioritásos sorok egy olyan megvalósítására, amely mentes a letámadásos keresés minden formájától!
A megvalósitás működése
A prioritásos sorok rendezetlen listán alapuló megvalósítása nagyon egyszerű. Új elem beszúrásakor egyszerűen meg kell hívni a sor objektum add O metódusát, amely az elemet hozzáfűzi a lista végéhez. Elem kiolvasásakor egyszerűen végig kell lépkedni a sor belső listáján, így kiválasztva az összes tárolt elem közül a legnagyobbat. Miután ismerjük a lista legnagyobb elemének helyét, könnyűszerrel eltávolítha�uk a listábóL
Rendezetlen listás prioritásos sor megvalósítása
A fenti letámadásos letapogatás elkerülésének egy lehetséges módja annak biztosítása, hogy az elemeket beszúrásuk után rendezetten tároljuk, és így a dequeueO metódus meghívásakor azonnal rendelkezésre álljon a legnagyobb elem. Ezzel a módszerrel a dequeue() metódus nagyon gyors lesz, viszont az enqueue() metódus megvalósítása kevésbé egyszerű.
A következő gyakorlófeladatban az enqueue() metódus meghívása után beszúrásos rendezési mechanizmust építünk a programba, amelynek segítségével rendezett pozícióba kerülnek az újonnan beszúrt elemek. Ezek után a dequeue O metódus működése hihetetlenül egyszerű - csupán a lista végén található elemet kell lecsípni.
Gyakorlófeladat: rendezett listát használó priodtásos sor megvalósítása
A feladat rendezett listát használó prioritásos sor megvalósítása. Terjesszük ki az AbstractPri orityQueueTestcase tesztesetet a konkrét megva
lósítás alapján:
208
A prioritásos sorok áttekintése
pub1Tccl asssortei:l[i"stPri ori tyQUeueTest extends AbstractPriorityQueueTestCase {
protected QUeue createQueue(Comparator comparator) { return new sortedListPriorityQueue(comparator);
} }--------------------------�---�----------
A megvalósítás struktúrája alapvetően megegyezik a korábban bemutatott, rendezet
len listás változat felépítéséyel. Ebben a megvalósításban is ugyanazokat a példány
tagokat, illetve ugyanazt a konstruktort használjuk:
putill c el ass sorteaCi stPrtörhyQueue i iiiPl ements ·Ciüeue { private final List _list;
}
private final Comparator _comparator;
public sortedListPriorityQueue(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator; _list= new LinkedList();
}
Az enqueue() metódusban lépkedjünk visszafelé a listában egészen addig, amíg meg
nem találjuk a beszúrandó elemnek megfelelő helyet:
públic void enqueue(oli)ect value)-{ int pos = _list.size();
}
while (pos> O && _comparator.compare(_list.get(pos- 1), value) > O) {
--pos; } _l{st.insert(pos, value);
A dequeue() metódusnak pedig annyi a feladata, hogy eltávolítsa az utolsó elemet a
listábóL Ne felejtsünk el kivételt dobni abban az esetben, ha a lista üres, és nincs mit
eltávolítani belőle:
putilic·Öb-ject dequeue():-t hrows EJRptyQUeueException{ if (isE�pty()) {
}
throw new EJRptyQueueException(); } return _list.delete(_list.size() - l);
Ezek után már csak néhány metódus megvalósítása van hátra, amelyek semmiben
sem térnek el a korábban már megszakott sor megvalósításokban látottaktól.
209
Prioritásos sarok
public void clear() { _list.clear();
}
public int size() { return _list.size();
}
public boolean isEmpty() { return _list.isEmpty();
}
Ezzel végeztünk a prioritásos sorok rendezett listás megvalósításávaL A tesztesetet
lefuttatva látható, hogy a prioritásos sor viselkedése megfelel a korábban kitűzött
kritériumoknak. A következő részben a prioritásos sorok legbonyolultabb, de egy
ben leghatékonyabb és legpraktikusabb megvalósítását vizsgáljuk, amely egy halom
nak nevezett adatstruktúra használatán alapul.
A megvalósitás müködése
Az enqueue() metódus megvalósítása ebben az esetben valamivel bonyolultabb,
mint az előző módszereknél volt. Feladata most is egy listában megtalálni a beszú
randó elemnek megfelelő helyet. A célját úgy éri el, hogy hátulról kezdve végighalad
a tárolt elemeken, egészen addig, amíg egy kisebb elemet nem talál, vagy amíg a lista
elejére nem ér. Mihelyt megtalálja az optimális pozíciót, beszúrja a szóban forgó
elemet. Ezzel a módszerrel a metódus biztosítja, hogy a legnagyobb tárolt elem
minden pillanatban a lista végén helyezkedik el.
A módszerben rejlő pluszköltséget azért érdemes vállalni, mert ekkor a dequeue()
metódusnak már nincs más dolga, mint a lista legutolsó elemét kiemelni a sorból és
visszaadni a hívó programnak.
Halmon alapuló prioritásos sorok működése
A halom nagyon hasznos és érdekes adatstruktúra, ezért elengedheteden hogy ebben
a részben megértsük, hogyan működik. A halom működési elvének megértése után
az olvasó képes lesz prioricisos sorokat hatékony módon megvalósítani.
A halom egy bináris fastruktúra, amelynek minden csomópon�a nagyobb, mint
saját gyermekei. Ezt a kritériumot halomfeltételnek szokás nevezni. V együk észre a
8.7. ábrán, hogy minden egyes csomópont értéke nagyobb, mint gyermekeié (ha
egyáltalán van gyermeke).
210
A prioritásos sarok áttekintése
8. 7. ábra. A halom
Óvakodjunk attól, hogy a halmot rendezettnek tekintsük, ugyanis egyáltalán nem az. Van azonban egy hasznos tulajdonsága, amely a feladat szempontjából nagyon is fontos: a halom definíciójából adódóan a legnagyobb tárolt elem éppen a gyökércsomópontban helyezkedik el. A többi tárolt elem rendezettsége vajmi keveset szárnit. Vegyük észre például, hogy várakozásainkkal ellentétben a halom legkisebb eleme G elen esetben az A betű) nem a fa legalsó szin�én helyezkedik el.
Az olvasó bizonyára kiváncsi, hogy a fejezet tartalmazza-e egy Heap interfész vagy Tree interfész definícióját, illetve megvalósítását. Ebben a példában nem ezt a szemléletmódot követjük. Esetünkben egy egyszerű listát használunk a halornstuktúra reprezentálására. A 8.8. ábrán látható technikával könnyen megfeleltethetjük a halomban tárolt elemek pozícióját a listaelemek pozíciójának. A fa gyökerét O-val indexeljük, majd lefelé, balról jobbra haladva sorszámozzuk az elemeket.
s.s. ábra. Halom elemeinek számoi!sa listában elfoglalt he!Jiiknek megfelelően
Ez a megközelítés lehetővé teszi, hogy mentális modellt alkossunk a fastruktúráról egy olyan megvalósításban, amely egyáltalán nem is használ fastruktúrát. A 8.9. ábrán látható, hogyan nézne ki egy lista, ha a példabeli halomstuktúrát tartalrnazná.
A halomstuktúra hatékony alkalmazása érdekében fontos, hogy képesek legyünk felfelé, illetve lefelé mozogni a képzeletbeli fában. Ezért egy csomópont indexét ismerve meg kell tudnunk határozni a bal és a jobb oldali gyermekének az indexét, valamint hasonlóképpen szülőcsomópon�ának az indexét is. Ezt a következőképpen tehe�ük:
211
Prioritásos sorok
• Az X indexű csomópont bal oldali gyermekének indexe (2 X X+ 1).
• Az X indexű csomópont jobb oldali gyermekének indexe (2 x X + 2).
• Az X indexű csomópont szülőjének indexe ((X - 1) / 2); és a O indexű
csomópontnak természetesen nincs szülője.
A fenti képletek helyességét belátandó, próbáljuk ki őket az ábrákon található ha
lomstuktúrákon! Amennyiben jobb oldali gyermeket vizsgálunk, a szülőindex képle
te csonkolásorr alapul. Erről azonnal meggyőződhetünk, amint megpróbálunk hoz
záférni a 3.5-ös listaindexhez.
8.9. ábra. Listábafoglalt halomstrnktúra
Lesüllyed és felszivárog
Prioritásos sor halommal történő megvalósításakor képesnek kell lennünk elemek
halomba való beillesztésére, illetve eltávolítására. Ez első körben triviális feladatnak
tűnhet, de ne feledjük, hogy a műveletek elvégzése közben is teljesülnie kell a ha
lomfeltételnek - vagyis biztosítanunk kell, hogy a halom elemek hozzáadása és eltá
volítása után is halom marad.
Bővítsük ki a példabeli halmot egy P betűvel! Első körben tegyük a fasttuktúra
legalj ára, ahogy a 8.1 O. ábra is mutatja.
Mivel a halmot egyszerű listában tároljuk, egész egyszerűen a lista végéhez fűz
zük az új elemet. A probléma csak annyi, hogy a halom már nem halom! Ez azért
van, mert az új elem (P) szülőcsomópontja (A) kisebb, mint maga az új elem. Ah
hoz, hogy helyreállítsuk a halmot, és teljesüljön a halomfeltétel, az új elemnek fel kell
jutnia a fastruktúrában egészen addig a pontig, ahol már nem sérül a halomfeltéteL
Ezt jelsz!várgásnak nevezzük.
212
A prioritásos sarok áttekintése
8.1 o. ábra. A halomba 4/ elemet szúnmk be, ame!J filborítja a halomfiltételt
A felszivárgás annyiból áll, hogy felcseréljük az elemet a szülőjével, amennyiben a
szülő értéke kisebb a szóban forgó elemnéL Ezt az eljárást mindadclig folytatjuk, anúg
vagy nagyobb nem lesz az elemnél a szülője, vagy el nem érjük a fastruktúra gyökerét.
A 8.11. ábrán látható, miként cseréljük fel az új elemet a szülőcsomóponttal.
8.11. ábra. Az tij elem he!Jet cserél a szüli!Jével
Az első csere után még minclig nem teljesül a halomfeltétel, mivel az új elem (P) még
minclig nagyobb a szülőjénél (M), ezért folytatnia kell a felszivárgást. A 8.12. ábrán
látható módon újból helyet kell cserélnie a szülőjével.
8. 12. ábra. Az 4J elem eléri végső po:ifciiját a halomstmktúrában
A halomfeltételt ezzel helyreállitottuk, és a halom egy elemmel többet tartalmaz,
mint korábban. A következő kihívás ugyanezt fordítva megtenni, amikor eltávolí
tunk egy elemet.
213
Prioritásos sarok
A legnagyobb tárolt elemet megtalálni könnyű, de kiemelni a halomból már nem
annyira. Ha egyszerűen törölnénk a listából, a fasttuktúra teljesen szétesne, és újból
kezdhetnénk az egész struktúra felépítését. (Az olvasóra bízzuk ennek kipróbálását,
amennyiben szabadidejében motivációt érez erre.) Ahelyett hogy az elem pozícióját
egyszerűen üresen hagynánk, belehelyezhetjük a fa legalsóbb szintjének legszélső,
jobb oldali pozíciójában elhelyezkedő elemet, ahogy a 8.13. ábra mutatja.
8.13. ábra. A legnagyobb elemet eftávo!íijuk, és a lista utolsó e/emét ralguk a helJére
Annak ellenére, hogy maga a fasttuktúra sérteden marad, a halomfeltétel ismét sérül:
a legkisebb elem most a gyökérben helyezkedik el. Ahhoz, hogy a halomstruktúra
helyreálljon, az elemnek lejjebb kell kerülnie a fastruktúrában. Ezt a folyamatot le
sü!!Jedésnek nevezzük.
A lesüllyedés az a folyamat, amikor egy elem helyet cserél a gyermekei közill a
nagyobbik csomóponttal egészen addig, amíg vagy nem teljesül a halomfeltétel, vagy
el nem éri a fa legalsó szintjét. Példánkban a süllyedő elem gyermekei közill a P a
nagyobb, ezért helyet kell cserélnie az A betűvel. A 8.14. ábra mutatja a halom álla
potát, miután megtörtént az első helycsere.
8.14. ábra. A legfelső elem egy s�nttel fdjebb süi!Jedt
A halomfeltétel még így is sérül, mert az A betű mindkét gyermekénél kisebb. A gyer
mekek közill az Ma nagyobb, ezért helyet cserél a szülővel. A 8.15. ábrán látható a ha
lom jelenlegi állapota.
214
A prioritásos sorok áttekintése
0
8.15. ábra. A ha/omtulqjdonság a sülfyedés fofyamata után hefyreá/1
A halomfeltétel ismét teljesül, miközben eltávolítottuk a legnagyobb elemet, és a ha
lom immár eggyel kevesebb elemet tartalmaz, mint korábban. Ezekkel az új ismere
tekkel felvértezve rátérhetünk a prioritásos sor megvalósítására.
A következő gyakorlófeladatban olyan prioritásos sort valósítunk meg és teszte
lünk, amelyben az elemek halmot reprezentáló listában helyezkednek el. A fejezet
ben tárgyalt három megvalósítás közül ez a legbonyolultabb.
Gyakorlófeladat: halomalapúprioritásos sor tesztelése
Először is hozzunk létre a halamalapú prioritásos sorhoz tartozó tesztesetet:
public class HeaporderedListPriorityQueueTest
}
extends AbstractPriorityQueueTestcase { protected Queue createQueue(Comparator comparator) {
return new HeaporderedListPriorityQueue(comparator);
}
A megvalásítást ugyanolyan alapegységekre bontjuk, mint a korábbi kétprioritásos
sornál tettük: definiálunk egy listát, amely az elemek tárolását szolgálja, illetve egy, az
elemek sorrendezésére használatos Comparator objektumot:
public class HeaporderedList�riorityQueue 1mplements Queue { private final List _list;
}
private final comparator _comparator;
public HeaporderedListPriorityQueue(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator; _list = new ArrayList();
}
Az enqueue() metódus hozzáadja a lista végéhez az új elemet, majd felszivárogtatja
a lialomstruktúrában.
215
Prioritásos sarok
public void enqueue(ooject value) { _list.add(value); swim(_list.size() - l);
}
Egy swimO metódust is létrehozunk, amely paraméterként a felszivárogtatott elem
indexét fogadja el. Az indexhez tartozó elemet összehasonlítjuk a szülőjével (ha
van), és felcseréljük a kettőt, amennyiben a szülője kisebb. A swim() metódust re
kurzív módon újrahívjuk, ha folytatnunk kell a folyamatot:
private void swim(int index) { if (index == O) {
return; } int parent = (index - l) l 2; if (_comparator.compare(_list.get(index),
_list.get(parent)) > O) {
} J.
swap(index, parent); swim(parent);
A korábbiakban már számos swap() metódust valósítottunk meg, ezért a következő
kódrészlet már bizonyára nem okoz gondot:
private void swap(int indexl, int index2) { Object temp = _list.get(indexl); _list.set(indexl, _list.get(index2)); _list.set(index2, temp);
.}
Térjünk rá a dequeue() metódus megvalósításárai A metódus a lista legelején talál
ható elemet adja vissza. Mielőtt azonban tényleg visszatérne, a lista utolsó elemét át
kell helyeznie a lista elejére, majd le kell süllyesztenie mindaddig, amíg nem teljesül a
halomfel tétel:
216
public object �equeue() throws EmptyQueueException { if (isEmpty()) {
}
throw new EmptyQueueException(); } object result = _list.get(O); if (_list.size() > l) {
}
_list.set(O, _list.get(_list.size() - l)); si nk(O);
_list.delete(_list.size() - l); return result;
A prioritásos sorok áttekintése
Most hozzunk létre egy si nk() metódust, amely a bemenő elemet felcseréli nagyobbik
gyermekével. Ne felejtsük el azokat a speciális eseteket is kezelni, amikor egy csomó
pontnak nem két, hanem egy gyermeke van, vagy esetleg egyáltalán nincs gyermeke.
private voia:SinK(i�index)-{ int left ; index * 2 + l; int right = index * 2 + 2 ;
l
if (left >= _list.size()) { return;
}
int largestChild = left; if (right < _list.size()) {
}
if (_comparator.compare(_list.get(left), _list.get(right)) < O) {
largestchild = right; }
if (_comparator.compare(_list.get(index), _list.get(largestchild)) < 0) {
swap(index, largestchild); sink(largestChild);
}
Ennek a kódnak az áttekintése fárasztó lehet, ám a jó hir az, hogy a maradék metó
dus a lehető legegyszerubb:
pulllic voiaclear()-
{ _li st. clear();
}
public int size() { return _list.size();
}
public boolean isEmpty() { return _list.isEmpty();
l
A megvalósitás müködése
Az enqueue O metódus működése egyszerű, mert minden bonyolult feladatát a
swi m () metódusnak adja át, mihelyt hozzáadta az új elemet a lista végére. A swi m ()
metódus paramétere annak az elemnek az indexe, amelyet fel szeretnénk szivárog
tatni. A swim() metódus feladata, hogy összehasonlítsa a paraméterben kapott in
dexhez tartozó elemet a szülőjével, és hogy felcserélje a kettőt abban az esetben, ha
217
Prioritásos sarok
az elem nagyobb a szülőjénél. Amennyiben valóban szükség van cserére, a metódus újból meghívja önmagát, hogy rekurzív módon folytatódhasson a folyamat a halom feljebbi részein. A metódus leáll, ha a paraméterként kapott index O, inivel ez azt jelenti, hogy elérkeztünk a halom gyökeréhez. V együk észre, hogy az elemek szüleinek indexét a korábban ismertetett képlet segítségével számoljuk ki.
A dequeue() metódus megvalósításában először is megkeressük az eredményül visszaadandó elemet. Ez nem nehéz feladat, mivel ez mindig a lista 0-s indexéhez tartozó elem. Bár a metódusnak ezt az elemet kell visszaadnia, nem feltétlenül ezt kell kitörölnünk a listábóL A listából kivétel nélkül mindig az utolsó elemet töröljük. Amennyiben a lista csak egyetlen elemet tartalmaz, akkor ezt az elemet kell törölni; minden más esetben fel kell cserélni az utolsó elemmel, majd az új gyökérelemet le kell süllyeszteni mindaddig, amíg újból nem teljesül a halomfeltéteL
A sink() metódusunk sajnos jóval bonyolultabb, mint a swim() metódus, mert jó néhány érdekes esetet figyelembe kell vennünk. Lehet, hogy a szóban forgó elemnek csak egy gyermeke van, vagy egyáltalán nincs is gyermeke. Ha létezik jobb oldali gyermeke, akkor biztosan van bal oldali is, ezért azzal az esettel nem foglalkozunk, amikor egy csomópontnak csak jobb oldali gyermeke van.
Először is ki kell számítani a gyermekek indexeit. Ha az így kapott indexek kívül
esnek az elemek indexeinek érvényes tartományán, akkor lejjebb már nem süllyedhet a szóban forgó elem. Ezt követően el kell dönteni, hogy a gyermekek közül (most már tudjuk, hogy legalább egy gyermek van) melyik a nagyobb. Ha egyáltalán szükség van cserére, a két gyermek közül a nagyobbikkal cserél helyet az elem. Kezdetben feltételezzük, hogy a bal oldali gyermek értéke a nagyobb, és ezen a feltételezésen csak akkor változtatunk, ha egyáltalán létezik jobb oldali gyermek, és az nagyobb is a bal oldalinál.
Ezen a ponton tehát tudjuk, melyik a nagyobb gyermek. A feladatunk már csak annyi, hogy összehasonlítsuk magát az elemet a nagyobbik gyermekével. Ha a gyermek nagyobb, fel kell cserélni a kettőt, majd rekurzív módon újra meg kell hívni a si nk() metódust, hogy tovább süllyedhessen az elem a halomban.
A prioritásos sorok halamalapú irnplementációja a fejezetben található legfejlettebb változat. A módszer azért is érdekes, mert o(log N) komplexitásban képes elemeket beszúrni a sorba, illetve elemeket törölni belőle. Minden olyan algoritmus, amelynek a bonyolultsága arányos az elemeket tartalmazó bináris fa mélységével, rendelkezik ezzel a tulajdonsággal, és hatalmas előnyei vannak az elemeket lineáris módon kezelő algoritmusokkal szemben. A következő szakaszban összehasonli�uk a három prioritásos sor megvalósítást, és megvizsgáljuk, miképpen viszonyulnak egymáshoz.
218
Prioritásos sorok megvalósításainak összehasonlítása
Prioritásos sorok megvalósításainak összehason litása
Ahogy a korábbi fejezetekben is tettük, most is inkább gyakorlati, mint elméleti szemszögből közelí�ük meg a bemutatott módszerek összehasonlítását. Ismét a ca ll counti ngcomparator osztályt használjuk annak megállapítására, hogy a különböző megvalósítások mekkora számítási igény árán érik el céljukat. Óvakodjunk attól, hogy a kiértékelésnek ezt a formáját egyedüli irányadónak tekintsük. Ehelyett használjuk arra, hogy további vizsgálódásokat ösztönözve új látásmódot szerezzünk. Számos, elméleti síkon is kifogástalan összehasonlítási szempont létezik. Ha az olvasót ezek mélyebben érdeklik, a B függelékben további információkat olvashat róluk.
Ahogy az előző fejezet összehasonlításokról szóló szakaszában, most is a legjobb, a legrosszabb és az átlagos eseteket vizsgáljuk. Műveletek egész halmazát végezzük el, amikor elemeket adunk hozzá, illetve távolítunk el a vizsgált sorokbóL A legjobb esetben rendezett sorrendben szúrjuk be a sorba az adatokat. A legrosszabb esetben fordítottan rendezett sorrendben érkeznek az elemek a sorba, az átlagos esetekben pedig véletlen módon érkező adatokat szúrunk bele.
A tesztfuttató osztály alapvető struktúráját az alábbiakban mutatjuk be. Először is, a tesztek méretének kézben tartása végett definiálunk egy konstanst, majd definiáljuk a legjobb, a legrosszabb, illetve az átlagos esetekhez tartozó listákat. Végül létrehozunk egy calleounti ngcomparator-t, hogy gyűjtse a számunkra fontos statisztikákat:
pubii"C"'"'Ci as'sPriori tyQueűeca ll C:ountfngTest extends' Testeas e{ private static final int TEST_5IZE = 1000;
}
private final List _sortedList =new ArrayList(TEST_SIZE); private final List _reverseList =new ArrayList(TEST_SIZE); private final List _randomList =new ArrayList(TEST_SIZE);
private Callcountingeomparator _co.parator;
A setup() metódus példányosítja az összehasonlítót, és feltölti a három listát a megfelelő tesztadatokkal:
protectedvoi(f setup() -throW"s .. exó!pt:ion .. { super. setup(); _comparator =
new callcountingeomparator(Naturalcomparator.INSTANCE);
for (int i = l; i < TEST_SIZE; ++i) { _sortedList.add(new rnteger(i));
l
219
Prioritásos sorok
}
for (int i = TEST_SIZE; i > 0; --i) { _reverseList.add(new Integer(i));
}
for (int i = l; i < TEST_SIZE; ++i) { _randomList.add(new Integer((int)(TEST_SIZE * Math.random())));
}
Ezután következik a három legrosszabb eset forgatókönyve, amelyek mindegyikét a
runscenario() metódus kezeli:
public void testworstcaseunsortedList() {
}
runscenario(new unsortedListPriorityQueue(_comparator), _reverseList);
public void testworstcasesortedList() {
}
runscenario(new sortedListPriorityQueue(_comparator), _reverseList);
public void testworstcaseHeapOrderedList() {
.l
runscenario(new HeaporderedListPriorityQueue(_comparator), _reverseList);
Ezután definiáljuk a három legjobb eset forgatókönyvét, minden prioritásossor-megvalósításhoz egyet -egyet:
public void testBestcaseunsortedList() {
}
runscenario(new unsortedListPriorityQueue(_comparator), _sortedList);
public void testBestcasesortedList() {
}
runscenario(new sortedListPriorityQUeue(_comparator), _sortedList);
public void testBestcaseHeapOrderedList() {
}
runscenario(new HeaporderedListPriorityQueue(_comparator), _sortedList);
Végül tekintsünk három tipikus forgatókönyvet:
220
publ i c voi d te-stAveragecaseunsortedL i st() {
runscenario(new unsortedListPriorityQueue(_comparator), _randomList);
}
Prioritásos sorok megvalósításainak összehasonlítása
putiíicvoi'éf tésiAvera.QecisesortedCistO� { runscenario(new sortedListPriorityQueue(_comparator),
_randomList); }
public void testAverageeaseHeapOrderedList() { runscenario(new HeaporderedListPriorityQueue(_comparator),
_randomList); }
A következőkben a runscenari o() metódust vizsgáljuk. A metódus két paramétert
fogad el: a tesztelendő sort és a bemenő adatokat tartalmazó listát.- Feladata annyi,
hogy végiglépked a bemenő adatokon, hozzáadva őket a tesztelés alatt álló sorhoz.
Minden századik elem beszúrása után azonban megáll, és 25 elemet eltávolít. Ezek a
paraméterek teljesen hasraütésszerűen születtek, és céljuk csupán annyi, hogy a gya
korlati alkalmazásokat szimulálandó megfelelő módon keveredjen az enqueue() me
tódus, illetve a dequeue () metódus használata. lVIielőtt a metódus befejezné munká
ját, kitörli az egész lis tát, és meglúvja a r epo r tea ll s () metódust, amely kiírja a teszt
eredményeit a szabványos kimenetre:
p ri vat e voi d ruiiscenari o (Queue -qüeue:· ·c;st"-·i ii"put) -{ int i = O;
l
Iterator iterator = input.iterator(); iterator.first(); while (!iterator.isoone()) {
}
++i; queue.enqueue(iterator.current()); if (i % 100 == O) {
}
for (int j= O; j.< 25;++ j) { queue. dequeue () ;
}
iterator.next();
while (!queue.isEmpty()) { queue. dequeue () ;
} reportealls();
Az illesztőprogram utolsó metódusa összesítést készít a tesztfuttatás alatt végzett
összehasonlítások számáról:
private voia reportealls()--{
}
int calleount = _comparator.getealleount(); system.out.println(getName() + ": "+ calleount + "hívás");
221
Prioritásos sorok
A legrosszabb esetben kapott eredmények az alábbiakban láthatók a három prioritásos sor megvalósításhoz kapcsolódóan:
testworstcaseunsortedList: 387000 hívás testworstcasesortedList: 387000 hívás testworstcaseHea rderedList: 15286 ívás
Egyértelmű a haloroalapú módszer fölénye, miközben a két egyszerűbb változat döntetlenül áll. A következőkben a legjobb esethez tartozó teszt eredményeit foglaljuk össze:
testBestcaseunsortedList:
estBestcaseHea
hívás hívás h'vás
Ez első ránézésre érdekes, de ha figyelembe vesszük, hogy a beszúrásos rendezés rendkívül hatékonyan működik eleve rendezett bemeneten, akkor egyáltalán nem meglepő, hogy ebben a tesztben a rendezett listás sor megvalósítása teljesít a legjobban. A letámadásos változat szinte semmiben sem különbözik a rendezett listástól, viszont a haloroalapú módszerrel körülbelül 50 százalékkal több ro űveletet kell elvégezni.
Végezetül tekintsük meg az ahhoz a teszthez tartozó eredményeket, amely leginkább tükrözi a valóságban előforduló eseteket:
testAveragecaseunsortedList: 386226 hívás testAveragecasesortedList: 153172 hívás testAveragecas�HeapQ deredList: 17324 hívás
Szemmel látható, hogy a rendezett listás változat körülbelül feleannyi összehasonlítást végez, mint a letámadásos változat, miközben a haloroalapú megvalósítás ismét átveszi a vezető helyet. A halomstruktúrára alapozott megvalósítás egyértelműen a leghatékonyabb, ha ezt a tesztet mérvadónak tekin�ük - ez pedig az adott helyzettól függ. Komprornisszumot kell kötni a nagyobb komplexitás és a nagyobb hatékonyság között ahhoz, hogy eldöntsük, melyik módszer felel meg leginkább az alkalmazás elvárásainak.
Összefoglalás
Ebben a fejezetben néhány fontos témakört tárgyaltunk.
222
• . Új adatstruktúrát ismertünk tneg: a priorirásos sort. A prioritásos sor a 4. fe
jezetben tárgyalt sor általánosabb változata.
• A prioricisos sor bármely pillanatban hozzáférést biztosít az éppen legnagyobb tárolt elernhez. A sorban elhelyezkedő elemek egymáshoz való viszonyát ös szehason l í tó segítségével állapítottuk meg.
Gyakorlatok
• A prioritásos sorok három megvalósítását készítettük el. Ezek közill a legegyszerűbb egy egyszerű listának a végére fűzte az újabb elemeket, majd az összes elemet érintő lineáris keresést végzett a legnagyobb elem törlésekor. A második megvalósítás a korábbiakhoz képest abban hozott újítást, hogy a tárolt elemeket mindig rendezett listában tartotta, ezzel jelentősen megkönnyítve a legnagyobb elem megkeresését és törlését. Az utolsó megoldásban listaként tárolt halomstruktúrát használtunk, és ezzel szemmel látható módon javítottunk a korábbi megvalósítások hatékonyságán, mind a beszúrás, mind a törlés műveleteinél. Mélyrehatóan megvizsgáltuk a halom működését.
• A három megvalásítást összehasonlítottuk, és inkább gyakorlati, mint elméleti szempontok alapján rangsoroltuk őket.
Gyakorlatok
Az újonnan szerezett ismeretek további elmélyítése érdekében oldjuk meg a következő feladatokat:
1. Prioritásos sor felhasználásával valósítsunk meg egy Stack-et!
2. Prioritásos sor felhasználásával valósítsunk meg egy FI FO-sort!
3. Prioritásos sor felhasználásával valósítsunk meg egy L istsorter objektumot!
4. Készítsünk prioritásos sort, amely a legnagyobb elem helyett a legkisebb elemhez biztosít hozzáférést!
223
KILENCEDIK FEJEZET
Bináris keresés és beszúrás
A könyv eddigi fejezeteiben alapvető struktúrák áttekintésével foglalkoztunk, ame
lyek adatok tárolását és rendezését szolgálták, a keresési módszerek közül azonban
csak kezdecleges megközelítéseket vizsgál tunk.
A korszeru számítógépes alkalmazásokban gyakran előfordul, hogy jelentős
mennyiségú adattal kell dolgoznunk, így a keresés múvelete kulcsfontosságú lehet.
Fontos lehet például egy kórházi beteg adatait gyorsan megtalálni több tízezer beteg
között, és a keresés hatékonyságán állhat vagy bukhat egy egész alkalmazás. A könyv
további fejezetei leginkább olyan algoritmusokat és adatstruktúrákat mutatnak be,
amelyek kifejezetten adatok hatékony tárolására, valamint adatok közötti hatékony
keresésre valók.
A bináris keresés a memóriában elhelyezkedő adatok közötti keresés egyik leg
hatékonyabb módszere. A bináris beszúrás a bináris keresés egy variációja, amelynek
segítségével hatékonyan tudunk keresni a tárolt adatok között.
A fejezetben a következő témaköröket vesszük szemügyre:
•
•
•
•
hogyan kell binárisan keresni,
bináris keresés megvalósítása iteratív és rekurzív módszerekkel,
bináris keresés összehasonlítása más keresési technikákkal,
bináris beszúrás összehasonlítása más rendezési technikákkal .
A bináris keresés müködése
A bináris keresést rendezett listában történő keresésre használjuk. A bináris keresés a
lineáris keresési technikákkal szemben kihasználja a rendezett listák néhány tulajdon
ságát: míg egy letámadásos lineáris keresés időbeni komplexitása O(N), addig a bináris
keresés komplexitása o (l o g N), amennyiben rendezett listában keresünk adatokat.
Amint a 2. fejezetben láthattuk, a rendezeden listában való keresés legegyszerűbb
módja, hogy a legelső elemtől elindulva végigmegyünk a listán egészen addig, amíg
vagy megtaláljuk az elemet, vagy a lista végére nem érünk. Ennek a módszernek az át
lagos költsége O(N). A pontos futási idő ádagosan N/2, mivel ádagosan a lista feléig el
kell jutnunk ahhoz, hogy megtaláljuk a keresett elemet. A rendezett listákban tárolt
adatok esetében azonban ennél sokkal jobb keresési hatékonyságot érhetünk el.
Bináris keresés és beszúrás
A bináris keresés neve onnan származik, hogy a keresés során minden egyes lé
pésben megfelezzük a kereséshez használt adatmennyiséget, ily módon folyamato
san leszűkítve a keresési teret egészen addig, amíg vagy célt nem érünk, vagy üresre
nem zsugorodik a keresési tér.
Tekintsünk például egy angolszótárt. Ha azt a feladatot kapnánk, hogy keressük
ki az algoritmus szót, hol kezdenénk a keresést? Valószínűleg a könyv elejétől elindul
va egyesével lapozgatnánk az oldalakat.
Ha azonban a /ama szót kellene kikeresnünk, valószínűleg a közepe táján nyitnánk
ki a szótárat. Miért is? Miért nem kezdenénk a szótár hátuljánál a keresést? Az ok ter
mészetesen az, hogy előre tudjuk, hogy a szótárban levő szavak ábécérendben helyez
kednek el, ezért viszonylag jó becslést tudunk végezni arra vonatkozóan, hogy hol he
lyezkednek el az l betűvel kezdődő szavak. Ha, a példánál maradva, a /ama szót keres
sük, és a szótárt felütve a mandarin szót lá�uk, akkor tudjuk, hogy már elhagytuk az l
betűs szavakat, és visszafelé kell lapoznunk. Ha viszont a kenguru szónál nyilik ki a szó
tár, tudha�uk, hogy még tovább, előbbre kell keresnünk. Tehát ha rossz helyen nyi�uk
ki a szótárt, könnyen kideríthe�ük, milyen irányban kell továbblapoznunk. A követke
ző megválaszolandó kérdés az, hogy mennyit lapozzunk előre vagy hátra.
Ennél a példánál maradva, a szótár nyelvének, illetve a különböző betűkkel kez
dődő szavak előfordulási gyakoriságának ismeretében elég jól meg tudjuk becsülni,
milyen messzire kell ellapozni. De mi történne, ha a fenti feltételezéssel ellentétben,
semmilyen előzetes ismeretünk nem lenne a kinyitott könyv tartalmáról? Mi lenne,
ha csak annyit tudnánk róla, hogy a szavak ábécérendben helyezkednek el benne?
A bináris keresés szerint ily�n helyzetben úgy járunk el, hogy minden lépésben
megfelezzük az adatok számát - innen ered a bináris elnevezés - és az aktuális kö
rülményeknek megfelelően vagy az adathalmaz egyik felében, vagy a másikban kere
sünk tovább. A bináris keresés lépéseit a következőképpen foglalha�uk össze.
226
1. Induljunk el a lista közepéről.
2. Hasonlítsuk össze a keresési kulcsot az aktuális pozícióban elhelyezkedő
elem kulcsával.
3. Ha a kettő megegyezik, célt értünk.
4. Ha a keresési kulcs kisebb, mint az aktuális elem kulcsa, akkor biztosan a
lista alsó felében helyezkedik el a keresett adat (ha egyáltalán szerepel a lis
tában), tehát osszuk fel a listát két részre, majd a lista alsó részét megtartva
ugorjunk vissza az 1. ponthoz.
5. Ellenkező esetben biztosan a lista felső felében lesz a keresett adat (ha egy
általán szerepel a listában), ezért a lista felső felét megtartva, ugorjunk vissza
az .1. ponthoz.
A bináris keresés működése
A következő példában bemutatjuk, hogyan keresnénk meg egy betűkből álló rende
zett listában aK-betűt (9.1. ábra). A lista kilenc betűt tartalmaz, ábécébe sorolva.
o 1 2 3 4 5 6 7 8
A l D l F l H Kl L G p
9. 1 . ábra. Betűket ábécérendben tároló lista
A keresést a lista közepén kezdjük, és ahogy a 9 .2. ábrán látható, a keresési kulcsot
összehasonlítjuk az I betűvel.
o 1 2 3 4 5 6 7 8
A l D l F l H Kl LG p
9.2. ábra. A keresés mindig a kö'zépső elemnél kezdődik
Mivel még nem találtunk egyezést, a listát két részre osztjuk. Ekkor, mivel a keresési
kulcs (K betű) nagyobb, mint az aktuális elem, a továbbiakban csak a lista felső felé
vel foglalkozunk (9.3. ábra).
o 1 2 3 4 5 6 7 8
9.3. ábra. A keresési kulcs bi'{/osan a lista felső felében he!Jezkedik el
A megmaradt lista 4 betűből áll: egy K, egy L, egy M és egy P betűből. Ez páros
számú elemet jelent. Nyilvánvalóan értelmetlen páros számú elemből a középsőt
megtalálni. Szerencsére azonban egyáltalán nem szükséges feltétel, hogy a lista szét
bontása során két teljesen azonos méretű listát kapjunk, ezért tetszés szerint választ
hatunk a két középső elem, az L és az M közül. Ebben a példában az L betűt válasz
tottuk (9.4. ábra).
o 1 2 3 4 5 6 7 8
9 .4. ábra. A "kö'zépső" elemmel fo!Jtaijuk a keresést
Most hasonlítsuk össze a keresési kulcsot a kiválasztott elemmel, az L betűvel. Most
sem egyezik a kettő, ezért ismét két részre kell bontanunk a keresési teret. Jelen
esetben azonban, a korábbival ellentétben, a keresési kulcs kisebb, mint a kiragadott
elem - a K betű megelőzi az L betűt -, ezért feltételezzük, hogy a keresett elem a lis
ta alsó felében helyezkedik el- ha egyáltalán szerepel a listában.
A 9.5. ábrán látható, hogy a keresési tér végül is egyetlen elemre szűkül ( aK be
tűre), amely esetünkben meg is egyezik a keresett kulccsal.
227
Bináris keresés és beszúrás
o 2 3 4 5 6 7 8
9.5. ábra. A keresési tér végül egyetlen elemre szűkül
Ezzel a keresés végére értünk, és mindössze három darab összehasonlítás árán sike
rült megtalálnunk a keresett elemet: egy-egy összehasonlítást végeztünk az I betűvel
és az L betűvel, illetve egyet az egyetlen elemként fennmaradóK betűvel. Letámadá
sos keresési módszert alkalmazva ugyanezt csak hat darab összehasonlítással érhet
tük volna el: először az A betűvel, majd a D, F, H, I, és végül a K betűvel való ösz
szehasonlítás útján.
Joggal merülhet fel az olvasóban az ellenvetés, hogy a bináris keresés azért tűn
het optimálisabbnak a letámadásos módszernél, mert a példában felhasznált keresési
kulcs a lista közepe táján, és nem a lista elején helyezkedik el. Például, ha az A betűt
kerestük volna, akkor letámadásos módszerrel egyetlen lépésben, rnig bináris keresés
útján csak négy lépésben találtuk volna meg.
Biztonsággal állitható tehát, hogy bizonyos speciális esetekben a letámadásos
módszereken alapuló keresés hatékonyabb, mint a bináris keresés. Az esetek több
ségében azonban a bináris keresés sokkal jobb teljesítményt ér el - ezt a fejezet ké
sőbbi részében konkrétan be is fogjuk bizonyítani.
A bináris keresés megközelitései
Most, hogy áttekintettük az algoritmus elvi működését, térjünk rá a kódolásral Ebben
a részben két bináris keresési megközelítést mutatunk be: az egyik rekurzív, a másik
pedig iteratív módszert használ. Teljesítmény szempontjából mindkettő ugyanazokkal
a tulajdonságokkal rendelkezik, de látni fogjuk, hogy az egyik kézenfekvőbbnek tűn
het, mint a másik.
Listabeli kereső
A most következő gyakorlófeladatban olyan interfészt készítünk, amely a bináris ke
resésnek mind a rekurzív, mint az iteratív megvalósításában közös. Ezáltal lehetősé
günk nyílik rá, hogy különböző megvalósításokat egységes módon teszteljünk, mű
ködés és teljesítménykiértékelés szempontjából is.
A listabeli kereső lehetőséget kínál listában való keresésre egy adott keresési kulcs
szerint (esetünkben rendezett listákról van szó), egyetlen metódus- a search() me
tódus - segítségéveL A metódus összehasonlítót használ annak eldöntésére, hogy a
keresési kulcs megegyezik-e a lista bármely elemével. Ha a kulcsot megtalálja a listá
ban, a search() metódus visszaadja a pozíciójának indexét (O, 1, 2 ... ). Ha a kulcs
228
A bináris keresés működése
nem szerepel a listában, a search() metódus negatív értéket ad vissza, amely abszo
lút értékében annak a pozíciónak felel meg, ahol az elem szerepelt volna, ha a kere
sés találattal végződik. Ezen a ponton felmerülhet a kérdés, hogyan adhatunk vissza
olyan értéket, amely pozícióadat, és egyben azt is jelzi, hogy a keresés sikertelen.
A válasz részben a negatív értékben rejlik. li y módon pozitív visszaadott érté
kekkel jelezzük a keresés sikerességét, és negatív értékekkel a sikertelen kereséseket.
Ha azonban a pozíciónak megfelelő negatív értékeket tekintjük (például az l-esből
-1, a 2-esből-2 lesz stb.), mit kezdjünk a lista legelső pozíciójával, a O-val? A -O ér
téknek nincs értelme.
Az alkalmazott trükk szerint a visszaadott értékeket úgy módosí�uk, hogy a 0.
hely esetében -1-et, az 1. hely esetében -2-t adunk vissza, és így tovább. Ezzel a
módszerrel egyszerre tudjuk jelezni a keresés sikertelenségét, és a megfelelő pozíció
adatot is visszaadjuk.
Gyakorlófeladat: listabeli kereső interfész létrehozása
Először is az alábbi módon hozzunk létre egy Java-interfészt:
�-,;-ac:k:a9e-·com:w-.:ox:·:-at9or�fthins-:bsea-rcl1;
import com.wrox.algorithms.lists.List;
public interface Listsearcher { public int search(List list, Object key);
----·� ·= �--��-"""-' ' � ;-· ---- -.:.-- ·-·
A megvalósitás müködése
Az interfészben a korábban tárgyalt search()műveletnek megfelelően egyetlen metó
dust definiálunk. Ez a metódus egy listát és egy keresési kulcsot fogad el bemenő pa
raméterként, és egy egész számot ad vissza, amely egy listabeli pozíciónak felel meg.
Vegyük észre, hogy összehasonlító objektumot nem adunk át paraméterként a
search() metódusnak, annak ellenére, hogy szükségünk lesz ilyenre. Ehelyett felté
telezzük, hogy akármilyen keresőt hozunk létre, az már tartalmazni fog egy összeha
sonlítót. A problémakötök ilyen módon történő szétválasztása lehetővé teszi listabe
li keresők paraméterként történő átadását anélkül, hogy a szóban forgó kód bármi
lyen információt tartalmazna arra vonatkozóan, hogJan végezzük el az elemek közötti
összehasonlításokat. A tényleges tesztelő kód írása közben egyértelműbbé fog válni
ennek a döntésnek az oka.
229
Bináris keresés és beszúrás
Gyakorlófeladat: tesztelő kód írása
Most, hogy az interfészt már létrehoztuk, áttérhetünk a tesztek megírására. A koráb
biakból már tudjuk, hogy legalább két keresőmegvalósításunk lesz - egy iteratív és
egy rekurzív -, és a fejezet vége előtt még egy harmadik keresőt is megvalósítunk.
Kezdetben olyan tesztcsomagot hozunk létre, amelynek minden listabeli keresőnek
meg kell felelnie. lly módon nem kell minden egyes megvalósítás után újraírnunk a
tesztelés kódját.
Kezdjük magával a tesztosztály megvalósításával:
package com.wrox.algorithms.bsearch;
import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.Comparator;
import com.wrox.algorithms.sorting.Naturalcomparator; import junit.framework.Testcase;
public abstract class AbstractListsearcherTestcase extends Testcase {
private static final object[] VALUES = {"B", "c", "o", "F", "H", "I", "J", "K .. , "L", "M", "P", "Q"};
private Listsearcher _searcher; private List _list;
protected abstract Listsearther createsearcher(Comparator comparator);
protected void setup() throws Exception { super. setUp();
}
_searcher = createsearcher(Naturalcomparator.INSTANCE); _list= new ArrayList(VALUES);
}----------------------------------------�---- --·--
A megvalósitás működése
Az Abstract:L i stsearcherTest:case teszteset definiál néhány tesztadatot (V ALU ES) ,
természetesen egy listabeli keresőt, és egy listát, amelyben keresni lehet. Tartalmaz
ezen kívül egy absztrakt metódust, a creat:esearcher() metódust, amelyet a teszt
osztály alosztályaiban külön meg kell valósítanunk ahhoz, hogy különböző listabeli
keresőmegvalósításokat teszteljünk.
Ezt követően a setup() metódusban meghívjuk a createsearcher() metódust,
amely létrehoz egy listabeli keresőt, végül pedig az értékeket tartalmazó tömb ből lét
rehozunk egy listát, amelyet a tesztek során felhasználhatunk.
230
A bináris keresés működése
Vegyük észre, hogy a createsearcher() metódus összehasonlítót fogad el pa
raméterként. Emlékezzünk vissza, hogy a L i stsearcher osztály search() metódusa
nem említ összehasonlítót, ezért egyedül a listabeli kereső létrehozásakor kell össze
hasonlítókkal foglalkoznunk.
A következő gyakorlófeladatban létrehozunk néhány tesztet.
Gyakorlófeladat: tesztek létrehozása
A következő teszt segítségével meggyőződhetünk róla, hogy amikor a listában sze
replő értékeket keresünk, helyes pozíciót kapunk vissza.
pub l i c voi d testSearchForExi sti ngv"al ues () for (int i =O; i < _list.size(); ++i) {
assertEquals(i, _searcher.search(_list, _list.get(i))); }
}
Ezt követően, készítsünk olyan tesztet, amely a listában nem szereplő elemre keres.
Ismét meg kell róla győződnünk, hogy a visszaadott érték megfelel annak a pozíció
nak, ahol a keresett elem helyet foglalna, amennyiben benne lenne a listában.
publicvoi'd testSearchi=orNonExi st i ngVa lueLessTtianFi rS'tftem() assertEquals(-1, _searcher.searchLlist, ''A"));
A következő teszt is nem létező értéket keres, de ebben az esetben az elem a lista
végén helyezkedne el (12. pozíció):
puli'fic ._voi(fiest'Sear'Ch.f:or!ÍioríE-xi sti ri"Qvalue"Greate.r'rhanüS"trtemcr · { assertEquals(-13, _searcher.search(_list, "Z"));
Végül szintén nem létező elemet keresünk, de most valahol a lista közepén helyez
kedne el:
··liíJblic···vöiCi.'té5.t:séarét1Fo"r-Arb";1:·raryNoi1'EXi5ti iígváJúe C) assertEquals(-4, _searcher.search(_list, "E"));
}
A megvalósitás müködése
A legelső teszt a lista minden elemén végigmegy (_l i st. get(i )), és elvégzi a kere
sését. Minden keresés eredményét ellenőrzi, hogy a visszaadott szám valóban meg
egyezik-e az aktuális pozícióvaL Itt akár iterátort is használhatnánk, de akkor külön
számon kellene tartanunk az aktuális pozíciót. Ezért inkább egy egész számot hasz
nálunk a pozíció jelölésére, és mindig a get() metódust hívjuk meg.
231
Bináris keresés és beszúrás
A másoclik tesztben A betűt keresünk, amely nyilvánvalóan nem szerepel a lis
tában. Ha azonban mégis szerepeille benne, akkor a lista legelején találnánk meg - a
O. pozícióban-, mivel a sorrendben az összes többi elemet megelőzi. Ezért a vissza
adott érték várhatóan -(0+1) = -1. Ne feledjük, hogy a listában nem szereplő ele
mek keresésekor visszaadott érték -(beszúrási pont + 1).
A harmaclik tesztben Z betűt keresünk. Ahogy korábban az A, a Z betű sem
szerepel a listában, de ezt a lista végén kellene megtalálnunk (12. pozíció). Ezért a
visszaadott érték várhatóan -(12+1) = -13.
Az utolsó tesztben E betűt keresünk, amelyet a 3. helyen találhatnánk meg, ha
szerepeille a listában. A keresőfüggvénynek ekkor -(3+1) = -4-et kellene visszaad
nia, ezzel jelezve, hogy a keresési kulcs nem szerepel a listában.
Rekurzív bináris kereső
Most, hogy a tesztfüggvényeken túljutottunk, térjünk rá a bináris keresőalgoritmus
megvalósítására. A bináris keresés folyamata lépésről lépésre egyre kisebb részekre
osztja a keresési teret. Erről az "oszd meg és uralkodj" típusú megközelítésről süt a
rekurzív megvalósítás lehetősége, és az első megvalósításunk valóban rekurzív mó
don működik.
Gyakorlófeladat: rekurzív bináris kereső megvalósítása és tesztelése
Hogy meggyőződhessünk rekurzív bináris keresőnk helyes működéséről, először
hozzunk létre egy tesztosztályt:
package com.wrox.algorithms.bsearch;
import com.wrox.algorithms.sorting.Comparator;
public class RecursiveBinaryListsearcherTest extends AbstractListsearcherTestcase {
}
protected Listsearcher createsearcher(comparator comparator) { return new RecursiveBinaryListsearcher(comparator);
}
Ezt követően hozzuk létre magát a listabeli keresőt:
232
package com.wrox.algorithms.bsearch;
import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.comparator;
public class RecursiveBinaryListsearcher implements Listsearcher { private final Comparator co�.P�•a�ra�t�o�r�;�------�-------------------�
A bináris keresés működése
pub li c Rei:ursi veBinaryLi stsearcher(Comparator comparator)-
{ assert comparator != null : "a 'comparator'nem lehet null";
_comparator = comparator;
}
private int searchRecursively(List list, object key, int lowerindex, int upperindex) {
assert list != null : "a 'list' nem lehet null";
if (lowerindex > upperindex) { return -(lowerindex + l);
}
int index = lowerindex + (upperindex - lowerindex) l 2;
int cmp = _comparator.compare(key, list.get(index));
if (cmp < 0) { index = searchRecursively(list, key, lowerindex, index - l);
} else if (cmp > 0) { index = searchRecursively(list, key, index + l, upperindex);
}
return index;
}
public int search(List list, Object key) { assert list != null : "a 'list' nem lehet null"; return searchRecursively(list, key, 0, list.size() - l);
l
A megvalósitás müködése
Mivel már korábban definiáltuk a teszteseteket az AbstractL i stsearcherTestcase
osztályban, nem marad más teendőnk, mint hogy Iciterjesszük ezt az osztályt, és meg
valósítsuk a createsearcher() metódust, amelynek a RecursiveBi n aryL i stSearcher
osztály egy példányát kell visszaadnia.
A Recursi veL i stsearcher osztály, túl azon, hogy megvalósí�a a L i stsearcher
interfészt, egy összehasonlító példányát is tartalmazza, amelyet a konstruktorfügg
vényben inicializál. Ez a megközelítés lehetővé teszi, hogy az alkalmazás kódja úgy
végezzen kereséseket, hogy semmit sem tud az összehasonlítási mechanizmusróL
A keresési feladat elvégzése a searchRecursively() metódusra hárul. A kere
séshez használt listán és a keresett kulcson kívül a searchRecursi vel y() metódus
még két paramétert fogad el: lowerindex-et és upperindex-et. Ez a két paraméter a
keresési tér határindexeit definiálja. Ha átnézzük az ábrákat a 9.1.től a 9.5.-ig, láthat
juk hogy valahányszor két részre osztjuk a listát, a lista más intervallumát kell tekin
tenünk. Az első lépésben (9 .l. ábra) a lista O-tól 8-as indexig elhelyezkedő elemeit
233
Bináris keresés és beszúrás
vizsgáltuk, mint a keresési kulccsal potenciálisan megegyező elemeket. A következő
lépésben a keresési teret leszűkítettük az 5-östől a 8-as pozícióig (9.3. ábra). A kere
sés végén egyetlen pozícióra csökkent az a tér, amelyben a keresett kulcsot megtalál
hattuk: az 5-ös pozícióra (9.5. ábra). Ezek a felső és alsó korlátok egy az egyben
megfelelnek az upperrndex, illetve a lowerrndex paraméter szerepkörének.
A leállási feltételt egy pillanatra félretéve megállapíthatjuk, hogy a keresési fo
lyamat első lépése a "középső" elem meghatározása. Ezt úgy kapha�uk meg, hogy a
felső és az alsó indexet kivonjuk egymásból, majd a különbséget megfelezzük:
int index = lowerindex + (upperindex - lowerrndex) l 2;
A 9.1. ábrából kiindulva láthatjuk, hogy a példában a "
középső" elem indexe a kép
letből adódóan: 0+(8-0)/2 = 0+4 = 4. Ahogy a 9.2. ábra mutatja, a példában pon
tosan így jártunk el. Első ránézésre talán kevésbé tűnik nyilvánvalónak, hogy rniért
kellett még külön hozzáadni a kapott értékhez az alsó indexet. Ennek a kérdésnek a
megválaszolásához tekintsük a 9.3. ábrát. Az alsó és a felső index értéke 5, illetve 8.
Ha ezeket a számokat behelyettesítjük a képletbe, az eredmény: 5+(8-5)/2 = 5+3/2
= 5+1 = 6 (pontosan, ahogy a 9.4. ábrán is látszik). Ha az alsó indexet nem adtuk
volna hozzá a képletben a tört értékéhez, akkor a kapott eredmény (8-5)/2 = 3/2
= l lett volna! Ez nyilvánvalóan hibás eredmény. Ha egyszerűen csak kivonjuk a fel
ső indexből az alsó indexet, a két index relatív távolságát kapjuk, magyarán, az alsó
indextől egy ofszet pozíció t.
A következő lépésben egy összehasonlító segítségével összeve�ük a jelenleg
vizsgált pozíció értékét a kulcs értékéveL Az összehasonlítás eredményét ezek után a
cmp változóban tároljuk:
int cmp = _comparator.compare(key, list.get(index));
Az összehasonlító nulla értéket ad vissza, ha a két argumentum megegyezik, negatív
értéket, ha az első argumentum értéke kisebb, mint a másodiké, és pozitív értéket,
ha az első argumentum értéke nagyobb, mint a másodiké. A bináris keresés esetében
ez minden információ, arnire szükségünk van ahhoz, hogy eidöntsük megtaláltuk-e a
keresett kulcsot, és ha nem, akkor a lista mely részében kell tovább keresnünk.
Amennyiben a keresett kulcs kisebb, mint az aktuális pozícióbeli elem, rekurzív
hívást végzünk, és a lista alsó felében folytatjuk a keresést: a lista alsó fele mindig az al
sóbb korlát indexétől éppen az aktuális index előtti értékű pozícióig tart (index - 1):
if (cmp < O) { index searchRecursively(list, key, lowerrndex, index - l);
}
234
A bináris keresés működése
Ha viszont a keresett kulcs nagyobb az aktuális elemnél, akkor rekurzív hívást végzünk, amely a lista felső felében folyta�a a keresést: a lista felső fele mindig éppen az aktuális elemet követő pozíciótól (index + l) egészen a felső index pozíciójáig tart:
} else if (cmp > O) { index = searchRecursively(list, key, index + l, upperindex);
}
Végül ha a keresett kulcs megegyezik az aktuális elemmel (az egyetlen fennmaradó lehetőség), akkor nincs szükség további keresésre, és a metódus az aktuális pozíció indexét adja vissza. Ezen a ponton már csak egyetlen kódrészlet van hátra: a leállási feltétel, amelyet korábban elhanyagoltunk.
Emlékezzünk vissza, tniképpen járunk el, amikor a keresett kulcs nem egyezik az aktuális pozíció elemével: az alsó indexet növeljük, a felső indexet pedig csökkentjük, így a kettő egy ponton összetalálkozik és helyet cserél - vagyis az alsó index nagyobbá válik, mint a felső index. Ez csak akkor történhet meg, ha az utolsó elemmel sincs egyezés.
Tekintsük ismét a 9.5. ábrát, azt a pontot, amikor egyetlen elemre szúkítettük a keresési teret, az 5. pozícióban helyet foglaló K betűre. Ekkor tehát mind az alsó, mind a felső indexérték 5. Az eredeti példában a keresés itt leállt, mert megtaláltuk a keresett elemet, de ha az 5. pozícióban J betű állna, akkor nem egyezne a két elem; és mivel a K betű a J után következik, ezért a fennmaradó lista felső felében kellene folytatnunk a keresést.
Ilyen esetekben tehát ellenőriznünk kell, hogy a lowerindex és az upperindex
értékei keresztezték-e egymást. Ha a válasz igen, akkor kifogytunk a lehetséges pozíciókból, és a metódusnak le kell állnia. Ilyen esetekben mindig az alsó index tartalmazza annak a pozíciónak az indexét, amelyben megtalálhattuk volna a keresett elemet, ha szerepeloe a listában:
if (lowerindex > upperindex) { return -(lowerindex +l);
}
Ezzel végeztünk is a search() metódus megvalósításával. Lényegében nem csinál mást, mint hogy a lista első és az utolsó elemének indexét átadja a searchRecursi
ve l y() metódusnak.
235
Bináris keresés és beszúrás
lterativ bináris kereső
A következő gyakorlófeladatban létrehozunk és tesztelünk egy iteratív bináris keresőt.
Az iteratív megvalósítás megértése a rekurzív verzió ismeretében nagyon egyszerű.
Gyakorlófeladat: jterativ bjnáds kereső megvalósítása és tesztelése
Ahogy a rekurzív verzió teszteléséhez, az iteratív megvalósítás teszteléséhez is szük
ség lesz egy különálló tesztosztályra. Jelen esetben nem teszünk sokkal többet, mint
hogy egyszerűen kiterjesztjük az absztrakt tesztosztályt:
package com.wrox.algorithms.bsearch;
import com.wrox.algorithms.sorting.comparator;
public class IterativeBinaryListsearcherTest extends AbstractListsearcherTestcase {
}
protected Listsearcher createsearcher(comparator comparator) { return new IterativeBinaryListSearcher(comparator);
}
Jelen esetben a createsearcher() metódusnak az IterativeBi naryL i stSearcher
osztály egy példányát kell visszaadnia, amelyet a következő módon hozunk létre:
package com.wrox.algorithms.osearch;
import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.Comparator;
public class IterativeBinaryListSearcher implements Listsearcher { private final comparator _comparator;
public IterativeBinaryListsearcher(comparator comparator) { assert comparator != null : "a 'comparator' nem lehet null";
_comparator = comparator;
}
public int search(List list, object key) { assert list != null : "a 'list' nem lehet null";
int lowerindex __ _..,"""""""'U=J>e r Index
O;
list.size() - l;
236
A bináris keresés működése
} }
wnile-(lowerrnaex <= upperrnaex) -{
}
int index = lowerrndex + (upperrndex - lowerrndex) l 2;
int cmp = _comparator.compare(key, list.get(index));
if (cmp == O) { return index;
} else if (cmp < O) { upperrndex = index - l;
} else { lowerrndex
} index + l;
return -(lowerrndex +l);
A megvalósitás müködése
Ugyanúgy, ahogy a rekurzív megvalósítás esetében is, az Iteratívesi naryL i stsearcher
osztály a L i stsearcher interfészt valósí* meg, és összehasonlítót is tartalmaz. A konst
ruktorfüggvényen kívül az osztály egyetlen metódusa maga a search() metódus, amely
egy az egyben megfelel a Recu r si ve B i naryL i stsearche.r osztályban foglaltaknak
A rekurzív megvalósítás mélyebb vizsgálatakor kiderül, hogy a rekurzív hívások
kor nincs szó egyébről, mint a felső és az alsó indexek módosításáról. Innen köny
nyen eljuthatunk annak felismeréséig, hogy a rekurziótól megszabadulhatunk oly
módon, hogy egy whi l e-ciklusban folyamatosan az aktuális helyzetnek megfelelően
módosí�uk az alsó, illetve a felső indexeket.
A keresés iteratív megvalósítása tehát az alsó és felső indexnek a lista első, illetve
utolsó pozíciójára való inicializálásával kezdődik:
int lowerrndex = O; int upperrndex = list . size() - l;
Ez teljesen megfelel a rekurzív megvalósítás azon lépésének, amikor a search() metó
dus a lista első és utolsó elemének indexét átadta a searchRecu r si ve l y() metódusnak.
Ezt követően a fentieknek megfelelően belépünk egy whi l e-ciklus ba:
while (lowerrndex <= upperrndex) {
}
return -(lowerrndex +l);
237
Bináris keresés és beszúrás
A rekurzív esethez hasonlóan itt is feltételezzük hogy az alsó és a felső határindex egy ponton helyet cserél, amennyiben a keresett elem nem szerepel a listában. A vezérlés tehát mindaddig a ciklusan belül lesz, amíg ez be nem következik (lower
Index <= upperindex). Mihelyt ez a feltétel már nem teljesül, a ciklus véget ér, és a metódus visszaadja annak a pozíciónak az indexét, ahol az elem szerepelt volna, ha benne lett voln� a listában (-(lowerindex + 1)). Ellenkező esetben, amíg nem szűkül le kellően a keresési tér, ki kell számolnunk a "középső" pozíciót, és el kell végeznünk az összehasonlítást:
int index = lowerindex + (upperindex - lowerindex) j 2; int cmp = _comparator.compare(key, list.get(index));
Ha az összehasonlítás során egyezést tapasztal, a metódus azonnal visszatér az aktuális index értékével:
if (cmp == O) { return index;
}
Ha ellenben a keresett kulcs kisebb, mint az aktuális elem, a lista alsó felében folytatjuk a keresést, miután csökkentettük a felső határindexet:
} else if (cmp < O) { upperindex = index - l;
}
Végezetül, ha a keresett kulcs nagyobb, mint az aktuális elem, a lista felső felében folytatjuk a keresést, miután növeltük az alsó határindexet
} else {
}
lowerindex index + l;
A listabeli kereső teljesítményének vizsgálata
Ebben a szakaszban listakereső algoritmusainkat több különböző esetre lefuttatjuk annak érdekében, hogy statisztikákat gyűjthessünk teljesítményükről, és megállapíthassuk, hogy a bemutatott bináris keresési algoritmusok közül melyik teljesít sokkal jobban, mint a letámadásos jellegű, lineáris keresés. A 6. és a 7. fejezet tesztelésról szóló szakaszához hasonlóan itt is ca ll Counti ngcomparator objektumot használunk annak megszámolására, hogy az egyes keresési esetekben hány darab összehasonlítás megy végbe.
238
A bináris keresés működése
Lineáris keresés az összehasentitás végett
Mielőtt megvizsgáljuk a bináris keresők teljesítményét, összehasonlítási alapot kell
teremtenünk a lineáris kereséshez képest. Egy lehetséges megoldás, hogy a lista in
terfészből egy az egyben felhasználjuk az i ndexof() metódust, mint ahogy a letá
madásos, lineáris keresés megvalósításakor tettüle Sajnos azonban az i ndexof() me
tódus nem használ összehasonlítót, és semmilyen hatékony módszert nem nyújt az
összehasonlítások számának nyilvántartására. Ezért, a következő gyakorlófeladatban
olyan listabeli keresőt valósítunk meg, amely rendezett listában lineáris keresést vé
gez, és közben összehasonlítót használ, ezzel lehetővé téve statisztikák gyűjtését egy
mélyrehatóbb vizsgálathoz.
Gyakorlófeladat: Uneáris kereső megvalósítása és tesztelése
Annak ellenére, hogy a lineáris keresőt kizárólag a bináris kereső algoritmusokkal va
ló összehasonlítás végett valósítjuk meg, aligha bízhatnánk meg az összehasonlítás
eredményében, ha hiba volna a kódban. Ezért, a korábbiakban fejlesztett progra
mokhoz hasonlóan, most is tesztdési környezet létrehozásával indítunk:
package com.wrox.algorithms.bsearch;
import com.wrox.algorithms.sorting.comparator;
public class LinearListsearcherTest extends AbstractListsearcherTestcase {
protected Listsearcher createsearcher(comparator comparator) { return new LinearListsearcher(comparator);
}
Ezt követően hozzuk létre magát a kereső megvalósírási osztályt:
package coni:wrox.a i'gorithms. bseárch7
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.Comparator;
public class LinearListsearcher implements Listsearcher { private final comparator _comparator;
public LinearListsearcher(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet null";
_comparator = comparator; l
239
Bináris keresés és beszúrás
public int search(List list, object key) {
}
assert list ! = null : "a 'list' nem lehet null";
int index = O; Iterator i = list.iterator();
for (i.first(); !i.isoone(); i.next()) {
}
int cmp = _comparator.compare(key, i.current()); if (cmp = O) {
return index;
} else if (cmp < O) { break;
}
++index;
return -(index + l);
A megvalósitás működése
Szerencsére mivel a lineáris kereső működése kívülről szemlélve teljesen azonos az
összes többi listakereső megvalósítással, ismét kihasználhatjuk az absztrakt tesztosz
tály korábbi megvalósítását. Annyi a feladatunk, hogy elérjük, a createsearcher()
metódus a L i nea r L i stsearcher osztály egy példányát adja vissza.
A L i nea r L i stsearcher osztály a L i stsearcher interfészt valósí�a meg, és a vára
kozásokkal összhangban összehasonlítót is tartalmaz a későbbi feladatok elvégzéséhez.
A search() metódusban lényegében lemásoltuk a 2. fejezetben megtalálható,
i ndexof() metódus fejlesztésekor létrehozott kódot, néhány apró kivételtől eltekintve.
Az egyik változtatás az, hogy az i ndexof() metódus kódjával ellentétben, az equals O metódus meghívása helyett a fenti kódban összehasonlítót használunk. A komparátor
meghívása után pedig, miután az eredményt a cmp változóban eltároltuk, ha a két érték
megegyezik, megtaláltuk a keresett értéket, és azonnal visszatérhet a metódus:
int cmp = _comparator.compare(key, i .current()); if (cmp == 0) {
ret:urn index;
}
A második eltérés a fenti kód és a 2. fejezetben található kód között az optimalizá
lás. Amikor a keresett kulcs nem szerepel a listában, az eredeti megvalósításban el
megyünk a lista végéig. J elen esetben azonban kihasználhatjuk azt a tényt, hogy a lis
ta eleve rendezett (ez indokoltnak tűnik, mivel már a bináris keresési algoritmusok
megvalósításai is erre a feltételre építenek).
240
A bináris keresés működése
Ezért csak addig folyta�uk a keresést, amig úgy hisszük, van remény arra, hogy a
keresett elem szerepeljen a listában. Mihelyt olyan elemet találunk a listában, amely
nagyobb, mint a keresett elem, biztonsággal lezárhatjuk a ciklust:
} else if (cmp < 0) { break;
}
A fenti két változtatásan kívül, a search() metódus megegyezik az eredeti, i ndexof()
metódus megvalósításávaL
Teljesítmény tesztelése
A szó szaros értelmében nem fogunk valóeli teszteket készíteni, de mivel a ]Unit-ke
retrendszer kiváló környezetet biztosít teljesítményelemzésre, tesztmetódusok for
májában hozzuk létre a teljesítmény mérésére irányuló programjainkat. A készített
metódusok ugyanazokat a kereséseket fogják elvégezni a három különböző listabeli
keresőnkkeL
Ahogy korábban is tettük, most is a futás során történő összehasonlítások szá
mát mérjük, nem pedig a futás idejét. Ennek eléréséhez újra felhasználhatjuk a 6. fe
jezetben bemutatott ca ll counti ngcomparator osztályt.
Gyakorlófeladat: tesztosztály létrehozása
Kezdjük a B i narysearchéa ll co untingTest nevű osztály létrehozásával, amely,
ahogy a neve is jelzi, az összehasonlítások számát hivatott mérni:
package com.wrox.algorithms.bsearch;
import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.callcountingeomparator; import com.wrox.algorithms.sorting.Naturalcomparator; i.port junit.framework.Testcase;
public class BinarysearchcallcountingTest extends Testcase { private static final int TEST_SIZE = 1021;
private List _sortedList; private.callcountingcomparator _comparator;
protected void setup() throws Exception { super.setUp();
=.5..2E!�9Li st = new Arr:��L i g_(liSLSIZE). ·
241
Bináris keresés és beszúrás
J.
for (int i = 0; i < TEST_SIZE; ++i) { _sortedList.add(new Integer(i));
}
_comparator new callcountingcomparator(Naturalcomparator.INSTANCE);
}
private void reportcalls() { System.out.println(getName() + ": "
+ _comparator.getcallcount() + " calls");
}
A megvalósitás müködése
A létrehozott tesztosztály definiál egy konstans t, a TEST _SIZE-t, amelyet rövidesen arra fogunk használni hogy feltöltsük és keresésre felhasználjuk a példányváltozót, a _sortedl i st-et, valamint definiál egy másik változót, a _comparator-t, amelynek feladata az összehasonlító hívások számolásai a statisztikák könnyebb elkészítése végett.
A setUp() metódusban létrehoztunk egy tömblista típusú változót, majd növekvő sorrendben feltöltöttük O-tól TEST _SIZE-ig terjedő egész számokkal. Ezt követően, a jelentések készítését megkönnyítendő, l�trehoztunk egy hívásszámláló összehasonlítóba csomagolt természetes összehasonlítót. Biztonsággal használhatunk természetes összehasonlítót, mivel az Integer osztály megvalósítja a comparator interfészt.
A reportcalls() metódust az egyedi tesztek arra fogják használni, hogy kiírják az összehasonlítóhoz irányuló hívások számát, a következő formában:
test-name: #### calls
Most, hogy van egy rendezett elemekkel feltöltött listánk, egy összehasonlítónk statisztikák összegyűjtésére, és képesek vagyunk a statisztikák felhasználásával jelentések készítésére, a következő gyakorlófeladatokban megvalósíthatunk néhány tesztet és megvizsgálhatjuk az egyes listakeresők teljesítményét.
Gyakorlófeladat: tesztesetek megvalósítása
A tesztek egy része minden, O és TEST _SIZE között elhelyezkedő értékre növekvő sorrendben keresést végez, és kinyomtatja az összehasonlítások számát:
242
public void testRecursiveBinarySearch() { performrnordersearch(new
RecursiveBinaryListsearcher(_comparator));
}
A bináris keresés működése
pub11c void- te'Stiterat"fvesi narysearch -0�{ performrnordersearch(new
IterativeBinaryListsearcher(_comparator)); }
public void testLinearsearch() { performrnordersearch(new LinearListsearcher(_comparator));
}
private void performrnordersearch(Listsearcher searcher) { for (int i = 0; i < TEST_SIZE; ++i) {
searcher.search(_sortedList, new Integer(i)); }
reportcall sO; }
A következő teszttípus véletlen kereséseket végez:
public void testRandomRecursiveBinarysearcn()-{ performRandomsearch(new
RecursiveBinaryListsearcher(_comparator)); }
public void testRandomiterativeBinarysearch() { performRandomsearch(new
IterativeBinaryListsearcher(_comparator)); }
public void testRandomLinearSearch() { performRandomsearch(new LinearListSearcher(_comparator));
}
private void performRandomsearch(Listsearcher searcher) { for (int i = 0; i < TEST_SIZE; ++Í) {
searcher.search(_sortedList, new Integer((int) (TEST_SIZE * Math.random())));
}
reportcalls(); l
A megvalósitás működése
Az in-order tesztelők a három különböző listakereső közül egyet létrehoznak, amelyet
aztán átadnak a performordersearch() metódusnak, hogy elvégezze az in-order (O, l,
2 ... ) keresést, és a végén kiírja a keresés során elvégzett összehasonlítások számát.
A random tesztek szintén létrehoznak egy listakeresőt, számláló összehasonlító
val együtt, de a performRandomsearch() metódusnak adják át, amely véletlen kere
sési kulcsokat generál.
243
Bináris keresés és beszúrás
A teszteket lefuttatva a felhasznált fejlesztőeszköztől függőerr többé-kevésbé az alábbiakat láthatjuk:
testRecursi veBi naryséarch: 919 7 ca ll s testiterativeBinarysearch: 9197 calls testLinearsearch: 521731 calls testRandomRecursiveBinarysearch: 9197 calls testRandomiterativeBinarysearch: 9132 calls testRandomLinearsearch: 531816 calls
M-0 - '
Az eredményeket a 9 .l. táblázatban foglaljuk össze, hogy könnyebben össze lehessen hasonlítani a különböző keresési módszereket.
Rekurziv bináris lterativ bináris Lineáris
Összehasonlítások száma 9 197 9 197 521 731
(in-Order)
Összehasonlítások száma* 9 158 9 132 531 816
(random)
Összehasonlítások száma* 9 9 515
(átlagos)
* A teszt véletlenszerű jellege rniatt a kapott eredmények valamelyest eltérhetnek a fent szereplóktól.
9.1. táblázat. Rendezett lisfákra alkalmazott keresési módszerek te!Jesítmét!J-ö"sszehasonlítása
1021 db keresés alapján
A sorrendben történő keresés esetében a rekurzív és az iteratív megvalósítások egyformán jól teljesítenek (A két megvalósításban tapasztalt különbség kizárólag a teszt véletlenszerűségének tulajdonítható.) Körülbelül sejthető volt a kapott eredmény, de jobb, ha az ilyesmiről saját magunk győződünk meg. Szintén érdemes megjegyezni, hogy a rekurzív megvalósítás némiképpen hátrányos az iteratív megvalósításhoz képest abból a szempontból, hogy a rekurzív hívások mindig segédszámítási költséggel járnak. Ez a különbség azonban elhanyagolható.
Amit fontos megjegyezni, és ami különösen fontos számunkra, az a bináris és a lineáris keresés közötti teljesítménybeli különbség. A bináris keresés esetében az átlagosan elvégzett összehasonlítások száma körülbelül 9 OOO l l OOO = 9, míg lineáris keresés esetében ugyanez a szám hozzávetőleg 500 OOO l l OOO = 500. Ezek az értékek ténylegesen bizonyítják nemcsak a bináris, hanem a lineáris keresés teljesítőképességével kapcsolatos előzetes feltételezéseink igazságát. Korábban úgy számoltunk, hogy a bináris keresés átlagosan o(log N) komplexitású, a lineáris keresés pedig O(N) komplexitású.
244
Bináris beszúrás működése
Ezzel együtt fontos megjegyezni, hogy a bináris keresés teljesítménye mindadclig
kiváló, amíg olyan adatstruktúrákon használjuk, amelyek támoga�ák a gyors, index
alapú adatelérést (ilyen például a tömblista is). Ha ellenben másfajta adatstruktúrát
használunk - például láncolt listát - akkor annak ellenére, hogy az összehasonlítások
száma ugyanannyi, mint tömblista esetében, a listában való folyamatos ugrálás miatt
a minclig éppen következő elem megkeresése jelentős időbeni veszteséget okoz.
Bináris beszúrás müködése
A bináris beszúrás a bináris keresésre alapozott technika, amelynek segítségével
hosszú távon rendezetten tarthatjuk adatainkat. Nyilvánvalóan a könyvben már ko
rábban tárgyalt rendezési algoritmusokat is használhatunk listák rendezett állapotban
tartására, de ahogy azt később látni fogjuk, minden beszúrás után újra és újra lefut
tatui egy rendezési algoritmust nagyori költséges megoldás lehet. Még az is viszony
lag költségesebb megoldás, ha csak az összes elem beszúrása után végzünk rende
zést, mint ha eleve rendezetten szúrnánk be az adatokat.
A bináris beszúrás gyakorlatilag ugyanúgy műköclik, mint a bináris keresés. Az
egyetlen különbség a kettő között valójában az, hogy míg a bináris keresés a keresett
elem listában elfoglalt pozíciójával tér vissza, a bináris beszúrás, ahogy az neve is jel
zi, egy új elem megfelelő helyre történő beszúrásával végződik.
Képzeljük el, hogy a korábbi példában bemutatott, betűkből álló listába (lásd
9.1. ábra) be szetetnénk szúrni egy G betűt. Ahogy a bináris keresés esetében, most
is a középső elemmel indítunk, az I betűvel. Ezt összehasonlítva a beszúrandó
elemmel, a G betűvel megállapíthatjuk, hogy a beszúrás pontjának a lista alsó felé
ben kell lennie (az I betű később következik, mint a G betű).
A 9.6. ábrán látható, hogy a következő elem, amivel a G betűt összehasonlítjuk,
a D. Ez esetben a beszúrandó érték nagyobb, mint az aktuális pozíció tartalma, ezért
a továbbiakban csak a megmaradt lista felső felével foglalkozunk.
o 1 2 3 4 5 6 7 8
9.6. ábra. A keresést a lista alsó felében fo!Jta!Juk
Ezek után már csak két elem maradt: az F és a H betű. A 9.7. ábrán látha�uk, hogy a
beszúrandó elem nagyobb, mint az aktuális (F betű), ezért a következőkben a H be
tűt vesszük alapul, a 9.8. ábrán látható módon.
A beszúrandó G betű nagyobb, mint az aktuális elem, a H betű, de mivel ezúttal
nem maradt több elem, ideje elvégeznünk a beszúrást.
245
Bináris keresés és beszúrás
o 2 3 6 7 8
9. 7. ábra. A keresést a megmaradt lista felső felében jo!Jtatjuk
o 2 3 4 5 6 7 8
i lK L lM l P
9.8. ábra. A megmaradt lista e!!Jietlen elemre szűkül
A beszúrandó érték a legutóbbi elem elé való, ezért az összes tőle jobbra elhelyezke
dő elemet, beleértve a H betűt is, egy pozícióval jobbra toljuk, ezzel helyet csinálva a
G betűnek, ahogy a 9.9. ábrán látható.
o 2 3 4 5 6 7 8 9
9. 9. ábra. A kulcsot a somndezettséget .fi!!Jielembe véve illesztjük be a listába
Ezt követően a G betűt beszúrhatjuk a helyes pozícióba, ily módon továbbra is ren
dezett marad a lista. Most, hogy áttekintettük a bináris beszúrás működését, rátérhe
tünk a bináris beszúrást elvégző kód megírására.
Listabeszű ró
Ebben a szakaszban egy nagyon egyszerű osztályt készítünk, amely képes elemek lis
tába való beszúrására oly módon, hogy a lista rendezettsége megmarad. A beszúrási
pont megkeresésére a már korábban megvalósított listabeli keresőt fogjuk használni.
Gyakorlófeladat: tesztek készítése
Maguk a tesztek bináris beszúrási algoritmust fognak használni számok listába történő
beszúrására. A számokat rendezett, valamint véletlenszerű sorrendben is beszúrjuk a
listába. Akármilyen sorrendben is érkeznek a számok, helyes működés esetén helyes
sorrendben kell szerepelniük a listában. Kezdjük a tesztosztály létrehozásával:
246
package com.wrox.algorithms.bsearch;
import cem.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.Naturalcomparator; im�ort junit.framework.Testcase;
Bináris beszúrás müködése
pu6lic class'Li stinserterTest extends Testcase { private static final int TEST_SIZE = 1023;
private Listinserter _inserter; private List _list;
protected void setUp() throws Exception { super. setup();
_inserter = new Listinsertere new IterativeBinaryListsearcher(Naturalcomparator.INSTANCE));
_list= new ArrayList(TEST_SIZE); }
private void verify() {
}
int previousvalue = Integer.MIN_VALUE; Iterator i = _list.iterator();
for (i.first(); ·li.isoone(); i.next()) {
}
int currentvalue =((Integer) i.current()).intvalue(); assertTrue(currentvalue >= previousvalue); previousvalue = currentvalue;
1---�-----------------------'
Az első tesztesetben növekvő sorrendben szúrjuk be a számokat a listába. Magyarán
kezdjük a O-tól, majd folytassuk az l-essel, 2-essel stb. egészen addig, amíg az előre
meghatározott maximális számig, a TEST_SIZE-ig el nem jutottunk:
public void testAscendinginorderrnsertion()-{ for (int i = 0; i < TEST_SIZE; ++i) {
assertEquals(i, _inserter.insert(_list, new Integer(i))); }
verify(); }
A következő teszt valójában variáció az elsőre. Ahelyett, hogy növekvő sorrendben
szúrnánk be az elemeket, ezúttal csökkenő sorrendben illesztjük be őket. Tehát a
meghatározott maximális számtól indulunk el, egészen addig, amíg a O-t el nem érjük:
public void testoescendingrnorderrnsertion()-{ for (int i = TEST_SIZE - l; i >= 0; --i) {
assertEquals(O, _inserter.insert(_list, new Integer(i))); }
verify(); . }
247
Bináris keresés és beszúrás
Az utolsó teszt véletlen értékeket szúr be a listába. Ezáltal megbizonyosodhatunk arról, hogy a beszúró nem a körülrnények szerenesés összejátszásának köszönhetően működött helyesen, amikor a beszúrandó elemek sorrendben érkeztek.
public void testRandomrnsertion() {
}
for (int i = 0; i < TEST_SIZE; ++Í) { _inserter.insert(_list,
new Integer((int) (TEST_SIZE * Math.random()))); }
verify();
A megvalósitás müködése
A tesztosztály tartalmazza a L i strnserter osztály egy példányát, valamint egy listát, amelybe az egyes tesztek elemeket szúrhatnak be. Mindkét adatstruktúrát a setUp()
metódus ban inicializál juk. Minden teszt meglúvja a verify() metódust annak biztosítására, hogy az eredő
lista elemei helyes sorrendben vannak. Ezt a listán keresztül történő iterálás útján éri el. Az iterálás során minden soron következő értéket, amelyet a (currentvalue) változóban tárolunk, összehasonlítjuk az előző értékkel, (P rev i ousva l ue)-val. Így megbizonyosodunk arról, hogy az elemek nem csökkenő sorrendben szerepeillek a listában. Figyeljük meg, hogy a kódban az előző értéket Integer. MIN_VALUE-ként inicializáljuk. liy módon garantálható, hogy még akkor is igaz legyen a fenti állítás, ha még csak a lista első elem ét vizsgáljuk, így nincs "előző érték".
Az első tesztben egy egyszerű for-cikluson belül szúrjuk be a listába a növekvő sorrendben érkező számokat, a beszúró egy példányának segítségéveL V alahányszor beszúrunk egy elemet, megbizonyosodunk róla, hogy a visszaadott érték hűen tükrözi a beszúrási pontot. Jelen esetben, arnikor növekvő sorrendben érkeznek a beszúrandó számok, a beszúrási pont mindig az aktuálisan beszúrt értékkel egyezik meg: a O a O. helyre kerül, az l az 1. helyre stb. Végül miután minden elemet beszúrtunk, meglúvjuk a ve ri fy () metódust, hogy megbizonyosodjunk róla, minden elem valóban jó helyre kerül - pusztán az a tény, hogy az i n se rt() helyes értékekkel tért vissza, még nem jelenti azt, hogy tényleg jó helyre kerültek az értékek!
A második teszt is for-ciklust alkalmaz az elemek csökkenő sorrendben való beszúrására. Ezúttal, mivel csökkenő sorrendben szúrjuk be a számokat, ellenőrizzük, hogy az i n se rt() metódus minden lúvás után O-t ad vissza (minden egyes alkalommal eggyel jobbra csúsztatjuk a már létező értékeket). Végezetül meglúvjuk a verify() metódust, hogy megbizonyosodjunk róla, a beszúrás tényleg a várt módon működik - megint csak bízva abban, hogy a korábban kapott visszatérési értékek a tényleges beszúrási pontokat jelezték.
248
Bináris beszúrás működése
Az utolsó teszt szintén TEST _SIZE darab egész számot szúr be a listába, de a be
szúrandó értékeket a Ma th. random() metódus segítségével határozza meg. Emlékez
zünk vissza, hogy a Math. random() kétszeres pontosságú lebegőpontos számot ad
vissza a O. 0-tól l. O-ig tartó intervallumban- ezért az eredményt TEST _SIZE-zal meg
szorozzuk, így biztosítva, hogy a beszúrt számok O és TEST _SIZE közötti egész szá
mok. Vegyük észre, hogy ezúttal semmilyen megállapítást nem tehetünk az insert()
metódus által visszaadott értékeket illetően. Hogyan is tehetnénk? Az értékeket véletlen
sorrendben szúrjuk be a listába.
Ezzel végeztünk a tesztek megvalósításávaL A következő gyakorlófeladatban
magát a beszúrót valósítjuk meg.
Gyakorlófeladat: beszúró megvalósítása
A bináris beszúrás t végző kód felépítése viszonylag egyszerű. Létre kell hoznunk egy
L i strnserter osztályt, az itt látható módon:
pac�age com.wrox.algorithms:bsearcn;
import com.wrox.algorithms.lists.List;
public class Listinserter {
l
private final Listsearcher _searcher;
public Listinserter(Listsearcher searcher) { assert searcher l= null : "a 'searcher' nem lehet null"; _searcher = searcher;
}
public int insert(List list, object value) { assert list != null : "A 'list' nem lehet null";
}
int index = _searcher.search(list, value);
if (index < O) { index = -(index + l);
}
list.insert(index, value);
return index;
249
Bináris keresés és beszúrás
A megvalósitás müködése
Ahogy láthatjuk, a L i strnserter osztály konstruktora egyetlen argumentumként
egy L i stsearcher objektumot fogad el. Ezt a beszúrási pont megkereséséhez hasz
náljuk- rnivel a bináris beszúrás legnagyobb része a bináris keresésre épül, rniért ta
láljuk fel újra a kereket?
A listabeli kereső az insert() metódus segítségével megkeresi a beszúrási pon
tot. Ha a keresés sikeres, már létezik a listában ilyen értékű elem. Ne akadjunk fenn
ezen az apróságon, mert rnivel engedélyezett megkettőzött értékek tárolása, a kapott
helyre egyszerűen beszúrjuk az új értéket, a már létezőt pedig jobbra toljuk a listá
ban. Ellenben ha az érték még nem szerepel a listában (i n d ex < O), akkor a koráb
biak alapján tudhatjuk, hogy a visszakapott értéket érvényes pozícióindexre konver
tálhatjuk a -(index + l) képletteL Miután tudjuk, hová kell kerülnie az új értéknek,
beszúrjuk a listába, és visszaadjuk a hívónak a beszúrási pont indexét.
Teljesítmény vizsgálata
Van egy osztályunk, amely hatékony bináris keresési mechanizmust használ elemek
listába való beszúrására, miközben sorrendben tartja a lista elemeit. A kérdés most
az, hogy rnilyen a megírt kód teljesítménye. Konkrétabban: hogyan viszonyul a kü
lönböző rendezési algoritmusokhoz, amelyeket a 6. és a 7. fejezetben tárgyaltunk?
Bizonyára egyszerűbb lenne egy közönséges listába feltölteni az adatokat, majd az
egészet rendezni, talán nem?
A következő gyakorlófeladatban olyan tesztcsomagot készítünk, amelynek a se
gítségéve! összevethetjük a bináris beszúrás kódját más rendezési algoritmusokkal.
Gyakorlófeladat: bináris beszúró más rendezési algoritmusokkal való összehasonlítása
Ugyanúgy, ahogy a listabeli keresők teljesítményének vizsgálatakor, most is teszt
csomagot készítünk, amely megtornázta�a a bináris beszúrót, és összehasonlítja más
rendezési algoritmusokkal.
250
package com. w r ox. a l.gori thms·. bsearch;
import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.Callcountingcomparator; import com.wrox.algorithms.sorting.ListSorter; import com.wrox.algorithms.sorting.MergesortListsorter; import com.wrox.algorithms.sorting.Naturalcomparator; import com.wrox.algorithms.sorting.QuicksortListsorter; import com.wrox.algorithms.sorting.shellsortListSorter; import ·unit.framework.Testcase;
Bináris beszúrás működése
-pűblié-·c1a5s""" 8friár'Y"Insertca ll CountingTest- extends.Testease-I
private static final int TEST_5IZE • 4091;
private List _list; private callcountingCoaparator _co.parator;
protected void setUp() throws Exception { super. setUp() ;
_list= new ArrayList(TEST_SIZE); _comparator =
new callcountingcoaparator(Naturalcomparator.INSTANCE); }
}
A legelső teszt az imént létrehozott bináris beszúrót működteti úgy, hogy értékeket
szúr be a listába, és közben számon tartja a feladat elvégzéséhez szükséges összeha-
sonlítások számát. ·
�piAbffcvÖ.f"cf testB i naryÍnsert()-c-······-r·�-�-· ••• ••·---
Listinserter inserter = new Listiflserter( new Ite>ativeBinaryList5earcher(_ca.pa�ator));
for (int i = 0; i < TEST_SIZE; ++i) { inserter.insert(_list,
new Integer((int) (TEST_SIZE * Math.random()))); }
reportealls(); }
private void report<alls() { sys"tetR.out.println(ge"tHame() + ": "
_ _ _} _ _ _ ��- �
+ _li:OIIIpara-tor .,uetcallcount() + " calls");
Most, hogy van egy programunk a bináris beszúrás tesztelésére, hasonló teszteket
készítünk más rendezési módszerekre is. Ugyanazokat a rendezési algoritmusokat
használjuk, amelyeket a bináris keresés kapcsán a fejezet korábbi részében feleleve
nítettünk.
�� 1 i.c.voi"d .. �test:Mergeso·r:tcY·c-.-·� popul a-teAndsort-!. i s-t:(new Mergesortt. i s�Sorter(_c:0111para-tor)). ·
}
public void testshellsort() { populateAndSOrtlist(new shellsortlistsorter(_comparator));
}
251
Bináris keresés és beszúrás
pubiTé-void testQuickso-rtO { populateAndSOrtList(new QUicksortListserter(_co.parator));
}
private void populateAndsortList(Listsorter sorter) { for (int i % 0; i < TEST_SIZE; ++i) {
_list.add(new Integer((int) (TEST_SIZl * Math.ran�()))); }
_list= sorter.sort(_list);
reportcalls(); ... �l
A megvalósitás müködése
A tesztosztály listát tartalmaz, amelybe beszúrhatjuk az elemeket, valamint a sorren
dezést megkönnyítendő egy összehasonlítót is. Ahogy a korábbi teljesítménytesztek
nél, most is tömblistát használunk, illetve egy számláló összehasonlítót a különböző
megközelítések összevetéséhez szükséges statisztikák elkészítésére.
A testBi naryrnsert() először is létrehoz egy iteratív bináris listabeli keresőt.
(A rekurzív változatot is használhatnánk, de akkor a beágyazott hívások miatt segéd
számítási költséggel kellene számolnunk.) Majd TEST_SIZE darab véletlen egészt szú
runk be a listába. A r epo r tea ll sO metódus ezek után következő formátumban ki
írja az összehasonlítások számát a:
test-name: #### calls
Az utolsó három teszt mindegyike létrehoz egy-egy rendezési algoritmust megteste
sítő objektumot, és továbbadja a popul ateAndsortL i st() metódusnak, amely vég
rehajtja a tesztet
A populateAndsortList()metódusban TEST_SIZE darab véletlen egészt adunk
hozzá a listához (a korábbi technikát alkalmazva), rendezzük a listát az aktuális ren
dezési algoritmussal, majd kiíratjuk az összehasonlítások számát. Megint csak fel kell
szoroznunk a Ma th. random() metódus által visszaadott értéket, hogy a számok a O
és a TEST _SIZE közötti intervallum ba essenek.
A teszteket lefuttatva a következőket láthatjuk:
"testBinaryinsert: Ü471 ca Ú S
testMergeSOrt: 43928 calls testshellsort: 102478 calls
c_.___ tes��j_c;���?r::t:�!�SJ) c�Jl�---··· ____ ··--··· __
252
Bináris beszúrás müködése
Az eredményeket a 9.2. táblázatban foglaljuk össze.
[Rendezés tipusa összehasontitások száma*
Bináris beszúrás 41 471
Összefésüléses rendezés 43 928
Buborékrendezés 102 478
Gyorsrendezés 97 850
*A teszt véletlenszerű jellege rniatt a kapott eredmények valamelyest eltérhetnek a fent szereplóktól.
9.2. táblázat. Bes�rási módszerek te/jesítméf!J-ósszehasonlítása 4091 darab véletlenszerű
bes�rás alapján
A 9.2. táblázatban tisztán látható, hogy a bináris beszúrás teljesít a legjobban, jóllehet csak kevéssel előzi meg az összefésüléses rendezést. A buborékrendezés és a gyorsrendezés teljesítménye pedig messze elmarad az első kettőtől. Vegyük figyelembe azonban, hogy bár az összefésüléses rendezés teljesítménye összevethető a bináris beszúráséval, az összefésüléses rendezéshez külön listára van szükség az eredmények tárolására, míg a bináris beszúrás magába az eredeti listába szúr be.
A kapott eredményekből kiszámíthatjuk az összehasonlítások átlagos számát bináris keresés esetén. A bináris beszúrás első lépése egy bináris keresés, amelyhez, mint tudjuk, O(log N) összehasonlítás szükséges. Mivel a lista kezdetben üres, nem kell összehasonlítást végezni. A 2. elem beszúrásához l ogz2 összehasonlítás kell, és ez így megy tovább egészen l ogzN-ig. Egy leegyszerűsítő számítás nyomán a teljesítményt N l ogzN becsülhetjük meg, de az összehasonlítások száma valójában inkább a l ogzN ! -hez közeli t.
A fenti összehasonlítások valójában kicsit igazságtalanok. V alahányszor a bináris beszúrási algoritmus új értéket szúr be, rögtön a megfelelő helyre rakja, hogy fenntartsa a lista rendezettségét. Ez azt jelenti, hogy a lista mindig rendezett állapotban van, függetlenül attól, hogy hány elemet adunk hozzá és milyen sorrendben. Ezzel szemben a három rendezési algoritmust felhasználó tesztben csak azután rendeztük a listát, miután az összes elemet beszúrtuk Ez azt jelenti, hogy a lista többnyire rendezetlen marad, egészen a program futásának végéig. Mi történne, ha minden egyes beszúrás után rendeznünk kellene a listát?
A 9.3. táblázatban összefoglaljuk, hogyan változnak az eredmények, ha a popu
l ateAndsort () metódust átalakí�uk oly módon, hogy minden egyes beszúrás után rendezést végez:
253
Bináris keresés és beszúrás
private void populateAndSort(ListSorter sorter) { for (int i = 0; i < TEST_SIZE; ++i) {
_list.add(nextvalue()); _list = sorter.sort(_list);
}
reportca ll s ();
}
Rendezés tipusa ÖSSzehasonUlások száma*
Bináris beszúrás 41 481
Összefésüléses rendezés 48 852 618
Buborékrendezés 44 910 616
Gyorsrendezés Nem jellemző**
*A teszt véletlenszerű jellege miatt a kapott eredmények valamelyest eltérhetnek a fent szereplőktől.
** 5 perc után megszakítottuk a program futását, mert még ennyi idő alatt sem végzett a feladattal.
9.3. táblázat. Te!Jesítmb!J-Ö'sszehasonlítás núnden bes'{!Írás utáni rendezés esetén
Az eredmények tükrében világosan látszik, hogy a minden beszúrás utáni rendezés nem elfogadható alternatíva. Ba azt szeretnénk, hogy a beszúrások közben is rendezettek legyenek az adataink, a bináris beszúrás toronymagasan a legjobb módszer, a maga legalább 1000-szer kevesebb összehasonlításával.
Egy utolsó megjegyzés: mivel a gyakorlatban valószínűleg tömblistát (vagy egyéb gyors, indexelt hozzáférést biztosító adatstruktúrát) használunk, a kívánt teljesítmény mellett viszonylag lassú lehet az új elemek beszúrása. (Emlékezzünk vissza, hogy egy tömblistában a beszúrási pont után elhelyezkedő elemek mindegyikét egygyel jobbra kell tolnunk, hogy helyet csináljunk az új elemnek.) Kis adathalmazok esetében a segédszámítási költség elhanyagolható. Nagyobb adathalmazok esetén azonban a fentiek észlelhető hatást gyakorolhatnak a teljesítményre.
Összefoglalás
Ebben a fejezetben a következő témaköröket vizsgáltuk
254
• A bináris keresés "oszd meg és uralkodj" megközelítést használ keresési · kulcsok megkereséséhez, és mindezt átlagosan O(log N) darab összehason
lítás ú�án éri el.
• A bináris keresést akár rekurzív, akár iteratív méclszerekkel hatékonyan valósítha�uk meg.
Összefoglalás
• A bináris keresés leghatékonyabban akkor tud múködni, ha a felhasznált
adatstruktúra támogatja a gyors, indexalapú adatelérést.
• A bináris beszúrás bináris keresést használ elemeknek a listába való beszú
rására - mindezt úgy, hogy a listabeli rendezettség megmarad - o (l o g N!)
darab összehasonlítás árán.
• A bináris beszúrás nagyon hatékonynak bizonyul, és legalább olyan jól, ha
nem jobban teljesít, mint a legismertebb rendezési algoritmusok.
255
TIZEDIK FEJEZET
Bináris keresőfák
A 9. fejezetben megismerkedhettünk egy olyan bináris keresési algoritmussal, amely le
hetővé teszi rendezett tömblisták hatékony keresését. Sajnos a bináris keresési algorit
musok megsínylik, amikor beillesztésekre és törlésekre kerül sor, mivel az elemeket
mindenfelé másolják. A bináris keresőfák ezzel szemben elérhetik a bináris keresési al
goritmusok ádagos O(l og N) keresési, beillesztési, illetve törlési idejét a velejáró segéd
szárrútási költség nélkül. Azáltal, hogy az értékeket fastruktúrában tároljuk - ahol az ér
tékek össze vannak láncolva -, könnyű új értéket hozzáadni és a törölteket eltávolítani.
Más fejezetekkel szemben a most következő nagyrészt elméleti. Azaz nem dol
gozunk gyakorlati példákkal, mivel a bináris keresőfák valójában számos más adat
sttuktúra alapját képezile Ez a fejezet a bináris keresőfák múködéséről szóló leírásra
szorítkozik, a gyakorlati használatukat nem tárgyalja. Későbbi fejezetekben, amelyek a
halmazokról (12. fejezet), a térképekről (13. fejezet) és a B-fákról (15. fejezet) szól
nak, látha�uk majd, hogyan épülnek ezek az adatstruktúrák az ebben a fejezetben
sémaként bemutatott kódra.
A fejezetben a következő témaköröket tárgyaljuk:
• a bináris keresőfákat érdekessé tévő tulajdonságokat,
• számos bináriskeresőfa-múveletet és múködésüket,
• az adatrendezésnek a teljesítményre gyakorolt befolyását,
• egy egyszerű technikát az egyensúly visszaállítására beillesztés és törlés után,
• nem kiegyensúlyozott bináris keresáfa tesztelését és megvalósítását.
A bináris keresőfákról
A második fejezetben megemlítettük, hogy egy fájlrendszerre gyakran könyvtárfa
ként hivatkozunk. Ennél formálisabb megközelítésben ugyanakkor egy fa egymással
kapcsolatban álló csomópontokból áll, ahol minden csomópont nulla vagy több
gyermekkel rendelkezik és legfeljebb egy szülővel. A csomópontok egyike a gyökér,
ez az egyeden, amelynek nincsen szülője. A gyermek nélküli csomópontokat levél
csomópontnak nevezzük.
Bináris keresőfák
Ahogyan egy könyvtárEa rendelkezhet bármennyi könyvtárral, alkönyvtárral és fájl
lal, úgy a fáknak is akárhány gyermeke lehet. Mindazonáltal a legáltalánosabb típusú fa
a bináris fa, amely nevét onnan kapta, hogy minden csomópontnak legfeljebb két
gyermeke van. (Ezekre a gyermekekre gyakran bal és jobb elnevezéssel hivatkozunk.)
A bináris keresáfa olyan bináris fa, amely még egy feltételnek megfelel: tetszőle
ges csúcspont bal oldali részfájának minden értéke kisebb, mint a csúcspont értéke,
míg a jobb oldali részfa minden értéke nagyobb, mint a csúcs értéke.
A 10.1. ábrán láthatunk egy példát a bináris keresőfára. Itt az I betű a gyökér
csomópont, az A, a H, a K és a P betű pedig mind levélcsomópont. Minden bal ol
dali csomóponton elhelyezkedő gyermek értéke kisebb, minden jobb csomóponton
elhelyezkedőé nagyobb, mint a csomóponté magáé.
Gyökércsomópont
Levélcsomópontok
1 O. 1. ábra. Egyszerű bináris keresója
Ezek a tulajdonságok igen hatékony keresést, beillesztést és törlést tesznek lehetővé, a
bináris keresőfa átlagos keresési ideje egyenesen arányos a magasságával: O(h). A 10.1.
ábrán látható példa esetében a fa magassága (a leghosszabb út a gyökércsomóponttól
egy levélig) három, tehát az átlagos keresési idő várhatóan három összehasonlításból áll.
Egy kiegyensúlyozott fa esetében-mint amilyen a 10.1. ábrán szereplő is- a fa
magassága o(log N). Ugyanakkor bizonyos körülmények között ettől eltérő lehet,
ami az O(N) legrosszabb esetbeli keresési idejéhez vezet.
Minimum
A bináris keresőfa minimuma a legkisebb értékkel rendelkező csomópont, és - hála a bi
náris keresáfa tulajdonságainak - a minimum megtalálása nem is lehetne egyszerűbb.
Egyszerűen kövessük a bal oldali hivatkozásokat a fa gyökerétől kezdve a legkisebb ér
tékkel rendelkező megtalálásához. Másképpen a minimum a fa bal szélső csomópontja.
258
A bináris keresőfákról
Ha követjük a bal oldali hivatkozásokat a 10.1. ábrán látható példában a gyökércsomóponttól, az I-től kiindulva egészen a levélcsomópontig, az A betűhöz, a fa legkisebb értékéhez jutunk.
Maximum
Míg a bináris keresáfa minimuma a legkisebb értékkel rendelkező csomópont, a maxi
mum a legnagyobb értékkel rendelkező csomópont. A maximum megtalálása nagyon hasonlít a minimum megtalálásához, csak éppen a bal oldaliak helyett a jobb oldali hivatkozásokat kell követni. Másképpen a maximum a jobb szélső csomópont a fán.
Ennek bizonyításához kövessük a jobb oldali hivatkozásokat a gyökércsomóponttól a 1 0.1. ábrán egészen a levélcsomópontig. A P betűhöz kell érkeznünk- a fa legnagyobb értékéhez.
A következő csomópont
Egy csomópont után kó.vetkező csomópont a következő legnagyobb értékkel rendelkező csomópont a fán. Például a 10.1. ábrán látható fán az A után következő csomópont D, a H után következő csomópont I, az I után következő csomópont pedig K. A következő csomópont megtalálása nem bonyolult, de két külön esetet foglal magában.
Az első esetben, amikor a csomópontnak egy jobb oldali gyermeke van, az utána következő csomópont annak a jobb oldali részfájának a minimuma. Például az I után következő csomópont keresésekor láthatjuk, hogy I egy jobb oldali gyermekkel rendelkezik, L-lel, tehát az L gyökerű részfa minimumát, K-t vesszük. Ugyanez vonatkozik az L betűre: rendelkezik egy jobb oldali gyermekkel, M-mel, tehát megkeressük az M gyökerű részfa minimumát, ami ebben az esetben maga az M.
Ennek megfelelően, ha a csomópont nem rendelkezik gyermekkel - ez az eset a H-nál -, visszafelé kell keresni a fán, amíg megtaláljuk az első "jobb kanyart". Ez alatt azt kell érteni, hogy addig keresünk felfelé a fán, amíg találunk egy csomópontot, amely bal oldali gyermek, és annak szülőjét használjuk. Ebben a példában felfelé mozgunk a fán, balra tartva (tehát jobb oldali hivatkozásokat követünk) a H-tól az F-ig, aztán ismét balra a D-ig, és végül jobbra kanyaradunk az I-hez.
259
Bináris keresőfák
A megelőző csomópont
Egy csomópontot megelőző csomópont a következő legkisebb értékű csomópont. Pél
dául a P-t megelőző csomópont M, az F-et megelőző csomópont D, és az I-t meg
előző csomópont H.
A megelőző csomópont megtalálására alkalmas algoritmus épp az ellentéte an
nak, amelyet a következő csomópont megtalálásához használnánk, és két hasonló
esetet foglal magában. Az első esetben, ha a csomópont rendelkezik bal oldali gyer
mekkel, akkor a bal oldali rész fa maximumát vesszük. A második esetben - amikor a
csomópont nem rendelkezik bal oldali gyermekkel - addig haladunk felfelé a fán,
arrúg találunk egy "bal kanyart".
Keresés
A bináris keresőfában egy érték megtalálásához a gyökércsomópontból indulunk, és
addig követjük a bal, illetve a jobb oldali hivatkozásokat, amíg vagy megtaláljuk a ke
resett értéket, vagy nincs több követhető hivatkozás. Ezt a következő lépésekben
foglalhatjuk össze.
1. A gyökércsomóponttól indulunk.
2. Ha nincs aktuális csomópont, a keresett értéket nem találtuk meg, és készen
vagyunk. Ha nem így van, térjünk át a 3. lépésre.
3. Hasonlítsuk össze a keresett értéket az aktuális csomópontban található
kulccsal!
4. Ha a kulcsok megegyeznek, megtaláltuk a keresési kulcsot, és készen va
gyunk. Ha nem így van, lépjünk tovább a 5. lépéshez.
5. Ha a keresési kulcs kisebb, mint az aktuális csomópontban található, köves
sük a bal oldali hivatkozást, és térjünk vissza a 2. lépéshez.
6. Ha nem így van, a keresési kulcs magasabb, mint az aktuális csomópontban
található, tehát kövessük a jobb oldali hivatkozást, és lépjünk a 2. lépéshez.
A következő példában láthatjuk, hogyan keresnénk a K betút a 10.1. ábrán látható
bináris keresőfában.
A gyökércsomópontból indulva (1. lépés) összehasonlí�uk a keresett értéket, K-t,
az I betűvel, ahogyan az a 1 0.2. ábrán is látható.
Mivel aK az I után található, a jobb oldali hivatkozást köve�ük (6. lépés), amely
az L betút tartalmazó csomóponthoz vezet Oásd 10.3. ábra).
260
10.2. ábra. A keresés mindig a gyökércsomópontból indul
10.3. ábra. Kó"vessük a jobb oldali hivatkozást, amikor a keresési kulcs
az aktuális csomópontbeli kulcs után van
A bináris keresőfákról
Még mindig nincs egyezés, ám mivel K kisebb, mint L, a bal oldali hivatkozást kö
vetjük (5. lépés), ahogyan az a 10.4. ábrán látható.
1 0.4. ábra. Kó"vessük a bal oldali hivatkozást, amikor a keresési kulcs
az aktuális csomópontbeli kulcs előtt van
Végül találtunk egyezést (lásd 10.4. ábra): a keresett érték megegyezik az aktuális
csomópontban található értékkel (4. lépés), és a keresés véget ér. Egy kilenc értéket
tartalmazó fán kerestünk, és mindössze 3 összehasonlítás alapján megtaláltuk a kere
sett értéket. Ezenkívül jegyezzük meg, hogy az egyezést három szinttel lejjebb talál
tuk a fán- o(h).
261
Bináris keresőfák
Minden egyes alkalommal, amikor lefelé haladunk a fán, kizárjuk az értékek felét
- épp úgy, mint amikor bináris keresést végzünk egy rendezett listában. Valójában
egy rendezett listát véve alapul könnyedén előállitható a megfelelő bináris keresőfa,
ahogyan az a 10.5. ábrán látható.
o 2 3 4 5 6 7 8
A l D l F H l l K L lM l p
+ 4
10.5. ábra. Rendezett lista kie!!Jensú!Jozott bináris keresőfaként ábrázolva
Ha összehasonlítjuk az éppen elvégzett keresést a 9. fejezetben található példákkal,
láthatjuk, hogy az összehasonlítások sorrendje azonos. Ez azért van, mert egy ki
egyensúlyozott bináris keresőfa ugyanolyan teljesítménykarakterisztikával rendelke
zik, mint egy rendezett lista.
Beszúrás
A beszúrás majdnem azonos a kereséssel, azzal a kivétellel, hogy amikor az érték
nem létezik, levélcsomópontként hozzáadjuk a fához. Az előbbi keresési példánál ha
a J értéket szetettük volna beszúrni, a bal oldali hivatkozást követtük volna a K-tól,
és arra jutottunk volna, hogy nincs több csomópont. Ezért tehát nyugodtan hozzá
adhattuk volna a J-t a K bal oldali gyermekeként, ahogyan az a 1 0.6. ábrán is látható.
Az újonnan beszúrt értéket levélként adtuk hozzá, amely ebben az esetben nem
érintette a fa magasságát.
Viszonylag véletlen adatok beillesztése többnyire lehetővé teszi a fa számára,
hogy fenntartsa o(log N) magasságát, de mi történik, amikor nem véletlen adatokat
szúrunk be, mint például egy szólistát egy szótárból, vagy neveket egy telefonkönyv
ből? El tudjuk képzelni, mi történne, ha egy üres fát vennénk alapul, és a következő
értékeket ábécésotrendben beszúrnánk: A, D, F, H, I, K, L, M és P?
262
A bináris keresőfákról
10.6. ábra. A beszúrás mindig egy új levélcsomópont létrehozásávaljár
Figyelembe véve, hogy az új értékeket minclig levélcsomópontként szúrjuk be, és
gondolva arra, hogy valamennyi nagyobb érték jobb oldali gyermekévé válik a szülő
jének, az értékek növekvő sorrendben történő beillesztése súlyosan kiegyensúlyozat
lan fához vezet, amilyen a 10.7. ábrán látható.
1 O. 7. ábra. Rendezett adatok beszúrása kijvetke'{!ében /étrqott kiegyensúlyozatlan fa
Akárhányszor rendezett adatokat szúrunk be, a bináris fa valójában láncolt listává
módosul, és a fa magassága- az ádagos keresési idővel együtt- O(N) lesz.
Azért nincs még veszve minden, ha rendezett adatunk van. Számos bináriskere
sőfa-variáció létezik, amely kiegyensúlyozást végez, mint például a piros-fekete fák,
az A VL-fák, a hajlító fák és így tovább, amelyek mind komoly újrastrukturálással ál
litják vissza a fa egyensúlyának egy részét. Ezért ezekkel a könyvnek ebben a részé
ben nem foglalkozunk. Mindazonáltal egy újszerű, ugyanakkor egészen egyszerű, B
fa névre hallgató variációval megismerkedünk a 15. fejezetben.
263
Bináris keresőfák
Törlés
A törlés valamivel több erőfeszítéssel jár, mint a keresés vagy a beszúrás. Alapvetően
három esetet kell figyelembe vennünk. A törlendő csomópont az alábbiakban felso
rolt valamelyik feltételnek fog megfelelni.
• Nem található gyermek (egy levél): ebben az esetben egyszerűen eltávolí�uk.
• Egy gyermek található (akár balra, akár jobbra): ekkor a törölt csomópontot
a gyermekkel helyettesí�ük.
• Két gyermek található: ilyenkor a csomópontot felcseréljük a következő
csomóponttal, és ismét próbátkozunk vagy az első, vagy a második esettel.
Most valamennyi esetet kifejtjük bővebben is; a legegyszerűbbel kezdjük, amely a le
vélcsomópont törlése. Minden esetben feltételezzük, hogy a kiindulás olyan fa, mint
amelyet a 1 0.1. ábrán láthatunk.
A legegyszerűbb esetben egy levélcsomópontot törlünk. Mivel a levélcsomó
pontnak nincsenek gyermekei, csupárt a szülőhöz vezető hivatkozást kell megszün
tetnünk. A 10.8. ábrán láthatjuk a H érték fáról való törlésének módját.
10.8. ábra. A levélcsomópontok törlésekor a siftlóhiiz vezető hivatkozást szünteijük meg
A második legegyszerűbb esetben egy olyan csomópontot törlünk, amely csak egy
gyermekkel rendelkezik. A mindössze egy gyermekkel rendelkező csomópont törlé
sekor a szülópont gyermekké konvertálásával kibogozzuk azt. A 10.9. ábra a fát mu
ta�a az M betű törlése után.
Figyeljük meg, hogy az M és szülője, az L, valamint gyermeke, a P között fennálló
hivatkozásokat hogyan váltottuk fel egy L és P között fennálló közvetlen kapcsolattal.
Egy két gyermekkel rendelkező csomópont törlése egy kicsit trükkösebb. Példá
ul képzeljük el, hogy aK gyökércsomópontot kell törölnünk a 1 0.1. ábrán látható fá
ról. Melyik csomópontot használnánk fel cserének? Alakilag a két gyermekcsomó
pont bármelyikét használhatnánk (a D-t vagy az L-t), és a bináris keresáfa tulajdon
ságai megmaradnának Mindazonáltal mindkét csomópont rendelkezik két saját
gyermekkel, ami megnehezíti a törölt csomópont egyszerű kibogozását.
264
A bináris keresőfákról
10.9. ábra. A mindössze egy gyermekkel rendelkező csomópontokat kibogozifik
Ezért a két gyermekkel rendelkező csomópont törléséhez először meg kell találni a
következő csomópontot (ugyanúgy választhatnánk a megelőző csomópontot is, mi
vel ugyanolyan jól működne) és kicsetélni az értékeket. A 10.10. ábrán láthatjuk I és
K felcserélésének az eredményét. Figyeljük meg, hogy ez átmenetileg megsérti a bi
náris keresőfákra vonatkozó szabályokat.
1 O. 1 O. ábra. A födendő csomópontban található értéket Jelcseré!fük a kö"vetkező csomóponttal
Az érték mozgatása után ahelyett, hogy törölnénk az eredeti csomópontot, amelynek
értéke most már K, azt a csomópontot töröljük, amellyel értéket váltottunk Ez a fo
lyamat már garantáltan az első vagy a második esetbe tartozik. Honnak tudhatjuk
ezt? Kezdetben az eredetileg törölni kívánt csomópontnak két gyermeke volt, ami
azt jelenti, hogy egy jobb oldali gyermekének is kellett lennie. Ezenkívül a jobb oldali
gyermekkel rendelkező csomópont után következő csomópont a jobb oldali gyer
mek minirnurna (vagy a bal szélső csomópontja). Ennélfogva a következő csomó
pont vagy levélcsomópont (gyermekek nélküli csomópont) lehet, vagy legfeljebb egy
jobb oldali gyermekkel rendelkezhet. Ha bal oldali gyermeke volna, defuúció szerint
nem lehetne a minirnurn.
A példában az értékek felcserélésével (lásd 10.10. ábra) biztonságosan törölhet
jük az I betűt tartalmazó levélcsomóponto t, ahogyan az a 1 0.11. ábrán látható.
265
Bináris keresőfák
1 O. 11. ábra. A kö"vetkezó csomópontot tó.riiljük
A törlés kiegyensúlyozacianná teheti a bináris keresőfát, ami a teljesítmény romlásá
hoz vezet. Ahogy rendezett adat beszúrása egy fába kiegyensúlyozacianná teheti azt,
a rendezett adatok törlése is járhat ilyen eredménnyel. Például a 10.12. ábrán látható,
milyen hatással járt az A, D, F és H értékek törlése a 10.1. ábrán látható fáról.
1 O. 12. ábra. Rendezett adatok tó"rlése kó.vetke'{!ében létrqóft kiegyensú!Jozatlan fa
Mivel valamennyi felsorolt érték a fa bal oldalán található (a gyökértől kezdve), az
eredmény egy féloldalas fa.
Bármelyik törlési esetre is van szükség, és függetlenül attól, hogy a törlés után a
fa kiegyensúlyozott marad-e, a legtöbb időt a törlendő csomópont kiválasztása
igényli (és talán a következő csomópont megtalálása). Ezért, mint a keresés és a be
szúrás esetében is, a törlés ideje még mindig O(h).
lnorder bejárás
Az inorder bqárás, mint a neve is sugallja, rendezett sorrendben látoga�a meg a biná
ris keresőfán található értékeket. Ez hasznos lehet az adatok sorrendben történő ki
nyomtatásánál, vagy akár feldolgozásánál. Ismét csak a 10.1. ábrán látható példabeli
fát véve egy inorder bqárás az értékeket a következő sorrendben látogatná meg: A, D,
F, I, K, L, M, P.
266
A bináris keresőfákról
Az inorder bejárás végrehajtásának két egyszerű módja van: rekurzív vagy iteratív. Rekurzív inorder bejárás végrehajtásához a gyökércsomópontból indulunk.
1. J árj uk be a csomópont bal oldali részfáját!
2. Vizsgáljuk meg magát a csomópontot!
3. Járjuk be a jobb oldali részfát!
Iteratív inorder bejárás végrehajtásához egy bináris keresőfán a fa minimumában kezdjünk, látogassuk meg azt és minden azt követő csomópontot, ainíg nem marad több.
Preorder bejárás
A preorder bejárás először a gyökércsomópontot látogatja meg, majd minden egyes részfát. A 10.1. ábrán látható fa preorder bejárása a következő értékeket eredményezné: I, D, A, F, H, L, K, M, P.
Az inorder bejáráshoz hasonlóan a preorder bejárást is könnyú rekurzív módon definiálni. Egy preorder bejárás végrehajtásához a gyökércsomópontból indulunk:
1. Vizsgáljuk meg magát a csomópontod
2. J árj uk be a bal oldali rész fát!
3. Járjuk be a jobb oldali részfát!
Mindazonáltal az inorder bejárással ellentétben a preorder bejárás inkább iteratív formát használ, és implicit processzorverem helyett a verem explicit használatára van szükség (lásd 5. fejezet) rekurzív hívások végrehajtásakor.
Posztorder bejárás
A posztorder bejárás a gyökércsomópontot minden egyes részfa meglátogatása után
érinti. A 10.1. ábrán látható fa posztorder bejárása a csomópontokat a következő sorrendben érintené: A, H, F, D, K, P, M, L, I.
Egy posztorder bejárás végrehajtásához a gyökércsomópontból indulunk:
1. J árj uk be a bal oldali részfát!
2. Járjuk be a jobb oldali részfát!
3. Vizsgáljuk meg magát a csomópontot!
A preorder bejáráshoz hasonlóan itt is iteratív formát és vermet használunk.
267
Bináris keresőfák
Kiegyensúlyozás
A bináris keresőfába beszúrt és az abból törölt adatok sorrendje hatással van a teljesítményre. Pontosabban a rendezett adat beszúrása és törlése a fa kiegyensúlyozatlanná válását okozha�a, és a legrosszabb esetben egyszerű láncolt listává degradálja. Ahogyan már említettük, a kiegyensú!Jozás a fa kívánt tulajdonságainak visszaállításához használható módszer. Bár ennek megvalósítása túlmutat a könyv keretein, mégis úgy gondoljuk, fontos legalább megérteni, hogyan működnek a kiegyensúlyozó algoritmusok, még ha nem is mutatunk be programpéldákat. E célból egy ilyen ún.
A VL-fa rövid összefoglalását adjuk közre: A kiegyensúlyozott fa fenntartásakor felmerülő egyik legnehezebb feladat azon
nal észlelni az kiegyensúlyozatlanságot. Képzeljünk el egy fát több száz, vagy akár több ezer csomóponttal. Hogyan fedezhe�ük fel a kiegyensúlyozatlanságot törlés vagy beszúrás végrehajtása után az egész fa áttekintése nélkül?
Két orosz matematikus, név szerint G. M. Adelszon-Velszkij és E. M. Landisz (innen származik az A VL elnevezés) felismerte, hogy a bináris keresáfa egyensúlyának fenntartására igen egyszerű módszer az egyes részfák magasságának a követése. Ha két testvér magassága valaha is több mint eggyel eltér, a fa kiegyensúlyozacianná válik.
A 1 0.13. ábrán egy újra kiegyensúlyozandó fát láthatunk. Figyeljük meg, hogy a gyökércsomópont gyermekeinek magassága kettővel tér eL
+2
1 0.13. ábra. A gyökércsomópont gyermekeinek magassága kö"'{!i eltérés tö"bb mint egy
Miután észleltük a kiegyensúlyozatlariságot, ki kell javítanunk, de hogyan? A megoldás csomópontok forgatása a kiegyensúlyozatlanság megszüntetésének érdekében. Az egyensúly visszaállítását a beszúrt vagy törölt csomóponttól a fán felfelé haladva hajtjuk végre, szükség szerint a csomópontok elforgatásával, minden egyes alkalommal, amikor egy csomópontot beszúrunk az A VL-fába vagy törlünk belőle.
A kiegyensúlyozatlanság természete alapján négy különböző forgatási módot különböztetünk meg: egyszeres és kétszeres forgatást, mindkét esetben balra, illetve jobbra. A 10.1. táblázat azt muta�a, hogyan állapíthatjuk meg, hogy egyszeres vagy kétszeres forgatásra van-e szükség.
268
Kiegyensúlyozatlan
Balról nehéz
Jobbról nehéz
A gyermek kiegyensúlyozott
Egyszeresen
Egyszeresen
1 o. 1. táblázat. A rotációs szám meghatározása
A gyermek balról nehéz
Egyszeresen
Kétszeresen
A bináris keresőfákról
A gyermek jobbról nehéz
Kétszeresen
Egyszeresen
A 10,13. ábrán a gyökércsomópont jobbról nehéz, és jobb oldali gyermeke, az L
szintén az. A 10.1. táblázatban található információ azt mutatja, hogy csak egy forgatás végrehajtására van szükség - balra az I alsóbb, és az L felsőbb osztályba sorolásához, amelynek eredménye a 10.14. ábrán látható fa.
-1
10.14. ábra. AzAVL magassági tulcijdonságait a csomópont eJJJszeres
balra forgatása he!Jreállíija
Ha a fa a 10.15. ábrán látható módon fest, két forgatásra van szükség- a gyökércsomópont jobbról, a gyermek balról nehéz. Ez akkor történhet meg, ha például épp most illesztettük be aK-t.
+2
10.15. ábra. Két forgatás! igé1!Jiő fa
Az L-t először egyszer jobbra, majd egyszer balra forga�uk, ahogyan az a 10.16. ábrán látható.
Bár az A VL-fák nem garantáltan kiegyensúlyozottak, kiváló teljesítménykarakterisztikát mutatnak. Egymillió csomóponttal számolva egy tökéletesen kiegyensúlyozott fa esetében l og2lOOOOOO = 20 összehaso?lltásra volna szükség, míg egy A VL-fa esetében l. 44· l og2lOOOOOO = 28 összehasonlításra. Ez egészen biztosan sokkal jobb, mint a legrosszabb esetbeli bináris keresésnél szükséges 500 OOO összehasonlítás!
269
Bináris keresőfák
+2 ,.-o
�� o o
l L
1 o. 16. ábra. Az első forgatás az L gyermekcsomópontot jobbra, a második a kiegyensú/yozatlan I csomópontot balra mo:<gaija
Bár itt nem foglalkozunk vele, az önkiegyensúlyozó bináris fa egy másik variációja a
piros-fekete fa. A piros-fekete fával kapcsolatos bővebb információért lásd Cormen
et al.: Algoritmusok (2001) című könyvét.
A bináris keresőfa tesztelése és megvalósítása
Lássunk végül néhány kódot is. Mint mindig, kezdetnek teszteket alakítunk ki. Ami
kor ezzel végeztünk, megírjuk a megvalósítás kódját. A megvalósítás részeként két
osztályt hozunk létre: Node és B i narysearchTree. A Node, ahogyan a neve is sugall
ja, a fában található csomópontokat modellezi, míg a B i narysearchTree a gyökér
csomópont köré biztosít burkolás t, és tartalmazza valamennyi search() , de l e te O
és i n se rt() kódot.
Jvlivel a B i narySearchTree osztály nem sokat jelent a csomópontok nélkül, a
következő gyakorlófeladatban néhány csomóponttesztet fogunk írni. Ezután megír
juk magát a csomópontosztályt.
Gyakorlófeladat: a csomópontosztály tesztelése
Hozzuk létre a következő csomópontteszteket:
270
package com.wrox.algorithms.bstrees;
import junit.framework.Testcase;
public class NodeTest extends Testcase { private Node _a; private Node _d; private Node _f; private Node _h; private Node _i; .Private Node k·
A bináris keresőfa tesztelése és megvalósítása
private Node .::.1; private Node _m;
private Node _p;
protected void setUp() throws Exception {
super. setup();
_a= new Node("A"); _h= new Node("H");
_k = new Node("K");
_p= new Node("P");
_f = new Node("F", null, _h);
_m = new Node("M", null, _p);
_d= new Node("o", _a, _f);
_l= new Node("L", _k, _m);
_i = new Node("!", _d, _l);
}
public void testMinimum() {
assertsame(_a, _a.minimum());
assertSame(_a, _d.minimum());
assertSame(_f, _f.minimum());
assertsame(_h, _h.minimum());
assertsame(_a, _i.minimum());
assertsame(_k, _k.minimum()); assertsame(_k, _l.minimum());
assertsame(_m, _m.minimum());
assertSame(_p, _p.minimum());
}
public void testMaximum() {
assertsame(_a, _a.maximum());
assertSame(_h, _d.maximum());
assertsame(_h, _f.maximum());
assertsame(_h, _h.maximum());
assertsame(_p, _i.maximum());
assertsame(_k, _k.maximum());
assertsame(_p, _l.maximum());
assertsame(_p, _m.maximum());
assertsame(_p, _p.maximum());
}
public void testsuccessor() {
assertsame(_d, _a.successor());
assertsame(_f, _d.successor());
assertsame(_h, _f.successor());
assertsame(_i, _h.successor());
assertsame(_k, _i.successor());
assertsame(_l, _k.successor());
assertsame(_m, _l.successor()); assertsame(_p, _m.successor());
assertNull(_p.successor());
}
271
Bináris keresőfák
272
public voia testPreaecessor() { assertNull(_a.predecessor()); assertsame(_a, _d.predecessor()); assertsame(_d, _f.predecessor()); assertsame(_f, _h.predecessor()); assertsame(_h, _i.predecessor()); assertsame(_i, _k.predecessor()); assertsame(_k, _l.predecessor()); assertsame(_l, _m.predecessor()); assertsame(_m, _p.predecessor());
}
public void testissmaller() { assertTrue(_a.issmaller()); assertTrue(_d.issmaller()); assertFalse(_f.issmaller()); assertFalse(_h.issmaller()); assertFalse(_i.issmaller()); assertTrue(_k.isSmaller()); assertFalse(_l.isSmaller()); assertFalse(_m.issmaller()); assertFalse(_p.issmaller());
}
public void testisLarger() { assertFalse(_a.isLarger()); assertFalse(_d.isLarger()); assertTrue(_f.isLarger()); assertTrue(_h.isLarger()); assertFalse(_i.isLarger()); assertFalse(_k.isLarger()); assertTrue(_l.isLarger()); assertTrue(_m.isLarger()); assertTrue(_p.isLarger());
}
public void testsize() { assertEquals(l, _a.size()); assertEquals(4, _d.size()); assertEquals(2, _f.size()); assertEquals(l, _h.size()); assertEquals(9, _i.size()); assertEquals(l, _k.size()); assertEquals(4, _l.size()); assertEquals(2, _m.size()); assertEquals(l, _p.size());
}
public void testEquals() { Node a = new Node("A"); Node h= new Node("H"); Node k = new Node("K"); Node .. P. =_new NOde("J>':).·
A bináris keresőfa tesztelése és megvalósítása
} l
Node f = new Node("F"-;--r1Ull -;-·h}; Node m = new Node("M", null, p); Node d= new Node("o", a, f); NOde l = new Node("L", k, m); Node i = new Node("I", d, l);
assertEquals(a, _a); assertEquals(d, _d); assertEquals(f, _f); assertEquals(h,,_h); assertEquals(i, _i); assertEquals(k, _k); assertEquals(l, _l); assertEquals(m, _m); assertEquals(p, _p);
assertFalse(_i.equals(null)); assertFalse(_f.equals(_d));
A megvalósitás müködése
Valamennyi teszt ugyanolyan csomópontstruktúrával indul, mint amilyen a 10.1. áb
rán látható. (Ez megfelelő alkalom az emlékezetünk felfrissítésére.)
A NodeTest osztály néhány példányváltozót definiál - a 10.1. ábrán látható
minden csomóponthoz egyet-, és inicializálja őket a setup() keretein belül a tesz
teseteknél való használathoz. Az első négy csomópont mind levélcsomópont (aho
gyan a példákban), így csak egy értékre van szüksége. A fennmaradó csomópontok
nak mind van vagy bal oldali, vagy jobb oldali gyermekük, amelyek a második, illetve
a harmadik konstruktorparaméterbe kerülnek:
package com.wrox.algorithms.bstrees;
import junit.framework.Testcase;
public class NodeTest extends Testcase { private Node _a; private Node _d; private Node _f; private Node _h; private Node _i; private Node _k; private Node _l; private Node _m; private Node _p;
protected void setup() throws Exception { super. setup();
273
Bináris keresőfák
_a _h _k _p _f _m _d _l _i
}
}
new new new new
= new = new
new new new
Node("A"); Node("H"); Node("K"); Node("P"); Node(" F", null, _h); Node("M", null, _p); Node("D", _a, _f); Node("L", _k, _m); Node("!", _d, _l);
A minimum() és a maximum() metódusoknak (többek között) a Node osztály részét kell
képezrúük. Ez lehetővé teszi egy fa mirúmumának és maximumának a megtalálását a
gyökércsomópont lekérdezésével. A tesztelést is jelentősen megkönnyíti. A test
Mi ni mum() és a testMaxi mum() metódusok meglehetősen egyértelműek: egyszerűen
meggyőződünk afelől, hogy a fán található valamennyi csomópontra meghivva a he
lyes értéket kapjuk mirúmumként, illetve maximumként:
public void testMinimum() { assertsame(_a, _a.minimum()); assertsame(_a, _d.minimum()); assertsame(_f, _f.minimum()); assertsame(_h, _h.minimum()); assertsame(_a, _i .minimum()); assertsame(_k, _k.minimum()); assertsame(_k, _l .minimum()); assertsame(_m, _m.minimum()); assertsame(_p, _p.minimum());
}
public void testMaximum() { assertsame(_a, _a.maximum()); assertsame(_h, _d.maximum()); assertsame(_h, _f.maximum()); assertsame(_h, _h.maximum()); assertsame(_p, _i .maximum()); assertsame(_k, _k.maximum()); assertsame(_p, _l.maximum()); assertsame(_p, �m.maximum()); assertsame(_p, _p.maximum());
}
A következők a successor() (következő csomópont) és a predecessor() (előző
csomópont). Ezeket a metódusokat is a Node-ba helyezzük, ahelyett hogy a B i nary
SearchTree segédmetódusaként használnánk őket.
274
A bináris keresőfa tesztelése és megvalósítása
A testsuccessor() metódus például megadja, hogy az "A" után következő
csomópont a "D", a "D" után következő az "F" és így tovább, úgy, mint a korábbi
példákban. Figyeljük meg, hogy mivel az "A" előtt és a "P" után nincs másik csomó
pont, a várható eredmény mind a két esetben nu ll:
public void testsuccessor() { assertsame(_d, _a.successor()); assertsame(_f, _d.successor()); assertSame(_h, _f.successor()); assertsame(_i, _h.successor()); assertsame(_k, _i.successor()); assertsame(_l, _k.successor()); assertsame(_m, _l.successor()); assertsame(_p, _m.successor()); assertNull(_p.successor());
}
public void testPredecessor() { assertNull(_a.predecessor()); assertsame(_a, _d.predecessor()); assertsame(_d, _f.predecessor()); assertsame(_f, _h. predecessor()); assertsame(_h, _i.predecessor()); assertSame(_i, _k.predecessor()); assertsame(_k, _l.predecessor()); assertSame(_l, _m.predecessor()); assertsame(_m, _p.predecessor());
}
Létrehozunk még egy tesztpárt- testrssmaller() és testrsLarger()- az eddig
nem említett metódusokra, amelyek később még hasznosnak bizonyulhatnak Egy
csomópontot akkor kezelünk kisebb gyermekként, ha szülőjének a bal oldali gyer
meke. Ugyanez fordítva is érvényes: egy csomópont csak akkor esik nagyobb gyer
mek besorolás alá, ha jobb oldali gyermeke a szülőnek.
public void testisSmaller() { assertTrue(_a.issmaller()); assertTrue(_d.issmaller()); assertFalse(_f.issmaller()); assertFalse(_h.issmaller()); assertFalse(_i.issmaller()); assertTrue(_k.issmaller()); assertFalse(_l.issmaller()); assertFalse(_m.issmaller()); assertFalse(_p.issmaller());
}
public void testisLarger() { assertFalse(_a.isLarger()); assertFalse(_d.isLarger());
275
Bináris keresőfák
}
assertTrue(_f.i sLarger()); assertTrue(_h.i sLarger()); assertFalse(_i.isLarger()); assertFalse(_k.isLarger()); assertTrue(_l.i sLarger()); assertTrue(_m.i sLarger()); assertTrue(_p.isLarger());
Végül létrehozunk néhány tesztet az equals O metódushoz. Az equals O metódus
akkor jut majd fontos szerephez, amikor a B i narysearchTree osztályt fogjuk tesz
telni, mivel lehetővé teszi a csomópontok beszúrásakor és törlésekor létrejött struk
túrák összehasonlítását az elvárt eredménnyel. A megvalósítás az aktuális csomó
pontból indul, és összehasonlí�a az értékeket, valamint a bal oldali és a jobb oldali
gyermekeket, ahogyan lefelé halad a levélcsomópontokig.
A testEqua ls O esetében a csomópontstruktúra másolatát hozzuk létre. Aztán
összehasonlítjuk az egyes példányváltozókat lokálisváltozó-megfelelőjükkel, és el
lenőrzünk korlátfeltételeket is, hogy meggyőződjünk róla, nem úgy kódoltuk az
equals O metódust, hogy mindig true legyen az eredmény!
public voi d testEquals() {
}
Node a = new Node("A"); Node h new Node("H"); Node k = new Node("K"); Node p = new Node("P"); Node f = new Node("F", Node m new Node("M", Node d new Node("D", Node l new Node("L", Node new Node("!",
assertEquals(a, _a); assertEquals(d, _d); assertEquals(f, _f); assertEquals(h, _h); assertEquals(i, _i); assertEquals(k, _k); assertEquals(l, _l); assertEquals(m, _m); assertEquals(p, _p);
null, null, a, f); k, m); d, l);
assertFalse(_i.equals(null)); assertFalse(_f.equals(_d));
h); p);
Miután elkészültünk a tesztekkel, a következő gyakorlófeladatban magát a csomó
pontosztályt alkotjuk meg.
276
A bináris keresőfa tesztelése és megvalósítása
Gyakorlófeladat: a csomópontosztály tesztelése
Hozzuk létre a következő csomópontosztályt:
package com.wrox.algoritnms.ostrees;
public class Node implements Cloneable { private object _value; private Node _parent; private Node _smaller; private Node _larger;
public Node(object value) { this(value, null, null);
}
public Node(object value, Node smaller, Node larger) { setvalue(value);
}
setsmaller(smaller); setLarger(larger);
if (smaller != null) { smaller.setParent(this);
}
if Clarger != null) { larger.setParent(this);
}
public object getvalue() { return _value;
}
public void setvalue(object value) {
}
assert value != null : "az érték nem lehet NULL"; _value = value;
public Node getParent() { return _parent;
}
public void setParent(Node parent) { _parent = parent;
}
public Node getsmaller() { return _smaller;
277
Bináris keresőfák
278
public void setsmaller(Noae smaller) { assert smaller != getLarger() "a kisebb nem lehet azonos
a nagyobbal"; _smaller = smaller;
}
public Node getLarger() { return _larger;
}
public void setlarger(Node larger) { assert larger != getsmaller() "a nagyobb nem lehet azonos
a kisebbel";
_larger = larger;
}
public boolean issmaller() {
return getParent() != null && this
}
public boolean isLarger() {
return getParent() != null && this
}
public Node minimum() { Node node = this;
}
while (node.getsmaller() != null) { node = node.getsmaller();
}
return node;
public Node maximum() { Node node = this;
}
while (node.getlarger() != null) { node = node.getLarger();
}
return node;
public Node successor() { if (getLarger() != null) {
return getlarger().minimum();
}
Node node this;
getParent().getsmaller();
getParent().getLarger();
A bináris keresőfa tesztelése és megvalósítása
}
while -(nodé-:-isLarger0) {
node = node.getParent();
}
return node.getParent();
public Node predecessor() {
}
if (getsmaller() != null) { return getsmaller().maximum();
}
Node node = this;
while (node.issmaller()) { node = node.getParent();
}
return node.getParent();
public int size() { return size(this);
}
public boolean equals(Object object) { if (this == object) {
}
return true;
}
if (object == null l l object.getclass() != getclass()) { return false;
}
Node other = (Node) object;
return getvalue().equals(other.getvalue()) && equalssmaller(other.getsmaller()) && equalsLarger(other.getLarger());
private int size(Node node) { if (node == null) {
return O;
}
return l+ size(node.getsmaller()) + size(node.getlarger());
}
279
Bináris keresőfák
}
private boolean equalssmaller(Node other) { return getsmaller() == null && other == null
11 getsmaller() != null && getsmaller().equals(other);
}
private boolean equalslarger(Node other) { return getlarger() == null && other == null
l l getlarger() l= null && getlarger().equals(other);
}
A megvalósitás müködése
Minden csomópontban található egy érték, egy hivatkozás a szülőre, a kisebb (vagy
bal oldali) és a nagyobb (vagy jobb oldali) gyermekre:
package com.wrox.algorithms.bstrees;
public class Node { private object _value; private Node _parent; private Node _smaller;
private Node _larger;
}
Két konstruktort is biztosítottunk. A:z első konstruktor feladata levélcsomópontok lét
rehozása- amelyeknek nincs gyermeke -, ezért az egyeden argumentuma egy érték:
public Node(object value) { this(value, null, null);
}
A második konstruktor ugyanakkor valamiféle kényelmet nyújt, mivel lehetővé teszi
olyan csomópontok létrehozását, amelyeknek gyermekük is van. Jegyezzük meg,
hogy ha megadunk egy nem null gyermeket, a konstruktor igen kényelmes módon
beállí* annak a gyermeknek a szülőjét. Ez, ahogy talán emlékszünk a tesztekből,
triviálissá teszi a csomópontok fastruktúrává való összefűzését:
280
public Node(Object value, Node smaller, Node larger) { setvalue(value);
setsmaller(smaller); setLarger(larger);
if (smaller != null) { smaller.setParent(this);
}
A bináris keresőfa tesztelése és megvalósítása
}
if Clarger != null) { larger.setParent(this);
}
Ha már létrejött, hozzáférésre van szükség a csomópont értékéhez, szülójéhez és
bármely gyermekéhez. Ehhez létrehozunk néhány szabványos kiemelőt és beállitót.
Semmi különös, csupán néhány pluszkijelentést helyeztünk el - például annak tesz
telésére, hogy nem helyeztük-e mindkét gyermeket azonos csomópontba:
public object getvalue() { return _value;
}
public void setvalue(Object value) {
}
assert value != null : "az érték nem lehet NULL"; _value = value;
public Node getParent() { return _parent;
}
public void setParent(Node parent) { _parent = parent;
}
public Node getsmaller() { return _smaller;
}
public void setsmaller(Node smaller) {
assert smaller != getLarger() : "a kisebb nem lehet azonos
a nagyobbal"; _smaller = smaller;
}
public Node getLarger() { return _larger;
}
public void setLarger(Node larger) {
}
assert larger != getsmaller() : "a nagyobb nem lehet azonos a kisebbel";
_larger = larger;
Ezután kövessünk néhány kényelmi módszert az egyes csomópontok tulajdonságai
nak megállapítására.
281
Bináris keresőfák
Az issmaller() és az isLarger() metódus eredménye csak akkor true, ha a
csomópont kisebb, illetve nagyobb a szülőjénél:
public boolean issmaller() {
return !isRoot() && this getParent().getsmaller();
}
public boolean isLarger() { return !isRoot() && this == getParent().getLarger();
}
A minimum vagy a maximum megtalálása sem sokkal bonyolultabb. Emlékezzünk,
hogy a csomópont minimuma a legkisebb gyermeke, a maximum pedig a legna
gyobb (vagy amelynek nincs saját gyermeke). Figyeljük meg, hogy a maximum() kódja
majdnem azonos ami ni mum() kódjával; míg ami ni mum() a getsmall er() metódust,
a maximum() a getLarge r() metódust hívja meg:
public Node minimum() { Node node = this;
}
while (node.getSmaller() != null) { node = node.getsmaller();
}
return node;
public Node maximum() { Node node = this;
}
while (node.getLarger() != null) { node = node.getLarger();
}
return node;
Egy csomópontot megelőző és az utána következő csomópont megtalálása már
komolyabb feladat. Emlékezzünk, hogy a csomópontot követő csomópont vagy a
legnagyobb gyermek minimuma- ha van ilyen -, vagy az első olyan csomópont,
amellyel találkozunk egy "jobb kanyar" után, amikor felfelé haladunk a fán.
A successor() megtekintésekor láthatjuk, hogy ha a csomópontnak van egy
nagyobb gyermeke, akkor a minimumát vesszük. Ha nincs, akkor elindulunk felfelé
a fán a "jobb kanyart" keresve, úgy, hogy ellenőrizzük, az aktuális csomópont-e a
legnagyobb a szülőjének gyermekei közül. Ha ez a nagyobb, akkor jobbra kell lennie
a szülőjétől, és balra visszamegyünk felfelé a fán.
282
A bináris keresőfa tesztelése és megvalósítása
Lényegében felfelé mozgunk a fán az első olyan csomópontot keresve, amely a
szülő kisebb (azaz bal oldali) gyermeke. Ha megtaláltuk, "jobb kanyart" kell tenni
ahhoz, hogy a szülőhöz jussunk - pontosan, amire vártunk.
Figyeljük meg azt is, hogy mint a minimum() és a maximum() esetében a
successor() és a predecessor() a tükörképei egymásnak: ahol a successor() a
minimum, ott a predecessor() a maximum értéket veszi fel; amikor a successor az
i sLarge r() metódust hívja meg, akkor a predecessor az i ssma ll e r() metódust:
public Node successor() {
}
if (getLarger() l= null) { return getLarger().minimum();
}
Node node = this;.
while (node.isLarger()) { node = node.getParent();
}
return node.getParent();
public Node predecessor() {
}
if (getsmaller() != null) { return getsmaller().maximum();
}
Node node = this;
while (node.issmaller()) { node = node.getParent();
}
return node.getParent();
Végül elérkeztünk az equals() metódushoz. Ez a csomópont leginkább összetett
(bár még mindig elég érthető) metódusa, de sokkal később fogjuk csak használni a
B i narysearchTree osztály által létrehozott fák felépítésének a vizsgálatára.
A sablonkód mellett az általános equals() metódus hasonlítja össze minden egyes
csomópont három aspektusát, hogy kiderüljön, azonosak-e: az értéket, a kisebb és a na
gyobb gyermeket. Az értékek összehasonlítása egyszerű: tudjuk, hogy az érték sosem
lehet null; tehát elegendő az értékek egyszerű delegálása az equals() metódusba:
283
Bináris keresőfák
public boolean equals(Object object) { if (this == object) {
}
return true;
}
if (object == null l l object.getclass() != getclass()) { return false;
}
Node other = (Node) object;
return getvalue().equals(other.getvalue()) && equalssmaller(other.getsmaller()) && equalsLarger(other.getLarger());
Gyermekcsomópontok összehasonlítása kissé nehezebb munka, mivel nemcsak
egyik vagy akár mindkét gyermek értéke lehet nu ll, de a gyermekek gyermekeit,
azok gyermekeit és így tovább is ellenőrizni kell egészen a levélcsomópontokig. Eh
hez két segédmetódust hoztunk létre: az equalssmaller() és az equalsLarger()
elnevezésűt. Ezek a metódusok összehasonlítják az aktuális csomópont gyermekeit a
másik csomópont megfelelő gyermekével. Például az equalssmaller() összeveti az
aktuális csomópont kisebbik gyermekét a másik csomópont kisebbik gyermekével.
Ha mindkét gyermek értéke null, akkor a csomópontokat egyenlőnek tekintjük.
Amennyiben csak az egyik gyermek értéke null, semmiképpen sem lehetnek egyen
lők. Ha azonban mind az aktuális csomópontnak, mind a másik csomópontnak van
egy kisebb gyermeke, akkor rekurzív módon meghívjuk az equals() metódust, hogy
folytassuk az ellenőrzést a fán lefelé:
private boolean equalssmaller(Node other) { return getsmaller() == null && other == null
ll getsmaller() != null && getsmaller() .equals(other);
}
private boolean equalsLarger(Node other) { return getLarger() == null && other == null
l l getLarger() != null && getLarger().equals(other);
}
Ennyit a csomópontosztályokróL A következő gyakorlófeladatban teszteket hozunk
létre a végső bináris keresőfára való felkészülésként.
284
A bináris keresőfa tesztelése és megvalósítása
Gyakorlófeladat: a bináris keresőfa tesztelése
Hozzuk létre a tesztosztályt a következőképpen:
j)ad<a9é�C:om':Wrö'X:·a19ori thms. bstreés;' " = , ·- �= - ------ - - ----�- ---�-.
import com.wrox.algorithms.sorting.Naturalcomparator; import junit.framework.Testcase;
public class BinarysearchTreeTest extends Testcase { private Node _a; private Node _d; private Node _f; private Node _h; private Node _i; private Node _k; private Node _l; private Node _m; private Node _p; private Node _root; private BinarysearchTree _tree;
protected void setup() throws Exception { super. setUp();
}
_a= new Node("A"); _h= new Node("H"); _k = new Node("K"); _p= new Node("P"); _f"' new Node("F", null, _h); __m "' new Node("M", null, _p); _d= new Node("D", _a, _f); _l = new Node("L", _k, _m); _i= new Node("I", _d, _l); _root= _i;
_tree = new BinarysearchTree(Naturalcomparator.INSTANCE); _tree.insert(_i.getvalue()); _tree.insert(_d.getvalue()); _tree.insert(_l.getvalue()); _tree.insert(_a.getvalue()); _tree.insert(_f.getvalue()); _tree.insert(_k.getvalue()); _tree.insert(_m.getvalue()); _tree.insert(_h.getvalue()); _tree.insert(_p.getvalue());
public void testinsert() { assertEquals(_root, _tree.getRoot());
285
Bináris keresőfák
286
public void testsearch() {
}
assertEquals(_a, _tree.search(_a.getvalue())); assertEquals(_d, _tree.search(_d.getvalue())); assertEquals(_f, _tree.search(_f.getvalue())); assertEquals(_h, _tree.search(_h.getvalue())); assertEquals(_i, _tree.search(_i.getvalue())); assertEquals(_k, _tree.search(_k.getvalue())); assertEquals(_l, _tree.search(_l.getvalue())); assertEquals(_m, _tree.search(_m.getvalue())); assertEquals(_p, _tree.search(_p.getvalue()));
assertNull Ltree. search("UNKNOWN"));
public void testoeleteLeafNode() {
}
Node deleted = _tree.delete(_h.getvalue()); assertNotNull(deleted); assertEquals(_h.getvalue(), deleted.getvalue());
_f.setLarger(null); assertEquals(_root, _tree.getRoot());
public void testDeleteNodewithonechild() {
}
NOde deleted = _tree.delete(_m.getvalue()); assertNotNull(deleted); assertEquals(_m.getvalue(), deleted.getvalue());
_l.setLarger(_p); assertEquals(_root, _tree.getRoot());
public void testDeleteNodewithTWoChildren() {
}
Node deleted = _tree.delete(_i.getvalue()); assertNOtNull(deleted); assertEquals(_i.getvalue(), deleted.getvalue());
_i .setvalue(_k.getvalue()); _l.setsmaller(null); assertEqualsLroot, _tree.getRoot());
public void testDeleteRootNodeuntilTreeisEmpty() {
}
while Ltree.getRoot() != null) {
}
Object key= _tree.getRoot().getvalue(); Node deleted = _tree.delete(key); assertNotNull(deleted); assertEquals(key, deleted.getvalue());
}.�------------�----
A bináris keresőfa tesztelése és megvalósítása
A megvalósitás müködése
Minden tesztünk használja a B i narysearchTree osztályt a fa alakítására, hogy úgy nézzen ki, mint a 10.1. ábrán látható. Aztán, ahogyan a csomóponttesztek esetében is, összehasonlítjuk ezt a fát a kézzel készítette!. Ha megfelelnek egymásnak, akkor tudha�uk, hogy a kód az elvárt módon működik.
A B i narysearchTreeTest osztály néhány csomópontot definiál az összehasonlításhoz, és létrehozza a B i narysearchTree osztályt a csomópontokkal azonos értékekkel. Figyeljük �eg, hogy az értékeket egy nagyon speciális sorrendben, de nem ábécérendben szúrtuk be. Ügyeljünk rá, hogy fánk nem hajt végre kiegyensúlyozás!. Ha az értékeket ábécérendben illesztenénk be, az eredmény csökevényes fa volna - amely úgy néz ki, mint egy láncolt lista (lásd 10.7. ábra). Ehelyett az értékeket speciilisan meghatározott sorrendben illesz�ük be, amelynek célja a 10.1. ábrán láthatóhoz hasonló kiegyensúlyozott fa létrehozása. Meg tudjuk mondani, vajon miért működik ez? Az értékeket rendezettség előtti állapotban szúrtuk be. Azaz a beszúrási sorrend olyan, hogy minden egyes részfa szülőcsomópon�át hozzáadjuk a fához az összes gyermeke előtt:
package com.wrox.algorithms.bstrees;
import com.wrox.algorithms.sorting.Naturalcomparator; import junit.framework.Testcase;
public class BinarysearchTreeTest extends Testcase { private Node _a; private Node _d; private Node _f; private Node _h; private Node _i;
private Node _k; private Node _l;
private Node _m; private Node _p; private Node _root; private BinarysearchTree _tree;
protected void setUp() throws Exception { super. setup();
_a= new Node("a"); _h= new Node("h"); _k = new Node("k"); _p= new Node("p"); _f = new Node("f", null, _h); _m = new Node("m", null, _p);
d= new Node("d", _a, _f); l = new Node("l", _k, _m);
new Node("i", _d, _l); root= _i;
287
Bináris keresőfák
}
_tree = new BinarysearchTree(Naturalcomparator.INSTANCE); _tree.insert(_i .getvalue());
_tree.insert(_d.getvalue());
_tree.insert(_l.getvalue());
_tree.insert(_a.getvalue());
_tree.insert(_f.getvalue());
_tree.insert(_k.getvalue()); _tree.insert(_m.getvalue());
_tree.insert(_h.getvalue());
_tree.insert(_p.getvalue());
Miután felállitottuk a küoduló állapotot, a következő dolgunk meggyőződni arról,
hogy a felépített fa éppen úgy néz ki, rrúnt az, amelyet az összehasonlításhoz fogunk
használni.
A testinsert() során meggyőződünk róla, hogy létezik a getRoot() metódus
a B i narySearchTree osztályon, amely lehetővé teszi a gyökércsomópontba való be
jutást. Ezután az equals() illetáclust használjuk a Node-on a strukturális egyenlőség
megállapí�sára:
public void testinsert() {
assertEquals(_root, _tree.getRoot());
}
Most, hogy van egy ismert állapotú fánk (és a folyamat során teszteltük az insert()
metódust), a B i narysearchTree osztály maradékának a viselkedését teszteljük, elő
ször a search() segítségéveL
Azt várjuk, hogy a search() eredménye találat esetén egy meghatározott érték
nek megfelelő csomópont; amennyiben nincs találat, null. Ennélfogva a test
search() segítségével keresést végzünk núnden ismert értékre, összehasonlítva az
eredményül kapott csomópontot a kézzel előállitott megfelelő csomóponttal. Figyel
jük meg az ellenőrzést, amelynek célja megbizonyosodni afelől, hogy egy ismeretlen
érték eredménye nu ll:
288
public void testsearch() {
}
assertEquals(_a, _tree.search(_a.getvalue())); assertEquals(_d, _tree.search(_d.getvalue()));
assertEquals(_f, _tree.search(_f.getvalue()));
assertEquals(_h, _tree.search(_h.getvalue())); assertEquals(_i, _tree.search(_i.getvalue()));
assertEquals(_k, _tree.search(_k.getvalue())); assertEquals(_l, _tree.search(_l.getvalue()));
assertEquals(_m, _tree.search(_m.getvalue()));
assertEquals(_p, _tree.search(_p.getvalue()));
assertNull(_tree.search("UNKNOWN"));
A bináris keresőfa tesztelése és megvalosítása
Az egyetlen ismert és tesztelt metódus a de l e te O volt. Amint azt tudjuk, létezik néhány különbözó forgatókönyv a tesztelésre: levélcsomópontok, csomópontok egy és két gyermekkel.
Egy levélcsomópont egyszerű törlésével elkezdve lá�uk, mi történik a H érték törlésekor, ahogyan az a 1 0.8. ábrán látható. A testDel eteLeafNode O metódus először törli az értéket a fából, aztán rögzíti a törölt csomópontot. Ekkor megbizonyosodunk róla, hogy a hívás után valóban a csomópont volt az eredmény, és hogy a törölt csomópont értéke tényleg H volt. Végül módosítjuk a tesztcsomópontot, hogy M szülójének (az F-nek) már ne legyen nagyobb gyermeke, ahogyan azt a törlésalgoritmus a várakozások szerint teljesítette. Most összehasonlíthatjuk a csomópontstruktúrát a fa gyökerével; azonosnak kell lenniük:
public void testDeleteLeafNode() {
}
Node deJeted = _tree.delete(_h.getvalue()); assertNotNull(deleted); assertEquals(_h.getvalue(), deleted.getvalue());
_f.setLarger(null); assertEquals(_root, _tree.getRoot());
Ezután töröltük az egy gyermekkel rendelkező csomópontot, az M-et, ahogyan az a 10.9. ábrán látható. Ez alkalommal a testDeleteNodewithonechildOtörli az "M"
értéket a fából; majd az eredményül kapott érték ellenőrzése után ismét módosítjuk a csomópontstruktúrát, hogy emlékeztessen a fa várt struktúrájára. Ezt a kettót aztán összehasonlítjuk, hogy egyenlők-e. Figyeljük meg, hogy P-t az L nagyobb gyermekévé tettük, kivágv� az M betűt és a "csonkokat" összeillesztve:
public void testDeleteNodewithonechild() {
}
Node deleted = _tree.delete(_m.getvalue()); assertNotNull(deleted); assertEquals(_m.getvalue(), deleted.getvalue());
_l.setLarger(_p); assertEquals(_root, _tree.getRoot());
Végül megpróbáltuk törölni a két gyermekkel rendelkező csomópontot (az "I" gyökércsomópontot), ahogyan azt a 10.1 O. és a 1 0.11. ábrán látha� uk. Az I fából való törlésével a testDel eteNodewi thTwochi l d ren O a megfelelő módon frissíti a várt struktúrát, és összehasonlítja a fa gyökerével:
289
Bináris keresőfák
public void testDeleteNodewithTwochildren() { Node deleted = _tree.delete(_i.getvalue()); assertNotNull(deleted);
}
assertEquals(_i .getvalue(), deleted.getvalue());
_i .setvalue(_k.getvalue()); _l.setsmaller(null); assertEquals(_root, _tree.getRoot());
Megbizonyosadva róla, hogy megvizsgáltuk a fa viselkedését, a következő gyakorló
feladatban magát a bináriskeresőfa-osztályt valósítjuk meg.
Gyakorlófeladat: bináris keresőfa megvalósítása
Hozzuk létre az B i narysearchTree osztályt a következőképpen:
290
package com.wrox.algorithms.bstrees;
import com.wrox.algorithms.sorting.comparator;
public class BinarysearchTree { private final comparator _comparator; private Node _root;
public BinarysearchTree(Comparator comparator) {
}
assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
public Node search(Object value) {
}
assert value != null : "a 'value' nem lehet NULL";
Node node = _root;
while (node != null) { int cmp = _comparator.compare(value, node.getValue()); if (cmp == O) {
}
break; }
node cmp < O ? node.getsmaller()
return node;
node.getLarger();
public Node insert(Object value) { Node parent= null; Node node = _root; int cmQ = O;
}
whil'e (nÖde' T=:"" null){ parent = node;
A bináris keresőfa tesztelése és megvalósítása
cmp = _comparator.compare(value, node.getvalue()); node = cmp <=O? node.getsmaller() : node.getLarger();
}
Node inserted = new Node(value); inserted.setParent(parent);
if (parent == null) { _root = inserted;
} else if (cmp < 0) { parent.setsmaller(inserted);
} else { parent.setLarger(inserted);
}
return inserted;
public Node delete(object value) { Node node = search(value); if (node == null) {
return null; }
Node deleted = node.getsmaller() != null && node.getLarger() != null ? node.successor() : node;
assert deleted != null : "a 'deleted' nem lehet null";
Node replacement = deleted.getsmaller() != null ? deleted.getsmaller() : deleted.getLarger();
if (replacement != null) { replacement.setParent(deleted.getParent());
}
if (deleted == _root) { _root = replacement;
} else if (deleted.issmaller()) { deleted.getParent().setsmaller(replacement);
} else { deleted.getParent().setLarger(replacement);
}
if (deleted != node) { object deletedvalue = node.getvalue(); node.setValue(deleted.getvalue()); deleted.setvalue(deletedvalue);
291
Bináris keresőfák
}
public Node getRoot() { return _root;
}
A megvalósitás működése
A B i narySearchTree osztály összehasonlítót használ az értékek összehasonlításához:
a gyökércsomópontéhoz, amely lehet nu ll, ha a fa üres, valamint egy olyan metódust,
amely hozzáférést biztosít a tesztek során használt gyökércsomóponthoz. Figyeljük
meg, hogy nem valósítottunk meg egyetlen interfészt sem, és nem terjesztettünk ki
egyetlen alaposztályt sem. Ezt a bináriskeresőfa-megvalósítást nem a jelenlegi formájá
ban való használatra találták ki (a 12. és 13. fejezetekből majd megtudha�uk, miért):
package com.wrox.algorithms.bstrees;
import com.wrox.algorithms.sorting.comparator;
public class BinarysearchTree {
}
private final Comparator _comparator; private Node _root;
public BinarysearchTree(comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
}
public Node getRoot() { return _root;
}
A legegyszerűbb megvalósított módszer a search() volt. Ez a metódus egy bizo
nyos értéket keres a fában, és az eredménye a megfélelő csomópont vagy nu ll, ha az
érték nem található. A gyökércsomópontból indul, és addig folytatódik, amíg vagy
talál egyezést, vagy kifogy a csomópontokbóL A whi l e ciklus valamennyi lépésre so
rán a keresett értéket az aktuális csomópontban találhatóval hasonlí�a össze. Ha az
értékek azonosak, megtaláltuk a keresett csomópontot, és kiszállhatunk a ciklusból;
ha nem, akkor a megfelelő, a kisebb vagy a nagyobb hivatkozást köve�ük:
292
A bináris keresőfa tesztelése és megvalósítása
public Node search(Object value) { assert valu� != null : "a 'value' nem lehet NULL";
}
Node node = _root;
while (node != null) { int cmp = _comparator.compare(value, node.getvalue()); if (cmp == O) {
break; }
node cmp < O ? node.getsmaller() node.getLarger(); }
return node;
Az insert() első fele egyszerűen végigkutatja a fát a megfelelő levélcsomópont
után, amelyhez az új értéket csatolni fogjuk, a megfelelő, a kisebb vagy a nagyobb
hivatkozás követésével: amikor a whi l e ciklus véget ér, a parent változó vagy null,
amikor a fa üres volt, és az új csomópontot beállitha�uk gyökércsomópontnak, vagy
az lesz az új csomópont szülője. Ekkor, miután már meghatároztuk az új csomó
pont szülőjét, a megfelelő, a kisebb vagy a nagyobb gyermekként állitjuk be - a cmp
változóban még megtalálható a legutóbbi érték-összehasonlítás eredménye.
Rá tudunk-e mutatni bármely különbségre a whi l e ciklusban az insert() és a
search() kód között? A search() esetében kiszállunk a ciklus ból, amint egy illesz
kedő értéket találunk (cmp == O). Az insert() során ugyanakkor egy,azonos értéket
kisebbként kezelünk (bár éppilyen egyszerűen kezelhetnénk nagyobbként is). Vajon
mi történne, ha ugyanazt az értéket adnánk hozzá kétszer? Kiegyensúlyozadan fa
jönne létre.
public Node insert(Object value) { Node parent= null; Node node =_root; int cmp = O;
while (node != null) { parent = node;
}
cmp = _comparator.compare(value, node.getvalue()); node = cmp <=O? node.getsmaller() : node.getLarger();
Node inserted = new Node(value); inserted.setParent(parent);
if (parent == null) { _root = inserted;
} else if (cmp < O) {
293
Bináris keresőfák
}
parent.setsmaller(inserted);
} else { parent.setlarger(inserted);
}
return inserted;
Végül, de nem utolsósorban, használjuk a delete() metódust. Ahogyan várható, a bináris keresőfából értéket törölni sokkal bonyolultabb, mint akár keresni, akár beszúrni, rnivel számos különböző helyzetet kell figyelembe venni. Ehhez képest valójában nem túl nehéz az esetek érthető kódrészletté kombinálása.
A de l e te() metódus először keresést végez, hogy megtalálja az eltávolítandó csomópontot. Ha az értéket nem találja (node == null), akkor nyilvánvalóan nincs tennivaló, és azonnal eredménnyel jár. Ha azonban talál, akad még némi elvégzendő munka.
Ha megvan a törlendő csomópont, meg kell határoznunk, hogy a csomópont önmagában törölhető-e, vagy meg kell találni az azt követő csomópontot. Emlékezzünk, ha a csomópont nulla vagy egy gyermekkel rendelkezik, azonnal eltávolítható. Ám ha a csomópontnak két gyermeke van, ki kell cserélnünk a következő csomóponttal, és a másik csomópontot kell eltávolitanunk
Miután eldöntöttük, hogy valijában melyik csomópontot távolitsuk el, a következő lépés a helyettesítő megtalálása. Ezen a ponton tudjuk, hogy az előző lépés rniatt az eltávolítandó csomópont legfeljebb egy gyermekkel rendelkezik, vagy talán eggyel sem. Ezért egyszerűen fogjuk a gyermeket (ha van ilyen), és ugyanahhoz a szülőhöz rendeljük, mint amelyikhez a törölt csomópont tartozott.
A helyettesítő megtalálása után helyre kell hozni a szülőtől eredő hivatkozást. Amennyiben a törölt csomópont a gyökércsomópont volt, a helyettesítő lesz az új gyökér. Egyébként pedig a helyettesítőt a megfelelő kisebb vagy nagyobb gyermek helyére tesszük.
Végül egy kis takarítás: ha a fából eltávolitott csomópont nem az eredetileg megtalált volt - a következő csomóponttal való kicserélés rniatt -, az értékeket is meg kell cserélnünk, rnielőtt a törölt csomópontot visszaküldjük a hívónak:
294
public Node delete(Object value) { Node node = search(value); if (node == null) {
return null;
}
Node deleted node.getsmaller() != null &&
node.getLarger() != null 7
node.successor() : node; assert deleted != null : "a 'deleted' nem lehet null";
A bináris keresőfa teljesítményének megállapítása
}
Node replacement = deleted.getsmaller() != null ?
deleted.getsmaller() : deleted.getLarger(); if (replacement != null) {
replacement.setParent(deleted.getParent()); }
if (deleted == _root) { _root = replacement;
} else if (deleted.issmaller()) { deleted.getParent().setsmaller(replacement);
} else { deleted.getParent().setLarger(replacement);
}
if (deleted != node) {
}
object deletedvalue = node.getvalue(); node.setvalue(deleted.getvalue()); deleted.setvalue(deletedvalue);
return deleted;
A bináris keresöfa teljesítményének megállapitása
Eddig csupán a bináris keresőfák teljesítményéről esett szó, így a következő gyakor
lófeladatban olyan kódot írunk, amely ténylegesen bemutatja a bináris keresőfák tu
lajdonságait. Ehhez létrehozunk néhány tesztet az adatok beszúrása közbeni össze
hasonlítások számának mérésére. Ezután összehasonlíthatjuk a véletlen módon elő
állitott adatok beszúrásának eredményeit a rendezett adatok beszúrásáévaL
Gyakorlófeladat: teljesitménytesztek megvalósitása és futtatása
Alkossuk meg a teljesítményteszt-osztályt a következőképpen:
íiaékaQé'"'Cöm:-wrox:a:i9QrTft1ms:b5tr-ees·;-
import com.wrox.algorithms.lists.ArrayList; impprt com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.Callcountingcomparator; import com.wrox.algorithms.sorting.Naturalcomparator; i�r::t_j uni t. framework. Test.Case;
295
Bináris keresőfák
296
public-class BinarysearchTreecallcountingTest exténds Testcase { private static final int TEST_SIZE = 1000;
}
private callcountingcomparator _comparator; private BinarysearchTree _tree;
protected void setup() throws Exception { super. setup();
}
_comparator =
new callcountingcomparator(Naturalcomparator.INSTANCE); _tree = new BinarysearchTree(_comparator);
public void testRandominsertion() { for (int i = 0; i < TEST_SIZE; ++i) {
_tree.insert(new Integer((int) (Math.random() * TEST_SIZE))); }
reportcall s();
public void testinOrderinsertion() { for (int i = 0; i < TEST_SIZE; ++i) {
_tree.insert(new Integer(i)); }
reportcalls(); }
public void testPreorderinsertion() { List list= new Arraylist(TEST_SIZE);
}
for (int i = 0; i < TEST_SIZE; ++i) { list.add(new Integer(i));
}
preorderrnsert(list, O, list.size() - l);
reportcalls();
private void preorderinsert(List list, int lowerindex, int upperrndex) {
if (lowerindex > upperindex) { return;
}
A bináris keresőfa teljesítményének megállapítása
l
}
_tree:""fnsertClist.getCinaex)); p.reorderinsert(list , lowerindex, index - l); preorderinsert(list , index + l, upperindex);
private void reportcalls() {
}
System.out.print:ln(getName() + " : "
+
_comparator.getcallcount() + " calls");
A megvalósitás müködése
Kényelmi okokból a B i narysearchTreeca ll Counti ngTest osztályt szabványos ]Unit tesztosztályba csomagoltuk Mint a 9. fejezetben található bináris kereséssel kapcsolatos teljesítménytesztek, ezek a tesztek sem "valódi" tesztek- nem tesznek állitásokat -, a ]Unit ismerete is elég késztető erő ennek a megközelítésnek az alkalmazásához.
Az osztály a bináris fát olyan osztályba sorolja, amelybe néhány értéket illesztünk, egy összehasonlítót az értékek összehasonlításához, valamint egy konstanst, amely az értékek számát határozza meg- ez a TEST_SIZE. Hozzáadtunk egy reportcall s() elnevezesű metódust is, amely az összehasonlítóba érkezett hívásszámot írja ki, a test
name: #### hívás formában.
package com.wrox.algorithms.bstrees;
import com.wrox.algorithms.sorting.CallcountingComparato�; import com.wrox.algorithms.sorting.Naturalcomparator; import junit.framework.Testcase;
public class BinarysearchTreecallcountingTest extends Testcase { private static final int TEST_SIZE = 1000;
private callcountingcomparator _comparator; private BinarysearchTree _tree;
protected void setUp() throws Exception { super. setUp();
_comparator =
new callcountingcomparator(Naturalcomparator.INSTANCE); _tree = new BinarySearchTree(_comparator);
}
}
private void reportcalls() {
}
System.out.println(getName() + " : " + _comparator.getcallcount() + " cal ls");
297
Bináris keresőfák
A testRandomrnsert() metóduson belül beszúrjuk a TEST_SIZE darab véletlen mó
don generált számot, ezzel felépítve a viszonylag kiegyensúlyozottnak gondolt fát:
public void testRandomrnsertion() { for (int i = 0; i < TEST_SIZE; ++i) {
_tree.insert(new Integer((int) (Math.random() * TEST_SIZE)));
}
reportcalls();
}
Ezután a testrnorderrnsertion() metóduson belül beszúrunk (rendezett) értékeket
O és a TEST _SIZE között, hogy létrehozzuk a súlyosan kiegyensúlyozaciannak vélt fát:
public void testrnorderrnsertion() { for (int i = 0; i < TEST_SIZE; ++i) {
_tree.insert(new Integer(i));
}
reportcalls();
}
A teszteket lefuttatva a környezettől függően a következőkhöz hasonló kimenetet
láthatunk:
testRandomrnsertion: 11624 hívás testrnorderrnsertion: 499500 hívás
A 10.2. táblázatban látható az eddigiek összefoglalása.
Beszúrás tipusa Összehasonlitások száma*
V életlen beszúrás 11 624
Inorder beszúrás 499 500
*A teszt véletlenszerű jellege miatt a kapott eredmények valamelyest eltérhetnek a fent vázoltaktól.
1 O. 2. táblázat. Te!Jesítméf!)-összehasonfítás egy keresőfába való 1 OOO beiffesifés es etén
Ahogy látjuk, a beszúrás akkor teljesít legjobban, amikor az adatok rendezetlenek -
valójában, ahogyan azt a 10.2. táblázat világosan megmuta�a, jelentősen jobban: a
véletlen beszúrás teljesítésének átlagos ideje összehasonlításoknál 11624 l 1000 =
ll vagy O(l o g N); az inorder esetében 499500 l 1000 = 499 vagy O(N).
298
Összefoglalás
Összefoglalás
A fejezetben a bináris keresőfák működésének leírását olvashattuk Most már valószínűleg biztos alappal rendelkezünk a későbbi fejezetekben található gyakorlati példák megértéséhez (halmazok a 12. fejezetben és leképezés a 13. fejezetben).
A fejezetből a következőket tudhattuk meg:
•
•
•
•
•
•
•
•
•
•
A bináris keresőfák adatokat tartalmaznak, és hivatkoznak egy bal oldali és egy jobb oldali gyermekre.
A bal oldali gyermekek mindig kisebbek, �t a szüleik .
A jobb oldali gyermekek mindig nagyobbak, mint a szüleik.
A fák vagy kiegyensúlyozottak, vagy kiegyensúlyozatlanok.
Minden bináris keresáfa rendelkezik egy átlagos keresési idővel, amely O(h) .
A kiegyensúlyozott fák magassága h = O(l og N) .
A legrosszabb esetben a kiegyensúlyozatlan fák magassága h = O(N) lesz .
Véletlen adatok beszúrása és törlése általában viszonylag kiegyensúlyozott fákat eredményez.
Rendezett adatok beszúrása és törlése kiegyensúlyozatlan fához vezet .
A mindössze a gyermekcsomópontok relatív magasságát figyelembe vevő egyszerű technika alkalmas egy fa viszonylagos kiegyensúlyozartsági állapotának megítélésére.
Gyakorlatok
1. Írjuk meg a mi ni mum () rekurzív változatát!
2. Írjuk meg a search() rekurzív változatát!
3. Írjunk egy metódust, amely fogja a gyökércsomópontot, és rekurzív módon, rendezett formában kiírja a fában található valamennyi adatot!
4. Írjunk egy metódust, amely fogja a gyökércsomópontot, és iteratív módon, rendezett formában kiírja a fában található valamennyi adatot!
5. Írjunk egy metódust, amely fogja a gyökércsomópontot, és rekurzív módon, preorder formában kiírja a fában található valamennyi adatot!
299
Bináris keresőfák
6. Írjunk egy metódust, amely fogja a gyökércsomópontot, és rekurzív módon, postorder formában kiírja a fában található valamennyi adatot!
7. Írjunk egy metódust, vagy metódusokat, amelyek egy rendezett listából adatokat szúrnak be a bináris keresőfába úgy, hogy fenntartják az egyensúlyi állapotot, anélkül hogy explicit kiegyensúlyozásta volna szükség!
8. Adjunk metódus(ok)at a Node-hoz a méretének rekurzív módon történő kiszámításához!
9. Adjunk metódus(ok)at a Node-hoz a magasságának rekurzív módon történő kiszámításához!
300
TIZENEGYEDIK FEJEZET
Hasitás
A hasítás technikája biztosí�a az 0(1) sebességű adatfellapozást. Ez nem jelenti azt,
hogy csak egy összehasonlítás fog készülni, de az összehasonlítások száma az adat
halmaz méretétől függetlenül ugyanannyi marad. Hasonlítsuk ezt össze az o (N) ke
resési idejű egyszerű láncolt listákkal vagy az o(l og N) sebességű bináris keresőfák
kal. Ha ezt megtettük, akkor nagyon vonzónak fogjuk találni a hasítást.
A fejezet a következőket tárgyalja:
• A hasítás megértése.
• Munka a hasítófüggvényekkeL
• A teljesítmény megállapítása.
• Eredmények összehasonlítása.
A hasitás megértése
Lehet, hogy még nem ismertük fel, de hasítással minden alkalommal nagyon jók a kilá
tásaink a terveink megvalósítására. Amikor egy könyvesboltban az informatikai köny
vek osztálya felé sétálunk, szintén a hasítás algoritmusának egy formáját visszük vég
hez. Vagy amikor egy zenei CD-t keresünk egy bizonyos előadótól, nem kétséges,
hogy egyenesen a zenei részleg felé indulunk, és a vezetéknevének kezdőbetűjénél
kezdünk el keresni. Mindkét példában a keresési feltételeinknek megfelelően fedezhet
jük fel a hasítás tulajdonságait, legyen az egy könyvkategória vagy egy előadó neve. Ta
pasztaljuk, hogy a keresési időt nagyban csökkenti az algoritmus. Egy könyv esetében,
ha tudjuk, hogy informatikai könyvről van szó, akkor egyenesen a megfelelő osztály
felé megyünk, ha pedig a CD-ről van szó, akkor a kereséshez tudjuk az előadó nevét.
A hasítás hasítófüggvénnyel kezdődik. A hasító függvénynek van egy értéke, amely
lehet egy string, egy objektum, egy szám és így tovább, majd pedig kapunk egy hasító
értéket, amely általában egész vagy valamilyen egyéb numerikus érték. A kapott hasító
érték pedig általában meghatároz egy helyet a hasítótáblában, amely egy speciális tömb.
Nézzünk egy példát arra, hogyan is működik a hasítás művelete. A következők
ben először megmutatjuk, hogyan készül a hasítóérték szöveg részére, utána pedig a
hasítóértéket használjuk fel a szövegek eltárolásához és elhelyezéséhez.
Hasítás
Egy nagyon egyszerű szöveghasítási technika a betűk értékének összeadásából áll.
A hasítóérték eredményét azután helymeghatározóként használhatjuk a hasítótáblán
belül a szöveg eltárolásához. A következő példaprogram megmuta�a az "elvis", a
"madonna" és a "sting" szövegek hasításának eredményét, ha az ábécé betűinek érté
kéhez egész számokat rendelünk l-et az a-hoz, majd folytatólagosan 26-ot a z-hez:
e + l + v + i + 5 5 + 12 + 22 + 9 + 19 67
m + a + d + o + n + n + a 13 + l + 4 + 15 + 14 + 14 + l 62
5 + t + + n + g 19 + 20 + 9 + 14 + 7 69
A kapott értékek alapján látjuk, hogy az "elvis" szöveg eltárolható például a 67. hely
re a sorban, míg a "madonna" a 62.-re, "sting" pedig a 69.-re. Meg kell jegyeznünk,
hogy a szövegek nem tárolhatóak el egyéni sorrendben. A pozíciójuk véletlenszerű
nek tűnik, és éppen emiatt a hasítás néha a véletlenszám-generálásta emlékeztet. Ez
kissé különbözik az összes korábban megismert adatstruktúrától és algoritmustól,
amelyek bizonyosfajta műveleti rendben érik el a megfelelő teljesítményt.
A hasítófüggvény, úgy tűnik, kielégítően működik. Egyszerűen tárolhatunk érté
keket egyedi helyekre, és ugyanilyen egyszerűen ellenőrizhe�ük létezésüket. Akárho
gyan is, ebben a megközelítésben két fontosabb problémával kell szembenéznünk
Vessünk egy másik pillantást a generált értékekre. Ha ezeket a tömbpn belül in
dexpozícióban használjuk, akkor megfelelően nagy tömbre lesz szükségünk a 69.
pozíció hozzáillesztéséhez. Ha a 70 lehetségesből csak 3 pozíció van kitöltve, akkor
O és 69 között van még 67 üres hely is. Most képzeljük el, hogy az értékek 167, 162
és 169 voltak, ekkor 167 üres hely keletkezik. Ez nagyon leegyszerűsített hasítási
sémának tűnhet, amely eléggé hatástalan a tárolás függvényében.
Az egyik mód a probléma megoldására, ha a hasítófüggvényt úgy módosí�uk,
hogy az értékeket egy bizonyos tartományon belül állitsa elő. Az előző példát figye
lembe véve, ha a hasítótábla mérete kötött volt, és például tíz helyet· foglal, akkor a
hasítófüggvény módosítható az eredeti eredmény eléréséhez, és használjunk hozzá
maradékképzést - ami az osztás után marad -, hogy megtaláljuk a maradékot tízzel
osztva, ahogy a következő példában látható:
302
e + l + v + i + 5 5 + 12 + 22 + 9 + 19 67 % 10 7
m + a + d + o + n + n + a 13 + l + 4 + 15 + 14 + 14 + l 62 % 10 2
5 + t + + n + g 19 + 20 + 9 + 14 + 7 69 % 10 9
A hasítás megértése
A címek most már a O és 9 közötti tartományba esnek, és 10 helyet foglalva tárolhatóak a hasítótáblában. Így messze jobb eredményt érünk el.
Sajnálatos módon marad még egy problémánk a hasítófüggvénnyel: tartani kell az ütközések magas arányától, amelyek abból adódnak, hogy a különböző értékek hasítása ugyanarra a címre mutat. Bemutatjuk, mit is jelent az ütközés: a következő kódban a "lives" szöveget hasítjuk Tudomásul kell vennünk, hogy az eredmény ugyanarra a címre képződik, mint amit az "elvis" szövegnél generáltunk, ennek következtében ütközik a meglévő értékkel:
l + i + v + e + 5 5 + 12 + 22 + 9 + 19 67 % 10 7
Már láthattunk egy megoldást az ütközések számának csökkentésére: növeljük meg a címterületet. A címterület megnövelésével csökkentjük az ütközések valószínűségét, és ezzel egy időben növeljük az elvesztegetett memória mennyiségét is. A hasítóalgoritmusok többsége ennek következtében kompromisszumot jelent a területfoglalás hatékonysága és ideje között.
Az ütközések elkerülésének másik módja, ha pontosabban próbáljuk megválasztani a hasítótábla méretét. Ennek eredménye az, hogy a prímszámok jobb eredményt adnak, mint az összetett számok. A kívánt mérethez közeli prímszám választásával csökkentjük a csoportosítás nagyságát, következésképpen az ütközések számát is. Könyvünk éppen azzal foglalkozik, hogy ez hogyan működik.
Képzeljünk el egy ideális esetet, amikor egy tökéletes hasító algoritmussal rendelkezünk, amely teljes mértékben mentes az ütközésektőL Sajnálatos módon a tökéletes hasító algoritmust sokkal nehezebb megtalálni, mint az elsőre látszik. Amennyiben kevés és jól ismert bemenő adattal dolgozunk, nagyobb az esély rá, hogy megtaláljuk a tökéletest, de még egy nagyon jó hasító algoritmusnál is nagyon kicsi a megtalálás valószínűsége és az, hogy nem lesznek benne ütközések. A legjobb megoldás az, ha próbálunk valamilyen szabályozható dolgot találni, és azzal csökkentjük az ütközések számát.
A hasító függvény tárgyalása addig elég szegényes, amíg el nem érkezünk az ütközésekhez. Például a betűk sorrendje ugyancsak nem okoz különbséget. Ahogyan már láthattuk az "elvis" és a "lives" szövegeknél, mindkettő ugyanazt a címet kapta meg hasításkor. Tény, hogy az anagrammák-ugyanazokat a betűket, csak más sorrendben tartalmazó szavak - mindig ugyanazt az értéket kapják hasításnáL Amire szükségünk van, az nem más, mint egy algoritmus, amely valahogyan tekintettel van a betűk sorrendjére, ami nagyon jelentős a probléma megoldásában.
303
Hasítás
Íme egy példa, amely meglehetősen egyszerű, de azért hatékony hasító algoritmust tartalmaz a JDK String osztályában. Az algoritmus önmaga biztos matematikai alapokon nyugszik, de bizonyítása természetesen túlhaladja könyvünk kereteit. Mindazonáltal a tényleges megvalósítás nagyon könnyen értelmezhető.
A legtöbb jó hasító algoritmus, mint például amelyet a JDK String osztályában alkalmazunk, a matematikán alapul. Ilyen a CRC ( cyclic redundancy check) is, amely ciklikus redundanciavizsgálat algoritmus. Jó néhány alkalmazás használja a CRC-t fájlok tömörítésére és továbbítására hálózatokon az adatok sértedenségének megóvása érdekében. A CRC algoritmusa vesz egy adatfolyamot, és kiszámol hozzá egy egész hasító értéket. A CRC-számítás egyik tulajdonsága, hogy az adatok sorrendbe állitása alapvető követelmény. Ez annyit jelent, hogy az "elvis" és a "lives" szövegek hasítása majdhogynem garantáltan más eredményre fog vezetni. Azért tesszük hozzá, hogy majdhogynem, mivel az alap CRC nem tökéletes, és ezért még mindig nem nulla (de azért elég kicsi) az ütközés esélye.
Az alapödet az, hogy adjuk össze a számokat, ahogy korábban is csináltuk. Ez alkalommal azonban a működő értéket megszorozzuk 31-gyel, mielőtt újabb betűket adnánk hozzá. A következő egyenlet megmutatja, milyen bonyolult kiszámítani a hasító értéket az "elvis" szöveg részére:
(((e * 31 + l) * 31 + v) * 31 + i) * 31 + s
Alkalmazva ugyanezt az algoritmust az összes példabeli sztringen látha�uk az eredményt a következő kódban:
"elvis"
"madonna"
"sting"
lll i ves"
(((5 * 31 + 12) * 31 + 22) * 31 + 9) * 31 + 19 499653 7
(((((13 * 31 + l) * 31 + 4) * 31 + 15) * 31 + 14) *
* 31 + 14) * 31 + l
11570331842
(((19 * 31 + 20) * 31 + 9) * 31 + 14) * 31 + 7 18151809
(((12 * 31 + 9) * 31 + 22) * 31 + 5) * 31 + 19 11371687
Az értékek nagyon különbözőek a többitől, és az "elvis" és a "lives" többé nem ütközik. Összeadásnál meg kell jegyeznünk, hogy az értékek milyen magasak. Nyilvánvalóan nem kezdünk el egy hasítótáblát a következő tartalommal: 11,570,331,843, csak azért, hogy négy szöveget eltároljunk. Ahogyan korábban csináltuk, vesszük megint a maradékot a hasító értéket osztva a hasítótábla méretével (ez a maradékképzés), és ebben az esetben a 11-et fogjuk használni mint a 1 O-hez legközelebbi prímszámot, hogy generáljunk egy végleges címet, ahogy a következő kódban látható:
304
A hasítás megértése
"elvis" = 4996537 % ll 7
"madonna" = ll570331842 % ll 3
"sting" = 18151809 % ll 5
"lives" = ll371687 % ll 8
A hasítófüggvény feltűnően jobban dolgozik a példaadatokkaL Nincsenek többé ütkö
zések, így az értékek biztonságosan tárolhatóak a kívánt helyeken. Egyszer azonban fel
lesz töltve a hasítótábla, és ütközések következnek be. Akkor is előfordulhat ütközés,
mielőtt a hasítótábla teljesen feltöltődne, így ez az algoritmus szintén nem tökéletes.
A következő példában képzeljük el, hogy a "fred"
szöveget szeretnénk hozzáadni
a meglévő hasítótáblához. A hasító értéket ehhez a szöveghez így számoljuk ki: 196203
% ll = 7, ez azonban ütközik az "elvis"
szöveggel. Az első lehetőségünk, hogy egy
szerűen növeljük (vagy éppen csökkentjük) a hasítótábla méretét, és az új méret szerint
ismét kiszámítjuk az összes címet. A következő kód megmutatja az új hasító értékeket
a hasítótábla átrnéretezése után 17 -re az új "fred" szöveg el tárolásához:
"elvis" = 4996537 % 17 16
"madonna" = ll570331842 % 17 7
"sting" = 18151809 % 17 8
"lives" = ll371687 % 17 13
"fred" = 196203 % 17 6
Most már az összes szövegnek egyedi címe van, és tökéletesen eltárolhatók és visz
szakereshetők. Akárhogyan is, alaposan megfizettük az árat a címek egyedivé tételé
ért. Példánkban a hasítótábla méretét hattal megnöveltük, hogy még egy értéket el
tudjunk helyezni. A hasítótáblában most 17 hely van a szövegnek, de csak 5 van le
tárolva. A kihasználtság mértéke: (5/17) * 100 = 29%. Nem túl hatásos megoldás,
és a helyzet lehet ennél még rosszabb is. Ha további szövegeket adunk hozzá a
hasítótáblához, egyre nagyobb hasítótáblára van szükség, hogy megelőzzük az ütkö
zéseket, az eredménye viszont további elvesztegetett terület lesz.
305
Hasítás
Ahogy láthatjuk, az újraméretezés részben hatásos megoldás arra, hogy vissza
szontsuk az ütközések számát, mégis előfordulnak. Ennek következtében szüksé
günk lesz egyéb technikákra a probléma megoldásához.
Az egyik, lineáris vizsgálatnak (linear probing) nevezett módszer lehet a gyógyír az
ütközésfeloldásra. A lineáris vizsgálat nagyon egyszerű technika: a lényege az, hogy üt
közés esetén lineárisan megy tovább a következő rendelkezésre álló résre. A 11.1. áb
rán látható az a három lépés, amely szükséges a "fred" szöveg hozzáadásához.
o o o
2 2 2
3 madonna 3 madonna 3 madonna
4 4 4
5 sting 5 5
6 6 6
7 elvis 7 elvis 7 elvis
8 li ves 8 lives 8 lives
9 9 9 fred
10 10 10
11. 1. ábra. A lineáris vizsgálat (linear probing) megkeresi a'{/ a szabad rést, ahova a ,fred"
szijveget el tuqja tárolni
A keresés a 7. pozíciónál kezdődik, ez az eredeti hasítóérték, amely az ütközést
okozta. Ahogy megtalálja, hogy ez a pozíció már foglalt, a keresés a 8.-on folytató
dik, ha ez már szintén foglalt, végül a 9. helyen talál egy szabad rést.
Mi történik, ha a keresés eléri a tábla végét, és nem talál szabad rést? Ahelyett,
hogy feladná a keresést, visszaugrik, és a tábla elejéről újrakezdi a keresést. A 11.2.
ábrán látható a keresés menete, amikor a "tim" szöveget (9. hasítóérték) szetetnénk
hozzáadni, elfogadva, hogy a "mary" szöveget már hozzáadtuk (10. hasítóérték).
Az eredeti pozíció (ahogy a hasító értéket meghatároztuk) már ki volt töltve, aho
gyan a következő pozíció is. A tábla végének elérésekor a keresést az elejéről folytatja.
306
A hasítás megértése
o o o tim
2 2 2
3 madonna 3 madonna 3 madonna
4 4 4
5 sting 5 5 !
'
6 6 6
7 elvis 7 elvis 7 elvis
8 li ves 8 lives 8 lives
9 fred 9 fred 9 fred
10 mary 10 mary 10 mary
11.2. ábra. A "mary" szoveg hozzáadása "tim" előtt okozza, hogy a keresés visszaugrik
a tomb legelqére
Abban az esetben, ha talál egy rendelkezésre álló rést, azonnal el tudja helyezni a
szöveget. Ha viszont a tábla tele van - nincs több szabad rés -, újra kell méretezni,
de anúg erre nincs szükség, a lineáris keresés nagyon jól használható.
A lineáris vizsgálat egyszerűen végrehajtja a feladatát, és megfelelően dolgozik a
ritkásan kitöltött hasítótáblákon. A szabad rések száma a kitöltöttekkel összehason
lítva viszonylag nagy, de ahogy a sűrűség növekszik, a keresés gyorsasága és minősé
ge radikálisan csökken 0(1) -től O(N)-ig. Ebben az esetben nincs jobb, mint a letá
madásos megközelítés.
Egy másik megközelítésben az ütközések feloldásához indokolt használatba ven
nünk a vödröket, hogy több mint egy elemet tudjunk tárolni ugyanazon a pozíción.
Minden egyes vödör nulla vagy több azonos hasítóértékű elemet tartalmaz. A 11.3. áb
rán látható egy hasítótábla, amely 11 elemű, de 16 különböző szöveggel van feltöltve.
Láthatjuk, hogy a "lynn", "paula", "joshua" és "merle" szövegek mind azonos
hasítóértékűek (1), és ezután mindegyik ugyanabba a vödörbe kerül. A példából ki
tűnik, hogy a vödrök használata lehetőséget ad rá a hasítótáblának, hogy több elemet
tárolhasson, mint amennyit az egyszerű lineáris vizsgálat tud. Persze ennek a rugal
masságnak is meg kell fizetnünk az árát. Ahogy a hasítótábla megtelik, és a vödrök
mérete növekszik, az egy elem megtalálására fordított keresési idő is arányosan
megnő, de nem annyira feltűnően, mint a lineáris vizsgálatnál. Ennek ellenére egy
307
Hasítás
bizonyos ponton úgy érezzük, hogy az ár, amit a keresésekért a vödrökkel fizetünk, kezd túl magas lenni. Ha ez megtörténik, a megoldás az, hogy növeljük meg a rendelkezésre álló vödrök számát. Egy kiváló hasító függvénynél az elemek viszonylag egyenletesen újra megoszlanak a nagyobb mennyiségű vödrök között, elérve ezzel, hogy minden egyes vödör mérete csökkenjen. A kihívás az, hogy megtaláljuk, mikor van szükség újraméretezésre.
o
lyn n
2 talia
3 andrew
4 sara
5 kerri
6 roger
7 elvi3
8
9 tim
10 mary
aaron
sam
11.3. ábra. Minden egyes viJdiir azonos hasítóértékű elemeket tartalmaz
Az egyik megoldás annak meghatározására, hogy mikor érdemes újraméretezni a hasítótáblát, a terhelési tényező figyelése. A terhelési tényező a tárolt értékek és a rendelkezésre álló vödrök aránya .. Az alapötlet az, hogy állítsunk be egy terhelési tényező küszöbértéket, és amikor ezt elértük, akkor végezzük el az újraméretezést. Ahogy a 11.3.
ábrán látható, a vödrök száma 11, a tárolt értékeké pedig 16. Így a töltöttség jelenleg 16 j ll = l. 45 vagy 145%-a a fogalmi kapacitásnak (az eredmény pedig az, hogy néhány vödör kezd túl nagy méretú lenni): ez jó alkalom az újraméretezésre.
A feladatunk megint az, hogy megtaláljuk az egyensúlyt a tárterület és a teljesítményhatékonyság között. A terhelési tényező túl alacsonyra állításának eredménye a sok elvesztegetett tárhely. Ha túl magas, akkor a vödrök mérete növekszik, ennek pedig több ütközés lesz a következménye. Ha a terhelési tényező a kapacitás 75%-a körül alakul, viszonylag jó kompromisszumot érünk el a tárhely és az idő között.
308
Munka a hasítással
Munka a hasitással
A következő gyakorlati részben a tárgyalt fogalmakat futtatható kóddá formáljuk. Először is definiálunk néhány egységtesztet, amelyeket két hasítótábla-implementáció követ - lineáris vizsgálattal és vödrökkel -, végezetül pedig néhány egyéb teszt, amelyek összehasonlítják az eljárások relatív teljesítménykülönbségeit.
Az összes ilyen tesztben szövegeket fogunk letárolni, így szükségünk lesz a szövegeken dolgozó hasítófüggvényre. Örömteli, hogy a Java-nyelv szintén definiálta metódusban a hasítás metódusát- hashcode() -, amelyet minden osztály tud eszközként használni erre. Ráadásul a hashcode() JDK-megvalósítása a String osztályban megközelítőleg követi a fejezetben korábban tárgyaltakat, ami annyit jelent, hogy nem kell saját magunknak elkészítenünk Azt azonban továbbra is meg kell fontolnunk, hogyan akarjuk megvalósítani a dolgot, így hát íme egy példa, hogyan néz ki egy hashcode() megvalósítása:
public int int hash for (i nt
has h }
hashcode() { = O ;
i = O; i <length(); ++i) { 31* hash + charAt(i);
return hash; }
A kód a hasítás nullára inicializálásával kezdődik. Ezután összead karakterenként, minden alkalommal megszorozva az eredményt 31-gyel.
Próbáljunk létrehozni egy általános hasítótábla-felületet
Ahhoz, hogy kipróbáljuk a lineáris vizsgálat és a vödrök módszerét, hozzunk létre egy felületet, amely definiálja mindkét megoldás közös metódusait.
package com.wrox.algorithms.hashing;
public interface Hashtable { public void add(Object value);
}
public boolean contains(object value); public int size();
309
Hasítás
A megvalósitás müködése
A Hashtable interfész három metódust definiál: add(), contains() és size(). Mind
ezekre később szükségünk lesz a megvalósításhoz, amikor létrehozunk egy lineáris
vizsgálattal dolgozó vagy vödrös verziót. Ezek a metódusok ismerősek lehetnek, mivel
nagyon hasonlóak azokhoz, amelyek a l i sta interfészben definiálódtak. Az egyik kü
lönbség ott mutatkozik, hogy míg a listákat lehet duplikálni, a hasítótáblákat nem, így
ha az add() függvényt többször hívjuk meg ugyanazzal az értékkel, érdemben semmi
sem fog történni.
Gyakorlófeladat: tesztek létrehozása
Mielőtt kidolgoznánk az aktuális hasítótábla implementációját, elsőként is írnunk
kell pár tesztesetet, hogy megbizonyosodjunk róla, helyesen fog-e működni a meg
írandó kódunk. Kihasználva azt a tényt, hogy a felhasználó szempontjából bármely
hasítótábla ugyanazt az eredményt produkálja, tudunk készíteni egy generikus teszt
csomagot, amelyet bővíthetünk és újra felhasználhatunk; a következő módon:
310
package com.wrox.algorithms.hashing;
import junit.framework.Testcase;
public abstract class AbstractHashtableTestcase extends Testcase { private static final int TEST_SIZE = 1000;
private Hashtable _hashtable;
protected abstract Hashtable createTable(int capacity);
protected void setUp() throws Exception { super. setup();
}
_hashtable createTable(TEST_SIZE);
for (int i 0; i < TEST_SIZE; ++i) { _hashtable.add(String.valueof(i));
}
public void testcontains() {
}
for (int i = 0; i < TEST_SIZE; ++i) { assert:True(_hashtable.contains(string.valueof(i)));
}
Munka a hasítással
pulirfc voi d testooesntcontai n O
}
for (int i = 0; i < TEST_SIZE; ++i) { assertFalse(_hashtable.contains(String.valueOf(i +
TEST_SIZE)));
}
public void testAddingThesamevaluesDoesntChangeThesize() { assertEquals(TEST_SIZE, _hashtable.size());
}
for (int i = 0; i < TEST_SIZE; ++i) { _hashtable.add(String.valueof(i)); assertEquals(TEST_SIZE, _hashtable.size());
}
}�-------------------------------------
A megvalósitás müködése
Az AbstractHashtab leTestcase osztály definiál egy változó t a tesztelni kívánt hasító
tábla példány részére, amelyet a setUp() metódus inicializál, a createTable absztrakt
metódus meghívásával. Ahogy később majd tapasztaljuk, a createTable() metódus
megvalósítása alosztályok által történik, hogy egy Hashtable példányt hozzon létre.
Nézzük meg, hogyan ad a setup() metódus adatokat a hasítótáblának. Ha egész típu
sú változókat használtunk számokkal (0,1,2 és így tovább), mindegyik a saját pozíciójá
ra akarna hasítani az alapul szolgáló táblában, ezáltal próbálva kizárni az ütközés lehe
tőségét (ami alapvetően a cél lenne, de nem igazán tükrözi a valóságot). Ehelyett a
számokat átkonvertáljuk szöveges típusúvá az összetett hasítás alkalmazásához, ahogy
a St ri n g osztályban a hashcode () metódus definiálta.
public abstract class AbstractHashtableTestcase extends Testcase { private static final int TEST_SIZE = 1000;
}
private Hashtable _hashtable;
protected abstract Hashtable createTable(int capacity);
protected void setup() throws Exception { super. setUp();
}
_hashtable = createTable(TEST_SIZE);
for (int i = 0; i < TEST_SIZE; ++i) { _hashtable.add(string.valueof(i));
}
311
Hasítás
Miután hozzáadtunk valahány szöveget a hasítótáblához a setUp()-ban, az első dol
gunk ellenőrizni, hogy a contai ns újra megtalálja-e őket Az értékek eléggé haszonta
lanok lehetnek, ha úgy tároljuk le őket, hogy utána nem tudjuk megtalálni.
public void testcontains() {
}
for (int i = 0; i < TEST_SIZE; ++i) {
assertTrue(_hashtable.contains(String.valueof(i)));
}
A következő teszt ellenőrzi, hogy nem lehet-e tévedésből olyan értékeket találni, me
lyekről tudjuk, hogy nem léteznek:
public void testDoesntContain() {
}
for (int i = 0; i < TEST_SIZE; ++i) {
assertFalse(_hashtable.contains(String.valueOf(i + TEST_SIZE)));
}
Végezetül meggyőződünk róla, hogy ha több mint egyszer hozzáadtuk ugyanazokat
az értékeket a hasítótáblához, akkor ez nem vezetett a hasítótábla méretének növe
kedéséhez:
public void testAddingThesamevaluesDoesntchangeThesize() {
assertEquals(TEST_SIZE, _hashtable.size());
}
for (int i = 0; i < TEST_SIZE; ++i) {
_hashtable.add(String.valueof(i));
assertEquals(TEST_SIZE, _hashtable.size());
}
Láthattuk, hogy a méret állandóságát a duplikált értékek hozzáadása előtt és után is
letesztel tük.
Lineáris vizsgálat
A következő gyakorlófeladatban létrehozunk egy hasítótáblát, amely lineáris vizsgá
latot valósít meg. A lineáris vizsgálat szépsége abban rejlik, hogy a megvalósítása na
gyon egyszerű, ennek következtében könnyű a megértése is.
312
Munka a hasítással
Gyakorlófeladat: hasítótábla tesztelése és megvalósítása lineáris vizsgálattal
Először készítsünk egy tesztosztályt:
package com.wrox.algorithms.hashing;
public class LinearProbingHashtableTest extends AbstractHashtableTestcase {
protected Hashtable createTable(int capacity) { return new LinearProbingHashtable(capacity);
} �l
Következzen a hasítótábla implementációja:
package com. wrox .al go ri tt'ims. has ni ng;
public class LinearProbingHashtable implements Hashtable { private object[] _values;
private int _size;
public LinearProbingHashtable(int initialcapacity) { assert initialcapacity > O : "az 'initialcapacity' nem
lehet < l";
_values = new object[initialcapacity]; }
public void add(Object value) { ensurecapacityForoneMOre();
}
int index = indexFor(value);
if (_values[index] == null) { _values[index] = value; ++_size;
}
public boolean contains(Object value) { return indexof(value) != -1;
}
public int size() { return _size;
1 - -----
313
Hasítás
314
private int inaexFor(Object value) { int start= startingindexFor(value);
}
int index= indexFor(value, start, _values.length); if (index == - 1) {
}
index= indexFor(value, O, start); assert index == -1 : "no free slots";
return index;
private int indexFor(object value, int start, int end) { assert value != null : "az érték nem lehet NULL";
for (int i = start; i < end; ++i) { if (_values[i] null l l value.equals(_values[i])) {
return i;
} }
return -1;
}
private int indexof(Object value) { int start startingindexFor(value);
}
int index= indexof(value, start, _values.length); if (index == -1) {
index= indexof(value, O, start);
}
return index;
private int indexof(Object value, int start, int end) { assert value != null : "az érték nem lehet NULL";
}
for (int i = start; i < end; ++i) { if (value.equals(_values[i])) {
return i;
} }
return -1;
private int startingindexFor(Object value) {
}
assert value != null : "az érték nem lehet NULL";
return Math.abs(value.hashcode()% _values.length);
Munka a hasítással
}
private void ensúrecapacítyForoneMore() { if (size()== _values.length) {
resíze(); }
}
private void resize() { LínearProbingHashtable copy =
}
new LínearProbíngHashtable(_values.length * 2);
for (int i =O; í < _values.length; ++i) { if (_values[i] != null) {
copy.add(_values[i]); }
}
_values = copy._values;
A megvalósitás müködése
Az összes tesztesetet definiáltuk az AbstractHashtableTestcase-ben, így már csak
annyi a teendőnk, hogy ezt kiterjesszük, és megvalósítsuk a createTable() metó
dust, hogy az visszaadhassa a deftniálandó L i nearProbi ngHashtabl e egy példányát.
Ha ezt az osztályt létrehoztuk, és az összes teszteset az alaposztályban van, lineáris
vizsgálattal lefutta�uk a hasítótáblán.
package com.wrox.algorithms.hashíng;
public class LinearProbingHashtableTest extends AbstractHashtableTestcase {
}
protected Hashtable createTable(int capacity) { return new LinearProbingHashtable(capacity);
}
A megvalósítás kódjához a lineáris vizsgálat nagyon egyszerű módszer, ahogy az
osztálydefinícióból is látszik.
A L i nea r P robi ngHashtab l e osztály rendelkezik egy tömbbel, amelyben az érté
keket tárolja, és az egyeden konstruktorban beállíthatjuk a kezdetben tárolható ma
ximumértékek számát, magyarul a kapacitást:
package com.wrox.algorithms.hashing;
public class LinearProbingHashtable implements Hashtable { private object[] _values;
315
Hasítás
}
public LinearProbingHashtable(int initialcapacity) {
}
assert initialcapacity >O : "initialCapacity can't be < l"; _values = new object[initialcapacity];
A kapacitásról beszélve szót kell ejtenünk arról, hogy szükségünk lesz a hasítótábla akár többszöri újraméretezésére, hogy több értéket tároihassunk Ennek végrehajtásához létezik az ensurecapacityForoneMore() metódus, amely biztosítja, hogy a hasítótábla legalább eggyel több értéket tudjon tárolni. Ha ez nem lehetséges, akkor újra kell méretezni:
private void ensurecapacityForoneMore() { if (size()== _values.length) {
resize O;
} }
A res i ze() metódus ügyes és hatásos technikát alkalmaz a rendelkezésre álló rések számának növelésére. ·A kapacitás megduplázására ideiglenes táblát hozunk létre. Ebbe belekerül az összes érték, és az új tábla tömbjével kicseréljük a létező tömböt:
private void resize() { LinearProbingHashtable copy =
}
new L i nearProbi ngHashtab l e (_va l u es. l eng th * 2) ;
for (int i =O; i < _values.length; ++i) { if (_values[i] l= null) {
copy.add(_values[i]);
} }
_values copy._values;
A start i ngrndexFor() metódus központi szerepet játszik a hasítótáblában. A metódus kap egy értéket, és visszaadja azt a tömbindexet, ahol őt tárolni kell. Azt a hasítókódot használja, amelyet az érték definiál önmagának- minden objektum Javában defmiál egy hashcode() metódust -,ezután pedig veszi az osztás utáni maradékot a tábla kapacitása alapján. Ez biztosí�a azt, hogy a kapott indexérték a megfelelő határok közé essen:
316
private int startingrndexFÓr(Object value) {
}
assert value != null : "az érték nem lehet NULL"; return Math.abs(value.hashcode()% _values.length);
Munka a hasítással
A két indexForO metódus dolgozik együtt azon, hogy találjanak egy szabad helyet
az új érték elhelyezéséhez.
Az első metódus a természetes kezdeti ponttól kezd el keresni, amíg a tömb vé
gére nem ér. Ha nem talál üres helyet, a további keresés a tömb elejéről indul újra az
incializált kezdőpontig:
private int indexFor(Object value) { int start= startingindexFor(value);
}
int index= indexFor(value, start, _values.length); if (index == -1) {
}
index= indexFor(value, O, start); assert index == -1 : "no free slots";
return index;
A második metódus az első metódusban meghatározott határokon belül keres. Néz
zük meg közelebbről az aktuális tesztet. Vegyük észre, hogy egy helyet nemcsak ak
kor választunk ki, ha üres (_values[i]==null), hanem akkor is, ha már tartalmazza
az adott értéket (value.equals(_values[i])). Kevés értelme van annak, hogy
ugyanaz az érték kétszer kerüljön letárolásra, mivel a második előfordulást valószí
nűleg sosem találjuk meg:
private int indexFor(Object value, int start, int end) { assert value != null : ''az érték nem lehet NULL";
for (int i = start; i < end; ++i) { if (_values[i] == null l l value.equals(_values[i])) {
return i; }
}
return -1;
}
Az add O metódus megvalósítása nagyon egyszerű: először biztosítjuk, hogy legyen
hely egy következő érték letárolására, mielőtt az adott értéket tároljuk
public void add(Object value) { ensureCapacityForoneMore(); _values[indexFor(value)] = value;
}
A két i ndexofO metódus együtt dolgozik a két indexFor O metódussal, hogy talál
janak egy szabad helyet az új érték elhelyezéséhez.
317
Hasítás
Az első metódus koordinálja a keresést, a startindexForO metódus által ki
számolt pozícióval kezdve, és ha szükséges, akkor másik keresést is indít a tömb alsó
részén. Ha illeszkedő értéket találunk, akkor a pozícióját visszakapjuk, egyéb esetben
pedig a -1 érték jelöli, hogy az érték nem létezik:
private int indexof(Object value) { int start startingindexFor(value);
int index indexof(value, start, _values.length); if (index == - 1) {
index = indexof(value, O, start);
} return index;
}
A második metódus letámadásos keresés módszerével nézi át a tömböt - egy meg
adott kezdő és vége pozícióval - az értéket keresve:
private int indexof(Object value, int start, int end) { assert value ! = null : "az érték nem lehet NULL";
}
for (int i = start; i < end; ++i) { if (value.equals(_values[i])) {
return i;
} } return -1;
Ha az érték indexét megtaláltuk, akkor a contai ns () metódus csak egy sorból áll:
public boolean contains(object value) { return indexof(value) ! = -1;
}
A Hashtable interfészhez már csak size() metódusra van szükségünk, amely egy
szerűen iterál végig a tömbön egységnyivel megnövelve a méretet, arnint egy értéket
talál. (Feladatként próbáljuk ki a méret követését számolás helyett.)
318
public int size() { int size = O;
}
for (int i = O ; i < _values.length; ++i) { if (_values[i] != null) {
++size;
} } return size;
Munka a hasítással
Vödrös módszer
A következő feladatban létrehozunk egy hasítótáblát, amely vödröket alkalmaz az
értékek tárolására. Ahogyan eddig is, kezdjünk megint egy teszttel, mielőtt áttérnénk
a megvalósításra.
Gyakorlófeladat: hasítóhalmaz megvalósítása és tesztelése vödrös módszerrel
Kezdésként készítsünk egy tesztosztályt:
packagé com. wroul gori t:hms. has ti ing;
public class sucketingHashtableTest extends Abstr�ctHashtableTestcase {
}
protected Hashtable createTable(int capacity) { return new BucketingHashtable(capacity, 0.75f);
}
Most adjuk hozzá a megvalósításosztályt:
package -'com:Wrox. al g(ú;ithiií5.1ía'Sfiing;
import com.wrox.algorithms.iteration.rterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
public class BucketingHashtable implements Hashtable { private final float _loadFactor; private List[] _buckets; private int _size;
public BucketingHashtable(int initialcapacity, float loadFactor){ assert initialcapacity > O : "az 'initialcapacity' nem
lehet < l";
assert loadFactor > O : "a 'loadFactor' nem lehet <= O";
_loadFactor = loadFactor; _buckets = new List[initialcapacity];
}
public void add(object value) { List bucket = bucketFor(value);
if (!bucket.contains(value)) { bucket.add(value); ++_size; mai ntai n Load();
}
1-..-.----------=---
--
319
Hasítás
320
public boolean contains(object value) {
}
List bucket = _buckets[bucketindexFor(value)]; return bucket != null && bucket.contains(value);
public int size() { return _size;
}
private List bucketFor(object value) {
}
int bucketindex = bucketindexFor(value);
List bucket = _buckets[bucketindex]; if (bucket == null) {
bucket = new LinkedList(); _buckets[bucketindex] = bucket;
}
return bucket;
private int bucketindexFor(Object value) {
}
assert value != null : "az érték nem lehet NULL"; return Math.abs(value.hashcode()% _buckets.length);
private void maintainLoad() { if (loadFactorExceeded()) {
resize O;
} }
private boolean loadFactorExceeded() { return size()> _buckets.length * _loadFactor;
}
private void resize() { BucketingHashtable copy =
}
new BucketingHashtable(_buckets.length * 2, _loadFactor);
for (int i =O; i < _buckets.length; ++i) { if (_buckets[i] != null) {
copy.addAll(_buckets[i].iterator()); }
}
_buckets = copy._buckets;
Munka a hasítással
l
prfvatevoiCf addAll(fterator values)-{
}
assert values l= null : "az érték nem lehet null";
for (values.first(); !values.isoone(); values.next()) { add(values.current());
}
A megvalósitás müködése
Ismét az AbstractHashtabl eTestease-ben defmiált tesztünket használjuk, de most
a createTable() metódust alkalmazzuk, hogy a BucketingHashtable egy példányát
kapjuk vissza .. Bevezetünk egy extra konstruktorparamétert: O. 75f. Ez a terhelési
tényező, amely ebben az esetben megadja, hogy a hasítótábla méretének nőnie kell,
amikor a tárolt értékek száma eléri a rendelkezésre álló vödrök számának 75%-át:
package com.wrox.algorithms.hashing;
public class BucketingHashtableTest extends AbstractHashtableTestcase {
}
protected Hashtable createTable(int capacity) { return new �ucketingHashtable(capacity, 0.75f);
}
A vödrös módszer valamivel bonyolultabb, mint a lineáris vizsgálat, igy a megvalósi
tási osztályt egy kissé bővebben kell értelmezni.
A Bucketi ngHashtabl e osztály rögzíti a terhelési tényezőt a későbbi használat
hoz és a vödrök tömbjét is. A hasitás megértése cimű résznél már tárgyaltuk, hogy a
vödrök láncolt listaként látszanak és működnek, ezt használjuk ki a vödrös mód
szernél. A vödrök számának méretét (initialcapacity) konstrukciós időben adjuk
meg a kivánt terhelési tényezővel együtt:
package com.wrox.algorithms.hashing;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.Link edList; import com.wrox.algorithms.lists.List;
public class BucketingHashtable implements Hashtable { private final float _loadFactor; private List[] _buckets;
public BucketingHashtable(int initialcapacity, float loadFactor){ assert initialcapacity > O : "az 'initialcapacity' nem
lehet <l"; assert loadFactor > O : "a 'loadFactor' nem lehet <= O";
321
Hasítás
_loadFactor = loadFactor; _buckets = new List[initialCapacity];
}
}
A mai n ta i n Load() metódus egyszerűen ellenőrzi az aktuális terhelést. Ha elértük a ki
tűzött terhelést, akkor újramérerezés válik szükségessé nagyobb vödörszámra szétte
rítve az értékeket. A res i ze() metódus azonos elven múködik, mint az ugyanilyen
nevű a L i nea r P robi ngHashtabl e-ben: új hasítótáblát hozunk létre, ahová hozzáadjuk
az összes értéket, ezután pedig az új vödörtömböt használjuk a meglévő felülírására.
A tárolókapacitás minden újraméretezéskor megduplázódik Bármilyen értéket hasz
nálhatunk erre a célra, de mindig kompromisszumokra kényszerülünk a tárhely és az
idő között. Kisebb fokú méretnövelés esetén gyakrabban lesz szükségünk újramérete
zésre, ha pedig nagyobb mértékben növeljük meg, nagyobb lesz a felesleges tárhely.
322
private void maintainLoad() { if (loadFactorExceeded()) {
resize();
} }
private boolean loadFactorExceeded() { return size()> _buckets.length * _loadFactor;
}
private void resize() { BucketingHashtable copy
}
new BucketingHashtable(_buckets.length * 2, _loadFactor);
for (int i =O; i < _buckets.length; ++i) { if (_buckets[i] != null) {
copy.addAll(_buckets[i].iterator()):
} }
_buckets copy._buckets;
private void addAll(Iterator iterator) {
}
assert iterater != null : "az 'iterator' nem lehet null";
for (iterator.first(); !iterator.isoone(); iterator.next()) { add(iterator.current());
}
Munka a hasítással
A bucketrndexFor() metódus határozza meg, hogy egy adott érték melyik vödörbe kerüljön. Ahogyan eddig is, a L i nearProbi ngHashtab l e-nek a hashcode () metódusát hívjuk meg, és vesszük a vödrök számával való osztás utáni maradékot. Ez biztosítja, hogy megkapjuk azt az indexértéket, amely a megfelelő tartományba esik:
private int bucketindexFor(Object value) {
}
assert value != null : "az érték nem lehet NULL"; return Math.abs(value.hashcode()% _buckets.length);
A bucketForO metódus adja meg a pontos vödröt az adott érték számára. Alapvetően csak egy tömbbeli keresést szeretnénk végezni, de a bucketForO metódus azt is garantálja, hogy ha nem talál létező vödröt a megfelelő helyen, akkor készít egyet:
private List bucketFor(ob]ect value) {
}
int bucketindex = bucketrndexFor(value);
List bucket = _buckets[bucketrndex]; if (bucket == null) {
}
bucket = new LinkedList(); _buckets[bucketrndex] = bucket;
return bucket;
Az add O metódus éri el a megfelelő vödröt, és csak akkor adja hozzá az adott értéket, ha az még nem létezik. Újra átgondolva ez az jelenti, hogy ha két azonos értékünk van- melyekre equals true (igaz) értékkel tér vissza-, ezek nem létezhetnek a hasítótáblában egy időben:
public void add(Object value) { List bucket = bucketFor(value);
}
if (!bucket.contains(value)) { bucket.add(value); mai ntai n Load();
}
A con ta i ns O metódus szintén nagyon egyszerű. Először is találjunk egy megfelelő vödröt, majd ha a visszatérő érték true (igaz), a vödör létezik, és már tartalmazza a megadott értéket:
public boolean contains(Object value) {
}
List bucket = _buckets[bucketrndexFor(value)]; return bucket != null && bucket.contains(value);
323
Hasítás
Végül a size() metódus adja össze az értékek számát minden egyes vödörben, hogy
kiszámolhassa a teljes méretet:
public int size() { int size = O;
}
for (int i =O; i < _buckets.length; ++i) { if (_buckets[i] != null) {
size += _buckets[i].size(); }
} return size;
A teljesitmény megállapitása
Most, hogy már van két hasítótábla-implementációnk, itt az ideje megvizsgálnunk,
hogyan múködnek, nemcsak önmagukban, hanem egymással összehasonlítva is. Az
egyes teljesítmények megvizsgálásához a következő gyakorlati részben olyan teszte
ket fejlesztünk ki, amelyek használják az add() és a contains() metódusokat, hogy
láthas suk, hány alkalommal kerül meghívásra az equals O a tárolt értékeken: minél
kisebb ez a szám, annál hatékonyabb az algoritmus:
Gyakorlófeladat: a tesztek létrehozása
A tesztosztály létrehozásának menete:
324
package com.wrox.algorithms.hashing;
import junit.framework.Testcase;
public class HashtablecallcountingTest extends Testcase { private static final int TEST_SIZE = 1000;
private static final int INITIAL_CAPACITY .= 17;
private int _counter; private Hashtable _hashtable;
public void testLinearProbingWithResizing() {
}
_hashtable = new LinearProbingHashtable(INITIAL_CAPACITY); runAll O;
public void testLinearProbingNoResizing() { _hashtable = new LinearProbingHashtable(TEST_SIZE); runAll O;
}
A teljesítmény megállapítása
'� -�pubi1c 'Voidtestiii.icketsLoadFactorlOOPe rcent 0-{
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, l.Of); runAll O ;
public void testBucketsLoadFactor75Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 0.75f); runAll();
public void testBucketsSOPercent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, O.SOf); runAll();
public void testBuckets25Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 0.2Sf); runAll();
public void testBucketslSOPercent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, l.SOf); runAll();
public void testBuckets200Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 2.0f); runAll();
private void runAll() { runAdd O ; runconta i ns O ;
}
private void runAdd() { _counter = O;
}
for (int i = 0; i < TEST_SIZE; ++i) { _hashtable.add(new value(i));
} reportcalls("add");
private void runcontains() { _counter = O;
l
for (int i = 0; i < TEST_SIZE; ++i) { _hashtable.contains(new value(i));
} reportcalls("contains");
325
Hasítás
}
private void reportcalls(string metnoa) { System.out.println(getName() + "(" + metódus + "): " +
_counter + " calls");
}
private final class value { private final String _value;
}
public value(int value) { _value = String.valueof(Math.random() * TEST_SIZE);
}
public int hashcode() { return _value.hashCode();
}
public boolean equals(Object object) { ++_counter; return object != null &&
_value.equals(((Value) object)._value);
}
A megvalósitás müködése
A HashtablecallcountingTest kiterjeszti a Testcase metódust, nagyon egyszerűvé
téve a futását. Az osztály tartalmazza a hasítótábla egy példányát a jelenlegi teszthez,
valamint egy számláló t, amely az e qua ls() metódus meghívásának számát adja:
package com.wrox.algorithms.hashing;
import junit.framework.Testcase;
public class HashtablecallcountingTest extends Testcase { private static final int TEST_SIZE = 1000; private static final int INITIAL_CAPACITY = 17;
private int _counter; private Hashtable _hashtable;
}
A va l u e belső osztály ad lehetőséget az e qua ls() meghívásának észlelésére és szám
lálására. Ha sima szöveges változókat tárolunk el, akkor nincs mód megtudni, hány
szor került meghívásra az equals() metódus. Továbbá a String osztály final-ként
van megjelölve, így hát nincs mód a bővítésre és az e qua ls() metódus direkt felül
definiálására.
326
A teljesítmény megállapítása
De létrehozhatunk egy saját osztályt, amely tartalmazza a szöveget, és eggyel
növeli a _counter számlálóértéket minden esetben, amikor az e qua ls() meghívásra
kerül. Vegyük észre, hogy a konstruktor véletlenszerűen generálja az értékeket annak
érdekében, hogy a rendezett adatok beszúrása ne adjon hamis eredményt:
private final class Value { private final String _value;
}
public value() { _value = String.valueof(Math.random() * TEST_SIZE);
}
public int hashcode() {' return _value.hashcode();
}
public boolean equals(Object object) { ++_counter; return object != null &&
_value.equals(((Value) object)._value);
}
A reportcalls() metódus lehetővé teszi az equals() metódus futásának számlálását
a következő formában: a test-name(method): #### call s (ahol a method az "add"
vagy a "contai ns" attól fuggően, hogy éppen melyiket vizsgáljuk egy adott időben):
private void reportcalls(String method) { System. out. pr i nt l n ( getName () + "(" + method + "): " +
_counter + " calls");
}
A runAdd () és a runcontai ns () metódusok alaphelyzetbe állitják a számláló t, mie
lőtt az add(), illetve a contains() metódusok TEST_SIZE darab iterációját futtatjuk,
majd a végén jelentést készít az eredményekről:
private void runAdd() { _counter = O;
}
for (int i = 0; i < TEST_SIZE; ++i) { _hashtable.add(new value());
} reportcalls("add");
private void runcontains() { _counter = O;
}
for (int i = 0; i < TEST_SIZE; ++Í) { _hashtable.contains(new Value());
} reportcalls("contains");
327
Hasítás
A r unA ll O metódus kényelmes megoldás a tesztesetek futtatására mindkét esetben
egy hívással:
private void runAll() { runAdd O; runcontainsO;
}
Most megnézhe�ük a tényleges teszteseteket A tesztesetek első halmaza a lineáris vizs
gálathoz készült. Nincs túl sok kipróbálási konfigurációs változat, ténylegesen csak ket
tó, mivel a L i nearProbi ngHashtab l e-hez az egyetlen beállitható paraméter a kezdeti
kapacitás: az első készít egy hasítótáblát az adathalmaz méreténél kisebb kezdeti kapaci
tással, remélhetőleg jó néhány újraméretezési helyzetet eredményezve. A második teszt
kapacitása pontosan a megfelelő, hogy egyáltalán ne legyen szükség újraméretezésre:
public void testLinearProbingwithResizing() {
}
_hashtable = new LinearProbingHashtable(INITIAL_CAPACITY);
runAll O;
public void testLinearProbingNoResizing() {
_hashtable = new LinearProbingHashtable(TEST_SIZE); runAll O;
}
A következő teszthalmazpélda a vödrös változathoz készült. Ez nemcsak a relatív
teljesítménykülönbséget demonstrálja a lineáris vizsgálattal szemben, arról is infor
mációt nyújt, hogy mennyi minden függhet a kezdeti konfigurációtóL Minden eset
ben készít egy hasítótáblát olyan kezdeti kapacitással, amely elég kicsi ahhoz, hogy
garantálja a későbbi újraméretezés szükségességét. A különbség a kettő között az,
hogy mikor kerül sor újraméretezésre. Figyeljük meg, hogy a terhelési tényező mind
egyik teszthez különbözó:
328
public void testBucketsLoadFactorlOOPercent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, l.Of); runAll 0;
public void testBucketsLoadFactor75Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 0.75f); runAllO;
public void testBuckets50Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 0.50f); runAllO;
A teljesítmény megállapítása
public void testBuckets25Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 0.25f); runAll();
public void testBuckets150Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 1.50f); runAll();
public void testBuckets200Percent() {
}
_hashtable = new BucketingHashtable(INITIAL_CAPACITY, 2.0f); runAll();
A teljesítmény-összehasonlítás futtatása adhat információkat a kimenetról a követ
kezókhöz hasonlóan. Tartsuk észben, hogy a tényleges eredmények a véletlen módú
tesztek következtében kissé különbözni fognak.
testLinearProbingwithResizing(add): 14704 hívás testLinearProbingWithResizing(contains): 1088000 hívás testLinearProbingNoResizing(add): 18500 hívás testLinearProbingNoResizing(contains): 1000000 hívás testBucketsLoadFactorlOOPercent(add): 987 hívás testBucketsLoadFactorlOOPercent(contains): 869 hívás testBucketsLoadFactor75Percent(add): 832 hívás testBucketsLoadFactor75Percent(contains): 433 hívás testBuckets50Percent(add): 521 hívás testBuckets50Percent(contains): 430 hívás testBuckets25Percent(add): 262 hívás testBuckets25Percent(contains): 224 hívás testBuckets150Percent(add): 1689 hívás testBuckets150Percent(contains): 903 hívás testBuckets200Percent(add): 1813 hívás testBuckets200Percent(contains): 1815 hívás
Ebben a formában kissé nehéz értelmezni a számadatokat, ezért az eredményeket a
11.1. táblázatban összegeztük
Konft ... ádó add contains összesen Átlat
Lineáris vizsgálat - 14 704 1 088 OOO 1 102 704 551,35
újraméretezéssel
Lineáris vizsgálat - 18 500 1 OOO OOO 1 018 500 509,25
újraméretezés nélkül
Vödrök- 100% 987 869 1 856 0,93
telítettséggel
329
Hasítás
l Konfiguráció add contains Összesen Átlag
Vödrök-75% 832 433 1 265 0,63 telítettséggel
Vödrök- 50% 521 430 951 0,48 telítettséggel
Vödrök-25% 262 224 486 0,24 telítettséggel
Vödrök-150% 1 689 903 2 592 1,30 telítettséggel
Vödrök-200% 1 813 1 815 3 628 1,81 teli tettséggel
11. 1. táblázat. Az equals O meghívása 10000 iterációval minden egyes add 0 és containsO esetében* * A tényleges eredmények a véleden jellegű tesztekből fakadóan különbözóek lehetnek
A legszembeötlőbb dolog a tesztek eredményeiben a lineáris vizsgálat és a vödrös módszer nyilvánvaló különbségeiből fakad. A táblázat utolsó oszlopában (Átlag) látha�uk, hogy a lineáris vizsgálat teljesítménye lényegében nem jobb, mint a láncolt listáé-O(N). A vödrök használata rendkívül jó eredményeket mutat. Még a legrosszabb esetben is, amikor a hasítótábla nincs újraméretezve, amíg a telítettség el nem éri a 200%-ot, az
equals() hívások számának átlaga még mindig 2 alatt marad! A legjobb esetben az átlag 0,24, vagyis egy hívás 4 értékenként. Természetesen ebben az esetben a hasítótábla csak 25%-os telítettségú, számos elvesztegetett hellyel. Minden esetben elmondható, hogy a vödrök használata nagyságrendekkel előnyösebb, mint a lineáris vizsgálat.
Közvetlen korrelációt fedezhetünk fel a vödörtelítettségi tényező és a hívásszámok között is: 100°/o-os telítettségnél egy hívás értékenként; 75% telítettségnél egy hívás az értékek mintegy 60%-ánál és így tovább. Az igazán érdekes tulajdonság az, hogy a teljesítmény a terhelési tényezőtől függetlenül bámulatosan közel marad az 0(1)-hez.
Ebből kifolyólag megállapíthatjuk, hogy a hasítótábla implementációja vödrös megvalósításban rendkívül jó általános teljesítményt nyújt, talán messze a legjobbat a tárolás és az értékek meghívása terén. Akárhogyan is, a megfelelő teljesítmény elérése véletlenszerű is lehet, amíg megtaláljuk a legjobb hasító függvényt.
330
Összefoglalás
Összefoglalás
A fejezetből a következőket tanulhattuk meg:
• A hasítás úgy működik, mint egy véletlenszám-generátor, elrontva az adatok
bárminemű rendezettségét.
• A tökéletes hasítófüggvényben nincsenek ütközések, de ezt nehéz elérni.
• A használandó hasítófüggvényt leginkább az input adatok természete és tu
lajdonságai határozzák meg, és ezt sok esetben nehéz, ha nem lehetetlen,
előre ismerni. Ezért az ütközések megszüntetése helyett inkább számuk rni
nimalizálása a cél.
• A táblaméret növelése csökkentheti az ütközések számát az elvesztegetett he
lyek rovására, használjunk inkább prímszámot a tábla méretének megadására.
• A lineáris vizsgálat láncolt listává fajul el.
• A vödrös megoldást párosítva egy jó hasító függvénnyel elérhető az 0(1)
keresési idő.
Gyakorlatok
1. Módosítsuk a Bucket i ngHashtab l e-t úgy, hogy csak prímszámot adunk meg a
vödrök számának. Milyen hatása van (ha van) ennek a teljesítményre?
2. Módosítsuk a L i nearProbi ngHashtabl e-t úgy, hogy tárolja az értékek számát a
táblában, mintsem hogy számolnunk kelljen núnden alkalommal.
3. Módosítsuk a Bucketi ngHashtab l e-t úgy, hogy tárolja az értékek számát a táb
lában, mintsem hogy számolnunk kelljen núnden alkalommal.
4. Készítsünk iterátort, ami hozzáférést biztosít a Bucket i ngHashtab l e-ben az
összes bejegyzéshez.
331
TIZENKETTEDIK FEJEZET
Halmazok
A halmaz különálló értékek gyűjteménye, amelyben egy elem csak egyszer szerepel
het. Tudományos alkalmazások során különösen nagy hasznuk vehető, gyakran
azonban adatok tárolására is alkalmasabb megoldást nyújtanak, mint a listák, ha
nincs szükség kétszerezett értékre. Az esetek többségében listák helyett célszerűbb
halmazokat használni.
A fejezetben a következő témaköröket tárgyaljuk
• Alapvető halmazműveletek.
• A kis mennyiségű adatok kezelésére szolgáló listahalmaz.
• A nagy mennyiségű rendezetlen adatok kezelésére használható hasító halmaz.
• A megjósolható iteráció sorrendű fahalmaz.
A halmazokról
A halmazok gyakorlatilag ismétlődés nélküli adatok rendezetlen készletei. A listákkal
és tömbökkel ellentétben a halmaz nem őrzi meg a beszúrási sorrendet, és nem tar
talmazhat egy elemet többször. A 12.1. ábrán az ábécé betűit a-tól k-ig tartalmazó
halmaz látható. Figyeljük meg, hogy az elemek sorrendje nem meghatározott.
000
0 800
0�0 12. 1. ábra. A halmaz külö'nálló, rendezetlen értékek készlete
Halmazok
A halmazokkal általában a 12.1. táblázatban látható műveleteket végezhetjük el.
JMavelet add
delete
contains
iterator
size
isEmpty
clear
Leirás A halmaz elemeihez új értéket ad hozzá. Ha a hozzáadás si
keres, a halmaz mérete eggyel növekszik, és a művelet true
értékkel tér vissza. Ha a hozzáadandó érték már eleme a
halmaznak, a visszatérési érték fa l se lesz.
A halmaz elemei közül töröl. Ha a törlés sikeres, a halmaz
mérete eggyel csökken, és a művelet true értékkel tér visz
sza. Ha a törlendő érték nem eleme a halmaznak, a visszaté
rési érték fa l se lesz.
Megállapítja, hogy az adott érték eleme-e a halmaznak.
A halmaz összes elemén végigfutó iterátort hoz létre.
A halmaz elemszámát állapítja meg.
Megállapítja, hogy a halmaz üres-e. Visszatérési értéke i gaz,
ha a halmaz üres (si ze() == O); egyébként hamis.
A halmaz összes elemének törlése. A halmaz mérete lenul
lázódik.
12. 1. táblázat. Halmazműveletek
Mivel a halmaz egy elemet nem tartalmazhat kétszer, az elem hozzáadása nem feltét
lenül sikerül. Ha a hozzáadandó elemet már tartalmazta a halmaz, nem kerül újra
hozzáadásra. Ezért az add művelet jelzi, sikeres volt-e a hozzáadás. Ehhez hasonló
an a de l et e művelet is jelzi, sikerült-e törölnie az adott elemet a halmazból - azaz
létezett-e az adott elem.
A halmaz elemeinek törlésén és hozzáadásán kívül lekérdezhe�ük, hogy egy
adott érték eleme-e a halmaznak, megtudhatjuk a halmaz számosságát, valamint vé
gigmehetünk a halmaz elemein. A halmaz elemeinek iterálása annyiban különbözik a
listák iterálásától, hogy a listák elemei meghatározott sorrenddel rendelkeznek, míg a
halmazok nem. Bár a halmaz elemei sorrendbe tehetők - minden további nélkül
megvalósíthatunk rendezett halmazokat -, a halmazok elemei általában egyenértékű
ek, ezért nem ismert az iterációs sorrendjük
334
A halmazokkal számos érdekes és hasznos mű,velet elvégezhető.
Tegyük fel, hogy a 12.2. ábrán látható két halmazunk van.
x
0 0 0 D 0 G
0
y
G D 0 0
G 0 0
A halmazokról
12. 2. ábra. A két halmaz: X = {A, B, D, E, F, I, J} és Y = {C, D, F, G, H, I, K}
Két halmaz uniója alatt a két halmaz összes elemét tartalmazó harmadik halmazt értjük, természetesen azzal a kikötéssel, hogy egy elem csak egyszer szerepelhet. Úgy is felfoghatjuk, mint két halmaz összeadását. A 12.3. ábra az X és Y halmaz unióját ábrázolja. Vegyük észre, hogy a két halmaz elemei között volt ugyan némi átfedés (D, I
és F), az új halmazban ezek az elemek is csak egyszer szerepelnek.
12.3. ábra. Két halmaz uniija: X + Y
12.4. ábra. Két halmaz metszete: X n Y
335
Halmazok
Két halmaz metszete alatt a mindkét halmazban megtalálható elemeket tartalmazó
újabb halmazt értjük, kétszerezett értékek természetesen itt sem fordulhatnak elő.
A 12.4. ábra az X és az Y halmaz metszetét mutatja. Figyeljük meg, hogy csak azok
az elemek kerülnek be, amelyek mindkét halmazban megtalálhatóak voltak.
Két halmaz különbségén azt a halmazt értjük, amely tartalmazza az első halmaz
összes olyan elemét, amely nem eleme a második halmaznak. Ez tulajdonképpen a
halmazműveletek között a kivonást jelenti. A 12.5. ábrán láthatjuk az X - Y hal
mazművelet eredményét. Csak azok az elemek felelnek meg, amelyek megtalálhatóak
voltak az X halmazban, de az Y-ban nem.
12.5. ábra. Két halmaz küliinbsége: X- Y
A következő gyakorlatban létrehozunk egy olyan interfészt, amely minden szükséges
metódust definiál. Erre azért lesz szükség, hogy későbbi alkalmazásaink igényei alap
ján bármilyen halmazmegvalósítást könnyedén kivitelezhessünk, továbbá azért, hogy
minden halmaztípust kipróbálhassunk.
Gyakorlófeladat: általános halmazinterfész létrehozása
Az alábbiak alapján hozzuk létre a set interfészt:
336
package com.wrox.algorithms.sets;
public interface set extends Iterable { public boolean add(Object value); public boolean delete(object value); public boolean contains(object value); public void clear(); public int size(); public boolean isEmpty();
}
A halmazokról
A megvalósitás müködése
A fenti Set interfész a 12.2. táblázatban bemutatott valamennyi múvelet Java-felületen metódusokkal megvalósított változata. Ezenfelül kibővítettük az iterator() metódust tartalmazó Iterable interfészt, így a halmaz bárhol használható, ahol szükség lehet az Iterable interfészre.
Halmazmegvalósitások tesztelése
A következő gyakorlatban létrehozunk egy tesztésomagot, amely minden halmaztípusta megfelel. Erre azért lesz szükség, hogy ellenőrizhessük, minden megvalósított halmazunk rendben múköclik-e.
Gyakorlófeladat: generikus halmaz-tesztcsomag létrehozása
Hozzuk létre a következő absztrakt tesztosztályt:
package·· com. wrox :·a l göri thms. sets;
iMPQrt com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutofsoundsException; import com.wrox.algorithms.iteration.Reverserterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algórithms.lists.List; import junit.framework.Testcase;
public abstract class AbstractsetTestcase extends Testcase { private static final object A". "a"; private static final Object B = "b"; private static final object c = "c"; private static final object D = "d"; private static final object E = "e"; private static final object F = "f";
private set _set;
protected void setUp() throws Exception { _set= createset();
}
_set.add(c); _set.add(A); _set.add(B); _set.add(o);
.....___p.J:9Jef_!ed abstract�et� c _reateset�()L..L.· --------------..1
337
Halmazok
338
public void testContainsExisting() { assertTrue(_set.contains(A)); assertTrue(_set.contains(B)); assertTrue(_set.contains(c)); assertTrue(_set.contains(D));
}
public void testcontainsNonExisting() { assertFalse(_set.contains(E)); assertFalse(_set.contains(F));
}
public void testAddNeWValue() { assertEquals(4, _set.size());
}
assertTrue(_set.add(E)); assertTrue(_set.contains(E)); assertEquals(S, _set.size());
assertTrue(_set.add(F)); assertTrue(_set.contains(F));
assertEquals(6, _set.size());
public void testAddExistingvalueHasNoEffect() { assertEquals(4, _set.size()); assertFalse(_set.add(C)); assertEquals(4, _set.size());
}
public void testDeleteExisting() { assertTrue(_set.delete(B));
assertFalse(_set.contains(B)); assertEquals(3, _set.size());
}
assertTrue(_set.delete(A)); assertFalse(_set.contains(A)); assertEquals(2, _set.size());
assertTrue(_set.delete(C)); assertFalse(_set.contains(C)); assertEquals(l, _set.size());
assertTrue(_set.delete(D)); assertFalse(_set.contains(D)); assertEquals(O, _set.size());
}.
public void testoeleteNonExisting()-{ assertEquals(4, _set.size()); assertFalse(_set.delete(E)); assertEquals(4, _set.size()); assertFalse(_set.delete(F)); assertEquals(4, _set.size());
}
public void testclear() { assertEquals(4, _set.size()); assertFalse(_set.isEmpty());
}
_set.clear();
assertEquals(O, _set.size()); assertTrue(_set.isEmpty());
assertFalse(_set.contains(A)); assertFalse(_set.contains(B)); assertFalse(_set.contains(C)); assertFalse(_set.contains(D));
public void testiteratorForwards() { checkiterator(_set.iterator());
}
public void testiteratorsackwards() { checkiterator(new Reverseiterator(_set.iterator()));
}
private void checkiterator(Iterator i) { List values = new LinkedList();
}
for (i.first(); !i.isoone(); i.next()) { values.add(i.current());
}
try { i. current(); fail();
} catch (IteratoroutofsoundsException e) { }
assertEquals(4, values.síze()); assertTrue(values.contains(A)); assertTrue(values.contains(B)); assertTrue(values.contains(C)); assertTrue(values.contains(D));
A halmazokról
339
Halmazok
A megvalósitás müködése
Az AbstractSetTestcase osztály a Testcase osztályt terjeszti ki, így az ]Unit-kom
patibilis tesztosztály lehet. Ezenkívül a példa kedvéért definiál néhány bejegyzést, va
lamint egy leképezést a teszteléshez. A halmaz a setup() metódusban kap értéket,
amely közvetlenül a tesztesetek előtt fut le, és a halmaz első négy példabeli értékét
határozza meg:
package com.wrox.algorithms.sets;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.Reverseiterator;
import com.wrox.algorithms.iteration.IteratoroutOfBoundsException; import com.wrox.algorithms.lists.LinkedList;
import com.wrox.algorithms.lists.List;
import junit.framework.Testcase;
public abstract class AbstractSetTestCase extends Testcase { private static final object A "a";
}
private static final Object B "b";
private static final Object c "c";
private static final Object D "d"; private static final object E "e"; private static final Object F "f";
private set _set;
protected void setup() throws Exception { _set= createset();
}
_set.add(c); _set.add(A); _set.add(B); _set.add(D);
protected abstract set createset();
A contai ns metódus visszatérési értékének true-nak kell lennie minden olyan értékre,
amely eleme a halmaznak, különben pedig fal se. Tudjuk, hogy a négy példabeli érték
biztosan eleme, ezért a testcontai nsExi st i n g() metódusban ellenőrizhe�ük, hogy a
con ta i ns () metódus valóban true értékkel tér-e vissza minden esetben:
340
public void testcontainsExisting() { assertTrue(_set.contains(A)); assertTrue(_set.contains(B)); assertTrue(_set.contains(C)); assertTrue(_set.contains(D));
}
A halmazokról
Ezzel ellentében a testeonta i nsNonExi st i n g O metódusban arról győződhetünk
meg, hogy a con ta i n sO metódus fal se értékkel tér vissza az olyan értékekre, ame
lyek nem elemei a halmaznak
public void testcontainsNonExisting() { assertFalse(_set.contains(E)); assertFalse(_set.contains(F));
}
A testAddNewva l u e O metódus a két érték hozzáadása előtt ellenőrzi a halmaz kez
deti méretét. Az add O metódus meghívásakor minden alkalommal megbizonyoso
dunk róla, hogy true értékkel tért vissza, azaz az érték még nem volt eleme a hal
maznak Ezután meghívjuk a con ta i ns O metódust, hogy lássuk, az új érték valóban
létezik-e, majd ellenőrizzük, hogy a halmaz mérete valóban megnövekedett-e eggyel:
public void testAddNewvalue() { assertEquals(4, _set.size());
}
assertTrue(_set.add(E)); assertTrue(_set.contains(E)); assertEquals(S, _set.size());
assertTrue(_set.add(F)); assertTrue(_set.contains(F)); assertEquals(6, _set.size());
A testAddExi st ingval ueHasNoEffect O metódus egyszerűen megpróbálja újra hoz
záadni az értékeket a halmazhoz. Minden létező érték hozzáadásakor a visszatérési ér
ték és a méret ellenőrzésével meggyőződünk róla, hogy a metódus nem járt sikerrel:
public void testAddExistingvalueHasNoEffect() { assertEquals(4, _set.size());
assertFalse(_set.add(A)); assertEquals(4, _set.size());
assertFalse(_set.add(B)); assertEquals(4, _set.size());
341
Halmazok
}
assertFalse(_set.add(C));
assertEquals(4, _set.size());
assertFalse(_set.add(D));
assertEquals(4, _set.size());
Ezután a testDeleteExistingO metódus segítségével letöröljük a négy kezdeti
próbaértéket. A de l et e O metódus minden egyes meghívásakor a visszatérési érték
és a méret vizsgálatával megbizonyosodunk róla, hogy a törlés megtörtént:
public void testDeleteExisting() { assertEquals(4, _set.size());
}
assertTrue(_set.delete(B));
assertFalse(_set.contains(B));
assertEquals(3, _set.size());
assertTrue(_set.delete(A));
assertFalse(_set.contains(A));
assertEquals(2, _set.size());
assertTrue(_set.delete(C));
assertFalse(_set.contains(C));
assertEquals(l, _set.size());
assertTrue(_set.delete(D));
assertFalse(_set.contains(D));
assertEquals(O, _set.size());
Természetesen azt is kipróbáljuk, mi történik, ha nem létező elemet akarunk törölni.
A halmaz méretének ellenőrzése után a testoeleteNonExistingO metódus meg
hívja a de l et e O metódust, hogy azzal töröljünk két biztosan nem létező értéket.
J'vlinden egyes alkalommal megvizsgáljuk a halmaz méretét, hogy megbizonyosod
junk róla, nem változott:
342
public void testDeleteNonExisting() { assertEquals(4, _set.size());
}
assertFalse(_set.delete(E));
assertEquals(4, _set.size());
assertFalse(_set.delete(F));
assertEquals(4, _set.size());
A halmazokról
A testclear() metódus előbb megvizsgálja, nem üres-e a halmaz. Majd meghívja a
c l e ar() metódust, utána pedig újra lekérdezi a halmaz számosságát, hogy lássuk,
valóban üres-e:
public void testclear() { assertEquals(4, _set.size()); assertFalse(_set.isEmpty());
}
_set.clear();
assertEquals(O, _set.size()); assertTrue(_set.isEmpty());
assertFalse(_set.contains(A)); assertFalse(_set.contains(B)); assertFalse(_set.contains(C)); assertFalse(_set.contains(D));
Végül megnézzük, hogy a halmaz elemeinek iterálásával (mind előre, mind pedig visz
szafelé) minden várt értéket megkapunk-e. A feladat nagy részét a checkiterator()
metódus végzi el. Először a halmaz minden elemét bejárja, és hozzáadja őket egy lis
tához. Majd - gondoskodva arról, hogy az iterátor befejezéskor a megfelelő kivételt
okozza- a lista ellenőrzésével meggyőződünk róla, hogy minden várt érték szerepel-e
a lista elemei között:
private void checkrterator(Iterator i) { List values = new LinkedList();
}
for (i.first(); !i.isDone(); i.next()) { va l u es. add (i .. eu r re nt());
}
try { i. current(); fai l();
} catch (IteratoroutOfBoundsException e) {
ll ezt várjuk }
assertEquals(4, values.size()); assertTrue(values.contains(A)); assertTrue(values.contains(B)); assertTrue(values.contains(C)); assertTrue(values.contains(D));
343
Halmazok
Ezután az előrehaladó iteráció tesztelése érdekében a testiteratorForwards() metódus egyszerűen létrehoz egy iterátort, majd átadja a checkrterator() metódusnak:
public void testrteratorForwards() { checkrterator(_set.iterator());
}
Végül pedig az inverz iterátor tesztelése végett a testiteratorBackwards() metódus a checkrterator() metódus meghívása előtt Reversenerator-ba burkolja az iterátort (lásd 2. fejezet). Így minden first() és next() metódushívás automatikusan l ast() és previ ous () hívássá alakul, tehát nem szükséges külön tesztcsomagot írni emiatt:
public void testrteratorBackwards() { checkrterator(new Reverserterator(_set.iterator()));
}
Listahalmaz
A következő gyakorlatban olyan halmazt hozunk létre, amelynek a mögöttes tárolási mechanizmusa a lista lesz. A megvalósítás meglehetősen lényegre törő és könnyen követhető. Bár nem kimondottan hatékony, kisebb adathalmazokhoz hasznos lehet.
Gyakorlófeladat: listahalmaz megvalósítása és tesztelése
Kezdjük a listahalmaz tesztjének létrehozásával:
package com.wrox.algorithms.maps;
public class ListMapTest extends AbstractMapTestcase { protected Map createMap() {
return new ListMap(); }
Ezután hozzuk létre magát a listahalmazosztályt:
344
package com.wrox.algorithms.sets;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
public class ListSet implements set { rivate final List yalue_s ___ ::_new _Lj _f1�edLigQ,_,;'--- --��--
}
publ ic bO<> l ei:ll1··e:ontai ns Cotiject ··valileY "{ return _values.contains(value);
}
public boolean add(Object value) { if (contains(value)) {
}
return false;
}
_values.add(value); return true;
public boolean delete(Object value) { return _values.delete(value);
}
public void clear() { _values.clear();
}
public int size() { return _values.size();
}
public boolean isEmpty() { return _values.isEmpty();
}
public Iterator iterator() { return _values.iterator();
}
A megvalósitás müködése
Listahalmaz
A L istsetTest osztály egyszerűen kibővíti az AbstractSetTestCase osztályt, így
minden tesztesetet örököl. Ezenkívül csak annyit tettünk, hogy megvalósítottuk a
createSet O metódust, amely a L istSet osztáJy egy példányával tér vissza, hogy
maguk a tesztesetek hasznáJhassák.
A L istset osztály megvalósítása meglehetősen lényegre törő. Az esetek többsé
gében a Set interfész metódusait közvetlenül a mögöttes lista ekvivalens metódusai
nak delegáJjuk
Mögöttes tárolási mechanizmusnak láncolt listát fogunk használni, bár technikai
lag bármilyen listamegvalósítás jó volna. Szinte az összes metódus - az add() kivéte
lével- egysoros. Nem tesznek mást, csak a mögöttes listának delegáJják a metódust.
Az add() metódus előbb ellenőrzi, hogy a hozzáadandó érték létezik-e már a
mögöttes listában. Ha létezik, fa l se értékkel visszatér, így jelezve, hogy a halmaz
nem változott. Ellenkező esetben az új értéket hozzáadja a listához:
345
Halmazok
public boolean add(Object value) { if (contains(value)) {
}
return false;
}
_values.add(value); return true;
Ahogy láthatjuk, a listaalapú halmaz igen egyszerű. Mind az add(), mind a dele
te(), mind pedig a contains() metódus futásideje O(N), ami kis számok kezelésé
nél megfelelő.
Hasítóhalmaz
Ha viszonylag sok elemet szetetnénk tárolni, és nem számit a a sorrendjük, valószí
nűleg a hasítótábla-alapú (hasítótáblákkal a 11. fejezetben foglalkoztunk) halmaz
megvalósítás a legjobb választás. A következő gyakorlatban megnézzük a hasító
halmaz megvalósítását. A biztonság kedvéért érdemes átlapozill a hasítótáblákat tár
gyaló fejezetet, különösen a vödröket használó hasítótáblák megvalósítását.
Gyakorlófeladat: hasítóhalmaz megvalósítása és tesztelése
Kezdjük a tesztosztály létrehozásával:
package com.wrox.algorithms.sets;
public class HashSetTest extends AbstractsetTestcase { protected set createset() {
return new Hashset();
}
} ______________________________________________ __
Majd hozzuk létre a hasítóhalmaz-osztályt:
346
package com.wrox.algorithms.sets;
import com.wrox.algorithms.hashing.Hashtablerterator; import com.wrox.algorithms.iteration.Arrayrterator; import com.wrox.algorithms.iteration.Iterator;
public class Hashset implements Set { public static final int DEFAULT_CAPACITY = 17; RUb]jc static final float DEFAULT LOAD FACTOR = 0.7Sf·
Hasítóhalmaz
private final-int:-::::; ni ti a leapaci ty; private final float _loadFactor; private ListSet[] _buckets; private int _size;
public Hashset() { this(DEFAULT_CAPACITY, DEFAULT_LOAO_FACTOR);
}
public Hashset(int initialcapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public Hashset(int initialcapacity, float loadFactor) { assert initialcapacity > O : "az 'initialcapacity' nem
lehet < l";
}
assert loadFactor > O : "a 'loadFactor' nem lehet <=O";
_initialcapacity = initialcapacity; _loadFactor = loadFactor; clear();
public boolean contains(object value) {
}
Listset bucket = _buckets[bucketindexFor(value)]; return bucket != null && bucket.contains(value);
public boolean add(Object value) { ListSet bucket = bucketFor(value);
}
if (bucket.add(value)) { ++_size;
}
mai ntai nLoad(); return true;
return false;
public boolean delete(Object value) {
}
int bucketindex = bucketindexFor(value); Listset bucket = _buckets[bucketindex]; if (bucket != null && bucket.delete(value)) {
--_size;
}
if (bucket.isEmpty()) { _buckets[bucketindex] = null;
} return true;
return false;
347
Halmazok
348
public Iterator iterator() { return new Hashtableiterator(new Arrayiterator(_buckets));
}
public void clear() { _buckets = new Listset[_initialcapacity]; _size = O;
}
public int size() { return _size;
}
public boolean isEmpty() { return size() == O;
}
private Listset bucketFor(object value) { int bucketindex = bucketindexFor(value);
}
Listset bucket = _buckets[bucketindex]; if (bucket == null) {
}
bucket = new ListSet(); _buckets[bucketindex] = bucket;
return bucket;
private int bucketindexFor(object value) {
}
assert value != null : "az érték net11 lehet NULL"; return Math.abs(value.hashcode()% _buckets.length);
private void maintainLoad() { if (loadFactorExceeded()) {
resize();
}
}
private boolean loadFactorExceeded() { return size()> _buckets.length * _loadFactor;
}
private void resize() { HashSet copy= new Hashset(_buckets.length *2, _loadFactor);
for (int i =O; i < _buckets.length; ++i) { if (_buckets[i] != null) {
copy.addAll(_buckets[i].iterator());
}
Hasítóhalmaz
}
_bückets copy ._bucl<ets; }
private void addAll(Iterator values) {
}
assert values != null : "az érték nem lehet null";
for (values.first(); !values.isoone(); values.next()) { add(values.current());
}
A megvalósitás működése
Ismét csak annyit tettünk, hogy a HashsetTest osztállyai kibővítettük az Abstract
SetTestCase osztályt, majd megvalósítottuk a createset() metódust, amely a
HashSet osztály egy példányával tér vissza a teszt kedvéért.
A Hashset osztály megvalósítása nagyrészt a 11. fejezetben található Bucke
ti ngHashtabl e kód közvetlen másolata, ezért itt csak a kód azon elemeit tárgyaljuk,
amelyek a set interfész megvalósítása érdekében eltérnek a 11. fejezetben található
Bucket i ngHashtab l e osztályétóL
A Set interfész megvalósításán kivül az első szembetűnő különbség a Has h set és
a BucketingHashtable között az, hogy a vödrök megvalósítására List helyett List
set osztályt használtunk Bizonyos értelemben a vödör maga is halmaz (nem tartal
mazhat kétszerezett értéket), a hasítóhalmaz pedig csak elosztja az értékeket a külön
böző halmazok között (a hasító kód alapján) a fellapozási idő csökkentése érdekében.
Azáltal, hogy a vödröket listák helyett halmazokkal valósítottuk meg, nemcsak egy
szerűsítettük a kódot, hanem, ami még fontosabb, világosabbá tettük a kód eredeti
célját. Ez látható az add() metódus megvalósításában is: az új érték hozzáadása előtt
már nem kell meghívni a contai ns()metódust:
public boolean add(Object value) { Listset bucket = bucketFor(value);
}
if (bucket.add(value)) { ++_size;
}
mai nt ai n Load(); return true;
return false;
A következő szembetűnő különbség, hogy kiegészítettük az eredeti osztályt a set in
terfész által megkövetelt de l e te() metódussal. Csakúgy, mint az add() metódus
esetében, a de l e te() metódusnál is kihasználha�uk, hogy a vödrök maguk is halma
zok, így ha megtaláltuk a megfelelő vödröt, az érték törléséhez elég a de l e te() me
tódust meghívnunk, egyéb műveletet nemkell végeznünk
349
Halmazok
public boolean delete(object value) {
}
int bucketindex = bucketindexFor(value); Listset bucket = _buckets[bucketindex]; if (bucket != null && bucket.delete(value)) {
--�size;
}
if (bucket.isEmpty()) { _buckets[bucketrndex]
} return true;
return false;
null;
Az utolsó, amit meg kell említenünk, hogy a halmaz elemeinek bejárására megvalósí
tottuk az iterator() metódust. Ehhez a 11. fejezetben található Hashtablerte
rator metódust használtuk. Vegyük észre, hogy ez azért volt lehetséges, mert a
Hashtabl erterator megvalósítása Iter ab l e interfész, nem pedig L i st alapú.
Ezekenfelül csak néhány kényelmi konstruktort adtunk hozzá az eredeti kódhoz
a használhatóság érdekében. Ha ettől eltekintünk, a Hashset gyakorlatilag a 11. feje
zetben található Bucketi ngHashtab l e másolata.
Ha vödör alapú hasítótáblát használunk, valamint a hashcode metódus formájá
ban jó hasítófüggvényünk van, algoritmusunk futásideje meg fogja közeliteni az
0(1) teljesítményt. Természetesen, ahogy a fejezet elején már ernlitettük is, a hasító
tábla használata kizárja a sorrend lehetőségét, így az iterátor látszólag véletlenszerű
en fogja bejárni az értékeket.
Fahalmaz
A halmazok elemei általában nincsenek sorba rendezve. Néha azonban előfordulhat,
hogy a halmazszemancika megtartásával megjósolható iterációsorrendre van szüksé
günk. Ilyen eset lehet például a felhasználó számára felkínálható lehetőségek halma
za, vagy egy telefonkönyvből származó névlista. Ilyenkor a bináris keresáfa (lásd 10.
fejezet) adatstruktúrája az ideális megoldás.
A fahalmazok megvalósításának tanulmányozása előtt érdemes feleleveníteni a
bináris keresőfáról olvasottakat, hogy biztosan megértsük az alapvető fogalmakat és
a kódot. A kérdés tárgyalása ismét csak a Treeset-kód és az eredeti B i narysearch-
Tree-kód közötti különbségekre korlátozódik. ·
350
Gyakorlófeladat: fahalmaz megvalósítása és tesztelése
Kezdjük a TreeSetTest osztály létrehozásával:
package com. w ro X. a l goritlims. se ts;
public class TreesetTest extends AbstractSetTestcase { protected set createset(� {
return new Treeset();
} }
Ezután pedig térjünk át a fahalrnaz megvalósítására:
package com.wrox.algoritlims.sets;
import com.wrox.algorithms.iteration.Iterator;
Fahalmaz
import com.wrox.algorithms.iteration.IteratoroutofsoundsException; import com.wrox.algorithms.sorting.comparator; import com.wrox.algorithms.sorting.Naturalcomparator;
public class Treeset implements set { private final comparator _comparator; private Node _root; private int _size;
public Treeset() { this(Naturalcomparator.INSTANCE);
}
public Treeset(Comparator comparator) {
}
assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
public boolean contains(Object value) { return search(value) != null;
}
public boolean add(Object value) { Node parent = null; Node node = _root; int cmp = O;
while (node != null) { parent = node; cmp = _comparator.compare(value, node.getvalue());
if (cmp == O) { return false;
}
351
Halmazok
352
}
}
node - cmp <o ? node.getsmaller() node.getlarger();
Node inserted = new Node(parent, value);
if (parent == null) { _root = inserted;
} else if (cmp < 0) { parent.setSmaller(inserted);
} else { parent.setLarger(inserted);
}
++_size; return true;
public boolean delete(Object value) { Node node = search(value); if (node == null) {
return false; }
Node deleted node.getsmaller() != null &&
node.getLarger() != null ?
node.successor() : node; assert deleted != null : "a 'deleted' nem lehet null";
Node replacement = deleted.getsmaller() != null ?
deleted.getsmaller() : deleted.getLarger(); if (replacement != null) {
replacement.setParent(deleted.getParent()); }
if (deleted == _root) { _root = replacement;
} else if (deleted.issmaller()) { deleted.getParent().setsmaller(replacement);
} else { deleted.getParent().setLarger(replacement);
}
if (deleted != node) {
}
object deletedvalue = node.getvalue(); node.setvalue(deleted.getvalue()); deleted.setvalue(deletedvalue);
--_size; return true;
public� Iteratoriteraior O�-{ return new valueiterator();
}
public void clear() { _root= null; _size = O;
}
public int size() { return _size;
}
public boolean isEmpty() { return _root== null;
}
private Node search(object value) { assert value != null : "az érték nem lehet NULL";
}
Node node = _root;
while (node != null) { int cmp = _comparator.compare(value, node.getvalue()); if (cmp == O) {
break; }
node = cmp < O ? node.getsmaller() node.getLarger(); }
return node;
private static final class Node { private object _value; private Node _parent; private Node _smaller; private Node _larger;
public Node(Node parent, object value) { setParent(parent); setvalue(value);
}
public object getvalue() { return _value;
}
Fa halmaz
353
Halmazok
354
public void setvalue(ob)ect value) {
}
assert value != null : "az érték nem lehet NULL"; _value = value;
public Node getParent() { return _parent;
}
public void setParent(Node parent) { _parent = parent;
}
public Node getsmaller() { return _smaller;
}
public void setsmaller(Node node) { assert node != getLarger() :
}
"a 'smaller' (kisebb) nem lehet azonos a 'l arger' rel (nagyobbal)";
_smaller = node;
public Node getLarger() { return _larger;
}
public void setLarger(Node node) { assert node != getsmaller() :
_larger = node; }
"a 'larger' (nagyobb) nem lehet azonos a 'smaller'-rel (kisebbel)";
public boolean issmaller() { return getParent() != null &&
this == getParent().getsmaller(); }
public boolean isLarger() { return getParent() != null &&
}
public Node minimum() { Node node = this;
this == getParent().getLarger();
while (node.getsmaller() != null) { node = node.getsmaller();
}
}
return node;
}
public Node maximum() {
Node node = this;
}
while (node.getLarger() != null) { node = node.getLarger();
}
return node;
public Node successor() {
}
if (getLarger() != null) { return getLarger().minimum();
}
Node node = this;
while (node.isLarger()) { node = node.getParent();
}
return node.getParent();
public Node predecessor() {
}
if (getsmaller() != null) {
return getSmaller().maximum();
}
Node node = this;
while (node.issmaller()) { node = node.getParent();
}
return node.getParent();
private final class valuerterator implements Iterator { private Node _current;
public void first() { _current = _root != null ? _root.minimum() null;
l
Fahalmaz
355
Halmazok
} l
public void last() { _current = _root != null ? _root.maximum()
}
public boolean isoone() { return _current == null;
}
public void next() { if (!isDone()) {
_current = _current.successor(); }
}
public void previous() { if (!isoone()) {
_current = _current.predecessor(); }
}
null;
public object current() throws IteratoroutofsoundsException { if (isoone()) {
throw new IteratoroutofsoundsException(); } return _current.getvalue();
}
A megvalósitás működése
A TreeSetTest osztály az Abstractse-nestcase osztályt bővíti ki, így újrahaszno
sítha�uk az összes korábbi tesztünket. A createSetO metódus a Treeset osztály
példányával tér vissza.
A Treeset osztály kódja nagyrészt megegyezik a 10. fejezetben létrehozott B i nary
SearchTree osztály kódjával, így most csak a két kód közötti különbségekről lesz szó.
Természetesen a legfontosabb különbség, hogy a Treeset osztály a Set inter
fészt valósítja meg. Ez azzal jár, hogy az eredeti i nsertO metódust átnevezzük
add O metódussá. A névváltoztatás mellé egy kis alaki módosítás is dukál. Hiszen az
eredeti i nsertO metódus megengedte a kétszérezett értékeket, a halmazszemancika
azonban tiltja, így az add O metódus kódját át kell írnunk. Az eredeti i n se rt O me
tódus whi l e ciklusa így nézett ki:
356
while (node != null) { parent = node;
}
cmp = _comparator.compare(value, node.getvalue()); node = cmp <=O ? node.getsmaller() : node.getLarger();
Összefoglalás
Látha�uk, hogy kétszerezett érték beszúrásakor az új érték a már létező bal oldali gyermeke lett. Az add() metódus azonban nem támogatha* a kétszerezett értékeket:
while (node != null) { parent = node; cmp = _comparator.compare(value, node.getvalue()); if (cmp == 0) {
return false;
} node = cmp < O 7 node.getsmaller() node.getLarger();
}
Így ha az érték már létezik (cm p == O), a metódus fa l se értékkel visszatér, hogy jelezze, semmilyen változás nem történt. Ellenkező esetben az eredetivel megegyezően működik.
A másik fontos változás, hogy a search() privát metódussá vált, helyét pedig a con ta i ns () vette át, ahogy a Set interfész megköveteli. A contai ns () metódus csak abban az esetben tér vissza true értékkel, ha a search() metódus talál illeszkedő csomópontot.
A set interfész által megkövetelt egyéb változásokon (c l e a r(), i s Empty (),
size() és iterator() metódusok hozzáadása) kívül az egyetlen jelentős különbség az, hogy a Node osztályt belső osztállyá tettük, és létrehoztuk a valuerterator belső osztályt, amely sorrendben, előre vagy hátra iterál a csomópontokon a successor()
vagy a predecessor() meghívásával. Készen is vagyunk: olyan halmazimplementációt sikerült létrehoznunk, amely
nek- ahogy a 10. fejezetből tudjuk- átlagos teljesítménye O(log N), és a felvétel sorrendjében őrzi meg az értékeket.
Összefoglalás
A fejezetből a következőket tudhattuk meg:
• A halmaz egymástól különböző értékek készlete.
• A halmazok iterációs sorrendje általában véletlenszerű.
• A listaalapú halmazok viszonylag kis mennyiségű adatok tárolására alkalmasak, mivel a futásidejük O(N).
• A hasítótábla-alapú halmazok futásideje 0(1), véletlenszerű iterációs sorrenddel.
• A bináris keresőfa-alapú halmazok teljesítménye o(l og N), és iterációs sorrendjük megjósolható.
357
Halmazok
Gyakorlatok
1. Írjunk olyan metódust, amely két halmazról megállapítja, egyenlőek-el
2. Írjuk meg a két halmaz unióját megvalósító metódust!
3. Írjuk meg a két halmaz metszetét megvalósító metódust!
4. Írjuk meg a két halmaz különbségét megvalósító metódust!
5. Írjuk át a Has h set osztály de l e te() metódusát úgy, hogy ha a vödör üres, a metódus szabadítsa azt fel!
6. Hozzunk létre rendezett listaalapú halmazmegvalósítást!
7. Hozzunk létre egy olyan halmazt, amely egyfolytában üres marad, ha módosítani kívánunk rajta, akkor pedig okozzon unsupportedoperati onExcepti on kivételt!
358
TIZENHARMADIK FEJEZET
Leképezések
A leképezések - vagyis a szótárak, fellapozási táblázatok és asszociatív tömbök - kü
lönösen alkalmasak indexek létrehozására.
A fejezet a következő témaköröket tárgyalja:
• alapvető leképezési műveletek,
• a kis mennyiségű adatok kezelésére szolgáló a listaleképezés,
• a nagy mennyiségű rendezeden adatok kezelésére használható hasítóleképezés,
• a leképezés harmadik típusa, amely megjósolható iteráció sorrenddel rendel
kezik, a faleképezés.
A leképezésekről
A leképezés kapcsolatot tart a kulcs és az érték között. A leképezésen belül minden
egyes kulcs egyedi és lehetővé teszi számunkra, hogy gyorsan beállithassunk és visz
szakaphassunk egy hozzárendelt értéket. Ez hasznos lehet, ha fellapozási táblázato
kat hozunk létre, amelyekbe egy kódot írunk, és egy leírást kapunk, vagy indexek lét
rehozásánál, amelyek lehetővé teszik, hogy meghatározzuk az információ helyét -
például egy személy rekordját néhány odaillő részlet alapján. A 13.1. ábra egy olyan
leképezést mutat, amelyben az emberek nevei a kulcsok, az értékek pedig az adatbá
zisrekord-számok
A leképezésekkel kapcsolatban lényeges, hogy míg a bennük lévő kulcsok garantál
tan egyediek, addig ez az értékekre nem vonatkozik. Egyébként ez hasznos lehet. Kép
zeljünk el egy indexet, amely telefonszámokat képez le az adatbázisrekordok számára, így
könnyen megtalálhatunk valakit a telefonszáma alapján. Elképzelhető, hogy egy ember
nek több telefonszáma is lehet - otthoni, üzleti, mobil és így tovább. Ebben az esetben
többszörös kulcsok képezhetnek le ugyanarra a rekordszámra. A 13.2. ábrán látha�uk,
ahogy Leonardo da Vinci két számhoz kapcsolható: 555-123-4560 és 555-991-4511.
A leképezéseket szótárként is ismerjük, nem nehéz megérteni, miért (tulajdon
képpen az eredeti leképezésosztályt a JDK-ban szótár-nak hívták.) Egy nyelvi szótár
a szóhoz egy meghatározást társít (illetve a kétnyelvű szótár esetében egy másik
szót). Ezekben az esetekben a szó a kulcs, a meghatározás pedig az érték.
Leképezések
13.1. ábra. Index, ame!J leképezi a neveket a megfelelő adatbáifsrekord-számokhoz
13.2. ábra. A kulcsok egyediek, az értékek nem
360
A leképezésekről
végül egy másik név a leképezésre az asszociatív tiimb. v alójában lehetséges, hogy úgy
tekintünk a tömbökre - vagyis ami azt illeti, a listákra - mint a leképezésre meglehe
tősen hasonlító dolgokra. Emlékezzünk vissza, hogy a tömbök egy bizonyos helyhez
kapcsolva tárolják az értékeket, az index és a leképezés pedig egy bizonyos kulcshoz
kapcsolva. Ezért ha az indexet mint kulcsot képzeljük el, akkor bizonyos értelemben
a tömb olyan, mint egy speciális leképezés.
A 13.1. táblázat összefoglalja a leképezési műveleteket.
{Müvelet get
set
delete
contains
i terata r
size
isEmpty
clear
Leirás
Kap egy értéket (ha van), amely egy adott kulcshoz van hozzárendelve.
Beállítja a hozzárendelt leképezést egy megadott kulcs alapján, és az
előző értékkel (ha van) tér vissza.
Adott kulcs alapján azonosított értéket távolít el, és az értékkel (ha
van) tér vissza.
Meghatározza, hogy egy bizonyos kulcs létezik-e a leképezésben.
Az összes leképezésben lévő kulcson/értékpáron végigfutó iterátort
hoz létre.
Megkapja a leképezésben lévő kulcs/ értékpárok számát.
Eldönti, hogy a leképezés üres-e vagy nem. Visszatérési értéke igaz, ha
a halmaz üres (si ze() == O); egyébként hamis.
Kitörli az összes kulcsot/ értékpárt a leképezésből. A leképezés mérete
alaphelyzetbe lesz állítva.
13.1. táblázat. Leképezésműveletek
A leképezések lehetővé teszik, hogy egy bizonyos kulcsra állitsuk be az értéket, kap
egy értéket (ha van), amely egy adott kulcshoz van hozzárendelve, és a kulcsot, illet
ve értékpárt együtt távolítja el. A leképezés arra is lehetőséget ad, hogy .sorra végig
járjuk a kulcsokat, illetve értékpárokat - bejegyzésekként is ismerjük -, és a halmazok
hoz hasonlóan általában a leképezések sem garantálják a rendezést.
A következő gyakorlatban létrehozunk egy olyan interfészt, amely minden szük
séges metódust definiál. Erre azért lesz szükség, hogy későbbi alkalmazásaink igé
nyei alapján bármilyen leképezés megvalósítását könnyedén kivitelezhessünk, továb
bá azért, hogy minden leképezéstípust könnyen kipróbálhassunk.
361
Leképezések
Gyakorlófeladat: Generic Map interfész létrehozása
Hozzuk létre a Map interfészt a következőképpen:
pacKage com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Iterable;
public interface Map extends Iterable { public Object get(Object key);
}
public object set(Object key, Object value); public Object delete(Object key); public boolean contains(object key); public void clear(); public int size(); public boolean isEmpty();
public static interface Entry { public object getKey();
public object getvalue();
}
A megvalósitás müködése
A Map interfész a 13.1. táblázatban felsorolt minden műveletet tartalmaz Java-metódusokra fordítva, és kiterjeszti az Iterable interfészt, így örökli az iterator() metódust, és mindenhol használha�uk, ahol az Iter ab l e-re szükség van. Figyeljük meg az Entry
belső interfészt, amely egy általános interfészt határoz meg a leképezésben lévő kulcs és értékpárok részére. A Map. En t r y példányok a leképezésiterátorból térnek vissza.
Úgy, ahogy a Map. Entry interfészt, a következő feladatban létrehozha�uk az alapértelmezett Map. En try megvalósítást, amelyet később, a leképezésosztályok egyikének kivételével mindegyik osztály használ majd.
Gyakorlófeladat: alapértelmezett Entry-megvalósítás létrehozása
Hozzuk létre a DefaultEntry osztályt a következőképpen:
362
package com.wrox.algorithms.maps;
public class DefaultEntry implements Map.Entry { private final object _key; Rrivate object value;
A leképezésekről
puolic oefaultEnt:ry(Öoject: key, ooject: value)-
{
}
as se rt: key ! = null : "a 'key' (kul cs) nem l ehet: NULL";
_key = key; set:value(value);
public object: get:Key() { ret:urn _key;
}
public object setvalue(Object: value) { object: oldvalue = _value; _value = value; return oldvalue;
}
public object get:value() { ret:urn _value;
}
public boolean equals(Object: object:) {
if (t:his == object:) { ret:urn t:rue;
}
if (object: == null l l get:Class() ! = object:.get:Class()) { ret:urn false;
}
oefault:Ent:ry ot:her = (Default:Ent:ry) object:;
return _key.equals(other._key) && _value.equals(ot:her._value);
}
A megvalósitás működése
A De fau l t: E nt: r y osztály tartalmazza a kulcsot és az értékpárt, és egyenként elérhetővé teszi őket a get:Key () és a get:va l u e() metódusokon keresztül. Létezik még egy e qua ls() metódus, amely lehetővé teszi annak gyors meghatározását, hogy két bejegyzés ekvivalens-e. Ez a következő tesztekben lesz használható.
V együk figyelembe, hogy a kulcsot az elkészítése után nem módosíthatjuk -
fi na l-ként van megjelölve - mivel nincs szükség változtatásra, ha már egyszer meghatároztuk. Az érték azonban módosítható. Azt is vegyük figyelembe, hogy amíg mindenképpen létre kell hoznunk egy kulcsot, az érték lehet null. A gyakorlatban a null kulcsok ritkán, szinte sohasem hasznosak. Ám a null értékek állandóan előfordulnak, különösen az adatbázis-alkalmazások esetében. A 13.3. ábra olyan jellemző helyzetet szemléltet, amelyben az adatbázisrekord leképezésként jelenik meg.
363
Leképezések
13. 3. ábra. A kulcsok a leképezésben kötelezőek, de az értékek lehetnek üresek
Jegyezzük meg, hogy a Mobiltelefon és a Jogosítvány értéke null, jelezve, hogy rrúnd a kettő hozzá van rendelve, de az értékek ismeretlenek.
Végül idézzük fel, hogy a leképezési interfész meghatározza, hogy egy adott kulcshoz tartózóan nemcsak egy érték frissíthető, hanem bármelyik előzőleg hozzárendelt értéket is visszakapjuk. Ez a tulajdonság úgy mutatkozik meg, hogy a set
va l u e() metódus a létező értékkel tér vissza. Minden rendelkezésünkre áll ahhoz, hogy elkezdjünk megírni néhány generikus
tesztet: egy interfészt, amelyhez bármely leképezés megvalósításnak alkalmazkodnia kell, és egy alapértelmezett Map. En try megvalósítás t.
Leképezésmegvalósitások vizsgálata
Így a tesztek újra felhasználhatóak lesznek bármely lekérdezéstipushoz, a következő feladatban elkészítünk egy absztrakt tesztosztályt, amely tartalmazza az összes tesztesetet. Ezt aztán kiterjeszthe�ük egy tesztosztállyal, amely jellemző bármely leképezésmegvalósításra, amelyet később készítünk majd el.
364
Leképezésmegvalósítások vizsgálata
Gyakorlófeladat: leképezéstesztek generikus csomagjának létrehozása
Hozzuk létre az AbstractMapTestcase osztályt a következőképpen:
package com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.Reverseiterator; import com.wrox.algorithms.iteration.IteratoroutofsoundsException; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List; import junit.framework.Testcase;
public abstract class AbstractMapTestCase extends Testcase { private static final Map.Entry A = new oefaultEntry("akey",
"avalue"); private static final Map.Entry B = new oefaultEntry("bkey",
"bvalue"); private static final Map.Entry c = new oefaultEntry("ckey",
"cvalue"); private static final Map.Entry o = new oefaultEntry("dkey",
"dvalue"); private static final Map.Entry E = new oefaultEntry("ekey",
"evalue"); private static final Map.Entry F = new oefaultEntry("fkey",
private Map _map;
protected void setup() throWs Exception { super. setup();
}
_map= createMap();
_map.set(c.getKey(), c.getvalue()); _map.set(A.getKey(), A.getvalue()); _map.set(B.getKey(), B.getvalue()); _map.set(D.getKey(), o.getvalue());
protected abstract Map createMap();
public void testcontainsExisting() { assertTrue (_map. contai ns (A. get Key())) ; assertTrue(_map.contains(B.getKey())); assertTrue(_map.contains(C.getKey())); assertTrue(_map.contains(D.getKey()));
l
"fvalue");
365
Leképezések
366
public void testcontainsNonExisting() { assertFalse(_map.contains(E.getKey())); assertFalse(_map.contains(F.getKey()));
}
public void testGetExisting() {
assertEquals(A.getvalue(), _map.get(A.getKey())); assertEquals(B.getvalue(), _map.get(B.getKey())); assertEquals(C.getvalue(), _map.get(c.getKey())); assertEquals(D.getvalue(), _map.get(o.getKey()));
}
public void testGetNonExisting() { assertNull(_map.get(E.getKey())); assertNull(_map.get(F.getKey()));
}
public void testSetNewKey() { assertEquals(4, _map.size());
}
assertNull(_map.set(E.getKey(), E.getvalue()));
assertEquals(E.getvalue(), _map.get(E.getKey())); assertEquals(S, _map.size());
assertNull(_map.set(F.getKey(), F.getvalue())); assertEquals(F.getvalue(), _map.get(F.getKey())); assertEquals(6, _map.size());
public void testsetExistingKey() { assertEquals(4, _map.size());
}
assertEqual s(c. getval ue(), _map. set(C. getKey O, "cvalue2")); assertEquals("cvalue2", _map.get(C.getKey())); assertEquals(4, _map.size());
public void testDeleteExisting() { assertEquals(4, _map.size());
assertEquals(B.getvalue(), _map.delete(B.getKey())); assertFalse(_map.contains(B.getKey())); assertEquals(3, _map.size());
assertEquals(A.getvalue(), _map.delete(A.getKey())); assertFalse(_map.contains(A.getKey())); assertEquals(2, _map.size());
assertEquals(C.getvalue(), _map.delete(C.getKey()));
assertFalse(_map.contains(C.getKey())); assertE�uals(l, maR.size());
Leképezésmegvalósítások vizsgálata
}
assertEquals(D.getvalue(), _map.delete(D.get�())); assertFalse(_map.contains(D.getKey())); assertEquals(O, _map.size());
public void testoeleteNonExisting() { assertEquals(4, _map.size()); assertNull(_map.delete(E.getKey())); assertEquals(4, _map.size()); assertNull(_map.delete(F.getKey())); assertEquals(4, _map.size());
}
public void testclear() { assertEquals(4, _map.size()); assertFalse(_map.isEmpty());
}
_map.clear();
assertEquals(O, _map.size()); assertTrue(_map.isEmpty());
assertFalse(_map.contains(A.getKey())); assertFalse(_map.contains(B.getKey())); assertFalse(_map.contains(c.getKey())); assertFalse(_map.contains(o.getKey()));
public void testiteratorForwards() { checkrterator(_map.iterator());
}
public void testiteratorBackwards() { checkrterator(new Reverserterator(_map.iterator()));
}
private void checkrterator(Iterator i) { List entries = new LinkedList();
for (i. first(); ! i. i sDone(); i. next()) { Map.Entry entry = (Map.Entry) i.current(); entries.add(new oefaultEntry(entry.getKey(),
entry.getvalue()));
}
try { i .current(); fail();
} catch (IteratoroutofsoundsException e) {
ll ezt várjuk
}
367
Leképezések
} }
assertEquals(4, entries.size()); assertTrue(entries.contains(A)); assertTrue(entries.contains(B)); assertTrue(entries.contains(C)); assertTrue(entries.contains(D));
A megvalósitás működése
Az AbstractSetTestcase osztály a Testcase osztályt bővíti ki, így az ]Unit-kompa
tibilis tesztosztály lehet. Ezenkívül a példa kedvéért definiál néhány bejegyzést, va
lamint egyleképezést a teszteléshez. A leképezés a setUp() metódusban kap értéket,
amely közvetlenül a tesztesetek előtt fut le, és a mintabejegyzések első négy kulcsát a
leképezésben lévő megfelelő értékeihez rendeli hozzá.
Az absztrakt createMap() metódus az AbstractMapTestcase minden konkrét
alosztályában végrehajtódik, és ahol a leképezés meghatározott példányát létrehozza,
ott teszteljük:
368
package com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.Reverseiterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List; import junit.framework.Testcase;
public abstract class AbstractMapTestcase extends Testcase { private static final Map.Entry A new oefaultEntry("akey",
"avalue"); private static fi nal Map.Entry B new oefaultEntry("bkey",
"bvalue"); private static fi nal Map.Entry c new DefaultEntry("ckey",
"cvalue"); private static fi nal Map.Entry D new oefaultEntry("dkey",
"dvalue"); private static final Map.Entry E new oefaultEntry("ekey",
"evalue"); private static fi nal Map.Entry F new oefaultEntry("fkey",
"fvalue");
private Map _map;
protected void setUp() throws Exception { super. setup();
_map= createMap();
Leképezésmegvalósítások vizsgálata
}
}
_map.set(c.getKey(), c.getvalue()); _map.set(A.getKey(), A.getvalue());
_map.set(B.getKey(), B.getvalue());
_map.set(D.getKey(), D.getvalue());
protected abstract Map createMap();
A containsO metódus true értékkel térhet vissza bármely olyan kulcs esetében,
amely a leképezésben megtalálható, különben fa l se értékkel. Tudjuk, hogy a négy
példabeli kulcs biztosan létezik, így a testeonta i nsExi st i n g O metódusban ellen
őrizhe�ük, hogy a contai nsOminden esetben true értékkel tér vissza.
public void testcontainsExisting() {
assertTrue(_map.contains(A.getKey()));
assertTrue(_map.contains(B.getKey()));
assertTrue(_map.contains(C.getKey()));
assertTrue(_map.contains(D.getKey()));
}
A testeonta i nsNonExi st i n g O metódussal viszont arról bizonyosodhatunk meg,
hogy a containsO metódus false értékkel tér vissza azokra a kulcsokra, amelyek
nem léteznek:
public void testcontainsNonExisting() {
assertFalse(_map.contains(E.getKey()));
assertFalse(_map.contains(F.getKey()));
}
Ezután a testGetExi st i n g O megerősíti, hogy a get O minden egyes, a setup()
metódusban hozzárendelt kulcsra helyes értékkel tér vissza:
public void testGetExisting() {
assertEquals(A.getvalue(), _map.get(A.getKey())); assertEquals(B.getvalue(), _map.get(B.getKey()));
assertEquals(C.getvalue(), _map.get(c.getKey()));
assertEquals(D.getvalue(), _map.get(D.getKey()));
}
Hasonlóképpen a testGetNonExi sti ngO azt igazolja, hogy null érték tér vissza ar
ra a pár kulcsra, amely a leképezésben nem létezik:
public void testGetNonExisting() {
assertNull(_map.get(E.getKey())); assertNull(_map.get(F.getKey()));
}
369
Leképezések
A testsetNewKey() metódus azt igazolja, hogy sikeresen ki tudjuk olvasni a tárolt
értékeket. Miután először ellenőrizte a leképezés kezdeti méretét, két kulcsot, illetve
értékpárt ad hozzá. A set() metódus meghívásakor minden alkalommal megbizo
nyosodunk róla, hogy null értékkel tér vissza, jelezve, hogy nem volt létező érték; a
get() metódust azért hívjuk meg, hogy biztosítsa, az új kulcshoz hozzárendeltük az
értéket, majd ellenőrizzük, valóban megnövekedett-e eggyel a méret:
public void testSetNewKey() { assertEquals(4, _map.size());
}
assertNull(_map.set(E.getKey(), E.getvalue())); assertEquals(E.getvalue(), _map.get(E.getKey())); assertEquals(S, _map.size());
assertNull(_map.set(F.getKey(), F.getvalue())); assertEquals(F.getvalue(), _map.get(F.getKey())); assertEquals(6, _map.size());
A testsetExi sti ngKey() metódus először ellenőrzi a leképezés kezdeti méretét. Ez
után meghívódik a set() metódus, hogy egy létező kulccsal összekapcsoljon egy új ér
téket, és ellenőrizzük, hogy a visszatérési érték egyezik-e az eredetivel. Keresést hajt
végre, hogy megbizonyosadjon róla, hogy az új érték a kulcshoz van hozzárendelve.
Végül összeve�ük az eredeti méretével, hogy meggyőződjünk róla, hogy nem változott:
public void testSetExistingKey() { assertEquals(4, _map.size());
}
as sertEqua ls (C. getval u e(), _map. set(c. getKey O, "cva l u e 2")); assertEquals (" cva l u e 2", _map. get (C. getKey O)); assertEquals(4, _map.size());
Ezt követően a testDeleteExisting() metódus meghívja a delete()-et, hogy a
setup()-ban hozzáadott minden egyes kulcsot eltávolítson, és ellenőrzi a visszatérő
értéket. Ezután a contai ns () metódust hívja meg, hogy megerősítse, a kulcs már
nem létezik, és bizonyságot szerez róla, hogy csökkentette a méretet:
370
public void testDeleteExisting() { assertEquals(4, _map.size());
assertEquals(B.getvalue(), _map.delete(B.getKey())); assertFalse(_map.contains(B.getKey())); assertEquals(3, _map.size());
assertEquals(A.getvalue(), _map.delete(A.getKey())); assertFalse(_map.contains(A.getKey())); assertEquals(2, _map.size());
Leképezésmegvalósítások vizsgálata
}
assertEquals(c.getValue(), _map.delete(C.getKey())); assertFalse(_map.contains(c.getKey())); assertEquals(l, _map.size());
assertEquals(D.getvalue(), _map.delete(D.getKey())); assertFalse(_map.contains(D.getKey())); assertEquals(O, _map.size());
A leképezés méretének ellenőrzése után a testDel eteNonExi st i n g O metódus
meghívja a de l e te O metódust, hogy töröljön egy nem létező kulcsot. Ezután ellen
őrzi a visszatérő értéket, hogy meggyőződj ön róla, hogy nu ll, és még egyszer ellen
őrzi a méretet, hogy biztosan nem változott-e:
public void testDeleteNonExisting() { assertEquals(4, _map.size()); assertNull(_map.delete(E.getKey())); assertEquals(4, _map.size()); assertNull(_map.delete(F.getKey())); assertEquals(4, _map.size());
}
A testel e ar() metódus először ellenőrzi, nem üres-e már a leképezés. Ezután a
cl e ar O metódust hívja meg, és újra ellenőrzi a méretet annak megerősítéseként,
hogy nullára állította. Végül a contai ns() metódust hívja meg minden egyes eredeti
kulcshoz, hogy igazolja, egyik sem létezik:
public void testclear() { assertEquals(4, _map.size()); assertFalse(_map.isEmpty());
}
_map.clear();
assertEquals(O, _map.size()); assertTrue(_map.isEmpty());
assertFalse(_map.contains(A.getKey())); assertFalse(_map.contains(B.getKey())); assertFalse(_map.contains(C.getKey())); assertFalse(_map.contains(D.getKey()));
Az i teráto r teszteléséhez szükséges csaknem összes munkát a checkrteratorO
ban végezzük el. Ez a metódus végigfut a leképezésben lévő összes bejegyzésen.
Minden alkalommal, ha egy bejegyzést kapunk, a kulcsot és az értéket arra használjuk,
hogy létrehozzuk a Defaul tEntry-t, amelyet ezután hozzáadunk a listához. Ezután
ellenőrizzük a listát, hogy megbizonyosodjunk róla, a méret megegyezik a bejegyzések
371
Leképezések
elvárt számával, valamint arról, hogy minden egyes elvárt bejegyzés létezik. Mért ne
adnánk hozzá a bejegyzéseket, ahogy visszatérnek magából a leképezésből? A válasz
meglehetősen bonyolult, és tisztában kell lennünk azzal, hogy nem csak erre a pél
dányra érvényes, hanem akkor is, amikor általában az interfészekkel dolgozunk.
A con ta i ns () metódust azért lúvjuk meg, hogy eldöntsük, hogy a várt bejegy
zések léteznek-e a listában, amely aztán meglúvja az e qua ls() metódust, hogy el
döntse, a keresett bejegyzés egyezik-e a listában bármelyikkeL Most idézzük fel, hogy
a Map. En try interfész, így az iterátortól visszatérő bejegyzések bármelyik osztályhoz
tartozhatnak, amely a Map. Entry-t hozza létre, nem feltétlenül a oefaul tEntry-t. Ez
azt jelenti, hogy nincs arra garancia, hogy az e qua ls() metódust végrehajtottuk, il
letve arra, hogy úgy fog működni, ahogyan kell, ha összehasonlítjuk a Default
Entry-veL (Az equals() tárgyalását lásd az Eifective]ava-ban[Block, 2001].) Ezért
ahelyett, hogy a szerencsére bíznánk a dolgot, és reménykednénk, hogy minden a
legnagyobb rendben lesz, vettük a kulcsot, illetve az értékpárokat és a Defaul tEntry
példányaként a listához adtuk őket, amely, mint tudjuk, létrehozza az e qua ls() me
tódust és ez ugyanaz az osztály, mint a várt bejegyzések:
private void checkrterator(Iterator i) { List entries = new LinkedList();
}
for (i.first(); !i.isDone(); i.next()) {
}
Map.Entry entry = (Map.Entry) i.current(); entries.add(new DefaultEntry(entry.getKey(),
entry.getvalue()));
try { i .current();
fai l O;
} catch (IteratoroutOfBoundsException e) {
ll ezt várjuk
}
assertEquals(4, entries.size()); assertTrue(entries.contains(A)); assertTrue(entries.contains(B)); assertTrue(entri es. contai ns(C)) ;.
assertTrue(entries.contains(D));
Majd az előrehaladó iteráció tesztelése érdekében a testiteratorForwards O me
tódus egyszerűen kap egy iterátort a leképezésből, majd átadja a checkrterator()
metódusnak:
372
public void testrteratorForwards() { checkrterator(_map.iterator());
}
Listaleképezés
Végül a fordított iterátor teszteléséhez a testrteratorBackwards() metódus a check
IteratorD metódus meghívása előtt Reversenerator-ba burkolja az iterátort Oásd 2. fejezet). Így minden fi r st() és next() metódushívás automatikusan l ast() és previous() hívássá alakul, tehát nem szükséges külön tesztcsomagot írni emiatt:
public void testiteratorBackwards() { checkrterator(new Reverserterator(_map.iterator()));
}
Listaleképezés
A következő gyakorlatban olyan leképezést hozunk létre, amelynek a mögöttes tárolási mechanizmusa a lista lesz. A megvalósítás meglehetősen lényegre törő és könynyerr követhető; ennek ellenére nem kimondottan hatékony, de kisebb adathalmazokhoz hasznos lehet.
Gyakorlófeladat: listaleképezés megvalósítása és tesztelése
Kezdjük a L istMapTest létrehozásával a következőképpen:
package coiii� w r o iC: a l gorittims . maps;
public class ListMapTest extends AbstractMapTestcase { protected Map createMap() {
return new ListMap(); }
.. }
Ezt követően alkossuk meg magát a L istMap osztályt:
·�"-packá9e--com:Wrox:ar9'órTtt1íiíS:.maps·;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
public class ListMap implements Map { private final List _entries =new LinkedList();
public object get(Object key) { oefaultEntry entry = entryFor(key); return entry != null ? entry.getvalue()
L . __ _._} _ _ _ _ _ _ _
null;
373
Leképezések
}
374
public Object set(Object key, object value) { oefaultEntry entry = entryFor(key);
}
if (entry != null) { return entry.setvalue(value);
}
_entries.add(new oefaultEntry(key, value)); return null;
public Object delete(Object key) { oefaultEntry entry entryFor(key); if (entry == null) {
}
return null; }
_entries.delete(entry); return entry.getvalue();
public boolean contains(Object key) { return entryFor(key) != null;
}
public void clear() { _entries.clear();
}
public int size() { return _entries.size();
}
public boolean isEmpty() { return _entries.isEmpty();
}
public Iterater iterator() { return _entries.iterator();
}
private oefaultEntry entryFor(Object key) { Iterater i = iterator();
}
for (i.first(); !i.isoone(); i.next()) { oefaultEntry entry = (oefaultEntry) i.current(); if (entry.getKey().equals(key)) {
return entry; }
}
return null;
Listaleképezés
A megvalósitás müködése
Mivel magukat a teszteseteket már elkészítettük, csak annyit kell tennünk, hogy a
L i stMapTest-et kiterjesztjük az AbstractMapTest-re és létrehozzuk a createMap()
et azért, hogy visszakapjukListMap osztályunk egy példányát:
package com.wrox.algorithms.maps;
public class ListMapTest extends AbstractMapTestCase { protected Map createMap() {
return new ListMap();
} }
A tesztekkel továbbhaladunk a L istMap osztály formájában magához a leképezés
megvalósításához. Ez az osztály semmi mást nem tartalmaz, mint azt a listát, amelyet a
tárolt bejegyzések elraktározására fogunk használni. A clear(), size(), isEmpty és
i terator() metódusok mindegyike az azonos nevű metódusokhoz delegál:
package com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
public class ListMap implements Map {
}
private final List _entries = new LinkedList();
public void clear() { _entries.clear();
}
public int size() { return _entries.size();
}
public boolean isEmpty() { return _entries.isEmpty();
}
public Iterator iterator() { return _entries.iterator();
}
375
Leképezések
A privát entryFor() metódus egy megadott Iruleshoz tartozó bejegyzést kap meg (ha
létezik). Ez a metódus egyszerűen megismétlődik a listában lévő összes bejegyzésen ke
resztül, összehasonlítva a bejegyzés kulcsát a keresési kulccsal. Ha illeszkedő bejegyzést
talál, ez visszatér, különben null érték tér vissza jelezve, hogy nincs ilyen bejegyzés:
private DefaultEntry entryFor(object key) { Iterator i = iterator();
}
for (i.first(); !i.isDone(); i.next()) { DefaultEntry entry = (DefaultEntry) i.current(); if (entry.getKey().equals(key)) {
return entry;
} }
return null;
Ezt alapul véve, végrehajtjuk a get() metódust, hogy visszakapjuk a hozzárendelt
értéket. Ebben a metódusban az entryFor()-t azért hívjuk meg, hogy megtalálja az
adott kulcshoz tartozó megfelelő bejegyzést. Ha talál ilyen bejegyzést (e nt r y ! =
null) , visszaadja a hozzá tartozó értéket; egyébként null értékkel tér vissza, jelezve,
hogy ilyen kulcs nem található:
public object get(Object key) { DefaultEntry entry = entryFor(key); return entry != null ? entry.getvalue()
}
null;
Hasonlóképpen végrehajtjuk a con ta i ns () metódust is, megpróbálva egy megadott
kulcshoz tarozó bejegyzést találni és true visszatérési értéket kapni, ha létezik:
public boolean contains(Object key) { return entryFor(key) != null;
}
A set() metódus először az entryFor() metódust hívja meg, hogy eldöntse, tarto
zik-e már bejegyzés a megadott kulcshoz. Ha talál bejegyzést, akkor ennek értékét
frissíti, és a régi érték visszatér. Ha viszont nem talál illeszkedő bejegyzést, akkor egy
újat ad hozzá az alaplista végéhez, és ennek megfelelően null értékkel tér vissza:
376
public object set(Object key, object value) { DefaultEntry entry = entryFor(key);
}
if (entry != null) { return entry.setvalue(value);
}
_entries.add(new DefaultEntry(key, value)); return null;
Hasítóleképezés
Végül a de l e te() metódus hívódik meg, hogy eltávolítsa a kulcsot, illetve értékpárt
a leképezésből. Ahogy az előző metódusnál, a del ete() metódus az entryFor()
meghívásával kezdődik. Ebben az esetben, ha nem talál bejegyzést, null értékkel tér
vissza, jelezve, hogy a kulcs nem létezik; máskülönben a bejegyzés törlődik az alap
listából, és az érték visszatér a hívóhoz:
public object delete(Object key) { DefaultEntry entry = entryFor(key); if (entry == null) {
}
return null;
}
_entries.delete(entry); return entry.getvalue();
Elkészültünk megvan az első leképezésmegvalósításunk. A L i stMap() osztályhoz
tartozó kód nagyon egyszerű, és a munka legnagyobb részét az alaplista hajtja végre.
Ebben az esetben, az egyszerűségnek ára van: a L istMap működése függ az alaplista
múködésétől, amely O(N). Ez nem igazán hatékony, de a viszonylag kis adathalma
zokra elég lehet a listaalapú leképezés.
Hasitóleképezés
A leképezés következő típusa, amelyet létre fogunk hozni, a hasítótáblákon alapszik (a
11. fejezetben tárgyaltuk). Ezen a ponton felfrissíthetjük a hasítókoncepciókra- külö
nösen a hasítótáblákra, amelyek vödröket használnak- és a Bucket i ngHashtab l e osz
tályra vonatkozó kódot.
A következő feladatot tesztek létrehozásával kezdjük, amelyek a megfelelő műkö
dést fogják biztosítani, mielőtt létrehoznánk a teljes hasítóleképezés implementációját.
Gyakorlófeladat: hasítóleképezés megvalósítása és tesztelése
Alkossuk meg a tesztosztályt a következőképpen:
package-
com. wroX.a l goritlims. maps;
public class HashMapTest extends AbstractMapTestcase { protected Map createMap() {
return new HashMap();
}
}
377
Leképezések
Majd hozzuk létre a hasítóleképezés megvalósítását:
378
package com.wrox.algorithms.maps;
import com.wrox.algorithms.hashing.Hashtableiterator; import com.wrox.algorithms.iteration.Arrayrterator; import com.wrox.algorithms.iteration.Iterator;
public class HashMap implements Map { public static final int DEFAULT_CAPACITY = 17; public static final float DEFAULT_LOAD_FACTOR 0.75f;
private final int _initialcapacity; private final float _loadFactor; private ListMap[] _buckets; private int _size;
public HashMap() { this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialcapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialcapacity, float loadFactor) { assert initialcapacity > O : "az 'initialcapacity' nem
lehet <l";
}
assert loadFactor > O : "a 'l oadFactor' nem l ehet <= O";
_initialcapacity = initialcapacity; _loadFactor = loadFactor; clear();
public object get(Object key) {
}
ListMap bucket = _buckets[bucketindexFor(key)]; return bucket != null? bucket.get(key) : null;
public object set(Object key, object value) { ListMap bucket bucketFor(key);
.}.
int sizeBetore bucket.size(); object oldvalue = bucket.set(key, value); if (bucket.size() > sizesefore) {
++_size; mai ntai n Load();
}
return oldvalue;
Hasítóleképezés
pulllic Ob-ject-del'eteCőoject 1<ey
}
ListMap bucket = _buckets[bucketindexFor(key)]; if (bucket == null) {
return null; }
int sizeBefore = bucket.size(); object value = bucket.delete(key); if (bucket.size() < sizeBefore) {
--_size; }
return value;
public boolean contains(Object key) {
}
ListMap bucket = _buckets[bucketindexFor(key)]; return bucket l= null && bucket.contains(key);
public Iterator iterator() { return new Hashtableiterator(new Arrayiterator(_buckets));
}
public void clear() { _buckets = new ListMap[_initialcapacity]; _size = O;
}
public int size() { return _size;
}
public boolean isEmpty() { return size() == O;
}
private int bucketindexFor(Object key) {
}
assert key != null : "a 'key' (kulcs) nem lehet NULL"; return Math.abs(key.hashcode() %._buckets.length);
private ListMap bucketFor(Object key) { int bucketindex = bucketindexFor(key); ListMap bucket = _buckets[bucketindex]; if (bucket == null) {
}
}
bucket = new ListMap(); _buckets[bucketindex] = bucket;
return bucket;
379
Leképezések
private void maintainLoad() { if (loadFactorExceeded()) {
resize(); }
}
private boolean loadFactorExceeded() { return size()> _buckets.length * _loadFactor;
}
private void resize() {
}
HashMap copy= new HashMap(_buckets.length *2, _loadFactor);
for (int i =O; i < _buckets.length; ++i) { if (_buckets[i] != null) {
copy.addAll(_buckets[i].iterator()); }
}
_buckets = copy._buckets;
private void addAll(Iterator entries) { assert entries != null : "az 'entries' na lehet null";
for (entries.first(); !entries.isoone(); entries.next()) { Map.Entry entry = (Map.Entry) entries.current(); set(entry.getKey(), entry.getYalue());
}
�--·-�-- -� ----
A megvalósitás müködése
A HashMapTest osztály az AbstractMapTestCase osztályt bővíti ki, hogy újrahasz
nosítsuk az összes korábbi tesztünket. Egyedüli feladatunk a createMap() metódus
megvalósítása, annak érdekében, hogy visszakapjuk a HashMap osztály egy példányát.
A HashMap osztály a legtöbb helyen a 11. fejezetben bemutatott Bucket ing
Hashtab l e osztály kódjának másolata. Ezért csak a HashMap és az eredeti Bucketi ng
Hashtab l e kódjának különbségeire koncentrálunk
A Map interfész megvalósítása mellett az első, amit észre fogunk venni - néhány
konstans és kényelmi konstruktor mellett -, talán az, hogy vödröknek az eredeti
BucketingHashtable-ben lévő List helyett egy ListMap-et használtunk A hasító
leképezést úgy képzelhetjük el, mint egy kulcsok, illetve értékpárok és listaleképezé
sek közötti (remélhetőleg nagyban egyenlő) elosztást. A 11. fejezetból tudjuk, hogy a
vödrök viszonylag kicsik maradnak, így a lista alapú leképezések jól működnek majd.
380
Faleképezés
Ezért, ha vödreink részére lista helyett leképezést használunk, akkor egyszerűsí�ük
a kódot - ha megtaláltuk a megfelelő vödröt, a kulcs, illetve az értékpár hozzáadásá
hoz szükséges minden munkát ennek adjuk át. Ez látható a get O, set O, de l et e O és
contai nsO metódusok kódjában, ahol a legtöbb munkát a vödör végzi, hagyva, hogy
a rutinmunkát, például az újraméretezést stb., a hasítóleképezés kódja végezze el.
A másik nyilvánvaló különbség a HashMap és a Bucketi ngHashtab l e között az,
hogy a vödrök tárolják a bejegyzéseket. Ezért ha méretet változtatunk, az ad dA ll O
metódus inkább végigfut minden egyes kulcson és értékpáron, mintsem az értéke
ken, ahogy az eredeti esetében tette.
Végül mivel a Map Iter ab l e is egyben- ebből következően a L istMap is az -
újrahasznosíthatjuk a Hashtableiterator-t a 11. fejezetből, hogy végigmenjünk a
bejegyzéseken. -
Ha a kulcsoknak feltételezünk egy jó hasítófüggvényt, a HashMap várhatóan meg
fogja közeliteni az 0(1) teljesítményt.
Faleképezés
Ahogy már említettük, a leképezések általában nem biztosítanak semmilyen különleges
kulcselrendezést: például a L istMap beszúrási sorrendben fogja mutatni a bejegyzése
ket, míg a HashMap iterátorból származó bejegyzések véletlen módon fognak megje
lenni. Néha azonban szükségünk lehet a kulcsok előre meghatározható sorrendjére.
Ebben az esetben a bináris keresőfára alapozott leképezés megvalósítása a megfelelő.
A faalapú leképezések megvalósításának elkezdése előtt érdemes feleleveníteni a
bináris keresőfáról olvasottakat (lásd 10. fejezet), hogy felfrissítsük és megértsük az
alapvető elképzeléseket és a kódot, mivel a leírás ismét csak a TreeMap kód és az
eredeti B i narysearchTree kód közötti különbségekre korlátozódik.
Gyakorlófeladat: fahalmaz megvalósítása és tesztelése
Kezdjük a TreeMapTest osztály létrehozásával:
- pac:'i(age-cOíl. wrox. a l gorithiis�ups;
public class TreeMapTest extends AbstractMapTestcase { protected Map createMap(){
} _L
return new TreeMap();
.-, .. _ ......... ---...._ .�--- �---- - - ��-- � -- -·---- - ---- -
Kísérjük figyelemmel a faleképezés megvalósítását:
381
Leképezések
382
package com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutofsoundsException; import com.wrox.algorithms.sorting.Comparator; import com.wrox.algorithms.sorting.Naturalcomparator;
public class TreeMap implements Map { private final comparator _comparator; private Node _root; private int _size;
public TreeMap() { this(Naturalcomparator.INSTANCE);
}
public TreeMap(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator;
}
public boolean contains(object key) { return search(key) != null;
}
public object get(Object key) { Node node = search(key); return node != null ? node.getvalue()
}
null;
public Object set(Object key, object value) { Node parent = null; Node node = _root;
int cmp = O;
while (node != null) { parent = node; cmp = _comparator.compare(key, node.getKey()); if (cmp == O) {
}
return node.setvalue(value);
}
node cmp < O ? node.getsmaller() node.getLarger();
Node inserted = new Node(parent, key, value);
Faleképezés
}
r-else�{ parent.setLarger(inserted);
}
++_size; return null;
public object delete(Object key) { Node node= search(key); if (node == null) {
return null; }
Node deleted = node.getsmaller() != null && node.getLarger() !=.null ? node.successor() : node;
assert deleted != null : "a 'deleted' nem lehet null'';
Node replacement = deleted.getsmaller() != null ? deleted.getsmaller() : deleted.getLarger();
if (replacement != null) { replacement.setParent(deleted.getParent());
}
}
if (deleted == _root) { _root � replacement;
} else if (deleted.issmaller()) { deleted.getParent().setsmaller(replacement);
} else { deleted.getParent().setLarger(replacement);
}
if (deleted != node) {
}
object deletedvalue = node.getvalue(); node.setKey(deleted.getKey()); node.setvalue(deleted.getvalue()); deleted.setvalue(deletedvalue);
--_size; return deleted.getvalue();
public Iterator iterator() { return new Entryrterator();
}
public void clear() { _root= null; _size = O;
}
383
Leképezések
384
publ-ic int size() { return _size;
}
public boolean isEmpty() { return _root == null;
}
private Node search(object value) {
}
assert value != null : "az érték nem lehet NULL";
Node node = _root;
while (node != null) {
}
int cmp = _comparator.compare(value, node.getKey()); if (cmp = O) {
break; }
node = cmp <O? node.getsmaller() node.getLarger();
return node;
private static final class Node implements Map.Entry { private object _key; private object _value; private Node _parent; private Node _smaller; private Node _larger;
public Node(Node parent, object key, object value) { setKey(key); setvalue(value); setParent(parent);
}
public object getKey() { return _key;
}
public void setKey(Object key) {
}
assert key != null : "a 'key' (kulcs) nem lehet NULL"; _key = key;
public object getvalue() { return _value;
Faleképezés
PtibfiE Ob]ectsetvaiueCob)éct'V'alue) -{
}
object oldValue = _value; _value = value; return oldValue;
public Node getParent() { return _parent;
}
public void setParent(Node parent) { _parent = parent;
}
public Node getsmaller() { return _smaller;
}
public void setsmaller(Node node) { assert node != getlarger() : "a 'smaller' (kisebb) nem lehet
azonos.a 'larger'-rel (nagyobbal)";
_sma ll e r = node ; }
public Node getLarger() { return �larger;
}
public void setLarger(Node node) {
}
assert node != getsmaller() : "a 'larger' (nagyobb) nem lehet azonos a 'smaller'-rel (kisebbel)";
_larger = node;
public boolean isSmaller() { return getParent() != null &&
this == getParent().getsmaller(); }
public boolean isLarger() { return getParent() != null &&
}
public Node minimum() { Node node = this;
this== getParent().getLarger();
while (node.getsmaller() != null) { node= node.getsmaller();
L... .} _ ___ _______________ __,
385
Leképezések
386
}
return node;
}
public Node maximum() { Node node = this;
}
while (node.getLarger() != null) { node= node.getLarger();
}
return node;
public Node successor() {
}
if (getLarger() != null) { return getLarger().minimum();
}
Node node = this;
while (node.isLarger()) { node= node.getParent();
}
return node.getParent();
public Node predecessor() {
}
if (getsmaller() != null) { return getsmaller().maximum();
}
Node node = this;
while (node.issmaller()) { node = node.getParent();
}
return node.getParent();
private final class Entryiterator implements Iterator { private Node _current;
public void first() {
}
_current= _root != null ? _root.minimum() null;
public void last() { _current= _root != null ? _root.maximum() null;
Faleképezés
} }.
public· boólean-· isöoneO- {
return _current== null; }
public void next() { if (!isoone()) {
_current= _current.successor(); }
}
public void previous() { if C ! i soon e O) {
_current= _current.predecessor(); }
}
public object current() throws IteratoroutofsoundsException { if (isoone()) {
throw new IteratoroutofsoundsException(); } return _current;
}
A megvalósitás müködése
A TreeMapTest osztály az AbstractMapTestcase osztályt bővíti ki, így újrahaszno
síthatjuk az összes createMap()-pel korábban létrehozott tesztünket, visszakapva
így a Treemap osztály egy példányát.
A TreeMap kódja nagyrészt megegyezik a 10. fejezetben létrehozott B i nary
searchTree kódjávaL Azonkívül, hogy ez az osztály implementálja a Map interfészt, a
legnyilvánvalóbb különbséget abban fogjuk látni, ha átvizsgáljuk a kódot, hogy
majdnem rnindenhol, ahol az eredeti kód értékre hivatkozik, a TreeMap a kulcsot
használja. E célból az összehasonlítót kulcsok összehasonlítására használjuk, nem
pedig értékekére, így a fa kulcs szerint rendezett lesz.
Azt is észrevesszük majd, hogy a Node belső osztály lett; és ahelyett hogy rnin
den egyes csomópont tartalmazna egy bejegyzést, a Node-ot közvetlen ül a Map. En t r y
létrehozására használjuk. Az eredeti csomópont-megvalósítás már tartalmazott egy
értéket, így csak annyit kellett tennünk, hogy hozzáadtunk egy kulcsot, és kissé mó
dosítottuk a se tv a l u e() -t, hogy az eredeti értéket adja vissza, csakúgy, mint azt a
oe fau l tEn t r y-vel korábban tettük.
Ezután az eredeti i n se rt() metódus nevét set() metódusra változtattuk Míg az
eredeti insert() metódus feldolgozta az értékeket és megengedte a duplikáció kat, a
leképezés kulcsokat használ, amelyek mindegyikének egyedinek kell lennie. Ráadásul
a set() minden előzőleg a kulcshoz rendelt értéket visszaad.
Az eredeti i n se rt() metódus whi l e ciklusa így nézett ki:
387
Leképezések
while (node ! = null) { parent = node;
}
cmp = _comparator.compare(value, node.getvalue()); node = cmp <=O? node.getsmaller() : node.getLarger();
Láthatjuk, hogy kétszerezett érték beszúrásakor az új érték a már létező ugyanolyan
érték bal oldali gyermeke lett. A set() metódus most így fest:
while (node ! = null) { parent = node; cmp = _comparator.compare(key, node.getKey()); if (cmp == O) {
return node.setvalue(value);
}
node cmp < O ? node.getsmaller() node.getLarger();
}
Itt, ha az érték már létezik (cmp == O) , a metódus frissíti az értéket, és azonnal visz
szaadja a régit; ellenkező esetben az eredetivel megegyezően működik.
A másik változás, hogy a search() privát metódussá vált, helyét pedig a
contai ns () vette át, ahogy a Map interfész megköveteli. A con ta i ns () metódus
csak abban az esetben tér vissza true értékkel, ha a search() metódus illeszkedő
csomópontot talál.
A clear(), isEmpty(), size() és iterator() metódusok hozzáadásán kívül
- amit a Map interfész követelt meg - az egyeden lényeges különbség az, hogy az
Entryrterator belső osztály, amely sorrendben, előre vagy hátra iterál a csomó
pontokon és ezért a bejegyzések, a successor() és a predecessor() meghívásával.
Meg is vagyunk: egy olyan leképezésmegvalósítás, amilyet a 10. fejezetből isme
rünk, ádagos teljesítménye o(log N), és van egy előnye is, mégpedig az, hogy a be
jegyzéseket sorrendben, kulcs szerint rendezve tartja meg.
Összefoglalás
A fejezet a következőket tárgyalta:
388
• A leképezések a kulcshoz rendelt értékeket tárolják.
• A leképezésen belül minden egyes kulcs egyedi, és lehetővé teszi számunkra,
hogy gyorsan megtalálhassuk a hozzárendelt értékét.
• A leképezéseket asszociatív tömbként, szótárakként, indexekként és fellapo
zási táblázatokként is ismerjük.
• Egy leképezés általában nem biztosít iterációs sorrendet.
Gyakorlatok
• Három ismert leképezésmegvalósítás létezik: a listaleképezés, a hasítóleké
pezés és a faleképezés.
• A lista alapú leképezések viszonylag kis mennyiségű adatok tárolására al
kalmasak, mivel a futásidejük O(N).
• A hasítótáblaalapú leképezés futásideje véletlenszerű iterációs sorrend ese
ténO(l).
• A bináris keresőfa-alapú halmazok teljesítménye O(l o g N), és iterációs sor
rendjük megjósolható.
Gyakorlatok
1. Hozzunk létre egy iterátort, amely csak a leképezésben lévő kulcsokkal tér vissza!
2. Hozzunk létre egy iterátort, amely csak a leképezésben lévő értékekkel tér vissza!
3. Hozzunk létre egy olyan halmazmegvalósítást, amely mögöttes tárolási mecha
nizmusaként használ leképezést az értékek számára!
4. Hozzunk létre egy olyan üres leképezést, amely unsupportedoperati onExcepti on
kivételt okoz, ha módosítási kísérletet akarunk rajta végrehajtani!
389
TIZENNEGYEDIK FEJEZET
Hármas keresőfák
Eddig az adatok tárolásának számos módját sajátitottunk már el - az egyszerű, ren
dezetlen listáktól a rendezetlen listákig, a bináris keresőfákig és a hasítótáblákig.
Ezek mindegyike kitűnően alkalmas tetszőleges típusú objektumok tárolására. Most
megismerünk egy utolsó, sztringek tárolására alkalmas adatstruktúrát, amely nem
csak gyors keresésre alkalmas, hanem néhány egészen más és érdekes keresési for
mát is lehetővé tesz.
A fejezetben a következő témaköröket tárgyaljuk:
• A hármas keresőfák általános tulajdonságai.
• A szavak tárolási módja.
• A szavak keresésének módja.
• A hármas keresőfák használata szótár létrehozására.
• Egyszerű alkalmazás megvalósítása a keresztrejtvények megoldásának segí
téséhez.
Hármas keresőfák
A hármas keresőfák sztringek tárolására és visszaolvasására használatos speciális
struktúrák. A bináris keresőfához hasonlóan minden csomópont tartalmaz egy hivat
kozást a kisebb és a nagyobb értékekre. Vele ell�ntétben azonban a hármas keresáfa
nem tartalmazza az összes értéket minden egyes csomópontban. Ehelyett egy cso
mópont egy szó egy betűjét tartalmazza, valamint egy másik hivatkozást - ezért hár
mas- egy csomópontokból álló részfához, amely egy másik betűt tartalmaz a szóból.
A 14.1. ábra azt mutatja, hogyan tárolhatjuk a "cup", az "ape", a "bat", a "map"
és a "man" szavakat a hármas keresőfában. A testvéreket (kisebb és nagyobb betű
ket) folytonos vonallal kötöttük össze, a gyerekeket (egymást követő betúket) pedig
szaggatott vonallal.
Bár nem úgy néz ki, de ha elhagyjuk az első szinten lévő gyerekkapcsolatokat,
tökéletesen érvényes bináris keresőfát kapunk, amely tartalmazza az a, a b, a c és az
m értéket. Minden egyes csomópontból kiindulva hozzáadtunk egy extra hivatkozást
a csomópont gyerekéhez.
Hármas keresőfák
C!J . Gc;J
GJ 14. 1. ábra. Minta hármas keresőfára, ahol
"c"jeliili a !!Jiökeret. A kiemeit csomópontok
jelölik a )}bat"-hez vezető utat
Minden szinten, csakúgy, rrúnt a bináris keresáfa esetén, rrúndegyik bal oldali csomópont kisebb (b nagyobb rrúnt a, amely kisebb rrúnt c, amely kisebb rrúnt m) , és minden egyes gyermekcsomópont a szóban lévő következő betűt mutatja (b után a jön, amelyet t követ a "hat" szóban).
Szó keresése
A fa núnden szintjén elvégezzük a bináris keresést a szó első betűjétól a keresett betűig. A bináris keresőfában való kereséshez hasonlóan a gyökértól indulunk, és a megfelelő módon követjük a kapcsolatokat balra és jobbra. Ha találunk egy illeszkedó csomópontot, akkor egy szintet lejjebb megyünk annak gyerekéhez, és újrakezdjük a keresést, de ilyenkor már a szó második betűjéveL Ez addig folytatódik, arrúg vagy meg nem találjuk az összes betűt - amely esetben egyezést találunk -, vagy el nem fogynak a csomópontok
Ahhoz, hogy megfelelő képet kapjunk arról, hogyan működik a hármas keresófa, a 14.1. ábrán azt a példát láthatjuk, hogyan megy végbe a fában a keresés a "hat" szóra.
A keresés a gyökércsomópontban (c) kezdődik, a keresett szó első betűjét keresve, az a-t, arrúnt azt a 14.2. ábra mutatja.
Mivel még nincs találatunk, meg kell vizsgálnunk az egyik testvért (ha van), és újra kell próbálkoznunk. Ebben az esetben keresóbetűnk, az a az aktuális csomópont (c) előtt választódik ki, így a bal oldali testvérntl próbálkozunk (lásd 14.3. ábra).
A következőkben összehasonlítjuk a keresett betűnket a következő csomópontban lévő betűvel, de újra nem egyezik. Mivel b a után választódik ki, ezúttal a jobb linket kell követnünk (lásd 14.4. ábra).
Végül egyezést kapunk a szó első betűj énél, így tovább léphetünk a második betűre (a) a b első gyerekével kezdve, ahogy a 14.5. ábrán látható.
392
Hármas keresőfák
cb . Gc;J
8
c;J
.� 14. 2. ábra. Az első betű keresése a fa gyökerében
c;J , Gc;J
8
�
14.3. ábra. A b betű a c előtt válasifódik ki, ezért a bal oldali linket követjük
LtJ G�
8
L;]
qJ
14.4. ábra. A b betű az a után válasifódik ki, ezért a jobb oldali linket kö.vetjük
Ezúttal közvetlenül kapjuk meg a találatot, így megismételve az eljárást a harmaclik
betűhöz jutunk (t), és a következő gyermekcsomópontban folytatjuk a keresést Oásd
a 14.6. ábrán).
393
Hármas keresőfák
14.5. ábra. A krivetkező betű keresése az első gyermekben kezdődik
o
o
!_i]
14.6. ábra. A keresés akkor jqeződik be, ha az utolsó betűt is megtaláltuk
Ismételten illeszkedő betűt találunk, és mivel a keresett szónak nincs több betűje, egy illeszkedő szót találunk, és így összesen öt karakter összehasonlítást végeztünk.
Minden egyes szinten bináris keresőfában keressük a betűt. Ha megtaláltuk, egy szinttel lejjebb megyünk, és másik keresést indítunk a bináris keresőfában a következő betű keresésére. Ezt a keresett szó összes betűjével elvégezzük. Innentől következtetni tudunk a keresés végrehajtásának futásidejére hármas keresőfánkban. Észrevehetjük, hogy a hármas keresőfák legalább annyira hatékonyak, mint a bin�risak. Az is kiderül azonban, hogy a hármas keresőfák tulajdonképpen hatékonyabbak lehetnek, mint az egyszerű bináris keresőfák
Tegyük fel, hogy a "man" szót kerestük a 14.1. ábra fájában. Számoljuk meg a karakter-összehasonlításokat! Először összehasonlítanánk az m betűt a c-vel (egy). Majd összehasonlítjuk az m betűt az m-mel (kettő), ezt követi az a az a-val (három). Elérkeztünk az utolsó betűhöz, ezért összehasonlítjuk az n betűt a p-vel (négy), és végül az n-t az n-nel (öt).
394
Hármas keresőfák
Most ugyanezt a keresést összehasonlí�uk egy bináris keresőfában végzettel, amely ugyanazokat a szavakat tartalmazza, amint ezt a 14.7. ábra muta�a. Először összehasonlí�uk a "man" szót a "cup" szóval, de az első betűnél rnivel nem illeszkedik, továbbléphetünk a következő csomópontra, amely csak egy betű-összehasonlítást végez. Majd összeve�ük a "man" szót a "map" szóval, ami három külön összehasonlítást jelent. Végezetül egybeve�ük a "man" szót a "man" szóval egy másik három összehasonlításra, ami megadja a betűnkénti összehasonlítások l + 3 + 3 = 7 végösszegét.
14.7. ábra. Ekvivalens bináris keresója
Még egy olyan egyszerű fában is, mint amelyet itt használtunk, látha�uk, hogy a hármas keresőfa jóval kevesebb karakterenkénti összeh�sonlítást végez az ekvivalens bináris keresőfával szemben. Ennek az az oka, hogy az általános előtaggal bíró szavakat tiitniiríti. Áttekintve a közös betűket, nincs szükség a további összehasonlításukra. Hasonlitsuk ezt össze egy bináris keresófával, amelyben folyamatosan összehasonlí�uk az összes betűt minden egyes csomópontból, ahogy ezt az előzőekben is láthattuk.
Emellett, hogy a pozitív eredmények megtalálásában hatékonyak legyenek, a hármas keresőfák különösen kiemelkedőek abban, hogy gyorsan elhagyják a fában nem létező szavakat. Míg egy bináris keresófa addig folytatja a keresést, amíg kifogy a leveleken lévő csomópontokból, egy hármas keresőfa abbahagyja a keresést, amint nem illeszkedő előtagot talál.
Most, hogy már világossá vált a keresés működése, kidolgozhatjuk az általános teljesítménykarakterisztikát a hármas keresőfákra. Képzeljük el, hogy minden szint tartalmazta az ábécé összes betűjét úgy elrendezve, ahogy a csomópontok vannak a bináris keresófában. Ha az ábécé méretét (például az angol ábécét a-tól z-ig) M-mel jelöljük, akkor tudjuk, hogy egy keresés egy bármely adott szinten ádagosan O(log M) összehasonlítást fog végezni. Természetesen ez csak egy betűre vonatkozik. Ahhoz, hogy megtaláljunk egy N hosszúságú szót, minden egyes betűre el kell végezni egy bináris keresést vagy O(N log M) összehasonlítást. A gyakorlatban azonban úgy tűnik, hogy a művelet az általános előtagoknak és annak köszönhetően, hogy az ábécé nem minden betűje jelenik meg a fában lévő egyes ágak minden egyes szintjén, sokkal hatékonyabb.
395
Hármas keresőfák
Szó beszúrása
Egy szó beszúrása a hármas keresőfába nem sokkal nehezebb, mint a keresés végrehajtása: egyszerűen új levélcsomópontokat adunk hozzá bármely már nem létező betűhöz. A 14.8. ábrán láthatjuk, hogy a "bats" szó beszúrása egyetlen gyermekcsomópont hozzáadását igényli, a létező "bat" szó végéhez hozzáfűzve, a "mat" szó beszúrása egyetlen csomópontot ad hozzá, a "map" -ban lévő p betű testvérekén t.
ctJ . GC!J
� GJ
14.8. ábra. A "bats'� illetve a "mat" szó beszúrása csak egyetlen tij csomópont hozzáadását igéf!Jii
Természetesen most olyan helyzetet vizsgáltunk, amikor a szavak - a "bat" és ennek többes száma, a "bats" - ugyanazzal az általános előtaggal rendelkeznek. Hogyan kü
lönböztethetjük meg ezeket? Hogyan állapí�uk meg, hogy a "bat", illetve a "bats" egy szó, de a "ba", a "b" vagy az "ap" nem?
A válasz egyszerű: minden egyes csomóponttal együtt eltárolunk néhány hozzáadott információt, amelyek jelzik, amikor megtaláltuk egy szó végét, ahogy ezt a 14.9.
ábra mutatja. Ez a kiegészítő információ felveheti az egyszerű logikai igen vagy nem jelző formáját, ha csak arra van szükségünk, hogy meghatározzuk, érvényes volt vagy nem a megadott keresett szó; illetve ez lehet a szó definíciója, ha például egy szótárt akarnánk megvalósítani. V alójában nem számít, mit használunk - a lényeg az, hogy csak azokat a csomópontokat jelöljük valahogy, amelyek egy szó utolsó betűjét képviselik.
Eddig csak kiegyensúlyozott fákat láttunk, de a bináris keresőfákhoz hasonlóan a hármas keresőfák is lehetnek kiegyensúlyozatlanok. A 14.9. ábrán látható fa a szavak ("cup", "ape", "bat", "map" és "man") sorrendben való beszúrásának az eredménye, de mi történne, ha ehelyett "ape", "bat", "cup", "man", "map" sorrendben szúrnánk be a szavakat? Sajnos a szavak inorder beszúrása a 14.10. ábrán látható, kiegyensúlyozatlan fát eredményezi.
396
0 GJctJ
WJ GJ
�
~ 14.9. ábra. A megjelbit csomópontok jelifk, hogy egy szó végét képviselik
ctJ . GJ� .
GJCZJ . G�
·� 14.10. ábra. Az inonler beszúrás kiegyensú!Jozatlan fát eredméf!Jez
Hármas keresőfák
Míg egy kiegyensúlyozott hármas keresőfában a keresés o (N log M) összehasonlítást
futtat, a kiegyensúlyozatlan fában O(NM) összehasonlítást igényel. A különbség meg
lehetősen számottevő lehet nagy M értéknél, bár a gyakorlatban a preflXIDegosztások
nak és annak köszönhetően, hogy az ábécé nem mindegyik betűje jelenik meg min
den egyes szinten, általában jobb működést figyelhetünk meg.
397
Hármas keresőfák
Prefix keresés
Talán már használtunk olyan alkalmazást, illetve weboldalt, ahol megengedett volt
úgy kiválasztani egy értéket egy listából, hogy a szó első néhány bemjét begépeljük.
Ahogy gépelünk, a lehetőségek felsorolása egészen addig szűkül, anúg végül csak
kevés értéket tartalmaz.
A hármas keresőfát olyan érdekes dologra is fel lehet használni, hogy megtalál
junk minden általános előtaggal rendelkező szót. A trükkje az, hogy csak a prefixeket
használva szabványos keresést végzünk a fán. Ezután a prefix minden részfájában
inorder bejárást alkalmazunk minden egyes szóvégjelre.
A hármas keresáfa inorder bejárása nagyon hasonlit a bináris keresőfáéra, kivéve
természetesen, hogy bele kell foglalnunk a gyermekcsomópont bejárását. Ha megta
láltuk az utolsó csomópontot a prefixben, akkor kövessük ezeket a lépéseket:
1. J árj uk be a csomópont bal oldali részfájá t!
2. Vizsgáljuk meg magát a csomópontot!
3. Járjuk be a csomópont gyerekeit!
4. Járjuk be a jobb oldali részfát!
A 14.11. ábra a "ma" prefix első egyezését mutatja.
14.11. ábra. A "ma"preftxhez tmtozó első végpont a "man"-hez tartozik
Ha megtaláltuk a prefixet, a bal oldali részfával kezdjük a bejárást, amely ebben az eset
ben a "man"-nel tér vissza. Majd magát a csomópontot járjuk be: "ma
", bár ez nem jel
zi a szó végét. Most bejárha�uk bármely gyereket, ha van. Végül bejárjuk·a jobb oldali
részfát, ahogy a 14.12. ábrán láthatjuk, és a "map"
egyezést kapjuk eredményül.
398
r;J GJ
pl
Hármas keresőfák
G
~ 14.12. ábra. A "ma"prefixhez tartozó kijvetkező végpont a "map"-hoz tartoifk
Ahogy bejárjuk a fát, akár ki is nyomtathatjuk a szavakat, amint megtaláljuk, illetve összegyűjthetjük és használhatjuk őket más módon - például kiírathatjuk őket egy listába a felhasználó számára.
Mintaillesztés
Töltöttünk már ki keresztrejtvényt, illetve játszottunk már szókirakót, és gondolkodtunk azon, hogy milyen szójöhet ki a már kirakott betűkből? Előttünk vannak a következő betűk: "a-r---t"- de semmilyen megoldás nem jut az eszünkbe.
Egy merőben új módszer, amelyben a hármas keresőfák használhatók, az ilyen problémák megoldására: minden olyan szót megtalálni, amely egyezik egy adott mintával. A minta szabályos - a-tól z-ig- betűkből áll és egy speciális helyettesítőkarakterből, amely bármire illeszkedik. Az aktuális példában a kötőjelet (-) használtuk helyettesítőkarakterként, de éppígy használhatnánk a pontot (.) vagy a kérdőjelet (?) is. Az a fontos, hogy olyat válasszunk, amely egy átlagos szóban nem fordul elő.
Már ismerősnek kell lennie, hogy hogyan működik az alapkeresés a hármas keresőfában. A legegyszerűbb módja annak, hogy mintaegyezést keressünk, talán a letámadásos módszer használata: vegyük a mintát, és alkossunk egy keresett szót úgy, hogy a helyettesítőkaraktereket helyettesítsük minden lehetséges betűkombinációval. Így az előző példánál maradva kezdhetjük az "aaraaat" szóval és folytathatjuk az
"aaraabt" és aztán az "aaraact" szóval, és így tovább egészen az "azrzzzt" szóig. Ez működne ugyan, de nagyon lassú lenne, és valószínűleg nagy arányban kapnánk eredménytelen keresést. Ehelyett sokkal kifmomultabb és hatékonyabb megközelítést alkalmazhatunk a hármas keresáfa struktúrájának felhasználásával.
A mintaillesztés hasonló az egyenes szókereséshez, kivéve azt, hogy bármikor, ha helyettesítőkarakterhez érünk, egy illeszkedő betűvel rendelkező csomópont keresése helyett (nem fogunk találni egyet sem), mindegyik csomópontot megvizsgáljuk, mintha illeszkedést találnánk.
399
Hármas keresőfák
Képzeljük el, hogy az "-a-" mintát keressük a fában a 14.1. ábrán látható mó
don. Csakúgy, mint a szabályos szókeresésnél, a gyökércsomópontból kezdjük a ku
tatást. Ebben az esetben, bár az első karakter helyettesítőkarakter, így minden egyes
csomópontot sorrendben megvizsgálunk az aktuális szinten. A 14.13. ábra megmu
tatja, hogy a keresés a legkisebb csomópontban (a) kezdődik.
14.13. ábra. A he!Jettesítőkarakter aif ké'!Jszeríti ki, hogy az aktuális sifnten
a legkisebbel kezdve minden egyes csomópontot megvizsgáljunk
Mivel helyettesítőkaraktert keresünk, azt "imitáljuk", hogy az aktuális szinten lévő
minden egyes csomópont illeszkedés, így folytatjuk a következő betű illesztését a
mintában, a gyerek aktuális csomópontján kezdve.
A 14.14. ábra azt mutatja, hogy ebben a példányban, a következő mintakarakter
(a) nem egyezik a gyermekcsomóponttal (p) így a fának ezt az ágát teljesen figyelmen
kívül hagyjuk.
14. 14. ábra. Egy nem illeszkedő karakter véget vet az adott ágon való keresésnek
Megállapítottuk, hogy nem lehetséges egyezés ezen az úton lefelé haladva, a keresés
visszamegy az előző szintre, hogy a következő legnagyobb csomópontban folytatód
jon (lásd a 14.15-ös ábrát).
400
Hármas keresőfák
)( l ll b l J l p l cb )(l � � GJ
14.15. ábra. A hefyettesítőkarakteres keresés a magasabb s'{jnten jofytatódik tovább,
a kiivetkező legnagyobb csomópontot megvizsgálva
Mivel helyettesítőkaraktert keresünk, ismételten imitáljuk, hogy egyezést találtunk, és
a minta következő betújével folytatjuk a 14.16. ábrán látható gyermekcsomópontnál
kezdve.
0 0 0
GJ
�
~ 14.16. ábra. Ez alkalommal találunk egyezést a kiivetkező mintakarakterrel
Ez alkalommal a mintában szereplő a egyezik a fában lévő a-val, de több egyező
minta is van, ezért a gyermekcsomópontban folyta�uk a keresést a következő betűre
(lásd a 14.17. ábrát). Ismételten helyettesítőkaraktert keresünk, így mindegyik cso
mópont egyezni fog, de kifogytunk a rnintákból, és egyúttal elértünk egy szóhoz
(lásd a 14.18. ábrát) - megtaláltuk az első teljes egyezést: "bat."
Ez a művelet addig folytatódik, amíg meg nem találunk minden illeszkedő szót.
A 14.19. ábra mutatja az összes illeszkedő és nem illeszkedő szót a fában.
Itt látha�uk, hogy három szó illeszkedett az "-a-" mintára: "bat," "man" és
"map". Mégis csupán ll karakter-összehasonlítással mindet megtaláltuk Hasonlítsuk
össze ezt a letámadásos megközelítéssel, amely megkísérelte volna megtalálni az összes
szót "aaa"-tól "zaz"-ig. Az utóbbi esetben 26 * 26 = 676lehetséges kombináció van,
ami azt jelenti, hogy legalább ennyi karakter-összehasonlítást kellene elvégeznünk.
401
Hármas keresőfák
14.17. ábra. Megta/ájjuk az első illeszkedő szót
14. 18. ábra. A keresés a magasabb s:(jnten fofytatódik a kó.vetkező
legnagyobb csomóponttal
14. 19. ábra. A befqezett keresés az illeszkedő és a nem illeszkedő szavakat mutaija
402
A hármas keresőfák gyakorlati alkalmazása
A hármas keresőfák gyakorlati alkalmazása
Most, hogy megértettük, milyen különböző módokon lehet használni a hánnas kereső
fát, ideje kipróbálni, és létrehozni egy igazit. Mint mindig, most is néhány teszteset elké
szítésével kezdjük, hogy megbizonyosodjunk róla, a megvalósításunk megfelelően mú
ködik. Ezután fogjuk elkezdeni magának a valódi hánnas keresőfának a megalkotását,
és végül fejlesztünk majd egy alkalmazást a keresztrejtvények megoldásának segítésére.
Gyakorlófeladat: hármas keresáfa tesztelése
Hozzuk létre a megfelelően elnevezett TernarysearchTreeTest osztályt a követke
zőképpen:
package com.wrox.algorithms.tstrees;
import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List; import junit.framework.Testcase;
public class TernarysearchTreeTest extends Testcase { private TernarysearchTree _tree;
protected void setUp() throws Exception { super. setup();
}
_tree = new TernarysearchTree();
_tree.add("prefabricate"); _tree.add("presume"); _tree.add("prejudice"); _tree.add("preliminary"); _tree.add("apple"); _tree.add("ape"); _tree.add("appeal"); _tree.add("car"); _tree.add("dog"); _tree.add("cat"); _tree.add("mouse"); _tree.add("mince"); _tree.add("minty");
public void testcontains() { assertTrue(_tree.contains("prefabricate")); assertTrue(_tree.contains("presume")); assertTrue(_tree.contains("prejudice")); asser..tTr;ue( tree.contains C: preliminary")). ; ,
403
Hármas keresőfák
404
}
assertTrue(_tree. contai ns ("apple")); assertTrue(_tree.contains("ape")); assertTrue(_tree.contains("appeal")); assertTrue(_tree.contains("car")); assertTrue (_tr ee. contai ns("dog")); assertTrue(_tree.contains("cat"));
assertTrue(_tree. contains("mouse")); assertTrue(_tree.contains("mince")); assertTrue(_tree.contains("minty"));
assertFalse(_tree.contains("pre")); assertFal se(_tree. con ta i ns("dogs")); assertFalse(_tree.contains("UNKNOWN"));
public void testPrefixsearch() {
}
assertPrefixEquals(new String[] {"prefabricate", "prejudice", "preliminary", "presume"}, "pre");
assertPrefixEquals(new String[] {"ape", "appeal", "apple"}, "ap");
public void testPatternMatch() {
}
assertPatternEquals(new String[] {"mince", "mouse"}, "m???e"); assertPatternEquals(new String[] {"car", "cat"}. "?a?");
private void assertPrefixEquals(String[] expected, String prefix) {
List words = new LinkedList();
_tree.prefixsearch(prefix, words);
assertEquals(expected, words); }
private void assertPatternEquals(String[] expected, String pattern) {
List words = new LinkedList();
_tree.patternMatch(pattern, words);
as sertEqua ls (expected, words)·;
}
private void assertEquals(String[] expected, List actual) { assertEquals(expected.length, actual.size());
}
J.
for (int i =O; i < expected.length; ++i) { assertEquals(expected[i], actual.get(i));
}
A hármas keresőfák gyakorlati alkalmazása
A megvalósitás működése
A TernarySearchTreeTest osztály tartalmazza egy hármas keresáfa egy példányát
egyedi tesztesetekre való használathoz, és a SetUp()-ban néhány szó megadásával
kap kezdőértéket.
package com.wrox.algorithms.tstrees;
import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List; import junit.framework .Testcase;
public class TernarysearchTreeTest extends Testcase { private TernarysearchTree _tree;
}
protected void setUp() throws Exception { super. setup();
}
_tree = new TernarysearchTree();
_tree.add("prefabricate"); _tree.add("presume"); _tree.add("prejudice"); _tree.add("preliminary"); _tree.add("apple"); _tree.add("ape"); _tree.add("appeal"); _tree.add("car"); _tree.add("dog"); _tree.add("cat"); _tree.add("mouse"); _tree.add("mince"); _tree.add("minty");
A testcontains() metódus ellenőrzi, hogy minden egyes setup()-ban hozzáadott
szó jelen van-e a fában. Ráadásul néhány olyan szót is megvizsgáltunk, amelyeknek
nem kellene létezniük. Jegyezzük meg, hogy a szavakat gondosan választottuk ki: A "pre" betűsorozat valójában fellelhető lesz a fában, de csak más szavak preflxe
ként, így a contain O-nek fa l se értékkel kellene vissza térnie; az "UNKNOWN" -nak
egyáltalán nem kellene léteznie.
public void testcontains() { assertTrue(_tree.contains("prefabricate")); assertTrue(_tree.contains("presume")); assertTrue(_tree.contains("prejudice")); assertTrue (_t re e. con ta i ns ("pre l i mi nary"));
405
Hármas keresőfák
}
assertTrue(_tree.contains("apple")); assertTrue(_tree.contains("ape")); assertTrue(_tree.contains("appeal")); assertTrue (_tr ee. con ta i ns ("car")); assertTrue(_tree.contains("dog")); assertTrue(_tree. contai ns ("cat")); assertTrue(_tree.contains("mouse")); assertTrue(_tree.contains("mince")); assertTrue (_tree. con ta i ns ("mi n ty"));
assertFalse(_tree.contains("pre")); assertFalse(_tree.contains("dogs")); assertFalse(_tree.contains("UNKNOWN"));
Hármaskeresőfa-megvalósításunkban csak két publikus elérésű metódus van: egy az
általános előtaggal rendelkező szavak megtalálására és egy másik a rnintára illeszkedő
szavak megtalálására. Mind a kettő a keresési eredményeket tartalmazó listával tér
vissza, így létrehoztunk egy egyszerű metódust annak megerősítésére, hogy a keresé
si eredmény illeszkedik az elvárásokhoz.
A szokásos assertEquals() összetevőről összetevőre összehasonlítja a várt
szavak tömbjét az éppen visszatért szólistával. Ha a lista mérete és tartalma illeszke
dik a tömbre, akkor biztosak lehetünk benne, hogy a keresés eredményes volt.
private void assertEquals(String[] expected, List actual) { assertEquals(expected.length, actual.size());
}
for (int i =O; i < expected.length; ++i) { assertEquals(expected[i], actual.get(i));
}
Ahhoz, hogy a prefixkeresést teszteljük, létrehoztuk a testPrefixsearch () metó
dust. Ez a metódus összeállítja az elvárt értékek listáját és egy prefixet, majd átadja a
munka nagy részét másik segédmetódusnak (assertPrefi xEqual sO):
public void testPrefixsearch() {
}
assertPrefixEquals(new String[] {"prefabricate", "prejudice", "preliminary", "presume"}, "pre");
assertPrefixEquals(new String[] {"ape", "appeal", "apple"}, "ap");
Az assertPrefixEquals() metódus ezután létrehoz egy listát az eredmények táro
lására, és meghívja a fa prefixSearch O metódusát, hogy feltöltse a listát. Ezt köve
tően a várt és a tényleges eredményeket átadja a rni assertEqual s() metódusunk
nak validálásra.
406
A hármas keresőfák gyakorlati alkalmazása
private void assertPrefixEquals(String[] expected, String prefix) { List words = new LinkedList();
_tree.prefixsearch(prefix, words);
assertEquals(expected, words); }
A testPatternMatch() metódus mintával együtt összegyűjti a várt eredmények
tömbjét és továbbítja egy másik, assertPatternEqual s(), segédmetódusnak:
public void testPatternMatch() { assertPatternEquals(new String[] {"mince", "mouse"}, "m???e");
assertPatternEquals(new String[] {"car", "cat"}, "?a?"); }
Az assertPatternEquals() metódus meghívja a patternMatch() metódust a fára,
és jóváhagyja az eredményeket. Figyeljünk a kérdőjel (?) helyettesítőkarakterként va
ló használatára. A karakterek választéka szinte korlátlan, de egy kérdőjel valószínűleg
nem fordulhat elő véletlenül egy szóban, és meglehetősen nyilvánvaló, hogy azt je
lenti, "valami, akármi jön ide":
private void assertPatternEquals(String[] expected, String pattern) {
List words = new LinkedList();
_tree.patternMatch(pattern, words);
assertEquals(expected, words); }
A következő gyakorlatban létrehozzuk a tényleges hármaskeresőfa-osztályt.
Gyakorlófeladat: hármas keresőfa létrehozása
Hozzuk létre az TernarysearchTree osztályt a következőképpen:
package --com:·wr<>x:al go ri thms. tstrees;
import com.wrox.algorithms.lists.List;
public class TernarysearchTree { public static final char WILDeARD
rivate Node root;
'?' . . '
407
Hármas keresőfák
408
public void add(CharSequence word) {
}
assert word != null : "A 'word' (szó) nem lehet null"; assert word.length() >O : "A 'word' (szó) nem lehet üres";
Node node = insert(_root, word, 0); if (_root == null) {
_root = node;
}
public boolean contains(charsequence word) { assert word != null : "'word' (szó) nem lehet null "; assert word.length() >O : " A 'word' (szó) nem lehet üres ";
Node node = search(_root, word, 0); return node != null && node.isEndofword();
}
public void patternMatch(Charsequence pattern, List results) { assert pattern != null : "A 'pattern' (minta) nem lehet null"; assert pattern.length() > O : "A 'pattern' (minta) nem lehet
üres";
}
assert results != null : "A 'results' (eredménylista) nem lehet null";
patternMatch(_root, pattern, 0, results);
public void prefixsearch(charsequence prefix, List results) { assert prefix != null : "A prefix hem lehet null"; assert prefix.length() >O: "A prefix nem lehet üres";
inorderTraversal(search(_root, prefix, 0), results);
}
private Node search(Node node, charSequence word, int index) { assert word != null : "A 'word' (szó) nem lehet null";
if (node null) { return null;
}
char c = word.charAt(index);
if (c == node.getchar()) { if (index + l< word.length()) {
node = search(node.getchild(), word, index + l);
} } else if (c < node.getchar()) {
node = searcn(node.getsmaller() .• . word, index);
A hármas keresőfák gyakorlati alkalmazása
search(node.getLarger(), word, index); }
return node; }
private Node insert(Node node, charsequence word, int index) { assert word != null : " A 'word' (szó) nem lehet null ";
}
char c = word.charAt(index);
if (node == null) { node = new Node(c);
}
if (c == node.getchar()) { if (index + l< word.length()) {
node.setchild(insert(node.getchild(), word, index + l)); } else {
node.setword(word.toString()); }
} else if (c < node.getchar()) { node.setsmaller(insert(node.getsmaller(), word, index));
} else { node.setLarger(insert(node.getLarger(), word, index));
}
return node;
private void patternMatch(Node node, charsequence pattern, int index, List results) {
assert pattern != null assert results != null
"A 'pattern' (minta) nem lehet NULL"; "A 'results' (eredménylista) nem
lehet NULL";
if (node == null) { return;
}
char c = pattern.charAt(index);
if (c == WILDeARD l l c < node.getchar()) { patternMatch(node.getsmaller(), pattern, index, results);
}
if (c == WILDCARD l l c == node.getchar()) { if (index + l< pattern.length()) {
�------�,patternMatch(node.getchild(), eattern,_index +l, result�);
409
Hármas keresőfák
410
}
}
} else if (node.isEndofWOrd()) { results.add(node.getword());
}
if (c == WILDCARD ll c > node.getchar()) { patternMatch(node.getLarger(), pattern, index, results);
}
private void inorderTraversal(Node node, List results) { assert results != null :
"A 'results' (eredménylista) nem lehet NULL";
}
if (node == null) { return;
}
inorderTraversal(node.getSmaller(), results); if (node.isEndofWord()) {
results.add(node.getword());
} inorderTraversal(node.getchild(), results); inOrderTraversal(node.getLarger(), results);
private static final class Node { private final char _c; private Node _smaller; private Node _larger; private Node _child; private string _word;
public Node(char c) { _c = c;
}
public char getchar() { return _c;
}
public Node getsmaller() { return _smaller;
}
public void setsmaller(Node smaller) { _smaller = smaller;
}
public Node getLarger() { return _larger;
A hármas keresőfák gyakorlati alkalmazása
}
public void setLarger(Node lar�)-{ _larger = larger;
}
public NOde getchild() { return _child;
}
public void setchild(Node child) { _child = child;
}
public String getword() { return _word;
}
public void setword(String word) { _word = word;
}
public boolean isEndofword() { return getword() !=null;
}
A megvalósitás müködése
A TernarysearchTree osztálydefiníciója elég üres, egy egypéldányos változót tar
talmaz a gyökércsomópont tárolására és egy helyettesítőkarakterként használatos
konstanst határoz meg mintaillesztés esetén:
package com.wrox.algorithms.tstrees;
import com.wrox.algorithms.lists.List;
public class TernarysearchTree { public static final char WILDCARD = '?';
private Node _root;
}
Létrehoztuk a Node osztályt is, amely elkészíti a fa szerkezetét, egy nagyon egyszerű
osztályt, amely tárolja és visszaadja a karakteres értéket, valamint a kisebb és na
gyobb testvérek és természetesen bármelyik gyerek referenciáját. Figyeljünk a külö
nös _word változóra. Emlékezzünk vissza, hogy valamilyen módon jelölnünk kellett
a szó végét. Használhattunk volna logikai tipust, de a gyakorlat céljából inkább ma-
411
Hármas keresőfák
gát az adott szót tároltuk Bár ez nyilvánvalóan több memóriát használ, könnyebben
elvégzi a szavak összegyűjtését, ha keresést végez. Létezik még egy kényelmi mód
szer, az isEndofword(), amely csak akkor tér vissza true értékkel, ha a csomópont
ban egy szót tárolunk:
412
private static final class Node { private final char _c; private Node _smaller; private Node _larger; private Node _child; private String _word;
public Node(char c) { _c = c;
}
public char getchar() { return _c;
}
public Node getsmaller() { return _smaller;
}
public void setSmaller(Node smaller) { _smaller = smaller;
}
public Node getLarger() { return _larger;
}
public void setLarger(Node larger) { _larger = larger;
}
public Node getchild() { re tu rn _chi l d;
}
public void setchild(Node child) { _child = child;
}
public String getword() { return _word;
}
}
A hármas keresőfák gyakorlati alkalmazása
public void setword(String word) { _word = word;
}
public boolean isEndofword() { return getword() != null;
}
Egy dolgot még meg kell jegyeif�ünk, mielőtt elmerülnénk a kódolás további részeiben,
mégpedig aif, hogy mivel a hármas keresőfán dolgozó algoritmusok alkalmasak a re
kur.{jóra, ebben az osifáfyban minden metódust így kódo/tunk.
A cont ai ns () metódus pontosan akkor tér vissza true értékkel, ha a szó létezik a fában (nem tekintve a prefixeket), máskülönben fa l se értékkel tér vissza. Miután először validáltuk a bejövő adatot, meglúvjuk a search() metódust, áthaladva a gyökércsomóponton (ha van), a keresett szón és az első karakter pozícióján. Végül true
értéket kapunk vissza, ha a szó végét jelző csomópontot megtaláltuk; máskülönben fa l se értéket, amely azt jelzi, hogy a szót nem találtuk:
public boolean contains(CharSequence word) {
}
assert word != null : " A 'word' (szó) nem lehet null";
assert word.length() >O : " A 'word' (szó) nem lehet üres";
Node node = search(_root, word, O); return node != null && node.isEndofword();
A privát search() metódus kiválaszt egy csomópontot, amelyből kiindul a szó keresése, és egy pozíciót a szón belül, ahonnan kezdődik a keresés. Így a search() viszszaadja a szó utolsó karakterét tartalmazó csomópontot, vagy null értéket, ha a szót nem találjuk.
Ha nincs aktuális csomópont (node == null) , a keresés rögtön befejeződhet. Egyébként visszahozzuk az aktuális pozíción lévő karaktert, és elkezdődik a keresés.
Ha az aktuális keresett karakter illeszkedik az aktuális csomópontban lévőre, és nincs több karakter a sztringben (index +l < word.length()), a keresés a következő betún folytatódik, a gyermekcsomópontnál kezdve.
Ha a karakter nem illeszkedik, a keresőkarakremek léteznie kell vagy az aktuális csomópont előtt, vagy az után. Ha az általunk keresett karakter az aktuális csomópont előtt helyezkedik el, akkor a keresés a kisebb testvérrel kezdődően folytatódik; máskülönben, az aktuális csomópont után kell lennie - és ebben az esetben a keresés a nagyobb testvérrel folytatódik.
Végül vagy a keresett szóból fogynak el a betűk, vagy a csomópontokból fogyunk ki. Ezen a ponton bármelyik csomópont, amelyben éppen vagyunk (ha van) eredményként tér vissza:
413
Hármas keresőfák
private Node search(Node node, charsequence word, int index) { assert word != null : " A 'word' (szó) nem lehet null";
if (node nu ll) { return null;
}
char c = word.charAt(index);
if (c == node.getchar()) { if (index + l< word.length()) {
node = search(node.getchild(), word, index + l);
} } else if (c < node.getchar()) {
node search(node.getsmaller(), word, index); } else {
node search(node.getLarger(), word, index); }
return node; }
, Az add() és az i nser:t O metódus együtt valósí�a meg egy új szó fába illesztését. . - "Mi�t� ellenŐriztük a metódushoz.tartozó argumentumokat, az add() meghívja
az insert-et, végighaladva a gyökércsomóponton (ha van), a hozzáadandó szón és a szóban lévő első karakter pozícióján. Az egyeden, amit meg kell tennünk, hogy frissítjük a gyökércsomópontot, ha szükséges, azzal a csomóponttal, amelyet az i nsertO metódus adott vissza.
public void add(charsequence word) {
}
assert word != null : " A 'word' (szó) nem lehet null"; assert word. length O > O : " A 'word' (szó) nem l ehet üres";
Node node = insert(_root, word, O); if (_root == null) {
_root = node; }
Az i n se rt O metódus a szó aktuális karakterének megszerzésével indul. Ezután, ha nincs aktuális csomópont, létrehozunk egyet - amelyet végül is épp most adunk hozzá.
Az aktuális karaktert ezután összehasonlí�uk az aktuális csomópont karakteréveL Ha illeszkedik, akkor két lehetőség van: amennyiben van még belliesztendő karakter, akkor rekurzív módon feldolgozzuk a következő karaktert a gyermekcsomópontból kiindulva; különben beállítha�uk az aktuális csomópontban lévő szót, hogy jelezze, ha készen vagyunk.
414
A hármas keresőfák gyakorlati alkalmazása
Ha a karakter nem illeszkedik, további két lehetőség van: a karakter vagy lejjebb
helyezkedik el, mint az aktuális csomópont, vagy magasabban. Mindkét esetet rekur
zív módon kell feldolgoznunk ugyanazzal a karakterrel, de a kisebb, illetve a na
gyobb csomópontot felhasználva.
Jegyezzük meg, hogyan használjuk a visszatérő értéket a megfelelő gyerek-, illetve
testvércsomópont referenciájának frissítésére. Ez azért működik, mert az insert()
metódus mindig az éppen beillesztett csomóponttal tér vissza (illetve a megfelelő lé
tező csomóponttal), vagyis a visszakapott csomópont a szó első karakterére vonatko
zik, nem az utolsóra, ahogy ezt esetleg feltételezhettük:
private Node insert(Node node, charsequence word, int index) { assert word != null : " A 'word' (szó) nem lehet null";
}
char c word.charAt(index);
if (node == null) { node = new Node(c);
}
if (c == node.getchar()) { if (index + l< word.length()) {
node.setchild(insert(node.getchild(), word, index + l));
} else { node.setword(word.toString());
} } else if (c < node.getchar()) {
node.setsmaller(insert(node.getSmaller(), word, index)); } else {
node.setLarger(insert(node.getLarger(), word, index)); }
return node;
A prefixsearch () metódus először általános keresést hajt végre, hogy megtalálja az
utolsó betűt, illetve prefixet tartalmazó csomópontot. Ezt a csomópontot ezután az
eredmények tárolására használt listával együtt átadja az i norderTraversal () metó
dusnak:
public void prefixsearch(CharSequence prefix, List results) { assert prefix != null : "a 'prefix' nem lehet null"; assert prefix.length() >O : "a 'prefix' nem lehet üres";
inorderTraversal(search(_root, prefix, O), results); }
415
Hármas keresőfák
Az inorderTraversal metódus rekurzív módon járja be a kisebb testvért, ezután a
csomópont gyerekét, és végül a nagyobb testvért. Minden alkalommal, ha egy szó
megfelel (node. i sEndofword O) , hozzáadjuk az eredményekhez:
private void inorderTraversal(Node node, List results) { assert results != null: "A 'results' (eredménylista) nem
lehet NULL";
}
if (node == null) { return;
}
inorderTraversal(node.getSmaller(), results); if (node.isEndofword()) {
results.add(node.getword());
} inorderTraversal(node.getchild(), results); inorderTraversal(node.getLarger(), results);
Az első patternMatchO metódus meghívja az egyező nevű privát metódust, végigha
ladva a gyökércsomóponton, az illeszteni kívánt mintán, a mintában lévő első karakter
pozícióján és természetesen azon a listán, amelyben az eredményeket fogja tárolni:
public void patternMatch(charsequence pattern, List results) { assert pattern != null :
"A 'pattern' (minta) nem lehet NULL"; assert pattern.lengthO >O: "A pattern (minta) nem lehet üres"; assert results != null : "A 'results' (eredménylista) nem
lehet NULL";
patternMatch(_root, pattern, O, results);
}
A második patternMatch O metódus, néhány megszorítással, inkább úgy néz ki,
mint a fa egy inorder bejárása.
Először ahelyett, hogy bejárnánk a bal, illetve a jobb testvéreket, ellenőrzést haj
tunk végre, hogy eldöntsük, szükséges-e a bejárás. Ha az aktuális mintakarakter az
aktuális csomópont előtt helyezkedik el, akkor a kisebb testvér bejárását végezzük el;
ha a csomópont után, akkor a nagyobb testvérét; és ha ugyanaz, mint az aktuális
csomópont, akkor rekurzív hívást hajtunk végre a mintában lévő következő karak
terrel az első gyereken.
Másodszor minden pontban, ha az aktuális mintakarakter WILDCARD, akkor
mindegy, hogy rnit járunk be. Ekkor a helyettesítőkarakter illeszkedik minden más
karakterre.
Végül a keresés csak a mintával egyenlő hosszúságú szavakat fogja figyelembe
venni - például az öt hosszúságú minta csak öt hosszúságú szavakra illeszkedik:
416
Keresztrejtvény megoldását segítő példa
private void patternMatch(Node node, charsequence pattern,
}
int index, List results) { assert pattern != null assert results != null
"A 'p.attern' (minta) nem lehet NULL"; "A 'results' (eredménylista) nem
lehet NULL";
if (node == null) { return;
}
char c = pattern.charAt(index);
if Cc == WILDCARD l l c < node.getchar()) { patternMatch(node.getsmaller(), pattern, index, results);
}
if (c == WILDCARD ·l l c == node.getchar()) {
}
if (index + l < pattern.length()) { patternMatch(node.getchild(), pattern, index + l, results);
} else if (node.isEndofword()) { results.add(node.getword());
}
if (c == WILDCARD l l c > node.getchar()) { patternMatch(node.getLarger(), pattern, index, results);
}
Keresztrejtvény megoldását segitő példa
Felfegyverkezve az alaposan tesztelt és létrehozott mintaillesztő kóddal, most már
megnézhetünk egy példaalkalmazást, amely bemutatja a hármas keresőfák egy új
használatát: a keresztrejtvény megoldását. Ebben a részben kifejlesztünk egy nagyon
kis méretű parancssori alkalmazást, amely argumentumként vesz át szavakat tartal
mazó fájlt - soronként egy szó - és egy mintát az illesztéshez, és tartalmazhat he
lyettesítőkaraktereket is.
417
Hármas keresőfák
Gy a kori ófelada t: keresztrej tv ény-segéda l ka lmazás létrehozása
Hozzuk létre az crosswordHel per osztályt a következőképpen:
418
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException;
public final class crosswordHelper { private CrosswordHelper() { }
public static void main(string[] args) throws IOException { assert args != null : "az argumentumlista nem lehet NULL";
}
if (args.length <2) {
}
System.out.println("Használat: CrosswordHelper <szó-lista> <minta> [ismétlések)");
System.exit(-1);
int repetitions = l;
if (args.length >2) { repetitions = Integer.parsernt(args(2]);
}
searchForPattern(loadwords(args[O]), args[l], repetitions);
private static void searchForPattern(TernarysearchTree tree, String pattern, int repetitions) {
assert tree l= null : "a 'tree' nem lehet null";
System.out. println(""Minta keresése: "' + pattern + " • • . ") ;
List words = null;
for (int i = O; i < repetitions; ++i) { words = new LinkedList(); tree.patternMatch(pattern, words);
}
Keresztrejtvény megoldását segítő példa
}
}
Iterator iterator = words.iterator();
for (iterator.first(); !iterator.isoone(); iterator.next()) { system.out.println(iterator.current());
}
private static TernarysearchTree loadwords(String fileName) throws IOException {
TernarysearchTree tree = new TernarysearchTree();
}
system.out.println("szavak betöltése a(z) '" + fileName +
fájlból ... ");
BufferedReader reader new BufferedReader(new FileReader(fileName));
try { String word;
while ((word= reader.readLine()) != null) { tree.add(word);
} } finally {
reader. close();
}
return tree;
A megvalósitás müködése
A c rossword He l pe r osztály meghatározza a ma i n() alkalmazás belépési pontot. Ez
a metódus először ellenőrzi, hogy van legalább két argumentum a parancssorban -
egy a szólistát tartalmazó fájlnak és egy másik a mintának. Az args [O] fájlnév át
adódik a loadwords()-nak, amely, ahogy mindjárt látni fogjuk, egy hármas keresőfá
val tér vissza, amely majd végigmegy a mintán, az args [l] pedig átadóclik a search
ForPattern() metódusnak, hogy létrehozza a tényleges illesztést:
package com.wrox.algorithms.tstrees;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException;
419
Hármas keresőfák
public final class CrosswordHelper { private crosswordHelper() {
}
}
public static void main(String[] args) throws IOException { assert args != null : "az argumentumlista nem lehet NULL";
}
if (args.length <2) {
}
System.out.println("Használat: CrosswordHelper <SZÓ-lista> <minta> [ismétlések]");
System.exit(-1);
searchForPattern(loadWords(args[O]), args[l]);
A l oadwo r ds() metódus a szavakat tartalmazó fájl nevét veszi át - soronként egyet -,
és egy teljesen telített hármas keresőfával tér vissza. Azzal kezdi, hogy létrehoz egy
·olyan hármas kereső fát, amelyben a szavakat fogja tárolni. Ezután megnyi�a a fájlt,
minden egyes sort kiolvas, és hozzáadja a szót a fához. Aztán bezárja a fájlt, és az
újonnan feltöltött fa visszatér a hívóhoz:
420
private static TernarysearchTree loadwords(String fileName) throws IOException {
}
TernarysearchTree tree = new TernarysearchTree();
system.out.println("szavak betöltése a(z) "' + fileName +
fájl ból ... ");
BufferedReader reader new BufferedReader(new FileReader(fileName));
try { String word;
while ((word = reader.readLine()) != null) { tree.add(word);
}
} finally { reader.close();
}
return tree;
Keresztrejtvény megoldását segítő példa
Végül megvan az a metódus, amely tulajdonképpen végrehaj�a a keresést: search
ForPattern O. Ez a metódus egyszerűen létrehoz egy listát az eredmények tárolásá
ra, meglúvja a patternMatch() metódust, végigfut a mintán és a listán, majd újra és
újra végigmegy az eredményeken, kiírva mindegyiket a konzolra:
private static void searchForPattern(TernarysearchTree tree, String pattern) {
}
assert tree != null : "A 'tree' (fa) nem lehet NULL";
System.out.println("Minta keresése: "' + pattern + "' ... ") ;
List words = new LinkedList(); tree.patternMatch(pattern, words);
Iterator iterator = words.iterator();
for (iterator.first(); !iterator.isDone(); iterator.next()) { System.out.println(iterator.current());
}
V égigfuttatva a keresztrejtvénysegédet körülbelül 114 OOO angol szó listáján az
"a?r???t" mintára, a következő eredményeket kapjuk:
szavak betöltése a(z) 'words.txt' fájlból ... Minta keresése: 'a?r???t' ... abreact abreas t acrobat aeriest airboat ai ri est ai r lift airport airpost alright apricot
Nagyon hasznos arra az esetre, ha legközelebb elakadunk keresztrejtvény, illetve
szókirakó játék közben.
421
Hármas keresőfák
Összefoglalás
A fejezetből a következőket tudhattuk meg a hármas keresőfákról és a hozzájuk kapcsolódó viselkedésről:
• Legjobban sztringek tárolására lehet használni.
• A reguláris keresés mellett prefixkeresésre is alkalmasak.
• Használhaták még mintaillesztésre és keresztrejtvények megoldására is.
• Olyanok, mint a bináris keresőfák egy extra gyermekcsomóponttal.
• A csomópontok az egész szó tárolása helyett egy-egy betűt tartalmaznak.
• A bináris keresőfákhoz hasonlóan a hármas keresőfák is válhatnak kiegyensúlyozadanná.
• Általában többszörösen hatékonyabbak, mint a bináris keresőfák, mivel átlagosan kevesebb számú karakter-összehasonlítást végeznek
Gyakorlat
1. Hozzuk létre a search() egy iteratív formáját!
422
T�ZENÖTÖDIK FEJEZET
B-fák
Az eddig tárgyalt adatstruktúrák mind kizárólag memóriabeli adatokkal dolgoznak.
A listáktól (3. fejezet) a hasítótáblákon át (11. fejezet) a bináris keresőfákig (1 O. feje
zet) valamennyi adatstruktúra és a hozzájuk kötődő algoritmusok azt feltételezték,
hogy a teljes adathalmaz kizárólag az operatív memóriában van tárolva. Mi a helyzet
azonban, ha az adatok háttértárolón vannak, ahogy a legtöbb adatbázis esetében?
Mit tegyünk, amikor több millió rekord közül egyet kell kikeresnünk az adatbázis
ból? Ez a fejezet azt mutatja be, hogyan kezeljük azokat az adatokat, amelyek nem a
memóriában vannak tárolva.
A fejezetben a következő témaköröket tárgyaljuk:
• Miért alkalmatlanok az eddig megismert adatstruktúrák a lemezen tárolt
adatok kezelésére?
• Hogyan oldják meg a B-fák a másfajta adatstruktúrákkal kapcsolatban fel
merülő problémákat?
• Hogyan valósíthatunk meg egy egyszerű B-fa alapú leképzést?
A B-fákról
Azt már tudjuk, hogyan lehet a bináris keresőfákkal olyan indexeket készíteni, amelyek
leképzésként használhatók. Nem nehéz elképzelni, hogyan működik a bináris fa le
mezről olvasása és lemezre írása. Ezzel a megközelítéssel azonban az a gond, hogy a
rekordok számának növekedésével a fa mérete is nő. Képzeljünk el egy adatbázistáb
lát, amely egymillió rekordot tartalmaz, és ehhez olyan indexet, ahol a kulcsok tíz ka
rakter hosszúak. Ha az index minden egyes kulcsa (négy karakter hosszúságú egész
ként tárolva) egy táblabeli rekordot képez le, és a fa minden csomópon�a hivatkozik a
szülő- és gyermekcsomópontjaira (amelyek mindegyike szintén négy karakter hosszú),
akkor ez l OOO OOO x (10+4+4+4+4) = l OOO OOO x 26 = 26 OOO OOO, azaz körül
belül 26 megabájt (MB) olvasását és írását jelenti minden változtatás alkalmával!
B-fák
Ez rengeteg lemez olvasási-írási művelet, és valószínűleg mindenki tisztában van
vele, hogy ennek nagy az időköltsége. Az operatív memóriához viszonyítva a lemez
olvasás-írás ezerszer, ha nem milliószor lassabb. Még ha 10:MB/másodperc adatátvi
teli sebességet sikerül megvalósítani, akkor is 2,6 másodpercbe telik egy indexmódo
sítás lemezre mentése. A legtöbb valós alkalmazás esetén, ahol több tíz vagy száz
felhasználó párhuzamosan dolgozik, 2,6 másodperc elfogadhatatlanul lassú. Ennél
egy �csit gyorsabb algoritmust várnak el tőlünk.
Már tudjuk, hogy a bináris keresáfa egyedi csomópontokból épül fel, tehát meg
próbálha�uk egyenként olvasni és írni a csomópontokat, nem pedig egyszerre mindet.
Bár ez először jó ötletnek tűnhet, a gyakorlatban nem az. Ne feledjük, hogy még a
legtökéletesebben kiegyensúlyozott bináris keresőfában is átlagosan o(log N) cso
mópontot kell érinteni egy keresési kulcs megtalálásához. A rni, egymillió rekordot
tartalmazó képzeletbeli adatbázisunk esetében ez l og2 l. OOO. OOO = · 20 csomópon
tot jelent. Ez elfogadható a memóriabeli műveletek esetén, amelyeknél egy csomó
pont elérési költsége nagyon alacsony, de nem olyan jó, ha ez 20 lemezolvasást jelent.
Még ha a csomópontok elég kicsik is - a rni példánkban csak 26 bájt körüliek -, a le
mezen az adatok nagyobb blokkokban vannak tárolva, amelyeket lapoknak neveznek.
Vagyis egyetlen csomópont olvasási költsége nem több és nem kevesebb, mint
mondjuk 20 csomópont olvasása. Hát ez nagyszerű, mondhatnánk, hiszen akkor csak
20 csomópontot kell átolvasnunk Miért ne olvassuk be ezeket egyszerre?
A probléma az, hogy a bináris keresáfa úgy épül fel, főleg ha ki van egyensú
lyozva, hogy nagyon kicsi a valószínűsége annak, hogy a kapcsolódó csomópontok
egymáshoz közel helyezkednek el, egyedül ugyanabban a szektorban. A helyzet en
nél rosszabb. Ugyanis nemcsak az átviteli időnek nevezett, nagyjából 20 lemezolvasás
költségével kell számolnunk, hanem minden lemezolvasás végrehajtása előtt át kell
pozicionálni a lemezfejeket, ezt keresési időnek nevezzük, majd a lemezeket be kell
forgatni a megfelelő helyzetbe, ez a késleltetés. Mindez összeadódik Akármilyen kifi
nomult gyorsító mechanizmust alkalmazunk is a végrehajtott fizikai olvasás-írás mű
veletek számának csökkentése érdekében, az általános teljesítmény így is elfogadha
tatlan. Nyilvánvaló, hogy ennél jobb megoldás szükséges.
A B-fák kifejezetten a másocllagas tárolókon, mint a merevlemezek, CD-k stb.
tárolt indexek kezelésére szolgálnak, hatékony beszúrás, törlés és keresés művelete
ket biztosítva.
424
A szabvátryos Bfának számos változata létezik, például B+fák, B*fák stb. Mind
egyik más módon olefia meg a külső táro/ón tiirténő keresést, bár mind az alap Bfán
alapszik. A Bfákról és változatairól bővebben lásd [Cormen, 2001], [SedgeJIJick,
2002) és [Folk, 1991) múveit.
A B-fákról
A bináris keresófákhoz hasonlóan a B-fák is csomópontokból állnak. A bináris kere
sófákkal ellentétben azonban a B-fa csomópontjai nem egy, hanem több kulcsot tar
talmaznak, amelyek maximális számát rendszerint a lemezblokk mérete határozza
meg. A csomópontokon belül a kulcsok rendezetten vannak tárolva, és kapcsolódik
hozzájuk egy gyermekcsomópont, amely a nála kisebb kulcsokat tartalmazza - azaz
minden, k kulcsot tartalmazó nem levélcsomópontnak k+1 gyermeke van.
A 15.1. ábrán látható B-fa A-tól K-ig tartalmazza a kulcsokat. Minden csomó
pontban legfeljebb három kulcs található. Ebben a példában a gyökércsomópont
csak két kulcsot tartalmaz (D és H), és három gyermekcsomópon�a van. A bal szél
ső gyermekcsomópont tartalmaz minden D-nél kisebb kulcsot. A középsó gyermek
a D és H közötti kulcsokból áll. A jobb szélső az összes többi, H-nál nagyobb kul
csot tartalmazza.
A 15.1. ábra. Legfe(jebb három kulcsot tartalmazó csomópontokból felépülő Bfa,
ame/y A-tól K-ig tartalmazza a kulcsokat
Kulcs keresése a B-fában hasonló módon történik, mint a bináris keresófában, csak
itt az egyes csomópontokban több kulcs található. Ezért B-fában történő kereséskor
nem két, hanem több gyermek közül kell választani.
Ha például a 15.1. ábrán látható fában keressük a G kulcsot, a gyökércsomópont
ból indulunk. A G keresési kulcsot először a D kulccsal ve�ük össze Oásd 15.2. ábra).
A K 15.2. ábra. A keresés agyokércsomópont első kulcsánál kezdődik
Miután sorrendben a G a D után következik, a keresés folytatódik a következő
kulccsal, a H-val Oásd 15.3. ábra).
[A I • I �'IJIK I 15.3. ábra. A keresés a csomópont kdvetkező kulcsával fo(ytatódik
Most a keresési kulcs sorrendben a csomópont aktuális kulcsa előtt található, tehát a
bal oldali gyermekre mutató hivatkozást követjük Oásd 15.4. ábra).
425
B-fák
15.4.· ábra. A keresési kulcs az aktuális kulcs előtt he!Jezkedik e4 így a keresés a bal oldali
gyermekre mutató hivatkozást kiivetve fo!Jtatódik
Ez addig folytatódik, amíg végül megtaláljuk a keresett kulcsot (lásd 15.5. ábra).
15. 5. ábra. A keresés végén megtalálJuk a keresett kulcsot
Bár a keresés során öt kalcs-összehasonlítást végeztünk, ez csak két csomópontot érintett. A bináris keresőfához hasonlóan az érintett csomópontok száma a fa magasságától függ. Mivel azonban a B-fa csomópontjai több kulcsot tartalmaznak, a fa magassága jóval kisebb, mint a megfelelő bináris keresőfáé, ami kevesebb csomópont áttekintését eredményezi és így kevesebb lemez olvasás-írást.
Eredeti példánkhoz visszatérve, ha feltételezzük, hogy a lemezblokkok egyenként 8 OOO bájtból állnak, ez azt jelenti, hogy az egyes csomópontok nagyjából 8 OOO
l 26 = 300 kulcsot tartalmazhatnak Ha egymillió kulcsot szeretnénk tárolni, ez 1 OOO OOO l 300 = 3,333 csomóponton lehetséges. Tudjuk továbbá, hogy a bináris keresőfához hasonlóan a B-fa magassága O(log N), ahol N a csomópontok száma. Elmondha�uk tehát, hogy egy tetszőleges kulcs keresése során az érintett csomópontok száma log300 3, 333 = 2 nagyságrendű. Ez a nagyságrend jobb, mint a bináris keresáfa esetében.
A B-fába úgy szúrunk be kulcsot, hogy a gyökérből kiindulva megkeressük a helyét egy levélcsomóponton. Miután megtaláltuk a megfelelő levélcsomópontot, az új értéket sorrendhelyesen illesztjük be. A 15.6. ábra a 15.1. ábrán látható B-fát muta�a az L kulcs beszúrása után.
15.6. ábra. A beszúrás mindig levélcsomópontban tiirténik
V együk észre, hogy az új kulcs beszúrását követően a csomópont túllépte a maximálisan megengedett kulcsszámot- ebben a példában legfeljebb három kulcsot tartalmazhat egy csomópont. Arnikor egy csomópont "betelik", kettéválasztódik, és mindkét új csomópont az eredeti csomópont kalcsainak felét tartalmazza, ahogy a 15.7. ábrán látható.
426
A B-fákról
A L----L---1 �
15.7. ábra. A "betelt" csomópont kettéválas'{jódik
Ezt követően az eredeti csomópont "középső" kulcsa felkerül a szülőbe, ott beszú
rásra kerül a sorba egy hivatkozással az újonnan létrejött csomópontra. Ebben az
esetben a J kerül a szülőcsomópontba a H után, és a belőle kiinduló hivatkozás a K
és az L kulcsot tartalmazó csomópontra mutat, ahogy a 15.8. ábrán látható.
A L
15.8. ábra. Az eredeti csomópont kO"zépső kulcsa el!)' si./nttel felJebb kerül a fában
Ily módon a fa szélessége kiterjed, és nem növekszik a magassága. A B-fák rendsze
rint szélesebbek és alacsonyabbak, mint a fasttuktúrák többsége, vagyis az érintett
csomópontok száma általában jóval kevesebb. A B-fa magassága addig nem növek
szik, arrúg a gyökércsomópont be nem telik és szét nem osztódik.
A 15.9. ábra a 15.8. ábrán látható fát mutatja az M és az N kulcs beszúrása után.
Itt ismét betelt a csomópont, ahová a kulcsokat beszúrtuk, így szükségessé vált a
szétválasztása.
A
1 s. 9. ábra. A levélcsomópontot szét kell válas '{jani
Ismét kettéválik a csomópont, és a "középső" kulcs (L) felkerül a gyökérbe, ahogy a
15.10. ábrán látható. -
0 15.10. ábra. A!!)lökércsomópont betelik
427
B-fák
Most azonban a gyökércsomópont is betelt - háromnál több kulcsot tartalmaz -,
ezért szét kell választani. A csomópontok szétválasztásakor az egyik kulcs rendsze
rint felkerül a szülócsomópontba, de rnivel ebben az esetben a gyökércsomópontról
van szó, ennek nincs szülóje. Arnikor a gyökércsomópont szétválik, új csomópont
jön létre, és ez lesz az új gyökér. A 15.11. ábra a gyökércsomópont szétválasztása
után mutatja a fát, amikor új csomópont jön létre az eredeti gyökér fölött, így növel
ve a fa magasságát. Az eredeti gyökércsomópont szétválasztásával létrejövő két
csomópont szülójeként létrehozott új csomópont a H kulcsot tartalmazza.
15. 11. ábra. A !!Jbkércsomópont szétválas!'(/ása no·veli a fa magasságát
A B-fából törölni jóval bonyolultabb, mint keresni és beszúrni, mivel ez a csomó
pontok összevonásával jár. A 15.12. ábra például a K kulcs törlése után mutatja a
15.11. ábrán látható fát. Ez így már nem érvényes B-fa, mert a J és az L kulcs között
nincs középsó gyermekcsomópont. Ne feledjük, hogy egy k kulcsot tartalmazó nem
levélcsomópontnak mindig k+ l gyermekének kell lennie.
15.12. ábra. A K kulcs tbrlése éroétryteien Bfát hoz létre
A struktúra kijavításához némely kulcsot újra szét kell osztani a gyermekcsomó
pontok között - ebben az esetben a J kulcs lekerül abba a csomópontba, amely
egyedül az I kulcsot tartalmazza, ahogy a 15.13. ábra muta�a.
15.13. ábra. A kulcsok tijra szétos!'(/ódnak a !!Jermekcsomópontok kb'zött a fastruktúra
kijavítása érdekében
428
B-fák a gyakorlatban
Ez a legegyszerűbb eset. Ha például törölnénk az I és a J kulcsot, akkor a fa a 15.14.
ábra szerint nézne ki.
15.14. ábra. A kulcsok tijra szétosifására van szükség a fastruktúra kijavításához
Ismét a kulcsok szétosztása szükséges a fa kiegyensúlyozása érdekében. Ezt többfé
leképpen is megvalósíthatjuk Mindegy hogyan osztjuk szét újra a kulcsokat, ugyanis
a szülőcsomópontok kulcsait a gyermekcsomópontok kulcsaival vonjuk össze. El
képzelhető olyan eset, arnikor a gyökércsomópontot lejjebb kell helyezni a fában
(vagy törölni). Ilyenkor a fa magassága eggyel csökken ..
A példa kedvéért most az L kulcsot összevonjuk a gyermekcsomópontjával, a
szülőjét, a gyökércsomópontot (H) pedig lejjebb helyezzük.
A N
15.15. ábra. A fa magassága mindig csokken, amikor a gyokércsomópontot osszevonjuk
vafame!Jik gyermekéve 4 vagy te !J esen kitöriiiJük
Látha�uk, hogy a törlés elég bonyolult eljárás és többféle esetét kell megkülönböz
tetni. (Részletesebb magyarázatot találunk [Cormen, 2001] művében.)
B-fák a gyakorlatban
Most, hogy már ismerjük a B-fák működését, és tudjuk, rniért hasznosak, próbáljunk
meg megvalósítani egyet. Korábban említettük, hogy a B-fákat általában indexként
használjuk, ezért ebben az egyszerű példában a 13. fejezetből ismert B-fán alapuló
Map interfész egy megvalósítását készítjük el. Annak érdekében, hogy ne csökkenjen
az alkalmazott algoritmusok működési hatékonysága, a létrehozott osztály teljes egé
szében a memóriában helyezkedjen el, ne háttértáron.
429
B-fák
A Map interfész minden metódusát a B-fákról tanultak felhasználásával fogjuk
megvalósítani, mert ez lesz az alapja a mögöttes adatstruktúrának Ezért a get O, a
con ta i ns O és a set O metódust a korábban tárgyalt keresési és beszúrási algoritmu
sok alapján valósí�uk meg. A de l e te O metódus esetén azonban egy kicsit csalni fo
gunk. Mivel a B-fa törlési algoritmusa nagyon bonyolult - legalább három esetet kell
megkülönböztetni, és a bejegyzéseket újra szét kell osztani a csomópontok között -,
a bejegyzések tényleges törlése helyett csak töröltnek jelöljük őket. Igaz ugyan, hogy
ennek az a szerencsétlen mellékhatása, hogy így a B-fa sohasem szabadít fel memóri
át, de a példánk céljának ez most megfelel. A B-fabeli törlés részletes magyarázata
megtalálható [Cormen, 2001] művében. A következő gyakorlófeladatban létrehozzuk a teszteket, amelyekkel meggyő
ződhetünk a B-fa leképzése megvalósításának helyes működéséről.
Gyakorlófeladat: B-fák tesztelése
Hozzuk létre a BTreeMapTest osztályt a következőképpen:
pacKage com.wrox.algorit�ms.btrees;
import com.wrox.algorithms.maps.AbstractMapTestcase; import com.wrox.algorithms.maps.Map; import com.wrox.algorithms.sorting.Naturalcomparator;
public class BTreeMapTest extends AbstractMapTestcase { protected Map createMap() {
return new BTreeMap(Naturalcomparator.INSTANCE, 2);
} }
A megvalósitás müködése
A 13. fejezetben már létrehoztuk a teszteseteket, így most csak az AbstractMap
TestCase osztályt kell kiterjesztenünk Egyedüli feladatunk a createMapO metódus
megvalósítása, valamint a BTreeMap osztály példányosítása. A BTreeMap konstruktor
két paramétert vár: egy összehasonlítót a kulcsok rendezéséhez és a csomópontban
elhelyezhető kulcsok maximális számát. Ebben az esetben a csomópontban tárolha
tó kulcsok számát tartsuk a lehető legalacsonyabb értéken, így biztosítható, hogy a
csomópontok száma a lehető legnagyobb legyen. Bár ez a B-fa struktúra céljával el
lentétes - azaz, hogy a csomópontok száma és a fa magassága a lehető legalacso
nyabb legyen -, a tesztek során így tudjuk biztosítani az olyan speciális esetek elő
fordulását, mint a levélcsomópont és a gyökércsomópont szétválasztása.
A következő gyakorlatban létrehozzuk a ténylegesB-fa-leképzés megvalósítását.
430
B-fák a gyakorlatban
Gyakorlófeladat: 8-fa-leképzés megvalósítása
Hozzuk létre az BTreeMap osztályt a következőképpen:
packa"ge "ci>m- wrox :·a lgöri thms- btrees;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.EmptyList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.maps.oefaultEntry; import com.wrox.algorithms.maps.Map; import com.wrox.algorithms.sorting.comparator;
public class BTreeMap implements Map { private static final int MIN_KEYS_PER_NODE = 2;
private final comparator _comparator; private final int _maxKeysPerNode; private Node _root; private int _size;
public BTreeMap(Comparator comparator, int maxKeysPerNode) { assert comparator != null : "a 'comparator' nem lehet NULL"; assert maxKeysPerNode >= MIN_KEYS_PER_NODE :
"A maxKeysPerNode nem lehet < " + MIN_KEYS_PER_NODE;
}
_comparator = comparator; _maxKeysPerNode = maxKeysPerNode; clear();
public object get(Object key) { Entry entry = _root.search(key); return entry != null ? entry.getvalue()
} null;
public Object set(Object key, Object value) { object oldValue = _root.set(key, value);
}
if (_root.isFull()) {
}
Node newRoot = new Node(false); _root.split(newRoot, 0); _root = newRoot;
return oldValue;
public object delete(object key) { ' En try en try . .:... r:_oot. sem h (key) ;
... = ---��-- -·
431
B-fák
432
}
if (entry == null) { return null;
}
entry.setoeleted(true); --_size;
return entry.setvalue(null);
public boolean contains(object key) { return _root.search(key) != null;
}
public void clear() { _root= new NOde(true); _size = O;
}
public int size() { return _size;
}
public boolean isEmpty() { return size() == O;
}
public Iterator iterator() { List list= new ArrayList(_size);
_root.traverse(list);
return list.iterator();
}
private final class Node { private final List _entries =
new ArrayList(_maxKeysPerNode +l); private final List _children;
public Node(boolean leaf) {
}
_children = ! leaf ? new ArrayList(_maxKeysPerNode + 2) (List) EmptyList.INSTANCE;
public boolean isFull() { return _entries.size() > _maxKeysPerNode;
}
public Entry search(Object key) { int index= indexof(� L) L; _______________ __,
B-fák a gyakorlatban
}
}
{ Entry entry = (Entry) _entries.get(index); return !entry.isoeleted()? entry: null;
return !isLeaf() ? ((Node) _children.get(-(index + l))).search(key) : null;
public object set(object key, object value) { int index= indexof(key); if (index >= O) {
return ((Entry) _entries.get(index)).setvalue(value);
}
return set(key, value, -(index + l));
}
private object set(Object key, object value, int index) {
}
if (isLeaf()) {
}
_entries.insert(inaex, new Entry(key, value)); ++_size; return null;
Node child= ((Node) _children.get(index)); object oldvalue = child.set(key, value);
if (child.isFull()) { child.split(this, index);
}
return oldvalue;
private int indexOf(object key) { int lowerindex = O; int upperindex = _entries.size() - l;
while (lowerindex <= upperindex) { int index = lowerindex + (upperindex - lowerindex) l 2;
int cmp = _comparator.compare(key, ((Entry) _entries.get(index)).getKey());
if (cmp = O) { return index;
} else if (cmp < 0) {
_t, -"--·--"�I!J!(>e r:'_!.fld_t!.� = index ,,:;_l; --- --- " �·-.
433
B-fák
434
}
} }
else { lowerindex index + l;
return -(lowerindex +l);
public void split(Node parent, int insertionPoint) {
}
assert parent != null : "A 'parent' (szülő) nem lehet NULL";
Node sibling = new Node(isLeaf());
int middle = _entries.size() l 2;
move(_entries, middle +l, sibling._entries); move(_children, middle +l, sibling._children);
parent._entries.insert(insertionPoint, _entries.delete(middle));
if (parent._children.isEmpty()) { parent._children.insert(insertionPoint, this);
} parent._children.insert(insertionPoint + l, sibling);
public void traverseCList list) {
}
assert list != null : "a list nem lehet NULL";
Iterator children = _children.iterator(); Iterator entries = _entries.iterator();
children.first(); entries. first();
while (!children.isoone() ll !entries.isoone()) { if (!children.isoone()) {
}
}
((Node) children.current()).traverse(list); children.next();
if (!entries.isoone()) {
}
Entry entry = (Entry) entries.current(); if (!entry.isoeleted()) {
list.add(entry); } entri es. next();
B-fák a gyakorlatban
privatE! voíi:l move(Ci st source, -
int from, L 1st target) { assert source != null : "A 'source' (forrás) nem lehet NULL"; assert target != null : "A 'target' (cél) nem lehet NULL";
}
}
}
while (from < source.size()) { target.add(source.delete(from));
}
private boolean isLeaf() { return _children == EmptyList.INSTANCE;
}
private static final class Entry extends oefaultEntry { private boolean _deleted;
}
public Entry(object key, object value) { super(key, value);
}
public boolean isoeleted() { return _deleted;
}
public void setoeleted(boolean deleted) { _deleted = deleted;
}
A megvalósitás működése
A BTreeMap osztályban az összehasonlító rendezi a kulcsokat, adott a csomópon
tokban tárolható kulcsok maximális száma, a gyökércsomópont és a leképzés bejegy
zésszáma. Vegyük észre, hogy a csomópontban tárolható kulcsok megengedett mi
nimális száma kettő. Amikor a csomópont szétválik, legalább egy kulcsnak maradnia
kell a bal oldali gyermekcsomópontban, egynek a jobb oldaliban, illetve egyet fel kell
vinni a szülőcsomópontba. Ha a tárolható kulcsok legkisebb száma egyre lenne állit
va, akkor a csomópont már két kulccsal betelne, és ez kevés a szétválasztáshoz.
package com.wrox.algorithms.btrees;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.EmptyList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.maps.DefaultEntry; import com.wrox.algorithms.maps.Map; import com.wrox.algorithms.sorting.comparator;
435
B-fák
public class BTreeMap implements Map {
}
private static final int MIN_KEYS_PER_NODE
private final comparator _comparator;
private final int _maxKeysPerNode; private Node _root;
private int _size;
2-'
public BTreeMap(Comparator comparator, int maxKeysPerNode) {
assert comparator != null : "a 'comparator' nem lehet NULL";
assert maxKeysPerNode >= MIN_KEYS_PER_NODE :
}
"A maxKeysPerNode nem lehet < "
+ MIN_KEYS_PER_NODE;
_comparator = comparator;
_maxKeysPerNode = maxKeysPerNode;
clear();
Két belső osztály is van- En try és Node - amelyek egy Map_ Entry, illetve egy B-fa
csomópontot jelölnek
Azonfelül, hogy kiterjeszti a DefaultEntry osztályt, az Entry belső osztályban
van egy logikai jelző is, amely azt mutatja, hogy a bejegyzés törölt-e vagy sem. Ez a
jelző a bejegyzések törlésekor kapcsolható be, illetve ki:
private static final class Entry extends oefaultEntry {
private boolean _deleted;
}
public Entry(Object key, object value) {
super(key, value);
}
public boolean isoeleted() {
return _deleted;
}
public void setoeleted(boolean deleted) {
_deleted = deleted;
}
A Node belső osztály végzi a legtöbb műveletet, ezért először ezt az osztályt Vizsgál
juk meg, és csak azután vesszük sorra a BTreeMap főosztály metódusait.
436
B-fák a gyakorlatban
Minden csomópont egy logikai jelzővel jön létre, amely mutatja, hogy a csomó
pont levélcsomópont-e. Ne feledjük, hogy a levélcsomópontoknak nincsenek gyer
mekeik; ezt jelzi a konstruktor. Ha a csomópont levélcsomópont, a gyermekek listája
üres lista, egyébként új tömblista adódik hozzá a gyermekek tárolására. Ezt mutatja
meg az i sLeaf() metódus, amelynek segítségével meghatározható, hogy egy cso
mópont levélcsomópont -e. Ezenkivül az i s Fu ll () metódus azt adja meg, hogy a
csomópont a maximálisan megengedettnél több kulcsot tartalmaz-e.
private final class Node {
}
private final List entries = new ArrayList(); private final List _children;
public Node(boolean leaf) {
}
_children = !leaf 7 new ArrayList() (List) EmptyList.INSTANCE;
public boolean isFull() {
return _entries.size() > _maxKeysPerNode; }
private boolean isLeaf() { return children == EmptyList.INSTANCE;
}
Először is szükségünk van egy metódusra, amely megkeres egy kulcsot bejegyzések
között. Az i ndexof() metódus egyszerű lineáris keresést hajt végre, hogy megtalálja
az illeszkedő kulcsot. Ha megtalálta, a listán belüli pozícióját (O, 1, 2 stb.) adja vissza;
egyébként negatív indexszel tér vissza, amely azt jelzi, hogy hol kellett volna lennie a
kulcsnak, ha létezik. (A 9. fejezetben részletesen tárgyaljuk a lineáris keresés műkö
dését. Ez a kód azonos a search() metódussal a L i nearL i stsearcher osztályból,
azzal a különbséggel, hogy ez először visszaadja a kulcsot a bejegyzésből, és utána
lúvja meg a compare() metódust.)
private int indexof(object key) {
int index = O; Iterator i = _entries.iterator();
for (i.first(); !i.isDone(); i.next()) { int cmp = _comparator.compare(key,
((Entry) i.current()).getKey());
if (cmp == O) { re tu r n i n d ex;
} else if (cmp < O) {
break;
}
437
B-fák
++index;
}
return -(index + l);
}
Most, hogy már meg tudunk keresni egy kulcsot a csomóponton belül, a csomó
pontok átnézése egy bejegyzés megtalálása végett elég egyszerű feladat.
A search() metódus először illeszkedő kulcsot keres. Ha talál egyet (i n d ex >=
O), azonnal visszaadja. Egyébként, ha a csomópont nem levélcsomópont, a keresés
rekurzív módon tovább folytatódik a megfelelő gyermekcsomópontban; különben
befejezi a futást, mert nem talált illeszkedő bejegyzést - hiszen a levélcsomópontok
nak nincsenek gyermekeik. Vegyük észre, hogy a search() metódus nem veszi fi
gyelembe a töröltnek jelölt bejegyzéseket. Ezt ne feledjük, mert később fontos lesz.
public Entry search(object key) { int index = indexof(key); if (index >= O) {
}
Entry entry = (Entry) _entries.get(index); return !entry.isoeleted()? entry: null;
return !isLeaf() ? ((Node) _children.get(-(index +
l))).search(key) : null;
}
Most azt szeretnénk, ha kulcsokat tudnánk beszúrni a csomópontba, de előtte még
meg kell valósítanunk azt a kódot, amely megoldja a csomópont szétválasztását.
A sp l i t() metódusnak szüksége van a szülőcsomópontra hivatkozásra, valamint
arra a pozícióra, ahová az újonnan létrehozott csomópontot be kell szúrnia. A sp l i t()
metódus először létrehoz egy új testvércsomópontot, ezért a levéljelzőt szintén átmá
solja (ugyanis a levélcsomópont testvére is levél). Ezt követően a középpont utáni
minden bejegyzés és gyermek átkerül a csomópontból a testvérbe. Majd a középső be
jegyzést beszúrja a szülőbe, és beilleszti a testvérre hivatkozást. A szétválasztott cso
mópontra hivatkozást csak akkor szúrja be a szülőbe, ha a szülő egy újonnan létreho
zott gyökércsomópon t, azaz még nincsenek gyermekei.
438
public void split(Node parent, int insertionPoint) { assert parent != null : "A 'parent' (szülő) nem lehet NULL";
Node sibling = new Node(isLeaf());
int middle = _entries.size() l 2;
move(_entries, middle +l, sibling._entries); move(_children, middle +l, sibling._children);
B-fák a gyakorlatban
}
parent._entries.insert(insertionPoint, _entries.delete(middle));
if (parent._children.isEmpty()) { parent._children.insert(insertionPoint, this);
} parent._children.insert(insertionPoint +l, sibling);
private void move(List source, int from, List target) {
}
assert source != null : "A 'source' (forrás) nem lehet NULL";
assert target != null : "A 'target' (cél) nem lehet NULL";
while (from < source.size()) { target.add(source.delete(from));
}
Most már szét tudunk választani csomópontot, tehát gondolkodhatunk az új bejegy
zés beszúrásán, nem feledve, hogy a leképzés garantálja a kulcsok egyediségét. Emi
att a bejegyzések nem mindig lesznek beszúrva. Ehelyett, ha már létezik az adott
kulcsra illeszkedő bejegyzés, a hozzá tartozó érték frissítődik.
Az első set() metódus először helyet keres a kulcsnak a csomóponton belül.
Ha megtalálta a kulcsot (index >= O), visszatér a megfelelő bejegyzéssel, ennek ér
tékét frissíti, és visszaadja az eredeti értéket. Ha nem találta meg a kulcsot, akkor be
kell szúrnia, amennyiben nincs valamelyik gyermekcsomópontban. Ezt a műveletet
egy másik set() metódus hajtja végre.
A második set() metódus először meghatározza, hogy a csomópont levélcso
mópont-e. Ha igen, akkor a kulcs nem található a fában, ezért új bejegyzésként be
szúrja az értékével együtt, és ennek megfelelően növeli a leképzés méretét. Ha azon
ban a csomópontnak vannak gyermekei, megkeresi a megfelelő gyermeket, és rekur
zívan meghívja az első set() metódust. Ebben az esetben, amennyiben a beszúrást
követően a gyermek betelik, szét kell választani.
public object set(Object key, Object value) { int index = indexof(key); if (index >= O) {
return ((Entry) _entries.get(index)).setvalue(value);
}
return set(key, value, -(index + l));
}
private object set(Object key, object value, int index) { if (isLeaf()) {
}
_entries.insert(index, new Entry(key, value)); ++_size; return null;
439
B-fák
Node child= ((Node) _children.get(index)); object oldvalue = child.set(key, value);
if C ch il d. is Fu ll ()) { child.split(this, index);
}
return oldvalue;
}
A csomóponton működő egyetlen további metódus - traverse()- az iterációhoz
szükséges. Ez a metódus a fában található összes bejegyzést beteszi egy listába. Elő
ször az aktuális csomópont nem törölt bejegyzéseit adja hozzá, ezután rekurzív mó
don meghivja a gyermekeit, amelyek ugyanezt teszik. Ez alapvetően preorder bejárás
(inorder bejárást is meg lehet valósítani, de ennek megoldását az olvasóra bízzuk).
public void traverseCList list) {
}
assert list != null : "a 'list' nem lehet NULL";
Iterator entries = _entries.iterator(); for (entri es. fi r st(); ! en tri es. i sDone(); e nt ri es. next()) {
Entry entry = (Entry) entries.current(); if (!entry.isDeleted()) {
list.add(entry);
}
}
Iterator children = _children.iterator(); for (children.first(); !children.isDone(); children.next()) {
((Node) children.current()).traverse(list);
}
Most, hogy áttekintettük a Node belső osztályt, folytathatjuk a BTreeMap fennmaradó
metódusaival, amelyeket a Map interfész igényel.
A get() metódus a kulcshoz rendelt értéket adja vissza. A gyökércsomópont
search() metódusát a megadott kulccsal hivjuk meg. Ha talál ilyen bejegyzést, visz
szaadja a hozzá tartozó értéket; egyébként null értékkel tér vissza, jelezve, hogy a
kulcs nem található a fában:
440
public object get(Object key) {
Entry entry = _root.search(key); return entry != null ? entry.getvalue()
}
null;
B-fák a gyakorlatban
A contai ns () metódus azt határozza meg, hogy a kulcs megtalálható-e a fában. Ez
megint a search() metódust hívja meg a gyökércsomópontra, és true a visszatérési
értéke, ha megtalálja a bejegyzést:
public boolean contains(object key) { return _root-search(key) != null;
}
A set() metódus a megadott kulcshoz értéket rendel, vagy frissíti. Itt a set() metó
dust a gyökércsomópontra hívjuk meg, hogy elvégezze a múveletet. A metódus vissza
térése után ellenőrizni kell, nem telt-e be a gyökércsomópont. Ha igen, új gyökércso
mópontot kell létrehozni, és a meglévőt szét kell választani. Ha nem, akkor ezt az ese
tet nem kell különösebben kezelni. Mindkét esetben a kulcshoz rendelt régi értéket (ha
van) adja vissza a hívónak, mert a Map interfésznek szüksége van erre az értékre:
public object set(Object key, object value) { object oldvalue = _root.set(key, value);
}
if (_root.isFull()) {
}
Node newRoot = new Node(false);
_root-split(newRoot, O); _root = newRoot;
return oldvalue;
A delete() metódus eltávolí�a a megadott kulcsot- és a hozzárendelt értéket- a
leképzésből. Ez ismét a search() metódust hívja meg a gyökércsomópontra, hogy
megtalálja a megadott kulcsú bejegyzést. Ha nem talál bejegyzést, null értékkel tér
vissza, jelezve, hogy a kulcs nem létezik. Egyébként a bejegyzést megjelöli töröltnek,
a leképzés méretét ennek megfelelően csökkenti, és a bejegyzéshez rendelt értéket
visszaadja a hívónak:
public object delete(object key) { Entry entry = _root_search(key);
if (entry == null) {
}
return null;
}
entry_setDeleted(true);
--_size;
return entry.setvalue(null);
441
B-fák
Az iterater O metódus a leképzés összes bejegyzését tetszőleges sorrendben befu
tó iterátonal tér vissza. Ez meghívja a traverse O metódust a gyökércsomópontra,
átad neki egy listát, amely a fa összes bejegyzését tárolja, és egy ehhez tartozó iterá
tort ad vissza a hívónak:
public Iterater iterator() {
List list = new ArrayList(_size);
_root.traverse(list);
return list.iterator();
}
A clearO metódus az összes bejegyzést törli a leképzésből. A fa kiürítéséhez a gyö
kércsomópontot beállitjuk levélcsomópontnak - mivel ennek nincsenek gyermekei -,
és méretét O-ra állitjuk
public void clear() {
}
_root new Node(true);
_size = O;
Végül a si ze O és az i sEm p ty O metódusok zárják le az interfészt:
public int size() {
re tu rn _si ze;
}
public boolean isEmpty() {
return size() == O;
}
A most létrehozott megvalósítás csak a memóriában működik. Valamivel több
munkát igényel, de viszonylag egyszerű olyan verziót készíteni, amely kimenthető
valarnilyen külső adathordozóra, például merevlemezre, illetve onnan visszaállitható.
Ezt bővebben lásd [Cormen, 2001] művében.
442
Összefoglalás
Összefogla l ás
A fejezetből a következőket tudhattuk meg:
• A B-fák alkalmasak a másoellagos tárolókon, például a merevlemezeken,
CD-ken stb. való keresésre.
• A B-fák a levélcsomópontoktól felfelé növekednek.
• A nem gyökér csomópontok mindig legalább félig be vannak töltve.
• Amikor a csomópontok "
betelnek", szétválasztjuk őket.
• A csomópont szétválasztásakor az egyik kulcs felkerül a szülőbe.
• A B-fa magassága csak akkor nő, amikor a gyökércsomópont válik szét.
• A B-fák mindig "kiegyensúlyozottak", így biztosí�ák az o(log N) nagyság
rendű keresési időt.
Gyakorlatok
1. Valósítsuk meg újra a traverse() metódust a Node osztályra úgy, hogy a kul
csok szerinti sorrendben adja vissza a bejegyzéseket.
2. Valósítsuk meg újra az indexof() metódust a Node osztályra úgy, hogy lineáris
keresés helyett bináris keresést végezzen.
443
TIZENHATODIK FEJEZET
Sztri ngkeresés
A részsztringkeresés problémája elég gyakran előfordul: a lemezen található fájlok
keresése, a DNS-keresések, sőt a Google is olyan keresőstratégiákon alapszik, ame
lyekkel hatékonyan lehet szöveget keresni. Ha használtunk már szövegfeldolgozót,
szövegszerkesztőt vagy kódírásra szolgáló szerkesztőt, akkor valamilyen szinten már
végeztünk sifringkeresést. Ez a Fi n d funkció.
Sok sztringkeresési algoritmus létezik - és kétségkívül még sokat fognak idővel
alkotni - amelyek mindegyikét megadott adattípus kezelésére optimalizálták. Néme
lyik algoritmus formázarlan szöveg esetén hatékonyabb, míg mások a sok ismédést
tartalmazó szövegek és/vagy minták, például DNS-töredékek esetén működnek jól. Ebben a fejezetben két, formázarlan szövegre alkalmazható keresési algoritmust
tárgyalunk. Az első a triviális letámadó algoritmus lesz, ezután rátérünk a kifinomul
tabb Boyer-Moore-algoritmusra. Mindkettőt részletesen ismerte�ük, majd bemutat
juk, hogy a letámadásos megközelítés egyszerű módosításával lényegesen gyorsítható
a Boyer-Moore-algoritmus. A fejezetben a következőket tanuljuk meg:
• letámadásos sztringkeresési algoritmus leírása és megvalósítása,
• a Boyer-Moore sztringkeresési algoritmus leírása és megvalósítása,
• a két algoritmus teljesítménykarakterisztikájának megértése,
• általános sztringillesztő iterátor leírása és megvalósítása,
• egyszerű fájlkereső alkalmazás leírása és megvalósítása.
Általános sztringkereső interfész
Mivel többfajta sztringkeresési algoritmust szetetnénk megvalósítani, beleértve ké
sőbb az igényeinknek megfelelő saját változatokat is, olyan interfészt érdemes kidol
gozni, amely a mögöttes eljárástól függedenill nem igényel módosítást. Ráadásul,
mivel valamennyi sztringkeresés egyeden API -hoz fog illeszkedni, elég lesz egyeden
tesztcsomagot írni a helyességük igazolás ához.
Sztringkeresés
Gyakorlófeladat: az interfész létrehozása
Először ezt az egyszerű interfészt hozzuk létre:
package com.wrox.algoritnms.ssearcn;
public interface Stringsearcher { public StringMatch search(charsequence text, int from);
}
A St ri ngMatch osztályt is létre kell hozni. Ez lesz a search() metódus visszatérési
típusa:
446
package com.wrox.algorithms.ssearch;
public class StringMatch {
}
private final Charsequence _pattern; private final charsequence _text; private final int _index;
public StringMatch(charsequence pattern,
charsequence text, int index) {
}
assert text != null : "A 'text' (szöveg) nem lehet NULL"; assert pattern != null : "A 'pattern' (minta) nem lehet NULL"; assert index >= O : "Az 'index' nem lehet negatív";
_text = text; _pattern = pattern; _index = index;
public charsequence getPattern() { return _pattern;
}
public charsequence getText() { return _text;
}
public int getindex() { return _index;
}
Általános tesztcsomag
A megvalósitás müködése
A St ri ngSearcher osztály egyetlen search() metódust definiál. Ez a metódus két argumentumot vár: a szöveget, amelyen bélill keresni kell, illetve a keresés kezdő pozícióját; és az illeszkedést (ha van) jelző objektummal tér vissza, amiről kicsit később bővebben írunk. Feltételezzük, hogy a kód létrehozásakor rögzí�ük a keresett mintát - egy konkrét megvalósításnál -, ezért ezt nem kell paraméterként átadni a search metódusnak.
Vegyük észre, hogy szöveg esetén a charSequence típust használtuk a String helyett. Ha szövegszerkesztő programot készítenénk, valószínűleg a StringBuffer típust használnánk a szerkesztett dokumentum szövegének tárolásához. Előfordulhat azonban olyan eset, arnikor csak sima string típusban kell keresnünk. Ebben a két osztályban rendszerint- String és StringBuffer - nincs semmi közös, ami azt jelenti, hogy meg kell írni az algoritmusok két különböző megvalósítását: egyet a String típusok kezelésére, egy másikat pedig a StringBuffer típusokhoz. Szerencsére a szabványos Java könyvtárban van egy interfész, a charsequence, amelyet mind a String, mind pedig a StringBuffer osztály megvalósít, és ez biztosí�a a két keresési algoritmushoz szükséges metódusokat.
A search() metódus hívásakor vagy a St ri ngMatch osztály egy példányát kapjuk vissza, vagy null értéket, ha nem volt illeszkedés. Ez az osztály teljes mértékben egységbe zárja az illeszkedés fogalmát, vagyis nemcsak az illeszkedés pozícióját (O, 1,
2 stb.), hanem a szöveget és magát a mintát is tárolja. Ily módon a keresés eredménye minden más objektumtól független, ami a kontextus t illeti.
Általános tesztcsomag
Annak ellenére, hogy a sztringkeresés elméletileg elég egyszerű, az algoritmusok tartalmaznak olyan finomságokat, amelyeken könnyen elcsúszhatunk. Mint mindig, itt is a tesztelés a legjobb védekezés. Ezek a tesztek fogják garantálni a kóclak helyességét: ez a mi védőhálónk, amely biztosí�a, hogy akármilyen bonyolulttá válnak is az általunk készített algoritmusok, a viselkedésük kifelé mindig egyforma lesz.
Számos tesztesetet fogunk létrehozni, többek között ezeket: mintakeresés szöveg elején, mintakeresés szöveg végén, mintakeresés szöveg közben és több, egymást átfedő minta-előfordulás keresése. Ezek mindegyike más vonatkozásban teszteli a sztringkeresőt, arnivel igazolható a helyessége.
447
Sztringkeresés
Gyakorlófeladat: a tesztosztály létrehozása
Ebben a fejezetben minden sztringkereső hasonlóan viselkedik, tehát kipróbált és
megbízható metódusunkat használjuk fel az általános tesztcsomag létrehozásához,
amely alosztálykezelésre is alkalmas lesz.
package com.wrox.algorithms.ssearch;
import junit.framework.Testcase;
public abstract class Abstractstringsearcher extends Testcase { protected abstract stringsearcher
createsearcher(Charsequence pattern);
}
Az első teszteset tényleg a legegyszerűbb valamennyi lehetséges eset közül: keresés
üres sztringben. Valahányszor olyan mintával hívjuk meg a search() metódust,
amely nem található a szövegben, null értéket kell visszaadnia, jelezve, hogy nem
volt illeszkedés. Az ilyen korlátfeltételek tesztelése nagyon fontos része a jó minősé
gű kód írásának.
public void testNotFoundinAnEmptyText() {
}
Stringsearcher searcher = createsearcher("NOT FOUND"); assertNull(searcher.search("", 0));
A következő esetben a szöveg legelején keressük a mintát:
public void testFindAtThestart() { String text= "Keress meg az elején"; String pattern = "Keress";
Stringsearcher searcher = createsearcher(pattern);
String�atch match = searcher.search(text, O); assertNotNull(match); assertEquals(text, match.getText()); assertEquals(pattern, match.getPattern()); assertEquals(O, match.getindex());
assertNull(searcher.search(text, match.getindex() +l)); J.
Miután megtörtént a mintakeresés a szöveg elején, megnézzük, hogy a szöveg végén
található-e:
448
Általános tesztcsomag
publ1c�id-testFindAtTh-eefidC)-{ String text ,. "Keress meg a végén"; string pattern = "végén";
l
stringsearcher searcher = createsearcher(pattern);
StringMatch match = searcher.search(text, 0); assertNotNull(match); assertEquals(text, match.getText()); assertEquals(pattern, match.getPattern()); assertEquals(lS, match.getindex());
assertNull(searcher.search(text, match.getindex() +l));
Ezután azt teszteljük, hogy megtalálható-e a minta a szöveg közepén:
public voia testfindinTheMiaclleO-{
}
String text = "Keress meg a szöveg közepén"; String pattern = "szöveg";
St ri ngsearcher searcher = createsearcher(p'attern);
StringMatch match = searcher.search(text, O); assertNotNull(match); assertEquals(text, match.getText()); assertEquals(pattern, match.getPattern()); assertEquals{lS, match.getindex());
assertNull(searcher.search(text, match.getindex() +l));
Végezetül azt szeretnénk ellenőrizni, hogy vannak-e egymást átfedő mintailleszkedé
sek. Nem mintha ez nagyon gyakran fordulna elő formázatlan szövegben, de meg kell
győződnünk az algoritmus helyes működéséről. Emellett ez a kereső azon képességét
is teszteli, hogy megtalál-e többes illeszkedéseket-ilyesmit eddig még nem csináltunk.
putille voia testFindoverlappingO-C String text = "abcdefffff-fedcba"; String pattern = "fff";
Stringsearcher searcher = createsearcher(pattern);
StringMatch match = searcher.search(text, O); assertNotNull(match); assertEquals(text, match.getText()); assertEquals(pattern, match.getPattern());
. asser,tEquals(S, match.ge�t�I� n� d �e�x�( L))�· --------------- -�--------�
449
Sztringkeresés
match = searcher.search(text, match.getindex() + l); assertNotNull(match); assertEquals(text, match.getText()); assertEquals(pattern, match.getPattern()); assertEquals(6, match.getindex());
match = searcher.search(text, match.getindex() +l); assertNotNull(match); assertEquals(text, match.getText()); assertEquals(pattern, match.getPattern());
assertEquals(7, match.getindex());
assertNull(searcher.search(text, match.getindex() +l)); } -------------�----------- --------- ��---- - - ---�----- -�
A megvalósitás működése
A létrehozandó sztringkeresések mindegyike magába ágyazza a keresett mintát - te
kintsük úgy ezeket, mint egy "intelligens" mintát, vagyis a createSearcher() egyeden
argumentumaként deklarálja a mintát. Ezután minden tesztmetódusban létrehozunk
egy keresőt a createsearcher () hívással a teszt további részének végrehajtása előtt.
Az első teszt üres sztringet keres, aminek az eredménye null lesz, jelezve, hogy
nem talált ilyet.
A következő tesztnél azt várjuk, hogy lesz illeszkedés a szöveg elején, ezért
gondoskodunk róla, hogy a search() ne null értéket adjon vissza. Ezután biztosít
juk, hogy az illeszkedés részletei helyesek legyenek, főként a pozíciót kell ellenőrizni:
ebben az esetben ez az első karakter. A szöveget áttekintve láthatjuk, hogy több il
leszkedés nem lehet. Ezt is tesztelni kell, vagyis újabb keresést indítunk, az előző il
leszkedéstől egy karakterpozícióval jobbra kezdve, és meggyőződünk róla, hogy
null lesz a visszaadott érték.
A harmadik teszt majdnem azonos az előzővel, csak ebben az esetben a minta a
szöveg jobb oldali végén, és nem a bal oldalin (az elején) fordul elő egyszer.
Az utolsó teszt valamivel bonyolultabb, mint az előzőek, mivel itt a minta több
ször - egészen pontosan háromszor -, némileg árlapolva fordul elő. A teszt igazolja,
hogy a kereső minden előfordulást megtalál, és helyes sorrendben.
Ennyit a tesztesetekrőL Jóval több tesztet is írhattunk volna, de az itt megvalósí
tottak eléggé jól lefedik az eseteket, és lehetővé teszik, hogy figyelmünket tényleg a
keresés problémájára fordítsuk.
450
Letámadásos algoritmus
Letámadásos algoritmus
A legegyszerűbb és legtriviálisabb megoldás a letámadásos keresés megvalósítása a
szövegen. Ez az algoritmus elég széles körben alkalmazott, és a legtöbb esetben
egész jó teljesítményt nyújt. A leírása és a kódolása is könnyű.
A letámadásos algoritmus nagyon egyszerű, ezért néhány egyszerű lépésben meg
adható. Képzeljük e� hogy a mintával lefedjük a szöveget, annak bal végénél kezdve és
mindig egy karakterrel jobbra csúsztatva a mintát, amíg illeszkedés t nem találunk.
1. A szöveg első (bal szélső) karakterétől indulunk.
2. Balról jobbra összehasonlítjuk a minta és a szöveg minden egyes karakterét.
Ha az összes karakter megegyezik, akkor illeszkedést találtunk.
Egyébként, ha elértük a szöveg végét, akkor már nem lehet illeszkedés, és
készen is vagyunk.
Ha az előző esetek egyike sem áll fenn, akkor egy karakterrel jobbra moz
gatjuk a mintát, és a 2. lépéstől megismételjük az eljárást.
A következő példa bemuta*, hogyan működik a letámadásos keresési algoritmus,
miközben a ri n g mintát keresi a St ri n g search i n g szövegben. Először a ri n g ka
raktersorozatot összehasonlí�a a St ri részsztringgel-látjuk, hogy itt nincs illeszke
dés-, ezután a ri n g mintát a tri n szöveggel, és végül harmadik próbálkozásra talál
illeszkedést. Figyeljük meg a minta csúsztatását: a letámadásos megközelítés minden
karaktert összehasonlít.
stri ng search l ring 2 ri n g 3 ri n g
Most tegyük fel, hogy a ri n g minta további előfordulásait keressük. Már tudjuk,
hogy a minta megtalálható a harmadik karakterpozícióban, vagyis nincs értelme in
nen indítani az újab b keresést. Ehelyett az előző eljárás szerint egy karakterrel jobbra
(a negyedik karakternél) kezdjük a keresést. A következő példa mutatja a keresés to
vábbi lépéseit, ahogy a mintát mindig egy pozícióval jobbra csúsztatjuk
stri ng search 4 ri ng 5 ri n g 6 ri n g 7 ri n g 8 ri n g 9 ring 10 ri ng
451
Sztri n g keresés
Ebben a példában a "ri n g" mintának nincs több előfordulása a "st ri n g search"
szövegben, így a keresés végigért a szövegen úgy, hogy nem talált illeszkedést. Ve
gyük észre, hogy a mintát nem kell végigcsúsztatill az utolsó karakterig, tulajdon
képpen csak addig, amíg a szöveg tart. Láthatjuk, hogy ha a tizedik karakteren túl
próbálnánk meg mozgatni, akkor a "ring" rnintát a "rch" részsztringgel kellene ösz
szehasonlítani. Ez a két sztring sohasem egyezhet meg, hiszen eltérő a méretük (az
egyik négy karakter hosszú, a másik három), ezért a mintát mindig csak addig kell
csúsztatni, amíg az utolsó karaktere el nem éri a szöveg végét.
Elég egyszerűen meghatározható, meddig tartson a keresés, azaz mikor ér véget az
összehasonlítandó szöveg: tetszőleges, M hosszúságú minta és N hosszúságú szöveg
esetén sohasem kell a mintát a szöveg N - M + l karakterpozícióján túl csúsztatni.
A rni példánkban a szöveg 13, a minta 4 karakter hosszú, arniből 13 - 4 + l = 10
adódik - és pontosan ezt láttuk a példában.
Most, hogy már értjük az algoritmus működését, továbbléphetünk, és megvaló
síthatjuk hozzá a kódot. Néhány tesztet is létre fogunk hozni, hogy meggyőződhes
sünk az algoritmus helyességérőL
Gyakorlófeladat: a tesztosztály Létrehozása
A munka nehezét már elvégeztük a fejezet korábbi részében, arnikor létrehoztuk a
tesztesetet. Ott leírtuk, hogyan használhatjuk fel újra a létrehozott teszteseteket
Most ki is próbálhatjuk.
package com.wrox.algorithms.ssearch;
public class BruteForcestringsearcherTest
}
extends AbstractStringsearcherTestcase {
protected Stringsearcher createSearcher(CharSequence pattern) {
return new BruteForcestringsearcher(pattern);
}
A megvalósitás müködése
Az Abstractstri ngSearcherTestCase osztály kiterjesztésével a tesztosztály örökli
az összes előre definiált tesztmetódust, ami azt jelenti, hogy nincs más dolgunk,
rnint hogy létrehozzuk a saját kereső osztályunk- ebben az esetben a BruteForce
Stri ngSearcher osztály- egy példányát a megadott mintával.
452
Letámadásos algoritmus
Gyakorlófeladat: az algoritmus megvalósítása
A következőkben létrehozzuk a BruteForcestri ngsearcher osztályt:
packag" e com ."w ro x. al go"ri thiils. s"search;
public class BruteForcestringsearcher implements Stringsearcher { private final charsequence _pattern;
public BruteForceStringsearcher(Charsequence pattern) { assert pattern != null : "A 'pattern' (minta) nem lehet NULL"; assert pattern.length() >O
"A 'pattern' (minta) nem lehet üres";
}
_pattern = pattern;
}
public StringMatch search(Charsequence text, int from) { assert text != null : "A 'text' (szöveg) nem lehet NULL"; assert from >= O : "A 'from' (kezdőpont) nem lehet negatív";
}
int s = from;
while (s <= text.length()- _pattern.length()) { int i = O;
while (i < _pattern.length() && _pattern.charAt(i)
++i;
}
if Ci == _pattern.length()) {
text.charAt(s + i)) {
return new StringMatch(_pattern, text, s);
}
++s;
}
return null;
A megvalósitás müködése
A BruteForcestri ngsearcher osztály megvalósítja a korábban definiált string
searcher interfészt. A konstruktor alapvető ellenőrzéseket végez, például megnézi,
hogy az osztály kapott-e mintát, és ha igen, akkor legalább egy karakter hosszú-e,
majd letárolja a hivatkozást a mintára.
453
Sztringkeresés
A search() metódus két egymásba ágyazott ciklust tartalmaz, ezek vezérlik az algoritmus működését: a külső whi l e ciklus azt vezérli, hogy az algoritmus meddig fusson a szövegen, núg a belső whi l e ciklus hajtja végre a tényleges balról jobbra haladó karakter-összehasonlítást a minta és a szöveg között.
A belső ciklus végén, ha a minta összes karakterét sikeresen összehasonlította, akkor illeszkedés van. Ellenkező esetben, ha nincs illeszkedés, akkor a szöveg aktuális pozícióját eggyel megnöveli, és a külső ciklus tovább folytatódik. Ez az eljárás addig ismétlődik, anúg vagy talál illeszkedést, vagy végigért a szövegen. Ez utóbbi esetben null értéket ad vissza, jelezve, hogy nincs további találat.
Ahogy korábban már említettük, ezt az algoritmust letámadásosnak nevezzük, a következők miatt: nincs benne semmi trükk, rövidítés és optimalizálás, amellyel megpróbálnánk csökkenteni az összehasonlítások számát. Legrosszabb esetben a minta minden karakterét összehasonlítjuk a szöveg (majdnem) minden karakteréveL Így a legrosszabb esetbeli futásidő O(NM)! A gyakorlatban azonban sokkal jobb a teljesítménye, ahogy a fejezet vége felé majd látni fogjuk.
A Boyer-Meore-algoritmus
Bár a letámadásos megközelítés elég jól teljesít, látható, hogy messze nem optimális a működése. Még átlagos esetben is sok a felesleges indulás és a részleges illeszkedés. Néhány egyszerűbb módosítással azonban sokat javíthatunk rajta.
R. S. Boyer és J. S. Moore alkotta meg azt az algoritmust, amely a jelenleg ismert leggyorsabb sztringkeresési algoritmusok közill többnek lett az alapja. Észrevették, hogy a letámadásos algoritmusban a mintamozgatások többsége felesleges. Sok esetben a részsztring karakterei nincsenek is a mintában, ami lehetővé teszi, hogy teljesen átugorjuk őket.
A következő példa az eredeti keresést muta�a be, de most a Boyer-Maore-algoritmust alkalmazva. Vegyük észre, milyen nagy szövegrészeket ugrik át, miáltal a szöveg-összehasonlítások száma 4-re csökken. Vessük össze ezt az értéket a letámadásos algoritmussal, amely összesen 10 összehasonlítást végzett!
String Search l ring 3 ri n g 4 ring 8 ri n g
A titok nyitja, hogy tudjuk, hány hellyel kell eltolni a keresést, amikor nincs illeszkedés. Ez a minta elemzésével határozható meg. V alahányszor sikertelen az illesztés, meg kell keresni a mintában a nem illeszkedő karakter utolsó Go bb szélső) előfordulását, és innen folytatni, a hibás karakter elmélet szerint:
454
A Boyer-Maore-algoritmus
Az eredeti Bqyer-Moore-algoritmus valrfjában a heurisztikus utótagra épít. Ezzel
szemben az adott témában született tö"bb cikk ÍS a!"(! bizof!JÍtja, hogy e!"(! f!JUgodtan fi
gyelmen kívüllehet hagyni, mert csak nagyon hosszú vagy ismétfódő minta esetén javítja a
teljesítméf!Jt. Témánk tárgyalásánál mi az egyszerűsített ver.?fóra szoritkozunk.
1. Ha a karakter megtalálható a mintában, akkor a mintát annyi hellyel toljuk
jobbra, hogy az adott karaktere illeszkedjen a szöveg azonos karakterére. A
példában a g és az i első sikertelen összehasonlitása után megállapítjuk,
hogy az i benne van a mintában, vagyis két hellyel jobbra toljuk a mintát,
hogy ezek a karakterek illeszked jenek.
2. Ha a karakter nem található meg a mintában, akkor a mintát annyi hellyel
toljuk jobbra, hogy a kérdéses karakter mögé kerüljön. A mi példánkban a 4.
pozícióban a g-t hasonlitjuk a szóközhöz. Maga a minta egyáltalán nem tar
talmaz szóközt, vagyis négy hellyel jobbra toljuk a mintát, hogy teljes egé
szében átugorjuk a szóközt.
3. V alahányszor az elmélet alapján negatív eltolásta lenne szükség, és csak eb
ben az esetben, ahhoz a naiv megközelítéshez folyamodunk, hogy egy hely
lyel jobbra toljuk a mintát, mielőtt visszatérnénk a helyes Boyer-Moore al
goritmushoz.
Ez az utolsó pont valószínűleg részletesebb magyarázatra szorul. Képzeljük el, hogy
az over mintát keressük az everythi ng szövegben:
everything over------
Jobbról balra haladva a mintában, először az r karaktert, majd az e és a v karaktert
hasonlitjuk össze a szöveggel, míg végül az o és az e nem illeszkedik. Ha vakon kö
vetnénk az elméletet, azt vennénk észre, hogy ebben az esetben visszafelé (balra)
kellene tolnunk a mintát:
--everything over--------
A mintában van ugyan "e" karakter, de ez a nem illeszkedő karaktertól jobbra talál
ható. Nyilvánvaló, hogy ez így nem jó. Ezért a példánknál maradva, a mintát egy
hellyel jobbra kell tolni, és folytatni az összehasonlitást jobbról balra haladva, azaz az
"over" mintát a "very" részsztringgel, és így tovább.
everything -over----------over-
455
Sztringkeresés
V alefjában annál valamivel hatékot!Jabb módszerek is léteiJ1ek az i(yen esetek kezelésé
re, mint ho!!J e!!J karakterpotfcióval jobbra to!Juk a mintát, de most a lehető lege!!Jsze
rűbbé próbáltuk tenni az algoritmust. Sqjnos ez azt jelenti, ho!!J a legrosszabb esetben ez
a Bf!Yer-Moore-megvalósítás nem telJesít jobban, mint a letámadásos megko·zelítés, de a
!!Jakorlatban jóval hatékof!Jabban műkOdik.
Később majd látni fogjuk, hogy az algoritmusnak az a képessége, hogy teljes szövegrészeket át tud ugrani, lenyűgöző teljesítménynövekedést eredményez a letámadásos kereséshez képest. Ha a szövegben netalán a mi.nta egyeden karaktere sem fordulna elő, akkor mi.nden lépésben át lehet ugrani a szövegben a mi.nta teljes hosszát. Így a legjobb esetbeli futásidő O(N/M), ahol N a szöveg hossza, és Ma mi.nta hossza.
Az előző megvalósításnál alkalmazott eljárást követve most is létrehozunk egy tesztosztályt és egy keresőt, amely az algoritmus nevét fogja viselni.
A tesztek létrehozása
Itt is felhasználha�uk azokat a teszteket, amelyeket az absztrakt tesztesetekhez hoztunk létre. Ezúttal azonban egy további tesztet is készítünk, kifejezetten a BoyerMoore-megvalósításhoz.
Gyakorlófeladat: a tesztosztály létrehozása
A következőkben létrehozzuk a tesztosztályt:
456
package com. wrox. a l go ri thms. ssearch;
public class BoyerMoorestringsearcherTest extends AbstractstringsearcherTestcase {
protected stringsearcher createsearcher(Charsequence pattern) { return new BoyerMOoreStringsearcher(pattern);
}
public void testShiftsDontErroneouslyignoreMatches() { String text = "aababaa";
}
string pattern = "baba"; stringsearcher searcher = createsearcher(pattern);
StringMatch match = searcher.search(text, 0); assertNotNull(match); assertEquals(text, match.getText()); assertEquals(pattern, �tch.getPattern()); assertEquals{2, match.getindex()); assertNull(searcher.search(text, match.getindex() + l));
}----------------------------------------------�
A Boyer-Maore-algoritmus
A megvalósitás működése
Mivel a Boyer-Moore-algoritmus egyszerre több pozícióval is el tudja tolni a mintát,
biztosítani kell, hogy a megfelelő számú hellyel történjen az eltolás. A mostani eset
ben a mintában minden karakter kétszer szerepel. Ha a kódban hibás az utolsó elő
fordulás számítása, akkor túl sok vagy túl kevés hellyel tolja el a mintát.
Az algoritmus megvalósftása
A Boyer-Moore-algoritmus megvalósítása több lépésben történik. Létre kell hozni a
sztringkeresési kódot, ki kell számítani az utolsó előfordulások táblázatát, és végül el
kell végezni a keresést.
Gyakorlófeladat: a BoyerMooreStringSearcher osztály létrehozása
· Először az alaposztály-defmíciót készítjük el:
r package com. wrox-:á lgonthiiis. 5 search;'
public class BoyerMoorestringsearcher implements stringsearcher { private final Charsequence _pattern; private final short(] _lastoccurrence;
public BoyerMOoreStringsearcher(CharSequence pattern) { assert pattern != null : "A 'pattern' (minta) nem lehet NULL"; assert pattern.length() >O: "A 'pattern' (minta) nem
lehet üres";
_pattern = pattern; _lastoccurrence = computeLastOccurrence(pattern);
}
l
A megvalósitás működése
Eddig az osztály nagyon hasonlit a letámadásos kódhoz, azzal a különbséggel, hogy
itt van egy _l as toccurrence tömb, amelyet a computeLastoccurrences metódus
hívással inicializálunk Ha visszaemlékezünk, a Boyer-Moore-algoritmusnak ismer
nie kell az egyes karakterek utolsó előfordulását a mintában. Ezt kiszámíthatjuk a
minta többszöri, igény szerint ismételt átolvasásával, de ez nyilvánvalóan jelentős se
gédszámítási költséget jelent. V agy kiszámítha�uk az értékeket egyszer, letároljuk, és
ha szükséges, kikeressük őket.
457
Sztringkeresés
A jellapozási táblázat létrehozása egyszeri segédszámítási költséggel jár, ame/y arátryos a
minta hosszával és az alkalmazott karakterkés::det méretéveL Kis karakterkés::detek
esetén, mint az ASCII, a segédszámítási költség minimális. A nagyobb karakterkés::de
tek azonban, mint az ázsiai és a kiizép-keleti tryelvek karakterkés�ete, botryolultabb
technikákat igétryelnek, de ezeket ebben a kónyvben nem tár;gya!juk.
Gyakorlófeladat: az utolsó előfordulások táblázatának kiszámítása
A computeLastoccurrences() metódusnak meg kell adni a mintát, és visszaad egy
tömböt, amely az egyes karakterek utolsó előfordulásának helyét (O, l, 2 ... ) tartal
mazza. Ezt azután a_ l as toccurrence változó tárolja.
private static short[] computeLastOccurrence(charsequence pattern) {
short[] lastoccurrence =new short[CHARSET_SIZE];
for (int i =O; i < lastoccurrence.length; ++i) { lastoccurrence[i] = -1;
}
for (int i =O; i < pattern.length(); ++i) { lastoccurrence[pattern.charAt(i)] =(short) i;
}
return lastoccurrence;
}------------------------------�--------------�
A megvalósitás müködése
Most az ASCII karakterkészlet használatát feltételezzük, vagyis először létrehozunk
egy 256 elemű tömböt - hogy minden karakter tárolható legyen -, és a tömb min
den elemét -1 értékre állitjuk be, jelezve, hogy alapértelmezésben nem található a
mintában.
Ezután a ITiintában balról jobbra haladva minden karakteren végigmegyünk, és
ezek karakterkódja lesz a mutató arra a tömbelemre, ahová a pozícióját bejegyezzük
A minta ilyen módon történő feldolgozásával biztosítható, hogy minden egyes ka
rakter pozícióját felülírjuk a következő előforduláséval, ami garantálja, hogy a tömb
mindig a legutolsó Go bb szélső) előfordulás pozícióját tartalmazza.
Tegyük fel, hogy nagyon leegyszerűsített karakterkészlettel dolgozunk, amely
csak öt karakterből áll: A, B, c, D és E. Ebből definiáljuk a DECADE mintát, és létrehoz
zuk a megfelelő utolsó előfordulások táblázatát (lásd a 16.1. ábrán).
Amintában van egy A karakter a 3. helyen és egy c a 2. helyen, de nincs benne
B, amely következésképpen -1lesz a tömbben. A D és az E azonban kétszer fordul
elő, és mindkettőhöz a jobb szélső pozíciót rendeljük: 4, illetve 5.
458
A Boyer-Moore-algoritmus
-1
16. 1. ábra. Az utolsó elijordulások táblázata
Gyakorlófeladat: a keresés végrehajtása
Ugyanúgy, ahogy a letámadásos megközelítésnél, minden alkalommal egyszerűen
megnövelhetnénk eggyel az aktuális pozíciót a szövegen belül, ám a Boyer-Maore
algoritmus ennél bonyolultabb eljárást igényel.
publi'C""""S'tringMatch searcti(cliarsequence text, int from) { assert text != null : "A 'text' (szöveg) nem lehet NULL";
assert from >= O : "A 'from' (kezdőpont) nem lehet negatív";
}
int s = from;
while (s <= text.length() - _pattern.length()) { int i = _pattern.length() - l;
}
char c = O;
while (i >= O
&& _pattern.charAt(i) --i;
}
if (i < 0) {
(c = text.charAt(s + i))) {
return new StringMatch(_pattern, text, s); }
s += Math.max(i - _lastoccurrence[c], l);
return null;
A megvalósitás működése
Maga a search() metódus szerkezeti felépítésében nagyon hasonlít a letámadásos
változathoz, két lényeges eltéréstől eltekintve.
• A mintát visszafelé, azaz jobbról balra hasonlítjuk össze a szöveggel.
• Az eltolás meghatározása tömbolvasással és számítással jár.
459
Sztringkeresés
Az eltolás kiszámítását végrehajtó kód a következő:
s += Math.maxCi - _lastoccurrence[c], l);
A számítás elvégzése elég egyszerű: vesszük a szövegből a nem illeszkedő karaktert,
és kikeressük a mintán belüli legutolsó ismertpozícióját (O, l, 2 ... ) . Ezt azután levonjuk
a mintán belüli aktuális pozícióbóL Példaként tételezzük fel, hogy az abcd mintát
akarjuk összevetni a bdaaedccda szöveggel:
bdaaedccda
abcd---�--
Először azonnal eltérést találunk, arnikor a d-t (mintán belüli pozíciója 3) az a-val ösz
szehasonlí�uk. Az a utolsó előfordulása a mintában a O. pozícióban van, így az egyik
ből kivonva a másikat az eltolás értéke 3 - O = 3. Három hellyel jobbra mozgatva a
mintát, az abcd karaktersorozatot most az aaed részsztringgel hasonlí�uk össze:
bdaaedccda
--abcd----
Bár a két d illeszkedik, az előtte levő két karakter (mintán belüli pozíciója 2) nem, és
mivel nincs e a mintában, a táblafellapozás eredménye -l; az eltolás számításánál ezt
felhasználva az eredmény 2 - C -l) = 3. A minta újabb három hellyel történő elto
lása révén most az abcd karaktersorozatot a dccd részsztringgel ve�ük össze:
bdaaedccda
-----abcd-
Itt a b (mintán belüli pozíciója l) és a szöveg c karaktere nem illeszkedik. A c utolsó
előfordulási helye a mintában a 2. pozíció, arniből az l - 2 = -l eltolásérték adó
dik. Nyilvánvaló azonban, hogy visszafelé nem akarjuk csúsztatni a mintát.
Emlékezzünk vissza, hogy a három pontban megfogalmazott elméletünk utolsó
része éppen a negatív eltolással foglalkozik. llyen esetekben a naiv megközelítéshez
kell folyamodnunk, vagyis eggyel jobbra kell tolni a mintát. Tehát a Ma th. max C ... , l)
hívás azt biztosí*, hogy függetlenül a kiszámított eltolásértéktől végül mindig legalább
eggyel jobbra mozga�uk a mintát.
460
Sztringillesztő iterátor
Sztri ngi llesztö iterátor
Ha megnézzük a teszteseteket, észrevehe�ük, hogy amikor több találatot is kere
sünk, mindig el kell tárolni az aktuális helyet. Ez a megközelítés, bár eddig megfelelt
a célj ainknak, végül. is kétszer annyi kódolást igényel: minden alkalommal, amikor
keresést akarunk végezni, nemcsak azt a szöveget kell eltárolni, amelyben keresünk,
hanem az aktuális pozíciót is. Tulajdonképpen egy másik, a kereső fölött álló osz
tályra van szükségünk, amely beágyazza ezt a viselkedést.
Gyakorlófeladat: a StringMatchlterator osztály létrehozása
A 2. fejezetben vezettük be az Iterator osztályt, amelyet a könyvben végig felhaszná
lunk. Hozzuk létre a következő, az egymást követő többszöri keresések végrehajtásá
hoz szükséges viselkedést és állapotot beágyazó osztályt, amely ismét igazolja az iterá
tor, valamint az általunk megkonstruált sztringkereső hatékonyságát és rugalmasságát:
package com.wrox.algortthm�search;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutOfBoundsException;
public class StringMatchiterator implements Iterator { private final stringSearcher _searcher; private final charsequence _text; private StringMatch _current;
public StringMatchiterator(StringSearcher searcher, charsequence text) {
}
assert searcher != null : "A 'searcher' (kereső) nem lehet NULL";
assert text != null : "A 'text' (szöveg) nem lehet NULL";
_searcher = searcher; _text = text;
public void last() { throw new unsupportedoperationException();
}
public void previous() { throw new unsupportedOperationException();
}
public boolean isoone() { return _current== null;
l
461
Sztringkeresés
}
public voüi first() { _current= _searcher.search(_text, O);
}
public void next() { if (!i soone()) {
_current = _searcher. search(_text, _current. getindex O + l); }
}
public Object current() throws IteratoroutofsoundsException { if (isoone()) {
throw new IteratoroutofsoundsException(); } return _current;
}
A megvalósitás müködése
A St ri ngMatchiterator osztályban található a sztringkereső, a szöveg, amelyben
keresünk, valamint az aktuális találat (ha van). Ebből következően feltételezzük,
hogy a sztrmgkereső már a sztringillesztő iterátor létrehozása előtt elkészült.
A last() és a previous() metódusok unsupportedoperationException hiba
jelzést adnak. Ennek az az oka, hogy a St ri ngsearcher interfész csak előrehaladó
keresést biztosít a szövegben.
Az i soone() metódus megvalósítása-egyszerű, mivel a sztrmgkereső mindig
null értéket ad vissza, amikor már nincs több találat.
Az első illeszkedést akkor találjuk meg, ha a sztringkeresőt a O kezdő karakterpo
zícióval hívjuk meg- ez jelzi a szöveg elejét. A következő és a további illeszkedéseket
ott kell keresnünk, ahol a legtöbbet nyerünk az iterátor használatával. Mivel mindig va
lamely megelőző találat eredményére támaszkodunk, ebből könnyen kiszámítható a tő
le jobbra eső következő karakterpozíció, ahonnan folytatható a keresés.
Végül az aktuális találatot a current() metóduson keresztül tesszük elérhetővé,
amely az IteratoroutOfBoundsExcepti on hibával tér vissza, ha nincs illeszkedés.
A teljesítmény összehason litása
Most, hogy sztrmgkeresési algoritmusaink működnek, vessük össze a teljesítményü
ket. Szmte biztosak lehetünk benne, hogy a Boyer-Maore-algoritmus jobb teljesít
ményt fog nyújtani, mint a letámadásos; de hogyan tudnánk ezt igazolni? A szokásos
eljárás az lenne, hogy készítünk egy tesztcsomagot, amely kiszámítja a legjobb, a leg
rosszabb és a tipikus eset időigényét. Ehelyett úgy gondoljuk, hogy egy idevágó gya
korlati példa jobb lenne: fájlkeresés.
462
A teljesítmény összehasonlítása
Ebben a részben készítünk egy egyszerű alkalmazást, amely úgy teszteli a sztring
keresőinket, hogy fájlban keres m.intákat. Az eljárás során bemutatunk egy egyszerű
technikát, amellyel mérhető az egyes megvalósítások relatív teljesítménye.
A teljesítmény mérése
Az algoritmusok teljesítményének mérésére több módszer is létezik. A legnyilvánva
lóbb természetesen az eltelt futásidő feljegyzése. Sajnos a futásidők mérésekor gyak
ran előfordulhatnak megjósolhatatlan zavaró hatások az operációs rendszer műkö
déséből adódóan, például a virtuális memória lapozásával, a feladatváltással, a háló
zati megszakításokkal stb. összefüggésben. Ennél pontosabban megjósolható mérő
számra van szükségünk.
A teljesítménnyel kapcsolatos eddigi tárgyalásainkat az elvégzett összehasonlítá
sok számára összpontosítottuk A letámadásos algoritmustól való eltérés alapja tu
lajdonképpen nemcsak a sztring-összehasonlítások számának, hanem a karakter
összehasonlítások számának is a csökkentése - ezzel csökken az elvégzendő munka
mennyisége, a munkamennyiség mérséklődésével pedig elméletileg csökken az álta
lános futásidő. Vagyis, ha meg tudnánk számolni az elvégzett összehasonlítások
számát, akkor mérni tudnánk az algoritmusok teljesítményét.
A kódot áttekintve látható, hogy m.indkét megvalósításban m.inden egyes összeha
sonlításnál két karakterfellapozás történik: egy azért, hogy kinyerjük a karaktert a szö
vegből, és egy másik, hogy kinyerjük a karaktert a m.intából. Ebből közvetlen össze
függést vonhatunk le a karakterfellapozások és az összehasonlítások között: ha sikerül
megszámolni a fellapozások számát, azzal mérni tudjuk a relatív teljesítményt.
Gyakorlófeladat: osztály létrehozása a karakterfellapozások megszámlálásához
Már említettük, hogy a szöveg és a m.inta esetén a String típus helyett a char
sequence interfészt használjuk. Egy másik ok az interfész használatára az, hogy ez
zel triviálissá válik egy olyan csomagoló modul elkészítése (lásd Decorator [Gamma,
1995]), amely képes elfogni és megszámolni m.inden charAt() metódushívást.
A következő osztály pontosan azt végzi el, amire szükségünk van, vagyis meg
számolja a karakter-fellapozásokat:
package com. wröX:"aigorfthms. ssearcli;
public class callcountingcharsequence implements charsequence { private final CharSequence _charsequence;
r:_ i v�!!!_i nt ca ll cou!!.!_;______ _ ---""' --� �-----..1
463
Sztringkeresés
}
public callcountingcharsequence(charsequence c�arsequence) { assert charSequence l= null : "A 'charsequence' nem
_charsequence = charSequence; }
public int getcallcount() { return _count;
}
public char charAtCint index) { ++_count;
lehet null";
return _charsequence.charAt(index); }
public int length() { return _charsequence.length();
}
public charsequence subsequence(int start, int end) { return _charsequence.subsequence(start, end);
}
A megvalósitás müködése
A Charsequence interfész megvalósítása mellett a ca ll Counti ngcharsequence osz
tály magában foglalja és ténylegesen delegálja az összes metódushívást egy másik,
mögöttes charsequence interfészhez. Vegyük észre, hogy minden charAt() metó
dushívás egy számláló értékét növeli. Ez a számláló azután a ge tea ll co u nt() metó
duson keresztül érhető el. ily módon nagyon könnyű meghatározni, hány karakter
összehasonlítás történt.
Gyakorlófeladat: fájlban kereső osztály létrehozása
Most, hogy már meg tudjuk számolni a karakterfellapozásokat, szükségünk van egy
eljárásra, amelynek a segítségével fájlokban kereshetünk:
import c011.wro·x.aigorithms. i teration .rterator; ·
import java.io.FileinputStream; import java.io.IOException; i�rt java.nio.ByteBuffer; i.port java.nio.charBuffer; iMPQrt java.nio.channels.Fileehannel;
._..___:i:.:= .. :""'_!"t ·ava_. ni o.· c:;har_!';et. Charset; ...
464
A teljesítmény összehasonlítása
-- -Pűbíic-fi r.�a:fclass ·c-o;pa.,;-at:rvest'rf09searcíleF { private static final int NUMBER_OF_ARGS = 2; private static final string OIARSETJW4E = "8859_1";
private final String _filenaMe; private final String _pattern;
public Ca.parativestringsearcher(string filena.e, String pattern) {
assert filename != null : "A 'fi l enéllile' (fájl név) nea� lehet NULL";
assert pattern != null : "A 'pattern' (IRinta) nem lehet NULL";
}
_filena.e = filena��e; _pattern = pattern;
public void run() throws IOException {
}
Filechannel fc = new Fileinputstrea.(_filena.e).getchannel(); try {
Bytesuffer bbuf = · fc.��ap(Filechannel.MapMode.READ_ONLY, O, (int) fc.size());
charsuffer file s Charset.forNaae(CHARSET_NAME).newDecoder().decode(bbuf);
Systetii.OUt.println("Searching "' + _filenillle + "' (" + file.length() + ") for'"+ _pattern·+ '" . . . ") ;
search(new BruteForcestringsearcher(_pattern), file); search(new BoyerMoorestringSearcher(_pattern), file);
} finally { fc.close();
}
private void search(Stringsearcher searcher, Charsequence file) { callcountingcharsequence text =
new callcountingCharsequence(file); Iterator i =new StringMatchiterator(searcher, text);
int occurrence = O;
long startTi.e - systeM.currentTiMeMillis();
for (i.first(); !i.isoone(); i.next()) { ++Occurrence;
}
--�1�_1'!9� .. e l aJ)sedTi me = s�g_ea�. _�l!!:�!IJ:Ti meMi ll is() - start_!:L�!!i!_i. ___ _
465
Sztringkeresés
}
system.out.println(searcner.getclass().getName() +
"· előfordulások: "
+ occurrence + összehasonlítások: "
+ text. getcall count O + idő: " + elapsedTime);
public static void main(String[] args) throws IOException { assert args != null : "az argumentumlista nem lehet NULL";
}
if (args.length < NUMBER_OF_ARGS) { system.err.println(
}
"usage: comparativestringsearcher <file> <pattern>"); system.exit(-1);
comparativestringsearcher searcher = new comparativestringsearcher(args[O], args[l]);
searcher. run O ;
} _____ _____ , _______________ , ________ __________________ _
A megvalósitás müködése
A legtöbb modern operációs rendszer lehetővé teszi memóriába leképzett fájlok
megnyitását - vagyis nem adatfolyamként olvassuk őket, hanem a memóriában ta
lálható folytonos bájttömbként hivatkozhatunk rájuk. A Java programokban többek
között a j ava. ni o. charsuffer osztály segítségével tudjuk kezelni a memóriába le
képzett fájlokat. De mi köze ennek mostani témánkhoz? A charsuffer osztályban
az az igazán nagyszerű, hogy megvalósítja a charsequence típust, azaz így a kereső
algoritmusoknak megadhatunk bemenetként fájlt, és pontosan ez volt a célunk az
osztály létrehozásával.
A run() metódus megnyitja a konstruktorban megadott fájlt, és létrehoz egy
charsuffer változót, amely lehetővé teszi, hogy olvasni tudjunk a fájlból memória
leképzett olvasás-írás segítségéveL Ezt azután egy sztringkeresővel együtt kétszer át
adjuk a search() metódusnak: a korábban létrehozott két sztringkereső megvalósí
tás mindegyikéhez egyszer.
A search() metódus először egy ca ll counti ngcharsequence metódusba burkolja
a fájlt- a charsuffer egy charsequence -,hogy megszámolja a karakterfellapozásokat,
majd a St ri ngMatchiterator segítségével megkeresi a minta összes előfordulását.
Végezetül a mai n() metódust hívjuk meg, amely lefuttatja a programot. Ez egy
szerűen azt biztosítja, hogy megfelelő szám ú argumentumot adjunk meg (egyet a
fájlnak, amelyben keresni fogunk, és egy másikat a mintának) a search() meghívása
előtt, amely elvégzi az összes feladatot.
466
A teljesítmény összehasonlítása
Bizonyára mindenkinek feltűnik ez a furcsa szám: "8859_1"
. Ez egy karakterkészlet neve, amelyre a charsuffer használatához van szükség; egyébként nem tudnánk, hogyan kell dekódolni a fájl szövegét. A "8859_1
" karakterkészlet az ISO Latin-1-nek felel meg, amelyet a nyugat -európai nyelvek, például az angol es etén használunk. (A karakterkészletekről és a karakterkódolásról bővebben a www. uni code. or g
webhelyen olvashatunk.) Most már csak ezekre a nagy kérdésekre várunk választ: milyen hatékony a két
algoritmus? Melyik a gyorsabb? És ami még fontosabb: mennyivel gyorsabb?
Az összehasonlítás eredménye
A két algoritmus teljesítményét jól mutatja, ha nagy szövegfájlokban kerestetünk velük különböző mintákat. Ehhez Lev Tolsztoj Háború és bék�ét használtuk fel - ennek mérete valamivel több, mint 3 MB -, amely a Gutenberg-projekt webhelyről www.gutenberg.org érhető el formázatlan szöveg formátumban. A kapott eredményeket a 16.1. táblázat muta�a.
Elöfordulások Letámadásos Boyer-Moore-
Százalékos Minta
száma algoritmus algoritmus*
eltérés teljesitménye teljesitménye
a 198 999 3 284 649 3 284 650 100,00%
the 43 386 3 572 450 1 423 807 39,86%
zebra o 3 287 664 778 590 23,68%
military 108 3 349 814 503 199 15,02%
independence 8 3 500 655 342 920 9,80%
* A Boyer-Maore-algoritmus teljesítményértékei egy pluszfellapozás t is magukban foglalnak minden karakter esetén, az utolsó elófordulások táblázatának elkészítéséhez.
16. 1. táblázat. Mintakeresés a Háború és békében
Látható, hogy a Boyer-Maore-algoritmus következetesen jobban teljesít, mint a naiv algoritmus: csaknem mindig több mint kétszer, a legtöbb esetben pedig több mint négyszer gyorsabb! Tulajdonképpen, ha jobban megnézzük, észrevehetjük, hogy minél hosszabb a minta, annál nagyobb a teljesítményjavulás mértéke. Ennek az az oka, hogy a Boyer-Maore-algoritmus gyakran képes átugrani nagy szövegrészeket; minél hosszabb a minta, annál több karaktert ugrik át, és ebből következően annál jobb lesz a teljesítménye.
467
Sztringkeresés
Összefoglalás
Ebben a fejezetben néhány gyakori és jól ismert sztringkeresési algoritmust mutattunk be - a letámadásos és a Boyer-Moore-algoritmust, -, valamint a közös sztringkeresó felület felett létrehozott iterátort, amely elhárí�a annak a kódolási akadályát, hogy több egymást követő keresést tudjuk végrehajtani. A fejezet legfontosabb megállapításait az alábbiakban foglaljuk össze.
468
• A letámadásos algoritmus úgy keres, hogy balról jobbra haladva egyszerre egy pozíciót lép, amíg illeszkedést nem talál. Tételezzük fel, hogy legroszszabb esetben a minta minden karakterét össze kell hasonítania a szöveg csaknem minden karakterével, ekkor a legrosszabb esetbeli futásidő O(NM),
ami kifejezetten rossz! A letámadásos megközelítésnél az az ideális eset, amikor az első karakter-összehasonlítás és jobbra haladva minden további sikertelen, egészen a szöveg végéig, ahol sikeres az illesztés. Ennek a legjobb esetnek a futásideje tehát o (N + M).
• A Boyer-Maore-algoritmus a mintában jobbról balra haladva végzi a karakter-összehasonlításokat, és mindig több karakterpozíciót ugrik. A legroszszabb esetbeli futásideje olyan rossz vagy egy kicsit rosszabb (a kezdeti mintafeldolgozás segédszámítási költsége miatt), mint a letámadásos algoritmusé. A gyakorlatban azonban észrevehetően jobban teljesít, mint a letámadásos algoritmus, és a legjobb esetbeli futásideje akár O(N/M) is lehet, ha folyamatosan át tudja ugorni a teljes mintát, egészen a szöveg végéig.
• Megvalósíthatunk egy iterátort, amellyel elkerülhető az ismételt keresések végrehajtásakor felmerülő fáradságos állapotkezelés. Mivel az iterátor csak a St ri ngsearcher interfésztól függ, bármely sztringkeresóvel használható. Ha például különbözó típusú szövegekben kell keresnünk, amelyek bonyolult és sokféle sztringkeresési algoritmus alkalmazását teszik szükségessé, ez az iterátonal - feltéve, hogy az új algoritmus illeszkedik a St ri ngsearcher
interfészhez - az alkalmazás kódjának módosítása nélkül megoldható.
• A két algoritmus működését úgy hasonlítottuk össze, hogy különböző angol szavakat kerestünk egy viszonylag nagy méretű (-3 MB) szövegfájlban. Nyilvánvaló, hogy a konkrét eredmények a keresett szöveg típusától, a minta felépítésétől, hosszától stb. függően eltérőek lesznek. Összességében reméljük, mindenki látja, hogy egy kis gondolkodással és munkával csaknem egy nagyságrendnyi javulás érhető el a letámadásos és a Boyer-Moore keresési algoritmusok teljesítménye között.
Összefoglalás
Sok más jól ismert sztringkeresési algoritmus is létezik, amelyekről itt nem beszél
tünk - ezek közül Rabin-Karp [Cormen, 2001] és Knuth-Morris-Pratt [Cormen,
2001] algaritmusai jutnak először eszünkbe. Közülük egyik sem teljesít olyan jól a
legtöbb alkalmazásban, mint a Boyer-Moore-algoritmus, sőt gyakran még a letáma
dásos megközelítésnél sem jobbak a formázadanszöveg-keresésben. A Rabin-Karp
algoritmus, amely egy okos hasító mechanizmust alkalmaz, több minta egyidejű ke
resésekor előnyös. Akármi legyen is az alkalmazás, az a fontos, hogy elemezzük a
keresett szöveg típusát, és meghatározzuk a jellemzőit, így elkerülhető a nyilvánvaló
an szükségtelen összehasonlítások többsége.
469
TIZENHETEDIK FEJEZET
Sztringillesztés
A 16. fejezetben láthattuk, hogyan találhatunk meg hatékonyan egy sztringen belül
egy másikat. Ebben a fejezetben teljes sztringek összehasonításával fogunk foglal
kozni, különös tekintettel a nem azonos, de hasonló sztringek illesztésére. Ennek
különösen akkor van nagy haszna, ha adatbázisokban szeretnénk megtalálni a két
szerezett bejegyzéseket, valamint helyesírás-ellenőrzéskor és DNS-illesztéskor.
A fejezetben a következő témaköröket tárgyaljuk
• a Soundex kódolás,
• a Levenshtein szótávolság.
A Soundex algoritmus
A Soundex a fonetikus kódolási algoritmusok csoportjába tartozik. A fonetikus kó
dolás a hasonló alakú szavakat ugyanarra az értékre kódolja (a hasítófüggvényekhez
hasonlóan).
A Soundex algoritmust kifejlesztője, R. C. Russel eredetileg az 1980-as nép
számlálási adatok feldolgozására találta ki. A Russel Soundex algoritmus néven is
ismert kódolási eljárást számos területen használják eredeti formájában és kisebb
nagyobb módosításokkal is, többek között az emberierőforrás-kezelés, a családfaku
tatás és természetesen a népszámlálás területén is. Célja, hogy kiszűrje a különbö
zően írt vezetéknévből adódó adatkettőzéseket.
A Ne1v York State Identification and Intelligence (NYSII) projekt kutatójaként Robert
L. Taft 1970-ben "Névkeresési módszerek" címen publikált tanulmányában össze
vetette két fonetikus kódolási módszer hatékonyságát. A kutatás a Soundex algorit
must a NYSII által kidolgozott, valós adatok kiterjedt statisztikai elemzésén alapuló
algoritmussal vetette össze. Az eredmények szerint a Soundex pontossága 95,99%,
szelektivitása pedig 0,213% keresésenként, rníg a másik (a könyvben nem taglalt) al
goritmus pontossága 98,72%, szelektivitás a pedig O, 164%.
Léteznek egyéb fonetikus kódolási módszerek is, ilyen többek között a Meta
phone, a Double-Metaphone és az eredeti Soundex számtalan variánsa.
Sztringillesztés
A Soundex algoritmus meglehetősen egyszerű és könnyen érthető. A bemeneti
sztring feldolgozásánál néhány szabályt kell követni. A bemeneti sztring általában ve
zetéknév vagy ahhoz hasonló. Feldolgozása balról jobbra, a karaktereket négykarakte
res kóddá alakítva történik. A kódolás eredményeként BSSS alakú sztringet kapunk,
ahol a B betűt, az S pedig O és 6 közötti tízes számrendszerbeli számjegyet jelent.
A bemeneti karaktereket a következő szabályok alapján alakítjuk át (próbáljunk
rájönni, mi a hasonlóság az azonos csoportba sorolt betűk között!):
l. Nagy- és kisbetűk között nem teszünk különbséget.
2. Az első betűt tartsuk meg.
3. A többi betű közül húzzuk ki az A, E, I, O, U, H, W, és Y betűket.
4. A maradék betűket a következő szabályok alapján kódoljuk le:
• B, F, P, és V helyett írjunk 1-et,
• C, G, J, K, Q, S, X, és Z helyett írjunk 2-t,
• D és T helyett írjunk 3-at,
• L helyett írjunk 4-et,
• M és N helyett írjunk 5-öt,
• R helyett írjunk 6-ot.
5. Azonos kódú, egymást követő betűk közül csak az elsőt kódoljuk le.
6. Ha szükséges, a maradék helyekre írjunk nullát.
Az első betű megtartása után kiejtjük az összes magánhangzót. Az angol nyelvben
legtöbbször a magánhangzók eltávolítása után is könnyen kiolvasható az eredeti szó.
Figyeljük meg, hogy a magánhangzókkal együtt a H, a W és az Y betűket is kihúztuk.
Ez azért történt így, mert ezek kiejtése gyakran azonos valamely magánhangzóéval.
A B, F, P és V hangoknak nemcsak a kiejtése hasonló, hanem a hang formálása
közben az ajak állása is. Próbáljuk csak meg kimondani a P hangot közvetlenül a B
után! Ugyanez igaz a T és D, valamint az M és N párokra is.
Ezenkívül az egymást követő azonos kódú betűket is figyelmen kívül ha&rtuk
kódoláskor. Ennek az ad értelmet, hogy az angol nyelvben a kettős hangzók kiejtése
gyakran megegyezik egyszeres párjukkal.
Hogy közelebbről is láthassuk, hogyan működik a Soundex algoritmus a gyakor
latban, vegyünk két vezetéknevet, legyen ez a Smith és a Smythe, és kódoljuk le
Soundex algoritmussal.
472
A Soundex algoritmus
Kezdetnek inicializáljuk az eredménypuffert négy karakter számára, hiszen az
algoritmus legfeljebb ilyen hosszú kódot eredményezhet. Ez látható a 17.1. ábrán.
Ezután elkezdjük karakterenként feldolgozni a bemeneti sztringet, balról jobbra.
Bemenet l S l m l i l t l h
Eredmény l l l l l 17. 1. ábra. Az eredméf!Ypuffert négy karakter számára inicializá!Juk
A második szabályból tudjuk, hogy a bemeneti sztring első karakterét megtar�uk, ez
lesz tehát az eredménypuffer első karaktere, ahogy a 17.2. ábrán is látható.
Bemenet l S l m l i l t l h
j Eredmény l S l l l l 17. 2. ábra. Az eredméf!Y első karaktere minden esetben a bemene ti s'{fring első karaktere lesz
A bemeneti sztring második karaktere az m. A negyedik szabály szerint ezt az ötös
számmal kell lekódolni. A 17.3. ábrán láthatjuk, hogy az eredménypuffer második
karakterpozíciójába 5-öt írunk.
Bemenet l S l m l i l t l h
i_+ Eredmény l S l 5 l 17.3. ábra. Az m betű kódolása 5
A bemeneti sztring harmadik karakterpozíciójában i betű áll. A harmadik szabály ér
telmében ezt figyelmen kívül kell hagyni (a többi magánhangzóval egyetemben), így
nem befolyásolja (lásd 17.4. ábra) az eredményt.
Bemenet l S l m l X l t l h
i_'f" Eredmény � 17.4. ábra. A magánhangzókat figyelmen kíviil hagyjuk
473
Sztringillesztés
Az i betű után a t következik, amelynek a kódolása az algoritmus szerint 3. Jelen
esetben az eredmény harmadik karakterpozíciójába így hármas kerül, ahogy a 17.5.
ábrán is láthatjuk.
Bemenet l S l m l X l h
t t If/ Eredmény l S l 5 l 3 l 17.5. ábra. A t betű kódolása 3
Az utolsó betű, a h, speciális karakter, amely az algoritmus tekintetében a magán
hangzókkal egyenértékű, ezért figyelmen kívül hagyjuk (lásd 17.6. ábra).
Bemenet l S l m l X l t lx l t t If../
Eredmény l S l 5 l 3 l 17.6. ábra. A H, a W és az Y kódolása megegJezjk a magánhangzókéva4
ezért ezeket filJeimen kívül hagJjuk
Kifogytunk a bemeneti karakterekből, de nem töltöttük fel az eredménypuffert, így a
hatodik szabály értelmében az üres helyekre nullát írunk. A 17.7. ábrán látha�uk,
hogy a Smith karaktersztring Soundex értéke S530.
Bemenet l S l m l X l t lx l t t If/
Eredmény l S l 5 l 3 l O l 17.7. ábra. Az üres hefyekre nu/lát írunk, így az eredméf!Y négykarakteres lesz
Most nézzük meg a Smythe név kódolását. Az előzőhöz hasonlóan kezdünk, azaz
négy karakter méretű eredménypuffer inicializálásával.
Bemenet l S l m l y
Eredmény !,_ -..I.---L--'----1
17.8. ábra. Ismét inicializál/uk a négykarakteres eredméf!Ypuffert
Ezúttal nem megyünk végig együtt a lépéseken, hiszen önállóan is könnyedén meg
tudjuk csinálni. Az eredmény a 17.9. ábrán látható.
474
A Soundex algoritmus
Bemenet l S l m l � l GE] t t ;t/
Eredmény l S l 5 l 3 l O l 17. 9. ábra. A "Smythe" vezetéknév végső kódolása
A 17.9. ábrából kitűnik, hogy a smythe vezetéknév kódolása 5530, csakúgy, rnint a
smith vezetéknévé. Ha vezetéknevekből álló adatbázishoz készítünk Soundex-alapú
indexet, a smith kulcsszó keresésékor megtalálnánk a smyhte nevűeket is, és fordít
va. Tehát jól működík a helyesírási hibák kiszűrésére és hasonló nevűek keresésére
tervezett algoritmus.
Bár ebben a példában nem olyan nagy gond, az algoritmus bonyolultsága O(N), hiszen a sztringben egyesével dolgozzuk fel a karaktereket.
Most, hogy tudjuk, rnilyen a Soundex algoritmus, néhány gyakorlattal megnéz
zük, hogyan működik a tényleges megvalósítás.
Gyakorlófeladat: Soundex-kódoló tesztelése
Hozzuk létre a következőképpen a tesztosztályt (viszonylag sok szabály van, a helyes
megvalósításhoz lehetőleg rninél többet le kell programozni):
package com.wrox.algorithms.wmatch;
import junit.framework.Testcase;
public class SoundexPhoneticEncoderTest extends Testcase { private soundexPhoneticEncoder _encoder;
protected void setUp() throws Exception { super. setup();
_encoder = SoundexPhoneticEncoder.INSTANCE;
}
public void testFirstLetterisAlwaysused() { for (char c = 'A'; c <= 'z'; ++c) {
}
String result = _encoder.encode(c + "-");
assertNotNull(result); assertEquals(4, result.length());
assertEquals(c, result.charAt(O));
475
Sztringillesztés
public voia testvowelsAreignored() {
}
assertAllEquals('O', new char[] {'A', 'E', 'I',· 'O', 'U', 'H', •w•, 'Y'});
public void testLettersRepresentedByone() { . assertAllEquals('l', new char[] {'B', 'F'', 'P', 'V'});
}
public void testLettersRepresentedByTWO() {
}
assertA11Equals('2', new char[] {'C', 'G', 'J', 'K', 'Q', 'S', 'X', 'Z'});
public void testLettersRepresentedByThree() { assertA11Equals('3', new char[_] {'D', 'T'});
l
public void testLettersRepresentedByFour() { assertAllEquals('4', new char[] {'L'});
}
public void testLettersRepresentedByFive() { assertAllEquals('S', new char[] {'M', 'N'});
}
public void testLettersRepresentedBySix() { assertA11Equals('6', new char[] {'R'});·
}
public void testouplicatecodesAreoropped() { assertEquals("BlOO", _encoder.encode("BFPV")); assertEquals("C200", _encoder.encode("CGJKQSXZ")); assertEquals("D300", _encoder.encode("DDT")); assertEquals("L400", _encoder.encode("LLL")); assertEquals("MSOO", _encoder.encode("MNMN"));
.assertEquals("R600", .;...encoder.encode("RRR")); }
pub l i c. voi d testsomeReal St ri n gs() { assertEquals("S530", _encoder.encode("smith")); assertEquals("S530", _encoder.encode("smythe")); assertEquals("M235", _encoder.encode("Mcoonald")); assertEquals("M235", _encoder.encode("Macoonald")); assertEquals("H620", _encoder.encode("Harris")); assertEquals("f-!620", _encoder.encode("Harrys"));
}
private void assertAllEquals(char expectedValue, char[] chars) { for (int =O; i < chars.length; ++i) {
char c chars[i]; .._ ___ -"s�t:.!.r.!.i�n,g result = encoder.e code("-" + c
476
A Soundex algoritmus
r - ··- - - -· � asseriNOt.Wil(res-üitYi ········- ··
-- - ·--- · · ----· ·-- ---·-
r assertEquals(4, result.length()); i
i } � }
assertEquals("-"· + expectedValue + "00", result);
i ..... �--1���- --��,�--= - - ··- · -··· � .............. .
A megvalósitás müködése A soundexPhoneti cEncoderTest osztályban megtalálható a soundexPhoneti cEncoder
egy példánya, amelyet a setup() metódus során inicializálunk a tesztesetek számára:
package com.wrox.algorithms.wmatch;
import junit.framework.Testcase;
public class soundexPhoneticEncoderTest extends Testcase { private soundexPhoneticEncoder _encoder;
}
protected void setup() throws Exception { super. setUp();
�encoder = soundexPhoneticEncoder.INSTANCE;
}
A második szabály értelmében az első betűt mindenképp meg kell tartanunk, így en
nek a tesztelésével kezdünk. A testFi rstLetterisAlwaysused() metódus végig
megy az ábécé betűill A-tól z-ig, és egyenként lekódolja őket a bemeneti sztring első
karaktereként. A kódolás után meggyőződünk róla, hogy az eredmértysztring nem
null, hossza pedig négy, hiszen minden Soundex érték hosszának négynek kell len
nie. Ezután meggyőződünk róla, hogy az eredmény első karaktere megegyezik a be
meneti sztring első karakterével:
public void testFirstLetterisAlwaysused() { for (char c = 'A'; c <= 'z'; ++c) {
} }
String result = _encoder.encode(c + "-");
assertNotNull(result); assertEquals(4, result.length());
assertEquals(c, result.charAt(O));
477
Sztringillesztés
A többi szabály tesztelése nagyjából megegyezik ezzel, a munka nagy részét segédmetódus segítségével végzik el. Az assertAllEquals() metódus paramétere a várt érték és a használandó karaktertömb. Minden karaktert kétbetűs bemeneti sztring második karaktereként kezelünk, azaz kódoljuk A visszatérési értéket megint ellenőrizzük, nem null-e, és megvizsgáljuk a hosszát is. A kódolt értéket ezután összeve�ük az elvárt eredménnyel. Az első karakternek minden esetben változatlanul kell maradnia, és mivel kétkarakteres sztringet kódoltunk, az utolsó két karakterpozícióba nullát kellett írni. Így csak a második karaktert kell leellenóriznünk, jelen esetben pedig a várt érték O, ami azt jelenti, hogy bevitt karaktert figyelmen kívül hagytuk
private void assertAllEquals(char expectedvalue, char[] chars) {
}
for (int = 0; i < chars.length; ++i) {
}
char c chars[i]; String result = _encoder.encode("-" + c);
assertNotNull(result); assertEquals(4, result.length());
assertEquals("-" + expectedvalue + "00", result);
A harmadik szabály értelmében az összes magánhangzót, valamint néhány mássalhangzót figyelmen kívül kell hagynunk. A testvowel sAreignored() metódus ezt ellenőrzi le. Létrehoz egy tetszőleges első karakterból (amelyet mindig megtartunk) és egy magánhangzóból álló sztringet. Kódolás után azt várjuk, hogy az eredmény utolsó három karaktere "OOO" lesz, mivel a inagánhangzót figyelmen kívül hagytuk, a további három karakter helyét pedig feltöltöttük nullával.
public void testVowelsAreignored() {
}
assertAllEquals('O', new char[] {'A', 'E', 'I', 'o', 'u', 'H', 'W'' 'y'});
A negyedik szabály mind a hat esetét is leteszteltük. Minden egyes esetben megillvtuk az assertAllEquals() metódust, amely paraméterként megkapta a várt értéket és a bevitt karaktereket.
478
public void testLettersRepresentedByOne() { assertAllEquals('l', new char[] {'B', 'F', 'P', 'v'});
}
public void testLettersRepresentedByTwo() {
}
assertAllEquals('2', new char[] {'C', 'G', 'J', 'K', 'Q', '5', 'X', 'Z'});
A Soundex algoritmus
public void testlettersRepresentedByThree() { assertAllEquals('3', new char[] {'D', 'T'});
}
public void testLettersRepresentedByFour() { assertAllEquals('4', new char[] {'L'});
}
public void testLettersRepresentedByFive() { assertAllEquals('S', new char[] {'M', 'N'});
}
public void testLettersRepresentedBySix() {
assertAllEquals('6', new char[] {'R'});
}
A2 ötödik szabály értelmében az azonos kódú, egymást követő betűk közül csak az elsőt kell megtartanunk Az ezt tesztelő testDupl i catecodesAreDropped() metódus azonban már nem olyan egyértelmű, mint a korábbiak.
Lényegében minden betűcsoportból létrehozunk egy sztringet. Természetesen tudjuk, hogy az első betűt változtatás nélkül kódoljuk. Azt is tudjuk, hogy a második betűt lekódoljuk, hiszen a tesztben egyetlen betű sem magánhangzó. Ám mivel a második betűt követő összes többi betű kódolása megegyezik az előzővel, arra számítunk, hogy ezeket nem kódolja le az algoritmus, és ezért a kódolt sztring utolsó két betűje nulla lesz.
public void testDuplicatecodesAreoropped() { assertEquals("BlOO", _encoder.encode("BFPV"));
assertEquals("c200", _encoder.encode("CGJKQSXZ"));
assertEquals("D300", _encoder.encode("DDT")); assertEquals("L400", _encoder.encode("LLL"));
assertEquals("MSOO", _encoder.encode("MNMN")); assertEquals("R600", _encoder.encode("RRR"));
}
Végül a testsomeRealstrings() metódus három azonos kódolású sztringpár értékét veti össze:
public void testsomeRealstrings() { assertEquals("S530", _encoder.encode("smith")); assertEquals("S530", _encoder.encode("smythe")); assertEquals("M235", _encoder.encode("McDonald"));
assertEquals("M235", _encoder.encode("Macoonald"));
assertEquals("H620", _encoder.encode("Harris")); assertEquals("H620", _encoder.encode("Harrys"));
}
Most, hogy láthattuk, a tesztcsomag garantálja a helyes megvalósítást, a következő gyakorlófeladatban megírjuk a tényleges Soundex kódolót.
479
Sztringillesztés
Gyakorlófeladat: a Soundex-kódoló megvalósitása-
Kezdjük az összes fonetikus kódolóban közös interfészdefiníció létrehozásával:
public interface PhoneticEncoéer { public String encode(charsequence string);
___ } _ _________ ___________ _
Majd a következőképpen hozzuk létre a Soundex kódoló osztályt: -
public final class soundexPhoneticEncoéer iMPl&Ments PheneticEnceder {
public static final soundexPhoneticEncoder INSTANCE =
new seundexPhoneticEncoeer();
private static final char[] �P = "Ol238120022455012&23010202".tocharArray();
private soundexPhoneticEncoder() { }
public string encode(charsequence string) { assert string l= null : "A 'string' na lehet NULL"; assert string.length() >O : "A 'string' ne11 lehet üresN;
char[] reswlt • {'0', 't', 'O', '8'};
result[O] = character.teUppercase(string.charAt(8));
int stringindex = l; int resultindex = l;
while (strintlndex < string.length() 6I resultindex <
result.length) {
}
char c • -.,(string.charAt(stringindex));
if (c != 'l' 6I c l= result[resulttndex - l]) { result[reswltindex] c c; ++resultintlex;
}
++stringindex;
return Stri•g.valueof(result); ---�L ______ ____ .. _______ _
480
A Soundex algoritmus
pl"ivate static char map(ctiar c)-
{ int index = Character.tOUpperCase(c)- 'A'; return isvalid(index)? CHARACTER_MAP[index] : 'O';
}
private static boolean isvalid(int index) { return index >= O && index < CHARACTER_MAP.length;
} }
A megvalósitás működése
A Phonetic En code r interfész definiálásával alkalmazásunkban más kódolási változa
tot is kifejleszthetünk, anélkül hogy közvetlenül az itt megvalósított módszertől
függnénk
package com.wrox.algorithms.wmatch;
public interface PhoneticEncoder { public String encode(Charsequence string);
}
A SoundexPhoneti c En code r osztály ezután mégvalósítja a Phoneti cEncoder interfészt,
így ha úgy kívánjuk, egyéb kódolási módszerrel is kompatibilis lesz a megoldásunk.
Figyeljük meg, hogy a konstruktor private, amely megakadályozza a példányo
sítást. Ne felejtsük el, hogy az osztálynak mindig csak egy példányára van szüksé
günk, ezért az osztályhoz minden hozzáférést a publikusan rendelkezésre álló kons
tans példányon (INSTANCE) keresztül kell elvégeznünk.
V essünk egy pillantást a CHARACTER_MAP karaktertömbre. Ez a karaktertömb
végzi el a leképezést a karakterek és a kódolt számjegyek között, ezért létfontosságú
az algoritmus működése szempontjábóL A leképezés az angol ábécé betűire műkö
dik az A betűtől a z betűig. Persze így a megvalósítás csak az angol nyelvvel működik
megfelelően, de mivel az algoritmus eleve csak az angol nevekre alkalmazható, ez
nem is olyan nagy gond.
package com.wrox.algorithms.wmatch;
public final class soundexPhoneticEncoder implements PhoneticEncoder {
}
public static final soundexPhoneticEncoder INSTANCE new SoundexPhoneticEncoder();
private static final char[] CHARACTER_MAP = "01230120022455012623010202". tocharArray O;
private SoundexPhoneticEncoder() {
}
481
Sztringillesztés
Mielőtt mélyebbre ásnánk magunkat az algoritmus működésében, megnézzük a map()
és az i sva l i d() segédmetódusokat. A két metódus együtt a bemeneti sztring egy karakteréből a Soundex szabályai alapján kódolt értéket állít elő. A karaktert előbb indexszé alakí�uk, amellyel megkereshe�ük a megfelelő értéket a CHARACTER_MAP tömbben. Ha az index megtalálható a tömbben, a karaktert lekódolja. Ellenkező esetben O értéket ad, amivel jelzi, hogy a karaktert figyelmen kívül kell hagyni - ahogy a magánhangzókat:
private static char map(char c) {
}
int index = character.touppercase(c)- 'A'; return isvalid(index)? CHARACTER_MAP[index] 'O';
private static boolean isvalid(int index) { return index >= O && index < CHARACTER_MAP.length;
}
Végül maga a Soundex-kódolási algoritmus: az encode() metódus. A metódus egy négykarakteres tömb inicializálásával kezdődik, melybe mind a négy helyre nullákat írunk. Így egyszerűbben oldha�uk meg a maradék helyek feltöltését nullákkal - hiszen tudjuk, hogy az eredmény mindenképp négy karakter hosszúságú lesz, hát miért ne töltenénk fel azonnal? A következő lépésben a bemeneti sztring első karakterét bemásoljuk az eredmény első karakterévé (és átalakítjuk nagybetússé, ha esetleg nem az volt, így biztosítva az első szabályt). A metódus eztán a bemeneti sztring minden karakterén végigmegy. Az összes karaktert átadjuk a map() metódusnak, a visszatérési értéket pedig eltároljuk az eredménypufferben, feltéve, hogy nem O és nem is egyezik meg az előző értékkel, mert ebben az esetben figyelmen kívül hagyjuk. Mindez addig folytatódik, míg az eredménypuffer meg nem telik (négy karaktert eltároltunk), vagy el nem fogynak a bemeneti karakterek. Az eredménypuffert ezután sztringgé alakítjuk, majd visszaadjuk a meghívó metódusnak:
482
public String encode(Charsequence string) { assert string != null : " A 'string' nem lehet null"; assert string.length() >O: "A 'string' nem lehet üres";
char[] result = {'O', 'O', 'O', 'O'};
result[O] = character.touppercase(string.charAt(O));
int stringindex int resultindex
l• '
l;
while estringindex < string.length() && resultindex < result.length) {
char c = map(string.charAt(stringindex));
A Levenshtein-szótávolság
}
}
if (c != 'O' && c != result[resultrndex - l]) { result[resultrndex] = c; ++resultrndex;
}
++stringrndex;
return String.valueof(result);
A Levenshtein-szótávolság
Bár a Soundexhez hasonló fonetikus kódolás kiválóarr alkalmazható az elgépelt angol
szavak fuzzy-illesztésénél valamint a kisebb helyesírási hibáknál, nem túl hatékony a na
gyobb helyesírási hibák felismerésekor. A "mistakes" és a "msitakes" szavak Soundex
értéke például megegyezik, a "shop" és a "sjop" értéke azonban már nem, pedig gyako
ri hiba a "h" helyett "j" -t gépelni, hiszen a billentyűzeterr egymás mellett vannak.
A Levenshtein-szótávolság néven ismert algoritmus a szavak hasonlóságát vizs
gálja: kiszámolja, legkevesebb hány beszúrás, törlés és helyettesítés szükséges ahhoz,
hogy az egyik sztringből a másikat kapjuk. Beállíthatjuk, hogy két szó között mekko
ra az a távolság (mondjuk 4), amely alatt még eléggé hasonlónak tekintjük a két
sztringet. Az itt bemutatott algoritmus tehát sok egyéb eljárás alapjául szolgál: töb
bek között a szövegszerkesztők helyesírás-ellenőrzőjének, a DNS-illesztésnek és a
plágiumfelismerésnek is ez az alapja.
Az algoritmus igen hatékony, letámadásos megközelítést használ, amely minden
lehetséges módon megpróbálja átalakítani a forrássztringet a célsztringgé, hogy így
megtalálja a legrövidebb megoldást.
Három különböző művelet hajtható végre. Minden művelethez kiJliséget rende
lünk, a legkisebb távolság pedig a legalacsonyabb költséggel járó átalakítások sora.
A Levenshtein-távolság kiszámításához készítsünk a forrás és a célszó betűinek
megfelelő sor- és oszlopszámú táblázatot. A 17.10. ábrán láthatjuk a "msteak" szó
"mistake" szóra történő átalakításához használt táblázatot.
Figyeljük meg, hogy egy további sorral és oszloppal egészítettük ki a táblázatot.
A sorba 1-7-ig írtuk a számokat, az oszlopba pedig 1-6-ig. A sor az üres forrásszót
jelenti, az értékek pedig az egyes karakterek beszúrásának összesített költségét. Az
oszlop az üres célszót jelenti, az értékek pedig az egyes karakterek törlésének össze
sített költségét.
483
Sztringillesztés
o
m 1
s 2
3
e 4
a 5
k "6
m s
1 2 3 4
a k e
5 6 7
17.10. ábra. A msteak és mistake szavak összehasonlítására létrehozott táblázat
-A következő lépésben kiszámoljuk a táblázat többi cellájának értékét az alábbi képlet alapján:
min(bal átló + helyettesítés költsége, felette + törlés költsége, balra+ beszúrás költsége)
Az elsó-cella értékét például a következőképpen számoljuk ki:
min(O +O, l+ l, l+ l) = min(O, 2, 2) =O
A törlés és a beszúrás költsége mindig egy, a helyettesítés költsége azonban csak akkor, ha a forrás- és a célkarakter nem egyezik meg.
Bizmryos műveletek - küliinósen a beszúrás és tóTlés - kóltségét érdemes lehet megnö-vel
ni, ha szerintünk egy karakter cseréje kevésbé kóltséges, mint tódése vagy t!! beszúrása.
A cella értékének kiszámítása után a 17 .11. ábrán látható táblázatot kapjuk.
m
o 1
m 1 o
s 2
3
e 4
a 5
k 6
s
2 3 4
a k e
5 6 7
17.11. ábra. Az első cella (m,m) értékének kiszámítása
484
A következő cella (m, i) kiszámításának képlete a következő:
min(l + l, 2 +l, O+ l) = min(2, 3, l) =l
Így a táblázat a 17_12. ábrán láthatóra módosul.
m
o 1
m 1 o
s 2
3
e 4
a 5
k 6
s
2 3
f
4
a k e
5 6 7
17.12. ábra. A kö'vetkezőcella (m, i) értékének kiszámítása
A Levenshtein-szótávolság
A folyamat addig folytatódik, rníg minden cellának ki nem számoljuk az értékét, ahogy a 17.13. ábrán látható.
m
o 1 2
m 1 o 1
s 2 1 1
3 2 2
e 4 3 3
a 5 4 4
k 6 5 5
s
3 4
2 3
1 2
2 1
3 2
4 3
5 4
a k e
5 6 7
4 5 6
3 4 5
2 3 4
2 3 3
2 3 4
3 2 3
17 .13. ábra. A te/jes táblázat. Az utolsó cella (k, e) értéke a legkisebb távolság koltsége
A táblázat jobb alsó cellájában láthatjuk, hogy a "msteak" és a "mistake" szavak közötti legkisebb távolság 3. A táblázatból a forrássztring célsztringgé alakításának pontos műveleteit is kiolvashatjuk. A 17 .14. ábra a számtalan lehetőség közül bemutatja az egyik lehetséges megoldás menetét.
485
Sztringillesztés
m
o, 1 2
m 1 i �H ... \ s 2 1 1
3 2 2
e 4 3 3
a 5 4 4
k 6 5 5
s
3 4
2 3
1" 1 ., 2
2 "� 3 � 4 3
5 4
a k e
5 6 7
4 5 6
3 4 5
2 3 4
2 3 3
"� 3 4
3 �'2-� 3
17. 14. ábra. A "msteak" szó "mistake" szóvá alakításához szükséges műveletek
egyik lehetséges sorrendje a táblázat alapján
A következőképpen értelmezhetjük a 17 .14. ábrát:
1. Helyettesítsük az m betűt m betűvel, ennek költsége: O.
2. Szúrjunk be egy i betűt, ennek költsége: 1.
3. Helyettesítsük az s betűt s betűvel, ennek költsége: O.
4. Helyettesítsük a t betűt t betűvel, ennek költsége: O.
5. Töröljük az e betűt, ennek költsége: 1.
6. Helyettesítsük az a betűt a betűvel, ennek költsége: O.
7. Szúrjunk be egy e betűt, ennek költsége: 1.
Ebből már rájöhettünk, hogy a lefelé átlósan való mozgás helyettesítés, jobbra be
szúrás, egyenesen lefelé pedig törlés.
Az itt leírt algoritmus bonyolultsága O(MN), hiszen a forrássztring minden karak
terét (M) összevetjük a célsztring minden karakterével (N), így kapjuk meg a teljes táb
lázatot. Ez egyúttal azt is jelenti, hogy ebben a formában gyakorlatilag alkalmatlan
megfelelő mennyiségű szót tartalmazó helyesírás-ellenőrző készítésére, hiszen vala
mennyi távolság kiszámítása igencsak használhatatlanná tenné a programot. A szö
vegszerkesztőkben ehelyett a fejezetben bemutatott eljárásokhoz hasonló módsze
rek együttesével érik el a kívánt eredményt.
A következő gyakorlatban néhány teszttel meggyőződünk róla, hogy az algorit
mus megvalósítása megfelelően működik.
486
A Levenshtein-szótávolság
Gyakorlófeladat: a távolságszámító tesztelese
Hozzuk létre a tesztosztályt a következőképpen:
package com. wrox. a i"gori t lllils ."wmatcti;
import junit.framework.Testcase;
public class LevenshteinwordoistancecalculatorTest extends Testcase {
private Levenshteinwordoístancecalculator _calculator;
protected void setUp() throws Exception { super.setup();
_calculator = Levenshteinwordoistancecalculator.DEFAULT; }
public void testEmptyToEmpty() { assertoistance(O, '"', "");
}
public void testEmptyToNonEmpty() { String target = "any"; assertoistance(target.length(),
}
public void testsamePrefix() { assertoistance(3, "unzip", "undo");
}
target);
public void testsamesuffix() { assertoístance(4, "eating", "running");
}
public void testArbitrary() {
}
assertoistance(3, "msteak", "místake"); assertoistance(3, "necassery", "neccessary"); assertoistance(S, "donkey", "mule");
private void assertoistance(int distance, String source, String target) {
assertEquals(distance, _calculator.calculate(source, target)); assertEquals(distance, _calculator.calculate(target, source));
} l
487
Sztringillesztés
A megvalósitás müködése
A levenshtei nwordDi stancecal culatorTest osztály tartalmazza a Levenshtei n
wordDi stanceca l eu l a tor teszt által használt példányát. Ezt az alapértelmezett példánnyal inicializáljuk, ahogy már korábban leírtuk:
package com.wrox.algorithms.wmatch;
import junit.framework.Testcase;
public class LevenshteinwordDistancecalculatorTest extends Testcase {
}
private LevenshteinwordDistancecalculator _calculator;
protected void setUp() throws Exception { super. setup();
_calculator = LevenshteinwordDistancecalculator.DEFAULT; }
A számított távolság és a várt érték összevetésére az összes tesztben az assertDi s
tance() metódust használjuk. Paraméterként a forrás- és a célsztringet kell megadnunk neki, ezután kiszátrú�a a szavak távolságát, és összeveti a várt értékkel. A metódus legfontosabb tulajdonsága, amiért tulajdonképpen létrehoztuk, az, hogy a sorrend felcserélésével kétszer számolja ki a szavak távolságát. Így a sorrendtől függetlenül garantálható, hogy azonos távolságértéket kapunk. _ _
private void assertDistance(int distance, String source,
}
String target) { assertEquals(distance, _calculator.calculate(source, target)); assertEquals(distance, _calculator.calculate(target, source));
A testEmptyToEmpty() metódus azt vizsgálja, hogy két üres sztring távolsága nulla-e - hiszen bár üresek, a két sztring gyakorlatilag megegyezik.
public void testEmptyToEmpty() { assertDistance(O, " " , "")_;
}
A testEmptyToNonEmpty() metódus üres sztringet hasonlít össze tetszőleges nem üres sztringgel: a távolságnak meg kell egyeznie a nem üres sztring hosszával.
488
A Levenshtein-szótávolság
public void testEmptyToNonEmpty() {
}
·st r� n g target "'. "any"; assertoistance(target.length(), "", target);
A következő lépésben a testSamePrefi x() metódussal megvizsgáljuk a közös elő
tagú sztringek viselkedését: a távolságnak meg kell egyeznie a hosszabb sztringnek
az előt�ggal csökkentett hosszával:
_public void testsamePrefix() { assertDj stance(3, "unzi p", ''undo");
}
Ezután a testSamesuffix() metódussal megvizsgáljuk a közös utátagú sztringek
viselkedését: a távolságnak meg kell egyeznie a hosszabb sztring utótaggal csökken
tett hosszával:
public void testsamesuffix() { assertDistance(4, "eating", "running");
}
Ezután megvizsgálunk néhány ismert távolságú sztringpárt:
public void testArbitrary() {
}
assertDi stance(3, "msteak", "mi stake"); assertDistance(3, "necassery", "neccessary"); assertDistance(5, "donkey", "mule");
Most, hogy a tesztekben láttuk, rendben inúködik az algoritmus, a gyakorlófeladat
ban megvalósítjuk a távolságszámítót.
Gyakorlófeladat: a távolságszámitó megvalósitása
Készítsük el a távolságszámítót a következőképpen:
package com.wrox.algorithms.wmatch;
public class Levenshteinwordoistancecalculator { public static final Levenshteinwordoistancecalculator DEFAULT =
new Levenshteinwordoistancecalc�lator(l, l, l);
private final int _costofsubstitution; private final int _costofoeletion;
_pr_ivate_.fi na l _i nt __ costOfinserti on;
489
Sztringillesztés
490
publié LevenshteinwordÓistancecalculator(int costofsubstitution, int costofoeletion,
}
int costOfinsertion) { assert costofsubstitution >= O :
"A costofsubstitution nem lehet negatív"; assert costofoeletion >= O :
"A costofoeletion nem lehet negatív"; assert costOfinsertion >= O :
"A costofinsertion nem lehet negatív";
_costofsubstitution = costofsubstitution; _costofoeletion = costofoeletion; _costofrnsertion = costofrnsertion;
public int calculate(Charsequence source, charsequence target) { assert source !=null "A 'source' (forrás) nem lehet NULL"; assert target != null : "A 'target' (cél) nem lehet NULL";
}
int sourceLength int targetLength
sou rce. l eng th() ; target.length();
int[][] grid =new int[sourceLength + l][targetLength +l];
grid[O][O] =O;
for (int row = l; row <= sourceLength; ++row) { grid[row][O] = row;
}
for (int col = l; col <= targetLength; ++col) { grid[O][col] =col;
}
for (int row = l; row <= sourceLength; ++row) {
}
for (int col = l; col <= targetLength; ++col) { grid[row][col] = mincost(source, target, grid, row, col);
}
return grid[sourceLength][targetLength];
private int mincost(charsequence source, charsequence target, int[][] grid, int row, int col) {
}
return min(
) ;
substitutioncost(source, target, grid, row, col), deletecost(grid, row, col), insertcost(grid, row, col)
}
A Levenshtein-szótávolság
private�int substitutioncost(charsequence source,
}
charsequence target, int[][] grid, int row, int col) { int cost = O; if (source.charAt(row - l) != target.charAt(col - l)) {
cost = _costofsubstitution; } return grid[row - l][col - l] + cost;
private int deletecost(int[][] grid, int row, int col) { return grid[row - l][col] + _costofoeletion;
}
private int insertcost(int[][] grid, int row, int col) { return grid[row][col -l]+ _costOfinsertion;
}
private static int min(int a, int b, int c) {
} return Math.min(a, Math.min(b, c));
·
A megvalósitás müködése
A Levenshtei nwordDi stanceca l c ul a tor osztály három példányváltozóban tárolja a
három művelet (helyettesítés, törlés és beszúrás) költségét. Az osztály alapértelme
zésként (DEFAULT) mindhárom művelethez egységköltséget rendel, ahogy a fenti pél
dában is volt. Ezenkívül létrehoztunk egy nyilvános konstruktort, amellyel szabadon
beállíthatjuk a műveletek költségét.
package com.wrox.algorithms.wmatch;
public class LevenshteinwordDistancecalculator { public static final LevenshteinwordDistancecalculator DEFAULT
new LevenshteinwordDistancecalculator(l, l, l);
private final int _costofsubstitution; private 1inal int _costOfDeletion; private final int _costOfinsertion;
public LevenshteinwordDistancecalculator(int costofsubstitution, int costOfDeletion, int costofrnsertion) {
assert costofsubstitution >= O :
"A costofsubstitution nem lehet negatív"; assert costOfDeletion >= O :
"A costOfDeletion nem lehet negatív"; assert costofrnsertion >= O :
"A costOfinsertion nem lehet negatív ";
491
Sztringillesztés
}
}
_costofsubstitution = costofsubstitution; _costOfDeletion = costOfDel etion;
_costofrnsertion = costofrnsertion;
Mielótt az algoritmus mélyére néznénk, vegyük szemügyre a köztes számítás�kat. Az
első köztes számítást a substitutioncost() metódus végzi el. Ahogy a névból sejthető, ez a metódus számolja ki a karakter helyettesítésének költségét. Emlékszünk rá: a helyettesítés költsége O, ha a két karakter azonos, ellenkező esetben l + a balra ádósan lévő cella értéke.
A metódus kezdetnek O-val inicializálja a költség értékét, azt feltételezve, hogy a karakterek meg fognak egyezni. Ezután összevetjük a két karaktert, és ha eltérnek, a költség ennek függvényében nő. Végül pedig hozzáadjuk a táblázatban balra á dó san lévő cella értékét az összesített értékhez, majd a metódus ezzel az értékkel visszatér a meghívóhoz.
private int substitutioncost(CharSequence source,
}
charsequence target, int[][] grid, int row, int col ) {
int cost = O; if (source.charAt(row - l) != target.charAt(col - l)) {
cost = _costofsubstitution;
} return grid[row - l][col - l]+ cost;
A de l etecos t O metódus a törlés költségét számítja ki úgy, hogy a törlés egységköltségéhez hozzáadja a táblázatban közvetlenül felette lévő cella értékét:
private int del etecost(int[][] grid, int row, int col ) { return grid[row - l][col] + _costofoel etion;
}
Végül az i n serteostO metódus a beszúrás költségét számítja ki. Ezúttal a beszúrás egységköltségéhez hozzáadjuk a közvetlenül balra álló cella összesített értékét, majd a metódus ezzel az értékkel visszatér a meghívóhoz:
private int insertcost(int[][] grid, int row, int col ) {
return grid[row][col - l] + _costofrnsertion;
}
A mi ni mumcost () metódus kiszámí�a mindhárom múvelet költségét, majd az értékeket átadja a mi n() metódusnak- így kényelmesen megtalálha�uk a legkisebb értéket:
492
A Levenshtein-szótávolság
private int minimumcost(Charsequence source, charsequence target, int[][] grid, int row, int col) {
return min(
) ;
}
substitutioncost(source, target, grid, row, col), deletecost(grid, row, col), insertcost(grid, row, col)
private static int min(int a, int b, int c) { return Math.min(a, Math.min(b, c));
}
Most megnézhetjük a tényleges algoritmust. Ehhez definiáltuk a ca l eu l a te O me
tódust, amely paraméterként a forrás- és a célsztringet fogadja el, visszatérési értéke
pedig a kettő közötti szótávolság.
A metódus első lépésként inicializálja a számítás elyégzéséhez szükséges tábláza
tot, a bal felső sarokba O-t írva. Ezt követően az első sor és az első oszlop minden
celláját inicializáljuk. Az így keletkező táblázat nagyjából a 17.11. ábrán láthatóval
fog megegyezni.
Ezután végignézzük az összes forrás- és célkarakter párt, mindegyikre kiszámít
juk a legkisebb költséget, majd a kapott értéket eltároljuk a megfelelő cellában. Ha az
összes lehetséges karakterkombinációt feldolgoztuk, kiválasztjuk a táblázat jobb alsó
sarkában lévő értéket (ahogy a 17.13. ábrán is tettük), és az így kapott értéket vissza
adjuk a meghívó metódusnak, ez a legkisebb távolság.
public int calculate(charsequence source, charsequence target) { assert source != null : "A 'source' (forrás) nem lehet NULL"; assert target != null : "A 'target' (cél) nem lehet NULL";
int sourceLength int targetLength
so u r ce. length O ; target. length O ;
i nt[] [] g ri d new int[sourceLength + l][targetLength +l];
grid[O][O] =O;
for (int row = l; row <= sourceLength; ++row) { grid[row][O] = row;
}
for (int col = l; col <= targetLength; ++col) { grid[O][col] = col;
}
493
Sztringillesztés
}
for (int row = l; row <= sourceLength; ++row) { for (int col = l; col <= targetLength; ++col) {
grid[row][col] = minimumcost(source, target, grid, row, col);
} }
return grid[sourceLength][targetLength];
Összefoglalás
• Az úgynevezett fonetikus kódolási algoritmusok, mint például a Soundex, hatékonyan megtalálják a hasonló alakú szavakat.
• A Soundex-értékeket általában adatbázisoknál, kétszerezett bejegyzések vagy elgépelt nevek keresésére használjuk.
• A Soundex a négykarakteres kódot O(N) bonyolultságú algoritmussal állítja elő.
• A Levenshtein-szótávolság algoritmusa kiszárrútja az egyik szónak a másikká történő alakításához szükséges legkevesebb művelet számát. Minél kisebb a távolság, annál hasonlóbbak a szavak.
• A Levenshtein-algoritmus a helyesírás-ellenőrzők, DNS-keresők, plágiumfelismerők és egyéb alkalmazások alapja.
• A Levenshtein-algoritmus idő- és 'térbeli bonyolultsága O(MN).
494
TIZENNYOLCADIK FEJEZET
Számitógépes geometria
Ebben a fejezetben megismerkedhetünk az algoritmustervezés egyik lenyűgöző terü
letével, a számítógépes geometriával. Mivel ezzel a témával könyvek tucatjait tölt
hetnénk meg, itt csak a felszínét érintjük. Ha többet szeretnénk megtudni róla, utá
nanézhetünk a hivatkozásokban vagy az interneten.
A számítógépes geometria a számítógépes grafika egyik alapköve, így ha érde
keltek vagyunk játékok vagy más grafikus területek szaftvereinek fejlesztésében, tisz
tában kell lennünk a számítógépes geometriával.
A fejezetben tárgyalt iisszes téma a kétdimenifós geometriára korlátozódik. A három
dimenifó előtt a fogalmakat két JimeniJóban kell megértenünk, mivel az már túlmutat
a fejezet határain. Nagyon sok kitűnő kö"tryv léteifk, me!Jek a háromdimenifós graft
kában használt algoritmusokra specializálódtak, a Függelék hivatkozásai kö"zölt, de
egy jó informatikai könyvesboltban is találunk i!Jeneket.
A fejezetben a következő témaköröket tárgyaljuk
• rövid geometriai ismétlés,
• két egyenes szakasz metszéspon�ának meghatározása,
• szórt ponthalmazban az egymáshoz legközelebb eső pontok párjainak meg
határozása.
Rövid geometriai ismétlés
Ez a rész megment minket attól, hogy a középiskolás matematikakönyvekben kelljen
keresgélnünk a fejezet hátralévő részének megértéséhez szükséges fogalmak átismét
léséhez.
Koordináták és pontok
A kétdimenziós fogalmak leírásához általában x-y koordinátarendszert használunk.
Ezt a rendszert két egyenes vonallal ábrázolhatjuk - ezek a tenge!Jek -, amelyek me
rőlegesek egymásra, ahogy a 18.1. ábrán láthatjuk.
Számítógépes geometria
Y tengely
X tengely
18. 1. ábra. A két tenge/y ből álló x-y koordinátarendszer
A vízszintes tengelyt x tenge!Jnek, a függőlegest y tengelJnek nevezzük. Az x tengelyhez
tartozó pozíciók számozása balról jobbra emelkedő értékekkel halad. Az y tengelyen
az értékek felfelé növekednek.
A pont a kétdimenziós térben olyan hely, amelyet két szám határoz meg (x,y) for
mában, ahol x a pont merőleges vetülete az x-tengelyre, y pedig a pont merőleges vetü
lete az y-tengelyre. A 18.2. ábra például a (3,4) pontot muta* a koordinátarendszerben.
Y tengely
(3 4) 4 -------
2 4 5
X tengely
18.2. ábra. A (3,4) pont az x-y koordinátarendszerben
Az x-y koordinátarendszer a tengelyektől balra és lefelé i� kiterjeszthető. A tengelyek
ezen végeihez rendelt koordináták negatív koordinátaként vannak definiálva, ahogy
a 18.3. ábrán látható, különböző régiókban található pontokkal.
(3 ,4) 4 •
3
(-5,1) 2
•
-5 -4 -3 -2 -1 2 3 4 5 -1
-2 •
• -3 (4,-2)
(-2,-3) -4
18.3. ábra. A koordináták mind az x, mind av tengelJen lehetnek negatívak is
496
Rövid geometriai ismétlés
Egyenes szakaszok
Egy egyenes szakasz egyszerűen két pont közötti egyenes vonal. A2 egyenes szakasz
meghatározásához két végpontra van szük.ség. Ezek alapján már megállapíthatjuk az
egyenes szakasz hosszát, meredekségét és egyéb érdekes dolgokat, melyekről később
még eleget beszélünk. A 18.4. ábrán az (1,1)-(5,4) egyenes szakaszt láthatjuk
4 ,•
3
2-1 //
•
2 3 4 5
18.4. ábra. Egyenes szakasz az x-y koordinátarendszerben
Háromszögek
Nem szeretnénk azzal megsérteni az olvasót, hogy elmondjuk, rni az a háromszög
(elnézést kérünk, hogy az előző részben az egyenes szakasszal kapcsolatban így tet
tünk). Ebben a fejezetben legtöbbször a derékszögű háromszögekkel fogunk foglal
kozni: ezeknek egyik szöge 90 fokos, ahogy a 18.5. ábrán láthatjuk.
a� b
18.5. ábra. Deréksziigű háromszög
A legjobb dolog a derékszögű háromszögekben az, hogy ha ismerjük két oldal hosz
szúságát, akkor a Pitagorasz-tétel segítségével meghatározhatjuk a harmadik oldalát.
A 18.5. ábrán az oldalakat a-val, b-vel és c-vel jelöltük. A Pitagorasz-tétel szerint
a2 + b2 = c2
abban az esetben, ha c a leghosszabb oldal, vagyis az átfogó. Az általános példa egy
olyan háromszög, amely a 18.6. ábrán látható: az oldalai 3, 4 és 5 egységnyiek
497
Számítógépes geometria
3� 4
18.6. ábra. Egy derékszögű háromszög adott oldalhosszakkal
Az ábráról egyszerűen látható, hogy
32 + 42 = 52
vagy
9 + 16 = 25
Ez minden, amit tudnunk kell az első számítógépes geometriai probléma előtt: meghatározni két egyenes szakasz metszéspontját.
Két egyenes szakasz metszéspontjának meghatározása
Ez a fejezet végigvisz egy olyan számítógépes geometriai problémán, amelyben meghatározzuk azt a pontot, melyben két egyenes szakasz metszi egymást. A 18.7. ábrán két egyenes szakaszt láthatunk, amelyek a P-vel jelölt pontban metszik egymást.
4
2
2 3 4 5
18.7. ábra. Két egymást metsző egyenes szakasz
Abban az esetben, ha tudjuk a négy pontot, melyek meghatározzák a két vonal vég. pon�ait, hogyan tudjuk megállapítani, hol (ha egyáltalán) metszi egymást a két egye
nes szakasz? Az első dolog, amivel tisztában kell lennünk, az egyenest meghatározó algebrai összefüggés:
y = mx + b
ahol x és y a már megismert koordináták, m az egyenes meredeksége, b pedig az a pont, amelyben az egyenes az y tengelyt metszi. Ne aggódjunk, ezeket a fogalmakat a későbbiekben tisztázzuk
498
Rövid geometriai ismétlés
Meredekség
Egy egyenes vagy egyenes szakasz meredeksége egyszerűen megmondja, mennyire
meredek. Ezt egy könnyű módszerrel írhatjuk le, amelyet a 18.8. ábrán ábrázoltunk
4
3
2
Emelkedés
Elmozdulás
2 3 4 5
18.8. ábra. Egy egyenes szakasz meredekségét az emelkedés és az elmozdulás hátryadosa a4Ja meg
Az emelkedés az a függőleges távolság (y mennyiség), amelyet az egyenes szakasz át
fog. Az elmozdulás az a vízszintes távolság (x mennyiség), anút az egyenes szakasz át
fog. V égezetill a meredekség az emelkedés és az elmozdulás aránya. Például annak
az egyenes szakasznak, amelynek az emelkedése és elmozdulása azonos, a meredek
sége 1, ahogy a 18.9. ábrán láthatjuk.
4
2
(4 .4)
' '
: Emelkedés=3
'
------------� (1, 1) Elmozdulás;3
2 3 4 5
18.9. ábra. 1 meredekségű egyenes szakasz
A meredekség lehet negatív is. A 18.10. ábrán egy -2 meredekségú egyenes szakasz
látható, ahol az emelkedés (süllyedés) az első ponttól a második pontig süllyed, vagy
negatív, és kétszer akkora, mint az elmozdulása.
4
3
2
(2.4)
(3.5,1) Elmozdulás;1 ,5
2 3 4 5
18. 1 o. ábra. Negatív meredekségű egyenes szakasz
499
Számítógépes geometria
Létezik még néhány említésre méltó, speciális eset. A vízszintes egyenes szakaszok meredeksége zérus, mivel nem szánút, mekkora az elmozdulás, az emelkedés mindig nulla. A függőleges egyenes szakasz bonyolultabb kérdés, ennek elmozdulása az emelkedéstől függetlenül zérus. Mint tudjuk, a meredekség az emelkedés és az elmozdulás aránya, vagyis az emelkedést elosztjuk az elmozdulással, hogy megkapjuk a meredekség értékét. A nullával való osztás természetesen lehetetlen, ezért a függőleges egyenes szakaszok meredeksége végtelen, ami egy számitógépnek nem sokat mond. Óvatosnak kell lennünk a kódolásnál, hogy elkerüljük a függőleges egyenes szakaszokat, ahogy azt később láthatjuk.
Az y tengely metszése
Azok az egyenes szakaszok, melyek meredeksége azonos, párhuzamosak egymással. Két azonos meredekségű egyenes szakasz különböző pontokban metszi az y tengelyt (kivéve ha függőlegesek, de ezzel még ne foglalkozzunk). A 18.11. ábra két párhuzamos egyenes szakaszt ábrázol, melyek meredeksége 0.5, és két különböző pontban metszik az y tengelyt.
Y=0.5x+2
18.11. ábra. Egy párhuzamos egyenes SifZkaszpár
Tudjuk, hogy a felső egyenes szakasz az y tengelyt az y 2 értékénél metszi, így a képlet
y=0.5x +2
Az alsó vonal az y tengelyt az y -1 értékénél metszi, így az egyenlet
y = O. 5 x - l
500
A metszéspont meghatározása
A metszéspont meghatározása
Most már rendelkezünk elég háttérrel ahhoz, hogy végigvigyünk egy példát, amelyben
két egyenes szakasz metszéspon�át határozzuk meg. Használjuk fel ehhez a 18.12. ábrát.
18. 12. ábra. Egymást metsző egyenes SifZkas'{/Jár példtija
A trükk az, hogy a metszéspont koordinátái mindkét egyenes egyenletét kielégítik.
Más szóval, ha az első egyenes egyenlete:
Y = mx + b
rrúg a második egyenes egyenlere:
y = nx + c
a metszéspont meghatározása:
mx + b = nx +c
az alábbi módon átrendezve:
mx - nx = c - b
majd újra átrendezve:
x = (c - b) l (m - n)
Ez tehát azt jelenti, hogy ha ismerjük a két egyenes egyenletét, a bemutatott egyenlet
alapján meghatározhatjuk belőle a metszéspont x koordinátáját. Ebben a példában a
képlet az alábbi:
x = (-2 -2) l (0.5 - -2)
501
Számítógépes geometria
Amiből következik, hogy
x = -4 l 2.5
amiből
x = -l. 6
Ha a 18.12. ábrát megnézzük, helyesnek tűnik az x koordinátára kapott érték. Az y koordináta meghatározása egyértelmű: helyettesítsük vissza a most meghatározott x
koordinátát akármelyik egyenes egyenletébe. Például:
y = O. 5 x + 2
y 0.5 x -1.6 +2
y -0.8 +2
y 1.2
Ebből következik, hogy a példaszakaszok esetében a metszéspont koordinátái (-1.6, 1.2).
A módszer kicsit változik, ha az egyenes szakaszok egyike függőleges. Az lépés, amellyel a metszéspont x koordinátáját határoztuk meg, nem használható, mivel a metszéspont x koordinátája, ha valamely egyenes szakasz függőleges, egyszerűen annak x koordinátája. Megoldva a nem függőleges egyenes egyenletét ezzel az x értékkel, befejezi a munkánkat. Itt az ideje, hogy az előzőekben tárgyalt elméletet kódba dolgozzuk. A következő gyakorlófeladatban a fogalmak nagy részét Java objektumokba képezzük, ezért érdemes erőfeszítéseket tenni a pontos megértésükért. Először egy osztályt készítünk, amely a pontokat képviseli.
Gyakorlófeladat: a Point osztály vizsgálata és kivitelezése
Indulásként határozzuk meg, mit csináljon a Po i nt osztály a ]Unit vizsgálati esetében. Csak két viselkedést várunk a Point osztálytól: határozza meg, ha egy pont egy másikkal azonos (azaz azonosak a koordináták), valamint állapítsa meg egy pontnak egy másik ponttól való távolságát.
502
Íme a kód:
package com.wrox.algorithms.geometry;
import junit.framework.Testcase;
public class PointTest extends Testcase { public void testEquals() {
assertEquals(new Point(O, O), new Point(O, O)); assertEquals(new Point(5, 8), new Point(5, 8)); assertE uals(new Point(-4, 6), new �oint(-4, 6)�)�· ···-" ______ _,
A metszéspont meghatározása
}
assertFal se(new Po i ntco-;- o). equa lS(new Point(l-;- O))); assertFalse(new Point(O, O).equals(new Point(O, l))); assertFalse(new Point(4, 4).equals(new Point(-4, 4))); assertFalse(new Point(4, 4).equals(new Point(4, -4))); assertFalse(new Point(4, 4).equals(new Point(-4, -4))); assertFalse(new Point(-4, 4).equals(new Point(-4, -4)));
public void testoistance() { assertEquals(l�d,
new Point(O, O).distance(new Point(O, 13)), 0); assertEquals(13d,
new Point(O, O).distance(new Point(13, O)), O); assertEquals(13d,
new Point(O, O).distance(new Point(O, -13)), 0); assertEquals(13d,
new Point(O, O).distance(new Point(-13, 0)), 0);
assertEquals(Sd, new Point(!, l).distance(new Point(4, 5)), O); assertEquals(Sd,
new Point(!, l).distance(new Point(-2, -3)), O); }
l
Hogy megkezdhessük a Point kivitelezését, deklaráljunk egy példányváltozót, hogy
mind az x, mind az y koordinátákat eltároljuk, valamint egy konstruktort, hogy inici
alizáljuk őket. Jegyezzük meg, hogy mindkét mező véges, és az osztály objektumai
legyenek megváltoztathatatlanok.
package�com.wrox.algoritnms.geometry;
public class Point { private final double _x; private final double _y;
public Point(double x, double y) { _x = x; _y = y;
}
Ezután gondoskodjunk a koordináták egyszerű hozzáférhetőségéről:
publiC" double getX() { return _x;
}
public double getY() { return _y;
l
503
Számítógépes geometria
Használjuk a Pitagorasz-tételt, hogy kiszámítsuk a pont és a di stance() metódus
nak megadott másikpont közötti távolságot:
public double distance(Point other) { assert other != null "az 'other' nem lehet null";
double rise = getY() other.getY();
double travel = getX() - other.getX();
return Math.sqrt(rise * rise + travel * travel);
}
Már csak az van hátra, hogy létrehozzuk az equals() és a hashcode() metódusokat
az alábbiak szerint:
public int hashcode() { return (int) (_x * _y);
}
public boolean equals(Object obj) { if (this == obj) {
return true;
}
if (obj == null l l obj.getclass() !=getclass()) { return false;
}
Point other = (Point) obj;
return getX() == other.getX() && getY() other.getY();
}
A megvalósitás müködése
A Po i nt osztály tagváltozók formájában tárolja az x és az y koordináták értékét. Eze
ket a változókat a konstruktor inicializálja, és később nem változtathatók meg. Hogy
egy pontnak egy másiktól való távolságát meghatározzuk, a kód a két pontot úgy ke
zeli, mint egy derékszögű háromszög csúcsait, és a Pitagorasz-tétel segítségével meg
határozza a háromszög átfogóját, amely most a két pont közötti távolsággal egyenlő.
A kód azt is megállapítja, ha két pont azonos. Két pont azonosságának megha
tározásához csak annyi kell, hogy koordinátáik megegyezzenek, így a kód csak egy
szeruen összehasonlítja a két pont x és y koordinátáit, és azonosság esetén true ér
tékkel tér vissza.
Ez minden, amit a Point osztály tartalmaz. A következő feladatban egy egyenes
szakasz meredekségét fogjuk modellezni.
504
A metszéspont meghatározása
Gyakorlófeladat: egyenes szakasz meredekségének vizsgálata
Kezdésként írjunk egy olyan próbaesetet, mely teszteli, hogy egy meredekség függő
leges-e:
package cóm--:wrox. al gorittlms. geometry;
import junit.framework.Testcase;
public class slopeTest extends Testcase {
}
public void testisVertical() { assertTrue(new slope(4, O).isvertical()); assertTrue(new slope(O, O).isvertical()); assertTrue(new slope(-5, O).isvertical()); assertFalse(new slope(O, S).isvertical()); assertFalse(new slope(O, -5).isvertical());
}
Következőben írjunk egy olyan vizsgálatot, amely meghatározza egy meredekségről,
hogy párhuzamos-e egy másik meredekséggel. Használjuk erre a standard e qua ls O
metódust:
public void testEquals()-
{
_l
assertTrue(new Slope(O, -5).equals(new slope(O, 10))); assertTrue(new slope(1, 3).equals(new slope(2, 6))); assertFalse(new slope(1, 3).equals(new slope(-1, 3))); assertFalse(new slope(1, 3).equals(new slope(1, -3))); assertTrue(new Slope(S, O).equals(new slope(9, 0)));
Készítsünk egy ellenőrző eljárást annak biztosítására, hogy egy nem függőleges me
redekség J ava do u b l e formátumban kiszámolható legyen:
pufi li c void testAsooutil e ForNonvert i ca ls l ope O-{ assertEquals(O, new Slope(O, 4).asoouble(), 0); assertEquals(O, new Slope(O, -4).asoouble(), O); assertEquals(1, new slope(3, 3).asoouble(), O); assertEquals(l, new slope(-3, -3).asoouble(), O); assertEquals(-1; new slope(3, -3).asoouble(), O); assertEquals(-1, new slope(-3, 3).asoouble(), O); assertEquals(2, new slope(6, 3).asoouble(), O); assertEquals(l.S, new slope(6, 4).asoouble(), O);
}
Végül ellenőriznünk kell, mi történik, ha valaki elég figyelmeden, és kiszámolta�a
egy függőleges egyenes szakasz do ub l e típus ú meredekségét. Biztosnak kell lennünk
benne, hogy kivétel keletkezik a megfelelő üzenettel együtt:
505
Számítógépes geometria
public voia testAsoouoleFailsForverticalslope() { try {
}
new slope(4, O).asoouble(); fail("felrobbanhat!");
} catch (IllegalstateException e) {
}
assertEquals("Függőleges egyenes szakasz nem reprezentálható -double értékkel",e.getMessage());
A megvalósitás müködése
A kód feltételezi, hogy a sl ope objektum két egész értékkel példányosítható, amelyek
megadják a meredekség emelkedését és elmozdulását. Nem szabad elfeledkeznünk ró
la, hogy ez nem jelöl ki egy fix pontot a kétdimenziós síkban. Ugyanígy egy meredek
ségnek hossza sincs. Csak magára a lejtő meredekségére kell koncentrálnunk Külön
böző pontok közötti egyenes szakaszok is rendelkezhetnek ugyanakkora meredekség
gel: az azonos meredekségű egyenes szakaszok párhuzamosak Ez a kódban egy olyan
vizsgálat formájában _jelenik meg, mely meghatározza, hogy egy meredekség azonos-e
egy másik meredekséggeL Ezt úgy érjük el, hogy mind pozitív, mind negatív tesztese
tekkel vizsgálódunk, hogy biztosak legyünk benne, hogy a megvalósítás helytálló.
Ismét elővéve az egyenes egyenletét (y = mx + b) , ahol az m lebegőpontos érték,
amely megadja az egyenes szakasz emelkedésének és elmozdulásának arányát. A közölt
teszt kód több állítást is tartalmaz, hogy meggyőződjünk róla, helyesen számí�a-e ki a
megvalósítás az értékét. Külön kell választanunk az ellenőrzőkód azon részét, amely a
függőleges egyenes szakasszal foglalkozik, attól, amely a nem függőleges egyenes szaka
szokat kezeli, mivel egy függőleges egyenes szakaszmeredekségét lehetetlen kiszámítani
Ezen a teszthalmazon átjutva egy hatásos megvalósításhoz jutunk, melyet a kö
vetkező gyakorlati részben építünk föL
Gyakorlófeladat: a Slope megvalósitása
A meredekség megvalósítását egy véges tagváltozópár és az azokat inicializáló konst
ruktor létrehozásával kezdjük, ahogy azt itt láthatjuk:
506
pac�age com.wrox.algorithms.geometry;
public class slope {
}
private final double _rise; private final double _travel;
public slope(double rise, double travel) { _rise = rise; _travel = travel;
}
A metszéspont meghatározása
Hozzuk létre az i sve r ti ca l () eljárást az alábbi triviális módon:
publi-c boolean isverticalÜ-{ return _travel == O;
Készítsük el a hashcode() és equals() eljárásokat, hogy meghatározhassuk, ha két
meredekség azonos:
pub l i c i nt hashcöae o ""{ return (int) (_rise * _travel);
}
public boolean equals(object object) { if (this == object) {
return true; }
if (object == null ll object.getclass() !=getclass()) { return false;
}
Slope other = (Slope) object;
if (isvertical() && other.isvertical()) { return true;
}
if (isvertical() l l other.isvertical()) { return false;
}
return (asoouble()) (other.asoouble()); } -
Végül számíts uk ki a meredekség numerikus értékét, közben kerüljük a függőleges
egyenes szakaszokat:
publ�uble asoouble() { if (isvertical()) {
}
throw new rllegalStateException("Függóleges egyenes szakasz nem reprezentálható double értékkel");
}
return _rise l _travel;
507
Számítógépes geometria
A megvalósitás működése
A korábbiakban már láthattunk olyan osztályokat, melyek rendelkeztek a konstruktor által inicializált véges tagváltozókkal (ilyen volt a már tárgyalt s l ope osztály), így tisztában vagyunk az osztály alapstruktúrájával.
Nagyobb kihívás meghatározni azt, ha két meredekség azonos. Létrehoztunk egy egyszerű hashcode () majd egy equals() eljárást elméletileg három esettel: első esetben mindkét egyenes szakasz függőleges, ebben az esetben a meredekségeik azonosak; a következő esetben csak az egyik meredekség függőleges, így ebben az esetben nem egyenlőek. Végezetül az általános eset, amelyben a két meredekség akkor egyenlő, ha a Java double típusú numerikus értékük egyenlő. A kódnak minden olyan esetet ki kell küszöbölnie, melyben függőleges egyenes szakasz szerepel azelőtt, hogy megpróbálná kiszámolni bármelyik egyenes szakasz numerikus meredekségét.
A végső metódus számítja ki a meredekséget, mint az emelkedés és az elmozdulás double típusú arányának értékét. Fontos, hogy elkerüljük a nullával való osztást, arni
kor az egyenes szakasz függőleges. A kód ebben az esetben kivételt képezve reagál. A következő gyakorlati részben elkészítjük azokat a vizsgálatokat, melyek az
egyenes szakasz több tulajdonságát is meghatározzák, beleértve, hogy függőleges-e, egy másik egyenes szakasszal párhuzamos-e stb.
Gyakorlófeladat: a Line osztály vizsgálata
Az egyenes szakaszok metszési problémájában az utolsó osztály a L i ne. Olyan tesztesetek megírásával kezdjük, melyek megadják, rnilyen működést várunk el a L i ne
osztálytóL Az első vizsgálatból megtudha�uk a L i ne osztálytól, hogy tartalmaz-e egy megadottPoint-ot-azaz a Po i nt az egyenes szakaszra esik-e:
508
package com.wrox.algorithms.geometry;
import junit.framework.Testcase;
public class LineTest extends Testcase { public void testcontainsForNonverticalLine() {
Point p new Point(O, O); Point q= new Point(3, 3);
Line l = new Line(p, q);
assertTrue(l.contains(p)); assertTrue(l.contains(q));
assertTrue(l.contains(new Point(l, l))); assertTrue(l.contains(new Point(2, 2))); assertTrue_ l. con ta i ns n� Po i nt(O. S O. 5)'-' )'--')'"-';'------------'
A metszéspont meghatározása
}
}
assertFalsé'(l:corítai ns(néw Po i ntC3-:1�-3-:-l))); assertFalse(l.contains(new Point(3, 3.1))); assertFalse(l.contains(new Point(O, l))); assertFalse(l.contains(new Point(-1, -1)));
Külön vizsgálnunk kell a működést a függőleges egyenes szakaszok esetében, hogy biztosak legyünk benne, az itt látható módon kezeltük e speciális esetet:
public void testcontainsForverticalLineÖ�{ Point p= new Point(O, O);
}
Point q= new Point(O, 3);
Line l = new Line(p, q);
assertTrue(l.contains(p)); assertTrue(l.contains(q));
assertTrue(l.contains(new Point(O, l))); assertTrue(l.contains(new Point(O, 2))); assertTrue(l.contains(new Point(O, 0.5)));
.assertFalse(l.contains(new Point(O, 3.1))); assertFalse(l.contains(new Point(O.l, l))); assertFalse(l.contains(new Point(l, 0))); assertFalse(l.contains(new Point(-1, -1)));
Azt szeretnénk, hogy egy egyenes szakasz jelezze, ha párhuzamos egy másik szakaszszal. Óvatosnak kell lennünk, és a függőleges egyenes szakaszt speciális esetként kell kezelnünk. Az első vizsgálat a megfelelő viselkedést bizonyítja abban az esetben, amikor a két egyenes szakasz párhuzamos, de nem függőlegesek
publ1c void testisParallelForTWONonVerticalParallelLines()-{ Point p= new Point(l, l);
}
Point q= new Point(6, 6); P�nt r : new Point(4, -2); Point s = new Point(6, 0);
Line l = new Line(p, q); Line m = new Line(r, s);
assertTrue(l.isParaltelTo(m));assertTrue(m.isParallelTo(l));
509
Számítógépes geometria
A következőkben két nem függőleges, nem párhuzamos egyenes szakasz esetén
vizsgáljuk a viselkedést:
public void testlsParalÍelForTWoNonverticaiNonParallelLines() { Point p= new Point(l, l);
}
Point q new Point(6, 4); Point r new Point(4, -2); Point s new Point(6, 0);
Line l = new Line(p, q); Line m = new Line(r, s);
assertFalse(l.isParallelTo(m)); assertFalse(m.isParallelTo(l));
A következő vizsgálatban megnézünk néhány szélsőséges esetet - először is azt, ami
kor mindkét egyenes szakasz függőleges (így magától értetődően párhuzamosak is):
puli l i c voi d testisParallelForTWoverticalParallelLines()
}
Point p new Point(l, l); Point q new Point(l, 6); Point r new Point( 4, -2); Point s new Point( 4, O);
Line l = new Line(p, q); Line m = new Line(r, s);
assertTrue(l.isParallelTo(m)); assertTrue(m.isParallelTo(l));
{
Az isParallel () eljárás utolsó vizsgálatában azt nézzük meg, amikor az egyik sza
kasz függőleges, a másik pedig nem:
510
public void Point p = Point q
testisParallelForoneverticalAndoneNonverticalLine() { new Point(l, l); new Point(l, 6);
}
Point r Point s
new Point(4, -2); = new Point(6, O);
Line l = new Line(p, q); Line m = new Line(r, s);
assertFalse(l.isParallelTo(m)); assertFalse(m.isParallelTo(l));
A metszéspont meghatározása
Most készítsünk néhány olyan vizsgálatot, melyben két egyenes szakasz metszéspont
ját határozzuk meg. A L i ne osztályban hozzunk létre egy i ntersecti onPoi nt() eljá
rást, melynek L i ne objektumot adunk át. Ez a metódus nu ll értékkel tér vissza, ha az
egyenes szakaszok nem metszik egymást, vagy Point objektummal, ha létezik met
széspont. Ismét külön figyelmet kell szentelni a függőleges egyenes szakaszoknak.
Először belátjuk, hogy két párhuzamos, nem függőleges egyenes szakasznak va
lóban nincs metszéspontja, ahogy azt a következő tesztmetódusban láthatjuk:
public void Point p Point q Point r Point s
Line l Line m
testParallelNonverticaliinesooNotintersect()-{ new Point(O, O); new Point(3, 3); new Point(S, O); new Point(S, 3);
new Line(p, q); new Line(r, s);
assertNull(l.intersectionPoint(m)); assertNull(m.intersectionPoint(l));
}
Most alakítsuk ki ugyanezt a viselkedést egy függőleges egyenes szakaszpár esetében is:
public void testvertical'l inesooNotlntersect()-{ Point p= new Point(O, O);
}
Point q= new Point(O, 3); Point r= new Point(S, 0); Point s = new Point(S, 3);
Line l Line m
new Line(p, q); new Line(r, s);
assertNull(l.intersectionPoint(m)); assertNull(m.intersectionPoint(l));
Az alábbi tesztesetben két egyenes szakasz egyszerűen meghatározható metszés
pon�ával teszteljük a helyes működést:
public void testintersectionofNonParallelNonvertical'lines()-{ Point p= new Point(O, O); Point q= new Point(4, 4); Point r= new Point(4, 0); Point s = new Point(O, 4);
Line l = new Line(p, q); .ine m= new Line�; $...--- ----'
. 511
Számítógépes geometria
Point i = new Point(2, 2);
a55ertEqual5(i, l.inter5ectionPoint(m)); a55ertEqual5(i, m.inter5ectionPoint(l));
Azt az esetet, amikor az egyik egyenes szakasz függőleges, a következőképpen kezeljük:
testinter5ectionofverticalAndNonverticalLine5() { new Point(O, O); new Point(4, 4);
public void Point p = Point q Point r Point 5
= new Point(2, 0); new Point(2, 4);
Line l = new Line(p, q); Line m = new Line(r, 5);
Point i =new Point(2, 2);
a55ertEqual5(i, l.inter5ectionPoint(m)); a55ertEqual5(i, m.inter5ectionPoint(l));
Végül képzeljük el azt, amikor két egyenes szakasznak csak ún. ehnéleti metszéspon�a van, azaz egyik vagy másik szakasz nem elég hosszú ahhoz, hogy tartahnazza a pontot. Az ilyen egyenes szakaszokat dis?Junktegyenes szakaszoknak nevezzük. A 18.13. ábrán egy diszjunkt egyenes szakaszpárt láthatunk ehnéleti metszéspon�uk megjelölésével:
5
18.13. ábra. Dis?Junkt egyenes szakasilJár
Álljon itt a kód, hogy biztosak legyünk a megfelelő működésben:
public voi .d te5tDi 5 j o i nt L i ne5ooNotintersectÖ Point p= new Point(O, 0); Point q= new Point(O, 3); Point r = new Point(S, O);
·--�--"Po.:::.if.lt 5= new Point(-1, -3);----- �----------�__.
512
A metszéspont meghatározása
��·� L i ne l = new'Lfne.(p7Q);.
}
Line ma new Line(r, s);
assertNull(l.intersectionPoint(m)); assertNull(N.intersectionPoint(l));
A megvalósitás müködése
Az előző tesztesetekben olyan helyzeteket vizsgáltunk, ahol az egyenes szakaszok metszették egymást vagy nem, és vagy függőlegesek voltak, vagy nem. Amikor vizs� gálatokat használunk a megvalósítás előreviteléhez, fontos, hogy minden esetet kezeljünk, és ne csak feltételezzük, hogy megtettük. Úgy tűnik, rengeteg teszteset létezik, de ne felejtsük el, hogy sok tulajdonságot vizsgálunk. Ha ennél több tesztre lenne szükségünk, meg kell fontolnunk, ne bontsuk-e több osztályra a működést. Éppen ez az oka annak, hogy elkészítettük a sl ope osztályt.
A következő feladatban magát a L i ne osztályt valósí�uk meg és futta�uk át ezeken a teszteken.
Gyakorlófeladat: a Line osztály megvalósítása
A L i ne osztály három példánytaggal rendelkezik: a két Po i nt objektummal, melyek a végpontokat definiálják, és egy slope objektummal, amely egységbe zárja a meredekséget. Készítsük el'a mezőket és a konstruktort az itt látható módon:
package com.wrox.algÖrithíls.geometry;
public class Line { private final Point _p; private final Point _q; private final Slope _slope;
public Line(Point p, assert l' l= null
assert q != null
_p= p; _q "' q;
Point q) { "egyenes szakaszt Illeghatározó pont ne��
lehet null"; "egyenes szakaszt meghatározó pont nem
lehet null";
_slope =new slope(_p.getY() - _q.getY(), _p.getX() -_q.getx());
}
1�� ... �- _, -----�- ·· ·----....
Hozzuk létre az isParallel To() metódust úgy, hogy kihasználjuk a slope azon tulajdonságát, hogy képes meghatározni, azonos-e egy másik 51 ope-pal.
513
Számítógépes geometria
public boolean isParallelTo(Line line) { return _slope.equals(line._slope);
}
Valósítsuk meg a contains() metódust, hogy meghatározzuk tartalmazza-e az
egyenes szakasz az adott pontot:
public boolean contains(Point a) { if (!isWithin(a.getX(), _p.getX(), _q.getX())) {
return false; }
if (!iswithin(a.getY(), _p.getY(), _q.getY())) { return false;
}
if (_slope.isvertical()) { return true;
}
return a.getY() .l
solvev(a.getx());
Készítsük el azt a metódust, mely meghatározza az egyenes szakaszon fekvő pont y
koorclinátáját az adott x koordináta segítségéve!:
private double solvev(double x) { return _slope.asoouble() * x + base();
}
Készítsünk egy eljárást arra, hogy az y = mx + b egyenletben meghatározzuk a b értékét:
private double base() { return _p.getY() - _slope.asoouble() * _p.getX();
}
Hozzunk létre egy egyszerű alkalmazást arra, hogy meghatározzuk egy adott szám
ról, hogy benne van-e másik két szám által meghatározott tartományban:
private static boolean iswithin(double test, double boundl, double bound2) {
return test >= Math.min(boundl, bound2) && test <= Math.max(boundl, bound2);
}
Most alkossuk meg azt a metódust, mely meghatározza két egyenes szakasz met
széspontját:
514
A metszéspont meghatározása
public Point i ntersectiOriPOi nt(Cine li ne)-{ if (isParallelTo(line)) {
l
return null; }
double x double y
getintersectionxcoordinate(line); getintersectionvcoordinate(line, x);
Point p= new Point(x, y);
if (line.contains(p) && this.contains(p)) { return p;
}
return null;
Az előző kód kiegészítéseként hozzunk létre egy olyan metódust, amely meghatá
rozza két egyenes szakasz elméleti metszéspontjának x koordinátáját:
private double getintersectionxcoordinate([ine line)- { if (_slope.isvertical()) {
}
return _p.getx(); }
if (line._slope.isvertical()) { return line._p.getX();
}
double m = _slope.asoouble(); double b= base();
double n double c
line._slope.asoouble(); line.base();
return (c - b) l (m - n);
Végezetül készítsünk egy olyan eljárást, mely meghatározza a metszéspont y koordi
nátáját:
privatedoub l e getintersect'i onvcoor if (_slope.isvertical()) {
return line.solvev(x); }
return solveY(x); l
515
Számítógépes geometria
A megvalósitás működése
A L i ne osztálynak három példánytagja van: két Po i nt objektum, amely a végpon�ait adja meg, és egy sl ope objektum, amely egységbe zárja a meredekségét. A L i ne objektum múködésének nagy részét ezek az egységbe zárt objektumok szolgáltatják. Ha például meg szeretnénk határozni, hogy egy egyenes szakasz párhuzamos-e egy másik egyenes szakasszal, egyszerűen csak azt kell meghatároznunk, hogy a hozzájuk tartozó meredekségek azonosak-e.
Ha azt szeretnénk megállapítani, hogy egy pont egy egyenes szakaszra esik-e, akkor azt nézzük meg, hogy a pont x koordinátája abba az x koordinátatartományba esik-e, melyet az egyenes szakasz végpontjai határoznak meg. Ha nem, akkor az egyenes szakasz biztosan nem tartalmazha�a a pontot. Ezek után megismételjük az eljárást az egyenes szakasz y koordinátáira is. Ha mindezt végrehajtottuk, már tudhatjuk, hogy az adott pont lehet-e az egyenes szakasz egyik pon*. Abban az esetben, ha az egyenes szakasz függőleges, következtethetünk rá, hogy a pont az egyenes szakaszon van. Ugyanakkor a 18.14. ábrán látható példa azt muta�a, hogy létezhet olyan pont, mely átmegy az összes teszten, még sincs rajta az egyenes szakaszon.
2 3 4 5
18.14. ábra. Egy pont, mefy nem része az egyenes szakastJ�ak, de ofyan x;y koordinátákkal
rendelke'(jk, amefyeket áifog a szakasz
Egy végső ellenőrzéssei megvizsgáljuk, hogy a pont koordinátái behelyettesítve kielégítik-e az egyenes egyenletét (y = mx + b). Ezért meg kell hívnunk a sol vev() eljárást. Megadva az x koordinátát, kiszámítja a hozzá tartozó y koordinátát. Ha a pont koordinátája helyesen értékelődik ki, akkor a pont az egyenes szakaszon fekszik.
Most értünk el a dolog lényegéhez: két egyenes szakasz metszéspontjának meghatározásához. Az alapgondolat a következő: ha az egyenes szakaszok párhuzamosak, nincs metszéspont; ha nem, előbb meghatározzuk az (elméleti) metszéspont x
koordinátáját, majd ezt az értéket felhasználva az y koordinátáját. Végül ellenőriznünk kell, hogy mindkét egyenes szakasz tartalmazza-e az elméleti metszéspontot, mielőtt visszatér vele.
Hogy meghatározzuk a metszéspont x koordinátáját, először meg kell állapítanunk a kérdéses egyenes szakaszokról, hogy függőlegesek-e. Ha bármelyik is az, akkor a válasz az illető egyenes szakasz bármely végpontjának x koordinátája. Ha nem, használjuk a korábbi képietet a meghatározására.
516
A legközelebbi pontpár meghatározása
A végső metódus meghatározza a metszéspont y koordinátáját. Ismét külön figyelnünk kell arra az esetre, ha valamelyik egyenes szakasz függőleges (nem kell ezt tennünk, hamindkettő az). Ez azt jelenti, hogy egyszerűen csak egy nem függőleges egyenes szakaszt kell használnunk a metszéspont y koordinátájának meghatározásához.
Ha lefutta�uk a vizsgálatot, azt tapasztaljuk, hogy mindegyik műköclik. Most már rendelkezünk absztrakt halmazok egy olyan osztályával, amely a geometriai fogalmakat reprezentálja néhány jól tesztelt és értékes működéssel. Így hát továbbléphetünk a következő kilúváshoz, melynek során egy ponthalmazban meghatározzuk az egymáshoz legközelebb eső pontpárt.
A legközelebbi pontpár meghatározása
Képzeljünk el egy nagy halmaz szórt pontot, amilyet a 18.15. ábrán láthatunk.
4 1 • •
• • 3
2 • •
• -r
-5 -4 -3 -2
_,r 2 3
4 5
• -1 • • -2
•
.18.15. ábra. Számosszórtpont
Megtaláljuk-e az egymáshoz legközelebb lévő pontpárt? Azt gondolhatnánk, ez nagyon egyszerű - csak össze kell hasonlítanunk minden pontot minden másik ponttal, kiszámítani az egymástól való távolságukat, majd csak azt a pontpárt tartjuk fejben, amelynek a legkisebb a távolsága. Mire ez működne, már biztosan kiütéseket kapnánk a letámadásos megoldásoktól, melyek sebessége O(N2), rnivel minden elemet minden másik elemmel dolgozna fel. Nem is törődünk a feladat ilyen naiv megoldásával. Ehelyett a következőkben a síkbejárási algoritmus nevű eljárásra koncentrálunk.
A síkbejárási algoritmus balról jobbra áttekinti az egyes pontokat a koordinátarendszerben. Egyetlen menetet vagy pásztázást hajt végre a pontokat tartalmazó kétdimenziós síkon úgy, hogy az aktuálisan ismert legkisebb távolságra és azokra a pontokra koncentrál, melyek közt ez a rninimális távolság van.
517
Számítógépes geometria
Könnyebb megértenünk az algoritmust, ha egy félig feldolgozott példát tekintünk. A 18.16. ábrán az algoritmusnak azt az állapotát látha�uk, amikor az ötödik pontot (balról) dolgozzuk fel (az x és az y tengelyt eltávolítottuk, hogy ne legyen sűrű a diagram) .
•
•
•
•
Söprés iránya
� •
•
Vonóháló
18.16. ábra. A síkbejárási algoritmus munka kiizben
Az ábra alapján megjegyezzük, hogy az éppen megtalált legközelebbi pontpár távolsága d. A pásztázásban aktuálisan vizsgált pontot úgy kezeljük, mintha egy vonóhálóként kezelt téglalap jobb oldala lenne. A vonóhálóval kapcsolatban kulcsfontosság megjegyeznünk, hogy a szélessége is d - azaz készítünk egy képzeletbeli dobozt az adott pont mögött, amely nem szélesebb, mint a legközelebbi pontpár közötti távolság. Lehet, hogy ez most egyszerre tömény volt, de a későbbiekben sokkal tisztább lesz.
Az alapödet az, hogy ha a vizsgált pont egy olyan pontpár tagja (egy bal oldali pontjával), amely közelebbi pontpár, mint az aktuálisan legközelebbi pár, akkor a következő pontnak abban az új párban benne kell feküdnie a vonóháló ban. Ha ez nem igaz, akkor nehezen létezhetne az aktuálisan ismert párnál közelebbi pontpár. Ezért az algoritmus ellenőrzi a söprés alatti pont távolságát a vonóhálón belüli pontokkal, hogy meghatározza, ha valamelyik kombináció közelebbi párt alkot, mint az aktuálisan ismert. Ha közelebbi párt talál, az algoritmus kisebb vonóhálóval fut tovább, miután minden pontot megvizsgált. Így a pontok eloszlásának függvényében viszonylag kevés összehasonlításra van szükség. Az algoritmus egy sokkal fejlettebb változatában képes figyelmen kívül hagyni azokat a pontokat, melyek y irányú távolsága a vizsgált ponttól nagyobb, mint d, még tovább csökkentve az összehasonlítások számát.
A 18.17. ábrán azt a helyzetet láthatjuk, amikor majdnem az összes pont fel van dolgozva.
A síkbejárási algoritmus elkészítése során két szempontot kell figyelembe vennünk. A pontokat az x koordinátáik szerint, azonos x koordináta esetén az y koordinátáik szerint sorba kell rendeznünk. Ez olyan összehasonlító eljárás megírását igényli, amely beilleszthető a rendező algoritmusba. A következő gyakorlati részben ennek az összehasonlítónak a vizsgálatát írjuk meg.
518
•
•
d ...-
•
•
•
A legközelebbi pontpár meghatározása
•
Söprés iránya
•
Vonóháló
18.17. ábra. A majdnem befgeződiitt algoritmus
Gyakorlófeladat: az XY pont összehasonUtá vizsgálata
Először egy olyan vizsgálatot írunk meg, amely ellenőrzi, hogy jól kezeli-e a kompa
rátor az azonos pontokat - azaz az eredmény nulla:
'�pa'CI<ageéöm o w r ox o al gorithms. geomet ry;
import junit.framework.Testcase;
public class XYPointcomparatorTest extends Testcase { private final XYPointcomparator _comparator
XYPointcomparator.INSTANCE;
public void testEqualPointsComparecorrectly() { Point p= new Point(4, 4);
}
Point q= new Point(4, 4);
assertEquals(O, _comparator.compare(p, q)); assertEquals(O, _comparator.compare(p, p));
} .�
Egy olyan ellenőrzésre is szükségünk van, amely biztosít róla bennünket, hogy a
pontok az x koordinátájuk szerinti sorrendben vannak, ahogy várjuk. Ezt úgy hajtjuk
végre, hogy veszünk három pontot, és megvizsgáljuk az egymáshoz mért relatív tá
volságukat, ahogy azt itt láthatjuk:
public voi d testXCooréFfnateisPr. imaryKey()-{ Point p= new Point(-1, 4); Point q= new Point(O, 4); Po i nt r = new Po i nt (1" _1)_;
519
Számítógépes geometria
}
assertEquals(-1, _comparator.compare(p, q)); assertEquals(-1, _comparator.compare(p, r)); assertEquals(-1, _comparator.compare(q, r));
assertEquals(1, _comparator.compare(q, p)); assertEquals(l, _comparator.compare(r, p)); assertEquals(l, _comparator.compare(r, q));
Végezetül olyan vizsgálatra van szükség, amely megmondja, számításba vannak-e véve a pontok y koordinátái olyankor, amikor az x koordinátáik megegyeznek. Álljon itt a vizsgálat kódja:
public void Point p Point q Point r
testvcoordinateissecondaryKey() { new Point(4, -1); new Point(4, O); new Point(4, l);
}
assertEquals(-1, _comparator.compare(p, q)); assertEquals(-1, _comparator.compare(p, r)); assertEquals(-1, _comparator.compare(q, r));
assertEquals(l, _comparator.compare(q, p)); assertEquals(l, _comparator.compare(r, p)); assertEquals(l, _comparator.compare(r, q));
A megvalósitás működése
Olyan összehasonlító eljárásta van szükségünk, amely x koordinátájuk szerint sorba rendezi a pontokat. Ez természetesen azt jelenti, hogy a negatív x koordináták hamarabb szerepelnek, mint a pozitívak, de mi a helyzet akkor, ha két pontnak azonos az x
koordinátája? Valamilyen módon sorba kell rendeznünk, í� azt v�asz�uk, hogy ebben az esetben az y koordináták szerint. rendezzük sorba. Elgondolkodhatunk azon, hogyan kezeljük azokat a pontokat, melyeknek az x és az y koordinátájuk is azonos. A válasz, hogy ez nem engedhető meg, egyszerűen a Set-et használva a vizsgálandó pontok tárolására. Mint tudjuk, a Set szemantikája nem engedi meg a dupla tételeket, így a Po i nt objektumokat, ha mindkét koordinátájuk azonos, egyenlőknek tekin�ük.
Az előző vizsgálat elegendően sok esethalmaz létrehozásával működik, így biztosítva az összehasonlító eljárás elvárt működését. Ennek már elégnek kell lennie ahhoz, hogy az összehasonlító eljárásunk működjön. A következő részben ezt fogjuk megírni.
520
A legközelebbi pontpár meghatározása
Gyakorlófelada t: XYPointComparator megvalósítása
Első lépésként deklaráljunk egy példányt és egy privát konstruktort, mivel az objek
tumnak nincs saját állapota:
pacl<age com. wrox:al goritnms. geometry;
import com.wrox.algorithms.sorting.comparator;
public final class XYPointcomparator implements Comparator { public static final XYPointcomparator INSTANCE
}
new XYPointComparator();
private XYPointcomparator() { }
Valósítsuk meg a compare() eljárást, amely erősen típusossá delegálódik azáltal,
hogy paraméterei Po i nt típusú objektumokká vannak kasztolva:
pubiic-int compare(Öbject lef�bject right) throws classeastException {
return compare((Point) left, (Point) right); l
Végezetül készítsük el a compare() erősen típusos változatát:
public-int ci>mpare(Poi�Point qy tnrowsc lasscastExceptiOil{ int result =
}
new oouble(p.getX()).compareTo(new Double(q.getx())); if (result != O) {
return result; } return new oouble(p.getY()).compareTo(new oouble(q.getY()));
A megvalósitás működése
Ne lepődjünk meg azon, hogy az összehasonJitó eljárás maga kevesebb sorból áll, mint
a hozzá tartozó egységteszt - ez teljesen normális! A:z egyetlen megvalósítandó metó
dus a compare (), amely erősen típusossá delegálódik azáltal, hogy paramétereit Po i nt
típusú objektumokká kasztolja. Ha nem pontok objektumait adjuk át, viszont a compa
rator által explicite engedélyezett, akkor cl asscastExcepti on-t fogunk kapni.
A Point osztály objektumaiként ismert compare() eljárás létrehozása tartalmaz
za az igazi logikai részeket. A visszatérési érték a saját objektumok x koordinátáitól
függ, az y koordinátákat csakis abban az esetben vesszük számításba, ha az x koor
dináták megegyeznek.
521
Számítógépes geometria
Ha elkészültünk az összehasonlító eljárásunkkal, megvalósítha�uk magát a sík
bejárási algoritmust, ezt a következő feladatban tesszük meg. Feltételezhetjük, hogy
létezik más módszer a legközelebbi pontpár problémájának megoldására (nézzük
meg a fejezet végi feladatokat), így olyan absztrakt vizsgálatokat készíthetünk, amely
többféle megvalósítás vizsgálatához is használható, majd kiegészíthe�ük az algorit
musunk speciális változata számára.
Gyakorlófeladat: A sikbejárási algoritmus vizsgálata
Definiáljunk egy absztrakt osztálygenerátor-metódust, hogy adott megvalósítások
nak lehetővé tegyük, hogy példányosítsák a megfelelő algoritmusosztályt:
package com. w ro x. a l go ri thms. geometry;
import com.wrox.algorithms.sets.Listset; import com.wrox.algorithms.sets.set; import junit.framework.Testcase;
public abstract class AbstractclosestPairFinderTestcase extends Testcase {
protected abstract closestPairFinder createclosestPairFinder();
}
Az első vizsgálatban azt nézzük meg, hogy ha üres ponthalmazt szolgáltatunk, a
visszatérési érték nu ll lesz:
public void testEmptySetOfPoints() {
}
closestPairFinder fínder = createClosestPairFinder(); assertNull(finder.findclosestPair(new Listset()));
Elég nehéz megtalálni a legközelebbi párt, ha egyetlen pon!Ullk van, így ebben az
esetben is n u ll a visszatérési érték:
publ i c voi d testASi ngl ePo i ntReturnsNull-0 {
closestPaírFinder fínder = createclosestPairFínder();
}
Set points = new Lístset(); points.add(new Point(l, l));
assertNull(finder.fíndClosestPaír(points));
A következő eset nyilvánvalóan csak akkor következik be, amikor két pont van
megadva a bemeneti halmazban. Ebben az esetben egyszerű meghatározni a legkö
zelebbi pontokat, amelyeket az alábbi kóddal vizsgálhatunk
522
A legközelebbi pontpár meghatározása
public voilf testAsingleP ai rofÍ>oi nts()-{ closestPairFinder finder: createclosestPairFinder();
},
set points: new Listset(); Point p: new Point(l, l); Point q: new Point(2, 4);
points.add(p); points.add(q);
set pair: finder.findclosestPair(points);
assertNotNull(pair); assertEquals(2, pair.size()); assertTrue(pair.contains(p)); assertTrue(pair.contains(q));
Most egy érdekes esethez értünk. Képzeljük el, hogy három pont fekszik egy egye
nesen, egyenlő távolságban. Melyik pár lehet a legközelebb? Azt várjuk az algorit
mustól, hogy az első párt vegye, amelyet a végigsöprés közben megtalált, ez pedig at
tól az összehasonlitó eljárástól függ, amely a pontokat sorba rendezi. Hogy erről
megbizonyosodjunk, végezzük el a következő vizsgálatot:
pul:ili c voi a testThreePofnfSEqÜallySpaceaAparf()�· { closestPairFinder finder = createclosestPairFinder();
}
set points = new Listset(); Point p= new Point(l, 0); Point q= new Point(l, 4); Point r= new Point(l, -4);
points.add(p); points.add(q); points.add(r);
set pair = finder.findclosestPair(points);
assertNotNull(pair); assertEquals(2, pair.size()); assertTrue(pair.contains(p)); assertTrue(pair.contains(r));
Hasonló eset következik be akkor, ha nagy ponthalmazunk van, melyben két párnak
azonos a távolsága. Ismét azt várjuk az algoritmustól, hogy azt a párt vegye figye
lembe, amelyiket először megtalált, így a következő teszttel ezt vizsgáljuk:
523
Számítógépes geometria
public void testLargesetOfPointswithTwoEquaÍshortestPairs() { ClosestPairFinder finder = createclosestPairFinder();
}
set points = new Listset();
points.add(new Point(O, 0)); points.add(new Point(4, -2)); points.add(new Point(2, 7)); points.add(new Point(3, 7)); points.add(new Point(-1, -5)); points.add(new Point(-5, 3)); points.add(new Point(-5, 4)); points.add(new Point(-0, -9)); points.add(new Point(-2, -2));
Set pair = finder.findclosestPair(points);
assertNotNull(pair); assertEquals(2, pair.size()); assertTrue(pair.contains(new Point(-5, 3))); assertTrue(pair.contains(new Point(-5, 4)));
Végül absztrakt teszteseteinket kiterjeszthetjük úgy, hogy egy olyan változatot készítünk, amely a saját síkbejárási algoritmusunkra specializálódott, ahogy az alábbiakban látjuk:
package com.wrox.algorithms.geometry;
public class PlanesweepclosestPairFinderTest
}
extends AbstractclosestPairFinderTestcase { protected closestPairFinder createclosestPairFinder() {
return PlanesweepclosestPairFinder.INSTANCE;
}
A megvalósitás müködése
Jvlint ahogy sok vizsgálatban, a megelőző kód számos nem szokásos esettel működik, például üres ponthalmazzal, egyelemű ponthalmazzal, kételemű ponthalmazzal és olyan pontokkal, melyek távolsága azonos. Időnként a tesztesetek száma több is lehet, mint amit várnánk, de ez a megoldandó probléma bonyolultságát jelzi. Önmagában minden egyedi tesztmetódus egyszerű.
A következő Gyakodófeladat részben megírjuk az algoritmust és lefutta�uk az összes ilyen vizsgálatot rajta.
524
A legközelebbi pontpár meghatározása
Gyakorlófeladat: A ClosestPairfinder interfész létrehozása
Az algoritmusunkat definiáló interfész elég egyszerű. Egyetlen eljárás van, amely Po i nt
objektumok halmazát várja, és visszatér egy másik Set-tel úgy, hogy az két Point ob
jektumot tartalmaz, melyek az eredeti ponthalmaz legközelebbi pontpárját alko�ák.
Visszatérhet null értékkel is abban az esetben, ha nem lehetséges a legközelebbi pár
meghatározása (például ha egyetlenPoint van megadva).
package com.wrox.algoritnms.geometry;
import com.wrox.algorithms.sets.set;
public interface closestPairFinder { public set findclosestPair(Set points);
}
Gyakorlófeladat: a sikbejárási algoritmus megvalósítása
Készítsük el az osztály deklarációját egy bináris beszúróval, amely lehetővé teszi,
hogy a pontok set-jét egy rendezett L i st-be alakítsuk:
package com.wrox.algorithms.geometry;
import com.wrox.algorithms.bsearch.IterativeBinaryListsearcher; import com.wrox.algorithms.bsearch.Listinserter;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList;
import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sets.Listset; import com.wrox.algorithms.sets.set;
public final class PlanesweepclosestPairFinder implements closestPairFinder {
public static final PlanesweepClosestPairFinder INSTANCE = new PlanesweepclosestPairFinder();
private static final Listinserter INSERTER = new Listinsertere new IterativeBinaryListSearcher(XYPointComparator.INSTANCE));
}
private PlanesweepClosestPairFinder() {
}
525
Számítógépes geometria
Az algoritmus, mely a legközelebbi pontpárt határozza meg, az alábbi:
public set findclosestPair(Set points) { assert points != null : "A 'points' (ponthalmaz) nem lehet NULL";
if (points.size() < 2) { return null;
}
List sortedPoints = sortPoints(points);
Point p Point q
(Point) sortedPoints.get(O); (Point) sortedPoints.get(l);
return findclosestPair(p, q, sortedPoints);
Készítsük el az alábbi eljárást (később részletezve):
private Set findclosestPair(Point p, Point q, List-sortedPoints) { set result = createPointPair(p, q);
}
double distance = p.distance(q); int dragPoint = O;
for (int i =2; i < sortedPoints.size(); ++i) { Point r= (Point) sortedPoints.get(i);
}
double sweepx = r.getX(); double dragx = sweepx - distance;
while (((Point) sortedPoints.get(dragPoint)).getX() < dragX) { ++dragPoint;
}
for (int j= dragPoint; j< i; ++j) {
}
Point test= (Point) sortedPoints.get(j); double checkoistance = r.distance(test); if (checkDistance < distance) {
distance = checkoistance; result = createPointPair(r, test);
}
return result;
Az előző kód az alábbi eljáráson alapul, hogy a pontokat x és y koordinátáik szerint
rendezzük, felhasználva az összehasonlító eljárást, melyet korábban e fejezetben de
finiáltunk:
526
A legközelebbi pontpár meghatározása
p ri \rate "stat i c .. L i st sortPoi n ts (Set poi nts) .. { assert points != null : "A 'points' (ponthalmaz) nem lehet NULL";
l
List list = new ArrayList(points.size());
Iterator i = points.iterator(); for (i. first(); l i. i soone(); i .next()) {
INSERTER.insert(list, i.current()); }
return list;
A legutóbbi egyszerű segédmetódus létrehoz egy set-et, hogy képviselje a legköze
lebbi párt, két Point objektumot szolgáltatva:
private s·e· f Cr·eatePoi"nt-Pai ·r(Poi nt· p;··· Po i nt q)"-{ set result = new Listset(); result.add(p); result.add(q); return result;
J
A megvalósitás müködése
Ez a síkbejárási algoritmus megvalósí�a a cl osestPai r Fi n der interfészt, melyet az
előző részben tárgyaltunk. Ez szingletonként is megvalósítható egy privát kon
struktor segítségéve!, mivel nincs saját állapota.
Korai kilépés történik abban az esetben, ha nincs annyi pont, hogy legalább egy
pár legyen. A pontokat koordinátáik szerint csoportosítjuk. Miután van egy rende
zett listánk, kiválaszthatjuk az első két Po i nt objektumot, és először tegyük fel, hogy
ezek a legközelebbi pontok. Majd átadhatjuk egy másik eljárásnak, amely végigsöpri
a maradék pontokat, meghatározva, van-e a kezdő párnál közelebbi pontpár.
A következő eljárás a síkbejárási algoritmus lelke. Valamivel bonyolultabb, mint
a többi eljárás ebben az osztályban, ezért ezt figyelmesen végignézzük A 18.16. és
18.17. á b rák alapján, melyek az algoritmust ábrázolják, ellenőrizhetjük, megfelelően
értjük-e a működését:
private set findclosestPair(Point p, Point q, List sortedPoints) { set result = createPointPair(p, q); double distance = p.distance(q); int dragPoint = O;
for (int i = 2 ; i < sortedPoints.size(); ++i) { Point r = (Point) sortedPoints.get(i); double sweepx = r.getX(); double dragx = sweepx - distance;
527
Számítógépes geometria
}
}
while (((Point) sortedPoints.get(dragPoint)).getX() < dragx) { ++dragPoint;
}
for (int j= dragPoint; j< i; ++j) {
}
Point test = (Point) sortedPoints.get(j); double checkoistance = r.distance(test); if (checkDistance < distance) {
distance = checkoistance; result = createPointPair(r, test);
}
return result;
Jegyezzük meg az alábbiakat, amikor a kócira nézünk:
• result (eredmény) a Point objektumokat tartalmazza, melyek a legköze
lebbi párt alkotják.
• d i stance (távolság) megadja az aktuálisan megtalált távolságot a legköze-
lebbi pár között. Természetesen ez a vonóháló szélessége is.
• dragpoint (vonópont) a balról az elsőPoint indexe a vonóhálón belül.
• sweepx (xsöprés) a Point x koordinátája, mely söprés alatt van.
• dragx a vonóháló bal szélének x koordinátája.
Ez az algoritmus a rendezett lista az első két pon�át kihagyja, a söprést így a
harmadik ponttól kezdi, mivel az első kettőt már felhasználtuk ahhoz, hogy megha
tározzuk az induló legközelebbi pontpárt. Ezek után azokat a vonóháló mögé csú
szott pontokat is kihagyja úgy, hogy lépteti a dragpoint változót. Végül megvizsgál
ja a söprési vonal aktuális pontjának távolságát a vonóhálón belüli minden egyes
ponttól, közben frissíti a legközelebbi pár eredményét és a köztük lévő távolságot,
ha egy közelebbi párt talál, mint ami éppen defmiálva van.
Ezzel befejeztük síkbejárási algoritmusunk megvalósítását. Ha inost az algoritmus
hoz létrehozott összes vizsgálatot lefutta�uk, látha�uk, hogy mindegyik jól szerepel.
528
Összefoglalás
Összefoglalás
• Ebben a fejezetben összefoglaltunk a kétdimenziós geometriában használt né
hány fogalmat, többek között a koordináta-rendszert, pontokat, egyeneseket és
háromszögeket.
• Két geometriai problémát tárgyaltunk részletesen: két egyenes metszéspon�át hatá
roztuk meg, és megtaláltuk egy ponthalmaz két egymáshoz legközelebbi pon�át.
• Ezeket a megoldásokat részletesen vizsgáltJava-kód formájában megvalósítottuk.
A számítógépes geometriának épp csak megkapargattuk a felszínét, pedig ez egy
csodálatos terület, amelybe beletartozik a háromszögelés (ez az alapja a GPS hely
meghatározó rendszernek), a 3D grafika és a számítógéppel támogatott tervezés.
Reméljük, az érdeklődés t azért sikerült felkelteni iránta.
Gyakorlatok
1. Valósítsuk meg a legközelebbi pár problémájának letámadásos megoldását!
2. Optimalizáljuk a síkbejárási algoritmust úgy, hogy a függőleges síkban túl mesz
sze lévő pontokat figyelmen kívül hagyjuk!
529
TIZENKILENCEDIK FEJEZET
Pragmatikus optimalizálás
Egyesek számára talán meglepő, hogy az optimalizálásról szóló fejezet a könyv végére került. Ezzel azt szetettük volna érzékeltetni, hogy alkalmazások fejlesztésekor nem az optimalizálás a legfontosabb szempont. Ebben a fejezetben megismerkedünk az optimalizálás szerepével, többek közt azzal, hogy mikor és hogyan érdemes alkalmazni, valamint bemutatunk néhány gyakorlati fogást, amelyekkel jelentős teljesítményjavulást érhetünk el programjainkban. Fejlesztéskor elsősorban a program áttekinthetőségére és hibátlanságára törekedjünk, az optimalizálási folyamatot pedig csak tényekre alapozzuk Így mérhetővé válik a folyamat, és képesek leszünk felismerni, ha az optimalizálási munka már nem eredményez jelentős javulást.
A fejezetből a következőket tanulhatjuk meg:
• milyen szerepe van az optimalizálásnak a fejlesztési folyamatban,
• mi a profilírozás, és hogyan működik,
• hogyan profilírozhatjuk alkalmazásunkat a Java virtuális gép szabványos eszközeivel,
• hogyan profilírozhatjuk alkalmazásunkat az ingyenesen hozzáférhető J ava Memory Proftler eszköz segítségéve!,
• hogyan ismerjük föl a processzor- és a memóriafelhasználással összefüggő teljesítményproblémákat,
• hogyan érhetünk el hatalmas teljesítménynövekedést a kód néhány apróbb stratégiai változtatásával.
Az optimalizálás szerepe
Az optimalizálás lényeges része ugyan a szoftverfejlesztésnek, mégsem olyan fontos, mint esetleg hinnénk. Tanácsos megismerkedni az általunk használt fejlesztői környezetre jellemző, programunk teljesítményét érintő problémák típusaival, és programozás közben folyamatosan fejben tartani őket. A Java-nyelvben például hosszú karaktersorozatok kezelésére érdemesebb StringBuffer objektumot használni, mint String objektumok konkatenációját, ezért ha így adódik, tegyük is ezt.
Pragmatikus optimalizálás
Veszélyes vizekre evezünk azonban, ha emiatt megváltoztatjuk programunk felépítését. Szaknyelven ezt idő előtti optimalizálásnak nevezzük. Akkor járunk el helyesen, ha a programunk fejlesztésekor a program érthetőségét tekintjük elsődlegesnek, és nem áldozzuk fel azt a gyorsaság kedvéért. A tapasztalatok szerint ugyanis alkalmazásunk teljesítménybeli szűk keresztmetszete mindig ott alakul ki, ahol legkevésbé számítunk rá. Ezért kizárólag a kész rendszer viselkedésének mérésekor találhatjuk meg a valódi problémákat, ezek orvoslásával pedig jelentős teljesítményjavulást érhetünk el. Arra egyszerűen nincs szükség, hogy az egész kód optimalizált legyen. Elég, ha csak a kritikus úton lévő programkód miatt aggódunk. Az pedig, hogy mely kódrészlet lesz az, még akkor is meglephet bennünket, ha magunk írtuk a programot. Itt jön a képbe a profilírozás, amelyet a következőkben tárgyalunk.
A legfontosabb, hogy észben tartsuk: egyszerű és áttekinthető felépítésű kódot sokkal könnyebb optimalizálni, mint azt, amelyet a fejleszták programozás közben már optimalizálni próbáltak. Az is nagyon fontos, hogy az elejétől fogva a megfelelő algoritmust használjuk. Optimalizálás előtt mindig tisztában kell lennünk megvalósításunk teljesítményprofiljával, azaz tudnunk kell, hogy algoritmusunk O(N), O(log N)
stb. bonyolultságú-e. A nem megfelelő algoritmus kiválasztása át nem léphető korlátot fog jelenteni a későbbi optimalizálás során. Ez újabb ok arra, hogy a könyv végére kerüljön ez a fejezet.
A tapasztalatok szerint a program első változatának teljesítménye valószínűleg még fokozható. Sajnos azonban a tapasztalat alapján az is valószínű, hogy - a triviális alkalmazások kivételével- magunktól nem jövünk rá, pontosan mi okozza a teljesítmény csökkenését. Programok írásakor ezért inkább a megfelelő, ne pedig a gyors működésre törekedjünk. V alójában érdemes az egész fejlesztési projektben különválasztani ezt a két folyamatot. Mostanra megtanultuk, hogy a programok megfelelő működését teszteléssei biztosítha�uk. A gyors működéshez pedig az ebben a fejezetben foglaltakat javasoljuk Teszteléssei a teljesítményjavítás érdekében végzett változtatások hatása is nyomon követhető, így biztos, hogy programunk működése megfelelő marad. Ahogy az egységtesztek is garantálják programunk helyes működését, az ebben a fejezetben bemutatott technikák is biztosí�ák majd az optimális teljesítményét.
Szerencsére a programok többségénél csupán néhány jelentősebb szűk keresztmetszet okozza a teljesítménycsökkenést, amelyek azonosításával megoldható a probléma. Programunkban így általában csak kevés helyen kell változtatui az eredeti kódon. Az itt bemutatott módszerek segítségével könnyen megtalálhatjuk és kijavíthatjuk a kérdéses programkódot, majd meggyőződhetünk róla, hogy valóban a várt eredményt értük-e el. Azt javasoljuk, hogy ne próbáljuk meg kitalálni, hogyan növelhetnénk kódunk teljesítményét, és ne is hallgassunk ösztöneinkre optimalizáláskor. Ahogy nem ajánlott átszervezni programunkat automatizált egységtesztek elvégzése nélkül, optimalizálni sem tanácsos automatizált teljesítménymérés nélkül.
532
A profilírozásról
Ha egy program nem triviális idő alatt fut le, biztos, hogy teljesítményét szűk ke
resztmetszet gátolja. Nem szabad elfelejtenünk, hogy ez akkor is így van, ha a program
már megfelelő sebességgel fut. Az optimalizálási folyamat célja objektív teljesítmény
feltételek elérése, nem pedig valamennyi teljesítménybeli szűk keresztmetszet felszá
molása. Ne tűzzünk ki magunk elé elérhetetlen célokat! Sehogy sem fogunk tudni át
préselni 2 MB-os fényképet 3 másodperc alatt 56K-s modemen! Az optimalizálás le
gyen teljesítményjavító eszköztárunk része, nem pedig az egyetlen módja alkalmazá
sunk gyorsításának A tervezési tudás, valamint az algoritmus- és az adatstruktúra
megválasztáshoz szükséges kompromisszumok ismerete sokkal fontosabb ennél.
A profilírozásról
A profilírozás a program viselkedésének mérését jelenti. A Java kiválóan alkalmas
profilirozásra, hiszen - ahogy a fejezetben később látni fogjuk - a virtuális gépet
eleve felszerelték profilírozó eszközökkel. A profilirozás egyéb nyelvekben nem fel
tétlenül ilyen egyszerű, azonban így is népszerű módszer. A programok profilírozá
sakor három főbb területet mérünk: a processzorkihasználást, a memóriafelhaszná
lást és az egyidejűséget.
Az egyidqűség nem tárg;ya fqezetünknek, ezért ha a tisifelt Olvasónak további itiformá
cióra van sifiksége, kérjük, nézze meg az ajánlott irodalmat az A fiiggelékben.
A processzorkihasználást a profilírozó úgy méri, hogy meghatározza, a program fu
tása alatt mennyi időbe telik egy-egy metódus végrehajtása. Fontos tudnunk, hogy
ezeket az adatokat a profilírozó a virtuális gépben futó egyes szálak végrehajtási
vermeinek megszabott időközönkénti mintavételezésével nyeri, így állapítja meg,
hogy egy-egy pillanatban mely metódusok aktívak. A hosszabb ideig futó programok
mérése pontosabb eredményre vezet. Ha programunk túl gyors, elképzelhető, hogy
pontatlan eredményt kapunk a mérés során. Bár ha a programunk úgyis ilyen gyors,
valószínűleg semmi szükség optimalizálásral
A profilírozó a következő statisztikákat szolgáltatja:
• egy-egy metódust hányszor hívott meg a program,
• az egyes metódusoknak mennyi volt a processzoridő-igénye,
• egy-egy metódusnak és az általa meghívott összes metódusnak összesen
mennyi volt a processzoridő-igénye,
• a futásidő megoszlása metódusonként.
533
Pragmatikus optimalizálás
Ezekkel a statisztikákkal már képet kaphatunk róla, hogy a kód mely részét lehet optimalizálással javítani. A memóriafelhasználás mérésére ehhez hasonlóan a profilírozó adatokat gyűjt az általános memóriahasználatról és a szemétgyűjtésről. Ezekből az adatokból a következő statisztikát állítja elő:
• az egyes osztályoknak hány objektumát példányosította a program,
• az egyes osztályoknak hány példányát szabadította fel a szemétgyűjtés,
• az egyes virtuális gépekhez mennyi memóriát rendelt az operációs rendszer (halomméret),
• a halmok közül mennyi volt szabad és mennyi volt használatban a program futása során.
Ezeknek az adatoknak köszönhetően alaposabban megismerhetjük programunk futásidejű viselkedését. Ahogy később egy példaprogram optimalizálásán keresztül látni fogjuk, gyakran meglepő eredményekre juthatunk belőlük. A profilírozó által szolgáltatott adatok alapján elvégezhetjük az optimalizálási munkát.
A következő részben megnézzük, hogyan optimalizáljuk Java-programunkat két különböző technikávaL Az első módszerben a Java virtuális gép beépített optimalizáló eszközeivel dolgozunk. Ezek ugyan egyszerű, de könnyedén hozzáférhető eszközök. A második módszerben a J ava Memory Proftler QMP) néven ismert nyílt forrású eszközt mutatjuk be. Bár ez utóbbi sokkal több hasznos információval lát el bennünket, valamint könnyen áttekinthető grafikus felülettel is segíti munkánkat, használatához előbb le kell tölteni, és telepíteni kell.
A következő részben a profilírozás során optimalizálandó példaprogramot ismerjük meg.
A FileSortingHelper példaprogram
Az optimalizálást a kifejezetten e célra kitalált példaprogramon fogjuk végezni. A program egy egyszerű Unix-szerű fJ..ltert valósít meg, amely a szabványos bemenetről fogadja a bemenetet, soronként egy
. szót, majd rendezés után a szabványos kimenetre írja a rendezett szavakat. Hogy kicsit érdekesebbé tegyük, az összehasonlító az eredeti szavakat úgy teszi ábécérendbe, mintha azok visszafelé lennének írva. Így például az
"ablak" szó a
"pite" után kerülne, hiszen ábécérendben a "kalba" az
"etip" után jön. Ennek egyedüli célja, hogy a program működését kicsit megnehezítsük, valamint érdekesebbé tegyük az optimalizálást. Ne aggódjunk, ha nem látjuk értelmét, mert valójában semmi célja nincs.
534
A FileSortingHelper példaprogram
Ha a példaprogrammal a következő szavakat szetetnénk sorba rendezni:
test drive n development is one small st ep for programmers but one giant leap for programming
a következő eredményt kapnánk:
one one programming small d ri ven leap st ep for for is programmers giant development test but
Az összehasonlító kódja:
package com.�rox.algorithms.sorting;
public final class Reversestringcomparator implements Comparator { public static final Reversestringcomparator INSTANCE = new
Reversestringcomparator();
private Reversestringcomparator() {
}
public int compare(Object left, Object right) throws classeastException {
assert left != null : "A 'left' (bal oldal) nem lehet NULL";
, asse...r.!...r.:ig_bt != null : "A 'right'_(j_Qbb old�l), nem lehet NULL";_
535
Pragmatikus optimalizálás
}
}
return reverse((String) left).compareTo(reverse((String) right));
private String reverse(String s) { StringBuffer result = new StringBuffer();
}
for (int i= O; i < s.length(); i++) { result.append(s.charAt(s.length() - l - i));
}
return result.tostring();
Most felesleges részletekbe menően megvizsgálni, hogyan műköclik a kód, hiszen
egyetlen programunkban sem fogjuk felhasználni. A mindkét argumentumaként
string objektumot váró szabványos comparator interfész megvalósítása után ösz
szeveti a két szó megfotelitott alakját.
Gyakorlófeladat: a FileSortingHelper osztály megvalósítása
A Fi l es o r ti n g He l pe r osztály itt látható:
536
pacKage com.wrox.algorithms.sorting;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;
public final class FilesortingHelper { private FilesortingHelper() {
}
}
public static void main(String[] args) throws Exception { sort(loadwords());
}
system.err.println("Kész ... a kilépéshez nyomja meg a CTRL-C bill entyúket");
Thread.sleep(lOOOOO);
A FileSortingHelper példaprogram
A megvalósitás müködése
Ahogy láthatjuk, az osztály egy példányosítást megakadályozó privát konstruktorból
és egy ma i n() metódusból áll. Ez utóbbi a munka nagy részét továbbadja a load
words() és a sort() metódusnak. Ezt követően első látszatra furcsa dolgot művel:
felszólít minket, hogy állitsuk le a programot, majd a Th read. sle ep() meghívásával
felfüggeszti a működését. Ezt egyszerűen azért teszi, hogy profilírozáskor több
időnk legyen szemügyre venni a Java Memory ProfiJer program eredményeit, úgy
hogy ne is törődjünk vele.
Most lássuk a sort() metódus kódját. Paraméterként szólistát vár, amelyet az
előbb definiált összehasonlító és burokrendezés segítségével sorrendbe tesz. Végül
egyszerűen kiírja a sorba tett szavakat.
private static voia sort(List wordCist)-{
}
assert wordList != null : "a 'tree' nem lehet null";
System.out.println("Rendezés inditása ... " );
Comparator comparator = Reversestringcomparator.INSTANCE; Listsorter sorter = new shellsortListsorter(comparator);
List sorted = sorter.sort(wordList);
Iterator i = sorted.iterator(); i.first(); while (! i.isoone()) {
System.out.println(i.current()); i .next();
}
Példaprogramunk utolsó metódusa a l oadwo r ds(). Ez egyszerűen beolvas sa a szab
ványos bemenetről érkező adatokat, majd ezeket L i st objektumba írja, és visszatér
vele a meghívóhoz, ha nincs több bemeneti adat. Az egyetlen felmerülő probléma,
hogy az esetlegesen felmerülő IOExcepti on kivételeket el kell fogni:
private static:List:loadwords()�hrows IOException-{ List result = new LinkedList();
BufferedReader reader = new BufferedReader(new InputstreamReader(system.in));
try { String word;
while ((word= reader.readLine()) != null) { result.add(word);
}
537
Pragmatikus optimalizálás
}
} finally { reader.close();
}
return result;
Ha lefordítjuk a fenti programot, futtatáskor a bemenetére egy fájlt kell irányítanunk,
ahogy a következő parancssorban is látható:
java com.wrox.algorithms.sorting.FilesortingHelper <words.txt
A program futtatásához abban a könyvtárban kell lennünk, amelyben megtalálha
tóak a példaprogram lefordított Java osztályfájljai. Ezenkívül létre kell hoznunk,
vagy szereznünk kell valahonnan egy words. txt nevű fájlt, amelyben soksornyi szö
veg van. Gyors internetes kereséssel találhatunk olyan szótárfájlokat, amelyben több
ezer szó van. A B függelékben annak a fájlnak a webcíme is megtalálható, amelyet
mi használtunk a könyv egyes példáihoz.
Az általunk használt Pentium 4 laptopon a program futtatása a 10 OOO szót tar
talmazó fájlon körülbelül egy percbe telt, a processzor kihasználtsága pedig 100%
volt. Sokkal türelmetlenebbek vagyunk, mint hogy ennyit várjunk az eredményre,
ezért optimalizálnunk kell a programot. A program profilírozásával nézzük meg, mi
is megy végbe a futtatás alatt.
Profilírozás a hprof modullal
A szabványos Sun Java virtuális gép telepítés után azonnal rendelkezik néhány alap
vető proftlírozóeszközzel. Az alábbi paranccsal megállapíthatjuk, hogy a nálunk te
lepített Java-környezetben is megtalálhatóak-e a szükséges eszközök:
java -xrunhprof:help
Az -xrun parancssori paraméter új modul betöltésére használható. Jelen esetben a
hprof modult töl�ük be. A modulnak parancsot is adunk: a hel p paranccsal lekér
dezzük, hogyan kell használni a modult. A következő felsorolás a parancs kimene
telét mutatja:
538
Hprof usage: -xrunhprof[:help] l [:<option>=<value>, ... ]
Option Name and value Description
heap=dump\sitesjall heap profiling
cpu=samples\times\old CPU usage monitor=y\n manitor contention
Default
al l
off n
A FileSortingHelper példaprogram
format=alb file=<file>
ascii or binary output write data to file
a java.hprof
net=<host>:<port> depth=<size> cutoff=<value> lineno=yln thread=yln doe=yln gc_okay=yln
send data over a sacket stack trace depth output cutoff point line number in traces? thread in traces?
(.txt for ascii) write to file 4 0.0001 y n
dump on exit? y GC okay during sampling y
Example: java -Xrunhprof:cpu=samples,file=log.txt,depth=3 Faoclass
Note: format=b cannot be used with cpu=oldltimes
A fenti kimenetből láthatjuk, hogy a hprof viselkedését számos paraméterrel meg
szabhatjuk Mostani példánkban a cpu=samp l es paramétert állitjuk be, ami azt jelen
ti, hogy profilírozáskor mintavételezéssel mérjük alkalmazásunk processzorhasznála
tát. A következő paranccsal beállitjuk ezt a paramétert, a bemenetet és kimenetet
pedig egy-egy fájlra irányítjuk az aktuális könyvtárban:
java -xrunhprof:cpu=samples com.wrox.algorithms.sorting. File-SortingHelper <words.txt >sorted.txt
Ha profilírozás funkcióval futtatjuk a programot, az előbbihez képest jelentősen le
lassul. Ez érthető is, hiszen a szükséges adatok összegyűjtése meglehetős többlet
munkát jelent. Minden hangoló jelentősen befolyásolja a teljesítményt, így az időér
tékek abszolút értékben nem lesznek ugyan helyesek, egymáshoz viszonyított ará
nyuk azonban eléggé pontos lesz.
A program befejeztével a következő üzenetet írja ki a képernyőre:
Dumping CPU usage by sampling running threads ... done.
Bár ebből nem tudtuk meg, de a profilírozás közben gyűjtött adatokat a program ki
írta a munkakönyvtárban található j ava. h prof. txt nevű fájlba. Ha szövegszerkesz
tőben megnyitjuk ezt a fájlt, a következőket látjuk (a fájl elején lévő sablonkód után):
THREAD START (obj=2b76bc0, id = l, name="Finalizer", group=" system")
THREAD START (obj=2b76cc8, id = 2, name="Reference Handler", group=" system")
THREAD START (obj=2b76da8, id = 3, name="main", group="main") THREAD START (obj=2b79bc0, id = 4, name="HPROF CPU profiler",
group=" system")
539
Pragmatikus optimalizálás
Ebből a virtuális gépben futó szálakról kapunk információkat. Ahogy ·látha�uk, a
hprof maga is létrehoz szálakat. A szálakról szóló információk után egy sor kis mé
retű J ava-veremkövetési adatot látunk:
TRACE 23:
java.lang.StringBuffer.<init>(<Unknown>:Unknown line) java.lang.StringBuffer.<init>(<Unknown>:Unknown line) com.wrox.algorithms.sorting.Reversestringcomparator.reverse
(Reversestringcomparator.java:48)
com.wrox.algorithms.sorting.Reversestringcomparator.compare (Reversestringcomparator.java:44)
TRACE 21:
com.wrox.algorithms.sorting.ReverseStringComparator.reverse (Reversestringcomparator.java:51)
com.wrox.algorithms.sorting.ReverseStringcomparator.compare (Reversestringcomparator.java:44)
com.wrox.algorithms.sorting.shellsortListsorter.sortsublist (ShellsortListsorter.java:79)
com.wrox.algorithms.sorting.shellsortListsorter.hsort
(Shell sortL istsorter. j ava: 69)
A kimeneti fájl nagy részét ezek a veremkövetések foglalják el. Ezek egyszerűen a
mintavételezési munka közben előforduló veremtartalmak Mintavételezéskor a
hprof megállapítja, hogy a verem tetején található metódushívás-kombináció előfor
dult-e már. Ha igen, a statisztikát frissíti, de újabb TRACE bejegyzést nem készít. A
TRACE utáni szám (itt például a TRACE 21) csak egy azonosító, amelyet a profilirozási
kimenetben később még használunk, ahogy hamarosan látni is fogjuk.
A kimenet vége a legérdekesebb. Ebből derül ki ugyanis, hogy rnivel tölti a
program a legtöbb időt. A kimeneti fájl végének első néhány sora:
CPU SAMPLES BEGIN (total = 1100) wed Jun 22 21:54:20 200 5 rank self ac cum co unt trace method
l 29.55% 29.55% 325 16 Reversestringcomparator.reverse 2 17.18% 46.73% 189 15 LinkedList.getElementBackwards
3 16.00% 62.73% 176 18 LinkedList.getElementForwards 4 13.09% 75.82% 144 17 LinkedList.getElementBackwards 5 ll. 55% 87.36% 127 14 LinkedList.getElementForwards 6 2.55% 89.91% 28 19 LinkedList.getElementBackwards 7 2.09% 92.00% 23 29 LinkedList.getElementBackwards 8 l. 91% 93.91% 21 24 LinkedList.getElementForwards
A táblázat legfontosabb oszlopai a self és az ac cum oszlop, valamint az utolsó, a
metódust megnevező oszlop. A self oszlop a végrehajtási idő magán a metóduson
belül eltöltött idejét jelöli az összes végrehajtási időhöz viszonyítva, rrúg az accum
oszlop a metódus és az általa meghívott összes további metódus százalékos idejét
mutatja. Ahogy látha� uk, a self oszlop értéke alapján van csökkenő sorrendbe ren-
540
A FileSortingHelper példaprogram
dezve, feltételezve, hogy számunkra az a legfontosabb, melyik metódus végrehajtása tart a leghosszabb ideig. A trace oszlop az azonosítást szolgálja: segítségével a fájl veremkövetési adatai között további információt találhatunk a kérdéses metódus végrehajtási verméről.
Mielőtt javítani próbálnánk a helyzeten, a Java Memory Profiler programmal is elvégezzük a profilírozást.
Profilírozás a Java Memory Profiter programmal
A Java Memory Profiler ingyenes, a következő címről szabadon letölthető eszköz:
http://www.khelekore.org/jmp/
A JMP dokumentációja nagyszerű, így telepítéshez csak az ott leírt utasításokat kell követnünk. Tartsuk észben, hogy a JMP nem Java-program, így ha a Java a megszakott programozási környezetünk, telepítése akár gondot is okozhat. Windows-rendszeren például a Windows-rendszerkönyvtárba DLL-eket kell másolnunk a telepítés során.
Ha ellenőrizni szeretnénk, helyesen telepítettük-e a JMP-programot, az előbb látott hprof modulhoz hasonlóan tehetjük meg. Írjuk be a következőt a parancssorba:
java -xrunjmp:help
Ezzel a JMP-program használatához kértünk segítséget. A súgó így fog kinézni:
jmp/jmp/0.47-win initializing: (help): ... help wanted .. java -xrunjmp[:[options]] package.Class options is a comma separated list and may include: help - to show this text. nomethods - to disable method profiling. noobjects - to disable object profiling. nomonitors - to disable monitor profiling. allocfollowsfilter - to group object allocations into filtered
methods. nogui - to run jmp without the user interface. dodump - to allow to be called with signals. dumpdir=<directoryr> - to specify where the dump-/heapdumpfiles go. dumptimer=<n> - to specify automatic dump every n:th second. filter=<somefilter> - to specify an initial recursive filter. threadtime - to specify that timing of methods and monitors
should use thread cpu time instead of absolute time. simulator - to specify that jmp should not perform any jni tricks.
probably only useful if you debug jmp.
An example may look like this: java -xrunjmp:nomethods,dumpdir=/tmp/jmpdump/ rabbit.proxy.Proxy
541
Pragmatikus optimalizálás
Ahogy láthatjuk, a JMP-program számos opcióval segíti a kívánt működés elérését.
A célunknak tökéletesen megfelel, ha az alapértelmezés szerinti konfigurációval fut
tatjuk a példaprogramunkon a következő parancssorral:
java -xrunjmp com.wrox.algorithms.sorting.FilesortingHelper
<words.txt >sorted.txt ·
Ekkor a 19.1. ábrán látható három ablak fog megjelenni a statisztikákkal.
69969 1.64063 2402630
14200 665.625 5 820
17914 388.57031 802187
17897 245.76563 797304
10001 234.39844 o
309 52.25 26
569 25.05469 90
20.26563 39
. ! sec: ___ cans
419.000000
19. 1. ábra. A JMP ablakai profilírozás kö"zben
A JMP-program főablaka (a 19.1. ábrán a legalsó ablak) grafikusan is ábrázolja a futó
alkalmazás memóriafelhasználását. Két érték változását követhetjük nyomon: a vir
tuális gép számára lefoglalt teljes halamméretet és a jelenleg objektumok használatá
ra rendelt értéket. Ahogy a grafikon alakjáról látha�uk, a használt memória mérete
folyamatosan változik az objektumok létrehozásával és a használaton kívüli objek
tumok felszabadításával. Ha a szükséges memóriaméret meghaladja a virtuális gép
számára lefoglalt aktuális teljes halomméretet, a virtuális gép az operációs rendszer
től többletterületet igényel, amelyet további objektumok tárolására fog használni.
A 19.1. ábra tetején látható JMP Objects ablak a virtuális gépben található pél
dányokról rengeteg érdekes információval szolgál. Az első oszlopban az osztály ne
vét olvashatjuk, közvetlenül utána pedig az osztály példányainak számát.
542
Az optimalizálásról
Ezt követi a program futása során elért legnagyobb példányszám, az aktuális példányok által használt memóriaméret, és végül a futás során szemétgyűjtés által felszabadított példányok száma. Ez utóbbi oszlop a programoptimalizálás során még hasznos információkat fog szolgáltatni számunkra.
A 19.1. ábra közepén látható JMP Methods ablak a program futása alatt meglúvott metódusok statisztikáit muta�a: a metódus nevét, a lúvások számát, a lúvások hosszát (másodpercben) és a metódus által meglúvott metódusok futási idejét (másodpercben). Ezek az adatok is rendkívül értékes információkkal szalgálnak majd a példaprogram optimalizálása során.
Az optimalizálásról
Prograrnak optimalizálása előtt érdemes tisztában lennünk vele, hogy ha helytelenül választottuk meg a használt algoritmust, az optimalizálással csak az időnket fecséreljük. Ha például programunk buborékrendezést használ egymillió bejegyzés rendezésére, és túl lassúnak találjuk, ne optimalizálással próbáljuk megjavítani. Akármennyit is próbáljuk finomhangolni algoritmusunkat, ha az O(N2) bonyolultságú, a műveletek befejezéséig nyugodtan megihatunk egy kávét. De talán meg is ebédelhetünk Ezért nem került a könyv elejére az optimalizálás. Valóban nem olyan fontos, mint hinnénk. A fejezet hátralévő részében feltételezzük, hogy a célunkhoz megfelelő algoritmust választottunk, és optimalizálással ebből szeretnénk kihozni a legjobbat.
Azzal is csak az időnket fecséreljük, ha a programunknak olyan részét optimalizáljuk, amely valójában nem is szűk keresztmetszet. Talán nyilvánvalónak hangzik, a programozók mégis gyakran rengeteg energiát fektetnek olyan kódrészletek felgyorsításába, amelyeket szinte soha, vagy csak az alkalmazás indulásakor lúvnak meg. Ezen erőfeszítések eredményeképpen szükségszerűen bonyolultabb és nehezebben átlátható kód születik, amely az alkalmazás általános teljesítményét semmivel sem javítja, bár valóban gyorsabban fut, mint korábban.
Az egész fejezet legfontosabb tanulsága: ne kitalálni akarjuk, miért fut lassan a programunk! Profilírozással vagy egyéb módszerekkel mérjük le a program teljesítményét. Így optimalizálás során koncentrált lépésekkel javíthatunk a program működésén. A programoptimalizálás ajánlott lépései:
1. Profilirozóval mérjük le programunk teljesítményét.
2. Azonosítsuk a teljesítményprobléma legfontosabb okait.
3. Javítsunk ki az okok közül egyet. Ha lehet, válasszuk a legfontosabbat, de ha van könnyebben megjavítható, másik jelentős ok, azt is választha� uk.
543
Pragmatikus optimalizálás
4. Mérjük le újra a teljesítményt.
5. Győződjünk meg róla, hogy a kívánt hatást értük-e el. Ha nem, írjuk vissza
az eredeti kódot.
6. Ismételjük a fenti lépéseket mindadclig, míg az elért javulás még megéri a
munkát, vagy már elfogadható a teljesítmény.
Semmi ördöngösség nincs ebben a módszerben. Egyszerűen a mért tények alapján
koncentráltan javítunk a működésen, meggyőződve arról, hogy minden változtatás
mérhető előnnyel jár. A következő részben ezzel a módszerrel optimalizáljuk a pél
daprogramot.
Optimalizálás a gyakorlatban
Most, hogy már a JMP-programmal is profiliroztuk példaalkalmazásunkat, kíváncsi
ak lehetünk rá, miért olyan lassú. A 19.2. ábrán látható Methods ablak alaposabb
megvizsgálásával meglátha�uk, mivel megy el az idő.
e Java Ucnuuy ���file r - Ucthods _ _ __ _ __ _ ___ __ ;.;:,.SL:; ����=-����---- ---� -_2�-- !caus ___ : __ s����-
����=�- 't�t���.:�� javaJang.Object VOid wait(long) 419.000000 5 o.oooooo 419.000000 83.000(
com.wrox.algorithm,.sorting.Re javaJang.String reverse (java.lí 49.000000 796832 47.000000 97.000000 o.ooor
com.wrox.algorttnmsJists.Unkec com.'WI"'x.algotithms.lists.Unkec 45.000000 463975 0.000000 45.000000 o.oooc
com.wrox.algoríthmsJists.Unket com:wrox.algolittlms.lists.Unkec 38.000000 393243 0.000000 38.000000 O.OOOC java.lang_strtng char charAr (int) 10.000000 7390575 o.oooooo 10.000000 o.oooc
.java.lang.StrlngButrer javaJang.stringBurter append ( 10.000000
<<T"" -------·,;----�lOOMithockcUdletl
19.2. ábra. A Methods ablak
7169252 0.000000 10.000000 0.000{···1
�)!v,
Látha�uk, hogy a String objektumok megfordítása, valamint a L i n ked L i st művele
tek rengeteg időbe telnek. (A lista tetején található elemet most figyelmen kívül
hagyhatjuk, hiszen az csupán a Ctrl+C billentyűkombináció megnyomása előtt eltelt
időt mutatja, azaz eddig nézegettük az eredményeket.)
Mi tehát a következő lépés? Kigondolhatnánk a String objektumok megfordí
tására egy hatékonyabb algoritmust, azonban a L i n ked L i st megoldása könnyebbnek
ígérkezik. Mivel eredetileg nem tudtuk, mennyi adat fog beérkezni a bemenetről,
csak annyit, hogy sok, úgy gondoltuk, hogy L i n ked L i st objektum tökéletesen meg
felel a tárolásra. Most azonban már emlékszünk rá, hogy a sorba rendezéshez a lista
elemeit indexelt kereséssel kell megtalálnunk, és a profilírozó szerint itt lassul le a
program. Ha a 19.2. ábrán látható ca ll s oszlopot alaposabban szemügyre vesszük,
látha�uk, hogy már tízezer szó rendezésére is több százezerszer kell meghívni a
L i n ked L i st műveleteket. Ezek szerint nem a megfelelő adatstruktúrát választottuk.
544
Optimalizálás a gyakorlatban
A künduló lista összeállításához csupán tízezerszer kell meghívni az add() metódust,
így olyan adatstruktúrát kell választanunk, amely a legfontosabb műveletet támogat
ja. Jelen esetben ez nem más, rnint a lista felépítése után az elemek közötti indexelt
keresés, tehátArrayList objektumra van szükségünk.
Gyakorlófeladat: az ArrayList megvalósítása
A Fi lesorti ngHelper osztály loadwords() metódusában nagyon könnyen kicserél
he�ük a L i n ked L i st objektumot ArrayLi st objektumra:
private static-Li51:loadwords()�hrows IOExcepti on-{ List result = new ArrayList();
BufferedReader reader = new BufferedReader(new InputStreamReader · (System. i n));
}
try { string word;
while ((word= reader.readLine()) != null) { result.add(word);
} } fi nally {
reader. close(); }
return result;
A következő lépés a program újrafordítása és profilírozása a következő paranccsal:
java -xrunjmp com.wrox.algorithms.sorti ng.Fi lesorti ngHelper <words.txt >sorted.txt
Ezúttal a JMP profilírozó program a 19.3. ábrán látható eredményt adja.
A megvalósitás müködése
Most nézzük meg ismét a JMP-program Methods ablakát, amely a 19.4. ábrán látható.
Már nyoma sincs a L i n ked L i st műveleteknek, és ami még ennél is fontosabb, a
most hozzáadott ArrayLi st műveleteknek sem. Bár gyakran megtörténik, hogy a
változtatás után a probléma csak máshová kerül, vagy még rosszabbá teszi a helyze
tet, ebben az esetben nem ez történt. Azonban ezért fontos, hogy rninden változta
tás után megmérjük az eredményeket: meg kell győződnünk róla, hogy optimalizálási
munkánk javítja a teljesítményt.
545
Pragmatikus optimalizálás
... - - - · ··· .. - - - - - ·· - - ··-·· .
"-�- ja(:\WÚH'"'�""-·._. __ .,..,; . .:. ......... -
-'-··· ........ •--·· [Ile e Ja:ot.s Mulll:�TY Profil ef- Ohjc� __
; lnstances Max !nstanc SiZe
37246 603 59 1.44835
13989 13989 655.73438
10473 18116 388.67188
10487 18100 245.78906
578 72.42188
308
19.3. ábra. A profilírozás eredméf!Ye a LinkedList oljektum cserije után
• Java Memory Protner - Methods =
Class j Method t secs ! cans
1\-U...--: llng--,.,.=Oiljed--,------''""--:---1 :-=-(long-c-)) �---------,1."11c=:9fi99799 .000000 \ subs sec .i� 'C
com:wrox.algorithm�.sortlng.Re javaJang.String reverse (Java Jang .strtngj
java.lang.String char charAl (Int) java .lang .StringBuffer
java.Jang.StringButrer
java.lang.strtngButfer
<l �_100111ethodsoo..tdlt.O
java.lang.StrlngButter append (char)
java.Jang.String toString O
VOid<U1it> (int)
51 .000000 796832
11.000000 7390497
10.000000 7169251
8.000000 797027
6.000000 797028
19.4. ábra. A Methods ablak azArrqyList megvalósítás után
ODODOOO 11 49.000000
0.000000
0.000000
8.000000
0 000000 1:;
(>l
A 19.4. ábrán láthatjuk, hogy a Reversestringcomparator osztály reverse() metódusa 51 másodpercig fut, míg a következő metódus csak ll másodpercig. Ha tovább szeretnénk javítani a teljesítményen, ideje átgondolni a String objektumok megfordítását. Nézzük csak meg, hányszor hívja meg a program a reverse() metódust: csaknem 800 000-szer! Ez utóbbi fontos adat. Ha úgy volna 51 másodperc a futásideje, hogy csak egyszer hívtuk meg, valóban nagyon rosszul megírt metódusról volna szó. Így viszont inkább az a baj, hogy túl sokszor hívjuk meg a metódust. Ez még nyilvánvalóbbá válik, ha megnézzük a 19.5. ábrán látható JMP Objects ablakot.
A #GC oszlop azt mutatja meg, hogy a program végrehajtása alatt az adott osztály hány objektumát szabadította fel szemétgyűjtés. Láthatjuk, hogy összesen mintegy 2,5 millió objektumról van szó. Ez nyilvánvalóan túl nagy szám, hiszen csupán egy tízezer szavas listát készítettünk.
546
• Java f.1emory Profiler. Objects L:Jé'.Q ... .. - . -- . - -- - - -- ... --- - --- -- r:-, Class _ _ ____ __: �t�;_�:._-Max in:!��:'.�. __ __ :_�<? ___ J T :�; Total 37246 60359 1.44835 2402769 java.nio.HeapCharBuffer cha�]
13989 10473 10487
13989 18116 18100
578
655.73438 6031 388.67188 802149 1 245.78906 797281 1
72.42188 100 1 ·java .lang .String java.lang.Objecq] byte))
494 287 308 52.25 251 �;
[<) . -··--·-- . ·---- -----JShöWi10 IOOdassosoutof 115
----- ------.!!'"
19.5. ábra. Az Oijects ablak
f>l //
Optimalizálás a gyakorlatban
A megoldást a szemétgyűjtés által felszabadított String objektumok jelentik. Ezek száma nagyjából megegyezik a reverse() metódus meglúvásainak számával, azaz körülbelül 800 OOO-rel. Biztosak lehetünk benne, hogy a probléma abból fakad, hogy valahányszor két St ri n g objektumot összehasonlítunk a rendezés miatt, minden alkalommal meg is kell fordítani ezeket a String objektumokat. Mivel minden egyes St ri n g objektumot többször is össze kell hasonlitani egy másikkal, ugyanazt a String objektumot újra és újra meg kell fordítani. Így állandóan új String objektumot hozunk létre, amely aztán így több ezer objektumot jelent a szemétgyűjtés számára. Biztosan van ennél jobb módszer is.
Lecsökkenthetnénk a program munkáját úgy, hogy a 10 OOO bemeneti String
objektumot csak egyszer kelljen megfordítania. Ha már azelőtt megfordítanánk az összes sztringet, hogy eltárolnánk a listában, az egész Reversestri ngcomparator
osztály feleslegessé válna. Ehelyett a String objektumok összehasonlítására használhatnánk természetes összehasonlitót. Így a jelenlegi rendezési algoritmusból kivennénk a munka nehezét, és jelentős mértékben lecsökkentenénk a futás során létrejövő ideiglenes objektumok számát. Legalábbis azt gondoljuk ... A végén természetesen méréssel meg is kell győződnünk róla, hogy valóban így volt-e.
Persze a rendezés után még ki is kell íratni a listát. Ehhez vissza kell fordítani a String objektumot, hogy visszanyerjük a karakterek eredeti, helyes sorrendjét, különben a felhasználó nem a várt eredményt kapja. Így további 10 OOO műveletet kell elvégezni a megfordítás miatt, de az algoritmusnak a mostaninál még így is sokkal hatékonyabbnak kell lennie. Biztosat persze megint csak akkor fogunk tudni, ha lemértük a változtatás hatását.
Gyakorlófeladat: a FileSortingHelper osztály optimalizálása
A Fil esorti ngHel per osztály egy kis módosításra szorul, ezért a kódpéldánkban létrehozunk egy OptimizedFilesortingHelper nevű osztályt. Így a két különböző megvalósítás gyorsreferencia céljából könnyen hozzáférhető lesz. Az Opti mi zed Fi l e
sorti n g He l per osztály eleje itt látható:
547
Pragmatikus optimalizálás
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List;
import java.io.BufferedReader; import java.io.IOException; import java.io.InputstreamReader;
public final class OptimizedFilesortingHelper {
}
private OptimizedFileSortingHelper() {
}
A FilesortingHelper osztályhoz hasonlóan az optimalizált változatban is található
egy privát konstruktor, nehogy véletlenül valamelyik másik program példányosítsa.
A ma i n O metódus így fest:
public static void main(String[] args) throws Exception {
List words = loadwords();
}
reverseAll(words);
System.out.println("Rendezés indítása ... " );
comparator comparator = Naturalcomparator.INSTANCE; Listsorter sorter = new shellsortListSorter(comparator);
List sorted = sorter.sort(words); reverseAll(sorted); printAll(sorted);
system.err.println("Kész ... a kilépéshez nyomja meg a CTRL-C billentyűket");
Thread.sleep(lOOOOO);
A mai n O metódus a munka nagy részét az alább bemutatott másik két metódusra hárí�a. Figyeljük meg, hogy a szavak betöltése után meghívjuk a reverseA ll O me
tódust, amely minden szót azonnal megfordít. A fordított szavak listáját ezután
Naturalcomparator összehasonlítóval hasonlí�uk őket össze, mintha szokványos
sztringek volnának. Az így rendezett lista karaktersorait aztán visszafordí�uk,. majd
kiírjuk a szabványos kimenetre.
548
Optimalizálás a gyakorlatban
Most lássuk a loadwords() metódust. A korábban bemutatott Filesorting
He l pe r osztályhoz képest nem történt változás:
priVate'Staticl'fst loadwords() throWs IOException { List result = new ArrayList();
BufferedReader reader = new BufferedReader(new InputstreamReader (System. in));
l
try { String word;
while ((word = reader.readLine()) != null) { result.add(word);
}
} finally { reader.close();
}
return result;
A fejezet elején készített, és mostanra feleslegessé vált Reversestri ngcomparator
osztályból csak az itt látható reverse() metódus menekült meg:
private static String reverse(String s)_{ __
StringBuffer result = new StringBuffer();
for (int i =O; i < s.length(); i++) { result.append(s.charAt(s.length() - l- i));
}
return result.toString();
A reverseA ll() metódus egyszerűen bejárja a paraméterként megkapott L i st min
den String elemét, megfordítja őket, majd visszateszi a L i st objektumba.
private static voii:l reverseAll([ist woras)-{ for (int i =O; i < words.size(); ++i) {
words.set(i, reverse((String) words.get(i))); }
} - � .l
A pr i n tA ll() metódus sem más, mint egy egyszerű listabeli iterációs rutin a para
méterként megkapott L i st elemeinek kiíratására:
549
Pragmatikus optimalizálás
private static void printAll(List stringList) { Iterater iterater = stringList.iterator(); iterator.first();
}
while (!iterator.isoone()) {
}
String word= (String) iterator.current(); system.out.println(word); iterator.next();
A megvalósitás működése
Most futtassuk le az optimalizált példaalkalmazásunkat. A következő paranccsal pro
filirozzuk a Opti mi zed Fi lesorting He l per programunkat:
java -xrunjmp com.wrox.algorithms.sorting. OptimizedFilesortingHelper <words.txt >sorted.txt
A JMP-program kimenete a 19 .6. ábrán látható.
19.6. ábra. A JMP-program kimenete az OptimizedPi/eS ortingHelper programra
A 19.7. ábrán látható Methods ablakban nézzük meg, hogy megszüntettük-e a sztring
megfordításával töltött 50 másodperces munkát.
550
Optimalizálás a gyakorlatban
• Java Mamorv Proft1er .llethods GJ®t:J Class Method : secs caus j subs sec total : totaVca" �1 ;-oang-011!0<1 --� 50.000000 6 0.000000 50.000000 SOOOOl l
com.wrax.algortthms.lis�.ArrayUst void cneckOutOfBounds (int) 4.000000 887218 com'I'ITOX.algortthms.sortlng.Shellsortl void sortSublist (com:wrox.algonthmsJ 3.000000 179
com.l!Kox.algorttttms.lists.Amlyl.lst javaJang.Object get (Int) 2.000000 468237
com.wrox.algorithms.sorting.NaturaiCo Int compare ijava.lang_Object. javaJar 2.000000 398416
com.wrox.algorithms.llsts.Arrayllst javaJang.Object set (Int, javaJang.Obj 2.000000 416981
<l IOOmethodso.td 179
1.000000 5.000000 16.000000 19.000000
2.000000 5.000000 4.000000 6.000000
2.000000 5.000000
0.0000(
0.0000(
0.0000!
0.0000(
O.OOOQ(r..,
Í>--.
19.7. ábra. Az OptimizedFileS ortingHelper program profilírozása utáni Methods ablak
Láthatjuk, hogy a reverse() metódusnak nyoma sincs a szűk keresztmetszetet je
lentő metódusok listáján. Azt is vegyük észre, hogy a leghosszabb ideig tartó metó
dus is csak négy másodpercig fut! Ez jelentős javulás.
Most nézzük meg a 19 .8. ábrán látható Objects ablakot, hogy meglássuk, a sze
métgyűjtést illetően a várakozásainknak megfelelően alakultak-e a dolgok .
• Ja� a r.1em_?ry: Profil er . Object� .::Jei U Class ; lnstances Max instanc Size ,#GC ,T� Total
char[)
java .lang .String
java .nlo .HeapCharButrer
java .lang Object[)
byte[)
r <r �iiifC!Osses_O<.t_'* ,,.
25982
10472
10486
272 7
496
288 . - - ·· · ·· . ... . .
53503 1.07574 �
16503 535.08594 f
16458 245.76563 f
12107 127.82813 f
564
305 - .. ---
72.47656 f
52.20313 f
-. . - - "]
- �-. . ... �-
79020
20801
204 57
17293
100
25 :vJ 1">1
,Ó
19.8. ábra. Az OptimizedFileSortingHelper program profílirozása utáni Objects ablak
Itt is jelentős változásokat láthatunk. A #GC oszlop első sorából kiderül, hogy a prog
ram végrehajtása alatt összesen hány objektumot szabadított fel szemétgyűjtés. Most
ez a szám kevesebb, mint 80 OOO, pedig az előző futás alkalmával meghaladta a két
milliót. Azt is figyeljük meg, hogy a szemétgyűjtés által felszabadított String objek
tumok száma 20 OOO körül volt, ami megfelel a 1 O OOO szó kétszeri megfordítás ának,
ahogy vártuk. Nagyon fontos, hogy a változások tükrében mindig világosak legyenek
ezek a számok. Ezért minden változtatás alkalmával, miután alkalmazásunkat profili
roztuk, nézzük meg őket.
Példaalkalmazásunkat kétlépésben optimalizáltuk, amelynek eredményeképpen
jelentősen lecsökkent a szűk keresztmetszeti metódusok futtatási ideje. Az utolsó lé
pésben térjünk vissza az alkalmazás rendes végrehajtására, hogy meglássuk, hogyan
teljesít. Ha még emlékszünk rá, a program futtatása eredetileg több mint egy percet
vett igénybe. Futtassuk most a JMP-parancssori kapcsoló nélkül:
java com.wrox.algorithms.sorting.OptimizedFilesortingHelper <words.txt >sorted.txt
551
Pragmatikus optimalizálás
A megvalósitás működése
Most alig két másodperc alatt lefutott, azaz az eredetinek körülbelül ötvenszeresére
nőtt a sebesség! Tapasztalataink alapján Java-kódok optimalizálásakor nagyjából
ilyen eredmény szokott kijönni. A legfontosabb, hogy kódolás közben nem kellett a
teljesítményre koncentrálnunk, elég volt a szükségleteinknek megfelelő tulajdonsá
gokkal rendelkező algoritmus kiválasztására törekednünk Az egyszerű és áttekinthe
tő, jól megtervezett kód később könnyedén optimalizálható. Az elért teljesítményja
vulás szempontjából kulcsfontosságú volt például az a tény, hogy a L i st és a compa
rator interfészt egyszerűen le tudtuk cserélni példaprogramunkban.
Összefoglalás
A fejezetből a következőket tanulhattuk meg:
552
• Az optimalizálás fontos része ugyan a szoftverfejlesztésnek, de nem annyira,
mint az algoritmusok alapos ismerete.
• A profilírozással Java-programunk futásidejű viselkedéséről tudharunk meg
adatokat.
• A Java virtuális gép egyszerű, parancssori paraméteres szintaxissal támogatja
a profllírozást.
• Az ingyenesen letölthető Java Memory ProfiJer program grafikusan szemlél
teti alkalmazásunk memóriafelhasználását, így gyorsan megtalálha�uk a
problémás területeket.
• Módszeres megközelítésú optimalizálással lassan futó példaprogramunkat
ötvenszer gyorsabbá tettük.
"A"
FÜGGELÉK
Ajánlott irodalom
Reméljük, hogy a könyv felkelti az olvasó érdeklődését az algoritmusok világának to
vábbi felfedezése iránt. Abban is reménykedünk, hogy a felfedezőútra magával viszi a
tesztvezérelt fejlesztés tervezési mintáit és ötleteit is. Könyvesboltba vagy könyvtárba
betérve a témában az alábbi köteteket érdemes áttanulmányozni, de néhány kulcsszó
segítségével az interneten is könnyen hozzáférhető forrásokat találhatunk
Kezdőkönyv a programozásró4 (Eredeti kiadás: Eeginning Programming, Wiley Publishing
Inc., 2005.) SZAK Kiadó, 2006.
Sima Dezső et al.: Korszerű számitógép-architektúrák I. és II. résv (Eredeti kiadás:
Advanced Computer Architectures, Addison Wesley Longman, 1997).
Fordította: dr. Cserny László, dr. FüstösJános és Tóth Zoltán. SZAK Kiadó, 1998.
Algorithms in Java, Third Edition, P arts 1--4: Fundamentals, Data Structures, Sorting,
Searching, by Robert Sedgewick. Addison Wesley, 2002.
Design Patterns, by Erich Gamma et al. Addison-Wesley, 1995.
File Structures, by Michael Folk and Bill Zoellick. Addison-Wesley, 1991.
Introduction to Algorithms, S econd Edition, by Thomas H. Cormen et al.
The MIT Press, 2001.
Java Perfonnanec Tuning, Second Edition, by Jack Shirazi. O'Reilly Associates, 2003.
]Unit in Action, by Vincent Massol with Ted H us ted. Manning, 2004.
Test-Driven Development: By Example, by Kent Beck. Addison-Wesley, 2002.
Test-Driven Development: A Practical Guide, by David Astels. Prentice Hall PTR, 2003.
The Art of Computer Programming, Volume 1: FundamentaiAlgorithms (Second Edition),
by Donald E. Knuth. Addison-Wesley, 1973.
The Art of Computer Programming, Volttme 3: Sorting andSearching (S econd Edition),
by Donald E. Knuth. Addison-Wesley, 1998.
" B"
FÜGGELÉK
Internetes források
Apache J akarta Commons honlapja: h t tp: l /j akarta. apache. o r g/ commans
Java Memory Profilet honlapja: www. khelekore.org/jmp/
J unit honlap: www. j uni t. org
National Institute of Standards and Technology honlapja: www. ni st. gov
Project Gutenberg honlapja: www. gutenberg. o r g
Unicode honlapja: www.unicode.org
Dél-dániai Egyetem Matematika és Számítástechnika Tanszék: http://imada.sdu.dk
Calgary Egyetem Számítástechnikai Tanszék: www. c p sc. u ca l gary. ca
�ikipedia: www.wikipedia.org
�ord Lists honlap: http: l /word l i st. sourceforge. n et/
" C"
FÜGGELÉK
Bibliográfia
[Astels, 2003] Astels, David. Test-Driven Development: A Practical Guide.
Prentice Hall PTR, 2003.
[Beck, 2000] Beck, Kent. Extreme Programming Explained
Boston: Addison-Wesley, 2000.
[Beck, 2002] Beck, Kent. Test-Driven Development: By Example.
Addison W esley Longman, 2002.
[Bloch, 2001] Bloch, Joshua. Eifective Java. Addison-Wesley, 2001.
[Cormen, 2001] Cormen, Thomas H., et al. Introduction to Algorithms, Second Edition.
The MIT Press, 2001.
[Crispin, 2002] Crispin, Lisa, and Tip House. Testing Extreme Programming.
Addison Wesley, 2002.
[Fowler, 1999] Fowler, Martin. Refactoring. Addison-Wesley, 1999.
[Gamma, 199 5] Gamma, Erich, Richard H elm, Ral ph Johnson, and J ohn Vlissides.
Design Patterns. Addison-Wesley, 1995.
[H unt, 2000] H unt, Andy, and Dave Thomas. The Pragmatic Programmer.
Addison-Wesley, 2000.
[Knuth, 1973] Knuth, Donald E. Fundamental Algorithms, Volume 1 of
The Art of Computer Programming, Second Edition. Addison-Wesley, 1973.
[Knuth, 1998] Knuth, Donald E. Sorting and Searching, Volume 3 of
The Art of Computer Programming, Second Edition. Addison-Wesley, 1998.
[Massol, 2004] Massol, Vincent. ]Unit in Action. Manning, 2004.
[Sanchez, 200 3] Sánchez-Crespo Dalmau, Daniel. C ore Techniques and Algorithms
in Game Programming. New Riders Publishing, 2003.
[Sedgewick, 2002] Sedgewick, Robert. Algorithms in Java, Third Edition, P arts 1--4:
Fundamentals, Data Structures, Sorting, Searching. Addison Wesley, 2002.
" D"
FÜGGELÉK
A gyakorlatok megoldásai
A függelékben csupán példákat mutatunk be az elképzelhető megoldásokra. Nem minden fejezet végén találhatók feladatok, de reméljük, hogy ezek elegendő lehetőséget nyújtanak a tanultak gyakorlatba való átültetésére. Arra biztatunk mindenkit, hogy kisérletezzen bátran a fejezetekben ismertetett módszerekkel.
2. fejezet
Gyakorlatok
1. Hozzunk létre iterátort, amely csak minden n-edik elem értékével tér vissza, ahol n nullánál nagyobb pozitív egész.
2. Hozzunk létre predikátumot, amely elvégzi a logikai ÉS (&&) múveletet két másik predikátum on.
3. Írjuk újra a Powe rca l eu l at o r-t, iteráció helyett rekurzió t használva.
4. Cseréljük le a tömbök használatát iterátotokra a rekurzív könyvtárfa-nyomtatóban.
5. Hozzunk létre egy iterátort, amely egyetlen értéket tartalmaz.
6. Hozzunk létre egy üres iterátort, amely mindig végzett állapotban van.
1. gyakorlat megoldása
- package com.wrox. algorithms. i terati on;
public class skipiterator implements Iterator { private final Iterator _iterator; private final int _skip;
public Skipiterator(Iterator iterator, int skip) {
}
assert iterator != null : "az iterátor nem lehet null"; assert skip > O : "skip értéke nem lehet < l"; _iterator = iterator; _skip = skíp;
public void first() { _iterator.first(); sk i pForwards ();
}
A gyakorlatok megoldásai
}
public void last() { _iterator.last();
skipBackwards();
}
public boolean isoone() {
return _iterator.isoone();
}
public void next() { _iterator.next();
sk i pForwards O ; }
public void previous() {
_iterator.previous();
skipBackwards();
}
public Object current() throws IteratorOutOfBoundsException { return _iterator.current();
}
private void skipForwards() {
}
for (int i= O; i < _skip && !_iterator.isoone(); _iterator.next());
private void skipBackwards() {
}
for (int i =O; i < _skip && !_iterator.isoone();
_iterator.previous());
2. gyakorlat megoldása
560
package com.wrox.algorithms.iteration;
public final class AndPredicate implements Predicate {
private final Predicate _left; private final Predicate _right;
public AndPredicate(Predicate left, Predicate right) { assert left != null : " left értéke nem lehet null"; assert right != null : " értéke nem lehet null";
_left = left; _right = right;
}.
A gyakorlatok megoldásai
public boolean evaluate(Öbject object)-{ return _left.evaluate(object) && _right.evaluate(object);
} }
3. gyakorlat megoldása
--package com. w ro x. a l go ri th-mS.i terati on;
public final class RecursivePowercalculator implements Powercalculator {
public static final Powercalculator INSTANCE = new Powercalculator();
private RecursivePowercalculator() { }
public int calculate(int base, int exponent) { assert exponent >=O : "a kitevő nem lehet < O";
return exponent > O ? base * calculate(base, exponent - l) : l; }
}
4. gyakorlat megoldása
-----pa·ckage com.wrox. al go ri thms. i teration;
import java.io.File;
public final class RecursiveoirectoryTreePrinter { private static final String SPACES = " ";
public static void main(string[] args) {
}
assert args != null : "paraméter nem lehet null";
if (args.length !=l) { System.err.println("Használata:
RecursiveoirectoryTreePrinter <dir>"); System.exit(4);
}
System.out.println("Rekurzív könyvtárstrútura-megjelenítés: " + args[O]);
print(new File(args[O]), "");
private static void printCiterator files, String indent) { assert files != ni.Jll : "files paraméter nem leh�nu11"; �-----�
561
A gyakorlatok megoldásai
}
}
for (files.first(); !files.isoone(); files.next()) { print((File) files.current(), indent);
}
private static void print(File file, String indent) { assert fi l e ! = null : "fi l e paraméter nem l ehet null"; assert indent != null : "behúzás mértéke nem lehet null";
}
System.out.print(indent); System.out.println(file.getName());
if (file.isoirectory()) { print(new Arrayiterator(file.listFiles()), indent + SPACES);
}
5. gyakorlat megoldása
562
package com.wrox.algorithms.iteration;
public class singletoniterator implements Iterator { private final Object _value; private boolean _done;
public Singletonrterator(Object value) { assert value != null : "az érték nem lehet null"; _value = value;
}
public void first() { _done = false;
}
public void last() { _done = false;
}
public boolean isoone() { return _done;
}
public void next() { _done = true;
}
public void previous() { _done = true;
}
A gyakorlatok megoldásai
l
publ i c-·oojeCt:-currentO .. throws IteratorciutofsoundsException { if (isoone()) {
throw new IteratorOutOfBoundsException(); } return _value;
}
6. gyakorlat megoldása
package com.wrox.algorithms:lterat1on;
public final class Emptyrterator implements Iterator { public static final Emptyrterator INSTANCE = new Emptyrterator();
}
private Emptyiterator() { ll semmit sem kell tenni
}
public void first() { ll semmit sem kell tenni
}
public void last() { ll semmit sem kell tenni
}
public boolean isoone() { ll mindig készen van! return true;
}
public void next() { ll semmit sem kell tenni
}
public void previous() { ll semmit sem kell tenni
}
public object current() throws IteratoroutOfBoundsException { throw new IteratoroutOfBoundsException();
}
563
A gyakorlatok megoldásai
3. fejezet
Gyakorlatok
1. Írjunk egy olyan konstruktort az ArrayList osztályhoz, amely szabványos Javatömböt vár a L i st kezdeti feltöltéséhez.
2. Írjunk egy e qua ls() metódust, amely bármely L i st megvalásításra működik.
3. Írjunk egy toStri n g() metódust, amely bármely L i st megvalásításra működik, és a lista tartalmát egyetlen sorba, az értékeket szögletes zárójelek közé zárva, vesszővel elválasztva kiírja. Például" [A, B, C]" vagy"[]" üres L i st esetén.
4. Írjunk egy iterátort, amely bármely L i st megvalásításra működik. Melyek a teljesítményt befolyásoló tényezők?
5. Frissítsük a L i n ked L i st osztályt úgy, hogy beszúrás vagy törlés esetén visszafelé keressen a listában, ha a kívánt index a lista közepén túl található.
6. Írjuk át az i ndexof () metódust úgy, hogy bármely listára működjön.
7. Hozzunk létre egy mindig üres L i st megvalósítást, amely módosítási kísérlet esetén unsupportedoperati onExcepti on kivételt dob.
1. gyakor!at megoldása
public ArrayList(Object[] array) {
}
assert array != null : "array nem lehet null";
_initialcapacity = array.length; clear();
System.arraycopy(array, 0, _array, 0, array.length); _size = array.length;
2. gyakorlat megoldása
public boolean equals(object object) { return object instanceof List ? equals((List) object)
}
public boolean equals(List other) { if (other == null l l size() != other.size()) {
return false; }
Iterator = iterator();
false;
___ I�t�e.._r� ator j = other. i terator (.",)�;'--�---------------
564
A gyakorlatok megoldásai
}
for:- CCfi rst(f, j:-firstO;
}
!i.isoone() && !j.isoone(); i.next(), j .next()) {
if (!i.current().equals(j.current())) { break;
}
return i.isoone() && j.isoone();
3. gyakorlat megoldása
public stri'ng tostring() {
}
StringBuffer buffer = new StringBuffer();
buffer.append('[');
if (!isEmpty()) {
}
Iterator i = iterator(); for (i.first(); !i.isoone(); i.next()) {
buffer. append(i. current()) .append(", ");
}
buffer.setlength(buffer.length() - 2);
buffer.append(']');
return buffer.toString();
4. gyakorlat megoldása
package com. w ro x. a l goritllmS.li sts;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutofBoundsException;
public class GenericListiterator implements Iterator { private final List _list; private int _current;
public GenericListiterator(List list) { assert list != null : "list nem lehet null"; _list = list;
}
565
A gyakorlatok megoldásai
}
public void first() { _current = O;
}
public void last() { _current = _list.size() - l;
}
public boolean isoone() { return _current < O l l _current>= _list.size();
}
public void next() { ++_current;
}
public void previous() { --_current;
}
public object current() throws IteratoroutofsoundsException { if (isoone()) {
throw new IteratoroutofsoundsException();
} return _list.get(_current);
}
5. gyakorlat megoldása
566
private Element getElement(int index) { if (index < _size l 2) {
}
return getElementForwards(index);
} else { return getElementBackwards(index);
}
private Element getElementForwards(int index) { Element element = _headAndTail.getNext();
}
for (int i = index; i > O; --i ) { element = element.getNext();
}
return �lement;
private Element getElementBackwards(int index) { Element element = headAndTail;
A gyakorlatok megoldásai
forCi�1 = _si�index; i > o; --i)� { element = element.getPrevious();
}
return element;
l____________ _,
6. gyakorlat megoldása
public int indexof(object value) { �
}
assert value != null : "value nem lehet null";
int index = O; Iterator i = iterator();
for (i.first(); !i.isoone(); i.next()) { if (value.equals(i.current())) {
return index; }
++index; }
return -1;
7. gyakorlat megoldása
package corri.wrox.algorit:hms.lists;
import com.wrox.algorithms.iteration.Emptyiterator; import com.wrox.algorithms.iteration.Iterator;
public final class EmptyList implements List { public static final EmptyList INSTANCE = new EmptyList();
private Emptylist() { }
public void insert(int index, object value) throws IndexoutofsoundsException {
throw new unsupportedOperationException(); }
public void add(Object value) { throw new UnsupportedOperationException();
}
public object delete(int index) throws IndexoutofsoundsException {
throw new unsupportedOperationException(); l
567
public boolean delete(object value) { throw new unsupportedoperationException();
}
public void clear() {
}
public object set(int index, object value)
}
throws IndexoutofsoundsException { throw new unsupportedoperationException();
public object get(int index) throws IndexoutofsoundsException { throw new unsupportedoperationException();
}
public int indexof(object value) { return -1;
}
public boolean contains(Object value) { return false;
}
public int size() { return O;
}
public boolean isEmpty() { return true;
}
public Iterator iterator() { return Emptylterator.INSTANCE;
}
4. fejezet
Gyakorlatok
1. Valósítsunk meg olyan szálbiztos sort, amelyben nincs várakozás! Néhány eset
ben nem lesz másra szükségünk, rnint egy többszálas környezetben működő
blokkolás nélküli sorra.
2. V alósítsunk meg egy sort, amely véletlen sorrendben olvassa vissza az értékeket!
568
A feladat egy csomag kártya lapjainak kiosztásához vagy bármely más, véletlen
kiválasztási folyamattal járó helyzethez hasonlatos.
A gyakorlatok megoldásai
1. gyakorlat megoldása
_ _ p_a_ c'7k_a _g _e_c _o _m _.-w-r� ox- . a""'l:;-g_o_r- ,;-,. t,.-;h_m_s_._q_u_e _u _e _s,..;------,-�,,-��··· - .,.....,""l'.t-t
public class SynchronizedQueue implements Queue { private final Object _mutex = new Object(); private final Queue _queue;
}
public synchronizedQueue(Queue queue) { assert queue != null : "a sor nem lehet üres"; _queue = queue;
}
public void enqueue(object value) { synchronized (_mutex) {
_queue.enqueue(value); }
}
public Object dequeue() throws EmptyQueueException { synchronized (_mutex) {
return _queue.dequeue(); }
}
public void clear() { synchronized (_mutex) {
_queue.clear(); }
}
public int size() { synchronized (_mutex) {
return _queue.size(); }
}
public boolean isEmpty() { synchronized (_mutex) {
return _queue.isEmpty(); }
}
2. gyakorlat megoldása
�package com.wrox.algorithms.queues;
import com.wrox.algorithms.lists.LinkedList; .1l!port c()m. w ro x,. a lgori thms. l i st:s. L i st;
569
A gyakorlatok megoldásai
public class RandomListQueue implements Queue { private final List _list;
}
public RandomListQueue() { this(new LinkedList());
}
public RandomListQueue(List list) {
}
assert list != null : "a lista nem lehet üres"; _list = list;
public void enqueue(object value) { _list.add(value);
}
public object dequeue() throws EmptyQueueException { if (isEmpty()) {
throw new EmptyQueueException();
} return _list.delete((int) (Math.random() * size()));
}
public void clear() { _list.clear();
}
public int size() { return _list.size();
}
public boolean isEmpty() { return _list.isEmpty();
}
6. fejezet
Gyakorlatok
l. Írjunk egy tesztet, amellyel bizonyítjuk, hogy minden fenti algoritmus tudja rendezni a véletlenszerűen generált kétszerezett objektumok listáját.
2. Írjunk egy tesztet, amellyel bizonyítjuk, hogy a fejezetben bemutatott buborékrendezéses és beszúrásos rendezési algoritmus stabil.
3. Írjunk egy összehasonlítót, amely ábécérendbe tudja rendezni a sztringeket, és nem tesz különbséget a kis- és a nagybetűk között.
4. Írjunk egy illesztőprogramot annak eldöntésére, hány objektumot mozgatnak meg az egyes algoritmusok a rendezési művelet során.
570
A gyakorlatok megoldásai
1. gyakorlat megoldása
publ1C:C lass ListSorterRandomooublesTest extends Testcase { private static final int TEST_SIZE = 1000;
l
private final List _randomList = new ArrayList(TEST_SIZE); private final Naturalcomparator _comparator =
Naturalcomparator.INSTANCE;
protected void setUp() throws Exception { super. setup();
for (int i = l; i < TEST_SIZE; ++i) { _randomList.add(new oouble((TEST_SIZE * Math.random())));
} }
public void testsortingRandomooubleswithBubblesort() { Listsorter listsorter = new BubblesortListsorter(_comparator); List result = listSorter.sort(_randomList); assertsorted(result);
}
public void testsortingRandomooubleswithselectionsort() { Listsorter listsorter =
}
new selectionsortListsorter(_comparator); List result = listsorter.sort(_randomList); assertsorted(result);
public void testsortingRandomooubleswithinsertionsort() { ListSorter listsorter =
}
new InsertionSortListsorter(_comparator); List result = listsorter.sort(_randomList); assertsorted(result);
private void assertsorted(List list) {
}
for (int i =l; i < list.size(); i++) {
}
object o = list.get(i); assertTrue(_comparator.compare(list.get(i - l),
list.get(i)) <=O);
2. gyakorlat megoldása
com. w ro x -:al go ri thiiis-: l i sts .Ar.rayL i st;
com.wrox.algorithms.lists.List; ·unit.framework.Testcase;
571
A gyakorlatok megoldásai
572
public class ListsorterstabilityTest extenas Testcase { private static final int TEST_SIZE = 1000;
private final List _list= new ArrayList(TEST_SIZE); private final comparator _comparator = new Fractioncomparator();
protected void setUp() throws Exception { super. setUp();
}
for (int i = l; i < TEST_SIZE; ++i) { _list.add(new Fraction(i %20, i));
}
public void testStabilityOfBubblesort() {
}
Listsorter listSorter = new BubblesortListsorter(_comparator); List result = listsorter.sort(_list); assertstablesorted(result);
public void teststabilityOfinsertionsort() { Listsorter listsorter =
}
new InsertionsortListsorter(_comparator); List result = listsorter.sort(_list); assertStablesorted(result);
private void assertstablesorted(List list) {
}
for (int i = l ; i < list.size(); i++) { Fraction fl= (Fraction) list.get(i - l); Fraction f2 = (Fraction) list.get(i); if(!(fl.getNumerator() < f2.getNumerator()
} }
l l fl.getoenominator() < f2.getoenominator())) { fail("what?!");
private static class Fraction { private final int _numerator; private final int _denominator;
public Fraction(int numerator, int denominator) { _numerator = numerator; _denominator = denominator;
}
public int getNumerator() { return _numerator;
}
A gyakorlatok megoldásai
}
}
publ fc-int getDenami nator()-{
return _denominator;
}
private static class Fractioncomparator implements comparator { public int compare(object left, object right)
}
throws ClasseastException { return compare((Fraction) left, (Fraction) right);
}
private int compare(Fraction l, Fraction r) throws classeastException {
return l.getNumerator() - r.getNumerator();
}
3. gyakorlat megoldása
public final class casernsensitivestringcomparator implements comparator {
l
public int compare(Object left, object right) throws classeastException {
assert left != null : "left nem lehet null"; assert right != null : "right nem lehet null";
}
String leftLower = ((String) left).toLowercase(); string rightLower =((String) right).toLowercase(); return leftLower.compareTo(rightLower);
4. gyakorlat megoldása -- public class ListsortercallcountingListTest extends Testcase {
private static final int TEST_SIZE = 1000;
private final List _sortedArrayList = new ArrayList(TEST_SIZE); private final List _reverseArrayList = new ArrayList(TEST_SIZE); private final List _randomArrayList = new ArrayList(TEST_SIZE);
private comparator _comparator = Naturalcomparator.INSTANCE;
protected void setup() throws Exception { super. setup() ;
for (int i = l; i < TEST_SIZE; ++i) { _sortedArrayList.add(new Integer(i));
l
573
A gyakorlatok megoldásai
574
}
for (int i = TEST_SIZE; i > 0; --i) { _reverseArrayList.add(new Integer(i));
}
for (int i = l; i < TEST_SIZE; ++i) { _randomArrayList.add(new rnteger((int)
(TEST_SIZE * Math.random()))); }
public void testworstcasesubblesort() {
}
List list= new callcountingList(_reverseArrayList); new subblesortListsorter(_comparator).sort(list); reportcalls(list);
public void testworstcaseselectionsort() {
}
List list= new callcountingList(_reverseArrayList); new selectionsortListsorter(_comparator).sort(list); reportcalls(list);
public void testworstcasernsertionsort() { List list = _reverseArrayList;
}
List result = new callcountingList(new ArrayList()); new rnsertionsortListsorter(_comparator).sort(list, result); reportcalls(result);
public void testsestcasesubblesort() {
}
List list= new callcountingList(_sortedArrayList); new BubblesortListSorter(_comparator).sort(list); reportcalls(list);
public void testsestcaseselectionsort() {
}
List list= new callcountingList(_sortedArrayList); new selectionsortListsorter(_comparator).sort(list); reportcalls(list);
public void testBestCasernsertionSort() { List list = _sortedArrayList;
}
List result = new callcountingList(new ArrayList()); new rnsertionSortListsorter(_comparator).sort(list, result); reportcalls(result); ·
public void testAveragecasesubblesort() {
J.
List list = new callcountingList(_randomArrayList); new BubblesortListSorter(_comparator).sort(list); reportcalls(list);
A gyakorlatok megoldásai
}
public voio testAveragecaseselectionsor-t()-{
}
List list = new callcountingList(_randomArrayList); new selectionsortListsorter(_comparator).sort(list); reportcalls(list);
public void testAveragecaseinsertionsort() { List list = _randomArrayList;
}
List result = new callcountingList(new ArrayList()); new InsertionsortListsorter(_comparator).sort(list, result); reportcalls(result);
private void reportCalls(List list) { System.out.println(getName() + " · " +list);
}
7. fejezet
Gyakorlatok
l. Valósítsuk meg az összefésüléses rendezést iteratív módon, ahelyett hogy rekurzív módon tennénk!
2. Valósítsuk meg a gyorsrendezést rekurzív helyett iteratív módon!
3. Soroljuk fel a listakezelés módszereit (például set(), add(), insert()) a gyorsrendezés és a Shell-rendezés esetében!
4. Valósítsuk meg a beszúrásos rendezés egy belső verzióját!
5. Hozzuk létre a gyorsrendezés egy olyan verzióját, amely beszúrásos rendezést alkalmaz olyan részlistáknál, amelyek kevesebb mint öt elemből állnak!
l. gyakorlat megoldása
pulilic elass IterativeMergesortListsorter implements Listsorter { private final comparator _comparator;
public IterativeMergesortListsorter(Comparator comparator) { assert comparator != null : "comparator nem lehet null"; _comparator = comparator;
}
public List sort(List list) { assert list != null : "list nem lehet null";
return mergesublists(createsublists(list));
}
575
A gyakorlatok megoldásai
576
private List mergeSublists(List sublists) { List remaining = sublists; while (remaining.size() > l) {
remaining = mergesublistPairs(remaining);
} return (List) remaining.get(O);
}
private List mergesublistPairs(List remaining) {
}
List result =new ArrayList(remaining.size() l 2+ l);
Iterater i = remaining.iterator(); i. first(); while (!i.isoone()) {
}
List left = (List) i.current(); i .next(); if (i.isoone()) {
result.add(left); } else {
}
List right =(List) i.current(); i. next(); result.add(merge(left, right));
return result;
private List createsublists(List list) { List result =new ArrayList(list.size());
}
Iterater i = list.iterator(); i. first(); while (!i.isoone()) {
}
List singletonList = new ArrayList(l); singletonList.add(i.current()); result.add(singletonList); i .next();
return result;
private List merge(List left, List right) { List result =new ArrayList();
Iterater l = left.iterator(); Iterator r = rig� h�t �.,�· t�e�r� a�t�o �r �()�; �--------------------------�
A gyakorlatok megoldásai
} l
1-:-fi rst:(); r. first:();
while (!(l.isoone() && r.isoone())) { if (l.isoone()) {
result.add(r.current());
}
r. next(); } else if (r.isoone()) {
result.add(l.current()); l.next();
} else if (_comparator.compare(l.current(), r.current()) <= O) {
result.add(l.current:()); l.next();
} else { result.add(r.current:()); r. next:();
}
ret:urn result;
2. gyakorlat megoldása
publ i c cl ass Iter.ativeQui cksortL istscirte·r-·i"mpl emen ts- Listsorter ·"{ privat:e final Comparator _comparat:or;
public It:erativeQuicksort:Listsorter(Comparator comparator) { assert comparator != null : "comparator nem lehet null"; _comparator = comparat:or;
}
public List sort:(List: list:) { assert list != null : "list nem lehet null";
quicksort:(list);
return list:; }
private void quicksort(List list) { Stack jobstack = new Listst:ack();
jobStack.push(new Range(O, list.size() - l));
while (!jobstack.isEmpty()) { Range range = (Range) jobstack.pop(); if (range.size() <= l) {
continue; l
577
A gyakorlatok megoldásai
578
} }
int startindex = range.getStartindex(); int endindex range.getEndindex();
Object value list.get(endindex);
int partition = partition(list, value, startindex, endindex - l);
if (_comparator.compare(list.get(partition), value) <O) { ++parti ti on;
}
swap(list, partition, endindex);
jobstack.push(new Range(startindex, partition - l)); jobstack.push(new Range(partition +l, endindex));
private int partition(List list, object value, int leftindex,
int rightindex) {
}
int left = leftindex; int right = rightindex;
while (left < right) {
}
if (_comparator.compare(list.get(left), value) <O) { ++left;
continue;
}
if (_comparator.compare(list.get(right), value) >=O) { --right; continue;
}
swap(list, left, right);
++left;
return left;
private void swap(List list, int left, int right) { if (left == right) {
J.
return;
} object temp = list.get(left); list.set(left, list.get(right)); list.set(right, temp);
},
private static final-class Range { private final int _startindex;
private final int _endindex;
}
public Range(int startindex, int endindex) { _startindex = startindex;
_endindex = endindex;
}
public int size() { return _endindex - _startindex + l;
}
public int getstartindex() { return _startindex;
}
public int getEndindex() { return _endindex;
}
A gyakorlatok megoldásai
3. gyakorlat megoldása
public class AdvancedListsortercallcountingCistTest extends Testcase {
private static final int TEST_SIZE = 1000;
private final List _sortedArrayList = new ArrayList(TEST_SIZE); private final List _reverseArrayList = new ArrayList(TEST_SIZE);
private final List _randornArrayList = new ArrayList(TEST_SIZE);
private comparator _comparator = Naturalcomparator.INSTANCE;
protected void setUp() throws Exception { super. setup();
}
for (int i = l; i < TEST_SIZE; ++i) { _sortedArrayList.add(new Integer(i));
}
for (int i = TEST_SIZE; i > 0; --i) { _reverseArrayList.add(new Integer(i));
}
for (int i = l; i < TEST_SIZE; ++i) { _randornArrayList.add(new Integer((int)
(TEST_SIZE * Math.random())));
}
579
A gyakorlatok megoldásai
580
}
public void testworstcaseQuicksort() {
}
List list= new callcountingList(_reverseArrayList); new QuicksortListSorter(_comparator).sort(list); reportcalls(list);
public void testworstcaseshellsort() {
}
List list= new callcountingList(_reverseArrayList); new ShellsortListSorter(_comparator).sort(list); reportcalls(list);
public void testBestcaseQuicksort() {
}
List list= new callcountingList(_sortedArrayList); new QuicksortListsorter(_comparator).sort(list); reportCalls(list);
public void testsestcaseshellsort() {
}
List list= new callcountingList(_sortedArrayList); new shellsortListsorter(_comparator).sort(list); reportcalls(list);
public void testAveragecaseQuicksort() {
}
List list= new callcountingList(_randomArrayList); new QuicksortListsorter(_comparator).sort(list); reportcalls(list);
public void testAverageCaseshellsort() {
}
List list= new callcountingList(_randomArrayList); new ShellsortListSorter(_comparator).sort(list); reportcalls(list);
private void reportcalls(List list) { system.out.println(getName() + " · " +list);
}
public class callcountingList implements List { private final List _list;
private int _insertcount; private int _addcount; private int _deletecount; private int _getcount; private int setcount;
A gyakorlatok megoldásai
public Calleounti ngL ist(List li st)-
{
}
assert list != null : "list nem lehet null"; _list = list;
public void insert(int index, object value) throws IndexoutOfBoundsException {
_list.insert(index, value); ++_insertcount;
}
public void add(Object value) { _list.add(value); ++_addcount;
}
public object delete(int index)
}
throws IndexoutOfBoundsException { ++_deletecount; return _list.delete(index);
public object delete(Object value) { ++_deletecount; return _list.delete(value);
}
public object get(int index) throws IndexoutOfBoundsException { ++_getcount; return _list.get(index);
}
public object set(int index, object value) throws rndexoutOfBoundsException {
++_setcount;
return _list.set(index, value);
}
public void clear() { _list.clear();
}
public int indexof(Object value) { return _list.indexof(value);
}
public boolean contains(object value) { return _list.contains(value);
}
581
A gyakorlatok megoldásai
}
public boolean isEmpty() { return _list.isEmpty();
}
public Iterator iterator() { return _list.iterator();
}
public int size() { return _list.size();
}
public String tostring() {
}
return new stringsuffer("call-counting List: ") .append("adja hozzá:" + _addcount) .append("sz�rja be:" + _insertcount) .append("törölje:" + _deletecount) .append("állitsa be:" + _setcount) . append("kérje le:" + _getcount). toString();
4. gyakorlat megoldása
582
public class InPlaceinsertionsortListsorter implements Listsorter { private final comparator _comparator;
}
public InPlaceinsertionsortListsorter(Comparator comparator) { assert comparator != null : "comparator nem lehet null"; _comparator = comparator;
}
public List sort(List list) {
}
assert list!= null : "list nem lehet null";
for (int i =l; i < list.size(); ++i) { object value = list.get(i);
}
int j; for (j= i; j> O; --j) {
}
object previousvalue = list.get(j - l); if (_comparator.compare(value, previousvalue) >= O) {
break;
} list.set(j, previousvalue);
list.set(j, value);
return list;
A gyakorlatok megoldásai
5. gyakorlat megoldása
public class HybridQuicksörtListsorter implements Listsorter private final Comparator _comparator;
public HybridQuicksortListsorter(comparator comparator) { assert comparator != null : "comparator nem lehet null"; _comparator = comparator;
}
public List sort(List list) { assert list != null : "list nem lehet null";
quicksort(list, O, list.size() - l);
return list; }
private void quicksort(List list, int startindex, int endindex) {
if (startindex < O l l endindex >= list.size()) {
}
return; }
if (endindex <= startindex) { return;
}
if (endindex - startindex < 5) { doinsertionsort(list, startindex, endindex);
} else { doQuicksort(list, startindex, endindex);
}
private void doinsertionsort(List list, int startindex, int endindex) {
for (int i = startindex + l; i <= endindex; ++i) { object value = list.get(i);
}
int j; for (j= i; j> startindex; --j) {
object previousvalue = list.get(j - l);
}
if (_comparator.compare(value, previousvalue) >= O) { break;
}
list.set(j, previousvalue);
list.set(j, value);
} .�-----------------�----------
583
A gyakorlatok megoldásai
}
584
private void doQuicksort(List list, int startindex, int endindex) {
}
object value = list.get(endindex);
int partition = partition(list, value, startindex, endindex - l);
if (_comparator.compare(list.get(partition), value) <0) { ++parti ti on;
}
swap(list, partition, endindex);
quicksort(list, startindex, partition - l); quicksort(list, partition +l, endrndex);
private int partition(List list, object value, int leftindex, int rightrndex) {
}
int left = leftindex; int right = rightindex;
while (left < right) {
}
if (_comparator.compare(list.get(left), value) <O) { ++left; continue;
}
if (_comparator.compare(list.get(right), value) >=0) { --ri ght; continue;
}
swap(list, left, right); ++left;
return left;
private void swap(List list, int left, int right) { if (left == right) {
}
return;
} object temp = list.get(left); list.set(left, list.get(right)); list.set(right, temp);
A gyakorlatok megoldásai
8. fejezet
Gyakorlatok
1. Prioritásos sor felhasználásával valósítsunk meg egy stack-et!
2. Prioritásos sor felhasználásával valósítsunk meg egy FI FO-sort!
3. Prioritásos sor felhasználásával valósítsunk meg egy L istSorter objektumot!
4. Készítsünk prioritásos sort, amely a legnagyobb elem helyett a legkisebb elem
hez biztosít hozzáférést!
1. gyakorlat megoldása
package com.wrox.algorit
import com.wrox.algorithms.queues.EmptyQueueException; import com.wrox.algorithms.queues.HeaporderedListPriorityQueue; import com.wrox.algorithms.sorting.comparator;
public class PriorityQueuestack extends HeapOrderedListPriorityQueue implements stack {
private final static comparator COMPARATOR =
private long _count = O;
public PriorityQueuestack() { super(COMPARATOR);
}
new Stackitemcomparator();
public void enqueue(Object value) { super.enqueue(new stackitem(++_count, value));
}
public object dequeue() throws EmptyQueueException { return ((Stackitem) super.dequeue()).getvalue();
}
public void push(object value) { enqueue(value);
}
public object pop() throws EmptystackException { try {
return dequeue(); } catch (EmptyQueueException e) {
throw new EmptystackException(); }
} --
585
A gyakorlatok megoldásai
}
public object peek() throws EmptyStackException { object result =pop();
push(result);
return result;
}
private static final class Stackrtem {
private final long _key;
}
private final object _value;
public Stackrtem(long key, object value) {
_key = key; _value = value;
}
public long getKey() { return _key;
}
public Object getvalue() { return _value;
}
private static final class Stackrtemcomparator
implements comparator {
public int compare(Object Jeft, object right) throws classeastException {
Stackrtem sil (Stackrtem) left; Stackrtem si2 = (Stackrtem) right;
return (int) (sil.getKey() - si2.getKey());
} }
2. gyakorlat megoldása
586
pacKage com.wrox.algorithms.queues;
import com.wrox.algorithms.sorting.comparator;
public class PriorityQueueFifoQueue extends HeaporderedListPriorityQueue {
private static final Comparator COMPARATOR =
new Queuertemcomparator(); private long _count = Long.MAX_VALUE;
public PriorityQueueFifoQueue() {
super(COMPARATOR);
J.
A gyakorlatok megoldásai
publ"i"c voia�enqueue(Ób-je"ct value)�{ super.enqueue(new QUeueitem(--_count, value));
}
public object dequeue() throws EmptyQueueException { return ((Queueitem) super.dequeue()).getvalue();
}
private static final class Queueitem { private final long _key;
}
private final object _value;
public Queuertem(long key, Object value) { _key = key; _value = value;
}
public long getKey() { return _key;
}
public object getvalue() { return _value;
}
private static final class Queueitemcomparator implements Comparator {
public int compare(object left, object right) throws classeastException {
Queueitem sil = (Queueitem) left;
}
_}_
}
Queuertem si2 = (Queueitem) right;
return (int) (sil.getKey() - si2.getKey());
3. gyakorlat megoldása
�ublic class PriorityQueueListsorter implements Listsorter { private final comparator _comparator;
public PriorityQueueListsorter(Comparator comparator) { assert comparator != null : "comparator nem lehet null"; _comparator = comparator;
}
public List sort(List list) { assert l i st ! = null : "l i st nem_lehet_nuU"..;
587
A gyakorlatok megoldásai
}
}
Queue queue createPriorityQueue(list);
List result new ArrayList(list.size());
while (!queue.isEmpty()) { result.add(queue.dequeue());
}
re tu r n res u l t;
private Queue createPriorityQueue(List list) {
}
comparator comparator = new Reversecomparator(_comparator); Queue queue = new HeapOrderedListPriorityQueue(comparator);
Iterator i = list.iterator(); i.first(); while (!i.isoone()) {
queue.enqueue(i.current()); i .next();
}
return queue;
4. gyakorlat megoldása
public class MinimumorientedHeaporderedListPriorityQueue extends HeaporderedListPriorityQueue {
}
public MinimumorientedHeaporderedListPriorityQueue(comparator comparator) {
super(new Reversecomparator(comparator)); }
10. fejezet
Gyakorlatok
1. Írjuk meg a mi ni mum() rekurzív változatát!
2. Írjuk meg a search() rekurzív változatát!
3. Írjunk egy metódust, amely fogja a gyökércsomópontot, és rekurzív módon, rendezett formában kiírja a fában található valamennyi adatot!
4. Írjunk egy metódust, amely fogja a gyökércsomópontot, és iteratív módon, rendezett formában kiírja a fában található valamennyi adatot!
5. Írjunk egy metódust, amely fogja a gyökércsomópontot, és rekurzív módon, preorder formában kiírja a fában található valamennyi adatot!
588
A gyakorlatok megoldásai
6. Írjunk egy metódust, amely fogja a gyökércsomópontot, és rekurzív módon, postorder formában kiírja a fában található valamennyi adatot!
7. Írjunk egy metódust, vagy metódusokat, amelyek egy rendezett listából adatokat szúrnak be a bu;_áris keresőfába úgy, hogy fenntartják az egyensúlyi állapotot, anélkül hogy explicit kiegyensúlyozásta volna szükség!
8. Adjunk metódus(ok)at a Node-hoz a méretének rekurzív módon történő kiszámításához!
9. Adjunk metódus(ok)at a Node-hoz a magasságának rekurzív módon történő kiszámításához!
1. gyakorlat megoldása
public Node minimum() { return getsmaller() != null ? Getsmaller()
2. gyakorlat megoldása
public Node searcn(Öbject value)-{ return search(value, _root);
}
private Node search(Object value, Node node) { if (node == null) {
return null; }
this;
int cmp = _comparator.compare(value, node.getvalue()); if (cmp == O) {
return node; }
return search(value, cmp < O ? node.getsmaller() : node.getLarger());
}
3. gyakorlat megoldása
pulil.i c void-i nOrderPri nt(Node node)- { if (node == null) {
l
return; }
inorderPrint(node.getsmaller()); system.out.println(node.getvalue()); inorderPrint(node.getLarger()));
589
A gyakorlatok megoldásai
4. gyakorlat megoldása
public void inoroerPrint(Nooe root) { for (Node node = root.minimum(); node
node system.out.println(node.getvalue());
}
!= null;
node.successor()) {
}------------------------------------------------�
5. gyakorlat megoldása
public voia preorderPrint(Node node) { if (node == null) {
}
return;
}
System.out.println(node.getvalue()); preorderPrint(node.getsmaller()); preorderPrint(node.getLarger()));
6. gyakorlat megoldása
pubiic void postorderPrint(Node node) { if (node == null) {
}
return;
}
postorderPrint(node.getsmaller()); postorderPrint(node.getLarger())); system.out.println(node.getvalue());
7. gyakorlat megoldása
590
public void preorderinsert(BinarysearchTree tree, List list) { preorderinsert(tree, list, 0, list.size() - l);
}
private void preorderinsert(BinarysearchTree tree, List list, int lowerindex,int upperindex) {
}
if (lowerindex > upperindex) { return;
}
int index = lowerindex + (upperindex - lowerindex) l 2;
tree.insert(list.get(index)); preorderinsert(tree, list, lowerindex, index - l); preorderinsert(tree, list, index + l, upperindex);
8. gyakorlat megoldása
private int size(Node node) { if (node == null) {
return O; }
A gyakorlatok megoldásai
return l+ size(node.getsmaller()) + size(node.getLarger());
1------------------------------------�
9. gyakorlat megoldása
public-i"nt�
neigntO- {
return height(this) - l; }
private int height(Node node) { if (node == null) {
}
return O; }
return l+ Math.max(height(node.getSmaller()), height(node.getLarger()));
11. fejezet
Gyakorlatok
1. Módosítsuk a Bucketi ngHashtabl e-t úgy, hogy csak prímszámot adunk meg a
vödrök számának. Milyen hatása van (ha van) ennek a teljesítményre?
2. Módosítsuk a L i nearProbi ngHashtabl e-t úgy, hogy tárolja az értékek számát a
táblában, mintsem hogy számolnunk kelljen minden alkalommal.
3. Módosítsuk a Bucketi ngHashtabl e-t úgy, hogy tárolja az értékek számát a táb
lában, mintsem hogy számolnunk kelljen minden alkalommal.
4. Készítsünk iterátort, amely hozzáférést biztosít a Bucketi ngHashtabl e-ben az
összes bejegyzéshez.
591
A gyakorlatok megoldásai
1. gyakorlat megoldása
592
package com.wrox.algorithms.hashing;
public final class SimplePrimeNumberGenerator {
}
public static final SimplePrimeNumberGenerator INSTANCE
new simplePrimeNumberGenerator();
private SimplePrimeNumberGenerator() {
}
public int generate(int candidate) { int prime = candidate;
while (!isPrime(prime)) { ++prime;
}
return prime;
}
private boolean isPrime(int candidate) {
}
for (int i = candidate l 2; i >= 2; --i) {
if (candidate % i == O) { return false;
} } return true;
package com.wrox.algorithms.hashing;
import com.wrox.algorithms.iteration.Iterator;
import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
public class BucketingHashtable implements Hashtable {
}
public BucketingHashtable(int initialcapacity, float loadFactor) {
}
assert initialcapacity > O : ."initialcapacity nem lehet < l"; assert loadFactor >O : "loadFactor nem lehet <= O";
_loadFactor = loadFactor; _buckets = new Bucket[
SimplePrimeNumberGenerator.INSTANCE.generate(initialCapacity)];
A gyakorlatok megoldásai
2. gyakorlat megoldása
package com.wrox.algorithms.hasning;
public class LinearProbingHashtable implements Hashtable {
}
private int _size;
public void add(Object value) { ensureCapacityForOneMore();
}
int index = indexFor(value);
if (_values[index] == null) { _values[index] = value; ++_size;
}
public int size() { return _size;
}
3. gyakorlat megoldása
package com.wrox.algorithms.hash1ng;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List;
public class BucketingHashtable implements Hashtable {
private int _size;
public void add(Object value) { List bucket = bucketFor(value);
}
if (!bucket.contains(value)) { bucket.add(value); ++_size; mai n ta i n Load();
}
public int size() { return _size;
}
593
A gyakorlatok megoldásai
4. gyakorlat megoldása
594
package com.wrox.algorithms.hashing;
import com.wrox.algorithms.iteration.Emptyiterator; import com.wrox.algorithms.iteration.Iterable;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutofBoundsException;
public class Hashtableiterator implements Iterator { private final Iterator _buckets;
private Iterator _values = Emptyiterator.INSTANCE;
public Hashtablerterator(Iterator buckets) {
}
assert buckets != null : "a vödrök száma nem lehet null"; _buckets = buckets;
public void first() {
_buckets.first();
}
_values = Emptyrterator.INSTANCE; next();
public void last() { _buckets.last();
}
_values = Emptyrterator.INSTANCE; previous();
public boolean isoone() { return _values.isDone() && _buckets.isoone();
}
public void next() { for (_values.next();
}
}
_values.isoone() && !_buckets.isoone();
_buckets.next()) { rterable bucket = (Iterable) _buckets.current(); if (bucket != null) {
}
_values = bucket.iterator();
_values.first();
public void previous() { for (_values.previous();
_values.isDone() && !_buckets.isoone(); _buckets.previous()) {
Iterable bucket = (Iterable) buckets.current();
A gyakorlatok megoldásai
}
} }
if-Cbucket !=·null)-{
}
_values = bucket.iterator(); _values .l ast();
public Object current() throws IteratorOutOfBoundsException { if (isoone()) {
throw new IteratoroutOfBoundsException(); } return _values.current();
}
12. fejezet
Gyakorlatok
1. Írjunk olyan metódust, amely két halmazról megállapítja, egyenlőek-el
2. Írjuk meg a két halmaz unióját megvalósító metódust!
3. Írjuk meg a két halmaz metszetét megvalósító metódust!
4. Írjuk meg a két halmaz különbségét megvalósító metódust!
5. Írjuk át a Has h set osztály de l e te ()metódusát úgy, hogy ha a vödör üres, a metódus szabadítsa fel!
6. Hozzunk létre rendezett lista alapú halmazmegvalósítást!
7. Hozzunk létre egy olyan halmazt, amely egyfolytában üres marad, ha módosítani kívánunk rajta, akkor pedig okozzon unsupportedoperati onExcepti on kivételt!
1. gyakorlat megoldása
public boolean equals(set a, set b) { assert a != null : "a nem lehet null"; assert b != null : "b nem lehet null";
}
Iterator i = a.iterator(); for (i.first(); !i.isoone(); i.next()) {
if (!b.contains(i.current())) { return false;
} }
return a.size() b. size();
595
A gyakorlatok megoldásai
2. gyakorlat megoldása
public Set union(Set a, Set b) {
}
assert a ! = null "a nem lehet assert b != null : "b nem lehet
set result new Hashset();
Iterator i a.iterator();
null";
null";
for (i.first(); !i.isoone(); i.next()) {
result.add(i.current());
}
Iterator j = b.iterator();
for (j.first(); !j.isoone(); j.next()) {
result.add(j.current());
}
return result;
3. gyakorlat megoldása
public set intersection(Set a, set b) {
assert a != null "a nem lehet. null";
assert b != null : "b nem lehet null";
set result new Hashset();
Iterator i a.iterator();
for (i.first(); !i.isoone(); i.next()) { if (b.contains(i.current())) {
result.add(i.current());
} }
return result;
}-------�--------------------------------------�
4. gyakorlat megoldása
596
public set difference(Set a, Set b) {
assert a != null "a nem lehet null";
assert b != null : "b nem lehet null";
Set result new Hashset();
Iterator i a.iterator();
A gyakorlatok megoldásai
�for Ci � fi"rst"o;··
·!i.isooneo;· i.nextO) {
if (!b.contains(i.current())) { result.add(i.current());
} }
return result;
_}_
5. gyakorlat megoldása
public boolean delete(Öbject value)-
{ int bucketindex = bucketindexFor(value);· ListSet bucket = _buckets[bucketindex]; if (bucket != null && bucket.delete(value)) {
--_size;
}
if (bucket.isEmpty()) { _buckets[bucketindex] = null;
} re tu r n t ru e;
return false;
}
6. gyakorlat megoldása
package com.wrox.algoritnms.sets;
import com.wrox.algorithms.bsearch.IterativeBinaryListsearcher; import com.wrox.algorithms.bsearch.Listsearcher; import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.comparator; import com.wrox.algorithms.sorting.Naturalcomparator;
public class sortedListset implements set { private final List _values = new ArrayList(); private final Listsearcher _searcher;
public sortedListset() { this(Naturalcomparator.INSTANCE);
}
public sortedListset(comparator comparator) { _searcher = new IterativeBinaryListsearcher(comparator);
}
public boolean contains(object value) { return indexof(value) >= O;
}
597
A gyakorlatok megoldásai
}
public ooolean add(Object value) {
}
int index = indexof(value); if (index < 0) {
_values.insert(-(index +l), value); return true;
}
_values.set(index, value); return false;
public boolean delete(object value) { int index = indexof(value);
}
if (index >= O) { _values.delete(index); return true;
}
return false;
public Iterator iterator() { return _values.iterator();
}
public void clear() { _values.clear();
}
public int size() { return _values.size();
}
public boolean isEmpty() { return _values.isEmpty();
}
private int indexof(object value) { return _searcher.search(_values, value);
}
7. gyakorlat megoldása
598
package com.wrox.algorithms.sets;
import com.wrox.algorithms.iteration.Emptyiterator; import com.wrox.algorithms.iteration.Iterator;
public final class Emptyset implements set { public static final EmpXy>et INSTANCE = new EmptyuS�e�t�(�)�;--------�
}
private Emptyset()-
{
}
public boolean contains(Object value) { return false;
}
public boolean add(Object value) { throw new unsupportedoperationException();
}
public boolean delete(object value) { throw new unsupportedoperationException();
}
public void clear() { }
public int size() { return O;
}
public boolean isEmpty() { return true;
}
public Iterator iterator() { return Emptyiterator.INSTANCE;
}
13. fejezet
Gyakorlatok
A gyakorlatok megoldásai
1. Hozzunk létre egy iterátort, amely csak a leképezésben lévő kulcsokkal tér vissza!
2. Hozzunk létre egy iterátort, amely csak a leképezésben lévő értékekkel tér vissza!
3. Hozzunk létre egy olyan halmazmegvalósítást, amely mögöttes tárolási mecha
nizmusaként használ leképezést az értékek számára!
4. Hozzunk létre egy olyan üres leképezést, amely unsupportedoperationException
kivételt okoz, ha módosítási kísérletet akarunk rajta végrehajtani!
599
A gyakorlatok megoldásai
1. gyakorlat megoldása
package com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutofsoundsException;
public class MapKeyiterator implements Iterator { private final Iterator _entries;
}
public MapKeyiterator(Iterator entries) { assert entries != null : "az entries nem lehet null"; _entries = entries;
}
public void first() { _entries.first();
}
public void last() { _entries.last();
}
public boolean isoone() { return _entries.isoone();
}
public void next() { _entries.next();
}
public void previous() { _entries.previous();
}
public object current() throws IteratoroutofsoundsException { return ((Map.Entry) _entries.current()).getKey();
}
2. gyakorlat megoldása
package com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.iteration.IteratoroutofsoundsException;
public class Mapvaluerterator implements Iterator { private final Iterator e�n� t�r� i�e �s�; ______________________ ._"---�--�
600
A gyakorlatok megoldásai
}
publ. i c Mapval uerterator(Iterator en t d es)·- {
assert entries l= null : "entries nem lehet null"; _entries = entries;
}
public void first() { _entries.first();
}
public void last() { _entries.last();
}
public boolean isoone() { return _entries.isDone();
}
public void next() { _en tri es. next();
}
public void previous() { _entries.previous();
}
public object current() throws IteratoroutofsoundsException { return ((Map.Entry) _entries.current()).getvalue();
}
3. gyakorlat megoldása
package cóm :· wrox. a l gorithms. maps;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.sets.set;
public class Mapset implements set { private static final object PRESENT = new object();
private final Map _map;
public MapSet(Map map) { assert map != null : "map nem lehet null"; ...map = map;
}
public boolean contains(Object value) { return _map.contains(value);
l
601
A gyakorlatok megoldásai
}
public boolean aad(Object value) { return _map.set(value, PRESENT) == null;
}
public boolean delete(Object value) { return _map.delete(value) == PRESENT;
}
public Iterator iterator() { return new MapKeyiterator(_map.iterator());
}
public void clear() { _map.clear();
}
public int size() { return _map.size();
}
public boolean isEmpty() { return _map.isEmpty();
}
4. gyakorlat megoldása
602
package com.wrox.algorithms.maps;
import com.wrox.algorithms.iteration.Emptyiterator; import com.wrox.algorithms.iteration.Iterator;
public final class EmptyMap implements Map { public static final EmptyMap INSTANCE = new EmptyMap();
private EmptyMap() {
}
public object get(Object key) { return null;
}
public object set(Object key, object value) { throw new unsupportedOperationException();
}
public object delete(object key) { throw new unsupportedoperationException();
}
A gyakorlatok megoldásai
public boolean contains(öoject key)-
{ return false;
}
public void clear() {
}
public int size() { return O;
}
public boolean isEmpty() { return true;
}
public Iterator iterator() {
return Emptyrterator.INSTANCE;
} _}
14. fejezet
Gyakorlat
1. Hozzuk létre a search() egy iteratív formáját!
1. gyakorlat megoldása
private Node searctl(Node node, charSequence word, ii1�index)--
{ assert word l= null : "word nem lehet null";
l
while (node l= null) {
}
char c = word.charAt(index); if (c == node.getchar()) {
if (index + l< word.length()) { node = node.getchild();
} else { break;
} } else {
}
node = c < node.getchar() ? node.getsmaller() node.getLarger();
return node;
603
A gyakorlatok megoldásai
15. fejezet
Gyakorlatok
l. Valósítsuk meg újra a traverse() metódust a Node osztályra úgy, hogy a kul
csok szerinti sorrendben adja vissza a bejegyzéseket.
2. Valósítsuk meg újra az i ndexof() metódust a Node osztályra úgy, hogy l ineáris
keresés helyett bináris keresést végezzen.
l. gyakorlat megoldása
public void traverseCList list) {
}
assert list != null : "list nem lehet null";
Iterator children = _children.iterator(); Iterator entries = _entries.iterator();
children. first(); entri es. fi r st();
while (!children.isoone() ll !entries.isoone()) { if (!children.isoone()) {
}
}
((Node) children.current()).inorderTraversal(list); children. next();
if (!entries.isoone()) {
}
Entry entry = (Entry) entries.current(); if (!entry.isoeleted()) {
list.add(entry); } entri es. next();
2. gyakorlat megoldása
private int indexof(object key) { int lowerindex = O; int upperindex = _entries.size() - l;
while (lowerindex <= upperindex) { int index = lowerindex + (upperindex - lowerindex) l 2;
int cmp = _comparator.compare(key, --------------------------�(�(E�n�t�ry) entries .g�e�t�(�in�d�e� x�) �) �- ������ �
604
A gyakorlatok megoldásai
}
}
i t cCriip == ö)"{ return index;
} else if (cmp < 0) { upperrndex = index - l;
} else { lowerrndex
} index + l;
return -(lowerrndex +l);
18. fejezet
Gyakorlatqk
1. Valósítsuk meg a legközelebbi pár problémájának letámadásos megoldását!
2. Optimalizáljuk a síkbejárási algoritmust úgy, hogy a függőleges síkban túl mesz
sze lévő pontokat figyelmen kívül hagyjuk!
1. gyakorlat megoldása
package com.wrox.algorithms.geometry;
import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sets.Listset; import com.wrox.algorithms.sets.set; import com.wrox.algorithms.bsearch.Listrnserter; import com.wrox.algorithms.bsearch.IterativeBinaryListsearcher;
public final class BruteForceclosestPairFinder implements closestPairFinder {
public static final BruteForceclosestPairFinder INSTANCE new BruteForceclosestPairFinder();
private BruteForceclosestPairFinder() { }
public Set findclosestPair(Set points) { assert points != null : "points nem lehet null";
if (points.size() < 2) { return null;
}
L .._L i. st J,i st._=.....2.�.!'oi nts (pJ>i nt�_.;,__ ________ .
605
A gyakorlatok megoldásai
}
}
Point p= null; Point q= null; double distance Double.MAX_VALUE;
for (int i =O; i < list.size(); Í++) { Point r= (Point) list.get(i);
}
for (int j =O; j < list.size(); j++) { Point s = (Point) list.get(j);
}
if (r != s && r.distance(s) < distance) { distance = r.distance(s);
}
p r; q = s;
return createPointPair(p, q);
private static List sortPoints(set points) { assert points != null : "points nem lehet null";
}
List list= new ArrayList(points.size());
Iterater i = points.iterator(); for (i.first(); ! i.isDone(); i.next()) {
INSERTER.insert(list, i.current());
}
return list;
private Set createPointPair(Point p, Point q) { Set result =new ListSet(); result.add(p); result.add(q); return result;
}
2. gyakorlat megoldása
606
package com.wrox.algorithms.geometry;
import com.wrox.algorithms.bsearch.IterativeBinaryListsearcher; import com.wrox.algorithms.bsearch.Listinserter; import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sets.Listset; im�ort com.wrox.al orithms.sets.set·
A gyakorlatok megoldásai
publi"cfi nal cl ass Pl anesweepoptimi ze del osestPai rFinder implements closestPairFinder {
public static final PlanesweepOptimizedclosestPairFinder INSTANCE= new PlanesweepoptimizedclosestPairFinder();
private static final Listinserter INSERTER = new Listinsertere new IterativeBinaryListsearcher(XYPointcomparator.INSTANCE));
private PlanesweepOptimizedclosestPairFinder() {
}
public Set findclosestPair(Set points) {
}
assert points l= null : "points nem lehet null";
if (points.size() < 2) { return null;
}
List sortedPoints = sortPoints(points);
Point p Point q
(Point) sortedPoints.get(O); (Point) sortedPoints.get(l);
return findClosestPair(p, q, sortedPoints);
private set findclosestPair(Point p, Point q, List sortedPoints) {
Set result = createPointPair(p, q); double distance = p.distance(q); int dragPoint = O;
for (int i =2; i < sortedPoints.size(); ++i) { Point r= (Point) sortedPoints.get(i); double sweepx = r.getX(); double dragx = sweepx - distance;
while (((Point) sortedPoints.get(dragPoint)).getX() < dragX) {
++dragPoint;
}
for (int j= dragPoint; j< i; ++j) { Point test = (Point) sortedPoints.get(j);
if (Math.abs(r.getY() - test.getY()) > distance) { continue;
} double checkoistance = r.distance(t��J:).;
607
A gyakorlatok megoldásai
}
608
}
} }
if (checkoistance < distance) { distance = checkoistance; result = createPointPair(r, test);
}
return result;
private static List sortPoints(Set points) { assert points != null : "points nem lehet null";
}
List list = new ArrayList(points.size());
Iterator i = points.iterator(); for (i.first(); !i.isoone(); i.next()) {
INSERTER.insert(list, i.current());
}
return list;
private Set createPointPair(Point p, Point q) { Set result = new ListSet(); result.add(p); result.add(q); return result;
}
Tárgymutató
A,Á
a beszúrás inverze, 64 a beszúrás költsége, 484, 492 a legjobb esetbeli futásidő, 456 a legjobb esethez tartozó, 5, 222 a megtalálás valószínűsége, 303 a rendelkezésre álló memória nagysága, 90 absztrakt alap, SS
absztrakt alaposztály, SS
absztrakt halmaz, 517 absztrakt metódus, 56, 95, 146, 148,230, 311 absztrakt osztály, SS, 93, 149, 154, 158, 203, 522 absztrakt osztálygenerátor-metódus, 203, 522 absztrakt tesztelési eset, 154, 203 absztrakt tesztosztály, 95, 146,236, 240, 337,
364 adatátviteli sebesség, 424 adatbázis, 23, 35, 363, 423 adatbázis-alkalmazás, 363 adatbázis-lekérdezés, 35 adatbázisrekord-szám, 360 adathalmaz,226,301,328,423 adatlista, 163 adatpont, 19 adatstruktúra, 24, 26, 51, 79, 136, 199,207,210,
255,257,423,533 adatápus, 138,445 aktuális csomópont, 260, 261,276, 282, 284,
292,392,400,413-416,440 aktuális elem, 24, 25, 31, 159, 186, 194, 226,
227,235,238,245 aktuális hely, 156, 237,461 aktuális keresett karakter, 413 aktuális könyvtár, 539 aktuális kulcs, 425, 426 aktuális növekmény, 175 aktuális példány, 543 aktuális pozíció, 29, 31, 40, 226, 231, 234, 235,
245,413,454,459-461 aktuális szál, 98, 100 aktuális szint, 400 aktuális teljes halomméret, 542 alapalgoritmus, 197 ala pérték, 21 alapértelmezés szerinti konfiguráció, 542 alapértelmezett érték, 71 alaphelyzetbe állit, 68, 71, 79, 327
alapos, 552 alaposztály, 457 alaposztály-definíció, 457 alapszintű művelet, 52, 53 alapvető funkció, 144 algoritmus bonyolultsága, 1, 8, 475,486 algoritmus megvalósítása, 453, 457, 486 algoritmusbonyolultság, 1, 4, 18 algoritmusok szerepe, 1 algoritmus-összehasonlitás, 166 algoritmustervezés, 495 algoritmus-végrehajtás, 196 alkalmazás, 12,31,57,64, 77,103,104,111,112,
117,205,222,225,233,304,391,419,424, 445,468,469,542,543,551
alkalmazás belépési pontja, 111 alkalmazás indulása, 54 3 alkalmazás kódja, 233 alkönyvtár, 44 állapotinformáció, 125, 130 alsó index, 234, 235, 237 alsó korlát, 234 áltilános, 1, 16,20,21,23,25,47,48,52,59,65,
98,107,118,139,141,143,169,173,179, 183,188,194,197,200,205,206,283,309, 330,336,362,391,395,396,398,406,415, 424,445,448,463,497,508,534,543
általános célú összehasonlitó, 183 általános előtag, 395, 396, 398, 406 általános érvényességvizsgálat, 16 általános felület, 23 általános futásidő, 463 általános memóriahasználat, 534 általános összehasonlitó, 141 általános sztringillesztő iterátor, 445 általános teljesítmény, 330, 395, 424, 543 angol nyelv, 472,481 Apache, 555 ASCII karakterkészlet, 458 asszociaóv tömb, 359, 361, 388 át nem léphető korlát, 532 átlagos bonyolultság, 5 átlagos futásidő, 82 átlagos keresési idő, 258, 263, 299 átlagos teljesítmény, 74, 357, 388 átviteli idő, 424 A VL-fa, 263, 268, 269 azonosító, 104, 540
Tárgymutató
B
bal oldali operandus, 141 bal oldali részfa, 260 bal szélső csomópont, 258, 265 bal szélső gyermek, 425 beállitható, 328 beállitható paraméter, 328 beépített, 141,200, 534 beépített Java-objektum, 141 bejárás, 30, 266, 267,416 bejárási idő, 78 beköthető, 91 beköthető megvalósítás, 91 belépési pont, 419 belső állapot, 79 belső ciklus, 150, 155, 159,454 belső interfész, 133, 362 belső osztály, 39, 79, 81, 84, 85, 86, 326, 357,
387,388,436,440 bemeneti lista, 186, 187 bemeneti szó, 202 bemeneti sztring, 472,473,477,482 bemenet-kimenet, 4 beszúrási algoritmus, 430 beszúrási múvelet, 133, 159 beszúrási pont, 56, 58, 59, 73, 82, 232, 246, 248,
250,254 beszúrási sorrend, 51, 87, 90,287, 333,381 beszúrásos rendezés, 135, 156, 158, 161, 166-
168,170,.172,174,175,197,198,208, 222,570,575
beszúrásos rendezési algoritmus, 135, 158, 168, 175,570
beszúrásos rendezési mechanizmus, 208 beszúró, 248, 249 betúk sorrendje, 303 betűnkénti összehasonlítás, 395 betúsorozat, 405 beviteli adathalmaz, 9 bevitt karakter, 478 B-fa,263,423,425-428,430,431,436,443 B-fabeli törlés, 430 bináris, 9, 210,218,225-228,232-240,244-270,
284,285,290,294-301,350,357,381,389, 391-398,422-426,443,525,589,604
bináris ábrázolás, 9 bináris beszúrás, 225, 245, 246, 249, 250-255 bináris beszúrási algoritmus, 246, 253 bináris beszúró, 250, 251, 525 bináris fa, 210, 218, 258, 263,423 bináris fastruktúra, 21 O bináris keresés, 225, 226, 228, 232, 234, 238, 240,
244,245,250-255,257,262,297,392,395, 443,604
bináris keresés teljesítménye, 245
610
bináris keresési algoritmus, 238, 240, 257 bináris keresési megközelítés, 228 bináris kereső, 232, 236, 239-270, 284, 285,
290,294-301,350,357,381,389-398, 422-426, 589
bináris keresőfa, 257-262, 264,268,270, 285, 290,295,299,350,357,389,392,395, 424,426
bináris keresáfa megvalósítása, 290 bináris számjegy, 9 bizonytalansági szint, 3 biztonsági óvintézkedés, l 03 bonyolult algoritmus, 2 bővithető megközelítés, 23 buborékrendezés, 143, 144, 145, 149, 151, 152,
158,161,164,166,167,253 buborékrendezéses megfelelő, 154 burokrendezés, 537
c
catch blokk, 16, 91 CD, 301, 424,443 célkarakter, 484, 493 célszó, 483 célsztring, 483, 485, 486, 488, 493 ciklikus redundanciavizsgálat, 304 ciklusváltozó, 22 címterület, 303 CRC, 304 CRC-számítás, 304
Cs
csomagelvú, 12 csomagelvű metódus, 12 csomópont, 210-212, 257-294, 387, 391-401,
413-416,424-428,435-439,443 csomópontok száma, 426,427,430 csökkent, 234 csökkentett, 489 csúcsterhelési időszak, 113
D
diagnosztikai információ, l 04 diszkrét forgatókönyv, 163 DLL, 541 D�S,445,471,483,494 D�S-illesztés, 471, 483 D lS-keresés, 445 D�S-töredék, 445 duplikált értékek hozzáadása, 312 DVD, 12 DVD-lejátszó, 12
E,É
egész, 1, 2, 9, 10, 20, 34, 49, 82, 107, 132, 137-142,145,152,170,171,173,179,180, 192,212,214,219,221,225,229,231, 242,249,268,301,302,304,311,422, 451,506,532,543,547,559
egész érték, 137, 506 egész osztály, 34 egész szorzás, 1 egy példány, 14, 56, 70, 78, 81, 95, 109, 140,
163,203,233,236,240,248,315,321, 326, 345, 349, 375, 380, 387, 405, 447, 452,477,481,503,521,537
egyedi cím, 305 egyedi csomópont, 424 egyedi hely, 196, 302 egyedi metódus, 34 egyedi összehasonlítá, 138 egyedi teszt, 55, 95, 242, 405, 524 egyedi tesztcsomag, 55 egyedi teszteset, 405 egyedi tesztmetódus, 524 egyelemű, 188,189,190,193,524 egyelemű lista, 188, 189 egyelemű részlista, 189, 190 egyenes vonal, 495, 497 egyenlet, 304, 500, 501 egyetlen API, 445 egyetlen csomópont, 396, 424 egyetlen gyermekcsomópont, 396 �gyetlen könyvtár, 45 egyetlen pont, 103, 522 egyetlen statikus példány, 140 egyidejűség, 533 egypéldányos változó, 411 egységteszt, 11-14, 16, 163, 521 egységtesztelés, 1, 10, 11, 13, 18 egyszeri segédszámítási költség, 458 ekvivruens, 102,345,363,395 ekvivruens metódus, 102, 345 elegáns algoritmus, 191 elegáns megoldás, 121 elemenkénti pontos illeszkedés, 148 elemlista, 182 elemszám, 27 elérhető elem, 39, 116 elérhető statikus változó, 140 elérhető teljesítmény, 5 éles kód, 12, 1 7 életszerű, 27, 48, 87, 115, 124, 162 életszerű rukrumazás, 48, 87 életszerű kód, 27 életszerű példa, 124 életszerű szituáció, 162 ellenőrizetlen kivétel, 25
Tárgymutató
elméleti előny, 194 elméleti metszéspont, 512, 515, 516 előre hruadó iteráció tesztelése, 66 előre iterál, 33, 65 előre iterálás, 65 előrehruadó iteráció, 32, 65, 344, 372 előrehruadó keresés, 462 elvárt eredmény, 39, 146, 148 elvárt szám, 372 elvárt teljesítmény, 5 elvárt viselkedés, 55 e-mail, 125 emberierőforrás-kezelés, 471 empirikus bizonyíték, 194 érdekek szétválasztása, 137 eredeti csomópont-megvruósítás, 387 eredeti gyökércsomópont, 428 eredeti megvruósítás, 240 eredménypuffer, 473,474,482 erőforrás, 4 erőforrás-fogyasztás, 4 értékek száma, 321 érték-összehasonlítás, 293 értékpár, 381 értelmetlen érvényességvizsgálat, 142 érvényes pozíció, 59,250 érvényes tartomány, 218 érvénytelen pozíció, 64 észlelhető hatás, 254 exkluzív zár, 101 explicit kiegyensúlyozás, 300, 589 explicit tesztelés, 118 extra funkció, 54 extra gyermekcsomópont, 422
F
faruapú, 381 fahrumaz megvruósítása, 351,381 fájllista, 45 fájlnév, 419, 465 faktoriális, 5, 1 O fáradságos állapotkezelés, 468 fastruktúra, 212-214,428,429 fejlesztési folyamat, 531 fejlesztési projekt, 532 fejlesztői környezet, 17, 531 fejlett rugoritmus, 195, 197 fekete doboz, 12 felhasználói felület, 12, 125, 131 fellapozási idő, 349 fellapozási táblázat, 359, 388, 458 felső indexérték, 235 felső korlát, 98, l 03 felső méretkorlát, 89, 114 Fibonacci-szám, 44
611
Tárgymutató
FIFC>,89,90,92,93,95,96, 114,200,204, 223,585
FIFC>-sor,90,92,93,95,96, 114,204,223,585 fogalmi kapacitás, 308 folyarnatközi, 97 folyarnatközi kommunikáció, 97 folyamatos kiértékelés, 35 fonetikus kódolás, 471,483,494 fonetikus kódoJási algoritmus, 471, 494 fonetikus kódoJási módszer, 471 fonetikus kódoló, 480 fontos tervezési elv, i 37 fordított sorrend, 23, 33,141, 196 formázatlan, 445, 449, 467 forrássztring,483, 485, 486 fő forrásfájl, 11 futásidejű, 6, 7, 91, 534, 552 futásidejű kiterjesztés, 91 futásidejű teljesítmény, 6 futásidejű viselkedés, 534, 552 futásidó, 7, 9, 463, 533 futó szál, 109, 540 futtatható, 146, 309 függőleges távolság, 499 függvény, 45, 125
G
gazdag grafikus felület, 16 Generic Map interfész, 362 generikus csomag, 365 generikus teszt, 55, 118,310, 364 generikus tesztcsomag, 31 O generikus tesztosztály, 55, 118 gépi kód, 4 Google, 445 grafikus ábrázolás, 115 grafikus felület, 534 grafikus terület, 49 5 grafikus változat, 17 Gutenberg, 467, 555 Gutenberg-projekt, 467
Gy
gyakorlati alkalmazás, 19, 221, 403 gyakorlati példa, 462 gyermekcsomópont,264,392,398,425 gyermekek listája, 437 gyors ismétlés, 1 O gyorsítótár, 115 gyorsreferencia, 547 gyorsrendezés, 175,176-182,187,188,196-198,
253,575 gyorsrendezés megvalósítása, 180, 197 gyorsrendezési algoritmus, 169, 179, 187
612
gyökércsomópont, 258,268-270,274,294,411, 425,427,428,430,435,438,440,441,443
gyökércsomópont szétválasztása, 428, 430
H
ha]fnazszemantika, 350, 356 halomfeltétel, 212,213,214, 215, 216, 218 hálomméret, 534 halomstruktúra, 212, 214,215, 222, 223 hálózati megszakítás, 463 hangoló, 539 Hanoi-torony, 124 háromelemű, 189 hasító érték, 304, 305, 306 hasító függvény, 301, 303, 308, 330 hasító halmaz, 333, 346 hasító kód, 349 hasító leképezés, 380 hasítótábla, 302-316, 321, 324, 326, 330, 346,
350,357 hasítótábla mérete, 302 hasítótábla példány, 311 használaton kívüli tár, 7 6 használhatóság, 350 hatalmas teljesítménynövekedés, 531 hatékony algoritmus, 136 hatékony beszúrás, 424 hatékony bináris keresés, 250 hatékony bináris keresési mechanizmus, 250 hatékony megoldás, 133 hatékony módszer, 239 hatékony tárolás, 225 hátra iterálás, 36 hátrány, 198 Heap interfész, 211 helyes beszúrási pont, 81 helyes érték, 39, 248, 274, 369 helyes pozíció, 67,231,246 helyes sorrend, 58, 59, 119, 128, 246,248,450,
547 helyes szintaxis, 13 helyes viselkedés, 55 helyesírás-ellenőrző, 483, 486, 494 helyettesítés költsége, 484, 492 helyettesítőkarakter, 399, 400, 416 helyettesítőkarakteres keresés, 401 helyi vegyesbolt, 115 heurisztikus, 3, 455 heurisztikus utótag, 455 hitelkártya-tranzakció, 19 hitelkártya-tranzakció végösszegének kiszámí-
tása, 19 hívás, 26, 31, 48,102-109,113,119,165,166,
196,197,221,222,248,289,297,298, 329,330,460
lúvás időtartama, 104 lúvásgenerálás, 11 O lúvásgenerátor, 1 02, 1 09, 113 lúvási időköz, 103 lúvások száma, 103 lúvásszám, 103 hosszabb ideig futó program, 533
l' í
ideális eset, 303, 468 ideiglenes objektum, 547 idő, 3, 5-9,93, 102, 103, 105, 107, 113, 134, 169,
179,198,225,254,308,322,466,494,532, 533,544
idő előtti optimalizálás, 532 időbeli bonyolultság, 7 illeszkedő bejegyzés, 376, 438, 439 illeszkedő betű, 394, 399 illeszkedő csomópont, 357, 388, 392 illeszkedő elem, 42, 85 illeszkedő érték, 293, 318 illeszkedő kulcs, 437, 438 illeszkedő szó, 394, 401, 402 illesztóprogram, 164, 221 illesztóprogram-osztály, 164 implicit processzorverem, 267 indexérték, 316 informatikai könyv, 301, 495 informatikai könyvek osztálya, 301 informatikai könyvesbolt, 495 inorder bejárás, 266,267, 398,416,440 inorder beszúrás, 197, 396, 397 inorder beszúrásos rendezés, 197 integrált fejlesztói környezet, 13 interfész, 25, 26, 29, 35, 54, 64, 71, 75, 77, 79,
86,91,96,97, 117,118,138,140,141, 143,146,184,207,211,229,310,337, 345,349,350,357,362,364,372,380, 388,429,430,440,445,446,447,462, 463,464,481,525,536
interfészdefirúció, 480 internetes keresés, 538 inverz iterátor, 344 inverz összehasonlitó, 200 ismédódő minta, 455 ISO, 467 iteráció, 19, 22, 23, 26, 31, 35, 38, 39, 48, 49,
64,66,559 iterációs sorrend, 334, 357, 389 iterál, 22, 46, 318,357, 388 iterálás, 28, 30, 33, 65, 248 iteratív, 19, 47, 175, 186, 198,225,228,230,
236,237,244,252,254,267,299,422, 575,588,603
iteratív bináris lista, 252
Tárgymutató
iteratív megvalósítás, 175, 228, 236, 237, 244 iteratív módon, 47, 186, 198, 299, 575, 588 iterátor, 23-29, 31-35, 39-42, 48, 55, 64-66, 85,
90,159,343,350,371,373,461,462, 468,559
iterátor megvalósítása, 27, 29, 34
J
Java, 11-17,24, 25, 45, 53, 91, 98, 100, 111, 117,138,139,194,229,309,337,362, 372,466,502,505,508,529,531,533, 534,537,538,541,552,553,555,557
J ava Collections Framework, 24 Java objektum, 502 Java osztály, 538 Java program, 13, 100, 466 Java virtuális gép, 531, 534, 538, 552 J ava -alaptípus, 15 Java-értelmező, 111 Java-fordító, 13 Java-környezet, 11, 538 Java-változat, 24 ]DK, 76, 304, 309, 359 JDK-megvalósítás, 76, 309 jelentős segédszárrútási költség, 457 JMP-parancssori, 551 jó minőségű kód, 448 jobb oldali, 141, 142, 176,211,212,214,218,
258,259,260,261,263,265,267,269,273, 275,276,280,299,393,398,435,450
jobb oldali argumentum, 141 jobb oldali operandus, 141 jobb oldali részfa, 258 jobb szélső csomópont, 259 jobbról balta,3,455,459,468 jól megrervezett kód, 552
K
kapcsolódó csomópont, 424 karakterenkénti összehasonlitás, 395 karakteres érték, 411 karakter-fellapozás, 463 karakterkészlet, 458, 467 karakterkészlet neve, 46 7 karakter-összehasonlitás, 394, 401, 422, 463,
464,468 karakterpozíció, 462 karaktersztring, 140, 4 7 4 karaktertömb, 478,481 kényelmi konstruktor, 350, 380 kényelmi módszer, 59, 281,412 kényelmi múvelet, 52, 53 képlet, 218, 484, 500, 501 képzeletbeli adatbázis, 424
613
Tárgymutató
képzeletbeli doboz, 518 kérdőjel, 407 keresés, 9, 75, 83, 136, 206, 225-231, 234, 235,
237,239,243-245,250,253,261,264,266, 306,307,317,392,394-398,400,402,406, 413,416,424-426,437,438,447,448,450-452,459,462,545
keresés megvalósítása, 239 keresési algoritmus, 6, 257,445,447,468 keresési eredmény, 406 keresési folyamat, 234 keresési idő, 82, 135, 301, 307, 331,424, 443 keresési kulcs, 226-229, 232, 254, 260, 261, 424,
425,426 keresési módszer, 75, 225, 228,244 keresési tér, 226, 227, 228, 233, 238 keresett érték, 67, 74, 75, 240, 260,261, 292 keresett szó, 135,392,394, 396, 399, 413 kereső, 228, 239,449,450,452, 461,464, 466 keresőfa, 258, 391, 392, 395, 398, 399, 403,
405,407 keresztrejtvény, 417, 418, 421 keretrendszer, 14, 15, 16, 241 késleltetés, 424 kétbetűs bemeneti sztóng, 4 78 kétdimenziós tér, 496 kételemű, 58, 189, 190, 524 kételemű részlis ta, 189, 190 kétnyelvű szótár, 359 kétszeres pontosságú, 249 kétszeres pontosságú lebegőpontos szám, 249 kétszerezett érték, 51, 64, 67,333,336, 349,356,
357,388 kezdeti eredmény, 22 kezdeti kapacitás, 70, 328 kezdeti konfiguráció, 328 kezdeti méret, 341, 370 kezdetleges megközelítés, 225 kezelő, 79, 218 kézzel készített, 287 kiegyensúlyozatlan fa, 263, 266 kiegyensúlyozó algoritmus, 268 kiértékelés, 196 kiinduló állapot, 288 kiinduló lista, 545 kimenet, 44, 45, 47,113,164,165,183,187,540 kimerítő összehasonlítás, 196 kis méretű Java-verem, 540 kis méretű parancssati alkalmazás, 417 kisebb testvér, 413, 416 kiterjedt statisztikai elemzés, 471 kiválasztási feltételek, 23 kiválasztásos rendezés, 135, 151-155, 161, 166,
167 kiválasztásos rendezés megvalósítása, 161 kivételt dob, 29, 52, 63, 87, 137, 209, 564
614
kód, 2, 4, 5, 12-17,26,31,45, SS-59, 84,97-99, 138, 142, 143, 149, 150, 165, 182, 183, 193-195,204,229,230,240,246,249,250,287, 293,305,309,349,350,356,377,381,387, 437,447,460,475,478-482,502,504,506, 508,512,515,524,526,529,531,532,534, 536,543
kód minősége, 17 kód tesztelése, 59 kódolás, 413, 471, 472, 477, 552 kódolási algoritmus, 482 kódolási módszer, 481 kódoló, 475,480 kódrészlet, 22, 23, 54, 106, 216, 235, 532 konkurens végrehajtás, 103 konstans idejű algoritmus, 7 konstans idő, 7 konstans időbeli bonyolultság, 7 konstans időbeli teljesítmény, 7 koordináta, 502, 514, 518, 529 korlátfeltétel, 69, 70 korszakalkotó algoritmus, 4 korszerű számítógépes alkalmazás, 225 kölcsönös kizárás, 98 kölcsönös kizárást megvalósító szemafor, 98 kölcsönös rekurzió, 48 könnyen követhető empirikus eredmény, 5 könyvtár, 12, 44, 47, 136 könyvtárfa, 43, 44,258 könyvtárfa-nyomtatási példa, 44 környezet, 239 következő csomópont, 259,260,264,265,266,
270,274,275,277,282,294,392,395 következő gyermekcsomópont, 393 következő rekurzív hívás, 192 követő csomópont, 267, 282,294 közép-keleti nyelv, 458 középpont, 438 középső gyermek, 425, 428 közös alaposztály, 162 közös funkció, 53 közös sztringkereső felület, 468 közös típus, 12 köztes számítás, 492 közvetlen kapcsolat, 264 közvetlen másolat, 349 közvetlen összefüggés, 463 kótikus út, 532 kulcsszó, 4 75, 553 külön szál, l 06, l 09 különálló alkalmazás, 103 külső adathordozó, 442 külső ciklus, 149, 150, 155, 454 külső osztály, 125 külső tároló, 424 kvadratikus idő,8,9
L
leállási eset, 4 7 leállási feltétel, 26, 4 7, 234, 235 lebegőpontos énék, 506 legegyszerűbb eset, 68, 264, 429 legfelső elem, 120, 214 legjobb esetbeli, 196, 468 legkisebb szám, 435 legközelebbi pár, 518, 522, 525, 527-529, 605 legközelebbi prímszám, 304 legkülső ciklus, 17 5 legnagyobb csomópont, 400-402. legnagyobb énék, 182, 259 legnagyobb lehetséges egész énék, 99 legnagyobb sorrnéret, 99 legrosszabb eset forgatókönyve, 220 legrosszabb esetbeli, 5, 75, 82, 164, 196,258,
269,454,468 legrosszabb esetbeli bináris keresés, 269 legrosszabb esetbeli eredmény, 196 legrosszabb esetbeli futásidő, 454, 468 legrosszabb esetbeli idő, 7 5 lehetőségek felsorolása, 398 lehetséges teljesítménybeli szúk keresztmet
szet, 135 leképezés, 299, 359, 361, 364, 368, 370, 371,
375,377,381,387,389,481 lemezblokk, 425 lemezolvasás, 424 lemezterület-foglalás, 4 lenyűgöző teljesítménynövekedés, 456 letámadásos, 75, 82, 167, 206, 208, 222, 225,
228,238,239,307,318,399,401,445, 451,454-459,462,463,468,469,483, 517,529,605
letámadásos algoritmus, 206,451,454,463,468 letámadásos keresés, 75, 208, 318, 451, 456 letámadásos keresési algoritmus, 451 letámadásos kód, 457 letámadásos letapogatás, 208 letámadásos lineáris keresés, 225 letámadásos megközelítés, 82, 307,401,445,
451,454,456,459,468,469,483 letámadásos megoldás, 517, 529, 605 letámadásos változat, 222, 459 létfontosságú előfeltétel, 135 letölthető, 13, 103, 541, 552 letölthető forrás, 103 letölthető forráskód, 103 levélcsomópont, 258, 263, 264, 265, 273, 289,
293,430,437,438,439 Levenshtein, 471, 483, 494 Levenshtein-algoritmus, 494 Levenshtein-távolság, 483 llFC>,90, 116,117,134,199,200,204
Tárgymutató
llFC>-sor, 90, 117, 134 UFC>-verem, 204 lineáris idő, 7 lineáris keresés, 74, 75, 223, 225, 238, 239, 244,
307,437,443,604 lineáris kereső, 239, 240 lineáris mód, 218 listabeli iteráció, 549 listabeli iterációs rutin, 549 listabeli kereső, 228-232,238,239,241,246,250 listaelem, 52, 196 listahalmaz, 333, 344 listainterfész, 53 listák listája, 51 listakezelés, 198, 575 listametódus, 129 listaosztály, 56 log M, 395, 397 logaritmikus algoritmus, 9 logaritmikus futásidő, 9 logaritmikus idő, 5 logaritmus, 5 logikai, 16,49,396,411,436,437,521,559 logikai énék, 16 logikai igen, 396 logikai jelző, 436, 437 lokális változó, 9 5 lokálisváltozó-megfelelő, 276
M
magánhangzó, 4 79 magas szintú, 102 main metódus, 16, 44 maradékképzés, 304 másik szál múvelete, 1 00 második konstruktor, 71, 99, 280 második menet, 155 másadiagos tároló, 424, 443 matematikai megközelítés, 161, 194 maximális hívásidő, 103, 111 mediánénék, 196 megabájt, 423 megalapozott döntés, 162, 167 megbízható dokumentáció, 13 megbízható éles kód, 125 megfelelő bejegyzés, 376,439 megfelelő csomópont, 288, 292 megfelelő énék, 23, 35, 41,368, 482 megfelelő teljesítmény, 302, 330 meghatározható sorrend, 89, 381 meghatározott algoritmus, 146 meghatározott példány, 368 megjegyzés, 254 megjósolható iteráció, 333, 350, 359 megjósolható iterációsorrend, 350, 359
615
Tárgymutató
megvalósít, 447 megvalósítási döntés, 161 megvalósítási osztály, 95, 96, 173, 192, 239, 321 megvalósítási részlet, 201 megvalósított, 24, 72, 123, 133, 206, 246, 292,
337,481 méltányolható ok, 117 memóriabeli, 90, 423, 424 memóriabeli adatok, 423 memóriabeli művelet, 424 memóriabeli üzenet, 90 memóriabeli üzenetsor, 90 memóriafelhasználás, 4, 531, 534, 542, 552 memóriaméret, 542, 543 mentális modell, 93, 211 metódushívás, 26, 344, 373, 464, 540 metszéspont, 501, 502, 511, 515, 516, 517 milliszekundum, 4, 112, 113 mintaillesztés, 399, 411, 422 mintaillesztó kód, 417 mintakarakter, 400, 416 mintavételezési munka, 540 mögöttes adatstruktúra, 23, 26, 122, 430 mögöttes tárolási mechanizmus, 92,344, 373,
389,599 munkamennyiség, 169, 463 mutex, 98-101, 569 működés, 13,113,161,228,246,542 műveletek száma, 4, 7
N
N faktoriális, 5 nagy adathalmaz, 135, 136, 169, 197 nagy énék, 6 nagy szám, 546 nagy szövegfájl, 467 nagyobb adathalmaz, 161 nagyobb karakterkészlet, 458 nagyságrend, 5, 426 naiv algoritmus, 467 naiv megközelítés, 455, 460 naiv megoldás, 517 n-edik elem, 49, 559 negatív beszúrási pont, 59 negatív egész, 137, 139, 142, 183 negatív egész eredmény, 139 negatív egész szám, 183 negatív eltolás, 455, 460 negatív énék, 22, 184, 229,234 negatív index, 437 negatív kitevő, 20 negatív koordináta, 496 negatív teszteset, 506 négykarakteres kód, 472,494
616
nem függőleges, 502, 505, 506, 509, 510, 511, 517
nem gyökér csomópont, 443 nem illeszkedő, 395, 400, 401, 402, 454, 455,
460 nem illeszkedő karakter, 400, 454, 455, 460 nem illeszkedő szó, 401 nem kivánt énék, 35 nem korlátos FIFO, 92 nem levél, 425, 428, 438 nem levélcsomópon t, 425, 428, 438 nem módosítható, 79 nem nulla, 184, 185, 304 nem nulla érték, 184 nem programozó személyek, 2 nem rendezett, 152, 171 nem törölt bejegyzés, 440 névlista, 350 normális kivétel, 16 normális kivételkezelés, 16 növekvő csoport, 158 növekvő sorrend, 156,242,247, 248,'263 NULL argumentum, 140 null énék, 15, 72, 81, 123, 363, 369, 370, 376,
377,413,440,441,447,448,450,454, 462, 511, 525
null objektum, 79 nulla, 2, 15,69, 77,93, 137,138,139,155,234,
257,294,307,479,488,500,519 nulladik hatvány, 20, 21, 22 numerikus énék, 301, 507, 508
Ny
nyílt forrású, 534 nyílt forrású eszköz, 534 nyilvános konstruktor, 491 nyilvános metódus, 11 O nyugat-európai nyelv, 467
o,ó
objektív teljesítményfeltételek, 533 odailló részlet, 359 operátor, 109, 113 optimalizálás,240,454,531,532,533,543,552 optimalizálás szerepe, 531 optimalizálási folyamat, 531, 533 optimalizálási munka, 531 osztálydefiníció, 107 osztálygenerátor metódus, 203, 522
ö,ő önkiegyensúlyozó bináris fa, 270 összeadással szorzó algoritmus, 2 összefésülés es rendezés, 169, 182, 186-192, 196,
198,253,575 összefésüléses rendezési algoritmus, 169, 186,
187, 192 összefésüléses rendezési példa, 189 összefésülési folyamat, 186, 187 összefésülési múvelet, 190 összehasonlítás eredménye, 138, 467 összehasonlítási logika, 142 összehasonlítási mechanizmus, 233 összehasonlítások száma, 167, 244, 245, 253,
301,454 összehasonlítás-szám, 7 5 összehasonlítá, 137, 138, 141-143, 150, 161,
162, 166, 167, 183, 184, 185, 196,222, 229,233,234,242,435,518,519,520-523,526,534,535,537
összehasonlító elemzés, 161, 166 összehasonlító múvelet, 138, 196 összehasonlító vizsgálat, 167, 519 összehasonlíták szerepe, 135 összesített érték, 492 összesített költség, 483 összetett hasítás, 311 összetett kulcs, 182, 183, 185 összetett összehasonlí tó, 169, 182, 183, 184,
185, 198 összetett rendezési kulcs, 184 összetett típus, 141
p
paraméter, 41, 45, 112, 233, 561, 562 parancsminta, 133
.
parancssor, 17 parancssori, 45, 110, 111, 538, 552 parancssori paraméter, 45, 110, 111, 538, 552 páros szám, 227 példaadatok, 11 példabeli érték, 340 példabeli forgatókönyv, 102 példabeli kulcs, 369 példabeli lista, 171, 188 példabeli sztring, 304 példány, 106, 132,206,209 példányok száma, 543 példaprogram, 302, 534, 538, 543 Pitagorasz-tétel, 497, 504 pocsékoló viselkedés, 196 ponthalmaz,525,526,527,529 pontos szám, 4, 5, 6 pontpár,517,518,522,527
Tárgymutató
posztorder, 267 posztorder bejárás, 267 pozícióadat, 229 pozitív egész szám, 183 pozitív visszaadott érték, 184, 229 predikátuminterfész, 35 preorde�267,299,440,588 preorderbejárás, 267, 440 primitív típus, 137 prímszám, 303 privát konstrukto�22,521,527,537,548 privát metódus, 357, 388,416 processzor, 134, 531, 538 processzoridő, 4, 533 Profuer eszköz, 531 profilirozó,533,534,544,545 programkód, 532 programoptimalizálás, 543
R
reflexió, 16 reguláris keresés, 422 rekordok száma, 4 rekur.ció,2, 19,42,44,47,48,49, 175,179,187,
189-191,193,197,559 rekurziós szint, 188, 189 rekurzív algoritmus, 19, 44, 47, 179, 188, 189,
194 rekurzív hívás, 48, 188, 234, 235, 237, 244, 267,
416 rekurzív könyvtárfa-nyomtató, 559 rekurzív módon, 19, 47, 48, 175, 180, 190,193,
194,198,216,218,232,267,284,299,300, 414,415,416,438,440,575,588,589
rekurzív módon feldolgoz, 175, 414 rekurzív módon meghív, 180, 284,440 rekurzív módszer, 194, 225 rekurzív összefésüléses rendezés, 188 rekurzív változat, 252, 299, 588 relatív hatékonyság, 6 relatív sorrend, 160 relatív távolság, 234, 519 relatív teljesítmény, 87, 161, 309, 328,463 rendelkezésre álló konstans példány, 481 rendelkezésre álló operátor, 109 rendelkezésre álló rés, 306, 307, 316 rendellenes programleállás, 42 rendes végrehajtás, 551 rendezés megvalósítása, 173 rendezési algoritmus, 135, 137, 158, 160, 161,
167,169,172,173,182,186,187,188,194, 198,199,203,245,250-253,255,547
rendezési folyamat, 182 rendezési múvelet, 168, 570 rendezési sorrend, 33, 138, 139, 154
617
Tárgymutató
rendezeden, 74,135,146,148,158,190,206, 208,209,225,253,333,359,391
rendezeden adatok, 135, 333, 359 rendezeden csoport, 158 rendezeden érték, 333 rendezeden készlet, 333 részfa, 258, 259,267,287,391-398 részleges illeszkedés, 454 részletes, 105, 174,430 részletes magyarázat, 174, 430 részlista, 175, 189,190,191, 194, 197 részsztring, 445-460 rossz helyen levő elem, 181 rögzített összehasonlítá, 183 rövid geometriai ismédés, 49 5 rövidítés, 454
s
sablonkód, 283, 539 segédalkalmazás, 418 segédmetódus, 478, 527 segédszámítási költség, 87, 96, 244, 252, 254,
257,458,468 síkbejárási algoritmus, 517, 518, 522, 524, 525,
527,528,529,605 sikertelen keresés, 229 Singleton, 22, 47,562 sorbaállit, 117 soron következő érték, 172, 248 soron következő lúvás, 113 Soundex algoritmus, 471,472, 475 Soundex érték, 474,477 Soundex kódoló, 479, 480 sourceforge, 555. speciális karakter, 4 7 4 stabil algoritmus, 160 stabil vezetéknév-rendezés, 161 statikus tag, 14 stratégiai változtatás, 531 String objektum, 531, 536, 544, 546, 547, 551 String osztály, 304, 309,311, 326 switch utasítás, 133
Sz
szabad rés, 306, 307 szabványos egyértékű összehasonlítá, 183 szabványos iterátor, 27 szabványos J ava könyvtár, 44 7 szabványos Java-tömb, 87, 564 szabványos keresés, 398 szabványos kiemelő, 281 szabványos kimenet, 221, 534, 548 szabványos megvalósítás, 27 szabványos összehasonlítá, 138
618
szálbiztos, 89, 97, 102, 106, 114, 568 szálbiztos csomagoló, 114 szálbiztos megvalósítás, 97 szálbiztos szinkronizáló kód, 102 szálszinkronizálás, 98 száltelítődés, 103 számítás, 20, 21, 253, 460, 493 számítógépes erőforrás, 6 számítógépes geometria, 495,498 számítógépes geometriai probléma, 498 számítógépes grafika, 49 5 számítógéprendszer, 18 számított távolság, 488 szavak beszúrása, 200 százalékos küszöbérték, 76 szekvenciális bejárás, 194 szekvenciális hozzáférés, 69 szemétgyújtés, 73, 76, 534, 543, 546, 547, 551 szétválasztás, 428 szimulátor, 112 szinkronizálási pont, 98 szoftvermérnökség, 3 szorzási folyamat, 1 szótávolság, 471,483,493, 494 szögletes zárójel, 87, 564 szövegszerkesztő, 445,447,483 sztring,205,452,463,472,477,479,482,488,
489,550 sztringillesztő iterátor, 445,461,462 sztringkeresés, 445, 447 sztringkereső, 445,448,461,462,466 sztring-összehasonlítás, 463 szükséges kód, 14, 77 szükséges paraméter, 111, 112 szükségrelen összehasonlítás, 469 szülőindex, 212 szúrőiterátor, 35, 39, 40, 41
T
táblafellapozás, 460 táblaméret, 331 takarítás, 294 támogató metódus, 15 tárolási mechanizmus, 23, 345 távolságszámító, 487, 489 telefonos ügyfélszolgálat, 89, 102-104, 107, 108,
109, 110, 112, 113 telefonos ügyfélszolgálat szimulátora, 102, 11 O telepítés, 538, 541 teljes halomméret, 542 teljesítmény mérése, 463 teljesítménybeli szúk keresztmetszet, 532, 533 teljesítményhatékonyság, 308 teljesítményjavulás, 467, 552 teljesítménykiértékelés, 228
teljesítménymérés, 532 teljesítmény-összehasonlítás, 244, 253, 329 teljesítményprobléma, 543 teljesítményteszt, 295 teljesítményteszt-osztály, 295 tényleges balról jobbra haladó karakter-
összehasonlítás, 454 tényleges beszúrás, 78, 248 tényleges egység, 15 tényleges eredmény, 329,330,406 tényleges FIFO, 95 tényleges megvalósítás, 54, 56, 93, 143, 146,
304,475 tényleges teljesítmény, 6, 82 tényleges tesztelő kód, 229 térbeli bonyolultság, 494 terhelési tényező, 308, 321, 328, 330 terhelési tényező küszöbértéke, 308 termelési rendszer, 197 természetes összehasonlító, 138-140, l 67, 242,
547 természetes rendezés, 138, 139 természetes rendezési sorrend, 138, 139 tervezési feltételek, 149 tervezési tudás, 533 testvér, 268, 391,416 testvércsomópont, 415, 438 tesztcsomag, 337,447-479 tesztelés alatt álló osztály, 13, 95 tesztdési cél, 36, 39 tesztdési módszer, 13 teszteset, 33, 39, 95, 140,203,204, 230,315,
403,448,513 tesztfuttatás, 14, 16, 221 tesztfuttató, 16, 219 tesztfuttató osztály, 219 tesztmetódus, 14, 56, 59, 61, 67, 70, 140, 142,
148 tesztosztály, 11, 20, 32, 39, 69, 78, 95, 125, 128,
230,241,242,246,248,252,324,340,346, 368,448,452,456
tetszőleges elem, 51 tetszőleges típus, 391 tipikus esethez tartozó adathalmaz, 163 tipikus esethez tartozó tesztadatok, 163 tipikus forgatókönyv, 220 tízes számrendszerbeli szám, 4 72 tízes számrendszerbeli számjegy, 472 több ügyfélszolgálati munkatárs, 109 tömbbeli keresés, 7, 323 tömbiterátor, 27,29 tömblista, 68, 69, 70, 74, 78, 86, 242, 245, 437 tömblista megvalósítása, 86 törlési eset, 266 törlési pont, 7 6 törvénybe foglalt, 68
Tárgymutató
triviális interfész, 54 triviális letámadó algoritmus, 445 try blokk, 16 try-catch blokk, 91 tudományos elemzés, 3 túlélési stratégia, 199 túlszárnyal, 6
U,Ú újra elvégez, 124 újra felhasznál, 55, 241, 310, 364 újra kiegyensúlyoz, 268 újra szétoszt, 428, 429 újra szétosztás, 429 újrafelhasználási minta, 44 újrarendez, 17 5 újrarendezés, 31 Unicode, 555 utolsó előfordulások táblázata, 459 utolsónak be, elsőnek ki, 116
ü,ű ügyfélprogram, 200, 201, 202 ügyfélszolgálat, 1 02, 112 ügyfélszolgálati munkatárs, 102-104, 106, 107,
109, 112, 113 ügynökök száma, 103 üres állapot, 16 üres célszó, 483 üres forrásszó, 483 üres iterátor, 39, 49, 559 üres lista, 65, 93, 97, 437 üres sztring,45,448,450,488 üres verem, 119, 121 üzenetárada t, 104 üzleti alkalmazás, 69
v
várakozási sor hossza, 15 várakozási sorból kiemel, 117 várakozásisor-objektum, 14, 16 várt állapot, 16 várt érték, 29, 343, 478, 488 várt hely, 58, 129 várt kivétel, 16 várt sorrend, 66 végállapot, 187 végpont, 398,399,513 végrehajtási idő, 540 végső kódolás, 475 végső megvalósítás, 20 végső metódus, 508, 517 végső növekmény, 173
619
Tárgymutató
végső tesztmetódus, 142 véletlen adatok, 262 véletlen beszúrás, 298 véletlen egész, 252 véletlen érték, 248 véletlen hozzáférés, 51 véletlen időköz, 110 véletlen jellegű, 330 véletlen keresés, 243 véletlen kiválasztási folyamat, 114, 568 véletlen mód, 219, 295,298, 329, 381 véletlen sor, 90, 114,249, 568 véletlen sorrend, 114, 249, 568 véletlenszám, 302, 331 verem, 47, 87, 115-125, 134,200, 267, 540 veremalapú, 129, 134 vezérlés, 238 vezetéknév, 160, 182, 472, 475 vezetéknév szerinti rendezés, 182 vezető, 222, 264, 392 virtuális gép,533,534,540,542 virtuális memória, 463 visszakeresési kísérlet, 91 visszakeresési sorrend, 90, 114 visszavonható, 125, 129 visszavonható lista, 129 vízszintes távolság, 499 vonóháló, 518-528 vödrök száma, 308, 594
w
Wikipedia, 555 Windows-rendszer, 541 Windows-rendszerkönyvtár, 541
x
XML, 46,124 x-y koordinátarendszer, 495-497
z
zenei CD, 301
620
A szerzőkről
Simon Harris még az általános iskolában kezdett mozgó szellemeket fejleszteni Commodore 64 szárrútógépre. Ezt több éves szünet követte, amely után viszont önerőből megtanulta a 80x86 és az IBM System/370 assembly nyelvét, és elkezdett az informatikai szakmában dolgozni. Azóta az assembly nyelvtől a C, a C++ és természetesen a Java nyelv felé fordult a figyelme. Vallja, hogy az algoritmusok alapvető ismerete és megbecsülése a jó prograrnak fejlesztésének alapköve. Amióta pedig megalapította saját cégét RedHill Consulting néven, a szaftverfejlesztési módszerek
kel és a szaftverfejlesztés gyakorlatával kapcsolatos tanácsadásból és oktatásból él. James Ross, több rrúnt 15 évnyi fejlesztési tapasztalattal a háta mögött, nagy
vállalati megoldások kialakításától a fordítóprogamok és a programozási nyelvek kutatásáig számos területtel foglalkozott. Az utóbbi években a programkód rninőségének biztosítása mellett kötelezte el magát, de szakértője lett az agilis módszereknek,
különösen a tesztvezérelt fejlesztés területén. James Ross a ThoughtWorks cég ta
nácsadója, amely a világ egyik vezető agilis szaftverfejlesztési cége. J elenleg egy nagy J2EE-projekt fejlesztését vezeti egy biztosító számára az ausztráliai Melbourne-ben,
ahol családjával él.
Felelős kiadó: a SZAK Kiadó Kft. ügyvezetője Felelős szerkesztő: Kis Ádám Olvasószerkesztő: Bíró Ágnes Tördelő: Mamira György Borítóterv: Flórián Gábor (Typoézis Kft.) Terjedelem 40,5 (BS) ív. Készült az OOK Press Nyomdában (Veszprém) Felelős vezető: Szathmáry Attila