Datové struktury a algoritmy (7.)

V minulých dílech jsme se seznámili s datovými strukturami zásobník a fronta, které jsme implementovali staticky, tedy pomocí pole. Pole sice bylo dynamicky alokované, ale to bylo jen z toho důvodu, abychom mohli určit délku fronty. Dnes se podíváme na poslední modifikaci fronty - frontu prioritní. Dnes také začneme s úvodem k dynamické struktuře nazývané lineární spojový seznam.

7.1 Datové struktury

7.1.1 Fronta prioritní (Priority Queue)

    Tento druh fronty se někdy také označuje jako fronta s předbíháním. Stejně jako se v normálním životě cítí někteří lidé důležití, že mohou předbíhat, tak je tomu i u prvků naší datové struktury. Proto do implementací fronty z minulého dílu přidáme další pole, které bude udávat prioritu prvku - použijeme pole složené z celých čísel.

    Nyní stojíme před problémem jak správně umístit prvek do pole. Uděláme to třeba následovně: pokusíme se přidat nový prvek, pak půjdeme od konce fronty (budeme uvažovat jednostrannou frontu) a podíváme se do pole priorit. Ověříme, zda-li je priorita vkládaného prvku vyšší než priorita prvku posledního, pokud ano, pak posuneme poslední prvek na místo nově vznikajícího prvku a vkládaný prvek budeme takhle umísťovat dále. Pokud narazíme na prvek s vyšší prioritou, pak je nově umísťovaný prvek na správném místě. Implementace:

#ifndef FRONTA_H
    #define FRONTA_H

    #include <iostream.h>

    template<class T> class CPrioFronta {
    private :
        T *m_Pole;
        int *m_Priority;
        int m_Velikost;
// maximalni velikost (delka) fronty
        int m_Zacatek, m_Konec;
// kde je zacatek a konec fronty
        int m_Pocet;
// pocet prvku prave se nachazejicich ve fronte

    public :
        CPrioFronta(int _Velikost);
        ~CPrioFronta();

        bool Vloz(const T& _Vklad, int _iPriorita);
        bool Vyjmi(T& _Vyber);

        bool JePrazdna() { return ((0 == m_Pocet) ? true : false); }
        bool JePlna() { return ((m_Pocet == m_Velikost) ? true : false); }

        void Vypis();
    };

    Vidíme, že deklarace třídy CPrioFronta je shodná s jednoduchou frontou, jen přibylo navíc celočíselné pole pro priority a u metody Vloz() pak přibyl parametr udávající prioritu pro prvek uložený v poli m_Pole se stejným indexem. Pokud vkládáme prvek, pak je zařazen právě podle tohoto čísla, pole priorit je sestupně setříděným polem.

    template<class T> CPrioFronta<T>::CPrioFronta(int _Velikost)
    {
        m_Velikost = -1;
        m_Pocet = 0;
        m_Zacatek = 0;
        m_Konec = -1;
// abychom v metode Vloz dostali spravny konec po prvnim vlozeni

        m_Pole = new T[_Velikost];


        if(m_Pole)
        {
           
// Vytvorime pole priorit pro prvky
            m_Priority = new int[_Velikost];

            if(m_Priority)
            {
               
// pokud se podarilo nastavime velikost
                m_Velikost = _Velikost;
            }
            else
            {
               
// jinak smazeme druhe pole
                delete[] m_Pole;
            }
        }
    }

    V konstruktoru pak navíc vytvoříme pole priorit a pokud by se nepovedlo přidělit paměť, pak smažeme i dříve vytvořené pole pro prvky. Všimněte si, že zde není inicializace pole priorit, vkládání funguje správně i bez něj.

        template<class T> CPrioFronta<T>::~CPrioFronta()
        {
            if(m_Priority)
            {
                delete[] m_Priority;
                m_Priority = NULL;
            }

            if(m_Pole)
            {
                delete[] m_Pole;
                m_Pole = NULL;
                m_Velikost = 0;
            }
        }

        template<class T> bool CPrioFronta<T>::Vloz(const T& _Vklad, int _iPriorita)
        {
            bool bNavrat = false;

            if(m_Pole && m_Priority)
            {
                if(!JePlna())
                {
                    if(m_Konec == (m_Velikost - 1))
                    {
                       
// pokud konec dosahne konce pole, pak koncime na prvnim prvku pole
                        m_Konec = 0;
                    }
                    else
                    {
                        m_Konec++;
                    }

                   
// hledame misto pro novy prvek

                    int zarazpoz = m_Konec;
                    int poz;

                   
// nasledujici promenna znaci jestli je index konce pred zacatkem, musime overit i prazdnost fronty
                    bool bPrekroucena = (m_Konec < m_Zacatek);

                    while((zarazpoz > m_Zacatek) || bPrekroucena)
                    {
                        poz = zarazpoz - 1;

                        if(poz < 0)
                        {
                            poz = m_Velikost - 1;
                        }

                        if(m_Priority[poz] < _iPriorita)
                        {
                            // posuneme prvek s nizsi prioritou
                            m_Pole[zarazpoz] = m_Pole[poz];
                            m_Priority[zarazpoz] = m_Priority[poz];

                           
// a jdeme na dalsi prvek - ovsem kdyz dosahneme zacatku, tak preskocime na konec
                            if(zarazpoz == 0)
                            {
                                zarazpoz = m_Velikost - 1;
                                bPrekroucena = false;
// uz je zacatek pred koncem
                            }
                            else
                            {
                                zarazpoz--;
                            }
                        }
                        else
                        {
                            break;
// prerusime cyklus
                        }
                    }

                   
// na nalezene misto ulozime novy prvek
                    m_Pole[zarazpoz] = _Vklad;
                    m_Priority[zarazpoz] = _iPriorita;
// ulozime prioritu
                    m_Pocet++; // pribyl prvek

                    bNavrat = true;
                }
            }

            return bNavrat;
        }
#endif

    Kód naleznete v sekci Downloads (projekt PrioFronta).

    Destruktor je jasný, jen navíc uvolníme pole priorit. Ovšem metoda Vloz() je upravená. Po kontrole, zda existují obě pole a tedy nedojde k chybnému přístupu do paměti si ověříme, zda můžeme do pole ještě přidat další prvek. Pokud ano, začneme od konce pole určeného indexem m_Konec procházet pole priorit až k prvku, který je určen proměnnou m_Zacatek. Ovšem využíváme cyklického pole a proto se jednoduše může stát, že pole bude překroucené. To znamená, že index m_Konec je menší než m_Zacatek a nebyla by tak splněna podmínka pro cyklus průchodu polem. Proměnná zarazpoz určuje, kam uložíme nový prvek, proměnná poz pak určuje prvek předchozí, přičemž po prvku s indexem nula následuje opět prvek s indexem posledního prvku v poli (tedy m_Velikost - 1). Pokud má prvek na pozici poz menší prioritu nežli prvek vkládaný, pak prvek přesuneme z pozice poz na pozici blíže ke konci našeho cyklického pole (tedy blíže k indexu m_Konec) a porovnáváme prioritu vkládaného prvku s prvkem předcházejícím přesunutému prvku. Tak pokračujeme dokud nenalezneme správnou pozici. Podmínka (zarazpoz == 0) nám řeší problém s překrouceným polem, přeskočí ze začátku fronty na konec fronty a zruší příznak, že pole je překroucené. Indexy jsou pak ve správném pořadí a nic nám nebrání v prozkoumání zbytku pole.

    Ostatní metody (Vyjmi() a Vypis()) jsou shodné jako v předchozí implementaci jednoduché fronty.

    Samozřejmě existují i jiné možnosti implementace, můžeme například vybírat prvky podle priority až v metodě Vyjmi(), stačilo by projít nesetříděné pole priorit a najít prvek s nejvyšší prioritou. Potom bychom ovšem stejně museli posunout všechny prvky obsažené v poli následující vypuštěný prvek. Jinou možností je použít pro vyhledání pozice algoritmu binárního vyhledávání, se kterým jsme se již seznámili. V tomto případě bude pole priorit polem setříděných čísel (ať už vzestupně nebo sestupně), pro nalezení správné pozice. Po nalezení správné pozice bychom odsunuli všechny následující prvky.

7.2 Datové struktury - strom (tree)

    V tomto odstavci si bez implementace povíme o datové struktuře nazvané strom. Strom lze ve speciálním případě implementovat pomocí statického pole. Je založen na tom, že každý prvek, kromě prvního obsahuje odkaz na maximálně n dalších prvků, kteří jsou jeho následovníky. První prvek nazýváme kořenem, další pak uzly a prvky, které nemají žádné následovníky pak listy stromu. Spojnice mezi prvky se označují jako větve. Pokud jste někdy viděli rodokmen, pak strom se graficky znázorňuje stejně:

    My se budeme zabývat stromem binárním, tedy případem, kdy n = 2. Využijeme toho, že pro každý uzel kromě prvního platí: pokud i je index prvku, pro který chceme určit následovníky, pak první následovník leží na pozici 2*i a druhý na 2*i + 1, jak je vidět z obrázku (tyto vztahy platí jen pro n = 2). Stačí tedy alokovat pole o velikosti 2n - 1, kde n udává výšku stromu (počet hladin stromu). Výhodou této implementace je, že známe-li prvek ze stromu, pak jednoduše určíme jeho předchůdce (je to prvek nacházející se na indexu prvku vyděleného dvěma). Ovšem velikou nevýhodou je paměťová náročnost, kterou odstraníme v některém z příštích dílů pomocí dynamické reprezentace. Pokud strom totiž není úplný, tedy nemá obsazeny všechny prvky, pak v poli zbytečně plýtváme pamětí.

7.3 Co bude příště

    Dnes jsme prozatím dokončili datové struktury využívající pole pro ukládání prvků - v některém z dalších dílů se ještě určitě seznámíme s haldou. V dalších kapitolách přejdeme na dynamické struktury - první z nich bude lineární spojový seznam, pomocí kterého budeme implementovat stromy.

Příště nashledanou.

Ondřej Burišin