Binaarne puu ehk kuidas valmistada binaarset otsingupuud

PrelĂŒĂŒd

See artikkel kĂ€sitleb binaarseid otsingupuusid. Hiljuti kirjutasin artikli Huffman'i meetodil andmete tihendamisest. Seal ei pööranud ma palju tĂ€helepanu binaarsetele puudele, kuna otsimise, lisamise ja kustutamise meetodid polnud aktuaalsed. NĂŒĂŒd otsustasin kirjutada artikli just puude kohta. Alustame siis.

Puud on andmestruktuurid, mis koosnevad sĂ”lmedest, mis on ĂŒhendatud servadega. VĂ”ib öelda, et puu on graafi erijuht. Siin on puu nĂ€ide:

Binaarne puu ehk kuidas valmistada binaarset otsingupuud

See ei ole binaarne otsingupuu! KÔik allpool!

Terminoloogia

Juurepunkt

Puulehe juurepunkt on selle kĂ”ige ĂŒlemine sĂ”lm. NĂ€ites on see sĂ”lm A. Puust juurepunktist vĂ”ib iga teise sĂ”lmeni viia ainult ĂŒks tee! Tegelikult saab ĂŒhtegi sĂ”lme kĂ€sitleda vastava alampuu juurepunktina.

Vanemad/ajalugu

KĂ”ik sĂ”lmed, vĂ€lja arvatud juurepunkt, omavad tĂ€pselt ĂŒhte serva, mis viib ĂŒlespoole teise sĂ”lmeni. SĂ”lm, mis asub praegusest kĂ”rgemal, nimetatakse vanemaks selle sĂ”lme jaoks. SĂ”lm, mis asub praegusest allpool ja on sellega ĂŒhendatud, nimetatakse jĂ€reltulijaks selle sĂ”lme jaoks. VĂ”tame nĂ€iteks sĂ”lme B, siis tema vanem on sĂ”lm A ja jĂ€reltulijad on sĂ”lmed D, E ja F.

Leht

Puu sÔlmed, millel ei ole jÀrgnenud sÔlmi, nimetatakse puu lehtedeks. Antud nÀites on lehtedeks sÔlmed D, E, F, G, I, J, K.

See on pÔhiterminoloogia. Teised mÔisted kÀsitletakse hiljem. Nii on binaarne puu puu, millel igal sÔlmel on mitte rohkem kui kaks jÀrglast. Nagu arvatavasti teate, ei ole antud puu binaarne, kuna sÔlmed B ja H omavad rohkem kui kahte jÀrglast. Siin on nÀide binaarsest puust:

Binaarne puu ehk kuidas valmistada binaarset otsingupuud

Puu sÔlmedes vÔib paikneda mis tahes teave. Binaarne otsingu puu on binaarne puu, millel on jÀrgmised omadused:

  1. MĂ”lemad alampuud — vasak ja parem — on binaarsed otsingu puud.
  2. KÔik vasakpoolse alampuu sÔlmed mistahes sÔlme X puhul on andmekohtade vÔtmete vÀÀrtused vÀiksemad kui sÔlme X enda vÔtme vÀÀrtus.
  3. KÔik parempoolse alampuu sÔlmed mistahes sÔlme X puhul on andmekohtade vÔtmete vÀÀrtused suuremad vÔi vÔrdsed kui sÔlme X enda vÔtme vÀÀrtus.

Ala — mĂ”ni sĂ”lme omadus (nĂ€iteks number). VĂ”ti on vajalik, et leida puu element, millele see vĂ”ti vastab. NĂ€ide binaarsest otsingu puust:

Binaarne puu ehk kuidas valmistada binaarset otsingupuud

Puu esitus

Etenemisprotsessis toon ma vÀlja mÔned (vÔimalikult mitte tÀielikud) koodilÔigud, et aidata teil paremini mÔista. TÀielik kood on artikli lÔpus.

Puu koosneb sÔlmedest. SÔlme struktuur:

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 Node getLeftChild() {
        return leftChild;
    }

    public Node getRightChild() {
        return rightChild;
    }
//...muud sÔlme meetodid
}

Igal sÔlmel on kaks last (tÔenÀoliselt vÔivad leftChild ja/vÔi rightChild sisaldada vÀÀrtust null). Olete ilmselt aru saanud, et antud juhul number data on sÔlmes hoitavad andmed; key on sÔlme vÔti.

NĂŒĂŒd, kui oleme sĂ”lmega tutvunud, rÀÀgime puude pĂ€evakajalistest probleemidest. Siin ja edaspidi mĂ”istan sĂ”na "puu" all binaarse otsingu puu mĂ”istet. Binaarse puu struktuur:

public class BinaryTree {
     private Node root;

    //puu meetodid
}

Klassi vÀljana vajame ainult puu juuri, kuna juuri kaudu on vÔimalik meetodite getLeftChild() ja getRightChild() abil pÀÀseda iga puu sÔlmeni.

Algoritmid puus

Otsi

Oletame, et teil on loodud puu. Kuidas leida elementi, millel on vĂ”ti key? Tuleb jĂ€rk-jĂ€rgult liikuda juurest allapoole puu ja vĂ”rrelda vÀÀrtust key jĂ€rgmise sĂ”lme vĂ”tmega: kui key on vĂ€iksem kui jĂ€rgmise sĂ”lme vĂ”ti, siis liikuda sĂ”lme vasakule alampuudule, kui suurem — paremale, kui vĂ”tmed on vĂ”rdsed — otsitav sĂ”lm on leitud! Vastav kood:

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

Kui current muutub nulliks, tĂ€hendab see, et lĂ€bimine on jĂ”udnud puu lĂ”ppu (kontseptuaalses mĂ”ttes viibite te puudulikkuse koha — lehes oleva jĂ€rglase juures).

Vaadakem otsingu algoritmi efektiivsust tasakaalustatud puus (puus, kus sĂ”lmed on jaotatud enam-vĂ€hem ĂŒhtlaselt). Seega on otsingu efektiivsus O(log(n)), kusjuures logaritm on alus 2. NĂ€iteks: kui tasakaalustatud puus on n elementi, tĂ€hendab see, et puu taset on log(n) alusel 2. Ja otsingus, ĂŒhe tsĂŒkli sammu jooksul, laskute ĂŒhe taseme vĂ”rra.

Sisestamine

Kui olete otsimise mĂ”tte tabanud, siis ei tohiks lisamine teile raskusi valmistada. Tuleb lihtsalt laskuda puu lehte (langemise reeglite kohaselt, nagu on kirjas otsingus) ja saada selle jĂ€reltulijaks — vasakuks vĂ”i paremaks, sĂ”ltuvalt vĂ”tmest. Teostus:

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

Sellisel juhul tuleb lisaks praegusele sÔlmele hoida teavet ka praeguse sÔlme vanema kohta. Kui current saab nulliks, sisaldab muutuja parent vajalikku lehte.
Sisestamise efektiivsus on kindlasti sama mis otsimise puhul — O(log(n)).

Kustuta

Kustutamine on kÔige keerulisem toiming, mida puu juures teha tuleb. Esiteks tuleb leida element, mille me kavatseme eemaldada. Aga mis siis edasi? Kui lihtsalt mÀÀrata selle viide vÀÀrtusele null, kaotame me teabe selle alampuu kohta, mille juureks on see sÔlm. Puu eemaldamise meetodid jagunevad kolme juhtumi vahel.

Esimene juhtum. Eemaldatav sÔlm ei oma jÀreltulijaid.

Kui eemaldatav sÔlm ei oma jÀreltulijaid, siis tÀhendab see, et ta on leht. Seega vÔib lihtsalt mÀÀrata tema vanema leftChild vÔi rightChild vÀljadele vÀÀrtuse null.

Teine juhtum. Eemaldatav sĂ”lm omab ĂŒhte jĂ€reltulijat.

See juht ei ole ka eriti keeruline. Naaseme meie nĂ€ite juurde. Oletame, et peame eemaldama elemendi, mille vĂ”tme number on 14. NĂ”ustute, et kuna see on parempoolne jĂ€reltulija sĂ”lme, mille vĂ”tme number on 10, siis igal tema jĂ€reltulijal (antud juhul parempoolsel) on kindlasti vĂ”tme number, mis on suurem kui 10. Seega saab selle lihtsasti „vĂ€lja lĂ”igata“ puust, ning vanemate sĂ”lme saab otse ĂŒhendada eemaldatava sĂ”lme jĂ€reltulijaga, st sĂ”lm, mille vĂ”tme number on 10, saab ĂŒhendatud sĂ”lmega 13. Sarnane olukord oleks ka siis, kui peaksime eemaldama sĂ”lme, mis on tema vanema vasak jĂ€reltulija. MĂ”elge sellele ise — tĂ€pselt analoogne.

Kolmas juhtum. SÔlm omab kahte jÀreltulijat.

Õige keerulisem juhtum. Lahkame selle uue nĂ€ite pĂ”hjal.

Binaarne puu ehk kuidas valmistada binaarset otsingupuud

JĂ€reltulija otsimine.

Oletame, et peame eemaldama sÔlme, mille vÔtme number on 25. Keda paneme tema asemele? Keegi tema jÀrgmistest (jÀreltulijatest vÔi jÀreltulijate jÀreltulijatest) peaks saama jÀreltulijaks.(see, kes asendab eemaldatavat sÔlme).

Kuidas mĂ”ista, kes peab olema jĂ€rglane? Intuitiivselt on selge, et see on puu sĂ”lm, mille vĂ”ti on eemaldatava sĂ”lme jĂ€rgmiseks suuruseks. Algoritm on jĂ€rgmine. Tuleb minna selle paremale jĂ€reltulijale (aina paremale, sest on juba öeldud, et jĂ€rglase vĂ”ti on suurem eemaldatava sĂ”lme vĂ”tmel), ja siis liikuda selle parema jĂ€reltulija vasakute jĂ€reltulijate ahelas. NĂ€ites peame liikuma sĂ”lme juurde, mille vĂ”ti on 35, ja seejĂ€rel liikuma alla lehe kaudu tema vasakute jĂ€reltulijate ahelas — antud juhul koosneb see ahel vaid sĂ”lmest, mille vĂ”ti on 30. Rangelt öeldes otsime me vĂ€ikseimat sĂ”lme nende sĂ”lmede kogumis, mis on otsitavast sĂ”lmest suuremad.

Binaarne puu ehk kuidas valmistada binaarset otsingupuud

JĂ€rglase otsimise meetodi kood:

    public Node getSuccessor(Node deleteNode) {
        Node parentSuccessor = deleteNode; // vanema jÀrglane
        Node successor = deleteNode; // jÀrglane
        Node current = successor.getRightChild(); // lihtsalt "lÀbiv" sÔlm
        while (current != null) {
            parentSuccessor = successor;
            successor = current;
            current = current.getLeftChild();
        }
        // tsĂŒklist vĂ€ljudes on meil jĂ€rglane ja vanema jĂ€rglane
        if (successor != deleteNode.getRightChild()) { // kui jĂ€rglane ei ĂŒhti kustutatava sĂ”lme parema lapsega
            parentSuccessor.setLeftChild(successor.getRightChild()); // siis tema vanem vÔtab jÀrglase jÀreltulija, et seda mitte kaotada
            successor.setRightChild(deleteNode.getRightChild()); // seome jÀrglase kustutatava sÔlme parema lapsega
        }
        return successor;
    }

Kogu delete meetodi kood:

public boolean delete(int deleteKey) {
        Node current = root;
        Node parent = current;
        boolean isLeftChild = false; // SÔltuvalt sellest, kas kustutatav sÔlm on oma vanemast vasak vÔi parem jÀreltulija, omandab loogiline muutuja isLeftChild vÀÀrtuse true vÔi false vastavalt.
        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) { // esimene juhtum
            if (current == root)
                current = null;
            else if (isLeftChild)
                parent.setLeftChild(null);
            else
                parent.setRightChild(null);
        }
        else if (current.getRightChild() == null) { // teine juhtum
            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 { // kolmas juhtum
            Node successor = getSuccessor(current);
            if (current == root)
                root = successor;
            else if (isLeftChild)
                parent.setLeftChild(successor);
            else
                parent.setRightChild(successor);
        }
        return true;
    }

Aja vÔib hinnata O(log(n))-ks.

Maksimaalse/minimaalse otsimine puus

Ilmselgelt, kuidas leida puus minimaalne/maximaalne vÀÀrtus — tuleb jĂ€rjestikku liikuda puu vasakute/paretute elementide kaudu; kui jĂ”uad lehtedeni, on see minimaalne/maximaalne 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;
    }

Aja keerukus — O(log(n))

SĂŒmbiootiline lĂ€bimine

LĂ€bimine — iga puu sĂ”lme kĂŒlastamine tegevuse sooritamiseks.

Rekursiivse sĂŒmbiootilise lĂ€bimise algoritm:

  1. Teha tegevus vasakul lapsel
  2. Teha tegevus endaga
  3. Teha tegevus paremal lapsel

Kood:

    public void inOrder(Node<T> current) {
        if (current != null) {
            inOrder(current.getLeftChild());
            System.out.println(current.getData() + " ");//Siin vÔib olla mis iganes
            inOrder(current.getRightChild());
        }
    }

KokkuvÔte

LÔpuks! Kui ma midagi ei selgitanud vÔi on mingeid mÀrkusi, ootan neid kommentaarides. Nagu lubatud, esitan tÀis koodi.

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.

Langenemine O(n) tasemeni

Paljud teist on vĂ”ib-olla mĂ€rganud, et mis juhtub, kui puud pole tasakaalus? NĂ€iteks, kui panna puusse jĂ€rjestikuseid sĂ”lmi: 1, 2, 3, 4, 5, 6
 Siis meenutab puu pigem ĂŒhendatud loendit. Ja jah, puu kaotab oma puulise struktuuri, seega ka andmete juurdepÀÀsu efektiivsuse. Otsingu, sisestamise ja kustutamise keerukus muutub sarnaseks ĂŒhendatud loendi omadega: O(n). See on ĂŒks olulisemaid, minu arvates, kahjusid, mis tulenevad binaarsetest puudest.

Ainult registreeritud kasutajad saavad kĂŒsitluses osaleda. Logige sisse, palun.

Ma olen hubastes hiljuti ning sooviksin teada, milliseid teemasid sooviksite rohkem nÀha?

  • Andmestruktuurid

  • Algoritmid (DĂŒnaamiline programmeerimine, rekurssioon, andmete tihendamine jne.)

  • Andmestruktuuride ja algoritmide rakendamine reaalses elus

  • Android-rakenduste programmeerimine Java-s

  • Veebirakenduste programmeerimine Java-s

HÀÀletas 2 kasutajat. 1 kasutaja hoidub.

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster