Binary Tree ili kako pripremiti binarno stablo pretraživanja

Preludij

Ovaj članak posvećen je binarnim stablima pretraživanja. Nedavno sam pisao članak o kompresiji podataka metodom Huffman. Tamo nisam previše obraćao pažnju na binarna stabla, jer metode pretraživanja, umetanja i brisanja nisu bile relevantne. Sad sam odlučio napisati članak upravo o stablima. Pa, da počnemo.

Stablo je struktura podataka koja se sastoji od čvorova povezanih granama. Može se reći da je stablo posebni slučaj grafa. Evo primera stabla:

Binary Tree ili kako pripremiti binarno stablo pretraživanja

To nije binarno stablo pretraživanja! Sve pod kat.

Terminologija

Korijen

Korijen stabla je njegov najgornji čvor. U primeru, to je čvor A. U stablu može postojati samo jedan put od korijena do bilo kojeg drugog čvora! U stvarnosti, svaki čvor se može smatrati korijenom odgovarajućeg podstabla.

Roditelji/ potomci

Svi čvorovi, osim korijena, imaju tačno jednu granju koja vodi prema drugom čvoru. Čvor koji se nalazi iznad trenutnog naziva se roditeljem ovog čvora. Čvor koji se nalazi ispod trenutnog i povezan je s njim, naziva se potomkom ovog čvora. Pogledajmo primer. Uzmimo čvor B, tada će njegov roditelj biti čvor A, a potomci će biti čvorovi D, E i F.

List

Čvor koji nema potomaka naziva se listem stabla. U primeru, listovi će biti čvorovi D, E, F, G, I, J, K.

Ovo je osnovna terminologija. Ostali pojmovi će biti razrađeni kasnije. Tako, binarno stablo je stablo u kojem svaki čvor može imati najviše dva potomka. Kako ste pogodili, stablo iz primera neće biti binarno, jer čvorovi B i H imaju više od dva potomka. Evo primera binarnog stabla:

Binary Tree ili kako pripremiti binarno stablo pretraživanja

U čvorovima stabla može se nalaziti bilo koja informacija. Binarno stablo pretraživanja je binarno stablo koje ima sledeće osobine:

  1. Oba podstabla — levo i desno — su binarna stabla pretraživanja.
  2. Svi čvorovi levog podstabla proizvoljnog čvora X imaju vrednosti ključeva podataka manje od vrednosti ključa podataka samog čvora X.
  3. Svi čvorovi desnog podstabla proizvoljnog čvora X imaju vrednosti ključeva podataka veće ili jednake od vrednosti ključa podataka samog čvora X.

Ključ — neka karakteristika čvora (na primer, broj). Ključ je potreban kako bi se mogla pronaći stavka stabla kojoj odgovara taj ključ. Primer binarnog stabla pretraživanja:

Binary Tree ili kako pripremiti binarno stablo pretraživanja

Predstavljanje stabla

Kako napredujem, navest ću neke (moguće nepotpune) dijelove koda kako bih poboljšao vaše razumijevanje. Cijeli kod bit će na kraju članka.

Stablo se sastoji od čvorova. Struktura čvora:

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;
    }
//...ostali metodi čvora
}

Svaki čvor ima dva potomka (moguće je da potomci leftChild i/ili rightChild sadrže vrijednost null). Vjerojatno ste shvatili da u ovom slučaju data predstavlja podatke pohranjene u čvoru; key je ključ čvora.

O čvorovima smo razgovarali, sada razgovarajmo o hitnim problemima stabala. Ovdje i dalje pod pojmom „stablo“ mislim na pojam binarnog stabla pretraživanja. Struktura binarnog stabla:

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

    //metode stabla
}

Kao polje klase trebat će nam samo korijen stabla, jer se od korijena pomoću metoda getLeftChild() i getRightChild() može doći do bilo kojeg čvora stabla.

Algoritmi u stablu

Pretraživanje

