Drzewa binarne, drzewa BST i kopce w Javie

Zaktualizowano 22 października 2026

Drzewa binarne, drzewa poszukiwań binarnych i kopce to w Javie podstawowe struktury danych, gdy dane nie mają leżeć tylko w jednym rzędzie. Listy, stosy i kolejki układają dane w rzędzie. Drzewo się rozgałęzia: każdy węzeł może mieć kilku następników. Dzięki temu w dobrze zbudowanym drzewie poszukiwań znajdziesz wartość w kilku krokach, a kopiec od razu podaje najmniejszy element. Prawie każda metoda na drzewach jest rekurencyjna, bo każda część drzewa sama jest drzewem.

Pojęcia

W drzewie binarnym każdy węzeł ma co najwyżej dwoje dzieci, lewe i prawe. W Javie węzeł to mała klasa, na przykład taka:

class TreeNode {    int value;    TreeNode left;    TreeNode right;}
  • Korzeń to najwyższy węzeł, jedyny bez rodzica. Przez niego dochodzisz do całego drzewa.
  • Liść nie ma dzieci: left i right są null. Wszystkie inne węzły nazywa się węzłami wewnętrznymi.
  • Poddrzewo to węzeł razem ze wszystkim, co wisi pod nim. Lewe poddrzewo korzenia samo jest więc drzewem.
  • Wysokość liczy tutaj poziomy: pojedynczy węzeł ma wysokość 1, puste drzewo wysokość 0. Niektóre książki liczą zamiast tego krawędzie, wtedy wszystko jest o 1 mniejsze.

Rekurencja i przechodzenie drzewa

Metoda na drzewie prawie zawsze ma ten sam schemat: warunkiem stopu jest puste drzewo (node == null), w przeciwnym razie przetwarzasz węzeł i wywołujesz siebie dla node.left i node.right. Każdy węzeł jest odwiedzany dokładnie raz, co kosztuje O(n).

Odwiedzanie wszystkich węzłów to przechodzenie drzewa. Trzy typowe kolejności różnią się tylko tym, kiedy przychodzi kolej na sam węzeł:

  • Preorder: węzeł, lewo, prawo. Korzeń jest na początku.
  • Inorder: lewo, węzeł, prawo. Korzeń jest w środku.
  • Postorder: lewo, prawo, węzeł. Korzeń jest na końcu.
public class Main {    static int countNodes(TreeNode node) {        if (node == null) {            return 0;        }        return 1 + countNodes(node.left) + countNodes(node.right);    }
    static int height(TreeNode node) {        if (node == null) {            return 0;        }        return 1 + Math.max(height(node.left), height(node.right));    }
    static void preorder(TreeNode node) {        if (node == null) {            return;        }        System.out.print(node.value + " ");        preorder(node.left);        preorder(node.right);    }
    static void inorder(TreeNode node) {        if (node == null) {            return;        }        inorder(node.left);        System.out.print(node.value + " ");        inorder(node.right);    }
    static void postorder(TreeNode node) {        if (node == null) {            return;        }        postorder(node.left);        postorder(node.right);        System.out.print(node.value + " ");    }
    public static void main(String[] args) {        //        1        //       / \        //      2   3        //     / \        //    4   5        TreeNode root = new TreeNode(1,                new TreeNode(2, new TreeNode(4), new TreeNode(5)),                new TreeNode(3));        System.out.println("Nodes: " + countNodes(root));        System.out.println("Height: " + height(root));        System.out.print("Preorder:  ");        preorder(root);        System.out.println();        System.out.print("Inorder:   ");        inorder(root);        System.out.println();        System.out.print("Postorder: ");        postorder(root);        System.out.println();    }}
class TreeNode {    int value;    TreeNode left;    TreeNode right;
    TreeNode(int value) {        this.value = value;    }
    TreeNode(int value, TreeNode left, TreeNode right) {        this.value = value;        this.left = left;        this.right = right;    }}

Dla drzewa z przykładu daje to 1 2 4 5 3, 4 2 5 1 3 i 4 5 2 3 1. Na kolokwium pomaga narysować drzewo i odhaczać każdy węzeł przy odwiedzeniu.

Drzewa poszukiwań binarnych (BST)

Drzewo poszukiwań binarnych porządkuje wartości: dla każdego węzła wszystkie wartości w lewym poddrzewie są mniejsze, a wszystkie w prawym większe. Przy wyszukiwaniu porównujesz z węzłem i idziesz w lewo albo w prawo, zupełnie jak przy wyszukiwaniu binarnym w tablicy. Nową wartość wstawia się w miejscu, w którym wyszukiwanie trafi na null.

  • Najmniejsza wartość jest na samym dole po lewej, największa na samym dole po prawej.
  • Inorder daje wartości posortowane.
  • Wyszukiwanie i wstawianie potrzebują tylu kroków, ile wynosi wysokość drzewa. Jeśli drzewo jest zrównoważone, to około log₂ n, czyli O(log n): 1 000 000 wartości w około 20 poziomach.
  • Jeśli wstawiasz wartości już posortowane (1, 2, 3, …), każdy nowy węzeł wisi po prawej stronie poprzedniego. Drzewo degeneruje się do listy, a wyszukiwanie kosztuje wtedy O(n). Drzewa samorównoważące się (AVL, czerwono-czarne, w Javie TreeMap) temu zapobiegają.
public class Main {    static TreeNode insert(TreeNode root, int value) {        if (root == null) {            return new TreeNode(value);        }        if (value < root.value) {            root.left = insert(root.left, value);        } else if (value > root.value) {            root.right = insert(root.right, value);        }        return root;    }
    static boolean contains(TreeNode root, int value) {        TreeNode node = root;        while (node != null) {            if (value == node.value) {                return true;            }            node = value < node.value ? node.left : node.right;        }        return false;    }
    static int min(TreeNode root) {        TreeNode node = root;        while (node.left != null) {            node = node.left;        }        return node.value;    }
    static void inorder(TreeNode node) {        if (node == null) {            return;        }        inorder(node.left);        System.out.print(node.value + " ");        inorder(node.right);    }
    public static void main(String[] args) {        TreeNode root = null;        for (int v : new int[] {50, 30, 70, 20, 40, 60, 80}) {            root = insert(root, v);        }        System.out.println("contains 40? " + contains(root, 40));        System.out.println("contains 45? " + contains(root, 45));        System.out.println("min: " + min(root));        System.out.print("sorted: ");        inorder(root);        System.out.println();    }}
class TreeNode {    int value;    TreeNode left;    TreeNode right;
    TreeNode(int value) {        this.value = value;    }}

Kopce w tablicy

Kopiec minimalny (min-heap) to drzewo binarne z własnością kopca: każdy węzeł jest mniejszy lub równy swoim dzieciom. Minimum jest więc zawsze w korzeniu. Inaczej niż w drzewie BST rodzeństwo nie jest między sobą uporządkowane.

Kopiec jest zawsze zupełny, wypełniany poziom po poziomie od lewej. Dlatego mieści się bez luk w tablicy:

  • korzeń pod indeksem 0
  • dzieci i: 2 * i + 1 i 2 * i + 2
  • rodzic i: (i - 1) / 2 (dzielenie całkowite)

Wstawianie: dopisz na końcu i przesuwaj w górę, dopóki wartość jest mniejsza od rodzica. Usuwanie minimum: wstaw ostatni element pod indeks 0 i przesuwaj go w dół, zawsze zamieniając z mniejszym dzieckiem. Obie operacje przechodzą przez drzewo co najwyżej raz z góry na dół: O(log n). Java oferuje to gotowe jako PriorityQueue.

import java.util.Arrays;
public class Main {    static int[] data = new int[10];    static int size = 0;
    static void insert(int value) {        data[size] = value;        int i = size;        size++;        // sift up: swap with the parent while smaller        while (i > 0 && data[i] < data[(i - 1) / 2]) {            swap(i, (i - 1) / 2);            i = (i - 1) / 2;        }    }
    static int poll() {        int min = data[0];        size--;        data[0] = data[size];        int i = 0;        // sift down: swap with the smaller child while larger        while (2 * i + 1 < size) {            int child = 2 * i + 1;            if (child + 1 < size && data[child + 1] < data[child]) {                child++;            }            if (data[i] <= data[child]) {                break;            }            swap(i, child);            i = child;        }        return min;    }
    static void swap(int i, int j) {        int tmp = data[i];        data[i] = data[j];        data[j] = tmp;    }
    public static void main(String[] args) {        for (int v : new int[] {5, 3, 8, 1, 4}) {            insert(v);            System.out.println("insert " + v + ": " + Arrays.toString(Arrays.copyOf(data, size)));        }        System.out.println("poll: " + poll());        System.out.println("heap: " + Arrays.toString(Arrays.copyOf(data, size)));    }}

Sortowanie przez kopcowanie (Heapsort)

Heapsort sortuje za pomocą kopca maksymalnego bezpośrednio w tablicy:

  1. Zbuduj kopiec: od a.length / 2 - 1 do 0 przesuwaj w dół każdy węzeł wewnętrzny. Potem maksimum jest pod indeksem 0.
  2. Zamień maksimum z ostatnim elementem kopca. Stoi ono teraz na swoim ostatecznym miejscu, a kopiec jest o jeden element krótszy. Przesuń nowy korzeń w dół i powtarzaj.

Każde przesunięcie w dół kosztuje O(log n) i dzieje się n razy: O(n log n), także w najgorszym przypadku. W przeciwieństwie do quicksorta heapsort nie potrzebuje dodatkowej pamięci i nie ma złego przypadku z O(n²). Za to nie jest stabilny: równe wartości mogą zamienić się kolejnością.

import java.util.Arrays;
public class Main {    static void heapSort(int[] a) {        // 1. build a max-heap: let every inner node sink, from back to front        for (int i = a.length / 2 - 1; i >= 0; i--) {            siftDown(a, i, a.length);        }        // 2. move the maximum to the end, shrink the heap, repair it        for (int end = a.length - 1; end > 0; end--) {            swap(a, 0, end);            siftDown(a, 0, end);        }    }
    static void siftDown(int[] a, int i, int size) {        while (2 * i + 1 < size) {            int child = 2 * i + 1;            if (child + 1 < size && a[child + 1] > a[child]) {                child++;            }            if (a[i] >= a[child]) {                return;            }            swap(a, i, child);            i = child;        }    }
    static void swap(int[] a, int i, int j) {        int tmp = a[i];        a[i] = a[j];        a[j] = tmp;    }
    public static void main(String[] args) {        int[] a = {4, 10, 3, 5, 1};        heapSort(a);        System.out.println(Arrays.toString(a));    }}

Typowe błędy

  • Niepodpięte wywołanie rekurencyjne

    Samo insert(root.left, value); tworzy wprawdzie nowy węzeł, ale nikt go nie zapamiętuje. Poprawnie jest root.left = insert(root.left, value);. Tak samo przy pierwszym wstawianiu do pustego drzewa: root = insert(root, 5);, bo inaczej root zostaje null.

  • Drzewo BST sprawdzone tylko względem dzieci

    Nie wystarczy, że każde lewe dziecko jest mniejsze, a każde prawe większe. W drzewie z korzeniem 8, lewym dzieckiem 3 i jego prawym dzieckiem 9 każda para pasuje, a mimo to 9 stoi w lewym poddrzewie ósemki. Sprawdzaj każdy węzeł względem dolnej i górnej granicy, które przekazujesz w dół przy schodzeniu.

  • Złe indeksy w kopcu

    Gdy korzeń jest pod indeksem 0, dzieci to 2 * i + 1 i 2 * i + 2. Wzory 2 * i i 2 * i + 1 obowiązują tylko wtedy, gdy korzeń jest pod indeksem 1. A przy przesuwaniu w dół dziecko istnieje tylko wtedy, gdy jego indeks jest mniejszy od size, a nie od data.length: dalej stoją stare wartości.

Zadania z tego tematu

Przećwicz to, czego się nauczyłeś, od razu w przeglądarce. Testy natychmiast sprawdzają twoje rozwiązanie.

Powiązane artykuły