kezdokonyv.az.algoritmusokrol.2006.ebook digit

631
Simon Harris - James Ross Kezdőkön az algoritmusokról

Upload: danikaa3

Post on 13-Aug-2015

261 views

Category:

Documents


10 download

TRANSCRIPT

Page 1: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Simon Harris - James Ross

Kezdőkönyv az

algoritmusokról

Page 2: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Simon Harris - James Ross

Kezdőkenyv az

algoritmusokról

200 6

Page 3: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 Prog­rammer, és a hozzá kapcsolódó arculat a John Wiley & Sons, Inc. és/vagy partnerei véd­jegye 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éd­jegye 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él­kü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]

Page 4: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 5: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 6: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 7: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 8: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 9: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 10: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 lehe­tő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 szargalma­san 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ádom­nak, 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égteszt­jeinket, és aki lankadatlan figyelemmel és gondossággal olvasta át a fejezeteimet, va­lamint 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.

Page 11: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 12: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 13: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 14: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 15: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 se­gé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 optimali­zálás és a teljesítmény érdekessége, hogy a szűk keresztmetszetek sohasem ott van­nak, 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ódo­lással előállitott spagettikódot, és tapasztalataink szerint az egyszerű terv eredménye­ként készített kód kis optimalizálás mellett is remekül teljesít.

xvi

Page 16: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 17: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 18: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 19: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 20: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 21: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 22: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 23: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 24: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Az algoritmusok definíciója

dig a legegyszerűbb megoldás a legnyilvánvalóbb. Bár a szigorú, tudományos elem­zé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 meg­közelítést, és járjunk utána megérzéseinknek Vizsgáljuk meg, miért működnek bi­zonyos 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 ismerte­tett algoritmusok nagy része determinis'{!ikus- azaz az algoritmus eredménye a beme­netek 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 megkere­sé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 he­urisztiká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ó át­kelé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 va­ló á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 algo­ritmusra 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

Page 25: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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éko­nyabbnak tudni, tehát be kell bizonyítanunk, hogy a hozzá fűzött reményeknek meg­felelőerr működik. De pontosan mit értünk hatékonyság alatt? Processzoridőt, me­móriafelhasználást, lemez bemenet-kimenetet? És hogyan mérhetjük az algoritmu­sok 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 algo­ritmus hatékonyságának fokmérője. Noha igaz, hogy végeredményben az erőforrás­fogyasztá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öveke­dé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 algo­ritmus 1000 rekordot 30 milliszekundum alatt dolgoz fel, rníg egy másik algoritmus 40 milliszekundum alatt, akkor az első algoritmust tekinthe�ük "jobbnak". Ha azon­ban az első algoritmus 300 milliszekundum alatt 10 OOO rekordot (tízszer annyit) dol­goz 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égrehaj­tá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ér­nünk. Sokkal fontosabb az, hogyan változik a végrehajtott múveletek száma a prob­lé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 prob­léma méretével lineárisan nő? Vagy exponenciálisan? Ezt kell az algoritmusbonyo­lultsá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

Page 26: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 27: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 28: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 algo­ritmus egyeden művelet segítségével végrehajtja a funkciót. Noha ez valóban lehet­sé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 telje­sítmény legegyszerűbb példája a számítógép operatív memóriáját címezi, és kiterjesz­tésként tömbbeli keresést hajt végre. A tömb egy elemének keresése a mérettől füg­gedenill általában ugyanannyi ideig tart.

Bonyolultabb problémák esetén azonban nagyon nehéz konstans ideig futó al­goritmust találni: a "Listák" című fejezet (3.) és a "Hasítás" című fejezet (11.) beve­zeti 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 átlago­san 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 fe­ledjü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 ha­tékonynak tekinthetők, mint az 0(1) futásidejű algoritmusok, de ahogy már említet­tük, igen nehéz konstans idejű algoritmust találni. Ha sikerül lineáris idővel futó al­goritmust 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

Page 29: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ösz­szes 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 ösz­szes 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 ki­voná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

Page 30: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 bi­ná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

Page 31: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 32: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ismer­tetett algoritmus esetén bemuta�uk, hogyan működik az adott algoritmus, és egység­tesztek 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 bete­kinté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 fej­lesztő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 gyako­rolni. 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ét­rehozott 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ég­teszteket 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

Page 33: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 34: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 35: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 36: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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. No­ha 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 viselke­dését teszteli, ha a sor üres, és valaki megpróbál egy elemet kivenni belőle; ezt az ob­jektum 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ámoga­tó metódustól.

• A metódus első sora az assertequals O metódust alkalmazza annak ellenőr­zé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 objek­tum 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

Page 37: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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őd­hetü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 na­gyon 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 meg­hívja a ]Unit-keretrendszer fai l O metódusát. Ha a fai l O metódus meg­szakítja a tesztet, és hibát jelez, azaz a metódus várt kivételt jelez, a végre­hajtá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 megta­lá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észekkel­egészen az egyszerű szövegalapú konzolfelülettól kezdve a gazdag grafikus felületig.

16

Page 38: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 39: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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?

Page 40: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 41: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 42: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 43: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 44: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Tömbök feldolgozása

Vannak esetek, amikor egy tömböt fordítva szetetnénk feldolgozni. Például for­dí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 csu­pán minden második elemet szetetnénk feldolgozni; bizonyos kiválasztási feltételek­nek 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 dol­goznak, 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ög­zíti a tömbhasználathoz, hanem szükségessé teszi, hogy az elemkiválasztás és feldol­gozá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 dupli­ká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énylege­sen 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 mi­lyen típusú rendezés, illetve előfeldolgozás szükséges, az iterátor lehetővé teszi egy­szerűbb, általánosabb algoritmusok írását.

23

Page 45: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 46: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 47: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 48: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Tömbök feldolgozása

Mindkét icliómát bátran használhatjuk, és mindkettőt nagyjából ugyanolyan gya­korisá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ó algo­ritmusok 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

Page 49: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

}

Page 50: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ömb­hö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ő hely­re 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 iga­zolja, 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 gyakorlat­ban 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 meg­való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

Page 51: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 52: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ak­tuá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 pa­ramé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éte­lezve, 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 feldol­gozó 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 visz­szafelé járja végig. Egy fordított iterátorral ugyanezt a magatartást elérhetjük újra­rendezé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

Page 53: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());,

Page 54: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 55: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

}

Page 56: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 57: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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));

Page 58: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

·; 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

Page 59: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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)�;�--------------------------------�

Page 60: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 61: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

}

Page 62: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 63: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ál­va 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 illesz­kedő 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 tar­talmazott 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özis­mert, hogy a fájlrendszernek van egy gyökérkönyvtára számos alkönyvtárral (és fáj­lokkal), amelyek további alkönyvtárakat (és fájlokat) tartalmaznak.

42

Page 64: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 65: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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]), "");

Page 66: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 67: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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-------�------�

Page 68: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 69: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ta­lálkozunk. Miután kiírtuk a nevét, fel szeretnénk dolgozni az összes könyvtárban ta­lá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 to­vá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 esetek­ben 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ű alkal­mazá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 is­mé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.

Page 70: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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árfanyom­tató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

Page 71: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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"

Page 72: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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()) .

Page 73: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 74: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ódusai­vá alakítottuk őket, mindet a megfelelő paraméterekkel, visszatérési tipusokkal és ki­vé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 ren­delkezik, é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 pil­lantá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()); }

Page 75: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ód­ban 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 fe­lelnie. Ezek a tesztek fogják kódban megvalósítani a 3.1. és a 3.2. táblázatban bemu­tatott követelményeket, és ezek adják a listaság deflllÍcióját. Ráadásul, ha végignéz­zük a teszteket, tisztán fogjuk látni a listák elvárt viselkedését, ami jelentősen meg­kö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ál­hatunk 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

Page 76: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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étre­hozni, 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 lis­tá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ódja­nak, 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));

Page 77: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ódszer­rel 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 programo­zá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

Page 78: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ke­vesebb 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 po­zí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

Page 79: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 80: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 81: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 rndexout­ofsoundsExcepti on kivétel dobását várjuk.

A testsetoutOfBounds() metódus alapjában véve megegyezik a testGetOutof­Bounds() 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

Page 82: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;._ _____________ _,

Page 83: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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 Index­OutOfBoundsExcepti 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

Page 84: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 85: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 86: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ér­té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ő ele­métő� majd egymás után meghívja a next() és a current() metódusokat annak ellenőr­zé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 visz­sza-, a current() meghívásával megpróbáljuk elérni az iterátort. Ennek Iterator­

outOfBoundsExcepti on kivételt kell dobnia.

66

Page 87: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 88: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 su­gallja, 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 op­cioná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. Ép­pen ezért mindkettőre vonatkozóan megvizsgáljuk a speciilisan mellette és ellene szóló érveket, kódmagyarázattal együtt.

68

Page 89: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 90: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 91: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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észe­tesen 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 argumen­tuma 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

Page 92: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 93: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ele­met. 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 visz­szatér, akkor tudjuk, hogy már van elég helyünk, és biztonságosan eltolhatjuk a meg­lé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, át­másolja az elemeket, és elengedi a régi tömböt, amely ezáltal szabaddá válik a sze­mé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ódus­nak, 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

Page 94: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 95: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 96: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ó ele­mének törlésekor ne okozzunk ArrayrndexoutofBoundsException kivételt. Tulaj­donké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 el­mentettü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 je­lenti, hogy ha a lista mérete nagyon nagyra nő, majd jelentősen lecsökken, akkor na­gyon sok "elpazarolt" tárolónk lesz. Ezt a problémát megkerülhe�ük, ha megvalósít­juk 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ókapa­citá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

Page 97: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 98: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 99: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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át­felté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 őr­szemre, mintha az csupán a lánc egy eleme lenne. A:z. őrszem fogalma nehezen meg­fogható, ezért semmi ok az aggodalomra, ha első látásra egy kicsit bizarrnak tűnik. Va­lójában egy őrszemet használó algoritmus megalkotása általában nem igényel különle­ges 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

Page 100: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 101: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 102: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 103: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 104: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 tulaj­donké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ér­jük a tömböt. Ugyanakkor a lánclisták esetében magukat a hivatkozásokat kell hasz­nálnunk arra, hogy az egyik elemről a következőre lépjenek. Ha az érték létezik, a po­zí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;

Page 105: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 106: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 107: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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 listamegva­lósítást és relatív teljesítménykarakterisztikájukat. A tömblisták és a láncolt listák ha­sonló 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íta­nak. Másrészről viszont a láncolt listáknál nem áll fenn a másolás és átméretezés se­gé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 ese­tek 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 Java­tö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 tel­jesí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

Page 108: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 109: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 110: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 egyet­len, amit magunknak kell definiálnunk, a dequ eu e() metódus által jelzett Empty­QueueExcepti 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 any­nyit jelent, hogy a try-catch blokkokat a dequeue() hívások körül nem kell becso­magolni. Ennek elsődleges oka az, hogy az üres sarok visszakeresési kísérleteit prog­ramozási hibának tekintjük; a dequeue() hívása előtt bármikor meghívhatjuk az i sEmpty() metódust.

91

Page 111: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 112: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 113: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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�----------------- '"------------�

Page 114: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 115: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ódo­lá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 konst­ruktort definiál. Az első konstruktor egyetlen argumentuma egy lista, amely az ada­tokat tárolja (természetesen ellenőrizzük, hogy a mérete null) . A második- alapér­telmezett 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ömblis­tával, amely- ha emlékszünk rá- folyamatosan az elemek eltávolításával járó segéd­számítási költségekkel küzd!

Most, hogy már létre tudunk hozni listaalapú FIFO-sort, itt az ideje, hogy a sor­hoz 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

Page 116: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ő, azo­nos 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 biz­tonságos kezelésére, arnikor egyidejűleg több fogyasztó is hozzáfér a sorhoz. Ehe­lyett 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

Page 117: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 118: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 119: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 120: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 121: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 122: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 123: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 124: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 125: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 126: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 127: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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")

Page 128: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ő rendelke­zésre álló operátor" fogadja, ami nem azt jelenti, hogy a hívás nem fontos számunk­ra, 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 mun­kát. Az összes dolgozó operátor számára a GO_HOME speciális hívást kell a sorba he­lyezni. Á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 ügy­fé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 rend­szert, 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

Page 129: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 130: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 131: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 meg­felelő 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étre­hozott 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 ad­dig, 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

Page 132: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 so­ron 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ásod­percre (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 vala­melyest 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

Page 133: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 134: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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

Page 135: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Vermek

Észre fogjuk venni, hogy míg a listák és sorok általában balról jobbra futnak, a ver­mek 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 is­mert, 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 be­muta�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

Page 136: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ál­tuk. É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 folya­mata közben, igy nem akarjuk a fejleszt6t arra kényszeríteni, hogy fölöslege­sen 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

Page 137: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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)·

Page 138: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 139: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

Page 140: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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 sza­bad 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. Ehe­lyett azt a megoldást válasz�uk, amelyet a listákról szóló fejezetben, tehát felhasz­nálha�uk az előnyt, hogy egy lista mindennel rendelkezik, amire egy verem létreho­zásához szükség van.

Láthatjuk majd, hogy egészen egyszerűen valósíthatunk meg vermet, amely a lis­ta á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ábbfej­lesztünk, bővítünk egy már létező listamegvalósítást, vagy egy teljesen új osztályt ho­zunk 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, ame­lyek 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 lista­megvaló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

Page 141: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 142: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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, mi­kö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 át­há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

Page 143: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 vissza­adná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 ki­keresé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 len­gyel 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 fel­merü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őnk­ben) és a visszavon/újra elvégez parancs. A következőkben ez utóbbiról lesz szó.

124

Page 144: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 vissza­vonását. Minden alkalommal, amikor a felhasználó végrehajt egy műveletet, tárol­nunk 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 po­zí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 meg­valósí�uk a L i st interfészt, bővíthe�ük az AbstractL istTestcase osztályt, és egy­szerű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

Page 145: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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; ------�---------- ------ -----------

Page 146: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 147: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 148: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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él­hetü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ív­tuk 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 fog­juk, 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ú visz­szavoná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 vi­szonylag 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 bebur­kolnia 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

Page 149: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 150: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 151: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 152: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 153: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 Ja­vát is, veremalapú.

• A verem mindig a tetejéhez ad hozzá és távolít el értéket- ennélfogva gyak­ran 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 megmu­tatta, 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

Page 154: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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!

Page 155: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 156: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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, ami­kor ö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őfordul­hat viszont, hogy a létrehozás dátuma, az utolsó módosítás dátuma vagy éppen fájl­tí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 össze­hasonlító felel azért, hogy adott rendbe állitsa az objektumokat, így könnyen előfor­dulhat, hogy a fájlok rendezésekor külön összehasonlítónk lesz a fájlnevekre, a fájl­típusokra és megint másik a módosítás idejére. Ezek az összehasonlítók teszik lehe­tő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 ob­jektumot (ö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 viszo­nyított sorrendje határozható meg. Visszatérési értéke negatív egész, nulla vagy pozi­tí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

Page 157: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 158: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 159: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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él­dá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él­dá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 ar­gumentum 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

Page 160: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ít­ja 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 összehason­lí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 argu­mentum 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 ki­terjeszthető. Az összetett típusoknál, mint arnilyen a Person vagy a Fi l e, végül min­dig 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 kezel­nünk az összehasonlítás lehetséges eredménytípusainak megfelelően. Ebben a teszt­ben a korábban definiált Natural comparator segítségével hasonlí�uk össze az egy­szerű sztringértékeket.

141

Page 161: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 162: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 összehason­lítónak delegálunk; viszont ha jobban megnézzük, látha:tjuk, hogy a két argumentu­mot á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 ponto­san 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őalgoritmu­sunkat: 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 rende­zőalgoritmusnak ugyanazon az alapvető teszten kell megfelelnie (vagyis bizonyítania kell, hogy helyesen rendezi sorba az objektumokat), az egységtesztekhez létrehozunk

143

Page 163: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 sor­ba, 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 fi­gyelmü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ás­hoz 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érdez­zü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, melyi­kü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

Page 164: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ő, ren­dezett 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 hajt­juk 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

Page 165: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 166: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 167: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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); }

Page 168: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ófel­adatban 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ó meg­való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égsze­rű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

Page 169: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Alapvető rendezés

Minden sorozatban minden elempárt összehasonlítunk, és ha (az általunk meg­adott ö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 he­lyé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

Page 170: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 leg­nagyobbtól a legkisebbik. A 6.6. ábra mutatja a könyvespolcot, mielőtt hozzáfog­ná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, ame­lyik 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 feje­zet 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

Page 171: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 egy­gyel 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 tapasz­taljuk, 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 he­lyére kerül, ahelyett hogy kis lépésekben haladnánk a cél felé (mint a buborékrende­zé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 algo­ritmusánál végzett munkánk nagy részét. A következő gyakorlófeladatban egy teszt­esetet hozunk létre, megvalósítjuk magát az algoritmust, majd ellenőrizzük, hogy si­kerrel veszi-e a tesztet, vagyis helyes-e a megvalósításunk.

152

Page 172: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 173: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

}

Page 174: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 fi­gyelmet, 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 bubo­ré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

Page 175: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 176: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 177: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 178: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 179: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 180: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 181: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 182: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ése­kor 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él­dányváltozóra, amely az előző szakaszban létrehozott ca ll counti ngcomparator hi­vatkozá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 ko­rábbi példákban a sztringek is tették:

163

Page 183: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 184: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 185: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 186: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Összefoglalás

• A buborékrendezés és a kiválasztásos rendezés esetén a szükséges összeha­sonlí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 ösz­szehasonlí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 gyakor­latban 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ó egy­sé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 bonyo­lultabb rendezőalgoritmust mutat be, amelyekkel hatalmas mennyiségű adatot tu­dunk bámulatosan jól rendezni.

167

Page 187: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Alapvető rendezés

Gyakorlatok

l. Írjunk egy tesztet, amellyel bizonyítjuk, hogy minden fenti algoritmus tudja ren­dezni a véletlenszerűen generált kétszerezett objektumok listáját.

2. Írjunk egy tesztet, amellyel bizonyítjuk, hogy a fejezetben bemutatott buborék­rendezé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

Page 188: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 algorit­musoknak egyszerű az alkalmazása, szükségünk van további rendezési algoritmusok­ra 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, ugyan­akkor a leghatékonyabb általános célú rendezési rutinok közé tartoznak. Az a nagy­szerű 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ő ele­meket a megfelelő helyre mozgassák. A fejezet során tárgyalt fejlettebb rendezési al­goritmusok 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.

Page 189: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 190: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 191: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 192: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 193: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 194: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 195: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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álaszta­ni 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 ki­emelt 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 ele­mek 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

Page 196: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 197: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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úl­haladá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

Page 198: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 199: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 )� �; _____________ _,

Page 200: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 201: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ket­té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 ér­té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án­junk 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ő algorit­mus - az összefésüléses rendezés - stabil, tehát itt az ideje, hogy beszéljünk a Shell­rendezé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ód­szerrel foglalkozik, amely kompenzálni tudja ezt a hiányosságat ennek a két algorit­musnak 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 ve­zeté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évcso­portba 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ék­név szerinti rendezésnéL

182

Page 202: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 203: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 204: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 205: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 206: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 207: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Fejlettebb rendezés

Hasonlítsuk ezt össze a gyorsrendezéssel, ahol egy elemet a végső rendezett po­zí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ít­sé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öt­tünk, hogy addig folyta�uk a rekurzív hívásokat, amíg elérünk egy egyetlen elemből ál­ló 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, ren­dezzük a feleket, majd összefésüljük őket.

A 7 .34. ábrán látható a helyzet a harmadik rekurziós szinten.

188

Page 208: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 209: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 210: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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, aho­gyan az a 7 .42. ábrán látható. (Kihagytunk néhány lépést az előző és a következő áb­ra 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 ösz­szefé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

Page 211: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 212: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Az összefésüléses rendezési algoritmusról

Hozzuk létre a mergesort metódust, amely ezek után kezeli majd azokat a helyzete­ket 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 fel­osztja 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ár­ná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övet­kező 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

Page 213: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 214: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 215: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 216: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 217: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 algorit­musok 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 sta­bilitá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 egy­szerű összehasonlítását mutattuk be, amely lehetővé teszi, hogy megértsük a számta­lan 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 rekur­zí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 gyors­rendezé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

Page 218: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 219: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 220: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 221: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 222: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 223: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 UPO­verem esetében a sor már ezen a ponton nem felelne meg a teszt elvárásainak En­nek 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·------------------�··�·----------�

Page 224: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ó alkalma­zások esetében általánosnak mondható, hogy az enqueue() illetve a dequeue() me­tó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 sze­rinti 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

Page 225: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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·

Page 226: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 227: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 megva­ló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 me­tódus meghívásakor azonnal rendelkezésre álljon a legnagyobb elem. Ezzel a mód­szerrel 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 rende­zett 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

Page 228: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 229: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 230: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ér­csomó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 halornstuk­tú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. áb­rá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 ismer­ve 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

Page 231: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 232: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 233: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 234: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 235: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 236: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 237: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 je­lenti, 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ó elem­nek 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éte­lezzü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 gyer­mek 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ó legfejlet­tebb változat. A módszer azért is érdekes, mert o(log N) komplexitásban képes ele­meket 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, ren­delkezik 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

Page 238: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 szem­szögből közelí�ük meg a bemutatott módszerek összehasonlítását. Ismét a ca ll coun­ti ngcomparator osztályt használjuk annak megállapítására, hogy a különböző meg­való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 ar­ra, hogy további vizsgálódásokat ösztönözve új látásmódot szerezzünk. Számos, el­mé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 eset­ben 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 meg­felelő 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

Page 239: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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-meg­való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);

}

Page 240: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 241: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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öntet­lenü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 meg­lepő, 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 leg­inká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éko­nyabb, 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 legna­gyobb tárolt elernhez. A sorban elhelyezkedő elemek egymáshoz való viszo­nyát ös szehason l í tó segítségével állapítottuk meg.

Page 242: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Gyakorlatok

• A prioritásos sorok három megvalósítását készítettük el. Ezek közill a legegy­szerű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 eleme­ket mindig rendezett listában tartotta, ezzel jelentősen megkönnyítve a legna­gyobb 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űve­leteiné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övet­kező 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

Page 243: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 244: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 245: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 246: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 247: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 248: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 249: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 250: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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�;�------�-------------------�

Page 251: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 252: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 253: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ég­zü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ök­kentjü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 tartal­mazza 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

Page 254: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 255: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 256: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ve­zé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 folytat­juk 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ít­hassuk, 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 összeha­sonlítás megy végbe.

238

Page 257: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 258: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 259: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 260: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 fel­adata 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 sta­tisztiká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));

}

Page 261: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 262: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 lehes­sen 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 egy­formá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 át­lagosan 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

Page 263: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 264: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 265: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 266: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Bináris keresés és beszúrás

Az utolsó teszt véletlen értékeket szúr be a listába. Ezáltal megbizonyosodhatunk ar­ró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ál­tozó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ük­rözi a beszúrási pontot. Jelen esetben, arnikor növekvő sorrendben érkeznek a be­szú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úr­tunk, 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őriz­zük, hogy az i n se rt() metódus minden lúvás után O-t ad vissza (minden egyes alka­lommal 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

Page 267: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 268: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 269: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 270: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 271: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ólle­het 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 figye­lembe 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 bi­ná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 telje­sítményt N l ogzN becsülhetjük meg, de az összehasonlítások száma valójában in­ká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 fenn­tartsa 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 ren­dezetlen 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

Page 272: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 rende­zettek 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 telje­sí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 egy­gyel 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 va­lósítha�uk meg.

Page 273: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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

Page 274: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 275: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ér­csomó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

Page 276: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ér­csomó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 hi­vatkozá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 vo­natkozik az L betűre: rendelkezik egy jobb oldali gyermekkel, M-mel, tehát megke­ressü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ópon­tot, 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

Page 277: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 278: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 279: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 280: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 281: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 282: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 283: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 284: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 itera­tí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 kezd­jü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

Page 285: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 telje­sítményre. Pontosabban a rendezett adat beszúrása és törlése a fa kiegyensúlyozat­lanná 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ó algo­ritmusok, 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 megol­dá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 alka­lommal, 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

Page 286: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Kiegyensúlyo­zatlan

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 forga­tá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ér­csomó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. áb­rán látható.

Bár az A VL-fák nem garantáltan kiegyensúlyozottak, kiváló teljesítménykarakte­risztiká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 ese­té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

Page 287: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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·

Page 288: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 289: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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>':).·

Page 290: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 291: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 292: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 293: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 294: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 295: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

Page 296: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 297: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

}

Page 298: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 299: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 300: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 301: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 302: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 303: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

}.�------------�----

Page 304: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ékek­kel. 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 ér­té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ó ki­egyensú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

Page 305: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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"));

Page 306: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 megbizonyo­sodunk 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ésalgo­ritmus a várakozások szerint teljesítette. Most összehasonlíthatjuk a csomópont­struktú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 az­tán összehasonlítjuk, hogy egyenlők-e. Figyeljük meg, hogy P-t az L nagyobb gyer­meké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

Page 307: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 308: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 309: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 310: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 311: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 be­szú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ó cso­mópontot. Ha az értéket nem találja (node == null), akkor nyilvánvalóan nincs tenni­való, é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ékez­zü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övetke­ző 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";

Page 312: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 313: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 314: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 kapcsola­tos 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() el­nevezesű 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

Page 315: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 316: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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él­dá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 állapo­tá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, ren­dezett 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

Page 317: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ő ki­számításához!

300

Page 318: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 319: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 320: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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árolha­tó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óal­goritmusok többsége ennek következtében kompromisszumot jelent a területfogla­lá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álasz­tani 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 rendelke­zünk, amely teljes mértékben mentes az ütközésektőL Sajnálatos módon a tökéletes ha­sí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éle­test, 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 üt­kö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 sor­rendben 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

Page 321: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Hasítás

Íme egy példa, amely meglehetősen egyszerű, de azért hatékony hasító algorit­must tartalmaz a JDK String osztályában. Az algoritmus önmaga biztos matemati­kai 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 hoz­zá, 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 ha­sí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 ered­mé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 eb­ben 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

Page 322: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 323: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 324: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 325: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ren­delkezé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 rendel­kezé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ít­mé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

Page 326: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 esz­közként használni erre. Ráadásul a hashcode() JDK-megvalósítása a String osz­tá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

Page 327: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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)));

}

Page 328: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 329: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 330: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 331: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 332: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 333: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 me­tódus kap egy értéket, és visszaadja azt a tömbindexet, ahol őt tárolni kell. Azt a ha­sí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);

Page 334: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 335: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 336: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 337: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 338: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 339: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

}

Page 340: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ódu­sát hívjuk meg, és vesszük a vödrök számával való osztás utáni maradékot. Ez bizto­sí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. Alapve­tő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

Page 341: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 342: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 343: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 344: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 345: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 346: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 347: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ód­szer 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 át­lag 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

Page 348: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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

Page 349: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 350: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 351: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ért­jü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 áb­rá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

Page 352: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

}

Page 353: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 le­het 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

Page 354: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

Page 355: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}.

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

Page 356: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 357: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 358: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

Page 359: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 360: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Halmazok

Ezután az előrehaladó iteráció tesztelése érdekében a testiteratorForwards() me­tó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,_,;'--- --��--

Page 361: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 362: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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·

Page 363: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 364: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

}

Page 365: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 366: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 367: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 368: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 369: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 370: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

}

Page 371: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 372: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

Page 373: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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 je­lezze, semmilyen változás nem történt. Ellenkező esetben az eredetivel megegye­ző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 illeszke­dő 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 alkalma­sak, mivel a futásidejük O(N).

• A hasítótábla-alapú halmazok futásideje 0(1), véletlenszerű iterációs sor­renddel.

• A bináris keresőfa-alapú halmazok teljesítménye o(l og N), és iterációs sor­rendjük megjósolható.

357

Page 374: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 me­tó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

Page 375: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 376: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 377: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 378: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ódu­sokra 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;

Page 379: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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érhe­tő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 meg­hatá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 jellem­ző helyzetet szemléltet, amelyben az adatbázisrekord leképezésként jelenik meg.

363

Page 380: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 teszt­esetet. Ezt aztán kiterjeszthe�ük egy tesztosztállyal, amely jellemző bármely leképe­zésmegvalósításra, amelyet később készítünk majd el.

364

Page 381: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 382: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

Page 383: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 384: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

Page 385: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 386: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

Page 387: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 388: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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());

}

Page 389: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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áro­lási mechanizmusa a lista lesz. A megvalósítás meglehetősen lényegre törő és köny­nyerr követhető; ennek ellenére nem kimondottan hatékony, de kisebb adathalma­zokhoz 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

Page 390: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 391: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 392: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 393: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 394: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 395: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 396: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 397: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 398: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 399: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 400: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 401: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 402: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 403: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 404: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 405: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

• 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

Page 406: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 407: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 cso­mó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 be­tű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 illeszke­dó csomópontot, akkor egy szintet lejjebb megyünk annak gyerekéhez, és újrakezd­jü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 ke­resve, 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 új­ra 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ópont­ban 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 be­tűre (a) a b első gyerekével kezdve, ahogy a 14.5. ábrán látható.

392

Page 408: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 409: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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övetke­ző betű keresésére. Ezt a keresett szó összes betűjével elvégezzük. Innentől követ­keztetni tudunk a keresés végrehajtásának futásidejére hármas keresőfánkban. Ész­revehetjü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 le­hetnek, 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

Page 410: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 összehason­lí�uk a "man" szót a "cup" szóval, de az első betűnél rnivel nem illeszkedik, továbblép­hetü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ár­mas keresőfa jóval kevesebb karakterenkénti összeh�sonlítást végez az ekvivalens bi­ná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. Ha­sonlitsuk 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 tel­jesítménykarakterisztikát a hármas keresőfákra. Képzeljük el, hogy minden szint tar­talmazta 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) összeha­sonlí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

Page 411: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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égre­hajtása: egyszerűen új levélcsomópontokat adunk hozzá bármely már nem létező be­tűhöz. A 14.8. ábrán láthatjuk, hogy a "bats" szó beszúrása egyetlen gyermekcso­mópont hozzáadását igényli, a létező "bat" szó végéhez hozzáfűzve, a "mat" szó be­szú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 jel­ző 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 akar­ná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 sza­vak ("cup", "ape", "bat", "map" és "man") sorrendben való beszúrásának az ered­mé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ó, ki­egyensúlyozatlan fát eredményezi.

396

Page 412: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 413: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 414: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 gondolkod­tunk 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 min­tával. A minta szabályos - a-tól z-ig- betűkből áll és egy speciális helyettesítőkarak­terből, amely bármire illeszkedik. Az aktuális példában a kötőjelet (-) használtuk he­lyettesí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 ke­resőfában. A legegyszerűbb módja annak, hogy mintaegyezést keressünk, talán a le­tá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 ke­resése helyett (nem fogunk találni egyet sem), mindegyik csomópontot megvizsgál­juk, mintha illeszkedést találnánk.

399

Page 415: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 416: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 417: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 418: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 419: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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));

}

Page 420: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 421: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 422: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 423: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 424: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 425: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 426: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 427: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 428: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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ó kere­sése, és egy pozíciót a szón belül, ahonnan kezdődik a keresés. Így a search() visz­szaadja 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övetke­ző 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 kere­sé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 fo­gyunk ki. Ezen a ponton bármelyik csomópont, amelyben éppen vagyunk (ha van) eredményként tér vissza:

413

Page 429: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 fris­sí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ő ka­rakter, 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

Page 430: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 431: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 432: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 433: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

}

Page 434: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 435: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 436: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 437: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 kiegyen­súlyozadanná.

• Általában többszörösen hatékonyabbak, mint a bináris keresőfák, mivel át­lagosan kevesebb számú karakter-összehasonlítást végeznek

Gyakorlat

1. Hozzuk létre a search() egy iteratív formáját!

422

Page 438: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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!

Page 439: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 440: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 441: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ma­gassá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 egyen­ké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 bi­náris keresáfa esetében.

A B-fába úgy szúrunk be kulcsot, hogy a gyökérből kiindulva megkeressük a he­lyé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

Page 442: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 443: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 444: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 445: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 446: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 447: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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; _______________ __,

Page 448: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 449: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

Page 450: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 451: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 452: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 453: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 454: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 455: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 456: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 457: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 458: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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

Page 459: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 460: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 461: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Á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 he­lyett. 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 azon­ban olyan eset, arnikor csak sima string típusban kell keresnünk. Ebben a két osz­tá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ípu­sok 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 algorit­mushoz szükséges metódusokat.

A search() metódus hívásakor vagy a St ri ngMatch osztály egy példányát kap­juk 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 tar­talmaznak 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, egy­mást átfedő minta-előfordulás keresése. Ezek mindegyike más vonatkozásban tesz­teli a sztringkeresőt, arnivel igazolható a helyessége.

447

Page 462: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 463: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Á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

Page 464: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 465: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 466: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 467: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 468: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ha­ladó 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 tel­jesí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 illeszke­dé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-algo­ritmust 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áma­dá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 illeszke­dé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őfordu­lását, és innen folytatni, a hibás karakter elmélet szerint:

454

Page 469: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 470: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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öveg­ré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 hoz­tunk létre. Ezúttal azonban egy további tesztet is készítünk, kifejezetten a Boyer­Moore-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));

}----------------------------------------------�

Page 471: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 472: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 473: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 474: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 475: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 476: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 477: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 478: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 479: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 480: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 481: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

A teljesítmény összehasonlítása

Bizonyára mindenkinek feltűnik ez a furcsa szám: "8859_1"

. Ez egy karakter­készlet neve, amelyre a charsuffer használatához van szükség; egyébként nem tud­nánk, hogyan kell dekódolni a fájl szövegét. A "8859_1

" karakterkészlet az ISO La­tin-1-nek felel meg, amelyet a nyugat -európai nyelvek, például az angol es etén hasz­ná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 ve­lük különböző mintákat. Ehhez Lev Tolsztoj Háború és bék�ét használtuk fel - en­nek 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 mi­né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

Page 482: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Sztringkeresés

Összefoglalás

Ebben a fejezetben néhány gyakori és jól ismert sztringkeresési algoritmust mutat­tunk be - a letámadásos és a Boyer-Moore-algoritmust, -, valamint a közös sztring­keresó 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 legrosz­szabb 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 karak­ter-összehasonlításokat, és mindig több karakterpozíciót ugrik. A legrosz­szabb esetbeli futásideje olyan rossz vagy egy kicsit rosszabb (a kezdeti min­tafeldolgozás segédszámítási költsége miatt), mint a letámadásos algoritmu­sé. 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 fo­lyamatosan á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 bonyo­lult é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 min­ta felépítésétől, hosszától stb. függően eltérőek lesznek. Összességében re­mé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 kere­sési algoritmusok teljesítménye között.

Page 483: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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

Page 484: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 485: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 486: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 487: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 488: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 489: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 490: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 491: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Sztringillesztés

A többi szabály tesztelése nagyjából megegyezik ezzel, a munka nagy részét segéd­metó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 össze­ve�ü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ással­hangzót figyelmen kívül kell hagynunk. A testvowel sAreignored() metódus ezt el­lenő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 utol­só három karaktere "OOO" lesz, mivel a inagánhangzót figyelmen kívül hagytuk, a to­vá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 megillv­tuk 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'});

Page 492: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 493: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 494: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 495: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ka­rakteréből a Soundex szabályai alapján kódolt értéket állít elő. A karaktert előbb index­szé 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 - hi­szen 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ő karakte­ré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 hagy­juk. 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));

Page 496: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 497: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ak­kor, 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

Page 498: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 bemu­tatja az egyik lehetséges megoldás menetét.

485

Page 499: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 500: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 501: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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él­dá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 megad­nunk neki, ezután kiszátrú�a a szavak távolságát, és összeveti a várt értékkel. A me­tódus legfontosabb tulajdonsága, amiért tulajdonképpen létrehoztuk, az, hogy a sor­rend felcserélésével kétszer számolja ki a szavak távolságát. Így a sorrendtől függet­lenü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

Page 502: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 503: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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)

Page 504: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 505: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 sejt­hető, 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ég­kö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éke­ket átadja a mi n() metódusnak- így kényelmesen megtalálha�uk a legkisebb értéket:

492

Page 506: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 507: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 el­gé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ágiumfelis­merők és egyéb alkalmazások alapja.

• A Levenshtein-algoritmus idő- és 'térbeli bonyolultsága O(MN).

494

Page 508: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 509: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 510: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 511: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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: meg­hatá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 meg­hatá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

Page 512: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 513: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 el­mozdulá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 ten­gelyt (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

Page 514: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 515: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ér­tékkel, befejezi a munkánkat. Itt az ideje, hogy az előzőekben tárgyalt elméletet kód­ba dolgozzuk. A következő gyakorlófeladatban a fogalmak nagy részét Java objek­tumokba 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)�)�· ···-" ______ _,

Page 516: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 517: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 518: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 519: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 520: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 521: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 konstruk­tor á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ő eset­ben mindkét egyenes szakasz függőleges, ebben az esetben a meredekségeik azono­sak; 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 egyen­lő, ha a Java double típusú numerikus értékük egyenlő. A kódnak minden olyan ese­tet 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 tesz­tesetek 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)'-' )'--')'"-';'------------'

Page 522: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 szakasz­szal. Ó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

Page 523: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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));

Page 524: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 525: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 526: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ke­zeljünk, és ne csak feltételezzük, hogy megtettük. Úgy tűnik, rengeteg teszteset léte­zik, de ne felejtsük el, hogy sok tulajdonságot vizsgálunk. Ha ennél több tesztre len­ne szükségünk, meg kell fontolnunk, ne bontsuk-e több osztályra a működést. Ép­pen 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 eze­ken 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 mere­deksé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 tu­lajdonságát, hogy képes meghatározni, azonos-e egy másik 51 ope-pal.

513

Page 527: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 528: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 529: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ob­jektum 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, ak­kor 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 tudhat­juk, 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 szaka­szon 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 meg­határozásához. Az alapgondolat a következő: ha az egyenes szakaszok párhuzamo­sak, 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őriz­nü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íta­nunk a kérdéses egyenes szakaszokról, hogy függőlegesek-e. Ha bármelyik is az, ak­kor 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

Page 530: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 figyel­nünk kell arra az esetre, ha valamelyik egyenes szakasz függőleges (nem kell ezt ten­nünk, hamindkettő az). Ez azt jelenti, hogy egyszerűen csak egy nem függőleges egye­nes 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 fo­galmakat reprezentálja néhány jól tesztelt és értékes működéssel. Így hát továbblép­hetü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 na­gyon 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. Ehe­lyett 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áta­rendszerben. 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

Page 531: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 (bal­ró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 megje­gyeznü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. Le­het, 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áli­san ismert. Ha közelebbi párt talál, az algoritmus kisebb vonóhálóval fut tovább, mi­utá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 ven­nü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

Page 532: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 533: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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. Áll­jon 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 hama­rabb 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 eb­ben 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 bizto­sí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

Page 534: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 535: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 536: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 537: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ábbiak­ban 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. Önma­gá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

Page 538: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 539: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 540: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 541: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 542: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Ö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

Page 543: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 megismerke­dü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 telje­sí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 felis­merni, 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 esz­kö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ör­nyezetre jellemző, programunk teljesítményét érintő problémák típusaival, és prog­ramozá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.

Page 544: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 helye­sen, 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 alkal­mazá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 érhe­tü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ód­ré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ít­mé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 fejezet­ben foglaltakat javasoljuk Teszteléssei a teljesítményjavítás érdekében végzett változta­tá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 kereszt­metszet 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ít­hatjuk 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övel­hetné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

Page 545: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 546: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

Pragmatikus optimalizálás

Ezekkel a statisztikákkal már képet kaphatunk róla, hogy a kód mely részét lehet op­timalizálással javítani. A memóriafelhasználás mérésére ehhez hasonlóan a profilíro­zó 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 fu­tásidejű viselkedését. Ahogy később egy példaprogram optimalizálásán keresztül lát­ni 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 optimali­záló eszközeivel dolgozunk. Ezek ugyan egyszerű, de könnyedén hozzáférhető esz­kö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 is­merjü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 beme­netrő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ít­sük, valamint érdekesebbé tegyük az optimalizálást. Ne aggódjunk, ha nem látjuk ér­telmét, mert valójában semmi célja nincs.

534

Page 547: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 548: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 549: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 550: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 551: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 552: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 553: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 554: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 555: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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érel­jü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 hin­nénk. A fejezet hátralévő részében feltételezzük, hogy a célunkhoz megfelelő algo­ritmust 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 optimali­zá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 felgyor­sí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 ja­ví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ít­mé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

Page 556: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 557: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 558: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 to­vá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 osz­tály hány objektumát szabadította fel szemétgyűjtés. Láthatjuk, hogy összesen mint­egy 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

Page 559: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

• 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 al­kalommal 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 objektu­mot 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 hasz­nálhatnánk természetes összehasonlitót. Így a jelenlegi rendezési algoritmusból ki­vennénk a munka nehezét, és jelentős mértékben lecsökkentenénk a futás során lét­rejö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 le­mé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

Page 560: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 561: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 562: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 563: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 564: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 565: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

"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.

Page 566: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

" 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/

Page 567: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

" 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.

Page 568: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

" 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 ();

}

Page 569: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}.

Page 570: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 571: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 572: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 573: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

A gyakorlatok megoldásai

3. fejezet

Gyakorlatok

1. Írjunk egy olyan konstruktort az ArrayList osztályhoz, amely szabványos Java­tö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 tel­jesí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

Page 574: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 575: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 576: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 577: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 578: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 579: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ren­dezni a véletlenszerűen generált kétszerezett objektumok listáját.

2. Írjunk egy tesztet, amellyel bizonyítjuk, hogy a fejezetben bemutatott buborék­rendezé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

Page 580: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 581: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 582: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 583: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 584: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 rekur­zí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 gyors­rendezé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

Page 585: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 �()�; �--------------------------�

Page 586: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 587: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 588: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

},

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

Page 589: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 590: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 591: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

Page 592: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 593: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 594: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 595: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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.

Page 596: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 597: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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, ren­dezett 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

Page 598: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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ő ki­szá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

Page 599: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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);

Page 600: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 601: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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)];

Page 602: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 603: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

Page 604: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 me­tó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

Page 605: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

Page 606: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 607: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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�(�)�;--------�

Page 608: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

}

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

Page 609: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 610: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 611: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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();

}

Page 612: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 613: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 614: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 615: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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·

Page 616: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 617: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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;

}

Page 618: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 619: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 620: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 621: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 622: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 623: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 624: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 625: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 626: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

ö,ő ö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

Page 627: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 628: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 629: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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

Page 630: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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 ön­erő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 ku­tatá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.

Page 631: Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

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