Gregorics Tibor
PROGRAMOZÁS 1. kötet
TERVEZÉS
egyetemi jegyzet
2011
1
ELŐSZÓ
TARTALOM ELŐSZÓ ...................................................................................................................... 4 BEVEZETÉS .............................................................................................................. 6 I. RÉSZ PROGRAMOZÁSI FOGALMAK ............................................................. 9 1. ALAPFOGALMAK................................................................................................. 10 1.1. Az adatok típusa ......................................................................................... 10 1.2. Állapottér ................................................................................................... 14 1.3. Feladat fogalma ......................................................................................... 15 1.4. Program fogalma ....................................................................................... 17 1.5. Megoldás fogalma ...................................................................................... 21 1.6. Feladatok ................................................................................................... 25 2. SPECIFIKÁCIÓ...................................................................................................... 28 2.1. Kezdőállapotok osztályozása...................................................................... 28 2.2. Feladatok specifikációja ............................................................................ 31 2.3. Feladatok ................................................................................................... 37 3. STRUKTURÁLT PROGRAMOK ............................................................................... 38 3.1. Elemi programok ........................................................................................ 40 3.2. Programszerkezetek ................................................................................... 43 3.3. Helyes program, megengedett program ..................................................... 55 3.4. Feladatok ................................................................................................... 58 II. RÉSZ PROGRAMTERVEZÉS MINTÁK ALAPJÁN .................................... 61 4. PROGRAMOZÁSI TÉTELEK ................................................................................... 63 4.1. Analóg programozás .................................................................................. 64 4.2. Programozási tétel fogalma ....................................................................... 69 4.3. Nevezetes minták ........................................................................................ 72 4.4. Feladatok ................................................................................................... 84 5. VISSZAVEZETÉS .................................................................................................. 85 5.1. Természetes visszavezetés .......................................................................... 85 5.2. Általános visszavezetés ............................................................................... 86 5.3. Alteres visszavezetés ................................................................................... 87 5.4. Paraméteres visszavezetés.......................................................................... 91 5.5. Visszavezetési trükkök ................................................................................ 94 5.6. Rekurzív függvény kiszámítása ................................................................... 99 5.7. Feladatok ................................................................................................. 105 6. TÖBBSZÖRÖS VISSZAVEZETÉS ........................................................................... 106 6.1. Alprogram ................................................................................................ 108 6.2. Beágyazott visszavezetés .......................................................................... 111 6.3. Program-átalakítások .............................................................................. 129 6.4. Rekurzív függvény kibontása .................................................................... 136 6.5. Feladatok ................................................................................................. 147 III. RÉSZ TÍPUSKÖZPONTÚ PROGRAMTERVEZÉS................................... 149
2
ELŐSZÓ
7. TÍPUS ................................................................................................................ 151 7.1. A típus fogalma ........................................................................................ 152 7.2. Típus-specifikációt megvalósító típus ...................................................... 157 7.3. Absztrakt típus .......................................................................................... 162 7.4. Típusok közötti kapcsolatok ..................................................................... 171 7.5. Feladatok ................................................................................................. 180 8. PROGRAMOZÁSI TÉTELEK FELSOROLÓ OBJEKTUMOKRA ................................... 181 8.1. Gyűjtemények ........................................................................................... 183 8.2. Felsoroló típus specifikációja .................................................................. 185 8.3. Nevezetes felsorolók ................................................................................. 188 8.4. Programozási tételek általánosítása ........................................................ 194 8.5. Visszavezetés nevezetes felsorolókkal ...................................................... 204 8.6. Feladatok ................................................................................................. 216 9. VISSZAVEZETÉS EGYEDI FELSOROLÓKKAL ....................................................... 217 9.1. Egyedi felsoroló ....................................................................................... 218 9.2. Feltétel fennállásáig tartó felsorolás ....................................................... 219 9.3. Csoportok felsorolása .............................................................................. 222 9.4. Összefuttatás ............................................................................................ 234 9.5. Rekurzív függvény feldolgozása ............................................................... 248 9.6. Felsoroló gyűjtemény nélkül .................................................................... 254 9.7. Feladatok ................................................................................................. 257 10. FELADATOK MEGOLDÁSA ............................................................................... 260 IRODALOM JEGYZÉK ....................................................................................... 336
3
ELŐSZÓ Ez a könyv az Eötvös Loránd Tudományegyetem programtervező informatikus szakának azon tantárgyaihoz ajánlom, amelyeken a hallgatók az első benyomásaikat szerezhetik meg a programozás szakmájáról. A könyvre erősen rányomja bélyegét az a gondolkodásmód, amely C. A. R. Hoare és E. W. Dijkstra munkáira épülve alakult és csiszolódott ki az informatika oktatásának során az ELTE Informatika Karán (illetve annak jogelődjén). A nyolcvanas évek eleje óta tartó kutató és oktató munkában részt vevő hazai alkotócsapatból magasan kiemelkedik Fóthi Ákos, aki úttörő és vezető szerepet játszott és játszik egy sajátos programozási szemléletmód kialakításában, aki a szó klasszikus értelmében iskolát teremtett maga körül. Ennek a szemléletmódnak, programozási módszertannak sok eleme tudományos közleményekben is megjelent már, ugyanakkor igen számottevő – tudományos folyóiratokban nem közölhető – eredmény gyűlt össze a mindennapos oktatás során használt példatárak, segédanyagok, didaktikai elemek (bevezető példák, segédtételek, demonstrációs feladatok) formájában. Hangsúlyozom, hogy az alapgondolat és számos ötlet Fóthi Ákostól származik, de sok részletet mások dolgoztak ki. Hadd említsem meg ezek közül Kozics Sándort, aki akárcsak Fóthi Ákos, tanárom volt. Ma már igen nehéz azt pontosan meghatározni, hogy egy-egy részlet kitől származik: egyrészt azért, mert bizonyos ötletek megszületése és kidolgozása nem mindig ugyanahhoz a személyhez fűződnek, másrészt pedig a szakfolyóiratokban nem közölhető elemek szerzőinek neve sehol nincs dokumentálva. A legteljesebb névsort azokról, akik ennek a csapatmunkának részvevői voltak, valószínűleg a „Workgroup on Relation Models of Programming: Some Concepts of a Relational Model of Programming,Proceedings of the Fourth Symposium on Programming Languages and Software Tools, Ed. prof. Varga, L., Visegrád, Hungary, June 9-10, 1995. 434-44.” publikációban találjuk. Ez a könyv, amely a programozáshoz kapcsolódó munkáim első kötete, a Tervezés címet viseli, és az ELTE IK programtervező informatikus szak Programozás tárgyának azon előadásaihoz és gyakorlataihoz szolgál jegyzetként, ahol a programtervezésről esik szó.
4
ELŐSZÓ
(A második kötetben majd a program futtatható változatának előállításáról, tehát az implementálásról és a tesztelésről lesz szó.) Mivel az említett tantárgynak nem célja a tervezésnél használt módszertan elméleti hátterének bemutatása (ezt egy másik tantárgy teszi meg), ezért ez a kötet nem is tér ki erre részleteiben, csak nagy vonalakban említi meg a legfontosabb fogalmakat. A kötet elsősorban a visszavezetés technikájának gyakorlati alkalmazására fókuszál, arra, amikor algoritmus minták, úgynevezett programozási tételek segítségével oldunk meg feladatokat. A megoldás egy absztrakt program formájában születik meg, amelyet aztán tetszés szerinti programozási környezetben lehet megvalósítani. A könyv didaktikájának összeállításakor elsősorban saját előadásaimra és gyakorlataimra támaszkodtam, de felhasználtam Fóthi Ákossal folytatott beszélgetéseknek tapasztalatait is. A didaktika, a bevezetett jelölések kialakításában sokat segítettek azok a kollégák is, akikkel közösen tartjuk a fent említett tárgy gyakorlatait. Külön köszönettel tartozom közöttük Szabóné Nacsa Rozáliának, Sike Sándornak, Veszprémi Annának és Nagy Sárának. Köszönet jár azoknak a hallgatóknak is, akik a könyv kéziratához helyesbítő észrevételeket tettek, köztük különösen Máté Gergelynek. A jegyzet tananyagának kialakítása az Európai Unió támogatásával, az Európai Szociális Alap társfinanszírozásával valósult meg (a támogatás száma TÁMOP 4.2.1./B-09/1/KMR-2010-0003). A jegyzet megjelentetését az ELTE IK támogatta.
5
BEVEZETÉS „A programozás az a mérnöki tevékenység, amikor egy feladat megoldására programot készítünk, amelyet azután számítógépen hajtunk végre.” E látszólag egyszerű meghatározásnak a hátterében több figyelemre méltó gondolat rejtőzik. Az egyik az, hogy a programkészítés célja egy feladat megoldása, ezért a programozás során mindig a kitűzött feladatot kell szem előtt tartanunk; abból kell kiindulnunk, azt kell elemeznünk, részleteiben megismernünk. A másik gondolat az, hogy a program fogalma nem kötődik olyan szorosan a számítógéphez, mint azt hinnénk. A programozás valójában egymástól jól elkülöníthető – bár a gyakorlatban összefonódó – résztevékenységekből áll: először kitaláljuk, megtervezzük a programot (ez az igazán nehéz lépés), aztán átfogalmazzuk, azaz kódoljuk azt egy számítógép számára érthető programozási nyelvre, végül kipróbáljuk, teszteljük. Nyilvánvaló, hogy az első a lépésben, a programtervezésben szinte kizárólag a megoldandó feladatra kell összpontosítanunk, és nem kell, nem is szabad törődnünk azzal, hogy a program milyen programozási környezetbe ágyazódik majd be. Nem veszhetünk el az olyan kérdések részletezésében, hogy mi is az a számítógép, hogyan működik, mi az operációs rendszer, mi egy programozási nyelv, hogyan lehet egy programot valamely számítógép számára érthetővé, végrehajthatóvá tenni. A program lényegi része ugyanis – nevezetesen, hogy a feladatot megoldja – nem függhet a programozási környezettől. A programtervezést egy olyan folyamatnak tekintjük, amely során a feladatot újabb és újabb, egyre részletesebb formában fogalmazzuk meg, miáltal egyre közelebb kerülünk ahhoz a számítógépes környezethez, amelyben majd a feladatot megoldó program működik. Ehhez azt vizsgáljuk, hogyan kell egy feladatot felbontani részekre, majd a részeket továbbfinomítani egészen addig, amíg olyan egyszerű részfeladatokhoz jutunk, amelyekre már könnyen adhatók azokat megoldó programok. Ennek a könyvnek a gyakorlati jelentősége ezeknek a finomító (más szóval részletező, részekre bontó, idegen szóval dekomponáló) technikáknak a bemutatásában rejlik. A programtervezésnek és a programozási környezetnek a szétválasztása nemcsak a programozás tanulása szempontjából hasznos,
6
BEVEZETÉS
hanem a programozói mesterségnek is elengedhetetlen feltétele. Aki a programozást egy konkrét programozási környezettel összefüggésben sajátítja el, az a megszerzett ismereteit mind térben, mind időben erősen korlátozottan tudja csak alkalmazni. Az állandóan változó technikai környezetben ugyanis pontosan kell látni, melyik ismeret az, amit változtatás nélkül tovább használhatunk, és melyik az, amelyet időről időre „le kell cserélnünk”. A programtervezésnek az alapelvei, módszerei, egyszóval a gondolkodásmód nem változik olyan gyorsan, mint a programozási környezet, amelyben dolgoznunk kell. A bevezető mondathoz kapcsolódó harmadik gondolat a mérnöki jelzőben rejlik. E szerint a programkészítést egy világosan rögzített technológia mentén, szabványok alapján kell végezni. A legösszetettebb feladat is megfelelő elemzés során felbontható olyan részekre, amelyek megoldására már rendelkezünk korábbi mintákkal. Ezek olyan részmegoldások, amelyek helyessége, hatékonysága már bizonyított, használatuk ezért biztonságos. Kézenfekvő, hogy egy új feladat megoldásához ezeket a korábban már bevált mintákat felhasználjuk, ahelyett, hogy mindent teljesen elejéről kezdjünk el kidolgozni. Ha megfelelő technológiát alkalmazunk a minták újrafelhasználásánál, akkor a biztonságos mintákból garantáltan biztonságos megoldást tudunk építeni. Ebben a kötetben a programtervezést állítottam középpontba: a feladatok specifikációjából kiindulva azokat megoldó absztrakt programokat készítünk. Célom egy olyan programozási módszertan bemutatása, amelyik feladat orientált, elválasztja a program tervezését a terv megvalósításától, és a tervezés során programozási mintákat követ. A programozási mintáknak egy speciális családjával foglalkozom csak, mégpedig algoritmus mintákkal vagy más néven a programozási tételekkel. Számos példa segítségével mutatom be ezek helyes használatát, amely szavatolja az előállított programnak, mint terméknek a minőségét. Ez a kötet három részre tagolódik, amelyet a kitűzött feladatot megoldásainak gyűjteménye zár le. Az első rész a legfontosabb programozási fogalmakat vezeti be, meghatározza, hogy a feladataink leírásához milyen eszközt, úgynevezett specifikációs jelölést használjunk, bevezeti a strukturált programok fogalmát (elemi programok, programszerkezetek) és egy jelölésrendszert (struktogramm) az absztrakt strukturált programok leírására. A második rész ismerteti a gyakran alkalmazott programozási tételeket, példákat
7
BEVEZETÉS
mutat azok alkalmazására, végül összetett feladatokon mutatja be azt a tervezési folyamatot, ahogyan a megoldást olyan részprogramok összességeként készítjük el, amelyek egy-egy programozási tétel mintájára készültek. A harmadik részben olyan feladatok megoldásra kerül sor, amelyekben összetett típusú adatokat használunk. Definiáljuk a korszerű adattípus fogalmát, mutatunk egy rendszerezési szempontot az összetett típusok csoportosítására, és különös figyelemmel fordulunk az úgynevezett gyűjtemények (tárolók) típusai felé. Általánosítjuk a programozási tételeket felsorolóra, azaz olyan objektumra, amely például egy gyűjtemény elemeit képes bejárni és egymás után felmutatni. Végül a felsorolóra általánosított programozási tételek segítségével összetett feladatokat fogunk megoldani. A könyv – különösen a második és harmadik része – tulajdonképpen egy példatár, számos feladat megoldásának részletes leírását tartalmazza. Az egyes fejezetek végén kitűzött gyakorló feladatok megoldásai a tizedik fejezetben találhatók.
8
I. RÉSZ PROGRAMOZÁSI FOGALMAK Ebben a részben egy programozási modellt mutatunk be, amelynek keretében a programozással kapcsolatos fogalmainkat vezetjük be. Ez valójában egy matematikai modell, de itt kerülni fogjuk a pontos matematikai definíciókat, ugyanis ennek a könyvnek nem célja a programozási modell matematikai aspektusainak részletes ismertetése. Ehelyett sokkal fontosabb meglátni azt, hogy ez a modell egy sajátos és nagyon hasznos gondolati hátteret szolgáltat a kötetben tárgyalt programtervezési módszereknek. Fontos például azt megérteni – és ebben ez a matematikai modell segít –, hogy mit tekintünk megoldandó feladatnak, mire kell egy feladat megfogalmazásában figyelni, hogyan érdemes az észrevételeinket rögzíteni. Tisztázni kell továbbá, hogy mi egy program, hogyan lehet azt általános eszközökkel leírni úgy, hogy ez a leírás ne kötődjön konkrét programozási környezetekhez (tehát absztrakt programot írjunk), de könnyedén lehessen belőle konkrét programokat készíteni. Választ kell kapnunk arra is, mikor lesz helyes egy program, azaz mikor oldja meg a kitűzött feladatot. E kötet későbbi részeiben alkalmazott különféle programtervezési módszereknek ugyanis ebben az értelemben állítanak elő garantáltan helyes programokat. Végül, de nem utolsó sorban, a programozási modell jelölés rendszere alkalmas eszközt nyújt a tervezés dokumentálására. Az 1. fejezet a programozási modell alapfogalmait vezeti be. A 2. fejezet egy a feladatok leírására szolgáló formát, a feladat specifikációját mutatja be. A 3. fejezet a strukturált program fogalmát definiálja, és vázolja, hogy hogyan lehet matematikai korrektséggel igazolni azt, hogy egy strukturált program megold egy feladatot. A fejezet végén fogalmazzuk meg azt az igényünket, hogy egy program ne csak helyes legyen, hanem megengedett is, amely azt garantálja, hogy egy absztrakt programot egyszerűen kódolhassunk egy konkrét programozási környezetben.
9
1. Alapfogalmak Induljunk ki a bevezetés azon megállapításából, amely szerint a programozás egy olyan tevékenység, amikor egy feladat megoldására programot készítünk. Ennek értelmében a programozás három alapfogalomra épül: a feladat, a program és a megoldás fogalmaira. Mi az a feladat, mi az a program, mit jelent az, hogy egy program megold egy feladatot? Ebben a fejezetben erre adjuk meg a választ. 1.1. Az adatok típusa Egy feladat megoldása azzal kezdődik, hogy megismerkedünk azzal az információval, amelyet a probléma megfogalmazása tartalmaz. Nem újdonság, nemcsak a programozási feladatokra érvényes, hogy a megoldás kulcsa a feladat kitűzése során megjelenő információ értelmezésében, rendszerezésében és leírásában rejlik. Feladatokkal mindenki találkozott már. A továbbiakban felvetődő gondolatok szemléltetése kedvéért most egy „iskolai” feladatot tűzünk ki, amely a következőképpen hangzik: „Egy nyolc kilogrammos, zöld szemű sziámi macska sétálgat egy belvárosi bérház negyedik emeleti ablakpárkányán. Az eső zuhog, így az ablakpárkány síkos. A cica megcsúszik, majd a mintegy 12.8 méteres magasságból lezuhant. Szerencsére a talpára esik! Mekkora földet éréskor a (becsapódási) sebessége?” A feladat által közvetített információ első pillantásra igen összetett. Vannak lényeges és lényegtelen elemei; szerepelnek közöttük konkrét értékeket hordozó adatok és az adatok közötti kapcsolatokat leíró összefüggések; van, ami közvetlenül a feladat szövegéből olvasható ki, de van, ami rejtett, és csak a szövegkörnyezetből következtethetünk rá. A „macskás” feladatban lényegtelen adat például a macska szemének színe, hiszen a kérdés megválaszolása, azaz a becsapódási sebesség kiszámolása szempontjából ez nem fontos. Lényeges adat viszont a magasság, ahonnan a cica leesett. A kinematikát jól ismerők számára nyilvánvaló, hogy egy zuhanó test becsapódási sebességének kiszámolásához nincs szükség a test tömegére sem. Feltételezve ugyanis azt, hogy a macskának pontszerű teste a Földön csapódik be, a becsapódás sebességét – a feladatban közvetlenül nem említett, tehát
10
1.1. Az adatok típusa
rejtett – v 2 g h képlet (h a magasság, g a nehézségi gyorsulás) segítségével számolhatjuk ki. Itt a nehézségi gyorsulás is egy rejtett adat, amelynek az értékét állandóként 10 m/s2-nek vehetjük, ennek 20 h -ra módosul. megfelelően a fenti képlet v Abban van némi szabadságunk, hogy a feladat szövegében nem pontosan leírt elemeket hogyan értelmezzük. Például a nehézségi gyorsulást is kezelhetnénk bemeneti adatként ugyanúgy, mint a magasságot. Sőt magát a becsapódási sebesség kiszámításának képletét is be lehetne adatként kérni annak minden paraméterével (pl. nehézségi gyorsulással, súrlódási együtthatóval, stb.) együtt, ha a problémát nemcsak a fent leírt egyszerűsítés (pontszerű test zuhanása légüres térben) feltételezése mellett akarnánk megoldani Ezek és az ehhez hasonló eldöntendő kérdések teszik a feladat megértését már egy ilyen viszonylag egyszerű példánál is bonyolulttá. A feladat elemzését a benne szereplő lényeges adatok összegyűjtésével kezdjük, és az adatokat a tulajdonságaikkal jellemezzük. Egy adatnak igen fontos tulajdonsága az értéke. A példában szereplő magasságnak a kezdeti értéke 12.8 méter. Ez egy úgynevezett bemenő (input) adat, amelynek értékét feltétlenül ismerni kell ahhoz, hogy a feltett kérdésre válaszolhassunk. Kézenfekvő a kitűzött feladatnak egy olyan általánosítása, amikor a bemeneti adathoz nem egy konkrét értéket rendelünk, hanem egy tetszőlegesen rögzített kezdeti értéket. Ehhez fontos tudni azt, hogy milyen értékek jöhetnek egyáltalán szóba; melyek lehetnek a bemeneti adatnak a kezdőértékei. A példánkban szereplő magasság adat egy nem-negatív valós szám. Furcsának tűnhet, de adatnak tekintjük az eredményt is, azaz a feladatban feltett kérdésre adható választ. Ennek konkrét értékét nem ismerjük előre, hiszen éppen ennek az értéknek a meghatározása a célunk. A példában a becsapódási sebesség egy ilyen – az eredményt megjelenítő – úgynevezett eredmény vagy kimeneti (output) adat, amelynek értéke kezdetben definiálatlan (tetszőleges), ezért csak a lehetséges értékeinek halmazát adhatjuk meg. A becsapódási sebesség lehetséges értékei is (nem-negatív) valós számok lehetnek. Az adatok másik jellemző tulajdonsága az, hogy az értékeikkel műveleteket lehet végezni. A példánkban a valós számokhoz kapcsolódó műveletek a szokásos aritmetikai műveletek, a feladat megoldásánál ezek közül a szorzásra és a négyzetgyökvonásra lesz szükségünk. Az adatokról további jellemzőket is össze lehetne gyűjteni
11
1. Alapfogalmak
(mértékegység, irány stb.), de a programozás szempontjából ezek nem lényegesek. Egy adat típusát az adat által felvehető lehetséges értékek halmaza, az úgynevezett típusérték-halmaz, és az ezen értelmezett műveletek, az úgynevezett típusműveletek együttesen határozzák meg, más szóval specifikálják1. Tekintsünk most néhány nevezetes adattípust, amelyeket a továbbiakban gyakran fogunk használni. A típus és annak típusértékhalmazát többnyire ugyanazzal a szimbólummal jelöljük. Ez nem okoz később zavart, hiszen a szövegkörnyezetből mindig kiderül, hogy pontosan mikor melyik jelentésre is gondolunk. Természetes szám típus. Típusérték-halmaza a természetes számokat (pozitív egész számokat és a nullát) tartalmazza, jele: ℕ. (ℕ+ a nullát nem tartalmazó természetes számok halmazát jelöli.) Típusműveletei az összeadás (+), kivonás (–), szorzás (*), az egész osztás (div), az osztási maradékot előállító művelet (mod), esetleg a hatványozás, ezeken kívül megengedett még két természetes szám összehasonlítása ( , , , , , ). Egész szám típus. Típusérték-halmaza (jele: ℤ) az egész – számokat tartalmazza (ℤ+ a pozitív, ℤ a negatív egész számok halmaza), típusműveletei a természetes számoknál bevezetett műveletek. Valós szám típus. Típusérték-halmaza (jele: ℝ) a valós – számokat tartalmazza (ℝ+ a pozitív, ℝ a negatív valós számok halmaza, ℝ0+ a pozitív valós számokat és a nullát tartalmazó halmaz), típusműveletei az összeadás (+), kivonás (–), szorzás (*), osztás (/), a hatványozás, ezeken kívül megengedett két valós szám összehasonlítása ( , , , , , ) is. Logikai típus. Típusérték-halmaza (jele: ) a logikai értékeket tartalmazza, típusműveletei a logikai „és” ( ), logikai „vagy” ( ), a logikai „tagadás” ( ) és logikai „egyenlőség” ( ) illetve „nem-egyenlőség” ( ). 1
A típus korszerű definíciója ennél jóval árnyaltabb, de egyelőre ez a „szűkített” változat is elegendő lesz az alapfogalmak bevezetésénél. A részletesebb meghatározással a 7. fejezetben találkozunk majd.
12
1.1. Az adatok típusa
Karakter típus. Típusérték-halmaza (jele: ) a karaktereket (a nagy és kisbetűk, számjegyek, írásjelek, stb.) tartalmazza, típusműveletei a két karakter összehasonlításai ( , , , , , ). Halmaz típus. Típusértékei olyan véges elemszámú halmazok, amelyek egy meghatározott alaphalmaz részei. E alaphalmaz esetén a jele: 2E. Típusműveletei a szokásos elemi halmazműveletek: halmaz ürességének vizsgálata, elem kiválasztása halmazból, elem kivétele halmazból, elem berakása a halmazba. Sorozat típus. Típusértékei olyan véges hosszúságú sorozatok, amelyek egy meghatározott alaphalmaz elemeiből állnak. E alaphalmaz esetén a jele: E*. Típusműveletei: egy sorozat hosszának (elemszámának) lekérdezése (|s|); sorozat adott sorszámú elemére történő hivatkozás (si), azaz egy elem kiolvasása illetve megváltozatása; sorozatba új elem beillesztése; adott elem kitörlése. Vektor típus. (Egydimenziós tömb) Típusértékei olyan véges, azonos hosszúságú sorozatok, más néven vektorok, amelyek egy meghatározott alaphalmaz elemeiből állnak. A sorozatokat m-től n-ig terjedő egész számokkal számozzuk meg, más néven indexeljük. Az ilyen vektorok halmazát E alaphalmaz esetén az Em..n jelöli, m 1 esetén egyszerűen E n . Típusművelete egy adott indexű elemre történő hivatkozás, azaz az elem kiolvasása illetve megváltoztatása. A vektor első és utolsó eleme indexének (az m és az n értékének) lekérdezése is lényegében egy-egy típusművelet. Mátrix típus. (Kétdimenziós tömb) azaz Vektorokat tartalmazó vektor, amely speciális esetben azonos módon indexelt El..m-beli vektoroknak (úgynevezett soroknak) a vektora (El..m)k..n vagy másképp jelölve El..m×k..n, ahol E az elemi értékek halmazát jelöli), és amelyet k=l=1 esetén egyszerűen Em×n-ként is írhatunk. Típusművelete egy adott sor adott elemére (oszlopra) történő hivatkozás, azaz az elem kiolvasása, illetve megváltoztatása. Típusművelet még a mátrix egy teljes sorára történő hivatkozás, valamint a sorokat tartalmazó vektor indextartományának, illetve az egyes sorok indextartományának lekérdezése.
13
1. Alapfogalmak
1.2. Állapottér Amikor egy feladat minden lényeges adata felvesz egy-egy értéket, akkor ezt az érték-együttest a feladat egy állapotának nevezzük. A „macskás” feladat egy állapota például az, amikor a magasság értéke 12.8, a sebességé 16. Vezessünk be két címkét: a h-t a magasság, a v-t a sebesség jelölésére, és ekkor az előző állapotot a rövidített (h:12.8, v:16) formában is írhatjuk. Az itt bevezetett címkékre azért van szükség, hogy meg tudjuk különböztetni egy állapot azonos típusú értékeit. A (12.8, 16) jelölés ugyanis nem lenne egyértelmű: nem tudnánk, hogy melyik érték a magasság, melyik a sebesség. Ugyanennek a feladatnak állapota még például a (h:5, v:10) vagy a (h:0, v:10). Sőt – mivel az adatok között kezdetben semmiféle összefüggést nem tételezünk fel, csak a típusérték-halmazaikat ismerjük – a (h:0, v:10.68)-t és a (h:25, v:0)-t is állapotoknak tekintjük, noha ez utóbbiak a feladat szempontjából értelmetlenek, fizikai értelemben nem megvalósuló állapotok. Az összes lehetséges állapot alkotja az állapotteret. Az állapotteret megadhatjuk úgy, hogy felsoroljuk a komponenseit, azaz az állapottér lényeges adatainak típusérték-halmazait egyedi címkékkel ellátva. Az állapottér címkéit a továbbiakban az állapottér változóinak hívjuk. Mivel a változó egyértelműen azonosítja az állapottér egy komponensét, ezért alkalmas arra is, hogy egy konkrét állapotban megmutassa az állapot adott komponensének értékét. A „macskás” példában az állapottér a (h:ℝ0+, v:ℝ0+). Tekintsük ennek a (h:12.8, v:16) állapotát. Ha megkérdezzük, hogy mi itt a h értéke, akkor világos, hogy ez a 12.8. Ha előírjuk, hogy változzon meg az állapot v komponense 31-re, akkor a (h:12.8, v:31) állapotot kapjuk. Egy állapot komponenseinek értékét tehát a megfelelő változó nevének segítségével kérdezhetjük le vagy változtathatjuk meg. Ez utóbbi esetben egy másik állapotot kapunk. Állapottere nemcsak egy feladatnak lehet. Minden olyan esetben bevezethetjük ezt a fogalmat, ahol adatokkal, pontosabban adattípusokkal van dolgunk, így például egy matematikai kifejezés vagy egy program esetében is.
14
1.3. Feladat fogalma
1.3. Feladat fogalma Amikor a „macskás” feladatot meg akarjuk oldani, akkor a feladatnak egy olyan állapotából indulunk ki, ahol a magasság ismert, például 12.8, a sebesség pedig ismeretlen. Ilyen kezdőállapot több is van az állapottérben, ezeket (h:12.8, v:*)-gal jelölhetjük, ami azt fejezi ki, hogy a második komponens definiálatlan, tehát tetszőleges. A feladatot akkor oldottuk meg, ha megtaláltuk azt a célállapotot, amelyiknek sebességkomponense a 16. Egy fizikus számára a logikus célállapot a (h:0, v:16) lenne, de a feladat megoldásához nem kell a valódi fizikai célállapot megtalálnunk. Az informatikus számára a magasság-komponens egy bemenő adat, amelyre előírhatjuk például, hogy őrizze meg a kezdőértéket a célállapotban. Ebben az esetben a (h:12.8, v:16) tekinthető célállapotnak. Ha ilyen előírás nincs, akkor minden (h: *, v:16) alakú állapot célállapotnak tekinthető. Állapodjunk meg ennél a példánál abban, hogy megőrizzük a magasság-komponens értékét, tehát csak egy célállapotot fogadunk el: (h:12.8, v:16). Más magasság-értékű kezdőállapotokhoz természetesen más célállapot tartozik. (Megjegyezzük, hogy ha az állapottér felírásánál nem zártuk volna ki a negatív valós számokat, akkor most ki kellene kötni, hogy a feladatnak nincs értelme olyan kezdőállapotokon, amelyek magasság-komponense negatív, például (h:-1, v: *), hiszen ekkor a becsapódási sebességet kiszámoló képletben negatív számnak kellene valós négyzetgyökét kiszámolni.). Általánosan leírva az értelmes kezdő- és célállapot kapcsolatokat, a következőt mondhatjuk. A feladat egy (h:h0, v: *) kezdőállapothoz, ahol h0 egy tetszőlegesen rögzített (nem-negatív valós) szám, a * pedig egy tetszőleges nemdefiniált érték, a (h:h0, v: 20 h0 ) célállapotot rendeli. Tekintsünk most egy másik feladatot! „Adjuk meg egy összetett szám egyik valódi osztóját!” Ennek a feladatnak két lényeges adata van: az adott természetes szám (címkéje legyen n) és a válaszként megadandó osztó (címkéje: d). Mindkét adat természetes szám típusú. A feladat állapottere tehát: (n:ℕ, d:ℕ). Rögzítsük az állapottér komponenseinek sorrendjét: megállapodás szerint az állapotok jelölésénél elsőként mindig az n, másodikként a d változó értékét adjuk meg, így a továbbiakban használhatjuk az (n:6, d:1) állapot-jelölés helyett a rövidebb (6,1) alakot. A feladat kezdőállapotai között találjuk
15
1. Alapfogalmak
például a (6,*) alakúakat. Ezekhez több célállapot is tartozik: (6,2) és (6,3), hiszen a 6-nak több valódi osztója is van. Nincs értelme a feladatnak viszont a (3,*) alakú kezdőállapotokban, hiszen a 3 nem összetett szám, azaz nincs valódi osztója. A feladat általában az (x,*) kezdőállapothoz, ahol x egy összetett természetes szám, azokat az (x,k) állapotokat rendeli, ahol k az x egyik valódi osztója. A feladat egy olyan kapcsolat (leképezés), amelyik bizonyos állapotokhoz (kezdőállapotokhoz) állapotokat (célállapotokat) rendel. Ez a leképezés általában nem egyértelmű, más szóval nemdeterminisztikus, hiszen egy kezdőállapothoz több célállapot is tartozhat. (Az ilyen leképezést a matematikában relációnak hívják.) Érdekes tanulságokkal jár a következő feladat: „Adjuk meg egy nem-nulla természetes szám legnagyobb valódi osztóját!” Itt bemeneti adat az a természetes szám, aminek a legnagyobb valódi osztóját keressük. Tudjuk, hogy a kérdésre adott válasz is adatnak számít, de azt már nehéz észrevenni, hogy a válasz itt két részből áll. A feladat megfogalmazásából következik, hogy nemcsak egy összetett szám esetén várunk választ, hanem egy prímszám vagy az 1 esetén is. De ilyenkor nincs értelme legnagyobb valódi osztót keresni! A számszerű válaszon kívül tehát szükségünk van egy logikai értékű adatra, amely megmutatja, hogy van-e egyáltalán számszerű megoldása a problémának. A feladat állapottere ezért: (n:ℕ, l: , d:ℕ). A feladat ebben a megközelítésben bármelyik olyan állapotra értelmezhető, amelyiknek az n komponenshez tartozó értéke nem nulla. Az olyan (x,*,*) állapotokhoz (itt is az n,l,d sorrendre épülő rövid alakot használjuk), ahol x nem összetett szám, a feladat az (x,hamis,*) alakú célállapotokat rendeli; ha x összetett, akkor az (x,igaz,k) célállapotot, ahol k a legnagyobb valódi osztója az x–nek. A feladatok változóit megkülönböztethetjük aszerint, hogy azok a feladat megoldásához felhasználható kezdő értékeket tartalmazzák-e (bemenő- vagy input változók), vagy a feladat megoldásának eredményei (kimenő-, eredmény- vagy output változók). A bemenő változók sokszor megőrzik a kezdőértéküket a célállapotban is, de ez nem alapkövetelmény. A kimenő változók kezdőállapotbeli értéke közömbös, tetszőleges, célállapotbeli értékük megfogalmazása viszont a feladat lényegi része. Számos olyan feladat is van, ahol egy változó egyszerre lehet bemenő és kimenő is, sőt lehetnek olyan (segéd)
16
1.3. Feladat fogalma
változói is, amelyek csak a feltételek könnyebb megfogalmazásához nyújtanak segítséget. 1.4. Program fogalma A szakkönyvek többsége a programot utasítások sorozataként definiálja. Nyilvánvaló azonban, hogy egy utasítássorozat csak a program leírására képes, és önmagában semmit sem árul el a program természetéről. Ehhez ugyanis egyfelől ismerni kellene azt a számítógépet, amely az utasításokat értelmezi, másfelől meg kellene adni, hogy az utasítások végrehajtásakor mi történik, azaz lényegében egy programozási nyelvet kellene definiálnunk. A program fogalmának ilyen bevezetése még akkor is hosszadalmas, ha programjainkat egy absztrakt számítógép számára egy absztrakt programozási nyelven írjuk le. A program fogalmának bevezetésére ezért mi nem ezt az utat követjük, hiszen könyvünk bevezetőjében éppen azt emeltük ki, hogy milyen fontos elválasztani a programtervezési ismereteket a számítógépeknek és a programozási nyelveknek a megismerésétől. A fenti érvek ellenére időzzünk el egy pillanatra annál a meghatározásnál, hogy a program utasítások sorozata, és vegyünk szemügyre egy így megadott programnak a működését! (Ez a példa egy absztrakt gép és absztrakt nyelv ismeretét feltételezi, ám az utasítások megértése itt remélhetőleg nem okoz majd senkinek sem gondot.) 1. Legyen d értéke vagy az n-1 vagy a 2. 2. Ha d értéke megegyezik 2-vel akkor amíg d nem osztja n-t addig növeljük d értékét 1-gyel, különben amíg d nem osztja n-t addig csökkentsük d értékét 1-gyel. A program két természetes szám típusú adattal dolgozik: az elsőnek címkéje n, a másodiké d, azaz a program (adatainak) állapottere az (n:ℕ, d:ℕ). Vizsgáljuk meg, mi történik az állapottérben, ha ezt a programot végrehajtjuk! Mindenekelőtt vegyük észre, hogy a program bármelyik állapotból elindulhat. Például a (6,8) kiinduló állapot esetén (itt is alkalmazzuk a rövidebb, az n,d sorrendjét kihasználó alakot az állapot jelölésére) a
17
1. Alapfogalmak
program első utasítása vagy a (6,2), vagy a (6,5) állapotba vezet el. Az első utasítás ugyanis nem egyértelmű, ettől a program működése nemdeterminisztikus lesz. Természetesen egyszerre csak az egyik végrehajtás következhet be, de hogy melyik, az nem számítható ki előre. Ha az első utasítás a d-t 2-re állítja be, akkor a második utasítás elkezdi növelni azt. Ellenkező esetben, d=5 esetén, csökkenni kezd a d értéke addig, amíg az nem lesz osztója az n-nek. Ezért az egyik esetben – amikor d értéke 2 – rögtön meg is áll a program, a másik esetben pedig érintve a (6,5), (6,4) állapotokat a (6,3) állapotban fog terminálni. Bármelyik olyan kiinduló állapotból elindulva, ahol az első komponens 6, az előzőekhez hasonló végrehajtási sorozatokat kapunk: a program a működése során egy (6,*) alakú állapotból elindulva vagy a (6,*) (6,2) állapotsorozatot, vagy a (6,*) (6,5) (6,4), (6,3) állapotsorozatot futja be. Ezek a sorozatok csak a legelső állapotukban különböznek egymástól, hiszen egy végrehajtási sorozat első állapota mindig a kiinduló állapot. Induljunk el most egy (5,*) alakú állapotból. Ekkor az első utasítás eredményétől függően vagy a (5,*) (5,2) (5,3) (5,4) (5,5) , vagy a (5,*) (5,4) (5,3) (5,2) (5,1) végrehajtás következik be. A (4,*) alakú állapotból is két végrehajtás indulhat, de mindkettő ugyanabban az állapotban áll meg: (4,*) (4,2) , (4,*) (4,3) (4,2) Érdekes végrehajtások indulnak az (1,*) alakú állapotokból. Az első utasítás hatására vagy az (1,2) , vagy az (1,0) állapotba kerülünk. Az első esetben a második utasítás elkezdi a d értékét növelni, és mivel az 1-nek nincs 2-nél nagyobb osztója, ez a folyamat soha nem áll le. Azt mondjuk, hogy a program a (1,*) (1,2) (1,3) (1,4) … végtelen végrehajtást futja be, mert végtelen ciklusba esik. A másik esetben viszont értelmetlenné válik a második utasítás, hiszen azt kell vizsgálni, hogy a nulla értékű d osztja-e az n értékét, de a nullával való osztás nem értelmezhető. Ez az utasítás tehát itt illegális, abnormális lépést eredményez. Ilyenkor a program „abnormálisan” működik tovább, amelyet az (1,*) (1,0) (1,0) … végtelen végrehajtási sorozattal modellezünk. Ez azt fejezi ki, mintha a program megpróbálná újra és újra végrehajtani az illegális lépést, majd mindig visszatér az utolsó legális állapothoz. Hasonló a helyzet a (0,*) kiinduló állapotokkal is, amelyekre az első utasítás megkísérli a d értékét (ennek tetszőleges értékék rögzítsük és jelöljük y-nal) -1-re állítani. A (0,-1) ugyanis nem tartozik bele az állapottérbe, így az első utasítás a program abnormális
18
1.4. Program fogalma
működését eredményezheti, amelyet a (0,y) (0,y) (0,y) … végtelen sorozattal jellemezhetünk. Mintha újra és újra megpróbálnánk a hibás utasítás végrehajtását, de a (0,-1) illegális állapot helyett mindig visszatérünk a (0,y) kiinduló állapothoz.. Természetesen itt is lehetséges egy másik végrehajtás, amikor az első lépésben a (0,2) állapotba kerülünk, ahol a program rögtön le is áll, hiszen a 2 osztja a nullát. A program által egy állapothoz rendelt állapotsorozatot végrehajtásnak, működésnek, a program futásának hívunk. A végrehajtások lehetnek végesek vagy végtelenek; a végtelen működések lehetnek normálisak (végtelen ciklus) vagy abnormálisak (abortálás). Ha a végrehajtás véges, akkor azt mondjuk, hogy a program végrehajtása megáll, azaz terminál. A normális végtelen működést nem lehet biztonsággal felismerni, csak abból lehet sejteni, ha a program már régóta fut. Az abnormális működés akkor következik be, amikor a végrehajtás során egy értelmezhetetlen utasítást (például nullával való osztás) kellene végrehajtani. Ezt a programunkat futtató környezet (operációs rendszer) fel szokta ismerni, és erőszakkal leállítja a működést. Innen származik az abortálás kifejezés. A modellünkben azonban nincs futtató környezet, ezért az abnormális működést olyan végtelen állapotsorozattal ábrázoljuk, amelyik az értelmetlen utasítás végrehajtásakor fennálló állapotot ismétli meg végtelen sokszor. A későbbiekben bennünket egy programnak csak a véges működései fognak érdekelni, és mind a normális, mind az abnormális végtelen végrehajtásokat megpróbáljuk majd elkerülni. Egy program minden állapotból indít végrehajtást, sőt egy állapotból akár több különbözőt is. Az, hogy ezek közül éppen melyik végrehajtás következik be, nem definiált, azaz a program nemdeterminisztikus. Vegyünk szemügyre egy másik programot! Legyen ennek az állapottere az (a:ℕ, b:ℕ, c:ℕ). Az állapotok leírásához rögzítsük a továbbiakban ezt a sorrendet. 1. Vezessünk be egy új változót (i:ℕ) és legyen az értéke 1. 2. Legyen c értéke kezdetben 0. 3. Amíg az i értéke nem haladja meg az a értékét addig adjuk a c értékéhez az b értékét, majd növeljük meg i értékét 1-gyel.
19
1. Alapfogalmak
Ez a program például a (2,3,2009) állapotból indulva az alábbi állapotsorozatot futja be: (2,3,2009), (2,3,2009,1), (2,3,0,1), (2,3,3,1), (2,3,3,2), (2,3,6,2), (2,3,6,3), (2,3,6) . A végrehajtási sorozat első lépése kiegészíti a kiinduló állapotot egy új komponenssel és kezdőértéket is ad neki. Megállapodás szerint az ilyen futás közben felvett új komponensek a program befejeződésekor megszűnnek: ezért jelenik meg a fenti végrehajtási sorozat legvégén egy speciális lépés. A program állapottere tehát a végrehajtás során dinamikusan változik, mert a végrehajtásnak vannak olyan lépései, amikor egy állapotból attól eltérő komponens számú (eltérő dimenziójú) állapotba kerülünk. Az új állapot lehet bővebb, amikor a megelőző állapothoz képest extra komponensek jelennek meg (lásd a fenti programban az i:ℕ megjelenését előidéző lépést), de lehet szűkebb is, amikor elhagyunk korábban létrehozott komponenseket (erre példa a fenti végrehajtás utolsó lépése). Megállapodunk abban, hogy a végrehajtás kezdetén létező komponensek, azaz a kiinduló állapot komponensei végig megmaradnak, nem szűnhetnek meg. A program kiinduló- és végállapotainak közös terét a program alap-állapotterének nevezzük, változói az alapváltozók. A működés során megjelenő újabb komponenseket segédváltozóknak fogjuk hívni. A program tartalmazhat segédváltozót megszüntető speciális lépéseket is, de az utolsó lépés minden segédváltozót automatikusan megszüntet. A segédváltozó megjelenésekor annak értéke még nem feltétlenül definiált. A program az általa befutható összes lehetséges végrehajtás együttese. Rendelkezik egy alap-állapottérrel, amelyiknek bármelyik állapotából el tud indulni, és ahhoz olyan véges vagy végtelen végrehajtási sorozatokat rendel, amelyik első állapota a kiinduló állapot. Ugyanahhoz a kiinduló állapothoz akár több végrehajtási sorozat is tartozhat. Egy végrehajtási sorozat további állapotai az alap-állapottér komponensein kívül segéd komponenseket is tartalmazhatnak, de ezek a véges hosszú végrehajtások esetén legkésőbb az utolsó lépésében megszűnnek, így a véges végrehajtások az alap-állapottérben terminálnak. Egy program működése nem változik meg attól, ha módosítjuk az alap-állapoterét. Egy alapváltozó ugyanis egyszerűen átminősíthető segédváltozóvá (ezt nevezzük az alap-állapottér leszűkítésének), és
20
1.4. Program fogalma
ekkor csak a program elindulásakor jön létre, a program befejeződésekor pedig megszűnik. Fordítva, egy segédváltozóból is lehet alapváltozó (ez az alap-állapottér kiterjesztése), ha azt már eleve létezőnek tekintjük, ezért nem kell létrehozni és nem kell megszüntetni. A program alap-állapotterét megállapodás alapján jelöljük ki. Ebbe nemcsak a program által ténylegesen használt, a program utasításaiban előforduló változók szerepelhetnek, hanem egyéb változók is. Az ilyen változóknak a kezdő értéke tetszőleges, és azt végig megőrzik a program végrehajtása során. Azt is könnyű belátni, hogy az alapállapottér változóinak neve is lecserélhető anélkül, hogy ettől a program működése megváltozna. 1.5. Megoldás fogalma Mivel mind a feladat fogalmát, mind a program fogalmát az állapottér segítségével fogalmaztuk meg, ez lehetővé teszi, hogy egyszerű meghatározást adjunk arra, mikor old meg egy program egy feladatot. Tekintsük újra azt a feladatot, amelyben egy összetett nem-nulla természetes szám egyik valódi osztóját keressük. A feladat állapottere: (n:ℕ, d:ℕ), és egy (x,*) kezdőállapothoz (ahol x pozitív és összetett) azokat az (x,y) célállapotokat rendeli, ahol a y értéke osztja az x-et, de nem 1 és nem x. Az előző alfejezet első programjának állapottere megegyezik e feladatéval. A feladat bármelyik kezdő állapotából is indítjuk el a program mindig terminál, és olyan állapotban áll meg, ahol a d értéke vagy a legkisebb, vagy a legnagyobb valódi osztója az n kezdőértékének. Például a program a (12,8) kiinduló állapotból elindulva nem-determinisztikus módon vagy a (12,2), vagy a (12,6) végállapotban fog megállni, miközben a feladat a (12,8) kezdőállapothoz a (12,2), (12,3), (12,4), (12,6) célállapotokat jelöli ki. A program egy végrehajtása tehát ezek közül talál meg mindig egyet, azaz megoldja a feladatot. Egy program akkor old meg egy feladatot, ha a feladat bármelyik kezdőállapotából elindulva biztosan terminál, és olyan állapotban áll meg, amelyet célállapotként a feladat az adott
21
1. Programozási alapfogalmak
kezdőállapothoz rendel.2 Ahhoz, hogy a program megoldjon egy feladatot, nem szükséges, hogy egy adott kezdőállapothoz tartozó összes célállapotot megtalálja, elég csak az egyiket. A program nem-determinisztikussága azt jelenti, hogy ugyanazon állapotból indulva egyszer ilyen, másszor olyan végrehajtást futhat be. A megoldáshoz az kell, hogy bármelyik végrehajtás is következik be, az megfelelő célállapotban álljon meg. Nem oldja meg tehát a program a feladatot akkor, ha a feladat egy kezdőállapotából indít ugyan egy célállapotba vezető működést, de emellett egy másik, nem célállapotban megálló vagy egyáltalán nem termináló végrehajtást is produkál. A megoldás vizsgálata szempontjából közömbös, hogy azokból az állapotokból indulva, amelyek nem kezdőállapotai a feladatnak, hogyan működik a program. Ilyenkor még az sem számít, hogy megálle egyáltalán a program. Ezért nem baj, hogy a (0,*) és az (1,*) alakú állapotokból indulva a vizsgált program nem is biztos, hogy terminál. Az is közömbös, hogy egy olyan (x,*) állapotból indulva, ahol x egy prímszám, a program vagy az x-et, vagy az 1-et „találja meg”, azaz nem talál valódi osztót. A megoldás tényét az sem befolyásolja, hogy a program a működése közben mit csinál, mely állapotokat érinti, milyen segédváltozókat vezet be; csak az számít, hogy honnan indulva hová érkezik meg, azaz a feladat kezdőállapotaiból indulva megáll-e, és hol. A (4,*) alakú állapotokból két különböző végrehajtás is indul, de mindkettő a (4,2) állapotban áll meg, hiszen a 2 a 4-nek egyszerre a legkisebb és legnagyobb valódi osztója. A (6,*) alakú állapotokból két olyan végrehajtás indul, amelyeknek a végpontja is különbözik, de mindkettő a (6,*)-hoz tartozó célállapot. A programnak azt a tulajdonságát, amely azt mutatja meg, hogy mely állapotokból indulva fog biztosan terminálni, és ezen állapotokból indulva mely állapotokban áll meg, a program hatásának nevezzük. Egy program hatása figyelmen kívül hagyja azokat az állapotokat és az abból induló összes végrehajtást, amely állapotokból végtelen hosszú végrehajtás is indul, a többi végrehajtásnak pedig nem mutatja meg a 2
Ismert a megoldásnak ennél gyengébb meghatározása is, amelyik nem követeli meg a leállást, csak azt, hogy ha a program leáll, akkor egy megfelelő célállapotban tegye azt.
22
1.5. Megoldás fogalma
„belsejét”, csak a végrehajtás kiinduló és végállapotát. Egy program hatása természetesen attól függően változik, hogy mit választunk a program alap-állapotterének. Ekvivalensnek akkor mondunk két programot, ha bármelyik közös alap-állapottéren megegyezik a hatásuk. Ahhoz, hogy egy feladatot megoldjunk egy programmal, a program alapváltozóinak a feladat (bemenő és eredmény) változóit kell választanunk. A program ugyanis csak az alapváltozóin keresztül tud a környezetével kapcsolatot teremteni, csak egy alapváltozónak tudunk „kívülről” kezdőértéket adni, és egy alapváltozóból tudjuk a terminálás után az eredményt lekérdezni. Ezért a megoldás definíciójában feltételeztük, hogy a feladat állapottere megegyezik a program alapállapotterével. A programozási gyakorlatban azonban sokszor előfordul, hogy egy feladatot egy olyan programmal akarunk megoldani, amelyik állapottere nem azonos a feladatéval, bővebb vagy szűkebb annál. A megoldás fogalmát ezekben az esetekben úgy ellenőrizzük, hogy először a program alap-állapotterét a feladatéhoz igazítjuk, vagy kiterjesztjük, vagy leszűkítjük (esetleg egyszerre mindkettőt) a feladat állapotterére. Abban az esetben, amikor a program alap-állapottere bővebb, mint feladaté, azaz van a programnak olyan alapváltozója, amely nem szerepel a feladat állapotterében (ez a változó nem kommunikál a feladattal: nem kap tőle kezdő értéket, nem ad neki eredményt), akkor ezt a változót a program segédváltozójává minősítjük. Jó példa erre az, amikor rendelkezünk egy programmal, amely egy napon keresztül óránként mért hőmérsékleti adatokból ki tudja számolni a napi átlaghőmérsékletet és a legnagyobb hőingadozást, ugyanakkor a megoldandó feladat csak az átlaghőmérséklet kiszámolását kéri. A program alap-állapottere tehát olyan komponenst tartalmaz, amely a feladatban nem szerepel (ez a legnagyobb hőingadozás), ez a feladat szempontjából érdektelen. A program első lépésében hozzáveszi a feladat egy kezdőállapotához a legnagyobb hőingadozást, ezután a program ugyanúgy számol vele, mint eredetileg, de a megállás előtt elhagyja ezt a komponenst, hiszen a feladat célállapotában ennek nincs helye; a legnagyobb hőingadozás tehát segédváltozó lett. A másik esetben – amikor a feladat állapottere bővebb a program alap-állapotterénél – a program állapotterét kiegészítjük a feladat állapotterének azon komponenseivel, amelyek a program állapotterében nem szerepelnek. Ha egy ilyen új komponens segédváltozóként szerepelt az eredeti programban, akkor ez a kiegészítés a segédváltozót
23
1. Programozási alapfogalmak
a program alapváltozójává emeli. Ha az új változó segédváltozóként sem szerepelt eddig a programban, akkor ez a változó a program által nem használt új alapváltozó lesz, azaz kezdeti értéke végig megőrződik egy végrehajtás során. De vajon van-e olyan feladat, amelyet így meg lehet oldani? Talán meglepő, de van. Ilyen az, amikor egy összetett feladat olyan részének a megoldásával foglalkozunk, ahol az összetett feladat bizonyos komponenseit ideiglenesen figyelmen kívül hagyhatjuk, de attól még azok a részfeladat állapotterének komponensei maradnak, és szeretnénk, ha az értékük nem változna meg a részfeladatot megoldó program működése során. Az itt bevezetett megoldás definíció egy logikus, a gyakorlatot jól modellező fogalom. Egyetlen probléma van vele: a gyakorlatban történő alkalmazása szinte lehetetlen. Nehezen képzelhető ugyanis az el, hogy egy konkrét feladat és program ismeretében (miután a program alap-állapotterét a feladat állapotteréhez igazítottuk), egyenként megvizsgáljuk a feladat összes kezdőállapotát, hogy az abból indított program általi végrehajtások megfelelő célállapotban állnak-e meg. Ezért a következő két fejezetben bevezetünk mind a feladatok, mind a programok leírására egy-egy sajátos eszközt, majd megmutatjuk, hogy ezek segítségével hogyan lehet igazolni azt, hogy egy program megold egy feladatot.
24
1.6. Feladatok
1.6. Feladatok 1.1. Mi az állapottere, kezdőállapotai, adott kezdőállapothoz tartozó célállapotai az alábbi feladatnak? „Egy egyenes vonalú egyenletesen haladó piros Opel s kilométert t idő alatt tesz meg. Mekkora az átlagsebessége?” 1.2. Mi az állapottere, kezdőállapotai, adott kezdőállapothoz tartozó célállapotai az alábbi feladatnak? „Adjuk meg egy természetes számnak egy valódi prím osztóját!” 1.3. Tekintsük az A = (n:ℤ, f:ℤ) állapottéren az alábbi programot! 1. Legyen f értéke 1 2. Amíg n értéke nem 1 addig 2.a. Legyen f értéke f*n 2.b. Csökkentsük n értékét 1-gyel! Írjuk fel e program néhány végrehajtását! Hogyan lehetne a végrehajtásokat általánosan jellemezni? Megoldja-e a fenti program azt a feladatot, amikor egy pozitív egész számnak kell a faktoriálisát kiszámolni? 1.4. Tekintsük az A = (x:ℤ, y:ℤ) állapottéren az alábbi programot! 1. Legyen x értéke x-y 2. Legyen y értéke x+y 3. Legyen x értéke y-x Írjuk fel e program néhány végrehajtását! Hogyan lehetne a végrehajtásokat általánosan jellemezni? Megoldja-e a fenti program azt a feladatot, amikor két, egész típusú változó értékét kell kicserélni? 1.5. Tekintsük az alábbi két feladatot: 1) „Adjuk meg egy 1-nél nagyobb egész szám egyik osztóját!” 2) „Adjuk meg egy összetett természetes szám egyik valódi osztóját!” Tekintsük az alábbi három programot az A = (n:ℕ, d:ℕ) állapottéren:
25
1. Programozási alapfogalmak
1) „Legyen a d értéke 1” 2) „Legyen a d értéke n” 3) „ Legyen a d értéke n-1. Amíg a d nem osztja n-t addig csökkentsük a d-t.” Melyik program melyik feladatot oldja meg? Válaszát indokolja! 1.6. Tekintsük az A=(m:ℕ+, n:ℕ+, x:ℤ) állapottéren működő alábbi programokat! 1) Ha m
n, akkor legyen az x értéke először a (-1)n, majd adjuk hozzá a (-1)n+1-t, utána a (-1)n+2-t, és így tovább, végül a (-1)m-t!” 2) Legyen az x értéke a ((-1)m+ (-1)n)/2! Írjuk fel e programok néhány végrehajtását! Lássuk be, hogy mindkét program megoldja az alábbi feladatot: Két adott nem nulla természetes számhoz az alábbiak szerint rendelünk egész számot: ha mindkettő páros, adjunk válaszul 1-et; ha páratlanok, akkor -1-et; ha paritásuk eltérő, akkor 0-át! 1.7.
Tekintsük az A 1,2,3,4,5 állapottéren az alábbi fiktív programot. (Itt tényleg egy programot adunk meg, amelyből kiolvasható, hogy az egyes állapotokból indulva az milyen végrehajtási sorozatot fut be.)
1 S
1,2,4,5 2
4 5
2,1 4,1,5,1,4 5,2,4
1
1,4,3,5,2 2
4 5
2,4 4,3,1,2,5,1 5,3,4
1 3 4 5
1,3,2,... 3,3,... 4,1,5,4,2 5,2,3,4
Az F feladat az alábbi kezdő-célállapot párokból áll: {(2,1) (4,1) (4,2) (4,4) (4,5)}. Megoldja-e az S program az F feladatot? 1.8. Legyen S olyan program, amely megoldja az F feladatot! Igaz-e, hogy
26
1.6. Feladatok
a) ha F nem determinisztikus, akkor S sem az? b) ha F determinisztikus, akkor S is az? 1.9. Az S1 bővebb, mint az S2, ha S2 minden végrehajtását tartalmazza. Igaz-e, hogy ha az S1 program megoldja az F feladatot, akkor azt megoldja az S2 is. 1.10. Az F1 bővebb, mint az F2, ha F2 minden kezdő-célállapot párját tartalmazza. Igaz-e, hogy ha egy S program megoldja az F1 feladatot, akkor megoldja az F2-t is.
27
2. Specifikáció Ebben a fejezetben azzal foglalkozunk, hogy hogyan lehet egy feladat kezdőállapotait a megoldás ellenőrzésének szempontjából csoportosítani, halmazokba rendezni úgy, hogy elég legyen egy-egy ilyen halmaznak egy tetszőleges elemére ellenőrizni azt, hogy onnan indulva az adott program megfelelő helyen áll-e meg. Ehhez a feladatnak egy sajátos formájú megfogalmazására, a feladat specifikációjára lesz szükség. 2.1. Kezdőállapotok osztályozása A megoldás szempontjából egy feladat egy kezdőállapotának legfontosabb tulajdonsága az, hogy milyen célállapotok tartoznak hozzá. Megfigyelhető, hogy egy feladat különböző kezdőállapotokhoz sokszor ugyanazokat a célállapotokat rendeli. Kézenfekvőnek látszik, hogy ha egy csoportba vennénk azokat a kezdőállapotokat, amelyekhez ugyanazok a célállapotok tartoznak, akkor a megoldás ellenőrzését nem kellene a kezdőállapotokon külön-külön elvégezni, hanem elég lenne az így keletkező csoportokat egyben megvizsgálni: azt kell belátni, hogy a feladat kezdőállapotainak egy-egy ilyen csoportjából elindulva a program megfelelő célállapotban áll-e meg. Tekintsünk példaként egy már korábban szereplő feladatot: „Egy adott összetett számnak keressük egy valódi osztóját”! A feladat állapottere (n:ℕ, d:ℕ). Az állapotok egyszerűbb jelölése érdekében rögzítsük a fenti sorrendet a komponensek között: egy állapot első komponense a megadott szám, a második a valódi osztó. Kössük ki, hogy az első komponens kezdőértéke a célállapotokban is megmaradjon. Az 2.1. ábrán egy derékszögű koordinátarendszer I. negyedével ábrázoljuk ezt az állapotteret, hiszen ebben minden egész koordinátájú rácspont egy-egy állapotot szimbolizál. (A feladat csak azokban az állapotokban értelmezett, ahol az első komponens összetett szám.) Külön koordinátarendszerbe rajzoltuk be a feladat néhány kezdőállapotát, és külön egy másikban az ezekhez hozzárendelt célállapotokat. Az ábra jól tükrözi vissza azt, hogy a feladat állapotokhoz állapotokat rendelő leképezés.
28
2.1. Kezdőállapotok osztályozása
{(0, *)}
{(4, *)} {(6, *)}
d (6,8)
(6,3)
n 1
2
3
4
5
6
d F
{(0, *)}
(6,3) (6,2)
(4,2)
n 1
2
3
4
5
6
2.1. Ábra Egy feladat: Keressük egy összetett szám valódi osztóit.
A feladat például a (6,8) kezdőállapothoz a (6,3) és a (6,2) célállapotokat rendeli. De ugyanezek a célállapotok tartoznak a (6,6), a (6,3) vagy a (6,127) kezdőállapotokhoz is, mivel ezek első komponensében ugyancsak a 6 áll. Tehát az összes (6,*) alakú állapot (a * itt egy tetszőleges értéket jelöl) ugyanazon csoportba (halmazba) sorolható az alapján, hogy hozzájuk egyformán a (6,3) és a (6,2) célállapotokat rendeli a feladat. Úgyis fogalmazhatunk, hogy a feladat a
29
2. Specifikáció
{(6,*)} halmaz elemeihez a {(6,3), (6,2)} halmaz elemeit rendeli. Ezen megfontolás alapján külön csoportot képeznek a (0,*), a (4,*), a (6,*) alakú állapotok is, általánosan fogalmazva azok az (n’,*) alakú kezdőállapotok, ahol az n’ összetett szám. Ezt láthatjuk 2. ábra felső részén. Nem kerül viszont bele egyetlen ilyen halmazba sem például az (1,1) vagy a (3,5) állapot, hiszen ezek nem kezdőállapotai a feladatnak. (Könnyű megmutatni, hogy ez a csoportosítás matematikai értelemben egy osztályozás: minden kezdőállapot pontosan egy halmazhoz (csoporthoz) tartozik.) Amikor a feladathoz megoldó programot keresünk, azt kell belátnunk, hogy minden ilyen halmaz egyik (tetszőleges) eleméből (ez tehát a feladat egy kezdőállapota) indulva a program a feladat által kijelölt valamelyik célállapotban áll meg. Ha ügyesek vagyunk, akkor elég ezt a vizsgálatot egyetlen jól megválasztott halmazra elvégezni, mondjuk az általános (n’,*) alakú kezdőállapotok halmazára, ahol az n’ összetett szám. Általában egy feladat kezdőállapotainak a fent vázolt halmazait nem könnyű előállítani. Ennek illusztrálására tekintsük azt a példát (lásd 1.1. feladat), amikor egy egyenes vonalú egyenletes mozgást végző testnek kell az átlagsebességét kiszámolni a megtett út és az eltelt idő függvényében. Ennek a feladatnak az állapottere (s:ℝ, t:ℝ, v:ℝ), ahol a komponensek rendre az út (s), az idő (t), és a sebesség (v). (Rögzítjük ezt a sorrendet a komponensek között.) A kezdőállapotokban az első komponens nem lehet negatív, a második komponensnek pedig pozitívnak kell lennie. Melyek azok a kezdőállapotok, amelyekhez ez a feladat a ( *,*,50) alakú célállapotokat rendeli? Nem is olyan könnyű erre a válasz. A (100,2,*) alakú kezdőállapotok ilyenek, a (200,4,*) alakúak is. Ismerve a feladat megoldásához használt v=s/t képletet (ezt a képletet csak az ilyen egyszerű feladatok esetében látjuk ilyen világosan), kis gondolkodás után kitalálhatjuk, hogy az (50a,a,*) alakú kezdőállapotokhoz (ahol a egy tetszőleges pozitív valós szám), és csak azokhoz rendeli a feladat a (*,*,50) alakú célállapotokat. De nem lehetne-e ennél egyszerűbben, az eredményt kiszámoló képlet alkalmazása nélkül csoportosítani a kezdőállapotokat? Miért ne tekinthetnénk külön csoportnak a (100,2,*) kezdőállapotokat, és külön a (200,4,*) kezdőállapotokat? Mindkettő halmazra teljesül, hogy a benne levő kezdőállapotokhoz ugyanazon célállapotok tartoznak. Igaz, hogy
30
2.1. Kezdőállapotok osztályozása
ezen célállapotok mindkét csoport esetében ugyanazok, de miért lenne ez baj? Ezek kijelöléséhez csak azt kell tudnunk, hogy a feladat első két adata a bemenő adat, az eredmény kiszámolásának képlete nem kell. Azonos bemenő adatokhoz ugyanis ugyanazon eredmény tartozik, tehát azonos bemenő adatokból felépített kezdőállapotokhoz a feladat ugyanazokat a célállapotokat rendeli. Az azonos bemenő adatokból felépített kezdőállapotok halmazai is osztályozzák a kezdőállapotokat, hiszen minden kezdőállapot pontosan egy ilyen halmazhoz tartozik. Ezért ha a feladat kezdőállapotainak minden ilyen halmazáról belátható, hogy belőle indulva a program megfelelő célállapotban áll-e meg, akkor igazoltuk, hogy a vizsgált program megoldja a feladatot. Természetesen ilyenkor sem kell az összes halmazt megvizsgálni. Elég egy általános halmazzal foglalkozni. Rögzítsük a példánk bemenő adatainak értékeit az s’, t’ tetszőlegesen kiválasztott nem negatív paraméterekkel, ahol t’ nem lehet nulla. Egy vizsgált program akkor oldja meg a feladatot, ha az {(s’,t’,*)} állapot-halmazból kiindulva a program az {(*,*, s’/t’)} állapot-halmaz valamelyik állapotában áll meg. Néha találkozhatunk olyan feladattal is, amelynek nincsenek bemeneti adatai. Ilyen például az, amikor egy prímszámot keresünk. Ennek a feladatnak az állapottere a lehetséges válaszoknak az N halmaza. Itt a válasz nem függ semmilyen bemeneti értéktől. Az összes állapot tehát egyben kezdőállapot is, amelyeket egyetlen halmazba foghatunk össze, hiszen mindegyikhez ugyanazon célállapotok, a prímszámok tartoznak. 2.2. Feladatok specifikációja Vegyük elő újra azt a feladatot, amikor az egyenes vonalú egyenletes mozgást végző testnek a megtett út és az eltelt idő függvényében kell az átlagsebességét kiszámolni! A feladat lényeges adatai a megtett út, az eltelt idő és az átlagsebesség. Ennek megfelelően a feladat állapottere a változókkal: A = (s:ℝ, t:ℝ, v:ℝ). Bemenő adatai a megtett út (s) és az eltelt idő (t). Rögzítsünk a bemenő adatok számára egy-egy tetszőlegesen kiválasztott értéket! Legyen az út esetén ez az s’, az idő esetén a t’, ahol egyik sem lehet negatív, sőt a t’ nulla sem. A kezdőállapotoknak az s’, t’ bemenő adatokkal rendelkező részhalmaza az {(s’,t’,*)}, az ezekhez tartozó célállapotok pedig a {(*,*, s’/t’)}.
31
2. Specifikáció
A kezdőállapotoknak és a célállapotoknak a fent vázolt halmazait logikai állítások (feltételek) segítségével is megadhatjuk. A logikai állítások minden esetben az állapottér állapotait minősítik: ha egy állapot kielégíti a logikai állítást, azaz a logikai állítás igaz értéket rendel hozzá, akkor az benne van a logikai állítás által jelzett halmazban. Például a {(100,2,*)} halmazt az s=100 és t=2 logikai állítással, a {(200,4,*)} halmazt az s=200 és t=4 állítással is jelölhetjük. Általában az {(s’,t’,*)} halmazt az s=s’ és t=t’ állítás írja le, a {(*,*, s’/t’)} halmazt pedig a v=s’/t’. Ha ki szeretnénk kötni, hogy az út hossza nem negatív és az idő pozitív, akkor a fenti állítások mellé oda kell írnunk azt is, hogy s’≥0 és t’>0. A kezdőállapotokat leíró előfeltételt Ef-fel, a célállapotokat leíró utófeltételt Uf-fel fogjuk jelölni. Megfigyelhető, hogy mindkettő feltétel a változókra fogalmaz meg megszorításokat. Ha egy változót egy feltétel nem említ, az azt jelzi, hogy arra a változóra nézve nincs semmilyen megkötés, azaz annak az értéke tetszőleges lehet. Példánk előfeltételben ilyen a v, az utófeltételben az s és a t. Tömören leírva az eddig elmondottakat a feladatnak az alábbi úgynevezett specifikációjához jutunk: A = (s:ℝ, t:ℝ, v:ℝ) Ef = (s=s’ és t=t’ és s’≥0 és t’>0) Uf = (v= s’/t’) A feladat specifikálása során meghatározzuk a bemenő adatok tetszőlegesen rögzített értékeihez tartozó kezdőállapotok halmazát, valamint az ehhez tartozó célállapotok halmazát. A feladat specifikációja tartalmazza: 1. az állapotteret, azaz a feladat lényeges adatainak típusértékhalmazait az egyes adatokhoz tartozó változó nevekkel együtt; 2. az előfeltételt, amely a kezdőállapotok azon halmazát leíró logikai állítás, amely rögzíti a bemenő változók egy lehetséges, de tetszőleges kezdőértékét (ezeket általában a megfelelő változónév vesszős alakjával jelöljük); 3. az utófeltételt, amely a fenti kezdőállapotokhoz rendelt célállapotok halmazát megadó logikai állítás. Meg lehet mutatni, hogy ha rendelkezünk egy feladatnak a specifikációjával és találunk olyan programot, amelyről belátható, hogy egy az előfeltételt kielégítő állapotból elindulva a program az
32
2.2. Feladatok specifikációja
utófeltételt kielégítő valamelyik állapotba kerül, akkor a program megoldja a feladatot. Ezt mondja ki a specifikáció tétele. Egy feladat állapotterében megjelenő változók minősíthetőek abból a szempontból, hogy bemenő illetve kimenő változók-e vagy sem. Ugyanaz a változó lehet egyszerre bemenő is és kimenő is. A bemenő változók azok, amelyeknek a feladat előfeltétele kezdőértéket rendel, amelyek explicit módon megjelennek az előfeltételben. Az utófeltételben explicit módon megjelenő változók közül azok a kimenő változók, amelyekre nincs kikötve, hogy értékük megegyezik az előfeltételben rögzített kezdőértékükkel. *** Specifikáljuk most azt a feladatot, amikor egy összetett szám legnagyobb valódi osztóját keressük. A feladatnak két lényeges adata van: az adott természetes összetett szám és a keresett valódi osztó. Mindkét adat természetes szám típusú. Ezt a tényt rögzíti az állapottér. A = (n:ℕ, d:ℕ) A két adat közül az első a bemeneti, a második a kimeneti adat. Legyen n’ a tetszőlegesen kiválasztott bemenő érték. Mivel a feladat nem értelmes a 0-ra, az 1-re és a prímszámokra; ezért az n’ csak összetett szám lehet. Az előfeltétel: Ef = ( n = n’ és n’ összetett szám ) Kiköthetjük, hogy a célállapot első komponensének, azaz a bemeneti adatnak értéke ne változzon meg, maradjon továbbra is n’; a második adat pedig legyen az n’ legnagyobb valódi osztója: Uf = ( Ef és d valódi osztója n-nek és bármelyik olyan k szám esetén, amelyik nagyobb mint d, a k nem valódi osztója n-nek ) Ha elemi matematikai jelöléssel akarjuk az állításainkat megfogalmazni, azt is megtehetjük. Az utófeltételben egy osztó "valódi" voltát egyszerűen kifejezhetjük a d [2..n–1] állítással, hiszen az n szám valódi osztói csak itt helyezkedhetnek el. (Választhatnánk a [2..n div 2] intervallumot is, ahol az n div 2 az n felének egészértéke.) Ha az intervallum egy eleme osztója az n-nek, akkor valódi osztója is. Alkalmazzuk a d n jelölést arra, hogy a d osztója az n-nek. Ennek megfelelően például azt az állítást, hogy a "d valódi osztója n-nek" úgy is írhatjuk, hogy "d [2..n–1] és d n". Aki otthonosan mozog az elsőrendű logika nyelvében, tömörebben is felírhatja a specifikáció
33
2. Specifikáció
állításait, ha használja a logikai műveleti jeleket. Ez természetesen nem változtat a specifikáció jelentésén. (Az előfeltételnél kihasználjuk azt, hogy egy szám összetettsége azt jelenti, hogy létezik a számnak valódi osztója.) Ennek megfelelően az elő- és az utófeltételt az alábbi formában is felírhatjuk: Ef = ( n = n’ k [2..n–1]: k n ) Uf = ( Ef d [2..n–1] d n k [d+1..n–1] : k n ) Az elő- és utófeltételben megjelenő k a specifikáció egy olyan eszköze (formula változója), amelyet állításaink precíz megfogalmazásához használunk. A k [a..b]:tulajdonság(k) (van olyan, létezik olyan k egész szám az a és b között, amelyre igaz a tulajdonság(k)) formula azt az állítást írja le, hogy a és b egész számok közé eső egész számok egyike kielégíti az adott tulajdonságot. A k [a..b]:tulajdonság(k) (bármelyik", "minden olyan" k egész szám az a és b között, amelyre igaz a tulajdonság(k)) formula azt jelenti, hogy a és b egész számok közé eső mindegyik egész szám kielégíti az adott tulajdonságot. A formulák helyes értelmezéséhez rögzíteni kell a kifejezésekben használt műveleti jelek közötti precedencia sorrendet. Az általunk alkalmazott sorrend kissé eltér a matematikai logikában szokásostól, amennyiben az implikációt szorosabban kötő műveletnek tekintjük, mint a konjunkciót (logikai „és”-t). Ennek az oka az, hogy viszonylag sokszor fogunk olyan állításokat írni, ahol implikációs formulák lesznek „összeéselve”, és zavaró lenne, ha kifejezéseinkben túl sok zárójelet kellene használni. Például a d n l d [2..n–1] kifejezést a matematikai logikában a d n (l d [2..n–1]) formában kellene írni.. A logikai műveleti jelek precedencia sorrendje tehát: , , , , , . A könyvben használt jelöléseknél a kettőspont a kvantorok ( , ) hatásának leírásához tartozó jel. Ezért alkot egyetlen összetartozó kifejezést a k [2..n–1] : k n . A kifejezések tartalmazhatnak a logikai műveleteken kívül egyéb műveleteket is. Ezek prioritása megelőzi a két-argumentumú logikai műveleti jelekét. Ilyen például a nem logikai értékű kifejezések egyenlőségét vizsgáló (=) operátor is, ezért írhatjuk az x = y z = y kifejezést az (x = y) (z = y) kifejezés helyett. Az egyéb, nem logikai műveletek között ez az egyenlőség operátor az utolsó a precedencia sorban, ezért senki ne olvassa például az x+y = z kifejezést x + (y = z) kifejezésnek. Nem fogjuk megkülönböztetni a nem logikai értékek
34
2.2. Feladatok specifikációja
egyenlőség operátorát a logikai értékek egyenlőség operátorától Erre a matematikai logikában az ekvivalencia ( ) műveleti jelet szokták használni. A szövegkörnyezetből mindig kiderül, hogy melyik egyenlőség operátorról van szó, legfeljebb zárójelezéssel tesszük majd a formuláinkat egyértelművé. Ez egyben azt is jelenti az egyenlőség operátor precedenciája jobb, mint az implikációjé. Sokszor segít, ha a specifikáció felírásához bevezetünk néhány saját jelölést is. Bár ezeket külön definiálni kell, de használatukkal olvashatóbbá, tömörebbé válik a formális leírás. Ha például az összetett(n) azt jelenti, hogy az n természetes szám egy összetett szám (az összetett(n) pontosan akkor igaz, ha k [2..n–1]: k n), az LVO(n) pedig megadja egy összetett n természetes szám legnagyobb valódi osztóját, akkor az előző specifikáció az alábbi formában is leírható. Ef = ( n = n’ összetett(n) ) Uf = ( Ef d = LVO(n) ) Módosítsuk az előző feladatot az alábbira: Keressük egy természetes szám legnagyobb valódi osztóját. (Tehát nem biztos, hogy van az adott számnak egyáltalán valódi osztója.) Itt az állapottérbe az előző feladathoz képest egy logikai komponenst is fel kell vennünk, ami azt jelzi majd a célállapotban, hogy van-e egyáltalán legnagyobb valódi osztója a megadott természetes számnak. Az előfeltétel most nem tartalmaz semmilyen különleges megszorítást. Az utófeltétel egyrészt bővül az l változó értékének meghatározásával (l akkor igaz, ha n összetett), másrészt a d változó értékét csak abban az esetben kell specifikálnunk, ha az l értéke igaz. Ha összevetjük a specifikációt az előző feladat specifikációjával, akkor azt vehetjük észre, hogy az n összetettségét vizsgáló állítás ott az előfeltételben, itt az utófeltételben jelenik meg. A = (n:ℕ, l: , d:ℕ) Ef = ( n = n’ ) Uf = ( n = n’ l = k [2..n–1]: k n l ( d [2..n–1] d n k [d+1..n–1]: k n) ) A fenti specifikáció olvashatóbbá válik, ha az előző feladatnál bevezetett összetett és LVO jelöléseket használjuk:
35
2. Specifikáció
A = (n:ℕ, l: , d:ℕ) Ef = ( n=n’ ) Uf = ( n=n’ l=összetett(n) l d=LVO(n) ) Ugyanazt a feladatot többféleképpen is lehet specifikálni. Minél összetettebb egy probléma, annál változatosabb lehet a leírása. A következő igen egyszerű példát háromféleképpen specifikáljuk: növeljünk meg egy egész számot eggyel. Az első két változatban külön komponens képviseli a bemenő (a változó) és külön a kimenő adatot (b változó). Az első változat nem tartalmaz semmi különöset a korábbi specifikációkhoz képest. A bemeneti adat megőrzi a kezdő értékét a célállapotban is, ezért a kimeneti változó értékének meghatározásánál mindegy, hogy a bemeneti változó vagy a paraméter változó értékére hivatkozunk. A második specifikációnál nem követeljük meg, hogy az a változó ne változzon meg, ezért a b változó értékének meghatározásánál az a változó kezdő értékét jelző a' paramétert kell használnunk. Egészen más felfogáson alapul a harmadik specifikáció. Ez úgy értelmezi a feladatot, hogy van egy adat, amelynek értékét „helyben” kell megnövelni eggyel. Ilyenkor az állapottér egykomponensű és az utófeltétel megfogalmazásánál elengedhetetlenül fontos, hogy a kezdőértéket rögzítő paraméter használata. Ez a specifikáció rávilágít arra is, hogy egy feladatban nem feltétlenül különülnek el a bemeneti és a kimeneti adatok. A = (a:ℤ, b:ℤ) A = (a:ℤ, b:ℤ) A = (a:ℤ) Ef = ( a=a’ ) Ef = ( a=a’ ) Ef = ( a=a’ ) Uf = ( a=a’ b=a+1 ) Uf = ( b=a’+1 ) Uf = ( a=a’+1 ) Végül egy bemeneti adat nélküli feladat: Adjunk meg egy prímszámot! A = (p:ℤ) Ef =( igaz ) Uf =( p egy prímszám ) = ( k [2..p-1] : (k p) ) Ez a specifikáció az összes állapotot kezdőállapotnak tekinti, amelyek mindegyikéhez az összes prímszámot rendeli. Az előfeltétel egy azonosan igaz állítás, hiszen semmilyen korlátozást nem kell bevezetni a p változóra, az kezdetben tetszőleges értéket vehet fel, amelytől nem függ az eredmény. ( Az utófeltételben a [2..p-1] intervallum helyett írhatnánk a [2.. n ] intervallumot is.)
36
2.3. Feladatok
2.3. Feladatok 2.1. Mit rendel az alább specifikált feladat a (10,1) és a (9,5) állapotokhoz? Fogalmazza meg szavakban a feladatot! (prím(x) = x prímszám.) A = (k:ℤ, p:ℤ) Ef = ( k=k’ k>0 ) Uf = ( k=k’ prím(p)
i>1: prím(i)
k-i
k-p )
2.2. Írja le szövegesen az alábbi feladatot! (A perm(x') az x' permutációinak halmaza.) A = (x:ℤn) Ef = ( x=x' ) Uf = ( x perm(x')
i,j [1..n]: i<j
x[i]
x[j])
2.3. Specifikálja: Adott egy hőmérsékleti érték Celsius fokban. Számítsuk ki Fahrenheit fokban! 2.4. Specifikálja: Adott három egész szám. Válasszuk ki közülük a legnagyobb értéket! 2.5. Specifikálja: Adottak egy ax+b=0 alakú elsőfokú egyenlet a és b valós együtthatói. Oldjuk meg az egyenletet! 2.6. Specifikálja: Adjuk meg egy természetes szám természetes osztóit! 2.7. Specifikálja a feladatot: Adjuk meg egy természetes szám valódi természetes osztóját! 2.8. Specifikálja: Döntsük el, hogy prímszám-e egy adott természetes szám? 2.9. Specifikálja: Adott egy sakktáblán egy királynő (vezér). Helyezzünk el egy másik királynőt úgy, hogy a két királynő ne üsse egymást! 2.10. Specifikálja: Adott egy sakktáblán két bástya. Helyezzünk el egy harmadik bástyát úgy, hogy ez mindkettőnek az ütésében álljon!
37
3. Strukturált programok Ebben a fejezetben azt vizsgáljuk meg, hogy ha egy programot meglevő programokból építünk fel meghatározott szabályok alapján, akkor hogyan lehet eldönteni róluk, hogy megoldanak egy kitűzött feladatot. Ennek érdekében először meghatározzuk az elemi program fogalmát, definiálunk néhány nevezetes elemi programot, majd háromféle építési szabályt, úgynevezett programszerkezetet (szekvencia, elágazás, ciklus) mutatunk az összetett programok készítésére. Ezekkel a szabályokkal fokozatosan építhetünk fel egy programot: elemi programokból összetettet, az összetettekből még összetettebbeket. Az ilyen programokat sajátos, egymásba ágyazódó, más néven strukturált szerkezetük miatt strukturált programoknak hívják. Strukturált programokat többféleképpen is le lehet írni. Mi egy sajátos, a program szerkezetét, struktúráját a középpontba állító, a fent említett három szerkezeten kívül más szerkezetet meg nem engedő absztrakt leírást, a struktogrammokat fogjuk erre a célra használni. Ez a programleírás egyszerű, könnyen elsajátítható, és természetes módon fordítható le ismert programozási nyelvekre, azaz bármelyik konkrét programozási környezetben jól használható; egyszerre illeszkedik az emberi gondolkodáshoz és a magas szintű programozási nyelvekhez.3 A struktogramm egy program leírására szolgáló eszköz, de felfogható a megoldandó feladat egyfajta leírásának is. Egy program készítésekor ugyanis a feladatot próbáljuk meg részfeladatokra bontani és meghatározni, hogy a részfeladatok megoldásait, hogyan, melyik 3
A modellünkben bevezetett program fogalma annyira általános, hogy nem minden ilyen program írható le struktogramm segítségével. Ez nem a struktogramm-leírás hibája, ugyanis ezen programok más eszközökkel (folyamatábra, C++ nyelv vagy Turing gép) sem írhatók le. A Church-Turing tézis szerint csak a parciális rekurzív függvényeket kiszámító (megoldó) programok algoritmizálhatók. Ezek mind leírhatók folyamatábrákkal; a Bhöm-Jaccopini tétele szerint pedig, bármelyik folyamatábrával megadott program struktogrammal is leírható. A struktogramm tehát azon túl, hogy a programokat egy jól áttekinthető szerkezetet segítségével adja meg, egy kellően általános eszköz is: bármely algoritmizálható program leírható vele.
38
programszerkezet segítségével kell egy programmá építeni. A programtervezés közbülső szakaszában tehát egy struktogramm nem elemi programokból, hanem megoldandó részfeladatokból építi fel a programot. Ez azért nem ellentmondás, mert elméletben minden feladat megoldható egy elemi programmal (lásd 3.3. alfejezet), és ezért csak nézőpont kérdése, hogy egy részfeladatot feladatnak vagy programnak tekintünk-e. A programkészítés során lépésről lépésre jelennek meg az egyre mélyebben beágyazott programszerkezetek, ami a feladat egyre finomabb részletezésének eredménye. Ezáltal a program kívülről befelé haladva, úgynevezett felülről-lefele (top-down) elemzéssel születik. A feladat finomításának végén a struktogrammban legbelül levő, szerkezet nélküli egységeknek már magától értetődően megoldható részfeladatokat kell képviselniük.
39
3. Strukturált programok
3.1. Elemi programok Elemi programoknak azokat a programokat nevezzük, amelyek végrehajtásai nagyon egyszerűek; működésüket egy vagy kettő hosszú azonos állapotterű állapotsorozatok vagy a kiinduló állapotot végtelen sokszor ismétlő (abnormálisan) végtelen sorozatok jellemzik. 3.1.1. Üres program Az üres program a legegyszerűbb elemi program. Ez az úgynevezett „semmit sem csináló” program bármelyik állapotból indul is el, abban az állapotban marad; végrehajtásai a kiinduló állapotból álló egyelemű sorozatok. Az üres programot a továbbiakban SKIP-pel jelöljük. Nyilvánvaló, ahhoz, hogy semmit sem csinálva eljussunk egy állapotba, már eleve abban az állapotban kell lennünk. A gyakorlatban kitűzött feladatok azonban jóval bonyolultabbak annál, hogy azokat üres programmal megoldhassuk. Gyakori viszont az, hogy egy feladat részekre bontásánál olyan részfeladatokhoz jutunk, amelyet már az üres programmal meg lehet oldani. Az üres program tehát gyakran jelenik meg valamilyen programszerkezetbe ágyazva. (Szerepe a programban ahhoz hasonlítható, amikor a kőműves egy üres teret rak körbe téglával azért, hogy ablakot, ajtót készítsen az épülő házon. A „semmi” ezáltal válik az épület fontos részévé.) Nézzünk most egy erőltetett példát arra, amikor egy feladatot az üres program old meg. Legyen a feladat specifikációja az alábbi: A = (a:ℕ, b:ℕ, c:ℕ) Ef = ( a=2 b=5 c=7 ) Uf = ( Ef (c=a+b c=10) ) Itt az Ef állításból következik az Uf állítás (Ef Uf). Ez azt jelenti, hogy ha egy Ef-beli állapotból (tehát amire teljesül az Ef állítás) elindulunk, akkor erre rögtön teljesül az Uf állítás is: ezért nem kell semmit sem tenni ahhoz, hogy Uf-ben tudjunk megállni, hiszen már ott vagyunk. Ezt a feladatot tehát megoldja a SKIP. Általánosítva az itt látottakat azt mondhatjuk, ha egy feladat előfeltételéből következik az utófeltétele, akkor a SKIP program biztosan megoldja azt.
40
3.1. Elemi programok
3.1.2. Rossz program Rossz program a mindig abnormálisan működő program. Bármelyik állapotból indul is el, abnormális végrehajtást végez: a kiinduló állapotot végtelen sokszor ismétlő végtelen végrehajtást fut be. A rossz programot a továbbiakban ABORT-tal jelöljük. A rossz program nem túl hasznos, hiszen semmiképpen nem terminál, így sosem tud megállni egy kívánt célállapotban. A rossz programmal csak az üres feladatot, azaz a sehol sem értelmezett, tehát értelmetlen feladatot lehet megoldani: azt, amelyik egyik állapothoz sem rendel célállapotokat. A feladatok megoldása szempontjából az ABORT-nak tehát nincs túl nagy jelentősége. Annak az oka, hogy mégis bevezettük az, hogy program leírásunkat minél általánosabbá tegyük, segítségével minél több programot, még a rosszul működő programokat is le tudjuk írni. 3.1.3. Értékadás A programozásban értékadásnak azt hívjuk, amikor a program egy vagy több változója egy lépésben új értéket kap. Mivel a program változói az aktuális állapot komponenseit jelenítik meg, ezért az értékadás során megváltozik az aktuális állapot. Vegyük példaként az (x:ℝ, y:ℝ) állapottéren azt a közönséges értékadást, amikor az x változónak értékül adjuk az x y kifejezés értékét. Ezt az értékadást bármelyik állapotra végre lehet hajtani, és minden esetben két állapotból (a kiinduló- és a végállapotból) álló sorozat lesz a végrehajtása. Például < (2,-5) (-10,-5) >, < (2.3,0.0) (0.0,0.0) >, < (0.0,4.5) (0.0,4.5) > vagy <(1,1) (1,1) >. Ezt az értékadást az x:=x y szimbólummal jelöljük. Az x y kifejezés hátterében egy olyan leképezés (függvény) áll, amely két valós számhoz (az aktuális állapothoz) egy valós számot (az x változó új értékét) rendeli és minden valós számpárra értelmezett. Tekintsük most az (x:ℝ, y:ℝ) állapottéren az x:=x/y parciális értékadást. Azokban a kiinduló állapotokban, amikor az y-nak az értéke 0, az értékadás jobboldalon álló kifejezésének nincs értelme. A jobboldali kifejezés tehát nem mindenhol, csak részben (parciálisan) értelmezett. Ebben az esetben úgy tekintünk az értékadásra, mint egy rossz programra, azaz a nem értelmezhető kiinduló állapotokból
41
3. Strukturált programok
indulva abortál.4 Ezzel biztosítjuk, hogy ez az értékadás is program legyen: minden kiinduló állapothoz rendeljen végrehajtási sorozatot. A feladatok megoldásakor gyakran előfordul, hogy egy időben több változónak is új értéket akarunk adni. Ez a szimultán értékadás továbbra is egy állapotból egy másik állapotba vezet, de az új állapot több komponensében is eltérhet a kiinduló állapottól.5 A háttérben itt egy többértékű függvény áll, amelyik az aktuális állapothoz rendeli azokat az értékeket, amelyeket a megfelelő változóknak kell értékül adni. Egy értékadás jobboldalán álló kifejezés értéke nem mindig egyértelmű. Ekkor a baloldali változó véletlenszerűen veszi fel a kifejezés által adott értékek egyikét. Erre példa az, amikor egy változónak egy halmaz valamelyik elemét adjuk értékül. Ekkor az állapottér az (x:ℕ, h:2ℕ) (a h értéke egy természetes számokat tartalmazó halmaz), az értékadás során pedig az x a h valamelyik elemét kapja. Ezt az értékadást értékkiválasztásnak (vagy nemdeterminisztikus értékadásnak) fogjuk hívni és az x: h kifejezéssel jelöljük.6 Ez a példa ráadásul parciális értékadás is, hiszen ha a h egy üres halmaz, akkor a fenti értékkiválasztás értelmetlen, tehát abortál. Ez rávilágít arra, hogy az értékadásnak fent bemutatott általánosításai (parciális, szimultán, nem-determinisztikus) egymást kiegészítve halmozottan is előfordulhatnak. A háttérben itt egy nem-egyértelmű leképezés (tehát nem függvény), egy reláció áll. 4
Ezzel a jelenséggel a programozó például akkor találkozik, amikor az operációs rendszer "division by zero" hibaüzenettel leállítja a program futását. 5 A szimultán értékadásokat a kódoláskor egyszerű értékadásokkal kell helyettesíteni (lásd 6.3. alfejezet), ha a programozási nyelv nem támogatja ilyenek írását. 6 A nem-determinisztikus értékadásokat a mai számítógépeken determinisztikus értékadások helyettesítik. Ez a helyettesítés azonban nem mindig a programozó feladata, hanem gyakran az a programozási környezet végzi, ahol a programot használjuk. Ez elrejti a programozó elől azt, hogyan válik determinisztikussá egy értékadás, így az kvázi nem-determinisztikus. Ez történik például a véletlenszám-generátorral történő értékadás során. Habár ez a nevével ellentétben nagyon is kiszámítható, az előállított érték a program szempontjából mégis nemdeterminisztikus lesz.
42
3.1. Elemi programok
Általános értékadásnak egy szimultán, parciális, nemdeterminisztikus értékadást tekintünk. A nem-szimultán értékadást egyszerűnek, a nem parciálist teljesnek (mindenhol értelmezettnek) nevezzük, az értékkiválasztás ellentéte a determinisztikus értékadás. A közönséges értékadáson az egyszerű, teljes és determinisztikus értékadást értjük. Az értékadás kétséget kizáróan egy program, amelyik az állapottér egy állapotához az adott állapotból induló kételemű vagy abnormálisan végtelen állapotsorozatot rendel. (Nem-determinisztikus esetben egy kiinduló állapothoz több végrehajtás is tartozhat.) Jelölésekor a „:=” szimbólum baloldalán feltüntetjük, hogy mely változók kapnak új értéket, a jobboldalon felsoroljuk az új értékeket előállító kifejezéseket. Ezek a kifejezések az állapottér változóiból, azok típusainak műveleteiből, esetenként konkrét típusértékekből (konstansokból) állnak. A kifejezés értéke a változók értékétől, azaz az aktuális állapottól függ, amit a kifejezés egy értékké alakít és azt a baloldalon álló változónak adódik értékül. Nem-determinisztikus értékadás, azaz értékkiválasztás esetén a „:=” szimbólum helyett a „: ” szimbólumot használjuk. Az értékadást nemcsak azért tekintjük elemi programnak, mert végrehajtása elemi lépést jelent az állapottéren, hanem azért is, mert könnyű felismerni, hogy milyen feladatot old meg. Ha például egy x, y, z egész változókat használó feladat utófeltétele annyival mond többet az előfeltételnél, hogy teljesüljön a z = x+y állítás is, akkor nyilvánvaló, hogy ezt a z:=x+y értékadás segítségével érhetjük el. Ha a megoldandó feladat az, hogy cseréljük ki két változó értékét, azaz A = (x:ℤ, y:ℤ), Ef = (x=x’ y=y’) és Uf = (x=y’ y=x’), akkor ezt az x,y:=y,x értékadás oldja meg. 3.2. Programszerkezetek A strukturált programok építésénél három nevezetes programszerkezetet használunk: szekvenciát, elágazást, ciklust. 3.2.1. Szekvencia Két program szekvenciáján azt értjük, amikor az egyik program végrehajtása után – feltéve, hogy az terminál – a másikat hajtjuk végre.
43
3. Strukturált programok
Ez a meghatározás azonban nem elegendő a szekvencia pontos megadásához. Ahhoz ugyanis, hogy egy szekvencia végrehajtási sorozatait ez alapján felírhassuk, meg kell előbb állapodni abban, hogy mi legyen a szekvencia alap-állapottere. A tagprogramok alapállapotterei ugyanis különbözhetnek egymástól, és ilyenkor nyilvánvalóan meg kell állapodni egy közös alap-állapottérben, amely majd a szekvencia állapottere lesz. A tagprogramokat majd erre a közös állapottérre kell átfogalmazni, azaz változóik alap- illetve segédváltozói státuszát ennek megfelelően kell megváltoztatni, esetleg bizonyos változókat át kell nevezni. (Említettük, hogy az ilyen átalakítás nem változat egy program lényegén.) A közös alap-állapottér kialakítása során előfordulhat az is, hogy az eredetileg kizárólag csak az egyik tagprogram alap- vagy segédváltozója a szekvencia közös alapváltozójává válik, vagy a szekvencia alap-állapotterébe olyan komponensek kerülnek be, amely az egyik tagprogramnak alap- vagy segédváltozói, a másik tagprogramban pedig segédváltozók voltak. (A segédváltozóra vonatkozó létrehozó és megszüntető utasítások ilyenkor érvényüket vesztik.) A közös alap-állapottér kialakítása még akkor sem egyértelmű, ha a két tagprogram alap-állapottere látszólag megegyezik. Elképzelhető ugyanis például az, hogy egy mindkét alap-állapotérben szereplő ugyanolyan nevű és típusú változót nem akarunk a szekvenciában közös változónak tekinteni. Végül szögezzük le, hogy a közös alap-állapottér komponensei kizárólag a szekvenciába fűzött tagprogramok változói közül kerülhetnek ki. Egy azoknál nem szereplő változót nem veszünk fel a közös alap-állapottérbe (ennek ugyanis nem lenne semmi értelme). A szekvencia végrehajtási sorozatai, miután mindkét tagprogramot a közös alap-állapottéren újraértelmeztük, úgy képződnek, hogy az első tagprogram egy végrehajtásához – amennyiben a végrehajtás véges hosszú – hozzáfűződik a végrehajtás végpontjából a második tagprogram által indított végrehajtás. A szekvencia is program, hiszen egyrészt a közös alap-állapottér minden állapotából indít végrehajtást (hiszen az első tagprogram is így tesz), másrészt ezeknek a végrehajtásoknak az első állapota a kiinduló állapot (ugyancsak amiatt, hogy az első tagprogram egy program). Harmadrészt minden véges végrehajtási sorozat a közös alapállapottérben áll meg, hiszen ezek utolsó szakaszát a második tagprogram generálja, amelynek véges végrehajtási sorozatai a közös
44
3.2. Programszerkezetek
alap-állapottérre való átalakítása után ebben az állapottérben terminálnak (lévén a második tagprogram is program). Természetesen ezen az elven nemcsak kettő, hanem tetszőleges számú programot is szekvenciába fűzhetünk. Az S1 és S2 programokból képzett szekvenciát az (S1;S2) szimbólummal jelölhetjük vagy az alábbi rajzzal fejezhetjük ki. Ezek a jelölések értelemszerűen használhatók a kettőnél több tagú szekvenciákra is. S1 S2 Milyen feladat megoldására használhatunk szekvenciát? Ha egy feladatot fel lehet bontani részfeladatokra úgy, hogy azokat egymás után megoldva az eredeti feladat megoldását is megkapjuk, akkor a megoldó programot a részfeladatot megoldó programok szekvenciájaként állíthatjuk elő. Formálisan: ha adott egy feladat (A,Ef,Uf) specifikációja, amelyből elő tudunk állítani egy (A,Ef,Kf) specifikációjú részfeladatot, valamint egy (A,Kf,Uf) specifikációjú részfeladatot, ahol Kf az úgynevezett közbeeső feltétel, akkor a részfeladatokat megoldó programok szekvenciája megoldja az eredeti feladatot. Ugyanis ekkor az első részfeladatot megoldó program elvezet Ef-ből (egy Ef által kielégített kezdőállapotból) Kf-be, a második a Kfből Uf-be, azaz összességében a szekvenciájuk eljut Ef-ből Uf-be. Tekintsük például azt a feladatot, amikor két egész értékű adat értékét kell kicserélni. (Korábban már megoldottuk ezt a feladatot egy szimultán értékadással. Ha azonban az adott programozási környezetben nem alkalmazható szimultán értékadás, akkor mással kell próbálkozni.) Emlékeztetőül a feladat specifikációja: A = (x:ℤ, y:ℤ) Ef = ( x=x’ y=y’ ) Uf = ( x=y’ y=x’ ) Bontsuk fel a feladatot három részre az alábbi, kicsit trükkös Kf1 és Kf2 közbeeső állítások segítségével. Ezáltal három, ugyanazon az állapottéren felírt részfeladat specifikációjához jutottunk. A részfeladatok állapottere azonos az eredeti állapottérrel, az első feladat
45
3. Strukturált programok
előfeltétele az Ef, utófeltétele a Kf1, a második előfeltétele a Kf1, utófeltétele a Kf2, a harmadik előfeltétele a Kf2, utófeltétele az Uf. Kf1 = ( x=x’ – y’ y=y’ ) Kf2 = ( x= x’ – y’ y=x’ ) Először próbáljunk meg az első részfeladatot megoldani, azaz eljutni a Ef-ből a Kf1-be. Nyilvánvaló, hogy ezt a részfeladatot az x:=x– y megoldja, hiszen kezdetben (Ef) x az x’, y az y’ értéket tartalmazza. A második részfeladatban az y-nak kell az x eredeti értékét, az x’-t megkapni, ami (x’–y’)+y’ alakban is felírható. Mivel a Kf1 (ez a második feladat előfeltétele) szerint kezdetben x=x’–y’ és y=y’, az ami (x’–y’)+y’-t az x+y alakban is írhatjuk. A második feladat megoldásához tehát megfelel az y:=x+y értékadás. Hasonlóan okoskodva jön ki, hogy a harmadik lépéshez az x:=y–x értékadás szükséges. A teljes feladat megoldása tehát az alábbi szekvencia lesz, amelyben mindhárom tagprogram és a szekvencia közös alapállapottere is a feladat állapottere: x:=x–y y:=x+y x:=y–x Oldjuk meg most ugyanezt a feladatot másképpen: segédváltozóval. Bontsuk fel a feladatot három részre úgy, hogy először tegyük „félre” egy z:ℤ változóba az x változó értékét, (ezt a kívánságot rögzíti a Kf1), ezt követően az x értéke az y változó értékét vegye fel, miközben a z változó őrzi az értékét (ezt mutatja a Kf2), végül kerüljön át a z változóba félretett érték az y változóba. Mindhárom részfeladat állapottere az eredeti állapottérnél bővebb: A’ = (x:ℤ, y:ℤ, z:ℤ). Kf1 = ( x=x’ y=y’ z=x’ ) Kf2 = ( x=y’ y=y’ z=x’ ) Az első részfeladatot könnyű megoldani: a Kf1 a z=x’ állítással mond többet az Ef-nél, és az x’ éppen az x változó értéke, ezért a z:=x értékadás vezet el az Ef-ből a Kf1-be. Ennek a programnak a z egy eredmény (nem pedig segéd) változója, az y pedig akár el is hagyható az állapotteréből. Mivel a z változó a programban ennek az
46
3.2. Programszerkezetek
értékadásnak a baloldalán jelenik meg először, ezért rá a struktogramm első értékadása mellett külön felhívjuk a figyelmet a típusának megadásával. A második részfeladatnál Kf1-ből a Kf2-be az x:=y értékadás visz át, hiszen itt az egyetlen különbség a két állítás között az, hogy az x változó értékét az y-ban tárolt y’ értékre kell átállítani. A későbbiek miatt fontos viszont, hogy ennek a programnak állapottere tartalmazza a z (bemenő és eredmény) változót is. A harmadik részfeladatban Kf2-ből a Uf-be kell eljutni. Itt az y-t kell megváltoztatni úgy, hogy vegye fel az x eredeti értékét, az x’-t, amit a Kf2 szerint a zben találunk. Tehát: y:=z. Most a z egy bemenő változó. Mindezek alapján könnyű látni, hogy az alábbi szekvencia megoldja a kitűzött feladatot. Ehhez azonban először a program alapállapotterét kell a feladatéhoz igazítani, leszűkíteni, amely hatására a z változó szekvencia egy segédváltozójává válik: z:=x
z:ℤ
x:=y y:=z A programtervezés során az a legfontosabb kérdés, hogy miről lehet felismerni, hogy egy feladatot szekvenciával kell megoldani, milyen lépésekből álljon a szekvencia, hogyan lehet megfogalmazni azokat a részcélokat, amelyeket a szekvencia egyes lépései oldanak meg, és mi lesz ezen lépéseknek a sorrendje. Ezekre a kérdésekre a feladat specifikációjában kereshetjük a választ. Például, ha az utófeltétel egy kapcsolatokkal felírt összetett állítás, akkor gyakran az állítás egyes tényezői szolgálnak részcélként egy szekvenciához. (De vigyázat! Nem törvényszerű, hogy az utófeltétel kapcsolata mindenképpen szekvenciát eredményez.) A részcélok közötti sorrend azon múlik, hogy az egyes részek utalnak-e egymásra, és melyik használja fel a másik eredményét. Kijelölve az utófeltételnek azt a részét, amelyet először kell teljesíteni, előáll az a közbülső állapotokat leíró logikai állítás, amely az első részfeladat utófeltétele és egyben a második részfeladat előfeltétele lesz. Hozzávéve ehhez a feladat utófeltételének itt még nem érintet felét vagy annak egy részét, megkapjuk a második részfeladat utófeltételét, és így tovább. Az
47
3. Strukturált programok
eredeti előfeltétel az első részfeladat előfeltétele, az eredeti utófeltétel az utolsó részfeladat utófeltétele lesz. 3.2.2. Elágazás Amikor egy feladatot nem egymás után megoldandó részfeladatokra, hanem alternatív módon megoldható részfeladatokra tudunk csak felbontani, akkor elágazásra van szükségünk. Elágazás az, amikor két vagy több programhoz egy-egy feltételt rendelünk, és mindig azt a programot – úgynevezett programágat – hajtjuk végre, amelyhez tartozó feltétel az aktuális állapotra teljesül. Ha egyszerre több feltétel is igaz, akkor ezek közül valamelyik programág fog végrehajtódni, ha egyik sem, akkor az elágazás abortál.7 Az elágazás alap-állapotterét megállapodás alapján alakítjuk ki az elágazás feltételeit alkotó kifejezésekben használt változókból és a programágak változóiból, többnyire azok uniójából. A feltételek változói a közös állapottér bemenő változói lesznek. Előfordulhat itt is, hogy két programág látszólag ugyanazon változóit, előbb átnevezéssel meg kell egymástól különböztetni. Készítsük el az alábbi elágazást! Vegyük az (x:ℤ, y:ℤ, max:ℤ) állapottéren a max:=x és a max:=y értékadásokat. Rendeljük az elsőhöz az x y, a másodikhoz az x y feltételt. Tekintsük azt a programot, amelyik az x y feltétel teljesülése esetén a max:=x, az x y feltétel bekövetkezésekor a max:=y értékadást hajtja végre. Az így konstruált elágazás a (8,2,0) állapothoz (mivel erre az első feltétel teljesül) a (8,2,0),(8,2,8) sorozatot, az (1,2,0)-hoz (erre a második feltétel teljesül) az (1,2,0),(1,2,2) sorozatot rendeli. A (2,2,0) állapot mindkét 7
A magas szintű programozási nyelvek többsége ettől eltérő módon definiálja az elágazást. Egyrészt rögzítik az ágak közti sorrendet azzal, hogy mindig az első olyan ágat hajtják végre, amelyiknek feltételét kielégíti az aktuális állapot. Másrészt, ha egyik feltétel sem teljesül, akkor az elágazás az üres programmal azonos. Mi ezt azért nem vesszük át, hogy a programjaink helyessége nem függjön attól, hogy egyrészt az elágazás eseteit milyen sorrendben gondoltuk végig, másrészt, ha egy esetre elfelejtettünk gondolni (egyik feltétel sem teljesül), akkor a program erre figyelmeztetve hibásan működjön.
48
3.2. Programszerkezetek
feltételt kielégíti, ezért rá tetszés szerint bármelyik értékadás végrehajtható. A példában ezek végrehajtásának eredménye azonos; mindkét esetben a (2,2,0),(2,2,2) sorozatot kapjuk. Az elágazás ágai között semmiféle elsőbbségi sorrend nincs, ezért ha egy kiinduló állapotra egyszerre több feltétel is teljesül, akkor az elágazás véletlenszerűen választja ki azt az ágat, amelynek feltételét az adott állapot kielégíti. Az elágazás tehát annak ellenére nemdeterminisztikussá válhat, ha az ágai egyébként determinisztikus programok. Az már a programozó felelőssége, hogy ilyen esetben is biztosítsa, hogy az elágazás jól működjön. (A példában a feltételeknek legalább egyike mindig teljesül.) Olyan elágazásokat is lehet szerkeszteni, ahol egy kiinduló állapot egyik feltételt sem elégíti ki. Ilyenkor azt kell feltételeznünk, hogy a programozó nem számolt ezzel az esettel, az elágazásnak tehát "hivatalból" rosszul kell működnie. Ezért ilyenkor az elágazás definíció szerint abortál. Sokszor a feltételeket úgy alakítjuk ki, hogy azok közül az állapottér minden elemére pontosan az egyik teljesüljön. Ezzel kizárjuk mind a nemdeterminisztikussá válás, mind az abortálás esetét. Az elágazás nyilvánvalóan program, hiszen minden állapothoz rendel önmagával kezdődő végrehajtási sorozatot: ha egy állapot valamelyik feltételt kielégíti, akkor az ahhoz tartozó programág által előállított sorozatot, ha egyik feltételt sem elégíti ki, akkor az abortáló sorozatot. Az S1, ... ,Sn A állapottér feletti programokból és az f1, ... ,fn A állapottér feletti feltételekből képzett elágazást az IF(f1:S1, ... ,fn:Sn)-nel jelöljük és az alábbi ábrával rajzoljuk: f1
...
fn
S1
...
Sn
Ha olyan n ágú elágazást kell jelölnünk, ahol az n-edik ág feltétele akkor teljesül, ha az előző ágak egyik feltétele sem, azaz fn= f1 … fn-1, akkor az fn–t helyettesíthetjük az else jelöléssel. A leggyakrabban előforduló elágazások kétágúak, amelyek között gyakran fogunk találkozni olyanokkal, amelyekben egyik feltétel a
49
3. Strukturált programok
másik ellentéte. Az alábbi ábrák egymással egyenértékű módon fejezik ki az ilyen elágazásokat. f
f
S1
S2
f S1
S2
Milyen feladat megoldására használhatunk elágazást? Például, ha a feladat utófeltétele esetszétválasztással határozza meg a célt. Ezt gyakran a „ha-akkor” szófordulatokkal (logikai implikáció jelével) fejezzük ki. Ilyenkor először meg kell határoznunk az egyes eseteket leíró feltételeket (fi). Ezután meg kell bizonyosodnunk arról, hogy az Ef-nek eleget tevő bármelyik állapotra legalább ez egyik feltétel teljesül (azaz Ef f1 ... fn), mert különben az elágazás abortál. A részfeladatok kialakításánál az alábbiak szerint járunk el. Az i-edik esethez (ághoz) tartozó részfeladat kezdőállapotai az eredeti feladat azon a kezdőállapotai, amelyek eleget tesznek az i-edik feltételének is. A részfeladat célállapotai az eredeti feladat célállapotai. Az i-edik ág részfeladatának specifikációja tehát: (A, Ef fi, Uf). Mindezek alapján belátható, hogy ha Ef f1 ... fn és minden i {1..n}-re az Si programág megoldja az (A, Ef fi, Uf) részfeladatot, akkor az IF(f1:S1, ... ,fn:Sn) elágazás megoldja az (A, Ef, Uf) feladatot. Példaként oldjuk meg azt a feladatot, amelyben egy egész számot a szignumával kell helyettesítenünk. A pozitív számok szignuma 1, a negatívoké –1, a nulláé pedig 0. A feladat specifikációja: A = (x:ℤ ) Ef = ( x=x’ ) Uf = ( x=sign(x’) ) ha
z 0
0 ha 1 ha
z 0 z 0
1
ahol sign(z) =
Az utófeltétel három esettel specifikálja a x változó új értékét: x>0, x=0 és x<0. Ez a három eset lefedi az összes lehetséges esetet és egyszerre közülük csak az egyik lehet igaz. A szignum függvény definíciójából látszik, hogy az első esetben az x új értéke az 1 lesz, második esetben a 0, harmadik esetben a –1. Világos tehát, hogy az alábbi elágazás megoldja a feladatot.
50
3.2. Programszerkezetek
x>0
x=0
x<0
x:=1
x:=0
x:= –1
3.2.3. Ciklus Ciklus működése során egy adott programot, az úgynevezett ciklusmagot egymás után mindannyiszor végrehajtjuk, valahányszor olyan állapotban állunk, amelyre egy adott feltétel, az úgynevezett ciklusfeltétel teljesül. Tekintsünk egy példát. Legyenek az állapottér állapotai a [–5 .. + [ intervallumba eső egész számok. Válasszuk ciklusmagnak azt a programot, amely az aktuális állapotot (egész számot) mindig a szignumával növeli meg, azaz ha az állapot negatív, akkor csökkenti eggyel, ha pozitív, növeli eggyel, ha nulla, nem változtatja az értékét. Legyen a ciklusfeltétel az, hogy az aktuális állapot nem azonos a 20szal. Amikor az 5 állapotból indítjuk a ciklust, akkor a ciklusmag első végrehajtása elvezet a 6 állapotba, majd másodszorra a 7-be és így tovább egészen addig, amíg tizenötödször is végrehajtódik a ciklusmag, és a 20 állapotba nem jutunk (ez már nem elégíti ki a ciklusfeltételt), itt a ciklus leáll. A ciklus tehát az 5 kiinduló állapotból az 5,6,7, ... ,19,20 állapotsorozatot futja be. Amennyiben a 20 a kiinduló állapot, a ciklus rögtön leáll, a 20-hoz tehát a 20 egy elemű végrehajtás tartozik. A 21 vagy annál nagyobb állapotokról indulva a ciklus sohasem áll le: a ciklusmag minden lépésben növeli eggyel az aktuális állapotot, és az egyre növekedő állapotok ( 21,22,23, ... ) egyike sem lesz egyenlő 20szal, azaz nem elégítik ki a ciklusfeltételt. Ugyanez történik amikor a 0 állapotból indulunk el, hiszen ekkor a ciklusmag sohasem változtatja meg az aktuális állapotát, azaz a <0,0,0, … > végtelen sorozat hajtódik végre. (Mindkét esetre azt mondjuk, hogy „a program végtelen ciklusba esett”.) A –1-ről indulva viszont lépésről lépésre csökkenti a ciklus az aktuális állapotot, de amikor a –5-höz ér, a ciklus működése elromlik. A ciklusmag ugyanis nem képes a –5-öt csökkenteni, hiszen ez kivezetne az állapottérből; a ciklusmag a –5-ben abortál. A teljes végrehajtási sorozat a –1,–2,–3,–4,–5,–5,–5, ... .
51
3. Strukturált programok
Egy ciklus végrehajtását jelképező állapotsorozat annyi szakaszra bontható, ahányszor a ciklusmagot végrehajthattuk egymás után. Egyegy szakasz a ciklusmag egy-egy végrehajtása. A példánkban ez egyetlen lépés volt (tehát egyetlen érték képviselt egy szakaszt), de általában a ciklusmag egy végrehajtása hosszabb állapotsorozatot is befuthat. Egy-egy szakasz kiinduló állapota mindig kielégíti a ciklusfeltételt. (A szakasz többi állapotára ennek nem kell fennállnia.) Amennyiben a szakasz véges hosszú és a végpontja újra kielégíti a ciklusfeltételt, a ciklusmag ebből a pontból újabb szakaszt indít. A ciklus egy végrehajtása lehet véges vagy végtelen. A véges végrehajtások véges sok véges hosszú szakaszból állnak, ahol a szakaszok kiinduló állapotai kielégítik a ciklusfeltételt, az utolsó szakasz végállapota viszont már nem. A véges végrehajtások egy speciális esete az, amikor már a ciklus kiinduló állapota sem elégíti ki a ciklusfeltételt, így nem kerül sor a ciklusmag egyetlen végrehajtására sem. Ilyenkor a végrehajtás egyelemű sorozat lesz. A végtelen végrehajtások kétfélék. Vannak olyanok, amelyek végtelen sok véges hosszú szakaszból állnak. Itt minden szakasz egy ciklusfeltételt kielégítő állapotban végződik. („végtelen ciklus”). Más végtelen végrehajtások ellenben véges sok szakaszból állnak, de a legutolsó szakasz végtelen hosszú. Ilyenkor a ciklusmag egyszer csak valamilyen okból nem terminál. Ez a „hibás ciklusmag” esete. A ciklus alap-állapotterét a ciklusmag alap-állapottere és a ciklusfeltételt adó kifejezés változóiból megállapodás alapján alakítjuk ki. A ciklus tényleg program, hiszen az állapottér minden pontjához rendel önmagával kezdődő sorozatot. A ciklust a továbbiakban a LOOP(cf, S) szimbólummal, illetve a cf S ábrával fogjuk jelölni. Felismerni, hogy egy feladatot ciklussal lehet megoldani, majd megtalálni ehhez a megfelelő ciklust, nem könnyű. Ez csak sok gyakorlás során megszerezhető intuíciós készség segítségével sikerülhet. Bizonyos „ökölszabályok” természetesen felállíthatóak, de a szekvencia illetve az elágazásokhoz képest összehasonlíthatatlanul
52
3.2. Programszerkezetek
nehezebb egy helyes ciklus konstruálása. Ahhoz, hogy egy ciklus megold-e egy Ef és Uf-fel specifikált feladatot, két kritériumot kell belátni. Az egyik, az úgynevezett leállási kritérium, amely azt biztosítja, hogy az Ef-ből (Ef által kijelölt állapotból) elindított ciklus véges lépés múlva biztosan leáll, azaz olyan állapotba jut, amelyre a ciklusfeltétel hamis. A másik, az úgynevezett haladási kritérium, azt biztosítja, hogy ha az Ef-ből elindított ciklus valamikor leáll, akkor azt jó helyen, azaz Uf-ben (Uf-beli állapotban) teszi. A leállási kritérium igazolásához például elegendő az, ha a ciklus végrehajtása során a ciklusfeltétel ellenőrzésekor érintett állapotokban meg tudjuk mondani, hogy a ciklus magját még legfeljebb hányszor fogja a ciklus végrehajtani, és ez a szám a ciklusfeltétel teljesülése esetén mindig pozitív és a ciklusmag egy végrehajtása után legalább eggyel csökken. Ekkor ugyanis csak véges sokszor fordulhat elő, hogy ezen állapotokban a ciklusfeltétel igazat ad, tehát a ciklusnak véges lépésen belül meg kell állni. Vannak olyan feladatokat megoldó ciklusok is, amelyek leállásának igazolása ennél jóval nehezebb, van, amikor nem is dönthető el. A haladási kritérium igazolásához a ciklus úgynevezett invariáns tulajdonságát kell megtalálnunk. Ez a ciklusra jellemző olyan állítás (jelöljük I-vel), amelyet mindazon állapotok kielégítenek, ahol a ciklus akkor tartózkodik, amikor sor kerül a ciklusfeltételének kiértékelésére. Megfordítva, egy I állítást akkor tekintünk invariánsnak, ha teljesül rá az, hogy az I-t és a ciklusfeltételt egyszerre kielégítő állapotból elindulva a ciklusmag végrehajtása egy I-t kielégítő állapotban fog megállni, azaz I cf-ből I-be jut. Az invariáns állítást kielégítő állapotokból indított ciklus működése tehát ebben az értelemben jól kiszámítható, hiszen biztosak lehetünk abban, hogy amennyiben a ciklus leáll, akkor I-beli állapotban fog megállni. Egy ciklusnak több invariáns állítása is lehet, de ezek közül nekünk arra van szükségünk, amely segítségével meg tudjuk mutatni, hogy a ciklus megoldja az Ef és Uf-fel specifikált feladatot, azaz – ha leáll – akkor eljut az Ef-ből az Uf-be. Ehhez egyrészt a I invariánsnak érvényesnek kell lennie már minden Ef-beli kezdőállapotra (azaz Ef I), hiszen a I-nek már a ciklus elindulásakor (a ciklusmag első végrehajtásának kezdőállapotára) teljesülnie kell. Másrészt bizonyítani kell, hogy a megálláskori állapot, amely még mindig kielégíti a I-t, de már nem elégíti ki a ciklusfeltételt (hiszen ezért állt meg a ciklus), az egyben az Uf-et is kielégíti (tehát I cf Uf).
53
3. Strukturált programok
Alkalmazzuk az elmondottakat egy konkrét példára. Legyen a megoldandó feladat az, amikor keressük egy összetett szám legnagyobb valódi osztóját. Ezt a feladatot korábban már specifikáltuk, de attól eltérő módon az előfeltételben most tegyük fel azt is, hogy a d eredmény változó értéke kezdetben az n–1 értékkel egyezik meg. (Ennek hiányában a ciklus előtt egy értékadást is végre kellene hajtani, de a példában most nem akarjuk a szekvenciákról tanultakat is alkalmazni.) A = (n:ℕ, d:ℝ) Ef=( n=n’ összetett(n) d=n–1 ) Uf=( n=n’ összetett(n) d=LVO(n) ) Tekintsük az A állapottéren az alábbi programot: d n d:=d–1 Invariáns állításnak válasszuk azt, amely azokat az állapotokat jelöli ki, amelyekben a d változó értéke valahol 2 és n-1 közé esik, és a d-nél nagyobb egész számok biztosan nem osztói az n-nek. I=( n=n’ összetett(n) LVO(n) d n–1 ) Ez tényleg invariáns a ciklusmag működésére, hiszen ha egy állapotra teljesül a I és még a ciklusfeltétel is (azaz a d maga sem osztja az n-t, és emiatt d biztos nagyobb, mint az n legnagyobb valódi osztója, és persze nagyobb 2-nél is), akkor eggyel csökkentve a d értékét (ezt teszi a ciklus magja) ismét olyan állapotba jutunk, amely kielégíti az It. Nyilvánvaló, hogy I gyengébb, mint Ef, tehát Ef I. Az is teljesül, hogy I cf Uf (ha LVO(n) d n–1 és d n, akkor d=LVO(n)). Összességében tehát a ciklus egy Ef-beli állapotból elindulva, ha megáll valamikor, akkor Uf-beli állapotban fog megállni. A leállási kritérium igazolása érdekében minden állapotban tekintsünk a d értékére úgy, mint a hátralevő lépések számára adott felső korlátra: ez, mint látjuk a ciklusfeltétel teljesülése esetén pozitív és minden lépésben eggyel csökken az értéke, tehát a program biztosan meg fog állni. Tekintsük most azt a feladatot, amelynek állapottere az a:ℤ és bármelyik kiinduló egész számhoz a nullát rendeli célállapotként. Ezt
54
3.2. Programszerkezetek
nyilván megoldaná az a:=0 értékadás is, de mi most az alábbi ciklust vizsgáljuk meg. a 0 a 0
a 0
a:=a–1
a: ℕ
A leállási kritérium igazolásához itt a hátralevő lépések számára adott felső becslés módszere (amit az előző példában használtunk) nem alkalmazható. Egy tetszőleges kiinduló állapotra ugyanis csak akkor tudjuk felülről megbecsülni a hátralevő lépések számát, ha a kiinduló érték nem negatív. Ezt ugyanis a ciklusmag újra és újra eggyel csökkenti, amíg az nulla nem lesz. Ha azonban az a változó kiinduló értéke negatív, akkor ilyen becslés nem adható. A ciklusmag első végrehajtása során a változó értéke egy előre meghatározhatatlan nemnegatív egész szám lesz, amelyből tovább haladva a ciklus (az előzőekben elmondottak szerint) már biztosan le fog állni a nullában. A ciklus tehát véges lépésben biztos a kívánt célállapotban (a nullában) fog befejeződni, de nem minden kiinduló állapotra lehet megadni a hátralevő lépések számának felső korlátját. A haladási kritériumhoz viszont elegendő a semmitmondó azonosan igaz invariánst választani. Nyilvánvaló ugyanis, hogy ha a ciklus leáll, akkor azt kíván célban, a nulla értékű a-val teszi. 3.3. Helyes program, megengedett program A programtervezés célja az, hogy egy feladathoz olyan absztrakt (például struktogrammal leírt) programot adjon, amelyik megoldja a feladatot és ugyanakkor könnyen kódolható a konkrét programozási nyelveken. Ez előbbi kritériumot a program helyességének, az utóbbit a megengedhetőségének nevezzük. Ha egy program megold egy feladatot, akkor azt a feladat szempontjából helyes programnak hívjuk. Talán első olvasásra meglepőnek tűnik, de helyes programot nagyon könnyen lehet készíteni, hiszen minden feladat megoldható egyetlen alkalmas értékadás segítségével. Amikor ugyanis egy feladat
55
3. Strukturált programok
specifikációjában – közvetve vagy közvetlenül – meghatározzuk minden változónak a cél (célállapotbeli) értékét, tulajdonképpen kijelölünk egy értékadást, amelynek a feladat által előírt értékeket kell átadnia a megfelelő változóknak. Elméletben tehát minden feladathoz megkonstruálható az őt megoldó értékadás, amelyet a feladat triviális megoldásának nevezünk.8 Ilyen lehetne például a korábban ciklussal megoldott legnagyobb valódi osztót előállító feladat esetében a d:=LVO(n) értékadás. A programozási gyakorlatban azonban igen ritka, hogy egy feladatot egyetlen értékadással oldanánk meg. Ennek az oka az, hogy a triviális megoldás jobboldalán többnyire olyan kifejezés áll, amelyik a rendelkezésünkre álló programozási környezetben nem megengedett, azaz nem ismertek a kifejezésben szereplő műveletek vagy értékkonstansok, tehát a kifejezés közvetlenül nem kódolható. Habár a programtervezés során szabad, sőt célszerű elvonatkoztatni attól a programozási környezettől, ahol majd a programunknak működnie kell, de a végleges programterv nem távolodhat el túlságosan tőle, mert akkor nehéz lesz kódolni. Számunkra azok a programtervek a kívánatosak, amelyek egyfelől kellően általánosak (absztraktak) ahhoz, hogy előállításuknál ne ütközzünk minduntalan a programozási környezet által szabott korlátokba, másfelől bármelyik programozási nyelven könnyen kódolhatóak. Szerencsére a programozási környezetek fejlődésük során egyre inkább közelítettek és közelítenek egy hivatalosan ugyan nem rögzített, de a gyakorlatban nagyon is jól körvonalazott szabványhoz, amely kijelöli a legtöbb programozási nyelv által biztosított nyelvi elemeket (adattípusokat, programszerkezeteket). Célszerű a tervezés során készített programokat e szabványhoz, ezen ideális környezethez igazítani. Az ilyen programokat fogjuk a továbbiakban megengedett programoknak nevezni. 8
A modellünkben bevezetett feladatok általános leképezések, amelyekből jóval több van, mint az úgynevezett parciális rekurzív függvényekből. Említettük már, hogy csak ez utóbbiak megoldásához lehet olyan programokat készíteni, amelyek algoritmizálhatóak is. Modellünkben ellenben bármelyik feladathoz kreálható megoldó program (például a triviális megoldás), ezek között tehát bőven vannak olyanok, amely nem algoritmizálhatók.
56
3.3. Helyes program, megengedett program
Programozási modellünkben megállapodás alapján jelöljük ki a megengedett típusokat. Ide tartoznak a számok alaptípusai (természetes-, egész-, valós számok) a karakterek és a logikai értékek típusa. Megengedett típusokból megfelelő (azaz megengedett) eljárásokkal (úgynevezett konstrukciókkal) olyan összetett típusokat készíthetünk (például ugyanazon megengedett típusú értékeket tartalmazó egy vagy többdimenziós tömb, karakterekből összefűzött lánc vagy sztring), amelyeket ugyancsak megengedettnek tekintünk. Később (lásd 7. fejezet) tovább bővítjük a megengedett típusok körét. Egy kifejezés megengedett, ha azt megengedett típusok értékeiből, műveleteiből és ilyen típusú változókból alakítunk ki a megadott alaki szabályoknak (szintaktikának) megfelelően.9 Egy értékadás akkor megengedett, ha a jobboldalán szereplő kifejezés megengedett. A bevezetőben említett feladat finomítás során olyan elemi részfeladatokra kívánjuk bontani a megoldandó feladatot, amelyek megengedett értékadással megoldhatók, azaz olyan programmal, amelyet nem kell már mélyebben részletezni. Egy megengedett tagprogramokból felépülő szekvenciát megengedettnek tekintünk. Az elágazások és ciklusok feltételei logikai értékű kifejezések, amelyek szintén lehetnek megengedettek. Ha egy elágazást vagy ciklust megengedett feltételekből és megengedett programokból konstruálunk, akkor megengedettnek mondjuk. A megengedett program szekvencia, megengedett feltételeket használó elágazások és megengedett ciklusfeltételű ciklusok segítségével az üres programból és megengedett kifejezésű értékadásokból felépített program. Összefoglalva, egy programozási feladat megoldásán azt a programot értjük, amelyik helyes és megengedett.
9
A típusműveletek valójában leképezések, ezért általános esetben relációként írhatók fel. Maga a kifejezés is egy leképezés, amely a típusműveleteknek, mint leképezéseknek a kompozíciójaként (összetételeként) áll elő. A fenti meghatározásban említett alaki szabályok tehát jól definiáltak.
57
3. Strukturált programok
3.4. Feladatok 3.1. Fejezzük ki a SKIP, illetve az ABORT programot egy tetszőleges S program és valamelyik programszerkezet segítségével! 3.2. a) Van-e olyan program, amit felírhatunk szekvenciaként, elágazásként, és ciklusként is? b) Felírható-e minden program szekvenciaként, elágazásként, és ciklusként is? 3.3. a) Adjunk meg az A = (p:ℝ, q:ℝ, r:ℝ) állapottéren az r:=p+q értékadás által befutott végrehajtási sorozatokat! Adjunk meg olyan feladatot, amelyet ez az értékadás megold! b) Adjunk meg az A = (n:ℕ, m:ℕ) állapottéren az n,m:=3,n-5 értékadás által befutott végrehajtási sorozatokat! Adjunk meg olyan feladatot, amelyet ez az értékadás megold! 3.4. Adjunk meg olyan végrehajtási sorozatokat, amelyeket a (r:=p+q; p:=r/q) szekvencia befuthat az A= (p:ℝ, q:ℝ, r:ℝ) állapottéren! Adjunk meg olyan egyetlen értékadásból álló programot, amely ekvivalens ezzel a szekvenciával! 3.5. Igaz-e, hogy az alábbi három program ugyanazokat a feladatokat oldja meg? f1
f2
f3
S1
S2
S3
f1 S1
f1 SKIP
f2
f2 S2
f3 SKIP
S1
S2
S3
SKIP
f3 S3
SKIP
3.6. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott elágazásnak! Mutassa meg, hogy a feladatot megoldja a program!
58
3.4. Feladatok
A = (x:ℤ, n:ℕ) Ef = ( x=x’ ) Uf = ( n=abs(x’) ) x≥0
x 0
a:=x
a:=-x
3.7. Adjunk programot a valós együtthatós ax2+bx+c=0 alakú egyenletet megoldására! 3.8. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott ciklusnak! Mutassa meg, hogy a feladatot megoldja a program. (Segítségül megadtuk a ciklus egy invariáns állítását és a hátralevő lépésszámra adható felső becslést.) A = (x:ℤ) Ef = ( igaz ) Uf = ( x = 0 ) x 0
hátralevő lépések: abs(n)
x:=x – sgn(x) invariáns: igaz 3.9. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott programnak! Mutassa meg, hogy a feladatot megoldja a program. (Segítségül megadtuk a ciklus egy invariáns állítását és a hátralevő lépésszámra adható felső becslést.) A = (n:ℕ, f:ℕ) Ef = ( n=n’ ) Uf = ( f=n’! )
59
3. Strukturált programok
f:=1 n 0
hátralevő lépések: n
f:=f n
invariáns: f n!=n’!
n:=n-1 3.10. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott programnak! Mutassa meg, hogy a feladatot megoldja a program. (Segítségül megadtuk a ciklus egy invariáns állítását és a hátralevő lépésszámra adható felső becslést.) A = (x:ℕ, n:ℕ, z:ℕ) Ef = ( n=n’ x=x’ ) Uf = ( z=(x’)n’ ) z:=1 hátralevő lépések: n
n 0 n páros x,n:=x x, n/2
60
z,n:=z x, n-1
invariáns: z (x)n=(x’)n’
II. RÉSZ PROGRAMTERVEZÉS MINTÁK ALAPJÁN A programtervezés során az a célunk, hogy egy feladat megoldására helyes és megengedett programot találjunk. A helyesség azt biztosítja, hogy a program megoldása legyen a feladatnak, a megengedettség pedig arra ad garanciát, hogy a programot különösebb nehézség nélkül kódolni tudjuk a választott konkrét programozási nyelvre. Önmagában már az is nehéz, ha adott feladat és adott program viszonyában el kell döntenünk, hogy a program helyes-e, azaz megoldja-e a feladatot. Erre különféle módszerek vannak kezdve a teszteléstől a szigorú matematikai formalizmust igénylő helyességbizonyításig. Mindkét említett módszer azonban feltételezi, hogy már van egy programunk, pedig egy feladat megoldása kezdetén nem rendelkezünk semmilyen programmal, csak a feladat ismert. Megfelelő program előállításánál érdemes egy eleve helyes megoldásból, a feladat triviális megoldásából (értékadásból) kiindulni. Mivel azonban ez többnyire nem-megengedett, fokozatosan, lépésről lépésre (top-down) kell azt átalakítunk úgy, hogy a program helyessége minden lépés után megmaradjon, és e lépésenkénti finomítás végére megengedetté váljon. Az átalakítás közbülső formái, amelyeket éppúgy tekinthetünk a feladat egyfajta változatának, mint programnak, egyre összetettebb programszerkezetek, amelynek nem-megengedett értékadásait részfeladatoknak foghatjuk fel, és amelyeket mélyebben részletezett programmal kell kiváltani. A finomítási eljárás közbülső formáinak eme kettős jelentése, miszerint a benne szereplő nemmegengedett értékadások feladatok is és programok is, a feladatorientált programozási módszertan egyik fő jellegzetessége. Egy nem-megengedett értékadást vagy egy bonyolultabb szerkezetű programmal kell helyettesíteni, vagy az értékadás jobboldali kifejezésének kiszámításánál használt típusműveleteket kell megengedetté tenni, amihez adattípusokat kell konstruálunk. Az első eljárást funkció orientált programtervezésnek hívjuk (ehhez kapcsolódik a könyv II. része); a másodikat pedig típus orientált programtervezésnek nevezzük (ezzel a III. részben találkozunk). Ez a két eljárás nem zárja ki egymást, vegyesen alkalmazhatóak.
61
Ebben a részben egy olyan lépésenkénti finomítást mutatunk be, amely korábbi megoldások analógiájára épül. Ennek lényege az, hogy egy feladat megoldását egy ahhoz hasonló, korábban már megoldott feladat megoldására támaszkodva állítjuk elő. Ennek az analóg programozásnak egyik fajtája a visszavezetés, amihez a 4. fejezetben nevezetes minta-megoldásokat (úgynevezett programozási tételeket) vezetünk majd be, és ezek gyakorlatban történő alkalmazását az 5. fejezet példáival illusztráljuk. A 6. fejezetben olyan összetett feladatokat vizsgálunk, amelyek megoldása visszavezetéssel előállított részprogramokból épül fel.
62
4. Programozási tételek Az analóg módon történő programozás a programkészítés általánosan ismert és alkalmazott technikája. Amikor egy olyan feladatot kell megoldani, amelyhez hasonló feladatot már korábban megoldottunk, akkor a megoldó programot a korábbi feladat megoldó programja alapján állítjuk elő. Ez az ötlet, nem új keletű, nem kizárólag a programozásnál alkalmazott gondolat, hanem olyan általános problémamegoldó módszer, amellyel az élet minden területén találkozhatunk. Amikor egy tevékenységgel kapcsolatban gyakorlati tapasztalatról, megtanult ismeretekről beszélünk, akkor olyan korábban megoldott problémákra és azok megoldásaira gondolunk, amelyek az adott tevékenységgel kapcsolatosak. Minél nagyobb egy emberben ez a fajta ismeret, minél biztosabban tudja ezt alkalmazni egy új feladat megoldásánál, annál inkább tekintjük őt a szakmája mesterének. Nem meglepő hát, hogy a programozásban az itt tárgyalt úgynevezett analóg programozás szinte kivétel nélkül minden gyakorló programozó eszköztárában jelen van. Az analóg programozást lehet alkalmazni ösztönösen vagy tudatosan, elnagyoltan vagy precízen, laza ajánlásként vagy pontos technológiai szabványként. Az ebben a könyvben alkalmazott visszavezetéses módszerénél mind a kitűzött feladatnak, mind a korábban megoldott mintafeladatnak ismerjük a precíz leírását, a specifikációját, amely alkalmas arra, hogy a két feladat közötti analógiát pontosan felfedjük: könnyen felismerhetjük a hasonlóságot, ugyanakkor világosan felderíthetjük az eltéréseket. A visszavezetés során a mintafeladat megoldó programját (a mintaprogramot) sablonként használva állítjuk elő a kitűzött feladat megoldó programját úgy, hogy figyelembe vesszük a kitűzött- és a mintafeladat specifikációkban felfedett eltéréseket. Ebben a fejezetben először összevetjük a visszavezetést más analóg programozási technikákkal, majd megvizsgálunk néhány olyan nevezetes mintafeladatot és annak megoldó algoritmusát, (programozási tételt), amelyeket a visszavezetések során használhatunk.
63
4. Programozási tételek
4.1. Analóg programozás Az analóg programozási technikák alapgondolata közös, de azok gyakorlatában jelentős különbségek fedezhetőek fel. Ha az analóg programozásban érvényesülő eltérő szemléletmódokat gondolatban egy egyenes mentén képzeljük el, akkor ennek egyik végén az a gondolkodásmód áll, amelyiknél az új feladat megoldásakor a mintafeladat megoldásának előállítási folyamatát másoljuk le; a másik véglet viszont csak az eredményt, a megoldó programot veszi át, és azt igazítja hozzá az új feladathoz. Az első esetre példa az, amikor a mintafeladatot megoldó programot algoritmikus gondolkodás útján (milyen programszerkezetet használjunk, mi után mi következzen, mi legyen a ciklus vagy elágazás feltétele, stb.) állítottuk elő, és az ehhez hasonló feladatokat a mintafeladatnál alkalmazott gondolatsorral, annak ötleteit kölcsönözve, lemásolva, analóg algoritmikus gondolkodással oldjuk meg. Ettől lényeges különbözik az, amikor nem a mintaprogram előállítási folyamatát, az előállításának lépéseit másoljuk le, ismételjük meg, hanem csak magát a mintaprogramot adaptáljuk a kitűzött feladatra. Az analóg programozásnak ezt a technikáját hívjuk visszavezetésnek. Ennek hatékony alkalmazásához elengedhetetlen a feladatok pontos leírása, hiszen csak így lehet felfedni azokat a részleteket, amelyekben egyik feladat a másiktól eltér (az ördög a részletekben bújik meg), és csak ezek után tudjuk megmondani azt is, hogy a már megoldott mintafeladat programjában mely részleteket mire kell kicserélni ahhoz, hogy a kitűzött feladat megoldását megkapjuk. Ebben segít a feladat specifikációja. Ha a feladatokat (mind a kitűzött, mind a mintákat) specifikáljuk, akkor nemcsak a közöttük levő hasonlóságot tudjuk könnyen felismerni, hanem pontosan felderíthetőek az eltérések is. Amíg tehát az előbbi esetben a programtervezés hangsúlya az algoritmikus gondolkodáson van, addig az utóbbiban már nem úgy tekintünk a megoldó programra, mint egy működő folyamatra, annak tervezése a feladat elemzésén, tehát egyfajta statikus szemléleten alapul. Könyvünkben bemutatott módszertannak talán a legnagyobb vívmánya ez a precíz specifikációra épülő visszavezetési technika, amely az analóg programozást lényegesen gyorsabbá és biztonságosabbá teszi. Most algoritmikus gondolkodás segítségével megoldunk egy olyan feladatot, amely mintafeladatként fog szolgálni a későbbi
64
4.1. Analóg programozás
feladatok megoldásához, amelyek közül egyet analóg algoritmikus gondolkodással, a többit visszavezetéssel oldunk majd meg. 4.1. Példa. Adjuk össze egy n elemű egész értékű tömb elemeinek négyzeteit. Specifikáljuk először a feladatot. A = (v:ℤ n, s:ℤ) Ef = ( v=v’ ) n
Uf = ( Ef
v[i]2 )
s i 1
A megoldás során a kívánt összeget fokozatosan állítjuk. Ehhez végig kell vezetnünk egy i egész típusú változót a tömb indexein, és minden lépésben hozzá kell adni a kezdetben nullára beállított s változóhoz a v[i]2 értéket. A megoldó program tehát a kezdeti értékadások után (ahol az s változó értékét nullára, az i változóét egyre állítjuk) egy ciklus, amely az i változót egyesével növeli egészen n-ig. A ciklus működése alatt az s változó mindig a v tömb első i-1 elemének négyzetösszegét tartalmazza, Kezdetben, amikor az i értéke 1, ez az érték még nulla, befejezéskor, amikor az i eléri az n+1 értéket, már a végleges eredmény. s,i := 0,1 i n s : s v[i]2
i:ℕ Az n nem új változó, hanem a tömb hossza
i:=i+1 4.2. Példa. Számítsuk ki az n pozitív egész szám faktoriálisát, azaz az 1*2*…*n szorzatot! Írjuk fel először a feladat specifikációját. Az utófeltételben használjuk azt a produktum-kifejezést, amelynek segítségével egy n
m (m 1) ... (n 1) n szorzat a
i zárt alakban felírható. A
i m
jelölésről tudni kell, hogy ha m>n, akkor a produktum értéke 1. (Az 1
65
4. Programozási tételek
ugyanolyan szerepet tölt be az egész számok szorzásánál, mint a 0 az összeadásnál. Egy 0-hoz adott szám vagy egy 1-gyel megszorzott szám maga a szám lesz. Azt mondjuk, hogy a 0 az összeadásra vett nulla elem, míg az 1 a szorzás nulla eleme. Egy nulla elemmel történő műveletvégzés nem változtat azon az értéken, amelyre a művelet irányul.) A = (n:ℕ, f:ℕ) Ef = ( n=n’ ) n
Uf = ( Ef
f
i ) i 2
A kitűzött feladat és az 4.1. példa feladata hasonlít egymásra, ami a specifikációk összevetéséből kiválóan látszik. Ott a v[i]2 kifejezések (i=1..n) értékeit kellett összeadni és ezt az s változóban elhelyezni, itt a i számokat (i=2..n) összeszorozni és az f változóban tárolni. Készítsünk megoldást analóg algoritmikus gondolkodással! Figyeljük meg, hogy csak az aláhúzott szavak változtak meg 4.1. példa megoldásának gondolatmenetéhez képest. A megoldás során a kívánt szorzatot fokozatosan állítjuk elő egy ciklussal. Ehhez végig kell vezetnünk egy i egész típusú változót a 2..n tartomány indexein, és minden lépésben hozzá kell szorozni a kezdetben egyre beállított f változóhoz az i értéket. A megoldó program tehát a kezdeti értékadások után (ahol az f változó értékét egyre, az i változóét kettőre állítjuk) egy ciklus, amely az i változót egyesével növeli egészen n-ig. A ciklus működése alatt az f változó mindig az i-1 faktoriálisát tartalmazza, Kezdetben, amikor az i értéke 2, ez az érték még egy, befejezéskor, amikor az i eléri az n+1 értéket, már a végleges eredmény. f,i := 1,2
i:ℕ
i n f := f * i i := i+1 4.3. Példa. Adott két azonos dimenziójú vektor, amelyek valós számokat tartalmaznak. Számítsuk ki ezek skaláris szorzatát!
66
4.1. Analóg programozás
A feladat bemenő adatait két 1-től n-ig indexelt tömbben tároljuk, eredménye a skaláris szorzat; ezt tükrözi az állapottér. Az utófeltételben a skaláris szorzat definíciója jelenik meg. A = (a:ℝn, b:ℝn, s:ℝ) Ef = ( a=a’ b=b’ ) n
Uf = ( Ef
a[i] * b[i] )
s i 1
Ez a feladat nagyon hasonlít az első összegzési feladathoz (4.1. példa). Készítsünk megoldást visszavezetéssel! Vegyük számba két feladat közötti eltéréseket. A különbség az, hogy itt nem egy egész elemű tömb elemeinek önmagával vett szorzatait, a négyzeteit (v[i]2), hanem a két valós elemű tömb megfelelő elem párjainak szorzatait (a[i]*b[i]) kell összegezni úgy, hogy az i az 1..n tartományt futja be. Fogjuk meg a mintaként szolgáló 4.1. példa programját, és cseréljük le benne a v[i]2 kifejezést az a[i]*b[i] kifejezésre. Ezzel készen is vagyunk: s,i := 0,1
i:ℕ
i n s := s+a[i]*b[i] i := i+1 4.4. Példa. Igaz-e, hogy egy adott g:ℤ ℤ függvény az egész számok egy [m..n] nem üres intervallumán mindig páros számot vesz fel értékül, azaz g(m) is páros, g(m+1) is páros, és így tovább, g(n) is páros? Az ( g (m) páros ) ( g (m 1) páros ) ... ( g (n) páros ) összetett feltételt a feladat specifikációjának utófeltételében az
n i m
( g (i) páros )
zárt alakban írjuk fel. A jelölésről tudni kell, hogy ha m>n, akkor a kifejezés értéke igaz.
67
4. Programozási tételek
A = (m:ℤ, n:ℤ, l: ) Ef = ( m=m’ n=n’ ) n
s
Uf = ( Ef
i m
( g (i) páros ) )
A kitűzött feladat visszavezethető 4.1. példára. A specifikációk abban különböznek, hogy itt egész számok (v[i]2) összeadása helyett logikai állítások (g(i) páros) „összeéselésről” van szó. A logikai állítások „összeéselésének” nulla eleme a logikai igaz érték – ez az, amelyhez bármit „éselünk” is hozzá a bármit kapjuk meg. Eltér még ezen kívül az intervallum is: a korábbi 1..n helyett most az általánosabb m..n. Az eredményváltozó most s helyett az l. Tekintsük az 4.1. példa programját, és cseréljük le benne a v[i]2 kifejezést az g(i) páros logikai kifejezésre, a „+”-t az „ ”-re, a kezdeti értékadásban az l változónak a 0 helyett a logikai igaz értéket, az i változó kezdőértékének pedig az m-et adjuk. Táblázatos formában: [1..n] +, 0 s:=s+v[i]2
~ ~ ~
[m..n] , igaz l:= l (az g(i) páros)
l,i := igaz, m
i:ℤ
i n l:=l
(g(i) páros) i := i+1
4.5. Példa. Gyűjtsük ki egy n elemű (az n pozitív) egész számokat tartalmazó tömb páros értékeit egy halmazba! A = (x:ℤn, h:2ℤ) Ef = ( x=x’ ) n
Uf = ( h
{x[i ]} ) U i 1
x[i ] páros
A halmazok uniója is rokon művelet a számok feletti összeadással. Egységeleme az üres halmaz. A műveletben szereplő
68
4.1. Analóg programozás
kifejezés most egy úgynevezett feltételes kifejezés: ha az x[i] páros feltétel teljesül, akkor az {x[i]} egy elemű halmazt, különben az üres halmazt „uniózzuk” a h-hoz. Oldjuk meg visszavezetéssel a feladatot! Soroljuk fel az eltéréseket a 4.1. példa mintafeladata és a most kitűzött feladat között: +, 0 ~ , s ~ h 2 s:=s+ v[i] ~ h:= h (ha x[i] páros akkor x[i] különben ) Ezeket az eltéréseket átvezetve a mintaprogramra a kitűzött feladat megoldását kapjuk. Ez azonban nem megengedett, hiszen a feltételes értékadást nem tekintjük annak. A feltételes értékadás azonban nyilvánvalóan helyettesíthető egy elágazással. Ez a helyettesítés természetesen már nem a visszavezetés része, hanem egy azután alkalmazott átalakítás. h, i :=
i:ℕ
,1
i n x[i] páros h := h {x[i]}
SKIP
i := i+1 4.2. Programozási tétel fogalma Az előző alfejezetben látott feladatoknál úgy találtuk, hogy az eltérések ellenére ezek a feladatok annyira hasonlóak, hogy ugyanazon séma alapján lehet őket megoldani. De melyek is azok a tulajdonságok, amelyekkel feltétlenül rendelkeznie kell egy feladatnak, hogy az eddig látott feladatok közé sorolhassuk? Hogyan lehetne általánosan megfogalmazni egy ilyen feladatokat? A fenti feladatok egyik közös tulajdonsága az volt, hogy mindegyikben fontos szerepet töltött be az egész számok egy zárt intervalluma. Vagy egy 1-től n-ig indexelt tömbről volt szó, vagy egy adott g:ℤ ℤ függvénynek az egész számok egy [m..n] intervallumán felvett értékeiről, vagy egyszerűen csak az egész számokat kellett 1-től
69
4. Programozási tételek
n-ig összeszorozni. Ezek az intervallumok határozták meg, hogy a feladatok megoldásában oroszlánrészt vállaló ciklus úgynevezett ciklusváltozója mettől meddig „fusson”, azaz hányszor hajtódjon végre a ciklus magja. A másik fontos tulajdonság, hogy az intervallum elemeihez tartozik egy-egy érték, amit a számításban kell felhasználni. Az első feladatban az intervallum i eleméhez rendelt érték a v[i]2 szám, a másodikban maga az i, a harmadikban az a[i]*b[i], a negyedikben a g(i) páros logikai kifejezés, az ötödikben az {x[i]} egy elemű halmaz. Mindegyik esetben megadható tehát egy leképezés, egy függvény, amely az egész számok egy intervallumán értelmezett, és az intervallum elemeihez rendel valamilyen értéket: számot, logikai értéket, esetleg halmazt. A harmadik lényeges tulajdonság az a számítási művelet, amely segítségével az intervallum elemeihez rendelt értékeket összesíteni lehetett: összeadni, összeszorozni, „összeéselni”, uniózni. Ez a művelet éppen azon értékekre értelmezett, amelyeket az intervallum elemeihez rendelünk. Mindig létezett egy olyan érték, amelyhez bármelyik másik értéket adjuk-szorozzuk-éseljük-uniózzuk hozzá, az eredmény ez a másik érték lett: 0+s=s, 1*s=s, igaz l=l, h=h. Úgy mondjuk, hogy mindegyik műveletnek van (baloldali) nulla eleme. A vizsgált műveletek egymás utáni alkalmazásának eredménye nem függött a végrehajtási sorrendtől, az tetszőlegesen csoportosítható, zárójelezhető (például az a+b+c értelmezhető a+(b+c)-ként és (a+b)+c-ként is, és mi ez utóbbit használtuk), azaz a vizsgált műveletek asszociatívak voltak. Foglaljuk össze ezeket a közös tulajdonságokat, és fogalmazzuk meg azt az általános feladatot, amely az előző alfejezetben közölt összes feladatot magába foglalja. Legyen adott az egész számok egy [m..n] intervallumán értelmezett f:[m..n] H függvény (erről általános esetben nem kell tudni többet), amelynek H értékkészletén definiálunk egy műveletet. Az egyszerűség kedvéért hívjuk ezt összeadásnak, de ez nem feltétlenül a számok megszokott összeadási művelete, minthogy a H sem feltétlenül egy számhalmaz. A művelet lehet a számok feletti szorzás, logikai állítások feletti „éselés”, halmazok feletti uniózás, stb. Ami a lényeg: a művelet legyen asszociatív és rendelkezzen (baloldali) nulla elemmel. A feladat az, hogy határozzuk meg az f függvény [m..n]-en felvett értékeinek az összegét (szorzatát, „éseltjét”, unióját stb.) azaz a
70
4.2. Programozási tétel fogalma
n
f (k )
f (m)
f (m 1) ...
f (n) kifejezés értékét! (m>n esetén
k m
ennek az értéke definíció szerint a nulla elem). Az előző alfejezet példái mind ráillenek erre a feladatra. Ezt az általánosan megfogalmazott feladatot specifikálhatjuk is. Az állapottérbe a függvény értelmezési tartományának végpontjait és az eredményt vesszük fel, előfeltétele rögzíti a bemenő adatokat, utófeltétele pedig az összegzési feladatot írja le. A = (m:ℤ, n:ℤ, s:H) Ef = ( m=m’ n=n’ ) n
Uf = ( Ef
f (i) )
s i m
A kívánt összeget fokozatosan állítjuk elő. Ehhez végig kell vezetnünk egy i egész típusú változót a tömb indexein, és minden lépésben hozzá kell adni a kezdetben nullára beállított s változóhoz az f(i) értéket. A megoldó program tehát a kezdeti értékadások után (ahol az s változó értékét nullára, az i változóét m-re állítjuk) egy ciklus, amely az i változót egyesével növeli egészen n-ig. A ciklus működése alatt az s változó mindig az f függvény [m..i–1] intervallumon felvett értékeinek összegét tartalmazza, Kezdetben, amikor az i értéke 1, ez az érték még nulla, befejezéskor, amikor eléri az n+1 értéket, már a végleges eredmény. s, i := 0, m
i:ℤ
i n s := s + f(i) i := i + 1 Az most bemutatott feladat-program pár egy nevezetes minta: az úgynevezett összegzés. Ez, mint azt az előző alfejezetben láthattuk, számos feladat megoldásához szolgálhat alapul. A visszavezetés során mintaként használt feladat-program párt (ahol a program megoldja a feladatot) programozási tételnek hívjuk. Az elnevezés onnan származik, hogy egy mintafeladat-program párt
71
4. Programozási tételek
egy matematikai tételhez hasonlóan alkalmazunk: ha egy kitűzött feladat hasonlít a minta feladatára, akkor a minta programja lényegében megoldja a kitűzött feladatot is. A "hasonlít" és a "lényegében" szavak értelmét az előző példák alapján már érezzük, de majd az 5.1. alfejezetben részletesen is kitérünk értelmezésükre. A visszavezetés sikere azon múlik, hogy rendelkezünk-e kellő számú, változatos és eléggé általános mintafeladattal ahhoz, hogy egy új feladat ezek valamelyikére hasonlítson. Ezért a mintafeladatokat és megoldásaikat gondosan kell megválasztani. Természetesen minden programozó maga dönti el azt, hogy munkája során milyen programozási tételekre támaszkodik – ezek mennyisége és minősége a programozói tudás, a szakértelem fokmérője –, ugyanakkor meghatározható a programozási tételeknek az a közös része, amit a többség ismer és használ, amelyek feladatai a programozási feladatok megoldásánál gyakran előfordulnak. Alig több mint fél tucat ilyen programozási tétellel a gyakorlatban előforduló problémák többsége, nyolcvan-kilencven százaléka lefedhető. A különféle programozási módszertant tanító iskolák lényegében ugyanazokat a programozási tételeket vezetik be, legfeljebb azok megfogalmazásában, csoportosításában és felhasználási módjukban van az iskolák között eltérés. Ott, ahol a programozási tételeket nem visszavezetésre használják, mert az analóg programozás másik irányzatát, az algoritmikus gondolkodás folyamatának lemásolását követik, egy tétel megfogalmazása kevésbé precíz: annak több változata is lehetséges. A visszavezetésnél viszont a tételeket sokkal pontosabban, és lehetőleg minél általánosabban kell megadni. Egy tétel ugyanis valójában nem egy konkrét feladatot, hanem egy feladatosztályt ír le, és minél általánosabb, annál több feladat illeszkedik ehhez a feladatosztályhoz. Vigyázni kell azonban arra, hogy ha a mintafeladat túl elvont, akkor nehéz lehet annak alkalmazása. 4.3. Nevezetes minták Most nyolc, a szakirodalomban jól ismert programozási tételt mutatunk be. Mindegyiküknél az egész számok egy intervallumán értelmezett függvény (vagy függvények) értékeinek feldolgozása a cél. Ezért ezeket a tételeket intervallumos programozási tételeknek is szoktuk nevezni.
72
4.3. Nevezetes minták
A nyolc tétel közül egyedül a lineáris kiválasztáshoz nem kapcsolható nyilvánvaló módon az egész számok egy intervalluma. Ez egy olyan keresés, amelyik az egész számok számegyenesen folyik, de elég megadni a keresés kezdőpontját, a keresés addig tart, amíg a keresési feltétel (egy egész számokon értelmezett logikai függvény) igaz nem lesz. Erről azonban tudjuk, hogy előbb-utóbb bekövetkezik. Itt tehát a keresés során egy olyan intervallumot járunk be, amelynek a végpontja csak implicit módon van megadva. Az intervallumokra az egyes programozási tételek különféle előfeltételeket fogalmaznak meg. Nem lehet üres az intervallum a rekurzív függvény helyettesítési értékének meghatározásakor, illetve a közönséges maximum kiválasztásnál. A maximum kiválasztásnak van egy általánosabb változata is (feltételes maximumkeresés), amely csak egy logikai feltételnek megfelelő egész számokon felvett függvényértékek között keresi a legnagyobbat, és itt az is megengedett, ha az intervallum egyetlen egy egész száma sem elégíti ki ezt a feltételt, vagy az intervallum üres. Egy logikai változó jelzi majd, hogy egyáltalán volt-e a feltételt kielégítő vizsgált függvényérték. Az alábbi programozási tételek f(i), (i), h(i, y, y1, ... , yk–1) kifejezései az f, , h függvények adott argumentumú helyettesítési értékeit jelölik. Feltesszük, hogy ezek a helyettesítési értékek kiszámíthatóak.
73
4. Programozási tételek
1. Összegzés Az összegzés programozási tételét az előző alfejezetekben alaposan elemeztük. Most csak a teljesség igénye miatt ismételjük meg. Feladat: Adott az egész számok egy [m..n] intervalluma és egy f:[m..n] H függvény. A H halmaz elemein értelmezett egy asszociatív, baloldali nulla elemmel rendelkező művelet (nevezzük összeadásnak és jelölje ezt a +). Határozzuk meg az f függvény [m..n]-en felvett értékeinek az összegét, azaz a
n
f(k) kifejezés értékét! (m>n esetén
k m
ennek az értéke definíció szerint a nulla elem). Specifikáció: A = (m:ℤ, n:ℤ, s:H) Ef = ( m=m’ n=n’ ) n
Uf = ( Ef
f(i) )
s i m
Algoritmus: s, i := 0, m i n s := s + f(i) i := i + 1
74
i:ℤ
4.3. Nevezetes minták
2. Számlálás Gondoljunk arra a feladatra, amikor egy 20 fős osztályban azon diákok számát kell megadnunk, akik ötöst kaptak történelemből. A diákokat 1-től 20-ig megsorszámozzuk, és a történelem jegyeiket egy 1-től 20-ig indexelt t tömbben tároljuk: az i-edik diák osztályzata a t[i]. Definiálunk egy :[1..20] logikai függvényt, amely azokhoz a diákokhoz rendel igaz értéket, akik ötöst kaptak, más szóval (i) = (t[i]=5). Azt kell meghatároznunk, hogy a által felvett értékek között hány igaz érték szerepel. Ilyenkor valójában 0 illetve 1 értékeket összegzünk attól függően, hogy a logikai kifejezés hamis vagy igaz. A számlálás tehát az összegzés speciális eseteként fogható fel, amelyben az s:=s+1 értékadást csak a (i) feltétel teljesülése esetén kell végrehajtani. Feladat: Adott az egész számok egy [m..n] intervalluma és egy :[m..n] feltétel. Határozzuk meg, hogy az [m..n] intervallumon a feltétel hányszor veszi fel az igaz értéket! (Ha m>n, akkor egyszer sem.) Specifikáció: A = (m:ℤ, n:ℤ, c:ℕ) Ef = ( m=m’ n=n’ ) n
Uf = ( Ef
c
1) i m β (i)
Algoritmus: c, i := 0, m
i:ℤ
i n (i) c := c+1
SKIP
i := i + 1
75
4. Programozási tételek
3. Maximum kiválasztás Ha arra vagyunk kíváncsiak, hogy egy 20 fős osztályban kinek van a legjobb jegye történelemből, akkor ehhez a diákokat 1-től 20-ig megsorszámozzuk, és a történelem jegyeiket egy 1-től 20-ig indexelt t tömbben tároljuk: az i-edik diák osztályzata a t[i]. Ez a tömb tekinthető egy f:[1..20] ℕ függvénynek (f(i)=t[i]), amely értékei között keressük a legnagyobbat. Feladat: Adott az egész számok egy [m..n] intervalluma és egy f:[m..n] H függvény. A H halmaz elemein értelmezett egy teljes rendezési reláció. Határozzuk meg, hogy az f függvény hol veszi fel az [m..n] nem üres intervallumon a legnagyobb értéket, és mondjuk meg, mekkora ez a maximális érték! Specifikáció: A = (m:ℤ, n:ℤ, ind:ℤ, max:H) Ef = ( m=m’ n=n’ n m ) Uf = ( Ef ind [m..n] max=f(ind) i [m..n]: max f(i) ) = másképpen jelölve, illetve rövidített jelölést használva: = ( Ef
n
max = f(ind) = max f(i) )
ind [m..n]
i m
= ( Ef
n
max,ind= max f(i) ) i m
Algoritmus: max, ind, i := f(m), m, m+1
i:ℤ
i n max
SKIP
i := i + 1 Az ind változó értékadásai törölhetők az algoritmusból, ha nincs szükség az ind-re.
76
4.3. Nevezetes minták
4. Feltételes maximumkeresés A maximum kiválasztást sokszor úgy kell elvégezni, hogy a vizsgált elemek közül csak azokat vesszük figyelembe, amelyek eleget tesznek egy adott feltételnek. Például, egymás utáni napi átlaghőmérsékletek között azt a legmelegebb hőmérsékletet keressük, amelyik fagypont alatti. Feladat: Adott az egész számok egy [m..n] intervalluma, egy f:[m..n] H függvény és egy :[m..n] feltétel. A H halmaz elemein értelmezett egy teljes rendezési reláció. Határozzuk meg, hogy az [m..n] intervallum feltételt kielégítő elemei közül az f függvény hol veszi fel a legnagyobb értéket, és mondjuk meg, mekkora ez az érték! (Lehet, hogy egyáltalán nincs feltételt kielégítő elem az [m..n] intervallumban vagy m>n.) Specifikáció: A = (m:ℤ, n:ℤ, l: , ind:ℤ, max:H) Ef = ( m=m’ n=n’ ) Uf = ( Ef l = i [m..n]: (i) l ind [m..n] max=f(ind) (ind) i [m..n]: (i) max f(i) ) Itt is bevezetünk rövid jelölést. n
Uf = ( Ef
l,max,ind) = max f(i) ) i m (i)
Algoritmus: l, i := hamis, m
i:ℤ
i n (i) SKIP
l
(i)
l
(i)
max< f(i)
l, max, ind := igaz, f(i), i max, ind := f(i), i SKIP i := i + 1
77
4. Programozási tételek
5. Kiválasztás (szekvenciális vagy lineáris kiválasztás) Ennek a mintának kapcsán azokra a feladatokra gondoljunk, ahol egymás után sorba állított elemek között keressük az első adott tulajdonságút úgy, hogy tudjuk, hogy ilyen elem biztosan van. Például egy osztályban keressük az első ötös történelem jeggyel rendelkező diákot, ha tudjuk, hogy ilyen biztosan van. A diákok történelem jegyeit ilyenkor egy 1-től 20-ig indexelt t tömbben tároljuk (az i-edik diák osztályzata a t[i]), ha a diákokat 1-től 20-ig sorszámoztuk meg. A keresés szempontjából közömbös, hogy egy 20 fős osztállyal van dolgunk. Definiálunk egy :[1..20] logikai függvényt, amely azokhoz a diákokhoz rendel igaz értéket, akik ötöst kaptak, más szóval (i) = (t[i]=5). Feladat: Adott egy m egész szám és egy m-től jobbra értelmezett :ℤ feltétel. Határozzuk meg, hogy az m-től jobbra eső első olyan számot, amely kielégíti a feltételt, ha tudjuk, hogy ilyen szám biztosan van! Specifikáció: A = (m:ℤ, ind:ℤ) Ef = ( m=m’ i m: (i) ) Uf = ( Ef ind m (ind) i [m..ind–1]: (i) ) vagy rövidített jelölést alkalmazva: Uf = ( Ef
ind
select (i) ) i m
Algoritmus: ind := m (ind) ind := ind + 1 Az előző tételekkel szemben itt nincs szükség a bejárt száok intervallumának felső határára, mintahogy egy külön i változóra sem a számok bejárásához.
78
4.3. Nevezetes minták
6. Keresés (szekvenciális vagy lineáris keresés) Ez a minta hasonlít az előzőre, hiszen itt is egymás után sorba állított elemek között keressük az első adott tulajdonságút, de lehet, hogy ilyet egyáltalán nem találunk. Feladat: Adott az egész számok egy [m..n] intervalluma és egy :[m..n] feltétel. Határozzuk meg az [m..n] intervallumban balról az első olyan számot, amely kielégíti a feltételt! Specifikáció: A = (m:ℤ, n:ℤ, l: , ind:ℤ) Ef = ( m=m’ n=n’ ) Uf = ( Ef l=( i [m..n]: (i)) l (ind [m..n] (ind) i [m..ind–1]: (i)) ) vagy rövidített jelölést alkalmazva n
Uf = ( Ef
search (i) )
l , ind
i m
10
Algoritmus : l, i := hamis, m l
i:ℤ
i n
l, ind := (i), i i := i+1
10
A lineáris keresés algoritmusának más változatai is ismertek:
l, ind := hamis, m–1 l
ind
ind := ind+1 l := (ind)
l, ind := hamis, m l
ind n (ind)
l:=igaz ind := ind+1
ind := m ind n
(ind)
ind := ind+1 l := ind n
79
4. Programozási tételek
7. Logaritmikus keresés Speciális feltételek megléte esetén a lineáris keresésnél jóval gyorsabb kereső algoritmust használhatunk. Amíg a lineáris keresés egy h elemű intervallumot legrosszabb esetben h lépésben tud átvizsgálni (a lépések száma az elemszám lineáris függvénye), addig a most bemutatásra kerülő keresésnek legrosszabb esetben ehhez csak log2h lépésre van szüksége. A logaritmikus kereséssel növekedően rendezett értékek között kereshetünk egy adott értéket. Ennek során a keresési intervallumot felezzük el, és a felező pontnál levő értéket összevetjük a keresettel. Ha megegyeznek, akkor megtaláltuk a keresett értéket, ha a vizsgált érték nagyobb a keresett értéknél, akkor a keresett érték (ha egyáltalán ott van az értékek között) a keresési intervallum első felében található, ha kisebb, akkor a második felében, azaz a keresés intervalluma elfelezhető. Feladat: Adott az egész számok egy [m..n] intervalluma és egy f:Z H függvény, amelyik az [m..n] intervallumon monoton növekvő. (A H halmaz elemei között értelmezett egy rendezési reláció.) Keressünk meg a függvény [m..n] intervallumon felvett értékei között egy adott értéket! Specifikáció: A = (m:ℤ, n:ℤ, h:H, l: , ind:ℤ) Ef = ( m=m’ n=n’ h=h’ j [m..n-1]: f(j) f(j+1) ) Uf = ( Ef l=( j [m..n]:f(j)=h) l (ind [m..n] f(ind)=h)) Algoritmus: ah,fh,l := m,n,hamis l
ah, fh:ℤ
ah fh
ind := (ah+fh) div 2
80
f(ind)>h
f(ind)
f(ind)=h
fh := ind-1
ah := ind+1
l := igaz
4.3. Nevezetes minták
8. Rekurzív függvény kiszámítása Tekintsük a Fibonacci számokat: 1, 1, 2, 3, 5, 8, 12,…. Ezeket az alábbi Fib:ℕ ℕ függvénnyel állíthatjuk elő. Az első két Fibonacci szám definíció szerint az 1, azaz Fib(1)=1 és Fib(2)=1. A további Fibonacci számokat a megelőző két Fibonacci szám összegeként kapjuk meg: Fib(i)=Fib(i–1)+Fib(i–2). Ha az n-edik Fibonacci számhoz szeretnénk eljutni (n>2), akkor először a harmadik, utána a negyedik és így tovább Fibonacci számot kell előállítani, azaz végig kell mennünk a 3..n intervallumon. Egy másik példa az n faktoriálisának meghatározása. Ezt az a Fact:ℕ ℕ függvény adja meg, amely a 0-hoz definíció szerint az 1-et rendel, az 1-nél nagyobb számok faktoriálisa pedig a megelőző faktoriális segítségével állítható elő: Fact(i)=i*Fact(i–1). Az n szám faktoriálisának meghatározásához (n>0) sorban egymás után meg kell határoznunk a 1..n intervallum elemeinek faktoriálisát. Feladat: Legyen az f:ℤ H egy k-ad rendű m bázisú rekurzív függvény (m egész szám, k pozitív egész szám), azaz f(i) = h(i, f(i–1), ... ,f(i–k) ) ahol i≥m és a h egy ℤ×Hk H függvény. Ezen kívül f(m–1) = em–1, ... , f(m–k) = em–k, ahol em-1, ... , em–k H-beli értékek. Számítsuk ki az f függvény adott n (n≥m) helyen felvett értékét! Specifikáció: A = (n:ℤ, y:H) Ef = ( n=n’ n m ) Uf = ( Ef y=f(n) ) Algoritmus: segédváltozók: y1, ... , yk–1 , i:ℤ y, y1, ... , yk–1 , i := em–1, em–2, ... , em–k, m i n y, y1, y2,... , yk–1 := h(i, y, y1, ... , yk–1), y, y1, ... , yk–2 i := i + 1
81
4. Programozási tételek
A kiválasztás, lineáris keresés és a logaritmikus keresés kivételével a fenti algoritmus minták ciklusszervezése megegyezik: egy indexváltozót vezetnek végig az m..n intervallumon. Az ilyen esetekben alkalmazhatunk egy rövidített jelölést az algoritmus leírására. Ezt a változatot szokták számlálós ciklusnak nevezni. Az elnevezés kicsit félrevezető, hiszen, mint látjuk, ez egy kezdeti értékadásnak és egy ciklusnak a szekvenciája. A számlálós ciklus alkalmazhatóságának jelentőségét egyrészt az adja, hogy ilyenkor pontosan meg lehet mondani, hány iteráció után fog leállni a ciklus, másrészt a különféle programozási nyelvek külön nyelvi elemet (például „for”) szoktak biztosítani a kódolásukhoz. i := m i n feldolgozás
rövidebben
i = m .. n feldolgozás
i := i + 1 A továbbiakban ott, ahol lehetőség van rá, mi is ezt a rövidebb változatot fogjuk használni. A programozási tételek helyességét a 3. fejezetben bemutatott eszközökkel be lehet bizonyítani. Ezen bizonyításoknak sarkalatos része a programok ciklusainak vizsgálata. A ciklusok helyes működéséhez azok haladási és leállási kritériumát kell igazolni. A leállási kritériumot könnyű belátni az első négy valamint a kilencedik tételnél, hiszen ezeknél az egész számok egy intervallumát futja be egy számlálós ciklus. A kiválasztás esetében az előfeltétel (biztos találunk véges lépésben keresett tulajdonságú egész számot) biztosítja a terminálást, a lineáris keresésnél legrosszabb esetben az intervallum végéig tart a keresés. A logaritmikus keresés legfeljebb addig tart, amíg az általa kézben tartott [ah..fh] intervallum nem üres, ugyanakkor ez az intervallum minden lépésben kisebb és kisebb lesz (feltéve, hogy nem találunk keresett elemet, de ebben az esetben rögtön leáll a ciklus). A haladási kritérium részletes bizonyításával nem foglalkozunk, de jellemezzük ezen bizonyítás nélkülözhetetlen eszközét, a tételek
82
4.3. Nevezetes minták
ciklusainak invariáns tulajdonságát. Az első négy programozási tétel esetében a ciklus invariánsa az utófeltétel állításának egy olyan gyengítése, amelyik nem a teljes [m..n] intervallumra, hanem csak az [m..i-1] intervallumra vonatkozik: az egyes tételeknél rendre azt fejezi ki, hogy a ciklus működése során ezen részintervallum feletti összeggel, darabszámmal vagy maximummal rendelkezünk csak. Látható, ahogy nő az i értéke, úgy kerülünk egyre közelebb az utófeltétel állításához. A rekurzív függvény helyettesítési értékének kiszámolásakor is hasonló a helyzet: az invariáns a függvény i-1-dik helyen felvett értékét tekinti ismertnek, ez az eredmény változó aktuális értéke. A kiválasztásnál az invariáns azt mutatja, hogy eddig még nem találtunk keresett tulajdonságú elemet. A lineáris és logaritmikus keresés esetében az invariáns egyrészt azt tartalmazza, hogy a korábban vizsgált helyeken nem teljesül a keresett feltétel, másrészt azt, hogy a keresés eredményét jelző logikai változó pontosan akkor igaz, ha a legutoljára vizsgált helyen a feltétel teljesül.
83
4. Programozási tételek
4.4. Feladatok Specifikáljuk az alábbi feladatokat, vessük össze azokat a programozási tételek specifikációival és próbáljuk meg ennek figyelembevételével felírni a megoldó programokat! 4.1. Számoljuk ki két természetes szám szorzatát, ha a szorzás művelete nincs megengedve! 4.2. Keressük meg egy természetes szám legkisebb páros valódi osztóját! 4.3. Válogassuk ki egy egész számokat tartalmazó tömbből a páros számokat! 4.4. Egymást követő napokon megmértük a déli hőmérsékletet. Hányadikra mértünk először 0 Celsius fokot azelőtt, hogy először fagypont alatti hőmérsékletet regisztráltunk volna? 4.5. Egy tömbben a tornaórán megjelent diákok magasságait tároljuk. Keressük meg a legmagasabb diák magasságát! 4.6. Az f:[m..n] ℤ függvénynek hány értéke esik az [a..b] vagy a [c..d] intervallumba? 4.7. Egy tömbben színeket tárolunk a színskála szerinti növekvő sorrendben. Keressük meg a tömbben a világoskék színt! 4.8. A Föld felszínének egy vonala mentén egyenlő távolságonként megmértük a terep tengerszint feletti magasságát (méterben), és a mért értékeket egy tömbben tároljuk. Melyik a legmagasabban fekvő horpadás a felszínen? 4.9. Egymást követő napokon megmértük a déli hőmérsékletet. Hányszor mértünk 0° Celsiust úgy, hogy közvetlenül utána fagypont alatti hőmérsékletet regisztráltunk? 4.10. Egy n elemű tömbben tárolt egész számoknak az összegét egy rekurzív függvénv n-edik helyen felvett értékével is megadhatjuk. Definiáljuk ezt a rekurzív függvényt és oldjuk meg a feladatot a rekurzív függvény helyettesítési értékét kiszámoló programozási tétel segítségével!
84
5. Visszavezetés Ebben az alfejezetben példákat látunk a visszavezetésre, nevezetesen arra, amikor a programozási tételek mintájára oldjuk meg a feladatainkat, és közben azt próbáljuk megvilágítani, hogy a visszavezetéssel előállított programok miért lesznek biztosan megoldásai a kitűzött feladatoknak. Az alább bemutatott esetek egymással kombinálva is megjelenhetnek a visszavezetések során. 5.1. Természetes visszavezetés Természetes visszavezetésről akkor beszélünk, amikor az alkalmazott programozási tétel feladata (a mintafeladat) átnevezésektől eltekintve teljesen megegyezik a megoldandó (a kitűzött) feladattal. Háromféle átnevezéssel találkozhatunk. Egyrészt a kitűzött feladat változóit nem kell ugyanúgy hívni, mint a mintafeladatban, de attól a feladat ugyanaz marad. Másrészt a mintafeladat általános (jelentés nélküli) kifejezéseinek (ilyen az f(i), (i) ) helyére az adott feladat konkrét jelentéssel bíró és az i értékétől függő kifejezéseket írhat be. Harmadrészt gyakran előfordul, hogy a mintafeladat utófeltételének egy-egy állandó értékű kifejezését (például az intervallum végpontját) egy másik állandó értékű kifejezésre cseréljük. Az alábbi feladat megoldásánál mindhárom esettel találkozhatunk. Mivel ezek az átnevezések a programozási tétel helyességét nem rontják el, az átnevezések eredményeként kapott program nyilvánvalóan megoldja a mintafeladattól csak átnevezéseiben eltérő kitűzött feladatot. 5.1. Példa. Hányszor vált előjelet (pozitívról negatívra vagy negatívról pozitívra) az [a..b] egész intervallumon az f:ℤ ℝ függvény? A = (a:ℤ, b:ℤ, db:ℕ) Ef = ( a=a’ b=b’ ) b
Uf = ( Ef
db
1) j a 1 f(j)* f(j 1) 0
Ez a specifikáció olyan, mint a számlálás programozási tételének specifikációja. A különbség csak annyi, hogy az ott szereplő feltételt itt nem az [m..n], hanem az [a+1..b] intervallumon értelmezzük; az
85
5. Visszavezetés
intervallumon nem az i, hanem a j változóval futunk végig; az s változót a db változó helyettesíti; és a (i) helyén az f(j)*f(j-1)<0 kifejezés áll. Alkalmazzuk ezt a megfeleltetést a számlálás programjára és megkapjuk a kitűzött feladat megoldását. m..n s i (i)
~ ~ ~ ~
a+1..b db j f(j)*f(j-1)<0 db := 0
j = a+1 .. b
j:ℤ
f(j) * f(j-1) < 0 db := db+1
SKIP
5.2. Általános visszavezetés Általános visszavezetésről akkor beszélünk, amikor a kitűzött feladat megengedőbb a mintafeladatánál. Ez kétféleképpen is előállhat. Egyfelől akkor, ha a kitűzött feladat kevesebb kezdőállapotra van értelmezve, mint a mintafeladat (azaz a kitűzött feladat előfeltétele szigorúbb a mintafeladaténál). Ez nyilván nem okoz problémát, hiszen senkit sem érdekel, hogy a programozási tétel programja a számunkra érdekes kezdőállapotokon kívül más kezdőállapotokra mit csinál. Erre példa, ha a kitűzött feladat a természetes számok egy intervallumán van megfogalmazva, de azt az egész számok intervallumára felírt programozási tételre vezetjük vissza. Másfelől a kitűzött feladat egy kezdőállapothoz olyan célállapotot is rendelhet, amelyet a mintafeladat nem (azaz a feladat utófeltétele gyengébb a mintafeladaténál). Mivel egy megoldó programnak elég több lehetséges célállapotból az egyiket elérni, így a programozási tétel szigorúbb mintafeladatot megoldó programja megoldja a „megengedőbb” kitűzött feladatot is. Ez utóbbira ad példát az alábbi feladat, ahol egy adott tulajdonságú elemet kell megtalálnunk (ilyen több is lehet), de a lineáris kereséssel ezek közül a legelső adott tulajdonságút fogjuk megkeresni. Természetesen az olyan
86
5.2. Általános visszavezetés
eltérések, mint amelyeket a természetes visszavezetésnél említettünk itt is előfordulhatnak. 5.2. Példa. Keressük egy f:[m..n] ℝ függvénynek azt az arumentumát, amelyre teljesül, hogy f(k)=(f(k-1)+ f(k+1))/2. A = (m:ℤ, n:ℤ, l: , ki:ℤ ) Ef = ( m=m’ n=n’ ) Uf = ( Ef l=( i [m+1..n-1]: f(i)=(f(i–1)+f(i+1))/2 ) l ( ki [m+1..n-1] f(ki)=(f(ki-1)+f(ki+1))/2 ) ) Szigorítsunk a feladaton. Ha a szigorúbb feladathoz találunk megoldást, akkor ez az eredeti feladatot is megoldja. Uf = ( Ef l=( i [m+1..n-1]: f(i)=(f(i–1)+f(i+1))/2 ) l ( ki [m+1..n-1] f(ki)=(f(ki-1)+f(ki+1))/2 i [m+1..ki-1]: f(i) (f(i–1)+f(i+1))/2 ) )
l , ki
= ( Ef
n 1
search( f(i) i m 1
( f(i 1)
f(i 1)) /2 ) )
Ez a feladat visszavezethető a lineáris keresés programozási tételére. m..n (i) ind
~ ~ ~
m+1..n-1 f(i)=(f(i–1)+ f(i+1))/2 ki
l, i := hamis, m+1 l
i n-1
i:ℤ
l, ki := f(i)=(f(i–1)+ f(i+1))/2, i i := i+1
5.3. Alteres visszavezetés Alteres visszavezetésről akkor beszélünk, amikor a kitűzött feladat állapottere a mintafeladat állapotterének nem minden komponensét tartalmazza (altere a kitűzött feladat állapotterének), azaz
87
5. Visszavezetés
a kitűzött feladat állapotterének kevesebb komponenssel bír. Ennek két figyelemre méltó alesete van. (Ezen túlmenően a természetes visszavezetésnél megengedett eltérések is előfordulhatnak.) Az egyik eset az, amikor a kitűzött feladat kevesebb eredmény komponenst tartalmaz, mint a mintafeladat. A programozási tételeink között ugyanis vannak olyanok, amelyek egyszerre több eredményt is adnak, de gyakori, hogy egy konkrét feladatban ezek közül nincs mindre szükségünk. Ezek közül egyiket-másikat el lehet hagyni a feladat állapotteréből, ha arra nincs szükség, hiszen egy eredmény komponens kezdőértékétől nem függ a többi eredmény. Természetesen a minta feladatból így „elhagyott” komponensnek a változója ott marad a programozási tétel programjában, de most már csak, mint segédváltozó, és a program alap-állapottere a kitűzött feladat állapotterével lesz azonos. A két feladat előfeltétele megegyezik (hiszen az eredménykomponensek sohasem szerepelnek az előfeltételben), a mintafeladat utófeltétele szigorúbb (mert az elhagyott komponensekre is tesz megkötéseket), de ez – az előző alfejezet szerint – nem jelent akadályt. A program tehát megoldja a kitűzött feladatot. Tipikus példa erre az, amikor egy maximum kiválasztásnál a maximális érték nem érdekel bennünket, csak az intervallumnak azon eleme, amelyhez a maximum tartozik. 5.3. Példa. Keressük egy g:[a..b] ℝ függvénynek olyan argumentumát, ahol a függvény a maximális értékét veszi fel!. A = (a:ℤ, b:ℤ, ind:ℤ ) Ef = ( a=a’ b=b’ a≤b ) Uf = ( Ef
ind [a..b]
b
g(ind) = max g(i) ) i a
A kitűzött feladat állapottere kiegészíthető komponenssel, az utófeltétele pedig szigorítható Uf = ( Ef
b
max = max g(i)
ind [a..b]
a
max:ℝ
g(ind) = max )
i a
és ez a szigorúbb feladat visszavezethető a maximum kiválasztás programozási tételére.
88
5.3. Alteres visszavezetés
m..n ~ f(i) ~
a..b g(i)
max, ind := g(a), a
max:ℝ
i = a+1 .. b
i:ℤ
g(i) > max max, ind := g(i), i
SKIP
A max változó nem eredményváltozója, hanem segédváltozója a fenti programnak. Megfelelő átalakítás után el is lehetne hagyni, de ez egyrészt a program hatékonyságán ronthat, másrészt egy ilyen módosítás veszélyezteti a program helyességét. Kivételt jelent az, amikor a fordított feladatot kell megoldanunk: a maximális értéket keressük és annak indexére nem vagyunk kíváncsiak. Ilyenkor az index változót (ind), pontosabban az arra vonatkozó értékadásokat el hagyhatjuk, hiszen ezen kívül más átalakítást nem kell a programon végezni. Hasonló eset az, amikor el kell döntenünk, hogy egy intervallum valamelyik elemére teljesül-e egy megadott feltétel, de nem vagyunk kíváncsiak arra, hogy melyik ez az elem. Az ilyen eldöntéses feladatot a lineáris keresésre vezethetjük vissza, „eltűrve” azt, hogy a keresés „melléktermékként” a feltételnek eleget tevő első elemet is megadja, ha van ilyen. 5.4. Példa. Felveszi-e a g:[a..b] ℝ függvény a nulla értéket? A = (a:ℤ, b:ℤ, l: ) Ef = ( a=a’ b=b’ ) Uf = ( Ef l = i [a..b]: g(i)=0 ) Ez az úgynevezett eldöntéses feladat a lineáris keresés programozási tételére vezethető vissza. Ez nyilvánvaló, ha kiegészítjük az állapotterét a ind:ℤ komponenssel, és a feladatot szigorítjuk azzal, hogy meg is kell keresni az elő zérus helyet a g függvényben. A megoldó programból most elhagyhatjuk a ind változót, pontosabban a
89
5. Visszavezetés
ind:=i értékadást, hiszen a ind változó értékét a program nem használja fel. l, i := hamis, a l
i
i:ℤ
b
l := g(i)=0 i := i+1 Ezt a gondolatmenetet tükrözi majd az, amikor a későbbiekben ilyen eldöntéses feladatok specifikálására az alábbi rövidített jelölést fogjuk használni: b
Uf = ( Ef
l
search (g(i) i a
0) )
Az alteres visszavezetéseknek másik esete az, amikor a mintafeladat egy olyan bemenő változóját, amely a program során nem változtatja az értékét, a visszavezetéskor egy előre rögzített értékű (konstans értékű) kifejezés helyettesíti. Ez a komponens ilyenkor hiányzik a kitűzött feladat állapotteréből, ezért a feladat állapottere kevesebb komponensből áll, mint a programozási tételé. A specifikációban a konstansokat általában nem jelentetjük meg az állapottér komponenseként, azaz nem definiálunk hozzá változót, bár megtehetnénk; kiegészíthetjük a kitűzött feladat állapotterét olyan komponenssel, amelyik kezdetben felvett értéke állandó. Ettől a feladat nem fog megváltozni. Az így módosított specifikáció előfeltétele rögzíti az új állandó változó értékét. Ha ez a módosított specifikációjú feladat visszavezethető valamelyik programozási tételre, akkor a visszavezetéssel előállított program az eredeti feladatot is megoldja, hiszen a bevezetett konstans értékű változókat a program segédváltozóinak tekinthetjük. A módosított előfeltétel szigorúbb, mint a programozási tétel előfeltétele, mert egy értékét megőrző változónak egy konkrét értéket ír elő tetszőleges rögzített kezdőérték helyett. 5.5. Példa. Adjuk meg egy természetes szám valódi osztóinak számát!
90
5.3. Alteres visszavezetés
A = (n:ℕ, s:ℕ) Ef = ( n=n’ ) n div2
Uf = ( Ef
1)
s i 2 in
Átalakíthatjuk ezt a specifikációt az alábbi formára: A = (m:ℕ, n:ℕ, s:ℕ) Ef = ( m=2 n=n’ ) n div2
s
Uf = ( Ef
1) i m in
Itt már ugyanolyan állapotterű feladatról van szó, mint a számlálás mintafeladata, csak a feladat előfeltétele szigorúbb, de ezt az általános visszavezetés megengedi. m..n (i)
~ ~
1..n div 2 i n s := 0 i = 1 .. n div 2
i:ℤ
i n s := s+1
SKIP
5.4. Paraméteres visszavezetés Találkozhatunk olyan visszavezetésekkel is, ahol a megoldandó feladat állapottere a visszavezetésre kiválasztott programozási tétel mintafeladatánál nem szereplő, olyan bemeneti adatkomponenseket is tartalmaz, amelyeknek értéke állandó. (Ezen túlmenően a természetes visszavezetésnél megengedett eltérések is előfordulhatnak.)
91
5. Visszavezetés
5.6. Példa. Határozzuk meg a g:[a..b] ℕ legnagyobb, k-val osztható értékét és az [a..b] intervallumnak azt az elemét (argumentumát), ahol a g függvény ezt az értéket felveszi! A = (a:ℤ, b:ℤ, k:ℕ, l: , ind:ℤ, max:ℤ) Ef = ( a=a’ b=b’ k=k’) b
l,max,ind = max g(i) )
Uf = ( Ef
i a k g(i)
Ebben a feladatban a k változó értéke állandó. (Nem konstans, hiszen ez egy bemenő adat, de a kezdeti értéke már nem változik.) Ha ennek egy értékét rögzítjük, akkor a feladatnak egy speciális, a rögzített értékhez tartozó, azzal paraméterezett változatához jutunk. Ebben a változatban a paraméter már egy konstans, amelyet nem kell felvennünk az állapottér komponensei közé, így a paraméterezett feladat állapottere már meg fog egyezni a visszavezetésre kiválasztott mintafeladatéval. Például k=2 esetben: A = (a:ℤ, b:ℤ, l: , ind:ℤ, max:ℤ) Ef = ( a=a’ b=b’ ) b
l,max,ind = max g(i) )
Uf = ( Ef
i a 2 g(i)
Ez a paraméterezett feladat már minden gond nélkül visszavezethető a feltételes maximumkeresés programozási tételére. m..n f(i) (i)
~ ~ ~
a..b g(i) 2 g(i) l: = hamis i = a .. b
92
2 g(i)
l
2 g(i)
SKIP
max
i:ℤ l
2 g(i)
l, max, ind := max, ind := g(i), i SKIP igaz, g(i), i
5.4. Paraméteres visszavezetés
Mivel a k minden lehetséges értéke mellett ez a visszavezetés megtehető, ezért az eredeti feladatot is meg tudjuk oldani úgy, hogy összes paraméteres változatát megoldjuk. Természetesen ezt elég általánosan egy tetszőlegesen rögzített k értékkel paraméterezett feladatra megtenni. l:= hamis i = a .. b k g(i)
l
k g(i)
SKIP
max
i:ℤ l
k g(i)
l, max, ind := igaz, g(i), i SKIP
Ebben a k egy olyan változó, amelyik a program legelején kap egy kezdőértéket, azaz a megoldó program alap-állapotterének komponense lesz. A paraméterezett visszavezetésre példák még azok a feladatok is, ahol egy tömb elemeit kell feldolgozni. Ilyenkor maga a tömb lesz a paraméter. Az ilyen feladatok ráadásul egyben az alteres visszavezetésre is példák, hiszen a programozási tétel intervallumát gyakran a tömb indextartománya határozza meg, tehát annak végpontjai, az intervallum nem szerepel az állapottér komponensei közt, azokat a paraméterből lehet kinyerni. 5.7. Példa. Adott két tömb: egy x és egy b. Fektessük a b-t folyamatosan egymás után, és ezt helyezzük az x tömb mellé. Hány esetben kerül egymás mellé ugyanaz az érték? Indexeljük nullától a megadott tömböket, legyen az x tömb n, a b tömb m elemű. Ekkor megfigyelhető, hogy az x tömb i-edik eleme mellé mindig a b tömb i mod m-edik eleme kerül.
93
5. Visszavezetés
A = (x:ℤ 0n -1 , b:ℤ 0n -1 , s:ℕ) Ef = ( x=x’ b=b’ ) n 1
s
Uf = ( Ef
1) i 0 x[i ] b[i mod m]
Itt az x és b tömbök olyan értéküket nem változtató bemenő változók, amelyeket – értékeiket állandó paramétereknek véve – elhagyhatunk az állapottérből. Az így kapott állapottér azonban még mindig nem feleltethető meg a számlálás programozási tétele állapotterének, hiszen abban a számlálás intervallumát kijelölő egész számok is szerepelnek. Itt lép be a képbe az alteres visszavezetés. A feladat implicit módon tartalmazza az intervallum határait: az alsó határ a 0 (ez konstans), a felső határ pedig az x tömb utolsó elemének az indexe, amelyik ugyancsak állandó értékű bemenő adat. Ennél fogva az intervallum beemelhető az állapottér bemenő adatkomponensei közé. m..n (i)
~ ~
0..n-1 x[i] = b[i mod m] s := 0 i = 0 .. n-1
i:ℕ
x[i] = b[i mod m] s := s+1
SKIP
5.5. Visszavezetési trükkök Bizonyos feladatok visszavezetéssel történő megoldásának érdekében úgy kell eljárnunk, hogy vagy a választott programozási tételt általánosítjuk, vagy a feladatot megfogalmazásán alakítunk. Nézzünk erre példákat. Azokat a feladatokat, amelyekben a "legtöbb", "legnagyobb", "legdrágább", "legtávolabbi" stb. szófordulatokkal találkozunk, a maximum kiválasztással társítjuk. A maximum kiválasztással társítható feladatok megítélésénél körültekintően kell eljárni. Egyfelől azért, mert
94
5.5. Visszavezetési trükkök
hasonló szófordulatok jelezhetik a feltételes maximumkeresét is, ahol nem egy intervallumhoz hozzárendelt összes elem között, hanem csak bizonyos tulajdonságú elemek között keressük a maximumot. Másfelől a maximum kiválasztásnál ügyelni kell az előfeltételre, amely szerint a maximális elem kiválasztásához léteznie kell legalább egy elemnek, azaz az intervallum nem lehet üres. Ilyenkor vagy egy elágazásba építjük be a maximum kiválasztást azért, hogy csak nem-üres intervallum esetén kerüljön sor a maximum kiválasztásra, vagy feltételes maximumkeresést használunk.11 Gondoljunk most arra a feladatra, amelynél egy adott f:[m..n] H függvény értékei között a legkisebb elemet keressük. A "legkevesebb", "legnagyobb", "legolcsóbb", "legközelebb" melléknevek minimum kiválasztásra utalnak. A kérdés az, hogyan lehet egy minimum kiválasztásos feladatot a maximum kiválasztás programozási tételére visszavezetni. Azt mindenki érzi, hogy minimum kiválasztás programja mindössze annyiban tér el a maximum kiválasztásétól, hogy másik relációt használunk az elágazásának feltételében: A maxf(i)-t. (Célszerű lesz a max változó nevét min-re cserélni.) De hogyan bizonyíthatjuk, hogy ez a program tényleg helyes? Kétféle utat is mutatunk. min, ind := f(m), m i = m+1 .. n
i:ℤ
min>f(i) min, ind := f(i), i
SKIP
Az egyik út az, hogy a maximum kiválasztás programozási tételét általánosítjuk. A maximum kiválasztásnál használt " " reláció tulajdonságai közül ugyanis csak azt használtuk ki, hogy ez egy teljes 11
Itt hívjuk fel a figyelmet az olyan feladatokra is, amelyekben egy intervallum pontjai között keressük a legnagyobb adott tulajdonságút. Ilyen például a legnagyobb közös osztó keresése. Ezeket a feladatokat nem érdemes feltételes maximumkeresésre visszavezetni, tökéletesen megfelel az intervallumban egy fordított irányú kiválasztás vagy lineáris keresés is.
95
5. Visszavezetés
rendezési reláció, de a speciális jelentése egyáltalán nem volt érdekes. Ezért ez bármilyen H halmaz feletti teljes rendezési relációval, speciálisan a " " relációval is helyettesíthető. A másik út a feladat átalakításával kezdődik. Az f függvény értékei közötti minimumot megkereshetjük függvényértékek ellentettjei közötti maximum kiválasztással. Igaz, hogy ekkor a program által megtalált maximum éppen a keresett minimum ellentettje lesz, ezért a programban a max helyére mindenhol –min-t kell majd írnunk. -min, ind := –f(m), m i = m+1 .. n
i:ℤ
-min<–f(i) -min, ind:= –f(i), i
SKIP
Ez a program azonban nem megengedett, mert olyan értékadást tartalmaz, amelynek a baloldalán egy változónál általánosabb kifejezés áll. A –min:= –f(i) pszeudo-értékadás hatása azonban azonos a min:= f(i) értékadáséval, így ezzel lecserélhető. Ha még az elágazás-feltételt is beszorozzuk -1-gyel, akkor megkapjuk a végleges változatot. Hasonló módszerekkel oldható meg az a keresés, amikor nem a legelső, hanem a legutolsó adott tulajdonságú elem megtalálása a cél. Ekkor a számegyenes [m..n] intervallumát kell tükrözni az origóra, és az így kapott [–n..–m] intervallumban keressük az első adott tulajdonságú elemet. A program által talált i helyett a –i-t kell eredménynek tekinteni. l,i: = hamis,–n l
i:ℤ
i –m
l, ind := (-i), -i i := i+1 A végleges változatot, a fordított irányú keresést ebből úgy kaphatjuk meg, ha a programban az i változó helyére mindenhol –i
96
5.5. Visszavezetési trükkök
pszeudo-változót írjuk (ez egy változó átnevezés, amelyre i=–(–i) áll fenn), majd a –i:=–i+1 és –i:=–n pszeudo-értékadásokat átalakítjuk normális értékadásokká, és végül a ciklusfeltételt is egyszerűsítjük. l,i := hamis,n l
i:ℤ
i m
l, ind := (i), i i := i–1 A lineáris keresést az eldöntés jellegű feladatok megoldására is fel lehet használni (alteres visszavezetés). Ilyenkor csak azt vizsgáljuk, hogy a vizsgált feltétel bekövetkezik-e az intervallum valamelyik pontján. Mivel nem vagyunk kíváncsiak arra, hogy melyik ez a pont, az algoritmusból az ind:=i értékadást el is hagyhatjuk. Sokszor keressük arra a kérdésre a választ, hogy vajon az intervallum minden eleme rendelkezik-e egy adott tulajdonsággal (l= i [m..n]: (i)). Ezt az eldöntéses feladatot is vissza lehet vezetni a lineáris keresésre. Ha ezt egy olyan lineáris kereséssel oldjuk meg, amely azt vizsgálja, hogy van-e olyan elem, amelyik nem rendelkezik az adott tulajdonsággal (u= i [m..n]: (i)), akkor a kapott válasz alapján az eredeti kérdés is könnyen megválaszolható (l= u). Másik megoldáshoz jutunk az alábbi gondolatmenettel. Tekintsük a megoldandó feladat utófeltételét, majd ezt alakítsuk át vele ekvivalens, de a lineáris keresésre visszavezethető formára: Uf = ( Ef l = k [m..n]: (k) ) = = ( Ef l = ( k [m..n]: (k) ) ) = = ( Ef l = k [m..n]: (k) ) = n
= ( Ef
l
search i m
(i) )
97
5. Visszavezetés
i:ℤ
l,i := hamis,m l
i n
l:=
(i)
i := i+1 A program l:=hamis és l:= (i) pszeudo-értékadásait átalakítva és a ciklusfeltételt egyszerűsítve megkapjuk a végleges változatot, amelyet akár új programozási tételként is –tagadott vagy „ jeles” vagy optimista lineáris keresés néven – jegyezhetünk meg. Ezt legkifejezőbben az l = ( k [m..n]: (k) ) ) specifikációs formula fejezi n
ki, de bevezethetjük rá az l
search (i) jelölést is. i m
l,i:= igaz,m l
i:ℤ
i n
l := (i) i := i+1 Végül megemlítünk egy olyan gyakorta előforduló feladatot, amelyet önálló programozási tételként is bevezethettünk volna. Ez az úgynevezett feltételes összegzés. Erről akkor beszélünk, amikor (i) logikai feltétel értékétől függ, hogy az összegzés során az f(i)-t hozzá kell-e adnunk az eredményhez. Ilyenkor a feladat utófeltétele az alábbi: n
Uf = ( Ef
g(i) ), ahol g(i)
s i m
vagy másképp
98
f(i)
ha β (i)
0
különben
5.5. Visszavezetési trükkök
n
Uf = ( Ef
f(i) )
s i m (i)
Ezt a feladatot visszavezethetjük a közönséges összegzésre, amelyben az s:=s+g(i) értékadást egy (i) feltételű elágazással helyettesítjük, amelyben az s:=s+f(i) értékadás kap szerepet. s: = 0 i = m .. n
i:ℤ
(i) s := s + f(i)
SKIP
A feltételes összegzésnek speciális esete a számlálás (amikor az f(i) mindig 1) vagy a kiválogatás (amikor az összeadás helyén az összefűzés művelete áll). Ebben a könyvben a számlálást gyakorisága miatt önálló programozási tételként vezettük be, de az összegzés többi esetét nem. 5.6. Rekurzív függvény kiszámítása Nem mutattunk ez idáig példát a rekurzív függvény helyettesítési értékét kiszámító programozási tétel alkalmazására. Habár ennek a tételnek az alkalmazása sem bonyolultabb a többi programozási tételre történő visszavezetésnél, egy feladatban azt felismerni, hogy az eredményt egy rekurzív függvénnyel lehet kiszámolni, sok gyakorlatot igényel. Az ugyanis, hogy egy feladat megoldásához maximumkeresésre vagy éppen számlálásra van szükség az sokszor explicit módon kifejezésre jut a feladat szövegében. A rekurzív függvény definícióját viszont minden esetben nekünk kell kitalálni. Az ilyen feladatok specifikációja nehéz, ugyanakkor éppen ez a kulcsa egy bonyolult feladat egyszerű és hatékony megoldásának. 5.8. Példa. Adott egy egész számokat tartalmazó tömb. Keressük meg a tömb legnagyobb és legkisebb értékét!
99
5. Visszavezetés
Ez a feladat nyilván megoldható külön egy maximum- és külön egy minimum kiválasztás segítségével. De nem lehetne a feladat eredményét egy egyszerűbb és gyorsabb programmal előállítani? Például olyan függvény segítségével, amelyik egyszerre két értéket is felvesz, amely az i-dik helyen előállítja az x tömb első i darab elemének a maximumát is, és a minimumát is, és ennek megfelelően az n-edik helyen az x tömb összes elemének a maximumát és a minimumát adja. A = (x:ℤ n, max:ℤ, min:ℤ) Ef = ( x=x’ n≥1 ) Uf = ( Ef (max, min) = f(n) ) Definiáljuk az f függvényt rekurzív módon. Az f(i) értékei ugyanis könnyen meghatározhatóak f(i–1) (azaz az x tömb első i-1 darab elemének maximuma és minimuma), valamint az x[i] érték alapján. Azt is látni, hogy f(1)= (x[1], x[1]), hiszen a vektor első elemének maximuma is, minimuma is a tömb első eleme. f:[1..n] ℤ × ℤ f(1) = (x[1], x[1]) f1(i) = max( f1(i–1), x[i] ) i [2..n] f2(i) = min( f2(i–1), x[i] ) i [2..n] Ez egy 2 bázisú elsőrendű kétértékű rekurzív függvény. m..n y e0 h1(i, f1(i–1)) h2(i, f2(i–1))
~ ~ ~ ~ ~
2..n (max, min) (x[1], x[1]) max( f1(i–1), x[i] ) min( f2(i–1), x[i] ) max, min := x[1], x[1] i = 2 .. n
max, min := max( max, x[i] ), min( min, x[i] ) Vagy a ciklusmagot más alakra hozva:
100
i:ℕ
5.6. Rekurzív függvény kiszámítása
max, min := x[1], x[1] i = 2 .. n
i:ℕ
x[i] < min
min ≤ x[i] ≤ max
x[i] > max
min := x[i]
SKIP
max := x[i]
Ez a példa rávilágított arra is, hogy a rekurzív függvény kiszámítása programozási tétellel helyettesíteni lehet más programozási tételeket. 5.9. Példa. Egy tömb egy b alapú számrendszerben felírt természetes szám számjegyeinek értékeit tartalmazza úgy, hogy a magasabb helyiértékek vannak elől. Számoljuk ki a tömbben tárolt természetes szám értékét! A = (x:ℕn, b:ℕ, s:ℕ) Ef = ( x=x’ b=b’ b≥2 i [1..n]: x[i]
Uf = ( Ef
x[k ] b n
s
k
)
k 1
A feladat visszavezethető az összegzés programozási tételére, de ebben az esetben minden lépésben hatványozni kell a b-t, és ezzel együtt – mint az utófeltétel képletéből látszik – összesen n(n–1)/2+1 darab szorzást kell elvégezni. Megfigyelhető, hogy ha bevezetjük az f: [0..n] N i
x[k ] b i
f(i) =
k
k 1
függvényt, akkor a feladat tulajdonképpen az f(n) kiszámítása. Uf = ( Ef s = f(n) ) Belátható, hogy minden 1-nél nagyobb i-re az f(i) = f(i–1)*b+x[i] rekurzív összefüggés áll fenn. Definiáljuk tehát az f függvényt másképpen:
101
5. Visszavezetés
f: [0..n] ℕ f(0) = 0 f(i) = f(i–1)*b+x[i] i [1..n] Ekkor a feladat visszavezethető a rekurzív kiszámításának tételére. m .. n y e0 h(i, f(i–1))
~ ~ ~ ~
függvény
1..n s 0 f(i–1)*b+x[i]
s := 0 i = 1 .. n
i:ℕ
s := s*b+x[i] Ez az algoritmus szemben az előzővel mindössze n–1 szorzást használ.12 5.10. Példa. Két n hosszúságú tömb egy-egy b alapú számrendszerben felírt természetes szám számjegyeinek értékeit tartalmazza úgy, hogy a magasabb helyiértékek vannak elől. Állítsuk elő ennek a két természetes számnak az összegét úgy, hogy a számjegyeit elhelyezzük egy harmadik n hosszúságú tömbben és azt külön is jelezzük, hogy történt-e túlcsordulás, azaz elfért-e az eredmény a harmadik tömbben (n darab helyiértéken)! (Feltesszük, hogy csak számjegyeket tudunk összeadni, azaz nem jó megoldás a tömbökben ábrázolt számok értékét kiszámolni, azokat összeadni, majd az eredményt elhelyezni egy harmadik tömbben.)
12
Ezt az algoritmust szokták Horner algoritmusnak is nevezni, mert az alábbi Hornerről elnevezett séma alapján számolja ki egy természetes számnak az értékét a szám számjegyei alapján: n
x[k ] b n k 1
102
k
(...(( x[1] b
x[2]) b
x[3])...) b
x[n]
5.6. Rekurzív függvény kiszámítása
A túlcsordulást ne egy logikai érték, hanem egy 0 vagy 1 érték mutassa. Két szám összeadásánál ugyanis akkor keletkezik túlcsordulás, ha a legnagyobb helyiérték számjegyének kiszámolásakor 1 értékű átvitel keletkezik. Általában átvitelről akkor beszélünk, amikor az összeadandó két szám adott helyiértékén levő számjegyinek, valamint az eggyel kisebb helyiértékről származó átvitel összege nagyobb, mint a legnagyobb számjegy, és ezért az eggyel magasabb helyiértékhez kell majd hozzá adni még egyet, azaz az átvitel 1. Ha viszont a legmagasabb helyiértéken keletkezik az átvitel, akkor azt már nincs hová hozzáadni: ez a túlcsordulás. A = (x:ℕn, y:ℕn, z:ℕn, b:ℕ, c:{0,1}) Ef = ( x=x’ y=y’ b=b’ b≥2 i [1..n]:x[i]
103
5. Visszavezetés
m .. n ~ 1..n y ~ z,c e0 ~ *,0 h1(i, f(i–1))[n–i+1] ~ (x[n–i+1]+y[n–i+1]+f2(i–1)) mod b h1(i, f(i–1))[n–i+2..n]~ f1(i–1)[n–i+2..n] h2(i, f(i–1)) ~ (x[n–i+1]+y[n–i+1]+f2(i–1)) div b c := 0 i = 1 .. n z[n–i+1] := (x[n–i+1]+y[n–i+1]+c) mod b c := (x[n–i+1]+y[n–i+1]+c) div b
104
i:ℕ
5.7. Feladatok
5.7. Feladatok 5.1. 5.2.
5.3. 5.4.
5.5. 5.6.
5.7. 5.8. 5.9. 5.10.
Adott a síkon néhány pont a koordinátáival. Keressük meg az origóhoz legközelebb eső pontot! Egy hegyoldal hegycsúcs felé vezető ösvénye mentén egyenlő távolságonként megmértük a terep tengerszint feletti magasságát, és a mért értékeket egy vektorban tároljuk. Megfigyeltük, hogy ezek az értékek egyszer sem csökkentek az előző értékhez képest. Igaz-e, hogy mindig növekedtek? Határozzuk meg egy egész számokat tartalmazó tömb azon legkisebb értékét, amely k-val osztva 1-et ad maradékul! Adottak az x és y vektorok, ahol y elemei az x indexei közül valók. Keressük meg az x vektornak az y-ban megjelölt elemei közül a legnagyobbat! Igaz-e, hogy egy tömbben elhelyezett szöveg odafelé és visszafelé olvasva is ugyanaz? Egy mátrix egész számokat tartalmaz sorfolytonosan növekedő sorrendben. Hol található ebben a mátrixban egy tetszőlegesen megadott érték! Keressük meg két természetes szám legkisebb közös többszörösét! Keressük meg két természetes szám legnagyobb osztóját! Prím szám-e egy egynél nagyobb egész szám? Egy n elemű tömb egy b alapú számrendszerben felírt természetes szám számjegyeinek értékeit tartalmazza úgy, hogy a magasabb helyiértékek vannak elől. Módosítsuk a tömböt úgy, hogy egy olyan természetes szám számjegyeit tartalmazza, amely az eredetinél eggyel nagyobb, és jelezzük, ha ez a megnövelt érték nem ábrázolható n darab helyiértéken!
105
6. Többszörös visszavezetés A visszavezetés technikájának megértése, elsajátítása önmagában könnyű feladat. Alkalmazása akkor válik nehézzé, amikor egy összetett probléma megoldásához egyszerre több programozási tételt is fel kell használni. Már egy kezdő programozónál is kialakul az a képesség, hogy egy összetett feladatban észrevegye a megoldáshoz szükséges programozási tételeket, de ezek egymáshoz való viszonyát, az egyiknek a másikba való beágyazásának módját még nem látja tisztán. Ehhez arra van szükség, hogy a megoldandó feladatot részekre tudjuk bontani úgy, hogy a részfeladatokat a programozási tételek segítségével meg lehessen oldani, és az így kapott részmegoldásokat kell egy programmá összeépíteni. A részfeladatok kijelölését hatékonyan támogatja, ha a feladat megfogalmazásakor az egyes fogalmak jelölésére egy-egy alkalmas függvényt vezetünk be. Ezek a kitalált, tehát absztrakt függvények a megoldás bizonyos fázisaiban elrejtik az általuk definiált részfeladatokat. A beágyazott részfeladatok elrejtése következtében világosabban látható, hogy magasabb szinten milyen programozási tétellel oldható meg a feladat. Csak ezt követően kell foglalkozni a részfeladatokkal, amelyek megoldását a feladat többi részétől függetlenül, a részfeladatot elrejtő függvény kiszámításával állíthatjuk elő. Ezt a függvényabsztrakciós programozási technikát procedurális (modularizált) programkészítésnek is nevezik, mert az így készült programok kódolásakor elkülönítjük a részfeladatokat megoldó programokat, ezáltal a teljes program részprogramokra bomlik. Mivel a különválasztott programrészek (procedurák, modulok) jól átlátható, ellenőrzött kapcsolatban állnak, ezért a részprogramok végrehajtási sorrendje pontosan ki van jelölve. Amikor egy részprogramról átkerül a végrehajtási vezérlés egy másikra, akkor az első adatokat adhat át a másodiknak. Amennyiben egy részprogram leírását fizikailag is elválasztjuk a teljes programról, akkor azt alprogramnak hívjuk. Egy alprogram maga is több alprogramra bontható, ami által összetett, hierarchikus szerkezete lesz a programunknak. Ebben a fejezetben először bevezetjük az absztrakt programok szintjén az alprogramok fogalmát, majd néhány példán keresztül
106
bemutatjuk, hogyan alkalmazható többszörösen a visszavezetés technikája az összetett feladatok megoldásánál, és az így kapott programot a feladat részfeladatai mentén hogyan tördeljük alprogramokra. A harmadik alfejezetben áttekintjük azokat a programátalakító szabályokat, amelyek alkalmazása kényelmesebbé teszi a programkészítést, hatékonyabbá az elkészített programot. Az utolsó alfejezetben egy különleges program-átalakítást mutatunk arra az esetre, amikor egy ciklusban szereplő részfeladat eredménye rekurzív függvénnyel számolható ki.
107
6. Többszörös visszavezetés
6.1. Alprogram Egy program leírásában szereplő bármelyik részprogram egy jól meghatározott részfeladatot old meg. Ha egy részprogram fizikailag is elkülönül a program leírásában, akkor azt alprogramnak nevezzük. Struktogrammos jelölés esetén egy részprogramból úgy készíthetünk alprogramot, hogy a részprogramot kiemeljük az azt tartalmazó program struktogrammjából, a kiemelt struktogrammot egyedi azonosítóval látjuk el, és a részprogram helyét a program struktogrammjában ezzel az azonosítóval jelöljük. Ez lehetőséget ad majd arra is, hogy ugyanarra az alprogramra – ha kell – több helyen is hivatkozhassunk részprogramként. Mivel minden feladat megoldható egy (általában nem megengedett) értékadással, ezért az alprogramjaink által megoldott részfeladatok is, ennél fogva minden alprogram azonosítható ezzel az értékadással. Ezért a továbbiakban az alprogramot leíró struktogrammot ezzel az értékadással azonosítjuk. Ez az alprogram feje. Egy program struktogrammjában, ahol az alprogramot részprogramként látni kívánjuk, ugyanezt az értékadást írjuk. Ezt az alprogramot meghívó utasításnak, röviden hívásának nevezik. Amikor egy program végrehajtása során elérünk egy alprogram hívásához, akkor onnan kezdve az alprogram határozza meg a végrehajtás menetét, azaz „átkerül a vezérlés” az alprogramhoz. Az alprogram befejeződésekor a vezérlés visszatér a hívó programhoz és a hívó utasítás után folytatódik. Más szavakkal, a hívó programnak (amely a hívást tartalmazza) a hívás pillanatáig befutott végrehajtása (állapotsorozata) felfüggesztődik, az ekkor elért állapotból az alprogram egy végrehajtásával (állapotsorozatával) folytatódik, majd az így elért állapotból a hívó programnak a hívás utáni utasításai alapján folytatódik végrehajtás. Az alprogram kiinduló állapottere tartalmazza a hívó program állapotterének a hívás pillanatában (segédváltozókkal együtt) meglevő komponenseit. Más szavakkal az alprogram használhatja a hívó program azon változóit, amelyek az alprogram hívásakor élnek. Ezeket az alprogram szempontjából globális változóknak nevezzük. Az alprogram – mint minden program – bevezethet új változókat is, de ezek az alprogram befejeződésekor megszűnnek. Ezeket nevezzük az alprogram lokális változóinak. Egy lokális változó neve megegyezhet
108
6.1. Alprogram
egy globális változó nevével, de ekkor gondoskodni kell arról, hogy a két ugyanolyan nevű, de természetesen egymástól független változót megkülönböztessük valahogyan egymástól. Mi erre nem vezetünk be külön jelölést, de megállapodunk abban, hogy az ilyen esetekben, amikor egy változó neve nem azonosítja egyértelműen a változót, akkor azon mindig a lokális változót értjük. Természetesen az alprogramnak nem kell minden globális változót használnia. Ennél fogva egy alprogram más, eltérő állapotterű programokból is meghívható, feltéve, hogy azok állapotterében a hívás pillanatában vannak olyan változók, amelyeket az alprogram globális változóként használni akar. A legrugalmasabb alprogramok ebből a szempontból azok, amelyek egyáltalán nem használnak globális változókat, mert ezeket bármelyik programból meghívhatjuk. De hogyan tud ebben az esetben az alprogram a hívó program változóiban tárolt értékekhez hozzáférni, és hogyan tud azoknak új értékeket adni? Az alprogramot meghívó utasításnak és az alprogram fejének nem kell betű szerint megegyeznie. Az alprogram fejeként használt értékadás különbözhet a hívó utasításként szerepeltetett értékadástól abban, hogy egyrészt az értékadás baloldalán álló változók neve eltérhet (típusaik és sorrendjük azonban nem), másrészt az értékadás jobboldalán levő részkifejezések helyén azokkal azonos típusú változók állhatnak. Ezek az alprogram paraméterváltozói. A kizárólag az értékadás jobboldali kifejezésében lévő paraméterváltozókat szokták a bemenő-változóknak, az értékadás jelétől balra levőket eredményváltozóknak nevezni. Egy paraméterváltozó lehet egyszerre bemenő és eredmény-változó is. Ebben az esetben a hívóutasításban a változó helyén csak egy változó állhat, konstans vagy kifejezés nem. A fej paraméterváltozóinak balról jobbra felsorolását (az esetleges ismétlődések megszűntetése után) formális paraméterlistának szokták hívni. A hívó utasításban a paraméterváltozóknak megfeleltetett változók és kifejezések (ismétlődés nélküli) felsorolását aktuális paraméterlistának, elemeit paramétereknek nevezzük. A formális és aktuális paraméter lista elemszáma és elemeinek típusa azonos kell hogy legyen. A paraméterváltozók az alprogramban jönnek létre és az alprogram befejeződésekor szűnnek meg, tehát lokális változói az alprogramnak, de különleges kapcsolatban állnak az alprogram hívásával. Az alprogram hívásakor ugyanis a bemenő paraméterváltozók megkapják a hívó utasításban a helyükön álló
109
6. Többszörös visszavezetés
kifejezések (speciális esetben változók) értékét kezdőértékként. Az alprogram befejeződésekor pedig az eredmény-változók értékei átmásolódnak a hívó értékadás baloldalán álló megfelelő változókba. (A bemenő változók kivételével az alprogram többi lokális változójának kiinduló értéke nem-definiált.) Egy alprogram hívására úgy is sor kerülhet, hogy a hívó utasításnak csak a jobboldalát tüntetjük fel, de ez olyan környezetben jelenik meg, amely képes elkapni, feldolgozni az alprogram eredményváltozói által visszaadott értékeket. Hogy egy egyszerű példával megvilágítsuk ezt, képzeljünk el egy olyan alprogramot, amelyet az l:=felt(i) értékadás azonosít, ahol l egy logikai, az i pedig egy egész értékű változó. Ez az alprogram meghívható például egy u:=felt(5) értékadással (ahol u egy logikai változó), vagy egy olyan elágazással, ahol az elágazás feltétele felt(5), azaz a feltétel logikai értékét az alprogram eredménye adja, a felt(i). Az első esetben a hívás a teljes értékadás megadásával történt, a második esetben csak egy kifejezéssel. Valójában csak formai szempontból van különbség a kétféle hívás között: az első esetben hívó utasítással történő eljárásszerű hívásról, a másodikban hívó kifejezéssel kiváltott függvényszerű hívásról beszélünk.13 A hívó kifejezés lehet a hívó programnak önálló kifejezése (például egy elágazás vagy ciklus feltétele, vagy akár egy értékadás jobboldala) vagy egy kifejezés része. A hívó kifejezés értéke több eredmény-változó esetén több komponensű. Eljárásszerű hívás esetén az aktuális (a hívásban szereplő) eredmény-változók kapják meg a formális eredmény-változók értékét. Függvényszerű híváskor az alprogram eredmény-változóinak értéke a hívó kifejezés értékeként jelenik meg a hívó programban.
13
A függvényszerű hívással elindított alprogramot a konkrét programozási nyelvek függvénynek, az önálló utasítással (értékadással) meghívott alprogramot eljárásnak nevezik.
110
6.2. Beágyazott visszavezetés
6.2. Beágyazott visszavezetés Ebben az alfejezetben a többszörös visszavezetéssel megoldható feladatokkal foglalkozunk, pontosabban olyanokkal, amelyek megoldásánál egy programozási tételt egy másikba kell beágyazni. Ilyenkor a programnak azt a részét, amelyet a beágyazott tételre vezettünk vissza, gyakran alprogramként szerepeltetjük. 6.1. Példa. Egy iskola egyik n diákot számláló osztályában m különböző tantárgyból osztályoztak a félév végén. A jegyek egy táblázat formájában rendelkezésünkre állnak. (A diákokat és a tantárgyakat most sorszámukkal azonosítjuk.) Állapítsuk meg, van-e olyan diák, akinek csupa ötöse van! A feladat egy "van-e olyan az elemek között, hogy ..." típusú eldöntésből áll, ami alapján sejthető, hogy a lineáris keresés programozási tételére lehet majd visszavezetni. Ebben a keresésben a diákokat kell egymás után megvizsgálni, hogy csupa ötösük van-e. Egy diák megvizsgálása is egy eldöntéses feladat, csakhogy itt arra keressük a választ, hogy a diáknak minden jegye ötös-e. Az ilyen "minden elem olyan-e, hogy ..." típusú eldöntéseknél az optimista lineáris kereséssel adhatjuk meg a választ. A feladat megoldása tehát elsősorban egy lineáris keresés lesz, amelyik magába foglalja a "csupa ötös" tulajdonságot eldöntő optimista lineáris keresést. Nézzük meg ezt alaposabban! A feladat bemenő adata az osztályzatok táblázata: egy n sorú és m oszlopú mátrix, amelynek elemei 1 és 5 közé eső egész számok. Kimenő adata egy logikai érték, amely igaz, ha van kitűnő tanuló, hamis különben. A = (t:ℕn×m , l: ) Ef = ( t=t’ ) Az utófeltétel megfogalmazásához bevezetünk egy függvényt, amelyik a t táblázat i-edik sora alapján megmondja, hogy az i-edik diáknak csupa ötöse van-e. Ez a színjeles függvény egy intervallumon értelmezett logikai függvény, amely akkor ad igazat az i-edik értékre, ha a táblázat i-edik sorának minden eleme ötös.
111
6. Többszörös visszavezetés
színjeles :[1..n] színjeles(i) = j [1..m]: t[i,j]=5 Ennek segítségével könnyen felírható a feladat utófeltétele: az l pontosan akkor igaz, ha van olyan diák, azaz 1 és n közé eső egész szám, amelyre a színjeles feltétel igazat ad. Uf = ( Ef l = i [1..n]: színjeles(i) ) Ez a feladat visszavezethető a lineáris keresés programozási tételére. A lineáris keresés specifikációs jelölésénél most csak a logikai komponens van eredményként feltüntetve, hiszen csak ennek megadása a feladat. m..n ~ 1..n (i) ~ színjeles(i) l,i := hamis,1 l
i:ℕ
i n
l := színjeles(i) i := i+1 Ebben a programban az l:=színjeles(i) értékadás nemmegengedett. Hangsúlyozzuk, hogy a színjeles(i) önmagában csak egy kifejezés, nem tekinthető sem feladatnak, sem programnak, szemben az l:=színjeles(i) értékadással. Ezt az értékadást, mint részfeladatot, tovább kell finomítani, azaz egy megengedett programmal kell majd helyettesíteni, azaz meg kell oldani. A megoldást egy alprogram keretében írjuk most le, amelynek hívását az l:=színjeles(i) értékadás jelzi. (Dönthettünk volna úgy is, hogy az l:=színjeles(i) értékadást megoldó részprogramot bemásoljuk l:=színjeles(i) értékadás helyére, de most inkább az alprogramként való meghívást részesítjük előnyben.) A részfeladatnak a specifikációja az alábbi lesz. A = (t:ℕ n×m, i:ℕ, l: ) Ef = ( t=t’ i=i’ [1..n] ) Uf = ( Ef l = színjeles(i) ) Figyelembe véve a színjeles(i) definícióját ez a részfeladat is visszavezethető a lineáris keresés programozási tételére, pontosabban
112
6.2. Beágyazott visszavezetés
az optimista lineáris keresésre. Ügyelni kell arra, hogy ciklusváltozónak nem használhatjuk az i-t, hiszen az a részfeladatnak egy bemenő adata, foglalt változónév. Használjuk helyette a j-t. Ennek megfelelően a visszavezetésnél valójában nem a (i)-t, hanem a (j)-t kell helyettesíteni a konkrét t[i,j]=5 feltétellel, amelyben az i a vizsgált diák sorszámára utal. m..n ~ 1..m i ~ j (i) ~ t[i,j]=5 l:=színjeles(i) l,j := igaz,1 l
j:ℕ
j m
l := t[i,j]=5 j := j+1 Az alprogram feje az l:=színjeles(i) értékadás, amely most szóról szóra megegyezik a hívással. Korábbi megállapodásunk szerint a fejben szereplő l és i változók (a formális paraméterváltozók) az alprogram lokális változói, nem azonosak a hívásnál szereplő ugyanolyan nevű globális változókkal (az aktuális paraméterekkel), de sajátos kapcsolatban állnak azokkal. (Természetesen használhattunk volna más lokális változó neveket.) A lokális i (bemenő-változó) a híváskor megkapja a globális i értékét, a lokális l (eredmény-változó) a hívás (azaz az alprogram) befejeződésekor átadja értékét a globális l-nek. A paraméterváltozók névegyezése miatt az alprogramban nem hivatkozhatunk a globális l és i változókra. Használhatja ellenben az alprogram a globális t változót. Az alprogram bevezet még egy lokális j változót is, amely az alprogram befejeződésekor megszűnik majd. 6.2. Példa. Egy n diákot számláló osztályban m különböző tantárgyból adtak jegyeket a félév végén. Ezek az osztályzatok egy táblázat formájában rendelkezésünkre állnak. (A diákokat és a tantárgyakat most is a sorszámukkal azonosítjuk.) Tudjuk, hogy az osztályban van kitűnő diák, adjuk meg az egyiknek sorszámát!
113
6. Többszörös visszavezetés
Ez a feladat nagyon hasonlít a 6.1. példához. Ott nem tudtuk, hogy van-e kitűnő diák, ezért a lineáris keresés programozási tételére vezettük vissza a feladatot, itt viszont tudjuk, hogy van, és egy ilyen diákot keresünk, ezért elegendő a feladatot a kiválasztás tételére visszavezetni. (Természetesen a 6.1. példánál kapott program is alkalmazható, hiszen a lineáris keresés nemcsak azt dönti el, hogy vane kitűnő diák, hanem az elsőt meg is keresi.) A = (t:ℕn×m, i:ℕ) Ef = ( t=t’ k [1..n]: színjeles(k) ) Itt már az előfeltétel megfogalmazásához bevezetjük a színjeles függvényt. Uf = ( Ef i [1..n] színjeles(i) ) = = ( Ef i select színjeles (i) ) i 1
Ez a feladat visszavezethető a kiválasztás programozási tételére. m ~ 1 (i) ~ színjeles(i) i := 1 színjeles(i) i := i+1 Ebben a programban nem-megengedett feltétel a színjeles(i) kifejezés, amely értékét az előző példában elkészített l:=színjeles(i) alprogram határozza meg. Most erre az alprogramra egy függvényszerű hívást adunk. Amikor a ciklusfeltétel kiértékelésére kerül sor, akkor meghívódik az alprogram, és a lokális eredmény-változójának (l: ) értéke közvetlenül a ciklusfeltételnek adódik vissza. Ezzel tehát készen is vagyunk. Egy alprogram nem lesz más attól, hogy függvényszerűen vagy eljárásszerűen hívjuk meg. Az alprogram mindig egy értékadás által kijelölt feladatot old meg, van eredmény-változója, hiszen egy értékadást valósít meg. A 6.1. példában éppen ezért függvényszerű hívásról is és eljárásszerű hívásról is beszélhetünk egyszerre, nincs e kettő között látható különbség. Egy eljárásszerű hívás ugyanis mindig felfogható függvényszerű hívásnak is. Fordítva ez már nem igaz, de
114
6.2. Beágyazott visszavezetés
mindig átalakítható a hívó program úgy, hogy egy függvényszerű hívást eljárásszerű hívással helyettesíthessünk. Ezt akkor tesszük meg, ha ezt valamilyen más szempont (például hatékonyság) indokolja. Az aktuális példánkban nincs ilyen érv, de a játék kedvéért mutassuk meg ezt az átalakítást. A cél az, hogy a hívó programmal olyan ekvivalens programot készítsünk, ahol a ciklusfeltétel színjeles(i) nemmegengedett kifejezése egy l:=színjeles(i) nem-megengedett értékadásba kerül át. Ehhez be kell vezetnünk egy új logikai változót, és a hívó programot az alábbi változatok valamelyikére kell alakítanunk. i, l := 1, színjeles(1) l
l:
i, l := 0, hamis
l:
l
i := i+1
i := i+1
l := színjeles(i)
l := színjeles(i)
6.3. Példa. Egy iskola n diákot számláló osztályában m különböző tantárgyból osztályoztak a félév végén. Ezek a jegyek egy táblázat formájában rendelkezésünkre állnak. (A diákokat és a tantárgyakat sorszámukkal azonosítjuk.) Igaz-e, hogy minden diáknak van legalább három tárgyból négyese? A feladat most egy " minden elem olyan-e, hogy ..." típusú eldöntés, ami az optimista lineáris keresés programozási tételére utal. Ebben a keresésben is a diákokat kell egymás után megvizsgálni, de most a vizsgálat tárgya az, hogy van-e legalább három négyese az illetőnek. Ehhez meg kell számolni minden diáknál a négyes osztályzatait. Vegyük észre, hogy ezeket a számlálásokat nem kell a feladatot megoldó optimista lineáris keresés előtt egy „előfeldolgozással” elvégezni, hiszen az optimista kereséshez mindig csak az aktuális diák négyeseinek száma kell, amit ott „helyben” kiszámolhatunk, majd utána el is felejthetünk. Az így kapott megoldás nemcsak a tárigény és futási idő szempontjából lesz hatékonyabb, hanem a program előállítása is egyszerűbb. Arra van csupán szükségünk, hogy a négyesek számát előállító részfeladatot egy absztrakt függvény mögé rejtsük. Ez a függvény a táblázat i-edik sora alapján megadja, hogy az i-edik diáknak hány négyese van. Arra a
115
6. Többszörös visszavezetés
kérdésre, hogy miért pont erre a fogalomra vezettünk be egy új függvényt – miért nem arra, hogy van-e legalább három négyese az iedik diáknak – az a válasz, hogy az általunk választott függvény kiszámolását közvetlenül visszavezethetjük majd a számlálás programozási tételére. A feladat adatai hasonlítanak az előző feladatéra, ami az állapottér és az előfeltétel felírásában tükröződik: A = (t:ℕn×m, l: ) Ef = ( t=t’ ) A feladat utófeltétele a kimenő adatkomponenst írja le: az l akkor igaz, ha minden diáknak három vagy annál több négyese van. Bevezetve a négyesdb függvényt, amelyre a négyesdb(i) az i-edik diák négyeseinek számát adja meg a t táblázat i-edik sora alapján, az utófeltétel az alábbi módon írható fel: Uf = ( Ef l = i [1..n]: (négyesdb(i) 3) ) = = ( Ef ahol
l
n
search (négyesdb(i ) i 1
3) )
négyesdb :[1..m] ℕ
négyesdb(i) =
m
1 j 1 t [i , j ] 4
Ezt a feladatot egy optimista lineáris keresés oldja meg: m..n ~ 1..n (i) ~ négyesdb(i) 3
l,i := igaz,1 l
i:ℕ
i n
l := négyesdb(i) 3 i := i+1 A visszavezetéssel előállított programban az l:=négyesdb(i) 3 értékadás nem-megengedett. Ha tüzetesebben megvizsgáljuk az értékadás jobboldalán álló kifejezést, akkor észrevehetjük, hogy annak
116
6.2. Beágyazott visszavezetés
négyesdb(i) részkifejezése a nem-megengedett. Készíthetünk ennek kiszámolására egy s:=négyesdb(i) értékadást megvalósított függvényszerűen meghívott alprogramot, ahol az s egy darabszámot tartalmazó eredmény-változó. (Alternatív megoldás, ha a hívó programban bevezetünk egy s:N segédváltozót, és az l:=négyesdb(i) 3 értékadást az (s:=négyesdb(i); l:=s 3) szekvenciára bontjuk fel, és az s:=négyesdb(i) alprogramot eljárásszerűen hívjuk.) Az s:=négyesdb(i) értékadás által kijelölt részfeladat specifikációja: A = (t:ℕn×m, i:ℕ, s:ℕ) Ef = ( t=t’ i=i’ [1..n] ) Uf = ( Ef
s = négyesdb(i) ) = ( Ef
m
s=
1) j 1 t [i , j ] 4
Ez visszavezethető a számlálás programozási tételére. m..n ~ 1..m (i) ~ t[i,j]=4 s:=négyesdb(i) s := 0 j = 1..m
j:ℕ
t[i,j]=4 s := s+1
SKIP
6.4. Példa. Egy iskola n diákot számláló egyik osztályában m különböző tantárgyból adtak jegyeket a félév végén. Ezek az osztályzatok egy táblázat formájában állnak rendelkezésünkre. (A diákokat és a tantárgyakat sorszámukkal azonosítjuk.) Számoljuk meg, hány kitűnő diák van az osztályban! A szövegéből egyértelműen kiderül, hogy egy számlálással lehet a feladatot megoldani. A számlálást a diákok között, tehát az [1..n] intervallum felett végezzük, és azt a feltételt vizsgáljuk, amely megmondja egy diákról, hogy csupa ötöse van-e. Ennek a feltételnek a jelölésére a korábban már bevezetett színjeles logikai függvényt használjuk.
117
6. Többszörös visszavezetés
Lássuk a specifikációt! A = (t:ℕn×m, s:ℕ) Ef = ( t=t’ ) n
Uf = ( Ef
1)
s= i 1 színjeles( i )
ahol színjeles :[1..n] színjeles(i) = j [1..m]: t[i,j]=5 A feladatot tehát egy számlálásra vezetjük vissza, ahol az intervallum az [1..n], a (i) feltétel a színjeles(i). s:=0 i = 1 .. n
i:ℕ
színjeles(i) s := s+1
SKIP
A színjeles(i) nem-megengedett feltétel a korábban már definiált alprogram függvényszerű hívását jelöli. 6.5. Példa. Egy iskola n diákot számláló egyik osztályában m különböző tantárgyból osztályoztak a félév végén. Ezek a jegyek egy táblázat formájában állnak rendelkezésünkre.(A diákokat és a tantárgyakat sorszámukkal azonosítjuk.) Melyik diáknak van a legtöbb négyese? Ennél a feladatnál a maximum kiválasztás programozási tételét kell használnunk. Mivel egy iskolai osztályhoz biztos tartoznak diákok, így a maximum kiválasztás értelmes. A maximális értéket most nem közvetlenül egy intervallum pontjai közül, hanem az intervallum pontjaihoz (azaz a diákokhoz) rendelt értékek (egy diák négyeseinek száma) közül kell kiválasztani. A feladatban jól körülvonalazódik a 6.3. példából már ismert részfeladat is, amely egy diák négyeseinek számát állítja elő.
118
6.2. Beágyazott visszavezetés
A = (t:ℕn×m, ind:ℕ ) Ef = ( t=t’ n>0 m>0 ) Uf = ( Ef
n
négyesdb(ind) = max négyesdb (i) ) i 1
A feladatot egy maximum kiválasztásra vezetjük vissza, ahol az intervallum az [1..n], és az f(i) kifejezést a négyesdb(i) helyettesíti. max, ind := négyesdb(1), 1 i = 2 .. n
max:ℕ i:ℕ
max
SKIP
Mivel rendelkezünk az s:=négyesdb(i) alprogrammal, amelyet speciálisan a négyesdb(1) kiszámolására is felhasználhatunk, tulajdonképpen készen vagyunk: az alprogram függvényszerű hívására van szükség. Hatékonysági okból azonban célszerű a ciklusmagbeli elágazásból a négyesdb(i)-t egy értékadásba kiemelni és az alprogramot eljárásszerűen hívni, mert így az i egy rögzített értékére nem kell a négyesdb(i) értékét esetenként kétszer is kiszámolni. max, ind := négyesdb(1), 1 i = 2 .. n
max:ℕ i:ℕ
s := négyesdb(i)
s:ℕ
max<s max,ind := s, i
SKIP
6.6. Példa. Egy iskola n diákot számláló egyik osztályában m különböző tantárgyból osztályoztak a félév végén. Ezek a jegyek egy táblázat formájában állnak rendelkezésünkre.(A diákokat és a tantárgyakat sorszámukkal azonosítjuk.) Ki az a diák, aki a csak 4-es illetve 5-ös osztályzatú diákok között a legjobb átlagú? Ha van ilyen, adjuk meg az átlagát is!
119
6. Többszörös visszavezetés
Ez a feladat egy feltételes maximumkereséssel oldható meg, hiszen csak az adott tulajdonságú diákok átlagai között keressük a legjobbat. Ezen belül önálló részfeladatot alkot annak eldöntése, hogy egy diák csak 4-es illetve 5-ös osztályzatú-e, illetve önálló részfeladat egy diák átlagának kiszámolása is. Az előbbi a feltételes maximum keresés feltételét adja, az utóbbi a maximumkeresésnél használt f függvényt. Ezeket a részfeladatokat egy-egy absztrakt függvénnyel jelöljük ki. Bevezetjük a "csak 4-es illetve 5-ös osztályzatú" tulajdonságot eldöntő jó logikai függvényt, amely az i-edik diák esetén akkor ad igaz értéket, ha a táblázat i-edik sorában csak négyesek vagy ötösök állnak. Bevezetjük továbbá az összeg függvényt, amely megadja az i-edik diák jegyeinek összegét. Azért az összegét, és nem az átlagát, mert a maximum keresés eredménye mindkét esetben ugyanaz a diák. Természetesen a végeredmény előállításához külön kell majd gondoskodni a legjobb diák átlagának kiszámolásáról. A = (t:ℕn×m, l: , ind:ℕ, átl:ℝ ) Ef = ( t=t’ ) Uf = ( Ef
n
l,max,ind = max összeg (i ) i 1 jó(i )
(l
átl=max/m) )
ahol jó:[1..n] jó(i) = j [1..m]: (t[i,j]=5 összeg:[1..n] ℕ
t[i,j]=4)
m
összeg(i) =
t [i , j ] j 1
A feladat az utófeltétel alapján két részre bontható: egy [1..n] intervallumú, jó(i) feltételű és összeg(i) függvényű feltételes maximumkeresésnek, valamint egy elágazásnak a szekvenciájára. Az elágazás feltétele az l, igaz ága az átl:=max/m értékadás, hamis ága a SKIP lesz.
120
6.2. Beágyazott visszavezetés
l := hamis i =1 .. n jó(i)
l
jó(i)
i:ℕ l jó(i)
max<összeg(i)
SKIP
max, ind := összeg(i), i
l, max, ind := SKIP igaz, összeg(i), i max:ℕ
l átl := max/m
SKIP
A jó(i) kiszámolásához az u:=jó(i) értékadást megoldó alprogramra van szükség, ahol az u egy logikai változó. Az u:=jó(i) nem-megengedett értékadás egy optimista lineáris kereséssel helyettesíthető, amelyben futóindexnek a j-t használjuk. Az összeg(i) meghatározásához az össz:=összeg(i) értékadást megoldó alprogramra van szükség, ahol az össz egy egészszám változó. Az össz:=összeg(i) részfeladat egy összegzésre vezethető vissza, amelyben futóindexnek ugyancsak a j-t használjuk, de ez nem ugyanaz a j, mint előbb. össz:=összeg(i)
u:=jó(i)
össz := 0
u,j := igaz,1 u
j:ℕ
j m
u := t[i,j]=5
t[i,j]=4
j = 1..m
j:ℕ
össz := össz +t[i,j]
j := j+1 A főprogram fenti verziója ezen alprogramok függvényszerű hívását mutatja, de a hatékonyság érdekében ezeket a hívásokat érdemes egy-egy értékadásba kiemelni. Az u:=jó(i) hívást a ciklusmag hármas elágazása előtt kell végrehajtani, az elágazás feltételeiben pedig az u változót használni. Az össz:=összeg(i) hívásra a középső ágban található másik elágazás előtt van szükség, amelyben elég az össz változóra hivatkozni. Mind az u, mind össz egy újonnan bevezetett segédváltozója a programnak.
121
6. Többszörös visszavezetés
l := hamis i =1..n
i:ℕ
u := jó(i) u SKIP
l
u
u: l
össz := összeg(i) max<össz max, ind := össz, i
u
l, max, ind := össz,max:ℕ igaz, összeg(i), i
SKIP l
átl:=max/m
SKIP
6.7. Példa. Adott egy egész számokat tartalmazó vektor, és annak egy k indexe. Igaz-e, hogy van olyan elem a megadott index előtt a vektorban, amelyik nagyobb vagy egyenlő az indextől kezdődő elemek mindegyikénél! A feladat talán nem tűnik túl érdekesnek, de a gyakorlás szempontjából nézve igen hasznos, mert sokféle módon lehet megoldani. A feladat állapottere, előfeltétele könnyen megfogalmazható: A = (t:ℤn, k:ℕ, l: ) Ef = ( v=v’ k=k’ 1 k n ) Az utófeltételt azonban számos – más-más programot eredményező – változatban lehet felírni. Ezek közül bemutatunk néhányat. Az egyes utófeltételekhez tartozó programok megadását az olvasóra bízzuk. Az első megoldás a feladat szószerinti értelmezéséből születik: "Van olyan elem a vektor első részében, amelyik a vektor hátsó részének mindegyik eleménél nagyobb vagy egyenlő." A feladatot tehát egy lineáris keresésbe ágyazott optimista kereséssel oldhatjuk meg. Ennek specifikációját láthatjuk itt:
122
6.2. Beágyazott visszavezetés
Uf = ( Ef
l= i [1..k–1]: j [k..n]: v[i] l, i := hamis, 1 l
i
i:ℕ
k–1
l, j := igaz, k l
v[j] )
j
j:ℕ
n
l := v[i]
v[j]
j := j + 1 i := i + 1 A megoldásban szereplő két programozási tétel egymáshoz való viszonya megcserélhető. A feladat úgy is megfogalmazható, hogy igaze, hogy a vektor hátsó részének minden eleme kisebb, a vektor első részének legalább egy eleménél. Ilyenkor a megoldás egy optimista lineáris keresésbe ágyazott lineáris keresés lenne. Ez rávilágít arra, hogy a két részfeladat nincs alá- illetve fölérendelt viszonyban. Mindkét változatnak a futási ideje (a v[i] és v[j] elemek összehasonlításainak száma) legrosszabb esetet feltételezve: (k–1)*(n– k+1). A második megoldás már egy kis ötletet igényel: Ha a vektor első részének legnagyobb eleme nagyobb vagy egyenlő a hátsó rész legnagyobb eleménél, akkor van az első részben olyan elem, amelyik a hátsó rész minden eleménél nagyobb vagy egyenlő. Nem kell mást tenni, mint a vektor első részében is, és a hátsó részében is kiválasztani a maximális elemet, és a kapott maximumokat összehasonlítani. Itt tehát két maximum kiválasztásnak, és egy összehasonlításnak a szekvenciájáról van szó. Ez a megoldás azonban feltételezi, hogy 2 k n, hiszen csak ekkor lesz a vektor két szakasza, ahol a maximumot keressük, biztos nem üres. Ez azonban nem következik az eredeti előfeltételből. Külön kell tehát rendelkezni a k=1 esetről, amikor biztosan hamis a feladatra adandó válasz.
123
6. Többszörös visszavezetés
Uf = ( Ef
l = k>1
k 1
n
i 1
j k
max v[i] max v[ j ] )
Az utófeltételből kiolvasható, hogy ha k 2, akkor a k vizsgálatával együtt n+1 összehasonlítást kell végezni, k=1 esetén csak egyet. k=1 max1 := v[1]
max1:ℤ
i = 2..k–1
i:ℕ
max1
SKIP
max2 := v[k]
max2:ℤ
j = k..n
j:ℕ
max2
SKIP
l := max1 max2
A harmadik megoldás is a maximum kiválasztással kapcsolatos, de új ötleten alapul. Válasszuk ki a maximális elemet a vektorból úgy, hogy a maximum kiválasztás több maximális elem esetén a legelsőnek az indexét adja meg. Ha ez az index a vektor első részébe esik, akkor igenlő választ adhatunk a feladat kérdésére. A vektor első részébe eső maximális elem ugyanis minden elemnél nagyobb vagy egyenlő, így a vektor hátsó részének elemeinél is. Ehhez a megoldáshoz – csak úgy, mint az előzőhöz – pontosan n összehasonlítást kell végezni. Uf = ( Ef
n
max, ind = max v[i] i 1
124
l = ind
6.2. Beágyazott visszavezetés
max, ind := v[1], 1
max:ℤ ind:ℕ
i = 2..n
i:ℕ
max
SKIP
l := ind < k A negyedik megoldás az eredeti megfogalmazáshoz nyúl vissza: a vektor első részében keres alkalmas tulajdonságú elemet. Az alkalmasságot azonban – és itt a második megoldás ötlete jelenik meg újra – a hátsó rész legnagyobb elemével való összehasonlítással döntjük el: keresünk az első részben a hátsó rész maximális eleménél nagyobb vagy egyenlő elemet. Az Uf = ( Ef
n
l = i [1..k–1]: v[i] max v[ j ] ) j k
utófeltétel azonban ügyetlen, mert a lineáris keresésbe ágyazza be a maximum kiválasztást, holott a hátsó rész maximum kiválasztása független a kereséstől – az első és hátsó rész feldolgozása nincs aláilletve fölérendelt viszonyban –, ezért elég, ha a maximum kiválasztást a lineáris keresés előtt (szekvenciában) egyszer végezzük el: Uf = ( Ef
n
max = max v[ j ]
l = i [1..k–1]: v[i] max )
j k
max := v[k]
max:ℤ
i = k+1..n
i:ℕ
max
SKIP
l, i := hamis, 1 l
i
k–1
l := v[i]
max
i := i+1
125
6. Többszörös visszavezetés
Ebben a megoldásban az összehasonlítások száma legrosszabb esetben is csak n–1 lesz. 6.8. Példa. Adott a síkon n darab pont. Állapítsuk meg, melyik két pont esik legtávolabb egymástól! Specifikáljuk először a feladatot! A síkbeli pontokat egy derékszögű koordináta rendszerbeli koordináta párokkal adjuk meg; külön-külön tömbökben az x és az y koordinátákat. Így az i-edik pont koordinátái az x[i] és y[i] lesznek. A feladat központi eleme, az i-dik és j-dik pont távolsága a ( x[i ] x[ j ]) 2
( y[i ] y[ j ]) 2
képlet alapján számolható, de mivel a pontos távolságot nem kéri a feladat, a tényleges távolságok helyet azok négyzeteivel érdemes dolgozni, mert ezek kiszámolásához nincs szükség gyökvonásra. A táv(i,j)-vel az i-dik és j-dik pont távolságának négyzetét fogjuk jelölni. Ezeket az értékeket gondolatban egy n×n-es szimmetrikus mátrixba rendezhetjük, és a feladat tulajdonképpen ezen mátrix főátló nélküli alsóháromszög részében történő maximum kiválasztás. A megoldás attól nehéz, hogy az intervallumra megfogalmazott maximum kiválasztás csak egy dimenziós alakzatba rendezett elemeket tud vizsgálni, itt pedig a vizsgált táv(i,j) elemek kétdimenziós alakzatot alkotnak. Ezért vagy felfűzzük gondolatban a vizsgált értékeket egy egydimenziós tömbbe (vektorba), vagy az alsóháromszög rész minden sorában külön-külön meghatározzuk a maximumot, és ezen sormaximumok között egy külön maximum kiválasztással keressük meg a legnagyobbat. Nézzük meg mindkét megoldást. (A 8. fejezet útmutatása alapján egy harmadik megoldást is adhatunk majd erre a problémára, és a 8.5. példában egy hasonló feladattal találkozhatunk.) Az első megoldás egy maximum kiválasztásba ágyazott maximum kiválasztás lesz. A beágyazott maximum kiválasztás egy sor maximális értékét és annak az indexét állítja elő, azaz minden sorra egy két-komponensű értéket, a külső maximum kiválasztásnak pedig ilyen két-komponensű értékeket kell összehasonlítani. Ezért meg kell határoznunk azt, hogy egy ilyen két-komponensű érték mikor nagyobb
126
6.2. Beágyazott visszavezetés
egy másik két-komponensű értéknél: ha annak első komponense nagyobb a másik első komponensénél. A = (x:ℤn, y:ℤn, ind:ℕ, jnd:ℕ) Ef = ( x=x’ y=y’ n≥2 ) n
(max, jnd), ind = max g(i) )
Uf = ( Ef g:[2..n]
i 2
ℝ×ℕ és i 1
g(i) = max táv(i,j) és j 1
táv(i,j) ( x[i] x[ j ]) 2
( y[i] y[ j ]) 2
A feladat visszavezethető a maximum kiválasztás programozási tételére (a 2..n intervallumon a g függvény értékei felett), ahol a g(i) ≥ g(j) akkor teljesül, ha g(i)1 ≥ g(j)1. max, jnd, ind := g(2), 2 i = 3 .. n
max:ℕ i:ℕ
m, k := g(i)
m:ℝ k:ℕ
m > max max, jnd, ind =m, k, i
SKIP
Az m, k:=g(i) részfeladat is a maximum kiválasztás programozási tételére vezethető vissza az 1..i-1 intervallumon a táv(i,j) értékeivel. Ezt a maximum kiválasztást a főprogram két helyen is meghívja. Ezek közül az egyik a kezdeti értékadásban a max és a jnd értékeit előállító g(2), amelyet fejben is kiszámolhatunk, hiszen a képzeletbeli mátrix alsóháromszög részének második sorában mindössze egyetlen elem van, így a kezdeti értékadás max, jnd, ind:=táv(2,1), 1, 2 -re módosul.
127
6. Többszörös visszavezetés
m, k:=g(i) m, k:=táv(i,1), 1 j = 2 .. i-1
j:ℕ
s := táv(i,j)
s:ℝ
s>m m, k:=s, j
SKIP
A második megoldáshoz azt kell megvizsgálnunk, hogyan lehet egy n×n-es négyzetes mátrix alsóháromszög részét egy n(n–1)/2 hosszú egydimenziós tömbbe kiteríteni. Pontosabban azt, hogy ha sorfolytonosan fűzzük egymás után az elemeket (a [2,1]-dik elem lesz az [1] elem, a [3,1]-dik a [2]-dik, a [3,2]-dik a [3]-dik, a [4,1]-dik a [4]dik, stb.), akkor a k-dik elem a mátrix hányadik sorának hányadik eleme lesz. Mivel az alsóháromszög rész sorai egyre növekedő elemszámúak, az i-dik sor előtt (i–1)(i–2)/2 darab elem van, így a mátrix [i,j]-dik eleme a sorfolytonos kiterítésben az (i–1)(i–2)/2+j-dik elem lesz, ahol j 2k ( 2k –1) teljesül, akkor i= 2k +1, különben i = 2k áll fenn.14 Ezután a feladatot az itt elképzelt vektor elemeinek maximum kiválasztásaként specifikáljuk. 14
Ez a megfordítás bizonyításra szorul. A k=(i–1)(i–2)/2+j (j 2k ( 2k –1), akkor 2k =i– 1, ugyanis az ellenkezőjét ( 2k =i) feltéve a 2k>i(i–1) összefüggést kapnánk, ami ellentmond az első megállapításnak. Ha viszont 2k≤ 2k ( 2k –1), akkor i= 2k , ugyanis az ellenkezőjét ( 2k =i–1) feltéve a 2k≤(i–1)(i–2) összefüggést kapnánk, ami ugyancsak ellentmondás.
128
6.2. Beágyazott visszavezetés
n ( n 1) / 2
Uf = ( Ef
max, knd =
max k 1
táv(h(k)) (ind,jnd)=h(knd) )
h:[1..n(n–1)/2] ℕ×ℕ h( k )
( 2k +1, 2k(
2k ( 2k -1 ) ) ha 2k
2k , 2k- ( 2k -1)(
2k -2 ) ) ha 2k
(
2k -1)
2k
(
2k -1)
2k
Ez a feladat visszavezethető a maximum kiválasztás programozási tételére, amelyet az 1..n(n–1)/2 intervallumon, a táv h függvénykompozíció értékeire kell alkalmazni. A kezdeti értékadásban szereplő táv(h(1)) a táv(2,1)-gyel azonos. max, knd := táv(2,1), 1 k = 2 .. n(n–1)/2
k:ℕ
táv(h(k))>max max, knd := táv(h(k)), k
SKIP
ind, jnd :=h(knd)
6.3. Program-átalakítások Korábbi feladat-megoldásainkban gyakran alkalmaztunk program-átalakításokat. A program-átalakítás során egy programból egy olyan másik programot állítunk elő, amely megoldja mindazokat a feladatokat, amelyeket az eredeti program is megoldott. Ezt gyakran ekvivalens átalakítással érjük el, azaz úgy, hogy az új program hatása teljesen megegyezik az eredeti program hatásával. Példaként emlékeztetünk arra, ahogyan 6.2. példában a kiválasztás programozási tételét kétféleképpen is átalakítottuk azért, hogy explicit módon egy értékadásba emeljük ki a ciklusfeltételében szereplő nem-megengedett kifejezést. Egy-egy program-átalakításnak a helyessége a programozási modellünk eszközeivel belátható, de itt nem foglakozunk a
129
6. Többszörös visszavezetés
bizonyítással, hanem a lustább „látszik rajta, hogy jó” magyarázatot használjuk. Egy program-átalakításnak igen sokféle célja lehet. Esetenként csak szebbé, áttekinthetőbbé formálja a programot, néha az implementáció, azaz a konkrét számítógépes környezetbe ültetés végett van rá szükség, máskor a program hatékonyságán lehet javítani. Az előbb felsorolt szempontokon kívül különösen fontosak azok a program-átalakítások, amelyek a program előállítását segítik a programtervezés fázisában. A programtervezést támogató átalakítások közé sorolható például az, amikor egy új változó bevezetésének segítségével emeltünk ki nemmegengedett kifejezést (másik értékadás jobboldali kifejezésének részét, egy elágazás vagy ciklus feltételét) egy önálló értékadásba, és ezáltal kijelöltük a programunknak egy olyan részfeladatát, amelyet aztán megoldottunk. Ugyancsak ilyen átalakításnak tekintjük a rekurzív függvények kibontását is, amelyre a következő alfejezetben mutatunk példákat. Egy szimultán értékadás egyszerű értékadások szekvenciájára történő felbontása is támogathatja a programtervezést, de többnyire egy implementálást támogató átalakítás, amennyiben olyan programozási nyelven kell leírnunk a programunkat, amelyik nem ismeri (és többnyire nem ismerik) a szimultán értékadást. Eddig főleg olyan szimultán értékadásokkal találkoztunk, amelyet alkotó egyszerű értékadások függetlenek voltak egymástól, és ebben az esetben az átalakítás könnyű volt. Egy egyszerű értékadás akkor függ egy másiktól, ha a jobboldali kifejezésében szerepel egy olyan változó, amely a másik értékadás baloldali változója. Ha ilyen függés nem áll fenn a szimultán értékadást alkotó egyszerű értékadások között, akkor azokat tetszőleges sorrendben végrehajtva a szimultán értékadással azonos hatású szekvenciához jutunk. Ha van függő viszony, de ezek rendszere nem alkot kört, azaz a szimultán értékadás egyszerű értékadásainak bármelyik csoportjában lehet találni olyat, amelyik nem függ a csoport többi egyszerű értékadásától, akkor az egyszerű értékadásokat olyan sorrendben kell végrehajtani, hogy előre azt az egyszerű értékadást vesszük, amelyiktől senki más nem függ, majd mindig azt, amelyiktől csak az előtte végrehajtottak függnek. Egy teljesen általános szimultán értékadás felbontásához azonban – amikor a felbontással kapott
130
6.3. Program-átalakítások
egyszerű értékadások kölcsönösen függnek egymástól – új változók bevezetésére van szükség. Az x1, … , xn := F1(x1, … , xn), … , Fn(x1, … , xn) értékadás helyett az alábbi két programot használhatjuk: y1:=x1
y1:=x1
…
…
yn-1:=xn-1
yn-1:=xn-1
x1:=F1(x1, x2, … , xn-1, xn)
vagy
x1:=F1(y1, y2, … , yn-1, xn)
x2:=F2(y1, x2, … , xn-1, xn)
x2:=F2(y1, y2, … , yn-1, xn)
…
…
xn-1:=Fn-1(y1, y2, … , yn-1, xn)
xn-1:=Fn-1(y1, y2, … , yn-1, xn)
xn:=Fn(y1, y2, … , yn-1, xn)
xn:=Fn(y1, y2, … , yn-1, xn)
Az implementáció egyszerűsítését támogatja a szekvenciák sorrendjének megváltoztatása is. Természetesen, ha a szekvencia második tagja függ az elsőtől, akkor erre nincs lehetőség. Programrészek (itt a szekvenciák tagjai is) függetlenségén ugyanazt kell érteni, mint az értékadások esetében, ugyanis minden programrész helyettesíthető egy vele ekvivalens – többnyire nem-megengedett – értékadással. Program hatékonyságát támogató átalakítások közé soroljuk a különféle összevonási és kiemelési szabályokat, a rekurzív függvény kibontását (lásd következő alfejezet), vagy alkalmas segédváltozók bevezetését többször felhasznált értékek tárolására. Összevonásról például akkor beszélhetünk, amikor egymáshoz hasonló, de egymás működésétől független ciklus szekvenciájából egyetlen ciklust készítünk úgy, hogy az új ciklus magja az eredeti ciklusmagok szekvenciája lesz. Az alábbi átalakításnál szekvenciacserét is végeztünk, amikor a ciklusok előtti inicializáló
131
6. Többszörös visszavezetés
programrészeket mind előre hoztuk. Természetesen ezek sem függhetnek egymástól. S11
S11
i = m .. n
…
S1
Sk1
…
i =m..n
Sk1
helyett
S1
i = m..n
…
Sk
Sk
Sok egymást kizáró, de az összes esetet lefedő feltételű és egymás működésétől független kétágú elágazásnak a szekvenciájából egyetlen sokágú elágazást készítünk.
felt1 S1
SKIP
helyett
felt1
…
feltk
S1
…
Sk
… feltk Sk
SKIP
A baloldali algoritmusnak nem lehet olyan végrehajtása, amikor minden elágazásnak az „else” ága hajtódik vége, mert a feltételrendszer a feltevésünk szerint teljes. Ha ez nem állna fenn, akkor a jobboldali elágazást ki kell egészíteni egy „else” ággal. Kiemelési szabály alkalmazására mutat speciális példát az alábbi átalakítás:
132
6.3. Program-átalakítások
i = 0..10
S1
i=0 S1
helyett
i = 1 .. 10
S2
S2
Az összevonásokkal illetve kiemelésekkel nemcsak hatékonyságot lehet javítani, hanem a program áttekinthetőségén, olvashatóságán is. Az alábbi példa ezt is jól illusztrálja. 6.9. Példa. Soroljuk fel azokat a műkorcsolyázó versenyzőket, akik holtversenyben az első helyen végeztek a kötelező gyakorlatuk bemutatása után. Az n versenyző programját egy m tagú zsűri pontozta, és egy versenyző összesített pontszámát úgy kapjuk, hogy a legjobb és legrosszabb pontot elhagyva a többi pontszám átlagát képezzük. A = (t:ℝn×m, s:ℕ*) Ef = ( t=t’ n>0 m>2 ) n
n
Uf = ( Ef
s= i 1 pont ( i ) max
i
max = max pont(i) ) i 1
ℝ
pont:[1..n] m
pont(i) = ( i 1
m
m
j 1
j 1
t[i, j ] - max t[i, j ] - min t[i, j ] )/(m–2)
Az érvényes összesített pontszámhoz legalább három pontszám kell, illetve a maximum kiválasztás miatt minimum egy versenyző; ezt tükrözi az előfeltétel. Az utófeltételben szereplő szimbólumot az összefűzés műveletének jelölésére használjuk. Ennek segítségével tudunk sorozatokat (itt egy elemű sorozatokat) egy sorozattá összefűzni. A maximum kiválasztás szempontjából a pont(i) képletében szereplő m–2-vel történő osztásra nincs szükség. Az utófeltételben bevezettünk egy max:R változót. Világos, hogy először ennek az értékét kell meghatározni, és csak ezután tudjuk a kiválogatást elvégezni. A megoldás egy maximum kiválasztás, majd egy összegzés (kiválogatás) szekvenciája lesz, amelyeken belül a pont(i)-t több helyen is használjuk. A pont(i) kiszámolásához egy összegzésre, egy maximum és egy minimum kiválasztásra van szükségünk.
133
6. Többszörös visszavezetés
Nézzük meg először a megoldó programot program-átalakítások nélkül. Először a főprogramot: max := pont(1)
max:ℝ
i = 2 .. n
i:ℕ
d := pont(i)
d:ℝ
d>max max := d
SKIP
s:= <> i = 1 .. n max = pont(i) s := s
SKIP
d := pont(i) o := 0
o:ℝ
j = 1 .. m
j:ℕ
o := o+t[i,j] maxi := t[i,1]
maxi:ℝ
j = 2 .. m t[i,j]>maxi maxi := t[i,j]
SKIP
mini := t[i,1]
mini:ℝ
j = 2 .. m t[i,j]<mini mini := t[i,j]
SKIP
d := (o-maxi-mini)/(m-2) Az így kapott program azonban javítható. Egyrészt összevonhatjuk a pont(i)-t kiszámoló alprogram három programozási
134
6.3. Program-átalakítások
tételét egyetlen ciklusba. Ehhez mindössze annyit kell tenni, hogy az összegzés ciklusát is (j=2..m) formájúra kell alakítani, amely egy kiemeléssel elvégezhető. d := pont(i) o,maxi,mini := t[i,1], t[i,1], t[i,1] j = 2 .. m
o, maxi, mini,:ℝ j:ℕ
o := o+t[i,j] t[i,j]>maxi
t[i,j]<mini
maxi := t[i,j]
mini := t[i,j]
SKIP
d := (o-maxi-mini)/(m-2) Az eredeti három ciklus kezdeti értékadásait az összevonással kapott ciklus elejére emeltük ki. Az ily módon közös ciklusmagba került két elágazás pedig (lévén egymástól függetlenek, a feltételeik kizárják egymást, bár nem teljesek) egy három ágú elágazásba vonhatók össze. Másrészt bevezethetjük a versenyzők pontszámait tartalmazó p:ℝn tömböt, amelyet a program elején feltöltünk a pont(i) értékekkel, így később mindenhol a pont(i) helyett a p[i]-t használhatjuk. A p tömb feltöltése, azaz a i [1..n]:p[i]=pont(i) részfeladat lényegében egy p= n i 1
pont (i)
összegzés lesz, amelyet a program legelején kell
elvégezni. Ennek ciklusa – ha azt (i=2..n) formájúra alakítjuk – viszont összevonható a max értékét meghatározó maximum kiválasztással.
max, p := inicializálás
135
6. Többszörös visszavezetés
p[1] := pont(1) max := p[1] i = 2..n
i:ℕ
p[i]:=pont(i) p[i]>max max := p[i]
SKIP
Ezek után a főprogram az alábbi lesz: max, p := inicializálás
p:ℝn, max:ℝ
s := <> i = 1 .. n
i:ℕ
max=p[i] s := s
SKIP
6.4. Rekurzív függvény kibontása A beágyazott visszavezetéseknél láthattuk, hogy a problémamegoldás során egy-egy új függvény bevezetése mennyire jól szolgálja egy feladat részfeladatokra bontását. Az ilyen függvények kitalálása, absztrakciója a visszavezetéses technika alkalmazásának katalizátora. Ebben az alfejezetben olyan szellemes, trükkös megoldásokat mutatunk be, amelyekhez úgy jutunk, hogy a visszavezetés során bevezetett absztrakt függvények rekurzív függvények lesznek. A bemutatott megoldásokban azonban ezeknek a rekurzív függvényeknek az értékét nem a rájuk vonatkozó programozási tétellel számítjuk ki, hanem egy ügyes programátalakítási technikát alkalmazunk. 6.10. Példa. Két táblázat (be és ki vektorok) azt mutatja, hogy az i-edik órában hány látogató érkezett egy múzeumba (be[i]) és hány
136
6.4. Rekurzív függvény kibontása
látogató távozott (ki[i]) a múzeumból. Melyik órában (óra végén) volt a legtöbb látogató a múzeumban? (Feltehetjük, hogy az adatok nem hibásak és kezdetben üres volt a múzeum.) Az első óra végén be[1]-ki[1] látogató maradt benn a múzeumban. A második órában ehhez hozzájött még be[2] látogató, és elment ki[2]; tehát a második óra végén be[1]–ki[1]+be[2]–ki[2] látogató volt bent. Így tovább folytatva megkonstruálhatunk egy függvényt, amelyik azt mutatja meg, hogy az i-edik óra végén hány látogató volt a múzeumban. Ahelyett azonban, hogy ezt a függvényt összegzésként fogalmaznánk meg, egy rekurzív definíciót adunk rá: az i-edik óra végén a múzeumban levő látogatók száma az i–1-edik óra végén benn levők plusz az i-edik órában érkezők (be[i]), mínusz a távozók száma (ki[i]). f:[1..n] ℕ f(1) = be[1]–ki[1] f(i) = f(i–1)+be[i]–ki[i] i [2..n] A feladat ezek után az, hogy keressük meg, hol veszi fel ez a függvény a maximumát. Ennek megfelelően a specifikáció: A = (be:ℕn, ki:ℕn, ind:ℕ) Ef = ( be=be’ ki=ki’ ) n
Uf = ( Ef
f(ind)= max f(i) ) i 1
A feladatot visszavezetjük a maximum kiválasztás programozási tételére max,ind:=f(1),1
max:ℕ
i = 2 .. n
i:ℕ
max
SKIP
Ennek ciklusmagjában egy rekurzív függvény értékét kell kiszámolni. Ezt azonban hatékonysági okból nem egy olyan alprogrammal végezzük el, amely a rekurzív függvény helyettesítési értékét számolná ki, hanem egy speciális technikát – a rekurzív függvény kibontását – alkalmazzuk. Ennek alapötlete az, hogy az f(i) kiszámolásához az előző iterációs lépésében kiszámolt f(i–1)-et felhasználhatjuk, ha azt egy s
137
6. Többszörös visszavezetés
segédváltozóban megjegyezzük, azaz feltesszük, hogy a ciklusfeltétel ellenőrzésekor álljon fenn az s= f(i–1) feltétel. Ezt a ciklusba lépéskor – amikor az s=f(1) feltételt kell beállítani – az s:=be[1]–ki[1] értékadással biztosíthatjuk, a ciklusmagban pedig elég az s:=f(i) értékadással, hiszen az ez után végrehajtódó i:=i+1 értékadás hatására újra be fog állni az s=f(i–1). Az s:=f(i) értékadást pedig az f(i) definíciója alapján az s:=s+be[i]–ki[i] értékadással váltjuk ki. s := be[1]–ki[1]
s:ℕ
max, ind := s, 1
max:ℕ
i = 2 .. n
i:ℕ
s := s+be[i]–ki[i] max<s max, ind := s, i
SKIP
Ennek a program-átalakítási technikának elengedhetetlen feltétele, hogy a kibontott rekurzív függvény értelmezési tartománya és az azt tartalmazó ciklus által befutott egész számok intervalluma egybe essen. Sőt a legjobb az, ha a ciklus indexváltozójának első értéke (a fenti példában ez a 2) a rekurzív függvény bázisa is egyben, hogy a ciklus előtt a rekurzív függvény közvetlenül definiált értékeit kelljen a segédváltozó(k)nak adni, a ciklusmagban pedig a rekurzív képlet alapján módosítsuk az(oka)t. Ezt biztosítandó sokszor kis mértékben módosítani is kell a rekurzív függvény definícióján. Ha a fenti rekurzív függvényt mondjuk egy számláláson belül kellene használnunk (például arra vagyunk kíváncsiak, hogy hányszor volt benn a múzeumban ötszáznál is több látogató), akkor a ciklus az 1..n intervallumot futná be, ezért a ciklus előtt az f(0) értékére lenne szükség. Ehhez módosítani kellene a rekurzív függvény definícióján: ki kellene terjeszteni az értelmezési tartományát a 0..n intervallumra az f(0) = 0 értelmezéssel, a korábban megadott rekurzív képlet pedig már az f(1)-re is jó, azaz a bázist 1-re módosítjuk. Általánosan is leírhatjuk a rekurzív függvény kibontása során végrehajtott lépéseket. Tegyük fel, hogy rendelkezünk egy f függvény
138
6.4. Rekurzív függvény kibontása
értékeit felhasználó általános algoritmussal programozási tételből származik).
(amely valamelyik
S1 i = m .. n S2(f(i)) Az f az m–k+1..n intervallumon értelmezett k-ad rendű m bázisú rekurzív függvény (lásd 4.3. alfejezet), az S1 egy tetszőleges, az S2 pedig az f(i)-től függő program. Látható, hogy a rekurzív függvény bázisa a ciklushoz igazodik. Emeljük az f(i) kifejezést egy segédváltozóba (ha f több komponensű, akkor több segédváltozóba). S1 i = m .. n s1:=f(i) S2(s1) A fenti segédváltozók mellé vezessünk be még annyi segédváltozót, hogy azok száma a rekurzió rendjével (k) legyen egyenlő. (Ha a függvény több komponensű, akkor a változók száma a komponensszám és a rend szorzata.) Módosítsuk a programot úgy, hogy a ciklusfeltétel ellenőrzésekor az eddig érvényesülő állítások mellett az s1=f(i–1), … , sk=f(i–k) egyenlőségek is teljesüljenek.15 s1, … ,sk := f(m-1), … , f(m-k) 15
Mindegyik programozási tétel ciklusához választhatunk egy invariáns állítást (lásd 3.4. alfejezet), amely segítségével a tétel és az abból származtatott programok helyessége is bizonyítható. Itt tulajdonképpen ennek az invariánsnak a kiegészítéséről van szó. Ezt követően úgy kell módosítani a programot, hogy annak helyességét ezzel a bővített ciklus-invariánssal be lehessen látni.
139
6. Többszörös visszavezetés
S1 i = m .. n s1, … ,sk := f(i), … , f(i–k+1) S2(s1) Helyettesítsük az f(m-1), … , f(m-k) kifejezéseket a rekurzív függvény definíciójában rögzített e1, … ,ek értékekkel, az f(i)-t ugyancsak a definícióból származó h(i)-vel, a korábban feltett egyenlőségek miatt pedig az f(i–1), … , f(i–k+1) kifejezéseket az s1, … ,sk–1 változókkal. s1, … ,sk := e1, … ,ek S1 i = m .. n s1, … ,sk := h(i, s1, … ,sk), s1, … ,sk–1 S2(s1) 6.11. Példa. Egy repülőgéppel elrepültünk a Csendes Óceán szigetvilágának egy része felett, és meghatározott távolságonként megmértük a felszín tengerszint feletti magasságát. Az így kapott nemnegatív valós értékeket egy n hosszúságú x vektorban tároljuk. Adjuk meg a mérések alapján, hogy mekkora a leghosszabb sziget, és a repülés hányadik mérésénél kezdődik, illetve végződik ezek közül az egyik. Ennek a feladatnak többféle megoldása is van, amelyek közül most egy rekurzív függvényt bevezető megoldást ismertetünk. Képzeljük el azt a függvényt, amelyik a repülés során végzett mérési pontokon van értelmezve; sziget felett azt mutatja meg, hogy a sziget kezdete óta hány mérési pontot tett meg a repülő, a tenger felett viszont nulla az értéke.
140
6.4. Rekurzív függvény kibontása
ℕ
f:[0..n] f(0) = 0 f(i) =
f(i 1) 1 ha
x[i ]
0
ha
x[i ]
0
0
i [1..n]
A feladat megoldásához elég ennek a függvénynek megtalálni a maximumát. Ahol ez a függvény a maximális értékét felveszi, ott van a leghosszabb sziget végpontja, amelyből a szigethosszának ismeretében a sziget kezdőpontja is könnyen meghatározható. (Megjegyezzük, hogy ha a rekurzív függvényt nem a vektor elejéről hátrafelé haladva képeznénk, hanem a végéről az eleje felé, akkor egy hátulról előre haladó maximum kiválasztással közvetlenül a leghosszabb sziget elejét találnánk meg.) A = (x:ℕn, hossz:ℕ, kezd:ℕ, végz:ℕ ) Ef = ( x=x’ ) n
Uf = ( Ef
hossz,végz = max f (i )
kezd=végz–hossz+1 )
i 1
A feladat egy maximum kiválasztás és egy értékadás szekvenciájaként oldható meg. A nem-megengedett f(i) kifejezéseket egy segédváltozó segítségével kiemeltük. h := f(1)
h:ℕ
hossz, ind := h, 1 i = 2 .. n
i:ℕ
h := f(i) hossz
SKIP
kezd := végz–hossz+1 Most bontsuk ki a rekurzív függvényt.
141
6. Többszörös visszavezetés
x[1]>0 h := 1
h := 0
h:ℕ
hossz, ind := h, 1 i = 2..n
i:ℕ
x[i]>0 h := h+1
h := 0
hossz
SKIP
kezd := végz–hossz+1 Mivel a rekurzív függvény bázisa 0 és nem 1, ezért a h:=f(1) értékadást is a rekurzív képlet alapján valósítottuk meg (ha x[1] pozitív, akkor az f(1) értéke 1 lesz, különben 0). 6.12. Példa. Egy m hosszúságú karakterekből álló sorozatot (nevezzük ezt szövegnek) kódolni szeretnénk egy másik, n hosszúságú karaktersorozat (nevezzük ezt táblázatnak) segítségével. A kód egy olyan m hosszúságú szigorúan növekedő számsorozat legyen, amelyiknek i-edik értéke indexként mutat a táblázatnak arra a karakterére, amely éppen az eredeti szöveg i-edik karaktere. Döntsük el, hogy egy adott szöveg és táblázat esetén elkészíthető-e egyáltalán a szöveg kódja! A feladat megoldásához tehát azt kell megvizsgálni, hogy a szöveg minden karaktere megfelelően kódolható-e. A = (szöveg: m, tábla: n, l: ) Ef = ( szöveg=szöveg’ tábla=tábla’ ) Uf = ( Ef l= i [1..n]: kódolható(i) ) A szöveg i-edik karakterét a táblázat akkor képes kódolni, ha ez a karakter benne van a táblázatban, mégpedig azon karakter után, amelyikkel a szöveg i–1-edik karakterét kódoltuk. Ha tehát ismerjük, hogy a táblázat hányadik karakterével (jelöljük ezt ind-del) kódoltuk a szöveg[i–1]-et, akkor a táblázatban az ind+1-edik pozíciótól elindulva kell megkeresni a szöveg[i]-t. Ha találunk ilyet, akkor a szöveg[i]
142
6.4. Rekurzív függvény kibontása
kódolható és a találat pozíciója majd az új ind, ahonnan a következő keresés indul. Az itt körvonalazott lineáris keresés precíz megfogalmazásához egy rekurzív definíciójú függvényt kell bevezetnünk. Mivel a lineáris keresésnek két eredménye van, a rekurzív függvény is kétértékű, kétkomponensű lesz. Az f(i) első komponense (f1(i)) egy logikai érték, amely azt mutatja majd meg, hogy sikerült-e a szöveg[i]-t kódolni; a második komponens (f2(i)) pedig a táblázat azon indexe, amely előtti pozíción a táblázatban a szöveg[i]-t megtaláljuk. Ez tehát nem a keresés megszokott eredménye (ind), hanem egy eggyel nagyobb érték. Mindkét értéket az a lineáris keresés állítja elő, amelyik az előző sikeres lineáris keresés által talált pozícióról (f2(i-1)) indul, és keresi a szöveg[i]=tábla[j] feltételnek eleget tevő j-t. A rekurzív függvényt a nem létező 0-dik karakterre is definiálhatjuk. f :[0..m] ×ℕ f(0)= (igaz, 1) i [1..m]: f(i) = (l, ind+1) n
ahol (l, ind) = search szöveg[i] tábla[ j ] j f 2(i 1)
A korábban bevezetett kódolható(i) feltétel tehát megegyezik a rekurzív függvény i-edik helyen felvett logikai értékével. Ennek megfelelően a feladat utófeltétele: Uf = ( Ef l= i [1..m]: f1(i) ) formában is írható, amelyből látható, hogy egy optimista lineáris keresésbe ágyazott rekurzív függvénnyel oldható meg a feladat. A megoldás során a rekurzív függvényt természetesen kibontjuk egy l logikai változó és egy j egész változó segítségével. A j változót egyben az f(i)-t kiszámoló lineáris keresés futóindexeként is használhatjuk, hiszen ennek értéke a keresés leállásakor éppen az ind+1 lesz. Ennél fogva az ind változót és arra vonatkozó értékadást elhagyhatjuk a programból.
143
6. Többszörös visszavezetés
i, l :=1, igaz l
i, l, j := 1, igaz, 1
i:ℕ
i m
l
i,j:ℕ
i m
n
l := f1(i)
l, j := search szöveg[i] tábla[ j ] j
i := i+1
i := i+1
Az első megoldás tehát: i, l, j := 1, igaz, 1
i,j:ℕ
l i m l := hamis l j n l := szöveg[i]=tábla[j] j := j+1 i := i+1 Amennyiben a rekurzív függvényre azt is megköveteljük, hogy ha f1(i) valamely i-re hamis, akkor az f2(i) legyen az n+1 (a fenti lineáris keresés éppen így működik), akkor a rekurzív függvény első komponense az összes i-nél nagyobb egészre is hamis lesz. Ezért az utófeltétel az alábbi formában is megfogalmazható: Uf = ( Ef l=f1(m) ) Ez egy rekurzív függvény adott helyen felvett helyettesítési értékének kiszámításának programozási tételére vezethető vissza, amely egy másik megoldását adja a feladatnak. Megjegyezzük, hogy ez a megoldás futási idő szempontjából rosszabb az előzőnél.
144
6.4. Rekurzív függvény kibontása
l, j:= igaz, 1
j:ℕ
i = 1 .. m
i:ℕ
l := hamis l
j n
l := szöveg[i]=tábla[j] j := j+1 Keressünk most egy harmadik megoldást! Vezessünk be egy másik f függvényt, ahol az f(j) azt mutatja meg, hogy a táblázat első j darab elemével a szöveg legfeljebb hány karakterét lehet kódolni. Ha az f(n) felveszi a szöveg hosszát értékül, akkor ez azt jelenti, hogy a szöveg kódolható a táblázattal. Uf = ( Ef l = (f(n)=m) ) Az f függvényt rekurzív módon definiáljuk. A táblázat első nulla darab elemével egyetlen szövegbeli karaktert sem lehet kódolni, azaz f(0)=0. Ha feltesszük, hogy ismerjük azt a számot (f(j–1)), hogy legfeljebb hány karakter kódolható a táblázat első j–1 elemével, akkor az f(j) értéke attól függ, hogy a táblázat j-edik elemével kódolható-e a szöveg soron következő eleme, a szöveg[f(j–1)+1]. Ha igen, akkor f(j) = f(j–1)+1, különben f(j) = f(j–1). f:[0..n] ℕ f(0)= 0 j [1..n]: f(j 1) 1 ha f(j 1) m
f(i)
tábla[ j ] f(j 1)
szöveg[ f(j 1) 1]
különben
A feladatot az i:=f(n) kiszámolásának és egy értékadásnak a szekvenciájával lehet megoldani. Az első részt a rekurzív függvény helyettesítési értéke kiszámításának programozási tételére vezetjük vissza.
145
6. Többszörös visszavezetés
i := 0
i:ℕ
j = 1 .. n
j:ℕ
tábla[j]=szöveg[i+1] i := i+1
SKIP
l := i=m A hatékonyabb negyedik megoldáshoz úgy juthatunk, ha észrevesszük azt, hogy nincs mindig szükség a rekurzív függvény nedik helyen felvett értékének meghatározására, hiszen ha a függvény korábban felveszi az m-et, akkor az már a sikeres kódolásnak a jele. Ennek megfelelően az utófeltétel: Uf = ( Ef l = j [1..n]: f(j)=m ) Ez a feladat visszavezethető a lineáris keresés programozási tételére, amelyben kibontjuk a rekurzív függvényt. l, j := hamis, 1 l
j n
l := f (j)=m j := j+1
j:ℕ
l, j, i := hamis, 1, 0 l j n t[j]=sz[i+1] i := i+1
SKIP
l := i=m j := j+1
146
i,j:ℕ
6.5. Feladatok
6.5. Feladatok 6.1. Volt-e a lóversenyen olyan napunk, amikor úgy nyertünk, hogy a megelőző k napon mindig veszítettünk? (Oldjuk meg kétféleképpen is a feladatot: lineáris keresésben egy lineáris kereséssel, illetve lineáris keresésben egy rekurzív függvénnyel!) 6.2. Egymást követő napokon délben megmértük a levegő hőmérsékletét. Állapítsuk meg, hogy melyik érték fordult elő leggyakrabban! 6.3. A tornaórán névsor szerint sorba állítottuk a diákokat, és megkérdeztük a testmagasságukat. Hányadik diákot előzi meg a legtöbb nála magasabb? 6.4. Állapítsuk meg, hogy egy adott szó (egy karakterlánc) előfordul-e egy adott szövegben (egy másik karakterláncban)! 6.5. Keressük meg a t négyzetes mátrixnak azt az oszlopát, amelyben a főátlóbeli és a feletti elemek összege a legnagyobb! 6.6. Egy határállomáson feljegyezték az átlépő utasok útlevélszámát. Melyik útlevélszámú utas fordult meg leghamarabb másodszor a határon? A feladat kétféleképpen is értelmezhető. Vagy azt az utast keressük, akiről legelőször derül ki, hogy korábban már átlépett a határon, vagy azt, akinek két egymást követő átlépése közt a lehető legkevesebb másik utast találjuk. 6.7. Egymást követő hétvégeken feljegyeztük, hogy a lóversenyen mennyit nyertünk illetve veszítettünk (ez egy előjeles egész szám). Hányadik héten fordult elő először, hogy az összesített nyereségünk (az addig nyert illetve veszített pénzek összege) negatív volt? 6.8. Döntsük el, hogy egy természetes számokat tartalmazó mátrixban! a) van-e olyan sor, amelyben előfordul prím szám (van-e a mátrixban prím szám) b) van-e olyan sor, amelyben minden szám prím c) minden sorban van-e legalább egy prím d) minden sorban minden szám prím (a mátrix minden eleme prím)
147
6. Moduláris programtervezés
6.9. Egy kutya kiállításon n kategóriában m kutya vesz részt. Minden kutya minden kategóriában egy 0 és 10 közötti pontszámot kap. Van-e olyan kategória, ahol a kutyák között holtverseny (azonos pontszám) alakult ki? 6.10. Madarak életének kutatásával foglalkozó szakemberek n különböző településen m különböző madárfaj előfordulását tanulmányozzák. Egy adott időszakban megszámolták, hogy az egyes településen egy madárfajnak hány egyedével találkoztak. Hány településen fordult elő mindegyik madárfaj?
148
III. RÉSZ TÍPUSKÖZPONTÚ PROGRAMTERVEZÉS Az előző rész feladataiban és az azokat megoldó programokban olyan adatokat használtunk, amelyeket a legtöbb magasszintű programozási nyelvben megtalálunk, így eddig az adatok tervezésére és megvalósítására nem kellett sok energiát fordítanunk. Az igazán nehéz feladatok adatai azonban nem ilyenek. Ezek az adatok jóval összetettebbek, a rajtuk elvégezhető műveletek bonyolultabbak. Ezen adatok konkrét tulajdonságaitól az egyszerűbb kezelés érdekében célszerű elvonatkoztatni; helyettük olyan adatokkal dolgozni, amelyeket úgy kapunk, hogy a lényeges tulajdonságokat kiemeljük a lényegtelenek közül, azaz általánosítjuk azokat. Annak következtében, hogy az adatok elvonatkoztatnak a feladattól, lehetővé válik a feladatnak egy jobban átlátható, egyszerűbb modelljét felállítani, amellyel a feladatot könnyebben fogalmazhatjuk és oldhatjuk meg. Ugyanakkor az így kapott kitalált adatok többnyire elvonatkoztatnak azon programozási környezettől is, ahol majd a megoldó programnak működnie kell. A tervezés során ezért ki kell térni annak vizsgálatára, hogyan lehet a kitalált adatainkat a számítógépen megjeleníteni, azaz hogyan lehet az adat értékeit ábrázolni (reprezentálni); továbbá azokat a tevékenységeket (műveleteket), amelyeket az adaton el akarunk végezni, milyen programokkal lehet kiváltani (implementálni). Amikor egy adatot így jellemzünk: azaz megadjuk az értékeinek reprezentációját és az adattal végezhető műveletek implementációját, akkor az adat típusát adjuk meg. Sokszor előfordul, hogy egy adattípus bevezetésénél elsőre nem sikerül olyan reprezentációt és implementációt találni, amely a konkrét programozási környezetben is leírható. Ennek nem az ügyetlenség az oka, hanem az, hogy elsősorban a megoldandó feladatra koncentrálunk, és azt vizsgáljuk, milyen típusműveletek segíthetik a feladat megoldását. Az ilyen nem-megengedett, úgynevezett absztrakt típust ezért aztán egy megfelelő típussal kell majd a konkrét programozási környezetben helyettesítenünk, megvalósítanunk. Egy feladat valamely adattípusának egy-egy típusművelete a feladatot megoldó program része lesz. A típusműveletek bevezetésével
149
7. Típus
valójában részfeladatokat jelölünk ki a megoldandó feladatban. A típusműveletek implementálása az ilyen részfeladatok megoldását, összességében tehát a feladat finomítását jelenti. A feladat megoldása így két részre válik: egyrészt a bevezetett típus műveleteinek segítségével megfogalmazott megoldásra (főprogramra), másrészt a típusműveleteket – mint részfeladatokat megoldó – részprogramokra. A típusoknak az alkalmazása tehát az eredeti feladatot részfeladatokra felbontó speciális, típus központú feladatfinomítási technika. A 7. fejezetben az általunk használt korszerű típusfogalomnak definiálására kerül sor. Megmutatjuk, hogy miképpen lehet megvalósítani egy tervezett típust. Kitérünk a típusok közötti kapcsolatok formáira, és részletesen elemezzük a típus egyik fontos tulajdonságát, a típus szerkezetét. A 8. fejezetben definiáljuk a felsoroló fogalmát, amely többek között az iterált szerkezetű adatok (gyűjtemények) elemeinek bejárására is használható. Definiálunk néhány nevezetes iterált típust, megmutatjuk, hogyan sorolhatók fel az elemei. Ezután a programozási tételeinket általánosítjuk felsorolókra, azaz felsorolóval megadható elemek sorozatára. Ennek következtében lényegesen megnő a programozási tételeinkkel megoldható feladatok köre, amelyet a 9. fejezetben számos példával illusztrálunk majd.
150
7. Típus Már a fenti bevezetőből is látszik, hogy egy adat típusát több nézőpontból lehet szemlélni. Amikor arra vagyunk kíváncsiak, hogy mire használhatjuk az adatot, azaz melyek a lehetséges értékei és ezekkel milyen műveleteket lehet végezni, akkor a típus felületét, kifelé megmutatkozó arcát nézzük; ezt szokták a típus interfészének, a típus specifikációjának nevezni. Amikor viszont az adat típusának egy adott programozási környezetbe való beágyazása a cél, akkor azt vizsgáljuk, hogy milyen elemekkel helyettesíthetőek, ábrázolhatóak a típusértékek, illetve melyek azok a programok, amelyek hatása a típusműveletekével azonos annak ellenére, hogy nem közvetlenül a típus értékeivel, hanem az azokat helyettesítő elemekkel dolgoznak. Ha vesszük a típusértékek helyettesítő elemeit, a helyettesítés módját, azaz a típusértékek reprezentációját (ábrázolását), valamint a típusműveleteket kiváltó programokat, azaz a típusműveletek implementációját, akkor ezeket együtt a típus megvalósításának (realizálásának) nevezzük.16 Egy adat típusa magába foglalja a típus-specifikációt és az ennek megfelelő, az ezt megvalósító típus-realizációt. A továbbiakban egy példán keresztül vezetjük be a korszerű típus fogalmát, majd formális meghatározást adunk rá (7.1. alfejezet). A programtervezés során előbb csak a típus specifikációját adjuk meg, és ezután próbálunk egy ehhez illeszkedő, azt megvalósító típust találni. Annak feltételeit, hogy egy típus mikor valósít meg egy típusspecifikációt, a 7.2. alfejezetben adjuk meg. A típus-specifikációt gyakran nem közvetlenül, hanem közvetve, egy másik, önmagában nem-megengedett, úgynevezett absztrakt típus segítségével írjuk le, ezért külön kitérünk arra (7.3. alfejezet), amikor egy ilyen absztrakt típus által kijelölt típus-specifikációhoz keresünk azt megvalósító típust. A 7.4. alfejezetben többek között a típus szerkezetének fogalmával foglalkozunk.
16
A műveletek implementációja a reprezentációra épül, ezért sokszor az implementáció fogalmába a reprezentációt is beleértik, azaz típusimplementáció alatt a teljes típus-megvalósításra gondolnak.
151
7. Típus
7.1. A típus fogalma Most egy olyan példát fogunk látni, amelyik megoldása során pontosítjuk a típus korábban bevezetett fogalmát, és ezáltal eljutunk a korszerű típus definíciójához. 7.1. Példa. Képzeljük el, hogy olyan űrállomást állítanak Föld körüli pályára, amelynek az a feladata, hogy adott számú észlelt ismeretlen tárgy közül megszámolja, hány tartózkodik az űrállomás közelében (mondjuk 10000 km-nél közelebb hozzá). A feladat egyik adata az űrállomás; a másik az ismeretlen tárgyakat tartalmazó sorozat, pontosabban egy tömb; és természetesen az eredmény. A = (Űrszonda , UFOn , ℕ) Mindenek előtt válasszuk el a feladat adataiban a lényegest a lényegtelentől. Az űr ismeretlen tárgyait egy-egy térbeli pontként szemlélhetjük, hiszen mindössze a térben elfoglalt pozíciójuk lényeges a feladat megoldása szempontjából. Egy űrállomás megannyi tulajdonsága közül annak pozíciója és a védelmi körzete lényeges: az űrállomás így helyettesíthető egy térbeli gömbbel. Ennek a valóságos feladattól való elvonatkoztatásnak az eredménye az alábbi specifikáció. A = (g:Gömb, v:Pontn, s:ℕ) Ef = ( g=g' v=v' ) Uf = ( Ef
n
s
1)
i 1 v[i ] g
A feladat megoldható egy számlálás segítségével. A megoldó programnak az a feltétele, hogy vajon benne van-e a v[i] pont a g gömbben, nem megengedett, mert feltesszük, hogy programozási környezetünkben nem ismert sem a gömbnek, sem a pontnak a típusa. Emeljük ki egy l logikai változó segítségével a számlálás nemmegengedett feltételét egy l:= v[i ] g értékadásba, és tekintsük ezt a továbbiakban egy gömb műveletének. Egy Gömb típusú adat lehetséges értékei gömbök, amelyeken most egyetlen műveletet akarunk végezni: a „benne van-e egy pont egy gömbben” vizsgálatot.
152
7.1. A típus fogalma
Egy gömb azon pontok halmaza (mértani helye), amelyeknek egy kitüntetett ponttól (a középponttól) mért távolsága egy megadott értéknél (a sugárnál) kisebb vagy egyenlő. Formálisan: Gömb={g 2Pont| van olyan c Pont középpont és r ℝ sugár, hogy bármely p g pontra a távolság(c,p) r }.) A típusműveletet, mint részfeladatot az alábbi módon specifikálhatjuk: A = (g:Gömb, p:Pont, l: ) Ef = ( g=g' p=p' ) Uf = ( Ef l = p g ) Ezzel körvonalaztuk, más szóval specifikáltuk a Gömb típust. Általában egy adattípus specifikációjáról (ez a típus-specifikáció) akkor beszélünk, amikor megadjuk az adat által felvehető lehetséges értékek halmazát, a típusérték-halmazt, és az ezen értelmezett típusműveleteknek, mint feladatoknak a specifikációit. Ezeknek a feladatoknak közös vonása, hogy állapotterüknek legalább egyik komponense a megadott típusérték-halmaz. A „benne van-e egy pont egy gömbben” műveletét megoldja az l:=p g értékadás, amely nem-megengedett (végtelen elemű g halmaz esetén ez a részfeladat nem oldható meg véges lépésben). Ezért egy megengedett programmal kell helyettesíteni. Ehhez először a gömböt kell másként, nem pontok halmazaként ábrázolni, hanem például a középpontjával és a sugarával. Természetesen egy térbeli pont és egy valós szám önmagában nem egy gömb, de helyettesíti, reprezentálja a gömböt. Ezt a reprezentációt Pont matematikai értelemben egy :Pont ℝ 2 függvény írja le, amely egy c középponthoz és egy r sugárhoz az annak megfelelő gömböt Pont rendeli. Formálisan: (c,r) = {q Pont | távolság(c,q) r } 2 . Ez a függvény negatív sugárra is értelmes, de célszerű volna a negatív sugarú gömböket kizárni a vizsgálatainkból. Ezt megtehetjük úgy, hogy megszorítást adunk a gömböket helyettesítő (c,r) Pont R párokra: r 0. Ezt a megszorítást a típus invariáns állításának hívják. Ez formálisan egy I:Pont ℝ logikai függvény, ahol I(c,r) = r 0. A típus invariánsa leszűkíti a reprezentációs függvény értelmezési tartományát. A gömbök itt bemutatott reprezentációja azért előnyös, mert a „benne van-e egy pont egy gömbben” típusművelet így már
153
7. Típus
visszavezethető két pont távolságának kiszámolására, hiszen az l:=p g értékadás kiváltható az l:=távolság(c,p) r értékadással. Hangsúlyozzuk, hogy a két értékadás nem azonos hatású, hiszen – hogy mást ne mondjunk – eltér az állapotterük: az elsőé a (Gömb, Pont, ) a másodiké a (Pont, ℝ, Pont, ). A második értékadás állapotterében nem szerepel a gömb, hiszen ott azt egy pont és egy valós szám helyettesíti. Mégis a második értékadás abban az értelemben megoldja az első értékadás által megfogalmazott feladatot, amilyen értelemben egy gömb reprezentálható a középpontjával és a sugarával. Más szavakkal úgy mondjuk, hogy az l:=távolság(c,p) r értékadás a reprezentáció értelmében megoldja, azaz implementálja az l:=p g műveletet. Az l:=távolság(c,p) r értékadás közvetlenül az alábbi feladatot oldja meg A = (c:Pont, r: , p:Pont, l: ) Ef = ( c=c' r=r' p=p' ) Uf = (Ef l = távolság(c,p) r) de közvetve, a reprezentáció értelmében megoldja az eredeti feladatot is: A = (g:Gömb, p:Pont, l: ) Ef = ( g=g' p=p' ) Uf = ( Ef l = p g ) Ezzel el is érkeztünk a korszerű típusfogalom definíciójához. Egy adat típusán azt értjük, amikor megadjuk, hogy az adat által felvehető lehetséges értékeket hogyan ábrázoljuk (reprezentáljuk), azaz milyen elemmel (értékkel vagy értékcsoporttal) helyettesítjük, továbbá leírjuk a típus műveleteit implementáló programokat, amelyek közös vonása, hogy állapotterükben komponensként szerepel a reprezentáló elemek halmaza. A típusértékeket helyettesítő (ábrázoló) reprezentáns elemeket gyakran egy bővebb halmazzal és egy azon értelmezett logikai állítással, a típus invariánsával jelöljük ki: ekkor az állítást kielégítő elemek a reprezentáns elemek. A reprezentáció több, mint a reprezentáns elemek halmaza: a reprezentáns elemek és a típus-értékek kapcsolatát is leírja. Ez tehát egy leképezés, amely gyakran egy típusértékhez több helyettesítő elemet is megad, sőt ritkán előfordul, hogy különböző típusértéket ugyanazon reprezentáns elemmel helyettesíti. A Gömb típusleírása sokkal árnyaltabban és részletesebben mutatja be a gömböket, mint a Gömb korábban megadott típus-
154
7.1. A típus fogalma
specifikációja. Igaz, hogy a típusleírásban explicit módon csak a gömbök ábrázolásáról (reprezentációjáról) és egy erre az ábrázolásra támaszkodó művelet programjáról (l:=távolság(c,p) r) esik szó, de ez implicit módon definiálja a gömbök halmazát (a típusérték-halmazt) és az azokon értelmezett („benne van-e egy pont a gömbben”) típusműveletet is. Általában is igaz, hogy ha ismerjük a típus-invariánst és a reprezentációs leképezést, akkor a típus-invariáns által kijelölt elemekhez ez a leképezés hozzárendeli a típusértékeket, így megkapjuk a típusérték-halmazt. A típusművelet programjából pedig (mint minden programból) kiolvasható az a feladat, amit a program megold, és ha ebben a feladatban a reprezentáns elemek helyére az azok által képviselt típusértékeket képzeljük, akkor megkapjuk a típusművelet feladatát. Így előáll a típus-specifikáció. Ezt a jelenséget úgy nevezzük, hogy a típus kijelöli önmaga specifikációját. Sajnos a Gömb típus műveletének programja nem megengedett, hiszen két térbeli pont távolságának (távolság(c,p)) kiszámítása sem az. Ezért ki kell dolgoznunk a Pont típust, amelynek művelete a d:=távolság(c,p) értékadás lesz. A Pont típus értékei a térbeli pontok, amelyek például egy háromdimenziós derékszögű koordináta-rendszerben (ennek az origója tetszőleges rögzített pont lehet) képzelhetőek el, és ilyenkor azok a koordinátáikkal helyettesíthetők. ( :ℝ×ℝ×ℝ Pont) Hangsúlyozzuk, hogy három valós szám nem azonos egy térbeli ponttal, de egy rögzített koordináta-rendszerben egyértelműen reprezentálják azt. Mivel bármilyen koordináta-hármas egy térbeli pontot helyettesít, ezért a típus invariánsával (I:ℝ×ℝ×ℝ ) nem kell megszorítást tennünk, az invariáns tehát az azonosan igaz állítás. Ebben a reprezentációban két pont távolságát az euklideszi képlet segítségével számolhatjuk ki. A képlet alapján felírt értékadás megoldja, implementálja a két pont távolságát meghatározó részfeladatot annak ellenére, hogy a művelet állapotterében pontok, az azt megoldó program állapotterében pedig valós számok szerepelnek. Mivel a képlet valós számokkal és a valós számok műveleteivel dolgozik, értéke már egy megengedett programmal kiszámolható.
155
7. Típus
A = (p.x:ℝ, p.y:ℝ, p.z:ℝ, q.x:ℝ, q.y:ℝ, q.z:ℝ, d:ℝ) d:
( p.x q.x) 2
( p. y q. y ) 2
( p.z q.z ) 2
Felhasználva a Gömb és a Pont típus definícióját visszatérhetünk az eredeti feladat megoldásának elején említett számláláshoz, és az abban szereplő l:=v[i] g értékadást az l:= (v[i ]. x
g.c.x) 2
(v[i]. y
g.c. y ) 2
(v[i ]. z
g.c.z ) 2
g.r
programmal helyettesíthetjük. Ebben a v[i].x a v[i] pont x koordinátáját, v[i].y az y koordinátáját és v[i].z a z koordinátáját jelöli, továbbá a g.c a g gömb középpontja, g.r pedig a sugara. Az eredményül kapott program állapottere nem egyezik meg a feladat állapotterével: az eredeti állapottér gömbje és pontjai helyett a megoldó program valós számokkal dolgozik. Mégis, ez a program nyilvánvalóan megoldja a feladatot, még ha ezt nem is a megoldás korábban megadott definíciójának értelmében teszi. A feladat megoldásában tetten érhető az adatabsztrakciós programozási szemléletmód, amely az adattípust állítja középpontba. Első lépésként elvonatkoztattuk a feladatot az űrszondák és tárgyak világából a térbeli gömbök és pontok világába. Itt megtaláltuk a feladat megoldásának „kulcsát”, azt a részfeladatot, amelyik el tudja dönteni, hogy egy pont benne van-e egy gömbben. Ezt felhasználva már elkészíthettük az eredeti feladatot megoldó számlálást. Egyetlen, bár nem elhanyagolható probléma maradt csak hátra: a megoldás kulcsának nevezett részfeladatot, mint a Gömb típus egy műveletét, kellett megoldani. Ehhez azonban nem az absztrakt gömböket tartalmazó eredeti állapottéren kerestünk megoldó programot, hanem ott, ahol a gömböt a középpontjával és sugarával helyettesítettük. Más szavakkal, a Gömb típust meg kellett valósítani: először reprezentáltuk a gömböket, majd a reprezentáns elemek segítségével újrafogalmaztuk a részfeladatot. Az újrafogalmazott részfeladat megoldásához újabb adatabsztrakciót alkalmaztunk: bevezettük a Pont típust, amelyhez a két pont távolságának részfeladatát rendeltük típusműveletként. Végeredményben egy ismert típus, a valós számok típusának segítségével reprezentáltuk a pontokat és a gömböket, a műveleteket megoldó programok pedig ugyancsak a valós számok megengedett környezetében működnek. Az adatabsztrakció alkalmazása intuíciót és sok gyakorlást igényel. Hogy mást ne említsünk, például nehezen magyarázható meg
156
7.1. A típus fogalma
az, honnan láttuk az előző példában már a megoldás elején, hogy a "benne van-e a pont a gömbben" műveletet a Gömb típushoz és nem a Pont típushoz kell rendelni. Ez a döntés alapvetően meghatározta a megoldás irányát: először a Gömb típussal és utána a Pont típussal kellett foglalkozni; de kijelölte azt is, hogy a Gömb típus reprezentálásához felhasználjuk a Pont típust. 7.2. Típus-specifikációt megvalósító típus Ha van egy típus-specifikációnk és külön egy típusunk, akkor joggal vetődik fel az a kérdés, vajon a típus megfelel-e az adott típusspecifikációnak: megvalósítja-e a típus-specifikációt. Ezt vizsgálhatnánk úgy is, hogy összehasonlítjuk a vizsgált típusspecifikációt a típus által kijelölt típus-specifikációval, de közvetlenül úgy is, hogy megvizsgáljuk vajon a típus reprezentáns elemei megfelelően helyettesíthetik-e a típus-specifikáció típusértékeit, és hogy a típus programjai rendre megoldják-e a típus-specifikáció feladatait (természetesen úgy értve, hogy a programok a típusértékek helyett azok reprezentáns elemeivel számolnak.) Annak, hogy a típus megvalósítson egy típus-specifikációt két kritériuma van. Egyrészt minden típusérték helyettesíthető kell legyen reprezentáns elemmel, és minden reprezentáns elemhez tartoznia kell típusértéknek. Másrészt a típus-specifikáció minden feladatát a típus valamelyik programja a reprezentáció (alapján) értelmében megoldja (implementálja). Egy típus-specifikációhoz több azt megvalósító típus is tartozhat. Például a Gömb típus-specifikációját nemcsak az előző alfejezetben bemutatott típus valósítja meg, hanem az is, ahol a gömböket közvetlenül négy valós számmal reprezentáljuk; az első három a középpont koordinátáit, az utolsó a sugarát adná meg. Ilyenkor nincs szükség a Pont típusra, az l:=p g feladatot pedig közvetlenül egy valós számokkal felírt programmal kell implementálni. A Pont típusát is lehetne másként, például polár koordinátákkal reprezentálni, de ekkor természetesen más programot igényel két pont távolságának kiszámítása. Nézzünk most egy olyan feladatot, amelynek megoldásához úgy vezetünk be egy új típust, hogy először a specifikációját adjuk meg, majd ehhez keresünk azt megvalósító típust.
157
7. Típus
7.2. Példa. Adott egy n hosszúságú 1 és 100 közé eső egész számokat tartalmazó tömb. Számoljuk meg, hogy ez a tömb hány különböző számot tartalmaz! A feladatot számlálásra vezetjük vissza. A tömb egy eleménél akkor kell számolni, ha az még nem fordult elő korábban. Ezt egy lineáris kereséssel is eldönthetnénk, de ez nem lenne túl jó hatékonyságú megoldás, hiszen a tömböt újra és újra végig kellene vizsgálni. Ráadásul ilyenkor azt sem használnánk ki, hogy a tömb elemei 1 és 100 közé esnek. Számolhatnánk az 1 és 100 közé eső egész számokra, hogy melyik szerepel a tömbben, de hatékonyság szempontjából ez sem lenne lényegesen jobb az előzőnél (hosszú tömbök esetén egy kicsit jobb) hiszen a számlálás feltételének eldöntése itt is lineáris keresést igényel. Harmadik megoldás az, ha először előállítunk egy a tömb elemeiből képzett halmazt, és megnézzük, hogy ez a halmaz hány elemű. (Ebben a megoldásban a számlálás programozási tétele meg sem jelenik.) A = (a:ℕn, s:ℕ) Ef = ( a=a’ ) n
Uf = ( Ef
s
{a[i]}
)
i 1
A specifikáció szerint a megoldáshoz egy halmazra, pontosabban egy halmaz típusú adatra van szükségünk. Ebbe először sorban egymás után bele kell rakni a tömb elemeit, majd le kell kérdezni az elemeinek számát. A megoldó program ezért egy szekvencia lesz, amelynek első fele (az uniózás) az összegzés programozási tételére vezethető vissza, a második fele pedig a halmaz típus „elemszám” műveletére épül: m..n ~ 1..n s ~ h f(i) ~ {a[i]} +,0 ~ , h := i = 1 .. n h := h {a[i]} s := h
158
7.2. Típus-specifikációt megvalósító típus
A halmaztípus egy típusértéke egy olyan halmaz, amely elemei 1 és 100 közötti egész számok. Megengedett művelet egy halmazt üressé tenni, egy elemet hozzá uniózni, és az elemeinek a számát lekérdezni. A halmaztípus típus-specifikációját formálisan az alábbi (értékhalmaz, műveletek) formában írhatjuk le: {1..100}
(2 ,{h:= , h:=h {e}, s:= h }). Keressünk egy olyan konkrét típust, amely megvalósítja ezt a típus-specifikációt. Reprezentáljunk például egy 1 és 100 közé eső számokból álló halmazt 100 logikai értéket tartalmazó tömbbel.
h=
6 2 1
8 5
2.
3.
4.
5.
6.
7.
8.
9.
100.
v= hamis igaz hamis hamis igaz igaz hamis igaz hamis . . . hamis db=4 7.1. Ábra Egy halmaz és azt reprezentáló logikai tömb
A logikai tömbben az e-edik elem akkor és csak akkor legyen igaz értékű, ha a tömb által reprezentált halmaz tartalmazza az e-t. Vegyük még hozzá ehhez a tömbhöz külön azt az értéket, amely mindig azt mutatja, hogy hány igaz érték van a tömbben (ez éppen a reprezentált halmaz elemszáma). Ez a kapcsolat a logikai tömb és e darabszám között a típus-invariáns. Itt a reprezentációs függvény egy (logikai tömb, darabszám) párhoz rendeli azon egész számoknak a halmazát, amely a logikai tömb azon indexeit tartalmazza, ahol a tömbben igaz értéket tárolunk. Nyilvánvaló, hogy minden (tömb, darabszám) pár kölcsönösen egyértelműen reprezentál egy halmazt. Térjünk most rá a típusműveletek implementálására. Az egyes műveleteket először átfogalmazzuk úgy, hogy azok ne közvetlenül a halmazra, hanem a halmazt reprezentáló tömb-darabszám párra legyenek megfogalmazva. Például a h:= feladatnak az a feladat
159
7. Típus
feleltethető meg, hogy töltsük fel a logikai tömböt hamis értékekkel és nullázzuk le a darabszámot. Formálisan felírva az eredeti és a reprezentációra átfogalmazott feladatot: {1..100}
A =(h:2 Ef = ( h=h' )
)
100
A = (v: , db:ℕ) Ef = ( (v,db)= (v',db')) = = (v=v' db=db') Uf = ( h= ) Uf = ( (v,db)= ) = = ( i [1..100]:v[i]=hamis db=0 ) Az átfogalmazott feladat elő- és utófeltételét kétféleképpen is felírtuk. Az első alak mechanikusan áll elő az eredeti specifikációból: a halmazt a tömb és a darabszám helyettesíti. A második alaknál már figyelembe vettük a reprezentáció sajátosságait. Könnyű belátni, hogy az átalakított feladatot az alábbi program megoldja: h := db := 0 i = 1 .. 100 v[i] := hamis Ez a program bár közvetlenül nem halmazzal dolgozik, mégis abban az értelemben megoldja a h:= feladatot, amilyen értelemben egy halmazt egy logikai tömb és egy darabszám reprezentál. Ezt a fenti két specifikáció közötti kapcsolat szavatolja. A másik két művelet esetében nem leszünk ennyire akkurátusak. (Meglepő módon ezek a műveletek jóval egyszerűbbek a fentinél, megvalósításuk nem igényel ciklust.) Könnyű belátni, hogy a h:=h {e} értékadást a v[e]:=igaz értékadás váltja ki, hiszen amikor egy halmazba betesszük az e számot, akkor a halmazt reprezentáló tömb e-edik elemét igaz-ra állítjuk. A típus-invariáns fenntartása érdekében azonban azt is meg kell nézni, hogy az e elem a halmaz szempontjából új-e, azaz v[e] hamis volt-e. Ha igen, akkor a reprezentációt képező darabszámot is meg kell növelni. A megoldás egy elágazás.
160
7.2. Típus-specifikációt megvalósító típus
típusértékek típusspecifikáció
típusműveletek betesz
{1..100}
h:=h {e}
2
1 és 100 közé eső egész számokat tartalmazó halmazok
legyen üres h := elemszáma s:= h
:
100
{1..100}
ℕ 2 100
(v,db) = v[i] i 1
100
1)
I(v,db) =( db=
v: 100, db:ℕ, e: ℕ, l: , s:ℕ
i 1 v[i ]
típus megvalósítás
betesz 100
ℕ
logikai tömb és egy szám
v[e]=hamis v[e]:=igaz db:=db+1
SKIP
legyen üres db := 0 i [1..100]: v[i]:=hamis elemszáma s:=db 7.2. Ábra Halmaz típusa
161
7. Típus
h:=h {e} v[e]=hamis v[e] := igaz db := db+1
SKIP
Ha a db-t nem vettük volna hozzá a reprezentációhoz, akkor az s:= h értékadást egy számlálással oldhatnánk meg. Így azonban elég helyette az s:=db értékadás. s:= h s := db A 7.2 ábrán összefoglaltuk a halmaz típus specifikációjával és megvalósításával kapcsolatos megjegyzéseinket. A típusspecifikációban a műveletek feladatait értékadások formájában adtuk meg, a műveletek állapottere pedig kitalálható az értékadás változóinak típusából. A típus leírásban rövidített formában szerepel a „legyen üres” művelet programja. 7.3. Absztrakt típus Adatabsztrakción azt a programtervezési technikát érjük, amikor egy feladat megoldása során olyan adatot vezetünk be, amely több szempontból is elvonatkoztat a valóságtól. Egyrészt elvonatkoztathat a feladattól, ha annak eredeti megfogalmazásában közvetlenül nem szerepel; csak a megoldás érdekében jelenik meg. Másrészt elvonatkoztathat a konkrét programozási környezettől, ahol majd az adat típusát le kell írni. Ezt a típust nevezzük absztrakt típusnak. Absztrakt típus lehet egy típus-specifikációt, de egy olyan típus is,amelynek a reprezentációja és implementációja nem hatékony (ezért nem fogjuk ebben a formában alkalmazni) vagy nem-megengedett (lásd a megengedettség 3. fejezetben bevezetett fogalmát) vagy hiányos (mert például nem ismert a műveleteinek programja, csak azok hatása, vagy
162
7.3. Absztrakt típus
még az sem).17. Egy absztrakt típustól mindössze azt várjuk, hogy világosan megmutassa az általa jellemzett adat lehetséges értékeit és azokon végezhető műveleteket, tehát az egyetlen szerepe az, hogy kijelöljön egy típus-specifikációt Ha ennek definiálásához nincs szükség reprezentációra, akkor a típus-specifikációt közvetlen18 módon adjuk meg, de ez a programozási gyakorlatban viszonylag ritka. Sokszor ugyanis nem tudjuk önmagában definiálni a típusérték-halmazt (úgy mint a gömbök vagy az 1 és 100 közötti elemekből képzett halmazok esetében tettük), csak annak egy reprezentációjával (példaként gondoljunk a pont fogalmára, amelyet mindannyian jól értünk, de definiálni nem lehet, csak valamilyen koordináta rendszer segítségével megadni). Általában a típusműveletek viselkedését is szemléletesebben lehet elmagyarázni, ha azok nem elvonatkoztatott típusértékekkel, hanem „megfogható” reprezentáns elemekkel dolgoznak. A „benne van-e egy pont a gömbben” művelet sokak számára érthetőbb, ha nem halmazelméleti alapon (benne van-e a pont a gömböt alkotó pontok halmazában), hanem geometriai szemlélettel (a pontnak a gömb középpontjától mért távolsága kisebb, mint a gömb sugara) magyarázzuk el. Ezért gyakoribb (lásd később 7.3, 7.4. példákat) az, amikor egy típus-specifikációt közvetett módon írunk fel: adunk egy típust, ami kijelöli a típus-specifikációt. Ha ez a típus csak a típus-specifikáció kijelölésére szolgál, akkor tényleg nem fontos, hogy megengedett legyen, sőt az sem baj, ha hiányos. Az ilyen absztrakt típus csak abból a szempontból érdekes, hogy segíti-e megértetni a kijelölt típus-specifikációt, és nem számít, milyen ügyesen tudja reprezentálni a típusértékeket vagy mennyire hatékonyan implementálni az azokon értelmezett műveleteket.
17
Ez magyarázza, hogy miért használják sokszor az absztrakt típus elnevezést magára a típus-specifikációra. Könyvünkben azonban az absztrakt típus tágabb fogalom, mint egy típus-specifikáció, hiszen tartalmazhat olyan elemeket is (például reprezentáció vagy típusművelet hatása, esetleg a részletes programja), amelyek nem részei a típus-specifikációnak. 18 Egy típus-specifikációt nemcsak a könyvünkben alkalmazott, úgynevezett elő- utófeltételes módszerrel adhatunk meg közvetlen módon. Más leírások is léteznek, ilyen például az úgynevezett algebrai specifikáció is. Ezek tárgyalásával itt nem foglalkozunk.
163
7. Típus
Ha egy feladat megoldásánál absztrakt típust alkalmazunk, akkor később ezt olyan megengedett típussal (konkrét típussal) kell kiváltanunk, amely megvalósítja az absztrakt típus által kijelölt specifikációt, röviden fogalmazva megvalósítja az absztrakt típust. Ha az absztrakt típus nemcsak egy típus-specifikációból áll, hanem rendelkezik reprezentációval és a típusműveleteket implementáló programokkal, vagy legalább azok hatásainak leírásával, akkor az absztrakt típust kiváltó konkrét típust úgy is elkészíthetjük, hogy közvetlenül az absztrakt típus elemeit (a reprezentációt és a programokat) valósítjuk meg. Ha a reprezentáció nem-megengedett vagy csak nem hatékony, akkor a reprezentáns elemeket próbáljuk meg helyettesíteni más konkrét elemekkel, azaz a reprezentációt reprezentáljuk. Ha a típusműveleteket megvalósító programok nemmegengedettek, akkor ezeket (és nem az eredeti típusműveleteket) az új reprezentáció alapján implementáljuk. Könnyű belátni, hogy az így kapott konkrét típus megvalósítja az absztrakt típus által leírt típusspecifikációt is. Most mutatunk néhány, az itt vázolt folyamatot illusztráló úgynevezett adatabsztrakciós feladat-megoldást. A feladatok megoldása során olyan absztrakt típusokat vezetünk be, amelyek műveletei támogatják a feladat megoldását, annak ellenére, hogy a feladat eredeti megfogalmazásában semmi sem utal ezen adattípusok jelenlétére. Ezek a feladat szempontjából elvonatkoztatott típusok, ugyanakkor a programozási környezet számára is absztraktak. Emiatt gondoskodni kell a megvalósításukról is, azaz az absztrakt típusokat az őket megvalósító konkrét típusokkal kell helyettesíteni. 7.3. Példa. Adott egy n hosszúságú legfeljebb 100 különböző egész számot tartalmazó tömb. Melyik a tömbnek a leggyakrabban előforduló eleme! A feladat megoldható egy maximum kiválasztásba ágyazott számlálással, ahol minden tömbelemre megszámoljuk, hogy hányszor fordult addig elő, és ezen előfordulás számok maximumát keressük. A 7.2. példa nyomán azonban itt is bevezethetünk egy halmazszerű objektumot azzal a kiegészítéssel, hogy amikor egy elemet ismételten beleteszünk, akkor jegyezzük fel ennek az elemnek a halmazban való előfordulási számát (multiplicitását). Ezt a multiplicitásos halmazt zsáknak szokták nevezni. Miután betettük ebbe a zsákba a tömb összes
164
7.3. Absztrakt típus
elemét, akkor már csak arra van szükség, hogy megnevezzük a legnagyobb előfordulás számú elemét. Specifikáljuk a feladatot! A = (t:{1..100 n, b:Zsák, e:{1..100}) Ef = ( t = t’ n≥1 ) n
Uf = ( Ef
b
{t[i]}
e = MAX(b) )
i 1
A specifikációban használt unió és maximum számítás a zsák speciális műveletei lesznek. A feladat egy összegzésre vezethető vissza, amit a zsák maximális gyakoriságú elemének kiválasztása követ: m..n ~ 1..n s ~ b f(i) ~ {t[i]} +,0 ~ ,
b := i = 1 .. n b := b {t[i]} e := MAX(b) A zsáktípust absztrakt típussal definiáljuk. Egy számokat tartalmazó zsák ugyanis legegyszerűbben úgy fogható fel, mint számpárok halmaza, ahol a számpár első komponense a zsákba betett elemet, második komponense ennek az elemnek az előfordulási számát tartalmazza. Reprezentáljuk tehát kölcsönösen egyértelmű módon egy {1..100}×ℕ zsákot a 2 hatványhalmaz egy (számpárokat tartalmazó) halmazával. Válasszuk a típus invariánsának azt az állítást, amely csak azokra a számpárokat tartalmazó halmazokra teljesül, amelyekben bármelyik két számpár első komponense különböző, azaz ugyanazon elem 1 és 100 közötti szám nem szerepelhet kétszer, akár két különböző előfordulás számmal a halmazban. A zsáktípus műveletei: a „legyen üres egy zsák” (b:= ), a „tegyünk bele egy számot egy zsákba” (b:=b {e}), „válasszuk ki a zsák leggyakoribb elemét”
165
7. Típus
(e:=MAX(b)), ahol b egy zsák típusú, az e egy 1 és 100 közötti szám típusú értéket jelöl. A műveletek közül az „ ” művelet nem a szokásos halmazművelet, ezért ennek külön is felírjuk a specifikációját. {1..100}
×ℕ A= (b:2 , e:ℕ) Ef = ( b=b' e=e’ ) Uf = ( e=e’ (e,db) b’ (b=b’–{(e,db)} {(e,db+1)}) (e,db) b’ (b=b’ {(e,1)}) ) Ezzel a zsák absztrakt típusát megadtuk, kijelöltünk a zsák típusspecifikációját. Valósítsuk most meg az absztrakt zsáktípust! Egy zsákot helyettesítő számpárok halmaza leírható egy 100 elemű természetes számokat tartalmazó tömbbel (v), ahol a v[e] azt mutatja, hogy az e szám hányszor van benne a zsákban. Ha v[e] nulla, akkor az e szám még nincs benne. Vegyük észre, hogy nem közvetlenül a zsákot próbáltuk meg másképp reprezentálni, hanem a zsákot az absztrakt típus által megadott korábbi reprezentációját. Ezt a kétlépcsős megvalósítást alkalmazzuk a műveletek {1..100}×ℕ implementálásánál is. A 2 -beli halmazokra megfogalmazott műveleteket próbáljuk megvalósítani, és ezzel közvetve az eredeti, a zsákokon megfogalmazott műveleteket is implementáljuk. A zsákot reprezentáló halmaz akkor üres, ha minden 1 és 100 közötti szám egyszer sem (nullaszor) fordul benne elő, azaz a b:= értékadást a e [1..100]: v[e]:=0 értékadás-sorozattal helyettesíthetjük. A b:=b {e} művelet implementálása még egyszerűbb: nem kell mást tenni, mint az e elem előfordulás számát megnövelni, azaz v[e]:=v[e]+1. A legnagyobb előfordulás számú elemet a reprezentáns v tömbre alkalmazott maximum kiválasztás határozza meg. Megjegyezzük, hogy ez még akkor is visszaad egy elemet, ha a zsák üres, mert ilyenkor minden elem nulla előfordulás számmal szerepel a tömbben. A megoldásban jól elkülönül a típus megvalósítás három szintje (7.3. ábra). Legfelül van a zsák fogalma, amit az alatta levő absztrakt típus jelöl ki. Ezért nincs definiálva az absztrakt típus reprezentációs függvénye, csak a típus invariánsa. A típusműveletek állapottere kitalálható a műveletet leíró értékadás változóinak típusából. Az absztrakt típus alatt találjuk az azt megvalósító konkrét típust. Ennek invariáns állítása mindig igaz, ezért ezt külön nem tüntettük fel. A
166
7.3. Absztrakt típus
műveleteket implementáló programok programok változóinak típusából.
típus specifikáció
állapottere
kitalálható
típusértékek
típusműveletek
legfeljebb 100 különböző egész számot tartalmazó
legyen üres
Zsák
betesz
a
maximum {1..100}×ℕ
:2
Zsák
I(b)= e1, e2 b: e1.1 e2.1 absztrakt típus
{1..100}
×ℕ 2 -beli számpárok halmaza
{1..100}×ℕ
b: 2 e:{1..100}
,
b:= b:= b {e} e:=MAX(b)
{1..100}×ℕ
: ℕ100 2 100
ρ (v)
{(i, v[i])}
v: ℕ100, e:{1..100}
i 1
konkrét típus
ℕ100 előfordulás számok tömbje
e [1..100]: v[e]:=0 v[e]:=v[e]+1 100
max, e : max v[i] i 1
7.3. Ábra Zsák absztrakt típusa és annak megvalósítása
7.4. Példa. Adott egy n hosszúságú karaktereket tartalmazó tömbben egy olyan szöveg, amelyben a szavakat vessző választja el egymástól. Készítsük el azt a másik tömböt, amelybe a szövegben szereplő szavakat az eredeti sorrendjükben, de megfordítva helyezzük
167
7. Típus
el! Ügyeljünk arra is, hogy a szavakat továbbra is vesszők válasszák el. (Feltehetjük, hogy a szavak 30 karakternél nem hosszabbak.) Ennek a feladatnak a megoldásához egy vermet használunk. A v verem egy olyan gyűjtemény, amelyből a korábban beletett elemeket a betevésük sorrendjével ellentétes sorrendben lehet kiolvasni. Egy veremre öt műveletet vezetünk be. Egy elemnek a verembe rakása (v:=push(v,e)), a legutoljára betett elem kiolvasása (top(v)), a legutoljára betett elem elhagyása (v:=pop(v)), annak lekérdezése, hogy a verem üres-e (empty(v)) illetve a verem kiürítése (v:= ). Tegyük be sorban egymás után a verembe a feldolgozandó szöveg karaktereit, de valahányszor a szövegben egy vesszőhöz érünk, akkor ürítsük ki a vermet, és írjuk át a veremből kivett karaktereket az eredmény-tömb soron következő helyeire, amelyek után írjunk még oda egy vesszőt is. A feldolgozás legvégén tegyük az új szöveg végére a veremben maradt karaktereket. A fenti leírásban körvonalazódik ugyan a veremtípus specifikációja, de ennek pontos megadása közvetett módon szemléletesebb. (7.4.ábra) Ábrázoljuk a legfeljebb 30 karaktert tartalmazó vermeket legfeljebb 30 elemű, karaktereket tartalmazó sorozatokkal. Ha s a vermet reprezentáló s1 , … , s s sorozat, akkor ezen a verem műveletei könnyen definiálhatóak. Ezt az absztrakt típust kell ezek után megvalósítanunk. Reprezentáljuk a vermet leíró legfeljebb 30 hosszú karaktersorozatokat egy 30 elemű, karaktereket tartalmazó tömbbel, valamint egy természetes számmal, amely azt mutatja, hogy a tömb első hány eleme alkotja a vermet leíró sorozatot. Ha ez a szám nulla, akkor a verem üres. Ha ezt a számot nullára állítjuk, akkor ezzel a vermet kiürítettük. A verembe egy új elemet úgy teszünk be, hogy megnöveljük ezt a számlálót, és a tömbben a számláló által mutatott helyre beírjuk az új elemet. A veremből való kivétel a tömb-számláló által mutatott elemének kiolvasását, majd a számláló csökkentését jelenti. A reprezentációs függvény nem kölcsönösen egyértelmű, hiszen a reprezentáció szempontjából közömbös, hogy a t tömbben milyen karakterek találhatók az m-nél nagyobb indexű helyeken.
168
7.3. Absztrakt típus
típus értékek típus specifikáció
típus műveletek
legfeljebb 30 karaktert tartalmazó verem
betesz: v := push(v,e) kivesz: v := pop(v) olvas: e := top(v) üres-e: l := empty(v) legyen üres: v :=
I(s)= s absztrakt típus
*
s:
100
legfeljebb 30 hosszú * -beli sorozat
, e: , l:
s := s, e s := s1, … ,s e := s
s -1
s
l := s= s := :
30
ℕ
*
m
(t,m) =
t[i ]
t:
30
, m: ℕ, e: , l:
i 1
konkrét típus
ℕ tömb-szám pár 30
t[m+1], m :=e, m+1 m := m-1 e := t[m] l := (m=0) m := 0
7.4. Ábra Verem absztrakt típusa és annak megvalósítása
Az eredeti feladatot egy rekurzív függvény segítségével specifikáljuk.
169
7. Típus
A = (a: n, b: n) Ef = ( a=a’ ) Uf = ( Ef b=f1(n+1) ) n Az f:[0..n+1] ×ℕ×Verem egy három-értékű függvény. Tetszőleges i [0..n] index esetén az f(i)-nek három komponense van: f1(i) az n hosszúságú eredmény tömb; f2(i) ennek egy olyan indexe, amely azt mutatja, hogy az eredmény tömb első hány elemét állítottuk már elő eddig; és végül az f3(i), amely a feldolgozáshoz használt verem. A rekurzív definícióban sokszor kell az f(i-1) komponenseit használnunk, amelyekre az átláthatóság végett rendre b, k, és s névvel hivatkozunk majd. (Célszerű lesz később ugyanezen neveket választani segédváltozóknak a rekurzív függvény kiszámításának programozási tételében.) Az egyszerűbb jelülés érdekében az <s> szimbólummal hivatkozunk a veremben levő karakterek sorozatára (ennek első eleme a verem teteje, és így tovább), az s szimbólum pedig a verembeli elemek számára utal majd. f(0)=( b, 0, ) i [1..n]:
f(i)
(b, k , push(s , a[i] )) (b[1..k ]
s
', ', k
s
ha a[i] ' , ' 1,
) ha a[i] ' , '
A rekurzív képletnek az első sora azt írja le, hogy egy karaktert, ami nem vessző, a verembe kell betenni, a tömb és az index komponensek ilyenkor nem változnak; a második sora pedig azt, hogy vessző esetén a verem tartalmát a b tömb k-adik indexe utáni helyeire kell átmásolni majd utána írni még a vesszőt is. Ilyenkor új értéket kap a k index is, a verem pedig kiürül. Mivel a legutolsó szót nem zárja le vessző, ezért az a tömb végére (n-hez) érve a legutolsó szó még a veremben marad. Ezért definiáljuk a rekurzív függvényt az n+1-edik helyen, amely a verembe (f3(n), amit továbbra s jelöl) levő elemeket átmásolja az eredménytömbbe (b-vel jelölt f3(n), amelynek első k, azaz f2(n) eleme után kell az újabb karaktereket írni).
f(n 1)
b[1..k ]
s ,k
s,
A rekurzív függvény kiszámításának programozási tételét felhasználva kapjuk a megoldó programot. Hatékonysági okból a függvény n+1-dik helyen történő kiszámolását kiemeljük a ciklusból,
170
7.3. Absztrakt típus
és azt a ciklus után külön határozzuk meg. A verem tartalmának a b tömb k-adik indexe utáni helyeire történő átmásolása lényegében egy összegzés, amelynek precíz visszavezetését csak a következő fejezetben bemutatott felsorolókkal lehetne elvégezni. Annyit azonban könnyű látni, hogy ennek a részfeladatnak a megoldása egy ciklust igényel, amely a veremből veszi ki az elemeket addig, amíg a verem ki nem ürül, és teszi át a b tömb soron következő helyére. Nézzük meg ezek után a programot. k, s := 0, i = 1 .. n a[i]=’,’ empty(s)
s:= push(s, a[i])
b[k+1], k:=top(s), k+1 s:=pop(s) b[k+1], k:= ’,’ , k+1 empty(s) b[k+1], k:=top(s), k+1 s:=pop(s)
7.4. Típusok közötti kapcsolatok Egy bonyolultabb feladat megoldásában sok, egymással összefüggő típus jelenhet meg. A típusok közötti kapcsolatok igen sokrétűek lehetnek. A 7.1. példa űrszondás feladatában például a Pont típus közvetlenül részt vett a Gömb típus reprezentációjában, azaz megjelent annak szerkezetében, és ennek következtében a Gömb típus műveletét megvalósító program használhatja a Pont típus műveletét. Megtehetnénk azonban azt is, hogy a Gömb típust máshogy, például négy valós számmal reprezentáljuk. Ekkor a Pont típus nem vesz részt a Gömb típus reprezentációjában, ugyanakkor tagadhatatlanul ilyenkor is van valamilyen kapcsolat a két típus között: egyrészt minden gömb pontok mértani helyének tekinthető, másrészt a "benne van-e egy pont
171
7. Típus
egy gömbben" művelet is összeköti ezt a két típust. A típusok közötti kapcsolatok, úgynevezett társítások (asszociációk) felfedése segíti a programtervezést, ugyanakkor egyszerűbbé, átláthatóbbá, hatékonyabbá teheti programjaink kódját is, különösen, ha a használt programozási nyelv támogatja az ilyen kapcsolatok leírását. A típusok közötti társítási kapcsolatok lényegében különböző típusok közötti relációt jelent. Ezek lehetnek többes vagy bináris relációk. Bináris társítások esetén például azt lehet vizsgálni, hogy a reláció az egyik típus egy értékéhez hány típusértéket rendelhet a másik típusból. Ez 1-1 formájú, ha reláció kölcsönösen egyértelmű; 1-n formájú, ha a reláció inverze egy függvény; és m-n formájú, ha tetszőleges nem-determinisztikus reláció. Az utóbbi két esetben további vizsgálat lehet az n és m értékének, vagy értékhatárainak meghatározása is. Ezek a vizsgálatok azt a célt szolgálják, hogy a valóságos dolgok közötti viszonyokat a feladatnak az adatabsztrakció után kapott modelljében is megjelenítsük. Említettük már, hogy a Gömb típust másképpen is definiálhattuk volna, amikor közvetlenül négy valós számmal reprezentálunk egy gömböt. Ebben az esetben nem tartalmaz Pont típusú komponenst a Gömb, és ilyenkor annak az eldöntése, hogy a "benne van-e egy pont egy gömbben" művelet melyik típushoz tartozzon, nem magától értetődő. Ha a Gömb típus műveleteként valósítjuk meg, akkor két lehetőségünk is van. Vagy a gömb középpontját reprezentáló valós számokból kell egy pontot készíteni (ez nyilvánvalóan a Pont típus egy új művelete lesz), és ezután – a korábbi megoldáshoz hasonlóan – hivatkozunk a pontok távolságát meghatározó műveletre. Vagy közvetlenül az euklideszi távolság-képlettel dolgozunk, de ehhez előbb meg kell tudnunk határozni egy pont térbeli koordinátáit (ezek is a Pont típus egy új műveletei lesznek). Bizonyos programozási nyelvek úgy kerülik ki, hogy a Pont típushoz újabb műveleteket kelljen bevezetni és számolni kelljen azok meghívásával járó időveszteséggel, hogy bevezetik a barátság (friend) fogalmát, mint speciális társítási kapcsolatot. Ez lehetővé teszi, hogy egyik típus – mondjuk a Gömb típus – beleláthasson a másik – a Pont típus – reprezentációjába. Ilyenkor a "benne van-e egy pont egy gömbben" művelet az euklideszi távolság-képlet alapján közvetlenül megvalósítható; noha a művelet a Gömb típushoz tartozik, a vizsgált pont koordinátáit azonban a barátság miatt a Gömb típusban is közvetlenül látni. Még szebb az a megoldás, amely ezt a "benne van-e egy pont egy gömbben" műveletet kiveszi a
172
7.4. Típusok közötti kapcsolatok
Gömb típusból, azt egy független eljárásnak tekinti, de ezt az eljárást mind a Pont, mind a Gömb típus barátjává teszi, így azoktól közeli, de egyenlő távolságra helyezi el. A barátság kapcsolat segítségével lehetővé válik az eljárás számára mind egy gömb, mind egy pont reprezentációjára történő hivatkozás. Típusok közötti kapcsolatnak tekinthetjük azt is, amikor egy típust (pontosabban az általa kijelölt típus-specifikációt) egy másik típussal megvalósítunk. Ugyancsak típusok közötti kapcsolatot fogalmaz meg egy típus reprezentációja. Pontosabban, itt a típus és annak egy értéket helyettesítő elemi értékek típusai közötti reprezentációs kapcsolatról van szó. A reprezentációs kapcsolat egyik vetülete az a tartalmazási kapcsolat, amelyik úgy tekint a reprezentált típusértékre, mint egy burokra, amely az őt reprezentáló elemi értékeket tartalmazza. Erre láttunk példát az űrszondás feladatban, ahol a Pont és a valós szám típusa építő eleme volt a Gömb típusnak: egy gömb reprezentációja egy pont és egy sugár volt. A tartalmazási kapcsolat tehát arra hívja fel a figyelmet, amikor egy típus részt vesz egy másik típus reprezentációjában. A reprezentációs kapcsolat másik vetülete a reprezentáló típusok egymáshoz való viszonya, a típus-reprezentáció szerkezete. A reprezentáció szempontjából ugyanis lényeges az, hogy egy típusértéket helyettesítő elemi értékeknek mi az egymáshoz való viszonya: egy típusértéket különböző típusú elemi értékek együttese reprezentál-e, vagy különböző típusú értékek közül mindig az egyik, vagy azonos típusú értékek sokasága (halmaza, sorozata). Ez a reprezentáló típusok közötti szerkezeti kapcsolat fontos tulajdonsága a reprezentált típusnak. Azokat a típusokat, ahol a helyettesítő elemek nem rendelkeznek belső szerkezettel, vagy az adott feladat megoldása szempontjából nem lényeges a belső szerkezetük, elemi típusoknak nevezzük. Ha egy típus nem ilyen, akkor összetett típusnak hívjuk. Egy típus szerkezetén a helyettesítő elemek gyakran igen összetett belső szerkezetét értjük, azaz azt, hogyan adható meg egy helyettesítő elem elemibb értékek együtteseként, és milyen kapcsolatok vannak ezen elemi értékek között. Első hallásra ez nem tűnik korrekt meghatározásnak, hiszen a legegyszerűbb elemek is felbonthatók további részekre. A
173
7. Típus
számítógépek világában célszerűnek látszik az elemi szintet a biteknél meghúzni, hiszen egy programozó számára az már igazán nem érdekes, hogy hány szabad elektron formájában ölt testet egy 1-es értékű bit a számítógép memóriájában. A programkészítési gyakorlatban azonban még a bitek szintje is túlságosan alacsony szintűnek számít. A legtöbb feladat megoldása szempontjából közömbös az, hogy a programozási környezetben adott (tehát megengedett) típus értékei hogyan épülnek fel a bitekből. Például a feladatok túlnyomó többségében nem kell azzal foglalkoznunk, hogy a számítógépben egy természetes szám bináris alakban van jelen; nem fontos ismernünk az egész számok kódolására használt 2-es komplemens kódot; nem kell tudnunk arról, hogy a számítógép egy valós számot 2 és 4 közé normált egyenes kódú mantisszájú és többletes kódú karakterisztikájú binárisan normalizált alakban tárol. (Legfeljebb csak a legnagyobb és a legkisebb ábrázolható szám, a fellépő kerekítési hiba vonatkozásában érdekelhet ez bennünket.) Ezért – hacsak a feladat nem követel mást – nyugodtan tekinthetjük a természetes számokat, az egész számokat, és a valós számokat is elemi típusoknak. Hasonló elbírálás alá esik a karakterek típusa, és a logikai típus is. Egy-egy feladat kapcsán természetesen más elemi típusok is megjelenhetnek. Ha egy típus használata megengedett és nem kell hozzáférnünk a típusértékeket helyettesítő elemek egyes részeihez, akkor a típust elemi típusnak tekintjük. Hangsúlyozzuk, hogy ez a meghatározás relatív. Nem kell csodálkozni azon, ha egy típus az egyik feladatban elemi típusként, a másikban összetett típusként jelenik meg, sőt ugyanazon feladat megoldásának különböző fázisában lehet egyszer elemi, máskor összetett. Az összetett típusok műveletei többnyire egy-egy típusérték helyettesítő elemének összetevő részeit kérdezik le, azokat módosítják, esetleg az összetevés módját változtatják meg. Nyilvánvaló, hogy az ilyen típusok műveleteinek elemzésénél, definiálásánál nagy segítséget jelent a helyettesítő elemek szerkezetének ismerete. Erre a típusoknak típus-specifikációval történő megadása nem alkalmas, mert az reprezentáció nélkül kevésbé szemléletes; ráadásul a programozó úgysem kerülheti el, hogy előbb utóbb gondoskodjon a típus reprezentálásáról, azaz döntsön a típusértékek belső logikai, majd fizikai szerkezetéről.19 19
Az adatszerkezeteket középpontba állító vizsgálatoknál célszerű a típus szerkezetének definiálásánál egy típusértéket helyettesítő
174
7.4. Típusok közötti kapcsolatok
Nézzünk meg most példaként néhány jellegzetes típusszerkezetet. Egy típust rekord szerkezetűnek nevezünk, ha egy típusértékét megadott (típusérték-) halmazok egy-egy eleméből álló érték-együttes (rekord) reprezentálja. A T = rec( m1:T1, … , mn:Tn ) azt jelöli, hogy a T egy rekord (direktszorzat) típus, azaz minden típusértékét egy T1×… ×Tn-beli elem (érték-együttes) reprezentálja. Ha t egy ilyen T típusnak egy értéke (rekord típusú objektum vagy röviden rekord), akkor a t reprezentánsának i-edik komponensére a t.mi kifejezéssel hivatkozhatunk (lekérdezhetjük, megváltoztathatjuk). Reprezentáljuk például a gömböket a középpontjukkal és a sugarukkal. Ekkor a Gömb típus rekord szerkezetű. Vezessünk be jól megjegyezhető neveket a reprezentáció komponenseinek azonosítására. Jelölje c a középpontot, r a sugarat. Ekkor a g.c–vel a g gömb középpontjára, g.r-rel pedig a sugarára hivatkozhatunk. Mindezen információt a Gömb=rec(c:Pont,r:R) jelöléssel adhatjuk meg. A típus alternatív szerkezetű, ha egy típusértékét megadott (típusérték-) halmazok valamelyikének egy eleme reprezentálja. A T = (reprezentáló) elemet olyan irányított gráffal ábrázolni, amelyben a csúcsokat elemi adatértékekkel címkézzük meg, az irányított élek pedig az ezek közti szomszédsági kapcsolatokat jelölik. Például egy sorozatszerkezetű típus egy értékét olyan gráffal írhatjuk le, ahol a csúcsokat sorba, láncba fűzzük, és mindegyik csúcsból (az utolsó kivételével) egyetlen irányított él vezet ki, amelyik a rákövetkező adatelemre mutat. A helyettesítő (reprezentáns) elemeknek irányított gráfokkal történő megjelenítése nem feltétlenül jelenti azt, hogy a típusnak konkrét programozási környezetben történő megvalósításakor az irányított gráfban rögzített szerkezetet hűen kell követni. A típus szerkezetének ilyen ábrázolása elsősorban a típus megadását, megértését szolgálja, ezért többnyire egy absztrakt típus definiálásához használjuk. Az absztrakt típus szerkezete, más szóval a típus absztrakt szerkezete lehetővé teszi a típus műveleteinek szemléletes bemutatását, és a műveletek hatékonyságának elemzését. Az irányított gráfok, mint absztrakt szerkezetek lehetőséget adnak a típusok különféle szerkezet szerinti rendszerezésére. Például az összes sorozatszerkezetű típus szerkezetét egy láncszerű irányított gráf, úgynevezett lineáris adatszerkezet jellemzi. Az absztrakt adatszerkezet jellemzésére meg szoktak még különböztetni ortogonális-, fa-, általános gráf-szerkezetű típusszerkezeteket is.
175
7. Típus
alt( m1:T1; … ; mn:Tn ) azt jelöli, hogy T egy alternatív (unió) típus, azaz minden típusértékét egy T1 … Tn-beli elem reprezentál. Ha t egy ilyen T típusnak egy értéke (alternatív típusú objektum), akkor a t.mi logikai érték abban az esetben igaz, ha t-t éppen a Ti típus egyik értéke reprezentálja. Példaként tekintsük azt a típust, amely egy fogorvosi rendelőben egy páciensre jellemző adatokat foglalja össze. Mivel egy páciens lehet felnőtt és gyerek is, akiket fogorvosi szempontból meg kell különböztetnünk, hiszen például a gyerekeknél külön kell nyilván tartani a tejfogakat: Páciens=alt(f:Felnőtt;g:Gyerek). Természetesen egy ilyen példa akkor teljes, ha definiáljuk a Felnőtt és a Gyerek típusokat is. Ezek rekord típusok lesznek: Felnőtt=rec(nev: * , fog:ℕ, kor:ℕ), Gyerek=rec(nev: *, fog:ℕ, tej:ℕ, kor:ℕ). Ha p egy Páciens típusú objektum, akkor a p.f állítással dönthetjük el, hogy a p páciens felnőtt-e; a p.fog:=p.fog-1 értékadás pedig azt fejezi ki, hogy kihúzták a p egyik fogát. Ha egy típus tetszőleges értékét sok azonos típusú elemi érték reprezentálja, akkor azt iterált (felsorolható, gyűjtemény) típusnak is szokták nevezni. Az elnevezés onnan származik, hogy a típusértéket reprezentáló elemi értékeket azok egymáshoz való viszonyától függő módon sorban egymás után fel lehet sorolni. Az elemi értékek kapcsolata a reprezentált típusérték belső szerkezetét határozza meg. Ez lehet olyan, amikor nincs kijelölve az elemi értékek között sorrendiség; a típusértéket elemi értékek közönséges halmaza reprezentálja. Ez a halmaz lehet multiplicitásos halmaz (zsák) is, amelyben megengedjük ugyanazon elem többszöri előfordulását. Lehet a reprezentáló elemsokaság egy sorozat is, amely egyértelmű rákövetkezési kapcsolatot jelöl ki az elemek között, de lehet egy olyan irányított gráf, amelynek csúcsai az elemi értékek, az azok közötti élek pedig sajátos, egyedi rákövetkezési relációt jelölnek. A T=it(E) azt a T iterált (szerkezetű) típust jelöli, amelynek típusértékeit az E elemi típus értékeiből építjük fel. A következő fejezetben számos nevezetes iterált szerkezetű típust fogunk bemutatni. A típusszerkezethez kötődő típusok közötti kapcsolat a típusok közti szerkezeti rokonság. Két típust akkor nevezünk rokonnak, ha típusértékeik ugyanazon típus (esetleg típusok) elemeiből hasonló szerkezet alapján épülnek fel. Szerkezetileg rokon például egy egész számokat tartalmazó tömb és egy egész számokból álló sorozat. Rokon típusok műveletei természetesen különbözhetnek. Ilyenkor megkísérelhetjük az egyik típus műveleteit a rokon típus műveleteinek
176
7.4. Típusok közötti kapcsolatok
segítségével helyettesíteni. A rokonság gyakran egyoldalú, azaz a rokon típusoknak csak az egyike helyettesíthető a másikkal, fordítva nem; gyakran részleges, azaz nem az összes, csak néhány művelet helyettesíthető; esetenként névleges, mert a szerkezeti hasonlóság ellenére sem helyettesíthető egyik típus a másikkal. Típusok között nemcsak szerkezeti rokonság, hanem műveleti rokonság is fennállhat. Erről akkor beszélhetünk, amikor két típusnak megegyeznek a típusműveletei. Például a Gömb típus illetve egy Kocka típus ilyen típusműveleti rokonságot mutathat, ha mindkettőnél olyan műveleteket definiálunk, mint „add meg a test súlypontját”, „számítsd ki a térfogatot”, „benne van-e egy pont a testben”. Ezen műveletek specifikációja is azonos, csak a megvalósításuk tér el egymástól. Speciális típusműveleti rokonság a típus-altípus kapcsolat. Az altípus ugyanazokkal a műveletekkel rendelkezik, mint az általánosabb típus, csak azok értelmezési tartománya és értékkészlete szűkül. Például egész számok típusának egy altípusa az 1 és 10 közé eső egész számok típusa. Az altípus fogalmának általánosításával juthatunk el az objektum orientált programozásban legtöbbet emlegetett típusok közötti kapcsolathoz, az öröklődéshez. Láttuk, hogy az altípus rendelkezik az általánosabb típus reprezentációjával és típus-műveleteivel, azaz „örökli” azokat az általánosabb típustól. Ezt az öröklést rugalmasabban is lehet érvényesíteni: meglevő típusból (esetleg típusokból) származtatunk egy új típust úgy, hogy egyenként megmondjuk, milyen tulajdonságokat (szerkezetet, szerkezet összetevőit, műveleteket) vesszünk át, más néven öröklünk a meglévő típus(ok)tól, és azokhoz milyen új tulajdonságokat teszünk hozzá. Az így létrejött új típust származtatott típusnak, a meglévő típusokat őstípusoknak nevezik.20 Az objektum elvű programtervezés például a Gömb és a Pont típus megadásánál észreveszi, hogy egy gömb általánosabb fogalom, mint a pont, hiszen a pont felfogható egy speciális, nulla sugarú gömbnek. Ilyenkor az úgynevezett megszorító öröklődést alkalmazva a Pont típust származtathatjuk a Gömb típusból, azaz azt mondjuk, hogy a Pont típus majdnem olyan, mint a Gömb típus, csak minden elemének a sugara nulla. A Pont típus tehát Gömb típus megszorításaként áll elő. Meg kell jegyezni, hogy ebben az esetben a gömb – a korábban 20
Az objektum elvű programtervezés a típusok helyett az osztály elnevezést használja.
177
7. Típus
bemutatott megvalósításával ellentétben – már nem egy pont és egy valós szám segítségével, hanem négy valós számmal van reprezentálva. Ha a Gömb típuson definiálunk egy olyan műveletét, amely eldönti, hogy egy gömb tartalmaz-e egy másik gömböt, akkor ezt a műveletet speciálisan úgy is meg lehet majd használni, ha a másik gömböt egy pont helyettesíti (hiszen az is egy gömb). Ha a Gömb típus tartalmazná két gömb távolságát (pontosabban a gömbök középpontjának távolságát) kiszámoló műveletet, akkor ezt a művelet örökölné a Pont típus is, ahol azonban ez már megszorítva, két pont távolságának meghatározására szolgálna. Egyébként ugyanígy örökölhető a „benne van-e egy gömbben egy másik gömb” művelet is, amely a Pont típusra nézve a „benne van-e egy pontban egy másik gömb” vagy akár „benne van-e egy pontban egy másik pont” (ami lényegében az „azonos-e két pont” művelet) értelmet nyerne. A megszorító öröklődésnél elvben azt is el lehet érni, hogy az őstípusból származó bizonyos műveletek ne öröklődjenek a leszármazott típusra. Mivel a példában a Pont típus és a Gömb típus ugyanazon műveletekkel rendelkezik (hiszen a Pont örökli a Gömb egyetlen műveletét és nem vezet be mellé újat), ezért itt a Pont a Gömb altípusa is. A megszorító öröklődés nemcsak az itt bemutatott specializáció mentén jöhet létre, hanem úgy, hogy az egymáshoz hasonló típusokat általánosítjuk, és ennek során elkészítjük azt az őstípust, amelyből a vizsgált típusok öröklődéssel származtathatók. Létezik az objektum elvű programozás gyakorlatában egy másik, a fentiektől eltérő gondolkodás is, amely azért vezet be ősosztályokat, hogy az öröklődés révén a kód-újrafelhasználást támogassa. Ennek a gondolatnak jegyében mondhatjuk például azt, hogy egy gömböt leíró kód tartalmazza a pontot leíró kódot, ezért érdemes előbb a Pont típust megalkotni, és majd ebből származtathatjuk a Gömb típust. A származtatás során kiegészítjük egy pont reprezentációját még egy valós számmal (ez lesz a gömb sugara); átvesszük (örököljük) a két pont távolságát kiszámoló műveletet, amely most már két gömb középpontjainak távolságát számítja majd ki; és csak itt a gömbökre definiáljuk a "benne van-e egy pont egy gömbben" új műveletet. Az öröklődésnek ezt a fajtáját, amikor új tulajdonságokat veszünk hozzá az őstípushoz, analitikus öröklődésnek nevezik. Ebben a megközelítésben előbb a Pont típust kell megalkotni, és utána a Gömb típust, tehát nem a programtervezésnél eddig látott felülről lefelé haladó finomítási
178
7.4. Típusok közötti kapcsolatok
technikát, hanem ellenkezőleg, egy alulról felfelé haladó technikát alkalmazunk. Ez a szemlélet persze csak akkor kifizetődő, ha a programozási nyelv, amelyen implementáljuk majd a programunkat, rendelkezik az öröklődést támogató nyelvi elemekkel.
179
7. Típus
7.5. Feladatok 7.1. Valósítsuk meg a racionális számok típusát úgy, hogy kihasználjuk azt, hogy minden racionális szám ábrázolható két egész számmal, mint azok hányadosa! Implementáljuk az alapműveleteket! 7.2. Valósítsuk meg a komplex számok típusát! Ábrázoljuk ezeket az algebrai alakkukkal (x+iy)! Implementáljuk az alapműveleteket! 7.3. Valósítsuk meg a síkvektorok típusát! Implementáljuk két vektor összegének, egy vektor nyújtásának, és forgatásának műveleteit! 7.4. Valósítsuk meg a négyzet típust! Ennek értékei a sík négyzetei, amelyeket el lehet tolni egy adott vektorral, és fel lehet nagyítani! 7.5. Valósítsuk meg azt a zsák típust, amelyikről tudjuk, hogy csak 1 és 100 közötti természetes szám kerülhet bele, de egy szám többször is! Implementáljuk az új elem behelyezése, egy elem kivétele és „egy elem hányszor van benne a zsákban” műveleteket! 7.6. Valósítsuk meg az egész számokat tartalmazó sor típust! Az elemeket egy tömbben tároljuk. Műveletei: a sor végére betesz, sor elejéről kivesz, megvizsgálja, hogy üres-e illetve tele-e a sor. 7.7. Valósítsuk meg a nagyon nagy természetes számok típusát! A számokat decimálisan ábrázoljuk, és számjegyeit egy kellően hosszú tömbben tároljuk! 7.8. Valósítsuk meg a valós együtthatójú polinomok típusát az összeadás, kivonás, szorzás műveletekkel! 7.9. Valósítsuk meg a diagonális mátrixtípust (amelynek mátrixai csak a főátlójukban tartalmazhatnak nullától különböző számot)! Ilyenkor elegendő csak a főátló elemit reprezentálni egy sorozatban. Implementáljuk a mátrix i-edik sorának j-edik elemét visszaadó műveletet, valamint két mátrix összegét és szorzatát! 7.10. Valósítsuk meg az alsóháromszög mátrixtípust (a mátrixok a főátlójuk felett csak nullát tartalmaznak)! Ilyenkor elegendő csak a főátló és az alatti elemeket reprezentálni egy sorozatban. Implementáljuk a mátrix i-edik sorának j-edik elemét visszaadó műveletet, valamint két mátrix összegét és szorzatát!
180
8. Programozási tételek felsoroló objektumokra Ha egy adatot elemi értékek csoportja reprezentál, akkor az adat feldolgozása ezen értékek feldolgozásából áll. Az ilyen adat lényeges jellemzője, hogy az őt reprezentáló elemi értékeknek mi az egymáshoz való viszonya, a reprezentáció milyen jellegzetes belső szerkezettel rendelkezik. Számunkra különösen azok az adattípusok érdekesek, amelyek típusértékeit azonos típusú elemi értékek sokasága reprezentál. Ilyen például egy tömb, egy halmaz vagy egy sorozat. Ezeknek az úgynevezett gyűjteményeknek közös tulajdonsága, hogy a bennük tárolt elemi értékek egymás után felsorolhatók. Egy halmazból egymás után kivehetjük, egy sorozatnak vagy egy tömbnek egymás után végignézhetjük az elemeit. Éppen ezért az ilyen típusokat szokták felsorolhatónak (enumerable) vagy iteráltnak (iterált szerkezetűnek) is nevezni. Felsorolni azonban nemcsak gyűjtemények elemeit lehet, hanem például egy egész szám valódi osztóit, vagy két egész szám által meghatározott zárt intervallum egész számait. Ennél fogva a felsorolható adat fogalma nem azonos a gyűjteménnyel, hanem annál általánosabb, az előbbi példákban szereplő úgynevezett virtuális gyűjteményeket is magába foglalja. A felsorolhatóság azt jelenti, hogy képesek vagyunk egy adatnak valamilyen értelemben vett első elemére ráállni, majd a soron következőre, az azt követőre, és így tovább, meg tudjuk kérdezni, van-e újabb soron következő elem (vagy van-e egyáltalán első elem), és lekérdezhetjük a felsorolás során érintett aktuális elemnek az értékét. A felsorolást végző műveletek nem annak az adatnak a saját műveletei, amelynek elemeit felsoroljuk. Furcsa is lenne, ha egy egész szám (amelyiknek valódi osztóit kívánjuk felsorolni) alapból rendelkezne ilyen („vedd a következő valódi osztót” féle) műveletekkel. De egy egész számokat tartalmazó intervallumnak is csak olyan műveletei vannak, amivel az intervallum határait tudjuk lekérdezni, az intervallum egész számainak felsorolásához már egy speciális objektumra, egy indexre van szükség. Ráadásul egy intervallum felsorolása többféle lehet (egyesével vagy kettesével; növekvő, esetleg csökkenő sorrendben), ezeket mind nem lenne értelme az intervallum típusműveleteivel leírni. A felsorolást végző műveleteket mindig egy külön objektumhoz kötjük. Ha szükség van
181
8. Programozási tételek felsoroló objektumokra
egy adat elemi érékeinek felsorolásra, akkor az adathoz hozzárendelünk egy ilyen felsoroló objektumot. Egy felsoroló objektum feldolgozása azt jelenti, hogy az általa felsorolt elemi értékeket valamilyen tevékenységnek vetjük alá. Ilyen tevékenység lehet ezen értékek összegzése, adott tulajdonságú értékek megszámolása vagy a legnagyobb elemi érték megkeresése, stb. Ezek ugyanolyan feladatok, amelyek megoldására korábban programozási tételeket vezettünk be, csakhogy azokat a tételeket intervallumon értelmezett függvényekre fogalmaztuk meg, most viszont ennél általánosabb, felsorolóra kimondott változatukra lesz szükség. Ezt az általánosítást azonban könnyű megtenni, hiszen egy intervallumhoz nem nehéz felsorolót rendelni, így a korábban bevezetett intervallumos tételeket egyszerűen átfogalmazhatjuk felsoroló objektumra. Először (8.1. alfejezet) a nevezetes típusszerkezeteket fogjuk megvizsgálni, de ezek között is a legnagyobb hangsúlyt az iterált szerkezetű, tehát felsorolható típusok bemutatására helyezzük. A 8.2. alfejezetben a felsoroló típus műveleteit fogjuk jellemezni, majd megadjuk egy felsoroló objektum általános feldolgozását végző algoritmus-sémát. A 8.3. alfejezetben nevezetes felsoroló típusokat mutatunk. A 8.4. alfejezetben a programozási tételeinket mondjuk ki felsoroló típusokra. Végül (8.5. alfejezet) feladatokat oldunk meg.
182
8.1. Gyűjtemények
8.1. Gyűjtemények Gyűjteményeknek (és itt nem foglalkozunk a virtuális gyűjteményekkel) az összetett szerkezetű adatokat nevezzük. Ezek legfőbb jellegzetessége, hogy reprezentációjuk elemi értékekből épül fel, azaz elemi értékeket tárol, amelyeket megfelelő műveletek segítségével be tudunk járni, fel tudunk sorolni. Leggyakrabban az iterált szerkezetű adatokat használjuk gyűjteményként, amelyek típusértékeit azonos típusú elemi értékek sokasága reprezentálja. Vizsgáljunk meg most néhány nevezetes gyűjteményt. A halmaz (szerkezetű) típus típusértékeit egy-egy véges elemszámú 2E-beli elem (E-beli elemekből képzett halmaz) reprezentálja. Egy halmaz típusú objektumnak a típusműveletei a halmazok szokásos műveletei lesznek. Értelmezzük: halmaz ürességének vizsgálatát (h= , ahol h egy halmaz típusú értéket jelöl), halmaz egy elemének kiválasztását (e: h, ahol e egy elemi értéket hordozó segédváltozó), halmazból egy elem kivonását (h:=h–{e}), új elemnek a hozzáadását a halmazhoz (h:=h {e}). A típus-megvalósítás szempontjából egyáltalán nem közömbös, hogy itt a szokásos unió illetve kivonás műveletével van-e dolgunk, vagy a megkülönböztetett (diszjunkt) unió illetve megkülönböztetett kivonással. Ez utóbbiak esetén feltételezzük, hogy a művelet megváltoztatja a halmazt, azaz unió esetén bővül (mert a hozzáadandó elem új, még nincs benne a halmazban), kivonás esetén fogy (mert a kivonandó elem benne van a halmazban). Ezeknek a műveleteknek az implementálása egyszerűbb, mert nem kell ezen feltételeket külön ellenőrizniük, igaz, a feltétel nem teljesülése estén abortálnak. A halmaz típust a set(E) jelöli. Látjuk, hogy egy halmaz egy elemének kiválasztása (e: h) a nem-determinisztikus értékkiválasztással történik, amely ugyanazon halmazra egymás után alkalmazva nem ugyanazt az eredményt fogja adni. Be lehet vezetni azonban a determinisztikus elemkiválasztás műveletét (e:=mem(h)), amelyet ha ugyanarra a halmazra többször egymás után hajtjuk végre, akkor mindig ugyanazon elemét adja vissza a halmaznak. Az igazat megvallva, a halmaz szóba jöhető reprezentációi sokkal inkább támogatják ennek az elemkiválasztásnak a megvalósítását, mint a valóban véletlenszerű, nem-determinisztikus értékkiválasztást.
183
8. Programozási tételek felsoroló objektumokra
A sorozat (szerkezetű) típus típusértékeit egy-egy véges hosszú E*-beli elem (E-beli elemekből képzett sorozat) reprezentálja, típusműveletei pedig a sorozatokon értelmezhető műveletek. Jelöljön a t egy sorozat típusú objektumot, amelyet egy sorozat reprezentál. Most a teljesség igénye nélkül felsorolunk néhány típusműveletet, amelyeket a sorozatra be szoktak vezetni. Ilyen egy sorozat hosszának lekérdezése (|t|), a sorozat valahányadik elemére indexeléssel történő hivatkozás (ti ahol az i indexnek 1 és a sorozat hossza közé kell esni), egy elem törlése egy sorozatból (ha a sorozat nem üres) vagy egy új elem beszúrása. Magát a sorozat típust a seq(E) jelöli. Speciális sorozattípusokhoz jutunk, ha csak bizonyos típusműveleteket engedünk meg. Ilyen például a verem és a sor. A verem esetén csak a sorozat elejére szabad új elemet befűzni, a sornál pedig csak a sorozat végére. Mindkettő esetén megengedett művelet annak eldöntése, hogy a reprezentáló sorozat üres-e. Mindkettőnél ki lehet olvasni a sorozat első elemét, és azt el is lehet hagyni a sorozatból. A szekvenciális outputfájl (jelölése: outfile(E)) két műveletet enged meg: üres sorozat létrehozását (t:=<>) és a sorozat végéhez új elem vagy elemekből képzett sorozat hozzáillesztését (jelölése: t:write(e) vagy t:write(<e1, … ,en>)). A szekvenciális inputfájlnak (jelölése: infile(E)) is egyetlen művelete van: a sorozat első elemének lefűzése, más szóval az olvasás művelete. Matematikai értelemben ezt egy olyan függvénnyel írhatjuk le, amely egy sorozat típusú objektumhoz (pontosabban az őt reprezentáló sorozathoz) három értéket rendel: az olvasás státuszát, a sorozat első elemét (ha van ilyen), és az első elemétől megfosztott sorozatot. Az olvasást az st,e,t:=read(t) értékadással, vagy rövidítve az st,e,t:read szimbólummal jelöljük. Az st az olvasás státusza. Ez egy speciális kételemű halmaznak (Státusz ={abnorm, norm}) az elemét veheti fel értékként. Ha a t egy üres sorozat, akkor az st,e,t:read művelet során az st változó az abnorm értéket veszi fel, a t-beli sorozat továbbra is üres marad, az e pedig definiálatlan. Ha a t-beli sorozat nem üres, akkor az st,e,t:read művelet végrehajtása után az st változó az norm-ot, az e a t sorozat első elemét, a t pedig az eggyel rövidebb sorozatot veszi fel. Sorozat szerkezetűnek tekinthetjük a vektor típust, vagy más néven egydimenziós tömböt. Ha eltekintünk egy pillanatra attól, hogy a vektorok tetszőlegesen indexelhetőek, akkor a vektor felfogható egy olyan sorozatnak, amelynek az elemeire a sorszámuk alapján lehet közvetlenül hivatkozni, de a sorozat hossza (törléssel vagy beszúrással)
184
8.1. Gyűjtemények
nem változtatható meg. Egy vektor típusához azonban a fent vázolt sorozaton kívül azt az indextartományt is meg kell adni, amely alapján a vektor elemeit indexeljük. A vektor típus tehát valójában egy rögzített hosszúságú sorozatból és egy egész számból álló rekord, amelyben a sorozat tartalmazza a vektor elemeit, az egész szám pedig a sorozat első elemének indexét adja meg. A vektor típust a vec(E) jelöli. A vektor alsó és felső indexének lekérdezése is éppen úgy típusműveletnek tekinthető, mint a vektor adott indexű elemére való hivatkozás. Ha v egy vektor, i pedig egy indexe, akkor v[i] a vektor i indexű eleme, amit lekérdezhetünk vagy megváltoztathatunk, azaz állhat értékadás jobb vagy baloldalán. Általánosan a v vektor indextartományának elejét a v.lob, végét a v.hib kifejezések adják meg. Ha azonban a vektor típusra az általános vec(E) jelölés helyett továbbra is a korábban bevezetett E m..n jelölést alkalmazzuk, akkor az indextartományra egyszerűen az m és n segítségével hivatkozhatunk. Világosan kell azonban látni, hogy itt az m és az n a vektor egyedi tulajdonságai, és nem attól független adatok. A kétdimenziós tömbtípusra, azaz a mátrix típusra vektorok vektoraként gondolunk. Ennek megfelelően egy t mátrix i-edik sorának j-edik elemére a t[i][j] hivatkozik (ezt rövidebben t[i,j]-vel is jelölhetjük), az i-edik sorra a t[i], az első sor indexére a t.lob, az utolsóéra a t.hib, az i-edik sor első elemének indexére a t[i].lob, az utolsó elem indexére a t[i].hib. A mátrix típust a matrix(E) jelöli. (Ezek a műveletek általánosíthatóak a kettőnél több dimenziós tömbtípusokra is.) Speciális, de a leggyakrabban alkalmazott mátrix (téglalap-mátrix) az, amelynek sorai egyforma hosszúak és ugyanazzal az indextartománnyal indexeltek. Ezt jelöli az El..n×k..m, ahol a sorokat az [l..n] intervallum, egy sor elemeit pedig a [k..m] intervallum indexeli. Ha k=1 és l=1, akkor az En×m-jelölést is használjuk, és ilyenkor n*m-es mátrixokról (sorok száma n, oszlopok száma m) beszélünk. Könyvünkben megengedettnek tekintjük mindazokat a típusokat, amelyeket megengedett típusokból az itt bemutatott típusszerkezetek segítségével készíthetünk. 8.2. Felsoroló típus specifikációja A felsorolható adatoknak (vektornak, halmaznak, szekvenciális inputfájlnak, valódi osztóit felkínáló természetes számnak; tehát a valódi vagy virtuális gyűjteményeknek) az elemi értékeit egy külön
185
8. Programozási tételek felsoroló objektumokra
objektum segítségével szoktuk felsorolni. Természetesen egy felsoroló objektumnak hivatkoznia kell tudni az általa felsorolt adatra, amelyen keresztül támaszkodik a felsorolt adat műveleteire, de ezen kívül egyéb segédadatokat is használhat. Egy felsoroló objektum21 (enumerator) véges sok elemi érték felsorolását teszi lehetővé azáltal, hogy rendelkezik a felsorolást végző műveletekkel: rá tud állni a felsorolandó értékek közül az elsőre vagy a soron következőre, meg tudja mutatni, hogy tart-e még a felsorolás és vissza tudja adni a felsorolás során érintett aktuális értéket. Azt a típust, amely biztosítja ezeket a műveleteket, és ezáltal felsoroló objektumok leírására képes, felsoroló típusnak nevezzük. Egy t felsoroló objektumra tehát definíció szerint négy műveletet vezetünk be. A felsorolást mindig azzal kezdjük, hogy a felsorolót a felsorolás során először érintett elemi értékre – feltéve, hogy van ilyen – állítjuk. Ezt általánosan a t:=First(t) értékadás valósítja meg, amit a továbbiakban t.First()22-tel jelölünk. Minden további, tehát soron következő elemre a t.Next() művelet (amely a t:=Next(t) rövidített jelölése) segítségével tudunk ráállni. Vegyük észre, hogy mindkettő művelet megváltoztatja a t felsoroló állapotát. A t.Current() művelet a felsorolás alatt kijelölt aktuális elem értéket adja meg. A t.End() a felsorolás során mindaddig hamis értéket ad vissza, amíg van kijelölt aktuális elem, a felsorolás végét viszont igaz visszaadott értékkel jelzi. Nem szükségszerű, de ajánlott e két utóbbi műveletet úgy definiálni, hogy ne változtassa meg a felsoroló állapotát. Mi a továbbiakban ezt következetesen be is tartjuk. Fontos kritérium, hogy a felsorolás vége véges lépésben (a t.Next() véges sok végrehajtása után) bekövetkezzék. A felsoroló műveletek hatását általában nem definiáljuk minden esetre. Például nem-definiált az, hogy a t.First() végrehajtása előtt (tehát a felsorolás kezdete előtt) illetve a t.End() igazra váltása után (azaz a felsorolás befejezése után) mi a hatása a t.Next(), a t.Current() és a t.End() műveleteknek. Általában nem definiált az sem, hogy mi történjen akkor, ha a t.First() műveletet a felsorolás közben ismételten végrehajtjuk. 21
Amikor a felsorolható adat egy gyűjtemény (iterált), akkor a felsoroló objektumot szokták bejárónak vagy iterátornak is nevezni, míg maga a felsorolható gyűjtemény a bejárható, azaz iterálható adat. 22 A műveletek jelölésére az objektum orientált stílust használjuk: t.First() a t felsorolóra vonatkozó First() műveletet jelöli.
186
8.2. Felsoroló típus specifikációja
Minden olyan típust felsorolónak nevezünk, amely megfelel a felsoroló típus-specifikációnak, azaz implementálja a First(), Next(), End() és Current() műveleteket. A felsoroló típust enor(E)-vel jelöljük, ahol az E a felsorolt elemi értékek típusa. Ezt a jelölés alkalmazhatjuk a típus értékhalmazára is. Egy felsoroló objektum mögé mindig elemi értékeknek azon véges hosszú sorozatát képzeljük, amelynek elemeit sorban, egymás után be tudjuk járni, fel tudjuk sorolni. Ezért specifikációs jelölésként megengedjük, hogy egy t felsoroló által felsorolható elemi értékre úgy hivatkozzunk, mint egy véges sorozat elemeire: a ti a felsorolás során iedikként felsorolt elemi érték, ahol i az 1 és a felsorolt elemek száma (jelöljük ezt t -vel) közé eső egész szám. Hangsúlyozzuk, hogy a felsorolható elemek száma definíció szerint véges. Ezzel tulajdonképpen egy absztrakt megvalósítást is adtunk a felsoroló típusnak: a felsorolókat a felsorolandó elemi értékek sorozata reprezentálja, ahol a felsorolás műveleteit ezen a sorozat bejárásával implementáljuk. Egy felsoroló típus konkrét reprezentációjában természetesen nem szerepel ez a sorozat (kivéve, ha éppen sorozat bejárására definiálunk felsorolót), de mindig megjelenik valamilyen hivatkozás arra az adatra, amelyet fel akarunk sorolni. Ez az adat lehet egyetlen természetes szám, ha annak az osztóit kell előállítani, lehet egy vektor, szekvenciális inputfájl, halmaz, esetleg multiplicitásos halmaz (zsák), ha ezek elemeinek felsorolása a cél, vagy akár egy gráf, amelynek a csúcsait valamilyen stratégiával be kell járni, hogy az ott tárolt értékekhez hozzájussunk. A reprezentáció ezen az érték-szolgáltató adaton kívül még tartalmazhat egyéb, a felsorolást segítő komponenseket is. A felsorolás során mindig van egy aktuális elemi érték, amelyet az adott pillanatban lekérdezhetünk. Egy vektor elemeinek felsorolásánál ehhez elég egy indexváltozó, egy szekvenciális inputfájl esetében a legutoljára kiolvasott elemet kell tárolni illetve, azt, hogy sikeres volt-e a legutolsó olvasás, az egész szám osztóinak felsorolásakor például a legutoljára megadott osztót. Egy felsoroló által visszaadott értékeket rendszerint valahogyan feldolgozzuk. Ez a feldolgozás igen változatos lehet; jelöljük ezt most általánosan az F(e)-vel, amely egy e elemi értéken végzett tetszőleges tevékenységet fejez ki. Nem szorul különösebb magyarázatra, hogy a felsorolásra épülő feldolgozást az alábbi algoritmus-séma végzi el.
187
8. Programozási tételek felsoroló objektumokra
Megjegyezzük, hogy mivel a felsorolható elemek száma véges, ezért ez a feldolgozás véges lépésben garantáltan befejeződik. t.First() t.End() F( t.Current() ) t.Next() 8.3. Nevezetes felsorolók Az alábbiakban megvizsgálunk néhány fontos felsoroló típust, olyat, amelynek reprezentációja valamilyen nevezetes – egy kivételével – iterált típusra épül. Vizsgálatainknak fontos része lesz, hogy megmutatjuk, hogyan specializálódik a fenti általános feldolgozást végző algoritmus-séma egy-egy konkrét felsoroló típusú objektum esetén. Természetesen minden esetben ki fogunk térni arra, hogy a vizsgált típus hogyan feleltethető meg a felsoroló típusspecifikációnak, azaz mi a felsorolást biztosító First(), Next(), End() és Current() műveletek implementációja. Tekintsük először az egész-intervallumot felsoroló típust. Itt egy [m..n] intervallum elemeinek klasszikus, m-től n-ig egyesével történő felsorolására gondolunk. Természetesen ennek mintájára lehet definiálni a fordított sorrendű vagy a kettesével növekedő felsorolót is. Az egész számok intervallumát nem tekintjük iterált szerkezetűnek, hiszen a reprezentációjához elég az intervallum két végpontját megadni, műveletei pedig ezeket az intervallumhatárokat kérdezik le. Ugyanakkor mindig fel lehet sorolni az intervallumba eső számokat. Az egész-intervallumra épülő felsoroló típus attól különleges számunkra, hogy a korábban bevezetett programozási tételeinket is az egész számok egy intervallumára fogalmaztuk meg, amelyeket valójában ezen intervallum elemeinek felsorolását végezték. Ennél fogva a korábban mutatott programozási tételeket könnyen tudjuk majd felsorolókra általánosítani. Az egész-intervallumot felsoroló típus egy típusértékét egy [m..n] intervallum két végpontja (m és n) és az intervallum elemeinek felsorolását segítő egész értékű indexváltozó (i) reprezentálja. Az i
188
8.3. Nevezetes felsorolók
változó az [m..n] intervallum aktuálisan kijelölt elemét tartalmazza, azaz implementálja a Current() függvényt. A First() műveletet az i:=m értékadás, a Next() műveletet az i:=i+1 értékadás váltja ki. Az i>n helyettesíti az End() függvényt. (A First() művelet itt ismételten is kiadható, és mindig újraindítja a felsorolást, mind a négy művelet bármikor, a felsoroláson kívül is terminál.) Ezek alapján a felsoroló objektum feldolgozását végző általános algoritmus-sémából előállítható az egész-intervallum klasszikus sorrendű feldolgozását végző algoritmus, amelyet számlálós ciklus formájában is megadhatunk. i := m i n F(i)
i = m .. n F(i)
i := i+1 Könnyű végiggondolni, hogy miként lehetne az intervallumot fordított sorrendben ( i:=n, i:=i-1, i<m), vagy kettesével növekedően (i:=m, i:=i+2, i>n) felsorolni. Magától értetődően lehet sorozatot felsoroló típust elkészíteni. (Itt is a klasszikus, elejétől a végéig tartó bejárásra gondolunk, megjegyezve, hogy más bejárások is vannak.) A reprezentáció ilyenkor egy sorozat típusú adat (s) mellett még egy indexváltozót (i) is tartalmaz. A sorozat bejárása során az i egész típusú indexváltozót az 1 .. s intervallumon kell végigvezetni éppen úgy, ahogy ezt az előbb láttuk. Ekkor az si-t tekinthetjük a Current() által visszaadott értéknek, az i:=1 a First() művelet lesz, az i:=i+1 a Next() művelet megfelelője, az i> s pedig az End() kifejezéssel egyenértékű. (A First() művelet ismételten is kiadható, amely újraindítja a felsorolást, a Next() és End() bármikor végrehajtható, de a Current()-nek csak a felsorolás alatt van értelme.) Ezek alapján az s sorozat elemeinek elejétől végéig történő felsorolását végző programot az alábbi két alakban írhatjuk fel.
189
8. Programozási tételek felsoroló objektumokra
i := 1 i s F(si)
i = 1 .. s F(si)
i := i+1 A vektort (klasszikusan) felsoroló típus a sorozatokétól csak abban különbözik, hogy itt nem egy sorozat, hanem egy v vektor bejárása a cél.23 A bejáráshoz használt indexváltozót (jelöljük ezt most is i-vel) a bejárandó v vektor indextartományán (jelöljük ezt [m..n]-nel) kell végigvezetnünk, és az aktuális értékre a v[i] formában hivatkoznunk. Ennek értelmében az v[i]-t tekinthetjük a Current() műveletnek, az i:=m a First() művelet lesz, az i:=i+1 a Next() művelet megfelelője, az i>n pedig az End() kifejezéssel egyenértékű. (A műveletek felsoroláson kívüli viselkedése megegyezik a sorozat felsorolásánál mondottakkal.) Egy vektor elemeinek feldolgozását az intervallumhoz hasonlóan kétféleképpen írjuk fel:
23
Könyvünkben úgy tekintünk a vektor indextartományára, mint egy egész intervallumra. A vektor típus fogalma azonban általánosítható úgy, hogy indextartományát egy felsoroló objektum írja le. Ilyenkor a vektort felsoroló objektum műveletei megegyeznek az ő indextartományát felsoroló objektum műveleteivel egyet kivéve: a Current() műveletet a v[i.Current()] implementálja, ahol i az indextartomány elemeit felsoroló objektumot jelöli.
190
8.3. Nevezetes felsorolók
i := 1 i = 1 .. s
i s
F(si)
F(si) i := i+1
Mivel a mátrix vektorok vektora, ezért nem meglepő, hogy mátrixot felsoroló típust is lehet készíteni. A mátrix esetén az egyik leggyakrabban alkalmazott úgynevezett sorfolytonos felsorolás az, amikor először az első sor elemeit, azt követően a másodikat, és így tovább, végül az utolsó sort járjuk be. Egy a jelű n*m-es mátrix (azaz téglalap-mátrix) sorfolytonos bejárásánál a mátrixot felfoghatjuk egy n*m elemű v vektornak, ahol minden 1 és n*m közé eső k egész számra a v[k] = a[((k-1) div m) +1, ((k-1) mod m) +1]. Ezek után a bejárást a vektoroknál bemutatott módon végezhetjük. Egyszerűsödik a képlet, ha a vektor és a mátrix indextartományait egyaránt 0-tól kezdődően indexeljük. Ilyenkor v[k] = a[k div m, k mod m], ahol a k a 0..n*m-1 intervallumot futja be. A mátrix elemeinek sorfolytonos bejárása igen egyszerű lesz, bár nem ez az általánosan ismert módszer. k = 0 .. n*m-1 F(a[k div m, k mod m]) Mivel a mátrix egy kétdimenziós szerkezetű típus, ezért a bejárásához az előbb bemutatott módszerrel szemben két indexváltozót szoktak használni. (Más szóval a mátrix felsorolóját a mátrix és két indexváltozó reprezentálja.) Sorfolytonos bejárásnál az egyiket a mátrix sorai közötti bejárásra, a másikat az aktuális sor elemeinek a bejárására használjuk. A bejárás során a[i,j] lesz a Current(). Először a a[1,1]-re kell lépni, így a First() műveletet az i,j:=1,1 implementálja. A soron következő mátrixelemre egy elágazással léphetünk rá. Ha a j bejáró még nem érte el az aktuális sor végét, akkor azt kell eggyel megnövelni. Ellenkező esetben az i bejárót növeljük meg eggyel, hogy a következő sorra lépjünk, a j bejárót pedig a sor elejére állítjuk. Összességében tehát az IF(j<m: j:=j+1; j=m: i,j:=i+1,1) elágazás
191
8. Programozási tételek felsoroló objektumokra
implementálja a Next() műveletet. Az End() kifejezést az i>n helyettesíti. (Ez a megoldás könnyen általánosítható nem téglalapmátrixra is, ahol a sorok eltérő hosszúak és eltérő indexelésűek.) i, j := 1, 1 i n F( a[i,j] ) j<m j := j+1
i, j := i+1, 1
Ennek a megoldásnak a gyakorlatban jobban ismert változata az alábbi kétciklusos algoritmus. Mindkét változat ugyanabban a sorrendben sorolja fel az (i,j) indexpárokat az (1,1)-től indulva az (n,m)-ig. Az egyetlen eltérés a két változat között az, hogy leálláskor (amikor i=n+1) a fenti változatban a j értéke 1 lesz, míg a lenti változatban m+1. i = 1 .. n j = 1 .. m F( a[i,j] ) A szekvenciális inputfájl felsoroló típusa egy szekvenciális inputfájllal (f), az abból legutoljára kiolvasott elemi értékkel (e), és az utolsó olvasás státuszával (st) reprezentál egy bejárót. A szekvenciális inputfájl felsorolása csak az elejétől végéig történő olvasással lehetséges. st, e, f : read st=norm F(e) st, e, f : read
192
8.3. Nevezetes felsorolók
A First() műveletet az először kiadott st,e,f:read művelet váltja ki. Az ismételten kiadott st,e,f:read művelet az f.Next() művelettel egyenértékű. Az st,e,f:read művelet az aktuális elemet az e segédváltozóba teszi, így a Current() helyett közvetlenül az e értékét lehet használni. Az f.End() az olvasás sikerességét mutató st státuszváltozó vizsgálatával helyettesíthető: st=abnorm. Mindegyik művelet bármikor alkalmazható, mert a read művelet mindig értelmezett, de a bejárást nem lehet ismételten elindítani vele, hiszen egy szekvenciális inputfájl logikai értelemben csak egyszer járható be, minden olvasás elhagy belőle egy elemet. Mind a négy művelet minden esetben terminál. A halmazt felsoroló típus reprezentációjában egy a felsorolandó elemeket tartalmazó h halmaz szerepel. Ha a h= , akkor a halmaz bejárása nem lehetséges vagy nem folytatható – ez lesz tehát az End() művelet. Ha a h , akkor könnyen kiválaszthatjuk felsorolás számára a halmaznak akár az első, akár soron következő elemét. Természetesen az elemek halmazbeli sorrendjéről nem beszélhetünk, csak a felsorolás sorrendjéről. Ez az elemkiválasztás elvégezhető a nem-determinisztikus e: h értékkiválasztással éppen úgy, mint a halmazokra korábban bevezetett determinisztikus elemkiválasztás (e:=mem(h)). Mi ez utóbbit fogjuk a Current() művelet megvalósítására felhasználni azért, hogy amikor a halmaz bejárása során tovább akarunk majd lépni, akkor éppen ezt, a felsorolás során előbb kiválasztott elemet tudjuk majd kivenni a halmazból. Ehhez pedig pontosan ugyanúgy kell tudnunk megismételni az elemkiválasztást. A Next() művelet ugyanis nem lesz más, mint a mem(h) elemnek a h halmazból való eltávolítása, azaz a h:=h–{mem(h)}. Ennek az elemkivonásnak az implementációja egyszerűbb, mint egy tetszőleges elem kivonásáé, mert itt mindig csak olyan elemet veszünk el a h halmazból, amely biztosan szerepel benne, ezért ezt külön nem kell vizsgálni. A Next() művelet is (akárcsak a Current()) csak a bejárás alatt – azaz amíg az End() hamis – alkalmazható. Láthatjuk azonban, hogy sem az End(), sem a Current(), sem a Next() művelet alkalmazásához nem kell semmilyen előkészületet tenni, azaz a felsorolást elindító First() művelet halmazok bejárása esetén az üres (semmit sem csináló) program lesz. Ezen megjegyzéseknek megfelelően a halmaz elemeinek feldolgozását az alábbi algoritmus végzi el:
193
8. Programozási tételek felsoroló objektumokra
h F(mem(h)) h := h – {mem(h)} 8.4. Programozási tételek általánosítása A programozási tételeinket korábban az egész számok intervallumára fogalmaztuk meg, ahol egy intervallumon értelmezett függvénynek értékeit kellett feldolgozni. Ennek során bejártuk, felsoroltuk az intervallum elemeit, és mindig az aktuális egész számhoz tartozó függvényértéket vizsgáltuk meg. Az egész számok intervallumához – mint azt láttuk – elkészíthető egy klasszikus sorrendű felsoroló, amely éppen úgy képes az intervallumot bejárni, ahogy azt az intervallumos programozási tételekben láttuk.. Ezek alapján általánosíthatjuk az intervallumos programozási tételeinket bármilyen más felsoroló típusra is. A feladatokat ilyenkor nem intervallumon értelmezett függvényekre, hanem egy felsorolóra, pontosabban a felsoroló által felsorolt értékeken értelmezett függvényekre mondjuk ki. A megoldó programokban az intervallumot befutó i helyett a Current(), az i:=m értékadás helyett a First(), az i:=i+1 értékadás helyett a Next(), és az i>n feltétel helyett az End() műveletet használjuk. Az így kapott általános tételekre aztán bármelyik konkrét felsorolóra megfogalmazott összegzés, számlálás, maximum vagy adott tulajdonságú elem keresése visszavezethető. Ezek között természetesen az intervallumon értelmezett függvényekkel megfogalmazott feladatok is, tehát a korábban megszerzett feladatmegoldási tapasztalataink sem vesznek el. A programozási tételek ehhez hasonló általánosításaival egyszeregyszer már korábban is találkoztunk. Már korábban is oldottunk meg olyan feladatokat, ahol nem intervallumon értelmezett függvényre, hanem vektorra alkalmaztuk a tételeinket. Ezt eddig azzal magyaráztuk, hogy a vektor felfogható egy táblázattal megadott egész intervallumon értelmezett függvénynek. Most már egy másik magyarázatunk is van. Az intervallum és a vektor rokon típusok: mindkettőhöz készíthető felsoroló. Egy felsorolóra megfogalmazott összegzés, számlálás, maximum vagy adott tulajdonságú elem keresés során pedig mindegy,
194
8.4. Programozási tételek általánosítása
hogy egy intervallumon értelmezett függvény értékeit kell-e sorban megvizsgálni vagy egy vektornak az elemeit, hiszen ezeket az értékeket úgyis a felsorolás állítja elő. Hangsúlyozzuk, hogy az általános programozási tételekben nem közvetlenül a felsorolt elemeket dolgozzuk fel (adjuk össze, számoljuk meg, stb.), hanem az elemekhez hozzárendelt értékeket. Ezeket az értékeket bizonyos tételeknél (pl. összegzés, maximum kiválasztás) egy f:E→H függvény (ez sokszor lehet az identitás), másoknál (pl. számlálás, keresés) egy :E→ logikai függvény jelöli ki. Ennek következtében a feldolgozás során általában nem a t.Current() értékeket, hanem az f(t.Current()) vagy (t.Current()) értékeket kell vizsgálni. A specifikációk utófeltételében egy felsorolás elemeire hivatkozhatunk indexeléssel (lévén egy absztrakt sorozat a felsoroló hátterében), de a visszavezetés szempontjából ez sok lényegtelen elemet tartalmaz. Ezért egy új specifikációs jelölést is bevezetünk: az e t kifejezéssel (amelyik nem halmazműveletet jelöl, hiszen a t nem egy halmaz, hanem egy felsoroló) azt a szándékot fejezzük ki, hogy az e változóban sorban egymás után jelenjenek meg a t felsorolás elemei.
195
8. Programozási tételek felsoroló objektumokra
1. Összegzés Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy f:E→H függvény. A H halmazon értelmezzük az összeadás asszociatív, baloldali nullelemes műveletét. Határozzuk meg a függvénynek a t elemeihez rendelt értékeinek összegét! (Üres felsorolás esetén az összeg értéke definíció szerint a nullelem: 0). Specifikáció: A = (t:enor(E), s:H) Ef = ( t=t’ ) t'
Uf = ( s = i 1
Algoritmus: s := 0 t.First()
f (ti, ) ) =
=( s
f(e) )
t.End() s := s+f(t.Current()) t.Next()
e t'
2. Számlálás Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy :E feltétel. A felsoroló objektum hány elemére teljesül a feltétel? Specifikáció: A = (t:enor(E), c:ℕ) Ef = ( t=t’ ) t'
Uf = ( c
1 )= i 1 β(t i, )
=( c
1) e t' β(e )
196
Algoritmus: c:=0 t.First() t.End() ( t.Current() ) c := c+1 t.Next()
SKIP
8.4. Programozási tételek általánosítása
3. Maximum kiválasztás Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy f:E→H függvény. A H halmazon definiáltunk egy teljes rendezési relációt. Feltesszük, hogy t nem üres. Hol veszi fel az f függvény a t elemein a maximális értékét? Specifikáció: A = (t:enor(E), max:H, elem:E) Ef = ( t=t’ t >0 ) ' Uf = ( ind [1.. t’ ]:elem= t ind
= ( (max, ind )
t'
' max=f( t ind )= max f(ti, ) ) =
i 1
t'
, , max f(ti ) elem= tind ) = i 1
= ( max, elem
max f(e) ) e t'
Algoritmus: t.First() max, elem:= f(t.Current()), t.Current() t.Next() t.End() f(t.Current())>max max, elem:= f(t.Current()), t.Current()
SKIP
t.Next()
197
8. Programozási tételek felsoroló objektumokra
4. Kiválasztás Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy :E feltétel. Keressük a t bejárása során az első olyan elemi értéket, amely kielégíti a :E feltételt, ha tudjuk, hogy biztosan van ilyen. Specifikáció: A = (t:enor(E), elem:E) Ef = ( t=t’ i [1.. t ]: (ti) ) Uf = ( ind [1.. t’ ]: elem=t’ind (elem)
j [1..ind-1]:
Algoritmus: t.First() (t.Current()) ( t 'j )
elem:=t.Current()
t = t’[ind+1.. t’ ] ) = = ( (ind,t)
t.Next()
' ' select β (t ind ) elem= tind ) = ( (elem, t) select β (elem) ) ind 1 elem t '
5. Lineáris keresés Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy :E feltétel. Keressük a t bejárása során az első olyan elemi értéket, amely kielégíti a feltételt. Specifikáció: A = (t:enor(E), l: , elem:E) Ef = ( t=t’ )
Algoritmus: l := hamis; t.First()
Uf = ( l= i [1.. t’ ]: ( ti' ) l ind [1.. t’ ]:elem=t’ind (elem) j [1..ind-1]:
l
t.End()
elem := t.Current() l := (elem)
'
(t j )
t.Next()
t = t’[ind+1.. t’ ] ) = t'
= ( (l,ind,t)
198
search β (t i' ) i 1
' elem= tind ) = ( (l, elem,t)
search β (e) ) e t'
8.4. Programozási tételek általánosítása
6. Feltételes maximumkeresés Feladat: Adott egy E-beli elemeket felsoroló t objektum, egy :E feltétel és egy f:E→H függvény. A H halmazon definiáltunk egy teljes rendezési relációt. Határozzuk meg t azon elemeihez rendelt f szerinti értékek között a legnagyobbat, amelyek kielégítik a feltételt. Specifikáció: A = (t:enor(E), l: , max:H, elem:E) Ef = ( t=t’ ) Uf = ( l = i [1.. t’ ]: ( ti' ) (elem)
' ind [1.. t’ ]: elem= t ind
l
i [1.. t’ ]: ( ( ti' )
max=f(elem) t'
= ( (l, max, ind) = max f(t i, )
f( ti' ) max) ) =
' elem= tind )=
i 1 β (t'i )
= ( (l, max, elem) = max f(e) ) e t' β(e)
Algoritmus: l:= hamis; t.First() t.End() (t.Current()) SKIP
(t.Current())
l
f(t.Current())>max max, elem:= f(t.Current()), t.Current()
SKIP
( t.Current())
l
l, max, elem := igaz, f(t.Current()), t.Current()
t.Next()
199
8. Programozási tételek felsoroló objektumokra
Az általánosított programozási tételekhez az alábbi megjegyzéseket fűzzük: 1. A programozási tételek alkalmazásakor – ha körültekintően járunk el – szabad az algoritmuson hatékonyságot javító módosításokat tenni. Ilyen például az, amikor ahelyett, hogy sokszor egymás után lekérdezzük a t.Current() értékét (miközben a Next()-tel nem lépünk tovább), azt az értéket az első lekérdezésénél egy segédváltozóba elmentjük. A maximum kiválasztás illetve feltételes maximumkeresés esetén a feldolgozás eredményei között szerepel mind a megtalált maximális érték, mind pedig az elem, amelyhez a maximális érték tartozik. Konkrét esetekben azonban nincs mindig mindkettőre szükség. Például olyan esetekben, ahol a f függvény identitás, azaz egy elem és annak értéke megegyezik, a maximális elem és maximális érték közül elég csak az egyiket nyilvántartani az algoritmusban. 2. Kiválasztásnál nem kell a felsoroló által szolgáltatott értéksorozatnak végesnek lennie, hiszen ez a tétel más módon garantálja a feldolgozás véges lépésben történő leállását. 3. A lineáris keresésnél és kiválasztásnál az eredmények között szerepel maga a felsoroló is. Ennek az oka az, hogy ennél a két tételnél korábban is leállhat a feldolgozás, mint hogy a felsorolás véget érne, és ekkor maradnak még fel nem sorolt (fel nem dolgozott) elemek. Ezeket az elemeket további feldolgozásnak lehet alávetni, ha a felsorolót tovább használjuk. Felhívjuk azonban a figyelmet arra, hogy ha egy már korábban használt felsorolóval dolgozunk tovább, akkor nem szabad majd a First() művelettel újraindítani a felsorolást. 4. Nevezetes felsorolók alkalmazása esetén érdemes saját specifikációs jelöléseket bevezetni. Ilyenkor ugyanis a specifikációt nem egy absztrakt felsorolóra (t:enor(E)), hanem közvetlenül a feldolgozandó gyűjteményre (intervallumra, tömbre, halmazra, szekvenciális fájlra) fogalmazzuk a felsoroláshoz használt segédadatokkal. a) Egy szekvenciális inputfájl felsorolóját maga a szekvenciális inputfájl, az abból utoljára kiolvasott elem és az olvasás státusza reprezentálja. Szekvenciális inputfájlok feldolgozása esetén ehhez a megállapításhoz igazíthatjuk a specifikációs jelöléseket. Ilyenkor az állapottérben magát a szekvenciális inputfájlt vesszük fel, és az e x (x a szekvenciális inputfájl) azt jelöli, hogy sorban egymás után ki akarjuk
200
8.4. Programozási tételek általánosítása
olvasni az x fájl (amely egy sorozat) elemeit. Ennél fogva a korábban bevezetett specifikációs jelölésekben szereplő e t’ (ahol t’ a felsoroló kiinduló állapota) szimbólumot szekvenciális inputfájl bejárásakor kicserélhetjük az e x’ (x’ a szekvenciális inputfájl kezdeti állapota) szimbólumra. Az e x jelölés természetesen nem azt jelenti, hogy az utoljára kiolvasott elemet tartalmazó változót a programban is e-nek kell elnevezni, de célszerű ezt tenni. összegzés:
s
f(e) e x'
számlálás:
c
1 e x' β (e)
maximum kiválasztás:
max,elem
max f(e) e x'
feltételes maximumkeresés: l,max,elem
max f(e) e x' β (e)
Láthattuk, hogy a kiválasztás és a lineáris keresés azelőtt is leállhat, hogy a felsorolás befejeződne, és ezért fontos eredménye ezen programozási tételeknek ez a be nem fejezett felsoroló is. Amennyiben az st,e,x:read műveletet használjuk a x szekvenciális inputfájl bejárására, akkor a „megkezdett” t felsorolót az st, e, x hármassal helyettesíthetjük a specifikációs jelölés baloldalán. lineáris keresés: l,elem,(st ,e,x) search β (e) e x'
kiválasztás:
elem,(st,e,x)
select β (elem)
elem x'
Az st és az e azonban redundáns információt hordoz. Kiválasztásnál az elem azonos az e-vel, az st pedig biztosan norm, hiszen ilyenkor garantáltan találunk keresett elemet. Lineáris keresésnél st=abnorm, ha a keresés sikertelen (azaz l értéke hamis); sikeres termináláskor (ha l igaz) az elem azonos az e-vel, az st pedig biztosan norm. Ezért megengedjük a fenti jelölés minden olyan egyszerűsítését, ami nem megy az egyértelműség rovására. Ilyen például az alábbi: lineáris keresés: l,elem,x search β (e) e x'
kiválasztás:
elem,x
select β (elem)
elem x'
201
8. Programozási tételek felsoroló objektumokra
b) Egy h halmaz felsorolóját maga a h halmaz reprezentálja. Ilyenkor a specifikációnál e t’ (ahol t’ a felsoroló kiinduló állapota) szimbólum helyett az e h’ (h’ a halmaz kezdeti állapota) szimbólumot írhatjuk. Jelentése: vegyük sorban egymás után a halmaz elemeit. c) Indexelhető gyűjtemények (vektor, mátrix, sorozat, stb.) esetén a felsorolót a gyűjtemény és az azon végigvezetett index (mátrixoknál indexpár) reprezentálják. Tulajdonképpen ilyenkor közvetlenül nem is a gyűjteményben tárolt értékeket, hanem azok indexeit soroljuk fel, hiszen egy indexhez bármikor hozzárendelhető az általa megjelölt érték. Ilyenkor például egy maximum kiválasztásnál az f függvény sohasem identitás, mert az f rendeli az indexhez (a felsorolt elemhez) a gyűjtemény megfelelő értékét. A specifikációkban szereplő elem ilyenkor egy indexet tartalmaz, ezért az alábbiakban ind-ként (mátrixok esetén dupla indexként: ind, jnd) jelenítjük meg. Ezen megfontolások miatt használhatjuk a korábbi fejezetek specifikációs jelöléseit, amelyből az is látható, hogy a korábbi intervallumos programozási tételek a felsorolós tételek speciális esetei. összegzés:
n
f(v[i] )
s= i m
számlálás:
n
c
1) i m β (v[i ] )
maximum kiválasztás:
n
max, ind = max f(v[i] ) i m
n
feltételes maximumkeresés:
l, max, ind = max f(v[i ] )
lineáris keresés:
l , ind
kiválasztás:
ind
i m β (v[i ] )
n
searchβ (v[i ] ) i m
select β (v[i] ) i m
Már többször felhívtuk a figyelmet arra, hogy a keresés és kiválasztás előbb leállhat, mint maga a felsorolás. Vektorok esetén a
202
8.4. Programozási tételek általánosítása
még fel nem dolgozott elemek az ind index után állnak, ezért azok külön jelölésére nincs szükség. d) Az n×m-es mátrixokra bevezetett specifikációs jelölések (standard felsorolás esetén) csak abban térnek el a vektorokétól, hogy indexpárokat tartalmaznak. n, m
összegzés:
f(a[i, j ] )
s= i 1, j 1 n, m
számlálás:
c
1 i 1, j 1 β (a[i , j ] )
maximum kiválasztás:
n, m
max, ind, jnd = max f(a[i, j ] ) i 1, j 1
feltételes maximumkeresés:
n, m
l, max, ind, jnd =
lineáris keresés:
l , ind, jnd
kiválasztás:
ind, jnd
max
f(a[i, j ] )
i 1, j 1 β (a[i , j ] )
n, m
search β (a[i, j ] )
i 1, j 1
m
select β (a[i, j ] )
i 1, j 1
203
8.5. Visszavezetés nevezetes felsorolókkal A most bevezetett tételek segítségével egyszerűvé válik az olyan feladatok megoldása, amelyek felsorolt értékek összegzését, megszámolását, azok közül maximum kiválasztását vagy adott tulajdonság keresését írják elő. Ha a feladat specifikációjában rámutatunk arra, hogy milyen felsorolóra vonatkozik a feladat (hogyan kell implementálni a First(), Next(), End() és Current() műveleteket) és milyen programozási tétel alapján kell a felsorolt értékeket feldolgozni (mi helyettesíti az adott programozási tételben szereplő függvényt, függvényeket), akkor visszavezetéssel könnyen megkaphatjuk a megoldást. Ráadásul, ha az alkalmazott felsorolás a 8.3. alfejezetben részletesen tárgyalt nevezetes felsorolók egyike, akkor nagyon egyszerű lesz a dolgunk. Most ez utóbbi esetre mutatunk néhány példát, amellyekkel azt kívánjuk illusztrálni, hogy a fentiekkel milyen erős eszközt kaptunk a módszeres programtervezés számára. (Azon feladatok megoldásával, amelyek megoldásához egyedi módon kell a felsorolót megtervezni, a következő fejezetben foglalkozunk.) Először nagyon egyszerű, a visszavezetés technikáját bemutató feladatokat oldunk meg. Az első két feladatban halmazok illetve szekvenciális inputfájl feldolgozásáról, és a maximum kiválasztás és kiválasztás tételeinek alkalmazásáról lesz szó. Utána egy „mátrixos” feladat megoldása következik. Végül az úgynevezett elemenkénti feldolgozásokra, az összegzés tételének speciális alkalmazásaira mutatunk példákat. Itt az összegzés eredménye is egy gyűjtemény, az összeadás művelete e gyűjteménybe történő új elem beírása. 8.1. Példa. Egy halmaz egész számokat tartalmaz. Keressük meg a halmaz maximális elemét! A feladat specifikációjának felírása nem jelent problémát. Az utófeltételből látszik, hogy itt egy halmaz elemeinek felsorolására épített maximum kiválasztásról van szó.
204
8.5. Visszavezetés nevezetes felsorolókkal
A = (h:set(ℤ), max:ℤ) Ef = ( h=h’ ) Uf = ( max = max e ) e h'
Ebben a specifikációban nem jelenik meg explicit módon a felsoroló, de átalakítható úgy, hogy annak állapotterében a halmaz helyett, a halmaz elemeit felsoroló objektum szerepeljen: A = (t:enor(ℤ), max:ℤ) Ef = ( t=t’ ) Uf = ( max = max e ) e t'
Ez a forma nem mond el többet a feladatról, mint az előző, de az általános maximum kiválasztás programozási tételére való visszavezetés most már formálisan végrehajtható: az általános specifikáció és az itt látható specifikáció néhány, jól azonosítható ponton tér csak el. A felsoroló egész számokat állít elő, a feldolgozást végző függvény pedig az egész számokon értelmezett identitás. E ~ ℤ H ~ ℤ f(e) ~ e Az identitás miatt a feldolgozásnak most csak egy eredménye (max) van, hiszen a megtalált maximális értékű elem és annak értéke egy és ugyanaz. Ennek megfelelően átalakítjuk az általános algoritmust: t.First() max:= t.Current() t.Next() t.End() t.Current()>max max:=t.Current()
SKIP
t.Next()
205
8. Programozási tételek felsoroló objektumokra
Ezek után foglalkozunk a felsoroló műveletekkel. A h halmaz felsorolása ismert (lásd 8.3. alfejezet), így a műveletek implementációja adott: First() ~ SKIP, End() ~ h= , Current() ~ mem(h), Next() ~ h:=h-{mem(h)}. Ezek alapján elkészíthető a feladatot megoldó program végső változata. Ebben egy segédváltozót vezettünk be annak érdekében, hogy ne kelljen a mem(h)-t ugyanarra a h-ra többször egymás után végrehajtani. A felsorolót a h halmaz reprezentálja. max:= mem(h) h:=h-{max} h e:= mem(h) e>max max:= e
SKIP
h:=h-{e} A gyakorlatban természetesen nem kell ennyire részletesen dokumentálni a visszavezetés lépéseit. A fenti gondolatmenetből elegendő a feladat eredeti specifikációját, a visszavezetés analógiáját (itt maximum kiválasztás: E ~ Z, H ~ Z és f(e) ~ e), a felsoroló műveleteit (itt elég a h halmaz nevezetes felsorolására hivatkozni, mert ebből következik, hogy a First() ~ SKIP, End() ~ h= , Current() ~ mem(h), Next() ~ h:=h-{mem(h)}), és végül az algoritmust megadni. 8.2. Példa. Keressük meg egy szekvenciális inputfájlban található szöveg első szavának kezdetét! Ez a feladat gyakori része a szöveg feldolgozási problémáknak. Vagy az első nem szóköz karaktert kell megtalálnunk, vagy ha ilyen nincs, akkor a fájl végét; és ez az összetett feltétel biztosan teljesül. A fájl elemeinek felsorolásához az st,e,x:read műveletet használjuk, ahol az st az olvasás státuszát, e a kiolvasott karaktert tartalmazza. Ha a fájl csak szóközökből áll, akkor a feldolgozás st=abnorm feltétellel álljon le. Ha leálláskor st=norm, akkor az e tartalmazza a keresett első nem
206
8.5. Visszavezetés nevezetes felsorolókkal
szóköz karaktert, és a még fel nem sorolt karakterek az f szekvenciális inputfájlban maradnak. Ezt tükrözi az alábbi specifikáció. A = (f:infile( ), st:Státusz, e: ) Ef = ( f=f’ ) Uf = ( e, f select ( st abnorm e ' ' ) ) e f'
A feladatot a kiválasztás programozási tételére vezetjük vissza az f szekvenciális inputfájl felsorolásával. Visszavezetés E ~ (e)
Felsorolás felsoroló
~
~ st=abnorm e ’’
f:infile( ), st:Státusz, e:
First()
~
st,e,f:read
Next()
~
st,e,f:read
Current()
~
e
End()
~
st = abnorm
A visszavezetéssel előállított megoldás: st,e,f:read st=norm
e=’ ’
st,e,f:read Kétdimenziós jellegénél fogva a mátrixokra értelmezett maximum kiválasztás és lineáris keresés is érdekes tanulságokkal jár. 8.3. Példa. Keressünk egy egész elemű mátrixban egy páros számot és adjuk meg annak indexeit!
207
8. Programozási tételek felsoroló objektumokra
A = (a:ℤ n×m, l: , ind:ℕ, jnd:ℕ) Ef = ( a=a’ ) Uf = ( a=a’
l , (ind , jnd )
n, m
search 2 a[i, j ] )
i 1,j 1
A feladat visszavezethető a lineáris keresés tételére a mátrix indexpárjainak sorfolytonos felsorolása mellett. Visszavezetés E ~ ℕ×ℕ
Felsorolás felsoroló
(i,j) ~ 2 a[i,j]
~ a:ℤ n×m, i, j:ℕ
First()
~ i, j:=1,1
Next()
~ ha j=m akkor i, j:=i+1,1 különben j:=j+1
Current( )
~ i, j
End()
~ i>n
A visszavezetéssel előállított megoldás: l, i, j:=hamis, 1, 1 l
i≤ n
l, ind, jnd := 2 a[i,j], i, j j<m j:=j+1
i, j:=i+1, 1
Ahogy azt korábban már jeleztük, ennek a megoldásnak megadható dupla számlálós ciklusos változata:
208
8.5. Visszavezetés nevezetes felsorolókkal
l, i:=hamis, 1 i≤ n
l
j:=1 l
j≤m
l, ind, jnd := 2 a[i,j], i, j j:=j+1 i:=i+1 Egy felsoroló típusú adat feldolgozásának gyakori esete az, amikor a kimenő adat is elemi értékek gyűjteménye (például halmaz, sorozat, vektor vagy szekvenciális outputfájl). Amikor a bemenő felsoroló adat aktuális elemét feldolgozzuk, a feldolgozás eredményeként kapott új elemmel vagy elemekkel kibővítjük a kimenő adatot (halmazhoz hozzáuniózunk, sorozathoz hozzáfűzünk, stb.). Mivel a kimenő adat megváltoztatása (az unió, hozzáfűzés, stb.) egy baloldali egységelemes asszociatív művelet, ezen feladatok megoldását az összegzés programozási tételére vezethetjük vissza. Ha egy ilyen feladatra még az is igaz, hogy a bemenő adat egy elemének feldolgozása nem függ más tényezőtől (nem függ a bemenő adat többi elemétől vagy a kimenő adattól) csak magától a feldolgozandó elemtől, azaz a bemenő adat feldolgozása annak felsorolására épül, akkor a feladatot elemenként feldolgozhatónak, a megoldását elemenkénti feldolgozásnak nevezzük. Létezik az elemenkénti feldolgozásnak egy ennél még szigorúbb értelmezése is, amely szerint az eddig felsorolt feltételeken túl annak is teljesülnie kell, hogy egy-egy elem feldolgozásának eredménye a kimenő adat többi elemétől se függjön, azaz az eredménnyel a kimenő adatot annak többi elemétől függetlenül lehessen bővíteni. Az elemenként feldolgozható feladatosztályhoz tartozik az adatfeldolgozás számos alapfeladata: a másolás, a kiválogatás, a szétválogatás, stb. Hangsúlyozzuk azonban, hogy ezek a feladatok mind az összegzés programozási tételére vezethetők vissza. Oldjunk meg először két „szigorúan” elemenként feldolgozható feladatot, amikor egy elem feldolgozásának eredménye nem függ attól, hogy a kimeneti gyűjtemény milyen elemeket tartalmaz éppen. Utána egy
209
8. Programozási tételek felsoroló objektumokra
olyan elemenkénti feldolgozást láthatunk, ahol ez a feltétel nem teljesül. Ezt követően egy olyan elemenkénti feldolgozásról lesz szó, ahol egy bemeneti gyűjteményből három kimeneti gyűjteményt kell egyszerre előállítani. 8.4. Példa. Másoljunk át egy karaktereket tartalmazó szekvenciális inputfájlt egy outputfájlba úgy, hogy minden karaktert megduplázunk! A = (x:infile( ), y:outfile( )) Ef = ( x=x’ ) e, e ) Uf = ( y e x'
Az utófeltételben használt művelet az összefűzés (konkatenáció) jele. Ez egy asszociatív művelet, amelynek az üres sorozat a nulla eleme. Ezért a feladat az összegzés programozási tételére vezethető vissza úgy, hogy a feldolgozás egy szekvenciális inputfájlra, annak nevezetes felsorolására épül. Speciálisan ez a feladat egy elemenkénti feldolgozás (értékek összefűzése), amely egy szekvenciális fájlból (felsoroláshoz használt művelet: st,e,x:read) olvassa azokat az elemeket, amelyekből összeállított értékeket egy másik (szekvenciális output) fájlba ír: E
~
+,0
~
,<>
f(e)
~
<e,e>
s
~
y
A szekvenciális outputfájlhoz a write művelettel lehet hozzáfűzni egy újabb sorozatot, azaz egy y:=y <e,e> értékadást az y:write(<e,e>) implementál.
210
8.5. Visszavezetés nevezetes felsorolókkal
y:=<> st,e,x:read st = norm y:write(<e, e>) st,e,x:read Látjuk, hogy az elemenkénti feldolgozható feladatok egy bemenő adatelemhez nem feltétlenül csak egy kimenő adatelemet állítanak elő, ugyanakkor nagyon gyakoriak azok a feladatok (ilyen a következő), amelyek a bemenő adat egy eleméhez vagy egy új elemet, vagy semmit sem rendelnek. 8.5. Példa. Válogassuk ki egy egész számokat tartalmazó halmazból a páros számokat, és helyezzük el őket egy másik halmazba! A feladat egy halmaz felsorolásán értelmezett „összegzés” (valójában uniózás, amelynek null-eleme az üres halmaz), ahol a feldolgozás f:ℤ 2ℤ függvénye egy páros számhoz a számot tartalmazó egy elemű halmazt, páratlan számhoz az üres halmazt rendeli. A = (x:set(ℤ), y:set(ℤ)) Ef = ( x=x’ ) Uf = ( y {e} ) e x' e páros
Az ilyen esetszétválasztós f függvényre épülő összegzést lehetne feltételes összegzésnek hívni. Ennek programjában megjelenő y:=y f(e) értékadást egy elágazás valósítja meg, amelyben y:=y {e} értékadás vagy a SKIP program szerepel.24 24
Megjegyezzük, hogy itt az y:=y {e} értékadás implementációja itt egyszerűbb, mint általában. Ugyanis biztosak lehetünk abban, hogy az e elem még nincs az y halmazban, ezért ezt nem is kell külön vizsgálni. Az elem-unió műveletének tehát ugyanúgy az egyszerűsített változatával dolgozhatunk itt, mint az x:=x–{e} elemkivonás esetében, ahol meg azt nem kell ellenőrizni, hogy az e elem tényleg benne van-e az x halmazban.
211
8. Programozási tételek felsoroló objektumokra
A megoldás tehát egy elemenkénti feldolgozás (egy úgynevezett kiválogatás) halmazból halmazba: ℤ
E
~
+,0
~
f(e)
~
ha e páros akkor {e} különben
s
~
y
,
y:= x e:=mem(h) e páros y:=y {e}
SKIP
x:=x–{e} 8.6. Példa. Másoljuk át egy egész számokat tartalmazó vektorból egy halmazba az elemek 7-tel vett osztási maradékait! A feladat egy vektor szokásos felsorolásán értelmezett „összegzés”, ahol a feldolgozás függvénye az adott elemnek 7-tel vett osztási maradékát állítja elő, amelyet az eredmény halmazhoz kell uniózni. Ennek a lépésnek a hatása nem független az eredmény halmaz tartalmától, hiszen ha az újonnan hozzáadandó maradék már szerepel a halmazban, akkor a halmaz nem fog megváltozni. A = (x:ℤn, y:set(ℤ)) Ef = ( x=x’ ) n
Uf = ( y
{xi mod 7} )
i 1
Ez egy elemenkénti feldolgozás (egy uniózás) vektorból halmazba:
212
8.5. Visszavezetés nevezetes felsorolókkal
ℕ
E
~
+,0
~
f(i)
~
{xi mod 7}
s
~
y
,
y:= i = 1 .. n y:=y {xi mod 7} (Érdekes, hogy ha ugyanez a feladat az eredményt egy sorozatba fűzve kérné és megengednénk azt, hogy ugyanazt a maradékot többször is betegyük oda – erre a sorozat lehetőséget ad –, akkor már „szigorúan” elemenként feldolgozható feladatról beszélhetünk.) A következő példa egy „szigorúan” elemenként feldolgozható feladat, amelynek az a sajátossága, hogy ugyanazon a gyűjteményen több, egymástól független feldolgozást kell elvégezni. Az ilyen feladat teljesen önálló részfeladatokra bontható, majd a részmegoldásokat a közös gyűjtemény bejárására szervezett egyetlen ciklusba lehet összevonni. Az így kapott megoldást szétválogatásnak is szokták nevezni. 8.7. Példa. Válogassunk szét egy szekvenciális inputfájlban rögzített bűnügyi nyilvántartásból egy outputfájlba, egy halmazba és egy vektorba a gyanúsítottakat aszerint, hogy az illető 180 cm-nél magasabb-e és barna hajú, vagy fekete hajú és 60 kg-nál könnyebb, vagy fekete hajú és nincs alibije! Az állapottérnek egy szekvenciális inputfájl a bemenő adata, a kimenő adatai pedig egy szekvenciális outputfájl, egy halmaz és egy vektor. Az állapottér a vektorról csak annyit mond, hogy az egy kellően hosszú véges sorozat, és majd az utófeltétel fogja a vektor első n elemét jellemezni. A = (x:infile(Ember), y:outfile(Ember), z:set(Ember), v: Ember*, n:ℤ) Ember=rec(név: *, mag:ℕ, súly:ℕ, haj: *, alibi: )
213
8. Programozási tételek felsoroló objektumokra
Ef = ( x=x’ ) Uf = ( y
e x' e.mag 180 e.haj 'barna'
v[1..n]
{e}
z
e
e x' e.súly 60 e.haj ' fekete'
e x' e.haj ' fekete' e.alibi
e
)
A specifikációban jól elkülönül a feladat három önállóan is megoldható része. Mindhárom egy esetszétválasztásos függvény összegzésére épül, amelyhez ugyanazon x szekvenciális inputfájl st,e,x:read segítségével soroljuk fel az elemeket. Itt három elemenkénti feldolgozás (három kiválogatás) szerepel, amelyek ugyanazon szekvenciális fájlból (st,e,x:read) egy szekvenciális fájlba, egy halmazba és egy sorozatba írnak: E
~
Ember
Ember
Ember
H +,0 s f(e)
~ ~ ~ ~
Ember*
Ember*
,<> y <e> ha
2Ember , z {e} ha
e.mag>180 e.haj = ”barna”
e.súly<60 e.haj = ”fekete”
e.haj = ”fekete” e.alibi
214
,<> v <e> ha
y:=<> st,e,x:read
z:= st,e,x:read
n:=0 st,e,x:read
st = norm
st = norm
st = norm
e.mag>180 e.haj=’barna’
e.súly<60 e.haj=’fekete’
e.haj=’fekete’ e.alibi
y :write(e) SKIP
z:=z {e} SKIP
n:=n+1 SKIP v[n]:=e
st,e,x:read
st,e,x:read
st,e,x:read
8.5. Visszavezetés nevezetes felsorolókkal
A megoldás csak akkor hatékony, ha a szekvenciális inputfájl elemeit egyszer járjuk be. A három különálló megoldást egyetlen ciklussá vonhatjuk össze (lásd 6.3. alfejezetbeli programátalakításokat), és a szekvenciális inputfájl minden elemét annyi szempont szerint dolgozzuk fel, ahány kimenő adat van. y:=<>; z:= ; n:=0 st,e,x:read st= norm e.mag>180
e.haj=’barna’
y:write(e)
SKIP
e.súly<60
e.haj=’fekete’
z:=z {dx} e.haj=’fekete’ n:=n+1; v[n]:=e
SKIP e.alibi SKIP
st,e,x:read
215
8. Programozási tételek felsoroló objektumokra
8.6. Feladatok Adott az egész számokat tartalmazó x vektor. Válogassuk ki az y sorozatba a vektor pozitív elemeit! 8.2. Számoljuk meg egy halmazbeli szavak között, hogy hány ’a’ betűvel kezdődő van! 8.3. Egy szekvenciális inputfájl egész számokat tartalmaz. Keressük meg az első nem-negatív elemét! 8.4. Egy szekvenciális fájlban egy bank számlatulajdonosait tartjuk nyilván (azonosító, összeg) párok formájában. Adjuk meg annak az azonosítóját, akinek nincs tartozása, de a legkisebb a számlaegyenlege! 8.5. Egy szekvenciális fájlban minden átutalási betétszámla tulajdonosáról nyilvántartjuk a nevét, címét, azonosítóját, és számlaegyenlegét (negatív, ha tartozik; pozitív, ha követel). Készítsünk két listát: írjuk ki egy output fájlba a hátralékkal rendelkezők, egy másikba a túlfizetéssel rendelkezők nevét! 8.6. Egy szekvenciális inputfájlban egyes kaktuszfajtákról ismerünk néhány adatot: név, őshaza, virágszín, méret. Válogassuk ki egy szekvenciális outputfájlba a mexikói, egy másikba a piros virágú, egy harmadikba a mexikói és piros virágú kaktuszokat! 8.7. Adott egy egész számokat tartalmazó szekvenciális inputfájl. Ha a fájl tartalmaz pozitív elemet, akkor keressük meg a fájl legnagyobb, különben a legkisebb elemét! 8.8. Egy szekvenciális inputfájl (megengedett művelet: read) egy vállalat dolgozóinak adatait tartalmazza: név, munka típus (fizikai, adminisztratív, vezető), havi bér, család nagysága, túlóraszám. Válasszuk ki azoknak a fizikai dolgozóknak a nevét, akiknél a túlóraszám meghaladja a 20-at, és családtagok száma nagyobb 4-nél; adjuk meg a fizikai, adminisztratív, vezető beosztású dolgozók átlagos havi bérét! 8.9. Adott két vektorban egy angol-latin szótár: az egyik vektor iedik eleme tartalmazza a másik vektor i-edik elemének jelentését. Válogassuk ki egy vektorba azokat az angol szavakat, amelyek szóalakja megegyezik a latin megfelelőjével. 8.10. Alakítsunk át egy magyar nyelvű szöveget távirati stílusúra! 8.1.
216
9. Visszavezetés egyedi felsorolókkal Ebben a fejezetben az általános programozási tételeket olyan feladatok megoldására alkalmazzuk, ahol nem lehet nevezetes felsorolókat használni, a First(), Next(), End() és Current() műveletek előállítása egyedi tervezést igényel. Gyakoribbak azonban az olyan feladatok, ahol egy nevezetes gyűjtemény elemeit kell bejárni, de nem a megszokott módon, ezért egyáltalán nem, vagy csak módosítva használhatjuk az azokon megszokott nevezetes felsorolókat. Ez utóbbira példa az, amikor egy vektor elemeit nem egyesével kell bejárni, ezért a Next() műveleten változtatni kell; vagy nem elejétől a vége felé, hanem fordítva, és ehhez a First(), Next(), és End() műveleteket kell újragondolni. Ilyen egy nevezetes gyűjteménynek az úgynevezett feltétel fennállásáig tartó feldolgozása (9.2. alfejezet) is, amikor az elemeket csak addig kell bejárni, amíg azokra egy adott feltétel teljesül. Ehhez az End() művelet megfelelő felülírása szükséges. A 9.1. alfejezet egy kétdimenziós gyűjteménynek (egy mátrixnak) bizonyos elemeit sorolja csak fel. Érdekesek azok a feladatok (9.3. alfejezet), ahol egy gyűjtemény elemeit csoportosítva, csoportokra bontva kell feldolgozni, és egy felsorolónak ezeket a csoportokat kell valamilyen formában bejárni. Egyedi felsorolóra van szükség akkor is, ha egy rekurzív függvény által előállított értékekre van szükségünk (9.5. alfejezet), vagy ha egyidejűleg több gyűjtemény elemeit kell szimultán módon bejárni (9.4. alfejezet). A 9.6. alfejezet olyan egyedi felsorolóra mutat példát, amelynek hátterében egyáltalán nincs gyűjtemény.
9. Visszavezetés egyedi felsorolókkal
9.1. Egyedi felsoroló 9.1. Példa. Adott a síkon n darab pont. Állapítsuk meg, melyik két pont esik legtávolabb egymástól! Ezzel a feladattal már találkoztunk a 6. fejezetben. Specifikáljuk újra a feladatot! A síkbeli pontokat egy derékszögű koordináta rendszerbeli koordináta párokkal adjuk meg; külön-külön tömbökben az x és az y koordinátákat. Így az i-edik pont koordinátái az x[i] és y[i] lesznek. A feladat központi fogalma az i-dik és j-dik pont távolsága, amelyet táv(i,j)-vel fogunk jelölni. Ezek a távolságok gondolatban egy n×n-es szimmetrikus mátrixba rendezhetők, és a feladat tulajdonképpen ennek a mátrixnak az alsóháromszög részében található értékek közötti maximum kiválasztás. A = (x:ℤn, y:ℤn, ind:ℕ, jnd:ℕ) Ef = ( x=x’ y=y’ n≥2 ) n,i 1
Uf = (Ef
max, (ind, jnd) = max táv(i,j) )
ahol táv(i, j )
i 2, j 1
( x[i] x[ j ]) 2
( y[i] y[ j ]) 2
A specifikációból látható, hogy most a képzeletbeli mátrix indexpárjainak felsorolását nem a szokásos módon kell megtenni, hiszen csak a mátrix alsóháromszög részét kell bejárni: (2,1) (3,1) (3,2), (4,1), … , (n,1), (n,2), … , (n,n-1). Formálisan újraspecifikálhatnánk a feladatot úgy, hogy a bemenő adatok helyett egy természetes számok párjait bejáró felsorolót veszünk fel az állapottérbe. A = (t:enor(ℕ×ℕ), ind:ℕ, jnd:ℕ) Ef = ( t=t' ) Uf = ( max, (ind, jnd) = max táv(i,j) ) (i,j) t'
A felsoroló First() művelete a (2,1) indexű elemre kell, hogy ráálljon, a Next() jn. Ezt a bejárást a 8.3. alfejezetben látottakhoz hasonlóan egy dupla számlálós ciklussal is leírhatjuk, amelyet a felsoroló típusra megfogalmazott maximum kiválasztás algoritmusával kell ötvöznünk.
218
9.1. Egyedi felsoroló
max, ind, jnd,:= táv(2,1), 2, 1 i = 3 .. n j = 1 .. i-1 táv(i,j)>max max, ind, jnd,:= táv(i,j), i, j,
SKIP
9.2. Feltétel fennállásáig tartó felsorolás Sokszor egy felsorolás nem úgy ér véget, hogy a felsorolandó elemek elfogynak, hanem akkor, amikor a felsorolás során olyan elemhez érünk, amelyre már nem teljesül egy külön megadott :E feltétel. Amennyiben a feltétel a felsorolás egyik elemére sem lesz hamis, akkor a feldolgozás a felsorolás végén leáll, azaz a felsoroló End() művelete nem kizárólag a feltétel lesz, hanem a nevezetes felsoroló eredeti End() műveletének és a feltételnek vagy kapcsolata. Programozási tételeinket mind meg lehet fogalmazni ilyen feltétel fennállásáig tartó változatúra a kiválasztás kivételével (ennél ugyanis ennek nincs sok értelme). A feltétel fennállásáig tartó feldolgozásokra is érvényes az, amit korábban a lineáris keresésnél vagy a kiválasztásnál elmondtunk, nevezetesen, hogy korábban is leállhat a feldolgozás, mint hogy a felsorolás összes elemét bejárnánk. Ezért az abbahagyott felsoroló is eredménye lesz az ilyen feltétel fennállásáig tartó feldolgozásoknak, hogy ha kell, a még fel nem sorolt (fel nem dolgozott) elemeket alá lehessen vetni majd további feldolgozásnak is. A nevezetes felsorolóknak feltétel fennállásáig tartó feldolgozássá alakításakor a specifikációjában a felsorolás befejezését definiáló :E feltételt az alábbi módon fogjuk jelölni. (e)
(e)
f ( e) , c , t
Összegzés illetve számlálás esetén: s, t
e t' (e)
e t'
maximum kiválasztás esetén: max, elem , t
1,
(e)
max f (e) , e t'
219
9. Visszavezetés egyedi felsorolókkal
(e)
lineáris keresés esetén: l , elem , t
search (e) . e t'
9.2. Példa. Adjuk meg egy szekvenciális inputfájl elején álló pozitív számok között a párosak számát! A = ( f:infile(ℤ), e:ℤ, st:Státusz, db:ℤ ) Ef = ( f=f’ ) e 0
1)
Uf = ( db,( st, e, f) = e f' 2e
A feladatot az f szekvenciális inputfájl felsorolására épített számlálásra vezetjük vissza, amely azonban korábban is leállhat, ezért a End() műveletet a megszokott st=norm helyett az st=norm e>0 kifejezés helyettesíti. db:=0; st, e, f : read st=norm
e>0
2 e db:=db+1
SKIP
st, e, f : read 9.3. Példa. Egy szekvenciális inputfájlban egy banknál számlát nyitott ügyfelek e havi kivét/betét forgalmát (tranzakcióit) tároljuk. Minden tranzakciónál nyilvántartjuk az ügyfél azonosítóját, a tranzakció dátumát és az összegét, ami egy előjeles egész szám (negatív a kivét, pozitív a betét). A tranzakciók a szekvenciális fájlban ügyfélazonosító szerint rendezetten helyezkednek el. Keressük meg az első ügyfél legnagyobb összegű tranzakcióját. A = (f:infile(Tranzakció), df:Tranzakció, sf:Státusz, max:Tranzakció) Tranzakció = rec(azon:ℕ, dátum:ℕ, össz:ℤ)
220
9.2. Feltétel fennállásáig tartó felsorolás
Ef = ( f=f’
f >0
f azon szerint rendezett ) ' e.azon f 1.azon e.össz ) Uf = ( max, maxelem, ( st , e, f ) max e f'
Ezt visszavezethetjük a maximum kiválasztás tételére. Az eredmények közül nincs szükség külön a maximális tranzakciós összeg értékére, hiszen azt a maximális elem (a rekord) tartalmazza. st, maxelem, f : read st, e, f : read st=norm
e.azon=maxelem.azon
e.össz > maxelem.össz maxelem:= e
SKIP
st, e, f : read
221
9. Visszavezetés egyedi felsorolókkal
9.3. Csoportok felsorolása 9.4. Példa. Több egymás utáni napon feljegyeztük a napi átlaghőmérsékleteket, és azokat egy szekvenciális inputfájlban rögzítettük. Volt-e olyan nap, amikor a megelőző naphoz képest csökkent az átlaghőmérséklet? A = (f:infile(ℝ), l: ) Ef = ( f=f’ ) f'
Uf = ( l
search( f 'i f 'i 1 0) ) i 2
A feladat nem oldható meg a szekvenciális inputfájl nevezetes bejárásával, hiszen az nem ad lehetőséget a szekvenciális inputfájl egymás után következő két elemének egyidejű vizsgálatára. Helyette a szekvenciális inputfájlra támaszkodó, de attól elvonatkoztatott, absztrakt felsorolóra van szükség, amely a szekvenciális inputfájl szomszédos elempárjait sorolja fel. Újrafogalmazzuk tehát a feladatot egy olyan állapottéren, ahol adottnak vesszük az előbb kitalált felsorolót. A = (t:enor(rec(tegnap:ℝ, ma:ℝ)), l: ) Ef = ( t=t’ ) Uf = ( l search(e.ma e.tegnap 0) ) e t'
Az új specifikáció célja az, hogy minél könnyebben lehessen a feladat megoldását a lineáris keresésre visszavezetni. Természetesen a bevezetett felsoroló típusát egyedi módon kell megvalósítani: reprezentálni kell az eredeti állapottér komponenseire támaszkodva és implementálni kell a bejárás műveleteit. Ezzel lényegében részfeladatokra bontottuk az eredeti problémát. Részfeladat az új állapottéren megfogalmazott lineáris keresés, és részfeladatok a First(), Next(), Current(), End() implementációi is. Az újrafogalmazott feladat visszavezethető az általános lineáris keresésre, amelyből – mivel ezt speciálisan eldöntésre használjuk – az elem:=t.Current() értékadást elhagyjuk. l := hamis; t.First()
222
9.3. Csoportok felsorolása
l
t.End()
l := t.Current().ma- t.Current().tegnap < 0 t.Next() Implementáljuk az absztrakt felsoroló műveleteit. A First() művelet feladata az első felsorolni kívánt elemre, esetünkben az első két hőmérsékletre, egy tegnap-ma számpárra (ezt nem rekordként, hanem két valós számként ábrázoljuk) való ráállás. Ehhez az f szekvenciális inputfájlból az első hőmérsékletet a tegnap változóba, a másodikat a ma változóba kell beolvasnunk. (A két olvasás akkor is végrehajtható, ha a fájl nem tartalmaz két értéket.) A Next() feladata a soron következő hőmérséklet-párra állni. Ehhez szükség van a korábbi számpár mai hőmérsékletére (a tegnapi mára), amiből az aktuális számpár tegnapi hőmérséklete (a mai tegnap) lesz, tehát tegnap:=ma, valamint be kell olvasni f fájlból a ma változóba az aktuális mai hőmérsékletet is. A Current() a tegnap-ma számpárt adja vissza. Az End() addig hamis, amíg sikerült újabb számpár előállításához újabb értéket olvasni az f fájlból, azaz amíg sf=norm feltéve, hogy a szekvenciális inputfájlból történő olvasás státuszát az sf tartalmazza. A fentiekből az is kiderül, hogy az absztrakt felsorolót a tegnap-ma számpár, az f szekvenciális inputfájl, és az utolsó olvasás sf státusza reprezentálja. Behelyettesítve az algoritmusba First(), Next(), Current(), End() implementációit: sf, tegnap, f : read sf, ma, f : read l:=hamis l
sf=norm
l:= ma-tegnap< 0 tegnap:=ma sf, ma, f : read
223
9. Visszavezetés egyedi felsorolókkal
9.5. Példa. Egy szekvenciális inputfájlban egy banknál számlát nyitott ügyfelek e havi kivét/betét forgalmát (tranzakcióit) tároljuk. Minden tranzakciónál nyilvántartjuk az ügyfél számlaszámát, a tranzakció dátumát és az összegét, ami egy előjeles egész szám (negatív a kivét, pozitív a betét). A tranzakciók a szekvenciális fájlban számlaszám szerint rendezetten helyezkednek el. Gyűjtsük ki azon számlaszámokat és az ahhoz tartozó tranzakcióknak az egyenlegét, ahol ez az egyenleg kisebb –100000 Ft-nál! A = (x:infile(Tranzakció), v:outfile(Egyenleg)) Tranzakció = rec(sz: *, dátum:ℕ, össz:ℤ) Egyenleg = rec(sz: *, össz:ℤ) Ef = ( x=x’ az x sz szerint rendezett ) Uf = (?) Az utófeltételt csak nagyon körülményesen lehetne leírni, mert a szekvenciális inputfájl nevezetes bejárása nem illeszkedik a feladathoz. Nekünk nem a tranzakciókat, hanem egy számla összes tranzakcióinak eredőjét kell egy lépésben feldolgozni. Olyan felsorolóra van szükség, amelyik egy számlaszám mellé ezt az eredőt meg tudja adni, azaz amelyik az azonos számlaszámú tranzakciók csoportját tudja egy lépésben beolvasni. Egy ilyen kitalált (absztrakt) felsorolóval a specifikáció megfogalmazása jóval egyszerűbb. A = (t:enor(Egyenleg), v:outfile(Egyenleg)) Ef = ( t=t’ ) e ) Uf = ( v e.össz
e t' 100000
Ez a feladat visszavezethető az összegzés programozási tételére, ahol az f:Egyenleg Egyenleg* típusú, és ha e.össz<-100000 (ahol e egy egyenleg), akkor f(e) megegyezik az <e> egyelemű sorozattal, különben f(e) az üres sorozat. t.First() t.End() t.Current().össz<-100000 v:write(t.Current()) t.Next()
224
SKIP
9.3. Csoportok felsorolása
Mind a First(), mind a Next() művelet a soron következő (aktuális) számlaszám tranzakcióit összegzi. Mivel az x szekvenciális inputfájl számlaszám szerint rendezett, így az ugyanolyan számlaszámú tranzakciók közvetlenül egymás után helyezkednek el, amelyekből az x fájl nevezetes bejárására épülő feltétel fennállásáig tartó összegzéssel lehet előállítani az aktuális számlaszámhoz tartozó egyenleget (egyl). Meg kell mondanunk azt is, hogy van-e egyáltalán újabb feldolgozandó tranzakció-csoport (vége). Az absztrakt felsorolót egyrészt az egyl:Egyenleg és vége: adatokkal, másrészt az x szekvenciális inputfájllal és annak végigolvasásához szükséges sx:Státusz és dx:Tranzakció adatokkal reprezentáljuk. Az aktuális számlaszám tranzakcióinak egyenlegét (egyl) a Current() művelettel kérdezhetjük le, az egyenlegek felsorolásának végét a vége változó jelzi: ezt adja vissza az End(). A Next() művelet végrehajtása előtt feltételezzük, hogy az x szekvenciális fájlt már korábban előreolvastuk, és ha van még feldolgozandó számlaszám (azaz sx=norm), akkor az ahhoz tartozó első tranzakció a dx-ben található. A vége értéke az sx kezdeti értékétől függ (vége = (sx=abnorm)). Ha a vége még hamis, akkor az adott számlaszámhoz tartozó összesített egyenleget úgy kell előállítani, hogy a már megkezdett x szekvenciális inputfájl elemeit kell tovább sorolni addig, amíg a tranzakciók számlaszáma nem változik. Hangsúlyozzuk, hogy ezen felsorolás első eleme kezdetben a dx-ben van, amit ugyancsak figyelembe kell venni az összegzésben. A felsorolás ezen sajátosságát (azaz hogy nem igényel előre olvasást) a dx (sx’,dx’, x’) szimbólummal fogjuk jelölni (ahol x’ a már előreolvasott fájl, sx’ az előreolvasás státusza, sikeres előreolvasás esetén dx’ a korábban kiolvasott elem). Amennyiben tudjuk, hogy sx’=norm, akkor a bejárás jelölésére a dx (dx’, x’) szimbólumot is használhatjuk, ami szemléletesen azt fejezi ki, hogy a gondolatban összefűzött dx’ és x’ elemeit kell felsorolni. A Next() művelet – amennyiben az előreolvasás sikeres volt – egy feltételig tartó összegzés lesz, hiszen le kell állnia akkor is, ha olyan tranzakciót olvasunk, amelyik azonosítója eltér az aktuális azonosítótól. ANext =(x:infile(Tranzakció), dx:Tranzakció, sx:Státusz, vége: , egyl:Egyenleg)
225
9. Visszavezetés egyedi felsorolókkal
EfNext = ( x = x’
dx = dx’ sx = sx’ x azon szerint rendezett ) UfNext = ( sx’=abnorm vége=igaz ( sx’=norm ( vége=hamis egyl.azon= dx’.azon dx.azon egyl.azon
egyl.össz , ( sx, dx, x)
dx.össz ) ) dx ( dx', x' )
t.Next() sx=norm vége := hamis egyl.azon, egyl.össz:=dx.azon, 0 sx=norm
vége := igaz
dx.azon=egyl.azon
egyl.össz := egyl.össz+dx.össz sx, dx, x: read A First() művelet csak egy előreolvasással „több”, mint Next(), hiszen itt kell elkezdeni az x fájlnak a felsorolását. Az első olvasás eredményére a specifikáció az sx’’, dx’’, x’’ értékekkel hivatkozik. AFirst =(x:infile(Tranzakció), dx:Tranzakció, sx:Státusz, vége: , egyl:Egyenleg) First Ef = ( x = x’ x azon szerint rendezett ) First Uf = ( sx’’, dx’’, x’’=read(x’) sx’’=abnorm vége=igaz sx’’=norm ( vége=hamis egyl.azon = dx’’.azon dx.azon egyl.azon
egyl.össz , ( sx, dx, x)
dx.össz ) ) dx ( dx' ', x' ' )
t.First() sx, dx, x:read t.Next()
226
9.3. Csoportok felsorolása
Jól látszik, hogy az absztrakt felsorolót az sx, a dx, az x, az egyl és a vége adatok együttesen reprezentálják. 9.6. Példa. Egy repülőgéppel elrepültünk a Csendes Óceán szigetvilágának egy része felett, és meghatározott távolságonként megmértük a felszín tengerszint feletti magasságát. Ott, ahol óceán felszínét észleltük, ez a magasság nulla, a szigeteknél pozitív. A mérést víz felett kezdtük és víz felett fejeztük be. Az így kapott nem-negatív valós értékeket egy n hosszúságú x vektorban tároljuk. Tegyük fel, hogy van legalább egy sziget a mérési adatokban. Adjuk meg a mérések alapján, hogy mekkora a leghosszabb sziget, és a repülés hányadik mérésénél kezdődik ezek közül az egyik. A feladat állapottere az alábbi: A = (x:ℝn, max:ℕ, kezd:ℕ) Most is – akárcsak e feladat korábbi megoldásában (6.9. példa) – maximum kiválasztást fogunk használni, de nem az állapottérbeli tömb indextartományát járjuk be közvetlenül, hanem a szigetek hosszait. Ehhez egy olyan felsorolót készítünk, amelyik a szigetek hosszait tudja sorban egymás után megadni azok tömbbeli kezdőindexével együtt. A feladat specifikációját ezért nem az eredeti állapottéren, hanem a kitalált felsorolóra írjuk fel, amely elrejti az eredeti megfogalmazásban szereplő x tömböt. A = (t:enor(Sziget), max:ℕ, kezd:ℕ) Sziget = rec(hossz:ℕ, eleje:ℕ) Ef = ( t=t’ |t|>0 ) Uf = ( max, elem = max akt.hossz kezd = elem.eleje ) akt t'
A feladatot maximum kiválasztás programozási tételére vezetjük vissza, ahol f:Sziget ℕ és f(akt)=akt.hossz. Mind a First(), mind a Next() műveletet el lehet készíteni úgy, hogy a tömb egy adott pozíciójáról indulva (kezdetben ez az 1) keressék meg a következő sziget elejét (az első nem-nulla értéket), majd annak a végét (az első nullát), ebből számolják ki a sziget hosszát és kezdőindexét. Ehhez a felsoroló reprezentálásánál szükségünk van az eredeti állapottér 1-től n-ig indexelt x tömbjére, és annak indextartományát befutó i indexre. (A háttérben tehát megjelenik az
227
9. Visszavezetés egyedi felsorolókkal
egydimenziós tömb, azaz a vektor klasszikus felsorolója.) A First() és a Next() művelet közti különbség csak annyi, hogy az i index kezdőértékét a Next() művelet megkapja, míg a First() kezdetben 1-re állítja. A Current() művelet a legutoljára talált sziget adatait adja meg, ezért a felsoroló reprezentációjában egy akt_sziget nevű Sziget típusú adatot is rögzíteni kell. Az End() azután ad vissza igaz értéket, ha egy újabb sziget keresésénél i>n bekövetkezik. Ezt a tényt egy nincs_több_sziget logikai változó fogja jelölni: ha találtunk újabb szigetet, akkor az értéke hamis, ha nem, akkor igaz. Összességében látszik, hogy a szigetek hosszainak felsorolóját a négy adat reprezentálja: az x tömb, az i index, a nincs_több_sziget és az akt_sziget. A Current() tehát az akt_sziget értékét adja vissza, az End() a nincs_több_sziget értékét. A Next() művelet bemenő adatai az x tömb és annak i indexe, kimenő adatai az akt_sziget , nincs_több_sziget és az i index. A soron következő sziget elejének keresését sajátos módon nem lineáris kereséssel, hanem kiválasztással végezzük el úgy, hogy keressük a sziget elejét vagy a tömb végét (ez az összetett feltétel biztosan teljesül). Ha nem találunk szigetet (i>n), akkor a nincs_több_sziget legyen igaz, különben hamis. Ha találunk szigetet, azaz az i változó a tömb egy nem-nulla elemére mutat, akkor az i ezen értékétől (ezt jelöli a specifikáció i1-gyel) kezdődően kell megkeresni a sziget végét. Ez is biztos létezik (vagy egy nulla érték vagy a tömb vége határolja), ezért itt is kiválasztást lehet alkalmazni. Felhívjuk a figyelmet arra, hogy egyik kiválasztás előtt sem kell az i-nek kezdőértéket adni, hiszen az rendelkezik a megfelelő értékkel: kezdetben a bemenetként kapott i’-vel, közben az i1-gyel. ANext = (x:ℕn, i:ℕ, nincs_több_sziget: , akt_sziget:Sziget) EfNext = ( x = x’ i=i’ ) UfNext = ( x = x’ i1 = select (i n x[i] 0) i i'
nincs_több_sziget = i1>n nincs_több_sziget ( akt_sziget.eleje = i1 select ( x[i ] 0 i n) i= i akt_sziget.eleje
akt_sziget.hossz=i–akt_sziget.eleje ) ) t.Next()
228
9.3. Csoportok felsorolása
i n
x[i] = 0 i:=i+1
nincs_több_sziget:= i >n nincs_több_sziget akt_sziget.eleje:= i i n
x[i]
0
SKIP
i:=i+1 akt_sziget.hossz:= i – akt_sziget.eleje A First() művelet a Next() művelet specializálása. Egyrészt itt i nem bemenő adat, ezért az első kiválasztás i=1-ről indul, másrészt mivel egy sziget biztosan van, a nincs_több_sziget értéke biztos hamis lesz. Nem bajlódunk azonban a First() egyedi implementálásával, hanem visszavezetjük a Next()-re. t.First() i:=1 t.Next() A főprogram pedig a már korábban körvonalazott maximum kiválasztás lesz, amelyben a maximális elem (elem) helyett elég csak annak eleje mezőjét a kezd eredmény változóban tárolni, a hossz mezőjének értéke pedig úgyis szerepel a max változóban. t.First() max, kezd:= t.Current().eleje
t.Current().hossz,
t.Next() t.End()
229
9. Visszavezetés egyedi felsorolókkal
t.Current().hossz>max max, kezd := t.Current().hossz, t.Current().eleje
SKIP
t.Next() 9.7. Példa. Egy karakterekből álló szekvenciális inputfájl egy szöveget tartalmaz. Számoljuk meg, hogy hány ’w’ betűt tartalmazó szó található a szövegben! (Egy szó olyan szóköz karaktert nem tartalmazó karakterlánc, amelyet egy vagy több szóköz, a fájl eleje vagy vége határol.) A feladat egy számlálás, de nem a szekvenciális inputfájl karakterei felett, hanem a szekvenciális inputfájlban található szavak felett. A számláláshoz egy olyan felsorolóra van szükség, amely annyi darab logikai értéket sorol fel, ahány szó szerepel a fájlban, és az i. logikai érték akkor igaz, ha az i. szó tartalmaz ’w’ betűt. A = (t:enor( ), db:ℕ) Ef = ( t=t’ ) Uf = ( db 1) e t' e
db:=0; t.First() t.End() t.Current() db:=db+1
SKIP
t.Next() Valósítsuk meg az absztrakt felsorolót kétféleképpen! Mindkét esetben a felsorolót az f:infile( ), annak felsorolásához szükséges df: és sf:Státusz, az aktuális szót minősítő logikai érték (van_w: ), amely megmutatja, hogy van-e a szóban ’w’ betű és a szavak felsorolásának végét jelző logikai érték (nincs_szó: ) reprezentálja. A Current() a szó értékelését adja vissza, az End() pedig azt, hogy elértük-e a fájl végét a soron következő szó elejének keresése során, azaz hogy nincsen több szó.
230
9.3. Csoportok felsorolása
Az első változatban a First() és Next() műveletek azonosak, hiszen mindkettőnek a soron következő szót kell kiértékelnie az aktuális szekvenciális inputfájlban. (Ez a First() esetében az eredeti fájl, a Next()-nél annak azon maradéka, amely az eddig feldolgozott szavakat már nem tartalmazza.) A kiértékeléshez először meg kell keresni a következő szó elejét, ami a szóközök átlépését, az első nemszóköz karakter vagy a fájl végének (ha nincs egyáltalán szó) megtalálását jelenti (azaz ez egy kiválasztás). Ha megtaláltuk a szó elejét, akkor egy ’w’ betűt kezdünk el keresni, de csak legfeljebb a szó végéig (feltételig tartó lineáris keresés), végezetül – ha kell – a szó végét keressük meg (újabb kiválasztás). A formális specifikáció utófeltételéből kiolvasható a megoldásnak ez a három szakasza: az f fájl felsorolására épülő kiválasztás, a felsorolás folytatására épülő feltételig tartó lineáris keresés, majd tovább folytatva a felsorolást egy újabb kiválasztás. Ezen szakaszok határán a felsoroló állapotait felső indexekkel különböztettük meg. Az első kiválasztás egy előreolvasással indul, a másik kettő már előreolvasott fájllal dolgozik. A = (f:infile( ), df: , sf:Státusz, nincs_szó: , van_w: ) Ef = ( f = f’ ) Uf = ( sf 1 , df 1 , f 1
select (sf df
abnorm
f'
df
' ')
nincs_szó=(sf1=norm) nincs_szó ( 2
2
van _ w, ( sf , df , f ) sf , df , f
df ' '
2
search
df ( df 1 , f 1 )
select
df ( sf 2 , df 2 , f 2 )
( sf
(df
abnorm
' w' ) df
' ') ) )
t.First()=t.Next() sf, df, f : read sf = norm
df=’ ’
sf, df, f : read nincs_szó:= sf=norm
231
9. Visszavezetés egyedi felsorolókkal
nincs_szó van_w:=hamis van_w
sf = norm
df ’ ’
van_w:=(df=’w’)
SKIP
sf, df, f: read sf = norm
df ’ ’
sf, df, f: read Valósítsuk most meg az absztrakt felsorolót másképpen! Kössük ki a Next() művelet implementációjánál, hogy a szekvenciális inputfájl a művelet végrehajtása előtt olyan állapotban legyen, hogy ha még nem értük el a fájl végét ( sf=norm), akkor a soron következő szó elejénél álljunk (df ’ ’). (Ez a felsoroló típus invariánsa.) Ekkor a Next() művelet implementációja nem igényel előreolvasást, és rögtön a soron következő szó vizsgálatával foglalkozhat (’w’ betű lineáris keresése majd a szó végének megtalálása), ha van egyáltalán következő szó. Viszont e vizsgálat után gondoskodni kell a szó utáni szóközök átlépéséről (kiválasztás) azért, hogy az újabb Next() művelet számára is biztosítsuk a kívánt előfeltételt. A First() művelet annyival több, mint a Next() művelet, hogy először biztosítania kell a fenti típus-invariánst. Ehhez a vezető szóközök átlépésére (kiválasztás) van szükség. A Current() és az End() implementációja nem változik. Építsünk bele a megoldásba egy másik változtatást is. A ’w’ betű lineáris keresése majd a szó végének megtalálása ugyanis helyettesíthető egyetlen összegzéssel pontosabban „összevagyolással”.
232
9.3. Csoportok felsorolása
ANext = (f:infile( ), df: , sf:Státusz, nincs_szó: , van_w: ) EfNext = ( f = f1 df = df1 sf = sf1 (sf = norm df ’ ’) ) UfNext = ( nincs_szó=(sf1=norm) nincs_szó (
van _ w, ( sf 2 , df 2 , f 2 )
df ' ' df ( df 1 , f 1 )
select
sf , df , f
df ( sf 2 , df 2 , f 2 )
( sf
(df
abnorm
' w' ) df
' ') ) )
t.First() sf, df, f : read sf = norm
df=’ ’
sf, df, f : read t.Next() AFirst = (f:infile( ), df: , sf:Státusz, nincs_szó: , van_w: ) EfFirst = ( f = f’ ) UfFirst = ( sf 1 , df 1 , f 1
select
df ( sf ', df ', f ' )
nincs_szó=(sf1=norm)
(sf
abnorm
nincs_szó
df
' ')
( lásd UfNext -ben ) )
t.Next() nincs_szó:= sf=norm nincs_szó van_w:=hamis sf = norm
df ’ ’
van_w:= van_w
SKIP
(df=’w’)
sf, df, f: read sf = norm
df=’ ’
sf, df, f: read
233
9. Visszavezetés egyedi felsorolókkal
9.4. Összefuttatás Az eddigi feladatoknál a felsorolók többségének hátterében egy adat, többnyire valamilyen gyűjtemény húzódott meg, annak elemeit vagy ezen elemek csoportjait kellett felsorolni. Mindeddig azonban egy felsorolóhoz legfeljebb egy adat tartozott. Találkozhatunk azonban olyan feladatokkal is, ahol a feldolgozandó elemek több bemeneti adatban helyezkednek el. Ilyenkor olyan felsorolásra lesz szükség, amely során képzeletben egyetlen sorozattá futtatjuk össze a bemeneti adatok elemeit. Vegyük például azt a feladatot, amikor két halmaz metszetét kell előállítani. Ha fel tudjuk sorolni a halmazok összes elemét, akkor készen is vagyunk, hiszen ezek közül csak azokat tesszük bele az eredmény halmazba, amelyek mindkét halmazban szerepelnek. Az alábbi specifikációban ezt a megközelítést tükrözi az utófeltétel második alakja, amelyben az unió arra utal, hogy a feldolgozás során nemcsak a közös elemeket kell megvizsgálni, hanem az összest, de azok közül csak bizonyos elemeket kell az eredménybe uniózni. Ez a megoldás az x’ y’ halmaz felsorolására épített összegzés (itt uniózás) lesz, amely fokozatosan fogyasztja el a két halmaz unióját. A = (x:set(E), y:set(E), z:set(E)) Ef = ( x = x’ y = y’ ) Uf = ( z = x’ y’ ) = ( z = f(e) ) e x' y '
F
ahol f:E 2 és
f(e)
ha ha
e e
x' e x' e
y' y'
{e} ha
e
x' e
y'
A megoldás hátterében tehát most is egy egyedi felsoroló áll, amellyel újrafogalmazható a feladat.
234
9.4. Összefuttatás
A = (t:enor(E), z:set(E)) Ef = ( t = t’ ) Uf = ( z = f(e) ) e t'
Az összefuttató felsorolás tulajdonképpen az x y halmaz elemeinek bejárása lesz. Ez akkor ér véget, ha x y üres lesz (t.End()). A t.Current() az x y halmazból választ ki determinisztikus módon egy elemet (mem(x y)), amelyet majd a t.Next() művelet fog elvenni az x y-ból, pontosabban x-ből, ha az x-beli, y-ból, ha y-beli, mindkettőből, ha mindkettőnek eleme. A t.First() művelet – mint azt a halmazok bejárásánál korábban láttuk – az üres program. z:= x y e:=mem(x y) e x
e y
e x
e y
SKIP
SKIP
x:=x–{e}
y:=y–{e}
e x
e y
z:= z
{e}
x, y := x–{e}, y– {e}
Az x y vizsgálat helyettesíthető az x y feltétellel. A mem(x y) végrehajtásához sem kell minden lépésben egyesíteni az x és y halmazokat. Ha x nem üres akkor a e:=mem(x), egyébként a e:=mem(y) valósítja meg a kiválasztást. A t.Next() ugyanazokat a feltételeket vizsgálja, mint amit a feladat specifikációja egy elem feldolgozásánál. Ezért a ciklusmagban egy elem feldolgozása és az azt követő Next() művelet ugyanazon háromágú elágazásba lett összevonva. Egyszerűsíthető a fenti program, ha figyelembe vesszük azt, hogy az x y halmaz elemei felsorolásának célja közös elemek kigyűjtése. Ezért a felsorolást már akkor befejezhetjük, ha az egyik halmaz kiürül, azaz az t.End() művelet akkor igaz, ha x= y= . A t.Current() egyszerűsíthető a mem(x) műveletre, hiszen egyfelől az x tartalmazza az összes közös elemet, másfelől erre az elem kiválasztásra akkor kerül
235
9. Visszavezetés egyedi felsorolókkal
sor, amikor t.End() teljesül, azaz x biztosan nem üres. A módosult t.Current() miatt egyszerűsíthető a t.Next() is, sőt egy elem feldolgozása is, mert sosem kerül végrehajtásra a korábbi háromágú elágazás középső ága. z:= x
y
e:=mem(x) e x
e y
SKIP x:=x–{e}
e x
e y
z:= z
{e}
x, y := x–{e}, y–{e}
Hasonlóan egyszerűsödne az x y elemeit felsoroló és feldolgozó általános program, ha nem az x és y közös elemeit, hanem az összest, vagy a kizárólag az x-belieket kellene feldolgozni. Általánosítsuk a fenti feladatot úgy, hogy a felsorolandó elemek ne két halmazban legyenek, hanem két tetszőleges felsoroló szolgáltassa azokat. Ha a felsorolásokat gondolatban az elemeikből képzett halmazokra cseréljük, akkor a megoldást a fentire vezethetjük vissza. A feladat egyértelmű megfogalmazása érdekében viszont célszerű feltenni, hogy egy elem egy felsorolásban legfeljebb egyszer forduljon elő. Feltesszük azt is, hogy a felsorolt elemeken értelmezett egy teljes rendezés, és mindkét felsoroló növekvő sorrendben járja be az elemeket. Ennek hasznossága akkor látszik majd, amikor egy eleméről el kell dönteni, hogy az eredetileg az x vagy az y felsorolásban szerepel-e. A feladat specifikációjában az {t} a t felsorolásból képzett halmazt szimbolizálja. A t azt jelöli, hogy a t elemei szigorúan növekvően rendezett sorrendben helyezkednek el, és természetesen feltételezzük, hogy E-n értelmezett ehhez egy teljes rendezés. Az eredményt egy tetszőleges sorozatba fűzzük fel.
236
9.4. Összefuttatás
A = (x:enor(E), y:enor(E), z:seq(E)) Ef = ( x=x’ y=y’ x y ) f(e) ) Uf = ( z = e { x '} { y '}
ahol f:E F* és
f(e)
f 1(e) ha e {x'} e { y '} f 2(e) ha e {x'} e { y '} f 3(e) ha e {x'} e { y '}
az f1, f2, f3:E F* pedig tetszőleges függvények. A megoldáshoz itt is egy egyedi (absztrakt) felsorolóra lesz szükség, amely a két konkrét felsorolás összefuttatására épül. A = (t:enor(E), z:seq(E)) Ef = ( t = t’ ) f(e) ) Uf = ( z = e {t '}
A t.Current()-nek az x vagy y felsorolásnak egy elemét kell kiválasztania úgy, hogy azt is megmondja, hogy a kiválasztott elem csak az x, csak y vagy mindkettő felsorolásban szerepel-e. Ehhez az x.Current() és y.Current() elemeket használhatja fel, ezek közül kell választania. Mindenek előtt tehát el kell indítani mindkét felsorolást, azaz a t.First() az x.First() és y.First() végrehajtásával lesz azonos. A t.Next() dolga annak a konkrét felsorolásnak a Next() műveletét végrehajtani, amelyik felsorolásnak része az éppen kiválasztott elem. Az összefuttatás általában addig tart (t.End()), amíg mindkét konkrét felsoroló véget nem ér. Ahhoz, hogy eldöntsük, hogy x.Current() benne van-e az y felsorolásban, általában egy külön keresésre lenne szükség. De nem minden felsoroló teszi lehetővé, hogy egy ilyen keresés érdekében újra és újra elindítsuk, ráadásul nem is tűnik ez túl hatékony megoldásnak. De ha feltesszük (és ezt feltettük), hogy mindkét konkrét felsorolás szigorúan növekedően rendezett, akkor a kérdés egy egyszerű vizsgálattal eldönthető. Mindenek előtt tegyük fel azt is (ez lesz az egyedi felsorolónk típus-invariánsa), hogy x.Current() és y.Current() nagyobb az x és y korábban felsorolt elemeinél. Ez kezdetben, a t.First() végrehajtása után biztos fenn áll, később pedig, ha a t.Next() műveletet a fentiek értelmében valósítjuk meg, meg is marad.
237
9. Visszavezetés egyedi felsorolókkal
Ennélfogva ha x.Current()y.Current() esetén az y.Current()-et fogjuk felsorolni. Az x.Current()=y.Current() esetén pedig olyan elemmel van dolgunk, amelyik mindkét felsorolásban szerepel. Az eddigi vizsgálatoknál feltettük, hogy egyik konkrét felsorolás sem ért véget ( x.End() y.End()). Nem vettük azonban eddig figyelembe azt, amikor valamelyik konkrét felsorolás már befejeződött, azaz nincs neki aktuális eleme, amelyet a másik felsoroló aktuális elemével összehasonlíthatnánk. Ha például már y.End() teljesül, akkor (az x.End() még biztos hamis) az x.Current() a típusinvariáns értelmében biztos nem szerepelt az y felsorolásában, azaz ugyanahhoz az esethez tartozik, mint a x.Current() x.First(); y.First(); x.End()
y.End()
y.End() x.End() ( x.End() ( y.End() x.Current()< x.Current()> y.Current() ) y.Current() )
x.End() y.End() x.Current()= y.Current()
z:write( z:write( z:write( f1(x.Current())) f2(y.Current())) f3(x.Current())) x.Next();
y.Next()
x.Next(); y.Next()
Vegyük észre, hogy az elágazás feltételei egyrészt a ciklusfeltétel miatt, másrészt az úgynevezett lusta kiértékelést feltételezve egyszerűsödtek. Például az első ág feltétele eredetileg az ( x.End() y.End()) ( x.End() y.End() x.Current()
238
9.4. Összefuttatás
Ha a feldolgozás függvény speciális, mert a három eset közül valamelyikben az üres sorozatot állítja elő (azaz nem kell ilyenkor semmit tenni a kimeneti adattal), akkor a program tovább egyszerűsíthető. Ha például csak az x és y felsorolások közös elemeit kell feldolgozni, azaz f1(e)= f2(e)=< >, akkor az alábbi program is megfelel. (Szigorodott a ciklus feltétel, és ennek következtében egyszerűsödtek az elágazások feltételei.) z := <> x.First(); y.First(); x.End() x.Current()< y.Current()
y.End()
x.Current()> y.Current()
x.Current()= y.Current()
SKIP
SKIP
z:write(f3(x.Current()))
x.Next();
y.Next()
x.Next(); y.Next()
A fentiek alapján az összefuttatásos feldolgozás könnyen adaptálható sorozatok, szekvenciális inputfájlok vagy vektorok összefuttatására is. Megjegyezzük, hogy az eredmény sorozat minden esetben egyértelmű és rendezett lesz. Egy ilyen feladat a következő is. 9.8. Példa. Adott két névsor egy-egy szekvenciális inputfájlban, mindkettő szigorúan növekedően rendezett az ábécé szabályai szerint. Az első névsor egy két féléves tantárgy első félévét felvevő hallgatók neveit, a másik a második félévre jelentkező hallgatók neveit tartalmazza. Kik azok, akik elkezdték a tantárgyat, de már a második félévet nem vették fel, azaz kik azok, akik csak az első névsorban szerepelnek, a másodikban nem?
239
9. Visszavezetés egyedi felsorolókkal
A = (x:seqin( *), y:seqin( *), z:seqout( *)) Ef = ( x=x’ y=y’ x y ) f(e) ) Uf = ( z = e { x '} { y '}
e f(e)
ha ha
e {x '} e { y '} e {x '} e { y '}
ha
e {x '} e { y '}
Halmazokkal megfogalmazva a feladatot, itt tulajdonképpen két halmaz különbségét kell meghatározni. A feladat a két szekvenciális inputfájl összefuttatására épített összegzéssel oldható meg. Az általános megoldást most úgy kell alkalmazni, hogy az x és y elemeinek felsorolásához a szekvenciális inputfájl nevezetes felsorolását kell használni. (Az x.First() és x.Next() helyett sx,dx,x:read, x.End() helyett sx=norm, x.Current() helyett dx.) A megoldó program ciklusfeltétele az sx=norm sy=norm helyett az sx=norm-ra egyszerűsödhet, és ennélfogva egyszerűbbek lehetnek az elágazás feltételei is. z := <> sx,dx,x:read; sy,dy,y:read sx=norm sy=abnorm dx
sy=norm dx>dy
sy=norm dx=dy
z:write(dx)
SKIP
SKIP
sx,dx,x:read
sy,dy,y:read
sx,dx,x:read; sy,dy,y:read
Az összefuttatás alkalmazásának egyik klasszikus példája az alábbi adatkarbantartásról szóló feladat. 9.9. Példa. Adott egy árukészletet, mint az áruk azonosítója szerint egyértelmű és növekvően rendezett szekvenciális inputfájl, továbbiakban törzsfájl, valamint az egyes árukra vonatkozó
240
9.4. Összefuttatás
változtatásokat tartalmazó, ugyancsak az áruk azonosítója szerint egyértelmű és növekvően rendezett másik szekvenciális inputfájl, továbbiakban módosító fájl. A változtatások három félék lehetnek: új áru felvétele (beszúrás), meglevő áru leselejtezése (törlés) és meglevő áru mennyiségének módosítása. Készítsük el a változtatásokkal megújított, úgynevezett időszerűsített árukészletet. A törzsfájl Áru=rec(azon:ℕ, me:ℕ) típusú elemek; a módosító fájl pedig Változás=rec(azon:ℕ, müv:{I, D, U}, me:ℤ) típusú elemek sorozata. Az I (beszúrás), D (törlés), U (módosítás) a megfelelő változtatás típusára utal, a mennyiség a törlés esetén érdektelen, módosítás esetén ennyivel kell az eddigi mennyiséget módosítani. Az új törzsfájl a régi törzsfájllal megegyező szerkezetű. A feladat állapottere: A= (t:infile(Áru) , m:infile(Változás) , u:outfile(Áru)) Ef = ( t = t' m = m' t azon m azon ) A t azon azt jelzi, hogy a t szekvenciális inputfájl elemei (rekordjai) az azon mezőjük alapján szigorúan növekedően rendezettek. Szükségünk van egy olyan felsorolóra, amelyik fel tudja sorolni a két szekvenciális inputfájlban előforduló összes áruazonosítót, meg tudja mondani, hogy az melyik inputfájlban található (törzsfájl, módosító fájl vagy mindkettő), és képes megadni az aktuális áruazonosítóhoz tartozó törzsfájlbeli illetve módosító fájlbeli rekordot. A specifikációban az azon(t’) az összes t’-beli azonosítót tartalmazó halmazt, az azon(m’) az összes m’-beli azonosítót tartalmazó halmazt jelöli. Ha a azon(t’), akkor a(t’) a t’-beli a azonosítójú rekordot jelöli. Hasonlóan, ha a azon(m’), akkor az a(m’) az m’-beli a azonosítójú rekord.
241
9. Visszavezetés egyedi felsorolókkal
A= (x:enor(ℕ), u:outfile(Áru)) Ef = ( x = x' ) Uf = ( u f(a) ) a x'
f(a)
f 2(a)
a (t ' ) f 2 (a)
ha ha
a a
azon (t ' ) azon (t ' )
a a
azon (m' ) azon (m' )
f 3 (a)
ha
a
azon (t ' )
a
azon (m' )
(a, a(m' ).me) hiba
hiba
f 3(a) g (a)
g(a)
ha e.müv k ülönben
ha
a(m' ).müv
ha ha
a(m' ).müv D a(m' ).müv U
a, a(t ' ).me a(m' ).me a(t ' ) hiba
I
I
ha a(t ' ).me a(m' ).me 0 ha a(t ' ).me a(m' ).me 0
A feladat megoldását a fent körvonalazott összefuttató felsoroló feletti összegzésre vezetjük vissza annak figyelembe vételével, hogy most a két konkrét felsoroló egy-egy szekvenciális inputfájl nevezetes felsorolója lesz. Az egyikhez st,dt,t:read, a másikhoz sm,dm,m:read műveletet használjuk. Az összefuttatás három esete vagy egy kizárólag törzsfájlbeli elemet (dt) ad meg (ilyenkor nincs változtatás a dt-re), vagy egy kizárólag módosító fájlbelit (ez olyan változtatás, amelyik a törzsfájlban nem szereplő árura vonatkozik; csak beszúrásként lehet értelmes), vagy mindkét fájl egyező azonosítójú elemeit (dt-t kell dm alapján módosítani; dm beszúrás nem lehet).
242
9.4. Összefuttatás
u := <> st, dt, t: read; sm, dm, m: read st=norm sm=abnorm (st=norm dt.azon < dm.azon)
sm=norm
st=abnorm (sm=norm dt.azon> dm.azon)
st=norm sm=norm dt.azon=dm.azon
dm.müv=
u:write(dt) dm.müv=I u:write( dm.azon, dm.me)
H I B A
I
D
U
H I B A
S K I P
c := dt.me+dm.me c<0 H I B A
dt.me:=c
u:write(dt) st, dt, t: read sm, dm, m: read
st, dt, t: read sm, dm, m: read
Életszerűbb lenne a probléma, ha nem követelnénk meg azt, hogy a módosító fájl egyértelmű legyen, azaz egy azonosítóhoz több változtatást is rendelhetünk. Ekkor a fenti algoritmusban a módosító fájlnak nem az egyes elemeit (rekordjait) kellene felsorolni, hanem a megegyező azonosítójú rekordjainak csoportjait, és egy-egy ilyen csoportot hasonlítanánk össze a törzsfájl egy-egy rekordjával, és egy ilyen csoport változtatásait hajtanánk végre egymás után ugyanarra az azonosítóra, pontosabban az ahhoz tartozó adatra. A csoportok kiolvasásához a korábban (9.4. alfejezet) mutatott technikával tudunk felsorolót készíteni. Egy másik megoldási lehetőség egy alkalmas rekurzív függvény bevezetése lehetne. Rekurzív függvények feldolgozásával majd a következő alfejezetben foglalkozunk.
243
9. Visszavezetés egyedi felsorolókkal
Hasonló problémát vet fel az, amikor a módosítások több különböző módosító fájlban találhatók. Ilyenkor előbb össze kell futtatni ezeket egyetlen módosító fájlba, vagy legalábbis egy olyan felsorolót kell bevezetni, amely egyetlen, azonosító szerint növekvő sorozatként bánik a módosító fájlokkal. Ennek lényegét emeli ki a következő feladat. 9.10. Példa. Adott n darab szekvenciális inputfájl, amelyek egész számokat tartalmaznak, mindegyik egyértelmű és növekvően rendezett. Készítsünk olyan felsorolót, amely a fájlokban előforduló összes egész számot felsorolja úgy, hogy ugyanazt a számot csak egyszer adja meg. A felsorolót az összes előreolvasott szekvenciális inputfájllal reprezentáljuk. Ez három 1-től n-ig indexelhető tömbbel ábrázolható: f:infile(ℤ)n, df:ℤn , sf:Státuszn. A kezdeti beolvasásokat a First() művelet végzi. Az End() művelet akkor ad igazat, ha mindegyik fájl olvasási státusza abnorm lesz. Ennek eldöntésére az 1..n intervallum feletti optimista lineáris keresésre van szükség: vizsgáljuk, hogy i [1..n]:sf[i]=abnorm teljesül-e. A Current() művelet a soron következő egész számot az aktuálisan beolvasott egész számok közötti feltételes minimumkereséssel határozza meg, ugyanis ki kell hagynia a vizsgálatból azon fájlokat, amelyek olvasási státusza már abnorm. Vegyük észre, hogy ez a feltételes minimumkeresés helyettesíti az End() műveletet implementáló optimista lineáris keresést, hiszen mellékesen eldönti, hogy van-e még nem üres fájl. Tegyük tehát bele ezt a feltételes minimumkeresést a First() és a Next() műveletek implementációiba, a felsoroló reprezentációjához pedig vegyük hozzá a minimum keresés eredményét tároló l logikai értéket, és min egész számot. Ekkor az End() értéke egyszerűen a l lesz, a Current() értéke pedig a min. A Next() műveletnek mindazon fájlokból kell új elemet olvasnia, amelyek aktuálisan kiolvasott értéke megegyezik a min értékkel, majd végre kell hajtania az előbb említett feltételes minimumkeresést. Az alábbi algoritmus nem részletezi a feltételes minimumkeresés struktogrammját, ezt az Olvasóra bízzuk.
244
9.4. Összefuttatás
First() i = 1 .. n sf[i],df[i],f[i]:read n
l, min := min df [i ] i 1 sf [ i ] norm
Next() i = 1 .. n sf[i]=norm sf[i],df[i],f[i]:read
df[i]=min SKIP
n
l, min := min df [i ] i 1 sf [ i ] norm
9.11. Példa. Egy vállalat raktárába több különböző cég szállít árut. A raktárkészletet egy szekvenciális inputfájlban (törzsfájl) tartják nyilván úgy, hogy minden áruazonosító mellett feltűntetik a készlet mennyiségét. A beszállító cégek minden nap elküldenek egy-egy ezzel megegyező formájú szekvenciális inputfájlt (módosító fájl), amelyek az adott napon szállított áruk mennyiségét tartalmazzák. Minden szekvenciális fájl áruazonosító szerint szigorúan növekvően rendezett. Aktualizáljuk a raktár-nyilvántartást. A= (t:infile(Áru), m:infile(Áru)n, u:outfile(Áru)) Áru=rec(azon:ℕ, menny:ℕ) Ef = ( t = t' m = m' t azon i [1..n]:m[i] azon ) A megoldáshoz az m tömbre olyan y felsorolót vezetünk be, amely a módosító fájlokból mindig a soron következő azonosítójú árura vonatkozó összesített beszállítási adatot olvassa be. (Alternatív megoldás lehetne az is, ha az összesítést nemcsak az n darab beszállítási fájlra végzi el a felsoroló, hanem a törzsfájllal együtt n+1 darab fájlra, és ilyenkor csak egyszerűen ki kell másolni a felsorolás elemeit a kimeneti fájlba. Ez utóbbi esetben azonban nincs lehetőség
245
9. Visszavezetés egyedi felsorolókkal
annak a hibának a kiszűrésére, amikor a nyilvántartásban nem szereplő áruból érkezik beszállítás.) Mind az y.First(), mind az y.Next() művelet a beszállításokban szerepelő legkisebb azonosítójú, még feldolgozatlan tételek mennyiségeit összegzi. Ehhez először egy feltételes minimumkereséssel megkeressük a legkisebb (min) azonosítójú árut az aktuálisan beolvasott tételek között (a feltétel az, hogy az adott beszállítás fájlnak van-e még feldolgozatlan tétele, azaz sm[i]=norm). Majd, ha találtunk ilyet (azaz nem mindegyik fájl üres), akkor a min azonosítójú tételek mennyiségeit összegezzük (ez egy feltételes összegzés az sm[i]=norm dm[i]=min feltétellel). Az y.First() művelet csak abban különbözik az y.Next()-től, hogy legelőször minden beszállítás fájl első (legkisebb azonosítójú) tételét be kell olvasni (ha van ilyen), később már csak azokból a beszállítás fájlokból kell újabb tételt olvasni, amelyek a korábbi lépésben összegzett rekordokat adták. Ezt az olvasást viszont ezért érdemes a megelőző Next (vagy First) művelet végére tenni, mert összevonható az ott található feltételes összegzéssel. Ezzel lényegében azt mondjuk, hogy a felsoroló invariánsa előírja, hogy a beszállítás fájlokból legyenek a soron következő, még feldolgozatlan tételek beolvasva. Az y.First() művelet tehát miután minden beszállítás fájlból olvasott egyet azonos az y.Next()-tel. A beszállítás fájlok bejárásához a szekvenciális inputfájl nevezetes felsorolóját használjuk. y.First() i = 1 .. n sm[i],dm[i],m[i]:read y.Next() Az y.Next() művelet a feltételes minimumkereséssel kezdődik (ennek struktogrammját nem adjuk meg, ezt az Olvasóra bízzuk), amelyet a feltételes összegzéssel összevont fájlonkénti továbbolvasás következik. Ezt a második részt csak akkor érdemes végrehajtani, ha van legalább egy olyan fájl, amely még nem üres, azaz ha a feltételes minimumkeresés logikai eredménye igaz.
246
9.4. Összefuttatás
y.Next() n
l, ind, min :=
dm[i].azon
min
i 1 sm[i ] norm
l dm.azon, dm.menny:=min, 0
SKIP
i = 1 .. n sm[i]=norm
dm[i]=min
dm.menny:= SKIP dm.menny+dm[i].menny sm[i],dm[i],m[i]:read Az y.Current() művelet a minimális azonosítót és az összesített beszállítási mennyiséget adja vissza (dm), az y.End() pedig akkor lesz igaz, ha a feltételes minimumkeresés sikertelen (l=hamis), azaz már minden beszállítás összes tételét megvizsgáltuk. Az y felsoroló segítségével újraspecifikáljuk a feladatot: A= (t:infile(Áru), y:enor(Áru), u:outfile(Áru)) Áru=rec(azon:ℕ, menny:ℕ) Ef = ( t = t' y = y' t azon y azon ) f(a) ) Uf = ( u a azon(t ') azon( y ')
a (t ' ) f(a)
hiba
g (a) g(a)
ha ha ha
a a a
azon (t ' ) azon (t ' ) az (t ' )
a a a
azon ( y ' ) azon ( y ' ) azon ( y ' )
a(t ' ) menny menny a ( y ' ).menny
A t fájl a szekvenciális inputfájlok nevezetes felsorolójával járjuk be. Ekkor a két felsorolóra épített összefuttatásos összegzéssel oldhatjuk meg a feladatot.
247
9. Visszavezetés egyedi felsorolókkal
u : =<> st,dt,t:read; y.First(); st=norm y.End() (st=norm dt.azon< y.Current().azon) u:write(dt)
y.End()
st=abnorm (st=norm dt.azon> y.Current().azon)
st=norm y.End() dt.azon= y.Current().azon
HIBA
dt.menny:= dt.menny+dm.menn y u:write(dt)
st,dt,t:read
y.Next()
st,dt,t:read; y.Next()
9.5. Rekurzív függvény feldolgozása Az intervallumon értelmezett programozási tételek közül nem általánosítottuk felsorolóra a rekurzív függvény helyettesítési értékét kiszámoló tételünket. Ennek az oka az, hogy a 4. fejezetben bevezetett rekurzív függvényeket az egész számok egy intervallumán definiáltuk, tehát ez a fogalom az intervallumhoz kötött, ezért nincs értelme a kiszámítását végző programozási tételt általánosítani. Előfordulhat azonban, hogy olyan esetben is rekurzív függvény értékének kiszámolásához folyamodunk, amikor a feladat adatai nem mutatják meg közvetlenül a rekurzív függvény értelmezési tartományát kijelölő intervallumot. Helyette egy felsorolónk van, amelynek ha az elemeit a felsorolás sorrendjében megsorszámozzuk, akkor a keresett intervallumhoz jutunk. A 6.4. alfejezetben láttuk, hogy milyen hatékony megoldást eredményez egy programozási tételbe ágyazott rekurzív függvény kibontásával kapott program. Ehhez ott arra volt szükség, hogy a programozási tétel intervallumán legyen értelmezve a rekurzív függvény is. Vajon lehet-e ezt a technikát alkalmazni akkor is, ha felsorolóra fogalmazott programozási tételekkel dolgozunk? Erre adnak választ az alábbi feladatok-megoldásai.
248
9.5. Rekurzív függvény feldolgozása
Az alábbi „szöveg-tömörítő” feladatot kétféleképpen is megoldjuk: először egy rekurzív függvény értékének kiszámolásával, majd egy felsorolóra épülő feldolgozásban történő rekurzív függvény kibontással. 9.12. Példa. Másoljuk át a karaktereket egy szekvenciális inputfájlból egy outputfájlba úgy, hogy ott, ahol több szóköz követte egymást, csak egyetlen szóközt tartunk meg! Képzeljük el azt a többkomponensű r:[0.. x ] * függvényt, amelyiknek első komponense az i-edik helyen az x inputfájl első i karakterének tömörített változatát adja meg, második komponense arra szolgál, hogy emlékezzen arra, hogy az i–1-dik karakter szóköz volt-e. Ezt a függvény az alábbi módon definiálhatjuk.
i [1.. x ] : r(i)
(r1 (i 1) xi , hamis ) ha (r1(i 1) xi , igaz ) ha (r(i 1)) ha
xi ' ' xi ' ' r2(i 1) xi ' ' r2(i 1)
r(0) ( , hamis ) Ezek után a feladat specifikációja igen egyszerű: számoljuk ki az r rekurzív függvény utolsó értékét. A = (x:infile( ), y:outfile( )) Ef = ( x=x’ ) Uf = ( y r1 ( x' ) ) A feladat visszavezethető a rekurzív függvény helyettesítési értékét kiszámoló programozási tételre, de a kapott algoritmussal van egy kis bökkenő. Nem megengedett az x szekvenciális inputfájl i-dik elemére történő közvetlen hivatkozás, ahhoz csak egy felsorolással tudunk hozzáférni.
249
9. Visszavezetés egyedi felsorolókkal
y, l, i :=<>, hamis, 1 i xi ’ ’
x xi =’ ’
y:write(xi) l := hamis
l
y:write(xi) l := igaz
xi =’ ’
l
SKIP
i := i+1 Ebben az esetben jól látszik, hogy az i indexváltozónak csupán az a szerepe, hogy segítségével bejárjuk az x szekvenciális inputfájl elemeit. Cseréljük hát le a szekvenciális fájl megengedett felsorolójára, azaz használjunk a read műveletet. y, l :=<>, hamis st, ch, x : read st = norm ch
’’
y:write(ch) l := hamis
ch =’ ’
l
y:write(ch) l := igaz
ch =’ ’
l
SKIP
st, ch, x : read Általában előfordulhat, hogy a rekurzív képletnek az i indexre közvetlenül is szüksége van, ezért olyankor az i-re vonatkozó műveletek is megmaradnak az algoritmusban, azaz egymás mellett zajlik az intervallum és mondjuk egy szekvenciális inputfájl felsorolása. A második megoldási út abba gondolatba kapaszkodik bele, hogy ez a feladat első látásra egy olyan elemenkénti feldolgozásnak tűnik, amelyeket az előző fejezetekben oldottunk meg, hiszen az eredmény (y) előállításához bizonyos értékeket kell egymás után fűzni. Alaposan szemügyre véve azonban látható, hogy itt nem egy másolásról van szó: ahhoz ugyanis, hogy eldöntsük, mit kell az aktuális karakterrel csinálni (hozzá kell-e írni az y-hoz vagy sem), ismerni kell, hogy a megelőző
250
9.5. Rekurzív függvény feldolgozása
i [1.. x ] : r(i)
( xi , hamis ) ha ( xi , igaz ) ha ( , r2 (i 1)) ha
xi ' ' xi ' ' r2(i 1) xi ' ' r2(i 1)
r (0) ( , hamis ) karakter szóköz volt-e vagy sem. A feladat leírásához ezért itt is egy rekurzív függvényt kell bevezetnünk, amely természetesen hasonlít az előző megoldásban használthoz. A lényeges különbség az, hogy most az r:[0.. x ] * rekurzív függvény első komponense csak az eredményhez hozzáfűzendő egy karakternyi vagy üres sorozatot állítja elő, és nem a teljes eredményt. A specifikáció pedig: A = (x:infile( ), y:outfile( )) Ef = ( x=x’ ) x'
Uf = ( y
i 1
r1 (i ) )
A feladatot tehát a rekurzív függvény első komponense által adott értékeinek az összefűzésével (összegzés) oldhatjuk meg. A soron következő ilyen érték előállításához a szekvenciális fájl aktuális karakterére és a megelőző karakterről információt adó logikai értékre van szükség. Ugyanakkor nincs közvetlenül szükség arra az indexre, amelyik a rekurzió lépéseit számolja. A rekurzív képlet általános h:ℤ×Hk H függvényét most egy h: * alakú függvény biztosítja, hiszen r(i)=h(x’i, r2(i–1)). Ha rendelkeznénk egy olyan felsorolóval, amely a h kiszámolásához szükséges karakter-logikai érték párokat a megfelelő sorrendben képes adagolni, akkor a feladat megoldásával készen is lennénk. Általánosságban is igaz az, hogy ha egy rekurzív függvény egymás utáni értékeit kell előállítanunk egy feldolgozás számára, akkor a rekurzív képlet h:ℤ×Hk H függvényét kell a számára szükséges k+1 darab argumentummal újra és újra kiszámolni, és ehhez ezen argumentum-csoportokat kell egy alkalmas felsorolóval megadni. Ezen gondolatmenet mentén a feladatunkat újraspecifikálhatjuk.
251
9. Visszavezetés egyedi felsorolókkal
A = (t:enor(Pár), y:outfile( )) Ef = ( t=t’ ) h1(ch,l) ) Uf = ( y
Pár = rec(e: , l: )
(ch, l ) t '
ahol h:
h(ch,l)
*
és
( ch , hamis ) ( ch , igaz )
ha ha
ch ' ' ch ' ' l
( ,l) ha ch ' ' l A felsoroló reprezentálásához egyfelől az x szekvenciális inputfájlra van szükség, továbbá az abból való olvasásához a kiolvasott aktuális karakterre (ch) és az olvasás státuszára (st). Másfelől kell egy logikai változó (l), amely az előző karakterről mondja meg, hogy szóköz volt-e vagy sem. A First() művelet egyrészt a szekvenciális inputfájl első karakterét kísérli meg beolvasni, másrészt a logikai komponenst a rekurzív függvény kezdeti definíciójának (r(0) = (<>,hamis)) megfelelően hamis-ra állítja. A Next() a függvénydefiníció rekurzív formulája alapján (itt kap szerepet a h2) új értéket ad a logikai változónak (l:=ch=’ ’), majd beolvassa a következő karaktert a szekvenciális inputfájlból. A Current() visszaadja a ch és l értékeit, az End() pedig akkor lesz igaz, ha az st=abnorm. Az általános összegzés f függvénye most a h1, amely a Current() művelet által szolgáltatott ch és l értékekhez rendel egy karaktersorozatot, amelyet az y-hoz kell hozzáírni. Mivel h1 egy esetszétválasztás, az y:write(h1(ch,l)) utasítást egy elágazással kódoljuk. Ez az algoritmus megegyezik az első megoldás során kapott változattal.
252
9.5. Rekurzív függvény feldolgozása
y:=<> l:=hamis st,ch,x:read sx = norm ch ’ ’
ch=’ ’
y:write(’ ’)
l
y:write(ch)
ch=’ ’
l
SKIP
l:= ch=’ ’ st,ch,x:read Ugyancsak egy rekurzív függvénynek egy másik programozási tételben történő kibontására ad példát az alábbi megoldás. 9.13. Példa. Egy kirándulás során bejárt útvonalon adott távolságonként mértük a tengerszint feletti magasságot (pozitív szám), és ezen értékeket egy szekvenciális inputfájlban rögzítettük. Azt az értéket, amelyik nagyobb az összes előzőnél, küszöbnek hívjuk. Hány küszöbbel találkoztunk a kirándulás során? Specifikáljuk először a feladatot egy alkalmas rekurzív függvény segítségével. A = (x:infile(ℝ), db:ℕ) Ef = ( x=x’ ) x'
Uf = ( db
1 x'i
)
i 2 f(i 1)
ahol f(i–1) az első i-1 elem maximuma. Ezt – szekvenciális inputfájl feldolgozásáról lévén szó – nem maximum kiválasztással, hanem egy alkalmas rekurzív függvény segítségével állítjuk elő: f:[1.. x’ ] ℤ f(1) = x’1 f(i) = max(f(i-1), x’i) (i>1) A feladat most is újrafogalmazható egy alkalmas felsoroló segítségével, ha észrevesszük, hogy a rekurzív képlet kiszámolásához a szekvenciális inputfájl aktuális elemét és a korábbi elemek maximumát kell megadni:
253
9. Visszavezetés egyedi felsorolókkal
A = (t:enor(Pár), db:ℕ) Ef = ( t=t’ ) 1 ) Uf = ( db
Pár = rec(e:ℝ, max:ℝ)
( e, max ) t ' e max
A feladat a számlálásra veszethető vissza. Az elem-maximum párok felsorolása azzal kezdődik (First), hogy a fájl első elemét beolvassuk a max, második elemét az e változóba. A következő maximum a korábbi maximumtól és az aktuális elemtől függ, ezt követően pedig újabb elemet kell olvasnunk (Next). A számlálás elágazásának feltétele ugyanaz, mint a Next() műveletbeli elágazás feltétele, ezért a két elágazás összevonható. A felsorolás aktuális eleme (Current) az (e,max) pár. A felsorolás addig tart, amíg a fájlból sikerül újabb elemet olvasni (End). db := 0 st,max,x:read st,e,x:read st = norm e>max db, max:=db+1, e
SKIP
st,e,x:read 9.6. Felsoroló gyűjtemény nélkül 9.14. Példa. Határozzuk meg egy n pozitív egész szám prímosztóinak az összegét! A feladat megfogalmazható egy feltételes összegzésként, amely során a [2..n] intervallum azon elemeit kell összeadnunk, amelyek osztják az n-t és prím számok.
254
9.6. Felsoroló gyűjtemény nélkül
A = (n:ℕ, sum:ℕ) Ef = ( n=n' n>0 ) n
Uf = ( Ef
i
sum = in
)
i 2 prim(i)
A feladat közönséges összegzésként is megfogalmazható, ha bevezetjük azt a felsorolót, amely közvetlenül az n szám prím osztóit sorolja fel, és a feladatot ennek megfelelően újraspecifikáljuk. A = (t:enor(ℕ), sum:ℕ) Ef = ( t=t' ) e ) Uf = ( sum = e t'
Ezt visszavezethetjük az általános összegzésre úgy, hogy abban az E=H=ℕ és minden e természetes számra f(e)= e. sum := 0 t.First() t.End() sum := sum+t.Current() t.Next() Az n szám prímosztói közül az elsőhöz, mondjuk a legkisebbhez, úgy juthatunk hozzá, hogy amennyiben az n legalább 2, akkor egyesével elindulunk 2-től és az n szám első (legkisebb) osztóját megkeressük. Ez biztosan prím. (Ez a tevékenység egy kiválasztás.) A soron következő prímosztó kereséséhez a korábban megtalált prímosztót – ezt tehát meg kell jegyezni – ki kell valahogy venni az n számból, azaz elosztjuk vele az n-t ahányszor csak tudjuk. A hányadosnak (az n új értékének) prímosztói az eredeti n számnak is prímosztói lesznek, és ezek éppen azok, amelyeket eddig még nem soroltunk fel. Tehát a következő prímosztó előállításához meg kell ismételni az előző lépéseket. A felsorolás addig tart, amíg az egyre csökkenő n nem lesz 1. Ez véges lépésben biztosan be fog következni.
255
9. Visszavezetés egyedi felsorolókkal
Az eredeti szám összes prímosztóját felsoroló objektumot egyfelől a folyamatosan csökkenő n, másfelől annak legkisebb prímosztója (d) reprezentálja. A First() művelet az eredeti n legkisebb prímosztóját (lko) keresi meg, ha n>1, és elhelyezi a d változóban. A Next() művelet elosztja az n-t a d-vel ahányszor csak tudja, majd ha így még mindig n>1, akkor megkeresi az n legkisebb prímosztóját, és elhelyezi a d változóban. A Current() művelet a kiszámolt d értékét adja vissza. Az End() n=1 esetén ad igazat. t ~ n, d t.First() ~ d:=lko(n) t.Next() ~ LOOP(d|n; n:=n/d); d:=lko(n) t.End() ~ n=1 t.Current() ~ d A d:=lko(n) megoldása az IF(n>1, d : select (d n) ) lesz. d 2
sum:=0; d:=2; d:=lko(n) n>1 sum := sum + d d|n
d:=lko(n) n>1 d|n
SKIP
d:=d+1
n:=n/d d:=lko(n) Az lko() megvalósításához a kiválasztás korábbi, egész számegyenesre megfogalmazott változatát éppúgy használhatjuk, mint a kiválasztás általános programozási tételét, amelyben a :E feltétel most a (d)= d|n lesz. A felsoroló az egész számokat járja be 2-től indítva, és leállásakor a felsorolást végző index megegyezik a kiválasztás eredményével. Sőt, ez még gyorsítható, ha már korábban találtunk egy d prímosztót, mert ilyenkor nem kell újra 2-től indítani a keresést, hanem elég d+1-től. Azért, hogy e módosítás után ne kelljen az első prímosztó keresését megkülönböztetni a többitől, a d értékét kezdetben a First() műveletben kell 2-re állítani.
256
9.7I.9.7. Feladatok
9.7. Feladatok 9.1.
9.2.
9.3.
9.4.
9.5.
Egy kémrepülőgép végigrepült az ellenség hadállásai felett, és rendszeres időközönként megmérte a felszín tengerszint feletti magasságát. A mért adatokat egy szekvenciális inputfájlban tároljuk. Tudjuk, hogy az ellenség a harcászati rakétáit a lehető legmagasabb horpadásban szokta elhelyezni, azaz olyan helyen, amely előtt és mögött magasabb a tengerszint feletti magasság (lokális minimum). Milyen magasan találhatók az ellenség rakétái? Számoljuk meg egy karakterekből álló szekvenciális inputfájlban a szavakat úgy, hogy a 12 betűnél hosszabb szavakat duplán vegyük figyelembe! (Egy szót szóközök vagy a fájl vége határol.) Alakítsunk át egy bitsorozatot úgy, hogy az első bitjét megtartjuk, utána pedig csak darabszámokat írunk annak megfelelően, hogy hány azonos bit követi közvetlenül egymást! Például a 00011111100100011111 sorozat tömörítve a 0362135 számsorozat lesz. Egy szöveges fájlban a bekezdéseket üres sorok választják el egymástól. A sorokat sorvége jel zárja le. Az utolsó sort zárhatja a fájl vége is. A sorokban a szavakat a sorvége vagy elválasztójelek határolják. Adjuk meg azon bekezdések sorszámait, amelyek tartalmazzák az előre megadott n darab szó mindegyikét! (A nem-üres sorokban mindig van szó. Bekezdésen a szövegnek azt a szakaszát értjük, amely tartalmaz legalább egy szót, és vagy a fájl eleje illetve vége, vagy legalább két sorvége jel határolja.) Egy szöveges állomány legfeljebb 80 karakterből álló sorokat tartalmaz. Egy sor utolsó karaktere mindig a speciális ’\eol’ karakter. Másoljuk át a szöveget egy olyan szöveges állományba, ahol legfeljebb 60 karakterből álló sorok vannak; a sor végén a ’\eol’ karakter áll; és ügyelünk arra, hogy az eredetileg egy szót alkotó karakterek továbbra is azonos sorban maradjanak, azaz sorvége jel ne törjön ketté egy szót. Az eredeti állományban a szavakat egy vagy több szóhatároló jel választja el (Az ’\eol’ is ezek közé tartozik), amelyek közül elég egyet megtartani. A szóhatároló jelek egy karaktereket
257
9. Visszavezetés egyedi felsorolókkal
9.6.
9.7.
9.8.
9.9.
258
tartalmazó – szóhatár nevű – halmazban találhatók. Feltehetjük, hogy a szavak 60 karakternél rövidebbek. Adott egy keresztneveket és egy virágneveket tartalmazó szekvenciális fájl, mindkettő abc szerint szigorúan növekedően rendezett. Határozzuk meg azokat a keresztneveket, amelyek nem virágnevek, illetve azokat a neveket, amelyek keresztnevek is, és virágnevek is! Egy x szekvenciális inputfájl egy könyvtár nyilvántartását tartalmazza. Egy könyvről ismerjük az azonosítóját, a szerzőjét, a címét, a kiadóját, a kiadás évét, az aktuális példányszámát, az ISBN számát. A könyvek azonosító szerint szigorúan növekvően rendezettek. Egy y szekvenciális inputfájl az aznapi könyvtári forgalmat mutatja: melyik könyvből hányat vittek el, illetve hoztak vissza. Minden bejegyzés egy azonosítót és egy előjeles egészszámot - ha elvitték: negatív, ha visszahozták: pozitív - tartalmaz. A bejegyzések azonosító szerint szigorúan növekvően rendezettek. Aktualizáljuk a könyvtári nyilvántartást! A feldolgozás során keletkező hibaeseteket egy h sorozatba írjuk bele! Egy vállalat dolgozóinak a fizetésemelését kell végrehajtani úgy, hogy az azonos beosztású dolgozók fizetését ugyanakkora százalékkal emeljük. A törzsfájl dolgozók sorozata, ahol egy dolgozót három adat helyettesít: a beosztáskód, az egyéni azonosító, és a bér. A törzsfájl beosztáskód szerint növekvően, azon belül azonosító szerint szigorúan monoton növekvően rendezett. A módosító fájl beosztáskód-százalék párok sorozata, és beosztáskód szerint szigorúan monoton növekvően rendezett. (Az egyszerűség kedvéért feltehetjük, hogy csak a törzsfájlban előforduló beosztásokra vonatkozik emelés a módosító fájlban, de nem feltétlenül mindegyikre.) Adjuk meg egy új törzsfájlban a dolgozók emelt béreit! Egy egyetemi kurzusra járó hallgatóknak három géptermi zárthelyit kell írnia a félév során. Két félévközit, amelyikből az egyiket .Net/C#, a másikat Qt/C++ platformon. Azt, hogy a harmadik zárthelyin ki milyen platformon dolgozik, az dönti el, hogy a félévközi zárthelyiken hogyan szerepelt. Aki mindkét félévközi zárthelyit teljesítette, az szabadon választhat platformot. Aki egyiket sem, az nem vehet részt a félévvégi
9.7I.9.7. Feladatok
zárthelyin, számára a félév érvénytelen. Aki csak az egyik platformon írta meg a félévközit, annak a félévvégit a másik platformon kell teljesítenie. Minden gyakorlatvezető elkészíti azt a kimutatást (szekvenciális fájl), amely tartalmazza a saját csoportjába járó hallgatók félévközi teljesítményét. Ezek a fájlok hallgatói azonosító szerint rendezettek. A fájlok egy eleme egy hallgatói azonosítóból és a teljesítmény jeléből (X – mindkét platformon teljesített, Q – csak Qt platformon, N – csak .net platformon, 0 – egyik platformon sem) áll. Rendelkezésünkre állnak továbbá a félévvégi zárthelyire bejelentkezett hallgatók azonosítói rendezett formában egy szekvenciális fájlban. Állítsuk elő azt a szekvenciális outputfájlt, amelyik a zárthelyire bejelentkezett hallgatók közül csak azokat tartalmazza, akik legalább az egyik félévközi zárthelyit teljesítették. Az eredmény fájlban minden ilyen hallgató azonosítója mellett tüntessük fel, hogy milyen platformon kell a hallgatónak dolgoznia: .Net-en, Qt-vel vagy szabadon választhat. 9.10. Az x szekvenciális inputfájlban egész számok vannak. Van-e benne pozitív illetve negatív szám, és ha mindkettő van, akkor melyik fordul elő előbb?
259
10. Feladatok megoldása
10. Feladatok megoldása 1. fejezet feladatainak megoldása 1.1. Mi az állapottere, kezdőállapotai, adott kezdőállapothoz tartozó célállapotai az alábbi feladatnak? „Egy egyenes vonalú egyenletesen haladó piros Opel s kilométert t idő alatt tesz meg. Mekkora az átlagsebessége?” Első megoldás: A bemenő adatok értékét megőrzi a célállapot Állapottér: A = (út:ℝ Kezdőállapotok: Olyan valós szám nem negatív, a Formálisan: { (s, t, Célállapotok: Egy (s, t, célállapot: (s, t, s/t)
, idő:ℝ , seb:ℝ) szám hármasok, ahol az első második pedig pozitív. x) s 0 t>0 } x) kezdőállapothoz tartozó
Második megoldás: A bemenő adatok értékét nem őrzi meg a célállapot Állapottér: A = (út:ℝ , idő:ℝ , seb:ℝ) Kezdőállapotok: Olyan valós szám hármasok, ahol az első szám nem negatív, a második pedig pozitív. Formálisan: { (s, t, x) s 0 t>0 } Célállapotok: Egy (s, t, x) kezdőállapothoz tartozó célállapotok (*, *, s/t), ahol az első két komponens tetszőleges. Harmadik megoldás: Egyik bemenő adat helyén képezzük az eredményt Állapottér: A = (a:ℝ , b:ℝ) Kezdőállapotok: Olyan valós szám párok, ahol az első szám nem negatív, a második pedig pozitív. Formálisan: { (s, t) s 0 t>0 } Célállapotok: Egy (s, t) kezdőállapothoz tartozó célállapotok (s/t, t) Negyedik megoldás: Az állapottér eleve kizárja a nem lehetséges kezdőállapotokat
260
1. fejezet feladatainak megoldása
Állapottér: A = (út:ℝ 0+ , idő:ℝ + , seb:ℝ 0+) Kezdőállapotok: Bármelyik (s, t, x) állapot a feladat kezdőállapota is Célállapotok: Egy (s, t, x) kezdőállapothoz tartozó célállapotok: (*, *, s/t). 1.2. Mi az állapottere, kezdőállapotai, adott kezdőállapothoz tartozó célállapotai az alábbi feladatnak? „Adjuk meg egy természetes számnak egy valódi prím osztóját!” Megoldás: Állapottér: A = (n:ℕ, d:ℕ, l: ) Kezdőállapotok: Az összes állapot. Célállapotok: Ha x összetett szám, akkor egy (x,y,z) kezdőállapothoz az összes olyan (x,p,igaz) célállapot tartozik, ahol a p az x-nek valódi prím osztója. Ha x nem összetett szám, akkor egy (x,y,z) kezdőállapothoz az (x, *,hamis) célállapotok tartoznak. 1.3. Tekintsük az A = (n:ℤ, f:ℤ) állapottéren az alábbi programot! 1. Legyen f értéke 1 2. Amíg n értéke nem 1 addig 2.a. Legyen f értéke f*n 2.b. Csökkentsük n értékét 1-gyel! Írjuk fel e program néhány végrehajtását! Válasz: < (5,y) (5,1) (5,5) (4,5) (4,20) (3,20) (3,60) (2,60) (2,120) (1,120) > < (1,y) (1,1) > < (0,y) (0,1) (0,0) (-1,0) (-1,0) (-2,0) … > (végtelen ciklus) < (–5,y) (–5,1) (–5,–5) (–6,–5) (–6,30) (–7,30) … > (végtelen ciklus) Hogyan lehetne a végrehajtásokat általánosan jellemezni? Válasz:
261
10. Feladatok megoldása
ha x > 0 < (x, y) (x,1) (x,x) (x–1,x) … (1, x!) > ha x ≤ 0 < (x, y) (x,1) (x,x) (x–1,x) (x–1,x(x–1)) (x–2,x(x–1)) … > Megoldja-e a fenti program azt a feladatot, amikor egy pozitív egész számnak kell a faktoriálisát kiszámolni? Válasz: Igen. Ehhez elég felírni a feladatot kezdőállapotcélállapot párok formájában: Állapottér: A = (n:ℤ, f:ℤ) Kezdőállapotok: Azok az (x,y) egész szám párok, ahol az x pozitív. Célállapotok: Egy (x,y) kezdőállapothoz a (*,x!) célállapot tartozik. Vigyázat! Nem lenne helyes, ha a feladatot úgy határoznánk meg, hogy őrizze meg a célállapotának első komponensében a bemenő adatot, azaz az (x,y) kezdőállapothoz csak az (x,x!) célállapot tartozzon. A program ugyanis „elrontja” az első komponens értékét. 1.4. Tekintsük az A = (x:ℤ, y:ℤ) állapottéren az alábbi programot! 1. Legyen x értéke x–y 2. Legyen y értéke x+y 3. Legyen x értéke y–x Írjuk fel e program néhány végrehajtását! Válasz: < (5,3) (2,3) (2,5) (3,5) > < (1,1) (0,1) (0,1) (1,1) > < (0,1) (–1,1) (–1,0) (1,0) > < (-5,3) (–8,3) (–8,–5) (3,–5) > Hogyan lehetne a végrehajtásokat általánosan jellemezni? Válasz: < (u,v) (u–v,v) (u–v,u) (v,u) > Megoldja-e a fenti program azt a feladatot, amikor két, egész típusú változó értékét kell kicserélni?
262
1. fejezet feladatainak megoldása
Válasz: Igen. Ehhez elég felírni a feladatot kezdőállapotcélállapot párok formájában. Állapottér: A = (x:ℤ, y:ℤ) Kezdőállapotok: Bármelyik állapot egyben a feladat kezdőállapota is. Célállapotok: Egy (u,v) kezdőállapothoz tartozó célállapot a (v,u). Ebből látható, hogy a feladat az (u,v) (v,u) alakú hozzárendelésekből áll. A program pedig az (u,v)-ből elindulva minden esetben terminál, és éppen a (v,u) állapotban áll meg. 1.7. Tekintsük az alábbi két feladatot: 1) „Adjuk meg egy 1-nél nagyobb egész szám egyik osztóját!” 2) „Adjuk meg egy összetett természetes szám egyik valódi osztóját!” Tekintsük az alábbi három programokat az A = (n:ℕ, d:ℕ) állapottéren: 1) „Legyen a d értéke 1” 2) „Legyen a d értéke n” 3) „ Legyen a d értéke n–1. Amíg a d nem osztja n-t addig csökkentsük a d-t. Melyik program melyik feladatot oldja meg? Válasz: Az első feladatot mindhárom program megoldja. A második feladatot csak a harmadik program. 1.8. Tekintsük az A=(m:ℕ+, n:ℕ+, x:ℤ) állapottéren működő alábbi programokat! 1) Ha mn, akkor legyen az x értéke először a (-1)n, majd adjuk hozzá a (-1)n+1-t, utána a (-1)n+2-t, és így tovább, végül a (-1)m-t!” 2) Legyen az x értéke a ((–1)m+ (–1)n)/2!
263
10. Feladatok megoldása
Írjuk fel e programok néhány végrehajtását! Lássuk be, hogy mindkét program megoldja az alábbi feladatot: Két adott nem nulla természetes számhoz az alábbiak szerint rendelünk egész számot: ha mindkettő páros, adjunk válaszul 1-et; ha páratlanok, akkor –1-et; ha paritásuk eltérő, akkor 0-át! Megoldás: Első program néhány végrehajtása: < (5,7,2351), (5,7,–1), (5,7,0), (5,7,–1) > < (7,7,333), (7,7,–1) > < (8,4,0), (8,4,1), (8,4,0), (8,4,1), (8,4,0), (8,4,1) > < (8,8,0), (8,4,1) > < (8,5,0), (8,4,1), (8,4,0), (8,4,1), (8,4,0) > Induljunk el egy (m,n,x) állapotból. Ha m n, akkor a harmadik komponens helyén tagonként képződik a (–1)m+(–1)m+1+ … + (– 1)n vagy a (–1)n+(–1)n+1+ … + (–1)m összeg attól függően, hogy mn. Ez az összeg –1 és +1 tagok váltakozásából áll, ahol a szomszédos tagok kinullázzák egymást. Ha n és m egyszerre páratlanok vagy párosak, akkor az összegnek csak a legutolsó tagja marad meg: (–1)n vagy (–1)m, amely páratlan esetben –1, páros esetben +1. Hasonló a helyezet m=n esetén is. Ha n és m paritása eltér – ilyenkor nyilván m n –, az összeg páros darab tagból áll, azaz az értéke éppen 0 lesz. Tehát a program éppen a feladat által kijelölt célállapotban áll meg. Második program néhány végrehajtása: < (5,7,2351), (5,7,–1) > < (7,7,333), (7,7,–1) > < (8,4,0), (8,4,1) > < (8,8,0), (8,4,1) > < (8,5,0), (8,4,0) > Elindulva egy (m,n,x) állapotból abban az állapotban állunk meg, ahol a harmadik komponens ((–1)m+(–1)n)/2. Páros n-re és m-re az a kifejezés +1, páratlan n-re és m-re –1, eltérő paritás esetén
264
1. fejezet feladatainak megoldása
pedig 0. Tehát a program éppen a feladat által kijelölt célállapotban áll meg. 1.7. Tekintsük az A 1,2,3,4,5 állapottéren az alábbi programot. (Itt tényleg egy programot adunk meg, amelyből kiolvasható, hogy az egyes állapotokból indulva az milyen végrehajtási sorozatot fut be.)
1 S
1,2,4,5 2
4 5
2,1 4,1,5,1,4 5,2,4
1
1,4,3,5,2 2
4 5
2,4 4,3,1,2,5,1 5,3,4
1 3 4 5
1,3,2,... 3,3,... 4,1,5,4,2 5,2,3,4
Az F feladat az alábbi kezdő-célállapot párokból áll: {(2,1), (4,1), (4,2), (4,4) ,(4,5)}. Megoldja-e az S program az F feladatot? Megoldás: Nem, mert a program nem minden esetben működik helyesen. A 2-es állapotból indulva a program vagy az 1-es, vagy a 4-es állapotban áll meg, de a feladat a 2-höz csak az 1-et rendeli. 1.8. Legyen S olyan program, amely megoldja az F feladatot! Igaz-e, hogy a) ha F nem determinisztikus, b) ha F determinisztikus, akkor S is az?
akkor
S
sem
az?
Megoldás: a) Nem igaz. Egy adott kezdőállapot esetén a megoldó programnak elég a feladat által kijelölt egyik célállapotban terminálnia. b) Nem igaz. Azokból az állapotokból indulva, ahol a feladat nincs értelmezve, a program bárhogyan viselkedhet. 1.9. Az S1 bővebb, mint az S2, ha S2 minden végrehajtását tartalmazza. Igaz-e, hogy ha egy S1 program megoldja az F feladatot, akkor azt megoldja az S2 is. Megoldás: Igaz. Egyrészt az S2 program a feladat kezdőállapotaiból indulva biztosan terminál. Ha ugyanis lenne a feladat egy kezdőállapotából induló végtelen hosszú végrehajtása, akkor ez az S1 programnak is része lenne, tehát az S1 program sem oldaná
265
10. Feladatok megoldása
meg a feladatot. Másrészt adott kezdőállapot esetén az S2 program a feladat által kijelölt célállapotok valamelyikében áll meg. Ha ugyanis máshol is meg tudna állni, akkor az S1 program is ilyen lenne, tehát az S1 program sem oldaná meg a feladatot. 1.10. Az F1 bővebb, mint az F2, ha F2 minden kezdő-célállapot párját tartalmazza. Igaz-e, hogy ha egy S program megoldja az F1 feladatot, akkor megoldja az F2-t is. Megoldás: Nem igaz. Egyfelől ugyan igaz, hogy S program az F2 feladat kezdőállapotaiból indulva biztosan terminál (hiszen ezek a kezdőállapotok egyben az F1 feladat kezdőállapotai is, és az S program megoldja az F1 feladatot. Másfelől azonban elképzelhető, hogy egy adott kezdőállapothoz az F1 feladat két célállapotot is rendel, mondjuk a-t és b-t, de ezek közül az F2 feladat csak a-t jelöli ki célállapotnak, ugyanakkor az S program a b-ben terminál. Az S program tehát megoldja az F1-et, de F2-öt nem.
266
2. fejezet feladatainak megoldása 2.1. Mit rendel az alább specifikált feladat a (10,1) és a (9,5) állapotokhoz? Fogalmazzuk meg szavakban a feladatot! (prím(x) = x prímszám.) A = ( k:ℤ, p:ℤ) Ef = ( k=k’ k>0 ) Uf = ( k=k’ prím(p)
i>1: (prím(i)
k–i
k–p ) )
Megoldás: „Állítsuk elő egy adott pozitív egész számhoz legközelebb eső prímszámot! (Néha kettő ilyen is van.)” (10,1)
(10,11), (9,5)
(9,7), (9,5)
(9,11)
2.2. Írjuk le szövegesen az alábbi feladatot! (A perm(x') az x' permutációinak halmaza.) A = (x:ℤn) Ef = ( x=x') Uf = ( x perm(x')
i,j [1..n]: (i<j
x[i]
x[j]) )
Megoldás: „Rendezzük növekvő sorba az x tömb elemeit!” 2.3. Specifikálja: Adott egy hőmérsékleti érték Celsius fokban. Számítsuk ki Farenheit fokban! Megoldás: A = ( c:ℝ, f:ℝ) Ef = ( c=c’ ) Uf = (Ef f=c*(9/5)+ 32 ) 2.4. Specifikálja: Adott három egész szám. Válasszuk ki közülük a legnagyobb értéket! Megoldás:
267
10. Feladatok megoldása
A = (a:ℤ, b:ℤ, c:ℤ, max:ℤ) Ef = ( a=a’ b=b’ c=c’ ) Uf = (Ef (a≥b a≥c) max=a (b≥a b≥c) max=b (c≥a c≥b) max=c ) 2.5. Specifikálja: Adottak egy ax+b=0 alakú elsőfokú egyenlet a és b valós együtthatói. Oldjuk meg! Megoldás: A = (a:ℝ, b:ℝ, x:ℝ, v: *) Ef = (a=a’ b=b’) Uf = ( Ef ( a=0 b=0 ) v=”Azonosság” ( a=0 b≠0 ) v=”Ellentmondás” a≠0 ( v=”Egy megoldás van”
x=–b/a ) )
2.6. Specifikálja: Adjuk meg egy természetes szám összes természetes osztóját! Megoldás: A = ( n:ℕ, h:2ℕ) Ef = ( n=n’ ) Uf = ( Ef h={d ℕ d n } ) 2.7. Specifikálja: Adjuk meg egy természetes szám valódi természetes osztóját! Megoldás: A = ( n:ℕ, l: , d:ℕ) Ef = ( n=n’ ) Uf = ( Ef l=( k [2 .. n-1]:k n) l (d [2 .. n–1] d n) = ( Ef l=( k ℕ: valódi_osztó(k,n)) l (valódi_osztó(d,n) ) 2.8. Specifikálja: Döntsük el, hogy prímszám-e egy adott természetes szám? Megoldás:
268
2. fejezet feladatainak megoldása
A = ( n:ℕ, l: ) Ef = ( n=n’ ) Uf = ( Ef l = (n 1 ( Ef l=prím(n) )
k [2 .. p–1]: (k p) ) =
2.9. Specifikálja: Adott egy sakktáblán egy királynő (vezér). Helyezzünk el egy másik királynőt úgy, hogy a két királynő ne üsse egymást! Megoldás: A = (x1:ℕ, y1:ℕ, x2:ℕ, y2:ℕ) Ef = (x1=x1’ y1=y1’ x1, y1 [1..8] ) Uf = ( Ef x2, y2 [1..8] ( (x1 x2 y1= y2) (x1= x2 y1 y2) ( x1 –x2 = y1– y2 )) ) 2.10. Specifikálja: Adott egy sakktáblán két bástya. Helyezzünk el egy harmadik bástyát úgy, hogy ez mindkettőnek az ütésében álljon! Megoldás: A = (x1:ℕ, y1:ℕ, x2:ℕ, y2:ℕ, l: , x:ℕ, y:ℕ) Ef = (x1=x1’ y1= y1’ x2=x2’ y2=y2’ x1, y1, x2, y2 [1..8]) Uf = ( Ef l = ( (x1=x2 y1– y2 1) (y1= y2 x1– x2 1) ) l ( x, y [1..8] (x1=x2) (x=x1 y1y2 y1>y>y2) (y1=y2) (y=y1 x1<x2 x1<x<x2 x1>x2 x1>x>x2 ) (x1 x2 y1 y2) (x=x1 y=y2 x=x2 y=y1) )
269
3. fejezet feladatainak megoldása 3.1. Fejezzük ki a SKIP, illetve az ABORT programot egy tetszőleges S program és valamelyik programszerkezet segítségével! Megoldás: SKIP:
ABORT: hamis
hamis
hamis
S
S1
S2
3.2. a) Van-e olyan program, amit felírhatunk szekvenciaként, elágazásként, és ciklusként is? Válasz: Igen. Például a SKIP ilyen. hamis
igaz
hamis
SKIP
S
SKIP
S
SKIP
b) Felírható-e minden program szekvenciaként, elágazásként, és ciklusként is? Válasz: Nem. Egy általános S program ciklusként nem írható fel. 3.3. a) Adjunk meg olyan végrehajtási sorozatokat, amelyeket az r:=p+q értékadás az A= (p:ℝ, q:ℝ, r:ℝ) állapottéren befuthat! Adjunk meg olyan feladatot, amelyet ez az értékadás megold!! Végrehajtások (1.1, 2.3, 0) < (1.1, 2.3, 0), (1.1, 2.3, 3.4)> (0, 0, 0) < (0, 0, 0), (0, 0, 0) > (–3, 0, 5) < (–3, 0, 5), (–3, 0, –3) > Feladat: A= (p:ℝ, q:ℝ, r:ℝ) Ef = ( p=p’ q=q’ ) Uf = ( Ef r=p+q ) b) Adjunk meg olyan végrehajtási sorozatokat, amelyeket az n,m:=3,n-5 értékadás az A= (n:ℕ, m:ℕ) állapottéren befuthat! Adjunk meg olyan feladatot, amelyet ez az értékadás megold!
270
3. fejezet feladatainak megoldása
Végrehajtások (7, 1) < (7, 1), (3, 2)> (3, 1) < (3, 1), (3, 1), (3, 1) …> Feladat: A= (n:ℕ, m:ℕ) Ef = ( n=n’ m=m’ n-5>0 ) Uf = ( Ef n=3 m= n–5 ) 3.4. Adjunk meg olyan végrehajtási sorozatokat, amelyeket a (r:=p+q; p:=r/q) szekvencia befuthat az A= (p:ℝ, q:ℝ, r:ℝ) állapottéren! Adjunk meg olyan egyetlen értékadásból álló programot, amely ekvivalens ezzel a szekvenciával! Végrehajtások (1,1,1) < (1,1,1), (1,1,2), (2,1,2) > (3,0,1) < (3,0,1), (3,0,3), (3,0,3), (3,0,3)…> (5,4,3) < (5,4,3), (5,4,9), (2.25,4,9) > Értékadás: p,r:=(p+q)/q, p+q 3.5. Igaz-e, hogy az alábbi három program ugyanazokat a feladatokat oldja meg? f1
f2
f3
S1
S2
S3
f1 S1
f1 SKIP
f2
f2 S2
f3 SKIP
S1
S2
S3
SKIP
f3 S3
SKIP
271
10. Feladatok megoldása
Válasz: Nem. Az első elágazás abortál, ha egyik feltétel sem teljesül, a másik kettő ilyenkor SKIP-ként működik. A második elágazás (S1;S2;S3) szekvenciaként működik, ha kezdetben f1 teljesül, majd S1 végrehajtása után f2 teljesül, végül S2 végrehajtása után f3 is teljesül. A harmadik program viszont csak S1 hajtja végre, ha kezdetben f1 teljesül. 3.6. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott elágazásnak! Mutassa meg, hogy a feladatot megoldja a program! A = (x:ℤ, n:ℕ) Ef = ( x=x’ ) Uf = ( n=abs(x’) ) x≥0
x 0
n:=x
n:=–x
Bizonyítás: Az elágazás feltételrendszere lefedi az összes előforduló esetet, azaz minden állapotra legalább az egyik feltétel teljesül. Ha x≥0, akkor az x abszolút értéke maga az x, ezért az n:=x értékadás megoldja a feladatot. Ha x 0, akkor az x abszolút értéke az x ellentetje, azaz n:=–x. 3.7. Oldjuk meg az ax2+bx+c=0 alakú egyenletet, amelynek adottak az a, b és c valós együtthatói! Specifikáció: A = ( a:ℝ, b:ℝ, c :ℝ, x1:ℝ, x2:ℝ, v: *) Ef =( a=a’ b=b’ c=c’ ) Uf = ( Ef (a 0 ( (b2+4ac < 0) v = „Nincs valós gyök” (b2+4ac = 0)
( v = „Közös valós gyök”
(b2+4ac > 0)
( v = „Két valós gyök”
x1
272
b
b2 2a
4ac
x2
b
b2 2a
x1
4ac
x2
))
b ) 2a
3. fejezet feladatainak megoldása
(a=0
( v = „Elsőfokú gyök”
(b 0
x1
c ) b
(b=0 c=0) v = „Azonosság” (b=0 c 0) v = „Ellentmondás” ) ) Algoritmus, amely az utófeltétel által sugallt többszörös elágazás lesz. a 0 d : b2
b 0
b=0 c=0
b=0 c 0
válasz:= Elsőfokú gyök
válasz := A z o n o s s á g
válasz := E l l e n t m o n d á s
4ac
d<0
d=0
d>0
válasz:= Nincs valós gyök
válasz:= Közös valós gyök
válasz:= Két valós gyök
x1 , x 2 :
x1 , x 2 :
b 2a
b d 2a
x1
c b
3.8. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott ciklusnak! Mutassa meg, hogy a feladatot megoldja a program. (Segítségül megadtuk a ciklus egy invariáns állítását és a hátralevő lépésszámra adható felső becslést.) A = (x:ℤ) Ef = ( igaz ) Uf = ( x = 0 ) x 0 x:=x – sgn(x)
legfeljebb abs(n) hátralevő lépés invariáns állítás: igaz
273
10. Feladatok megoldása
Bizonyítás: A feladat előfeltételéből következik az invariáns állítás, hiszen mindkettő az igaz állítás. Az invariáns állítás valóban invariáns, hiszen a ciklusmag végrehajtása után is teljesülni fog az igaz állítás. Ha egy állapotra az invariáns mellett a ciklusfeltétel tagadottja is teljesül, akkor teljesül a feladat utófeltétele is, hiszen mindkettő az x=0 állítás. Ezzel a haladási kritériumot igazoltuk. A leállási kritériumhoz azt kell csak látni, hogy az abs(x) értéke pozitív, ha a ciklusfeltétel igaz, és ilyenkor a ciklusmag eggyel csökkenti az értékét. Így véges lépésen belül olyan állapotba fogunk eljutni, amelyre a ciklusfeltétel hamis. 3.9. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott programnak! Mutassa meg, hogy a feladatot megoldja a program. (Segítségül megadtuk a ciklus egy invariáns állítását és a hátralevő lépésszámra adható felső becslést.) A = (n:ℕ, f:ℕ) Ef = ( n=n’) Uf = ( f=n’! ) f:=1 n 0 f:=f n
legfeljebb n hátralevő lépés ivariáns állítás: f n!=n’!
n:=n–1 Bizonyítás: A program egy értékadással indul, amely hatására egy (n’,1) alakú állapotba jutunk. Ez után egy ciklus következik. Az (n’,1) alakú állapotok megfelelő kiinduló állapotai a ciklusnak, mert biztosan kielégítik a ciklus invariáns állítását ( 1 n’!=n’! ). Az invariáns állítás valóban invariáns, hiszen a ciklusmag két értékadása egy invariánst kielégítő (n0,f0) állapotból (f0 n0!=n’!) az (n0–1, f0 n0) állapotba vezet, amelyre ugyancsak kielégíti az invariánst: „f0 n0 (n0–1)!=n’!” Ha egy állapotra az invariáns mellett a ciklusfeltétel tagadottja is teljesül, akkor teljesül a feladat utófeltétele is (hiszen f n!=n’! n=0 f=n’!). Ezzel a haladási kritériumot igazoltuk.
274
3. fejezet feladatainak megoldása
A leállási kritériumhoz azt kell csak látni, hogy az n változó értéke mindig pozitív, ha a ciklusfeltétel teljesül és minden iterációban eggyel csökken. Így véges lépésen belül olyan állapotba fogunk eljutni, amelyre a ciklusfeltétel hamis. 3.10. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási sorozatát a megadott programnak! Mutassa meg, hogy a feladatot megoldja a program. (Segítségül megadtuk a ciklus egy invariáns állítását és a hátralevő lépésszámra adható felső becslést.) A = (x:ℕ, n:ℕ, z:ℕ) Ef = (n=n’ x=x’) Uf = ( z=(x’)n’ ) z := 1 n 0 n páros
legfeljebb n hátralevő lépés ivariáns állítás: z xn=(x’)n’
x, n := z, n := x x, n/2 z x, n–1 Bizonyítás: A program egy értékadással indul, amely hatására egy (x’,n’,1) alakú állapotba jutunk. Ezután egy ciklus következik, amelynek invariánsát ez az állapot kielégíti, hiszen 1 (x)n=(x’)n’. Az invariáns állítás valóban invariáns, mert a ciklusmag egy invariánst kielégítő (x0,n0,z0) állapotból (z0 (x0)n0=(x’)n’) indulva az n0 párossága esetén az (x0 x0,n0/2,z0) állapotba jut, amelyre z0 (x0 x0)n0/2=(x’)n’ invariáns fennáll. Az n0 páratlansága esetén viszont az (x0,n0–1,x0 z0) állapotba jut, amelyre teljesül az x0 z0 (x0)n0–1=(x’)n’ invariáns. Továbbá, ha egy állapotra az invariáns mellett a ciklusfeltétel tagadottja is teljesül, akkor teljesül a feladat utófeltétele is (hiszen z xn=(x’)n’ n=0 z=(x’)n’). Ezzel a haladási kritériumot igazoltuk. A leállási kritériumhoz azt kell csak látni, hogy az n változó értéke mindig pozitív, ha a ciklusfeltétel teljesül és minden iterációban legalább eggyel csökken. Így véges lépésen belül olyan állapotba fogunk eljutni, amelyre a ciklusfeltétel hamis.
275
4. fejezet feladatainak megoldása 4.1. Számoljuk ki két természetes szám szorzatát, ha a szorzás művelete nincs megengedve! Specifikáció: A = ( x:ℕ, y:ℕ, z:ℕ) Ef = ( x=x’ y=y’ ) x
Uf = ( Ef
z=x*y ) = ( Ef
y)
z i 1
Összegzés: m..n ~ s ~ z f(i) ~
1..x y
Algoritmus: z, i :=0, 1 i
z := 0 i:ℕ
x z := z+y
rövidebben
i = 1 .. x
i:ℕ
z := z+y
i := i+1 4.2. Keressük meg egy természetes szám legkisebb páros valódi osztóját! Specifikáció: A = ( n:ℕ, l: , d:ℕ) Ef = ( n=n’ ) n/2
Uf = ( Ef
l, d
search(i n 2 i) ) i 2
Lineáris keresés: m..n ~ 2 .. n div 2 ind ~ d (i) ~ i|n 2|i
Algoritmus:
276
4. fejezet feladatainak megoldása
l, i:=hamis, 2 l
i
i:ℕ
n div 2
l, d:= i|n
2|i, i
i := i+1 4.3. Válogassuk ki egy egész számokat tartalmazó tömbből a páros számokat! Specifikáció: A = ( x:ℤn, y:ℤn, k:ℕ) Ef = ( x=x’ ) n
Uf = ( Ef
y[1..k]=
x[i ] ) (
az összefűzés jele)
i 1 2 x[ i ]
Összegzés: m..n ~ 1..n s ~ y[1..k] f(i) ~ ha x[i] páros akkor <x[i]> különben <> Algoritmus: k, i:=0, 1 k := 0 i:ℕ i
i = 1 .. n
n
x[i] páros k := k+1 y[k] := x[i]
SKIP
vagy
i:ℕ
x[i] páros k := k+1
SKIP
y[k] := x[i]
i := i+1 Egymást követő napokon megmértük a déli hőmérsékletet. Hányadikra mértünk először 0 Celsius fokot azelőtt, hogy először fagypont alatti hőmérsékletet regisztráltunk volna? Specifikáció:
277
10. Feladatok megoldása
A = ( x:ℝ, l: , d:ℕ) Ef = ( x=x’ ) n 1
Uf = ( Ef
search( x[i] 0
l, d
i 1
Lineáris keresés: m..n ~ 1..n-1 ind ~ d (i) ~ x[i]=0 x[i+1]<0 Algoritmus: l, i:=hamis, 1 l
i
x[i 1] 0) )
i:ℕ
n-1
l, d:= x[i]=0
x[i+1]<0, i
i := i+1 4.4. Egy tömbben a tornaórán megjelent diákok magasságait tároljuk. Keressük meg a legmagasabb diák magasságát! Specifikáció: A = (x:ℕn, max:ℕ) Ef = ( x=x’ 1 n ) Uf = ( Ef
n
max = max x[i] ) i 1
Maximum kiválasztás: (az maximum indexének elhagyásával) m..n ~ 1..n f(i) ~ x[i] Algoritmus: max, i := x[1], 2 max := x[1] i:ℕ vagy
i n max< x[i] max := x[i]
SKIP
i = 2 .. n
i:ℕ
max< x[i] max := x[i]
SKIP
i := i + 1 4.5. Az f:[m..n] ℤ függvénynek hány értéke esik az [a..b] vagy a [c..d] intervallumba?
278
4. fejezet feladatainak megoldása
Specifikáció: A = (m, n, a, b, c, d:ℤ, db:ℕ) Ef = ( m=m’ n=n’ a=a’ b=b’
c=c’
d=d’ )
n
Uf = ( Ef
db
1) i m f (i ) [ a..b] [c..d ]
Számlálás: m..n ~ m..n c ~ db (i) ~ a f(i) b Algoritmus: db, i := 0, m
db := db+1
i:ℤ vagy
i n a f(i) b
c f(i) d db := 0 i = m .. n
c f(i) d
a f(i) b
SKIP
db := db+1
i:ℤ
c f(i) d SKIP
i := i + 1 4.6. Egy tömbben színeket tárolunk az színskála szerinti növekvő sorrendben. Keressük meg a tömbben a világoskék színt! Specifikáció: A = (x:Színn, l: ) Ef = ( x=x’ x rendezett ) Uf = ( Ef l=( i [m..n]: x[i]=világoskék) l (i [m..n] x[i]=világoskék) ) Logaritmikus keresés: m..n ~ 1..n (i) ~ x[i]=világoskék
Algoritmus:
279
10. Feladatok megoldása
ah, fh, l :=1, n, hamis l
ah fh
i := (ah+fh) div 2
i:ℤ
f(i)>h
f(i)
f(i)=h
fh := i-1
ah := i+1
l := igaz
4.7. A Föld felszínének egy vonala mentén egyenlő távolságonként megmértük a terep tengerszint feletti magasságát (méterben), és a mért értékeket egy tömbben tároljuk. Melyik a legmagasabban fekvő horpadás a felszínen? Specifikáció: A = ( x:ℝn, ind:ℕ) Ef = ( x=x’ ) n 1
Uf = ( Ef
l,max,ind =
max
x[i ] )
i 2 x[i 1] x[i ] x[i 1]
Feltételes maximum keresés: m..n ~ 2..n-1 f(i) ~ x[i] (i) ~ x[i–1]>x[i]<x[i+1] Algoritmus: l := hamis i = 2 .. n-1 (x[i–1]>x[i] l x[i–1]>x[i] x[i]<x[i+1] x[i]<x[i+1]) SKIP
max< x[i] max, ind := x[i], i
SKIP
i:ℕ l
x[i–1]>x[i] x[i]<x[i+1]
l, max, ind := igaz, x[i], i
4.8. Egymást követő napokon megmértük a déli hőmérsékletet. Hányszor mértünk 0° Celsiust úgy, hogy közvetlenül utána fagypont alatti hőmérsékletet regisztráltunk? Specifikáció:
280
4. fejezet feladatainak megoldása
A = ( x:ℤn, db:ℕ) Ef = ( x=x’ ) n 1
db
Uf = ( Ef
1) i 1 x[i] 0 x[i 1] 0 ]
Számlálás: m..n ~ c ~ db (i) ~ Algoritmus:
1..n-1 x[i]=0
x[i+1]<0
db := 0 i = 1 .. n-1 x[i]=0
i:ℕ
x[i+1]<0
db := db+1
SKIP
4.9. Egy n elemű tömbben tárolt egész számoknak az összegét egy rekurzív függvénv n-edik helyen felvett értékével is megadhatjuk. Definiáljuk ezt a rekurzív függvényt és oldjuk meg a feladatot a rekurzív függvény helyettesítési értékét kiszámoló programozási tétel segítségével! Specifikáció: A = (x:ℤn, s:ℤ) Ef = ( x=x’ ) Uf = ( Ef s=f(n) )ahol f(0)=0 és i [1..n]: f(i)=f(i–1)+x[i] Rekurzív függvény helyettesítési értéke: k, m ~ 1, 1 y ~ s em–1 ~ 0 h(i,f(i–1)) ~ f(i–1)+x[i] Algoritmus: s := 0 i = 1 .. n
i:ℕ
s := s+x[i]
281
5. fejezet feladatainak megoldása 5.1. Adott a síkon néhány pont a koordinátáival. Keressük meg az origóhoz legközelebb eső pontot! Specifikáció: A = (x:ℝn, y:ℝn, ind:ℕ) Ef = ( x=x’ y=y’ n>0 ) Uf = ( Ef
n
min, ind = min ( x[i]2
y[i]2 ) )
i 1
(Az x[i] +y[i] az (x[i], y[i]) koordinátájú pont origótól vett távolságának négyzete.) Minimum kiválasztás: m..n ~ 1..n f(i) ~ x[i]2+y[i]2 max ~ min 2
2
min, ind:= x[1]2+y[1]2, 1 i = 2 .. n
i:ℕ
min > x[i]2+y[i]2 min, ind:= x[i]2+y[i]2, i
SKIP
5.2. Egy hegyoldal hegycsúcs felé vezető ösvénye mentén egyenlő távolságonként megmértük a terep tengerszint feletti magasságát, és a mért értékeket egy vektorban tároljuk. Megfigyeltük, hogy ezek az értékek egyszer sem csökkentek az előző értékhez képest. Igaz-e, hogy mindig növekedtek? Specifikáció: A = (x:ℝn, l: ) Ef = ( x=x’ i [2..n]:x[i] x[i–1] ) Uf = ( Ef l = i [2..n]:x[i] x[i–1] ) = n
= ( Ef
l
search( x[i] i 2
x[i 1]) )
Optimista lineáris keresés (eldöntés):
282
6. fejezet feladatainak megoldása
m..n (i)
~ ~
2..n x[i] x[i–1]
l,i:= igaz, 2 l
i:ℕ
i n
l := x[i] x[i–1] i := i+1 5.3. Határozzuk meg egy egész számokat tartalmazó tömb azon legkisebb értékét, amely k-val osztva 1-et ad maradékul! Specifikáció: A = (x:ℤn, l: min:ℤ, ind:ℕ) Ef = ( x=x’ ) n
Uf = ( Ef
l, min, ind =
min x[i ] )
i 1 x[ i ] mod k 1
Feltételes minimumkeresés: m..n ~ 1..n f(i) ~ x[i] (i) ~ x[i] mod k =1 max ~ min l := hamis i: ℤ
i = m .. n x[i] mod k 1
l x[i] mod k =1
SKIP
min< x[i] min, ind:= x[i], i
SKIP
l x[i] mod k =1 l, min, ind := igaz, x[i], i
5.4. Adottak az x és y vektorok, ahol az y elemei az x indexei közül valók. Keressük meg az x vektornak az y-ban megjelölt elemei közül a legnagyobbat! Specifikáció:
283
10. Feladatok megoldása
A = (x:ℝm, y:ℕn, max:ℝ, ind:ℕ) Ef = ( x=x’ n>0 m>0 j [1..n]:y[j] [1..m] ) Uf = ( Ef
n
max, ind = max x[ y[i]] ) i 1
Maximum kiválasztás: m..n ~ 1..n f(i) ~ x[y[i]] max, ind := x[y[1]], 1 i: ℕ
i = 2 .. n max <x[y[i]] max, ind:= x[y[i]], i
SKIP
5.5. Igaz-e, hogy egy tömbben elhelyezett szöveg odafelé és visszafelé olvasva is ugyanaz? Specifikáció: A = (x: n, l: ) Ef = ( x=x’ ) Uf = ( Ef l = i [1.. n div 2]:(x[i]=x[n–i+1]) ) = ndiv2
= ( Ef
l= search ( x[i] i 1
x[n i 1]) )
Optimista lineáris keresés: m..n ~ 1.. n div2 (i) ~ x[i]=x[n–i+1] l, i:= igaz, 1 l
i
i:ℤ
n div2
l := x[i]=x[n–i+1] i := i+1 5.6. Egy mátrix egész számokat tartalmaz sorfolytonosan növekedő sorrendben. Hol található ebben a mátrixban egy tetszőlegesen megadott érték!
284
6. fejezet feladatainak megoldása
Specifikáció: A = (t:ℤ0..n-1×0..m-1, l: , ind:ℕ, jnd:ℕ) Ef = ( t=t’ t sorfolytonosan növekedő ) Uf = ( Ef l= k [0..nm–1]:(t[k div m, k mod m]=h) l (k [0..nm–1] ind=k div m jnd = k mod m t[ind, jnd]=h) ) Logaritmikus keresés: m..n ~ 0.. nm–1 i ~ k (i) ~ t[k div m, k mod m]=h ah ,fh, l :=0, nm–1, hamis l
ah fh
k :=(ah+fh) div 2
k:ℕ
t[k div m, k mod m] >h
=h
fh := k–1
ah := k+1
l := igaz
l ind, jnd := k div m, k mod m
SKIP
5.7. Keressük meg két természetes szám legkisebb többszörösét! Specifikáció: A =(n:ℕ, m:ℕ, d:ℕ) Ef = ( n=n’ n=m’ ) Uf = ( Ef n|d m|d i [n..d–1]: (n|i m|i) ) = = ( Ef d= select (n d m d ) )
közös
d n
Kiválasztás: m ~ n i ~ d (i) ~
n|d
m|d
285
10. Feladatok megoldása
d: = n (n|d
m|d)
d := d+1 5.8. Keressük meg két természetes szám legnagyobb közös osztóját! Specifikáció: A = (n:ℕ, m:ℕ, d:ℕ) Ef = ( n=n’ n=m’ ) Uf = ( Ef d|n d|m i [d+1..n]: (i|n i|m) ) = = ( Ef d= select (d n d m) ) d n
Kiválasztás: m ~ n i ~ d (i) ~
n|d
m|d
d := n (d|n
d|m)
d := d–1 5.9. Prím szám-e egy egynél nagyobb egész szám? Specifikáció: A = (n:ℕ, l: ) Ef = ( n=n’ n>1 ) Uf = ( Ef l = i [2.. n ]: i|n ) = n
l
= ( Ef
search i n] ) i 2
Optimista lineáris keresés: m..n (i)
286
~ ~
1.. n i|n
6. fejezet feladatainak megoldása
l, i:= igaz, 2 l
i
i:ℕ
n
l := i|n i := i+1 5.10. Egy n elemű tömb egy b alapú számrendszerben felírt természetes szám számjegyeinek értékeit tartalmazza úgy, hogy a magasabb helyiértékek vannak elől. Módosítsuk a tömböt úgy, hogy egy olyan természetes szám számjegyeit tartalmazza, amely az eredetinél eggyel nagyobb, és jelezzük, ha ez a megnövelt érték nem ábrázolható n darab helyiértéken! Specifikáció: A = (x:{0..9}n, c:{0,1}) Ef = ( x=x’ ) Uf = ( (x,c) = f(n) ) f:[0 .. n] {0..9}n×{0,1} f(0) = (x’,1) i [1..n–1]: ha f(i)2=0 akkor f(i+1) = f(i) különben f(i+1)1 = f(i)1f(i)1[n i ] ( f(i)1[n i ] 1) mod 10 f(i+1)2 = ( f(i)1[n i] 1) div 10 Rekurzív függvény helyettesítési értéke: (Nem ez a leghatékonyabb megoldás.) k, m ~ 1, 0 y ~ x, c em–1 ~ x’, 1 y:=h(i,f(i–1)) ~ x[n–i], c:=(x[n–i]+1) mod 10, (x[n–i]+1) div 10 c := 1 i = 0 .. n
i:ℕ
c=0 SKIP
x[n–i], c := (x[n–i]+1) mod 10, (x[n–i]+1) div 10
287
10. Feladatok megoldása
6. fejezet feladatainak megoldása 6.1. Volt-e a lóversenyen olyan napunk, amikor úgy nyertünk, hogy a megelőző k napon mindig veszítettünk (Oldjuk meg kétféleképpen is a feladatot: lineáris keresésben egy lineáris kereséssel, illetve lineáris keresésben egy rekurzív függvénnyel!) a) Specifikáció: A = (x:ℤn, k:ℕ, l: ) Ef = ( x=x’ k=k’ ) Uf = ( Ef l = i [k+1..n]:(x[i]>0 múlt(i)) = n
l
= ( Ef
search ( x[i ] 0 i k 1
ahol múlt:[k+1..n]
múlt(i)) )
és i 1
múlt(i) = j [i–k..i–1]: x[j]<0 =
search ( x[ j ] 0 ) j i k
Lineáris keresésbe ágyazott optimista lineáris keresés: l, i:= hamis, k+1 l
l, j:= igaz, i–k
i n
l := x[i]>0
i: ℕ l:=múlt(i)
múlt(i)
i := i+1
l
j i–1 l := x[j]<0 j :=j+1
b) Specifikáció:
288
j: ℕ
6. fejezet feladatainak megoldása
A = (x:ℤn, k:ℕ, l: ) Ef = ( x=x’ k=k’ ) Uf = ( Ef l= i [2..n]:(x[i]>0
múlt(i)=k) )=
n
= ( Ef
l
search ( x[i ] 0 i 2
múlt(i)
k) )
ahol múlt(1) = 0 és múlt(i 1) 1 ha x[i 1] 0
múlt(i) =
0
ha x[i 1] 0
Lineáris keresésben rekurzív függvény kibontása: l, i:= hamis, 2 l
l, i, m:= hamis, 2, 0
i:ℕ
i n
l := x[i]>0
l
múlt(i)=k
i := i+1
i n
x[i–1]<0 m:ℕ
m := m+1
m:=0
l := x[i]>0
m=k
i := i+1 6.2. Egymást követő napokon délben megmértük a levegő hőmérsékletét. Állapítsuk meg, hogy melyik érték fordult elő leggyakrabban! A feladat megoldásakor azt a napot határozzuk meg, amelyiken mért hőmérséklet a leggyakrabban fordult elő. Specifikáció: A = (x:ℤn, nap:ℕ) Ef = ( x=x’ ) n
Uf = ( Ef
max, nap= max hány(i) ) i 1
ahol hány:[1..n] ℕ és hány(i) =
i 1
1 j 1
x[ j ] x[i ]
Maximum kiválasztásba ágyazott számlálás:
289
10. Feladatok megoldása
max, nap:= 1, 1
h:=hány(i)
i := 2 .. n
h := 0
i:ℕ
h := hány(i)
j = 1 .. i-1
h>max
j:ℕ
x[j]=x[i]
max, nap:=h, i SKIP
h := h+1
SKIP
6.3. A tornaórán névsor szerint sorba állítottuk a diákokat, és megkérdeztük a testmagasságukat. Hányadik diákot előzi meg a legtöbb nála magasabb? Specifikáció: A = (x:ℕn, diák:ℕ) Ef = ( x=x’ ) n
Uf = ( Ef
max,diák = max hány (i ) ) i 1
ℕ és hányt(i) =
ahol hány:[1..n]
i 1
1 j 1
x[ j ] x[i ]
Maximum kiválasztásba ágyazott számlálás: max, diák := 0, 1
h:=hány(i)
i = 2 .. n
h := 0
i:ℕ
h := hány(i)
j = 1 .. i–1
h>max
x[j]>x[i]
max, diák:=h, i SKIP
h := h+1 SKIP
j: ℕ
6.4. Állapítsuk meg, hogy egy adott szó (egy karakterlánc) előfordul-e egy adott szövegben (egy másik karakterláncban)! a) Specifikáció: A = (s: n, p: m, l: ) Ef = ( s=s’ p=p’ ) Uf = (Ef l = i [1..n–m+1]: (s[i..i+m–1]=p[1..m]) = = ( Ef
l
n m 1
m
i 1
j 1
search ( search ( s[i
j 1]
p[ j ])) )
Lineáris keresésbe ágyazott optimista lineáris keresés:
290
6. fejezet feladatainak megoldása
l, i:= hamis, 1 l
i:ℕ
l := s[i..i+m–1]= p[1..m] l, j:= igaz, 1
i n–m+1
l :=
l
j:ℕ
j m
s[i..i+m–1]=p[1..m]
l := s[i+j–1]= p[j]
i := i+1
j := j+1
6.5. Keressük meg a t négyzetes mátrixnak azt az oszlopát, amelyben a főátlóbeli és a feletti elemek összege a legnagyobb! Specifikáció: A = (t:ℝn×n, oszlop:ℕ) Ef = ( t=t’ ) n
Uf = ( Ef
max,oszlop = max (összeg ( j )) ) j 1
ahol összeg:[1..n] ℝ és összeg(j) =
j
t[i, j ] , és például i 1
összeg(1) = t[1,1]. Maximum kiválasztásba ágyazott összegzés: max, oszlop:= t[1,1], 1 j = 2 .. n
j:ℕ
s := összeg(j)
s := 0 i = 1 .. j
s>max max, oszlop:=s, j
s:=összeg(i)
i:ℕ
s := s+t[i,j] SKIP
6.6. Egy határállomáson feljegyezték az átlépő utasok útlevélszámát. Melyik útlevélszámú utas fordult meg leghamarabb másodszor a határon? A feladat kétféleképpen is értelmezhető. Vagy azt az utast keressük, akiről legelőször derül ki, hogy korábban már átlépett a határon, vagy azt, akinek két egymást követő átlépése közt a lehető legkevesebb másik utast találjuk. a) Specifikáció: A = (x:( *)n, l: , szám: *) Ef = ( x=x’ )
291
10. Feladatok megoldása
Uf = ( Ef = ( Ef
l = i [1..n]: j [1..i–1]: x[i]= x[j] ) = n
i 1
i 1
j 1
search ( search ( x[i]
l , ind
x[ j ]))
l szám=x[ind] ) Lineáris keresésbe ágyazott lineáris keresés: l,i:= hamis, 1 l
i:ℕ
l := belső keresés(i) l,j := hamis, 1
i n
l := belső keresés(i)
l
j:ℕ
j i–1
szám :=x[i]
l := x[i]= x[j]
i := i+1
j := j+1
b) Specifikáció: A = (x:( *)n, l: , i:ℕ) Ef = ( x=x’ ) n
Uf = ( Ef
l
min ( k
f 2(k)) )
k 1 f 1(k)
ahol f:[1..n] ×ℕ egy kétértékű függvény, azaz f1(k) egy logikai érték, f2(k) pedig egy természetes szám. 1
k [1..n]: f(k) - keres ( x[k ] j k 1
x[ j ])
Feltételes minimum keresésbe ágyazott fordított lineáris keresés: l := hamis k := 1 .. n
k:ℕ
ll, d := f(k) ll SKIP
l
ll
min>k–d min, i := k–d, k SKIP
292
ll: , d:ℕ l
ll
l, min, i := igaz, k–d, k
6. fejezet feladatainak megoldása
ll, d := f(k) ll, j := hamis, k-1 ll
j:ℕ
j 1
ll, d := x[k]=x[j], j j := j–1 6.7. Egymást követő hétvégeken feljegyeztük, hogy a lóversenyen mennyit nyertünk illetve veszítettünk (ez egy előjeles egész szám). Hányadik héten fordult elő először, hogy az összesített nyereségünk (az addig nyert illetve veszített pénzek összege) negatív volt? Specifikáció: A = (x:ℤn, l: , i:ℕ) Ef = ( x=x’ k=k’ ) n
Uf = ( Ef
l, i
search (nyereség ( i ) 0) ) i 1
i
ahol nyereség:[0..n] ℕ és nyereség(i) =
x[ j ] j 1
vagy nyereség(0)=0 és nyereség(i)=nyereség(i–1)+x[i] Lineáris keresésbe ágyazott rekurzív függvény: l, i:= hamis, 1 l
i:ℕ
i n
l
l := nyereség(i)<0 i := i+1
l, i, ny:= hamis, 1, 0 i n
ny := ny+x[i] ny:ℤ
l := ny<0 i := i+1
6.8. Döntsük el, hogy egy természetes számokat tartalmazó mátrixban e) van-e olyan sor, amelyben előfordul prím szám (van-e a mátrixban prím szám) Specifikáció:
293
10. Feladatok megoldása
A = (t:ℕn×m, l: ) Ef = ( t=t’ ) n
Uf = ( Ef
l
ahol prím:ℕ
m
search ( search prím (t[i, j ])) ) i 1
j 1
és prím(a) = (a>1
a
search( (k a)) ) k 2
Keresésben keresésbe ágyazott optimista keresés: 25 l := prím(t[i,j])
l := sor(i) l, i:= hamis, 1 l
l, j := hamis, 1
i n
l
l, k := t[i,j]>1, 2
j m
l
j
t[i, j ]
l := sor(i)
l := prím(t[i,j])
l := (k t[i,j])
i := i+1
j := j+1
k := k+1
f) van-e olyan sor, amelyben minden szám prím A = (t:ℕn×m, l: ) Ef = ( t=t’ ) n
Uf = ( Ef
l
ahol prím:ℕ
m
search( search prím (t[i, j ])) ) i 1
j 1
és prím(a) = (a>1
a
search( (k a)) ) k 2
Keresésben optimista keresésbe ágyazott optimista keresés: l := sor(i) l, i:= hamis, 1 l
i n
l, j := igaz, 1 l
j m
l := prím(t[i,j]) l, k := t[i,j]>1, 2 l
j
t[i, j ]
l := sor(i)
l := prím(t[i,j])
l := (k t[i,j])
i := i+1
j := j+1
k := k+1
g) minden sorban van-e legalább egy prím 25
Mostantól kezdve nem tüntetjük fel külön a programok segédváltozóit.
294
6. fejezet feladatainak megoldása
A = (t:ℕn×m, l: ) Ef = ( t=t’ ) n
Uf = ( Ef
l
ahol prím:ℕ
m
search ( search prím (t[i, j ])) ) i 1
j 1
és prím(a) = (a>1
a
search( (k a)) ) k 2
Optimista keresésben lineáris keresésbe ágyazott optimista keresés: l := prím(t[i,j])
l := sor(i) l ,i:= igaz, 1 l
l, j := hamis, 1
i n
l
l, k := t[i,j]>1, 2
j m
l
j
t[i, j ]
l := sor(i)
l := prím(t[i,j])
l := (k t[i,j])
i := i+1
j := j+1
k := k+1
h) minden sorban minden szám prím (a mátrix minden eleme prím) A = (t:ℕn×m, l: ) Ef = ( t=t’ ) n
Uf = ( Ef
l
ahol prím:ℕ
m
search( search prím (t[i, j ])) ) i 1
j 1
és prím(a) = (a>1
a
search( (k a)) ) k 2
Optimista keresésben optimista keresésbe ágyazott optimista keresés: l := prím(t[i,j])
l := sor(i) l, i:= igaz, 1 l
i n
l, j := igaz, 1 l
j m
l, k := t[i,j]>1, 2 l
j
t[i, j ]
l := sor(i)
l := prím(t[i,j])
l := (k t[i,j])
i := i+1
j := j+1
k := k+1
295
10. Feladatok megoldása
6.9. Egy kutya kiállításon n kategóriában m kutya vesz részt. Minden kutya minden kategóriában egy 0 és 10 közötti pontszámot kap. Van-e olyan kategória, ahol a kutyák között holtverseny (azonos pontszám) alakult ki? Specifikáció: A = (t:ℕn×m, l: ) Ef = ( t=t’ ) n
l
Uf = ( Ef
search (ért 1i (m) 1) ) i 1
ahol ért :[0..m] ℕ×ℕ egy rekurzív függvény, amelynek j-edik helyen felvett második értéke (max) azt mutatja, hogy az i-edik kategóriában az első j pontszám közül melyik a legnagyobb, az első értéke (db) pedig azt, hogy ez a pontszám az első j pontszám között hányszor fordult elő. i
érti
(0)=
(0,-1)
(ért 1i (j 1) 1, ért 2i (j 1)) ha t[i, j ]
ért i (j)
(1, t[i, j ])
ha t[i, j ]
(ért 1i (j 1), ért 2i (j 1))
ért 2i (j 1) ért 2i (j 1)
ha t[i, j ] ért 2i (j 1)
Lineáris keresésben rekurzív függvény: l ,i := hamis, 1 db:=érti1(m) l
j n
db, max :=0, -1
l := érti1(m)>1 i := i+1
j = 0 .. m t[i,j]=max
t[i,j]>max
db := db+1 db, max := 1,t[i,j]
t[i,j]<max SKIP
Az alprogram futási ideje gyorsítható, ha az közvetlenül az l:=érti1(m)>1 részfeladatot oldja meg, de úgy, hogy amikor a rekurzív függvény db komponense eléri a 2-t, akkor leáll, azaz az l := j [1..m]: érti1(m)=2 feladatra ad megoldást. Ekkor ezt egy lineáris keresésre
296
6. fejezet feladatainak megoldása
vezethetjük vissza, amelyen belül a rekurzív függvény kibontására kerül sor. l := j [1..m]: érti1(m)=2 l, j, db, max := hamis, 1, 0, -1 j ≤m
l t[i,j]=max
t[i,j]>max
t[i,j]<max
db := db+1
db, max := 1,t[i,j]
SKIP
l:=db=2 j:=j+1 6.10. Madarak életének kutatásával foglalkozó szakemberek n különböző településen m különböző madárfaj előfordulását tanulmányozzák. Egy adott időszakban megszámolták, hogy az egyes településen egy madárfajnak hány egyedével találkoztak. Hány településen fordult elő mindegyik madárfaj? Specifikáció: A = (t:ℕn×m, db:ℕ) Ef = ( t=t ’) n
Uf = ( Ef
db
1) i 1 mind(i)
ahol mind:[1..n]
és mind(i) =
m
search (t[i, j ] 0 ) j 1
Számlálásban optimista keresés: db := 0
l:=mind(i)
i = 1 .. n
l, j := igaz, 1
mind(i) db := db+1
l SKIP
j m
l := t[i,j]>0 j := j+1
297
7. fejezet feladatainak megoldása 7.1. Valósítsuk meg a racionális számok típusát úgy, hogy kihasználjuk azt, hogy minden racionális szám ábrázolható két egész számmal, mint azok hányadosa! Implementáljuk az alapműveleteket! Megoldás: ℚ
A = (a:ℚ, b:ℚ, c:ℚ) c := a b A = (a:ℚ, b:ℚ, c:ℚ) c := a*b A = (a:ℚ, b:ℚ, c:ℚ) c := a/b
:ℤ×ℤ ℚ
(x1, x2) = x1/x2
I(x1, x2) = x2 0
a-t az x1, x2, b-t az y1, y2, c-t a z1, z2 reprezentálja ahol x1, x2, y1, y2, z1, z2:ℤ ℤ×ℤ második szám nem nulla
z1, z2 := x1*y2 x2*y1, x2*y2 z1, z2 := x1*y1, x2*y2 z1, z2 := x1*y2, x2*y1
7.2. Valósítsuk meg a komplex számok típusát! Ábrázoljuk a komplex számokat az algebrai alakjukkal (x+iy)! Implementáljuk az alapműveleteket! Megoldás: A = (a:, b:ℂ, c:ℂ) ℂ c := a b A = (a:ℂ, b:ℂ, c:ℂ) c := a*b A = (a:ℂ, b:ℂ, c:ℂ) c := a/b
298
8. fejezet feladatainak megoldása
:ℝ×ℝ ℂ
(x1, x2) = x1 + i x2 a-t az x1, x2, b-t az y1, y2, c-t a z1, z2 reprezentálja ahol x1, x2, y1, y2, z1, z2:ℝ
ℝ×ℝ
z1, z2 := x1 y1, x2 y2 z1, z2 := x1y1 – x2y2 , x1y2 + x2y1 z1, z2 := (x1x2+y1y2)/(x12+y12), (x1y2–y1x2)/(x12+y12)
7.3. Valósítsuk meg a síkvektorok típusát! Implementáljuk két vektor összegének, egy vektor nyújtásának, és forgatásának műveleteit! Megoldás: Vektor A = (a:V, b:V, c:V) c := a+b A = (a:V, k:ℝ, c:V) c := k*a A = (a:V, :ℝ, c:V) c := F (a) :ℝ×ℝ V
(x1, x2) = Px1 x 2 a-t az x1, x2, b-t az y1, y2, c-t a z1, z2 reprezentálja ahol x1, x2, y1, y2, z1, z2, k, :ℝ
ℝ×ℝ
z1, z2 := x1+y1, x2 + y2 z1, z2 := k*x1, k* x2 z1, z2 := x1 cos
– x2 sin , x1 sin
+ x2 cos
7.4. Valósítsuk meg a négyzet típust! Ennek értékei a sík négyzetei, amelyeket el lehet tolni egy adott vektorral, és fel lehet nagyítani!
299
10. Feladatok megoldása
Megoldás: Ábrázoljuk a négyzetet (N) a középpontjával és az egyik átlójának felével, azaz két vektorral. A vektor típust (V) az előző feladatban már megvalósítottuk. A négyzet csúcsait úgy kaphatjuk meg, hogy a középponthoz hozzáadjuk a félátlót, annak ellentetjét, a derékszögű elforgatottját, és ennek ellentetjét. N
A = (a:N, v:V, c:N) c := eltol(a,v) A = (a:N, k:ℝ, c:N) c := k*a
:V×V N
(x1, x2) = … a-t x1, x2, a c-t z1, z2 reprezentálja ahol x1, x2, z1, z2: V, k : ℝ
V×V
z1 := x1+v z2 := k* x2
7.5. Valósítsuk meg azt a zsák típust, amelyikről tudjuk, hogy csak az 1 és 100 közötti természetes szám kerülhet bele, de egy szám többször is! Implementáljuk az új elem behelyezése, egy elem kivétele és „egy elem hányszor van benne a zsákban” műveleteket! Megoldás: Először egy absztrakt típussal írjuk le a zsák fogalmát, majd ezt egy olyan konrét típussal valósítjuk meg, ahol egy zsákot egy 100 elemű természetes számokból álló tömbben ábrázolunk úgy, hogy ennek e-edik eleme azt mutassa meg, hogy az e szám hányszorosan van benn a zsákban. Jól látható, hogy a konkrét típus sokkal hatékonyabb (tömörebb reprezentáció, rövidebb implementáció) lesz, mint az absztrakt.
300
1 és 100 közötti egész számokat tartalmazó
betesz
Zsák
hányszor
kivesz
8. fejezet feladatainak megoldása
:2ℕ
×
ℕ
Zsák
identitás
I(h)={ (e,m) h e [1..100]} h: 2ℕ×ℕ, e:ℕ, db:ℕ
1 és 100 közötti egész szám ás darabszám párok
(e,m) h (e,m) h
h:= h-{(e,m)}) h:=h {(e,1)}
halmaza 2ℕ×ℕ
(e,m) h
h:= h-{(e,m)}
(e,m) h (e,m) h
db:= m db:= 0
N×N
:ℕ100
2
(v)={(e,v[e]) | e [1..100]
{(e,m+1)}
v[e] 0} v:ℕ100, e:ℕ, db:ℕ
ℕ100
v[e]:=v’[e]+1 v[e]:=0 db:= v[e]
7.6. Valósítsuk meg az egész számokat tartalmazó sor típust! A sor elemeit egy tömbben tároljuk, a sor műveletei pedig a sor végére betesz, sor elejéről kivesz, megvizsgálja, hogy üres-e illetve telee a sor. Megoldás: Először egy absztrakt típussal írjuk le a sor fogalmát, majd ezt egy olyan konkrét típussal valósítjuk meg, ahol egy sort egy 0-tól 99-ig indexelt egész számokat tartalmazó tömbben ábrázoljuk. Ezt a tömböt ciklikusan képzeljük el, azaz utolsó elemét az első eleme követi. Ekkor egy i indexszel az alábbiak szerint lépünk a következő pozícióra: i:=(i+1) mod 100. A sor elemeit ebben a tömbben két megadott index között tároljuk. egész számokat tartalmazó
Sor
betesz kivesz Üres-e Tele-e
301
10. Feladatok megoldása
: ℤ* Zsák
identitás s:ℤ*, e:ℤ, l:
ℤ*
s:= s, e s, e: = s2, … ,s
s
, s1
l:= s = 0 l:= s = 100 : ℤ100×ℕ×ℕ×ℕ ℤ* (v,a,f,db) = v[a], … ,v[f] db=f–a+1 100 I(v,a,f,db) = v:ℤ
a,f [0..99] a,f,db:ℕ, e:ℤ
ℤ100×ℕ×ℕ×ℕ
db<100
f:=(f+1) mod 100 v[f]:=e db:=db+1
db>0
e:=v[a] a:=(a+1) mod 100 db:=db-1
l:= db=0 l:= db=100 7.7. Ábrázoljuk a nagyon nagy természetes számok típusát! A számokat decimálisan ábrázoljunk egy kellően hosszú tömbben! Megoldás: Ábrázoljuk a nagy számokat helyiérték szerint növekvő sorrendben, azaz az egyes helyiérték a sorozat legelején álljon. ℕ
A = (a:ℕ, b:ℕ, c:ℕ) c := a+b A = (a:ℕ, b:ℕ, c:ℕ) c := a*b
302
8. fejezet feladatainak megoldása
:{0..9}*
ℕ
x
xi 10 i 1 , I(x) = x x 0
(x) = i 1
I: {0..9}*
az a, b, c-t rendre az x,y,z :{0..9}* reprezentálja {0..9}*
(z) := (x)+ (y) (z) := (x)* (y)
Az összeadás egy rekurzív függvénynek a max( x , y ) helyen történő kiszámításával oldható meg: z=f1(max( x , y )) f2(max( x , y )). A második komponens a következő helyiértékre átvitt érték. f:[0..max( x , y )] {0..9}*×{0..9} f(0) = (<>,0) i>0: f(i) = (f1(i-1)
((xi+yi+f2 (i-1)) mod 10) , (xi+yi+ f2(i-1)) div 10)
Terjesszük ki az xi illetve yi értelmezését arra az esetre is, ha i> x illetve i> y . Ezzel az összeadásban szereplő kevesebb számjegyből álló számot annyi nullával egészítjük ki, hogy a nagyobbikkal azonos hosszúságú legyen. Az algoritmusban a műveletet egy sorozat új számjeggyel való kiegészítésére alkalmazzuk. (z):= (x)+ (y) vagy z:=x+y z, d, i := <>, 0 ,1 i x z, d, i: = z
i y
((xi+yi+d) mod 10), (xi+yi+d) div 10, i+1 z := z
A szorzást visszavezethetjük több egy számjeggyel való szorzásra és az így kapott szorzatok összeadására. Az egy számjeggyel (cvel) való szorzás is egy rekurzív függvénnyel fogalmazható meg: w = f1( x ) f2( x ).
303
10. Feladatok megoldása
f:[0.. x ] f(0) f(i) = (
= f1 (i-1) ((xi*c+f2 (xi*c+ f2 (i-1)) div 10
{0..9}*×{0..9} (<>,0) (i-1)) mod 10) , ) i [1..n]
(w):= (x)*c vagy w:=x*c w, d, i := <>, 0 ,1 i x w, d, i: = w
((xi*c+d) mod 10), (xi*c+d) div 10, i+1 w := w
Ezt, valamint a korábbi összeadás műveletet felhasználva kapjuk meg a két szám szorzását kiszámoló programot. A szorzást valójában az összegzés programozási tételére vezethetjük vissza, hiszen: y
( x) y k 10 k
( z)
1
azaz
k 1 y
z
0 ... 0 k 1
(x
yk )
k 1darab
A képletben látszik, hogy amikor a k-dik helyiértékről származó számjeggyel szorzunk egy számot, akkor azt még 10k-1-gyel is meg kell szorozni, azaz k-1 darab nullát kell az elejére (itt vannak az alacsonyabb helyiértékek) hozzárakni. z,k: = < >,0 k< y z := z+(<0..k-1 darab..0>
(x* yk))
k := k+1 7.8. Valósítsuk meg a valós együtthatójú polinomok típusát az összeadás, kivonás, szorzás műveletekkel!
304
8. fejezet feladatainak megoldása
Megoldás:
P
A = (p:P, q:P, r:P) r := p±q A = (p:P, q:P, r:P) r := p q
:ℝ* P I:ℝ*
(a) = a0+a1 x + … +a a x a , I(a) = a a 0 a p, q, r-t rendre az a,b,c :ℝ*reprezentálja
ℝ*
k [0..max( a , b )]: c[k ] a[k ] b[k ] ) k [0.. a * b ]:
n
c[k ]
a[l ] b[k
l]
l k n l 0
7.9. Valósítsuk meg a diagonális mátrixtípust (amelynek mátrixai csak a főátlójukban tartalmazhatnak nullától különböző számot)! Ilyenkor elegendő csak a főátló elemeit reprezentálni egy sorozatban. Implementáljuk a mátrix i-edik sorának j-edik elemét visszaadó műveletet, valamint két mátrix összegét és szorzatát! Megoldás: Diag(ℝn×n) a:Diag(ℝn×n), i, j:ℕ,e:ℝ e: = a[i,j] a, b, c: Diag(ℝn×n) c: = a+b a, b, c: Diag(ℝn×n) c := a*b :ℝn Diag(ℝn×n)
(x)[i,j] =
x[i ] ha i 0
ha i
j j
305
10. Feladatok megoldása
az a, b, c-t rendre az x ,y ,z : ℝn reprezentálja, továbbá i:ℕ, j:ℕ, e:ℝ ℝn
i=j i j
e
=
x[i]
e=0
i [1..n]: z[i] = x[i]+y[i] i [1..n]: z[i] = x[i]*y[i] 7.10. Valósítsuk meg az alsóháromszög mátrixtípust (a mátrixok a főátlójuk alatt csak nullát tartalmaznak)! Ilyenkor elegendő csak a főátló és az alatti elemeket reprezentálni egy sorozatban. Implementáljuk a mátrix i-edik sorának j-edik elemét visszaadó műveletet, valamint két mátrix összegét és szorzatát! Megoldás: AlsóH(ℝn×n)
a, b, c:AlsóH(ℝn×n) c := a+b a, b, c: AlsóH(ℝn×n) c := a*b a: AlsóH(ℝn×n),i:ℕ, j:ℕ,e:ℝ e := a[i,j]
:ℝn(n+1)/2
(x)[i,j] = x[n (i 1) j ] ha i
AlsóH(ℝn×n)
0
ha i
j j
az a, b, c-t rendre az x, y, z: ℝn(n+1)/2 reprezentálja, továbbá i:ℕ, j:ℕ, e:ℝ i [1..n(n+1)/2]: z[i] = x[i]+y[i] ℝ
n(n+1)/2
i,j [1..n]: ha i j akkor i
z[i(i 1) / 2
j]
x[i(i 1) / 2 k ] y[k (k 1) / 2
j]
k j
i j i<j
306
e e=0
=
x[i
(i–1)/2+j]
8. fejezet feladatainak megoldása
8. fejezet feladatainak megoldása 8.1.
Adott az egész számokat tartalmazó x vektor. Válogassuk ki az y sorozatba a vektor pozitív elemeit! Specifikáció: n
A = (x:ℤ , y:ℤ*) Ef = ( x=x’ ) Uf = ( x=x’
n
y
x[i]
)
i 1 x[i ] 0
Sorozatot előállító feltételes összegzés (kiválogatás) az x tömb nevezetes felsorolójával (i:ℤ), ahol az összeadás művelete az összefűzés. E
~
ℤ
H
~
ℤ*
+,0
~
f(e)
~
,<> <e> ha e>0 <> különben
Algoritmus: y :=<> i = 1 .. n x[i]>0 SKIP y :=y x[i] Számoljuk meg egy halmazbeli szavak között, hogy hány ’a’ betűvel kezdődő van! Specifikáció:
307
10. Feladatok megoldása
A = ( h:set( *), db:ℕ) Ef = ( h=h’ ) Uf = ( db 1 ) szó h ' szó1 'a '
Algoritmus: db := 0 h szó := mem(h) szó1=’a’ db := db+1
SKIP
h := h-{szó}
8.2.
Ez a számlálás programozási tétele a h halmaz felsorolásával (E ~ *), ahol a számlálás feltétele az aktuális elem (szó) első betűjének megfelelő vizsgálata ( (e) ~ szó1=’a’). A mem(h) (aktuális szó) értékének ideiglenes tárolására a szó segédváltozót vezetjük be. Egy szekvenciális inputfájl egész számokat tartalmaz. Keressük meg az első nem-negatív elemét! Specifikáció: A = (f:infile(ℤ), l: , elem:ℤ) Ef = ( f=f’ ) Uf = ( l, elem, f search(e 0) ) e f'
A feladatot szekvenciális inputfájl felsorolására (E ~ ℤ) épített lineáris keresésre vezetjük vissza, ahol a fájlban egy nemnegatív elemet keresünk ( (e) ~ e≥0). A szekvenciális inputfájlok esetén a felsorolót egy st, e, f értékhármas reprezentálja, ahol st az f-re vonatkozó olvasás státusza, e a kiolvasott elemet tartalmazó segédadatok.
308
8. fejezet feladatainak megoldása
Algoritmus: l:=hamis; l
st,e,f:read
st=norm elem:= e l:= e ≥ 0 st,e,f:read
8.3.
Egy szekvenciális fájlban egy bank számlatulajdonosait tartjuk nyilván (azonosító, összeg) párok formájában. Adjuk meg annak az azonosítóját, akinek nincs tartozása, de a legkisebb a számlaegyenlege! Specifikáció: A = (x:infile(Ügyfél), l: , a: *) Ügyfél=rec(az: *, egyl:ℤ) Ef = ( x=x’ ) Uf = ( l,min,elem = min e.egyl a=elem.az ) e x' e.egyl 0
Feltételes minimum keresés az x szekvenciális inputfájl nevezetes felsorolásával (st,e,x:read). E
~
Ügyfél
H
~
ℤ
max
~
min
(e)
~
e.egyl≥0
f(e)
~
e.egyl
309
10. Feladatok megoldása
Algoritmus: l := hamis st,e,x:read st = norm e.egyl<0
l
SKIP
e.egyl 0
l
min >e.egyl min, a:= e.egyl, e.az
e.egyl 0
l, min, a :=
SKIP
igaz, e.egyl, e.az
st,e,x:read 8.4.
Egy szekvenciális fájlban minden átutalási betétszámla tulajdonosáról nyilvántartjuk a nevét, címét, azonosítóját, és számlaegyenlegét (negatív, ha tartozik; pozitív, ha követel). Készítsünk két listát: írjuk ki egy output fájlba a hátralékkal rendelkezők, egy másikba a túlfizetéssel rendelkezők nevét! Specifikáció: A = (x:infile(Ügyfél), y:outfile( ), z:outfile( )) Ügyfél = rec(név: *, cím: *, az: *, egyl:ℤ) Ef = ( x=x’ ) Uf = ( y e.név z e.név ) e x' e.egyl 0
e x' e.egyl 0
Két szekvenciális outputfájlt előállító feltételes összegzés (szétválogatás) – amelyeket összevonunk –, az x szekvenciális inputfájl nevezetes felsorolójával (st,e,x:read). E
~
H
~
Ügyfél
+,0 ~ f(e) ~
310
~
*
H
~
*
,<>
+,0
~
,<>
f(e)
~
e.egyl ha e.egyl<0
Algoritmus:
Ügyfél
E
e.egyl ha e.egyl>0
8. fejezet feladatainak megoldása
st,e,x:read
y := <>
z := <>
st = norm e.egyl<0 y:write(e.név)
SKIP
e.egyl>0 z: write(e.név)
SKIP
st,e,x:read 8.5.
Egy szekvenciális inputfájlban egyes kaktuszfajtákról ismerünk néhány adatot: név, őshaza, virágszín, méret. Válogassuk ki egy szekvenciális outputfájlba a mexikói, egy másikba a piros virágú, egy harmadikba a mexikói és piros virágú kaktuszokat! Specifikáció: A = (x:infile(Kaktusz), y:outfile(Kaktusz), z:outfile(Kaktusz), u:outfile(Kaktusz)) Kaktusz = rec(név: *, virág: *, ős: *) Ef = ( x=x’ ) Uf = ( y e e x' e.virág " piros"
z
u
e x' e.ős " Mexikó"
e x' e.virág " piros" e.ős " Mexikó"
e
e
)
Három szekvenciális outputfájlt előállító feltételes összegzés (kiválogatások) – amelyeket összevonunk – az x szekvenciális inputfájl nevezetes felsorolójával (st,e,x:read).
311
10. Feladatok megoldása
E
~
H +,0 f(e)
~ ~ ~ ha
Kaktusz
Kaktusz
*
Kaktusz
*
*
,<> <e.név>
,<> <e.név>
,<> <e.név>
e.virág=„ piros”
e.ős= „Mexikó”
e.virág= „piros” e.ős= „Mexikó”
Algoritmus: st,e,x:read
y:=<> z:=<> u:=<> st = norm e.virág=”piros”
y:write(e)
SKIP
e.ős=”Mexikó” z:write(e) e.virág=”piros”
SKIP e.ős=”Mexikó”
u:write(e)
SKIP
st,e,x:read 8.6.
Adott egy egész számokat tartalmazó szekvenciális inputfájl. Ha a fájl tartalmaz pozitív elemet, akkor keressük meg a fájl legnagyobb, különben a legkisebb elemét! Specifikáció: A = (x:infile(ℤ), m:ℤ) Ef = ( x=x’ x’ >0) Uf = ( max = max e min = min e e x'
e x'
(max>0 m=max) (max 0 m=min) ) Egy ciklusba összevont maximum- és minimumkeresés (E és H mindkét esetben a ℤ, az f pedig identitás), az x szekvenciális inputfájl nevezetes felsorolójával (st,e,x:read).
312
8. fejezet feladatainak megoldása
Algoritmus: st,e,x:read max, min := e, e st,e,x:read st = norm max <e
min e max
min>e
max:= e
SKIP
min:= e
st,e,x:read max>0 m := max 8.7.
m := min
Egy szekvenciális inputfájl egy vállalat dolgozóinak adatait tartalmazza: név, munka típus (fizikai, adminisztratív, vezető), havi bér, család nagysága, túlóraszám. Válasszuk ki azoknak a fizikai dolgozóknak a nevét, akiknél a túlóraszám meghaladja a 20-at, és családtagok száma nagyobb 4-nél; adjuk meg ezen kívül a fizikai, adminisztratív, vezető beosztású dolgozók átlagos havi bérét! Specifikáció:
313
10. Feladatok megoldása
A = (x:infile(Dolgozó), y:outfile( *), f,a,b:ℝ) Dolgozó = rec(név: *, tipus:{fiz, adm, vez}, bér:ℕ, család:ℕ, túl:ℕ) Ef = ( x=x’ ) f ( e.bér ) ( / 1 ) Uf = ( y e e x'
e x' e.tipus fiz
e.tipus fiz e.tú l 20 e.család 4
a (
e.bér ) ( /
e x' e.tipus adm
v (
1
e x' e.tipus fiz
)
e x' e.tipus adm
e.bér ) ( /
e x' e.tipus vez
1 ))
e x' e.tipus vez
Az eredmény kiszámolásához négy feltételes összegzés (ebből egy kiválogatás) és három számlálás kell ugyanazon az x szekvenciális inputfájl felsorolásával (st,e,x:read). Algoritmus: st,e,x:read y,f,fdb,a,adb,v,vdb := <>,0,0,0,0,0,0 st = norm e.típus=fiz
e.túl>20
e.család>4
y:write(e)
SKIP
e.típus=fiz f, fdb := f+e.bér, fdb+1
SKIP
e.típus=adm a, adb := a+e.bér, adb+1
SKIP
e.típus=vez v, vdb := v+e.bér, vdb+1 st,e,x:read f , a, v := f/fdb, a/adb, v/vdb
314
SKIP
8. fejezet feladatainak megoldása
8.8.
Adott két vektorban egy angol-latin szótár: az egyik vektor iedik eleme tartalmazza a másik vektor i-edik elemének jelentését. Válogassuk ki egy vektorba azokat az angol szavakat, amelyek szóalakja megegyezik a latin megfelelőjével. Specifikáció: n
n
A = (x:ℤ , y:ℤ , z: *, k:ℤ) Ef = ( x = x' y = y') Uf = ( x = x'
y = y'
n
z[1..k ]
x[i ] )
i 1 x[ i ] y[ i ]
Ez egy tömböt előállító feltételes összegzés az 1..n intervallum felett. Algoritmus: k:=0 i = 1 .. n x[i] = y[i] z,k:=z x[i], k+1 8.9.
SKIP
Alakítsunk át egy magyar nyelvű szöveget távirati stílusúra! Specifikáció:
315
10. Feladatok megoldása
A = (x:infile( ), y:outfile( )) Ef = ( x=x’) Uf = ( y távirat(ch ) ) ch x'
*
távirat:
távirat (ch)
" aa"
ha
ch " á"
" ee" " i"
ha ha
ch " é" ch " í "
" oe" " ue"
ha ha
" o" " Aa"
ha ha
ch " ö" vagy ch " ő " ch " ü" vagy ch " ű " ch " ó" ch " Á"
" Ee" " I"
ha ha
ch " É" ch " Í "
ch " Ö" vagy ch " Ő" " Oe" ha "Ue" ha ch "Ü " vagy ch "Ű " " O" ha ch " Ó" ch különben
Algoritmus: st,ch,x:read y:= <> st = norm y: write(távirat(ch)) st,ch,x:read
316
9. fejezet feladatainak megoldása
9. fejezet feladatainak megoldása 9.1. Egy kémrepülőgép végig repült az ellenség hadállásai felett, és rendszeres időközönként megmérte a felszín tengerszint feletti magasságát. A mért adatokat egy szekvenciális inputfájlban tároljuk. Tudjuk, hogy az ellenség a harcászati rakétáit a lehető legmagasabb horpadásban szokta elhelyezni, azaz olyan helyen, amely előtt és mögött magasabb a tengerszint feletti magasság (lokális minimum). Milyen magasan találhatók az ellenség rakétái? Specifikáció: A = (f:infile(ℝ), max:ℝ) Ef = ( f = f' ) A feladat feltételes maximum kereséssel történő megoldásához (a feltétel, hogy van-e egyáltalán a mért felszínen mélyedés) a mért értékeket úgy kell felsorolni, hogy egyszerre három egymás után értéket lássunk: a megelőzőt, az aktuálisat és a rákövetkezőt. A = (t:enor(rec(e:ℝ, a:ℝ, k:ℝ)) , max:ℝ) Ef = ( t=t’ ) max elem.a ) Uf = ( max elem t' elem.e elem.a elem.k
Algoritmus: l:= hamis;
t.First()
t.End() e, a, k =t.Current() (e>a
l
e>a
max := a
l
e>a
l, max := igaz, a SKIP
t.Next() Nézzük ezek után a felsorolót! Ennek minden lépésben három értéket, az előző, az aktuális és a következő alkotta értékhármast
317
10. Feladatok megoldása
kell mutatnia (Current()). Az első hármas előállításához (First()) három olvasásra van szükség az f szekvenciális inputfájlból. A következő hármas előállításához (Next()) felhasználhatjuk az jelenlegi hármas aktuális és következő értékét, amelyekből a következő hármas előző és aktuális értékei lesznek, és egy olvasással megszerezhetjük a következő értéket. A felsorolás akkor ér véget (End()), ha már nem sikerül következő értéket olvasni az f szekvenciális inputfájlból. A felsorolót tehát három valós szám (e, a, k), valamint az utolsó olvasás státusza (sf) reprezentálja. A műveletek pedig: t.First()
t.Next()
l:= t.End()
sf,e,f:read
e:=a
l:= sf = abnorm
sf,a,f:read
a:=k
sf,k,f:read
sf,k,f:read
(e, a, k ):=t.Current() SKIP
9.2. Számoljuk meg egy karakterekből álló szekvenciális inputfájlban a szavakat úgy, hogy a 12 betűnél hosszabb szavakat duplán vegyük figyelembe! (Egy szót szóközök vagy a fájl vége határol.) Specifikáció: A = (f:infile( ), s:ℕ) Ef = ( f = f' ) A feladat megoldható egy olyan összegzéssel, amelyhez szövegbeli szavak hosszait kell felsorolnunk. Ha egy szó hosszabb, mint 12, akkor az összegzés eredményéhez kettőt, különben egyet kell hozzáadni. A = (t:enor(ℕ), s:ℕ) Ef = ( t=t’ ) f (e) ) Uf = ( s e t'
f:ℕ ℕ f(e)=
318
1 ha
e
12
2 ha
e
12
9. fejezet feladatainak megoldása
Algoritmus: s := 0 t.First() t.End() t.Current()>12 s := s+2
s := s+1 t.First()
A felsorolónak minden lépésben a soron következő szó hosszát kell mutatnia. Ha feltesszük, hogy minden lépés előtt a soron következő szó elején állunk, akkor egy olyan összegzéssel határozhatjuk meg az aktuális szó hosszát, amely annak minden betűjére egyet ad hozzá az összegzés eredményéhez. Ez egy feltétel fennállásáig tartó összegzés, hiszen a végét a fájl- vagy a szó vége jelzi. A szó végének elérése után egy kiválasztással megkeressük a következő szó elejét. ANext = (f infile( ), df: , sf:Státusz, van_szó: , hossz:ℕ) EfNext = ( f = f1 df = df1 sf = sf1 (sf=norm df ’ ’) ) UfNext = ( van_szó=(sf1=norm) 2
2
van_szó ( hossz , sf , df , f
df ' '
2
1 df ( df 1 , f 1 )
sf , df , f
select
df ( sf 2 , df 2 , f 2 )
( sf
abnorm
df
' ') ) )
A First() művelet annyival több, hogy először biztosítania kell a típus-invariánst. Ehhez a vezető szóközök átlépésére van szükség, ami a már látott kiválasztás lesz, és csak ezután hajtja végre a Next() műveletnél leírtakat. AFirst = (f:infile( ), df: , sf:Státusz, van_szó: , hossz:ℕ) EfFirst = ( f = f’ ) UfFirst = ( sf 1 , df 1 , f 1
select (sf df
f'
abnorm df
' ')
van_szó=(sf1=norm)
319
10. Feladatok megoldása
df ' '
van_szó ( hossz , sf 2 , df 2 , f 2
1 df ( df 1 , f 1 )
sf , df , f
select
df ( sf 2 , df 2 , f 2 )
t.First()
( sf
abnorm
df
' ') ) )
t.Next()
sf, df, f : read sf = norm
df=’ ’
sf, df, f : read van_szó:= sf=norm t.Next()
van_szó:= sf=norm van_szó hossz:=0 sf = norm
SKIP df ’ ’
hossz:=hossz+ 1 sf, df, f: read sf = norm
df=’ ’
sf, df, f: read A felsorolás akkor ér véget, ha a Next() művelet nem talál újabb szót. Ezt az információt a van_hossz logikai változó őrzi. Ennek értékét kérdezi le a t.End(). A t.Current() a hossz változó értékét adja vissza. Ezek szerint a t felsorolót az sf, df, f, van_szó, hossz együttesen reprezentálják. 9.3. Alakítsunk át egy bitsorozatot úgy, hogy az első bitjét megtartjuk, utána pedig csak darabszámokat írunk annak megfelelően, hogy hány azonos bit követi közvetlenül egymást! Például a 00011111100100011111 sorozat tömörítve a 0362135 számsorozat lesz. Specifikáció: A = (f:Bit*, g:ℕ*) Ef = ( f=f' ) Képzeljük el azt a felsorolót, amelyik már ez eredménysorozatban kívánt értékeket sorolja fel. Ekkor a megoldás egy másolás (speciális összegzés) lesz
320
9. fejezet feladatainak megoldása
A = (t:enor(ℕ), g:ℕ*) Ef = ( x=x' ) Uf = ( g=x' ) Algoritmus: g:= <> t.First() t.End() g:write(t.Current()) t.Next() A felsorolót az f sorozat, a sorozat elemeinek sorszámain végig vezetett i változó, az azonos bitekből álló aktuális szakasz hossza (hossz) és egyik bitje (bit) reprezentálja. A t.End() az i |f| kifejezés lesz, a t.Current() a hossz változó értékét adja vissza, ami a legeslegelső esetben az első bit értéke. AFirst = (f:Bit*, e:ℕ, i:ℕ) EfFirst = ( f = f’ ) UfFirst = ( f = f’ (|f| 0 i=1 hossz=f1) )
ANext = (f:Bit*, e:ℕ, i:ℕ) EfNext = ( f = f’ i = i’ ) UfNwxt = ( f = f’ hossz , i
fi fi ' i f
1 ))
i i'
t.Fist()
t.Next() bit, hossz:= fi, 0
|f| 0 i, hossz:= 1, 0
SKIP
i |f|
bit = fi
hossz:=hossz+1 i:=i+1 9.4. Egy szöveges állományban a bekezdéseket üres sorok választják el egymástól. A sorokat sorvége jel zárja le. Az utolsó sort zárhatja fájlvége jel is. A sorokban a szavakat a sorvége vagy
321
10. Feladatok megoldása
elválasztójelek határolják. Adjuk meg azon bekezdéseknek sorszámait, amelyek tartalmazzák az előre megadott n darab szó mindegyikét! (A nem-üres sorokban mindig van szó. Bekezdésen a szövegnek azt a szakaszát értjük, amely tartalmaz legalább egy szót, és vagy a fájl eleje illetve vége, vagy legalább két sorvége jel határolja.) Specifikáció: A = (f:infile( ), h:( *)n, g:outfile(ℕ)) Ef = ( f=f' h=h' ) Képzeljük, hogy minden bekezdéshez hozzárendeljük annak sorszámát és egy logikai értéket, amelyik akkor igaz, ha a bekezdésben szerepelnek a megadott szavak. Ha egy alkalmas felsorolóval ezeket a szám-logikai érték párokat soroltatjuk fel, akkor a feladat visszavezethető egy összegzésre (kiválogatásra). n
A = (x:enor(Pár), h:( *) , g:outfile(ℕ)) Pár = rec(ssz:ℕ , van: ) Ef = ( x=x' ) e.ssz ) Uf = ( g e x' e.van
Algoritmus: x.First(); g:= <> x.End() x.Current().van g : write(x.Current().ssz)
SKIP
x.Next() A x.First() és x.Next() műveletek egyaránt egy bekezdésnyit olvasnak a szövegből. Ehhez egy olyan absztrakt felsorolót (u) is használunk, amely szakaszokat (szavakat, csupa sorvége-jelből álló részeket, és egyéb jelekből álló részeket) olvas a szövegből. Amíg bekezdés végéhez (legalább két sorvége-jelből álló szakaszhoz) nem érünk, addig a szavakat egyenként beledobáljuk egy különleges tárolóba, amely számolja, hogy a kezdetben megadott szavak közül hány került már bele. Ha a tároló
322
9. fejezet feladatainak megoldása
számlálója a bekezdés feldolgozása végén éppen n, akkor a bekezdés megfelel a feladatban leírt feltételnek. Definiáljuk először a tároló típusát! Ennek létrehozásához megadjuk az n darab vizsgálandó szót. Ezek közül megjelöltek lesznek azok, amelyekkel már találkoztunk egy bekezdés szavai között. Nyilvántartunk egy számlálót (count) is, amely azt mutatja, hogy a megadott szavak közül eddig hányat jelöltünk meg. Három műveletet vezetünk be. Az Init() lenullázza a számlálót és az összes előre megadott szót jelöletlennek állítja be. A Mark(szó) újabb szót „helyez el” a tárolóban. Ha a szó szerepel az előre megadott szavak között (ezt egy lineáris keresés dönti el) és még jelöletlen, akkor megjelöljük és a számlálót eggyel növeljük. A Full() igazat ad, ha az előre megadott szavak mindegyike jelölt, azaz count=n. Tároló
Init() Mark(szó) l:=Full() v: (
(
*
× )n×ℕ
*
× )n, count:ℕ, szó:
*
, l:
i [1..n]: v[i].jel := hamis count := 0 n
l , i : search (v[i ].szó i 1
l
szó )
v[i].jel
v[i].jel := true count:= count+1
SKIP
l := count=n Az x.First() és x.Next() műveletek között csak annyi különbség van, hogy a bekezdés legelső szakaszát az u.First() művelettel
323
10. Feladatok megoldása
olvassuk, nem az u.Next()-tel, és a legelső bekezdés sorszámát 1re kell állítani, nem pedig az előző sorszám eggyel megnövelt értékére. A tárolóba mindenféle szakaszt bedobálhatunk (ezt programoztuk le), de hatékonyabb lenne, ha csak a szavakkal tennénk ezt. A BekVég() ellenőrzi, hogy a vizsgált szakasz (u.Current()) legalább két sorvége-jelből áll-e. Az x.Current() adja vissza a p-t, az x.End() pedig a vége-t. A műveleteket formálisan nem specifikáljuk. x.First()
x.Next() u.First()
u.Next()
vége:= u.End()
vége:= u.End()
vége
vége
p.ssz := 1; a.Init()
p.ssz := p.ssz +1; a.Init()
u.End() BekVég(u.Current())
S K
u.End() BekVég(u.Current())
S K
a .Mark(u.Current())
I
a .Mark(u.Current())
I
u.Next()
P
u.Next()
P
p.van:= a.Full()
p.van:= a.Full()
Egy szakasz kiolvasásához az eredeti szöveget karakterenként kell tudni bejárni. Ehhez a szöveget szekvenciális inputfájlként bejáró nevezetes felsorolást, az sf,df,f:read műveletet használjuk. Az u.Next() műveletnél feltételezzük, hogy a fájl előre olvasott, az u.First() műveletnél ezt az előre olvasást el kell végezni. A műveleteket formálisan nem specifikáljuk. Az u.Next() művelet egy feltétel fennállásáig tartó összegzés, amely az aktuális szakasz jeleit fűzi össze. Ehhez használjuk a Jel: {betű, köz, sorvég} függvényt, amely egy karakterről eldönti, hogy az milyen fajtájú. Az aktuális szakasz addig tart, amíg azonos jelű karaktereket olvasunk be egymás után. Az u.Current() az aktuális szakasz-t adja vissza, az u.End() pedig
324
9. fejezet feladatainak megoldása
akkor igaz, ha a szekvenciális inputfájl bejárása véget ért, azaz van_szakasz. x.First()
x.Next()
sf, df, f : read
van_szakasz:= sf=norm
u.Next()
van_szakasz szakasz:=”” jel:=Jel(df) sf = norm
Jel(df)=jel
S K
szakasz:=szak asz+df
I
sf, df, f: read
P
9.5. Egy szöveges állomány legfeljebb 80 karakterből álló sorokat tartalmaz. Egy sor utolsó karaktere mindig a speciális ’\eol’ karakter. Másoljuk át a szöveget egy olyan szöveges állományba, ahol legfeljebb 60 karakterből álló sorok vannak; a sor végén a ’\eol’ karakter áll; és ügyelünk arra, hogy az eredetileg egy szót alkotó karakterek továbbra is azonos sorban maradjanak, azaz sorvége jel ne törjön ketté egy szót. Az eredeti állományban a szavakat egy vagy több szóhatároló jel választja el (Az ’\eol’ is ezek közé tartozik), amelyek közül elég egyet megtartani. A szóhatároló jelek egy karaktereket tartalmazó – szóhatár nevű – halmazban találhatók. Feltehetjük, hogy a szavak 60 karakternél rövidebbek. Specifikáció: A = (f:infile( ), g:outfile( )) Ef = ( f=f' ) A feladat megoldásához a szöveg szavainak felsorolására lesz szükségünk, hiszen a szavakat kell más tördeléssel az outputfájlba kiírni. Ennek érdekében az eredményt egy speciális típusú objektumnak képzeljük el, amelyre bevezetjük a Write60() műveletet. Ez sort emel, mielőtt egy olyan szót írna ki, amelyiknek hossza (az eléírt szóközzel együtt) nem fér már ki az
325
10. Feladatok megoldása
aktuális sorba. Az objektumot a szekvenciális outputfájl mellett az a szám reprezentálja, amely megmutatja, hogy a legutolsó sorban hány karakternyi szabad hely van még. (Üres sor esetén ez éppen 60.) Open()
outfile60
Write60(szó) g: outfile( ), szó: outfile( )× ℕ
*
, free:ℕ,
g := <> free:=60 hossz(szó)+1>free g:write(sorvég) free:=60
g:write(’ ’) free:=free-1
g:write(szó) free:=free- hossz(szó) Ezek után újraspecifikáljuk a feladatot. A = (x:enor(Szó), y:outfile60( )) Ef = ( x=x' ) Uf = ( u ) e e x'
Algoritmus: x.First(); y.Open() x.End() x.Current().van y:write60(x.Current())
SKIP
x.Next() A felsorolónak minden lépésben a soron következő szót kell megadnia. Ezt a felsorolót már nem először készítjük el (lásd 9.2 feladat).
326
9. fejezet feladatainak megoldása
9.6. Adott egy keresztneveket és egy virágneveket tartalmazó szekvenciális fájl, mindkettő abc szerint szigorúan növekedően rendezett. Határozzuk meg azokat a keresztneveket, amelyek nem virágnevek, illetve azokat a neveket, amelyek keresztnevek is, és virágnevek is! Specifikáció: A = (c:infile(E), f:infile(E), ko:outfile(E), cf:outfile(E)) Ef = ( c = c’ j = j’ c f ) g (e) cf h ( e) ) Uf = ( ko e {c '} { f '}
e
e {c '} { f '}
ha e { c' } e { f ' }
g( e )
ha e { c' } e { f ' } ha e { c' } e { f ' } ha e { c' } e { f ' }
h( e ) e
ha e { c' } e { f ' } ha e { c' } e { f ' }
Ez egy összefuttatásos (lásd szekvenciális inputfájlok felett. Algoritmus:
9.5.
alfejezet)
összegzés
sc,dc,c:read ; sf,df,f:read ; ko := <>; cf := <> sc=norm sf=abnorm (sc=norm dc
sf=norm
sc=abnorm (sf=norm dc>df)
sc=norm sf=norm dc=df
ko:write(dc)
SKIP
cf:write(dc)
sc,dc,c:read
sf,df,f:read
sc,dc,c:read sf,df,f:read
9.7. Egy x szekvenciális inputfájl (megengedett művelet: read) egy könyvtár nyilvántartását tartalmazza. Egy könyvről ismerjük az azonosítóját, a szerzőjét, a címét, a kiadóját, a kiadás évét, az aktuális példányszámát, az ISBN számát. A könyvek azonosító szerint szigorúan növekvően rendezettek. Egy y szekvenciális
327
10. Feladatok megoldása
inputfájl (megengedett művelet: read) az aznapi könyvtári forgalmat mutatja: melyik könyvből hányat vittek el, illetve hoztak vissza. Minden bejegyzés egy azonosítót és egy előjeles egészszámot - ha elvitték: negatív, ha visszahozták: pozitív tartalmaz. A bejegyzések azonosító szerint szigorúan növekvően rendezettek. Aktualizáljuk a könyvtári nyilvántartást! A feldolgozás során keletkező hibaeseteket egy h sorozatba írjuk bele! Specifikáció: Vezessünk be néhány jelölést. Ha x olyan elemeknek a sorozata, amelyek olyan rekordok, hogy rendelkeznek például egy azon nevű komponenssel (ilyen ebben a feladatban a törzs- és a módosító fájl), akkor x azon jelölje azt, hogy x-ben az elemek azonosító szerint szigorúan növekedően rendezettek. Amennyiben x-re ez a feltétel fenn áll, akkor az azon(x) jelölje az x-ben található azonosítóknak (x összes elemének azonosítóinak) a halmazát, és ilyenkor ha a azon(x), akkor a(x) jelölje az x-nek az a azonosítójú elemét. A = (t:infile(Könyv), m:infile(Vált), u:outfile(Könyv), h:outfile( *)) Könyv = rec(azon: *, szerző: *, cím: *, kiadó: év:ℕ, pld:ℕ, isbn: *) Vált = rec(azon: *, forg:ℕ) Ef = ( t = t' m = m' t azon m azon ) f 1 (a) Uf = ( u
*
,
a azon(t ') azon( m')
h
a azon(t ') azon( m')
a(t ' ), f (a)
g (a)
328
, " hiba" g (a)
f 2 (a) )
ha
a
azon (t ' )
a
azon (m' )
ha ha
a a
azon (t ' ) azon (t ' )
a a
azon (m' ) azon (m' )
a(t ' ) , " hiba" a(t ' ) pld pld a ( m'). forg ,
ha a(t ' ). pld a(m' ). forg 0 ha a(t ' ). pld a(m' ). forg 0
9. fejezet feladatainak megoldása
Az a(t’)pld pld+a(m’).forg jelölés azt a Könyv típusú elemet jelzi, amely abban különbözik az a(t’) rekordtól, hogy annak pld mezője az a(m’).forg mezőjének értékével módosult. Az aktualizált nyilvántartást is, a hibafájlt is egy szekvenciális inputfájlok feletti azonosító szerinti összefuttatásos összegzéssel állíthatjuk elő. A hibaüzenet pontos szövegét a specifikáció nem tartalmazza. Algoritmus: u,h : =<>,<> st,dt,t:read st=norm sm=abnorm st=norm dt.azon
sm,dm,m:read sm=norm
st=abnorm sm=norm dt.azon>dm.azon
st=norm sm=norm dt.azon=dm.azon a := dt.pld+dm.forg a
u:hiext(dt)
h:hiext(„hiba”)
0
dt.pld := a
HIBA
u:hiext(dt) st,dt,t:read
sm,dm,m:read
st,dt,t:read sm,dm,m:read
9.8. Egy vállalat dolgozóinak a fizetésemelését kell végrehajtani úgy, hogy az azonos beosztású dolgozók fizetését ugyanakkora százalékkal emeljük. A törzsfájl dolgozók sorozata, ahol egy dolgozót három adat helyettesít: a beosztáskód, az egyéni azonosító, és a bér. A törzsfájl beosztáskód szerint növekvően, azon belül azonosító szerint szigorúan monoton növekvően rendezett. A módosító fájl beosztáskód-százalék párok sorozata, és beosztáskód szerint szigorúan monoton növekvően rendezett. (Az egyszerűség kedvéért feltehetjük, hogy csak a törzsfájlban előforduló beosztásokra vonatkozik emelés a módosító fájlban, de nem feltétlenül mindegyikre.) Adjuk meg egy új törzsfájlban a dolgozók emelt béreit.
329
10. Feladatok megoldása
Specifikáció: Legyen T=infile(DT) a törzsfájl típusa, ahol DT=rec(beo:ℕ, az: *, bér:ℕ). Az U=infile(DT) új törzsfájl a T-vel megegyező szerkezetű szekvenciális outputfájl. Legyen a módosító fájl típusa M=infile(DM), ahol DM=rec(beo:ℕ, emel:ℝ). Most is használjuk az előző feladat megoldásánál bevezetett jelöléseket. A= (t:T, m:M, u:U) Ef = ( t = t' m = m' beo(t’) beo(m’) t (beo,az) m beo ) Képzeljük el azt a felsorolót, amely alapvetően a t fájl elemeit, a dolgozókat járja be, de úgy, hogy minden dolgozónál végrehajtja a rávonatkozó fizetésemelést is. A= (x:enor(DT), u:U) Ef= ( x = x' ) Uf= ( u e ) e x'
Algoritmus: Ez a feladat a legegyszerűbb összegzésre, a másolásra vezethető vissza. A First() művelet beolvassa az első dolgozót és az első fizetésemelést. A Next() művelet beolvassa a következő dolgozót, és amennyiben a következő dolgozó beosztáskódja nagyobb lenne, mint az aktuális fizetésemelés beosztáskódja, akkor a következő fizetésemelést is. A beo(t’) beo(m’) előfeltétel garantálja, hogy mindkét művelet végrehajtása után az aktuális dolgozó beosztáskódja nem lehet nagyobb a fizetésemelés beosztáskódjánál. A Current() művelet ha a fizetésemelés beosztáskódja megegyezik a dolgozóéval, akkor azt a megemelt fizetéssel, egyébként az eredeti fizetéssel adja vissza a dolgozót. Az End() művelet akkor lesz igaz, ha a dolgozók felsorolása véget ér.
330
9. fejezet feladatainak megoldása
Algoritmus: u : =<> st,dt,t:read; sm,dm,m:read st=norm sm=norm
dt.beo=dm.beo
dt.bér:= dt.bér+ dt.bér* dm.emel
SKIP
u:write(dt) st,dt,t:read st=norm
sm=norm
sm,dm,m:read
dt.beo>dm.beo SKIP
9.9. Egy egyetemi kurzusra járó hallgatóknak három géptermi zárthelyit kell írnia a félév során. Két félévközit, amelyikből az egyiket .Net/C#, a másikat Qt/C++ platformon. Azt, hogy a harmadik zárthelyin ki milyen platformon dolgozik, az dönti el, hogy a félévközi zárthelyiken hogyan szerepelt. Aki mindkét félévközi zárthelyit teljesítette, az szabadon választhat platformot. Aki egyiket sem, az nem vehet részt a félévvégi zárthelyin, számára a félév érvénytelen. Aki csak az egyik platformon írta meg a félévközit, annak a félévvégit a mási platformon kell teljesítenie. Minden gyakorlatvezető elkészíti azt a kimutatást (szekvenciális fájl), amely tartalmazza a saját csoportjába járó hallgatók félévközi teljesítményét. Ezek a fájlok hallgatói azonosító szerint rendezettek. A fájlok egy eleme egy hallgatói azonosítóból és a teljesítmény jeléből (X – mindkét platformon teljesített, Q – csak Qt platformon, N – csak .net platformon, 0 – egyik platformon sem) áll. Rendelkezésünkre állnak továbbá a félévvégi zárthelyire bejelentkezett hallgatók azonosítói rendezett formában egy szekvenciális fájlban. Állítsuk elő azt a szekvenciális outputfájlt, amelyik a zárthelyire bejelentkezett hallgatók közül csak azokat tartalmazza, akik legalább az egyik félévközi zárthelyit teljesítették. Az eredmény fájlban minden ilyen hallgató azonosítója mellett tüntessük fel, hogy milyen
331
10. Feladatok megoldása
platformon kell a hallgatónak dolgoznia: .Net-en (N), Qt-vel (Q) vagy szabadon választhat (X). Specifikáció: A= (g:infile(Hallg)n, r:infile( *), v:infile(Hallg)) Hallg=rec(eha: *, jel: ). Ef = ( g = g' r = r' i [1..n]:g[i] eha r ) A feladat megoldásához egyrészt szükségünk van egy olyan felsorolóra (enor(Hallg)), amely a csoportok összes hallgatóját azonosító szerint rendezett módon tudja bejárni, azaz összefuttatni. Feltehetjük, hogy ugyanaz az azonosító nem szerepelhet két különböző csoportban. A félévvégi zárthelyikre adott jelentkezéseket tároló fájlt annak nevezetes felsorolójával járjuk be. A = (x:enor(Hallg), y:enor( *), v:outfile(Hallg)) Ef = ( x = x' y = y' t eha y ) f (a ) ) Uf = ( v a eha( x ') ( y ')
f(a)
ha a
eha( x' ) a { y' }
ha a g( a ) ha a
eha( x' ) a { y' } eha( x' ) a { y' }
a( x' ) g( a )
a( x' )
ha a( x' ). jel ' X ' eha( x' ). jel
' Q'
ha a( x' ). jel ' N'
eha( x' ). jel
' N'
ha a( x' ). jel ' Q'
a( x' )
ha a( x' ). jel ' 0'
Algoritmus: A feladatot egy összefuttatásos összegzés oldja meg. Az r fájl felsorolóját az sr,dr,r:read művelettel valósítjuk meg. Az összefuttatás hallgatói azonosító szerint történik, ezért a x.Current().eha értéket kell a dr értékkel összevetni.
332
9. fejezet feladatainak megoldása
x.First(); sr,dr,r:read; v := <> x.End()
sr=norm
r.End() x.End() ( x.End() ( r.End() x.Current().eha x.Current().eha dr) SKIP
SKIP
v:write(g(t.Current()))
x.Next();
sr,dr,r:read
x.Next(); sr,dr,r:read
x.First()
x.Next() i=1..n
sg[ind],dg[ind],g[ind]:read
sg[i],dg[i],g[i]:read l,
x.End() r.End() x.Current().eha=dr
ind,
min n
min
i 1 sg [i ] norm
dg[i].eha
:=
l,
ind, n
min
min
:=
dg[i].eha
i 1 sg [i ] norm
Az x.First() művelet a csoportokba beosztott legkisebb azonosítójú hallgatót keresi meg. Ehhez minden csoportnak az első (legkisebb azonosítójú) hallgatóját kell beolvasni (ha van ilyen), és ezek között megkeresni a legkisebbet. Ez egy feltételes minimumkeresés, ahol a feltétel az, hogy legyen az adott csoportban még feldolgozatlan hallgató. Az egyes csoportok bejárásához a szekvenciális inputfájl nevezetes felsorolóit használjuk. Az x.Next() művelet abban különbözik az x.First()-től, hogy a feltételes minimumkeresés előtt csak abból a csoportból kell újabb hallgatót olvasni, ahonnan az utoljára feldolgozott azonosító származik. Az x.Current() művelet a legkisebb azonosítójú hallgatót adja vissza (dg[ind]), de beállítja a hallgató jelét is: ha N volt, akkor Q-ra és fordítva, ha X vagy 0, akkor azt helyben hagyja. Az x.End() pedig akkor
333
10. Feladatok megoldása
lesz igaz, ha a feltételes minimumkeresés sikertelen (l=hamis), azaz már minden csoport összes hallgatóját megvizsgáltuk. 9.10. Az x szekvenciális inputfájlban egész számok vannak. Van-e benne pozitív illetve negatív szám, és ha mindkettő van, akkor melyik fordul elő előbb? Specifikáció: A = (x:infile(ℤ), poz: , neg: , első:ℤ) Ef = ( x=x’ ) Uf = ( (poz, neg, első) = f( x’ ) ) A poz akkor lesz igaz, ha tartalmaz a fájl pozitív számot, a neg akkor lesz igaz, ha tartalmaz a fájl negatív számot, és ha mindkettő igaz, akkor az első-ből kiolvasható a válasz: ha első>0, akkor a pozitív szám jelent meg előbb, ha első<0, akkor a negatív. A válasz három komponensét egy rekurzív függvény adja meg. f:[0.. x’ ] × ×ℤ f(0) = (hamis, hamis, 0) i [1.. x’ ]: (igaz , f 2 (i 1), x'i ) ha f 1 (i 1) f 2 (i 1) x 'i 0 (igaz , f 2 (i 1), f 3 (i 1)) ha f 1 (i 1) f 2 (i 1) x 'i 0 f(i) = ( f 1 (i 1), igaz , x'i ) ha f 1 (i 1) f 2 (i 1) x 'i 0 ( f 2 (i 1), igaz , f 3 (i 1)) ha f 1 (i 1) f 2 (i 1) x 'i 0 f (i 1) különben Ez a függvény nem függ közvetlenül az i-től, csak az x’i-től, ezért a kiszámítását az x szekvenciális inputfájl elemeinek felsorolására építhetjük. Az i:=1 és az i:=i+1 értékadást az st,e,x:read utasítással (First() és Next()), az i ≤ x feltételt az st=norm feltétellel ( End()), az xi helyett az e-t használhatjuk (Current()). Sőt az End() művelete szigorítható az st=abnorm (poz neg) feltételre, hiszen ha valahol a rekurzív függvény első két komponense igazra vált, a függvény továbbiakban felvett értékei már nem változnak.
334
9. fejezet feladatainak megoldása
Algoritmus: poz, neg, első:= hamis, hamis, 0 st,e,x:read st=norm poz poz neg e>0 neg e>0 poz, első:= igaz, e
poz:= igaz
( poz
neg)
poz poz neg neg e<0 e<0 neg, első:= igaz, e
else
neg:= igaz SKIP
st,e,x:read
335
IRODALOM JEGYZÉK 1. Dijkstra E.W., „A Discipline of Programming”,PrenticeHall, Englewood Cliffs, 1973. 2. Dijkstra E.W. and C.S. Scholten, „Predicate Calculus and Program Semantics”, Springer-Verlag, 1989. 3. Fóthi Ákos: „Bevezetés a programozáshoz” ELTE Eötvös Kiadó. 2005. 4. Fowler M., „Analysis Patterns: Reusable Object Models”, Addison-Wesley, 1997. 5. Gries D., „The Science of Programming”, Springer Verlag, Berlin, 1981. 6. Gregorics, T., Sike, S.: „Generic algorithm patterns”, Proceedings of Formal Methods in Computer Science Education FORMED 2008, Satellite workshop of ETAPS 2008, Budapest March 29, 2008, p. 141-150. 7. Gregorics, T.: „Programming theorems on enumerator”, Teaching Mathematics and Computer Science, Debrecen, 8/1 (2010), 89-108 8. Hoare C.A., „Proof of Correctness of Data Representations}, Acta Informatica” (1972), 271-281. 9. Kondorosi K.,László Z.,Szirmay-Kalos L.: Objektum orientált szoftverfejlesztés, ComputerBooks, Budapest, 1997. 10. Sommerville I., „Software Engineering”, Addison-Wesley, 1983. 11. „Workgroup on Relation Models of Programming: Some Concepts of a Relational Model of Programming,Proceedings of the Fourth Symposium on Programming Languages and Software Tools, Ed. prof. Varga, L., Visegrád, Hungary, June 9-10, 1995. 434-44.”
336