Сбалансированные деревья поиска. Включение: повороты и балансировка

Модуль "Деревья поиска" • урок 4 из 8 • ~8 минут

Суть урока. При включении в АВЛ-дерево узел сначала вставляется как в обычное дерево поиска, а затем, если у какого-то узла на пути баланс стал +2 или -2, баланс восстанавливается поворотом. Повороты бывают малыми, или простыми (LL/RR), и большими, или двойными (LR/RL). Важно: при включении всегда достаточно одного поворота, чтобы восстановить дерево.

Нарушение баланса при включении

Процесс включения нового узла в АВЛ-дерево начинается точно так же, как и в обычном дереве поиска: мы спускаемся от корня к листьям и добавляем новый элемент на свободное место. Однако добавление узла увеличивает высоту поддерева. Это может привести к тому, что для некоторого узла на пути от корня к новому элементу баланс станет равным +2 или -2.

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

Сбалансированные деревья поиска. Включение: четыре типа поворотов

В зависимости от того, в какую ветвь был добавлен новый узел, выделяют четыре случая нарушения баланса. Пусть C - узел, у которого нарушился баланс (он стал +2 или -2), B - его потомок со стороны перевеса, а A - новый узел. Именно так эти узлы названы и в коде ниже.

Визуализация: малый правый поворот (случай LL)

В АВЛ-дерево из двух узлов 50 и 25 приходит ключ 10. Баланс узла 50 становится равным -2, а его левый потомок перевешивает влево - это и есть случай LL.

Сбалансированные деревья поиска. Включение: малый правый поворот поднимает узел 25 на место узла 50, и все балансы становятся нулевыми
Узел B поднимается на место C, узел C опускается вправо вниз

Визуализация: большой поворот (случай LR)

Если в то же дерево вместо 10 приходит ключ 35, левый потомок перевешивает вправо. Одним поворотом такой дисбаланс не снять: сначала левый поворот сводит случай LR к уже разобранному LL, и только потом работает правый поворот.

Большой поворот при случае LR: левый поворот нижних узлов 25 и 35 сводит случай к LL, а правый поворот 35 и 50 восстанавливает баланс
Большой поворот - это два малых подряд, а не отдельная операция

Реализация на языке C (правый поворот)

На уровне указателей операция поворота выполняется очень быстро O(1), так как нам нужно перебросить всего несколько связей.

typedef struct node {
    int key, height;
    struct node *l, *r;
} node;

int h(node* p) { return p ? p->height : 0; }
void fix(node* p) { p->height = 1 + (h(p->l) > h(p->r) ? h(p->l) : h(p->r)); }

// Малый правый поворот вокруг C (случай LL). Возвращает новый корень поддерева.
node* rotateRight(node* C) {
    node* B = C->l;     // B поднимается наверх
    C->l = B->r;        // правое поддерево B становится левым поддеревом C
    B->r = C;          // C опускается вправо вниз
    fix(C);            // пересчитать высоты (сначала нижний узел)
    fix(B);
    return B;          // возвращаем новый корень поддерева
}
А как в Python? При реализации АВЛ-дерева в Python концепция остается той же. Ссылки между объектами просто переопределяются: node.left = left_child.right; left_child.right = node; return left_child. Благодаря сборщику мусора Python не нужно беспокоиться об утечках памяти во время перестановки "ветвей".

Коротко: главное

  • Включение узла может нарушить баланс на пути от корня к новому узлу (баланс становится +2 или -2).
  • Различают 4 случая дисбаланса: LL, RR лечатся малым поворотом; LR, RL - большим.
  • При включении всегда достаточно одного поворота: после него высота поддерева возвращается к исходной, и дисбаланс не распространяется выше к корню.

Проверьте себя

Сколько поворотов (в худшем случае) может потребоваться выполнить для восстановления АВЛ-баланса после операции вставки (включения) одного узла?

Как вам урок? Спасибо за отзыв!