You are on page 1of 336

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
megfelelően a fenti képlet v 20 h -ra módosul.
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ék-
halmazá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 nem-
definiá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 nem-
determinisztikus, 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 nem-
determinisztikus 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 nem-
determinisztikus.
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áll-
e 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)m, majd
adjuk hozzá a (-1)m+1-t, utána a (-1)m+2-t és így tovább,
végül a (-1)n-t!
Ha m=n, akkor legyen az x értéke a (-1)n!
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 1,2,4,5 1 1,4,3,5,2 1 1,3,2,...
2 2,1 2 2,4 3 3,3,...
S
4 4,1,5,1,4 4 4,3,1,2,5,1 4 4,1,5,4,2
5 5,2,4 5 5,3,4 5 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, *)}

(6,8)

(6,3)

n
1 2 3 4 5 6

d
F

(6,3)
{(0, *)} (4,2) (6,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ék-
halmazait 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 nem-
determinisztikus é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 nem-
determinisztikus lesz.

42
3.1. Elemi programok

Általános értékadásnak egy szimultán, parciális, nem-


determinisztikus é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 Kf-
bő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 z-
ben 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 nem-
determinisztikussá 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 nem-
determinisztikussá 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 f
S1 S2 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’) )
1 ha z 0
ahol sign(z) = 0 ha z 0
1 ha z 0
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 20-
szal. 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ő 20-
szal, 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. Egy-
egy 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 I-
t. 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 nem-
negatí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ék-
konstansok, 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 f1
S1 SKIP f2
f2 f3
S2 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
n 0 hátralevő lépések: n
n páros invariáns: z (x)n=(x’)n’
x,n:=x x, n/2 z,n:=z x, n-1

60
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ő nem-
megengedett értékadások feladatok is és programok is, a feladat-
orientá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 s v[i]2 )
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:ℕ


i n Az n nem új változó,

s : s v[i]2 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 s a[i] * b[i] )
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
n
feltételt a feladat specifikációjának utófeltételében az ( g (i) páros )
i m
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
Uf = ( Ef s ( g (i) páros ) )
i m

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] ~ [m..n]
+, 0 ~ , igaz
s:=s+v[i]2 ~ 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 U
i 1
{x[i ]} )
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 := ,1 i:ℕ
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 s f (i) )
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
n
értékeinek az összegét, azaz a 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 s f(i) )
i m
Algoritmus:
s, i := 0, m i:ℤ
i n
s := s + f(i)
i := i + 1

74
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:
n
= ( Ef ind [m..n] max = f(ind) = max f(i) )
i m
n
= ( Ef max,ind= max f(i) )
i m
Algoritmus:
max, ind, i := f(m), m, m+1 i:ℤ
i n
max<f(i)
max, ind := f(i), i 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.

Uf = ( Ef l,max,ind) = )
m
i
a
n
(
m
i
)
x
f
(
i
)
Algoritmus:
l, i := hamis, m i:ℤ
i n
(i) l (i) l (i)
SKIP max< f(i) l, max, ind :=
max, ind := f(i), i SKIP igaz, f(i), i

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 l , ind search (i) )
i m
10
Algoritmus :
l, i := hamis, m i:ℤ
l 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 := hamis, m ind := m
l ind<n l ind n ind n (ind)
ind := ind+1 (ind) ind := ind+1
l := (ind) l:=igaz 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 ah, fh:ℤ
l ah fh
ind := (ah+fh) div 2
f(ind)>h f(ind)<h f(ind)=h
fh := ind-1 ah := ind+1 l := igaz

80
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 rövidebben i = m .. n
feldolgozás 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 ~ a+1..b
s ~ db
i ~ j
(i) ~ 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 ) )
n 1
= ( Ef l , ki search( f(i) ( f(i 1) f(i 1)) /2 ) )
i m 1
Ez a feladat visszavezethető a lineáris keresés programozási
tételére.

m..n ~ m+1..n-1
(i) ~ f(i)=(f(i–1)+ f(i+1))/2
ind ~ 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 )
b
Uf = ( Ef ind [a..b] g(ind) = max g(i) )
i a

A kitűzött feladat állapottere kiegészíthető a max:ℝ


komponenssel, az utófeltétele pedig szigorítható
b
Uf = ( Ef max = max g(i) ind [a..b] 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 ~ a..b
f(i) ~ 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 i:ℤ
l 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) 0) )
i a

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 s 1)
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
Uf = ( Ef s 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 ~ 1..n div 2


(i) ~ 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
Uf = ( Ef l,max,ind = max g(i) )
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
Uf = ( Ef l,max,ind = max g(i) )
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 ~ a..b
f(i) ~ g(i)
(i) ~ 2 g(i)

l: = hamis
i = a .. b i:ℤ
2 g(i) l 2 g(i) l 2 g(i)
SKIP max<g(i) l, max, ind :=
max, ind := g(i), i SKIP igaz, g(i), i

92
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 i:ℤ
k g(i) l k g(i) l k g(i)
SKIP max<g(i) l, max, ind :=
max, ind := g(i), i SKIP igaz, g(i), i

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
Uf = ( Ef s 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 ~ 0..n-1
(i) ~ 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 max<f(i) helyett a
max>f(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 i:ℤ


l 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 i:ℤ


l 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) )
i m

97
5. Visszavezetés

l,i := hamis,m i:ℤ


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 i:ℤ


l 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
f(i) ha β (i)
Uf = ( Ef s g(i) ), ahol g(i)
i m 0 különben
vagy másképp

98
5.5. Visszavezetési trükkök

n
Uf = ( Ef s f(i) )
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 ~ 2..n
y ~ (max, min)
e0 ~ (x[1], x[1])
h1(i, f1(i–1)) ~ max( f1(i–1), x[i] )
h2(i, f2(i–1)) ~ min( f2(i–1), x[i] )

max, min := x[1], x[1]


i = 2 .. n i:ℕ
max, min := max( max, x[i] ), min( min, x[i] )

Vagy a ciklusmagot más alakra hozva:

100
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]<b)
n
Uf = ( Ef s x[k ] b n 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
f(i) = x[k ] b 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 függvény
kiszámításának tételére.

m .. n ~ 1..n
y ~ s
e0 ~ 0
h(i, f(i–1)) ~ 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
(...(( x[1] b x[2]) b x[3])...) b x[n]
k 1

102
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]<b y[i]<b )
Uf = ( Ef (z,c) = f(n) )
Az utófeltételben hivatkozunk egy olyan függvényre, amely két
értéket állít elő: egy tömböt (ez tartalmazza az x és y tömbökben tárolt
természetes számok összegének számjegyeit) és egy „túlcsordulás
bitet”, amely értéke akkor 1, ha nem fér el az összeg n helyiértéken.
Célunk, hogy minden i [1..n] esetén az f(i) egyrészt adjon meg egy
olyan tömböt, amelynek utolsó i darab eleme (az [n–i+1 .. n] indexű
elemei) már az eredményként előállított természetes szám utolsó i
darab számjegyét tartalmazza, másrészt adja meg, hogy keletkezett-e
átvitel a bi–1-es helyiértéken, azaz a tömb n–i+1-dik pozíciójának
kiszámolásánál. Az f1(i) tehát egy tömb, az f2(i) pedig egy 0 vagy 1-es
érték. Az f1(i)-nek (az eredmény tömbnek) utolsó i–1 darab eleme meg
kell egyezzen az f1(i–1) utolsó i–1 darab elemével (ezek az [n–i+2 .. n]
indexű elemek). A tömb n–i+1-dik pozíciójának meghatározásához
pedig az x[n–i+1] és y[n–i+1] elemeken kívül szükség van az előző
helyiértéken keletkezett átvitelre, az f2(i–1)-re is. Itt tehát egy elsőrendű
rekurzióról van szó. Kezdetben az átvitel értékét 0-ra, az eredmény
tömböt tetszőleges értékűre állítjuk.
f: [0..n] ℕn×{0,1}
f1(0) = * (ez egy tetszőleges érték), f2(0) = 0
i [1..n]:
f1(i)[n–i+2 .. n] = f1(i–1)[n–i+2 .. n],
f1(i)[n–i+1] = (x[n–i+1]+y[n–i+1]+f2(i–1)) mod b
f2(i) = (x[n–i+1]+y[n–i+1]+f2(i–1)) div b
A feladat egy 1 bázisú elsőrendű kétértékű rekurzív függvény
helyettesítési értékének kiszámolása lesz.

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 i:ℕ
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
5.7. Feladatok

5.7. Feladatok

5.1. Adott a síkon néhány pont a koordinátáival. Keressük meg az


origóhoz legközelebb eső pontot!
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?
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!
5.4. 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!
5.5. Igaz-e, hogy egy tömbben elhelyezett szöveg odafelé és
visszafelé olvasva is ugyanaz?
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!
5.7. Keressük meg két természetes szám legkisebb közös
többszörösét!
5.8. Keressük meg két természetes szám legnagyobb osztóját!
5.9. Prím szám-e egy egynél nagyobb egész szám?
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!

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ény-
vá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ény-
vá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 i:ℕ


l i n
l := színjeles(i)
i := i+1

Ebben a programban az l:=színjeles(i) értékadás nem-


megengedett. 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 j:ℕ
l 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 van-
e 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) nem-
megengedett 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: i, l := 0, hamis l:
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 i-
edik 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) ) =
n
= ( Ef l search (négyesdb(i ) 3) )
i 1
ahol négyesdb :[1..m] ℕ
m
négyesdb(i) = 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 i:ℕ


l 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] )
m
Uf = ( Ef s = négyesdb(i) ) = ( Ef 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 s= 1)
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 )
n
Uf = ( Ef 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 max:ℕ


i = 2 .. n i:ℕ
max<négyesdb(i)
max, ind:= négyesdb(i), i 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 max:ℕ


i = 2 .. n 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’ )
n
Uf = ( Ef l,max,ind = max összeg (i ) (l átl=max/m) )
i 1
jó(i )
ahol
jó:[1..n]
jó(i) = j [1..m]: (t[i,j]=5 t[i,j]=4)
összeg:[1..n] ℕ
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 i:ℕ
jó(i) l jó(i) l jó(i)
SKIP max<összeg(i) l, max, ind :=
max, ind := SKIP igaz, összeg(i), i max:ℕ
összeg(i), i
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.

u:=jó(i) össz:=összeg(i)
u,j := igaz,1 össz := 0
u j m j:ℕ j = 1..m j:ℕ
u := t[i,j]=5 t[i,j]=4 ö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:
u l u l u
SKIP össz := összeg(i) l, max, ind := össz,max:ℕ
max<össz igaz, összeg(i), i

max, ind := SKIP


össz, i
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] v[j] )

l, i := hamis, 1 i:ℕ
l i k–1
l, j := igaz, k j:ℕ
l 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 igaz-
e, 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

k 1 n
Uf = ( Ef l = k>1 max v[i] max v[ j ] )
i 1 j k
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<v[i]
max1 := v[i] SKIP
max2 := v[k] max2:ℤ
j = k..n j:ℕ
max2<v[j]
max2 := v[j] SKIP
l := hamis 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.
n
Uf = ( Ef max, ind = max v[i] l = ind<k )
i 1

124
6.2. Beágyazott visszavezetés

max, ind := v[1], 1 max:ℤ ind:ℕ


