Binary Tree ehk kuidas koostada binaarset otsingupuud

Eelajak

See artikkel on pĂŒhendatud binaarsetele otsingupuidule. Hiljuti kirjutasin artikli Huffmani meetodiga andmete tihendamisest. Seal ma ei pööranud liiga palju tĂ€helepanu binaarsetele puudele, kuna otsingu, sisestamise ja kustutamise meetodid ei olnud aktuaalsed. NĂŒĂŒd otsustasin kirjutada artikli just puude kohta. Alustame.

Puu on andmestruktuur, mis koosneb sĂ”lmedest, mis on ĂŒhendatud harudega. VĂ”ib öelda, et puu on graafi spetsiifiline juhtum. Siin on nĂ€ide puust:

Binary Tree ehk kuidas koostada binaarset otsingupuud

See ei ole binaarne otsingupuu! Kogu teave on peidetud!

Terminoloogia

Juurtipp

Puu juurtipp on selle kĂ”ige ĂŒlemine sĂ”lm. Antud nĂ€ites on see sĂ”lm A. Puust juurelt igale teisele sĂ”lmile vĂ”ib viia ainult ĂŒks tee! Tegelikult vĂ”ib iga sĂ”lme kĂ€sitleda kui vastava sĂ”lme alampuu juurt.

Vanem/alam

KĂ”igil sĂ”lmedel, vĂ€lja arvatud juurtipp, on tĂ€pselt ĂŒks haru, mis viib ĂŒles poole teise sĂ”lme juurde. SĂ”lm, mis asub hetke kohal, nimetatakse vanemaks selle sĂ”lme jaoks. SĂ”lm, mis asub hetke all ja on sellega ĂŒhendatud, nimetatakse alamaks selle sĂ”lme jaoks. Vaatame nĂ€idet. VĂ”tame sĂ”lme B, siis on selle vanem sĂ”lm A ja alamad on sĂ”lmed D, E ja F.

Leht

SÔlm, millel ei ole alamad, nimetatakse puu leheks. Antud nÀites on lehed sÔlmed D, E, F, G, I, J ja K.

See on pÔhiterminoloogia. Teisi mÔisteid kÀsitletakse hiljem. Nii et binaarne puu on puu, millel igal sÔlmel vÔib olla mitte rohkem kui kaks alamad. Nagu te juba arvata vÔisite, ei ole antud nÀide binaarne, kuna sÔlmed B ja H on enam kui kahe alamaga. Siin on nÀide binaarsest puust:

Binary Tree ehk kuidas koostada binaarset otsingupuud

Puu sÔlmedes vÔib olla mistahes teavet. Binaarne otsingupuu on binaarne puu, millel on jÀrgmised omadused:

  1. MĂ”lemad alampuud — vasak ja parem — on binaarsed otsingupuud.
  2. KÔigil vasaku alampuu sÔlmedel, mis kuuluvad suvalisele sÔlmele X, on andmete vÔtmete vÀÀrtused vÀiksemad kui sÔlme X enda vÔtme vÀÀrtus.
  3. KÔigil parema alampuu sÔlmedel, mis kuuluvad suvalisele sÔlmele X, on andmete vÔtmete vÀÀrtused suuremad vÔi vÔrdsed sÔlme X enda vÔtme vÀÀrtusega.

VÔti on mis tahes sÔlme omadus (nt number). VÔti on vajalik selleks, et leida puust element, millele see vÔti vastab. NÀide binaarsest otsingupuust:

Binary Tree ehk kuidas koostada binaarset otsingupuud

Puu esitus

Edasi liikudes toon ma vÀlja mÔned (vÔimalikult mittetÀielikud) koodilÔigud, et parandada teie arusaamist. TÀielik kood leiab aset artikli lÔpus.

Puu koosneb sÔlmedest. SÔlme struktuur:

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;
    }
//...teised sÔlme meetodid
}

Igal sĂ”lmel on kaks jĂ€reltulijat (vĂ”imalik, et jĂ€reltulijad leftChild ja / vĂ”i rightChild vĂ”ivad sisaldada vÀÀrtust null). Olete ilmselt aru saanud, et antud juhul number data — andmed, mis on sĂ”lmes salvestatud; key — sĂ”lme vĂ”ti.

SĂ”lmega oleme kursis, nĂŒĂŒd rÀÀgime puude praegustest probleemidest. Edaspidi mĂ”tlen sĂ”na "puu" all binaarset otsingu puud. Binaarse puu struktuur:

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

    //puu meetodid
}

Klassi vÀljana vajame ainult puu juurt, sest juure kaudu saab meetodite getLeftChild() ja getRightChild() abil jÔuda igasugusele puu sÔlmele.

Puu algoritmid

Vastuse otsimine peab olema keeruline — kui vastust on lihtne leida

Eeldame, et teil on ĂŒles ehitatud puu. Kuidas leida elementi, mille vĂ”ti on key? Tuleb jĂ€rjestikku liikuda juurelt alla puu kaudu ja vĂ”rrelda vĂ€list key sĂ”lme vĂ”tmega: kui key on vĂ€iksem kui jĂ€rgmise sĂ”lme vĂ”ti, siis liikuda sĂ”lme vasakule jĂ€reltulijale, 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 saab nulliks, tĂ€hendab, et otsing jĂ”udis puu lĂ”puni (kontseptsiooniliselt asute te mitteeksisteerivas puu kohas — lehe jĂ€reltulijas).

Vaadakem otsingu algoritmi efektiivsust tasakaalus puus (puus, kus sĂ”lmed on jaotatud ĂŒksjagu ĂŒhtlaselt). Sel juhul on otsingu efektiivsus O(log(n)), kusjuures logaritm on pĂ”hjal 2. Vaadake: kui tasakaalus puus on n elementi, siis see tĂ€hendab, et puul on log(n) taset. Otsingu kĂ€igus langete ĂŒhe sammuga tsĂŒklis ĂŒhe taseme vĂ”rra alla.

Sisestamine

Kui olete otsingu olemuse tabanud, siis ei ole lisamise mĂ”istmine teile raske. Tuleb lihtsalt liikuda puu lehe juurde (langemisreeglite kohaselt, nagu otsingus kirjeldatud) ja saada selle jĂ€reltulijaks — vasakuks vĂ”i paremaks, sĂ”ltuvalt vĂ”tme vÀÀrtusest. Rakendamine:

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

Antud juhul tuleb lisaks hetkelisele sÔlmele hoida teavet kÀesoleva sÔlme vanemast. Kui current muutub nulliks, siis on muutujas parent vajalik leht.
Selgelt on sisestamise tĂ”husus sama, mis otsingul — O(log(n)).

Kustutamine

Kustutamine on kÔige keerulisem operatsioon, mille tuleb puu peal teostada. On selge, et kÔigepealt tuleb leida element, mida me kavandame kustutada. Aga mis siis? Kui lihtsalt omistada selle lingile vÀÀrtus null, kaotame teabe selle sÔlme juures oleva alampuu kohta. Puu kustutamise meetodid jagunevad kolmeks juhtumiks.

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

Kui kustutataval sÔlmel ei ole jÀreltulijaid, tÀhendab see, et see on leht. SeetÔttu saab lihtsalt vanema leftChild vÔi rightChild vÀljadele mÀÀrata nulli.

Teine juhtum. Kustutataval sĂ”lmel on ĂŒks jĂ€reltulija

See juhtum ei ole samuti vĂ€ga keeruline. Naaseme tagasi meie nĂ€ite juurde. Oletame, et tuleb kustutada element vĂ”tmega 14. Olgu see nii, et kuna see on sĂ”lm, millel on vĂ”tmega 10 Ă”igus jĂ€reltulija, on selle jĂ€reltulija (antud juhul parem) vĂ”tme vÀÀrtus alati suurem kui 10, seega saab selle lihtsalt puust 'vĂ€lja lĂ”igata' ja vanema otse jĂ€reltulijaga ĂŒhendada, s.t. sĂ”le vĂ”tmega 10 ĂŒhendatakse sĂ”lmiga 13. Sarnane oleks olukord, kui peaksime kustutama sĂ”lme, mis on oma vanema vasak jĂ€reltulija. MĂ”elge selle ĂŒle ise — tĂ€pne analoogia.

Kolmas juhtum. SÔlmel on kaks jÀreltulijat

See on kĂ”ige keerulisem juhtum. AnalĂŒĂŒsime uut nĂ€idet.

Binary Tree ehk kuidas koostada binaarset otsingupuud

EelkÀija otsimine

Oletame, et peame kustutama sÔlme, mille vÔti on 25. Keda paneme selle asemele? Keegi tema jÀrglastest (jÀreltulijatest vÔi jÀreltulijate jÀreltulijatest) peab saama jÀrglaseks(see, kes vÔtab eemaldatava sÔlme koha).

Kuidas mĂ”ista, kes peaks saama jĂ€rglaseks? Intuitiivselt on selge, et see on puu sĂ”lm, mille vĂ”ti on eemaldatava sĂ”lme omast suurem. Algoritm on jĂ€rgmine. Tuleb liikuda tema paremale jĂ€reltulijale (alati paremale, kuna on juba öeldud, et jĂ€rglase vĂ”ti on suurem kui eemaldatava sĂ”lme oma) ning seejĂ€rel minna vasakute jĂ€reltulijate ahelas selle parema jĂ€reltulija juurde. NĂ€ites peame liikuma sĂ”lme, mille vĂ”ti on 35, ja seejĂ€rel jĂ€rgnema allapoole tema vasakute jĂ€reltulijate ahelas — antud juhul koosneb see ahel vaid sĂ”lmest, mille vĂ”ti on 30. Rikkalikult öeldes otsime toetava sĂ”lme seas kĂ”ige vĂ€iksemat sĂ”lme, mis on suurem kui otsitav sĂ”lm.

Binary Tree ehk kuidas koostada binaarset otsingupuud

JĂ€rglase otsimise meetodi kood:

    public Node<T> getSuccessor(Node<T> deleteNode) {
        Node<T> parentSuccessor = deleteNode; // jÀrglase vanem
        Node<T> successor = deleteNode; // jÀrglane
        Node<T> current = successor.getRightChild(); // lihtsalt 'lÀbivate' sÔlm
        while (current != null) {
            parentSuccessor = successor;
            successor = current;
            current = current.getLeftChild();
        }
        // tsĂŒklist vĂ€ljudes saame jĂ€rglase ja jĂ€rglase vanema
        if (successor != deleteNode.getRightChild()) { // kui jÀrglane ei lange kokku eemaldatava sÔlme parema jÀreltulijaga
            parentSuccessor.setLeftChild(successor.getRightChild()); // siis tema vanem vÔtab jÀrglase jÀreltulija endale, et mitte seda kaotada
            successor.setRightChild(deleteNode.getRightChild()); // seome jÀrglase eemaldatava sÔlme parema jÀreltulijaga
        }
        return successor;
    }

Kustutamise meetodi tÀielik kood:

public boolean delete(int deleteKey) {
        Node current = root;
        Node parent = current;
        boolean isLeftChild = false; // Olene lÀhetamine vastavalt, kas kustutatav sÔlm on oma vanema vasak vÔi parem laps, boolean muutuja isLeftChild omandab vastavalt true vÔi 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) { // esimene juhus
            if (current == root)
                current = null;
            else if (isLeftChild)
                parent.setLeftChild(null);
            else
                parent.setRightChild(null);
        } else if (current.getRightChild() == null) { // teine juhus
            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 juhus
            Node successor = getSuccessor(current);
            if (current == root)
                root = successor;
            else if (isLeftChild)
                parent.setLeftChild(successor);
            else
                parent.setRightChild(successor);
        }
        return true;
    }

Aja keerukus vÔib olla ligikaudu O(log(n)).

Puu maksimaalse/minimaalse vÀÀrtuse otsimine

Selgelt, kuidas leida puu minimaalne/maximaalne vÀÀrtus - tuleb jÀrjestikku liikuda vasakute/paremate puu elementide ahelas; kui jÔuate lehtedeni, siis see on minimaalne/maximaalne 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;
    }

Keerukus - O(log(n))

SĂŒmmeetriline lĂ€bimine

LĂ€bimine - iga puu sĂ”lme kĂŒlastamine, et teha selle kohta mingisugune toiming.

Rekursiivse sĂŒmmeetrilise lĂ€bimise algoritm:

  1. Teha toiming vasaku lapsega
  2. Teha toiming iseendaga
  3. Teha toiming parema lapsega

Kood:

    public void inOrder(Node current) {
        if (current != null) {
            inOrder(current.getLeftChild());
            System.out.println(current.getData() + " ");//Siin vÔib olla kÔik, mis tahes
            inOrder(current.getRightChild());
        }
    }

KokkuvÔte

LÔpuks! Kui ma midagi piisavalt ei selgitanud vÔi kui on mingeid mÀrkusi, ootan kommentaarides. Nagu lubatud, jagan tÀismahus 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) tasemele

Paljud teist on ehk mĂ€rganud: mis oleks, kui teha puust tasakaalustamata? NĂ€iteks paigutada puusse sĂ”lmed, millel on kasvavad vĂ”tmed: 1, 2, 3, 4, 5, 6... Siis hakkab puu meenutama seotud loendit. Jah, puu kaotab oma puulaadse struktuuri ja seega ka andmete juurdepÀÀsu efektiivsuse. Otsingu, sisestamise, kustutamise operatsioonide keerukus muutub sarnaseks seotud loendi omaga: O(n). See on ĂŒks peamine puudus, mida ma arvan, et binaarsetel puudel on.

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

Ma olen hiljuti Habr's ja tahaksin teada, milliseid teemasid sooviksite rohkem nÀha?

  • Andmestruktuurid

  • Algoritmid (DĂŒnaamiline Programmeerimine, rekursioon, andmete kokkusurumine jne)

  • Andmestruktuuride ja algoritmide rakendamine reaalses elus

  • Androidi rakenduste programmeerimine Java's

  • Veebirakenduste programmeerimine Java's

HÀÀletas 2 kasutajat. 1 kasutaja hoidus.

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster