Függvények sorozatok minimumértékének és minimumhelyének meghatározására

Számos esetben lehet szükség arra, hogy egy sorozat elemei közül a minimális értékűt kiválasszuk és meghatározzuk, hogy ez a sorozatban hol található. Ha az elemek egy iterátorból származnak, akkor a „hol” a sorrendpozíciót, ha pedig egy sorozattípusú konténerobjektumban állnak rendelkezésre, akkor az indexet jelenti.

Ha csak a minimumértékre lenne szükség, akkor egyszerű lenne a dolog, mert a min() beépített függvényt használhatjuk, ami nem csak konténert, hanem általánosabban, egy iterálható objektumot képes fogadni. Ennek nagyméretű adathalmaznál jelentősége van, mert így nem kell a teljes adatsort egy konténerben a memóriában tárolni (ha egyáltalán azt a memóriakapacitás engedi).

A helyzet azzal válik egy kicsit összetettebbé, hogy a minimumérték sorozatbeli helyét is tudni akarjuk. Ha a min() függvényt az előbb említett módon használjuk, vagyis a sorozatot adjuk argumentumnak, akkor a gond, hogy a végén az adatokat szolgáltató iterátor kimerül, és ezért a pozíciómeghatározás mint második lépés nem tehető meg. Ha tehát a min() függvényt így akarjuk használni, akkor a pozíció megállapításához az adatsort előzetesen el kell tárolni valamilyen sorozattípusú konténerben (list, tuple, deque). Ha ez megvan, akkor e konténer index() metódusnak átadva a min() függvénnyel meghatározott minimális értéket, megkapjuk annak helyét (indexét).

Ez az eljárás tehát extra memóriát igényel és két keresési lépésben határozza meg az igényelt adatokat: először a minimumértéket a teljes sorozaton végighaladva, majd az indexet szintén a teljes sorozatból.

Kérdés, hogy van-e más megoldás a minimumérték és minimumhely meghatározására? Igen van, több is.

Eljárhatunk úgy is, hogy bár eltároljuk a sorozatot, de a min() függvénnyel nem a minimális elemet határozzuk meg, hanem előbb a minimális indexet. Ezt úgy tesszük, hogy a min() argumentumaként egy olyan range objektumot adunk, amely a sorozat elemszámával megegyező indexértékeket ad ki, a key paraméternek pedig a sorozat __getitem__ metódusobjektumát. A minimálértékhez tartozó index ismeretében a minimumértéket egyszerűen elemkikéréssel kapjuk meg.

Az előző egy változata, ha a sorozatot és a range objektumot „összezipeljük”, vagyis a zip() függvényt ezekkel meghívjuk és az így kapott iterátort adjuk át a min() függvénynek. Az eredmény a minimumértéket és -indexet  tartalmazó kételemű tuple lesz.

Megoldható a feladat úgy is, hogy használjuk a min() függvényt, azonban nem kell tárolni a sorozatot és egyetlen lépésben kapjuk meg a minimumot és annak pozícióját. Ezt úgy érhetjük el, hogy a min() argumentuma az enumerate(sorozat). Ekkor a minimumkeresés ismérve az enumerate() által szolgáltatott kételemű tuple konténerek második eleme lesz. Ezt a key paraméterrel határozhatjuk meg lambdafüggvénnyel vagy az operator modul itemgetter függvényével.

Olyan megoldások is lehetnek, amelyek nem használják a min() függvényt.

Az egyik ilyen, amikor a sorozatot kétszer is tároljuk: egy tuple és egy list konténerben. Bár ez pazarlónak tűnik, de az az értelme, hogy a list sort() metódusa – amellyel a „Rendezéshez a sorted() függvényt vagy a list.sort() metódust használjuk?” című bejegyzésben is foglalkoztunk – gyors rendezést nyújt, és ezt követően a lista első eleme lesz a minimális érték. Ezt az eredeti sorrendet megörző tuple index() metódusának átadva megkapjuk a minimumhoz tartozó indexet.

Másik olyan módszer, ahol nem használjuk a min() függvényt és nem is kell tárolni a sorozatot az, hogy egy ciklusban végighaladunk a sorozat elemein és megvizsgáljuk, hogy az-e az éppen aktuális minimális érték. Ha nem, akkor továbbhaladunk. Ha igen, akkor ezt tesszük meg aktuális minimumnak. Ha a ciklusokat számláljuk, akkor a minimális érték indexét is meg tudjuk a végén kapni. A ciklusszámlálást megvalósíthatjuk egy változó értékének explicit növelésével vagy a ciklusban az enumerate(sorozat) iterátorból kérjük ki az index és érték elemeket.

A fent felsorolt megoldási változatoknak megfelelő függvények definícióit alább láthatjuk:

A függvénynevekben szereplő azonos számok a függvénytörzsben alkalmazott megoldási módszerek hasonlóságára utal: az 1-es csoportban nem tároljuk a sorozatot és ciklust alkalmazunk, a 2-es csoportban tároljuk a sorozatot és két keresési lépésben határozzuk meg a minimális értéket és annak pozícióját. A 3-as csoport függvényei mind használják a min() függvényt és egy keresési lépésben határozzák meg a minimális értéket és annak pozícióját.

A kérdés, hogy ezek közül melyiket válasszuk?

Az egyik szempont a memóriahasználat. Ha hosszú az adatsorozat (pl. fájlból olvassuk be), és amelynek egyben történő tárolása a felhasználható memóriát túllépné, akkor azok a függvények jönnek szóba, amelyek nem tárolják a sorozatot.

A másik szempont a futási idő. Ez szintén nagyméretű adathalmaz esetén fontos. Ezt azonban a függvények definíciójából nem tudjuk megítélni, ezért mérni kell. A végrehajtási idők vizsgálatára a következő tesztprogramot használjuk.

A teszt eredményeit két sorozathosszra az alábbi táblázatokban foglaltuk össze eltérő színekkel jelölve a hasonló futási idő teljesítményű függvényeket.

Látható, hogy mindkét sorozathossz esetén a 2-es csoport függvényei állnak az élen. Ezeket követik az 1-es, majd a 3-as csoport függvényei. Az eredmény annyiban meglepő, hogy a 2-es csoport a tárolási és két keresési idő ellenére bizonyul a leggyorsabbnak. Tehát, ha a memóriafelhasználás nem kritikus szempont, akkor érdemes ezek közül választani. Az eredmények alapján pedig konkrétan azt, amelyben a sorozat tárolására tuple konténer szolgál. Ha a memóriahasználat szempont, akkor az 1-es csoport mindkét függvénye megfelelő, bár az enumerate() használatával működő általában egy kicsivel nagyobb futási időt mutat.

Ha az adathalmaz nagyméretű, azaz a sorozat meglehetősen hosszú, a memóriafelhasználás jelentős lehet azoknál a függvényeknél, amelyek belül eltárolják a sorozatot. Ennek ellenére ezeket a függvényeket sem kell elvetni végleg. Ugyanis a sorozatot darabokban, szakaszokban is feldolgozhatjuk. Ilyenkor a teljes sorozat minimuma a részsorozatot minimumából a legkisebb érték lesz. A minimális érték teljes sorozatbeli pozícióját pedig ki lehet számolni a részsorozaton belüli indexet és a részsorozatokat számláló változó értékét,  valamint a részsorozat hosszát felhasználva. Ezt a feldolgozási eljárást valósítja meg az alább látható programban a min_using_chunks() függvény. Ennek a sorozaton kívül meg lehet adni a használni kívánt minimum-meghatározó függvényt, valamint a részsorozat hosszát.

Az ilyen módon meghatározott minimumérték és minimumhely számítási időszükségletét az előbbi program további soraival teszteljük. Az eredményeket az alábbi táblázatok foglalják össze az előző tesztnél alkalmazott sorozathosszakra. A relatív sorrend majdnem ugyanaz, mint az előző esetben azzal az eltéréssel, hogy most az egyértelmű befutó az a 2-es csoportba tartozó függvény, amely a list.sort() metódust alkalmazza.

Megfigyelhető, hogy a relatív sorrend majdnem ugyanaz, mint az előző esetben azzal az eltéréssel, hogy most az egyértelmű befutó az a 2-es csoportba tartozó függvény, amely a list.sort() metódust alkalmazza.

A beépített sorozattípusú konténereket, továbbá a beépített függvényeket, köztük a min() függvényt is, a Python tudásépítés lépésről lépésre című e-könyv „Beépített konténerobjektumok”, illetve „Beépített függvények” fejezetei ismertetik. Iterálható objektumokkal a „Kérem a következőt!” – iterálható objektumok című fejezet, iterárokkal az „Iterátorok és elemeik kinyerésének nyelvi megvalósítása” fejezet foglalkozik. A végrehajtási idő mérésével pedig a „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezeten belül a „A programvégrehajtás felfüggesztése és a futási idő mérése” című alfejezet.

Érdekel a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyv.