programozas - tervezes

336
1 Gregorics Tibor PROGRAMOZÁS 1. kötet TERVEZÉS egyetemi jegyzet 2011

Upload: schiman-daniel

Post on 29-Dec-2015

84 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Programozas - Tervezes

1

Gregorics Tibor

PROGRAMOZÁS 1. kötet

TERVEZÉS

egyetemi jegyzet

2011

Page 2: Programozas - Tervezes

ELŐSZÓ

2

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

Page 3: Programozas - Tervezes

ELŐSZÓ

3

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

Page 4: Programozas - Tervezes

4

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

Page 5: Programozas - Tervezes

ELŐSZÓ

5

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

Page 6: Programozas - Tervezes

6

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,

Page 7: Programozas - Tervezes

BEVEZETÉS

7

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

Page 8: Programozas - Tervezes

BEVEZETÉS

8

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.

Page 9: Programozas - Tervezes

9

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.

Page 10: Programozas - Tervezes

10

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

Page 11: Programozas - Tervezes

1.1. Az adatok típusa

11

rejtett – hg2v 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 hv 20 -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

Page 12: Programozas - Tervezes

1. Alapfogalmak

12

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

Page 13: Programozas - Tervezes

1.1. Az adatok típusa

13

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.

Page 14: Programozas - Tervezes

1. Alapfogalmak

14

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.

Page 15: Programozas - Tervezes

1.3. Feladat fogalma

15

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: 020 h ) 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

Page 16: Programozas - Tervezes

1. Alapfogalmak

16

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)

Page 17: Programozas - Tervezes

1.3. Feladat fogalma

17

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

Page 18: Programozas - Tervezes

1. Alapfogalmak

18

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

Page 19: Programozas - Tervezes

1.4. Program fogalma

19

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.

Page 20: Programozas - Tervezes

1. Alapfogalmak

20

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

Page 21: Programozas - Tervezes

1.4. Program fogalma

21

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

Page 22: Programozas - Tervezes

1. Programozási alapfogalmak

22

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.

Page 23: Programozas - Tervezes

1.5. Megoldás fogalma

23

„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

Page 24: Programozas - Tervezes

1. Programozási alapfogalmak

24

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.

Page 25: Programozas - Tervezes

1.6. Feladatok

25

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:

Page 26: Programozas - Tervezes

1. Programozási alapfogalmak

26

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 1,2,3,4,5A á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.)

5,2,3,455,3,455,2,45

4,1,5,4,2414,3,1,2,5,44,1,5,1,44

3,3,...32,422,12

1,3,2,...11,4,3,5,211,2,4,51

S

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

Page 27: Programozas - Tervezes

1.6. Feladatok

27

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.

Page 28: Programozas - Tervezes

28

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.

Page 29: Programozas - Tervezes

2.1. Kezdőállapotok osztályozása

29

{(0, *)} {(4, *)} {(6, *)}

d

(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

Page 30: Programozas - Tervezes

2. Specifikáció

30

{(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

Page 31: Programozas - Tervezes

2.1. Kezdőállapotok osztályozása

31

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’)}.

Page 32: Programozas - Tervezes

2. Specifikáció

32

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

Page 33: Programozas - Tervezes

2.2. Feladatok specifikációja

33

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ó

Page 34: Programozas - Tervezes

2. Specifikáció

34

á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

Page 35: Programozas - Tervezes

2.2. Feladatok specifikációja

35

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:

Page 36: Programozas - Tervezes

2. Specifikáció

36

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

Page 37: Programozas - Tervezes

2.3. Feladatok

37

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!

Page 38: Programozas - Tervezes

38

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.

Page 39: Programozas - Tervezes

39

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.

Page 40: Programozas - Tervezes

3. Strukturált programok

40

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.

Page 41: Programozas - Tervezes

3.1. Elemi programok

41

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

Page 42: Programozas - Tervezes

3. Strukturált programok

42

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.

Page 43: Programozas - Tervezes

3.1. Elemi programok

43

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

Page 44: Programozas - Tervezes

3. Strukturált programok

44

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

Page 45: Programozas - Tervezes

3.2. Programszerkezetek

45

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

Page 46: Programozas - Tervezes

3. Strukturált programok

46

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

Page 47: Programozas - Tervezes

3.2. Programszerkezetek

47

é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

Page 48: Programozas - Tervezes

3. Strukturált programok

48

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.

Page 49: Programozas - Tervezes

3.2. Programszerkezetek

49

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

Page 50: Programozas - Tervezes

3. Strukturált programok

50

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

ahol sign(z) =

1 0

0 0

1 0

ha

ha

ha

z

z

z

Az utófeltétel három esettel specifikálja a x változó új értékét:

x>0, x=0 és x<0. Ez a három eset lefedi az összes lehetséges esetet és

egyszerre közülük csak az egyik lehet igaz. A szignum függvény

definíciójából látszik, hogy az első esetben az x új értéke az 1 lesz,

második esetben a 0, harmadik esetben a –1. Világos tehát, hogy az

alábbi elágazás megoldja a feladatot.

Page 51: Programozas - Tervezes

3.2. Programszerkezetek

51

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

Page 52: Programozas - Tervezes

3. Strukturált programok

52

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

Page 53: Programozas - Tervezes

3.2. Programszerkezetek

53

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

Page 54: Programozas - Tervezes

3. Strukturált programok

54

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

Page 55: Programozas - Tervezes

3.2. Programszerkezetek

55

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

Page 56: Programozas - Tervezes

3. Strukturált programok

56

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.

Page 57: Programozas - Tervezes

3.3. Helyes program, megengedett program

57

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.

Page 58: Programozas - Tervezes

3. Strukturált programok

58

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!

Page 59: Programozas - Tervezes

3.4. Feladatok

59

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’! )

Page 60: Programozas - Tervezes

3. Strukturált programok

60

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

Page 61: Programozas - Tervezes

61

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.

Page 62: Programozas - Tervezes

62

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.

Page 63: Programozas - Tervezes

63

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.

Page 64: Programozas - Tervezes

4. Programozási tételek

64

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

Page 65: Programozas - Tervezes

4.1. Analóg programozás

65

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

Uf = ( Ef 2

n

1i

ivs ][ )

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

2][: ivss 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

n1n1mm )(...)( szorzat a n

mi

i zárt alakban felírható. A

jelölésről tudni kell, hogy ha m>n, akkor a produktum értéke 1. (Az 1

Page 66: Programozas - Tervezes

4. Programozási tételek

66

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

Uf = ( Ef n

i

if

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.

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!

f,i := 1,2 i:ℕ

i n

f := f * i

i := i+1

Page 67: Programozas - Tervezes

4.1. Analóg programozás

67

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

Uf = ( Ef ][*][1

ibiasn

i

)

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:

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 ))((...))(())(( párosngpáros1mgpárosmg összetett

feltételt a feladat specifikációjának utófeltételében az ))(( párosign

mi

zárt alakban írjuk fel. A jelölésről tudni kell, hogy ha m>n, akkor a

kifejezés értéke igaz.

s,i := 0,1 i:ℕ

i n

s := s+a[i]*b[i]

i := i+1

Page 68: Programozas - Tervezes

4. Programozási tételek

68

A = (m:ℤ, n:ℤ, l: )

Ef = ( m=m’ n=n’ )

Uf = ( Ef ))(( párosigsn

mi )

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)

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

Uf = ( Un

párosixi

ixh

][1

]}[{ )

A halmazok uniója is rokon művelet a számok feletti

összeadással. Egységeleme az üres halmaz. A műveletben szereplő

l,i := igaz, m i:ℤ

i n

l:=l (g(i) páros)

i := i+1

Page 69: Programozas - Tervezes

4.1. Analóg programozás

69

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

s:=s+ v[i]2 ~ 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.

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

h, i := ,1 i:ℕ

i n

x[i] páros

h := h {x[i]} SKIP

i := i+1

Page 70: Programozas - Tervezes

4. Programozási tételek

70

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

Page 71: Programozas - Tervezes

4.2. Programozási tétel fogalma

71

)(...)()()( nf1mfmfkfn

mk

kifejezés értékét! (m>n esetén

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

Uf = ( Ef )(ifsn

mi

)

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

Page 72: Programozas - Tervezes

4. Programozási tételek

72

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.

Page 73: Programozas - Tervezes

4.3. Nevezetes minták

73

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.

Page 74: Programozas - Tervezes

4. Programozási tételek

74

1. Összegzés

Az összegzés programozási tételét az előző alfejezetekben

alaposan elemeztük. Most csak a teljesség igénye miatt ismételjük meg.

Feladat: Adott az egész számok egy [m..n] intervalluma és egy

f:[m..n] H függvény. A H halmaz elemein értelmezett egy asszociatív,

baloldali nulla elemmel rendelkező művelet (nevezzük összeadásnak és

jelölje ezt a +). Határozzuk meg az f függvény [m..n]-en felvett

értékeinek az összegét, azaz a f(k)n

mk

kifejezés értékét! (m>n esetén

ennek az értéke definíció szerint a nulla elem).

Specifikáció:

A = (m:ℤ, n:ℤ, s:H) Ef = ( m=m’ n=n’ )

Uf = ( Ef f(i)sn

mi

)

Algoritmus:

s, i := 0, m i:ℤ

i n

s := s + f(i)

i := i + 1

Page 75: Programozas - Tervezes

4.3. Nevezetes minták

75

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

Uf = ( Ef 1

(i)

n

mi

c

β

)

Algoritmus:

c, i := 0, m i:ℤ

i n

(i)

c := c+1 SKIP

i := i + 1

Page 76: Programozas - Tervezes

4. Programozási tételek

76

3. Maximum kiválasztás

Ha arra vagyunk kíváncsiak, hogy egy 20 fős osztályban kinek

van a legjobb jegye történelemből, akkor ehhez a diákokat 1-től 20-ig

megsorszámozzuk, és a történelem jegyeiket egy 1-től 20-ig indexelt t

tömbben tároljuk: az i-edik diák osztályzata a t[i]. Ez a tömb tekinthető

egy f:[1..20] ℕ függvénynek (f(i)=t[i]), amely értékei között keressük

a legnagyobbat.

Feladat: Adott az egész számok egy [m..n] intervalluma és egy

f:[m..n] H függvény. A H halmaz elemein értelmezett egy teljes

rendezési reláció. Határozzuk meg, hogy az f függvény hol veszi fel az

[m..n] nem üres intervallumon a legnagyobb értéket, és mondjuk meg,

mekkora ez a maximális érték!

Specifikáció:

A = (m:ℤ, n:ℤ, ind:ℤ, max:H)

Ef = ( m=m’ n=n’ n m )

Uf = ( Ef ind [m..n] max=f(ind) i [m..n]: max f(i) ) =

másképpen jelölve, illetve rövidített jelölést használva:

= ( Ef ind [m..n] max = f(ind) = f(i)n

mimax )

= ( Ef max,ind= f(i)n

mimax )

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.

Page 77: Programozas - Tervezes

4.3. Nevezetes minták

77

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) = f(i)

(i)

n

mimax )

Algoritmus:

l, i := hamis, m i:ℤ

i n

(i) l (i) l (i)

SKIP max< f(i) l, max, ind :=

igaz, f(i), i max, ind := f(i), i SKIP

i := i + 1

Page 78: Programozas - Tervezes

4. Programozási tételek

78

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 (i)indmi

select )

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.

Page 79: Programozas - Tervezes

4.3. Nevezetes minták

79

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

Uf = ( Ef )(, iindln

misearch )

Algoritmus10

:

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

Page 80: Programozas - Tervezes

4. Programozási tételek

80

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

Page 81: Programozas - Tervezes

4.3. Nevezetes minták

81

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

Page 82: Programozas - Tervezes

4. Programozási tételek

82

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

Page 83: Programozas - Tervezes

4.3. Nevezetes minták

83

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.

Page 84: Programozas - Tervezes

4. Programozási tételek

84

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!

Page 85: Programozas - Tervezes

85

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

Uf = ( Ef 1db

1)f(jf(j)

b

aj

0*

1

)

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

Page 86: Programozas - Tervezes

5. Visszavezetés

86

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

Page 87: Programozas - Tervezes

5.2. Általános visszavezetés

87

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

= ( Ef ))((, /21)f(i1)f(if(i)kil1n

1misearch )

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

Page 88: Programozas - Tervezes

5. Visszavezetés

88

a kitűzött feladat állapotterének kevesebb komponenssel bír. Ennek két

figyelemre méltó alesete van. (Ezen túlmenően a természetes

visszavezetésnél megengedett eltérések is előfordulhatnak.)

Az egyik eset az, amikor a kitűzött feladat kevesebb eredmény

komponenst tartalmaz, mint a mintafeladat. A programozási tételeink

között ugyanis vannak olyanok, amelyek egyszerre több eredményt is

adnak, de gyakori, hogy egy konkrét feladatban ezek közül nincs

mindre szükségünk. Ezek közül egyiket-másikat el lehet hagyni a

feladat állapotteréből, ha arra nincs szükség, hiszen egy eredmény

komponens kezdőértékétől nem függ a többi eredmény. Természetesen

a minta feladatból így „elhagyott” komponensnek a változója ott marad

a programozási tétel programjában, de most már csak, mint

segédváltozó, és a program alap-állapottere a kitűzött feladat

állapotterével lesz azonos. A két feladat előfeltétele megegyezik

(hiszen az eredménykomponensek sohasem szerepelnek az

előfeltételben), a mintafeladat utófeltétele szigorúbb (mert az elhagyott

komponensekre is tesz megkötéseket), de ez – az előző alfejezet szerint

– nem jelent akadályt. A program tehát megoldja a kitűzött feladatot.

Tipikus példa erre az, amikor egy maximum kiválasztásnál a

maximális érték nem érdekel bennünket, csak az intervallumnak azon

eleme, amelyhez a maximum tartozik.

5.3. Példa. Keressük egy g:[a..b] ℝ függvénynek olyan

argumentumát, ahol a függvény a maximális értékét veszi fel!.

A = (a:ℤ, b:ℤ, ind:ℤ )

Ef = ( a=a’ b=b’ a≤b )

Uf = ( Ef ind [a..b] g(ind) = g(i)b

aimax )

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

komponenssel, az utófeltétele pedig szigorítható

Uf = ( Ef max = g(i)b

aimax ind [a..b] g(ind) = max )

és ez a szigorúbb feladat visszavezethető a maximum kiválasztás

programozási tételére.

Page 89: Programozas - Tervezes

5.3. Alteres visszavezetés

89

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

Page 90: Programozas - Tervezes

5. Visszavezetés

90

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:

Uf = ( Ef 0)(g(i)lb

aisearch )

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!

Page 91: Programozas - Tervezes

5.3. Alteres visszavezetés

91

A = (n:ℕ, s:ℕ)

Ef = ( n=n’ )

Uf = ( Ef 1s

ni

divn

2i

2

)

Átalakíthatjuk ezt a specifikációt az alábbi formára:

A = (m:ℕ, n:ℕ, s:ℕ)

Ef = ( m=2 n=n’ )

Uf = ( Ef 1s

ni

divn

mi

2

)

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

Page 92: Programozas - Tervezes

5. Visszavezetés

92

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

Uf = ( Ef l,max,ind = g(i)

g(i)k

b

aimax )

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

Uf = ( Ef l,max,ind = g(i)

g(i)

b

ai2

max )

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

igaz, g(i), i max, ind := g(i), i SKIP

Page 93: Programozas - Tervezes

5.4. Paraméteres visszavezetés

93

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

igaz, g(i), i max, ind := g(i), i SKIP

Ebben a k egy olyan változó, amelyik a program legelején kap

egy kezdőértéket, azaz a megoldó program alap-állapotterének

komponense lesz.

A paraméterezett visszavezetésre példák még azok a feladatok is,

ahol egy tömb elemeit kell feldolgozni. Ilyenkor maga a tömb lesz a

paraméter. Az ilyen feladatok ráadásul egyben az alteres

visszavezetésre is példák, hiszen a programozási tétel intervallumát

gyakran a tömb indextartománya határozza meg, tehát annak

végpontjai, az intervallum nem szerepel az állapottér komponensei

közt, azokat a paraméterből lehet kinyerni.

5.7. Példa. Adott két tömb: egy x és egy b. Fektessük a b-t

folyamatosan egymás után, és ezt helyezzük az x tömb mellé. Hány

esetben kerül egymás mellé ugyanaz az érték?

Indexeljük nullától a megadott tömböket, legyen az x tömb n, a b

tömb m elemű. Ekkor megfigyelhető, hogy az x tömb i-edik eleme

mellé mindig a b tömb i mod m-edik eleme kerül.

Page 94: Programozas - Tervezes

5. Visszavezetés

94

A = (x:ℤ -1n0 , b:ℤ -1n

0 , s:ℕ)

Ef = ( x=x’ b=b’ )

Uf = ( Ef 1s

mibix

n

i]mod[][

1

0

)

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

Page 95: Programozas - Tervezes

5.5. Visszavezetési trükkök

95

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.

Page 96: Programozas - Tervezes

5. Visszavezetés

96

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

Page 97: Programozas - Tervezes

5.5. Visszavezetési trükkök

97

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

= ( Ef (i)ln

misearch )

Page 98: Programozas - Tervezes

5. Visszavezetés

98

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

ki, de bevezethetjük rá az (i)ln

misearch jelölést is.

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:

Uf = ( Ef g(i)sn

mi

), ahol különben

(i)haf(i)g(i)

0

β

vagy másképp

Page 99: Programozas - Tervezes

5.5. Visszavezetési trükkök

99

Uf = ( Ef f(i)sn

(i)mi

)

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!

Page 100: Programozas - Tervezes

5. Visszavezetés

100

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:

Page 101: Programozas - Tervezes

5.6. Rekurzív függvény kiszámítása

101

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)

Uf = ( Ef knn

k

bkxs ][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

f(i) = kii

k

bkx ][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:

Page 102: Programozas - Tervezes

5. Visszavezetés

102

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:

][])...)[])[][(...((][1

nxb3xb2xb1xbkx knn

k

Page 103: Programozas - Tervezes

5.6. Rekurzív függvény kiszámítása

103

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.

Page 104: Programozas - Tervezes

5. Visszavezetés

104

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

Page 105: Programozas - Tervezes

5.7. Feladatok

105

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!

Page 106: Programozas - Tervezes

106

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

Page 107: Programozas - Tervezes

107

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.

Page 108: Programozas - Tervezes

6. Többszörös visszavezetés

108

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

Page 109: Programozas - Tervezes

6.1. Alprogram

109

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ó

Page 110: Programozas - Tervezes

6. Többszörös visszavezetés

110

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.

Page 111: Programozas - Tervezes

6.2. Beágyazott visszavezetés

111

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.

Page 112: Programozas - Tervezes

6. Többszörös visszavezetés

112

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

Page 113: Programozas - Tervezes

6.2. Beágyazott visszavezetés

113

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!

Page 114: Programozas - Tervezes

6. Többszörös visszavezetés

114

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 )(iszínjelesi1i

select )

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

Page 115: Programozas - Tervezes

6.2. Beágyazott visszavezetés

115

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

Page 116: Programozas - Tervezes

6. Többszörös visszavezetés

116

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

= ( Ef )( 3)négyesdb(iln

1isearch )

ahol négyesdb :[1..m] ℕ

négyesdb(i) = j

m

t i j

1

4

1

[ , ]

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

Page 117: Programozas - Tervezes

6.2. Beágyazott visszavezetés

117

négyesdb(i) részkifejezése a nem-megengedett. Készíthetünk ennek

kiszámolására egy s:=négyesdb(i) értékadást megvalósított

függvényszerűen meghívott alprogramot, ahol az s egy darabszámot

tartalmazó eredmény-változó. (Alternatív megoldás, ha a hívó

programban bevezetünk egy s:N segédváltozót, és az l:=négyesdb(i) 3

értékadást az (s:=négyesdb(i); l:=s 3) szekvenciára bontjuk fel, és az

s:=négyesdb(i) alprogramot eljárásszerűen hívjuk.)

Az s:=négyesdb(i) értékadás által kijelölt részfeladat

specifikációja:

A = (t:ℕn×m, i:ℕ, s:ℕ)

Ef = ( t=t’ i=i’ [1..n] )

Uf = ( Ef s = négyesdb(i) ) = ( Ef s = j

m

t i j

1

4

1

[ , ]

)

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.

Page 118: Programozas - Tervezes

6. Többszörös visszavezetés

118

Lássuk a specifikációt!

A = (t:ℕn×m, s:ℕ)

Ef = ( t=t’ )

Uf = ( Ef s =i

n

színjeles i1

1

( )

)

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

Page 119: Programozas - Tervezes

6.2. Beágyazott visszavezetés

119

A = (t:ℕn×m, ind:ℕ )

Ef = ( t=t’ n>0 m>0 )

Uf = ( Ef négyesdb(ind) = (i)négyesdbn

imax

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!

Page 120: Programozas - Tervezes

6. Többszörös visszavezetés

120

Ez a feladat egy feltételes maximumkereséssel oldható meg,

hiszen csak az adott tulajdonságú diákok átlagai között keressük a

legjobbat. Ezen belül önálló részfeladatot alkot annak eldöntése, hogy

egy diák csak 4-es illetve 5-ös osztályzatú-e, illetve önálló részfeladat

egy diák átlagának kiszámolása is. Az előbbi a feltételes maximum

keresés feltételét adja, az utóbbi a maximumkeresésnél használt f

függvényt. Ezeket a részfeladatokat egy-egy absztrakt függvénnyel

jelöljük ki. Bevezetjük a "csak 4-es illetve 5-ös osztályzatú"

tulajdonságot eldöntő jó logikai függvényt, amely az i-edik diák esetén

akkor ad igaz értéket, ha a táblázat i-edik sorában csak négyesek vagy

ötösök állnak. Bevezetjük továbbá az összeg függvényt, amely megadja

az i-edik diák jegyeinek összegét. Azért az összegét, és nem az átlagát,

mert a maximum keresés eredménye mindkét esetben ugyanaz a diák.

Természetesen a végeredmény előállításához külön kell majd

gondoskodni a legjobb diák átlagának kiszámolásáról.

A = (t:ℕn×m, l: , ind:ℕ, átl:ℝ )

Ef = ( t=t’ )

Uf = ( Ef l,max,ind =i

n

jó i

összeg i1

max

( )

( ) (l átl=max/m) )

ahol

jó:[1..n]

jó(i) = j [1..m]: (t[i,j]=5 t[i,j]=4)

összeg:[1..n] ℕ

összeg(i) = ],[ jitm

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.

Page 121: Programozas - Tervezes

6.2. Beágyazott visszavezetés

121

l := hamis

i =1 .. n i:ℕ

jó(i) l jó(i) l jó(i)

SKIP max<összeg(i) l, max, ind :=

igaz, összeg(i), i max, ind :=

összeg(i), i

SKIP max:ℕ

l

átl := max/m SKIP

A jó(i) kiszámolásához az u:=jó(i) értékadást megoldó

alprogramra van szükség, ahol az u egy logikai változó. Az u:=jó(i)

nem-megengedett értékadás egy optimista lineáris kereséssel

helyettesíthető, amelyben futóindexnek a j-t használjuk. Az összeg(i)

meghatározásához az össz:=összeg(i) értékadást megoldó alprogramra

van szükség, ahol az össz egy egészszám változó. Az össz:=összeg(i)

részfeladat egy összegzésre vezethető vissza, amelyben futóindexnek

ugyancsak a j-t használjuk, de ez nem ugyanaz a j, mint előbb.

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.

Page 122: Programozas - Tervezes

6. Többszörös visszavezetés

122

l := hamis

i =1..n i:ℕ

u := jó(i) u:

u l u l u

SKIP össz := összeg(i) l, max, ind :=

igaz, összeg(i), i

össz,max:ℕ

max<össz

max, ind :=

össz, i

SKIP

l

átl:=max/m SKIP

6.7. Példa. Adott egy egész számokat tartalmazó vektor, és annak

egy k indexe. Igaz-e, hogy van olyan elem a megadott index előtt a

vektorban, amelyik nagyobb vagy egyenlő az indextől kezdődő elemek

mindegyikénél!

A feladat talán nem tűnik túl érdekesnek, de a gyakorlás

szempontjából nézve igen hasznos, mert sokféle módon lehet

megoldani. A feladat állapottere, előfeltétele könnyen

megfogalmazható:

A = (t:ℤn, k:ℕ, l: )

Ef = ( v=v’ k=k’ 1 k n )

Az utófeltételt azonban számos – más-más programot

eredményező – változatban lehet felírni. Ezek közül bemutatunk

néhányat. Az egyes utófeltételekhez tartozó programok megadását az

olvasóra bízzuk.

Az első megoldás a feladat szószerinti értelmezéséből születik:

"Van olyan elem a vektor első részében, amelyik a vektor hátsó

részének mindegyik eleménél nagyobb vagy egyenlő." A feladatot tehát

egy lineáris keresésbe ágyazott optimista kereséssel oldhatjuk meg.

Ennek specifikációját láthatjuk itt:

Page 123: Programozas - Tervezes

6.2. Beágyazott visszavezetés

123

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.

Page 124: Programozas - Tervezes

6. Többszörös visszavezetés

124

Uf = ( Ef l = k>1 ][1

1

ivk

imax ][ jv

n

kjmax )

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.

Uf = ( Ef max, ind = ][1

ivn

imax l = ind<k )

Page 125: Programozas - Tervezes

6.2. Beágyazott visszavezetés

125

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

Uf = ( Ef l = i [1..k–1]: v[i] ][ jvn

kjmax )

utófeltétel azonban ügyetlen, mert a lineáris keresésbe ágyazza be

a maximum kiválasztást, holott a hátsó rész maximum kiválasztása

független a kereséstől – az első és hátsó rész feldolgozása nincs alá-

illetve fölérendelt viszonyban –, ezért elég, ha a maximum kiválasztást

a lineáris keresés előtt (szekvenciában) egyszer végezzük el:

Uf = ( Ef max = ][ jvn

kjmax l = i [1..k–1]: v[i] max )

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

Page 126: Programozas - Tervezes

6. Többszörös visszavezetés

126

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

22 ])[][(])[][( jyiyjxix

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

Page 127: Programozas - Tervezes

6.2. Beágyazott visszavezetés

127

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 )

Uf = ( Ef (max, jnd), ind = g(i)n

i 2max )

g:[2..n] ℝ×ℕ és

g(i) = táv(i,j)i

j

1

1max és

22 ])[][(])[][( jyiyjxixtáv(i,j)

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.

Page 128: Programozas - Tervezes

6. Többszörös visszavezetés

128

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> k2 ( k2 –1) teljesül, akkor i= k2 +1,

különben i = k2 á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)< k2 <i összefüggést kapjuk. Ebből

látszik, hogy a k2 az (i–1) környezetébe esik: a k2 (felső

egészrész) vagy i, vagy i–1. Ha 2k> k2 ( k2 –1), akkor k2 =i–

1, ugyanis az ellenkezőjét ( k2 =i) feltéve a 2k>i(i–1) összefüggést

kapnánk, ami ellentmond az első megállapításnak. Ha viszont 2k≤ k2

( k2 –1), akkor i= k2 , ugyanis az ellenkezőjét ( k2 =i–1)

feltéve a 2k≤(i–1)(i–2) összefüggést kapnánk, ami ugyancsak

ellentmondás.

Page 129: Programozas - Tervezes

6.2. Beágyazott visszavezetés

129

Uf = ( Ef max, knd = táv(h(k))nn

k

21

1

/)(

max (ind,jnd)=h(knd) )

h:[1..n(n–1)/2] ℕ×ℕ

2k -12k 2k -2 2k -1)( 2k , 2k- (2k

2k -12k 2k -1 2k ( 2k +1, 2k- 2k kh

)(ha) )(

)(ha) ))((

Ez a feladat visszavezethető a maximum kiválasztás

programozási tételére, amelyet az 1..n(n–1)/2 intervallumon, a htáv

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

Page 130: Programozas - Tervezes

6. Többszörös visszavezetés

130

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

Page 131: Programozas - Tervezes

6.3. Program-átalakítások

131

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ó

Page 132: Programozas - Tervezes

6. Többszörös visszavezetés

132

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:

Page 133: Programozas - Tervezes

6.3. Program-átalakítások

133

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 )

Uf = ( Ef s = i

ipont

n

1imax)(

max = pont(i)n

1imax )

pont:[1..n] ℝ

pont(i) = (m

1i

jit ],[ - ],[ jitm

1jmax - ],[ jit

m

1jmin )/(m–2)

Az érvényes összesített pontszámhoz legalább három pontszám

kell, illetve a maximum kiválasztás miatt minimum egy versenyző; ezt

tükrözi az előfeltétel. Az utófeltételben szereplő szimbólumot az

összefűzés műveletének jelölésére használjuk. Ennek segítségével

tudunk sorozatokat (itt egy elemű sorozatokat) egy sorozattá

összefűzni. A maximum kiválasztás szempontjából a pont(i) képletében

szereplő m–2-vel történő osztásra nincs szükség. Az utófeltételben

bevezettünk egy max:R változót. Világos, hogy először ennek az értékét

kell meghatározni, és csak ezután tudjuk a kiválogatást elvégezni.

A megoldás egy maximum kiválasztás, majd egy összegzés

(kiválogatás) szekvenciája lesz, amelyeken belül a pont(i)-t több helyen

is használjuk. A pont(i) kiszámolásához egy összegzésre, egy

maximum és egy minimum kiválasztásra van szükségünk.

Page 134: Programozas - Tervezes

6. Többszörös visszavezetés

134

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

Page 135: Programozas - Tervezes

6.3. Program-átalakítások

135

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:ℕ j = 2 .. m

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=

)(ipontn

1i összegzés lesz, amelyet a program legelején kell

elvégezni. Ennek ciklusa – ha azt (i=2..n) formájúra alakítjuk – viszont

összevonható a max értékét meghatározó maximum kiválasztással.

max, p := inicializálás

Page 136: Programozas - Tervezes

6. Többszörös visszavezetés

136

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

Page 137: Programozas - Tervezes

6.4. Rekurzív függvény kibontása

137

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

Uf = ( Ef f(ind)= f(i)n

1imax )

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

Page 138: Programozas - Tervezes

6. Többszörös visszavezetés

138

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

Page 139: Programozas - Tervezes

6.4. Rekurzív függvény kibontása

139

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

Page 140: Programozas - Tervezes

6. Többszörös visszavezetés

140

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.

Page 141: Programozas - Tervezes

6.4. Rekurzív függvény kibontása

141

f:[0..n] ℕ f(0) = 0

f(i) = 0

0

0

1

][ha

][ha

ix

ix1)f(i i [1..n]

A feladat megoldásához elég ennek a függvénynek megtalálni a

maximumát. Ahol ez a függvény a maximális értékét felveszi, ott van a

leghosszabb sziget végpontja, amelyből a szigethosszának ismeretében

a sziget kezdőpontja is könnyen meghatározható. (Megjegyezzük, hogy

ha a rekurzív függvényt nem a vektor elejéről hátrafelé haladva

képeznénk, hanem a végéről az eleje felé, akkor egy hátulról előre

haladó maximum kiválasztással közvetlenül a leghosszabb sziget elejét

találnánk meg.)

A = (x:ℕn, hossz:ℕ, kezd:ℕ, végz:ℕ )

Ef = ( x=x’ )

Uf = ( Ef hossz,végz = i

n

i1

max f ( ) kezd=végz–hossz+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.

Page 142: Programozas - Tervezes

6. Többszörös visszavezetés

142

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]

Page 143: Programozas - Tervezes

6.4. Rekurzív függvény kibontása

143

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)

ahol (l, ind) = ][][ jtáblaiszövegn

1)(i2fjsearch

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.

Page 144: Programozas - Tervezes

6. Többszörös visszavezetés

144

i, l :=1, igaz i:ℕ i, l, j := 1, igaz, 1 i,j:ℕ

l i m l i m

l := f1(i)

l, j := ][][ jtáblaiszövegn

jsearch

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.

Page 145: Programozas - Tervezes

6.4. Rekurzív függvény kibontása

145

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]:

különben

][][

ha

1)f(j

1)f(jszövegjtábla

m1)f(j1)f(j

f(i) 1

1

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.

Page 146: Programozas - Tervezes

6. Többszörös visszavezetés

146

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

Page 147: Programozas - Tervezes

6.5. Feladatok

147

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)

Page 148: Programozas - Tervezes

6. Moduláris programtervezés

148

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?

Page 149: Programozas - Tervezes

149

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

Page 150: Programozas - Tervezes

7. Típus

150

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.

Page 151: Programozas - Tervezes

151

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.

Page 152: Programozas - Tervezes

7. Típus

152

7.1. A típus fogalma

Most egy olyan példát fogunk látni, amelyik megoldása során

pontosítjuk a típus korábban bevezetett fogalmát, és ezáltal eljutunk a

korszerű típus definíciójához.

7.1. Példa. Képzeljük el, hogy olyan űrállomást állítanak Föld

körüli pályára, amelynek az a feladata, hogy adott számú észlelt

ismeretlen tárgy közül megszámolja, hány tartózkodik az űrállomás

közelében (mondjuk 10000 km-nél közelebb hozzá).

A feladat egyik adata az űrállomás; a másik az ismeretlen

tárgyakat tartalmazó sorozat, pontosabban egy tömb; és természetesen

az eredmény.

A = (Űrszonda , UFOn , ℕ)

Mindenek előtt válasszuk el a feladat adataiban a lényegest a

lényegtelentől. Az űr ismeretlen tárgyait egy-egy térbeli pontként

szemlélhetjük, hiszen mindössze a térben elfoglalt pozíciójuk lényeges

a feladat megoldása szempontjából. Egy űrállomás megannyi

tulajdonsága közül annak pozíciója és a védelmi körzete lényeges: az

űrállomás így helyettesíthető egy térbeli gömbbel. Ennek a valóságos

feladattól való elvonatkoztatásnak az eredménye az alábbi specifikáció.

A = (g:Gömb, v:Pontn, s:ℕ)

Ef = ( g=g' v=v' )

Uf = ( Ef 1s

giv

n

1i][

)

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.

Page 153: Programozas - Tervezes

7.1. A típus fogalma

153

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

matematikai értelemben egy :Pont ℝ 2Pont

függvény írja le, amely

egy c középponthoz és egy r sugárhoz az annak megfelelő gömböt

rendeli. Formálisan: (c,r) = {q Pont | távolság(c,q) r } 2Pont

. 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

Page 154: Programozas - Tervezes

7. Típus

154

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-

Page 155: Programozas - Tervezes

7.1. A típus fogalma

155

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

Page 156: Programozas - Tervezes

7. Típus

156

A = (p.x:ℝ, p.y:ℝ, p.z:ℝ, q.x:ℝ, q.y:ℝ, q.z:ℝ, d:ℝ) 222 zqzpyqypxqxpd )..()..()..(:

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:= rgzcgzivycgyivxcgxiv .)..].[()..].[()..].[( 222

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

Page 157: Programozas - Tervezes

7.1. A típus fogalma

157

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.

Page 158: Programozas - Tervezes

7. Típus

158

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

Uf = ( Ef n

1i

ias ]}[{ )

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

Page 159: Programozas - Tervezes

7.2. Típus-specifikációt megvalósító típus

159

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:

(2{1..100}

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

Page 160: Programozas - Tervezes

7. Típus

160

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:

A =(h:2{1..100}

) A = (v:100

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

Page 161: Programozas - Tervezes

7.2. Típus-specifikációt megvalósító típus

161

típusértékek típusműveletek

típus-

specifikáció

2{1..100}

1 és 100 közé eső egész

számokat tartalmazó

halmazok

betesz

h:=h {e}

legyen üres

h :=

elemszáma

s:= h

: 100 ℕ 2

{1..100}

(v,db) = 100

1i

iv ][

I(v,db) =( db=100

iv1i

1

][

)

v: 100, db:ℕ,

e: ℕ, l: , s:ℕ

típus

megvalósítás

100 ℕ

logikai tömb és egy szám

betesz

v[e]=hamis

v[e]:=igaz

db:=db+1

SKIP

legyen üres

db := 0

i [1..100]: v[i]:=hamis

elemszáma

s:=db

7.2. Ábra

Halmaz típusa

Page 162: Programozas - Tervezes

7. Típus

162

h:=h {e}

v[e]=hamis

v[e] := igaz

db := db+1

SKIP

Ha a db-t nem vettük volna hozzá a reprezentációhoz, akkor az

s:= h értékadást egy számlálással oldhatnánk meg. Így azonban elég

helyette az s:=db értékadás.

s:= h

s := db

A 7.2 ábrán összefoglaltuk a halmaz típus specifikációjával és

megvalósításával kapcsolatos megjegyzéseinket. A tí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

Page 163: Programozas - Tervezes

7.3. Absztrakt típus

163

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.

Page 164: Programozas - Tervezes

7. Típus

164

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

Page 165: Programozas - Tervezes

7.3. Absztrakt típus

165

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 )

Uf = ( Ef n

1i

itb ]}[{ e = MAX(b) )

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

zsákot a 2{1..100}×ℕ 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”

Page 166: Programozas - Tervezes

7. Típus

166

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

A= (b:2{1..100}×ℕ, 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

implementálásánál is. A 2{1..100}×ℕ-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

Page 167: Programozas - Tervezes

7.3. Absztrakt típus

167

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

specifikáció

legfeljebb 100 különböző

egész számot tartalmazó

Zsák

legyen üres

betesz

maximum

:2{1..100}×ℕ Zsák

I(b)= e1, e2 b: e1.1

e2.1

b:

2{1..100}×ℕ ,

e:{1..100}

absztrakt

típus 2

{1..100}×ℕ-beli

számpárok halmaza

b:=

b:= b {e}

e:=MAX(b)

: ℕ1002

{1..100}×ℕ

100

1i

ivi(v) ])}[,{(ρ

v: ℕ100, e:{1..100}

konkrét típus ℕ100 előfordulás számok

tömbje

e [1..100]: v[e]:=0

v[e]:=v[e]+1

][:, ivemax100

1imax

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

Page 168: Programozas - Tervezes

7. Típus

168

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.

Page 169: Programozas - Tervezes

7.3. Absztrakt típus

169

típus értékek típus műveletek

típus

specifikáció

legfeljebb 30 karaktert

tartalmazó verem betesz: v := push(v,e)

kivesz: v := pop(v)

olvas: e := top(v)

üres-e: l := empty(v)

legyen üres: v :=

I(s)= s 100 s: *, e: , l:

absztrakt típus legfeljebb 30 hosszú *-beli sorozat

s := s, e

s := s1, … ,s s -1

e := s s

l := s=

s :=

: 30 ℕ *

(t,m) = ][1

itm

i

t: 30, m: ℕ, e: , l:

konkrét típus 30 ℕ tömb-szám pár

t[m+1], m :=e, m+1

m := m-1

e := t[m]

l := (m=0)

m := 0

7.4. Ábra

Verem absztrakt típusa és annak megvalósítása

Az eredeti feladatot egy rekurzív függvény segítségével

specifikáljuk.

Page 170: Programozas - Tervezes

7. Típus

170

A = (a:n, b:

n)

Ef = ( a=a’ )

Uf = ( Ef b=f1(n+1) )

Az f:[0..n+1]n×ℕ×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]:

','][ha),,',']..[(

','][ha)][,,,(

ia1sksk1b

ia)iapush(skbf(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).

,,]..[ sksk1b1)f(n

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,

Page 171: Programozas - Tervezes

7.3. Absztrakt típus

171

é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

Page 172: Programozas - Tervezes

7. Típus

172

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

Page 173: Programozas - Tervezes

7.4. Típusok közötti kapcsolatok

173

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

Page 174: Programozas - Tervezes

7. Típus

174

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ő

Page 175: Programozas - Tervezes

7.4. Típusok közötti kapcsolatok

175

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.

Page 176: Programozas - Tervezes

7. Típus

176

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

Page 177: Programozas - Tervezes

7.4. Típusok közötti kapcsolatok

177

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.

Page 178: Programozas - Tervezes

7. Típus

178

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

Page 179: Programozas - Tervezes

7.4. Típusok közötti kapcsolatok

179

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.

Page 180: Programozas - Tervezes

7. Típus

180

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!

Page 181: Programozas - Tervezes

181

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

Page 182: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

182

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.

Page 183: Programozas - Tervezes

8.1. Gyűjtemények

183

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.

Page 184: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

184

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)

Page 185: Programozas - Tervezes

8.1. Gyűjtemények

185

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 m..nE 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

Page 186: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

186

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.

Page 187: Programozas - Tervezes

8.2. Felsoroló típus specifikációja

187

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.

Page 188: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

188

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

Page 189: Programozas - Tervezes

8.3. Nevezetes felsorolók

189

változó az [m..n] intervallum aktuálisan kijelölt elemét tartalmazza,

azaz implementálja a Current() függvényt. A First() műveletet az i:=m

értékadás, a Next() műveletet az i:=i+1 értékadás váltja ki. Az i>n

helyettesíti az End() függvényt. (A First() művelet itt ismételten is

kiadható, és mindig újraindítja a felsorolást, mind a négy művelet

bármikor, a felsoroláson kívül is terminál.)

Ezek alapján a felsoroló objektum feldolgozását végző általános

algoritmus-sémából előállítható az egész-intervallum klasszikus

sorrendű feldolgozását végző algoritmus, amelyet számlálós ciklus

formájában is megadhatunk.

i := m

i n

F(i)

i := i+1

i = m .. n

F(i)

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.

Page 190: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

190

i := 1

i s

F(si)

i := i+1

i = 1 .. s

F(si)

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.

Page 191: Programozas - Tervezes

8.3. Nevezetes felsorolók

191

i := 1

i s

F(si)

i := i+1

i = 1 .. s

F(si)

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

Page 192: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

192

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

Page 193: Programozas - Tervezes

8.3. Nevezetes felsorolók

193

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:

Page 194: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

194

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,

h

F(mem(h))

h := h – {mem(h)}

Page 195: Programozas - Tervezes

8.4. Programozási tételek általánosítása

195

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.

Page 196: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

196

1. Összegzés

Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy

f:E→H függvény. A H halmazon értelmezzük az összeadás asszociatív,

baloldali nullelemes műveletét. Határozzuk meg a függvénynek a t

elemeihez rendelt értékeinek összegét! (Üres felsorolás esetén az

összeg értéke definíció szerint a nullelem: 0).

Specifikáció:

A = (t:enor(E), s:H)

Ef = ( t=t’ )

Uf = ( s = )(,

'

i

t

i

tf

1

) =

= (

t'e

f(e)s )

Algoritmus:

s := 0

t.First()

t.End()

s := s+f(t.Current())

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ó:

A = (t:enor(E), c:ℕ)

Ef = ( t=t’ )

Uf = ( 1c

it

t

1i

),

β(

'

) =

= (

)β(et'e

1c )

Algoritmus:

c:=0

t.First()

t.End()

( t.Current() )

c := c+1 SKIP

t.Next()

Page 197: Programozas - Tervezes

8.4. Programozási tételek általánosítása

197

3. Maximum kiválasztás

Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy

f:E→H függvény. A H halmazon definiáltunk egy teljes rendezési

relációt. Feltesszük, hogy t nem üres. Hol veszi fel az f függvény a t

elemein a maximális értékét?

Specifikáció:

A = (t:enor(E), max:H, elem:E)

Ef = ( t=t’ t >0 )

Uf = ( ind [1.. t’ ]:elem= 'indt max=f( '

indt )= )f(t ,i

t

1imax

'

) =

= ( )f(tindmax ,i

t

1imax

'

),( elem=,ind

t ) =

= ( f(e)elemmaxte '

, max )

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

Page 198: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

198

4. Kiválasztás

Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy

:E feltétel. Keressük a t bejárása során az első olyan elemi értéket,

amely kielégíti a :E feltételt, ha tudjuk, hogy biztosan van ilyen.

Specifikáció:

A = (t:enor(E), elem:E) Ef = ( t=t’ i [1.. t ]: (ti) )

Uf = ( ind [1.. t’ ]: elem=t’ind

(elem) j [1..ind-1]: ( 'jt )

t = t’[ind+1.. t’ ] ) =

Algoritmus:

t.First()

(t.Current())

t.Next()

elem:=t.Current()

= ( )(t1

(ind,t) 'ind

indβselect elem= '

indt ) = ( (elem)t)(elem,telemβ'

select )

5. Lineáris keresés

Feladat: Adott egy E-beli elemeket felsoroló t objektum és egy

:E feltétel. Keressük a t bejárása során az első olyan elemi értéket,

amely kielégíti a feltételt.

Specifikáció:

A = (t:enor(E), l: , elem:E) Ef = ( t=t’ )

Uf = ( l= i [1.. t’ ]: ( 'it )

l ind [1.. t’ ]:elem=t’ind

(elem)

j [1..ind-1]: ( 'jt )

t = t’[ind+1.. t’ ] ) =

Algoritmus:

l := hamis; t.First()

l t.End()

elem := t.Current()

l := (elem)

t.Next()

= ( )(t(l,ind,t) 'i

t'

1iβsearch elem= '

indt ) = ( (e)t)elem,(l,te

β'

search )

Page 199: Programozas - Tervezes

8.4. Programozási tételek általánosítása

199

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’ ]: ( 'it ) l ind [1.. t’ ]: elem= '

indt

(elem) max=f(elem) i [1.. t’ ]: ( ( 'it ) f( '

it ) max) ) =

= ( (l, max, ind) = )f(t ,i

)'i(t

t

1i

β

'

max elem= 'indt ) =

= ( (l, max, elem) = f(e)

ete)β('

max )

Algoritmus:

l:= hamis; t.First()

t.End()

(t.Current()) (t.Current()) l ( t.Current()) l

SKIP f(t.Current())>max l, max, elem :=

igaz, f(t.Current()),

t.Current() max, elem:=

f(t.Current()),

t.Current()

SKIP

t.Next()

Page 200: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

200

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

Page 201: Programozas - Tervezes

8.4. Programozási tételek általánosítása

201

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:

x'e

f(e)s

számlálás:

(e)x'e

c

β

1

maximum kiválasztás: f(e)max,elemxe '

max

feltételes maximumkeresés: f(e)l,max,elem

(e)xe

β'

max

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: (e),e,x)l,elem,(stxe

β'

search

kiválasztás: (elem),x)elem,(st,exelemβ'

select

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: (e)l,elem,xxe

β'

search

kiválasztás: (elem)elem,xxelemβ'

select

Page 202: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

202

b) Egy h halmaz felsorolóját maga a h halmaz reprezentálja.

Ilyenkor a specifikációnál e t’ (ahol t’ a felsoroló kiinduló állapota)

szimbólum helyett az e h’ (h’ a halmaz kezdeti állapota) szimbólumot

írhatjuk. Jelentése: vegyük sorban egymás után a halmaz elemeit.

c) Indexelhető gyűjtemények (vektor, mátrix, sorozat, stb.) esetén

a felsorolót a gyűjtemény és az azon végigvezetett index (mátrixoknál

indexpár) reprezentálják. Tulajdonképpen ilyenkor közvetlenül nem is

a gyűjteményben tárolt értékeket, hanem azok indexeit soroljuk fel,

hiszen egy indexhez bármikor hozzárendelhető az általa megjelölt

érték. Ilyenkor például egy maximum kiválasztásnál az f függvény

sohasem identitás, mert az f rendeli az indexhez (a felsorolt elemhez) a

gyűjtemény megfelelő értékét. A specifikációkban szereplő elem

ilyenkor egy indexet tartalmaz, ezért az alábbiakban ind-ként (mátrixok

esetén dupla indexként: ind, jnd) jelenítjük meg. Ezen megfontolások

miatt használhatjuk a korábbi fejezetek specifikációs jelöléseit,

amelyből az is látható, hogy a korábbi intervallumos programozási

tételek a felsorolós tételek speciális esetei.

összegzés: s = )if(vn

mi

][

számlálás: 1

)i(v

n

mi

c

][β

)

maximum kiválasztás: max, ind = )if(vn

mi

][max

feltételes maximumkeresés: l, max, ind = )if(v

)i(v

n

mi

][

][β

max

lineáris keresés: )i(vindln

mi][β, search

kiválasztás: )i(vindmi

][βselect

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

Page 203: Programozas - Tervezes

8.4. Programozási tételek általánosítása

203

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.

összegzés: s = )jif(amn

ji

],[

,

, 11

számlálás: 111)ji(a

mn

ji

c

],[β

,

,

maximum kiválasztás: max, ind, jnd = )jif(amn

ji

],[,

,max

11

feltételes maximumkeresés: l, max, ind, jnd = )jif(a

)ji(a

mn

ji

],[

],[β

,

,max

11

lineáris keresés: )ji(aind, jndlmn

1j1i],[β,

,

,search

kiválasztás: )ji(aind, jndm

1j1i],[β

,select

Page 204: Programozas - Tervezes

204

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

Page 205: Programozas - Tervezes

8.5. Visszavezetés nevezetes felsorolókkal

205

A = (h:set(ℤ), max:ℤ) Ef = ( h=h’ )

Uf = ( max = ehe '

max )

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 = ete '

max )

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

Page 206: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

206

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

Page 207: Programozas - Tervezes

8.5. Visszavezetés nevezetes felsorolókkal

207

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 = ( )''('

eabnormstfe,fe

select )

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!

Page 208: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

208

A = (a:ℤ n×m, l: , ind:ℕ, jnd:ℕ)

Ef = ( a=a’ )

Uf = ( a=a’ ],[),(,,

jiajndindlmn

,ji2

11search )

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:

Page 209: Programozas - Tervezes

8.5. Visszavezetés nevezetes felsorolókkal

209

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

Page 210: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

210

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 = ( eey

xe

,

'

)

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.

Page 211: Programozas - Tervezes

8.5. Visszavezetés nevezetes felsorolókkal

211

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 = ( }{

'

párose

xe

ey )

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.

Page 212: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

212

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

Uf = ( n

i

i modxy

1

7}{ )

Ez egy elemenkénti feldolgozás (egy uniózás) vektorból

halmazba:

Page 213: Programozas - Tervezes

8.5. Visszavezetés nevezetes felsorolókkal

213

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

Page 214: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

214

Ef = ( x=x’ )

Uf = ( ey

barnahaje180magexe

''..

'

''..

'

}{

feketehaje60súlyexe

ez

en1v

alibiefeketehajexe

.''.

']..[ )

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:

y:=<>

st,e,x:read

z:=

st,e,x:read

n:=0

st,e,x:read

st = norm st = norm st = norm

e.mag>180

e.haj=’barna’

e.súly<60

e.haj=’fekete’

e.haj=’fekete’

e.alibi

y :write(e) SKIP z:=z {e} SKIP n:=n+1

v[n]:=e

SKIP

st,e,x:read st,e,x:read st,e,x:read

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.haj = ”barna”

e.súly<60

e.haj = ”fekete”

e.haj = ”fekete”

e.alibi

Page 215: Programozas - Tervezes

8.5. Visszavezetés nevezetes felsorolókkal

215

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

Page 216: Programozas - Tervezes

8. Programozási tételek felsoroló objektumokra

216

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!

Page 217: Programozas - Tervezes

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.

Page 218: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

218

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 )

Uf = (Ef max, (ind, jnd) = táv(i,j)in

ji

1

12

,

,max )

ahol 22 ])[][(])[][(),( jyiyjxixjitáv

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) = táv(i,j)t'(i,j)

max )

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.

Page 219: Programozas - Tervezes

9.1. Egyedi felsoroló

219

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.

Összegzés illetve számlálás esetén: )(,

)(

'

efts

e

te

, 1tc

e

e

te)(

)(

'

, ,

maximum kiválasztás esetén: )(,,)(

'

eftelemmaxe

temax ,

Page 220: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

220

lineáris keresés esetén: )(,,)(

'eteleml

e

tesearch .

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

Uf = ( db,( st, e, f) = 1

e2

0e

fe '

)

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

Page 221: Programozas - Tervezes

9.2. Feltétel fennállásáig tartó felsorolás

221

Ef = ( f=f’ f >0 f azon szerint rendezett )

Uf = ( összef

festmaxelemmaxazonazone

fe

.),,(,,..

'

'

1

max )

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

Page 222: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

222

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

Uf = ( )''(

'

0ffl 1ii

f

2isearch )

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

0tegnapemaelte

search )

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

Page 223: Programozas - Tervezes

9.3. Csoportok felsorolása

223

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

Page 224: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

224

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 = ( ev

100000összete

.'

)

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

Page 225: Programozas - Tervezes

9.3. Csoportok felsorolása

225

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)

Page 226: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

226

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

összdxxdxsxösszegyl

azonegylazondx

xdxdx

.),,(,.

..

)','(

) )

t.Next()

sx=norm

vége := hamis

vége := igaz egyl.azon, egyl.össz:=dx.azon, 0

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)

EfFirst

= ( x = x’ x azon szerint rendezett )

UfFirst

= ( sx’’, dx’’, x’’=read(x’)

sx’’=abnorm vége=igaz

sx’’=norm ( vége=hamis

egyl.azon = dx’’.azon

összdxxdxsxösszegyl

azonegylazondx

xdxdx

.),,(,.

..

)'',''(

) )

t.First()

sx, dx, x:read

t.Next()

Page 227: Programozas - Tervezes

9.3. Csoportok felsorolása

227

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 = hosszaktt'kta

.max kezd = elem.eleje )

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

Page 228: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

228

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 = )][( 0ixni

i'iselect

nincs_több_sziget = i1>n

nincs_több_sziget ( akt_sziget.eleje = i1

i = )][( ni0ix .elejeakt_szigeti

select

akt_sziget.hossz=i–akt_sziget.eleje ) )

t.Next()

Page 229: Programozas - Tervezes

9.3. Csoportok felsorolása

229

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

Page 230: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

230

t.Current().hossz>max

max, kezd := t.Current().hossz,

t.Current().eleje

SKIP

t.Next()

9.7. Példa. Egy karakterekből álló szekvenciális inputfájl egy

szöveget tartalmaz. Számoljuk meg, hogy hány ’w’ betűt tartalmazó szó

található a szövegben! (Egy szó olyan szóköz karaktert nem tartalmazó

karakterlánc, amelyet egy vagy több szóköz, a fájl eleje vagy vége

határol.)

A feladat egy számlálás, de nem a szekvenciális inputfájl

karakterei felett, hanem a szekvenciális inputfájlban található szavak

felett. A számláláshoz egy olyan felsorolóra van szükség, amely annyi

darab logikai értéket sorol fel, ahány szó szerepel a fájlban, és az i.

logikai érték akkor igaz, ha az i. szó tartalmaz ’w’ betűt.

A = (t:enor( ), db:ℕ)

Ef = ( t=t’ )

Uf = (

ete

1db

'

)

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

Page 231: Programozas - Tervezes

9.3. Csoportok felsorolása

231

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

dfabnormsffdfsffdf

111select

nincs_szó=(sf1=norm)

nincs_szó (

)''(),,(,_''

),(

wdffdfsfwvandf

fdfdf 11

222search

)''(,,

),,(

dfabnormsffdfsf

fdfsfdf 222select ) )

t.First()=t.Next()

sf, df, f : read

sf = norm df=’ ’

sf, df, f : read

nincs_szó:= sf=norm

Page 232: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

232

nincs_szó

van_w:=hamis

SKIP van_w sf = norm df ’ ’

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

Page 233: Programozas - Tervezes

9.3. Csoportok felsorolása

233

ANext

= (f:infile( ), df: , sf:Státusz, nincs_szó: , van_w: )

EfNext

= ( f = f1 df = df

1 sf = sf

1 (sf = norm df ’ ’) )

UfNext

= ( nincs_szó=(sf1=norm) nincs_szó (

)''(),,(,_''

),(

22211

wdffdfsfwvandf

fdfdf

)''(,,),,( 222

dfabnormsffdfsffdfsfdf

select ) )

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

= (

)''(,,)',','(

dfabnormsffdfsffdfsfdf

select111

nincs_szó=(sf1=norm) nincs_szó ( lásd Uf

Next -ben ) )

t.Next()

nincs_szó:= sf=norm

nincs_szó

van_w:=hamis

SKIP sf = norm df ’ ’

van_w:= van_w (df=’w’)

sf, df, f: read

sf = norm df=’ ’

sf, df, f: read

Page 234: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

234

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 = '' yxe

f(e) )

ahol f:E 2F és

''ha}{

''ha

''ha

yexee

yexe

yexe

f(e)

A megoldás hátterében tehát most is egy egyedi felsoroló áll,

amellyel újrafogalmazható a feladat.

Page 235: Programozas - Tervezes

9.4. Összefuttatás

235

A = (t:enor(E), z:set(E))

Ef = ( t = t’ )

Uf = ( z = 'te

f(e) )

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

Page 236: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

236

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.

Page 237: Programozas - Tervezes

9.4. Összefuttatás

237

A = (x:enor(E), y:enor(E), z:seq(E))

Ef = ( x=x’ y=y’ x y )

Uf = ( z = f(e)yxe }'{}'{

)

ahol f:E F* és

}'{}'{

}'{}'{

}'{}'{

yexeha(e)f

yexeha(e)f

yexeha(e)f

f(e)

3

2

1

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)te }'{

)

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.

Page 238: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

238

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

y.Current() )

x.End()

( y.End()

x.Current()>

y.Current() )

x.End()

y.End()

x.Current()=

y.Current()

z:write(

f1(x.Current()))

z:write(

f2(y.Current()))

z:write(

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.

Page 239: Programozas - Tervezes

9.4. Összefuttatás

239

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

y.Current()

x.Current()>

y.Current()

x.Current()=

y.Current()

SKIP SKIP z:write(f3(x.Current()))

x.Next(); y.Next() x.Next(); y.Next()

A fentiek alapján az összefuttatásos feldolgozás könnyen

adaptálható sorozatok, szekvenciális inputfájlok vagy vektorok

összefuttatására is. Megjegyezzük, hogy az eredmény sorozat minden

esetben egyértelmű és rendezett lesz. Egy ilyen feladat a következő is.

9.8. Példa. Adott két névsor egy-egy szekvenciális inputfájlban,

mindkettő szigorúan növekedően rendezett az ábécé szabályai szerint.

Az első névsor egy két féléves tantárgy első félévét felvevő hallgatók

neveit, a másik a második félévre jelentkező hallgatók neveit

tartalmazza. Kik azok, akik elkezdték a tantárgyat, de már a második

félévet nem vették fel, azaz kik azok, akik csak az első névsorban

szerepelnek, a másodikban nem?

Page 240: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

240

A = (x:seqin( *), y:seqin( *), z:seqout( *))

Ef = ( x=x’ y=y’ x y )

Uf = ( z = f(e)yxe }'{}'{

)

}'{}'{ha

}'{}'{ha

}'{}'{ha

yexe

yexe

yexee

f(e)

Halmazokkal megfogalmazva a feladatot, itt tulajdonképpen két

halmaz különbségét kell meghatározni. A feladat a két szekvenciális

inputfájl összefuttatására épített összegzéssel oldható meg. Az általános

megoldást most úgy kell alkalmazni, hogy az x és y elemeinek

felsorolásához a szekvenciális inputfájl nevezetes felsorolását kell

használni. (Az x.First() és x.Next() helyett sx,dx,x:read, x.End()

helyett sx=norm, x.Current() helyett dx.) A megoldó program

ciklusfeltétele az sx=norm sy=norm helyett az sx=norm-ra

egyszerűsödhet, és ennélfogva egyszerűbbek lehetnek az elágazás

feltételei is.

z := <>

sx,dx,x:read; sy,dy,y:read

sx=norm

sy=abnorm

dx<dy

sy=norm

dx>dy

sy=norm

dx=dy

z:write(dx) SKIP SKIP

sx,dx,x:read sy,dy,y:read sx,dx,x:read;

sy,dy,y:read

Az összefuttatás alkalmazásának egyik klasszikus példája az

alábbi adatkarbantartásról szóló feladat.

9.9. Példa. Adott egy árukészletet, mint az áruk azonosítója

szerint egyértelmű és növekvően rendezett szekvenciális inputfájl,

továbbiakban törzsfájl, valamint az egyes árukra vonatkozó

Page 241: Programozas - Tervezes

9.4. Összefuttatás

241

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.

Page 242: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

242

A= (x:enor(ℕ), u:outfile(Áru))

Ef = ( x = x' )

Uf = ( f(a)uxa '

)

)'()'(ha)(

)'()'(ha)(

)'()'(ha)'(

mazonatazonaaf

mazonatazonaaf

mazonatazonata

f(a)

3

2

Umüvmaag

Dmüvma

Imüvma

(a)f

Imüvememaa(a)f

hiba

hiba

).'(ha)(

).'(ha

).'(ha

ülönbenk

.ha)).'(,(

3

2

0

0

memametata

memametamemametaag(a)

hiba ).'().'(ha)'(

).'().'(ha).'().'(,

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

Page 243: Programozas - Tervezes

9.4. Összefuttatás

243

u := <>

st, dt, t: read; sm, dm, m: read

st=norm sm=norm

sm=abnorm

(st=norm

dt.azon <

dm.azon)

st=abnorm

(sm=norm

dt.azon>

dm.azon)

st=norm sm=norm

dt.azon=dm.azon

u:write(dt)

dm.müv=I

dm.müv=

I D U

u:write(

dm.azon,

dm.me)

H

I

B

A

H

I

B

A

S

K

I

P

c :=

dt.me+dm.me

c < 0

H

I

B

A

dt.me:=c

u:write(dt)

st, dt, t: read sm, dm, m: read st, dt, t: read

sm, dm, m: read

Életszerűbb lenne a probléma, ha nem követelnénk meg azt, hogy

a módosító fájl egyértelmű legyen, azaz egy azonosítóhoz több

változtatást is rendelhetünk. Ekkor a fenti algoritmusban a módosító

fájlnak nem az egyes elemeit (rekordjait) kellene felsorolni, hanem a

megegyező azonosítójú rekordjainak csoportjait, és egy-egy ilyen

csoportot hasonlítanánk össze a törzsfájl egy-egy rekordjával, és egy

ilyen csoport változtatásait hajtanánk végre egymás után ugyanarra az

azonosítóra, pontosabban az ahhoz tartozó adatra. A csoportok

kiolvasásához a korábban (9.4. alfejezet) mutatott technikával tudunk

felsorolót készíteni. Egy másik megoldási lehetőség egy alkalmas

rekurzív függvény bevezetése lehetne. Rekurzív függvények

feldolgozásával majd a következő alfejezetben foglalkozunk.

Page 244: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

244

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.

Page 245: Programozas - Tervezes

9.4. Összefuttatás

245

First()

i = 1 .. n

sf[i],df[i],f[i]:read

l, min := ][

][1

idf

normisf

n

imin

Next()

i = 1 .. n

sf[i]=norm df[i]=min

sf[i],df[i],f[i]:read SKIP

l, min := ][

][1

idf

normisf

n

imin

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

Page 246: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

246

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.

Page 247: Programozas - Tervezes

9.4. Összefuttatás

247

y.Next()

l, ind, min := azonidm

normism

n

1i

].[

][

min

l

dm.azon, dm.menny:=min, 0 SKIP

i = 1 .. n

sm[i]=norm dm[i]=min

dm.menny:=

dm.menny+dm[i].menny

SKIP

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 = ( f(a)uyazontazona )'()'(

)

)'()'(ha)(

)'()'(ha

)'()'(ha)'(

yazonatazaag

yazonatazona

yazonatazonata

f(a) hiba

mennyyamennymennytag(a) ).'()'(

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.

Page 248: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

248

u : =<>

st,dt,t:read; y.First();

st=norm y.End()

y.End()

(st=norm

dt.azon<

y.Current().azon)

st=abnorm

(st=norm

dt.azon>

y.Current().azon)

st=norm

y.End()

dt.azon=

y.Current().azon

u:write(dt)

HIBA

dt.menny:=

dt.menny+dm.menn

y

u:write(dt)

st,dt,t:read y.Next() st,dt,t:read; y.Next()

9.5. Rekurzív függvény feldolgozása

Az intervallumon értelmezett programozási tételek közül nem

általánosítottuk felsorolóra a rekurzív függvény helyettesítési értékét

kiszámoló tételünket. Ennek az oka az, hogy a 4. fejezetben bevezetett

rekurzív függvényeket az egész számok egy intervallumán definiáltuk,

tehát ez a fogalom az intervallumhoz kötött, ezért nincs értelme a

kiszámítását végző programozási tételt általánosítani.

Előfordulhat azonban, hogy olyan esetben is rekurzív függvény

értékének kiszámolásához folyamodunk, amikor a feladat adatai nem

mutatják meg közvetlenül a rekurzív függvény értelmezési tartományát

kijelölő intervallumot. Helyette egy felsorolónk van, amelynek ha az

elemeit a felsorolás sorrendjében megsorszámozzuk, akkor a keresett

intervallumhoz jutunk.

A 6.4. alfejezetben láttuk, hogy milyen hatékony megoldást

eredményez egy programozási tételbe ágyazott rekurzív függvény

kibontásával kapott program. Ehhez ott arra volt szükség, hogy a

programozási tétel intervallumán legyen értelmezve a rekurzív

függvény is. Vajon lehet-e ezt a technikát alkalmazni akkor is, ha

felsorolóra fogalmazott programozási tételekkel dolgozunk? Erre adnak

választ az alábbi feladatok-megoldásai.

Page 249: Programozas - Tervezes

9.5. Rekurzív függvény feldolgozása

249

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.

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 = ( )'( xry 1 )

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.

),(

''ha)(

''ha),(

''ha),((

:]..[

hamisr(0)

1)(irx1)r(i

1)(irxigazx1)(ir

xhamisx1)ir

r(i)

xi

i

ii

ii

2

21

1

1

Page 250: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

250

y, l, i :=<>, hamis, 1

i x

xi ’ ’ xi =’ ’ l xi =’ ’ l

y:write(xi)

l := hamis

y:write(xi)

l := igaz

SKIP

i := i+1

Ebben az esetben jól látszik, hogy az i indexváltozónak csupán az

a szerepe, hogy segítségével bejárjuk az x szekvenciális inputfájl

elemeit. Cseréljük hát le a szekvenciális fájl megengedett felsorolójára,

azaz használjunk a read műveletet.

y, l :=<>, hamis

st, ch, x : read

st = norm

ch ’ ’ ch =’ ’ l ch =’ ’ l

y:write(ch)

l := hamis

y:write(ch)

l := igaz

SKIP

st, ch, x : read

Általában előfordulhat, hogy a rekurzív képletnek az i indexre

közvetlenül is szüksége van, ezért olyankor az i-re vonatkozó

műveletek is megmaradnak az algoritmusban, azaz egymás mellett

zajlik az intervallum és mondjuk egy szekvenciális inputfájl

felsorolása.

A második megoldási út abba gondolatba kapaszkodik bele, hogy

ez a feladat első látásra egy olyan elemenkénti feldolgozásnak tűnik,

amelyeket az előző fejezetekben oldottunk meg, hiszen az eredmény (y)

előállításához bizonyos értékeket kell egymás után fűzni. Alaposan

szemügyre véve azonban látható, hogy itt nem egy másolásról van szó:

ahhoz ugyanis, hogy eldöntsük, mit kell az aktuális karakterrel csinálni

(hozzá kell-e írni az y-hoz vagy sem), ismerni kell, hogy a megelőző

Page 251: Programozas - Tervezes

9.5. Rekurzív függvény feldolgozása

251

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

Uf = ( )(

'

iry

x

i1

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.

),()(

''ha))(,(

''ha),(

''ha),(

:]..[

hamisr

1)(irxir

1)(irxigazx

xhamisx

r(i)

xi

i

ii

ii

0

1

1

22

2

Page 252: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

252

A = (t:enor(Pár), y:outfile( )) Pár = rec(e: , l: )

Ef = ( t=t’ )

Uf = ( (ch,l)hytlch

1'),(

)

ahol h: * és

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.

lchl

lchigazch

chhamisch

h(ch,l)

''ha),(

''ha),(

''ha),(

Page 253: Programozas - Tervezes

9.5. Rekurzív függvény feldolgozása

253

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

Uf = (

'

'

x

1)f(iixi

db

2

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:

Page 254: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

254

A = (t:enor(Pár), db:ℕ) Pár = rec(e:ℝ, max:ℝ)

Ef = ( t=t’ )

Uf = (

maxe

tmaxe

db

'),(

1 )

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.

Page 255: Programozas - Tervezes

9.6. Felsoroló gyűjtemény nélkül

255

A = (n:ℕ, sum:ℕ)

Ef = ( n=n' n>0 )

Uf = ( Ef sum =

prim(i)ni

n

i

i

2

)

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 =

'te

e )

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.

Page 256: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

256

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, )(: nddd 2select ) lesz.

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.

Page 257: Programozas - Tervezes

9.7I.9.7. Feladatok

257

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

Page 258: Programozas - Tervezes

9. Visszavezetés egyedi felsorolókkal

258

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

Page 259: Programozas - Tervezes

9.7I.9.7. Feladatok

259

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?

Page 260: Programozas - Tervezes

10. Feladatok megoldása

260

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

Page 261: Programozas - Tervezes

1. fejezet feladatainak megoldása

261

Á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:

Page 262: Programozas - Tervezes

10. Feladatok megoldása

262

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?

Page 263: Programozas - Tervezes

1. fejezet feladatainak megoldása

263

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!

Page 264: Programozas - Tervezes

10. Feladatok megoldása

264

Í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

Page 265: Programozas - Tervezes

1. fejezet feladatainak megoldása

265

pedig 0. Tehát a program éppen a feladat által kijelölt

célállapotban áll meg.

1.7. Tekintsük az 5,4,3,2,1A á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.)

4,3,2,554,3,554,2,55

2,4,5,1,441,5,2,1,3,444,1,5,1,44

,...3,334,221,22

,...2,3,112,5,3,4,115,4,2,11

S

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á

Page 266: Programozas - Tervezes

10. Feladatok megoldása

266

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.

Page 267: Programozas - Tervezes

267

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:

Page 268: Programozas - Tervezes

10. Feladatok megoldása

268

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:

Page 269: Programozas - Tervezes

2. fejezet feladatainak megoldása

269

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

Page 270: Programozas - Tervezes

270

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!

Page 271: Programozas - Tervezes

3. fejezet feladatainak megoldása

271

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

Page 272: Programozas - Tervezes

10. Feladatok megoldása

272

Válasz: Nem. Az első elágazás abortál, ha egyik feltétel sem

teljesül, a másik kettő ilyenkor SKIP-ként működik. A második

elágazás (S1;S2;S3) szekvenciaként működik, ha kezdetben f1

teljesül, majd S1 végrehajtása után f2 teljesül, végül S2

végrehajtása után f3 is teljesül. A harmadik program viszont csak

S1 hajtja végre, ha kezdetben f1 teljesül.

3.6. Értelmezze az alábbi feladatot! Adja meg néhány végrehajtási

sorozatát a megadott elágazásnak! Mutassa meg, hogy a feladatot

megoldja a program!

A = (x:ℤ, n:ℕ)

Ef = ( x=x’ )

Uf = ( n=abs(x’) )

x≥0 x 0

n:=x n:=–x

Bizonyítás: Az elágazás feltételrendszere lefedi az összes

előforduló esetet, azaz minden állapotra legalább az egyik feltétel

teljesül. Ha x≥0, akkor az x abszolút értéke maga az x, ezért az n:=x

értékadás megoldja a feladatot. Ha x 0, akkor az x abszolút értéke az x

ellentetje, azaz n:=–x.

3.7. Oldjuk meg az ax2+bx+c=0 alakú egyenletet, amelynek adottak az

a, b és c valós együtthatói!

Specifikáció:

A = ( a:ℝ, b:ℝ, c :ℝ, x1:ℝ, x2:ℝ, v: *)

Ef =( a=a’ b=b’ c=c’ )

Uf = ( Ef (a 0 (

(b2+4ac < 0) v = „Nincs valós gyök”

(b2+4ac = 0) ( v = „Közös valós gyök”

a

bxx

221

)

(b2+4ac > 0) ( v = „Két valós gyök”

a

acbbx

2

42

1 a

acbbx

2

42

2 ) )

Page 273: Programozas - Tervezes

3. fejezet feladatainak megoldása

273

(a=0 (b 0 ( v = „Elsőfokú gyök” b

cx1

)

(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

acbd 4: 2

b 0 b=0

c=0

b=0

c 0 d<0 d=0 d>0

válasz:=

Nincs

valós

gyök

válasz:=

Közös

valós

gyök

válasz:=

Két

valós

gyök

válasz:=

Elsőfokú

gyök

válasz

:=

A

z

o

n

o

s

s

á

g

válasz

:=

E

l

l

e

n

t

m

o

n

d

á

s

:, 21 xx

a

b

2

:, 21 xx

a

db

2

b

cx1

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

Page 274: Programozas - Tervezes

10. Feladatok megoldása

274

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.

Page 275: Programozas - Tervezes

3. fejezet feladatainak megoldása

275

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

x x, n/2

z, n :=

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.

Page 276: Programozas - Tervezes

276

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

Uf = ( Ef z=x*y ) = ( Ef yzx

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

Uf = ( Ef )(,/

i2nidl2n

2isearch )

Lineáris keresés:

m..n ~ 2 .. n div 2

ind ~ d

(i) ~ i|n 2|i

Algoritmus:

Page 277: Programozas - Tervezes

4. fejezet feladatainak megoldása

277

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

Uf = ( Ef y[1..k]= ][

][

ixn

ix21i

) ( az összefűzés jele)

Összegzés:

m..n ~ 1..n

s ~ y[1..k]

f(i) ~ ha x[i] páros akkor <x[i]> különben <>

Algoritmus:

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ó:

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

Page 278: Programozas - Tervezes

10. Feladatok megoldása

278

A = ( x:ℝ, l: , d:ℕ)

Ef = ( x=x’ )

Uf = ( Ef )][][(, 01ix0ixdl1n

1isearch )

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 )

Uf = ( Ef max = ][1

ixn

imax )

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

x[i]

SKIP max :=

x[i]

SKIP

i := i + 1

4.5. Az f:[m..n] ℤ függvénynek hány értéke esik az [a..b] vagy a

[c..d] intervallumba?

Page 279: Programozas - Tervezes

4. fejezet feladatainak megoldása

279

Specifikáció:

A = (m, n, a, b, c, d:ℤ, db:ℕ)

Ef = ( m=m’ n=n’ a=a’ b=b’ c=c’ d=d’ )

Uf = ( Ef 1

]..[]..[)( dcbaif

n

mi

db )

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

db+1

SKIP

i := i + 1

4.6. Egy tömbben színeket tárolunk az színskála szerinti növekvő

sorrendben. Keressük meg a tömbben a világoskék színt!

Specifikáció:

A = (x:Színn, l: )

Ef = ( x=x’ x rendezett )

Uf = ( Ef l=( i [m..n]: x[i]=világoskék)

l (i [m..n] x[i]=világoskék) )

Logaritmikus keresés:

m..n ~ 1..n

(i) ~ x[i]=világoskék

Algoritmus:

Page 280: Programozas - Tervezes

10. Feladatok megoldása

280

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

Uf = ( Ef l,max,ind = ][

][][][

ix

ixixix

n

i11

1

2max )

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]

x[i]<x[i+1])

l x[i–1]>x[i]

x[i]<x[i+1]

l x[i–1]>x[i]

x[i]<x[i+1]

SKIP max< x[i] l, max, ind :=

max, ind

:= x[i], i

SKIP igaz, x[i], i

4.8. Egymást követő napokon megmértük a déli hőmérsékletet.

Hányszor mértünk 0° Celsiust úgy, hogy közvetlenül utána

fagypont alatti hőmérsékletet regisztráltunk?

Specifikáció:

Page 281: Programozas - Tervezes

4. fejezet feladatainak megoldása

281

A = ( x:ℤn, db:ℕ)

Ef = ( x=x’ )

Uf = ( Ef 1

]01]x[i 0x[i]

1

1

n

i

db )

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]

Page 282: Programozas - Tervezes

282

5. fejezet feladatainak megoldása

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

origóhoz legközelebb eső pontot!

Specifikáció:

A = (x:ℝn, y:ℝn, ind:ℕ) Ef = ( x=x’ y=y’ n>0 )

Uf = ( Ef min, ind = )][][( 22

1

iyixn

imin )

(Az x[i]2+y[i]2 az (x[i], y[i]) koordinátájú pont origótól vett

távolságának négyzete.)

Minimum kiválasztás:

m..n ~ 1..n

f(i) ~ x[i]2+y[i]2

max ~ min

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] ) =

= ( Ef ])[][( 1ixixln

2isearch )

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

Page 283: Programozas - Tervezes

6. fejezet feladatainak megoldása

283

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

Uf = ( Ef l, min, ind = ][

mod][

ix

1kix

n

1imin )

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

x[i] mod k =1

l

x[i] mod k =1

SKIP min< x[i] l, min, ind :=

min, ind:=

x[i], i

SKIP igaz, x[i], i

5.4. Adottak az x és y vektorok, ahol az y elemei az x indexei közül

valók. Keressük meg az x vektornak az y-ban megjelölt elemei

közül a legnagyobbat!

Specifikáció:

Page 284: Programozas - Tervezes

10. Feladatok megoldása

284

A = (x:ℝm, y:ℕn, max:ℝ, ind:ℕ) Ef = ( x=x’ n>0 m>0 j [1..n]:y[j] [1..m] )

Uf = ( Ef max, ind = ]][[1

iyxn

imax )

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]) ) =

= ( Ef l= ])[][( 1inxix2ndiv

1isearch )

Optimista lineáris keresés:

m..n ~ 1.. div2n

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

l, i:= igaz, 1 i:ℤ

l i div2n

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!

Page 285: Programozas - Tervezes

6. fejezet feladatainak megoldása

285

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= )( dmdnnd

select )

Kiválasztás:

m ~ n

i ~ d

(i) ~ n|d m|d

Page 286: Programozas - Tervezes

10. Feladatok megoldása

286

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= )( mdndnd

select )

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

= ( Ef ]niln

i 2search )

Optimista lineáris keresés:

m..n ~ 1.. n

(i) ~ i|n

Page 287: Programozas - Tervezes

6. fejezet feladatainak megoldása

287

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 = 10inf(i)inf(i)f(i)

mod)][(][ 1111

f(i+1)2 = 10divinf(i) )][( 11

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

Page 288: Programozas - Tervezes

10. Feladatok megoldása

288

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

= ( Ef )][( múlt(i)0ixln

1kisearch )

ahol múlt:[k+1..n] és

múlt(i) = j [i–k..i–1]: x[j]<0 = )][( 0jx1i

kijsearch

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ó:

Page 289: Programozas - Tervezes

6. fejezet feladatainak megoldása

289

A = (x:ℤn, k:ℕ, l: )

Ef = ( x=x’ k=k’ )

Uf = ( Ef l= i [2..n]:(x[i]>0 múlt(i)=k) )=

= ( Ef )][( kmúlt(i)0ixln

i 2search )

ahol múlt(1) = 0 és

múlt(i) = 01ix0

01ix11)múlt(i

][ha

][ha

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

Uf = ( Ef max, nap= hány(i)n

1imax )

ahol hány:[1..n] ℕ és hány(i) = 1i

j

ixjx ][][1

1

Maximum kiválasztásba ágyazott számlálás:

Page 290: Programozas - Tervezes

10. Feladatok megoldása

290

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

Uf = ( Ef max,diák = )(ihányn

1imax )

ahol hány:[1..n] ℕ és hányt(i) = 1i

j

ixjx ][][1

1

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]) =

= ( Ef ]))[][(( jp1jislm

1j

1mn

1isearchsearch )

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

Page 291: Programozas - Tervezes

6. fejezet feladatainak megoldása

291

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

Uf = ( Ef max,oszlop = ))(( jösszegn

1jmax )

ahol összeg:[1..n] ℝ és összeg(j) = j

1i

jit ],[ , és például

ö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’ )

Page 292: Programozas - Tervezes

10. Feladatok megoldása

292

Uf = ( Ef l = i [1..n]: j [1..i–1]: x[i]= x[j] ) =

= ( Ef ]))[][((, jxixindl1i

1j

n

1isearchsearch

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

Uf = ( Ef )( (k)fkln

(k)f1k

2

1

min )

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.

k [1..n]: ])[][( jxkxf(k)1

kj 1keres-

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

Page 293: Programozas - Tervezes

6. fejezet feladatainak megoldása

293

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

Uf = ( Ef )(, 0 )i(nyereségiln

1isearch )

ahol nyereség:[0..n] ℕ és nyereség(i) = i

j

jx1

][

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ó:

Page 294: Programozas - Tervezes

10. Feladatok megoldása

294

A = (t:ℕn×m, l: )

Ef = ( t=t’ )

Uf = ( Ef ])),[(( jitprímlm

1j

n

1isearchsearch )

ahol prím:ℕ és prím(a) = (a>1 ))(( aka

2ksearch )

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 ],[ jit

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

Uf = ( Ef ])),[(( jitprímlm

1j

n

1isearchsearch )

ahol prím:ℕ és prím(a) = (a>1 ))(( aka

2ksearch )

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 ],[ jit

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.

Page 295: Programozas - Tervezes

6. fejezet feladatainak megoldása

295

A = (t:ℕn×m, l: )

Ef = ( t=t’ )

Uf = ( Ef ])),[(( jitprímlm

1j

n

1isearchsearch )

ahol prím:ℕ és prím(a) = (a>1 ))(( aka

2ksearch )

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 ],[ jit

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

Uf = ( Ef ])),[(( jitprímlm

1j

n

1isearchsearch )

ahol prím:ℕ és prím(a) = (a>1 ))(( aka

2ksearch )

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 ],[ jit

l := sor(i) l := prím(t[i,j]) l := (k t[i,j])

i := i+1 j := j+1 k := k+1

Page 296: Programozas - Tervezes

10. Feladatok megoldása

296

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

Uf = ( Ef )( 1(m)értl in

i1

1search )

ahol érti:[0..m] ℕ×ℕ egy rekurzív függvény, amelynek j-edik

helyen felvett második értéke (max) azt mutatja, hogy az i-edik

kategóriában az első j pontszám közül melyik a legnagyobb, az

első értéke (db) pedig azt, hogy ez a pontszám az első j pontszám

között hányszor fordult elő.

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

1)(jértjitha1)(jért1)(jért

1)(jértjithajit1

1)(jértjitha1)(jért11)(jért

(j)ért

i2

i2

i1

i2

i2

i2

i1

i

],[),(

],[]),[,(

],[),(

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

1,t[i,j]

SKIP

Az alprogram futási ideje gyorsítható, ha az közvetlenül az

l:=érti1(m)>1 részfeladatot oldja meg, de úgy, hogy amikor a rekurzív

függvény db komponense eléri a 2-t, akkor leáll, azaz az l := j [1..m]:

érti1(m)=2 feladatra ad megoldást. Ekkor ezt egy lineáris keresésre

Page 297: Programozas - Tervezes

6. fejezet feladatainak megoldása

297

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

Uf = ( Ef n

mind(i)i

1db

1

)

ahol mind:[1..n] és mind(i) = )],[( 0jitm

1jsearch

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

Page 298: Programozas - Tervezes

298

7. fejezet feladatainak megoldása

7.1. Valósítsuk meg a racionális számok típusát úgy, hogy

kihasználjuk azt, hogy minden racionális szám ábrázolható két

egész számmal, mint azok hányadosa! Implementáljuk az

alapműveleteket!

Megoldás:

ℚ A = (a:ℚ, b:ℚ, c:ℚ)

c := a b

A = (a:ℚ, b:ℚ, c:ℚ)

c := a*b

A = (a:ℚ, b:ℚ, c:ℚ) c := a/b

:ℤ×ℤ ℚ (x1, x2) = x1/x2 I(x1, x2) = x2 0

a-t az x1, x2, b-t az y1, y2, c-t a z1, z2 reprezentálja

ahol x1, x2, y1, y2, z1, z2:ℤ

ℤ×ℤ

második szám

nem nulla

z1, z2 := x1*y2 x2*y1, x2*y2

z1, z2 := x1*y1, x2*y2

z1, z2 := x1*y2, x2*y1

7.2. Valósítsuk meg a komplex számok típusát! Ábrázoljuk a komplex

számokat az algebrai alakjukkal (x+iy)! Implementáljuk az

alapműveleteket!

Megoldás:

ℂ A = (a:, b:ℂ, c:ℂ) c := a b

A = (a:ℂ, b:ℂ, c:ℂ) c := a*b

A = (a:ℂ, b:ℂ, c:ℂ) c := a/b

Page 299: Programozas - Tervezes

8. fejezet feladatainak megoldása

299

:ℝ×ℝ ℂ (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+y1

2),

(x1y2–y1x2)/(x12+y1

2)

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) = 21xxP

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!

Page 300: Programozas - Tervezes

10. Feladatok megoldása

300

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

egész számokat

tartalmazó

Zsák

betesz

kivesz

hányszor

Page 301: Programozas - Tervezes

8. fejezet feladatainak megoldása

301

:2ℕ×ℕ Zsák

identitás I(h)={ (e,m) h e [1..100]}

h: 2ℕ×ℕ, e:ℕ, db:ℕ

1 és 100 közötti

egész szám ás

darabszám párok

halmaza

2ℕ×ℕ

(e,m) h h:= h-{(e,m)}) {(e,m+1)}

(e,m) h h:=h {(e,1)}

(e,m) h h:= h-{(e,m)}

(e,m) h db:= m

(e,m) h db:= 0

:ℕ100 2N×N (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

tartalmazó

Sor

betesz

kivesz

Üres-e

Tele-e

Page 302: Programozas - Tervezes

10. Feladatok megoldása

302

: ℤ*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) =

I(v,a,f,db) =

ℤ*

v[a], … ,v[f] db=f–a+1 a,f [0..99]

v:ℤ100

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

Page 303: Programozas - Tervezes

8. fejezet feladatainak megoldása

303

:{0..9}* ℕ

I: {0..9}*

(x) =

x

1i

1ii 10x , I(x) = x x 0

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

Page 304: Programozas - Tervezes

10. Feladatok megoldása

304

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:

1

1

10)()( k

k

y

k

yxz azaz

)(0...011

kdarabk

y

k

yxz

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!

Page 305: Programozas - Tervezes

8. fejezet feladatainak megoldása

305

Megoldás:

P

A = (p:P, q:P, r:P)

r := p±q

A = (p:P, q:P, r:P)

r := p q

:ℝ* P

I:ℝ*

(a) = a0+a1 x + … +a a x a

, I(a) = a a 0

a p, q, r-t rendre az a,b,c :ℝ*reprezentálja

ℝ*

k [0..max( a , b )]: ][][][ kbkakc )

k [0.. a * b ]: ][][][ lkblakcn

l

nkl

0

7.9. Valósítsuk meg a diagonális mátrixtípust (amelynek mátrixai

csak a főátlójukban tartalmazhatnak nullától különböző számot)!

Ilyenkor elegendő csak a főátló elemeit reprezentálni egy

sorozatban. Implementáljuk a mátrix i-edik sorának j-edik elemét

visszaadó műveletet, valamint két mátrix összegét és szorzatát!

Megoldás:

Diag(ℝn×n)

a:Diag(ℝn×n), i, j:ℕ,e:ℝ

e: = a[i,j]

a, b, c: Diag(ℝn×n)

c: = a+b

a, b, c: Diag(ℝn×n)

c := a*b

:ℝn Diag(ℝn×n

)

(x)[i,j] =jiha

jihaix

0

][

Page 306: Programozas - Tervezes

10. Feladatok megoldása

306

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

jiha

jihajinx

0

])1([

az a, b, c-t rendre az x, y, z: ℝn(n+1)/2 reprezentálja,

továbbá i:ℕ, j:ℕ, e:ℝ

ℝn(n+1)/2

i [1..n(n+1)/2]: z[i] = x[i]+y[i]

i,j [1..n]: ha i j akkor

]2/)1([]2/)1([]2/)1([ jkkykiixjiizi

jk

i j e = x[i (i–1)/2+j]

i<j e = 0

Page 307: Programozas - Tervezes

8. fejezet feladatainak megoldása

307

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ó:

A = (x:ℤn, y:ℤ*

)

Ef = ( x=x’ )

Uf = ( x=x’ ][

][

ixyn

0ix

1i

)

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:

Számoljuk meg egy halmazbeli szavak között, hogy hány ’a’ betűvel

kezdődő van!

Specifikáció:

y :=<>

i = 1 .. n

x[i]>0

y :=y x[i] SKIP

Page 308: Programozas - Tervezes

10. Feladatok megoldása

308

A = ( h:set(*), db:ℕ)

Ef = ( h=h’ )

Uf = (

'''aszó

hszó

1

1db )

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 = ( )('

0efelem,l,fe

search )

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.

Page 309: Programozas - Tervezes

8. fejezet feladatainak megoldása

309

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

0egylexe

.

.'

min a=elem.az )

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

Page 310: Programozas - Tervezes

10. Feladatok megoldása

310

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

e.egyl, e.az

SKIP igaz, e.egyl, e.az

st,e,x:read

8.4. Egy szekvenciális fájlban minden átutalási betétszámla

tulajdonosáról nyilvántartjuk a nevét, címét, azonosítóját, és

számlaegyenlegét (negatív, ha tartozik; pozitív, ha követel).

Készítsünk két listát: írjuk ki egy output fájlba a hátralékkal

rendelkezők, egy másikba a túlfizetéssel rendelkezők nevét!

Specifikáció:

A = (x:infile(Ügyfél), y:outfile( ), z:outfile( ))

Ügyfél = rec(név:*, cím:

*, az:

*, egyl:ℤ)

Ef = ( x=x’ )

Uf = ( névey

0egylexe

.

.'

névez

0egylexe

.

.'

)

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:

Page 311: Programozas - Tervezes

8. fejezet feladatainak megoldása

311

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 = ( ey

pirosvirágexe

"".'

ez

Mexikóősexe

"".'

eu

Mexikóősepirosvirágexe

""."".

' )

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

Page 312: Programozas - Tervezes

10. Feladatok megoldása

312

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 = ex'e

max min = ex'e

min

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

E ~ Kaktusz Kaktusz Kaktusz

H ~ *

*

*

+,0 ~ ,<> ,<> ,<>

f(e) ~ <e.név> <e.név> <e.név>

ha e.virág=„

piros”

e.ős=

„Mexikó”

e.virág=

„piros”

e.ős=

„Mexikó”

Page 313: Programozas - Tervezes

8. fejezet feladatainak megoldása

313

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ó:

Page 314: Programozas - Tervezes

10. Feladatok megoldása

314

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 = ( ey

4 20

xe

e.család

le.tú

fize.tipus

')()(

fiztipusexe

fiztipusexe

1béref

.'

.'

/.

)()(

admtipusexe

admtipusexe

1bérea

.'

.'

/.

)()(

veztipusexe

veztipusexe

1bérev

.'

.'

/. )

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

Page 315: Programozas - Tervezes

8. fejezet feladatainak megoldása

315

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ó:

A = (x:ℤn, y:ℤ

n, z: *

, k:ℤ)

Ef = ( x = x' y = y')

Uf = ( x = x' y = y' ][]..[

][][

ixk1zn

iyix

1i

)

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ó:

Page 316: Programozas - Tervezes

10. Feladatok megoldása

316

A = (x:infile( ), y:outfile( ))

Ef = ( x=x’)

Uf = ( )chtáviratyxch

('

)

távirat: *

""

""""

""""

""

""

""""

""

""

""

""

""""

""

""

""

""

""""

""

""

""

""

""

""

""

)(

Óch

ŰchvagyÜch

ŐchvagyÖch

Ích

különben

ha

haha

ha

ch

O

UeOe

I

Éch

Ách

óch

űchvagyüch

ha

ha

ha

ha

Ee

Aa

o

ue

őchvagyöch

ích

éch

ách

ha

ha

ha

ha

oe

i

ee

aa

chtávirat

Algoritmus:

st,ch,x:read

y:= <>

st = norm

y: write(távirat(ch))

st,ch,x:read

Page 317: Programozas - Tervezes

9. fejezet feladatainak megoldása

317

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 = ( aelemmax

kelemaelemeelemelem

.

...t'

max )

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

Page 318: Programozas - Tervezes

10. Feladatok megoldása

318

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

'

)(

te

efs )

f:ℕ ℕ f(e)= 122

121

e

e

ha

ha

Page 319: Programozas - Tervezes

9. fejezet feladatainak megoldása

319

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

1 sf = sf

1 (sf=norm df ’ ’) )

UfNext

= ( van_szó=(sf1=norm)

van_szó ( 111

222''

),(

,,,

df

fdfdf

fdfsfhossz

)''(,,

),,(

dfabnormsffdfsf

fdfsfdf 222select ) )

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

= ( )''(,,'

dfabnormsffdfsffdf

111select

van_szó=(sf1=norm)

Page 320: Programozas - Tervezes

10. Feladatok megoldása

320

van_szó ( 1fdfsfhossz

df

1f1dfdf

222''

),(

,,,

)''(,,

),,(

dfabnormsffdfsf

fdfsfdf 222select ) )

t.First()

sf, df, f : read

sf = norm df=’ ’

sf, df, f : read

van_szó:= sf=norm

t.Next()

t.Next()

van_szó:= sf=norm

van_szó

hossz:=0 SKIP

sf = norm df ’ ’

hossz:=hossz+

1

sf, df, f: read

sf = norm df=’ ’

sf, df, f: read

A felsorolás akkor ér véget, ha a Next() művelet nem talál újabb

szót. Ezt az információt a van_hossz logikai változó őrzi. Ennek

értékét kérdezi le a t.End(). A t.Current() a hossz változó értékét

adja vissza. Ezek szerint a t felsorolót az sf, df, f, van_szó, hossz

együttesen reprezentálják.

9.3. Alakítsunk át egy bitsorozatot úgy, hogy az első bitjét megtartjuk,

utána pedig csak darabszámokat írunk annak megfelelően, hogy

hány azonos bit követi közvetlenül egymást! Például a

00011111100100011111 sorozat tömörítve a 0362135

számsorozat lesz.

Specifikáció:

A = (f:Bit*, g:ℕ*

)

Ef = ( f=f' )

Képzeljük el azt a felsorolót, amelyik már ez eredmény-

sorozatban kívánt értékeket sorolja fel. Ekkor a megoldás egy

másolás (speciális összegzés) lesz

Page 321: Programozas - Tervezes

9. fejezet feladatainak megoldása

321

A = (t:enor(ℕ), g:ℕ*)

Ef = ( x=x' )

Uf = ( g=x' )

Algoritmus:

g:= <>

t.First()

t.End()

g:write(t.Current())

t.Next()

A felsorolót az f sorozat, a sorozat elemeinek sorszámain végig

vezetett i változó, az azonos bitekből álló aktuális szakasz hossza

(hossz) és egyik bitje (bit) reprezentálja. A t.End() az i |f|

kifejezés lesz, a t.Current() a hossz változó értékét adja vissza,

ami a legeslegelső esetben az első bit értéke.

AFirst

= (f:Bit*, e:ℕ, i:ℕ)

EfFirst

= ( f = f’ )

UfFirst

= ( f = f’

(|f| 0 i=1 hossz=f1) )

ANext

= (f:Bit*, e:ℕ, i:ℕ)

EfNext

= ( f = f’ i = i’ )

UfNwxt

= ( f = f’

1ihossz

fiifif

ii

'

'

, ))

t.Fist()

|f| 0

i, hossz:= 1, 0 SKIP

t.Next()

bit, hossz:= fi, 0

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

Page 322: Programozas - Tervezes

10. Feladatok megoldása

322

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

A = (x:enor(Pár), h:(*)n, g:outfile(ℕ))

Pár = rec(ssz:ℕ , van: )

Ef = ( x=x' )

Uf = ( sszeg

vanexe

.

.'

)

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ó

Page 323: Programozas - Tervezes

9. fejezet feladatainak megoldása

323

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

)][(:, szószó.ivil

n

1isearch

l v[i].jel

v[i].jel := true

count:= count+1

SKIP

l := count=n

Az x.First() és x.Next() műveletek között csak annyi különbség

van, hogy a bekezdés legelső szakaszát az u.First() művelettel

Page 324: Programozas - Tervezes

10. Feladatok megoldása

324

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

u.First()

vége:= u.End()

vége

p.ssz := 1; a.Init()

u.End()

BekVég(u.Current())

S

K

a .Mark(u.Current()) I

u.Next() P

p.van:= a.Full()

x.Next()

u.Next()

vége:= u.End()

vége

p.ssz := p.ssz +1; a.Init()

u.End()

BekVég(u.Current())

S

K

a .Mark(u.Current()) I

u.Next() P

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

Page 325: Programozas - Tervezes

9. fejezet feladatainak megoldása

325

akkor igaz, ha a szekvenciális inputfájl bejárása véget ért, azaz

van_szakasz.

x.First()

sf, df, f : read

u.Next()

x.Next()

van_szakasz:= sf=norm

van_szakasz

szakasz:=””

jel:=Jel(df) S

sf = norm Jel(df)=jel K

szakasz:=szak

asz+df

I

sf, df, f: read P

9.5. Egy szöveges állomány legfeljebb 80 karakterből álló sorokat

tartalmaz. Egy sor utolsó karaktere mindig a speciális ’\eol’

karakter. Másoljuk át a szöveget egy olyan szöveges állományba,

ahol legfeljebb 60 karakterből álló sorok vannak; a sor végén a

’\eol’ karakter áll; és ügyelünk arra, hogy az eredetileg egy szót

alkotó karakterek továbbra is azonos sorban maradjanak, azaz

sorvége jel ne törjön ketté egy szót. Az eredeti állományban a

szavakat egy vagy több szóhatároló jel választja el (Az ’\eol’ is

ezek közé tartozik), amelyek közül elég egyet megtartani. A

szóhatároló jelek egy karaktereket tartalmazó – szóhatár nevű –

halmazban találhatók. Feltehetjük, hogy a szavak 60 karakternél

rövidebbek.

Specifikáció:

A = (f:infile( ), g:outfile( ))

Ef = ( f=f' )

A feladat megoldásához a szöveg szavainak felsorolására lesz

szükségünk, hiszen a szavakat kell más tördeléssel az

outputfájlba kiírni. Ennek érdekében az eredményt egy speciális

típusú objektumnak képzeljük el, amelyre bevezetjük a Write60()

műveletet. Ez sort emel, mielőtt egy olyan szót írna ki,

amelyiknek hossza (az eléírt szóközzel együtt) nem fér már ki az

Page 326: Programozas - Tervezes

10. Feladatok megoldása

326

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)

free:=60

g:write(’ ’)

free:=free-1

g:write(szó)

free:=free- hossz(szó)

Ezek után újraspecifikáljuk a feladatot.

A = (x:enor(Szó), y:outfile60( ))

Ef = ( x=x' )

Uf = ( euxe '

)

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

Page 327: Programozas - Tervezes

9. fejezet feladatainak megoldása

327

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 = ( )(}'{}'{

egkofce

)(}'{}'{

ehcffce

)

}'f{e}'c{eha

}'f{e}'c{eha

}'f{e}'c{ehae

)e(g

}'f{e}'c{ehae

}'f{e}'c{eha

}'f{e}'c{eha

)e(h

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

dc<df)

sc=abnorm

(sf=norm

dc>df)

sc=norm sf=norm

dc=df

ko:write(dc) SKIP cf:write(dc)

sc,dc,c:read sf,df,f:read sc,dc,c:read

sf,df,f:read

9.7. Egy x szekvenciális inputfájl (megengedett művelet: read) egy

könyvtár nyilvántartását tartalmazza. Egy könyvről ismerjük az

azonosítóját, a szerzőjét, a címét, a kiadóját, a kiadás évét, az

aktuális példányszámát, az ISBN számát. A könyvek azonosító

szerint szigorúan növekvően rendezettek. Egy y szekvenciális

Page 328: Programozas - Tervezes

10. Feladatok megoldása

328

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 = ( )()'()'(

afu 1mazontazona

)()'()'(

afh 2mazontazona

)

)'()'(ha)(

)'()'(ha"",

)'()'(ha),'(

)(

mazonatazonaag

mazonatazonahiba

mazonatazonata

af

0

0

forgmapldtata

forgmapldtahibataag forgmapldpld ).'().'(ha,)'(

).'().'(ha"",)'()( ).'(

Page 329: Programozas - Tervezes

9. fejezet feladatainak megoldása

329

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

dt.azon<dm.azon

st=abnorm

sm=norm

dt.azon>dm.azon

st=norm

sm=norm

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.

Page 330: Programozas - Tervezes

10. Feladatok megoldása

330

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= ( euxe '

)

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.

Page 331: Programozas - Tervezes

9. fejezet feladatainak megoldása

331

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

Page 332: Programozas - Tervezes

10. Feladatok megoldása

332

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 = ( )()'()'(

afvyxehaa

)

}'y{a)'x(ehaaha)a(g

}'y{a)'x(ehaaha

}'y{a)'x(ehaaha

)a(f

'0'jel).'x(aha

'Q'jel).'x(aha)'x(a

'N'jel).'x(aha)'x(a

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

)a(g'N'jel).'x(eha

'Q'jel).'x(eha

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.

Page 333: Programozas - Tervezes

9. fejezet feladatainak megoldása

333

x.First(); sr,dr,r:read; v := <>

x.End() sr=norm

r.End()

( x.End()

x.Current().eha

<dr)

x.End()

( r.End()

x.Current().eha

>dr)

x.End() r.End()

x.Current().eha=dr

SKIP SKIP v:write(g(t.Current()))

x.Next(); sr,dr,r:read x.Next(); sr,dr,r:read

x.First()

i=1..n

sg[i],dg[i],g[i]:read

l, ind, min :=

ehaidg

normisg

n

1i

].[

][

min

x.Next()

sg[ind],dg[ind],g[ind]:read

l, ind, min :=

ehaidg

normisg

n

1i

].[

][

min

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

Page 334: Programozas - Tervezes

10. Feladatok megoldása

334

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’ ]:

f(i) =

különben1if

0x1if1ifha1ifigaz1if

0x1if1ifhaxigaz1if

0x1if1ifha1if1ifigaz

0x1if1ifhax1ifigaz

i2132

i21i1

i2132

i21i2

)(

')()())(,),((

')()()',),((

')()())(),(,(

')()()'),(,(

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.

Page 335: Programozas - Tervezes

9. fejezet feladatainak megoldása

335

Algoritmus:

poz, neg, első:= hamis, hamis, 0

st,e,x:read

st=norm ( poz neg)

poz

neg e>0

poz

neg e>0

poz

neg e<0

poz neg

e<0

else

poz, első:=

igaz, e

poz:=

igaz

neg, első:=

igaz, e

neg:= igaz SKIP

st,e,x:read

Page 336: Programozas - Tervezes

336

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