C/C++ & Visual C++

DatovΘ struktury a algoritmy (11.)

┌vodem  |  DatovΘ struktury |  Kurz DirectX  |  Downloads  |  Otßzky a odpov∞di

Uplynul dalÜφ m∞sφc a je tu tedy dalÜφ dφl kurzu o datov²ch strukturßch. Minule jsme dokonΦili implementaci obousm∞rnΘho spojovΘho seznamu, mohli jsme tedy nap°φklad na tento seznam pou₧φt, narozdφl od jeho jednosm∞rnΘho brat°φΦka, algoritmy t°φd∞nφ bublinkovou metodou a dalÜφ. Dnes se seznßmφme s dalÜφ strukturou, kterß najde svΘ uplatn∞nφ v mnoha oblastech programovßnφ. Touto strukturou je strom. My se budeme nejprve zab²vat obecn²m stromem a pak si ukß₧eme n∞kterΘ specializovan∞jÜφ formy, nap°φklad binßrnφ vyhledßvacφ strom.

11.1 Pojmy a zßklady

Stromy jsou, podobn∞ jako lineßrnφ spojovΘ seznamy, slo₧eny z uzl∙ (angl. nodes). Tyto uzly jednak obsahujφ data a pak obsahujφ takΘ ukazatele na dalÜφ uzly. Tφm nßm vznikß rekurzivnφ datovß struktura. Ukß₧eme si grafickou interpretaci obecnΘho stromu:

Pokud si obrßzek obrßtφte, pak uvidφte proΦ tuto datovou strukturu naz²vßme stromem. Uvedeme si n∞kterΘ pojmy, kterΘ se vyskytujφ ve spojitosti se stromy:

  • ko°en (angl. root) - je poΦßteΦnφm uzlem stromu a je v₧dy jen jeden. Lze takΘ definovat jako uzel, kter² nemß rodiΦe. V naÜem obrßzku to je tedy uzel oznaΦen² 1

  • potomek (angl. child) - pokud k n∞kterΘmu uzlu vede Φßra z uzlu nad nφm (ten je na ni₧Üφ hladin∞, jak se povφme nφ₧e), pak je tento prvek potomkem prvku z kterΘho vede Φßra. Nap°φklad uzly 2, 3, 4 jsou potomky uzlu (ko°enu) 1

  • rodiΦ (angl. parent) - uvedeme p°φklad - rodiΦem prvku 3 je uzel 1

  • list (ang. leaf) - uzel, kter² nemß potomka naz²vßme listem. V naÜem diagramu jsou to uzly 5, 3, 6 a 7

  • v∞tev (branch) - spojnice dvou uzl∙

Prvky, kterΘ nejsou listy se takΘ n∞kdy oznaΦujφ jako vnit°nφ uzly stromu a listy se n∞kdy oznaΦujφ jako koncovΘ uzly.

Typ stromu je pak urΦen tφm, kolik potomk∙ majφ jednotlivΘ uzly. OznaΦme n poΦet potomk∙ uzlu, pak pro:

  • n = 2 - binßrnφ strom

  • n = 3 - ternßrnφ strom

  • n = 4 - quadtree

Pokud polo₧φme n = 1, vidφme, ₧e datovou strukturou, kterou dostaneme je jednosm∞rn² lineßrnφ spojov² seznam.

DalÜφ d∙le₧itou v∞cφ, kterou vidφme v diagramu je, ₧e ka₧d² uzel stromu vlastn∞ tvo°φ nov² strom - °φkßme mu podstrom. StaΦφ vzφt uzel 4 a dostßvßme strom s ko°enem 4 a jeho dv∞ma potomky - uzly 6 a 7.

Pokud bychom cht∞li, a v naÜφ implementaci se nßm to hodilo, pak je mo₧nΘ do ka₧dΘho uzlu navφc ulo₧it i ukazatel na rodiΦe tohoto uzlu, co₧ by v n∞kter²ch p°φpadech ulehΦilo pr∙chod stromem.

Spojov² seznam byla vlastn∞ Φßra, na kterΘ byly ulo₧eny jednotlivΘ uzly - ostatn∞ proto jsme mu takΘ °φkali lineßrnφ. Narozdφl od toho, u strom∙ zavßdφme hladiny. Vφce nßm o tom povφ obrßzek:

Na 0-tΘ hladin∞ le₧φ pouze ko°en a vÜichni jeho potomci majφ hladinu o jedna vyÜÜφ. Jako poΦßteΦnφ hladinu jsme si zvolili 0, nebo¥ pole nßm v naÜem oblφbenΘm C/C++ zaΦφnajφ takΘ od nuly.

11.2 ObecnΘ stromy

To jsou stromy, jejich₧ uzly ukazujφ na nekoneΦn² poΦet dalÜφch uzl∙. A₧ si prohlΘdnete obrßzek, uvidφte, ₧e se v ₧ivot∞ pou₧φvajφ celkem Φasto:

Jak takov²to strom, hlavn∞ s ohledem na nekoneΦn² poΦet potomk∙ jednotliv²ch uzl∙ realizovat? JednoduÜe, staΦφ si na pomoc vzφt spojov² seznam a do ka₧dΘho uzlu stromu umφstit jen odkaz na prvnφho potomka uzlu. Samoz°ejm∞ to nenφ jedinß mo₧nost, lze pou₧φt i dynamicky se zv∞tÜujφcφ pole nebo jinou strukturu. Uzly tedy budou v tomto p°φpad∞ obsahovat dva ukazatele - ukazatel na prvnφho potomka a na uzly na stejnΘ hladin∞ (dalo by se °φci na sourozence). To ovÜem krom∞ ko°ene, kter² bude mφt ukazatel na sourozence s hodnotou NULL.

V reßlnΘm problΘmu se nßm m∙₧e takov²to obecn² strom hodit t°eba k rozhodovßnφ um∞lΘ inteligence poΦφtaΦovΘ hry. V uzlu by byla nap°φklad otßzka, zda postava vidφ ·toΦnφka, mo₧nosti by byly ano/ne. V²hodou pak je, ₧e vybrßnφm nap°. mo₧nosti ano vylouΦφme vÜechny otßzky nßsledujφcφ v podstrom∞ ne, Φφm₧ se vyhneme zkoumßnφ mnoha neperspektivnφch otßzek a tedy i ·spo°e Φasu.

Zpracovßvat stromy rekurzivn∞ m∙₧eme celkem t°emi zp∙soby. Zpracovßnφm rozumφme provedenφ urΦitΘ operace nade vÜemi uzly stromu. U obecn²ch strom∙ se pou₧φvajφ dv∞ metody, kterΘ nßm zajistφ pr∙chod vÜemi uzly stromu - jsou to pre-order a post-order (t°etφ metoda se naz²vß in-order). O funkci vÜech t∞chto t°φ metod se zmφnφme pozd∞ji.

11.3 Binßrnφ stromy a binßrnφ vyhledßvacφ strom

My se nynφ budeme zab²vat a takΘ implementujeme strom, kter² se pou₧φvß velice Φasto. Je to p°φpad stromu pro n = 2, tedy ka₧d² uzel stromu bude ukazovat na dva dalÜφ uzly - oznaΦme si je t°eba lev² a prav². Datov²m prvkem kter² budeme vklßdat do stromu pak bude Φasto hodnota typu int. Pro vklßdßnφ prvku si zavedeme pravidlo, ₧e pokud je prvek menÜφ ne₧li hodnota v uzlu, pak prvek ulo₧φme do levΘ v∞tve uzlu. V opaΦnΘm p°φpad∞ do pravΘ. Pokud prvek bude ve strom∞ ji₧ obsa₧en, nestane se nic. Strom, kter² spl≥uje tyto specißlnφ podmφnky naz²vßme prßv∞ binßrnφ vyhledßvacφ strom a slou₧φ k rychlΘmu zjiÜt∞nφ, zda je hodnota ve strom∞ ulo₧ena Φi ne. JinΘ u₧itΘ pro binßrnφ stromy je pak reprezentace aritmetickΘho v²razu v pam∞ti poΦφtaΦe, pop°. k implementaci datovΘ struktury nazvanΘ halda, se kterou se setkßme v n∞kterΘm z dalÜφch dφl∙. N∞kdy lidΘ oznaΦujφ binßrnφ stromy jako B-stromy, co₧ ale nenφ sprßvnΘ. B-strom je ·pln∞ jinß datovß struktura, kterß nachßzφ uplatn∞nφ t°eba v souborovΘm systΘmu.

Implementujeme si tedy takov² binßrnφ strom:

#ifndef BINSTROM_H
    #define BINSTROM_H

    template<class T> class CUzel {
    private:
        T m_Data;
        CUzel<T> *m_lpLevy;
        CUzel<T> *m_lpPravy;
    public:
        CUzel() : m_lpLevy(NULL), m_lpPravy(NULL) { }
        CUzel(const T& _Data) : m_Data(_Data), m_lpLevy(NULL), m_lpPravy(NULL) { }

        T VratData() const { return m_Data; }
        CUzel<T>* &VratLevy() { return m_lpLevy; }
        CUzel<T>* &VratPravy() { return m_lpPravy; }
        void NastavLevy(CUzel<T> *_lpLevy) { m_lpLevy = _lpLevy; }
        void NastavPravy(CUzel<T> *_lpPravy) { m_lpPravy = _lpPravy; }
        void NastavData(const T& _Data) { m_Data = _Data; }
    };

    template<class T> class CBinStrom {
    private:
        CUzel<T> *m_lpKoren;
    public:
        CBinStrom() : m_lpKoren(NULL) { }
        ~CBinStrom() { if(!JePrazdny()) { SmazStrom(m_lpKoren); } }

        bool VytvorPrazdny();

        bool SmazStrom(CUzel<T>* &_lpUzel);

        bool JePrazdny();

        bool Vloz(const T& _Data);
// vklada do stromu
        bool VlozDoStromu(CUzel<T>* &_lpUzel, const T& _Data);

        CUzel<T>* Najdi(const T& _Data);
// vrati ukazatel na nalezeny prvek nebo NULL
        CUzel<T>* NajdiVeStromu(CUzel<T> *_lpUzel, const T& _Data);

        bool Smaz(const T& _Data);
// smaze data
    };

A nynφ k jednotliv²m metodßm. Jako prvnφ vezmeme metodu pro zjiÜt∞nφ, zda je strom prßzdn², co₧ nastane jen je-li ukazatel na ko°en NULL:

    template<class T> bool CBinStrom<T>::JePrazdny()
    {
        return (m_lpKoren == NULL);
    }

Nynφ metody pro novou inicializaci stromu a tedy i smazßnφ stromu:

    template<class T> bool CBinStrom<T>::VytvorPrazdny()
    {
        bool bNavrat = false;

        if(!JePrazdny())
        {
            bNavrat = SmazStrom(m_lpKoren);
// smazeme puvodni strom
        }

       
// nova inicializace promennych se provede jiz v metode SmazStrom()

        return bNavrat;
    }

    template<class T> bool CBinStrom<T>::SmazStrom(CUzel<T>* &_lpUzel)
// budeme menit ukazatel
    {
        bool bNavrat = false;

        if(_lpUzel)
        {
            SmazStrom(_lpUzel->VratLevy());
            SmazStrom(_lpUzel->VratPravy());

            cout << "Mazu " << (_lpUzel)->VratData() << endl;

            delete (_lpUzel);
            _lpUzel = NULL;

            bNavrat = true;
        }

        return bNavrat;
    }

Vidφme, ₧e mazßnφ stromu je rekurzivnφ zßle₧itost. Prost∞ sma₧eme levou podv∞tev, pravou podv∞tev a nakonec vlastnφ uzel. Pro jistotu nastavφme uzlu hodnotu NULL. Proto nestaΦφ p°edßvat jako parametr ukazatel - nemohli bychom ho toti₧ v t∞le funkce m∞nit.

Nynφ se podφvßme na vklßdßnφ prvku do stromu, kterΘ musφ probφhat podle naÜich pravidel - prvky menÜφ ne₧li prvek v uzlu jdou do levΘho podstromu, prvky v∞tÜφ do pravΘho. Prvky nejsou v seznamu obsa₧eny vφcekrßt ne₧ jednou. Metody budou dv∞, jedna kterß vykonß veÜkerou prßci. Ta umφstφ prvek na sprßvnΘ mφsto ve strom∞ pod prvkem, kter² ji p°edßme jako parametr. Druhß funkce pak u₧ jen zajistφ vlo₧enφ prvku do celΘho stromu:

    template<class T> bool CBinStrom<T>::VlozDoStromu(CUzel<T>* &_lpUzel, const T& _Data)
    {
        bool bNavrat = false;

       
// budeme hledat spravne misto pro umisteni uzlu a ukladat si posledni nadrazeny prvek
        CUzel<T> *lpRodic = NULL, *lpNovy = _lpUzel;

        while(lpNovy != NULL)
        {
            if(lpNovy->VratData() == _Data) { return true; }
// nalezneme-li prvek, pak koncime
            else
            {
                if(lpNovy->VratData() > _Data)
                {
                    lpRodic = lpNovy;
                    lpNovy = lpNovy->VratLevy();
                }
                else
                {
                    lpRodic = lpNovy;
                    lpNovy = lpNovy->VratPravy();
                }
            }
        }

       
// nyni mame misto pro novy prvek a tak ho vytvorime
        lpNovy = new CUzel<T>(_Data);

        if(lpNovy)
        {
            if(lpRodic == NULL)
            {
                _lpUzel = lpNovy;
// znaci, ze neexistoval prvni uzel

                bNavrat = true;
            }
            else
            {
                if(lpRodic->VratData() > _Data)
                {
                    lpRodic->NastavLevy(lpNovy);
// zapojime doleva
                }
                else
                {
                    lpRodic->NastavPravy(lpNovy);
// zapojime doprava
                }

                bNavrat = true;
            }
        }

        return bNavrat;
    }

    template<class T> bool CBinStrom<T>::Vloz(const T& _Data)
    {
        return VlozDoStromu(m_lpKoren, _Data);
    }

V t∞le funkce op∞t m∞nφme ukazatel p°edan² jako argument, tak₧e pou₧φvßme op∞t referenci na ukazatel. Tato funkce jde samoz°ejm∞, vzhledem k vlastnostem strom∙, napsat takΘ rekurzivn∞. K tomu se dostaneme jeÜt∞ v p°φÜtφm dφlu.

A nynφ metoda pro nalezenφ hodnoty v tomto vyhledßvacφm strom∞:

    template<class T> CUzel<T>* CBinStrom<T>::NajdiVeStromu(CUzel<T> *_lpUzel, const T& _Data)
    {
        CUzel<T> *lpRet = NULL;

        if(_lpUzel)
        {
            if(_lpUzel->VratData() == _Data)
            {
                return _lpUzel;
            }
            else
            {
                if(_lpUzel->VratData() < _Data)
                {
                    lpRet = NajdiVeStromu(_lpUzel->VratPravy(), _Data);
                }
                else
                {
                    lpRet = NajdiVeStromu(_lpUzel->VratLevy(), _Data);
                }
            }
        }

        return lpRet;
    }

    template<class T> CUzel<T>* CBinStrom<T>::Najdi(const T& _Data)
    {
        return NajdiVeStromu(m_lpKoren, _Data);
    }

#endif

    K≤d naleznete v sekci Downloads (projekt Strom).

11.4 Co bude p°φÜt∞

Tak dnes jsme si ukßzali zßkladnφ implementaci stromu a p°φÜt∞ si ukß₧eme n∞kterΘ nev²hody tΘto implementace. Povφme si o n∞kter²ch vlastnostech strom∙ jako je vyvß₧enost a ·plnost stromu. TakΘ jsme nakousli prochßzenφ vÜemi uzly stromu, ke kterΘmu se p°φÜt∞ vrßtφme a ukß₧eme si jak strom prochßzet do hloubky. U dalÜφho dφlu nashledanou.

Ond°ej BuriÜin a Ji°φ Formßnek