Adatstruktúrák és algoritmusok

A problémamegoldás menete
    Való problémák => absztrakt modellezés => algoritmus => program

Algoritmus
    Az algoritmus egy hatékony eljárás egy feladat vagy probléma megoldására, melynek helyessége bizonyítható.

Hatékonyság
    A hatékonyságot a futási idő és a memóriaigény határozza meg.


Algoritmusok futásidő elemzése
    Futási idő: Egy bizonyos bemenetre a végrehajtott (gépfüggetlen) alapműveletek, vagy lépések száma.

    A lépésszám pontos meghatározása helyett általában elegendő a lépésszám nagyságrendjének meghatározása,
    és ebből már (kis óvatossággal) következtetni lehet arra,
    hogy az algoritmus mennyire hatékony, avagy hogyan fog viselkedni nagyobb értékekre.

Aszimptotikus Hatékonyság
    Ha a bemenet mérete elég nagy, akkor az algoritmus futási idejének csak a nagyságrendje érdekes

Az O ordó jelölés
    Az O jelölést arra használjuk, hogy a futási idő növekedését aszimptotikusan alulról és felülről konstans távolságra behatároljuk.

    Például a bináris keresés futási ideje legrosszabb esetben O(log n), helytelen lenne azt állítani, hogy a bináris keresés futási ideje O (log n) minden esetben.
    A bináris keresés futási ideje soha nem rosszabb, mint O (log n), de van amikor ennél jobb.

    Jó lenne egy olyan aszimptotikus jelölés ami azt fejezné ki, hogy "a futási idő maximum ennyivel nő, de ennél lassabban is nőhet." Erre használjuk az O jelölést.

    Ha a futási idő O(f(n)), akkor elég nagy n esetén a futási idő maximum k*f(n) valamilyen konstans k érték mellett.

    Azt mondjuk "f(n) ordója" vagy egyszerűen "O(f(n))" (kiejtésben használatos még az "ordó f(n)" is).
    Az O jelölést aszimptotikus felső korlátként használjuk, mivel a futási időt felülről korlátozza, ha az input mérete elég nagy.

    Bináris keresés futási ideje: O(log<sup>2</sup>n)



Tömb
    A tömb egy olyan adatszerkezet, amely menet közben nem méretezhető át.
    Tehát ha új elemeket szeretnénk egy meglévő tömbhöz adni, az csak úgy fog működni,
    hogy létrehozunk egy új tömböt, ami az új elemek és a meglévő elemek tárolására alkalmas,
    ezután pedig bemásoljuk a meglévő elemeket és az új elemeket a teljesen új tömbünkbe.

    Tömbökben referencia típusokat is alkalmazhatunk,
    viszont ebben az esetben nem elég példányosítani a tömböt,
    az egyes elemeket is példányosítani kell, mivel ebben az esetben a tömb csak az objektumra mutató referenciát tárolja,
    így a példányosítás nélkül a tömb elemeinek értéke null lesz.

    Az osztályokat nem muszáj a konstrukroruk segítségével példányosítani.
    Erre a célra vezették be a nyelvben az Object Initializer szintaxist,
    amivel egy osztály adattagjai úgy adhatóak meg, mint egy tömb elemei.

    Ez akkor jön jól, ha van egy osztályunk, amely adattagokkal rendelkezik,
    de a konstruktor az objektum minden adattagjának beállításához nagyon összetett és komplikált lenne.
    Ebben az esetben nem érdemes konstruktort írni. Az objektum inicializáló szintaxis a következő:
    
    var objektum = new osztály(){
        Adattag = érték,
        Adattag2 = érték,
        Adattag3 = érték
    };

    Ez a szintaxis csak olyan adattagok esetén alkalmazható, amelyek publikusan is írhatóak.
    Egyéb védelmi szinttel rendelkező adattagok nem írhatóak ezzel a módszerrel.
    Ezen adattagok beállítására továbbra is a konstruktor szintaxis használható.

    Az osztályokat

Tömbök kezelését segítő metódusok

    Length{ get;} 

    long Length{get;}
    Visszaadja az aktuális tömb elemeinek a számát hosszú egész típusban.
    Akkor jön jól, ha nagyon nagy méretű tömböket szeretnénk kezelni.

    int Rank{get;}
    Visszaadja a tömb dimenzióinak a számát

    Ezen tulajdonságokon kívül az Array osztály számos statikus metódust tartalmaz,
    amelyeket felhasználhatunk tömbök kezelésére.
    Ezek közül a leghasznosabbak és legfontosabbak:
        Array.Clear(Array array, int index,int length);

        Array.Copy(Array sourceArray, Array destinationArray, int length);
        Array.Copy(Array sourceArray, Array destinationArray, long length);
        //harmadik paraméter  a másolandó elemek száma

        Array.Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length);

        int Array.IndexOf(Array array, object value);
        int Array.IndexOf(Array array, object value, int startIndex);

        int Array.LastIndexOf(Array array, object value);
        int Array.LastIndexOf(Array array, object value, int startIndex);

        Array.Reverse(Array array);
        Array.Reverse(Array array, int index, int length);
        // második elem a kezdőelem indexét adja meg, a harmadik az elemk számát  adja meg

        Array.Sort(Array array);
        //Akkor ha az IComperable<T> implem,entálva van

        Array.Sort(Array keys, Array items);
        //Két tömb elemeinek a sorbarendezése, méghozzá úgy,hogy az első paraméterként megadott tömb kulcsokat tartalmaz,
        //amelyhez a második paraméterként megadott tömb értékek társulnak.

Adatstruktúra interfészek
    A kollekciókhoz kapcsolódó interfészek közül az egyik legfontosabb az IEnumerable<T> interfész.
    Ez teszi lehetővé, hpgy mindegyik kollekción implementáéciótól függetlenül végig tudjuk uteráléni egy foreach ciklussal.

    Az absztrakció következő szintje az IReadOnlyCollection és a ICollection interfészek.
    Az ICollection<T> egy módosítható kollekciót ír le aminek at elemei törölhetőek és bővíthetőek.
    A IReadOnlyCollection<T> pedig egy olyat,amiből csak olvasni tudunk, de tudjuk az elemek számát.

    Az olyan típusok esetén, mint a lista halmaz és ... meg van valósítva mind a kettő.

Láncolt lista
    A tömb adatszerkezet kiváló, ha előre tudjuk, hogy menni elemre van szükségünk.
    A bővítés csak úgy lehetséges,ha létrehozunk egy újabb tömböt, aminek a mérete a hozzáadandó elemek számával meg van növelve.
    Az új tömbbe átmásoljuk a meglévő elemeit, majd az új tömbhöz hozzáadjuk az új elemeket.
    Végezetül pedig töröljük az eredeti tömböt.

    Az algoritmus leírásából kiolvasható, hogy et nem éppen ideális,mivel a sebességre igen negatív hatással van a másolás.
    Továbbá a másolás folyamán egy rövid időre duplázódik a programunk memóriahasználata.

    Egy sokkal jobb megoldása lehet nagy mennyiségű előre nem ismert számú adat tárolására a láncolt lista szerkezet.

    A láncolt lista egy eleme két részből épül fel. Egyrészt tartalmazza a tárolni kívánt adaott,
    vagy adatokat és tartalmaz egy olyan  mutatót, ami a lista eg másik elemét mutatja.
    Ha a referencia a következő elemre nem létezik, akkor a lánc végén vagyunk.

    A láncolt lista a dinamikus tömbhöz képest hátránya a közbülső elemnek elérhatőségéből ered.
    Míg egy tömb esetén ha tudjuk,hogy a k. elemet szeretnénk elérnim akkor a tömb indexelésével rögtön hozzáférhefünk ehhet az adathoz, addog a láncolt listában a lista elejéről indulva a mutatókon keresztül addig kell lépkedni, míg a k. elemhez nem értünk.
    A véletlenszerű lista elem megtalálása a lista hosszával arányos időt igényel.

Egyszeresen láncolt lista
    Egyszeresen láncolt listában egy darab mutató jelöli a lista rákövetkező elemét.
    Ha ismerjük a lista legelső elemét (lista feje), akkor abból elindulva a mutatók segítségével végig járhatjuk a listában tárolt elemeket.
    A lista legutolsó elemének mutatójának értéke null, ez jelzi, hohgy tovább nem tudunk haladni a listában.
    Láncolt lista esetén általában egyszeresen láncolt listára gondolunk.

    Csomopont osztály: az adatot és a következő elemre mutató referenciát tartalmazza.
    LancoltLista osztály: tartalmazza a listához szükséges főbb műveletekez:
        Hozzaad:  új elemet ad a lista végéhez
        Torol:  töröl
        Kiir:   kiír
    
    Pogram osztály: 
        Teszteli a fenit műveleteket, létrehozza a listát, hozzáad elemeket,
         töröl egy létező és egy nem létező elemet, majd kiírja a lista tartalmát.
    
Kétszeresen láncolt lista 
    Kétszeresen láncolt lsuita esetán 2db hivatkozűs van egy csomópontban,
    az egyik az előző a másik a következő csomópontra mutat, C# ban kétszeresen láncolt listák vannak.
    Hasonlóan mint a listák, szintén  osztályból vannak létrehozva és ezért referencia típus,
    természetesen referencia másolás történik értékadásnál.

    Mint a listák esetében is a LinkedList beírása utám a <> jelek közé kerül a láncolt listánk típusa és a megszokott név,
    egyenlőségjel a new operátor valamint újra a LinkedList és el ne felejtsük a zárójeleket!

    LinkedList<string> lancoltlista = new LinkedList<string>();

    Ezután a Linkedlist.AddLast(érték) metódussal tudunk a listánk végére beszúrni egy elemet,
    vagy például az AddFirst(érték) metódussal pedig a lista első helyére.

Láncolt listák fontos metódusai
    LinkedList.RemoveLast/First törli a lista utolsó vagy első elemét.

    LinkedListNode<T> = LinkedList.Last/First
        Visszaadja egy adott listában szereplő első vagy utolsó csomópontot, amelyben megtalálhatjuk a következő csinópontra mutató hivatkozást.
    
    A LinkedList<T> objektumban minden csomópont LinkedListNode<T> típusú.
    Mivel a LinkedList<T> kétszeresen kapcsolódik, minden csomópont előre mutat a köevtkező csomópontra, illetve hátra az előzőre.

    LinkedList.AddAfter(LinkedListNode, value) / AddBefore
        Egy csomópont elé vagy után szúr be adatot.

    LinkedListNode<T> = LinkedList.Find(keresett_ertek)
        Visszaadja a megadott értékhez tartozó csomópontot.
    
    Csomópontok - LinkedListNode
        Egy listából létrehozunk egy-egy csomópontot, melyben az adott csomóponthoz tartozó érték és a következő csomópontra mutató hivatkozás van.
        A következő csomópontra való ugráshoz a .Next metódust kell használnunk, hogy visszafele közlekedjünk pedig a .Previous metódust kell használnunk.
        A .Value metódus az adott csomópontban eltárolt adatot adja vissza.
        Mivel tudjuk, hogy a láncolt listák utolsó, illetve első eleme null értékű, így egy while ciklussal is végig tudunk menni az adott láncolt listán,
         a léptetéséről a Next gondoskodik.

Verem (stack)
    A verem egy olyan adatszerkezet, amelyben az utoljára betett elemet tudjuk feldolgozni.
    (LIFO)
    A veremmutató mindig a legfelső elemre mutat.
    A push a tetejére rak , a pop onnan vesz el.

    A Stack<T> osztály fontosabb tulajdonságai és metódusai:
    Stack(int capacity)
        Paraméteres konstruktor. A paraméter a kiindulásként tárolni kivánt elemej számát adja meg.
    Stack(IEnumerable<T> collection)
        Paraméteres konstruktor.
        A verem elemei  aparaméterklnt megadott IEnumerable felület implementáló osztály elemei lesznek.

    T Peek()
    Visszaadja a verem tetején lévő elemet anélkül, hogy azt kivenne a verembol.

    T Pop()
    Visszaadja a verem tetején lévő elemet és az elemet eltávolítja a veremből.
    
    void Push()
    A paraméterkéntmegadott elemet beilleszti a verem tetejére.

    T[] ToArray()
    A verem elemeit visszaadja egy tömbben.

    void TrimExcess()
    Átméretezi a veremet úgy, hogy csak annyi elemnek foglaljon helyet, mint amennyi ténylegesen használva van.

Verem osztály
    Generikus (a generikus fogalom a programozásban olyan technikára utal,
    amely lehetővé teszi, hogy osztályok, metódusok, vagy adatszerkezetek különböző típusoknál ..)

    program osztály:
        létrehoz egy Verem<int> példányt 5 kapacitással.
        Bemutatja az összes műveletet, beleértve a verem túlcsordulásának és kiürítésének kezelését.
        
Fák
#TODO egészítsd ki ezt a részt

Hash függvény, Hash tábla, Hasító tábla
    A hash egy rögzített hosszúságú érték, amelyet egy matematikai képlet segítségével állítanak elő.
    A hash értékeket adattömörítésben, kriptológiában stb. használják.
    Az adatindexelésében a hash értéket használjuk, mert rögzített hosszúságúak, függetlenül a generálásukhoz használt értékektől.
    Lehetővé teszi, hogy a ......

    A hah fgvény egy matematikai algoritmust alkalmaz a kulcs hash-é alakítására.
    Az ütközés akkor következik be, ha egy hash függvény ugyanazt a hash értéket állítja elő több kulcshoz.

Hash tábla
    A hash tábla egy olyan adatstruktúra, amely kulcs értékpár használatával értéket tárol.
    Minden értékhez egydi kulcs van hozzárendelve, amelyet egy hash függvény segítségével állítanak elő.

    A kulcs neve a hozzá tartozó érték elérésére szolgál.
    Ez nagyon felgyorsítja az értlkek keresését a hash táblában,
    függetlenül a hash táblában lévő elemek számától.

Hash funkciók
    Például, ha az alkalmazottak nyilvántartásait szeretnénk tárolni,
    és minden alkalmazott egyedileg azonosítható egy alkalmazotti szám segítségével.
    Kulcsként használhatjuk az alkalmazotti számot,
    értékként a munkavállalói adatokat rendelhetjük hozzá.

    A hash függvény megoldja a fenti problémát azáltal, hogy lekéri az alkalmazottu számot,
    és ennek segítségével generál egy hash egész értéket,
    rögzített számjegyeket, és optimalizálja a tárhelyet.
    A hash függvény célja egy kulcs létrehozása, amely a tárolni kívánt értékre hivatkozik.
    A függvény elfogadja a mentendő értéket, majd egy algoritmus segítségével kiszámítja a kulcs értékét.

Dictionary

    A szótár elempárok tárolására szolgál,
    melyek közül egyik a kulcs, amely azonosítja az elempárt,
    másik az érték, minden kulcs egyedi.

    Gyakorlatilag a szótár úgy viselkedik,
    mint egy lista, de az elemek indexe itt tetszőleges típusú lehet pl.: szöveg.

    Konstruktora generikus, paraméter nélküli:
        Dictionary<TKey, TValue>(): létrehoz egy szótárt, ahol Tkey a kulcs TValue az érték típusa.
    
    A szótár elemei a [] operátorral érhetőek el.
        A szótárat foreach ciklussal lehet végig olvasni, amellyel a szótárból KeyValuePair<Tkey, Tvalue>
        típusú elemeket kapunk. Ezek Key value mezői adják a megfelelő kulcs és érték párokat.

    Fő metódusai:
        Add(TKey, TValue)
        Bool ContainsKey(TKey)
        Bool ContainsValue(TValue)
        bool Remove(Tkey) // ha sikeres a művelet akkor true
        int Count()
        void Clear() szótár ürítése

    Elemek összehasonlítása
        Sok esetben lehet szükségünk az elemek sorba rendezésére,
        ehhez a Syste.Collections.Generic.IComparer<T>
        interfész megvalósító osztályra van szükségünk.
        A T azon adattípus, amelyen majd összehasonlítást végzünk.

    Ennek fő metódusai
        int Compare (T x, T y):
            összehasonlít két azonos típusú elemet, visszatérési értéke:
                negatív ha x kisebb mint y
                nulla, ha x egyenlő y
                pozitív ha x nagyobb mint y
    
Hashmaps alapjai
    A Hashmaps hatékony megoldást kínál az adatok hatékony tárolására ás visszakeresésére.
    A C# ban a HashMaps-t a Dictionary <Tkey, Tvalue> osztály képviseli,
    amely alapvető eszközként szolgál a kulcs értél párok kezeléséhez.
    Lényegében a HashMap kulcs-érték asszociációk gyűjteményeként működik,
    lehetővé téve azb egyedi kulcsokon alapuló értékekhez valóü gyors hozzáférést.

    A Hashmaps azon elven működik, hogy egyedi kulcsokat rendel hozzá a megfelelő értékekhez.
    Ezek a kulcsok a társított értékek azonosítóként működnek, gyors hozzáférést biztosítva az adatokhoz
    anélkül, hogy a teljes gyűjteményt végig kellene ismételni.

    A C# nyelvben a HashMaps a Dictionary<Tkey, Tvalue>
    osztály használatával példányosodik, amely a Tkey a kulcsok típusát,
    a TValue pedig az értékek típusát jelöli.

    Dictionary<string, int> ageMap = new Dictionary<string, int>();

    HashMap műveletek
    Elemek hozzáadása és visszakeresésére
    A HashMap-hez elemek hozzáadása magában foglalja az Add() metódust,
    amely értéket rendel egy adott kulcshoz.

    Az értékek lekérése a HashMap-ról úgy érhető el, hogy az értéket a megfelelő kulcsokkal érik el.
    int ageOfAlice = ageMap["Alice"];

    Létezés ellenőrzése
    .ContainsKey(TKey)

    Elemek eltávolítása
    .Remove(TKey)

    Iterálás a HashMap-bemenet
        használj foreach ciklust
        minden elem kulcs érték párt add vissza

    HashMap teljesítménye
        A HashMap CSharpban kiváló teljesítményjellemzőket kínál a visszakeresési beillesztési és törlési műveletekhez.
        Az alapul szolgáló megvalósítás hash kódokat használ az elemek hatékony lokalizálására és kezelésére, ami állandó idejű O1
        bonyulultságot eredményet a legtöbb műveletnél.

        Fontos azonban megjegyezni, hogy a tényleges teljesítmény olyan tényezőktől függően változhat, mint az elemek száma, a hash kód ütközései és a terhalési tényező.


Bináris Keresés 

    A bináris - más néven logaritmikus vagy felező módszer - 
    mivel n elem esetén log(n) futási idővel rendelkezik.

    A bináris keresés egy erősen optimalizált keresési eljárás,
    amely csak rendezett adatsoron alkalmazható.
    Pélédául, amikor egy nyomtatott szótárban keresünk egy szót vagy jelenléti íven keressük a nevünket.
    A keresett értéket egy mintaadattal összehasonlítjuk és az eredménytől függően - amennyire lehet -
    egy nagy részt kizárunk a tartományból.

    A módszer külön implementációt nem igényel, mivel az Array és a list osztály os tartalmaz bináris keresésre implementációt.

    int index = Array.BinarySearch(Array array, object value);

Buborékrendezés

    A rendezés során a tömb fennmaradó részén végighaladva az egymás utáni szomszédos elemeket összehasonlítjuk, és ha szükséges, megcseréljük őket, hogy közölük mindig a nagyobb helyezkedjen el feljebb.

    Pszeudokód:
        Ciklus i = 6 -tól 1-ig
            Ciklus j = 0-tól i-1-ig
                Ha a[j] > a [j+1] AKKOR
                    csere a[j] a[j+1]

    Az algoritmusnak csak akkor van értelme tovább futnia, ha a belső ciklusban volt csere.

    A javított algoritmus futási ideje legjobb esetben lineáris lesz,
    legrosszabb esetben pedig négyzetes.

Beszúrásos rendezés

    A beszúró rendezés nevét onnan kapta, hogy a működése leginkább a kártyalapok egyenként való kézbevételéhez s a helyükre igazításához hasonlítható.

    Vesszük a soron következő elemet, és megkeressük a helyét a tőle balra lévő,
    már rendezett részben, majd a kereséssel párhuzamosan a nagyobb elemeket rendre eggyel jobbra mozgatjuk.
    Az aktuális elemet egy segédváltozóban tároljuk,
    mert a mozgatások során értéke felülíródhat egy nagyobb elemmel.

    Ezen algoritmus használata akkor igazán előnyös,
    ha az adatsorunk már részben rendezett.
    Továbbá akkor igen hatékony, ha egy rendezett sorozatot bővítünk és a bővítés után is szeretnénk,
    hogy a sorozat rendezett maradjon.

    Az algoritmus futási ideje legjobb esetben konstans, legrosszabb esetben négyzetes.
    Pszeudokód:
            Ciklus i = 6 -tól 1-ig
                j = i - 1
                tmp = a[i]
                AMÍG (j >= 0) és (a[j] > tmp)
                    a[j+1] = a[j]
                    j = j - 1
                a[j+1] = tmp

