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:
leftirightsą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 + 1i2 * 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:
- Zbuduj kopiec: od
a.length / 2 - 1do 0 przesuwaj w dół każdy węzeł wewnętrzny. Potem maksimum jest pod indeksem 0. - 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 jestroot.left = insert(root.left, value);. Tak samo przy pierwszym wstawianiu do pustego drzewa:root = insert(root, 5);, bo inaczejrootzostajenull.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 + 1i2 * i + 2. Wzory2 * ii2 * i + 1obowiązują tylko wtedy, gdy korzeń jest pod indeksem 1. A przy przesuwaniu w dół dziecko istnieje tylko wtedy, gdy jego indeks jest mniejszy odsize, a nie oddata.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.
- Liczenie węzłów w drzewiePro
- Wysokość drzewaPro
- Suma liściPro
- Pseudokod: inorderPro
- Preorder i postorderPro
- Co to wypisze? Przechodzenie drzewaPro
- Binarne drzewo poszukiwań: wstawianie i szukaniePro
- Znajdź błąd: wstawianie do drzewa poszukiwańPro
- Kopiec minimalny na tablicyPro
- Czy to drzewo poszukiwań?Pro
- Sortowanie przez kopcowaniePro
Powiązane artykuły
- Rekurencja i backtrackingRekurencja i backtracking w Javie prosto wyjaśnione: warunek stopu, stos wywołań, permutacje i wieże Hanoi z przykładami oraz typowymi błędami.Czytaj artykuł
- Haszowanie i grafyTablica mieszająca, przeszukiwanie wszerz i w głąb oraz algorytm Dijkstry w Javie: przykłady kodu, złożoność i typowe błędy prosto wyjaśnione.Czytaj artykuł
- Szybkie sortowanieSortowanie przez scalanie, quicksort i sortowanie przez zliczanie w Javie: przykłady kodu, złożoność, stabilność i typowe błędy na kolokwium.Czytaj artykuł