Рубрики
Без рубрики

Руководство по деревьям AVL на Java

Узнайте о деревьях AVL и алгоритмах вставки, удаления и поиска значений

Автор оригинала: baeldung.

1. введение

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

2. Что Такое Дерево AVL?

Дерево AVL, названное в честь его изобретателей Адельсона-Вельского и Ландиса, представляет собой самобалансирующееся бинарное дерево поиска (BST).

Самобалансирующееся дерево-это двоичное дерево поиска, которое балансирует высоту после вставки и удаления в соответствии с некоторыми правилами балансировки.

Наихудшая временная сложность BST является функцией высоты дерева. В частности, самый длинный путь от корня дерева до узла. Для BST с N узлами предположим, что каждый узел имеет только ноль или один дочерний элемент. Поэтому его высота равна N, а время поиска в худшем случае равно O(N). Таким образом, наша главная цель в BST-сохранить максимальную высоту близко к log(N).

Коэффициент баланса узла N равен height(right(N)) – height(left(N)) . В дереве AVL коэффициент баланса узла может быть только одним из значений 1, 0 или -1.

Давайте определим объект Node для нашего дерева:

public class Node {
    int key;
    int height;
    Node left;
    Node right;
    ...
}

Далее давайте определим дерево AVL :

public class AVLTree {

    private Node root;

    void updateHeight(Node n) {
        n.height = 1 + Math.max(height(n.left), height(n.right));
    }

    int height(Node n) {
        return n == null ? -1 : n.height;
    }

    int getBalance(Node n) {
        return (n == null) ? 0 : height(n.right) - height(n.left);
    }

    ...
}

3. Как сбалансировать дерево AVL?

Дерево AVL проверяет коэффициент баланса своих узлов после вставки или удаления узла. Если коэффициент баланса узла больше единицы или меньше -1, дерево восстанавливает баланс.

Существует две операции для перебалансировки дерева:

  • правое вращение и
  • поворот влево.

3.1. Поворот вправо

Давайте начнем с правильного вращения.

У нас есть BST called T1, с корневым узлом, X как левый ребенок У, и Z как правый ребенок X. Given the characteristics of BST, мы знаем, что X < Z < Y.

После правого поворота у нас есть дерево, названное T2 с X как корень и как правый ребенок X и Z как левый ребенок Y. T2 все еще BST, потому что он держит порядок X < Z < Y.

Давайте рассмотрим правильную операцию вращения для нашего дерева AVL :

Node rotateRight(Node y) {
    Node x = y.left;
    Node z = x.right;
    x.right = y;
    y.left = z;
    updateHeight(y);
    updateHeight(x);
    return x;
}

3.2. Операция Поворота Влево

У нас также есть операция поворота влево.

Предположим, что BST называется T1, с Y в качестве корневого узла, X в качестве правого дочернего элемента Y и Z в качестве левого дочернего элемента X. Учитывая это, мы знаем, что Y < Z < X.

После левого поворота Y у нас есть дерево под названием T2 с X в качестве корня и Y в качестве левого потомка X и Z в качестве правого потомка Y. T2 все еще является BST, потому что он сохраняет порядок Y < Z < X.

Давайте взглянем на операцию поворота влево для нашего дерева AVL :

Node rotateLeft(Node y) {
    Node x = y.right;
    Node z = x.left;
    x.left = y;
    y.right = z;
    updateHeight(y);
    updateHeight(x);
    return x;
}

3.3. Методы восстановления баланса

Мы можем использовать операции поворота вправо и поворота влево в более сложных комбинациях, чтобы сохранить дерево AVL сбалансированным после любого изменения в его узлах . В несбалансированной структуре по крайней мере один узел имеет коэффициент баланса, равный 2 или -2. Давайте посмотрим, как мы можем сбалансировать дерево в этих ситуациях.

Когда коэффициент баланса узла Z равен 2, поддерево с Z в качестве корня находится в одном из этих двух состояний, считая Y правым дочерним элементом Z.

В первом случае высота правого дочернего элемента Y (X) больше высоты левого дочернего элемента (T2). Мы можем легко сбалансировать дерево, повернув его влево на Z.

Во втором случае высота правого дочернего элемента Y (T4) меньше высоты левого дочернего элемента (X). В этой ситуации требуется комбинация операций ротации.

В этом случае мы сначала поворачиваем Y вправо, чтобы дерево приняло ту же форму, что и в предыдущем случае. Затем мы можем перебалансировать дерево, повернув его влево на Z.

Кроме того, когда коэффициент баланса узла Z равен -2, его поддерево находится в одном из этих двух состояний, поэтому мы рассматриваем Z как корень, а Y-как его левый дочерний элемент.

Высота в левом дочернем элементе Y больше, чем у его правого дочернего элемента, поэтому мы уравновешиваем дерево правым поворотом Z.

Или во втором случае правый потомок Y имеет больший рост, чем его левый потомок.

Итак, прежде всего, мы преобразуем его в прежнюю форму с левым поворотом Y, затем уравновешиваем дерево с правым поворотом Z.

Давайте взглянем на операцию перебалансировки для нашего дерева AVL :

Node rebalance(Node z) {
    updateHeight(z);
    int balance = getBalance(z);
    if (balance > 1) {
        if (height(z.right.right) > height(z.right.left)) {
            z = rotateLeft(z);
        } else {
            z.right = rotateRight(z.right);
            z = rotateLeft(z);
        }
    } else if (balance < -1) {
        if (height(z.left.left) > height(z.left.right))
            z = rotateRight(z);
        else {
            z.left = rotateLeft(z.left);
            z = rotateRight(z);
        }
    }
    return z;
}

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

4. Вставьте узел

Когда мы собираемся вставить ключ в дерево, мы должны найти его правильное положение, чтобы передать правила BST. Итак, мы начинаем с корня и сравниваем его значение с новым ключом. Если ключ больше, мы продолжаем вправо — в противном случае мы идем к левому ребенку.

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

После вставки узла у нас есть BST, но это может быть не дерево AVL. Поэтому мы проверяем коэффициенты баланса и перебалансируем BST для всех узлов на пути от нового узла к корню.

Давайте взглянем на операцию вставки:

Node insert(Node node, int key) {
    if (node == null) {
        return new Node(key);
    } else if (node.key > key) {
        node.left = insert(node.left, key);
    } else if (node.key < key) {
        node.right = insert(node.right, key);
    } else {
        throw new RuntimeException("duplicate Key!");
    }
    return rebalance(node);
}

Важно помнить, что ключ уникален в дереве — нет двух узлов, имеющих один и тот же ключ.

Временная сложность алгоритма вставки зависит от высоты. Поскольку наше дерево сбалансировано, мы можем предположить, что временная сложность в худшем случае равна O(log(N)).

5. Удалите узел

Чтобы удалить ключ из дерева, мы сначала должны найти его в ЛУЧШЕМ.

После того, как мы найдем узел (называемый Z), мы должны ввести нового кандидата, чтобы заменить его в дереве. Если Z-лист, то кандидат пуст. Если у Z есть только один ребенок, этот ребенок является кандидатом, но если у Z есть два ребенка, процесс немного сложнее.

Предположим, что правый дочерний элемент Z называется Y. Сначала мы находим крайний левый узел Y и называем его X. Затем мы устанавливаем новое значение Z равным значению X и продолжаем удалять X из Y.

Наконец, мы вызываем метод баланса в конце, чтобы сохранить ЛУЧШЕЕ дерево AVL.

Вот наш метод удаления:

Node delete(Node node, int key) {
    if (node == null) {
        return node;
    } else if (node.key > key) {
        node.left = delete(node.left, key);
    } else if (node.key < key) {
        node.right = delete(node.right, key);
    } else {
        if (node.left == null || node.right == null) {
            node = (node.left == null) ? node.right : node.left;
        } else {
            Node mostLeftChild = mostLeftChild(node.right);
            node.key = mostLeftChild.key;
            node.right = delete(node.right, node.key);
        }
    }
    if (node != null) {
        node = rebalance(node);
    }
    return node;
}

Временная сложность алгоритма удаления зависит от высоты дерева. Аналогично методу вставки, мы можем предположить, что временная сложность в худшем случае равна O(log(N)).

6. Поиск узла

Поиск узла в дереве AVL выполняется так же, как и в любом BST .

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

Временная сложность поиска зависит от высоты. Мы можем предположить, что временная сложность в худшем случае равна O(log(N)).

Давайте посмотрим пример кода:

Node find(int key) {
    Node current = root;
    while (current != null) {
        if (current.key == key) {
            break;
        }
        current = current.key < key ? current.right : current.left;
    }
    return current;
}

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

В этом уроке мы реализовали дерево AVL с операциями вставки, удаления и поиска.

Как всегда, вы можете найти код на Github .