Simon Harris - James Ross
Kezdőkönyv az
algoritmusokról
Simon Harris - James Ross
Kezdőkenyv az
algoritmusokról
Kezdőkönyv az algoritmusokról
Eeginning algorithms, Simon Harris-James Ross
Copyright © 2005 by Wiley Publishing, Inc., Indianapolis, Indiana All rights reserved. This translation published by license.
Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/ or i ts affiliates, in the United States and other countries, and may not be used without written permission, SQL Server is a trademark of Microsoft Corporation in the United States and/ or other countries. All other trademarks are the property of their respective owners, Wiley Publishing, Inc., is not associated with any product or vendor mentioned in this book. The Wrox Brand trade dress is a trademark of Wiley Publishing, Inc. in the United States and/ or other countries. Used by permission.
Minden jog fenntartva. A fordítás a Wiley Publishing, Inc. engedélyével jelent meg. Védjegyek: Wiley, a Wiley embléma, Wrox, a Wrox embléma, a Programmer to Prog rammer, és a hozzá kapcsolódó arculat a John Wiley & Sons, Inc. és/vagy partnerei véd jegye vagy bejegyzett védjegye az Amerikai Egyesült Államokban és más országokban, és nem használható fel írásbeli engedély nélkül. SQL Server a Microsoft Corporation véd jegye az Egyesült Államokban és más országokban. Minden további védjegy a megfelelő védjegybirtokos tulajdona. A könyvben említett cégekkel és termékekkel sem a Wiley Publishing, Inc., sem pedig a SZAK Kiadó nem áll függőségi viszonyban. A Wrox Brand arculat a Wiley Publishing, Inc. védjegye az Amerikai Egyesült Államokban és más országokban. Felhasználva a Wiley Publishing, Inc. engedélyével.
Magyar fordítás (Hungarian translation) © SZAK Kiadó 2006.
Fordította a SZAK Kiadó fordítócsoportja: Baksáné Varga Erika, Barát Éva, Csapó Ádám, Csomay Dávid, Egenhoffer Norbert, Gyimesi Csaba, Herczeg Géza, Lucza Mónika, Tiber Melinda
Terminológiai előkészítés: Kis Balázs Lektor: dr. Csink László
ISBN 963 9131 89 X
A könyv fordítása a Kilgray Kft. MemoQ (http://www.memoqtrn.com) programjával készült, a szöveg helyességét és az elválasztásokat pedig a MorphoLogic Helyesek nevű programjával ellenőriztük
Minden jog fenntartva. Jelen könyvet, illetve annak részeit a kiadó engedélye nél kül tilos reprodukálni, adatrögzítő rendszerben tárolni, bármilyen formában vagy eszközzel elektronikus úton vagy más módon közölni.
SZAK Kiadó Kft. • Az 1795-ben alapított Magyar Könyvkiadók és Könyvte*sztők Egyesülésének a tagja • 2060 Bicske, Diófa u. 3. • Tel.: 36-22-350-209 • Fax: 36-22-565-311 • www.szak.hu • e-mail: info@szak.hu • Kiadóvezető: Kis Ádám, e-mail: adam.kis@szak.hu Főszerkesztő: Kis Balázs MCSE, MCT, e-mail: balazs.kis@szak.hu
Tartalomjegyzék
Köszönetnyi Ivánitás
Bevezetés
Kinek szól a könyv? Elvárt előismeretek A könyv témája A könyv használata A megközelités alapelveiTörekedjünk az egyszerűségre!
Ne optimalizáljunk előre!
.
Felhasználói interfészek
Tesztelni, tesztelnil
Legyünk alaposak!
Mire van szükség a könyv használatához? A könyvben használt jelölések
Forráskód Hibajegyzék p2p.wrox.com
1.
Az alapok
Az algoritmusok definíciója Az algoritmusok bonyolultsága A nagyO
jelölésKonstans 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ásx i
xiii
xiii xiii xiv xiv XV xvi xvi XV11 xvii xviii xix XX xxi xxi xxii1
1
4
5
7
7
8
9
9
10
11
13
13
17
18
Tartalomjegyzék
2.
Iteráció és rekurzió
19
Számítások végrehajtása
20
Tömbök feldolgozása
22
Iterátorak használata tömbalapú problémák megoldására 23
Rekurzió
42
Rekurzív könyvtárfa-nyomtatási példa 44
A rekurzív algoritmus működése 47
Összefoglalás
48
Gyakorlatok49
3.
Listák
51
A listákról51
A listák tesztelése55
Listák megvalósítása 68 A tömblista 69 Láncolt lista 77 Összefoglalás87
Gyakorlatok87
4.
Várakozási sorok
89
Asorok89
Sarműveletek 90 A sorinterfész 91 AFIFO-sor 92 A FIFO-sor megvalósítása 96 Blokkolósorok97
Példa: telefonos ügyfélszolgálat szimulátora
102
Az alkalmazás futtatása 112 Összefoglalás
114
Gyakorlatok114
5.
Vermek
115
Vermek115
A tesztek118
Megvalósítás121
Példa: az undo/redo parancs megvalósítása
124
Az undo/ red o parancs tesztelése 125
Összefoglalás
134
6.
7.
8.
Alapvető rendezés
A rendezés fontossága Rendezési alapismeretek Az összehasonlítókról Összehasonlító műveletek Az összehasonlító interfész Néhány szabványos összehasonlító A buborékrendezésrőlA ListSorter interfész
Az AbstractListSorter tesztelése A kiválasztásos rendezés alkalmazása A beszúrásos rendezésről
A stabilitásról
Az alapvető rendezési algoritmusok összehasonlítása CallCountingListComparator ListSorterCallCountingTest Az algoritmus-összehasonlításról Összefoglalás Gyakorlatok
Fejlettebb rendezés
A Shell-rendezési algoritmus alapjai A gyorsrendezésről
Az összetett összehasonlítóról és a stabilitásról Az összefésüléses rendezési algoritmusról
Összefésülés
Az összefésüléses rendezési algoritmus
A fejlettebb rendezési algoritmusok összehasonlításáról Összefoglalás
Gyakorlatok
Prioritásos sorok
A prioritásos sorok áttekintése Egyszerű példa prioritásos sorra Prioritásos sorok kezelése
Rendezetlen listás prioritásos sor áttekintése Rendezetlen listás prioritásos sor megvalósítása Halmon alapuló prioritásos sorok működése Prioritásos sorok megvalósításainak összehasonlítása Összefoglalás Gyakorlatok
135
135 136 137137
138
138
143
146
146
151 156 160 161162
163
166
167 168169
169 175 182 186186
187
194 198 198199
199200
203
206
208
210
219 222 223Tartalomjegyzék
9.
Bináris keresés és beszúrás
225
A
bináris keresés működése 225A bináris keresés megközelítései 228
Listabeli kereső 228
Iteratív bináris kereső 236
A listabeli kereső teljesítményének vizsgálata 238
Bináris beszúrás működése 245
Listabeszúró 246 Teljesítmény vizsgálata 250 Összefoglalás 254
10.
Bináris keresőfák
257
A bináris keresőfákról
257 Minimum 258 Maximum 259 A következő csomópont 259 A megelőző csomópont 260 Keresés 260 Beszúrás 262 Törlés 264 Inorder bejárás 266 Preorder bejárás 267 Posztorder bejárás 267 Kiegyensúlyozás 268A
bináris keresőfa tesztelése és megvalósítása 270A bináris keresőfa teljesítményének megállapítása
295Összefoglalás 299 Gyakorlatok 299
11.
Hasitás
301
A
hasítás megértése 301 Munka a hasítással 309 Lineáris vizsgálat 312 Vödrös módszer 319A
teljesítmény megállapítása 324 Összefoglalás 331' Gyakorlatok 331 ..12.
Halmazok
333
A halmazokról
333 Halmazmegvalósítások tesztelése 337 Listahalmaz 344 viiiTartalomjegyzék
Hasítóhalmaz346
Fahalmaz350
Összefoglalás357
Gyakorlatok358
13. Leképezések
359
A leképezésekró1359
Leképezésmegvalósítások vizsgálata364
Listaleképezés373
Hasítóleképezés377
F aleképezés381
Összefoglalás388
Gyakorlatok389
14. Hármas keresőfák
391
Hármas keresőfák391
Szó keresése392
Szó beszúrása396
Prefix keresés398
Mintaillesztés399
A hármas keresőfák gyakorlati alkalmazása
403
Keresztrejtvény megoldását segítő példa
417
Összefoglalás
422
Gyakorlat422
15.
B-fák
423
A B-fákról423
B-fák a gyakorlatban429
Összefoglalás443
Gyakorlatok443
16. Sztri ngkeresés
445
Általános sztringkereső interfész
445
Általános tesztcsomag
447
Letámadásos algoritmus451
A Boyer-Moore-algoritmus454
A tesztek létrehozása456
Az algoritmus megvalósítása457
Sztringillesztő iterátor461
A teljesítmény összehasonlítása462
A teljesítmény mérése463
Az összehasonlítás eredménye467
Összefoglalás468
Tartalomjegyzék
17.
Sztri ngillesztés
A Soundex algoritmus A Levenshtein-szótávolság Összefoglalás18.
Számítógépes geometria
Rövid geometriai ismédésKoordináták és pontok Egyenes szakaszok Háromszögek
Két egyenes szakasz metszéspontjának meghatározása Meredekség
Az y tengely metszése A metszéspont meghatározása
A legközelebbi pontpár meghatározása Összefoglalás Gyakorlatok
19.
Pragmatikus optimalizálás
Az optimalizálás szerepe A profilírozásról A FileSortingHelper példaprogram Profilirozás a hprof modullalProfilírozás a J ava Memory Profiler programmal Az optimalizálásról
Optimalizálás a gyakorlatban Összefoglalás
"A" függelék: Ajánlott irodalom
"B" függelék: Internetes források
"C"
függelék: Bibliográfia
"D"
függelék: A gyakorlatok megoldásai
Tárgymutató
A szerzőkről
x471
471 483 494495
495 495 497 497 498 499 500 501 517 529 529531
531 533 534 538 541 543 544 552553
555
557
559
609
621
Köszönetnyi lvánitás
Simon Harris
Először is hatalmas köszönet illeti J on Eavest, aki biztosította számunkra ezt a 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.
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
mikü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!
Bevezetés
A Kezdókiitryv az algoritmusokróllépésenkénti bevezetőt nyújt a szárrútástechnikai algo ritmusok életszerű használatának világába.
A fejleszták mindennapi munkájuk során algoritmusokkal és adatstruktúrákkal dolgoznak. Az algoritmusok alapos ismerete és annak felismerése, hogy mikor kell alkalmazni őket, nélkülözhetetlen a szoftverek készítése során, hogy azok nemcsak helyesen, hanem megfelelő teljesítménnyel is működjenek.
A könyv célja, hogy a napról napra haladó szaftverfejlesztés során leggyakrab ban előforduló algoritmusokat és adatstruktúrákat bemutassa, ugyanakkor maradjon gyakorlatias, pontos, lényegre törő, és igyekezzen nem eltérni az alapszintű témakö röktől és példáktóL
Kinek szól
a
könyv?
A könyv azoknak szál, akik alkalmazásokat fejlesztenek, vagy éppen fejlesztésbe fognak, és szeretnék megérteni az algoritmusokat és az adatstruktúrákat. A célkö zönség a programozók, fejlesztők, szoftvermérnök-hallgatók, információrendszer hallgatók és informatikushallgatók népes tábora.
A könyv szerzői feltételezik, hogy a számítógépes programozás általános ismere tei a birtokukban vannak, és remélik, hogy a kötet a kód kihagyásával - még ha nagy részt fogalmi szinten is - olvasható és követhető az első oldaltól az utolsóig. Ebből kifolyólag csoportvezetők, építészek és üzleti elemzők is haszonnal forgathatják.
Elvárt előismeretek
Mivel a példaprogramok mindegyike a Java programozási nyelv felhasználásával ké szült, használható Java-tudásra, valamint a szabványos Java-könyvtárak- különösen a j ava. l an g csomag - ismeretére szükség lehet. A tömb ökkel, ciklusokkal és egyéb
programozási technikákkal sem árt tisztában lenni, és természetesen a J ava-osztályok létrehozásának és fordításának mikéntje is lényeges.
Az itt említett előismereteken kívül más követelmény nem szükséges a kötet adatstruktúrákra vagy algoritmusokra vonatkozó ismeretanyagának elsajátításához.
A
könyv témája
A kötetben részletes magyarázatokat, néhány megvalósítást, a mindennapi használat ra vonatkozó példákat és gyakorlatokat találunk, amelyek mindegyikének célja, hogy olyan tudás birtokába jussunk, amellyel új ismereteinket az életben is kamataztami tudjuk. A könyvben található példák ritkán elméleti természetűek. Az egyes fejezetek kódjait különös gonddal válogattuk össze, és azokat a legtöbb esetben akár azonnal is használhatjuk életszerű alkalmazásokban.
Próbáltunk ragaszkodni a legáltalánosabban elfogadott szaftverfejlesztési gya korlatokhoz. Ezek közé tartozik a tervezési minták [GoF - Gang of Four, Design Patterns], a kódolási konvenciók, a minóség-ellenórzések és a teljesen automatizált egységtesztek használata. Remélhetőleg az algoritmusok és az algoritmusok problé mamegoldásban betöltött rendkívül fontos szerepének megértésén kívül megtanul juk, hogy a robusztus, bővíthető és természetesen működó szoftverek építése tiszte letet érdemlő tevékenység.
A Java-nyelvben járatos olvasók felfedezhetnek némi átfedést a könyvben is mertetett osztályok és a j ava. uti l csomag osztályai között. A könyv nem foglalko zik a Java-könyvtárakban található specifikus megvalósításokkal. Ehelyett inkább bepillantást enged abba, miért tartották fontosnak a Java-nyelv tervezői bizonyos al goritmusok és adatstruktúrák megvalósításainak beépítését csakúgy, mint azok mű ködését és használatát is.
A kötet nem a számítógépes programozás alapjait tanítja meg, sem általában, sem a Java-programozás tekintetében. Nem ismerteti a szabványos Java-könyvtárak használatának szabályait sem: nem ez célja. Noha a példaprogramok használják a j ava. l an g osztályait és néhány esetben a j ava. i o csomagokat, az összes többi J ava csomag túlmutat a könyv témáján. Ehelyett az összes szükséges osztályt kézzel épít jük meg, ezáltal tapasztalha�uk az algoritmusok felfedezésének örömét.
Noha az egységtesztelés minden fejezetben kiemeit figyelmet kap, a kötet nem egységtesztdési kézikönyv vagy útmutató. Inkább az egységtesztek kódolásának be mutatásával próbálja meg elsajátíttatni az alapszintű egységtesztelés alapismereteit.
A
könyv használata
A könyvet az elejétól a végéig érdemes elolvasni. Rendezési, keresési és egyéb meg határozott algoritmusok segítségével a kötet az algoritmusok, adatstruktúrák és telje sítménykarakterisztikák alapjain vezeti végig az olvasót. A könyv négy fő részból áll.
• 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.
Bevezetés
Törekedjünk az egyszerűségre!
Milyen gyakran halljuk ezt a megjegyzést:
"Ó,
ez túl bonyolult! Úgysem értené meg." Vagy: "A kódunk túl nehezen tesztelhető." A szoftvermérnökség lényege a bonyolultság kezelése.Ha sikerült a célnak megfelelő rendszert építenünk, de a rendszer ismertetése vagy tesztelése túl bonyolult, akkor a rendszer csak véletlenül működik. Gondolha�uk azt, hogy a megoldást szándékosan valósítottuk meg az adott módon, de a tény, hogy a rendszer működése inkább a valószínűségtől és nem a tiszta determinizmustól függ.
Ha túl összetettnek tűnik, bontsuk le a feladatot kisebb, könnyebben kezelhető részekre. Kezdjük a kisebb problémák megoldásával. Majd a közös kód, a közös megoldások alapján kezdjük el átszervezni és absztrahálni a problémákat. liy módon a nagy rendszerek kisebb feladatok összetett elrendezésévé alakulnak.
Az
"EHMM
-egyszeruen, hogy rnindenki megértse" jelszóhoz ragaszkodva akö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.
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.
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úlsok dolgot tesztelnek Hajlamosak lennénk egyetérteni velük,
de a megértés támogatása érdekében leegyszerűsí�ük a dolgokat, és alkalmanként
úgy gondoltuk, hogy vehe�ük magunknak a bátorságot, és néhány helyzetet össze
vonhatunk egyetlen tesztmetódusban.
A
lényeg, hogy mielótt bármilyen megvalósírási kódot elkészítenénk, először írjuk
meg a teszteket. Ez a megközelítés, a
tejifelésen alapuló programozás (test-driven development,
IDD) az osztályok megállapodására, azaz a közzétett viselkedésre összpontosít, nem a
megvalósításra. Lehetóvé teszi, hogy a teszteseteket majdhogynem a kód követelmé
nyeiként vagy használatának eseteiként kezeljük; a tapasztalatok szerint ez is egyszerű
síti az osztályok terveit. Mint a példákban látni fogjuk, azáltal, hogy az interfészekhez
kódoljuk tesz� einket, gyerekjáték lesz a tesztelésen alapuló programozás.
Legyünk alaposak!
A tesztelés szigorúsága miatt önelégültté válhatunk, és azt hihe�ük, hogy a kódunkat tel
jes alapossággal teszteltük, ezért az hibamentes.
Abaj 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.
Agyakorlat bebizonyította, hogy ezzel a programo
zási móddal hamarabb megtalálha�uk az összes különös programhibát, és nem kell a
Nu ll Po i n terException
kivételre várnunk.
Mielőtt bármilyen objektum állapotáról vagy paraméter típusáról bármit is felté
teleznénk, a kód vizsgálatával ellenőrizzük a feltevést. Ha bármikor azt gondoljuk,
hogy valami sohasem fordulhat elő, ezért nem is kell aggódnunk miatta, végezzünk
kódszintű érvényességvizsgálatot!
Képzeljük el például, hogy az adatbázisban van egy pénzügyi mezó, amelyról
"tudjuk", hogy "soha" nem tartalmaz majd negatív értéket. Ha a vizsgálatokat ki
kapcsoljuk, valamikor, valahogyan egy negatív érték egészen biztosan
megjelenik
a
mezőben. Lehet, hogy napok, hónapok vagy évek telnek el, mire észrevesszük ennek
következményeit. Előfordulhat, hogy a rendszer más részeiben is befolyásolja más
számítások működését. Ha az összeg
-0,01cent 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.
Ha engedélyeztük volna a kódszintű érvényességvizsgálatot, a szoftver teljesen megjósolható módon hibát jelzett volna abban a pillanatban, hogy a baj előállt, és valószínűleg a probléma diagnosztizálásához szükséges összes információ is a ren delkezésünkre állt volna. Ehelyett jóvátehetetlenül megsérültek a rendszer adatai.
Az éles kód vizsgálata lehetővé teszi, hogy a kód hibái megjósolható módon áll janak elő, ami lehetővé teszi a probléma okának és természetének könnyű és gyors azonosítását, és elhanyagolható segédszámítási költségekkel jár. Egyetlen pillanatig se gondoljuk, hogy a vizsgálatok hátrányosan befolyásolják a rendszer teljesítményét. Jó esélyünk van arra, hogy a kód összes vizsgálatához szükséges idő nem összemér hető egy távoli eljáráshívásban vagy adatbázis-lekérdezésben töltött idővel. Ajánlatos az éles kódban bekapcsolt állapotban hagyni a vizsgálatokat.
Mire van szükség a könyv
használatához 7
A felépítés és futtatás nem is lehetne könnyebb. Ha kezdeti előnnyel szetetnénk in dulni, a teljesen működőképes projektet forráskóddal, tesztekkel együtt, valamint az automatizált parancssori verziót letölthetjük a Wrox webhelyéről Oásd a "Forrás kód" cím ű részt).
Ha a "csináld magad" megközelítés hívei vagyunk, szerencsénk van, mert így mi nimalizálha�uk a függőségek számát. Kiindulásként a következőkre van szükségünk:
• Java Development kit QDK) 1.4 vagy újabb verziója, amely tartalmazza a kód fordításához és futtatásához szükséges összes komponenst;
• ]Unit-könyvtár, amely egyetlen jar fájlból áll, és ha egységteszteket szetet nénk fordítani és futtatni, a classpath környezeti változónak tartalmaznia kell a fájlt;
• szövegszerkesztő vagy integrált fejlesztői környezet (Integrated Development
Environment- IDE) a kódoláshoz.
Az első két tétel (a JDK és a ]Unit) ingyenesen letölthető az internetről Oásd a B függeléket). Az utolsó követelmény tekintetében nem szetetnénk vitát kirobbantani, ezért a választást az olvasóra bízzuk. Egészen biztosan van kedvencünk, ragaszkod junk hozzá! Ha nincsen olyan program, amellyel kódolhatnánk, kérdezzük meg bará tainkat, hallgatótársainkat, előadóinkat vagy kollégáinkat. Egészen biztosan szívesen megosztják velünk véleményüket.
Bevezetés
Mivel a Javáról van szó, a példaprogramokat bármely operációs rendszeren le� fordíthatjuk és futtathatjuk. A könyvet Apple Macintosh és Windows alapú számító� gépeken írtuk és fejlesztettük Egyetlen kód sem különösebben processzorintenzív, tehát a szaftverfejlesztéshez használt hardverünk biztosan megfelel majd.
A
könyvben használt jelölések
Annak érdekében, hogy a legtöbb új ismeret birtokába juthassunk, és nyomon tud� juk követni, mi történik, a könyvben az alábbi jelöléseket alkalmaztuk:
Gyakorlófeladat
A Gyakorlófeladat elnevezésű részben érdemes végigcsinálni a feladatot a könyv utasí� tásait követve.
1. A Gyakorlófeladat rendszerint több kódolt lépést tartalmaz.
2. A lépések nem mindig számozottak, néhányuk nagyon rövid, míg mások a nagyobb, végső célhoz vezető, kis lépések sorozatából állnak.
A megvalósitás müködése
Minden Gyakorlófeladat után A megvalósítás működése című részben találjuk a kódblokkok működésének részletes magyarázatát. A könyv témája, az algoritmusok kérdése nem igazán felel meg számozott feladatoksarok elvégzésének, sokkal inkább a gyakorlati példáknak, tehát észre fogjuk venni, hogy a Gyakorlófeladat és A megvalósítás műkodése megfelelően módosult. Az alapelv az, hogy alkalmazzuk a megszerzett tudást.
Az ilyen dobozokban a közvetlenül a
dobozt körülvev6 szövegre
vonatkozófontos információkat találunk, ameJ:yela6J.
nem 82:abad elfeledkeznünk. Az aktuális témára vonatkozó tippek, ö"tletek, trükko·k dőlt betűve� kissé be!Jebb húz va szerepelnek.A szövegben megjelenő betűtípusokkal kapcsolatban:
XX
• A fontos szavakat bevezetésük során kiemeljük.
• A fájlnevek, az URL-ek és a kódok a következőképpen szerepelnek a szö vegben: persistence.properties.
• A kódokat kétféle változatban láthatjuk:
A példaprogramokban azrú"f-ésfo-ntos kódot szürkeháttérrel , emeljük ki.
A szürke háttér nem jelenik meg az aktuális témában kevésbé fontos vagy már korábban bemutatott kód mögött.
Forráskód
A könyv példáinak elvégzésekor mi magunk is begépelhetjük kézzel a kódot, vagy használha�uk a könyvhöz. tartozó forráskódfájlokat is. A könyvben használt példák forráskódja letölthető a h t tp: l lwww. w r ox. com címrőL Ha már ezen a címen járunk, keressük meg a könyvet (a Search doboz vagy az egyik címlista segítségéve!), majd kattintsunk a könyvet részletező oldal Download Code hivatkozására, és töltsük le a könyv összes forráskódját!
Mivel tó'bb, hasonló dmű könyv található az oldalon, keressünk az ISBN-szám segítsé géve�· az eredeti kó'f!Yv ISBN száma: 0-7645-9674-8 (a 200 7 januá1jában bevezetésre
kerülő 4), 13jegyű ISBN-számozás szerint ez a szám 978-0-7645-9674-2 lesi). A kód letöltése után tomöntőeszközünk segítségével csomagoljuk ki a kódot. A másik
lehetőség, ha a Wrox-kód letöltési oldalára, a h t tp: l lwww. w ro x. comldynami clbooksl download. aspx címre lépünk, és megkeressük a könyv és más Wrox könyvek kód jait .
.
Hibajegyzék
Mindent elkövettünk annak érdekében, hogy a könyv szövege és a kódok ne tartal mazzanak hibákat. De senki sem tökéletes, és hibák előfordulhatnak. A könyvben talált hibákkal �apcsolatos visszajelzésekért hálásak vagyunk. Hibajegyzékek bekül désével egy másik olvasó számára megtakaríthatunk többórányi bosszankodást, ugyanakkor segíthetünk, hogy a könyv még jobb információkat biztosítson.
A könyv hibajegyzékoldalát a h t tp: l lwww. w ro x. com címen találj uk, ha a Search doboz vagy az egyik címlista segítségével megkeressük a könyvet. A könyvet részle tező oldalon kattintsunk a Book Errata hivatkozásral Az oldalon megtaláljuk a
könyvvel kapcsolatban már bejelentett hibákat, amelyeket a Wrox szerkesztői küld tek el. A teljes könyvlista, amely az egyes könyvek hibajegyzékeit tartalmazza, a www . w ro x. com/mi sc-pages/bookl i st. shtml címen található.
Ha nem találjuk "saját'' hibánkat a hibajegyzékoldalon, a www. w r ox. com/ contact/ techsupport. shtml oldalon töltsük ki az űrlapot, és küldjük el a felfedezett hiba leírá sát. A Wrox szerkesztői ellenőrzik az információkat, és ha szükséges, a könyv hibajegy zékében üzenet jelenik meg a hibáról, a könyv következő kiadásaiban pedig kijavi�uk.
p2p.wrox.com
A szerzőkkel és az olvasókkal a p 2 p. wrox. com címen a P2P vitafórumokhoz csatlakozva beszélgethetünk A fórum olyan webes rendszer, amelyben a Wrox-könyvekre és azok kal kapcsólatos technológiákra vonatkozó üzeneteket küldhetünk; és a többi olvasóval, illetve a technológia felhasználójával folytathatunk beszélgetéseket. A fórumok előfize tési funkciót biztosítanak, amelynek segítségével a számunkra érdekes témakörökhöz va ló új hozzászólás érkezésekor e-mailben értesítést kapunk. A Wrox szerzői, szerkesztői, számítástechnikai szakértői és olvasói küldenek üzeneteket ezekbe a fórumokba.
A http: l l p 2 p. w r ox. com címen több fórumot is találunk, amelyek nemcsak a könyv olvasását, de saját alkalmazásaink fejlesztését is segítik. Ha szetetnénk csatla kozni a fórumokhoz, kövessük az alábbi lépéseket:
1. Lépjünk a p2p. wrox. com címre, és kattintsunk a Register hivatkozásral 2. Olvassuk el a felhasználás feltételeit, majd kattintsunk az Agree gombral 3. Töltsük ki a csatlakozáshoz szükséges és az egyéb információkat, amelyeket
szetetnénk megadni, majd kattiusunk a Submit gombral
4. Ezután e-mailben kapunk értesítést arról, hogyan tudjuk ellenőrizni a fió kunkat, és befejezni a csatlakozási folyamatot.
_/
A P2P-hez való csatlakozás nélkül is olvashatjuk a fórum üzeneteit, de ha saját üze netet szetetnénk küldeni, akkor csatlakoznunk kell.
Ha csatlakoztunk, új üzeneteket küldhetünk, illetve válaszolhatunk más felhasz nálók üzeneteire. Az üzeneteket bármikor elolvasha�uk az interneten. Ha adott fó rum új üzeneteit szetetnénk e-maiben megkapni, a fórumok listájában kattintsunk a fórum neve mellett a Subscribe to this Forum ikonra.
A Wrox P2P használatáról további információkat a P2P gyakran ismétlődő kér dések listáiban találunk, ahol a fórumszerver működésére, a P2P-re és a Wrox köny vekre vonatkozó kérdéseinkre is választ kaphatunk. A gyakran ismétlődő kérdéseket a GYIK-hivatkozásta kattintva bármely P2P oldalon elolvashatjuk.
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
tekinthe�ük. Például az 5 X 2 kifejezés a 2 + 2 + 2 + 2 + 2 (illetve az 5 +
5)
kifeje zés gyorsírásos változata. Vagyis bármely két egész szám, például A és B esetén el mondhatjuk, hogy az A X B annyit jelent, hogy B-t A-szor önmagához adjuk. Ezt az alábbi lépések soraként fejezhetjük ki:1. Inicializáljunk egy harmadik egész számot, C-t O-ra.
2. Ha A nulla, akkor készen vagyunk, és az eredményt C tartalmazza. Ha nem ez a helyzet, haladjunk tovább a 3. lépéshez.
3. Adjuk B értékét C-hez.
4. Csökkentsük A értékét. 5. Lépjünk a 2. lépéshez.
Vegyük észre, hogy a torta receptjével ellentétben az összeadással szorzó algoritmus az 5. lépésben visszakanyarodik a 2. lépéshez. A legtöbb algoritmusban felfedezhe tünk valarnilyen ciklikusságot, amelynek a segítségével számításokat vagy egyéb mű veleteket ismétlünk Az iteráció! és a rekuqjót- a ciklusok két fő típusát - a követke ző fejezet részletesen ismerteti.
Az algoritmusokat gyakran pszeudokódként emlegetjük, amely még nem prog ramozó személyek számára is könnyen érthető, kitalált programozási nyelv. Az aláb bi kód a Mul ti pl y függvényt mutatja be, amely két egész szám- A és B- szorzatát, A X B-t adja vissza, és csak összeadást használ. A pszeudokód a két egész szám ösz szeadással való szorzásának műveletét muta�a be:
Function Multiply(Integer A, Integer B) Integer c = O
While A is greater than O C = C + B
A = A - l End
Return c End
A szorzás az algoritmusok nagyon egyszerű példája. A legtöbb alkalmazásban ennél jóval bonyolultabb algoritmusokkal találkozhatunk. A bonyolult algoritmusok megér tése nehezebb, és ezért azok nagyobb valószínűséggel tartalmaznak hibákat. (A számí tógép-tudomány nagy része tulajdonképpen azt próbálja igazolni, hogy bizonyos algo ritmusok helyesen működnek.)
Nem minden helyzetben alkalmazhatunk algoritmusokat. Előfordulhat, hogy adott problémát több algoritmussal is megoldhatunk Néhány megoldás egyszerű, mások bonyolultabbak, és egyik megoldás hatékonyabb lehet a többinéL Nem min-2
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 Aszámítógép-programozás múvészete
(írta Donald E. Knuth). A könyvben ismerte tett algoritmusok nagy részedeterminis'{!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 aheurisztikus
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.
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 algoritmus 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
1000rekordot
30milliszekundum alatt dolgoz fel, rníg egy másik algoritmus
40milliszekundum 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
80milliszekundum 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.
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.
Az 1.1. ábrán látjuk, hogy a különböző bonyolultság-nagyságrendek hogyan vi szonyulnak egymáshoz. A vízszintes tengely a probléma méretét jelenti - például a keresési algoritmussal feldolgozandó rekordok számát. A függőleges . tengely az egyes osztályok algoritmusainak számitásigényét jelenti. Az ábra nem jelzi a futásidőt vagy a szükséges processzorciklusokat; pusztán annyit mutat, hogy a számitógépes erőforrásigény a megoldani kívánt probléma méretével együtt nő.
1. 1. ábra. A bof!Yolultság küliinbiizó nagyságremijeinek ilsszehasonlítás a
Az előző listában talán feltűnt, hogy a nagyságrendek egyike sem tartalmaz kons tanst. Azaz, ha az algoritmus várt futásidejű teljesítménye az N, 2xN, 3xN vagy akár lOOxN értékekkel arányban áll, a bonyolultság minden esetben o (N). Első pillantásra kicsit furcsának tűnhet- természetes, hogy a 2xN jobb, mint a lOOxN - , de mint már korábban említettük, nem az a célunk, hogy megállapítsuk a műveletek pontos szá mát, hanem hogy a különböző algoritmusok relatív hatékonyságát összehasonlítsuk Más szóval az O(N) idő alatt befejezett algoritmus túlszárnyal egy másik, O(N2) ideig futó algoritmust. Továbbá, ha N nagy értékeivel akad dolgunk, a kanstansok nem sokat változtatnak a helyzeten: a teljes méret arányát tekintve az 1 OOO OOO OOO és a 20 OOO OOO OOO közötti különbség majdnem elhanyagolható, még akkor is, ha az egyik a másiknak a hússzorosa.
Természetesen szeretnénk összehasonlítani a különböző algoritmusok tényleges teljesítményét, különösen akkor, ha az egyik 20 perc alatt befejeződik, mig a másik csak 3 óra alatt, és mindkét algoritmus nagyságrendje O(N). Azt kell megjegyezünk, hogy sokkal könnyebb megfelezill egy O(N) bonyolultságú algoritmus idejét, mint módosítani egy olyan algoritmust, amely az O(N) nagyságrendhez képest O(N2) nagy ságrenddel bír.
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úlszé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 = 20perc kell tíz vásárló kiszolgálásához, és
2x40 = 80perc
40vásárlóhoz. A lényeg, hogy nem fontos, hány vásárló
álla 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
Ojelölés mindig minden konstans t figyelmen kívül hagy.
Az O(N)
futási idejű algoritmusok rendszerint elfogadhatóak Legalább olyan ha
tékonynak tekinthetők, mint az
0(1)futásidejű algoritmusok, de ahogy már említet
tük, igen nehéz konstans idejű algoritmust találni. Ha sikerül lineáris idővel futó al
goritmust találnunk, mint azt a "Sztringkeresés" című fejezetben
(16.)
látni fogjuk,
kis elemzéssel- és zseniális ötletekkel- még hatékonyabbá tehetjük.
Kvadratikus idő:
O(N2)
Képzeljünk el egy csoportot, amelynek tagjai most találkoznak egymással először, és az illemszabályoknak megfelelően mindannyian kézfogással üdvözlik a csoport ösz szes többi tagját. Ha a csoportban hatan vannak, akkor ez az
1.2.
ábrán látható mó don összesen 5+4+3+2+1 = 15 kézfogást jelent.1.2. ábra. A
csoport minden tagja iidvo'ifi a csoport osszes to'bbi tagját
Mi történne, ha a csoport hét főből állna? Az üdvözlés összesen 6+5+4+3+2+1 = 21 kézfogásba kerülne. És ha nyolcból? Ez 7+6+ ... +2+1 = 28 kézfogást jelentene. És, ha kilencen lennének a csoportban? Már nagyjából láthatjuk a lényeget: Ahányszor a csoport mérete egy fővel növekszik, egy további embernek kell kezet ráznia az ösz szes többivel.
Az N méretű csoportban a kézfogások száma (NLN) /2 lesz. Mivel a nagy O minden konstanst figyelmen kívül hagy - ebben az esetben a 2-t -, a kézfogások száma NLN. Mint azt az
1.1.
táblázatban látjuk, ahogy N egyre nő, N kivonása NLből a végeredményre egyre elhanyagolhatóbb hatással van, tehát bátran elhagyhatjuk a ki vonást, ezáltal az algoritmus bonyolultsága O(N2) lesz.Különbség
1
1
o100,00%
10
100
90
10,00%
100
10
OOO9 900
1,00%
Í
N N 2 N2-NKutönbség
1 OOO 1 OOO OOO 999 OOO 0,10%
10 OOO 100 OOO OOO 99 990 OOO 0,01%
1.1. táblázat. Az N kivonása N-ból az N növekedése me/lett
A kvadratikus idővel futó algoritmusok a programozók legvadabb rémálmaiban je lennek meg; bármely O(N2) bonyolultságú algoritmus kizárólag a legjelentéktelenebb problémák megoldására alkalmas. A keresést tárgyaló 6. és 7. fejezetben további ér dekes példákat találunk.
Logaritmikus idő: O(log N) és O(N log N)
Az 1.1. ábrán látható, hogy az O(l og N) jobb, mint az O(N), de nem olyan jó, mint az 0(1).
A logaritmikus algoritmus futásideje a probléma méretének - rendszerint 2-es alapú - logaritmusával együtt növekszik. Ez annyit jelent, hogy ha a beviteli adat halmaz mérete milliós szorzóval növekszik, a futásidő csak log (1000000) = 20 szorzóval növekszik. Az egész számok 2-es alapú logaritmusát egyszerűen kiszárnit hatjuk, ha megkeressük, hogy a szám tárolásához hány bináris számjegy szükséges. Például, a 300 2-es ala pú logari tm usa 9, mivel a decimális 300 megj elemtéséhez 9 bi náris számjegy szükséges (a bináris ábrázolás 100101100).
A logaritmikus futásidők megvalósításához az algoritmusnak a beviteli adathalmaz · nagy részét rendszerint figyelmen kívül kell hagynia. Ennek eredményeként a legtöbb algoritmus - amely így viselkedik - valamilyen keresést foglal magában. A "Bináris ke resés és beszúrás" cím ű fejezet (9.), és a "Bináris keresőfák" című fejezet (10.) o(l og N) algoritmusokat mutat be.
Ha ismét megtekin�ük az 1.1. ábrát, látha�uk, hogy az O(N log N) jobb, mint az O(N2), de nem olyan jó, mint az O(N). A 6. és a 7. fejezetben O(N log N) algorit musokkal találkozhatunk.
Faktoriális idő: O(N!)
Lehet, hogy nem gondolnánk, de néhány algoritmus még az O(N2)-nél is rosszabbul tel jesít-az 1.1. ábrán hasonlítsuk össze az O(N2) és O(N!) bonyolultságokat. (Valójában
Elég ritkán találkozhatunk ilyen funkciókkal, különösen, ha olyan példákat kere sünk, amelyek nem a kódolásról szólnak. Ha tehát elfelejtettük volna, hogy mi a fak toriilis - vagy még nem is találkoztunk vele soha -, íme egy gyors ismédés:
A faktoriilis az egész szám és az azt megelőző természetes számok szorzata. Például a 6! ("6 faktoriális'') = 6x5x4x3x2xl = 720 és a 10! = 10x9x8x7x6x5x 4x3x2xl= 3628800.
Az 1.2. táblázat az N2 és az N ! összehasonlítását tartalmazza 1 és 1 O közötti egész számokra. N N z Nl 1 1 1 2 4 2 3 9 6 4 16 24 5 25 120 6 36 720 7 49 5 040 8 64 40 320 9 81 362 880 10 100 3 628 800
1.2. táblázat. Az N2 és az N! összehasonlítása kis egész számok esetén
Mint lá�uk, ha az N értéke N=2 vagy annál kisebb, a faktoriális bonyolultság jobb, mint a kvadratikus, de ezen a ponton a faktoriilis bonyolultság nekilendül, és katasztrófával fenyeget. Következésképpen az O(N2) bonyolultsághoz képest még inkább remény kednünk kell abban, hogy algoritmusunk bonyolultsága ne O(N!) legyen.
Egységtesztelés
Mielőtt folytatnánk utazásunkat az algoritmusok világában, kicsit elkalandozva beszél nünk kell egy szívünknek igen kedves témáról: az egységtesztelésrőL A2 elmúlt évek ben az egységtesztelés nagyon népszerűvé vált azoknak a fejlesztáknek a körében, akiknek fontos az általuk készített rendszerek minősége. Közülük sokan kellemedenill érzik magukat, ha a szoftver készítése közben nem építenek egy automatikus teszteket
magában foglaló csomagot is, amely bizonyítja, hogy az elkészült szoftver
azelvárá
soknak megfelelően műköclik.
Miis 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 dolo
g
, 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