Az előző leckében megismert rendezési algoritmusok közül az alap rendezések megvalósítása és időbeli hatékonyságuknak a mérése C++ nyelven egy keretprogram felhasználásával.
{esszé}
Adja meg a rendezés feladat specifikációját!
{/esszé}
Vizsgáljunk meg néhány egyszerű rendezési algoritmus hatékonyságát, empirikusan! Mivel a rendezések hatékonysága a) a rendezendő sorozat hosszától, és b) a rendezettség fokától függ, ezért a programban ezeket igyekezni kell figyelembe vennünk. A következőképpen teszünk:
1. bekérjük a rendezendő elemek számát;
2. (esetleg véletlenszám-generátor használatával) feltöltjük a rendezésre váró tömböt megfelelő számú elemmel;
3. az összes számunkra érdekes rendezés eljárását ugyanarra a kezdő sorozatra lefuttatjuk, miközben mérjük és kiírjuk a hatékonysági jellemzőket.
A rendezettségi fokot definiáljuk négyféleképpen: rendezett, majdnem rendezett (azaz csak töredék számú elem lehet rossz helyen), véletlenszerűen feltöltött, illetve fordítva rendezett.
Hatékonysági jellemzők:
A vizsgálatban részt vevő rendezések:
Mi legyen a rendezendő elemek típusa? Mivel az a két jellegzetes művelet (hasonlítás, mozgatás) időigénye az elemtípus hosszától egyenes arányosan függ, ezért a rendezések összevetése szempontjából a legegyszerűbb számtípus (Egész) is éppolyan sokatmondó, mint bármi más, esetleg összetettebb. A kapott abszolút idők a típustól függhetnek, de az arányok nem.
Mivel számos függvényre bontjuk a programot, érdemes megfontolni a paraméterezés mikéntjét! Több lehetőséget ismertünk meg, amelyek közül a legegyszerűbbet választjuk most: a program paraméterei legyenek globálisan deklaráltak, ami lehetővé teszi, hogy a függvények tényleges paraméterátadási szertartás nélkül elérhessék őket.
Hogyan töltsük föl a rendezendő tömböt? Növekvően, illetve csökkenően feltölteni a tömböt gyerekjáték. Javasolhatók például az alábbi algoritmusok.
Növekedve feltöltés | Csökkenve feltöltés |
|---|---|
![]() | ![]() |
A véletlenszerűen feltöltött is –mint látható– pofonegyszerű algoritmusú. A legbonyodalmasabb a majdnem rendezett algoritmusa. Két lépésből áll: 1) rendezetten feltöltjük, majd 2) néhány (mondjuk 10%-nyi) elemet a jó helyéről kimozgatunk. Lássuk egy lehetséges megoldás algoritmusát.
Véletlenszerűen feltöltés | Majdnem rendezve feltöltés |
|---|---|
![]() | ![]() |
Véletlen számok generálása C++ nyelvben: egyetlen formulát kell csak felidéznünk az előadásból.
Algoritmikusan | C++-ban |
|---|---|
Véletlen(a..b) | rand() % (b-a+1) + a |
Ellenőrizze, hogy a C++ megoldás is teljesíti azt az elvárást, hogy az érték a és b között egész számokat ad!
Hogyan biztosítható, hogy a véletlen számok segítségével feltöltött tömb ugyanaz legyen az összes rendezés számára? Miután föltöltöttük a rendezendő r tömböt, az első rendezés az eredeti sorrendet elrontja (hiszen épp ez a célja!). Ahhoz, hogy a következő rendező algoritmus is az eredetin mutassa be képességeit, készítenünk kell egy másolatot a tömbről egy s segéd tömbben.
A C++ nyelv nyújtotta lehetőségek az időmérésre: a clock() függvény egy egész szám formájában adja vissza a program indulása óta eltelt processzoridőt. E függvény használatához inkludálnunk kell a <time.h> headerfájlt is.
(Ha ezt másodpercben szeretnénk kifejezni, akkor a
clock()/CLOCKS_PER_SEC
formulával kell számolnunk.)
Azért, hogy ne kelljen sok időt fordítani a gépelésre, az alábbi keretprogramot adjuk közre. Ebben a kitöltendő részeket egy kommentárral jelöltük ki:
/*Ide jön az r rendezése*/
Lássuk a némi magyarázattal fűszerezett keretprogramot!
A szokásos köteles kommenteket most is mellőztük. Az újdonság a time.h headerfájl, amely nemcsak a clock() függvénynek, hanem a véletlenszám-generáláshoz is szükséges, ui. a time() függvény segítségével fogjuk inicializálni a generátort:
#include <iostream> #include <time.h> //a time és clock függvényekhez #include <stdio.h> //a printf függvényhez #include <windows.h> //a srand függvényhez, a HANDLE típushoz, a system-hez (pause és a cls DOS-parancsokhoz) using namespace std;
A globális deklarációk következnek. A kiN szorul csak némi magyarázatra. A futás közben a tömböt ki fogjuk listázni a képernyőre, hogy ellenőrizhessük a rendezettséget. Viszont annak érdekében, hogy emészthető mennyiségben kapjuk e számsorozatot, a kiN-nel korlátozzuk a kiírandó elemek számát. Nyilván addig jelenthet előnyt ez a lista, amíg a rendezési algoritmusaink szemantikáját ellenőrizzük. Ekkor viszont az elemszámnak nem is célszerű túlon túl nagynak lennie.
const int maxN=8001;//a sorozat maximális hossza const int kiN=120;//a megjelenítendő (rész)sorozat hossza int s[maxN],r[maxN];//segéd és rendezendő elemek tömbje int elemSzam;//tényleges elemszám int kezdet,veg,elteltIdo;//futási idő kezdete, vége, hossza int hSzam, mSzam;//hasonlítások száma, mozgatások szám
A függvényprototípusok (szignatúrák) következnek:
//beolvassa a min..max közötti egész számot (max<min => max=végtelen) void be_int(string kerdes, int &n, int min, int max, string uzenet); //feltöltésmód beolvasása: int feltoltesMod(); //feltöltő eljárások: void feltoltVeletlennel(); void feltoltNovekedve(); void feltoltCsokkenve(); void feltoltMajdnemRendezve(); //hatékonyságméréshez: void oraIndul(); void oraAll(); //r-et rendező eljárások: void egyszeruCseres(); void minimumKivalsztasos(); void buborek(); void javitottBuborek(); void beilleszteses(); void javitottBeilleszteses(); //cout<=r[1..min(kiN,elemSzam)] void tombKiiras(string cim); void kiertekeles(string cim); //r<=s: void masol(); //billentyű-lenyomásra várakozik void billreVar(); //kurzor-pozícionálás a konzol-ablakban: void curPos(int s, int o); //képernyőtörlés: void ujLap();
A főprogram következik. Mint látható lesz: a feltöltés különböző módszereit tartalmazza ugyan a program, de közülük most mi, a program írói választjuk ki azt az egyet, amely szerint végezzük a vizsgálatot. Érdemes ezen majd túllépni, és rábízni a döntést a program használójára!
int main()
{
srand(time(NULL));//véletlenszám-generátor inicializálása
//a rendezendő tömb feltöltése:
be_int("Elemszám:",elemSzam,11,maxN,"Nem jó elemszám!");
switch (feltoltesMod())
{
case 0:
feltoltNovekedve(); break;
case 1:
feltoltCsokkenve(); break;
case 2:
feltoltMajdnemRendezve(); break;
case 3:
feltoltVeletlennel(); break;
} //a rendezések:
egyszeruCseres();
minimumKivalasztasos();
buborek();
javitottBuborek();
beilleszteses();
javitottBeilleszteses();
return 0;
}
A függvények definíciói jönnek. Először a jól ismert egész számot beolvasó, majd a feltöltési módot beolvasó függvény.
//beolvassa a min..max közötti egész számot (max<min => max=végtelen)
void be_int(string kerdes, int &n, int min, int max, string uzenet)
{
bool hiba;
string tmp;
do
{
if (max>=min)
{
cout << kerdes << " (" << min << ".." << max << "):"; cin >> n;
hiba=cin.fail() || n<min || n>max;
}
else
{
cout << kerdes << " (" << min << "..):"; cin >> n;
hiba=cin.fail() || n<min;
}
if (hiba)
{
cout << uzenet << endl;
cin.clear(); getline(cin,tmp,'\n');
}
}
while(hiba);
}
//feltöltésmód beolvasása:
int feltoltesMod()
{
int melyik;
ujLap();
cout << "Válasszon az a lábbi feltöltésmódok közül:" << endl;
cout << "0: növekedően" << endl;
cout << "1: csökkenve" << endl;
cout << "2: majdnem (növekedően) rendezve" << endl;
cout << "3: véletlenszerűen" << endl;
do
{
curPos(9,9);
cout << "Melyik (0..3): ";
cin >> melyik;
}
while (0>melyik || melyik>3);
return melyik;
}
A feltöltő függvények következnek. Annyit kell velük kapcsolatban észrevenni, hogy nem csupán az r tömböt állítják, hanem értelemszerűen a másolatként szolgáló s-et is.
void feltoltNovekedve()
{
s[1]=rand()%elemSzam+1;
r[1]=s[1];
for(int i=2;i<=elemSzam;++i)
{
s[i]=s[i-1]+rand()%3;
r[i]=s[i];
}
tombKiiras("Feltölt növekedve");
}
void feltoltCsokkenve()
{
s[1]=rand()%elemSzam+1;
r[1]=s[1];
for(int i=2;i<=elemSzam;++i)
{
s[i]=s[i-1]-rand()%3;
r[i]=s[i];
}
tombKiiras("Feltölt csökkenve");
}
void feltoltMajdnemRendezve()
{
int j,k,sv;
s[1]=rand()%elemSzam+1;
r[1]=s[1];
for(int i=2;i<=elemSzam;++i)
{
s[i]=s[i-1]+rand()%3;
r[i]=s[i];
}
for(int i=1;i<=elemSzam/10;++i)
{
j=rand()%elemSzam+1;
do
{
k=rand()%elemSzam+1;
}
while (j==k);
sv=s[j]; s[j]=s[k]; s[k]=sv;
r[j]=r[k]; r[k]=sv;
}
tombKiiras("Feltölt majdnem rendezve");
}
void feltoltVeletlennel()
{
for(int i=1;i<=elemSzam;++i)
{
s[i]=rand()%elemSzam+1;
r[i]=s[i];
}
tombKiiras("Feltölt véletlennel");
}
A hatékonyságmérés két központi függvénye jön. Az óraállításon felül a hasonlítás és a mozgatás számlálókat is alaphelyzetbe hozzák.
void oraIndul()
{
kezdet=clock();
hSzam=0; mSzam=0;
}
void oraAll()
{
veg=clock();
elteltIdo=veg-kezdet;
}
A megjelenítő eljárásban az igényes táblázatolás kedvéért felhasználjuk a C++ C-ből származó formázott kiíró műveleteit, és az ujLap-ban a konzolt törlő, az operációs rendszertől kölcsönvett cls parancsot.
void tombKiiras(string cim)
{
ujLap();
cout << cim << endl << endl;
int meddig; //ameddig listázza az elemeket... az ellenőrzés kedvéért
if (elemSzam<kiN)
{
meddig=elemSzam;
}
else
{
meddig=kiN;
}
for(int i=1;i<=meddig;++i)
{
printf("%4d",i); printf(":%5d",r[i]);
}
cout << endl;
billreVar();
}
void kiertekeles(string cim)
{
tombKiiras(cim);
cout << "\nEltelt ido:" << elteltIdo;
cout << " | Hasonlítások száma:" << hSzam <<
" | Mozgatások száma:" << mSzam << endl;
billreVar();
}
Az alábbiakban a hatékonyságméréssel kapcsolatot nyitó és záró tevékenységgel kibővített rendező függvények definícióit találjuk. A rendezést ide kell beillesztenünk.
void egyszeruCseres()
{
oraIndul();
/*Ide jön az r rendezése*/
oraAll();
kiertekeles("Egyszerű cserés");
masol(); /* Az eredeti érték visszaállítása*/
}
void minimumKivalasztasos()
{
oraIndul();
/*Ide jön az r rendezése*/
oraAll();
kiertekeles("Minimumkiválasztásos");
masol(); /* Az eredeti érték visszaállítása*/
}
void buborek()
{
oraIndul();
/*Ide jön az r rendezése*/
oraAll();
kiertekeles("Buborékos");
masol(); /* Az eredeti érték visszaállítása*/
}
void javitottBuborek()
{
oraIndul();
/*Ide jön az r rendezése*/
oraAll();
kiertekeles("Javított buborékos");
masol(); /* Az eredeti érték visszaállítása*/
}
void beilleszteses()
{
oraIndul();
/*Ide jön az r rendezése*/
oraAll();
kiertekeles("Beillesztéses");
masol(); /* Az eredeti érték visszaállítása*/
}
void javitottBeilleszteses()
{
oraIndul();
/*Ide jön az r rendezése*/
oraAll();
kiertekeles("Javított beillesztéses");
masol(); /* Az eredeti érték visszaállítása*/
}
S végül néhány segédfüggvény definíciója zárja a programot.
//r<=s:
void masol()
{
for (int i=1;i<=elemSzam;++i)
{
r[i]=s[i];
}
}
//billentyű-lenyomásra várakozik
void billreVar()
{
system("pause");//windows esetében!
}
//kurzor-pozícionálás a konzol-ablakban:
void curPos(int s, int o)
{
HANDLE hkonzol;
COORD hova;
hova.X=o; hova.Y=s;
SetConsoleCursorPosition (hkonzol,hova);
}
//képernyőtörlés:
void ujLap()
{
system("cls"); //képernyőtörlés
}
A lényegi függvények megoldását alább mellékeljük. Ne felejtse a hSzam, mSzam számlálókat a megfelelő helyen a megfelelő értékkel növelni!
void egyszeruCseres()
{
oraIndul();
int i,j,sv,n=elemSzam;
for (i=1;i<=n-1;++i)
{
for (j=i+1;j<=n;++j)
{
if (r[i]>r[j])
{
++hSzam;
sv=r[i]; r[i]=r[j]; r[j]=sv;
mSzam+=3;
}
}
}
oraAll();
kiertekeles("Egyszerű cserés");
masol(); /* Az eredeti érték visszaállítása */
}
void minimumkivalasztasos()
{
oraIndul();
int i,j,sv,minind,n=elemSzam;
for (i=1;i<=n-1;++i)
{
minind=i;
for (j=i+1;j<=n;++j)
{
if (r[j]<r[minind])
{
++hSzam;
minind=j;
}
}
sv=r[minind]; r[minind]=r[i]; r[i]=sv;
mSzam+=3;
}
oraAll();
kiertekeles("Minimumkiválasztásos");
masol(); /* Az eredeti érték visszaállítása */
}
void buborek()
{
oraIndul();
int i,j,sv,n=elemSzam;
for(i=n;i>=2;--i)
{
for( j=1;j<=i-1;++j)
{
if(r[j]>r[j+1])
{
++hSzam;
sv=r[j]; r[j]=r[j+1]; r[j+1]=sv;
mSzam+=3;
}
}
}
oraAll();
kiertekeles("Buborékos");
masol(); /* Az eredeti érték visszaállítása */
}
void javitottBuborek()
{
oraIndul();
int cs,i,j,sv,n=elemSzam;
i=n;
while (i>=2)
{
cs=0;
for(j=1;j<=i-1;++j)
{
if (r[j]>r[j+1])
{
++hSzam;
sv=r[j]; r[j]=r[j+1]; r[j+1]=sv;
cs=j;
mSzam+=3;
}
}
i=cs;
}
oraAll();
kiertekeles("Javított buborékos");
masol(); /* Az eredeti érték visszaállítása */
}
void beilleszteses()
{
oraIndul();
int i,j,sv,n=elemSzam;
for (i=2;i<=n;++i)
{
j=i-1;
while((j>0) && (r[j]>r[j+1]))
{
++hSzam;
sv=r[j]; r[j]=r[j+1]; r[j+1]=sv;
--j;
mSzam+=3;
}
}
oraAll();
kiertekeles("Beillesztéses");
masol(); /* Az eredeti érték visszaállítása */
}
void javitottBeilleszteses()
{
oraIndul();
int i,j,sv,n=elemSzam;
for (i=2;i<=n;i++)
{
j=i-1; sv=r[i];
++mSzam;
while((j>0) && (r[j]>sv))
{
++hSzam;
r[j+1]=r[j];
--j;
++mSzam;
}
r[j+1]=sv;
++mSzam;
}
oraAll();
kiertekeles("Javított beillesztéses");
masol(); /* Az eredeti érték visszaállítása */
}
A kész programmal végezheti végre a kitűzött vizsgálatot. Javasoljuk, hogy az alábbi táblázatot töltse ki a program futtatása alapján, majd próbáljon következtetésekre jutni az egyes minőségi jellemzők elemszámtól való függését illetően!
| Előrendezettség: rendezett | |||
|---|---|---|---|
| Elemszám | Futási idő | Hasonlításszám | Mozgatásszám |
| 11 | |||
| 22 | |||
| 44 | |||
| 88 | |||
| 110 | |||
| 1100 | |||
| Előrendezettség: majdnem rendezett | |||
|---|---|---|---|
| Elemszám | Futási idő | Hasonlításszám | Mozgatásszám |
| 11 | |||
| 22 | |||
| 44 | |||
| 88 | |||
| 110 | |||
| 1100 | |||
| Előrendezettség: véletlenszeruen feltöltött | |||
|---|---|---|---|
| Elemszám | Futási idő | Hasonlításszám | Mozgatásszám |
| 11 | |||
| 22 | |||
| 44 | |||
| 88 | |||
| 110 | |||
| 1100 | |||
| Előrendezettség: fordítottan rendezett | |||
|---|---|---|---|
| Elemszám | Futási idő | Hasonlításszám | Mozgatásszám |
| 11 | |||
| 22 | |||
| 44 | |||
| 88 | |||
| 110 | |||
| 1100 | |||
Mire következet? FutásIdő(N)=???, Hasonlításszám(N)= ???, …
Azt kapta, amit az előadáson elméleti megfontolások alapján hallott?
Az elkészült programot egészítse ki úgy, hogy A) a mérést akárhányszor (újraindítás nélkül) lehessen elvégezni, azaz a kilépés külön kérésre történjen; B) a mérési eredményeket „kulturált” szerkezetben egy szöveges fájlba írja ki! A B) feladatnál gondoljon arra, hogy könnyen lehessen a nyert nagyszámú adatot akár egy táblázatkezelő segítségével is feldolgozni!
![]() ![]() |
![]() |
![]() |