Bináris fa, avagy hogyan készítsünk egy bináris keresőfát

Előszó

Ez a cikk a bináris keresőfáknak szentelt. Nemrég írtam egy cikket a Huffman-féle adatkompresszióról. Ott nem figyeltem túlságosan a bináris fákra, mert a keresési, beszúrási és törlési módszerek nem voltak aktuálisak. Most úgy döntöttem, hogy kifejezetten a fákról írok. Kezdjük talán.

A fa egy adatstruktúra, amely csúcsokból áll, amelyeket élek kötnek össze. Mondhatjuk, hogy a fa a gráf egy különleges esete. Íme egy példa a fára:

Bináris fa, avagy hogyan készítsünk egy bináris keresőfát

Ez nem bináris keresőfa! Minden a hajtás alatt!

Terminológia

Gyökér

A fa gyökere az a legfelső csúcs. A példában ez az A csúcs. A fából a gyökértől bármely más csúcs felé csak egy út vezethet! Valójában bármely csúcsot úgy is tekinthetünk, mint a hozzá tartozó részfa gyökerét.

Szülők/leszármazottak

Minden csúcs, kivéve a gyökér csúcsot, pontosan egy éllel rendelkezik, amely felfelé vezet egy másik csúcs felé. A jelenlegi csúcs felett elhelyezkedő csúcsot szülőnek nevezik. A jelenlegi csúcs alatt elhelyezkedő csúcs, amely összeköttetésben áll vele, leszármazottnak nevezik. Vegyünk egy példát. Tegyük fel, hogy a B csúcsot nézzük, ebben az esetben a szülője az A csúcs, a leszármazottai pedig a D, E és F csúcsok.

Lapot

A csúcs, amelynek nincsenek leszármazottai, a fa levelének nevezik. A példában a levelek a D, E, F, G, I, J és K csúcsok.

Ez az alapvető terminológia. Más fogalmakat később tárgyalunk. Tehát a bináris fa — egy fa, amelyben minden csúcs legfeljebb két leszármazottal rendelkezik. Ahogy kitalálhattad, a példa fája nem lesz bináris, mivel a B és H csúcsoknak több mint két leszármazottjuk van. Íme egy példa bináris fára:

Bináris fa, avagy hogyan készítsünk egy bináris keresőfát

A fa csúcsában bármilyen információ lehet. A bináris keresőfa egy bináris fa, amelynek a következő tulajdonságai vannak:

  1. Mindkét részfa — a bal és a jobb — bináris keresőfák.
  2. X bármely csúcsának bal részfájában a csúcsok adatkulcsaival rendelkező értékek kisebbek, mint a X csúcs adatkulcsa.
  3. X bármely csúcsának jobb részfájában a csúcsok adatkulcsaival rendelkező értékek nagyobbak vagy egyenlők, mint a X csúcs adatkulcsa.

Kulcs — bármilyen jellemző (például szám). A kulcs arra szolgál, hogy megtaláljuk a fában azt az elemet, amelyhez ez a kulcs tartozik. Példa egy bináris keresőfára:

Bináris fa, avagy hogyan készítsünk egy bináris keresőfát

A fa megjelenítése

Ahogy haladunk előre, bemutatok néhány (lehetségesen nem teljes) kódrészletet, hogy jobban megértsd. A teljes kód a cikk végén lesz.

A fa csúcsokból áll. A csúcs szerkezete:

public class Node<T> {
    private T data;
    private int key;
    private Node<T> leftChild;
    private Node<T> rightChild;

    public Node(T data, int key) {
        this.data = data;
        this.key = key;
    }
    public Node<T> getLeftChild() {
        return leftChild;
    }

    public Node<T> getRightChild() {
        return rightChild;
    }
//...a csúcs további metódusai
}

Minden csúcsnak két leszármazottja van (valószínű, hogy a leftChild és/vagy rightChild értéke null lesz). Valószínűleg megérted, hogy ebben az esetben a data — a csúcsban tárolt adat; key — a csúcs kulcsa.

Megértettük a csúcsot, most beszéljünk a fák aktuális problémáiról. Itt és a továbbiakban a "fa" kifejezés alatt a bináris keresőfa fogalmát értem. A bináris fa struktúrája:

public class BinaryTree<T> {
     private Node<T> root;

    //fa metódusok
}

A osztály mezőjeként csak a fa gyökere szükséges, mivel a gyökérből a getLeftChild() és getRightChild() metódusok használatával elérhetjük a fa bármely csúcsát.

Algoritmusok a fában

Mint mindig, létezik egy weboldal, amely a jó kérdésekkel foglalkozik, és amelynek neve

Tegyük fel, hogy van egy felépített fád. Hogyan találod meg a kulcs key-vel rendelkező elemet? Fokozatosan kell haladni a gyökértől lefelé a fán, és össze kell hasonlítani a key értékét a következő csúcs kulcsával: ha a key kisebb, mint a következő csúcs kulcsa, akkor át kell lépni a csúcs bal leszármazottjára, ha nagyobb, akkor a jobb leszármazottra, ha pedig a kulcsok egyenlőek — a keresett csúcs megtalálva! A megfelelő kód:

public Node<T> find(int key) {
    Node<T> current = root;
    while (current.getKey() != key) {
        if (key < current.getKey())
            current = current.getLeftChild();
        else
            current = current.getRightChild();
        if (current == null)
            return null;
    }
    return current;
}

Ha a current null lesz, akkor a keresés elérte a fa végét (fogalmilag egy nem létező helyen vagy, a levél leszármazottjánál).

Nézzük az algoritmus keresési hatékonyságát egy kiegyensúlyozott fában (abban a fában, ahol a csúcsok viszonylag egyenletesen oszlanak el). Ekkor a keresés hatékonysága O(log(n)), ahol a logaritmus alapja 2. Nézd: ha a kiegyensúlyozott fában n elem van, akkor az azt jelenti, hogy log(n) alapú 2 szint lesz a fában. A keresés során minden ciklus lépésnél egy szinttel lejjebb haladsz.

Beszúrás

Ha megértetted a keresés lényegét, akkor a beszúrás megértése sem okoz nehézséget. Csak le kell menni a fa lapjára (a keresésben leírt leereszkedési szabályok szerint), és ennek megfelelően a bal vagy a jobb utódjává kell válni, attól függően, hogy mi a kulcs. Megvalósítás:

   public void insert(T insertData, int key) {
        Node current = root;
        Node parent;
        Node newNode = new Node(insertData, key);
        if (root == null)
            root = newNode;
        else {
            while (true) {
                parent = current;
                if (key < current.getKey()) {
                    current = current.getLeftChild();
                    if (current == null) {
                         parent.setLeftChild(newNode);
                         return;
                    }
                }
                else {
                    current = current.getRightChild();
                    if (current == null) {
                        parent.setRightChild(newNode);
                        return;
                    }
                }
            }
        }
    }

Ebben az esetben a jelenlegi csomópont mellett a szülő csomópontra vonatkozó információt is meg kell tárolni. Amikor a current null lesz, akkor a parent változóban megtaláljuk a szükséges lapot.
A beszúrás hatékonysága nyilvánvalóan ugyanaz lesz, mint a keresésé — O(log(n)).

Törlés

A törlés a legbonyolultabb művelet, amelyet a fával el kell végezni. Nyilvánvaló, hogy először meg kell találni az elemet, amelyet törölni kívánunk. De mi történik utána? Ha egyszerűen nullát rendelünk az értékéhez, akkor elveszítjük az al-fához tartozó információt, amelynek gyökere ez a csomópont. A fa törlési módszereket három esetre osztják.

Első eset. A törlendő csomópontnak nincsen utódja

Ha a törlendő csomópontnak nincsen utódja, akkor ez azt jelenti, hogy levél. Következésképpen a szülője leftChild vagy rightChild mezőjét egyszerűen null értékre kell állítani.

Második eset. A törlendő csomópontnak van egy utódja

Ez az eset sem túl bonyolult. Térjünk vissza a példánkhoz. Tegyük fel, hogy törölni kell a 14-es kulcsú elemet. Egyetértesz, hogy mivel ez a 10-es kulcsú csomópont jobb utóda, ezért bármely utódja (ebben az esetben a jobb) nagyobb kulccsal fog bírni, mint 10, így könnyen "kivághatjuk" a fából, és a szülőt közvetlenül összekapcsolhatjuk a törölni kívánt gyerekével, azaz a 10-es kulcsú csomópontot összekapcsolhatjuk a 13-as kulcsú csomóponttal. Hasonló lenne a helyzet, ha egy olyan csomópontot kellene törölni, amely a szülője bal utódja. Gondolkodj el ezen magad — pontos analógia.

Harmadik eset. A csomópontnak két utódja van

A legbonyolultabb eset. Vizsgáljuk meg egy új példán.

Bináris fa, avagy hogyan készítsünk egy bináris keresőfát

Az utód keresése

Tegyük fel, el kell távolítanunk a 25-ös kulccsal rendelkező csomópontot. Kit állítunk a helyére? Valakinek a követői közül (utódok vagy az utódok utódai) kell, hogy legyen a jogutód(az a személy, aki elfoglalja az eltávolított csomópont helyét).

Hogyan érthetjük meg, ki kell, hogy legyen a jogutód? Intuitíven világos, hogy ez a fában található csomópont lesz, amelynek a kulcsa a következő legnagyobb az eltávolított csomóponténál. Az algoritmus a következőképpen működik. Először a jobb utódjához kell lépnünk (mindig a jobbra, mert már említettük, hogy a jogutód kulcsa nagyobb, mint az eltávolítandó csomópont kulcsa), majd végig kell mennünk a bal utódok láncán a jobb utódnál. A példánkban a 35-ös kulcsú csomópontra kell lépnünk, majd lefelé kell haladnunk a bal utódok láncán - ebben az esetben ez a lánc csak a 30-as kulcsú csomópontból áll. Szó szerint a keresett csomópontok halmazában a legkisebb csomópontot keressük, amely nagyobb mint a keresett csomópont.

Bináris fa, avagy hogyan készítsünk egy bináris keresőfát

A jogutód keresési módszer kódja:

    public Node<T> getSuccessor(Node<T> deleteNode) {
        Node<T> parentSuccessor = deleteNode; // a jogutód szülője
        Node<T> successor = deleteNode; // jogutód
        Node<T> current = successor.getRightChild(); // egyszerű "bejárás" csomópont
        while (current != null) {
            parentSuccessor = successor;
            successor = current;
            current = current.getLeftChild();
        }
        // kilépéskor a jogutódot és a jogutód szülőjét kapjuk meg
        if (successor != deleteNode.getRightChild()) { // ha a jogutód nem egyezik meg az eltávolítandó csomópont jobb utódjával
            parentSuccessor.setLeftChild(successor.getRightChild()); // akkor a szülője átvállalja a jogutód utódját, hogy ne veszítse el
            successor.setRightChild(deleteNode.getRightChild()); // összekapcsoljuk a jogutódot az eltávolítandó csomópont jobb utódjával
        }
        return successor;
    }

A delete módszer teljes kódja:

public boolean delete(int deleteKey) {
        Node current = root;
        Node parent = current;
        boolean isLeftChild = false; // A változó, hogy a törölt csomópont bal vagy jobb gyermek-e a szülőjének, a logikai változó isLeftChild true vagy false értéket vesz fel ennek megfelelően.
        while (current.getKey() != deleteKey) {
            parent = current;
            if (deleteKey < current.getKey()) {
                current = current.getLeftChild();
                isLeftChild = true;
            } else {
                isLeftChild = false;
                current = current.getRightChild();
            }
            if (current == null)
                return false;
        }

        if (current.getLeftChild() == null && current.getRightChild() == null) { // első eset
            if (current == root)
                current = null;
            else if (isLeftChild)
                parent.setLeftChild(null);
            else
                parent.setRightChild(null);
        }
        else if (current.getRightChild() == null) { // második eset
            if (current == root)
                root = current.getLeftChild();
            else if (isLeftChild)
                parent.setLeftChild(current.getLeftChild());
            else
                current.setRightChild(current.getLeftChild());
        } else if (current.getLeftChild() == null) {
            if (current == root)
                root = current.getRightChild();
            else if (isLeftChild)
                parent.setLeftChild(current.getRightChild());
            else
                parent.setRightChild(current.getRightChild());
        } 
        else { // harmadik eset
            Node successor = getSuccessor(current);
            if (current == root)
                root = successor;
            else if (isLeftChild)
                parent.setLeftChild(successor);
            else
                parent.setRightChild(successor);
        }
        return true;
    }

A komplexitás körülbelül O(log(n))-nek tekinthető.

Maximum/minimum keresése a fában

Nyilvánvaló, hogy a minimum/máximális érték a fában úgy található meg, hogy fokozatosan haladunk a fa bal/jobb elemei mentén; amikor elérjük a levelet, az lesz a minimum/máximális elem.

    public Node getMinimum(Node startPoint) {
        Node current = startPoint;
        Node parent = current;
        while (current != null) {
            parent = current;
            current = current.getLeftChild();
        }
        return parent;
    }

    public Node getMaximum(Node startPoint) {
        Node current = startPoint;
        Node parent = current;
        while (current != null) {
            parent = current;
            current = current.getRightChild();
        }
        return parent;
    }

A komplexitás — O(log(n))

Szimmetrikus bejárás

A bejárás — a fa minden csomópontjának meglátogatása céljából, hogy valamilyen műveletet végezzünk rajta.

A rekuzív szimmetrikus bejárás algoritmus:

  1. Cselekedj a bal gyermekkel
  2. Cselekedj saját magaddal
  3. Cselekedj a jobb gyermekkel

Kód:

    public void inOrder(Node current) {
        if (current != null) {
            inOrder(current.getLeftChild());
            System.out.println(current.getData() + " ");//Itt bármi lehet
            inOrder(current.getRightChild());
        }
    }

Zárás

Végre! Ha valamit nem magyaráztam el jól, vagy bármilyen megjegyzésed van, várom a kommentek között. Ahogy ígértem, itt van a teljes kód.

Node.java:

public class Node {
    private T data;
    private int key;
    private Node leftChild;
    private Node rightChild;

    public Node(T data, int key) {
        this.data = data;
        this.key = key;
    }

    public void setLeftChild(Node newNode) {
        leftChild = newNode;
    }

    public void setRightChild(Node newNode) {
        rightChild = newNode;
    }

    public Node getLeftChild() {
        return leftChild;
    }

    public Node getRightChild() {
        return rightChild;
    }

    public T getData() {
        return data;
    }

    public int getKey() {
        return key;
    }
}

BinaryTree.java:

public class BinaryTree<T> {
    private Node<T> root;

    public Node<T> find(int key) {
        Node<T> current = root;
        while (current.getKey() != key) {
            if (key < current.getKey())
                current = current.getLeftChild();
            else
                current = current.getRightChild();
            if (current == null)
                return null;
        }
        return current;
    }

    public void insert(T insertData, int key) {
        Node<T> current = root;
        Node<T> parent;
        Node<T> newNode = new Node<>(insertData, key);
        if (root == null)
            root = newNode;
        else {
            while (true) {
                parent = current;
                if (key < current.getKey()) {
                    current = current.getLeftChild();
                    if (current == null) {
                         parent.setLeftChild(newNode);
                         return;
                    }
                }
                else {
                    current = current.getRightChild();
                    if (current == null) {
                        parent.setRightChild(newNode);
                        return;
                    }
                }
            }
        }
    }

    public Node<T> getMinimum(Node<T> startPoint) {
        Node<T> current = startPoint;
        Node<T> parent = current;
        while (current != null) {
            parent = current;
            current = current.getLeftChild();
        }
        return parent;
    }

    public Node<T> getMaximum(Node<T> startPoint) {
        Node<T> current = startPoint;
        Node<T> parent = current;
        while (current != null) {
            parent = current;
            current = current.getRightChild();
        }
        return parent;
    }

    public Node<T> getSuccessor(Node<T> deleteNode) {
        Node<T> parentSuccessor = deleteNode;
        Node<T> successor = deleteNode;
        Node<T> current = successor.getRightChild();
        while (current != null) {
            parentSuccessor = successor;
            successor = current;
            current = current.getLeftChild();
        }

        if (successor != deleteNode.getRightChild()) {
            parentSuccessor.setLeftChild(successor.getRightChild());
            successor.setRightChild(deleteNode.getRightChild());
        }
        return successor;
    }

    public boolean delete(int deleteKey) {
        Node<T> current = root;
        Node<T> parent = current;
        boolean isLeftChild = false;
        while (current.getKey() != deleteKey) {
            parent = current;
            if (deleteKey < current.getKey()) {
                current = current.getLeftChild();
                isLeftChild = true;
            } else {
                isLeftChild = false;
                current = current.getRightChild();
            }
            if (current == null)
                return false;
        }

        if (current.getLeftChild() == null && current.getRightChild() == null) {
            if (current == root)
                current = null;
            else if (isLeftChild)
                parent.setLeftChild(null);
            else
                parent.setRightChild(null);
        }
        else if (current.getRightChild() == null) {
            if (current == root)
                root = current.getLeftChild();
            else if (isLeftChild)
                parent.setLeftChild(current.getLeftChild());
            else
                current.setRightChild(current.getLeftChild());
        } else if (current.getLeftChild() == null) {
            if (current == root)
                root = current.getRightChild();
            else if (isLeftChild)
                parent.setLeftChild(current.getRightChild());
            else
                parent.setRightChild(current.getRightChild());
        } 
        else {
            Node<T> successor = getSuccessor(current);
            if (current == root)
                root = successor;
            else if (isLeftChild)
                parent.setLeftChild(successor);
            else
                parent.setRightChild(successor);
        }
        return true;
    }

    public void inOrder(Node<T> current) {
        if (current != null) {
            inOrder(current.getLeftChild());
            System.out.println(current.getData() + " ");
            inOrder(current.getRightChild());
        }
    }
}

P.S.

Degenerálás O(n)-re

Sokan észrevehettétek: mi lenne, ha a fát nem kiegyensúlyozottá tennénk? Például, ha növekvő kulcsokkal (1,2,3,4,5,6…) töltenénk fel a fát: akkor az fa valami olyasmit fog hasonlítani, mint egy láncolt lista. Igen, a fa elveszíti fára jellemző struktúráját, ami miatt csökken az adat-hozzáférési hatékonyság. A keresés, beszúrás és törlés műveletek időbeli bonyolultsága ugyanolyan lesz, mint a láncolt listáknál: O(n). Ez pedig egyike azoknak a legfontosabb hátrányoknak, amik a véleményem szerint a bináris fákhoz kapcsolódnak.

Csak a regisztrált felhasználók vehetnek részt a szavazáson. Kérem, jelentkezzen be., kérem.

Nem régóta vagyok a Hubon, és szeretném tudni, milyen témákban szeretnétek több cikket látni?

  • Adatszerkezetek

  • Algoritmusok (Dinamika Programozás, rekurzió, adatok tömörítése stb.)

  • Adatszerkezetek és algoritmusok alkalmazása a valós életben

  • Android alkalmazások programozása Javában

  • Webalkalmazások programozása Javában

2 felhasználó szavazott. 1 felhasználó tartózkodott.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster