Реализация бинарного дерева поиска в СИ


5 мин чтения
Реализация бинарного дерева поиска в СИ

Бинарное дерево поиска — это набор узлов, который удовлетворяет следующим условиям:

  1. Он может быть пустым.
  2. Либо обладает следующими элементами: корнем, правым и левым поддеревом.
  3. Ключи правого поддерева больше ключей левого поддерева.

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

Разновидности деревьев

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

Начало начал

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

typedef struct tree
{
    int key;
    struct tree *left;
    struct tree *right;
    struct tree *parent;
} node;

Комментарии к коду

  • Используем typedef для создания нового типа, чтобы в дальнейшем не писать слово struct.
  • int key — ключ, может быть любого типа.
  • struct tree *left —  указатель на левое поддерево.
  • struct tree *right — указатель на правое поддерево.
  • struct tree *parent — указатель на родителя.
  • node — название структуры.

Инициализируем дерево

node *create(node *root, int key)
{
// Выделение памяти под корень дерева
    node *tmp = malloc(sizeof(node));
// Присваивание значения ключу
    tmp -> key = key;
// Присваивание указателю на родителя значения NULL
    tmp -> parent = NULL;
// Присваивание указателю на левое и правое поддерево значения NULL
    tmp -> left = tmp -> right = NULL;
    root = tmp;
    return root;
}

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

Добавим узел в дерево

node *add(node *root, int key)
{
    node *root2 = root, *root3 = NULL;
// Выделение памяти под узел дерева
    node *tmp = malloc(sizeof(node));
// Присваивание значения ключу
    tmp -> key = key;
/* Поиск нужной позиции для вставки (руководствуемся правилом 
вставки элементов, см. начало статьи, пункт 3) */
    while (root2 != NULL)
    {
        root3 = root2;
        if (key < root2 -> key)
            root2 = root2 -> left;
        else
            root2 = root2 -> right;
    }
/* Присваивание указателю на родителя значения указателя root3 
(указатель root3 был найден выше) */
    tmp -> parent = root3;
// Присваивание указателю на левое и правое поддерево значения NULL
    tmp -> left = NULL;
    tmp -> right = NULL;
/* Вставляем узел в дерево (руководствуемся правилом
вставки элементов, см. начало статьи, пункт 3) */
    if (key < root3 -> key) root3 -> left = tmp;
    else root3 -> right = tmp;
    return root;
}

Комментарии к коду

  • tmp -> left и tmp -> right имеют значение NULL, так как указатель tmp расположен в конце дерева.
  • Указатель root2 использовался для того, чтобы сохранить адрес на родителя вставляемого узла.
  • Мы не проверяем дерево на пустоту, так как ранее дерево был инициализировано (имеется корень).

Найдем нужный узел в дереве

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

node *search(node * root, int key)
{
// Если дерево пусто или ключ корня равен искомому ключу, то возвращается указатель на корень
    if ((root == NULL) || (root -> key == key))
        return root;
// Поиск нужного узла
    if (key < root -> key)
        return search(root -> left, key);
    else return search(root -> right, key);
}

Комментарий к коду

Данная функция рекурсивная, поэтому комментарий «Если дерево пусто или ключ корня равен искомому ключу, то возвращается указатель на корень» является не совсем верным, потому что root указывает на корень только во время первой итерации, далее root ссылается на другие узлы дерева, но из-за рекурсивности функции условие if ((root == NULL) || (root -> key == key)) будет проверяться всегда.

Найдем узел с минимальным и максимальным ключом

// Минимальный элемент дерева
node *min(node *root)
{
    node *l = root;
    while (l -> left != NULL)
        l = l -> left;
    return l;
}

// Максимальный элемент дерева
node *max(node *root)
{
    node *r = root;
    while (r -> right != NULL)
        r = r -> right;
    return r;
}

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

Удалим узел из дерева

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

  1. Рассмотрим самый простой случай: у удаляемого узла нет левого и правого поддерева. В данной ситуации мы просто удаляем данный лист (узел).
  2. У удаляемого узла одно поддерево. В данной ситуации мы просто удаляем данный узел, а на его место ставим поддерево.

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

Общая функция поиска следующего за удаляемым элемента:

node *succ(node *root)
{
    node *p = root, *l = NULL;
// Если есть правое поддерево, то ищем минимальный элемент в этом поддереве
    if (p -> right != NULL)
        return min(p -> right);
/* Правое дерево пусто, идем по родителям до тех пор,
пока не найдем родителя, для которого наше поддерево левое */
    l = p -> parent;
    while ((l != NULL) && (p == l -> right))
    {
        p = l;
        l = l -> parent;
    }
    return l;
}

Комментарий к коду

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

Функция удаления узла из дерева:

node *delete(node *root, int key)
{
// Поиск удаляемого узла по ключу
    node *p = root, *l = NULL, *m = NULL;
    l = search(root, key);
// 1 случай
    if ((l -> left == NULL) && (l -> right == NULL))
    {
        m = l -> parent;
        if (l == m -> right) m -> right = NULL;
        else m -> left = NULL;
        free(l);
    }
// 2 случай, 1 вариант - поддерево справа
    if ((l -> left == NULL) && (l -> right != NULL))
    {
        m = l -> parent;
        if (l == m -> right) m -> right = l -> right;
        else m -> left = l -> right;
        free(l);
    }
// 2 случай, 2 вариант - поддерево слева
    if ((l -> left != NULL) && (l -> right == NULL))
    {
        m = l -> parent;
        if (l == m -> right) m -> right = l -> left;
        else m -> left = l -> left;
        free(l);
    }
// 3 случай
    if ((l -> left != NULL) && (l -> right != NULL))
    {
        m = succ(l);
        l -> key = m -> key;
        if (m -> right == NULL)
            m -> parent -> left = NULL;
        else m -> parent -> left = m -> right;
        free(m);
    }
    return root;
}

Вывод элементов дерева

Принято выделять три типа обхода дерева:

  1. Обход дерева в прямом порядке, будет напечатано A B D C E G F H J.
    void preorder(node *root)
    {
        if (root == NULL)
            return;
        if (root -> key)
            printf("%d ", root -> key);
        preorder(root -> left);
        preorder(root -> right);
    }
    
  2. Обход дерева в обратном порядке, будет напечатано D B G E H J F C A.
    void postorder(node *root)
    {
        if (root == NULL)
            return;
        postorder(root -> left);
        postorder(root -> right);
        if (root -> key)
            printf("%d ", root -> key);
    }
    
  3. Обход дерева в симметричном порядке, будет напечатано D B A E G C H F J
    void inorder(node *root)
    {
        if (root == NULL)
            return;
        inorder(root -> left);
        if (root -> key)
            printf("%d ", root -> key);
        inorder(root -> right);
    }
    

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

Подписывайтесь на T4S.TECH в Telegram. Публикуем новости, обзоры и забавные факты о технологиях.

Вверх

🎉 Вы успешно подписались на новости сайта T4S.TECH!
OK