Реализация бинарного дерева в Java

Реализация двоичного дерева в Java

1. Вступление

В этой статье мы рассмотрим реализацию двоичного дерева на Java.

Для этой статьиwe’ll use a sorted binary tree that will contain int values.

2. Бинарное дерево

Двоичное дерево - это рекурсивная структура данных, в которой каждый узел может иметь не более 2 дочерних элементов.

Распространенным типом двоичного дерева является двоичное дерево поиска, в котором каждый узел имеет значение, которое больше или равно значениям узла в левом поддереве и меньше или равно значениям узла в правом поддереве. дерево.

Вот краткое визуальное представление этого типа двоичного дерева:

image

Для реализации мы будем использовать вспомогательный классNode, который будет хранить значенияint и ссылаться на каждого потомка:

class Node {
    int value;
    Node left;
    Node right;

    Node(int value) {
        this.value = value;
        right = null;
        left = null;
    }
}

Затем давайте добавим начальный узел нашего дерева, обычно называемыйroot:

public class BinaryTree {

    Node root;

    // ...
}

3. Общие операции

Теперь давайте посмотрим, какие операции мы можем выполнять с двоичным деревом.

3.1. Вставка элементов

Первая операция, которую мы рассмотрим, - это вставка новых узлов.

Во-первых,we have to find the place where we want to add a new node in order to keep the tree sorted. Мы будем следовать этим правилам, начиная с корневого узла:

  • если значение нового узла ниже, чем значение текущего узла, мы переходим к левому дочернему элементу

  • если значение нового узла больше, чем значение текущего узла, мы переходим к правому дочернему элементу

  • когда текущий узел равенnull,, мы достигли листового узла и можем вставить новый узел в эту позицию

Сначала мы создадим рекурсивный метод для вставки:

private Node addRecursive(Node current, int value) {
    if (current == null) {
        return new Node(value);
    }

    if (value < current.value) {
        current.left = addRecursive(current.left, value);
    } else if (value > current.value) {
        current.right = addRecursive(current.right, value);
    } else {
        // value already exists
        return current;
    }

    return current;
}

Затем мы создадим общедоступный метод, который запускает рекурсию с узлаroot:

public void add(int value) {
    root = addRecursive(root, value);
}

Теперь давайте посмотрим, как мы можем использовать этот метод для создания дерева из нашего примера:

private BinaryTree createBinaryTree() {
    BinaryTree bt = new BinaryTree();

    bt.add(6);
    bt.add(4);
    bt.add(8);
    bt.add(3);
    bt.add(5);
    bt.add(7);
    bt.add(9);

    return bt;
}

3.2. Поиск элемента

Давайте теперь добавим метод, чтобы проверить, содержит ли дерево определенное значение.

Как и раньше, сначала мы создадим рекурсивный метод для обхода дерева:

private boolean containsNodeRecursive(Node current, int value) {
    if (current == null) {
        return false;
    }
    if (value == current.value) {
        return true;
    }
    return value < current.value
      ? containsNodeRecursive(current.left, value)
      : containsNodeRecursive(current.right, value);
}

Здесь мы ищем значение, сравнивая его со значением в текущем узле, а затем продолжаем в левом или правом дочернем элементе в зависимости от этого.

Затем давайте создадим общедоступный метод, который начинается сroot:

public boolean containsNode(int value) {
    return containsNodeRecursive(root, value);
}

Теперь давайте создадим простой тест, чтобы убедиться, что дерево действительно содержит вставленные элементы:

@Test
public void givenABinaryTree_WhenAddingElements_ThenTreeContainsThoseElements() {
    BinaryTree bt = createBinaryTree();

    assertTrue(bt.containsNode(6));
    assertTrue(bt.containsNode(4));

    assertFalse(bt.containsNode(1));
}

Все добавленные узлы должны содержаться в дереве.

3.3. Удаление элемента

Другой распространенной операцией является удаление узла из дерева.

Во-первых, мы должны найти узел для удаления таким же образом, как мы делали раньше:

private Node deleteRecursive(Node current, int value) {
    if (current == null) {
        return null;
    }

    if (value == current.value) {
        // Node to delete found
        // ... code to delete the node will go here
    }
    if (value < current.value) {
        current.left = deleteRecursive(current.left, value);
        return current;
    }
    current.right = deleteRecursive(current.right, value);
    return current;
}

Как только мы найдем узел для удаления, есть 3 основных случая:

  • a node has no children – это простейший случай; нам просто нужно заменить этот узел наnull в его родительском узле

  • a node has exactly one child – в родительском узле, мы заменяем этот узел его единственным дочерним узлом.

  • a node has two children - это самый сложный случай, потому что он требует реорганизации дерева

Давайте посмотрим, как мы можем реализовать первый случай, когда узел является листовым:

if (current.left == null && current.right == null) {
    return null;
}

Теперь давайте продолжим случай, когда у узла есть один дочерний элемент:

if (current.right == null) {
    return current.left;
}

if (current.left == null) {
    return current.right;
}

Здесь мы возвращаем дочерний элементnon-null, чтобы его можно было назначить родительскому узлу.

Наконец, мы должны обработать случай, когда узел имеет двух детей.

Во-первых, нам нужно найти узел, который заменит удаленный узел. Мы будем использовать самый маленький узел из правого поддерева удаляемого узла:

private int findSmallestValue(Node root) {
    return root.left == null ? root.value : findSmallestValue(root.left);
}

Затем мы присваиваем наименьшее значение удаляемому узлу, и после этого мы удалим его из правого поддерева:

int smallestValue = findSmallestValue(current.right);
current.value = smallestValue;
current.right = deleteRecursive(current.right, smallestValue);
return current;

Наконец, давайте создадим общедоступный метод, который начнет удаление изroot:

public void delete(int value) {
    root = deleteRecursive(root, value);
}

Теперь давайте проверим, что удаление работает должным образом:

@Test
public void givenABinaryTree_WhenDeletingElements_ThenTreeDoesNotContainThoseElements() {
    BinaryTree bt = createBinaryTree();

    assertTrue(bt.containsNode(9));
    bt.delete(9);
    assertFalse(bt.containsNode(9));
}

4. Путешествие по дереву

В этом разделе мы увидим различные способы обхода дерева и подробно рассмотрим поиск в глубину и в ширину.

Мы будем использовать то же дерево, что и раньше, и покажем порядок обхода для каждого случая.

Поиск в глубину - это тип обхода, который максимально углубляется в каждого ребенка, прежде чем исследовать следующего брата или сестру.

Есть несколько способов выполнить поиск в глубину: по порядку, предварительному заказу и после заказа.

Обход по порядку состоит из первого посещения левого поддерева, затем корневого узла и, наконец, правого поддерева:

public void traverseInOrder(Node node) {
    if (node != null) {
        traverseInOrder(node.left);
        System.out.print(" " + node.value);
        traverseInOrder(node.right);
    }
}

Если мы вызовем этот метод, вывод консоли покажет порядок обхода:

3 4 5 6 7 8 9

Обход предварительного заказа посещает сначала корневой узел, затем левое поддерево и, наконец, правое поддерево:

public void traversePreOrder(Node node) {
    if (node != null) {
        System.out.print(" " + node.value);
        traversePreOrder(node.left);
        traversePreOrder(node.right);
    }
}

И давайте проверим обход предварительного заказа в выводе консоли:

6 4 3 5 8 7 9

Обход после заказа посещает левое поддерево, правое поддерево и корневой узел в конце:

public void traversePostOrder(Node node) {
    if (node != null) {
        traversePostOrder(node.left);
        traversePostOrder(node.right);
        System.out.print(" " + node.value);
    }
}

Вот узлы в пост-порядке:

3 5 4 7 9 8 6

Это еще один распространенный тип обходаvisits all the nodes of a level before going to the next level.

Этот вид обхода также называется уровнем порядка и охватывает все уровни дерева, начиная с корня и слева направо.

Для реализации мы будем использоватьQueue для упорядочивания узлов каждого уровня. Мы извлечем каждый узел из списка, распечатаем его значения, а затем добавим его дочерние элементы в очередь:

public void traverseLevelOrder() {
    if (root == null) {
        return;
    }

    Queue nodes = new LinkedList<>();
    nodes.add(root);

    while (!nodes.isEmpty()) {

        Node node = nodes.remove();

        System.out.print(" " + node.value);

        if (node.left != null) {
            nodes.add(node.left);
        }

        if (node.right!= null) {
            nodes.add(node.right);
        }
    }
}

В этом случае порядок узлов будет:

6 4 8 3 5 7 9

5. Заключение

В этой статье мы увидели, как реализовать сортированное двоичное дерево в Java и его наиболее распространенные операции.

Полный исходный код примеров доступенover on GitHub.