Деревья поиска. Вставка и удаление: два простых случая и один хитрый

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

Суть урока. Вставка в BST проста: спускаемся по дереву как при поиске, пока не уткнемся в пустой указатель (NULL) - именно туда добавляем новый узел. Удаление сложнее и делится на 3 случая: лист (просто убрать), один потомок (заменить узел на него) и два потомка (заменить узел на самый правый узел левого поддерева ИЛИ самый левый правого).

Деревья поиска. Вставка и удаление: включение нового ключа

Для добавления нового элемента в дерево поиска необходимо найти для него подходящее место так, чтобы не нарушилось основное свойство дерева (слева меньше, справа больше). Процедура вставки идентична процедуре поиска: мы спускаемся по дереву, пока не наткнемся на пустой указатель (NULL). Именно на это место мы выделяем память под новый узел и прикрепляем его к дереву.

/* Рекурсивная вставка (поиск с включением) */
void insert(int x, struct node** p) {
    if (*p == NULL) {
        /* Нашли пустое место, создаем узел */
        *p = (struct node*)malloc(sizeof(struct node));
        (*p)->key = x;
        (*p)->l = NULL;
        (*p)->r = NULL;
    } else if (x < (*p)->key) {
        insert(x, &(*p)->l); /* Идем влево */
    } else if (x > (*p)->key) {
        insert(x, &(*p)->r); /* Идем вправо */
    }
    /* Если x == (*p)->key, элемент уже есть (дубликат игнорируется) */
}
Деревья поиска. Вставка и удаление: вставка ключа 80 идет вправо к вершине 75, вправо к вершине 90 и влево на пустое место
Маршрут вставки тот же, что и у поиска: новый узел всегда становится листом

Удаление элемента (исключение): три случая

Удаление элемента (исключение) - значительно более сложная операция, так как она может нарушить целостность древовидной структуры. При удалении узла необходимо рассмотреть три возможных случая:

  1. Узел является листом (не имеет потомков): Самый простой случай. Узел просто удаляется из памяти, а указатель родителя на него устанавливается в NULL.
  2. Узел имеет только одного потомка: Удаляемый узел заменяется на своего единственного потомка. Мы как бы "пробрасываем" ссылку от родителя удаляемого узла напрямую к его потомку.
  3. Узел имеет двух потомков: Наиболее сложный случай. Удаляемый узел нельзя просто стереть, иначе мы потеряем связи с двумя ветвями дерева. Вместо него ставится узел-заместитель, который сохранит правильность дерева поиска. Таким узлом может быть либо самый правый элемент левого поддерева (максимум среди меньших), либо самый левый элемент правого поддерева (минимум среди больших).
Три случая удаления из дерева поиска на одном дереве: лист 10, вершина 90 с одним потомком и корень 50 с двумя потомками
Одно дерево, три случая: какой именно узел удаляют, тот случай и разбирают

Визуализация: Удаление узла с двумя потомками

Рассмотрим удаление корня 50 из того же дерева. Мы находим самый правый узел в левом поддереве (в данном случае это 35) и заменяем удаляемый узел на него. При этом дерево сохраняет свои свойства сортировки.

Удаление корня 50 с двумя потомками: на его место встает ключ 35 - самый правый узел левого поддерева
Ключ 35 больше всех ключей слева и меньше всех справа, поэтому порядок сохраняется
А как в Python? Поскольку в стандартной библиотеке Python нет деревьев поиска (используются хеш-таблицы dict и set), операции удаления выполняются с помощью ключевого слова del или метода pop(), что обеспечивает удаление за время O(1). Если же вы пишете собственный класс BinarySearchTree в Python, вам придется запрограммировать алгоритм поиска узла-заместителя с помощью объектных ссылок точно так же, как это делается через указатели в языке Си.

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

  • Вставка в дерево поиска всегда происходит на место пустого указателя (листа). Маршрут прокладывается так же, как и при поиске.
  • Удаление узла без потомков или с одним потомком - тривиальная операция переброса указателя.
  • Удаление узла с двумя потомками требует его замены на самый правый элемент левого поддерева (максимум среди меньших) или самый левый элемент правого поддерева (минимум среди больших).

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

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

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