5. Tömbök¶
Egy algoritmusban számos értéket kell nyílván tartanunk változók segítségével. Ha azonban minden értéket egyszerűen egy külön változóban tárolunk, az sokszor elég nehézkessé teszi az adatok kezelését. Gondoljunk például egy futó versenyre, ahol a különböző rajtszámmal induló versenyzők pontszámait szeretnénk eltárolni az alábbi módon:
1 2 3 4 5 6 7 8 No_one = 92 No_two = 88 No_three = 43 No_four = 95 No_five = 74 ... No_seventynine = 29 No_eighty = 56
Ebben az esetben elég fárasztó lenne megírni azt az algoritmust, amely megmondja, hogy átlagosan hány pontot értek el a versenyzők.
A legtöbb pontot elért versenyző pontszámának meghatározása pedig már gyakorlatilag kivitelezhetetlen. Ilyenkor 80
darab if
utasításra
lenne szükségünk, ahol mindegyik feltétel 79
logikai részfeltételt tartalmazna.
1 2 3 4 5 6 7 8 9 10 11 if No_one>No_two AND No_one>No_three AND ... No_one>No_eighty then legtobb = No_one endif if No_two>No_one AND No_two>No_three AND ... No_two>No_eighty then legtobb = No_two endif ... if No_eighty>No_one AND No_eighty>No_two AND ... AND No_eighty>No_seventynine then legtobb = No_eighty endif output "A legmagasabb pontszám: ", legtobb
Hasonló helyzetekben egy tömb nevű eszköz használata jelentősen megkönnyíti a programozó munkáját. A tömb azonos jellegű,
összetartozó értékek sorozata, ahol az egyes értékekre egy egész jellegű sorszámmal hivatkozhatunk. Ez az ún. index. Az egyes
valós programozási nyelvek a tömbök indexelése szempontjából két csoportot alkotnak. Vannak olyan nyelvek, amelyekben a tömb első
elemének az indexe 1
az emberi logikának megfelelően, míg más nyelvek esetén a legelső elemre a 0
indexszel kell hivatkozni, mert
ez előnyös a számítógép számára. Az olvasó által hamarosan megismerendő nyelvek (C, Java) az utóbbi technikát használja, így
didaktikailag célszerű ebben a jegyzetben is 0
-val kezdeni a tömbök indexelését pszeudokódban. A tömböknek - akárcsak a változóknak -
van nevük. Az adott elem indexét mindig közvetlenül a tömb neve után elhelyezett szögletes zárójelben tüntetjük fel. Az előbbieknek
megfelelően, ha van egy pontok nevű tömbünk, amely N
darab értéket tartalmaz, akkor az első elemre pontok[0]
alakban hivatkozhatunk,
míg az utolsóra pontok[N-1]
formában.
A tömbök használatával a fenti átlag versenypontszám meghatározás jóval egyszerűbbé válik. Persze most is azzal kell kezdenünk, hogy eltároljuk a pontszámokat.
1 2 3 4 5 6 7 8 pontok[0] = 92 pontok[1] = 88 pontok[2] = 43 pontok[3] = 95 pontok[4] = 74 ... pontok[78] = 29 pontok[79] = 56
Amennyiben az értékeket nem akarjuk beleégetni az algoritmusba, hanem menet közben szeretnénk bekérni a felhasználótól, akkor azt is könnyen megtehetjük tömbök esetén. Először is meg kell tudnunk hány értéket akar megadni a felhasználó, majd egy ciklussal végig kell mennünk a tömb elemein folytonosan inkrementálva egy indexként használandó változót és közben bemenettel feltöltve az adott tömbelemet.
1 2 3 4 5 6 7 8 output "Hány versenyző van?" input elemszam index = 0 while index <= elemszam-1 do input pontok[index] index = index+1 enddo output "A pontszámok tárolása kész."
Ha ezután az összpontszámot kell meghatároznunk, nem egy 80
tagú összeget kell kézzel leírnunk. Ehelyett egy ciklussal végig kell
mennünk a tömb elemein, közben pedig az aktuális tömbelem értékét egy
részösszeget tartalmazó változóhoz kell hozzáadnunk. A ciklus végén a számok összegét természetesen elosztjuk a tömb elemeinek számával
és a hányadost kiíratjuk.
9 10 11 12 13 14 15 index = 0 osszeg = 0 while index <= elemszam-1 do osszeg = pontok[index] index = index+1 enddo output "Átlag: ", osszeg/elemszam
Mi a helyzet a legmagasabb versenypontszám megtalálásával? Ez egyfajta szélsőérték keresési probléma, ami gyakran előfordul az informatikában. Egy tömböt tartalmazó pszeudokódban megfogalmazott algoritmus sokkal gyorsabban megírható, mint a korábbi 6320 darab kézzel megadott összehasonlítást tartalmazó verzió. Minden elemet csak egyszer vizsgálunk meg egy ciklusban, miközben tároljuk az eddig talált legnagyobb értéket. Az alábbi megoldásban és az összes további mintakódban feltételezzük, hogy a tömbök már fel vannak töltve értékekkel.
1 2 3 4 5 6 7 8 9 10 elemszam = 80 maximum = pontok[0] index = 1 while index <= elemszam-1 do if pontok[index]>maximum then maximum = pontok[index] endif index = index+1 enddo output "A legagasabb pontszám: ", maximum
Az algoritmus elején azt mondjuk, hogy az első elem az eddig talált legnagyobb érték. Ezután minden további elemet összehasonlítjuk a maximum változó értékével, és ha az aktuális érték nagyobb, akkor felülírjuk vele a skalár változó értékét.
Gyakran találkozhatunk egy olyan problémával, ami csak kicsit tér el az előzőtől, amikor is nem a szélsőérték érdekel bennünket, hanem
annak a helye. Esetünkben például az, hogy milyen sorszámú (indexű) versenyző érte el a legtöbb pontot. Az előző algoritmuson csak
kis változást kell eszközölnünk. Ahelyett, hogy magát az eddigi legnagyobb pontszámot tárolnánk (maximum
), a legnagyobb értéket
tartalmazó tömbelem indexét (nyertes
) tároljuk.
1 2 3 4 5 6 7 8 9 10 11 elemszam = 80 nyertes = 0 index = 1 while index < elemszam do if pontok[index]>pontok[nyertes] then pontok[nyertes] = pontok[index] endif index = index+1 enddo output "A legmagasabb pontszámú versenyző: ", nyertes+1 output "Az ő pontszáma: ", pontok[nyertes]
Felhívnám a figyelmet arra, hogy az i
sorszámmal rendelkező versenyző pontszáma a tömb i-1
indexű elemében van eltárolva. Tehát
míg a pontszám kiírásához a nyertes
változó értékét használjuk, addig a versenyző (emberi logikájú) sorszámának kiírásához a
nyertes+1
kifejezés szükséges.
5.1. Keresés¶
Mint látható a tömbök használata számos probléma megoldása esetén előnyös lehet. A számítógéppel megoldandó problémák jelentős részében felmerül az a részfeladat, hogy el kell döntenünk, hogy egy érték el van-e már tárolva a tömbben. Néha az is fontos lehet, hogy megtudjuk a már tárolt érték a tömb milyen indexű elemében található meg. Ezt a problémacsoportot nevezzük keresés-nek. Számos megoldás ismert a problémára ezek közül az informatikai alap algoritmusok közül néhányat mutat be ez a fejezet.
Tételezzük fel tehát, hogy van egy A
nevű N
darab értékkel feltöltött tömbünk, amelyben keressük az X
értéket. Az első ötlet,
amely valószínűleg eszünkbe jut az, hogy sorban minden tömbelemet összehasonlítunk a keresett értékkel, amíg egyezést nem találunk.
Ezt pszeudokóddal így oldhatjuk meg:
1 2 3 4 5 6 7 8 9 i = 0 while i<N AND A[i]!=X do i = i+1 enddo if i==N then output "A keresett érték nincs a tömbben." else output "A keresett érték indexe: ", i endif
A ciklus feltétele egy összetett logikai kifejezés. Az ismétlés addig folytatódik, amíg van még hátra vizsgálandó tömbbelem és az aktuális elem nem
a keresett érték. (Mindkét feltételnek egyszerre teljesülnie kell a logikai AND operátor miatt.) Ha elhagytuk a ciklust, akkor ennek
két oka lehet. Egyik szerint az i
index értéke elérte az elemek számát, azaz már megvizsgáltuk az N-1
-dik (utolsó) elemet is. Ebben
az esetben tudjuk, hogy a keresett X
érték biztosan nem volt benne a tömbben. A másik ok az, hogy megtaláltuk az adott értéket az
N-1
-dik vagy korábbi cellában. Ekkor az i
értéke éppen az X
tárolási helyének indexe. Ezt az algoritmust nevezzük teljes
keresés-nek.
Sok elem esetén az algoritmus végrehajtási idejének jelentős részét a ciklus teszi ki. Általános esetben a fenti ciklus N/2
alkalommal fut le vagyis a
végrehajtási idő a tömb elemeinek számával egyenes arányosságban nő. Ebben az algoritmusban a ismétlési feltételben két összehasonlítási
műveletet is el kell végezni ismétlésenként, azaz átlagosan N
darabot.
Mindig léteznek alternatív algoritmusok. Ezeknek természetesen minden körülmények között ugyanazt az eredményt kell szolgáltatniuk, azonban bizonyos szempontokból egyik vagy másik előnyösebb lehet a többinél. Ennek szemléltetésére nézzük meg egy másik megoldását az előbbi keresési problémának. Támadhat egy olyan ötletünk, hogy a keresett elemet egy plusz lépéssel az algoritmus elején tegyük be a tömb utolsó eleme után. Ezután hajtsuk végre a keresést! Ilyenkor ez elemet mindenképpen megtaláljuk. A kérdés az, hogy hol. Az utolsó eredeti elem után vagy már korábban? Lássuk az ezt megvalósító pseudokódot!
1 2 3 4 5 6 7 8 9 10 A[N] = X i = 0 while A[i]!=X do i = i+1 enddo if i==N then output "A keresett érték nincs a tömbben." else output "A keresett érték indexe: ", i endif
A változás látszólag elhanyagolható, ráadásul még egy plusz utasítássort is alkalmaznunk kell. Melyik a hatékonyabb módszer? Általános
esetben itt is gyakorlatilag N/2
lépés után találjuk meg az értéket. Azonban a ciklus feltétele most csak egyetlen egyszerű
logikai kifejezést tartalmaz, vagyis általános esetben itt csak N/2
összehasonlítást végzünk az ismétlések során. Ez fele annyi,
mint az előző algoritmus esetén, határozottan gyorsítva ezzel az algoritmus végrehajtását, még a plusz egy utasítás ellenére is.
Ezt tehát azáltal érhetjük el, hogy egy ún. őrszemet (a tömb végére helyezett X
értéket) használunk arra, hogy a túlindexelést
elkerüljük (azaz ne hajtsuk végre a ciklust nem létező tömbelemekre).
Eddig nem volt semmilyen kikötésünk a tömbre vonatkozóan. Azonban ha szűkítjük a tervezett algoritmusunk alkalmazási körét, akkor érdekes
új lehetőségeket kell megfontolnunk. Tételezzük fel, hogy a tömbünk rendezett, azaz minden elemet nála nem nagyobb elemek előznek meg és
nála nem kisebb elemek követnek. Másképp fogalmazva: az utolsó elem kivételével bármely i
indexű elemre igaz az, hogy A[i]<=A[i+1]
.
(A növekvő rendezettség helyett természetesen a csökkenő rendezettséget is választhattuk volna.) Miért másabb a feladat ebben az esetben?
A különbség megértéséhez tekintsük a következő esetet! Keressük a 27
értéket egy növekvő rendezettségű tömbben! Amennyiben a tömb
egyik eleme 394
, akkor biztosak lehetünk benne, hogy a hátralévő elemek között biztosan nem fordulhat elő a keresett érték,
befejezhetjük a keresést. Írjuk meg a keresés pszeudokódját erre az esetre, feltételezve tehát, hogy a tömb rendezett!
1 2 3 4 5 6 7 8 9 i = 0 while i<N AND A[i]<X do i = i+1 enddo if i==N OR A[i]>X then output "A keresett érték nincs a tömbben." else output "A keresett érték indexe: ", i endif
Mint látható a ciklusból kétféleképpen léphetünk ki. Vagy túllépünk az utolsó elemen is (i==N
) vagy a keresett értéknél nem kisebb
tömbelemet találunk (A[i]>=X
). Ezek alapján meg tudjuk állapítani, hogy megtaláltuk-e a keresett értéket vagy nem.
Extrém esetben előfordulhat, hogy a ciklusmagba egyszer sem lépünk be, azaz már az első tömbelem nagyobb, mint a keresett érték.
Általános esetben elmondhatjuk, hogy bár a keresési idő egyenesen arányos a tömb elemeinek számával, mégsem szükséges mindig minden
tömbelem megvizsgálása. Ez hatékonyabb, mint a teljes keresés, aminek az az ára, hogy csak rendezett tömb esetén alkalmazható.
Ennek az algoritmusnak a neve lineáris keresés, utalva arra, hogy sorrendben
keresünk. A ciklus feltételében szereplő i<N
logikai kifejezés akkor igazán hasznos, ha az összes tömbelem kisebb, mint a keresett
érték. Ennek a helyzetnek az elkerülésére itt is használhatjuk az őrszem logikáját eltárolva X
értékét a tömb N
indexű pozíciójába.
Ekkor a ciklusfeltétel csak egy egyszerű logikai kifejezést fog tartalmazni, tovább gyorsítva az algoritmust.
Bizonyára mindenki ismeri azt a játékot, amikor az A
játékos gondol egy számra 1 és 100 között, a B
pedig próbálja kitalálni
melyik egész számra gondolt a társa. Mindezt úgy teszik, hogy B
mond egy tippet, amire A
megmondja, hogy a tipp nagyobb-e vagy
kisebb, mint a kigondolt szám vagy esetleg megegyezik-e vele. Mindezt addig ismétlik, amíg B
ki nem találja a számot. Egy okos
játékos nem véletlenszerűen tippelget. Első tippje az intervallum közepén lévő érték. Ha például a gondolt szám ennél nagyobb, akkor
tudja, hogy az csak az intervallum második felében lehet. Következő tippjével ezt a szűkebb intervallumot felezi meg, és így tovább.
A játék mindig nagyon hamar véget érhet.
Ehhez nagyon hasonló módszert alkalmazhatunk abban az esetben, ha egy rendezett tömbben keresünk egy konkrét értéket. Persze a tömbelemek nem biztos, hogy egyenközűen elhelyezkedő egész értékek, de a számuk mindig véges egész így egy elem kiválasztásával mindig szét tudjuk osztani a tömb egy folytonos részét két közel azonos méretű intervallumra. Ezek közül pedig mindig csak az egyik rejti a keresett értéket. Ennek a ciklikus intervallum felezésen alapuló algoritmusnak a neve bináris keresés. Pszeudokód segítségével a következőképpen reprezentálhatjuk:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 first = 0 last = N-1 if N%2==0 then middle = N/2 else middle = (N-1)/2 endif while A[middle]!=x AND first<=last do if A[middle]>x then last = middle-1 else first = middle+1 eldif if (last+first)%2==0 then middle = (last+first)/2 else middle = (last+first-1)/2 endif enddo if first>last then output "A keresett érték nincs a tömbben." else output "A keresett érték indexe: ", middle endif
A keresés során egy folyton szűkülő intervallumot vizsgálunk. Az intervallum első elemének az indexe first
, az utolsóé last
. A tömbelem
vizsgált folytonos szakaszának közepét a tömb middle
indexű eleme jelöli. Egy 7 elemű tömbben (ahol ez elemek indexei 0-tól 6-ig terjednek)
a középső elem indexe 3. Egy páros számú elemet tartalmazó tömbben nincs igazi középső elem. Amennyiben N=6
(azaz indexek 0-tól 5-ig),
akkor az middle=2
és az middle=3
is jó választás lehet. (A bemutatott algoritmus az utóbbi választással él, azaz az middle
indexű elem előtt
eggyel több eleme van a tömbnek, mint utána.) Az intervallum kezeléséhez szükséges három segédváltozó (first
, middle
, last
) a pszeudokód
első 7 sorában kap kezdőértéket.
Az algoritmus jelentős része egy ciklus, ami addig ismétlődik, amíg meg nem találjuk a keresett értéket vagy az intervallum nulla elemű nem lesz (amikor is az első elem indexe nagyobb, mint az utolsóé). A ciklus magjában lévő első feltételes utasítás a vizsgált intervallum méretét csökkenti vagy az első elem indexét vagy az utolsó elem indexét megváltoztatva. Ezt a keresett érték és a középső elem viszonya dönti el. Ezután meg kell határozni az új lerövidült intervallum közepének választott elem indexét. Ezt végzi el a második elágaztató utasítás.
Amikor hamissá válik a ciklus összetett logikai kifejezése és elhagyjuk a ciklust, akkor tudnunk kell, hogy ez miért is történt meg.
Erre utal a 20. sor feltétele. Ha üres az intervallumunk (first>last
), akkor nincs meg a keresett elem, különben az aktuális
intervallum közepén kell legyen, azaz az A
tömb middle
indexű eleme az.
Bizonyára észrevette a kedves olvasó, hogy ez az algoritmus sokkal bonyolultabb, a pszeudokód pedig sokkal hosszabb, mint például a
teljes keresés esetén. Miért éri meg a bináris keresés használata a fejlesztésbe fektetett energiát? Tudnunk kell, hogy mondjuk
legrosszabb esetben hány iterációs lépés szükséges X
megtalálásához. Minden ismétlés során durván feleződik az intervallum hossza. Ha
mondjuk egy 1000000
elemet tartalmazó rendezett tömböt használunk, akkor 21
ismétlés biztosan elég (mivel a kettő huszadik hatványa
több, mint egymillió). Emlékezzünk vissza, hogy teljes kereséssel legrosszabb esetben egymillió ismétlés szükséges. Ez az óriási
keresési idő különbség egyértelművé teszi, hogy megéri ügyes programozónak lenni.
5.2. Rendezés¶
A lineáris és bináris keresés gyorsaságának ára van. A tömb elemeinek rendezettnek kell lennie, tehát ahhoz, hogy alkalmazni tudjuk őket, először rendeznünk kell a tömböt. Tegyük fel, hogy növekvő rendezettségre van szükségünk, azaz bármely elem esetén igaznak kell lennie annak az álltásnak, hogy előtte nem lehetnek nála nagyobb elemek, mögötte pedig nem lehetnek kisebb elemek. Ha a tömb eredetileg véletlenszerű sorrendben tartalmazza az értékeket, akkor nekünk kell rendeznünk őket.
Az első ötlet az lehet, hogy hozzunk létre egy azonos méretű tömböt, majd keressük meg az eredeti tömb legkisebb elemét (a korábbi szélsőérték kereséssel) és azt tegyük az új tömb első helyére, a második legnagyobb elemet a második helyre és így tovább. Ez viszonylag egyszerű algoritmus, de szükségünk van egy új tömbre, azaz kétszer akkora lesz majd a programunk memóriaigénye. Általában emiatt helyben rendezzük az elemeket, azaz csak az eredeti tömbben dolgozunk és értékpárok megfelelő felcserélgetésével érjük el a rendezettséget.
Hogyan tudunk két értéket megcserélni? Tegyük fel, hogy az A
változó értéke 5
, a B
változóé pedig 7
. Írni akarunk egy algoritmust,
aminek a hatására A
értéke 7
lesz, B
pedig 5
lesz. A legtöbb kezdő az alábbi pszeudokódot írná:
1 2 A = B B = A
Értelmezzük ezt a két utasítást lépésenként! Az első azt jelenti, hogy B
értéke, vagyis a 7
tárolódjon el az A
változóba. Ekkor
a korábbi tartalmat, vagyis az 5
-öt felülírjuk az A
változóban, B
pedig változatlan marad. Vagyis mindkét változóban 7
lesz, elvesződött
az egyik érték és azt már a második utasítás sem tudja visszaállítani. Tehát ez egy hibás lépéssorozat.
Egy jó ötlet lehet az, ha az egyik változó értékét ideiglenesen elmentjük egy új változóba és csak ezután írjuk felül a másik változó
tartalmával, majd az új változó értékét másoljuk át a B
változóba. Ezt teszi az alábbi pszeudokód.
1 2 3 C = A A = B B = C
Ez a három lépés egy segédváltozó segítségével tehát képes felcserélni két változó értékét.
Átmeneti segédváltozó nélkül is megcserélhetjük az értékeket, csak egy kis trükkre van szükségünk. Nézzük az alábbi szintén háromlépéses megoldást pszeudokódban!
1 2 3 A = A+B B = A-B A = A-B
Kövessük nyomon hogyan alakul az egyes változók értéke az utasítások végrehajtása során! Először A
értéke 12
lesz, majd a második
utasítás hatására B
megkapja az 5
-ös értéket, végül A
7
-et kap értékül.
Valóban felcserélődtek az értékek. Bár ez a megoldás nem igényel segédváltozót, az első megoldás szélesebb körben alkalmazható
(és egyszerűbb is).
Ezek után már készen állunk elemcserék segítségével történő tömbrendezésre. Az egyik legkézenfekvőbb ötletet valósítja meg az ún. szélsőérték kiválasztásos rendezés. A növekvő sorba rendezést mondatszerű formában a következőképpen adhatjuk meg.
Vedd a teljes tömböt!
Keresd meg a vizsgált elemek közül a legkisebbet!
Cseréld meg a legkisebb elemet és a vizsgált elemek közül a legelsőt!
A vizsgált elemek közül hagyd ki a most előre helyezett legkisebbet!
Ha nincs több vizsgált elem, akkor kész vagy, különben folytasd a 2. lépéssel!
Ha alaposan megnézzük, ez a lépéssorozat már tartalmazz ismerős részeket. A legkisebb elem megtalálása nem más, mint egy szélsőérték keresés, ahol a legkisebb/legnagyobb elem értékét és indexét keressük meg. Ezt már korábban megírtuk, most csak újra fel fogjuk használni. (Jó példa ez az algoritmusok beágyazására.) A fenti algoritmusban a minimális elem helyét kell megtalálnunk. Emellett vegyük észre, hogy az ismétlések során nem mindig ugyanazokkal az elemekkel foglalkozunk. Kezdetben természetesen a teljes tömböt kell szem előtt tartanunk, de később az eredeti tömb végén lévő pásztázandó terület folyamatosan zsugorodik. A tömb elején eközben fokozatosan növekszik, az a szakasz, ahol az elemek már rendezetten helyezkednek el. Ez alapján rakjuk össze lépésről lépésre az algoritmus pszeudokódját!
A vizsgálandó szakasz kezdődjön a 0
indexű elemmel!
1 start = 0
A korábban már megismert módon keressük meg a legkisebb elem helyét (indexét)!
3 4 5 6 7 8 9 10 min_i = start i = start+1 while i<N do if A[i]<A[min_i] then min_i = i endif i = i+1 enddo
Tehát min_i
tárolja a szélsőérték indexét. Ezt az elemet meg kell cserélnünk a vizsgált szakasz első elemével, aminek az indexe
a start
.
11 12 13 tmp = A[start] A[start] = A[min_i] A[min_i] = tmp
Közben egy segédváltozót (tmp
) is használunk. A legkisebb elem tehát a szakasz elejére került, aminek indexe a start
változóban van.
Az összes ez utáni elem még rendezetlen. Ezért a szélsőérték keresést és az elemcserét többször is meg kell ismételnünk, folyton növelve
a rendezetlen szakasz kezdetének indexét. Ez utóbbi ciklust addig kell folytatni, amíg legalább 2 elem van a rendezetlen szakaszban
(utoljára úgyis a legnagyobb marad). A teljes pszeudokód tehát így néz ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 start = 0 while start<N-1 do min_i = start i = start+1 while i<N do if A[i]<A[min_i] then min_i = i endif i = i+1 enddo tmp = A[start] A[start] = A[min_i] A[min_i] = tmp start = start+1 enddo
Bizonyára észrevette az olvasó, hogy van egy belső ciklusunk a szélsőérték helyének keresésére, ami egy külső ciklus magjában fordul elő.
A külső ciklus majdnem N
-szer fut le, míg a belső átlagosan N/2
alkalommal hajtódik végre minden egyes külső ciklusmagban.
Összességében tehát elmondhatjuk, hogy a belső ciklus magjában lévő elempár összehasonlítás N(N-1)/2
alkalommal értékelődik ki. A
végrehajtási idő tehát négyzetesen függ a rendezendő tömb méretétől.
Most nézzünk a tömb elemeinek rendezésére egy alternatív algoritmust, amely tehát teljesen más elvek szerint működik, mégis ugyanazt
a rendezett tömböt adja eredményül. A most bemutatásra kerülő módszer neve buborék rendezés. Itt is cserélgetünk elemeket, de azok
mindig szomszédos elemei a tömbnek. Először menjünk végig tömb összes szomszédos értékpárján, és ha az elempáros nagyobbik tagja megelőzi a
kisebbet (növekvő rendezés esetén) akkor megcseréljük őket. Ennek hatására a nagy értékű elemek átlagosan kicsit hátrébb kerülnek, a
kisebbek pedig kicsit előrébb. Amikor végére érünk ennek az ismétlésnek, akkor a tömb még nem rendezett, csak azt tudhatjuk biztosan,
hogy a legnagyobb elem a tömb végére került. Ha ezt az egész ciklust még egyszer megismételjük, akkor a második legnagyobb elem is a
helyére kerül. Ha pedig N-1
alkalommal hajtjuk végre a ciklust a tömb rendezetté válik. Pszeudokód segítségével tehát a rendezés így
is kinézhet:
1 2 3 4 5 6 7 8 9 10 11 12 13 i = 1 while i<=(N-1) do j = 0 while j<(N-1) do if A[j]>A[j+1] then tmp = A[j] A[j] = A[j+1] A[j+1] = tmp endif j = j+1 enddo i = i+1 enddo
Ebben az algoritmusban a j
indexű elemnek és a rákövetkezőjének (A[j+1]
) összehasonlítása (N-1)(N-1)
alkalommal történik meg. Ez több,
mint a kiválasztásos rendezés esetén, azonban ezen még sokat javíthatunk, ha nyilvánvaló lehetőségeket figyelembe veszünk. A legnagyobb
elemek folyamatosan a tömb végére kerülnek, azaz mindig lesz egy szakasz, ami rendezett, azaz a következő ciklusban már nem is kell átnéznünk.
Változtassuk meg a külső ciklus számlálóját! Ne azt tárolja, hogy hányadik iterációnál járunk, hanem azt, melyik indexű helyen ér véget
a tömb elején lévő rendezetlen szakasz. Így a ciklusszámláló az idő előrehaladtával folyamatosan csökkenni fog. Emellett a belső ciklusban
nem kell mindig végigmenni az összes elempáron csak a rendezetlen szakaszon, aminek a végét a új külső ciklusszámlálónk tárolja. Így a
belső ciklus egyre rövidebb, ahogy haladunk a külső ismétléssel. A módosítások eredménye ez a pszeudokód:
1 2 3 4 5 6 7 8 9 10 11 12 13 i = N-1 while i>0 do j = 0 while j<i do if A[j]>A[j+1] then tmp = A[j] A[j] = A[j+1] A[j+1] = tmp endif j = j+1 enddo i = i-1 enddo
Ezzel az érték összehasonlítások száma lecsökkenhet annyira, mint kiválasztásos rendezés esetén. Sőt ezt tovább is csökkenthetjük még egy kis trükkel. Ha egyszer végighaladunk a belső ciklus összes lépésén és közben egyszer sem volt szükséges elemcsere, akkor későb sem lesz rá szükség, mert a tömb hamar rendezetté vált. Így néha korábban is befejezhetjük a rendezést egy módosított algoritmusban.
Több rendezési algoritmus bemutatását a jegyzet terjedelme nem teszi lehetővé, de érdemes megemlíteni, hogy számtalan további népszerű
rendezési algoritmus használatos. Példaként néhány: beszúrásos rendezés, gyorsrendezés, kupac rendezés, stb.
A helyben rendezésekről általában elmondhatjuk, hogy a tömb méretének duplájára növelésénél a szükséges műveletek száma több, mint
duplájára növekszik. Jobb esetben is végrehajtási idő az N log(N)
kifejezéssel lesz arányos. Ha nem kell ragaszkodnunk ahhoz,
hogy mennyi segédváltozót és segédtömböt használunk, akkor egy tömb rendezése lineáris idejű is lehet. Ilyen algoritmus például a
számjegyes (radix) rendezés, de ez túlmutat a tárgy keretein.
5.3. Több dimenziós tömbök¶
Az eddig alkalmazott tömbjeink ún. egydimenziós tömbök más szóval vektorok voltak. Ez azt jelentette, hogy egyetlen indexszel el
tudtuk érni az összes egymás utáni elemet. Vannak azonban olyan problémák, ahol szükség lehet ennél összetettem eszközre. Ilyen például
a mátrix, ami elemek egy kétdimenziós, azaz sorokba és oszlopokba szervezett elrendezése. Az egyes elemekre két egymástól független
indexszel hivatkozhatunk, amelyek megmondják melyik sornak és melyik oszlopnak a metszetében van a bizonyos elem. Jelölhetjük például így
a 9. sorban és 3. oszlopban lévő elemet: M[8][2]
(ne feledjük, az indexek mindkét dimenzióban 0-tól indulnak). A többdimenziós tömbök
használatát bemutathatjuk egy olyan pszeudokóddal, amely létrehoz egy egységmátixot, azaz értékek egy 5x5-ös négyzetes elrendezését, ahol a
főátlóban kizárólag 1 szerepel, míg az összes többi mátrixelem 0.
1 2 3 4 5 6 7 8 9 10 11 sor = 0 while sor<5 do oszlop = 0 while oszlop<5 do if sor==oszlop then M[sor][oszlop]=1 else M[sor][oszlop]=0 endif enddo enddo
Lieáris algebrára épülő feladatokban gyakran használhatjuk majd ezt az eszközt.
5.4. Szójegyzék¶
- index
Egy tömb elemeinek eléréséhez használt egész szám. Gyakorlatilag, mintha az adott elem sorszáma lenne az adott tömbön belül. Számos programozási nyelv az indexelést nullával kezdi, azaz a tömb első elemének indexe
0
. Ezt a logikát használjuk mi is.- keresés
Az informatikai alapalgoritmusok azon csoportja, amelynek keretében egy adatról el kell dönteni, hogy szerepel-e egy adatszerkezetben (tömbben), és ha igen, akkor hol.
- mátrix
Értékek téglalapszerű elrendezése sorokban és oszlopokban, ahol minden elem két index segítségével érhető el.
- rendezés
Egy olyan lépéssorozat, aminek hatására egy tömb elemei átrendezéssel növekvő vagy csökkenő sorrendbe kerülnek. Számos különböző rendezési algoritmus közismert: szélsőérték kiválasztásos rendezés, beszúrásos rendezés, buborékrendezés, gyorsrendezés, kupacrendezés, radix rendezés, shell rendezés.
- szélsőérték keresés
Ebben a témakörben a tömbben tárolt legnagyobb vagy legkisebb értéket megkeresését jelenti. Gyakran nem is magát az értéket, hanem annak helyét (indexét) keressük.
- tömb
Több azonos jellegű mennyiség tárolása egy egységben úgy, hogy minden adatelemre egy nem negatív egész számmal, az index-szel hivatkozhatunk.
5.5. Kérdések, feladatok¶
Írj le egy algoritmust pszeudokód segítségével, amely 10 darab értéket kér be a felhasználótól és fordított sorrendben kiíratja őket!
Írj le egy algoritmust pszeudokód segítségével, amely egy
A
nevűN
darab értékkel feltöltött tömb legkisebb értékű elemének indexét határozza meg!Írj le egy algoritmust pszeudokód segítségével, amely 10 darab számot vár bemenetként és meghatározza az értékek szórását!
Adott egy
A
nevű ésN
elemű tömb, amelynek elemei kizárólag 10-20 zárt intervallumbeli egész számok lehetnek (egy érték akár többször is). Határozd meg a tömb elemeinek móduszát, azaz találd meg a leggyakrabban előforduló elemet (ha több ilyen is van, akkor csak az egyiket)! [S504]Írj meg a lineáris keresés algoritmusát őrszem használatával, amely egy
A
nevűN
darab értékkel feltöltött tömbben keresi meg X értékét!Írj meg a bináris keresés algoritmusát csökkenő sorrendben rendezett
A
nevűN
elemű tömbre, ahol a keresett értéket inputként várod a felhasználótól!Írj egy algoritmust, amely a felhasználótól bekér egy számot, majd ennek az értéknek az összes előfordulását az
A
nevűN
elemű tömbben kicseréli nullára!Buborékrendezés segítségével rendezd az alábbi tömböt és rendszeresen írd le a tömb aktuális tartalmát! {92, 32, 4233, -23, 84, 93, 0, 11}
Járj utána az Internet segítségével, hogy hogyan működik a beszúrásos rendezés és írd meg a pszeudokódját! [S509]
Gondold át, hogyan gyorsíthatjuk az előbbi algoritmust bináris keresés használatával.
Adott egy
A
nevűN
darab számértéket tartalmazó tömb és egyB
nevűM
elemű tömb. Mindkét tömbben az értékek növekvő sorrendbe vannak rendezve. Hozz létre egyC
nevűN+M
elemű rendezett tömböt az előbbi két tömb elemeinek az összefésülésével! Az algoritmusod legyen hatékony! [S511]