Pretpostavimo da imate izgrađeno stablo. Kako pronaći element s ključem key? Trebate se postupno kretati od korijena prema dolje po stablu i uspoređivati vrijednost key s ključem trenutnog čvora: ako je key manji od ključa trenutnog čvora, pređite na lijevog potomka čvora, ako je veći — na desnog, ako su ključevi jednaki — traženi čvor je pronađen! Odgovarajući kod:

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;
}

Ako current postane jednak null, to znači da je pretraga dostigla kraj stabla (na konceptualnoj razini nalazite se na nepostojećem mjestu stabla — potomku lista).

Razmotrimo učinkovitost algoritma pretraživanja na uravnoteženom stablu (stablo u kojem su čvorovi raspoređeni više-manje ravnomjerno). Tada će učinkovitost pretraživanja biti O(log(n)), pri čemu je logaritam na osnovi 2. Pogledajte: ako u uravnoteženom stablu ima n elemenata, to znači da će biti log(n) na osnovi 2 razina stabla. A u pretraživanju, za jedan korak petlje, silazite na jedan nivo.

Umetanje

Ako ste shvatili suštinu pretraživanja, onda vam umetanje neće biti teško. Samo se treba spustiti do lista stabla (prema pravilima spuštanja opisanima u pretraživanju) i postati njegov potomak — levi ili desni, u zavisnosti od ključa. Implementacija:

   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;
                    }
                }
            }
        }
    }

U ovom slučaju je potrebno, osim trenutnog čvora, čuvati informacije o roditelju trenutnog čvora. Kada current postane null, u varijabli parent će biti potrebni list.
Efikasnost umetanja, očigledno, biće ista kao i kod pretraživanja — O(log(n)).

Brisanje

Brisanje je najteža operacija koju treba sprovesti sa stablom. Jasno je da prvo treba pronaći element koji nameravamo da obrišemo. Ali šta onda? Ako jednostavno dodelimo njegovom referencu vrednost null, izgubićemo informacije o podstablu čiji je ovaj čvor koren. Metode brisanja stabla delimo u tri slučaja.

Prvi slučaj. Brišemo čvor koji nema potomaka

Ako brišemo čvor koji nema potomaka, to znači da je list. Stoga se jednostavno može dodeliti poljima leftChild ili rightChild njegovog roditelja vrednost null.

Drugi slučaj. Brišemo čvor koji ima jednog potomka

Ovaj slučaj takođe nije previše složen. Vratimo se našem primeru. Recimo da treba da obrišemo element sa ključem 14. Slažete se da, pošto je on desni potomak čvora sa ključem 10, bilo koji njegov potomak (u ovom slučaju desni) će imati ključ veći od 10, stoga ga možemo lako „izrezati“ iz stabla, a roditelja direktno povezati sa potomkom brišućeg čvora, tj. čvor sa ključem 10 povezati sa čvorom 13. Slična bi situacija bila da smo trebali obrisati čvor koji je levi potomak svog roditelja. Razmislite o tome sami — tačna analogija.

Treći slučaj. Čvor ima dva potomka

Najkompleksniji slučaj. Razradićemo to na novom primeru.

Binary Tree ili kako pripremiti binarno stablo pretraživanja

Pretraga naslednika

Pretpostavimo da trebamo ukloniti čvor s ključem 25. Koga ćemo postaviti na njegovo mjesto? Netko od njegovih sljedbenika (potomaka ili potomaka potomaka) trebao bi postati nasljednik(onaj tko zauzima mjesto uklonjenog čvora).

Kako shvatiti tko bi trebao postati nasljednik? Intuitivno je jasno da je to čvor u stablu čiji je ključ sljedeći veći od uklonjenog čvora. Algoritam se svodi na sljedeće. Trebamo prijeći na njegovog desnog potomka (uvijek na desnog, jer je već rečeno da je ključ nasljednika veći od ključa uklonjenog čvora), a zatim proći kroz lanac lijevih potomaka tog desnog potomka. U primjeru trebamo prijeći na čvor s ključem 35, a zatim se spustiti do lista kroz lanac njegovih lijevih potomaka — u ovom slučaju, taj lanac se sastoji samo od čvora s ključem 30. Strogo govoreći, tražimo najmanji čvor u skupu čvorova, većih od traženog čvora.

Binary Tree ili kako pripremiti binarno stablo pretraživanja

Kod metode pretraživanja nasljednika:

    public Node<T> getSuccessor(Node<T> deleteNode) {
        Node<T> parentSuccessor = deleteNode; // roditelj nasljednika
        Node<T> successor = deleteNode; // nasljednik
        Node<T> current = successor.getRightChild(); // samo "prolazni" čvor
        while (current != null) {
            parentSuccessor = successor;
            successor = current;
            current = current.getLeftChild();
        }
        // na izlazu iz petlje imamo nasljednika i roditelja nasljednika
        if (successor != deleteNode.getRightChild()) { // ako nasljednik nije isti kao desni potomak uklonjenog čvora
            parentSuccessor.setLeftChild(successor.getRightChild()); // tada njegov roditelj preuzima potomka nasljednika, da ga ne izgubi
            successor.setRightChild(deleteNode.getRightChild()); // povezujemo nasljednika s desnim potomkom uklonjenog čvora
        }
        return successor;
    }

Puni kod metode delete:

public boolean delete(int deleteKey) {
        Node current = root;
        Node parent = current;
        boolean isLeftChild = false; // Ovisno o tome je li čvor koji se uklanja lijevi ili desni potomak svog roditelja, booleanska varijabla isLeftChild će imati vrijednost true ili 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) { // prvi slučaj
            if (current == root)
                current = null;
            else if (isLeftChild)
                parent.setLeftChild(null);
            else
                parent.setRightChild(null);
        }
        else if (current.getRightChild() == null) { // drugi slučaj
            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 { // treći slučaj
            Node successor = getSuccessor(current);
            if (current == root)
                root = successor;
            else if (isLeftChild)
                parent.setLeftChild(successor);
            else
                parent.setRightChild(successor);
        }
        return true;
    }

Vrijednost složenosti može se aproksimirati na O(log(n)).

Pronalaženje maksimuma/minimuma u stablu

Očigledno je da se minimalna/maksimalna vrijednost u stablu može pronaći slijednim prelaskom kroz lanac lijevih/desnih elemenata stabla; kada stignete do lista, on će biti minimalni/maksimalni element.

    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;
    }

Složenost — O(log(n))

Simetrično obilazak

Obilazak — posjetiti svaki čvor stabla s ciljem izvršenja neke radnje nad njim.

Algoritam za rekurzivni simetrični obilazak:

  1. Izvrši radnju nad lijevim potomkom
  2. Izvrši radnju nad sobom
  3. Izvrši radnju nad desnim potomkom

Kod:

    public void inOrder(Node current) {
        if (current != null) {
            inOrder(current.getLeftChild());
            System.out.println(current.getData() + " ");// Ovdje može biti sve, što god
            inOrder(current.getRightChild());
        }
    }

Zaključak

Nap finally! Ako sam nešto nedovoljno objasnio ili imate kakve komentare, čekam vas u komentarima. Kao što sam obećao, donosim puni kod.

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.

Degeneracija do O(n)

Mnogi od vas mogli su primijetiti: što ako napravimo da stablo postane nesmetano? Na primjer, stavljajući u stablo čvorove s rastućim ključevima: 1,2,3,4,5,6… Tada će stablo više ličiti na povezanu listu. I da, stablo će izgubiti svoju drvenastu strukturu, a samim time i učinkovitost pristupa podacima. Složenost operacija pretrage, umetanja i brisanja postat će kao kod povezane liste: O(n). U tome se očituje jedan od najvažnijih, po mom mišljenju, nedostataka binarnih stabala.

Samo registrirani korisnici mogu sudjelovati u anketi. Prijavite se, molimo.

Nisam dugo na habru i volio bih znati o kojim temama biste željeli vidjeti više članaka?

  • Strukture podataka

  • Algoritmi (DP, rekurzija, kompresija podataka itd.)

  • Primjena struktura podataka i algoritama u stvarnom životu

  • Programiranje Android aplikacija u Javi

  • Programiranje web aplikacija u Javi

Glasali su 2 korisnika. Suzdržan 1 korisnik.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster