Binarno drevo ali kako pripraviti binarno iskalno drevo

Predigra

Ta članek govori o binarnih iskalnih drevesih. Pred kratkim sem napisal članek o stiskanje podatkov po Huffmanovi metodi. Tam nisem posvečal veliko pozornosti binarnim drevesom, ker metode iskanja, vstavljanja in brisanja niso bile pomembne. Zdaj sem se odločil napisati članek o drevesih. Začnimo.

Drevo je podatkovna struktura, sestavljena iz vozlišč, povezanih z robovi. Lahko rečemo, da je drevo poseben primer grafa. Tukaj je primer drevesa:

Binarno drevo ali kako pripraviti binarno iskalno drevo

To ni binarno iskalno drevo! Vse je razrezano!

Terminologija

koren

Drevesna korenina - to je njegovo najvišje vozlišče. V primeru je to vozlišče A. V drevesu lahko samo ena pot vodi od korena do katerega koli drugega vozlišča! Pravzaprav lahko vsako vozlišče obravnavamo kot koren poddrevesa, ki ustreza temu vozlišču.

Starši/potomci

Vsa vozlišča razen korena imajo točno en rob, ki vodi do drugega vozlišča. Pokliče se vozlišče, ki se nahaja nad trenutnim starš to vozlišče. Pokliče se vozlišče, ki se nahaja pod trenutnim in je z njim povezano potomec to vozlišče. Uporabimo primer. Vzemimo vozlišče B, potem bo njegov nadrejeni vozlišče A, njegovi otroci pa bodo vozlišča D, E in F.

List

Vozlišče, ki nima otrok, se bo imenovalo list drevesa. V primeru bodo listi vozlišča D, E, F, G, I, J, K.

To je osnovna terminologija. O drugih konceptih bomo še razpravljali. Binarno drevo je torej drevo, v katerem vsako vozlišče ne bo imelo več kot dva otroka. Kot ste uganili, drevo iz primera ne bo binarno, ker imata vozlišči B in H več kot dva otroka. Tukaj je primer binarnega drevesa:

Binarno drevo ali kako pripraviti binarno iskalno drevo

Vozlišča drevesa lahko vsebujejo poljubne informacije. Binarno iskalno drevo je binarno drevo, ki ima naslednje lastnosti:

  1. Tako levo kot desno poddrevo sta binarna iskalna drevesa.
  2. Vsa vozlišča levega poddrevesa poljubnega vozlišča X imajo vrednosti podatkovnega ključa manjše od vrednosti podatkovnega ključa samega vozlišča X.
  3. Vsa vozlišča v desnem poddrevesu poljubnega vozlišča X imajo vrednosti podatkovnega ključa, ki so večje ali enake vrednosti podatkovnega ključa samega vozlišča X.

Ključ — katera koli značilnost vozlišča (na primer številka). Ključ je potreben, da lahko najdete element drevesa, ki mu ta ključ ustreza. Primer binarnega iskalnega drevesa:

Binarno drevo ali kako pripraviti binarno iskalno drevo

Drevesni pogled

Ko bomo napredovali, bom zagotovil nekaj (morda nepopolnih) delov kode za izboljšanje vašega razumevanja. Celotna koda bo na koncu članka.

Drevo je sestavljeno iz vozlišč. Struktura vozlišča:

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;
    }
//...остальные методы узла
}

Vsako vozlišče ima dva otroka (povsem možno je, da bodo otroci leftChild in/ali rightChild vsebovali ničelno vrednost). Verjetno ste ugotovili, da so v tem primeru številski podatki podatki, shranjeni v vozlišču; ključ — ključ vozlišča.

Razvrstili smo vozel, zdaj pa se pogovorimo o perečih težavah z drevesi. V nadaljevanju bom z besedo »drevo« mislil na koncept binarnega iskalnega drevesa. Binarna drevesna struktura:

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

    //методы дерева
}

Potrebujemo samo koren drevesa kot polje razreda, ker lahko iz korena z uporabo metod getLeftChild() in getRightChild() pridete do katerega koli vozlišča v drevesu.

Algoritmi v drevesu

iskanje

Recimo, da imate izdelano drevo. Kako najti element s ključnim ključem? Zaporedoma se morate premakniti od korena navzdol po drevesu in primerjati vrednost ključa s ključem naslednjega vozlišča: če je ključ manjši od ključa naslednjega vozlišča, pojdite na levega potomca vozlišča, če je večji, na desno, če sta ključa enaka, je želeno vozlišče najdeno! Ustrezna koda:

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

Če tok postane nič, je iskanje doseglo konec drevesa (na konceptualni ravni ste na neobstoječem mestu v drevesu - potomec lista).

Oglejmo si učinkovitost iskalnega algoritma na uravnoteženem drevesu (drevo, v katerem so vozlišča porazdeljena bolj ali manj enakomerno). Potem bo učinkovitost iskanja O(log(n)), logaritem pa osnova 2. Poglejte: če je v uravnoteženem drevesu n elementov, potem to pomeni, da bo log(n) na ravni osnove 2 drevo. In pri iskanju se v enem koraku cikla spustite eno raven nižje.

vstavite

Če dojamete bistvo iskanja, vam razumevanje vstavljanja ne bo težko. Samo spustiti se morate na list drevesa (v skladu s pravili spuščanja, opisanimi v iskanju) in postati njegov potomec - levo ali desno, odvisno od ključa. Izvedba:

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

V tem primeru je poleg trenutnega vozlišča potrebno shraniti tudi podatke o staršu trenutnega vozlišča. Ko tok postane enak nič, bo nadrejena spremenljivka vsebovala list, ki ga potrebujemo.
Učinkovitost vstavljanja bo očitno enaka kot pri iskanju - O(log(n)).

Brisanje

Odstranjevanje je najtežji poseg, ki ga bo treba izvesti na drevesu. Jasno je, da bomo morali najprej poiskati element, ki ga bomo izbrisali. Toda kaj potem? Če njegovo referenco preprosto nastavimo na nič, bomo izgubili informacije o poddrevesu, katerega koren je to vozlišče. Metode odstranjevanja dreves so razdeljene na tri primere.

Prvi primer. Vozlišče, ki se briše, nima otrok

Če vozlišče, ki ga brišete, nima otrok, potem to pomeni, da je list. Zato lahko preprosto nastavite polja leftChild ali rightChild njegovega nadrejenega elementa na nič.

Drugi primer. Vozlišče, ki ga želite izbrisati, ima enega podrejenega

Tudi ta primer ni zelo zapleten. Vrnimo se k našemu primeru. Recimo, da moramo izbrisati element s ključem 14. Strinjamo se, da ker je to desni potomec vozlišča s ključem 10, bo imel kateri koli od njegovih potomcev (v tem primeru desni) ključ, večji od 10, tako da ga lahko preprosto "izreže" iz drevesa in nadrejenega poveže neposredno s podrejenim vozliščem, ki se briše, tj. povežite vozlišče s ključem 10 na vozlišče 13. Situacija bi bila podobna, če bi bilo treba izbrisati vozlišče, ki je levi otrok svojega nadrejenega. Pomislite sami - natančna analogija.

Tretji primer. Vozlišče ima dva otroka

Najtežji primer. Poglejmo nov primer.

Binarno drevo ali kako pripraviti binarno iskalno drevo

Iskanje naslednika

Recimo, da moramo izbrisati vozlišče s ključem 25. Koga naj postavimo na njegovo mesto? Eden od njegovih sledilcev (potomcev ali potomcev potomcev) mora postati naslednik(tisti, ki bo prevzel mesto odstranjenega vozlišča).

Kako razumeti, kdo naj postane naslednik? Intuitivno je to vozlišče v drevesu, katerega ključ je naslednji največji od vozlišča, ki se briše. Algoritem je naslednji. Morate iti do njegovega desnega potomca (vedno do desnega, ker je bilo že rečeno, da je ključ naslednika večji od ključa vozlišča, ki se briše), nato pa iti skozi verigo levih potomcev tega desnega potomca . V primeru bi šli do vozlišča s ključem 35 in se nato spustili do lista skozi verigo njegovih levih otrok - v tem primeru je ta veriga sestavljena samo iz vozlišča s ključem 30. Strogo gledano iščemo za najmanjše vozlišče v nizu vozlišč, večje od tistega, ki ga iščemo vozlišče.

Binarno drevo ali kako pripraviti binarno iskalno drevo

Koda metode iskanja naslednika:

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

Celotna koda za metodo brisanja:

public boolean delete(int deleteKey) {
        Node<T> current = root;
        Node<T> parent = current;
        boolean isLeftChild = false;//В зависимости от того, является ли  удаляемый узел левым или правым потомком своего родителя, булевская переменная isLeftChild будет принимать значение true или 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;
    }

Kompleksnost je mogoče približati na O(log(n)).

Iskanje maksimuma/minimuma v drevesu

Očitno je, kako najti najmanjšo/največjo vrednost v drevesu - morate se zaporedno premikati po verigi levih/desnih elementov drevesa; ko pridete do lista, bo to najmanjši/največji element.

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

Kompleksnost - O(log(n))

Simetrična obvodnica

Traversal obišče vsako vozlišče drevesa, da z njim izvede nekaj dejanj.

Algoritem rekurzivnega simetričnega prehoda:

  1. Izvedite dejanje na levem otroku
  2. Naredite akcijo sami s seboj
  3. Izvedite dejanje na pravem otroku

Koda:

    public void inOrder(Node<T> current) {
        if (current != null) {
            inOrder(current.getLeftChild());
            System.out.println(current.getData() + " ");//Здесь может быть все, что угодно
            inOrder(current.getRightChild());
        }
    }

Zaključek

Končno! Če nisem ničesar pojasnil ali imate kakršne koli pripombe, jih pustite v komentarjih. Kot obljubljeno, nudim celotno kodo.

Node.java:

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 void setLeftChild(Node<T> newNode) {
        leftChild = newNode;
    }

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

    public Node<T> getLeftChild() {
        return leftChild;
    }

    public Node<T> 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());
        }
    }
}

PS

Degeneracija na O(n)

Mnogi od vas ste morda opazili: kaj pa, če naredite drevo neuravnoteženo? V drevo na primer postavite vozlišča z naraščajočimi ključi: 1,2,3,4,5,6 ... Potem bo drevo nekoliko podobno povezanemu seznamu. In ja, drevo bo izgubilo svojo drevesno strukturo in s tem učinkovitost dostopa do podatkov. Kompleksnost operacij iskanja, vstavljanja in brisanja bo postala enaka kot pri povezanem seznamu: O(n). To je po mojem mnenju ena najpomembnejših pomanjkljivosti binarnih dreves.

V anketi lahko sodelujejo samo registrirani uporabniki. Prijaviti se, prosim.

Nisem bil dolgo na vozlišču in rad bi vedel, katere članke o katerih temah bi radi videli več?

  • Podatkovne strukture

  • Algoritmi (DP, rekurzija, stiskanje podatkov itd.)

  • Uporaba podatkovnih struktur in algoritmov v realnem življenju

  • Programiranje aplikacij za Android v Javi

  • Programiranje spletnih aplikacij v Javi

Glasovali so 2 uporabniki. 1 uporabnik se je vzdržal.

Vir: www.habr.com

Dodaj komentar