Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT

November 26, 2017 | Author: danikaa3 | Category: N/A
Share Embed Donate


Short Description

Download Kezdokonyv.az.Algoritmusokrol.2006.eBook DigIT...

Description

Simon Harris - James Ross

Kezdőkönyv az algoritmusokról

Simon Harris - James Ross

Kezdőkenyv az algoritmusokról

200 6

Kezdőkönyv az algoritmusokról

Eeginning algorithms, Simon Harris-James Ross Copyright © 2005 by Wiley Publishing, Inc., Indianapolis, Indiana All rights reserved. This translation published by license.

Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its 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]

Tartalomjegyzék Köszönetnyi Ivánitás

xiii

Bevezetés Kinek szól a könyv?

xiii

Elvárt előismeretek

xiii

A könyv témája

xiv

A könyv használata

xiv

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.

xi

XV

xvi xvi XV11

xvii xviii

xix XX

xxi xxi xxii

Az alapok

1

Az algoritmusok definíciója

1 4 5

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

7 7 8 9 9 10 11 13 13 17 18

Tartalomjegyzék 2.

Iteráció és rekurzió Számítások végrehajtása Tömbök feldolgozása Iterátorak használata tömbalapú problémák megoldására Rekurzió Rekurzív könyvtárfa-nyomtatási példa A rekurzív algoritmus működése Összefoglalás Gyakorlatok

3.

Listák Alistákról Alisták tesztelése Listák megvalósítása A tömblista Láncolt lista Összefoglalás Gyakorlatok

4.

Várakozási sorok Asorok Sarműveletek A sorinterfész AFIFO-sor A FIFO-sor megvalósítása Blokkolósorok Példa: telefonos ügyfélszolgálat szimulátora Az alkalmazás futtatása

Összefoglalás Gyakorlatok

5.

Vermek Vermek Atesztek Megvalósítás Példa: az undo/redo parancs megvalósítása Az undo/ redo parancs tesztelése Összefoglalás

v

i

19 20 22 23

42 44 47

48 49 51 51 55 68 69 77

87 87 89 89 90 91 92 96

97 102 112

114 114 115 115 118 121 124 125

134

Tartalomjegyzék 6.

Alapvető rendezés A rendezés fontossága

135

Rendezési alapismeretek

136

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

151

A beszúrásos rendezésről

156

A stabilitásról

160

CallCountingListComparator ListSorterCallCountingTest Az algoritmus-összehasonlításról

161

162 163 166

Összefoglalás

167

Gyakorlatok

168

Fejlettebb rendezés

169

A Shell-rendezési algoritmus alapjai

169

A gyorsrendezésről

175

Az összetett összehasonlítóról és a stabilitásról

182

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

8.

137

137 138 138 143 146 146

A kiválasztásos rendezés alkalmazása

Az alapvető rendezési algoritmusok összehasonlítása

7.

135

186

186 187

A fejlettebb rendezési algoritmusok összehasonlításáról

194

Összefoglalás

198

Gyakorlatok

198

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

199 199

200 203 206 208 210

Prioritásos sorok megvalósításainak összehasonlítása

219

Összefoglalás

222

Gyakorlatok

223

vii

Tartalomjegyzék 9.

Bináris keresés és beszúrás A bináris keresés működése

228

Listabeli kereső

228

Iteratív bináris kereső

236

A listabeli kereső teljesítményének vizsgálata

238

Listabeszúró Teljesítmény vizsgálata Összefoglalás

Bináris keresőfák A bináris keresőfákról

11.

250 254

257 257 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 A bináris keresőfa teljesítményének megállapítása

295

Összefoglalás

299

Gyakorlatok

299

Hasitás

270

301

A hasítás megértése

301

Munka a hasítással

309

Lineáris vizsgálat

312 319

A teljesítmény megállapítása

324

Összefoglalás

331'

Gyakorlatok

331 ..

Halmazok A halmazokról Halmazmegvalósítások tesztelése Listahalmaz

viii

245 246

Minimum

Vödrös módszer

12.

225

A bináris keresés megközelítései

Bináris beszúrás működése

10.

225

333 333 337 344

Tartalomjegyzék Hasítóhalmaz Fahalmaz Összefoglalás Gyakorlatok

13.

Leképezések A leképezésekró1

Leképezésmegvalósítások vizsgálata Listaleképezés Hasítóleképezés Faleképezés Összefoglalás Gyakorlatok

14.

Hármas keresőfák Hármas keresőfák Szó keresése Szó beszúrása Prefix keresés Mintaillesztés A hármas keresőfák gyakorlati alkalmazása

Keresztrejtvény megoldását segítő példa Összefoglalás Gyakorlat

15.

B-fák A B-fákról

B-fák a gyakorlatban Összefoglalás Gyakorlatok

16.

Sztri ngkeresés Általános sztringkereső interfész Általános tesztcsomag Letámadásos algoritmus A Boyer-Moore-algoritmus

A tesztek létrehozása Az algoritmus megvalósítása Sztringillesztő iterátor A teljesítmény összehasonlítása

A teljesítmény mérése Az összehasonlítás eredménye Összefoglalás

346 350 357 358 359 359 364 373 377 381 388 389 391 391 392 396 398 399 403 417 422 422 423 423 429 443 443 445 445 447 451 454 456 457 461 462 463 467 468

ix

Tartalomjegyzék 17.

18.

Sztringillesztés A Soundex algoritmus

471

A Levenshtein-szótávolság

483

Összefoglalás

494

Számítógépes geometria Rövid geometriai ismédés

495 495

Koordináták és pontok

495

Egyenes szakaszok

497

Háromszögek

497

Két egyenes szakasz metszéspontjának meghatározása

498

Meredekség

499

Az y tengely metszése

500

A metszéspont meghatározása

19.

471

501

A legközelebbi pontpár meghatározása

517

Összefoglalás

529

Gyakorlatok

529

Pragmatikus optimalizálás

531

Az optimalizálás szerepe

531

A profilírozásról

533

A FileSortingHelper példaprogram

534

Profilirozás a hprof modullal

538

Profilírozás a J ava Memory Profiler programmal

541

Az optimalizálásról

543

Optimalizálás a gyakorlatban

544

Összefoglalás

552

" "A függelék: Ajánlott irodalom

553

" "B függelék: Internetes források

555

" "C függelék: Bibliográfia

557

" "D függelék: A gyakorlatok megoldásai

559

Tárgymutató

609

A szerzőkről

621

x

Köszönetnyi lvánitás Simon Harris Először is hatalmas köszönet illeti Jon 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.

Köszönetnyilvánítás

Ha már a hosszú éjszakákról esett szó, oszmtén meg kell vallanom, hogy a könyv Oegalábbis az én részem) nem készülhetett volna el az életemben fontos sze­ repet betöltő hölgyek szeretete és megértése nélkül: ők Catherine, a mi külön nap­ rendszerünk középpontj a, Jessica, Ruby és a kis Ella, aki hat hónapos volt, amikor a könyv írásába belekezdtem, és a munka során minden egyes éjjel legalább 12 órát aludt. Lehet, hogy soha nem olvasod el ezt a könyvet, kicsim, de ha én a kezembe veszem, mindig te jutsz eszembe!

xii

Bevezetés A Kezdókiitryv az algoritmusokróllépésenkénti bevezetőt nyújt a szárrútástechnikai algo­ ritmusok életszerű használatának világába. A fejleszták mindennapi munkájuk során algoritmusokkal és adatstruktúrákkal dolgoznak. Az algoritmusok alapos ismerete és annak felismerése, hogy mikor kell alkalmazni őket, nélkülözhetetlen a szoftverek készítése során, hogy azok nemcsak helyesen, hanem megfelelő teljesítménnyel is működjenek. A könyv célja, hogy a napról napra haladó szaftverfejlesztés során leggyakrab­ ban előforduló algoritmusokat és adatstruktúrákat bemutassa, ugyanakkor maradjon gyakorlatias, pontos, lényegre törő, és igyekezzen nem eltérni az alapszintű témakö­ röktől és példáktóL

Kinek szól

a

könyv?

A könyv azoknak szál, akik alkalmazásokat fejlesztenek, vagy éppen fejlesztésbe fognak, és szeretnék megérteni az algoritmusokat és az adatstruktúrákat. A célkö­ zönség a programozók, fejlesztők, szoftvermérnök-hallgatók, információrendszer­ hallgatók és informatikushallgatók népes tábora. A könyv szerzői feltételezik, hogy a számítógépes programozás általános ismere­ tei a birtokukban vannak, és remélik, hogy a kötet a kód kihagyásával- még ha nagy­ részt fogalmi szinten is - olvasható és követhető az első oldaltól az utolsóig. Ebből kifolyólag csoportvezetők, építészek és üzleti elemzők is haszonnal forgathatják.

Elvárt előismeretek Mivel a példaprogramok mindegyike a Java programozási nyelv felhasználásával ké­ szült, használható Java-tudásra, valamint a szabványos Java-könyvtárak- különösen a j ava. l an g csomag - ismeretére szükség lehet. A tömbökkel, ciklusokkal és egyéb programozási technikákkal sem árt tisztában lenni, és természetesen a Java-osztályok létrehozásának és fordításának mikéntje is lényeges. Az itt említett előismereteken kívül más követelmény nem szükséges a kötet adatstruktúrákra vagy algoritmusokra vonatkozó ismeretanyagának elsajátításához.

Bevezetés

A könyv témája A kötetben részletes magyarázatokat, néhány megvalósítást, a mindennapi használat­ ra vonatkozó példákat és gyakorlatokat találunk, amelyek mindegyikének célja, hogy olyan tudás birtokába jussunk, amellyel új ismereteinket az életben is kamataztami tudjuk. A könyvben található példák ritkán elméleti természetűek. Az egyes fejezetek kódjait különös gonddal válogattuk össze, és azokat a legtöbb esetben akár azonnal is használhatjuk életszerű alkalmazásokban. Próbáltunk ragaszkodni a legáltalánosabban elfogadott szaftverfejlesztési gya­ korlatokhoz. Ezek közé tartozik a tervezési minták [GoF - Gang of Four, Design Patterns], a kódolási konvenciók, a minóség-ellenórzések és a teljesen automatizált egységtesztek használata. Remélhetőleg az algoritmusok és az algoritmusok problé­ mamegoldásban betöltött rendkívül fontos szerepének megértésén kívül megtanul­ juk, hogy a robusztus, bővíthető és természetesen működó szoftverek építése tiszte­ letet érdemlő tevékenység. A Java-nyelvben járatos olvasók felfedezhetnek némi átfedést a könyvben is­ mertetett osztályok és a j ava. uti l csomag osztályai között. A könyv nem foglalko­ zik a Java-könyvtárakban található specifikus megvalósításokkal. Ehelyett inkább bepillantást enged abba, miért tartották fontosnak a Java-nyelv tervezői bizonyos al­ goritmusok és adatstruktúrák megvalósításainak beépítését csakúgy, mint azok mű­ ködését és használatát is. A kötet nem a számítógépes programozás alapjait tanítja meg, sem általában, sem a Java-programozás tekintetében. Nem ismerteti a szabványos Java-könyvtárak használatának szabályait sem: nem ez célja. Noha a példaprogramok használják a

j ava. l an g osztályait és néhány esetben a j ava. i o csomagokat, az összes többi Java­ csomag túlmutat a könyv témáján. Ehelyett az összes szükséges osztályt kézzel épít­ jük meg, ezáltal tapasztalha�uk az algoritmusok felfedezésének örömét. Noha az egységtesztelés minden fejezetben kiemeit figyelmet kap, a kötet nem egységtesztdési kézikönyv vagy útmutató. Inkább az egységtesztek kódolásának be­ mutatásával próbálja meg elsajátíttatni az alapszintű egységtesztelés alapismereteit.

A könyv használata A könyvet az elejétól a végéig érdemes elolvasni. Rendezési, keresési és egyéb meg­ határozott algoritmusok segítségével a kötet az algoritmusok, adatstruktúrák és telje­ sítménykarakterisztikák alapjain vezeti végig az olvasót. A könyv négy fő részból áll.

xiv

A megközelítés alapelvei •

Az első öt fejezet az algoritmusok alapjait, például az iterációt, a rekurziót ismerteti, mielőtt bevezetné az olvasót az alapvető adatstruktúrák, a listák, a vermek és a sorok világába.



A 6-10. fejezet különböző rendezési algoritmusokkal foglalkozik, valamint olyan nélkülözhetetlen témákkal, mint a kulcsok és a sorrend kérdése.



A 7-15. fejezet a tárolás és keresés hatékony módszereivel foglalkozik hasí­ tótáblák, fák, halmazok és leképezések segítségéveL



A 16-19. fejezet speciális és bonyolultabb témaköröket érint, emellett részle­ tezi az általános teljesítménybeli buktatókat és az optimalizálási módszereket.

Minden fejezetben újabb, az előző fejezetek témaköreire épülő fogalmakkal találko­ zunk, amelyek megalapozzák a következő fejezetek ismeretanyagát. Tehát a könyvet bármelyik fejezetnél felüthetjük, és néhány fejezet átlapozásával megfelelő képet kaphatunk a témáról. Mindenesetre tanácsos minden fejezetben elvégezni a példabeli megvalósításokat, példaprogramokat és gyakorlatokat, hogy a tárgyalt fogalmak és elvek teljesen letisztulhassanak. A könyv végén lévő függelékekben megtaláljuk a to­ vábbi ajánlott olvasmányok listáját, a felhasznált weboldalak listáját és a bibliográfiát.

A megközelítés alapelvei A kód megértésének többnyire az a legnehezebb része, hogy átlássuk a döntéshoza­ tali folyamatot befolyásoló, gyakran íratlan feltételezéseket és elveket. Ezért tartjuk fontosnak, hogy részleteiben megvilágítsuk a könyvben alkalmazott megközelítést. Betekintést engedünk a logikai alapokba, amelyeket alapvető fejlesztési gyakorlatnak tekintettünk a könyv megírása során. A könyv elolvasása után remélhetőleg az olva­ só is méltányolja majd, hogy miért hiszünk a következő elvekben. •

Az egyszerűség jobb kódot eredményez.



Ne optimalizáljunk idejekorán!



Az interfészek hozzájárulnak a tervezés rugalmasságához.



A kódot automatikus egység- és funkcionális tesztelésnek kell alávetni.



Az assertion technika a fejlesztő legjobb barátja.

XV

Bevezetés

Törekedjünk az egyszerűségre! Milyen gyakran halljuk ezt a megjegyzést: meg." Vagy:

"Ó, ez túl bonyolult!

Úgysem értené

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

A megközelítés alapelvei

Felhasználói interfészek A:z adatstruktúrák és algoritmusok nagy része ugyanazt a külsó működést muta�a, még

akkor is, ha a mögöttes megvalósítás eléggé eltérő. A:z életszerű alkalmazásokban a kü­ lönbözó megvalósítások között gyakran feldolgozási vagy memóriamegszorítások mi­ att kell választanunk. A:z esetek zömében ezek a megszorítások előre nem ismertek. Az interfészek lehetóvé teszik, hogy mögöttes megvalásításra való tekintet nél­ kül meghatározzuk a megállapodást. Ebből kifolyólag a tervezést a megvalósítás be­ köthetóségének támogatásával teszik rugalmassá. Ezért van szükség arra, hogy minél inkább az interfészeknek megfelelóen kódoljunk, és így lehetóvé tegyük a különbözó megvalósítások helyettesítését. A könyv minden példabeli megvalósítása a meghatározott· működés interfész­ műveletekre való fordításával kezdődik. A legtöbb esetben ezek a műveletek a kö­ vetkező két csoport valamelyikébe sorolhatóak: alapszintű vagy elhagyható. Az alapszintű műveletek biztosítják az adott interfészhez szükséges alapműkö­ dést. A megvalósítások általában az első elvekból származnak, és ezért szarosan ösz­ szefüggnek egymással. Az elhagyható műveleteket ezzel szemben az alapszintű műveletekre alapozva valósíthatjuk meg, és rendszerint a fejlesztő kényeimét szalgálják Szükség szerint magunk is könnyűszerrel megvalósítha�uk őket saját alkalmazáskódunkban. Mivel a gyakorlatban sokan használják őket, ezeket a műveleteket az alapszintű API részé­ nek tekinthetjük, és egy adott témakör tárgyalását addig nem fejezzük be, amíg mindegyiket részleteiben meg nem valósítottuk

Tesztelni, tesztelnil A korszeru fejlesztési gyakorlat megköveteli, hogy a szaftver szigorúan egyesített és funkcionálisan tesztelt legyen a kód integritásának biztosítása érdekében. A megkö­ zelítést követve az interfész definiálása után, de még bármilyen konkrét megvalósítás definiálása előtt funkcionális követelményeinket tesztesetre fordítjuk annak ellenőr­ zésére, hogy minden feltétellel foglalkoztunk, és megerősítettünk őket. A tesztek a J Unit segítségével készültek, amely a J ava tényleges szabványos tesz­ tdési keretrendszere, és a tesztek a megvalósítás minden funkcionális szempontját ellenőrzik. A tesztek a meghatározott interfészek alapján, nem pedig bármilyen konkrét meg­ valósítás alapján készültek. Ez lehetóvé teszi, hogy az összes megvalósítás esetén ugyanazokat a teszteket alkalmazzuk, és így biztosítsuk az egységes minóséget. Ezen­ kívül a különbözó teljesítménykarakterisztikákat is bemuta�ák, ami akkor fontos, ha az alkalmazásban a használni kívánt különbözó megvalósítások között válogatunk.

xvii

Bevezetés

A tesztelés puristái kifogásolják, hogy a tesztek az ó ízlésük szerint túl hosszúak, és egy metódusban túl sok dolgot tesztelnek Hajlamosak lennénk egyetérteni velük, de a megértés támogatása érdekében leegyszerűsí�ük a dolgokat, és alkalmanként úgy gondoltuk, hogy vehe�ük magunknak a bátorságot, és néhány helyzetet össze­ vonhatunk egyetlen tesztmetódusban. A lényeg, hogy mielótt bármilyen megvalósírási kódot elkészítenénk, először írjuk meg a teszteket. Ez a megközelítés, a tejifelésen alapulóprogramozás (test-driven development, IDD) az osztályok megállapodására, azaz a közzétett viselkedésre összpontosít, nem a megvalósításra. Lehetóvé teszi, hogy a teszteseteket majdhogynem a kód követelmé­ nyeiként vagy használatának eseteiként kezeljük; a tapasztalatok szerint ez is egyszerű­ síti az osztályok terveit. Mint a példákban látni fogjuk, azáltal, hogy az interfészekhez kódoljuk tesz�einket, gyerekjáték lesz a tesztelésen alapuló programozás.

Legyünk alaposak! 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 Null Poi 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.

A

xviii

Mire van szükség a könyv használatához?

Ha engedélyeztük volna a kódszintű érvényességvizsgálatot, a szoftver teljesen megjósolható módon hibát jelzett volna abban a pillanatban, hogy a baj előállt, és valószínűleg a probléma diagnosztizálásához szükséges összes információ is a ren­ delkezésünkre állt volna. Ehelyett jóvátehetetlenül megsérültek a rendszer adatai. Az éles kód vizsgálata lehetővé teszi, hogy a kód hibái megjósolható módon áll­ janak elő, ami lehetővé teszi a probléma okának és természetének könnyű és gyors azonosítását, és elhanyagolható segédszámítási költségekkel jár. Egyetlen pillanatig se gondoljuk, hogy a vizsgálatok hátrányosan befolyásolják a rendszer teljesítményét. Jó esélyünk van arra, hogy a kód összes vizsgálatához szükséges idő nem összemér­ hető egy távoli eljáráshívásban vagy adatbázis-lekérdezésben töltött idővel. Ajánlatos az éles kódban bekapcsolt állapotban hagyni a vizsgálatokat.

Mire van szükség a könyv használatához7 A felépítés és futtatás nem is lehetne könnyebb. Ha kezdeti előnnyel szetetnénk in­ dulni, a teljesen működőképes projektet forráskóddal, tesztekkel együtt, valamint az automatizált parancssori verziót letölthetjük a Wrox webhelyéről Oásd a "Forrás­ kód" című részt).

Ha a "csináld magad" megközelítés hívei vagyunk, szerencsénk van, mert így mi­ nimalizálha�uk a függőségek számát. Kiindulásként a következőkre van szükségünk: •

Java Development kit QDK) 1.4 vagy újabb verziója, amely tartalmazza a kód fordításához és futtatásához szükséges összes komponenst;



]Unit-könyvtár, amely egyetlen jar fájlból áll, és ha egységteszteket szetet­ nénk fordítani és futtatni, a classpath környezeti változónak tartalmaznia kell a fájlt;



szövegszerkesztő vagy integrált fejlesztői környezet (Integrated Development Environment- IDE) a kódoláshoz.

Az első két tétel (a JDK és a ]Unit) ingyenesen letölthető az internetről Oásd a B függeléket). Az utolsó követelmény tekintetében nem szetetnénk vitát kirobbantani, ezért a választást az olvasóra bízzuk. Egészen biztosan van kedvencünk, ragaszkod­ junk hozzá! Ha nincsen olyan program, amellyel kódolhatnánk, kérdezzük meg bará­ tainkat, hallgatótársainkat, előadóinkat vagy kollégáinkat. Egészen biztosan szívesen megosztják velünk véleményüket.

xix

Bevezetés

Mivel a Javáról van szó, a példaprogramokat bármely operációs rendszeren le� fordíthatjuk és futtathatjuk. A könyvet Apple Macintosh és Windows alapú számító� gépeken írtuk és fejlesztettük Egyetlen kód sem különösebben processzorintenzív, tehát a szaftverfejlesztéshez használt hardverünk biztosan megfelel majd.

A könyvben használt jelölések Annak érdekében, hogy a legtöbb új ismeret birtokába juthassunk, és nyomon tud� juk követni, mi történik, a könyvben az alábbi jelöléseket alkalmaztuk:

Gyakorlófeladat A Gyakorlófeladat elnevezésű részben érdemes végigcsinálni a feladatot a könyv utasí� tásait követve. 1.

A Gyakorlófeladat rendszerint több kódolt lépést tartalmaz.

2.

A lépések nem mindig számozottak, néhányuk nagyon rövid, míg mások a nagyobb, végső célhoz vezető, kis lépések sorozatából állnak.

A megvalósitás müködése Minden Gyakorlófeladat után A megvalósítás működése című részben találjuk a kódblokkok működésének részletes magyarázatát. A könyv témája, az algoritmusok kérdése nem igazán felel meg számozott feladatoksarok elvégzésének, sokkal inkább a gyakorlati példáknak, tehát észre fogjuk venni, hogy a Gyakorlófeladat és A megvalósítás műkodése megfelelően módosult. Az alapelv az, hogy alkalmazzuk a megszerzett tudást.

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 ilyen dobozokban

Az aktuális témára vonatkozó tippek, ö"tletek, trükko·k dőlt betűve� kissé be!Jebb húz­ va szerepelnek. A szövegben megjelenő betűtípusokkal kapcsolatban:

XX



A fontos szavakat bevezetésük során kiemeljük.



A billentyűleütések a következő formában jelennek meg: Ctrl+ A.

Forráskód



.

A fájlnevek, az URL-ek és a kódok a következőképpen szerepelnek a szö­ vegben: persistence.properties.



,

A kódokat kétféle változatban láthatjuk:

A példaprogramokban azrú"f-ésfo-ntos kódot szürkeháttérrel emeljük ki. A szürkehá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 ttp: l lwww. w rox. 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 2007januá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 ttp: l lwww. w rox.comldynamiclbooksl download.aspx címre lépünk, és megkeressük a könyv és más Wrox könyvek kódjait.

. 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 ttp: l lwww . w ro x. com címen találj uk, ha a Search doboz vagy az egyik címlista segítségével megkeressük a könyvet. A könyvet részle­ tező oldalon kattintsunk a Book Errata hivatkozásral Az oldalon megtaláljuk a

xxi

Bevezetés

könyvvel kapcsolatban már bejelentett hibákat, amelyeket a Wrox szerkesztői küld­ tek el. A teljes könyvlista, amely az egyes könyvek hibajegyzékeit tartalmazza, a www . wrox.

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. wrox. 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 p2p. 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: ll p2p. w rox. com címen több fórumot is találunk, amelyek nemcsak a könyv olvasását, de saját alkalmazásaink fejlesztését is segítik. Ha szetetnénk csatla­ kozni a fórumokhoz, kövessük az alábbi lépéseket: 1.

Lépjünk a p2p. wrox. com címre, és kattintsunk a Register hivatkozásral

2.

Olvassuk el a felhasználás feltételeit, majd kattintsunk az Agree gombral

3.

Töltsük ki a csatlakozáshoz szükséges és az egyéb információkat, amelyeket szetetnénk megadni, majd kattiusunk a Submit gombral

4.

Ezután e-mailben kapunk értesítést arról, hogyan tudjuk ellenőrizni a fió­ kunkat, és befejezni a csatlakozási folyamatot. _/

A P2P-hez való csatlakozás nélkül is olvashatjuk a fórum üzeneteit, de ha saját üze­ netet szetetnénk küldeni, akkor csatlakoznunk kell. Ha csatlakoztunk, új üzeneteket küldhetünk, illetve válaszolhatunk más felhasz­ nálók üzeneteire. Az üzeneteket bármikor elolvasha�uk az interneten. Ha adott fó­ rum új üzeneteit szetetnénk e-maiben megkapni, a fórumok listájában kattintsunk a fórum neve mellett a Subscribe to this Forum ikonra. A Wrox P2P használatáról további információkat a P2P gyakran ismétlődő kér­ dések listáiban találunk, ahol a fórumszerver működésére, a P2P-re és a Wrox köny­ vekre vonatkozó kérdéseinkre is választ kaphatunk. A gyakran ismétlődő kérdéseket a GYIK-hivatkozásta kattintva bármely P2P oldalon elolvashatjuk.

xxii

ELSŐ FEJEZET

Az alapok Az algoritmusok világába induló utazás előkészületekkel és háttér-információkkal kezdődik. Tudnunk kell néhány dolgot, mielőtt a könyvben található algoritmusok­ kal és adatstruktúrákkal megismerkedhetnénk. Bizonyára ég bennünk a vágy, hogy mielőbb belemerüljünk, de ennek a fejezetnek az elolvasása a könyv többi részét te­ szi érthetőbb� mert olyan fontos alapfogalmakat tartalmaz, amelyek elengedhetetle­ nek a kódok és az algoritmusok elemzésének megértéséhez. A fejezetben a következő témaköröket tárgyaljuk •

mi az algoritmus,



az algoritmusok szerepe a szaftverekben és mindennapi életünkben,



mit jelent az algoritmus bonyolultsága,



az algoritmusbonyolultság széles osztályai, amelyek segítségével gyorsan meg­ különböztethe�ük ugyanannak a problémának a különböző megoldásait,



a "nagy O" j�lölés,



mi az egységtesztelés, és miért fontos,



hogyan kell a ]Unit segítségével egységteszteket írni.

Az

algoritmusok definiciója

Az talán már tudjuk, hogy az algoritmusok a számitástechnika fontos részét képezik, de

egészen pontosan mik is azok? Mire használhatók? Kell egyáltalán törődnünk velük? Az algoritmusok valójában nem korlátozódnak a számitástechnika világára;

mindennapi életünkben is alkalmazunk algoritmusokat. Egyszerű meghatározással az

algoritmus

valarnilyen feladat végrehajtásához szükséges, jól meghatározott lépések

sora. Ha tortát sütünk, és a recept utasításait köve�ük, tulajdonképpen egy algorit­ must használunk. Az algoritmusok segítségével egy rendszert lehetőség szerint közbenső, átmeneti állapotok sorozatán keresztül adott állapotból egy másikba vihetünk. Az életből vett másik példa az egyszerű egész szorzás művelete. Noha általános iskolában mindany­ nyian bemagoltuk a szorzótáblákat, a szorzási folyamatot összeadások sorozatának is

Az alapok

tekinthe�ük. Például az 5 X 2 kifejezés a 2 + 2 + 2 + 2 + 2 (illetve az 5 +

5)

kifeje­

zés gyorsírásos változata. Vagyis bármely két egész szám, például A és B esetén el­ mondhatjuk, hogy az A X B annyit jelent, hogy B-t A-szor önmagához adjuk. Ezt az alábbi lépések soraként fejezhetjük ki: 1.

Inicializáljunk egy harmadik egész számot, C-t O-ra.

2.

Ha A nulla, akkor készen vagyunk, és az eredményt C tartalmazza. Ha nem ez a helyzet, haladjunk tovább a 3. lépéshez.

3.

Adjuk B értékét C-hez.

4.

Csökkentsük A értékét.

5.

Lépjünk a 2. lépéshez.

Vegyük észre, hogy a torta receptjével ellentétben az összeadással szorzó algoritmus az 5. lépésben visszakanyarodik a 2. lépéshez. A legtöbb algoritmusban felfedezhe­ tünk valarnilyen ciklikusságot, amelynek a segítségével számításokat vagy egyéb mű­ veleteket ismétlünk Az iteráció! és a rekuqjót- a ciklusok két fő típusát - a követke­ ző fejezet részletesen ismerteti. Az algoritmusokat gyakran pszeudokódként emlegetjük, amely még nem prog­ ramozó személyek számára is könnyen érthető, kitalált programozási nyelv. Az aláb­ bi kód a Mul ti ply 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 A

= =

C + B A l -

End Return c End

A szorzás az algoritmusok nagyon egyszerű példája. A legtöbb alkalmazásban ennél jóval bonyolultabb algoritmusokkal találkozhatunk. A bonyolult algoritmusok megér­ tése nehezebb, és ezért azok nagyobb valószínűséggel tartalmaznak hibákat. (A számí­ tógép-tudomány nagy része tulajdonképpen azt próbálja igazolni, hogy bizonyos algo­ ritmusok helyesen működnek.) Nem minden helyzetben alkalmazhatunk algoritmusokat. Előfordulhat, hogy adott problémát több algoritmussal is megoldhatunk Néhány megoldás egyszerű, mások bonyolultabbak, és egyik megoldás hatékonyabb lehet a többinéL Nem min-

2

Az algoritmusok definíciója

dig a legegyszerűbb megoldás a legnyilvánvalóbb. Bár a szigorú, tudományos 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

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

A nagy O jelölés

A könyvben az algoritmusok és adatstruktúrák bemutatása során a bonyolultsá­ gukat is elemezni fogjuk. Az elemzések megértéséhez nem lesz szükség matematikai doktorátusra. Az egyszerű elméleti bonyolultságelemzést minden esetben könnyen követhető empirikus eredmények követik tesztesetek formájában, amelyeket rni ma­ gunk is kipróbálhatunk, és kísérletezhetünk a bemenet módosításával, hogy kitapasz­ talhassuk a szóban forgó algoritmus hatékonyságát. A legtöbb esetben az ádagos bonyolultság adott - a kód elvárt tipikus esetbeli futási sebessége. Számos esetben a legrosszabb esetbeli és a legjobb esethez tartozó idő is adott. Az, hogy a legjobb, a legrosszabb és az ádagos esetek közül melyik a meghatározó, részben az algoritmus­ tól függ, de a legtöbbször attól az adattípustól, amelyet az algoritmus használ.. Min­ denesetre fontos megjegyeznünk, hogy a bonyolultság nem az elvárt teljesítmény pontos mértékét biztosítja, hanem az elérhető teljesítményt szorítja bizonyos hatá­ rok vagy korlátok közé.

A nagy O

jelölés

Mint már korábban említettük, a műveletek pontos száma valójában nem fontos. Az algoritmus bonyolultságát a funkció végrehajtásához szükséges műveletek számának

nagJságrencfjével

definiálhatjuk a nagy o jelöléssei

- order of (nagyságrend)

- innen a

nagy O. Az O mögötti kifejezés a probléma méretét jelölő N-hez képest a relatív nö­ vekedést jelenti. Az alábbi lista néhány gyakran alkalmazott nagyságrendet mutat be, a későbbiekben mindegyikre részletesen visszatérünk. •

0(1): az "ordó l" konstans futási idejű függvényt jelent



O(N): az "ordó N" lineáris futási idejű függvényt jelent



" O(N2): az "ordó N négyzet kvadratikus futási idejű függvényt jelent



o(log N): az "ordó logaritmus N" logaritmikus futási idejű függvényt jelent



O(N log N): az "ordó N logaritmus N" a probléma méretével és a logarit­ mikus idővel arányos futási idejű függvényt jelent



" O(N! ): az "ordó N faktoriális faktoriilis futási idejű függvényt jelent

Természetesen a fenti lista elemein kivül is van még néhány hasznos bonyolultság, de ezek elegendőek lesznek a könyvben bemutatott algoritmusok bonyolultságának leírására.

5

Az alapok

Az 1.1. ábrán látjuk, hogy a különböző bonyolultság-nagyságrendek hogyan vi­ szonyulnak egymáshoz. A vízszintes tengely a probléma méretét jelenti - például a keresési algoritmussal feldolgozandó rekordok számát. A függőleges tengely az . egyes osztályok algoritmusainak számitásigényét jelenti. Az ábra nem jelzi a futásidőt vagy a szükséges processzorciklusokat; pusztán annyit mutat, hogy a számitógépes erőforrásigény a megoldani kívánt probléma méretével együtt nő.

1. 1. ábra. A bof!Yolultság küliinbiizó nagyságremijeinek ilsszehasonlítása

Az előző listában talán feltűnt, hogy a nagyságrendek egyike sem tartalmaz kons­ tanst. Azaz, ha az algoritmus várt futásidejű teljesítménye az N, 2xN, 3xN vagy akár lOOxN értékekkel arányban áll, a bonyolultság minden esetben o (N). Első pillantásra kicsit furcsának tűnhet- természetes, hogy a 2xN jobb, mint a lOOxN - , de mint már korábban említettük, nem az a célunk, hogy megállapítsuk a műveletek pontos szá­ mát, hanem hogy a különböző algoritmusok relatív hatékonyságát összehasonlítsuk Más szóval az O(N) idő alatt befejezett algoritmus túlszárnyal egy másik, O(N2) ideig futó algoritmust. Továbbá, ha N nagy értékeivel akad dolgunk, a kanstansok nem sokat változtatnak a helyzeten: a teljes méret arányát tekintve az 1 OOO OOO OOO és a 20 OOO OOO OOO közötti különbség majdnem elhanyagolható, még akkor is, ha az

egyik a másiknak a hússzorosa. Természetesen szeretnénk összehasonlítani a különböző algoritmusok tényleges teljesítményét, különösen akkor, ha az egyik 20 perc alatt befejeződik, mig a másik csak 3 óra alatt, és mindkét algoritmus nagyságrendje O(N). Azt kell megjegyezünk, hogy sokkal könnyebb megfelezill egy O(N) bonyolultságú algoritmus idejét, mint módosítani egy olyan algoritmust, amely az O(N) nagyságrendhez képest O(N2) nagy­ ságrenddel bír.

6

A nagy O jelölés

Konstans idő:

O( 1)

Megbocsátható az a feltételezés, hogy az 0(1) bonyolultság azt jelenti, hogy az 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 konstanst 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

Az alapok

O(N2)

Kvadratikus idő:

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 don összesen 5+4+3+2+1

1.2. ábra. A

=

1.2. ábrán látható

mó­

15 kézfogást jelent.

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

kézfogásba kerülne. És ha nyolcból? Ez 7+6+ ... +2+1

=

=

21

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

A nagy O jelölés

Í

N

N2

N 2-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 alapú logaritmusa 9, mivel a decimális 300 megjelemté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(log N) algoritmusokat mutat be. Ha ismét megtekin�ük az 1.1. ábrát, látha�uk, hogy az O(N log N) jobb, mint az

O(N2), de nem olyan jó, mint az O(N). A 6. és a 7. fejezetben O(N log N) algorit­

musokkal találkozhatunk.

Faktoriális idő: O(N!) Lehet, hogy nem gondolnánk, de néhány algoritmus még az O(N2)-nél is rosszabbul tel­ jesít-

az

1.1. ábrán hasonlítsuk össze az O(N2) és O(N!) bonyolultságokat. (Valójában

vannak ennél sokkal rosszabb algoritmusok is, de ezeket a könyvben nem tárgyaljuk.)

9

·

Az alapok

Elég ritkán találkozhatunk ilyen funkciókkal, különösen, ha olyan példákat kere­ sünk, amelyek nem a kódolásról szólnak. Ha tehát elfelejtettük volna, hogy mi a fak­ toriilis - vagy még nem is találkoztunk vele soha -, íme egy gyors ismédés: A faktoriilis az egész szám és az azt megelőző természetes számok szorzata. Például a 6! ("6 faktoriális'')

=

6x5x4x3x2xl

=

720 és a 10!

=

10x9x8x7x6x5x

4x3x2xl= 3628800.

Az 1.2. táblázat az N2 és az N ! összehasonlítását tartalmazza 1 és 1 O közötti egész számokra.

N

Nz

Nl

1

1

1

2

4

2

3

9

6

4

16

24

5

25

120

6

36

720

7

49

5 040

8

64

40 320

9

81

362 880

10

100

3 628 800

1.2. táblázat. Az N2 és az N! összehasonlítása kis egész számok esetén

Mint lá�uk, ha az N értéke N=2 vagy annál kisebb, a faktoriális bonyolultság jobb, mint a kvadratikus, de ezen a ponton a faktoriilis bonyolultság nekilendül, és katasztrófával fenyeget. Következésképpen az O(N2) bonyolultsághoz képest még inkább remény­ kednünk kell abban, hogy algoritmusunk bonyolultsága ne O(N!) legyen.

Egységtesztelés Mielőtt folytatnánk utazásunkat az algoritmusok világában, kicsit elkalandozva beszél­ nünk kell egy szívünknek igen kedves témáról: az egységtesztelésrőL A2 elmúlt évek­ ben az egységtesztelés nagyon népszerűvé vált azoknak a fejlesztáknek a körében, akiknek fontos az általuk készített rendszerek minősége. Közülük sokan kellemedenill érzik magukat, ha a szoftver készítése közben nem építenek egy automatikus teszteket

10

Egységtesztelés

magában foglaló csomagot is, amely bizonyítja, hogy az elkészült szoftver az elvárá­ soknak megfelelően műköclik. Mi is ezt a hozzáállást képviseljük Ezért minden 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

Az alapok

src l - main - com l - wrox l - algorithms l l - Mütyür l l l test l - com - wrox - algorithms l - MütyürTeszt 1.3. ábra. Az egységtes:;;jforrásjeij/jaipá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: •

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.

12

Egységtesztelés •

Integrációs teszt: nagy, elosztott rendszer külön összetevőjének tesztelését

jelenti. Az ilyen tesztek célja, hogy egymástól független fejlesztésük folyamán biztosítsák a rendszerek előzetes megállapodásoknak megfelelő működését. Az egységtesztelés a tesztdési módszerek közill a legaprólékosabb, mivel az ilyen tesztek során bármely más osztálytól függetlenül egyetlen osztályt tesztelünk Ez annyit jelent, hogy az egységteszteket gyorsan futtathatjuk, és egymástól viszonylag függetlenek.

Miért fontos az egységtesztelés? Ha nem értjük, hogy miért olvashatunk egy alapvetően algoritmusokról szóló könyvben ilyen sokat az egységtesztelésről, gondoljunk csak a Java-fordítóra, amely lehetővé teszi Java programjaink futtatását. Működőképesnek vélhetünk egy megírt kódot anélkül, hogy lefordítanánk? Valószínűleg nem! Gondoljunk úgy a fordítóra, mint a program egyfajta tesztelésére - biztosítja, hogy a program helyes szintaxissal készült. Nagyjából ennyi. Nem ad visszacsatolást arról, hogy a program praktikus vagy hasznos feladatot hajt végre: ez az a pont, ahol az egységtesztek bekerülnek a képbe. Ha jobban érdekel bennünket az, hogy programjaink valóban valami hasznos dolgot hajtanak-e végre, mint az, hogy helyesen írtuk-e be a Java-kódot, akkor az egységtesztek segítségével komoly gátat emelhetünk mindenféle programhiba elé. Az egységtesztek másik előnye, hogy megbízható dokumentációt biztosítanak a tesztelés alatt álló osztály viselkedéséről. Ha majd látunk néhány egységtesztet mű­ ködés közben, észre fogjuk venni, hogy a tesztek vizsgálatával mennyivel könnyebb rájönni, mit csinál az osztály, mint ha magát a kódot nézegetnénk (A kódhoz akkor kell fordulni, ha azt szeretnénk kideríteni, hogy a program hogyan csinálja azt, amit csinál, de ez már teljesen tnás téma.)

J U nit-bevezető Az első hely, ahová el kell látogatnunk, a JUnit-webhelye a

www .

j uni t. or g/ címen.

Itt nemcsak a letölthető szoftvert találjuk meg, hanem a ]Unit integrált fejlesztői környezetünkben való használatával kapcsolatos információkat is, valamint a ]Unit kibővítéseit és fejlesztéseit, amelyek a speciális igények kielégítésére készültek. A szoftver letöltése után csak a junit.jar fájlt kell hozzáadnunk a classpath környezeti változó hoz, és már készen is állunk első egységtesztünk létrehozására. Az egységteszt létrehozásához készítsünk egy Java-osztályt, amely kibővíti a junit. framework. Testcase alaposztályt. Az alábbi kód a ]Unit segítségével készített egy­

ségteszt alapvető felépítését mutatja:

13

Az alapok package com.wrox.algorithms.queues; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List; import junit.framework.Testcase; public class RandomListQueueTest extends Testcase { private static final String VALUE...A

=

private static final String VALULB private static final String VALULC

=

"A"; "B"; "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

Egységtesztelés

protecteéfVO:fcCtearoown o throws Ei= O : "az 'expQflent' nem lehet int result

=

<

O";

l;

21

Iteráció és rekurzió

for (int i = O; i result *= base;

<

exponent; ++i) {

} return result;

} 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. Vegyük észre, hogy mindegyik iteráció összeve­ ti a ciklusváltozó értékét a tömb hosszával. -

22

Tömbök feldolgozása

Vannak esetek, amikor egy tömböt fordítva szetetnénk feldolgozni. Például 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 (int length

- 1),

és egységnyivel csökken�ük

(--i),

i

=

customers.

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

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, 1995] által táwalt iterátorho=?; Művelet

Leirás

previous

A megelőző elemhez pozíciónáL Ha nincs implementálva, akkor unsupportedoperationException hibaüzenetet dob.

isoone

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

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 unsupportedOperationExcepti 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­ OperationException 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 last() metódust meg nem hívtuk. Bizonyos iterátorimplementációk az első elemre vannak pozicionálva, míg léteznek olyanok, amelyek igénylik a first() vagy a last() metódus előzetes meghívását. Mindenesetre, aki ebben bízik, az a

véletlenre

hagyatkozva programo'(, ezt pedig el kell kerülni. Ehelyett, ha iterátorakat használunk, akkor győződjünk meg róla, hogy a fejezet későbbi, "Iterátor idiómák" című részben leírt idiómák egyikét köve�ük.

24

Tömbök feldolgozása

Az lterátor interfész Az elóbb bemutatott műveletekból meg tudjuk alkotill a következő Java interfészt:

package com.wrox.algorithms.iteration; public interface Iterater { public void first(); public void last(); public boolean isoone(); public void next(); public void previous(); public object current() throws IteratoroutOfBoundsException;

} Ahogy láttuk, igencsak szóról szára fordítottuk a műveleteket a Java-felületre, műve­ letenként egy metódust. Meg kell határoznunk a kivételt is, amelyet akkor dobhatunk, amikor aktuális elemet próbálunk meg elérni, de már nincs feldolgozandó elem:

package com.wrox.algorithms:iteration; public class IteratoroutOfBoundsException extends RuntimeException {



1�--------------�--�----

Mivel a határokon kívül eső iterátor elérését programozási hibának tekintjük, körül lehet írni kóddal. Ebből adódóan jó elképzelés az IteratoroutOfBoundsException létrehozása, amely a Run timeException-t terjeszti ki, úgynevezett ellenőrizetlen kivételt alkotva. Ez biztosítja, hogy a kliens kódnak nem kell kezelnie a kivételeket. Valójá­ ban, ha ragaszkodunk a később tárgyalandó idiómához, akkor szinte sohasem fo­ gunk találkozni az IteratoroutOfBoundsException hibával.

Az lterable interfész Az Iterater interEészen kívül létre fogunk hozni egy másik interfészt is, amely álta­ lános módot nyújt iterátorak elérésére, az azt támogató adatstruktúrákból:

package com.�roX7ilgorithms.iteration; public interface Iterable { public Iterater iterator();

l

25

Iteráció és rekurzió

Az Iterab l e interfész egy metódust határoz meg- itera torO -, amely a mögöttes adatstruktúrára vonatkozó iterátort ad meg. Bár ebben a fejezetben nem használtuk, az Iterab le 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: while vagy for 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 firstO vagy a l astO eljárást, amennyiben az iterátort már a megfelelő kezdési ponthoz pozicionáltuk. A for ciklus használata azonban valószínűleg sokkal ismerősebb, mivel megkö­ zelíti azt a módot, amellyel normális esetben egy tömb bejárásánál dolgoznánk: Iterator iterator

=

.

.

.

;

for (iterator.first(); !iterator.isoone(); iterator.next()) { Object object

=

iterator.current();

} Vegyük észre, mennyire hasonlít ez a tömbiterációhoz: az inicializálásból fi rstO hívás lesz; a leállási feltételnek az i sooneOellenőrzése felel meg; a léptetést pedig a next() meghívás valósítja meg.

26

Tömbök feldolgozása

Mindkét icliómát bátran használhatjuk, és mindkettőt nagyjából ugyanolyan 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 ':"wr ox. algorithtii"s�: iteration; 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üteratiormes J>ect"ssounC:IsO -{ object[] array = new Object[] {"A", "B", "c", "o", "E", "F"}; Arrayiterator iterator = new Arrayrterator(array, l, 3); iterator.first(); assertFalse(iterator.isDone()); assertSame(arrf!y[l],. iterator.currentQL;

.----1

27

Iteráció és rekurzió iterator.next(); assertFalse(iterator.isoone()); assertSame(array[2],

iterator.current());

iterator.next(); assertFalse(iterator.isoone()); assertSame(array[3], iterator.current()); iterator.next(); assertTrue(iterator.isoone()); try { iterator.current(); fail 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: 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(); fail();

} catch (IteratoroutOfBoundsException e) { ll ezt várjuk } }

28

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:a1 gorTtllm.s':"iieration; 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� lehet .. negatí v"_;

29

Iteráció és rekurzió

assert start < array.length : "a kezdőpozíció nem letlet > array.length (a tömb hossza)"; assert length >=O : "a length (hossz) nem lehet negatív"; _array = array; _first = start; _last = start + length

-

l;

assert _last < array.lengt:h

"start:

+

length (a kezdőérték és

a hossz) nem lehet: nagyobb, mint az array.lengt:h (a tömb méret:e)";

}

} Bár most a tömb egy részén történt az iterálás, természetesen lesznek olyan esetek, amikor az egészen szeretnénk. Kényelmi okokból érdemes lehet egy konstruktorról is gondoskodni, amelynek egyetlen paramétere a tömb, és amely kiszámítja nekünk a kezdő és a végső pozíciót: public Arrayiterator(Object[] array) { assert array != null : "az array (tömb) nem lehet: NULL"; _array = array; _first =O; _last array.length - l;

Most, hogy megvan a tömbünk, és kiszámítottuk a felső és alsó határokat, már nem is lehetne könnyebb a first() és a last() 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: public void next() { ++_current;

} public void previous() { --_current;

30

Tömbök feldolgozása

Használjuk az isDone() 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:

publicbooleanisoone(){ return _current < _first l l _current> _last;

l Ha a current (aktuális) pozíció a first (első) előtt vagy a last (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:

public ooject curr.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 isDone () 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 last() eljárást a mögöttes iterátorral.

31

Iteráció és rekurzió

Amikor pedig az alkalmazás a next() eljárást lúvja meg, a mogottes iterátor

previous() metódusa lúvódik meg, és így tovább. liy módon az iterátor magatartá­ sát meg lehet fordítani, anélkül hogy megváltoztatnánk a kliens kódját, amely az eredményeket megjeleníti, és anélkül hogy újra rendeznénk a tömböt, ami igazán költséges is tud lenni, ahogy azt a könyv későbbi részeiben is látha�uk, amikor már magunk írunk rendező algoritmusokat.

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:

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

32

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 lic void téStBiCkwardsrterationBecomesForwards() { Reverseiterater iterator

=

new Reverserterator(new Arrayrterator(ARRAY)); iterator.last(); 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 ous (előző) ele­ meit kapjuk, mindegyiknek a megfelelő párját. A második teszt azt biztosítja, hogy egy tömbön hátrafelé végzett iterálás való­ jában a mögöttes iterátor elemeit adja vissza, az elejétól a végéig.

33

Iteráció és rekurzió

Az utolsó teszt szerkezetileg nagyon hasonlít az előzőhöz, de ennél a fir 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:

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

} }

3.4

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 isoone() é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:"á gorithms. i teration; public interface Predicate { public boolean evaluate(Object object);

J� Az interfész nagyon egyszerű, csupán egy eval uate() 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 false üzenetet küld,akkor ez értéket figyelmen kívül fogjuk hagyni,és úgy kezeljük,mintha nem is létezett volna. Bár egyszerű,a predikátuminterfész lehetővé teszi igen bonyolult szűrők létre­ hozását. Predikátummal megvalósíthatunk

ÉS(&&), VAGY (ll), NEM(!)

és hason­

ló operátorokat,lehetővé téve ezzel bármilyen összetett predikátum megalkotását.

35

Iteráció és rekurzió

Gyakorlófeladat: a predikátumosztály tesztelése Most néhány tesztet írunk, hogy meggyőződjünk róla, helyesen működik-e szűrőite­ rátorunk. Ellenőriznünk kell, hogy a szűrő visszaadja-e a mögöttes iterátorból a pre­ dikátum által elfogadott értékeket. Négy tesztet hajtunk végre:

az

előre és hátra iterá­

lás két kombinációját: egyet, amelyben a predikátum elfogadja az értékeket, és egyet, amelyben elveti őket. package com.wrox.algorithms.iteration; import junit.framework.Testcase; public class FilteriteratorTest extends Testcase { private static final object[] ARRAY = {"A", "B", "c"};

} Azt akarjuk tudni, hogy a predikátumot egyszer hívják meg minden egyes elernhez, amely a mögöttes iterátorból visszatért. Ezért létrehozunk egy predikátumot kifeje­ zetten tesztdési céllal. private static final class oummyPredicate implements Predicate { private final Iterator _iterator; private final boolean _result; public oummyPredicate(boolean result, Iterator iterator) { _iterator = iterator; _result = result; _iterator.first(); } public boolean evaluate(object object) { assertsame(_iterator.current(), object); _iterator.next(); return _result; } } Az első teszttel azt ellenőrizzük, hogy a szűrő olyan értékeket ad vissza, amelyeket a

predikátum elfogad- az evaluate() true értéket ad vissza-, miközben előrehaladunk. public void testForwardsiterationincludesitemswhenPredicateReturnsTrue() { Iterator expectediterator = new Arrayiterator(ARRAY); Iterator underlyingiterator = new Arrayiterator(ARRAY); Iterator iterator = new Filteriterator(underlyingiterator, new oummypredicate(true, exQectediterator));

36

Tömbök feldolgozása ·; terator. fir 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

}--�----�-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 aluate() false értéket ad vissza: public void testForwardsiterationExcludesitemswhenPredicateReturnsFalse()

{

Iterator expectediterator = new Arrayiterator(ARRAY)j Iterator underlyingiterator = new Arrayiterator(ARRAY)j Iterator iterator = new Filteriterator(underlyingiterator, new oummyPredicate(false, expectediterator))j iterator.first()j assertTrue(iterator.isoone())j try

{

iterator.current(); fail()j

}

catch (IteratoroutOfBoundsException e)

ll

{

ezt várjuk

} assertTrue(expectediterator.isoone())j assertTrue(underlyingiterator.isoone());

}

37

Iteráció és rekurzió A másik két teszt nagyjából megegyezik az első kettővel, kivéve, hogy az iteráció

sorrendje megfordult: 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)�;�------ ------�

38

Tömbök feldolgozása 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 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 cate 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 aluate()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 false visszaadott értékével hozzuk létre. Miután a szűrő­ iterátort az első elemhez pozicionáltuk, azt várjuk, hogy egyszerűen befejeződjön, ugyanis a predikátum minden értéket elvet. Megint arra számítunk, hogy mindkét iterátor végezzen; és a mögöttes iterátortól külön még azt is várjuk, hogy minden ér­ tékét leellenőrizzen. Az utolsó tesztben figyeljünk a Reverseiterater használatára; az üres iterátor

még mindig azt hiszi, hogy előreiterál, pedig már hátrafelé lépked.

39

Iteráció és rekurzió

Gyakorlófeladat: a predikátumosztály megvalósítása A helyes teszteket közvetlenül megvalósíthatjuk Az interfészt már meghatároztuk a predikátumokhoz, így már csak a szűrőiterátor osztályt kell létrehoznunk package com.wrox.algorithms.iteration; public class Filterrterator implements Iterator { private final Iterator _iterator; private final Predicate _predicate; public Filteriterator(Iterator iterator, assert iterator != null :

Predicate predicate) {

"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: public void first() { _iterator.first(); filterForwards();

} public void next() { _iterator.next(); filterForwards();

} private void filterForwards() { while (!_iterator.isoone() && !_predicate.evaluate(_iterator.current())) { _iterator.next();

} }

40

Tömbök feldolgozása

Végül hozzáadjuk a last() é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() { while (!_iterator.isDone() && !_predicate.evaluate(_iterator.current())) { _iterator.previous();

l

}

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

-

isoone() és

current() -csak a mögöttes iterátor megfelelő metódusait kell delegálnia. Ez mű­

ködik, rnivel a mögöttes iterátor mindig olyan állapotban van, hogy csak a prediká­ tum által engedélyezett objektum lehet az aktuális objektum. Az iterátor igazi működése akkor megy végbe, rnikor az egyik bejárásmetódus meghívódik Bármikor, ha a first(), a next(), a l ast() vagy a previous() meghí­ vódik, a predikátumot kell használni a megfelelő értékek beemelésére vagy kizárásá­ ra, rniközben az iterátor szemantikáját megőrizzük: public void first() { _iterator.first(); filterForwards();

} public void next() { _iterator.next(); filterForwards();

}

41

Iteráció és rekurzió private void filterForwards() { whil e (!_iterator.isoone() && !_predicate.eval uate(_iterator.current())) { _iterator.next();

} }

Amikor a

filterForwards

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ást 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

first()

és a next(), a

last O

és a

previous O

talmazott osztály metódusait hívjuk, rnielőtt meghívnánk a

esetében, most is a tar­

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

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 isfák

43

Iteráció és rekurzió

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.

Az

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 main metódusra: package com.wrox. algorithms.iteration; 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 "); system.exit(4);

} print(new File(args[O]), "");

} }

44

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 main() függvény létrehoz egy j ava.i o. File objektumot a paraméter alapján, és átadja a print() metódusnak. Vegyü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ást.

A print() metódus egyetlen File 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 File objektumokat használ az egyedi fájlok és a könyvtárak esetén is), meghívunk egy eltérő print() 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 stFil es O metódus tömbbel tér vissza; és mivel még nincs olyan print O metódusunk amely ilyet elfogad, hozzunk létre egyet: · public static void print(File[] files, String indent) { assert files l= null : "a fájllista nem lehet NULL"; for (int i =O; i < files.length; ++i) { print(files[i], indent);

} .}.

45

Iteráció és rekurzió

Ez a metódus a tömbön iterál végig, az eredeti pri nt() metódust minden egyes fájl­ ra meglúvja. Látszik, hogy rekurzív? Emlékezzünk vissza, hogy az első prí 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 int() 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. seginníng Algorithms build classes com wrox algorithms iteration Arrayiterator.class ArrayiteratorTest.class Iterator.class

IteratoroutofsoundsException.class RecursíveDirectoryTreePrinter.class

Reverserterator.class ReverseiteratorTest.class

Singletoniterator.class

Si ngl e t on i te r ato rTe s t. class src build.xml conf build.properties checkstyle-header.txt checkstyle-main.xml checkstyle-test.xml checkstyle.xsl simian.xsl

l ib antl r-2. 7. 2. jar 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-------� ------�

46

Rekurzió main com wrox

algorithms iteration Arrayiterator.java

'-----

Iterator.java IteratoroutofaoundsException.java RecursiveoirectoryTreePrinter.java Reverseiterator.java ----=si.nglet.oniterator.java ---��

--

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éhajóval bOf!JOlultabb ésfáradságosabb, 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.

(veremtúlcsorduláschiba) reku1ifv algoritmus futtatása kiizben gyakranjelzi!Je annak, hogy a program hiáf!Jzó vagy nem elégséges leállásijelté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'{! isjelentheti, hogy a megoldandó feladat túl nagy a rendelke­ zésre álló erőforrásokhoz képest.

A Stackove rflowException

47

Iteráció és rekurzió

Az általános eset A legtöbbször fennálló általános eset az, ahol a rekurzív hívás történik. A példában az első rekurzív hívás akkor történik, arnikor egy könyvtárat reprezentáló fájllal 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ő pri 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. 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. •

48

Sok probléma megoldható az iteráció vagy a rekurzió használatával.

Gyakorlatok

Gyakorlatok Ezeknek a gyakorlatoknak (és az összes többi fejezethez tartozóknak is) a megoldá­ saira a D függelékben találhatunk példát. 1.

Hozzunk létre iterátort, amely csak minden n-edik elem értékével tér vissza, ahol n nullánál nagyobb pozitív egész.

2.

Hozzunk létre predikátumot, amely elvégzi a logikai

ÉS (&&)

műveletet két

másik predikátumon. 3.

Írjuk újra a Powerca l cuator-t iteráció helyett rekurziót használva.

4.

Cseréljük le a tömbök használatát iterátorokra a rekurzív könyvtárfanyom­ tató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.

49

HARMADIK FEJEZET

Listák Most, hogy már megismerkedtünk az iterációval és az algoritmusok néhány alapele­ mével, ideje rátérni az első összetett adatstruktúrára. A listák a legalapvetőbb adat­ struktúrák, amelyekre sok más adatstruktúra épül, és amelyeket még több algoritmus használ fel. A valós életben nem nehéz példát találni a listákra: bevásárlólisták, teendők listá­ ja, menetrendek, megrendelőlapok vagy éppen ez a "listák listája". A tömbökhöz ha­ sonlóan a listák is jól hasznosíthaták lesznek a legtöbb alkalmazásban, amelyet írunk. A listákkal tulajdonképpen kiválóan helyettesíthetők a tömbök; általában lehetséges

(és a legtöbb esetben kívánatos is), hogy a leginkább memóriaérzékeny/időkritikus alkalmazások kivételével mindenütt teljesen kiváltsuk a listákkal a tömböket. Ebben a fejezetben először áttekin�ük az alapvető listamúveleteket. Ezt követi egy teszt, majd megvizsgáljuk a listák két megvalósítását: a tömblistát és a láncolt lis­ tát. Mindkét megvalósítás ugyanarra az interEészre illeszkedik, ugyanakkor elég eltérő tulajdonságokkal rendelkeznek. Ezek a különbségek határozzák meg, hogy mikor és hogyan használhatjuk őket alkalmazásainkban. A fejezet végére a következőket is­ merjük meg:



mi a lista,



hogyan néz ki a lista,



hogyan használjuk a listákat,



hogyan valósíthatjuk meg a listákat.

A listákról A lista az elemek rendezett gyűjteménye, amely a tömbökhöz hasonlóan támogatja

az egyes elemekhez való véleden hozzáférést; a listából lekérdezhetjük egy tetszőle­ ges elem értékét. A lista megőrzi a beszúrási sorrendet is, így egy adott lista - feltéve, hogy semmilyen beavatkozással nem módosítjuk - ugyanarról a helyről mindig ugyanazt az értéket fogja visszaadni. A tömbökhöz hasonlóan a listák sem töreksze­ nek arra, hogy a tárolt értékek egyediek legyenek, vagyis a listák tartalmazhatnak két­ szerezett értékeket. Ha például egy listában az "úszás", "kerékpározás" és "tánc "

Listák

szerepel, majd ismét az "úszás" elemet szetetnénk felvenni, akkor azt tapasztaljuk, hogy a lista mérete megnő, és kétszer fogja tartalmazill az "úszás" elemet. A legfőbb különbség a tömbök és listák között abban rejlik, hogy míg a tömbök fix méretűek, a listák átméretezhetők: igény szerint csökkentheták vagy növelhetők. A listák minimálisan a

3.1.

táblázatban bemutatott négy alapszintű műveletet

támogatják.

jMűvelet

Letrás

i nse rt

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 rintl n("telefonos üg_y,;télszolgálaLmegnyitása");

107

Várakozási sorok for (int i

=

O; i

Thread thread

<

_numberofAgents;

++i) {

=

new Thread(new customerserviceAgent(i, _calls)); thread.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: public void close() { assert !_threads.isEmpty() : "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.join();

}

catch (InterruptedException e) {

ll } .}.

108

Ignore ("nem kell figyelembe venni")

Példa: telefonos ügyfélszolgálat szimulátora A megvalósitás müködése A callcenter metódus először létrehozza a sort - egészen pontosan a Blacking­ Queue egy példányát. Ezáltal probléma nélkül működtethetünk több ügyfélszolgálati

munkatársat is, mindegyiket saját külön szálon, amelyek ugyanahhoz a sorhoz férnek hozzá. Vegyük észre, hogy mivel több szálat indítunk, leállitanunk is több szálat kell. Ebből kifolyólag a pillanatnyilag futó szálakról listát kell vezetnünk Végül pedig tá­ rolnunk kell az elindítani kívánt munkatársak számát. Az open() metódus feladata az osztály építése során meghatározott számú munkatárs elindítása. Minden egyes customerserviceAgent - 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 : assert numberofcalls >O

:

assert maxcallouration >O

assert maxcallinterval >O

"

"callcenter nem lehet NULL ;

{

"numberofcalls nem lehet < l"; : :

"maxcallouration nem lehet

{

0) {

if (_comparator.compare(it.current(), result.get(slot break;

-

l))

>=

0) {

}

--slot;

}

result.insert(slot,

it.current());

} return result;

} Végül vegyük észre, hogy a belső ciklus while 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 áncol t l ist át

használ az eredménylista tárolására, mert ez sokkal jobb a beszúrási művelethez.

159

Alapvető rendezés

Vegyük még észre azt is, hogy az algoritmus visszafelé keresi a listában a megfe­ lelő pozíciót, nem előrehalad va. Ez nagyon nagy előny a rendezett vagy közel rende­ zett objektumok esetén, mint a fejezet későbbi szakaszában, az Alapvető rendezési algoritmusok összehasonlítása címú részben látni fogjuk. Ez az oka annak is, hogy az algoritmus stabil; erről szól a következő szakasz.

A stabilitásról Néhány rendezési algoritmus osztozik a stabilitás néven ismert érdekes tulajdonság­ ban. Az alapelv illusztrálásához nézzük meg a 6.1. táblázatban látható, keresztnév alapján rt::ndezett személyneveket. Keresztnév

Vezetéknév

Albert

Smith

Brian

Jackson

Davi d

Barn es

John

Smith

John

Wilson

Mary

smith

Tom

Barnes

Vince

De M arco

walter

clarke

6. 1. táblázat. Keresztnév alapján rendezett lista

Most tegyük fel, hogy ugyanezeket az embereket a vezetéknevük alapján szeretnénk rendezni. A 6.1. tábla listájában van néhány igen gyakori vezetéknév, például Smith és Barnes. Mit várunk, rni fog történni az azonos vezetéknevú emberek sorrendjé­ vel? Arra szárrúthatunk, hogy az azonos vezetéknevú személyek egymáshoz viszo­ nyítva ugyanolyan sorrendben maradnak, ahogyan az eredeti listában szerepeitek vagyis keresztév alapján rendezve az azonos vezetéknevűek csoportjában. Ez a sta­ bilitás. Ha egy rendezőalgoritmus megőrzi a közös rendezési kulccsal rendelkező elemek relatív sorrendjét, akkor azt stabil algoritmumak nevezzük. A 6.2. táblázat mutatja a példában szereplő személyek vezetéknév szerinti stabil rendezését.

160

Az alapvető rendezési algoritmusok összehasonlítása

Keresztnév

Vezetéknév

David

Barnes

Tom

Barnes

walter

clarke

Vince

De Marco

Bri 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 Listsorter inter­ fészt, felmerülhet a kérdés, hogy mikor melyiket használjuk. Ez a szakasz nem elméleti vagy matematikai megközelítésből, hanem gyakorlati szempontokból hasonlí�a össze az egyes algoritmusokat. Nem célja, hogy határozott feltétellistát adjon az algoritmus kiválasztására, inkább azt muta�a be, hogyan használha�uk fel az összehasonlító elem­ zést akkor, amikor rendszerünk építése közben megvalósítási döntéseket kell hoznunk. Emlékezzünk vissza a fejezet bevezetőjére, ahol említettük, hogy a rendezőalgo­ ritmusok két alapvető lépést ismételgetnek összehasonlí�ák és átmozga�ák az ele­ meket. Ez a leírás felméri a három rendezőalgoritmus viselkedését az első műveletre nézve, és próbára teszi az algoritmusok sebességét azáltal, hogy sokkal nagyobb adathalmazt használ, rnint rni a megvalósításukkor. Ez azért fontos, mert relatív tel­ jesítményük minden eltérése sokkal élesebben kirajzolódik, ha nagyobb adathalmazt használunk. Az is fontos, hogy minden algoritmus különböző elrendezésekben kapja meg a bemeneti adatokat, a következők szerint:

161

Alapvető rendezés



már rendezve (a legjobb eset),



már rendezve, de éppen a kívánt sorrenddel ellentétes sorrendben (legrosz­ szabb eset), véletlenszerű sorrendben (tipikus eset).



Ha minden tesztesetben ugyanazt a bemeneti adathalmazt adjuk minden algorit­ musnak, akkor megalapozott döntést hozhatunk a relatív erősségeikről, életszerű szi­ tuációban. Első feladatunk összegyűjteni, hogy hány összehasonlítást hajtanak végre.

CallCountinglistComparator A rendezőalgoritmusokban minden összehasonlítást a megfelelő összehasonlíróik

hajtanak végre. Ha össze akarjuk számolni, hogy az összehasonlító compare() me­ tódusa hányszor lesz meghívva, kissé módosítanunk kell az összehasonlíták kódját, hogy emlékezzenek a hívások számára. Vagy megírhatjuk az összehasonlítókat egy közös alaposztály kiterjesztéseként, és abban helyezhetjük el az összeszámlálást. Vi­ szont hogy a már megírt kódunk nagy részét újrahasznosíthassuk, a hívásszámláló funkciót bármely már meglévő összehasonlítóba beépíthetjük, rnint ahogyan a Reversecomparator esetében tettük: 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 _callcount

=

=

comparator;

O;

} public int compare(Object left, object right) { ++_callcount; return _comparator.compare(left, right);

} public int géteallcount O { return _callcount;

} }

162

Az alapvető rendezési algoritmusok összehasonlítása

Éppúgy, ahogyan a Reversecomparator, a callcountingcomparator is elfogadja a konstruktorában bármely másik comparator metódust. A callcountingcomparator 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 call 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 callcounti ngcomparator hi­ vatkozását tárolja: .. package com.wrox:- algorittiiiis.sorti 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

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­ sorter 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 repo r teall s ( ) metódust használja, amelyet a fejezet későbbi részében mutatunk be. Most három hasonló metódus kö­ vetkezik a legjobb eset forgatókönyvére, amelyben minden algoritmusnak a setUp() által létrehozott rendezett listát kell rendeznie:

164

Az alapvető rendezési algoritmusok összehasonlítása

public void test8estcasefii:d)l:Jlesort0�{ �w aubblesortListSOrter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount()); }

public void testsestcaseselectionsort() { new selectionsortListsorter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount()); }

public void testBestcaseinsertionsort() { new InsertionsortListsorter(_comparator).sort(_sortedArrayList); reportcalls(_comparator.getcallcount()); } Még három metódust hozunk létre a véletlenszerűen generált számlistát használó ti­ pikus eset tesztelésére:

pubfi C voi a testAverageCaseBubolesort()-{ new BubblesortListsorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount()); }

public void testAveragecaseselectionsort() { new SelectionsortListsorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount()); }

public void testAveragecaseinsertionsort() { new InsertionsortListSorter(_comparator).sort(_randomArrayList); reportcalls(_comparator.getcallcount()); } Végül definiáljuk a reportcalls() metódust, amely a korábban definiált forgató­ könyvekre hozza létre a kimenetet:

private voidreportcalls CTnt� callcounü { system.out.println(getName() + ": " + calleount

+

" hívás");

l Ez az egyszerű kód egyetlen érdekes pontot tartalmaz. A getName() metódust al­ kalmazza, amelyet a ]Unit Testcase szuperosztálya biztosít a forgatókönyv nevének kiíratására. A program által a legrosszabb esetre létrehozott kimenet itt látható:

testworstcaseaubblesort: 499500-hívás testworstcaseselectionsort: 499500 hívás testworstcaseinserti.onsort: 499500 hív:ás

165

Alapvető rendezés

Mint látható, mindhárom algoritmus pontosan ugyanannyi összehasonlítást hajtott végre a fordítottan rendezett ista rendezésekori Ebből azonban még ne gondoljuk, hogy ugyanannyi időbe telik a futásuk is; itt most nem mérjük a sebességet. Mindig vigyázzunk, nehogy túl messzemenő következtetéseket vonjunk le az ilyen egyszerű statisztikákból! Mindent egybevetve nagyon érdekes így együtt látni a három algo­ ritmus eredményét erre a forgatókönyvre. A következő számok a legjobb esetre vonatkoznak:

49850i hívás 498501 hívás testBestcaseinsertionsort: 998 hívás

testBestcaseBubbiesort:

testBestCaseselectionsort:

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:

498501 nívás 498501 hívás testAveragecaseinsertionsort: 262095 hívás testAverageCaseBubblesort:

testAveragecaseselectionsort:

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, újabb 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 inkedList é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. •

A buborékrendezés és a kiválasztásos rendezés mindig pontosan ugyanany­ nyi összehasonlítást végez.

166

Ö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

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

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



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

összetett összehasonlítóról és a stabilitásró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.

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áshoz 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ükfigyelembe, 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 B

l l l l l l I I lAl l I I I lAl l l l l l l l l l I I I E

G

G

N

N

H

G

G

E

N

H

L

M

L

G

N

R

G

N

N

7.4. ábra. Minden negyedik elem rendezése, az 1. pozíciótól kezdve

170

T T

l l I l l I o

M

o

R

s s

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 HlM l l GlNlNll l l lRlS l sGEJ GE lNlHlM l l GlNlNlLlrloiRisl L

T

O

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 lAlG l GlGlEl ll lHlMl lNlN-� Nl L l l lRl S B

l

l

T

O

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 3rendezzü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�INININILlrloiRis siAIGIGIGIEI l IHIMI INININILisloiRir 1

1

1

7.7. ábra. 3-rendezés O. kiinduló po'{jcióval

171

Fejlettebb rendezés

1. pozícióba mozgatunk, és véletlenül nincs mit tenni, ahogyan az a 7.8. ábrán látható.

H lM l l l l l H lM l l l l l

IBIAIGIGIGIE IBIAIGIGIGIE 7.8. ábra.

3-rendezés

1.

N

N

N

N

N

N

L

s

L

s

l l RI l l RI o

o

T T

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 IAIGIGIGIE B IAIEIGIGIGI 7. 9. ábra.

l IRI IHIMI l l IHIMI l l l l l l IRI N

N

N

L

s

L

N

N

N

s

o

o

T T

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 lB lE lG lG l G l H l l l l lM l l l l l R l l l

7.10. ábra. A

l

l

L

N

N

N

0

S

T

végső, rendezett lista

A Shell-rendezéssei kapcsolatos legtöbb kutatás a H soron következő értékeivel fog­ lalkozott. Az algoritmus feltalálója által javasolt eredeti számsor (1, 2, 4, 8, 16, 32 ...) valószínűleg borzalmas, mivel csak páratlan pozícióban található elemeket hasonlit össze páratlan pozícióban álló elemekkel az utolsó lépésig. A Shell-rendezés akkor működik jól, ha minden elemet különböző elemekkel összehasonlitva rendezünk minden lépésben. A (1, 4, 13, 40 ...) egyszerű és hatékony sorozat, ahol minden H 3xH+l. A következő feladatban a Shell-rendezést fogjuk megvalósítani.

Gyakorlófeladat: a Shell-rendezés tesztelése A következő részben a Shell rendezési algoritmust fogjuk megvalósítani, ugyanan­ nak a tesztsornak a használatával, amelyet az előző fejezet rendezési algoritmusában is láthattunk. A Shell-rendezési algoritmus tesz* mostanra már biztosan ismerős látvány. Csak kiterjeszti az Abstract l i stsorterTest használatát, és példányosítja a (még nem megírt) Shell-rendezés megvalósítását.

172

A

Shell-rendezési algoritmus alapjai

package com.wrox.algodthms.sórting; public class shellsortListsorterTest extends AbstractListsorterTest { protected ListSorter createListsorter(Comparator comparator) { return new shellsortListsorter(comparator);

}

}

A megvalósitás müködése Az előző teszt kiterjesztette a 6. fejezetben elkészített általános célú algoritmusren­ dezési tesztet. A Shell-rendezés megvalósításának tesztjéhez ezt csupán példányosí­ tani kell a createLi stsorter() metódus figyelmen kívül hagyásával. A következő gyakorlófeladatban egy Shell-rendezést valósítunk meg.

Gyakorlófeladat: a Shell-rendezés megvaló _ sitása A megvalósítás hasonló felépítésű, mint az alapvető rendezési algoritmusok eseté­ ben. A Listsorter 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; private final int[] _increments

=

{121, 40, 13, 4,

l};

public shellsortListsorter(Comparator comparator) { assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator

=

comparator;

} l A következőként elkészítendő sort() metódus csupán ciklusonként végighalad az előző tömbben definiált növekményeken, és valamennyi növekményhez meghívja a hSort() metódust a listához. Figyeljük meg: a siker azon múlik, hogy a végső nö­

vekmény

l legyen. Bátran kísérletezzünk más sorrendekkel, de tartsuk észben, hogy

a végső érték

l legyen, kül�nben a lista végül csak "majdnem" rendezett lesz!

173

Fejlettebb rendezés public List sort(List list) { assert list l= null : "a 'list' nem lehet NULL"; for (int i= O; i < _increments.length; i++) { int increment= _increments[i]; hsort(list, increment);

}

return list;

} Most hozzunk létre egy hsort() megvalósítást, gondosan figyelmen kívül hagyva a rendezni kívánt adathoz képest túl nagy növekményeket. Nincs sok értelme 50rendezni 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 sortsubLi 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 +increment és a -incre­ 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. private void sortsublist(List list,

int startindex, int increment) {

for (int

= startindex + increment;

i < list.size(); i += increment) {

Object value = list.get(i); int j;

for (j = i; j>

startrndex;

Object previousvalue

=

j-= increment) {

list.get(j - increment);

if (_comparator.compare(value, break;

}

list.set(j,

}

previousvalue);

list.set(j, value);

} }

174

previousvalue) >= O) {

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 7. 11. ábra.

S � EIAITIFIUIN

Példa a gyorsrendezéshez

175

Fejlettebb rendezés A gyorsrendezés első lépése az elválasztó elem meghatározása. Ez az az elem, amely

a végső, rendezett pozícióba kerül ez után a lépés után, és a listát két részre osztja, a kisebb elemekkel a bal oldalon (véletlenszerű elrendezéssel), és a nagyobb elemekkel a jobb oldalon (szintén véletlenszerű elrendezéssel). Sokféleképpen ki lehet vá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

sőbb következik, mint a kettéosztó érték

Q betűre mutat. Ez az ábécében 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.

176

Az első két rossz helJen lévő elemetJelcseréltük

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�

s0R EIAITialuiNI t

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

lAl 1lciKisloiRITI lsiGIRIEiuiTialuiNI t t 1

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.

A

F

c KlsloiR T t

s0R 0 t

U

T l l lN l Q

U

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 KlEloiRIT t

siGIRisluiTialuiNI t

7. 18. ábra. Az E és az S betű he!Jet cserél

A munka ugyanígy folytatódik. A következő rossz helyen lévő elemet a 7.19. ábrán láthatjuk. F

l

A

CIKIEloiRIT t

siGIRisluiTialuiNI t

7. 19. ábra. Az O és a G betű van rossz he!Jen

1n

Fejlettebb rendezés Megcseréljük őket a 7 .20. ábrán látható pozícióba. F

l l A

c

K

E

l GIRl t

T

s

7.20. ábra. Az O és a G betű helJet cserélt

l ol RI t

s

u

T

u

l l l l Q

N

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

l l t

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 ajobb index a kettéos'{jópozícióban találkoifk

A két index találkozási pontja a kettéosztó pozíció - azaz

az

a hely a listában, ahová

a kettéosztó érték valójában tartozik. Ezért végrehajtunk egy végső cserét e kőzött a pozíció között és a kettéosztó érték között a lista jobb végében, hogy a kettéosztó értéket a végső rendezett helyére tegyük. Arnikor ez kész, az N betű található a ket­ téosztó pozícióban, ahol a tőle balra található értékek mind kisebbek, és a jobbra ta­

lálhatóak mind nagyobbak (lásd 7 .24. ábra).

178

A gyorsrendezésről "N" előttiek

"N" utániak

l

A

Fl

l

l lK l l l INIRisloiRislulrlolulr t C

E

G

7.24. ábra. A kettéos'{fó elem a végső, rendezettpo: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 lgorithms -:sort"in g; public class QuicksortListsorterTest extends AbstractListsorterTest

{

protected Listsorter createListsorter(comparator com parator) return new QuicksortListSorter(com parator);

}

{

}

A megvalósitás működése A előző teszt kiterjesztette a

6. fejezetben elkészített általános célú algoritmusrende­

zési tesztet. A gyorsrendezés megvalósításának tesztjéhez csupán példányosítani kell a createLi stSorter() metódus figyelmen kívül hagyásával. A következő gyakorlófeladatban egy gyorsrendezést valósítunk meg.

179

Fejlettebb rendezés

Gyakorlófeladat: a gyorsrendezés megvalósítása Először is létrehozzuk a Qui cksortL istSorter metódust, amely már ismerős lesz, mivel az alapvető struktúrája nagyon hasonlit az eddig látott többi rendező algorit­ muséra. A Listsorter 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 _comparator

=

!= null : "a 'comparator' nem lehet NULL";

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 quicksort() metó­ dust a bal és a jobb részlistához is: private void quicksort(List list, int startindex, if estartindex

<

int endindex) {

o l l endindex >= list.size()) {

return;

}

if (endindex O) { result =i;

}

}

return result;

} Végezetül már csak a Queue interfész metódusainak megvalósítása van hátra. Ezek minden egyes listaalapú sor megvalósításában ugyanúgy néznek ki:

207

Prioritásos sarok

public void clear() { _list.clear();

} public int size() { return _list.size();

} public boolean isEmpty() { return _list.isEmpty();

} Ha ezt a kódot lefutta�uk, látni fogjuk, hogy a mégvalósítás pontosan az elvárt mó­ don műköclik. Feladatunk végeztével térjünk rá a prioritásos sorok egy olyan 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 addO 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 dequeueO 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 AbstractPriorityQueueTestcase tesztesetet a konkrét megva­ lósítás alapján:

208

A prioritásos sorok áttekintése

pub1Tcclasssortei:l[i"stPriorityQUeueTest extends AbstractPriorityQueueTestCase{ protected QUeue createQueue(Comparator comparator){ return newsortedListPriorityQueue(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 asssorteaCistPrtörhyQueue iiiiPl ements ·Ciüeue { private final List _list; private final Comparator _comparator; publicsortedListPriorityQueue(Comparator comparator){ assert comparator != null : "a 'comparator' nem lehet NULL"; _comparator = comparator; _list= new LinkedList();

} } Az enqueue() metódusban lépkedjünk visszafelé a listában egészen addig, amíg meg nem találjuk a beszúrandó elemnek megfelelő helyet:

públic void enqueue(oli)ect value)- { int pos = _list.size(); while (pos> O && _comparator.compare(_list.get(pos- 1), value) > O) { --pos;

}

_l{st.insert(pos, value);

} A dequeue() metódusnak pedig annyi a feladata, hogy eltávolítsa az utolsó elemet a listábóL Ne felejtsünk el kivételt dobni abban az esetben, ha a lista üres, és nincs mit eltávolítani belőle:

putilic·Öb-ject dequeue() -t : hrows EJRptyQUeueException{ if (isE�pty()) { throw new EJRptyQueueException();

}

return _list.delete(_list.size() - l);

} Ezek után már csak néhány metódus megvalósítása van hátra, amelyek semmiben sem térnek el a korábban már megszakott sor megvalósításokban látottaktól.

209

Prioritásos sarok

public void clear() { _list.clear();

} public int size() { return _list.size();

} public boolean isEmpty() { return _list.isEmpty();

} Ezzel végeztünk a prioritásos sorok rendezett listás megvalósításávaL A tesztesetet lefuttatva látható, hogy a prioritásos sor viselkedése megfelel a korábban kitűzött kritériumoknak. A következő részben a prioritásos sorok legbonyolultabb, de egy­ ben leghatékonyabb és legpraktikusabb megvalósítását vizsgáljuk, amely egy halom­ nak nevezett adatstruktúra használatán alapul.

A megvalósitás müködése Az enqueue() metódus megvalósítása ebben az esetben valamivel bonyolultabb, mint az előző módszereknél volt. Feladata most is egy listában megtalálni a beszú­ randó elemnek megfelelő helyet. A célját úgy éri el, hogy hátulról kezdve végighalad a tárolt elemeken, egészen addig, amíg egy kisebb elemet nem talál, vagy amíg a lista elejére nem ér. Mihelyt megtalálja az optimális pozíciót, beszúrja a szóban forgó elemet. Ezzel a módszerrel a metódus biztosítja, hogy a legnagyobb tárolt elem minden pillanatban a lista végén helyezkedik el. A módszerben rejlő pluszköltséget azért érdemes vállalni, mert ekkor a dequeue() metódusnak már nincs más dolga, mint a lista legutolsó elemét kiemelni a sorból és visszaadni a hívó programnak.

Halmon alapuló prioritásos sorok működése A halom nagyon hasznos és érdekes adatstruktúra, ezért elengedheteden hogy ebben a részben megértsük, hogyan működik. A halom működési elvének megértése után az olvasó képes lesz prioricisos sorokat hatékony módon megvalósítani. A halom egy bináris fastruktúra, amelynek minden csomópon�a nagyobb, mint saját gyermekei. Ezt a kritériumot halomfeltételnek szokás nevezni. Vegyük észre a 8.7. ábrán, hogy minden egyes csomópont értéke nagyobb, mint gyermekeié (ha

egyáltalán van gyermeke).

210

A prioritásos sarok áttekintése

8. 7. ábra. A

halom

Óvakodjunk attól, hogy a halmot rendezettnek tekintsük, ugyanis egyáltalán nem az. Van azonban egy hasznos tulajdonsága, amely a feladat szempontjából nagyon is fontos: a halom definíciójából adódóan a legnagyobb tárolt elem éppen a gyöké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

Prioritásos sorok



Az

X indexű csomópont bal oldali gyermekének indexe



Az

X indexű csomópont jobb oldali gyermekének indexe



Az

X indexű

csomópont szülőjének indexe ((X -

(2

1) /

X X+ 1).

(2

x X+

2).

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 Eztjelsz!várgásnak nevezzük.

212

A prioritásos sarok áttekintése

8.1o. ábra. A

halomba 4/ elemet szúnmk be, ame!J filborítja a halomfiltételt

A felszivárgás annyiból áll, hogy felcseréljük az elemet a szülőjével, amennyiben a szülő értéke kisebb a szóban forgó elemnéL Ezt az eljárást mindadclig folytatjuk, anúg vagy nagyobb nem lesz az elemnél a szülője, vagy el nem érjük a fastruktúra gyökerét. A 8.11. ábrán látható, miként cseréljük fel az új elemet a szülőcsomóponttal.

8.11. ábra. Az

tij elem he!Jet cserél a szüli!Jével

Az első csere után még minclig nem teljesül a halomfeltétel, mivel az új elem (P) még minclig nagyobb a szülőjénél (M), ezért folytatnia kell a felszivárgást. A 8.12. ábrán látható módon újból helyet kell cserélnie a szülőjével.

8.12. ábra. Az

4J elem eléri végsőpo:ifciiját a halomstmktúrában

A halomfeltételt ezzel helyreállitottuk, és a halom egy elemmel többet tartalmaz, mint korábban. A következő kihívás ugyanezt fordítva megtenni, amikor eltávolí­ tunk egy elemet. 213

Prioritásos sarok

A legnagyobb tárolt elemet megtalálni könnyű, de kiemelni a halomból már nem annyira. Ha egyszerűen törölnénk a listából, a fasttuktúra teljesen szétesne, és újból kezdhetnénk az egész struktúra felépítését. (Az olvasóra bízzuk ennek kipróbálását, amennyiben szabadidejében motivációt érez erre.) Ahelyett hogy az elem pozícióját egyszerűen üresen hagynánk, belehelyezhetjük a fa legalsóbb szintjének legszélső, jobb oldali pozíciójában elhelyezkedő elemet, ahogy a 8.13. ábra mutatja.

8.13. ábra. A

legnagyobb elemet eftávo!íijuk, és a lista utolsó e/emét ralguk a helJére

Annak ellenére, hogy maga a fasttuktúra sérteden marad, a halomfeltétel ismét sérül: a legkisebb elem most a gyökérben helyezkedik el. Ahhoz, hogy a halomstruktúra helyreálljon, az elemnek lejjebb kell kerülnie a fastruktúrában. Ezt a folyamatot

le­

sü!!Jedésnek nevezzük. A lesüllyedés az a folyamat, amikor egy elem helyet cserél a gyermekei közill a nagyobbik csomóponttal egészen addig, amíg vagy nem teljesül a halomfeltétel, vagy el nem éri a fa legalsó szintjét. Példánkban a süllyedő elem gyermekei közill a P a nagyobb, ezért helyet kell cserélnie az A betűvel. A 8.14. ábra mutatja a halom álla­ potát, miután megtörtént az első helycsere.

8.14. ábra. A

legfelső elem egy s�nttel fdjebb süi!Jedt

A halomfeltétel még így is sérül, mert az A betű mindkét gyermekénél kisebb. A gyer­ mekek közill az Ma nagyobb, ezért helyet cserél a szülővel. A 8.15. ábrán látható a ha­ lom jelenlegi állapota.

214

A prioritásos sorok áttekintése

0

8.15. ábra. A

ha/omtulqjdonság a sülfyedésfofyamata után hefyreá/1

A halomfeltétel ismét teljesül, miközben eltávolítottuk a legnagyobb elemet, és a ha­ lom immár eggyel kevesebb elemet tartalmaz, mint korábban. Ezekkel az új ismere­ tekkel felvértezve rátérhetünk a prioritásos sor megvalósítására. A következő gyakorlófeladatban olyan prioritásos sort valósítunk meg és teszte­ lünk, amelyben az elemek halmot reprezentáló listában helyezkednek el. A fejezet­ ben tárgyalt három megvalósítás közül ez a legbonyolultabb.

Gyakorlófeladat: halomalapúprioritásos sor tesztelése Először is hozzunk létre a halamalapú prioritásos sorhoz tartozó tesztesetet: public class HeaporderedListPriorityQueueTest extends AbstractPriorityQueueTestcase { protected Queue createQueue(Comparator comparator) { return new HeaporderedListPriorityQueue(comparator);

} } A megvalásítást ugyanolyan alapegységekre bontjuk, mint a korábbi kétprioritásos sornál tettük: definiálunk egy listát, amely az elemek tárolását szolgálja, illetve egy, az elemek sorrendezésére használatos Comparator objektumot: public class HeaporderedList�riorityQueue 1mplements Queue { private final List _list; private final comparator _comparator; public HeaporderedListPriorityQueue(Comparator comparator) { assert comparator != null :

"a 'comparator' nem lehet NULL";

_comparator = comparator; _list = new ArrayList();

} } Az enqueue() metódus hozzáadja a lista végéhez az új elemet, majd felszivárogtatja a lialomstruktúrában.

215

Prioritásos sarok

public void enqueue(ooject value)

{

_list.add(value); swim(_list.size() - l);

} Egy swimO metódust is létrehozunk, amely paraméterként a felszivárogtatott elem indexét fogadja el. Az indexhez tartozó elemet összehasonlítjuk a szülőjével (ha van), és felcseréljük a kettőt, amennyiben a szülője kisebb. A swim() metódust re­ kurzív módon újrahívjuk, ha folytatnunk kell a folyamatot: private void swim(int index) if (index

==

O)

{

{

return;

} int parent

=

(index - l)

l 2;

if (_comparator.compare(_list.get(index), _list.get(parent))

>

O)

{

swap(index, parent); swim(parent);

} J. 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 halomfeltétel: public object �equeue() throws EmptyQueueException if (isEmpty())

{

throw new EmptyQueueException();

}

object result

=

_list.get(O);

if (_list.size() _list.set(O,

>

l)

{

_list.get(_list.size() - l));

sink(O);

}

_list.delete(_list.size() - l); return result;

}

216

{

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 int right if (left

=

>=

index * 2

+

l; 2;

+

_list.size()) {

return;

} int largestChild if (right

<

=

left;

_list.size()) {

if (_comparator.compare(_list.get(left),

O)

{

_list.get(largestchild))

<

_list.get(right)) largestchild

}

=

<

right;

}

if (_comparator.compare(_list.get(index), swap(index,

0)

{

largestchild);

sink(largestChild);

l

}

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() { _list. 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 swim () metódusnak adja át, mihelyt hozzáadta az új elemet a lista végére. A swim()

metódus paramétere annak az elemnek az indexe, amelyet fel szeretnénk szivárog­ tatni. A swim() metódus feladata, hogy összehasonlítsa a paraméterben kapott in­ dexhez tartozó elemet a szülőjével, és hogy felcserélje a kettőt abban az esetben, ha

217

Prioritásos sarok

az elem nagyobb a szülőjénél. Amennyiben valóban szükség van cserére, a metódus újból meghívja önmagát, hogy rekurzív módon folytatódhasson a folyamat a halom feljebbi részein. A metódus leáll, ha a paraméterként kapott index O, inivel ez azt je­ lenti, hogy elérkeztünk a halom gyökeréhez. Vegyü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 sink() 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

Prioritásos sorok megvalósításainak összehasonlítása

Prioritásos sorok megvalósításainak összehasonlitá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 callcoun­

ti n gc o mp a ra t or 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 calleountingcomparator-t, hogy gyűjtse a számunkra fontos statisztikákat:

' pubii"C"'"'Ci assPriori tyQueűecallC:ountfngTest extends' Testease{ 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

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() { runscenario(new HeaporderedListPriorityQueue(_comparator), _reverseList);

.l 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: publ ic void testAveragecaseunsortedList() { runscenario(new unsortedListPriorityQueue(_comparator), _randomList);

}

220

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 listát, és meglúvja a r eporteall s ( ) metódust, amely kiírja a teszt eredményeit a szabványos kimenetre:

" ut)-{ private void ruiiscenario(Queue -qüeue:· ·c;st"-·iiip int i O; 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();

l Az illesztőprogram utolsó metódusa összesítést készít a tesztfuttatás alatt végzett összehasonlítások számáról:

private voia reportealls()--{ int calleount _comparator.getealleount(); system.out.println(getName() + ": "+ calleount + "hívás"); =

}

221

Prioritásos sorok A legrosszabb esetben kapott eredmények az alábbiakban láthatók a három prioritá­

sos sor megvalósításhoz kapcsolódóan: testworstcaseunsortedList:

387000 hívás 387000 hívás rderedList: 15286 ívás

testworstcasesortedList: testworstcaseHea

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:

hívás hívás

estBestcaseHea

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 153172 hívás testAveragecas�HeapQ deredList: 17324 hívás

testAveragecasesortedList:

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

.

Ú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 összehason l í tó segítségével állapítottuk meg.

222

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

4.

L istsorter

objektumot!

Készítsünk prioritásos sort, amely a legnagyobb elem helyett a legkisebb elemhez biztosít hozzáférést!

223

KILENCEDIK FEJEZET

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

könyv eddigi fejezeteiben alapvető struktúrák áttekintésével foglalkoztunk, ame­

lyek adatok tárolását és rendezését szolgálták, a keresési módszerek közül azonban csak kezdecleges megközelítéseket vizsgáltunk. 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 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­

A

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 og N), amennyiben rendezett listában keresünk adatokat. Amint a 2. fejezetben láthattuk, a rendezeden listában való keresés legegyszerűbb módja, hogy a legelső elemtől elindulva végigmegyünk a listán egészen addig, amíg vagy megtaláljuk az elemet, vagy a lista végére nem érünk. Ennek a módszernek az át­ lagos költsége O(N). A pontos futási idő ádagosan N/2, mivel ádagosan a lista feléig el kell jutnunk ahhoz, hogy megtaláljuk a keresett elemet. A rendezett listákban tárolt adatok esetében azonban ennél sokkal jobb keresési hatékonyságot érhetünk el.

Bináris keresés és beszúrás A bináris keresés neve onnan származik, hogy a keresés során minden egyes lé­

pésben megfelezzük a kereséshez használt adatmennyiséget, ily módon folyamato­ san leszűkítve a keresési teret egészen addig, amíg vagy célt nem érünk, vagy üresre nem zsugorodik a keresési tér. Tekintsünk például egy angolszótárt. Ha azt a feladatot kapnánk, hogy keressük ki az algoritmus szót, hol kezdenénk a keresést? Valószínűleg a könyv elejétől elindul­ va egyesével lapozgatnánk az oldalakat. Ha azonban a /ama szót kellene kikeresnünk, valószínűleg a közepe táján nyitnánk ki a szótárat. Miért is? Miért nem kezdenénk a szótár hátuljánál a keresést? Az ok ter­ mészetesen az, hogy előre tudjuk, hogy a szótárban levő szavak ábécérendben helyez­ kednek el, ezért viszonylag jó becslést tudunk végezni arra vonatkozóan, hogy hol he­ lyezkednek el az l betűvel kezdődő szavak. Ha, a példánál maradva, a /ama szót keres­ sük, és a szótárt felütve a mandarin szót lá�uk, akkor tudjuk, hogy már elhagytuk az l betűs szavakat, és visszafelé kell lapoznunk. Ha viszont a kenguru szónál nyilik ki a szó­ tár, tudha�uk, hogy még tovább, előbbre kell keresnünk. Tehát ha rossz helyen nyi�uk ki a szótárt, könnyen kideríthe�ük, milyen irányban kell továbblapoznunk. A követke­ ző megválaszolandó kérdés az, hogy mennyit lapozzunk előre vagy hátra. Ennél a példánál maradva, a szótár nyelvének, illetve a különböző betűkkel kez­ dődő szavak előfordulási gyakoriságának ismeretében elég jól meg tudjuk becsülni, milyen messzire kell ellapozni. De mi történne, ha a fenti feltételezéssel ellentétben, semmilyen előzetes ismeretünk nem lenne a kinyitott könyv tartalmáról? Mi lenne, ha csak annyit tudnánk róla, hogy a szavak ábécérendben helyezkednek el benne? A bináris keresés szerint ily�n helyzetben úgy járunk el, hogy minden lépésben

megfelezzük az adatok számát - innen ered a bináris elnevezés - és az aktuális kö­ rülményeknek megfelelően vagy az adathalmaz egyik felében, vagy a másikban kere­ sünk tovább. A bináris keresés lépéseit a következőképpen foglalha�uk össze. 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.

226

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 o A

1

l

2

l

D

9. 1 . ábra.

F

3

l

4

H

(9.1.

ábra). A lista kilenc betűt tartalmaz, ábécébe sorolva.

5

6

7

Kl

LG

8 p

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 A

1

l

2

l

D

F

3

l

4

H

5

6

7

Kl

LG

8 p

keresés mindig a köz' épső elemnél kezdődik

9.2. ábra. A

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 o

1

2

(9.3.

3

4

ábra). 5

6

7

8

keresési kulcs bi'{/osan a listafelsőfelében he!Jezkedik el

9.3. ábra. A

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­

(9.4.

tottuk o

1

ábra).

2

9.4. ábra. A

3

4

5

6

7

8

"kö'zépső" elemmelfo!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 - aK betű megelőzi az L betűt -, ezért feltételezzük, hogy a keresett elem a lis­ ta alsó felében helyezkedik el- ha egyáltalán szerepel a listában. A

9.5. ábrán

látható, hogy a keresési tér végül is egyetlen elemre szűkül ( aK be­

tűre), amely esetünkben meg is egyezik a keresett kulccsal.

227

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

2

9.5. ábra. A

3

4

5

6

7

8

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 aK betűvel való ösz­ szehasonlítás útján. Joggal merülhet fel az olvasóban az ellenvetés, hogy a bináris keresés azért tűn­ het optimálisabbnak a letámadásos módszernél, mert a példában felhasznált keresési kulcs a lista közepe táján, és nem a lista elején helyezkedik el. Például, ha az A betűt kerestük volna, akkor letámadásos módszerrel egyetlen lépésben, rnig bináris keresés útján csak négy lépésben találtuk volna meg. Biztonsággal állitható tehát, hogy bizonyos speciális esetekben a letámadásos módszereken alapuló keresés hatékonyabb, mint a bináris keresés. Az esetek több­ ségében azonban a bináris keresés sokkal jobb teljesítményt ér el - ezt a fejezet ké­ sőbbi részében konkrétan be is fogjuk bizonyítani.

A bináris keresés megközelitései Most, hogy áttekintettük az algoritmus elvi működését, térjünk rá a kódolásral Ebben a részben két bináris keresési megközelítést mutatunk be: az egyik rekurzív, a másik pedig iteratív módszert használ. Teljesítmény szempontjából mindkettő ugyanazokkal a tulajdonságokkal rendelkezik, de látni fogjuk, hogy az egyik kézenfekvőbbnek tűn­ het, mint a másik.

Listabeli kereső A most következő gyakorlófeladatban olyan interfészt készítünk, amely a bináris ke­ resésnek mind a rekurzív, mint az iteratív megvalósításában közös. Ezáltal lehetősé­ günk nyílik rá, hogy különböző megvalósításokat egységes módon teszteljünk, mű­ ködés és teljesítménykiértékelés szempontjából is. A

listabeli kereső lehetőséget kínál

listában való keresésre egy adott keresési kulcs

szerint (esetünkben rendezett listákról van szó), egyetlen metódus- a search() me­ tódus - segítségéveL A metódus összehasonlítót használ annak eldöntésére, hogy a keresési kulcs megegyezik-e a lista bármely elemével. Ha a kulcsot megtalálja a listá­

ban, a search() metódus visszaadja a pozíciójának indexét (O, 1, 2...) . Ha a kulcs

228

A bináris keresés működése

nem szerepel a listában, a search() metódus negatív értéket ad vissza, amely abszo­ lút értékében annak a pozíciónak felel meg, ahol az elem szerepelt volna, ha a kere­ sés találattal végződik. Ezen a ponton felmerülhet a kérdés, hogyan adhatunk vissza olyan értéket, amely pozícióadat, és egyben azt is jelzi, hogy a keresés sikertelen. A válasz részben a negatív értékben rejlik. liy módon pozitív visszaadott érté­ kekkel jelezzük a keresés sikerességét, és negatív értékekkel a sikertelen kereséseket. Ha azonban a pozíciónak megfelelő negatív értékeket tekintjük (például az l-esből -1, a 2-esből-2 lesz stb.), mit kezdjünk a lista legelső pozíciójával, a O-val? A -O ér­

téknek nincs értelme. Az alkalmazott trükk szerint a visszaadott értékeket úgy módosí�uk, hogy a 0. hely esetében -1-et, az 1. hely esetében -2-t adunk vissza, és így tovább. Ezzel a módszerrel egyszerre tudjuk jelezni a keresés sikertelenségét, és a megfelelő pozíció­ adatot is visszaadjuk.

Gyakorlófeladat: listabeli kereső interfész létrehozása Először is az alábbi módon hozzunk létre egy Java-interfészt:

�-,;-ac:k:a9e-·com:w-.:ox:·:-at9or�fthins-:bsea-rcl1; import com.wrox.algorithms.lists.List; public interface Listsearcher { public int search(List list, Object key); ---· � ·= �--��-"""-'

' � ;· --- -. :. -· ·

A megvalósitás müködése Az interfészben a korábban tárgyalt search()műveletnek megfelelően egyetlen metó­

dust definiálunk. Ez a metódus egy listát és egy keresési kulcsot fogad el bemenő pa­ raméterként, és egy egész számot ad vissza, amely egy listabeli pozíciónak felel meg. Vegyük észre, hogy összehasonlító objektumot nem adunk át paraméterként a

search() metódusnak, annak ellenére, hogy szükségünk lesz ilyenre. Ehelyett felté­ telezzük, hogy akármilyen keresőt hozunk létre, az már tartalmazni fog egy összeha­ sonlítót. A problémakötök ilyen módon történő szétválasztása lehetővé teszi listabe­ li keresők paraméterként történő átadását anélkül, hogy a szóban forgó kód bármi­ lyen információt tartalmazna arra vonatkozóan, hogJan végezzük el az elemek közötti összehasonlításokat. A tényleges tesztelő kód írása közben egyértelműbbé fog válni ennek a döntésnek az oka.

229

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

Gyakorlófeladat: tesztelő kód írása Most, hogy az interfészt már létrehoztuk, áttérhetünk a tesztek megírására. A koráb­ biakból már tudjuk, hogy legalább két keresőmegvalósításunk lesz - egy iteratív és egy rekurzív -, és a fejezet vége előtt még egy harmadik keresőt is megvalósítunk. Kezdetben olyan tesztcsomagot hozunk létre, amelynek minden listabeli keresőnek meg kell felelnie. lly módon nem kell minden egyes megvalósítás után újraírnunk a tesztelés kódját. Kezdjük magával a tesztosztály megvalósításával: package com.wrox.algorithms.bsearch; import com.wrox.algorithms.lists.ArrayList; import com.wrox.algorithms.lists.List; import com.wrox.algorithms.sorting.Comparator; import com.wrox.algorithms.sorting.Naturalcomparator; import junit.framework.Testcase; public abstract class AbstractListsearcherTestcase extends Testcase

{

private static final object[] VALUES =

{"B",

"c", "o", "F", "H", "I", "J", "K .. , "L", "M", "P", "Q"};

private Listsearcher _searcher; private List _list; protected abstract Listsearther createsearcher(Comparator comparator); protected void setup() throws Exception

{

super.setUp(); _searcher = createsearcher(Naturalcomparator.INSTANCE); _list= new ArrayList(VALUES);

} }----------------------------------------�---- --·-A megvalósitás működése Az Abstract:Li stsearcherTest:case teszteset definiál néhány tesztadatot (VALU 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ömbből lét­ rehozunk egy listát, amelyet a tesztek során felhasználhatunk.

230

A bináris keresés működése

Vegyük észre, hogy a createsearcher() metódus összehasonlítót fogad el pa­ raméterként. Emlékezzünk vissza, hogy a L i stsearcher osztály search() metódusa nem említ összehasonlítót, ezért egyedül a listabeli kereső létrehozásakor kell össze­ hasonlítókkal foglalkoznunk.

A következő gyakorlófeladatban létrehozunk néhány tesztet.

Gyakorlófeladat: tesztek létrehozása A következő teszt segítségével meggyőződhetünk róla, hogy amikor a listában sze­ replő értékeket keresünk, helyes pozíciót kapunk vissza.

public voi d testSearchForExistingv"alues() 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=orNonExistingValueLessTtianFi 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-xisti 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. ge t( i )), és elvégzi a kere­ sését. Minden keresés eredményét ellenőrzi, hogy a visszaadott szám valóban meg­ egyezik-e az aktuális pozícióvaL Itt akár iterátort is használhatnánk, de akkor külön számon kellene tartanunk az aktuális pozíciót. Ezért inkább egy egész számot hasz­ nálunk a pozíció jelölésére, és mindig a get() metódust hívjuk meg.

231

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

A másoclik tesztben A betűt keresünk, amely nyilvánvalóan nem szerepel a lis­ tában. Ha azonban mégis szerepeille benne, akkor a lista legelején találnánk meg - a O. pozícióban-, mivel a sorrendben az összes többi elemet megelőzi. Ezért a vissza­

adott érték várhatóan -(0+1)

=

-1. Ne feledjük, hogy a listában nem szereplő ele­

mek keresésekor visszaadott érték -(beszúrási pont + 1). A harmaclik tesztben Z betűt keresünk. Ahogy korábban az A, a Z betű sem szerepel a listában, de ezt a lista végén kellene megtalálnunk (12. pozíció). Ezért a visszaadott érték várhatóan -(12+1)

=

-13.

Az utolsó tesztben E betűt keresünk, amelyet a 3. helyen találhatnánk meg, ha szerepeille a listában. A keresőfüggvénynek ekkor -(3+1)

=

-4-et kellene visszaad­

nia, ezzel jelezve, hogy a keresési kulcs nem szerepel a listában.

Rekurzív bináris kereső Most, hogy a tesztfüggvényeken túljutottunk, térjünk rá a bináris keresőalgoritmus megvalósítására. A bináris keresés folyamata lépésről lépésre egyre kisebb részekre osztja a keresési teret. Erről az "oszd meg és uralkodj" típusú megközelítésről süt a rekurzív megvalósítás lehetősége, és az első megvalósításunk valóban rekurzív mó­ don működik.

Gyakorlófeladat: rekurzív bináris kereső megvalósítása és tesztelése Hogy meggyőződhessünk rekurzív bináris keresőnk helyes működéséről, először hozzunk létre egy tesztosztályt: package com.wrox.algorithms.bsearch; import com.wrox.algorithms.sorting.Comparator; public class RecursiveBinaryListsearcherTest extends AbstractListsearcherTestcase

{

protected Listsearcher createsearcher(comparator comparator)

{

return new RecursiveBinaryListsearcher(comparator);

} } Ezt követően hozzuk létre magát a listabeli keresőt: 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

232

{

co�.P�•a�ra�t�o�r�;�------�---�

A bináris keresés működése pub lic Rei:ursiveBinaryListsearcher(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,

}

else if (cmp

>

0)

key,

lowerindex, index -

l);

{

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 AbstractListsearcherTestcase 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 RecursiveBinaryListSearcher osztály egy példányát kell visszaadnia. A Recursi veListsearcher osztály, túl azon, hogy megvalósí�a a Listsearcher 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 vely() metódus még két paramétert fogad el: lowerindex-et és upperindex-et. Ez a két paraméter a keresési tér határindexeit definiálja. Ha átnézzük az ábrákat a 9.1.től a 9.5.-ig, láthat­ juk hogy valahányszor két részre osztjuk a listát, a lista más intervallumát kell tekin­ tenünk. Az első lépésben (9.l. ábra) a lista O-tól 8-as indexig elhelyezkedő elemeit

233

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

vizsgáltuk, mint a keresési kulccsal potenciálisan megegyező elemeket. A következő lépésben a keresési teret leszűkítettük az 5-östől a 8-as pozícióig (9.3. ábra). A kere­ sés végén egyetlen pozícióra csökkent az a tér, amelyben a keresett kulcsot megtalál­ hattuk: az 5-ös pozícióra (9.5. ábra). Ezek a felső és alsó korlátok egy az egyben megfelelnek az upperrndex, illetve a lowerrndex paraméter szerepkörének. A leállási feltételt egy pillanatra félretéve megállapíthatjuk, hogy a keresési fo­ lyamat első lépése a "középső" elem meghatározása. Ezt úgy kapha�uk meg, hogy a felső és az alsó indexet kivonjuk egymásból, majd a különbséget megfelezzük: int index

=

lowerindex + (upperindex - lowerrndex) l 2;

A 9.1. ábrából kiindulva láthatjuk, hogy a példában a letből adódóan: 0+(8-0)/2

=

0+4

=

középső" elem indexe a kép­ " 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+1

=

=

volna hozzá a képletben a tört értékéhez, akkor a kapott eredmény (8-5)/2 =

5+3/2

6 (pontosan, ahogy a 9.4. ábrán is látszik). Ha az alsó indexet nem adtuk =

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 if (cmp index

}

234

<

O) { searchRecursively(list,

key,

lowerrndex,

index - l);

1):

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

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 naryListSearcher 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 _comparator

=

!= null :

"a 'comparator' nem lehet null";

comparator;

} public int search(List list, object key) { assert list != null : int lowerindex _..,"""""""'U=J> er Index

__

236

"a 'list' nem lehet null";

O; list.size()

-

l;

A bináris keresés működése

wnile-(lowerrnaex ':).· ..

272

A bináris keresőfa tesztelése és megvalósítása Node f= new Node("F"-;--r1Ul l -;-·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, assertEquals(a,

_

assertEquals(d,

_

assertEquals(f,

_

l);

a );

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

l

}

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

_

private Node

_

l; m;

private Node _p; protected void setup() throws Exception

{

super.setup();

273

Bináris keresőfák _a

new Node("A");

_h

new Node("H");

_k

new Node("K"); new Node("P");

_p _f

=

_m

=

"

new Node(" F ,

null,

new Node("M",

null, _p);

_h);

_d

new Node("D", _a,

_f);

_l

new Node("L", _k,

_m);

_i

new Node("!",

_l);

_d,

}

} 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 nimum()

és a testMaximum() 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.mi nimum()); 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 SearchTree

274

segédmetódusaként használnánk őket.

B i nary­

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 null: public void testsuccessor() { assertsame(_d,

_a.successor());

assertsame(_f,

_d.successor());

assertSame(_h,

_f.successor());

assertsame(_i,

_h.successor());

assertsame(_k,

_i.successor());

assertsame(_l,

_k.successor());

assertsame(_m,

_l.successor());

assertsame(_p,

_m.successor());

assertNull(_p.successor());

} public void testPredecessor() { assertNull(_a.predecessor()); assertsame(_a,

_d.predecessor());

assertsame(_d,

_f.predecessor());

assertsame(_f,

_h. predecessor());

assertsame(_h,

_i.predecessor());

assertSame(_i,

_k.predecessor());

assertsame(_k,

_l.predecessor());

assertSame(_l,

_m.predecessor());

assertsame(_m,

_p.predecessor());

} Létrehozunk még egy tesztpárt- testrssmaller() és testrsLarger()- az eddig nem említett metódusokra, amelyek később még hasznosnak bizonyulhatnak Egy csomópontot akkor kezelünk kisebb gyermekként, ha szülőjének a bal oldali gyer­ meke. Ugyanez fordítva is érvényes: egy csomópont csak akkor esik nagyobb gyer­ mek besorolás alá, ha jobb oldali gyermeke a szülőnek. public void testisSmaller() { assertTrue(_a.issmaller()); assertTrue(_d.issmaller()); assertFalse(_f.issmaller()); assertFalse(_h.issmaller()); assertFalse(_i.issmaller()); assertTrue(_k.issmaller()); assertFalse(_l.issmaller()); assertFalse(_m.issmaller()); assertFalse(_p.issmaller());

} public void testisLarger() { assertFalse(_a.isLarger()); assertFalse(_d.isLarger());

275

Bináris keresőfák

assertTrue(_f.i sLarger()); assertTrue(_h.i sLarger()); assertFalse(_i.isLarger()); assertFalse(_k.isLarger()); assertTrue(_l.i sLarger()); assertTrue(_m.i sLarger()); assertTrue(_p.isLarger());

} Végül létrehozunk néhány tesztet az equals O metódushoz. Az equals O metódus akkor jut majd fontos szerephez, amikor a B i narysearchTree osztályt fogjuk tesz­ telni, mivel lehetővé teszi a csomópontok beszúrásakor és törlésekor létrejött struk­ túrák összehasonlítását az elvárt eredménnyel. A megvalósítás az aktuális csomó­ pontból indul, és összehasonlí�a az értékeket, valamint a bal oldali és a jobb oldali gyermekeket, ahogyan lefelé halad a levélcsomópontokig. A testEquals 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 Node Node Node Node Node Node Node Node Node

voi d testEquals() { new Node("A"); a h new Node("H"); k new Node("K"); p new Node("P"); new Node("F", null, h); f m new Node("M", null, p); d new Node("D", a, f); l new Node("L", k, m); new Node("!", d, l); =

=

=

=

assertEquals(a, assertEquals(d, assertEquals(f, assertEquals(h, assertEquals(i, assertEquals(k, assertEquals(l, assertEquals(m, assertEquals(p,

_a); _d); _f); _h); _i); _k); _l); _m); _p);

assertFalse(_i.equals(null)); assertFalse(_f.equals(_d));

} Miután elkészültünk a tesztekkel, a következő gyakorlófeladatban magát a csomó­ pontosztályt alkotjuk meg.

276

A bináris keresőfa tesztelése és megvalósítása

Gyakorlófeladat: a csomópontosztály tesztelése Hozzuk létre

a

következő csomópontosztályt:

package com.wrox.algoritnms.ostrees; public class Node implements Cloneable { private object _value; private Node _parent; private Node _smaller; private Node _larger; public Node(object value) { this(value, null, null);

} public Node(object value, Node smaller, Node larger) { setvalue(value); setsmaller(smaller); setLarger(larger); if (smaller != null) { smaller.setParent(this);

} if Clarger != null) { larger.setParent(this);

} } public object getvalue() { return _value;

} public void setvalue(object value) { assert value != null : _value = value;

"az érték nem lehet NULL";

} public Node getParent() { return _parent;

} public void setParent(Node parent) { _parent = parent;

} public Node getsmaller() { return _smaller;

277

Bináris keresőfák

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

getParent().getsmaller();

} 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

278

this;

getParent().getLarger();

A bináris keresőfa tesztelése és megvalósítása

while (nodé-:-isLarger0) { node = node.getParent();

} return node.getParent();

} public Node predecessor() { if (getsmaller() != null) { return getsmaller().maximum();

} Node node = this; while (node.issmaller()) { node = node.getParent();

} return node.getParent();

} public int size() { return size(this);

} public boolean equals(Object object) { if (this == object) { return true;

} if (object == null l l object.getclass() != getclass()) { return false;

} Node other = (Node) object; return getvalue().equals(other.getvalue()) && equalssmaller(other.getsmaller()) && equalsLarger(other.getLarger());

} private int size(Node node) { if (node == null) { return O;

} return l+ size(node.getsmaller())

+

size(node.getlarger());

}

279

Bináris keresőfák

{

private boolean equalssmaller(Node other) return getsmaller() == null && other

}

11

getsmaller() != null && getsmaller().equals(other);

private boolean equalslarger(Node other) return getlarger()

}

}

l l

null

==

==

null && other

==

{ null

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: public Node(Object value, Node smaller, Node larger) setvalue(value); setsmaller(smaller); setLarger(larger); if (smaller

!= null)

{

smaller.setParent(this);

}

280

{

A bináris keresőfa tesztelése és megvalósítása

if Clarger != null) { larger.setParent(this);

} } Ha már létrejött, hozzáférésre van szükség a csomópont értékéhez, szülójéhez és bármely gyermekéhez. Ehhez létrehozunk néhány szabványos kiemelőt és beállitót. Semmi különös, csupán néhány pluszkijelentést helyeztünk el - például annak tesz­ telésére, hogy nem helyeztük-e mindkét gyermeket azonos csomópontba: public object getvalue() { return _value;

} public void setvalue(Object value) { assert value != null : "az érték nem lehet NULL"; _value = value;

} public Node getParent() { return _parent;

} public void setParent(Node parent) { _parent = parent;

} public Node getsmaller() { return _smaller;

} public void setsmaller(Node smaller) { assert smaller != getLarger() : "a kisebb nem lehet azonos a nagyobbal"; _smaller = smaller;

} public Node getLarger() { return _larger;

} public void setLarger(Node larger) { assert larger != getsmaller() : "a nagyobb nem lehet azonos a kisebbel"; _larger larger; =

} Ezután kövessünk néhány kényelmi módszert az egyes csomópontok tulajdonságai­ nak megállapítására.

281

Bináris keresőfák

Az issmaller() és az isLarger() metódus eredménye csak akkor true, ha a csomópont kisebb, illetve nagyobb a szülőjénél: public boolean issmaller() { return !isRoot() && this

getParent().getsmaller();

} public boolean isLarger() { return !isRoot() && this == getParent().getLarger();

} A minimum vagy a maximum megtalálása sem sokkal bonyolultabb. Emlékezzünk, hogy a csomópont minimuma a legkisebb gyermeke, a maximum pedig a legna­ gyobb (vagy amelynek nincs saját gyermeke). Figyeljük meg, hogy a maximum() kódja majdnem azonos aminimum() kódjával; míg ami nimum() a getsmaller() metódust, a maximum() a getLarger() metódust hívja meg: public Node minimum() { Node node = this; while (node.getSmaller()

!= null) {

node = node.getsmaller();

} return node;

} public Node maximum() { Node node = this; while (node.getLarger()

!= null) {

node = node.getLarger();

} return node;

} Egy csomópontot megelőző és az utána következő csomópont megtalálása már komolyabb feladat. Emlékezzünk, hogy a csomópontot követő csomópont vagy a legnagyobb gyermek minimuma- ha van ilyen -, vagy az első olyan csomópont, amellyel találkozunk egy "jobb kanyar" után, amikor felfelé haladunk a fán. A successor() megtekintésekor láthatjuk, hogy ha a csomópontnak van egy nagyobb gyermeke, akkor a minimumát vesszük. Ha nincs, akkor elindulunk felfelé a fán a "jobb kanyart" keresve, úgy, hogy ellenőrizzük, az aktuális csomópont-e a legnagyobb a szülőjének gyermekei közül. Ha ez a nagyobb, akkor jobbra kell lennie a szülőjétől, és balra visszamegyünk felfelé a fán.

282

A bináris keresőfa tesztelése és megvalósítása

Lényegében felfelé mozgunk a fán az első olyan csomópontot keresve, amely a szülő kisebb (azaz bal oldali) gyermeke. Ha megtaláltuk, "jobb kanyart" kell tenni ahhoz, hogy a szülőhöz jussunk - pontosan, amire vártunk. Figyeljük meg azt is, hogy mint a minimum() és a maximum() esetében a successor()

és a predecessor() a tükörképei egymásnak: ahol a successor() a

minimum, ott a predecessor() a maximum értéket veszi fel; amikor a successor az isLarger()

metódust hívja meg, akkor a predecessor az issma ller() 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 BinarysearchTree osztály által létrehozott fák felépítésének a vizsgálatára.

A sablonkód mellett az általános equals() metódus hasonlítja össze minden egyes

csomópont három aspektusát, hogy kiderüljön, azonosak-e:

az

értéket, a kisebb és a na­

gyobb gyermeket. Az értékek összehasonlítása egyszerű: tudjuk, hogy

az

érték sosem

lehet null; tehát elegendő az értékek egyszerű delegálása az equals() metódusba:

283

Bináris keresőfák

public boolean equals(Object object) { if (this == object) { return true;

} if (object

==

!= getclass()) {

null l l object.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 null, de a gyermekek gyermekeit, azok gyermekeit és így tovább is ellenőrizni kell egészen a levélcsomópontokig. Eh­ hez két segédmetódust hoztunk létre: az equalssmaller() és

az

equalsLarger()

elnevezésűt. Ezek a metódusok összehasonlítják az aktuális csomópont gyermekeit a másik csomópont megfelelő gyermekével. Például az equalssmaller() összeveti az aktuális csomópont kisebbik gyermekét a másik csomópont kisebbik gyermekével. Ha mindkét gyermek értéke null, akkor a csomópontokat egyenlőnek tekintjük. Amennyiben csak az egyik gyermek értéke null, semmiképpen sem lehetnek egyen­ lők. Ha azonban mind az aktuális csomópontnak, mind a másik csomópontnak van egy kisebb gyermeke, akkor rekurzív módon meghívjuk az equals() metódust, hogy folytassuk az ellenőrzést a fán lefelé: private boolean equalssmaller(Node other) { return getsmaller()

==

null && other == null

ll getsmaller() != null && getsmaller().equals(other); } private boolean equalsLarger(Node other) { return getLarger()

==

null && other

==

null

l l getLarger() != null && getLarger().equals(other); } Ennyit a csomópontosztályokróL A következő gyakorlófeladatban teszteket hozunk létre a végső bináris keresőfára való felkészülésként.

284

A bináris keresőfa tesztelése és megvalósítása

Gyakorlófeladat: a bináris keresőfa tesztelése Hozzuk létre

a

tesztosztályt

a

következőképpen:

j)ad

:

"az

'initialcapacity'

nem

lehet O :

" A 'word' (szó) nem lehet üres ";

Node node = search(_root, word,

0);

return node != null && node.isEndofword();

} public void patternMatch(Charsequence pattern, assert pattern != null :

List results) {

"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, assert prefix

!= null :

List results) {

"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, assert word != null : if (node

int index) {

"A 'word' (szó) nem lehet null";

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,

408

index);

A hármas keresőfák gyakorlati alkalmazása

search(node.getLarger(),

word,

index);

} return node;

} private Node insert(Node node, charsequence word, int index) assert word

!= null

:

" A

{

'word' (szó) nem lehet null ";

char c = word.charAt(index); if (node == null)

{

node = new Node(c);

} if (c == node.getchar())

{

if (index + l< word.length())

{

node.setchild(insert(node.getchild(),

word, index + l));

} else { node.setword(word.toString());

} } else if (c< node.getchar()) { node.setsmaller(insert(node.getsmaller(),

word, index));

} else { node.setLarger(insert(node.getLarger(),

word, index));

} return node;

} private void patternMatch(Node node, charsequence pattern, int index,

List results)

{

assert pattern

!= null

"A

assert results

!= null

"A 'results' (eredménylista) nem

'pattern' (minta) nem lehet NULL";

lehet NULL"; if (node == null)

{

return;

} char c = pattern.charAt(index); if (c == WILDeARD

l l c< node.getchar()) {

patternMatch(node.getsmaller(),

pattern, index, results);

} if (c == WILDCARD

l l c == node.getchar()) { {

if (index + l< pattern.length()) �------�,patternMatch(node.getchild(),

eattern,_index +l,

result�);

409

Hármas keresőfák

} 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, assert results != null

:

List results) {

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

410

A hármas keresőfák gyakorlati alkalmazása

public void setLarger(Node la r�)-{ _larger =larger;

} public NOde getchild() { return _child;

} public void setchild(Node child) { _child =child;

} public String getword() { return _word;

} public void setword(String word) { _word = word;

} public boolean isEndofword() { return getword() !=null;

}

}

A megvalósitás müködése A TernarysearchTree osztálydefiníciója elég üres, egy egypéldányos változót tar­

talmaz a gyökércsomópont tárolására és egy helyettesítőkarakterként használatos konstanst határoz meg mintaillesztés esetén: package com.wrox.algorithms.tstrees; import com.wrox.algorithms.lists.List; public class TernarysearchTree { public static final char WILDCARD = '?'; private Node _root;

} Létrehoztuk a Node osztályt is, amely elkészíti a fa szerkezetét, egy nagyon egyszerű osztályt, amely tárolja és visszaadja a karakteres értéket, valamint a kisebb és na­ gyobb testvérek és természetesen bármelyik gyerek referenciáját. Figyeljünk a külö­ nös _word változóra. Emlékezzünk vissza, hogy valamilyen módon jelölnünk kellett a szó végét. Használhattunk volna logikai tipust, de a gyakorlat céljából inkább ma-

411

Hármas keresőfák

gát az adott szót tároltuk Bár ez nyilvánvalóan több memóriát használ, könnyebben elvégzi a szavak összegyűjtését, ha keresést végez. Létezik még egy kényelmi mód­ szer, az isEndofword(), amely csak akkor tér vissza true értékkel, ha a csomópont­ ban egy szót tárolunk: 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() { return _child;

} public void setchild(Node child) _child

=

child;

} public String getword() { return _word;

}

412

{

{

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 kelljegyeif�ü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 false é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 false é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, return node

word, O);

!= 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

Hármas keresőfák private Node search(Node node, assert word

!= null :

if (node

null) {

charsequence word,

int index) {

" A 'word' (szó) nem lehet null";

return null;

} char c = word.charAt(index); if (c == node.getchar()) { if (index + l< word.length()) { node = search(node.getchild(),

word,

index + l);

word,

index);

}

} else if (c < node.getchar()) { node

search(node.getsmaller(),

} else { node

search(node.getLarger(),

word,

index);

} return node;

}

, Az .

add()

és az inser:tO 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 az

insert-et,

add()

meghívja

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 metódus adott vissza.

insertO

public void add(charsequence word) { assert word != null assert word.lengthO

:

" A 'word' (szó) nem lehet null"; >

O : " A 'word' (szó) nem lehet üres";

Node node = insert(_root, word,

O);

if (_root == null) { _root = node;

} }

Az

insertO

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

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 : char c

" A 'word' (szó) nem lehet null";

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

} else {

node.setLarger(insert(node.getLarger(),

word, index));

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 inorderTraversal() metó­ dusnak: public void prefixsearch(CharSequence prefix, List results) { assert prefix

!= null : "a 'prefix' nem lehet null";

assert prefix.length() >O

:

"a 'prefix' nem lehet üres";

inorderTraversal(search(_root, prefix, O), results);

}

415

Hármas keresőfák

Az inorderTraversal metódus rekurzív módon járja be a kisebb testvért, ezután a csomópont gyerekét, és végül a nagyobb testvért. Minden alkalommal, ha egy szó megfelel (node.i sEndofword O ) , hozzáadjuk az eredményekhez: private void inorderTraversal(Node node, assert results

!= null:

List results) {

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

inorderTraversal(node.getLarger(),

results); 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, assert pattern

!= null

"

List results) {

A 'pattern' (minta) nem lehet NULL"; assert pattern.lengthO >O: "A pattern (minta) nem lehet üres"; assert results != null : "A 'results' (eredménylista) nem :

lehet NULL"; patternMatch(_root,

pattern, O,

results);

} A második patternMatch O metódus, néhány megszorítással, inkább úgy néz ki, mint a fa egy inorder bejárása. Először ahelyett, hogy bejárnánk a bal, illetve a jobb testvéreket, ellenőrzést haj­ tunk végre, hogy eldöntsük, szükséges-e a bejárás. Ha az aktuális mintakarakter az

aktuális csomópont előtt helyezkedik el, akkor a kisebb testvér bejárását végezzük el; ha a csomópont után, akkor a nagyobb testvérét; és ha ugyanaz, mint az aktuális csomópont, akkor rekurzív hívást hajtunk végre a mintában lévő következő karak­ terrel az első gyereken. Másodszor minden pontban, ha az aktuális mintakarakter WILDCARD, akkor mindegy, hogy rnit járunk be. Ekkor a helyettesítőkarakter illeszkedik minden más karakterre. Végül a keresés csak a mintával egyenlő hosszúságú szavakat fogja figyelembe venni - például az öt hosszúságú minta csak öt hosszúságú szavakra illeszkedik:

416

Keresztrejtvény megoldását segítő példa private void patternMatch(Node node, int index,

charsequence pattern, List results) {

assert pattern

!= null

"A 'p.attern'

assert results

!= null

"A 'results' (eredm énylista) nem

(minta) nem lehet NULL";

lehet NULL"; if (node

==

null) {

return;

} char c = pattern.charAt(index); if Cc

==

WILDCARD l l

c < node.getchar()) {

patternMatch(node.getsmaller(),

pattern, index,

results);

} if (c

==

WILDCARD ·l l

c == node.getchar()) {

if (index + l < pattern.length()) { patternMatch(node.getchild(),

pattern,

index + l,

results);

} else if (node.isEndofword()) { results.add(node.getword());

} } if (c == WILDCARD

l l

c > node.getchar()) {

patternMatch(node.getLarger(),

pattern,

index,

results);

} }

Keresztrejtvény megoldását segitő példa Felfegyverkezve az alaposan tesztelt és létrehozott mintaillesztő kóddal, most már megnézhetünk egy példaalkalmazást, amely bemutatja a hármas keresőfák egy új használatát: a keresztrejtvény megoldását. Ebben a részben kifejlesztünk egy nagyon kis méretű parancssori alkalmazást, amely argumentumként vesz át szavakat tartal­ mazó fájlt - soronként egy szó - és egy mintát az illesztéshez, és tartalmazhat he­ lyettesítőkaraktereket is.

417

Hármas keresőfák

Gyakoriófeladat: keresztrejtvény-segédalkalmazás létrehozása Hozzuk létre az crosswordHel per osztályt a következőképpen:

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

}

418

+

"





.

") ;

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) fájlból ...");

'"

+

fileName

+

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 Helpe r osztály meghatározza a main() 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­ For P attern( ) metód us nak hogy létrehozza a tényleges illesztést: ,

package com.wrox.algorithms.tstrees; import com.wrox.algorithms.iteration.Iterator; import com.wrox.algorithms.lists.LinkedList; import com.wrox.algorithms.lists.List; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException;

419

Hármas keresőfák public final class CrosswordHelper { private crosswordHelper() {

} public static void main(String[] args) throws IOException { assert args

!= null :

"az argumentumlista nem lehet NULL";

if (args.length O "A 'pattern' (minta) nem lehet üres"; _pattern

pattern;

=

} public StringMatch search(Charsequence text,

int from)

{

!= null : "A 'text' (szöveg) nem lehet NULL"; assert from >= O : "A 'from' (kezdőpont) nem lehet negatív"; assert text

int s = from; while (s =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 assert target !=null

"A 'source' :

"A

(forrás) nem lehet NULL";

'target' (cél) nem lehet NULL";

int sourceLength

source. length();

int targetLength

target.length();

int[][] grid =new int[sourceLength + l][targetLength +l]; grid[O][O] =O; for (int row = l; row
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF