Удаление узла дерева программирование

Разбираемся с двоичными деревьями поиска

Перевод статьи «Understanding Binary Search Trees».

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

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

Двоичное дерево и двоичное дерево поиска

У узла двоичного дерева есть самое большее два потомка: левый и правый. Это определение позволяет нам более эффективно писать алгоритмы для вставки, поиска и удаления узлов. Обратите внимание на иллюстрацию ниже. На примере этого двоичного дерева мы рассмотрим словарь ключевых слов, которые далее будут использоваться в статье.

Как вы, вероятно, догадались, двоичное дерево поиска (binary search tree, BST) это подвид двоичного дерева. Ключевое отличие BST состоит в том, что здесь узлы хранятся особым образом: узлы с меньшими значениями хранятся слева, а с большими — справа. Если вы сами не заметили, обращаю ваше внимание, что на рисунке изображено именно двоичное дерево поиска. Возможно, вам пока сложно понять, как упорядочены узлы, но не волнуйтесь, мы рассмотрим это подробнее в следующих разделах!

Создание классов Node и BST

Как обычно, я призываю читателей писать код вместе со мной и проверять все, о чем говорится. Для начала мы создадим наш класс Node , который будет представлять узлы нашего двоичного дерева поиска (BST):

Далее мы объявим базовую структуру нашего класса BinarySearchTree :

Наш следующий шаг — реализация нескольких методов. А именно:

  • insert(data)
  • inOrderTraverse()
  • preOrderTraverse()
  • postOrderTraverse()
  • search(data)
  • remove(data)

Вставка узла в BST

Чтобы вставить в дерево новый узел, нужно пройти два шага:

  1. Проверить, является ли наша вставка особым случаем. Другими словами, нам нужно проверить, не должен ли быть первым в нашем дереве тот узел, который мы хотим добавить. Если он таки должен быть первым, нам просто нужно направить root на этот новый узел, создав экземпляр класса Node и назначив его свойству root .
  2. Добавить узел на другую позицию (не root ).
insert(data) < let newNode = new Node(data); if(this.root === null) < this.root = newNode; >else < this.insertNode(this.root, newNode); // helper method below >> insertNode(node, newNode) < if(newNode.data < node.data) < if(node.left === null) < node.left = newNode; >else < this.insertNode(node.left, newNode); >> else < if(node.right === null) < node.right = newNode; >else < this.insertNode(node.right, newNode); >> >

В общем, insert(data) создает новый Node со значением data , и если дерево пустое, этот узел устанавливается в качестве root . В противном случае вызывается insertNode(this.root, newNode) .

insertNode(node, newNode) это наш вспомогательный метод, ответственный за сравнение данных нового узла с данными текущего узла и соответственное смещение влево или вправо. Это делается рекурсивно, пока не будет найден подходящий узел с нулевым значением, куда может быть добавлен новый узел.

Читайте также:  Денежное дерево семейство кактусовых

Рассмотрим пример. Если мы выполним следующий код…

const BST = new BinarySearchTree(); BST.insert(11); // establishes root node BST.insert(7); BST.insert(9); BST.insert(15); . BST.insert(6);

то сможем проиллюстрировать последнюю вставку такой диаграммой:

Обход двоичного дерева поиска

Обход дерева это процедура посещения всех узлов дерева и осуществления операций с каждым узлом. Главный вопрос состоит в том, как нам идти. Есть три основных подхода: центрированный (in-order), прямой (pre-order) и обратный (post-order).

Центрированный обход

Совершая центрированный обход, мы посещаем все узлы в восходящем порядке, начиная с заданного узла (опционально) и выполняя указанную функцию callback (тоже опционально). Здесь мы опять воспользуемся рекурсией:

inOrderTraverse(node, callback) < if(node != null) < this.inOrderTraverse(node.left, callback); callback(node.data); this.inOrderTraverse(node.right, callback); >>

Следующая диаграмма показывает путь нашего inOrderTraverse :

Прямой обход

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

preOrderTraverse(node, callback) < if(node != null) < callback(node.data); this.preOrderTraverse(node.left, callback); this.preOrderTraverse(node.right, callback); >>

Обратный обход

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

postOrderTraverse(node, callback) < if(node != null) < this.postOrderTraverse(node.left, callback); this.postOrderTraverse(node.right, callback); callback(node.data); >>

Поиск значений в BST

В нашей реализации node представляет текущий узел, а data — искомое значение:

search(node, data) < if(node === null) < return null; >else if(data < node.data) < return this.search(node.left, data); >else if(data > node.data) < return this.search(node.right, data); >else < return node; >>

Я советую вам протестировать этот ваш код и посмотреть в console.log, какие узлы при этом посещаются. Даже если вы не писали код со мной вместе, изучите одну из диаграмм в статье и попробуйте спрогнозировать путь метода при поиске заданного значения. Вы также заметите, насколько просто найти минимальное и максимальное значение!

Удаление узла из BST

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

remove(data) < this.root = this.removeNode(this.root, data); // helper method below >removeNode(node, data) < if(node === null) < return null; // if data to be deleted is less than the root's data, move to the left subtree >else if(data < node.data) < node.left = this.removeNode(node.left, data); return node; // if data to be deleted is greater than the root's data, move to the right subtree >else if(data > node.data) < node.right = this.removeNode(node.right, data); return node; // if data is similar to the root's data, delete the node >else < // delete node with no children (leaf node) if(node.left === null && node.right === null) < node = null; return node; >// delete node with one child if(node.left === null) < node = node.right; return node; >else if(node.right === null) < node = node.left; return node; >// delete node with two children // minimum node of the right subtree is stored in newNode let newNode = this.minNode(node.right); node.data = newNode.data; node.right = this.removeNode(node.right, newNode.data); return node; > >

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

Читайте также:  Топор чертеж из дерева

Удаление листового узла

Первый сценарий — с листовым узлом, т. е., таким, у которого нет ни левого, ни правого потомка. В этом случае нам нужно удалить узел, назначив ему null. Но не забудьте, что нужно также позаботиться о ссылках из родительского узла. На диаграмме показано удаление листового узла:

Удаление узла с одним потомком

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

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

Третий и последний сценарий предполагает, что у нашего узла есть оба потомка (левый и правый). Чтобы удалить такой узел, нужно следовать такому алгоритму действий:

  1. Найдя узел, который нужно удалить, нужно найти минимальный узел из его правого поддерева (на диаграмме — затененная часть дерева).
  2. Далее вы можете обновить значение удаляемого узла, назначив ему ключ минимального узла из его правого поддерева. Таким образом вы замените ключ узла, а это означает, что узел будет эффективно удален.
  3. Теперь у вас в дереве есть два узла с одинаковым ключом, чего не должно быть (на диаграмме — 18). Вам нужно удалить минимальный узел из правого поддерева, поскольку теперь он встал на место удаленного узла.
  4. Наконец, надо возвратить ссылку на обновленный узел его родителю.

Заключение

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

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

Источник

Как удалить узел в дереве C++?

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

#include using namespace std; struct Node < int key = NULL; //ключ int //индекс int level = 0; //уровень в иерархии Node* parent = nullptr; //указатель на родителя Node** son = nullptr; //массив указателей на сыновей Node* leftmost_child = nullptr; int count_son = 0; //количество сыновей >; Node* createTree(Node* node, int id, int level) < node->id = id; node->level = level; cout level id > node->count_son >> node->key; node->son = new Node*[node->count_son]; level++; for (int i = 0; i < node->count_son; i++) < node->son[i] = new Node; node->son[i]->id = id + i; node->son[i]->level = level; > for (int i = 0; i < node->count_son; i++) createTree(node->son[i], id + i, level); return node; > void printTree(Node* node) < if (node) < for (int i = 0; i < node->level; i++) cout son != NULL) cout level id key count_son != 0) for (int i = 0; i < node->count_son; i++) printTree(node->son[i]); > > void nodeKill(Node* node, int key) < >int main() < size_t number; int key; Node* root; setlocale(LC_ALL, "RUS"); root = new Node; root = createTree(root, 0, 0); cout > key; nodeKill(root, key); cout

Когда вызываете в цикле node->son[i] = new Node; , также прописывайте и указатель на parent — node->son[i]->parent = node; . Для root пропишите отдельно в main. Кстати, не понял, что такое у вас id , но вы создаете таким кодом массу узлов с одинаковым значением id

Читайте также:  Статья дома из дерева

1 ответ 1

В принципе алгоритм такой:
если потомок 1

  • в родительском узле указателю на удаляемый узел присваиваем указатель на потомка удаляемого узла
  • в потомке указателю parent присваиваем адрес родительского узла удаляемого узла
  • удаляем узел
  • в родительском узле указателю на удаляемый узел присваиваем указатель на самого левого потомка удаляемого узла
  • в самом левом потомке указателю parent присваиваем адрес родительского узла удаляемого узла
  • добавляем указатели на сыновей из удаляемого узла к массиву указателей на сыновей самого левого потомка
  • удаляем узел

Примерный код когда несколько потомков:

Node *DelNode = ; // каким-то образом вычисляем удаляемый узел Node *ParentNode = DelNode->parent; // запоминаем родителя Node *NewNode = DelNode->son[0]; // замещающим узлом будет самый левый потомок // находим в массиве указателей на потомков родительского узла указатель на текущий элемент. for( int i = 0; i < ParentNode->count_son; i++) if( ParentNode->son[i] == DelNode ) < ParentNode->son[i] = NewNode; // в массиве указателей на потомков родительского узла указателю на удаляемый элемент присваиваем значение самого левого потомка удаляемого узла break; > NewNode->parent = ParentNode; // указателю родителя дочернего узла присваиваем адрес родительского узла удаляемого элемента // теперь удаляемый элемент исключен из дерева и как-бы висит в воздухе вместе с потомками, кроме самого левого // нужно оставшихся потомков перепривязать к новому узлу Node *tmp = new Node[NewNode->count_son + DelNode->count_son - 1];// выделяем новую память под массив указателей на потомков в новом узле for(int j=0; jcount_son; j++) // копируем все указатели на потомков в новый массив tmp[j] = NewNode->son[j]; for(int j=1; jcount_son; j++) // копируем все указатели на потомков из удаляемого узла в новый массив нового узла tmp[NewNode->count_son + j - 1] = DelNode->son[j]; delete[] NewNode->son; // удаляем старый массив указателей на потомков NewNode->son = tmp; // запоминаем новый массив указателей NewNode->count_son = NewNode->count_son + DelNode->count_son - 1; // сохраняем новое количество потомков // Теперь удаляем старый узел delete[] DelNode->son; // сначала удаляем массив указателей на потомков delete DelNode; // а потом - сам узел 

Несколько замечаний по узлу дерева:

  • key — неиспользуемое поле
  • level — уровень иерархии нет особого смысла хранить, если этот параметр не используется в каких-то вычислениях
  • Node* leftmost_child — бессмысленное поле, только усложняющее логику. Если речь о самом левом элементе в массиве, то это son[0]
  • если разрешено условиями задачи, то для хранения указателей на потомков лучше использовать стандартные контейнеры vector<> или list<> . Тогда не нужно будет выделять/удалять память, количество элементов не надо хранить — всегда можно получить от контейнера и т.д.
    Как-то так:
struct Node < int //индекс Node* parent = nullptr; //указатель на родителя vectorson; //массив указателей на сыновей >; 

Источник

Оцените статью