i = 2..n i:ℕ
max<v[i]
max, ind := v[i], i 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
n
Uf = ( Ef 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:
n
Uf = ( Ef max = max v[ j ] l = i [1..k–1]: v[i] max )
j k

max := v[k] max:ℤ


i = k+1..n i:ℕ
max<v[i]
max := v[i] 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
Uf = ( Ef (max, jnd), ind = max g(i) )
i 2
g:[2..n] ℝ×ℕ é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 max:ℕ


i = 3 .. n 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<i. Megfordítva, a sorfolytonos kiterítés k-dik eleme a mátrixos
elrendezésben azon [i,j]-dik elem, amelyre egyrészt j=2k–(i–1)(i–2),
másrészt amennyiben 2k> 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<i) miatt
(i-1)(i-2)<2k≤i(i–1), amiből (i–2)< 2k <i összefüggést kapjuk. Ebből
látszik, hogy a 2k az (i–1) környezetébe esik: a 2k (felső
egészrész) vagy i, vagy i–1. Ha 2k> 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 táv(h(k)) (ind,jnd)=h(knd) )
k 1
h:[1..n(n–1)/2] ℕ×ℕ

( 2k +1, 2k- 2k ( 2k -1 ) ) ha 2k ( 2k -1) 2k


h( k )

( 2k , 2k- ( 2k -1)( 2k -2 ) ) ha 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 nem-
megengedett 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 szekvencia-
cseré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 felt1 … feltk


S1 SKIP helyett 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 helyett i = 1 .. 10
S1 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 max = max pont(i) )
i 1
pont ( i ) max
i 1

pont:[1..n] ℝ
m m m
pont(i) = ( t[i, j ] - max t[i, j ] - min t[i, j ] )/(m–2)
j 1 j 1
i 1

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 <i> 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] o, maxi, mini,:ℝ
j = 2 .. m 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
pont (i) összegzés lesz, amelyet a program legelején kell
i 1
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 <i> 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<f(i)
max,ind:= f(i), i 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 (amely valamelyik


programozási tételből származik).

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 nem-
negatí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 1) 1 ha x[i ] 0
f(i) = i [1..n]
0 ha x[i ] 0
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<h
hossz, ind := h, i 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<h
hossz, ind := h, i 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ét-
komponensű 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 i:ℕ i, l, j := 1, igaz, 1 i,j:ℕ


l i m l i m
l := f1(i) n
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 ] szöveg[ f(j 1) 1]
f(j 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 n-
edik 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 j:ℕ l, j, i := hamis, 1, 0 i,j:ℕ


l j n l j n
l := f (j)=m t[j]=sz[i+1]
j := j+1 i := i+1 SKIP
l := i=m
j := j+1

146
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ípus-
specifiká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ípus-
implementá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' )
n
Uf = ( Ef 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 nem-
megengedett 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ípus-
specifiká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ípus-
specifiká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 8
2 5

1 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} 100
A =(h:2 ) A = (v: , db:ℕ)
Ef = ( h=h' ) 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ípusműveletek
típus- betesz
specifikáció 2
{1..100}
h:=h {e}
1 és 100 közé eső egész legyen üres
számokat tartalmazó
halmazok h :=
elemszáma
s:= h
{1..100}
: 100
ℕ 2
100
(v,db) =  v[i]
i 1
100 v: 100, db:ℕ,
I(v,db) =( db= 1) e: ℕ, l: , s:ℕ
i 1
v[i ]

típus betesz
megvalósítás
100
ℕ v[e]=hamis
logikai tömb és egy szám
v[e]:=igaz SKIP
db:=db+1

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 SKIP
db := db+1

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ípus-
specifiká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 nem-
megengedettek, 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ípus-
specifiká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ípus-
specifiká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 állapottere kitalálható a


programok változóinak típusából.

típusértékek típusműveletek
típus legfeljebb 100 különböző legyen üres
specifikáció egész számot tartalmazó
Zsák betesz
maximum
{1..100}×ℕ {1..100}×ℕ
:2 Zsák b: 2 ,
I(b)= e1, e2 b: e1.1 e:{1..100}
e2.1
absztrakt {1..100}
×ℕ
2 -beli b:=
típus
számpárok halmaza b:= b {e}
e:=MAX(b)
{1..100}×ℕ
: ℕ100 2
100 v: ℕ100, e:{1..100}
ρ (v) {(i, v[i])}
i 1
konkrét típus ℕ100 e [1..100]: v[e]:=0
előfordulás számok v[e]:=v[e]+1
tömbje
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 műveletek


típus legfeljebb 30 karaktert betesz: v := push(v,e)
specifikáció tartalmazó verem

kivesz: v := pop(v)
olvas: e := top(v)
üres-e: l := empty(v)
legyen üres: v :=
*
I(s)= s 100 s: , e: , l:
absztrakt típus legfeljebb 30 hosszú s := s, e
*
-beli sorozat
s := s1, … ,s s -1

e := s s

l := s=
s :=

: 30
ℕ *

m t: 30
, m: ℕ, e: , l:
(t,m) = t[i ]
i 1

konkrét típus ℕ
30
t[m+1], m :=e, m+1
tömb-szám pár 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]:
(b, k , push(s , a[i] )) ha a[i] ' , '
f(i)
(b[1..k ] s ', ', k s 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 i-
edikké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 i = m .. n
F(i) 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 i = 1 .. s

F(si) 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 s i = 1 .. 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églalap-
má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 feladat-
megoldási tapasztalataink sem vesznek el.
A programozási tételek ehhez hasonló általánosításaival egyszer-
egyszer 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ó: Algoritmus:
A = (t:enor(E), s:H) s := 0
Ef = ( t=t’ )
t.First()
t'
Uf = ( s = f (ti, ) ) = t.End()
i 1
s := s+f(t.Current())
=( s f(e) )
e t' t.Next()

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ó: Algoritmus:
A = (t:enor(E), c:ℕ) c:=0
Ef = ( t=t’ )
t.First()
t'
Uf = ( c 1 )= t.End()
i 1
( t.Current() )
β(t i, )
c := c+1 SKIP
=( c 1)
e t' t.Next()
β(e )

196
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 )
t'
'
Uf = ( ind [1.. t’ ]:elem= t ind '
max=f( t ind )= max f(ti, ) ) =
i 1
t'
, ,
= ( (max, ind ) 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ó: Algoritmus:
A = (t:enor(E), elem:E) t.First()
Ef = ( t=t’ i [1.. t ]: (ti) )
Uf = ( ind [1.. t’ ]: elem=t’ind (t.Current())
(elem) j [1..ind-1]: ( t 'j ) t.Next()
t = t’[ind+1.. t’ ] ) = elem:=t.Current()
' '
= ( (ind,t) 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ó: Algoritmus:
A = (t:enor(E), l: , elem:E) l := hamis; t.First()
Ef = ( t=t’ )
l t.End()
Uf = ( l= i [1.. t’ ]: ( ti' )
l ind [1.. t’ ]:elem=t’ind elem := t.Current()
(elem) l := (elem)
'
j [1..ind-1]: (t j ) t.Next()
t = t’[ind+1.. t’ ] ) =
t'
= ( (l,ind,t) search β (t i' ) '
elem= tind ) = ( (l, elem,t) search β (e) )
i 1 e t'

198
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' ) l '
ind [1.. t’ ]: elem= t ind
(elem) max=f(elem) i [1.. t’ ]: ( ( ti' ) f( ti' ) max) ) =
t'
= ( (l, max, ind) = max f(t i, ) '
elem= tind )=
i 1
β (t'i )
= ( (l, max, elem) = max f(e) )
e t'
β(e)

Algoritmus:
l:= hamis; t.First()
t.End()
(t.Current()) (t.Current()) l ( t.Current()) l
SKIP f(t.Current())>max l, max, elem :=
max, elem:= SKIP igaz, f(t.Current()),
f(t.Current()), 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.
n
összegzés: s= f(v[i] )
i m
n
számlálás: c 1)
i m
β (v[i ] )
n
maximum kiválasztás: max, ind = max f(v[i] )
i m
n
feltételes maximumkeresés: l, max, ind = max f(v[i ] )
i m
β (v[i ] )
n
lineáris keresés: l , ind searchβ (v[i ] )
i m
kiválasztás: ind 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: s= f(a[i, j ] )
i 1, j 1
n, m
számlálás: c 1
i 1, j 1
β (a[i , j ] )
n, m
maximum kiválasztás: max, ind, jnd = max f(a[i, j ] )
i 1, j 1
n, m
feltételes maximumkeresés: l, max, ind, jnd = max f(a[i, j ] )
i 1, j 1
β (a[i , j ] )
n, m
lineáris keresés: l , ind, jnd search β (a[i, j ] )
i 1, j 1
m
kiválasztás: ind, jnd 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 Felsorolás
E ~ felsoroló ~ f:infile( ),
(e) ~ st=abnorm st:Státusz, e:
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’ )
n, m
Uf = ( a=a’ l , (ind , jnd ) 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 Felsorolás
E ~ ℕ×ℕ felsoroló ~ a:ℤ n×m, i, j:ℕ
(i,j) ~ 2 a[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
l i≤ n
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’ )
Uf = ( y e, e )
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 z {e}
e x'
e.mag 180 e.súly 60
e.haj 'barna' e.haj ' fekete'
v[1..n] e )
e x'
e.haj ' fekete'
e.alibi
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 ~ Ember* 2Ember Ember*
+,0 ~ ,<> , ,<>
s ~ y z v
f(e) ~ <e> ha {e} ha <e> ha
e.mag>180 e.súly<60 e.haj = ”fekete”
e.haj = ”barna” e.haj = ”fekete” e.alibi

y:=<> z:= n:=0


st,e,x:read st,e,x:read st,e,x:read
st = norm st = norm st = norm
e.mag>180 e.súly<60 e.haj=’fekete’
e.haj=’barna’ 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

214
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} SKIP
e.haj=’fekete’ e.alibi
n:=n+1; v[n]:=e SKIP
st,e,x:read

215
8. Programozási tételek felsoroló objektumokra

8.6. Feladatok

8.1. 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 i-
edik 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!

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) )
i 2, j 1

ahol táv(i, j ) ( 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() j<i-1 esetén növeli a j-t, j=i-1 esetén növeli az i-t és 1-
re állítja a j-t. Az End() akkor lesz igaz, ha i>n. 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)
Összegzés illetve számlálás esetén: s, t f ( e) , c , t 1,
e t' e t'
(e)
(e)
maximum kiválasztás esetén: max, elem , t 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
Uf = ( db,( st, e, f) = 1)
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él-
azonosí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
Uf = ( max, maxelem, ( st , e, f ) max e.össz )
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’ )
Uf = ( v e )
e t'
e.össz 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()) SKIP
t.Next()

224
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 vége := igaz

sx=norm 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
i= select ( x[i ] 0 i n)
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
SKIP
i n x[i] 0
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().hossz,
t.Current().eleje
t.Next()
t.End()

229
9. Visszavezetés egyedi felsorolókkal

t.Current().hossz>max
max, kezd := t.Current().hossz, SKIP
t.Current().eleje
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ő nem-
szó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 abnorm df ' ')
df f'
nincs_szó=(sf1=norm)
nincs_szó (
df ' '
2 2 2
van _ w, ( sf , df , f ) search (df ' w' )
df ( df 1 , f 1 )
sf , df , f select ( sf abnorm df ' ') ) )
df ( sf 2 , df 2 , f 2 )

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 ’ ’
SKIP
van_w:=(df=’w’)
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ó (
df ' '
van _ w, ( sf 2 , df 2 , f 2 ) (df ' w' )
df ( df 1 , f 1 )
sf , df , f select ( sf abnorm df ' ') ) )
df ( sf 2 , df 2 , f 2 )

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 (sf abnorm df ' ')
df ( sf ', df ', f ' )
nincs_szó=(sf1=norm) nincs_szó ( lásd UfNext -ben ) )

t.Next()
nincs_szó:= sf=norm
nincs_szó
van_w:=hamis
sf = norm df ’ ’ SKIP

van_w:= van_w (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
ha e x' e y'
f(e) ha e x' e 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 e x e y
SKIP SKIP z:= z {e}
x:=x–{e} y:=y–{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 e x e y
SKIP z:= z {e}
x:=x–{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 )
Uf = ( z = f(e) )
e { x '} { y '}
ahol f:E F* és
f 1(e) ha e {x'} e { y '}
f(e) 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’ )
Uf = ( z = f(e) )
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(), akkor a szigorúan növekedő


rendezettség miatt biztosak lehetünk abban, hogy az x.Current()
egyáltalán nem szerepelhet az y felsorolásban. Ha tehát ilyenkor
x.Current()-et választjuk az absztrakt felsorolás következő elemének,
akkor erről kijelenthetjük, hogy csak az x-ben lelhető fel. Szimmetria
okokból az 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()<y.Current().
Figyelembe véve az eddigieket az alábbi programhoz jutunk.

z := <>
x.First(); y.First();
x.End() y.End()
y.End() x.End() x.End()
( x.End() ( y.End() y.End()
x.Current()< x.Current()> x.Current()=
y.Current() ) y.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()<y.Current()) lett volna.
A t.Next() elágazását most is összevontuk a feldolgozás elágazásával.

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() y.End()
x.Current()< x.Current()> x.Current()=
y.Current() y.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 )
Uf = ( z = f(e) )
e { x '} { y '}

e ha e {x '} e { y '}
f(e) ha 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 sy=norm sy=norm
dx<dy dx>dy 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'
a (t ' ) ha a azon (t ' ) a azon (m' )
f(a) f 2 (a) ha a azon (t ' ) a azon (m' )
f 3 (a) ha a azon (t ' ) a azon (m' )

(a, a(m' ).me) ha e.müv I


f 2(a)
hiba k ülönben

hiba ha a(m' ).müv I


f 3(a) ha a(m' ).müv D
g (a) ha a(m' ).müv U

a, a(t ' ).me a(m' ).me ha a(t ' ).me a(m' ).me 0
g(a)
a(t ' ) hiba 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=norm
sm=abnorm st=abnorm st=norm sm=norm
(sm=norm dt.azon=dm.azon
(st=norm dt.azon>
dt.azon < dm.azon)
dm.azon)
u:write(dt) dm.müv=
dm.müv=I I D U
u:write( H H S c :=
dm.azon, I I K dt.me+dm.me
dm.me) B B I
c<0
A A P
H dt.me:=c
I
B
A
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 df[i]=min
sf[i],df[i],f[i]:read 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 := min dm[i].azon
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 )
Uf = ( u f(a) )
a azon(t ') azon( y ')

a (t ' ) ha a azon (t ' ) a azon ( y ' )


f(a) hiba ha a azon (t ' ) a azon ( y ' )
g (a) ha a az (t ' ) a azon ( y ' )

g(a) 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()
y.End() st=abnorm st=norm
(st=norm (st=norm y.End()
dt.azon< dt.azon> dt.azon=
y.Current().azon) y.Current().azon) y.Current().azon
dt.menny:=
u:write(dt) HIBA 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 ] :
(r1 (i 1) xi , hamis ) ha xi ' '
r(i) (r1(i 1) xi , igaz ) ha xi ' ' r2(i 1)
(r(i 1)) ha 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 x
xi ’ ’ xi =’ ’ l xi =’ ’ l
y:write(xi) y:write(xi) SKIP
l := hamis l := igaz
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 ’’ ch =’ ’ l ch =’ ’ l
y:write(ch) y:write(ch) SKIP
l := hamis l := igaz
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 ] :
( xi , hamis ) ha xi ' '
r(i) ( xi , igaz ) ha xi ' ' r2(i 1)
( , r2 (i 1)) ha 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 r1 (i ) )
i 1
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( )) Pár = rec(e: , l: )


Ef = ( t=t’ )
Uf = ( y h1(ch,l) )
(ch, l ) t '
ahol h: * és

( ch , hamis ) ha ch ' '


h(ch,l) ( ch , igaz ) ha 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=’ ’ l ch=’ ’ l
y:write(’ ’) y:write(ch) 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 )
i 2
x'i 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:ℕ) Pár = rec(e:ℝ, max:ℝ)


Ef = ( t=t’ )
Uf = ( db 1 )
( 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 sum = i )
i 2
in 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' )
Uf = ( sum = e )
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) d:=lko(n)
n>1 n>1
sum := sum + d d|n SKIP
d|n 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. 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?
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.)
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.
9.4. 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.)
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

257
9. Visszavezetés egyedi felsorolókkal

tartalmazó – szóhatár nevű – halmazban találhatók. Feltehetjük,


hogy a szavak 60 karakternél rövidebbek.
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!
9.7. 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!
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!
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

258
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:ℝ , 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állapot: (s, t, s/t)
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őállapot-
cé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őállapot-


cé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 m<n, akkor legyen az x értéke először a (–1)m, majd
adjuk hozzá a (–1)m+1-t, utána a (-1)m+2-t és így tovább,
végül a (-1)n-t!
Ha m=n, akkor legyen az x értéke a (–1)n!
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!

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
m<n vagy m>n. 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 1,2,4,5 1 1,4,3,5,2 1 1,3,2,...
2 2,1 2 2,4 3 3,3,...
S
4 4,1,5,1,4 4 4,3,1,2,5,1 4 4,1,5,4,2
5 5,2,4 5 5,3,4 5 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, akkor S sem az?
b) ha F determinisztikus, akkor S is 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 k [2 .. p–1]: (k p) ) =
( Ef l=prím(n) )
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 y1<y2 y1<y<y2 y1>y2 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 f1
S1 SKIP f2
f2 f3
S2 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”
b
(b2+4ac = 0) ( v = „Közös valós gyök” x1 x2 )
2a
(b2+4ac > 0) ( v = „Két valós gyök”
b b2 4ac b b2 4ac
x1 x2 ))
2a 2a

272
3. fejezet feladatainak megoldása

c
(a=0 (b 0 ( v = „Elsőfokú gyök” x1 )
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 4ac b 0 b=0 b=0
c=0 c 0
d<0 d=0 d>0
válasz:= válasz:= válasz:= válasz:= válasz válasz
Nincs Közös Két Elsőfokú := :=
valós valós valós gyök A E
gyök gyök gyök z l
x1 , x 2 : x1 , x 2 : c o l
x1
b b n e
b d
2a 2a o n
s t
s m
á o
g n
d
á
s
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 legfeljebb abs(n) hátralevő lépés
x:=x – sgn(x) 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 legfeljebb n hátralevő lépés
f:=f n 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 legfeljebb n hátralevő lépés
n páros 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 z y)
i 1

Összegzés:
m..n ~ 1..x
s ~ z
f(i) ~ y

Algoritmus:
z, i :=0, 1 z := 0
i x i:ℕ i = 1 .. x i:ℕ
z := z+y rövidebben 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 i:ℕ
l 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 i:ℕ k := 0
i n i = 1 .. n i:ℕ
x[i] páros vagy x[i] páros
k := k+1 SKIP k := k+1 SKIP
y[k] := x[i] 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 l, d search( x[i] 0 x[i 1] 0) )
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 i:ℕ
l 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 )
n
Uf = ( Ef 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 i:ℕ max := x[1]
i n vagy i = 2 .. n i:ℕ
max< x[i] max< x[i]
max := SKIP max := SKIP
x[i] x[i]
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 c f(i) d
Algoritmus:
db, i := 0, m i:ℤ db := 0
i n vagy i = m .. n i:ℤ
a f(i) b c f(i) d a f(i) b c f(i) d
db := db+1 SKIP db := SKIP
db+1
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)<h 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 i:ℕ
(x[i–1]>x[i] l x[i–1]>x[i] l x[i–1]>x[i]
x[i]<x[i+1]) x[i]<x[i+1] x[i]<x[i+1]
SKIP max< x[i] l, max, ind :=
max, ind SKIP igaz, x[i], i
:= 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
Uf = ( Ef db 1)
i 1
x[i] 0 x[i 1] 0 ]

Számlálás:
m..n ~ 1..n-1
c ~ db
(i) ~ x[i]=0 x[i+1]<0
Algoritmus:
db := 0
i = 1 .. n-1 i:ℕ
x[i]=0 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 )
n
Uf = ( Ef 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


2 2

távolságának négyzete.)
Minimum kiválasztás:
m..n ~ 1..n
f(i) ~ x[i]2+y[i]2
max ~ min

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] x[i 1]) )
i 2

Optimista lineáris keresés (eldöntés):

282
6. fejezet feladatainak megoldása

m..n ~ 2..n
(i) ~ x[i] x[i–1]

l,i:= igaz, 2 i:ℕ


l 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 = m .. n i: ℤ
x[i] mod k 1 l l
x[i] mod k =1 x[i] mod k =1
SKIP min< x[i] l, min, ind :=
min, ind:= SKIP igaz, x[i], i
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] )
n
Uf = ( Ef 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 = 2 .. n i: ℕ
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] x[n i 1]) )
i 1

Optimista lineáris keresés:


m..n ~ 1.. n div2
(i) ~ x[i]=x[n–i+1]

l, i:= igaz, 1 i:ℤ


l 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 =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 közös
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 ) )
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
= ( Ef l search i n] )
i 2
Optimista lineáris keresés:
m..n ~ 1.. n
(i) ~ i|n

286
6. fejezet feladatainak megoldása

l, i:= igaz, 2 i:ℕ


l 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
= ( Ef l search ( x[i ] 0 múlt(i)) )
i k 1
ahol múlt:[k+1..n] é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 i: ℕ l:=múlt(i)
l i n l, j:= igaz, i–k j: ℕ
l := x[i]>0 múlt(i) l j i–1
i := i+1 l := x[j]<0
j :=j+1

b) Specifikáció:

288
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 múlt(i) k) )
i 2
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 i:ℕ l, i, m:= hamis, 2, 0
l i n l i n
l := x[i]>0 múlt(i)=k x[i–1]<0
i := i+1 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
i 1
ahol hány:[1..n] ℕ és hány(i) = 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 i:ℕ h := 0
h := hány(i) j = 1 .. i-1 j:ℕ
h>max 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
i 1
ahol hány:[1..n] ℕ és hányt(i) = 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 i:ℕ h := 0
h := hány(i) j = 1 .. i–1 j: ℕ
h>max x[j]>x[i]
max, diák:=h, i SKIP h := h+1 SKIP
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]) =
n m 1 m
= ( Ef l search ( search ( s[i j 1] p[ j ])) )
i 1 j 1

Lineáris keresésbe ágyazott optimista lineáris keresés:

290
6. fejezet feladatainak megoldása

l, i:= hamis, 1 i:ℕ l := s[i..i+m–1]= p[1..m]


l i n–m+1 l, j:= igaz, 1 j:ℕ
l := l 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

j
ahol összeg:[1..n] ℝ és összeg(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 s:=összeg(i)
j = 2 .. n j:ℕ s := 0
s := összeg(j) i = 1 .. j i:ℕ
s>max s := s+t[i,j]
max, oszlop:=s, 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 l = i [1..n]: j [1..i–1]: x[i]= x[j] ) =


n i 1
= ( Ef l , ind search ( search ( x[i] x[ j ]))
i 1 j 1

l szám=x[ind] )
Lineáris keresésbe ágyazott lineáris keresés:
l,i:= hamis, 1 i:ℕ l := belső keresés(i)
l i n l,j := hamis, 1 j:ℕ
l := belső keresés(i) l 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 ] x[ j ])
j k 1
Feltételes minimum keresésbe ágyazott fordított lineáris keresés:
l := hamis
k := 1 .. n k:ℕ
ll, d := f(k) ll: , d:ℕ
ll l ll l ll
SKIP min>k–d l, min, i :=
min, i := k–d, k SKIP igaz, k–d, k

292
6. fejezet feladatainak megoldása

ll, d := f(k)
ll, j := hamis, k-1 j:ℕ
ll 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 i:ℕ l, i, ny:= hamis, 1, 0
l i n l i n
l := nyereség(i)<0 ny := ny+x[i]
i := i+1 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 m
Uf = ( Ef l search ( search prím (t[i, j ])) )
i 1 j 1
a
ahol prím:ℕ és prím(a) = (a>1 search( (k a)) )
k 2

Keresésben keresésbe ágyazott optimista keresés: 25


l := sor(i) l := prím(t[i,j])
l, i:= hamis, 1 l, j := hamis, 1 l, k := t[i,j]>1, 2
l i n l 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 m
Uf = ( Ef l search( search prím (t[i, j ])) )
i 1 j 1
a
ahol prím:ℕ és prím(a) = (a>1 search( (k a)) )
k 2

Keresésben optimista keresésbe ágyazott optimista keresés:


l := sor(i) l := prím(t[i,j])
l, i:= hamis, 1 l, j := igaz, 1 l, k := t[i,j]>1, 2
l i n l 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
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 m
Uf = ( Ef l search ( search prím (t[i, j ])) )
i 1 j 1
a
ahol prím:ℕ és prím(a) = (a>1 search( (k a)) )
k 2

Optimista keresésben lineáris keresésbe ágyazott optimista


keresés:
l := sor(i) l := prím(t[i,j])
l ,i:= igaz, 1 l, j := hamis, 1 l, k := t[i,j]>1, 2
l i n l 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 m
Uf = ( Ef l search( search prím (t[i, j ])) )
i 1 j 1
a
ahol prím:ℕ és prím(a) = (a>1 search( (k a)) )
k 2

Optimista keresésben optimista keresésbe ágyazott optimista


keresés:
l := sor(i) l := prím(t[i,j])
l, i:= igaz, 1 l, j := igaz, 1 l, k := t[i,j]>1, 2
l i n l 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

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
Uf = ( Ef l search (ért 1i (m) 1) )
i 1
ahol ért :[0..m] ℕ×ℕ egy rekurzív függvény, amelynek j-edik
i

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

érti (0)= (0,-1)


(ért 1i (j 1) 1, ért 2i (j 1)) ha t[i, j ] ért 2i (j 1)
ért i (j) (1, t[i, j ]) ha t[i, j ] ért 2i (j 1)
(ért 1i (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 j = 0 .. m
i := i+1 t[i,j]=max t[i,j]>max t[i,j]<max
db := db+1 db, max := SKIP
1,t[i,j]

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
l j ≤m
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)
m
ahol mind:[1..n] és mind(i) = 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) l j m
db := db+1 SKIP 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:ℤ
ℤ×ℤ z1, z2 := x1*y2 x2*y1, x2*y2
második szám
nem nulla 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.

1 és 100 közötti betesz


egész számokat
tartalmazó kivesz
Zsák hányszor

300
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 (e,m) h h:= h-{(e,m)}) {(e,m+1)}
egész szám ás
darabszám párok (e,m) h h:=h {(e,1)}
halmaza (e,m) h h:= h-{(e,m)}
2ℕ×ℕ
(e,m) h db:= m
(e,m) h db:= 0
N×N
:ℕ100 2 (v)={(e,v[e]) | e [1..100] 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 tele-
e 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 betesz


tartalmazó
kivesz
Sor
Ü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 a,f [0..99]
100
I(v,a,f,db) = v:ℤ
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
(x) = xi 10 i 1 , I(x) = x x 0
I: {0..9}* i 1

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 i y
z, d, i: = z ((xi+yi+d) mod 10), (xi+yi+d) div 10, i+1
z := z <d>

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 (c-
vel) 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 ] {0..9}*×{0..9}
f(0) = (<>,0)
f(i) = ( f1 (i-1) ((xi*c+f2 (i-1)) mod 10) ,
(xi*c+ f2 (i-1)) div 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 <d>

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
( z) ( x) y k 10 k 1
azaz
k 1
y
z 0 ... 0 (x yk )
k 1 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 (a) = a0+a1 x + … +a a x a , I(a) = a a 0
I:ℝ*
a p, q, r-t rendre az a,b,c :ℝ*reprezentálja
ℝ* k [0..max( a , b )]: c[k ] a[k ] b[k ] )

n
k [0.. a * b ]: 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 x[i ] ha i j
Diag(ℝn×n) (x)[i,j] =
0 ha i 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 e = x[i]
i j 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 AlsóH(ℝn×n) (x)[i,j] = x[n (i 1) j ] ha i j
0 ha i 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 e = x[i (i–1)/2+j]
i<j e=0

306
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’ )
n
Uf = ( x=x’ 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
y :=y x[i] SKIP
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ó}

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.
8.2. 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 nem-
negatí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; st,e,f:read
l 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 e.egyl 0 l e.egyl 0
SKIP min >e.egyl l, min, a :=
min, a:= SKIP igaz, e.egyl, e.az
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 x'
e.egyl 0 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 ~ Ügyfél E ~ Ügyfél
* *
H ~ H ~
+,0 ~ ,<> +,0 ~ ,<>
f(e) ~ e.egyl ha e.egyl<0 f(e) ~ e.egyl ha e.egyl>0
Algoritmus:

310
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 e
e x'
e.ős " Mexikó"
u e )
e x'
e.virág " piros"
e.ős " Mexikó"
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 ~ Kaktusz Kaktusz Kaktusz


* * *
H ~
+,0 ~ ,<> ,<> ,<>
f(e) ~ <e.név> <e.név> <e.név>
ha e.virág=„ e.ős= e.virág=
piros” „Mexikó” „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) SKIP
e.virág=”piros” 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 m := min

8.7. 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’ )
Uf = ( y e f ( e.bér )
(
/ 1 )
e x' e x' e x'
e.tipus fiz e.tipus fiz e.tipus fiz
e.tú l 20
e.család 4

a ( e.bér )
(
/ 1 )
e x' e x'
e.tipus adm e.tipus adm
v ( e.bér )
(
/ 1 ))
e x' e x'
e.tipus vez 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 SKIP
st,e,x:read
f , a, v := f/fdb, a/adb, v/vdb

314
8. fejezet feladatainak megoldása

8.8. Adott két vektorban egy angol-latin szótár: az egyik vektor i-


edik 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')
n
Uf = ( x = x' y = y' 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 SKIP

8.9. 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:
" aa" ha ch " á"
" ee" ha ch " é"
" i" ha ch " í "
" oe" ha ch " ö" vagy ch " ő "
" ue" ha ch " ü" vagy ch " ű "
" o" ha ch " ó"
távirat (ch) " Aa" ha ch " Á"
" Ee" ha ch " É"
" I" ha ch " Í "
" Oe" ha ch " Ö" vagy ch " Ő"
"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’ )
Uf = ( max max elem.a )
elem t'
elem.e elem.a elem.k
Algoritmus:
l:= hamis; t.First()
t.End()
e, a, k =t.Current()
(e>a<k) l e>a<k l e>a<k
SKIP max<a l, max := igaz, a
max := 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 (e, a, k ):=t.Current()
sf,k,f:read sf,k,f:read 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’ )
Uf = ( s f (e) )
e t'
1 ha e 12
f:ℕ ℕ f(e)=
2 ha e 12

318
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)
df ' '
2 2 2
van_szó ( hossz , sf , df , f 1
df ( df 1 , f 1 )
sf , df , f select ( sf abnorm df ' ') ) )
df ( sf 2 , df 2 , f 2 )
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 abnorm df ' ')
df f'
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 ( sf abnorm df ' ') ) )
df ( sf 2 , df 2 , f 2 )

t.First() t.Next()
sf, df, f : read van_szó:= sf=norm
sf = norm df=’ ’ van_szó
sf, df, f : read hossz:=0 SKIP
van_szó:= sf=norm sf = norm df ’ ’
t.Next() 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ény-
sorozatban 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:ℕ) ANext = (f:Bit*, e:ℕ, i:ℕ)


EfFirst = ( f = f’ ) EfNext = ( f = f’ i = i’ )
UfFirst = ( f = f’ UfNwxt = ( f = f’
(|f| 0 i=1 hossz=f1) ) fi fi ' i f
hossz , i 1 ))
i i'

t.Fist() t.Next()
|f| 0 bit, hossz:= fi, 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' )
Uf = ( g e.ssz )
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, count:ℕ, szó: *
, l:
*
( × )n×ℕ i [1..n]: v[i].jel := hamis
count := 0

n
l , i : search (v[i ].szó szó )
i 1

l v[i].jel
v[i].jel := true SKIP
count:= count+1

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


re 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() S u.End() S
BekVég(u.Current()) K BekVég(u.Current()) 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) S
sf = norm Jel(df)=jel K
szakasz:=szak I
asz+df
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.)
outfile60 Open()
Write60(szó)
*
g: outfile( ), szó: , free:ℕ,
outfile( )× ℕ g := <>
free:=60

hossz(szó)+1>free
g:write(sorvég) g:write(’ ’)
free:=60 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 )
Uf = ( ko g (e) cf h ( e) )
e {c '} { f '} e {c '} { f '}

e ha e { c' } e { f ' }
g( e ) ha e { c' } e { f ' }
ha e { c' } e { f ' }
ha e { c' } e { f ' }
h( e ) ha e { c' } e { f ' }
e ha e { c' } e { f ' }
Ez egy összefuttatásos (lásd 9.5. alfejezet) összegzés
szekvenciális inputfájlok felett.
Algoritmus:

sc,dc,c:read ; sf,df,f:read ; ko := <>; cf := <>


sc=norm sf=norm
sf=abnorm sc=abnorm sc=norm sf=norm
(sc=norm (sf=norm dc=df
dc<df) 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 )
Uf = ( u f 1 (a)
a azon(t ') azon( m')
h f 2 (a) )
a azon(t ') azon( m')

a(t ' ), ha a azon (t ' ) a azon (m' )


f (a) , " hiba" ha a azon (t ' ) a azon (m' )
g (a) ha a azon (t ' ) a azon (m' )
a(t ' ) , " hiba" ha a(t ' ). pld a(m' ). forg 0
g (a)
a(t ' ) pld pld a ( m'). forg , ha a(t ' ). pld a(m' ). forg 0

328
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 sm,dm,m:read
st=norm sm=norm
sm=abnorm st=abnorm st=norm
st=norm sm=norm sm=norm
dt.azon<dm.azon dt.azon>dm.azon dt.azon=dm.azon
a := dt.pld+dm.forg
a 0
u:hiext(dt) h:hiext(„hiba”) 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 dt.beo>dm.beo
sm,dm,m:read 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 )
Uf = ( v f (a ) )
a eha( x ') ( y ')

ha a eha( x' ) a { y' }


f(a) ha a eha( x' ) a { y' }
g( a ) ha a eha( x' ) a { y' }

a( x' ) ha a( x' ). jel ' X '


eha( x' ). jel ' Q'
a( x' ) ha a( x' ). jel ' N'
g( a ) eha( x' ). jel ' N'
a( x' ) ha a( x' ). jel ' Q'
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.End() ( r.End() x.Current().eha=dr
x.Current().eha x.Current().eha
<dr) >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, ind, min :=
n
l, ind, min := dg[i].eha
n min
i 1
min dg[i].eha sg [i ] norm
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 neg)
poz poz poz poz neg else
neg e>0 neg e>0 neg e<0 e<0
poz, első:= poz:= neg, első:= neg:= igaz SKIP
igaz, e igaz igaz, e
st,e,x:read

335
IRODALOM JEGYZÉK

1. Dijkstra E.W., „A Discipline of Programming”,Prentice-


Hall, 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

You might also like