Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 26

МІНІСТЕРСТВО ОСВІТИ ТА НАУКИ УКРАЇНИ

Національний технічний університет України «КПІ ім. Ігоря Сікорського»


Факультет інформатики та обчислювальної техніки
Кафедра інформаційних систем та технологій

Звіт
з комп’ютерного практикуму №8
«Основні типи двійкові дерева пошуку»
з дисципліни «Теорія алгоритмів»
Бригада - 9
Варіант №3

Виконали:
студенти гр. ІС-33:
Гуминюк А.Г., Овсяник О.В.,
Путіловський Н.Т.
Викладач:
ст. вик. Дорошенко К.С.

Київ – 2024
Тема: Основні типи двійкові дерева пошуку
Мета роботи: Дослідження характеристик продуктивності основних типів
двійкових дерев пошуку.
Теоретичні дані
Дерево пошуку з мінімальною висотою називається збалансованим, тобто
таким, в якому висота лівого і правого піддерев відрізняються не більше ніж на
одиницю.
Збалансоване воно тому, що в такому дереві в середньому потрібно
витратити найменшу кількість операцій для пошуку.
Щоб дерево мало мінімальну висоту, кількість вузлів лівого і правого
піддерев повинні максимально наближатися один до одного.
Для побудови такого дерева можна використати наступний принцип:
– отримання відсортованого масиву;
– середина кожного підрозділу масиву стає кореневим вузлом, а ліва і
права частини - відповідними для нього піддеревами;
– оскільки масив відсортований, то отримане дерево відповідає
визначенню бінарного дерева пошуку.

Червоно-чорні дерева
Червоно-чорні дерева — різновид збалансованих дерев, в яких за
допомогою спеціальних трансформацій гарантується, що висота дерева h не
буде перевищувати O (log n).
Зважаючи на те, що час виконання основних операцій на бінарних
деревах (пошук, видалення, додавання елементу) є O(h), ці структури даних на
практиці є набагато ефективнішими, аніж звичайні бінарні дерева пошуку.
Бінарне дерево називається червоно-чорним, якщо воно має такі
властивості:
– кожна вершина або червона, або чорна;
– корінь дерева — чорний;
– кожний лист (NIL) — чорний;
– якщо вершина червона, обидві її дочірні вершини чорні (інакше, батько
червоної вершини — чорний);
– усі прості шляхи від будь-якої вершини до листів мають однакову
кількість чорних вершин.
Такі властивості надають червоно-чорному дереву додаткового
обмеження: найдовший шлях з кореня до будь-якого листа перевищує
найкоротший шлях не більше ніж вдвічі. В цьому сенсі таке дерево можна
назвати збалансованим. Зважаючи на те, що час виконання основних операцій з
бінарними деревами пошуку залежить від висоти, таке обмеження гарантує
їхню ефективність в найгіршому випадку, чого звичайні бінарні дерева
гарантувати не можуть.
Етапи розробки програми:
Постановка задачі.
Є певна кількість приладів. Кожний прилад має свою унікальну
потужність. Необхідно швидко знайти той прилад, що точно відповідає заданій
потужності заряду робота. Потужність задається з клавіатури. Від кожного
приладу є лише два шляхи для робота, але він точно знає де потужність буде
збільшуватися та зменшуватися в порівнянні з поточним. Якщо для робота не
буде знайдено відповідний пристрій, то для його порятунку треба терміново
додати такий пристрій. Для обох випадків побудувати шлях знаходження
пристрою. Якщо пристрій зламався, то він вже не має можливості допомогти
роботу, тому видаляється з мережи приладів та надається повідомлення про це.
Для кожного варіанту кількість елементів змінюється декілька разів
починаючи з 15 та йде на збільшення.

Модель.
Основні величини:
Назва Тип Опис
height int Висота вузла
Size Int Розмір
newhead Node* Вткористовується для повороту вузла
root Node* Вузол
left RBNode* Змінна для повороту вліво
right RBNode* Змінна для повороту вправо
key long long Ключ
Color Int Колір вузла в червоно-чорному дереві
Додаткові величини:
Назва Тип Опис
Змінна для обрахунку часу, за який виконується
exec_time unsigned int
програма
Розробка алгоритму.
Додавання вузла у збалансованому дереві:

Видалення вузла у збалансованому дереві:


Пошук вузла у збалансованому дереві:

Додавання вузла у червоно-чорному дереві:


Видалення вузла у червоно-чорному дереві:

Пошук вузла у червоно-чорному дереві:


Аналіз алгоритмів та їх складності.
Дерево пошуку мінімальної висоти називається збалансованим, тобто
таким, де висоти лівого і правого піддерев відрізняються не більше ніж на одну
одиницю. Воно вважається збалансованим, тому що забезпечує мінімальну
кількість операцій для пошуку.
Реалізація червоно-чорних дерев повинна дотримуватися певних правил,
зокрема: кожен вузол має бути або червоним, або чорним, і кожен червоний
вузол повинен мати чорного батька.
Збалансовані і червоно-чорні дерева зазвичай реалізуються з
використанням вузлів, що містять дані та вказівники на нащадків. У
збалансованих деревах вузли можуть містити додаткову інформацію про
баланс, наприклад, висоту піддерева, для підтримки рівномірного розподілу.
Реалізація включає методи для вставки, видалення та пошуку вузлів з
певним ключем, забезпечуючи підтримку балансу дерева через ротації та
перебудови. У збалансованих деревах, таких як AVL-дерева, баланс
підтримується за допомогою ротацій. У червоно-чорних деревах баланс
досягається через перебудову і перекрашування вузлів.
Важливим аспектом є ефективність операцій вставки, видалення та
пошуку в залежності від розміру дерева. Обидва типи дерев зазвичай мають
логарифмічну складність операцій, проте червоно-чорні дерева можуть
вимагати менше перебудов, що призводить до кращої амортизованої
складності.
Реалізація алгоритму.
C++
avl.h:
#pragma once

#include "binarysearchtree.h"
#include <stack>

class avl : public BinarySearchTree


{
public:

avl() {
root = nullptr;
}

// Деструктор класу avl, видаляє всі вузли дерева


~avl() {
if (root == nullptr) return;
std::stack<Node*> st;
st.push(root);
while (!st.empty()) {
auto t = st.top();
st.pop();
if (t->getL() != nullptr) st.push(t->getL());
if (t->getR() != nullptr) st.push(t->getR());
delete t;
}
}

// Вставка нового елемента в дерево


void insert(long long val) {
root = _insert(root, val);
}

// Видалення елемента з дерева


void erase(long long val) {
root = _erase(root, val);
}

// Пошук елемента в дереві


Node* find(long long val, QString& path) {
return _find(root, val, path);
}

// Отримання кореневого вузла дерева


Node* getRoot() {
return root;
}

// Оновлення висоти вузлів дерева


void updateHeight(Node* head) {
if (head == nullptr) return;
updateHeight(head->left);
updateHeight(head->right);
head->height = 1 + std::max(height(head->left), height(head->right));
}

private:
Node* root; // Кореневий вузол дерева

// Отримання висоти вузла


int height(Node* head) {
if (head == nullptr) return 0;
return head->height;
}

// Поворот вузла вправо


Node* rightRotation(Node* head) {
Node* newhead = head->left;
head->left = newhead->right;
newhead->right = head;
head->height = 1 + std::max(height(head->left), height(head->right));
newhead->height = 1 + std::max(height(newhead->left), height(newhead->right));
return newhead;
}

// Поворот вузла вліво


Node* leftRotation(Node* head) {
Node* newhead = head->right;
head->right = newhead->left;
newhead->left = head;
head->height = 1 + std::max(height(head->left), height(head->right));
newhead->height = 1 + std::max(height(newhead->left), height(newhead->right));
return newhead;
}

// Вставка нового вузла


Node* _insert(Node* head, long long x) {
if (head == nullptr) {
return new Node(x);
}
if (x < head->getKey()) head->left = _insert(head->left, x);
else if (x > head->getKey()) head->right = _insert(head->right, x);

head->height = 1 + std::max(height(head->left), height(head->right));


int bal = height(head->left) - height(head->right);
// Балансування дерева
if (bal > 1) {
if (x < head->left->getKey()) {
return rightRotation(head);
}
else {
head->left = leftRotation(head->left);
return rightRotation(head);
}
}
else if (bal < -1) {
if (x > head->right->getKey()) {
return leftRotation(head);
}
else {
head->right = rightRotation(head->right);
return leftRotation(head);
}
}
return head;
}

// Видалення вузла
Node* _erase(Node* head, long long x) {
if (head == nullptr) return nullptr;

if (x < head->key) {
head->left = _erase(head->left, x);
}
else if (x > head->key) {
head->right = _erase(head->right, x);
}
else {
Node* r = head->right;
if (head->right == nullptr) {
Node* l = head->left;
delete(head);
head = l;
}
else if (head->left == nullptr) {
delete(head);
head = r;
}
else {
while (r->left != nullptr) r = r->left;
head->key = r->key;
head->right = _erase(head->right, r->key);
}
}

if (head == nullptr) return head;

head->height = 1 + std::max(height(head->left), height(head->right));


int bal = height(head->left) - height(head->right);

// Балансування дерева
if (bal > 1) {
if (height(head->left) >= height(head->right)) {
return rightRotation(head);
}
else {
head->left = leftRotation(head->left);
return rightRotation(head);
}
}
else if (bal < -1) {
if (height(head->right) >= height(head->left)) {
return leftRotation(head);
}
else {
head->right = rightRotation(head->right);
return leftRotation(head);
}
}
return head;
}

// Пошук вузла за значенням


Node* _find(Node* head, long long x, QString& path) {
if (head == nullptr) return nullptr;

long long k = head->key;


path += QString::number(k) + " "; // Додаємо поточний ключ до шляху

if (k > x) {
path += "(Left) -> "; // Додаємо напрямок пошуку (ліворуч)
return _find(head->left, x, path); // Переміщуємося вліво і додаємо ключ до шляху
}
if (k < x) {
path += "(Right) -> "; // Додаємо напрямок пошуку (праворуч)
return _find(head->right, x, path); // Переміщуємося вправо і додаємо ключ до шляху
}

// Якщо знайдено потрібний елемент, додаємо його ключ і повертаємо його


path += "|Found (" + QString::number(k) + ")";
return head;
}
};

-------------------

#pragma once

#include "binarysearchtree.h"

// Структура вузла червоно-чорного дерева

struct RBNode : Node {

RBNode* left;

RBNode* right;

NodeItem* node = nullptr;

int size, height;

long long key;

int color; // 0 - красный, 1 - черный

RBNode* parent;

RBNode(long long k, int _col = 0, RBNode* _l = nullptr, RBNode* _r = nullptr, RBNode* _p = nullptr)

: left(_l), right(_r), key(k), color(_col), parent(_p) {}

void updateSize() {

if (left != nullptr) left->updateSize();

if (right != nullptr) right->updateSize();


size = (left != nullptr ? left->size : 0) + (right != nullptr ? right->size : 0) + 1;

RBNode* getL() {

return this->left;

RBNode* getR() {

return this->right;

long long getKey() {

return this->key;

QColor getColor() {

return color == 0 ? QColor(255, 0, 0) : QColor(0, 0, 0);

};

// Клас червоно-чорного дерева

class RBTree : public BinarySearchTree {

private:

int n; // Кількість вузлів

RBNode* root; // Кореневий вузол

protected:

// Поворот вліво

void rotateLeft(RBNode*& ptr) {

RBNode* right_child = ptr->right;

ptr->right = right_child->left;

if (ptr->right != nullptr)

ptr->right->parent = ptr;

right_child->parent = ptr->parent;

if (ptr->parent == nullptr)
root = right_child;

else if (ptr == ptr->parent->left)

ptr->parent->left = right_child;

else

ptr->parent->right = right_child;

right_child->left = ptr;

ptr->parent = right_child;

// Поворот вправо

void rotateRight(RBNode*& ptr) {

RBNode* left_child = ptr->left;

ptr->left = left_child->right;

if (ptr->left != nullptr)

ptr->left->parent = ptr;

left_child->parent = ptr->parent;

if (ptr->parent == nullptr)

root = left_child;

else if (ptr == ptr->parent->left)

ptr->parent->left = left_child;

else

ptr->parent->right = left_child;

left_child->right = ptr;

ptr->parent = left_child;

// Виправлення після вставки

void fixInsert(RBNode*& ptr) {

RBNode* parent = nullptr;

RBNode* grandparent = nullptr;

while (ptr != root && getColor(ptr) == 0 && getColor(ptr->parent) == 0) {

parent = ptr->parent;
grandparent = parent->parent;

if (parent == grandparent->left) {

RBNode* uncle = grandparent->right;

if (getColor(uncle) == 0) {

setColor(uncle, 1);

setColor(parent, 1);

setColor(grandparent, 0);

ptr = grandparent;

else {

if (ptr == parent->right) {

rotateLeft(parent);

ptr = parent;

parent = ptr->parent;

rotateRight(grandparent);

std::swap(parent->color, grandparent->color);

ptr = parent;

else {

RBNode* uncle = grandparent->left;

if (getColor(uncle) == 0) {

setColor(uncle, 1);

setColor(parent, 1);

setColor(grandparent, 0);

ptr = grandparent;

else {

if (ptr == parent->left) {

rotateRight(parent);

ptr = parent;

parent = ptr->parent;

rotateLeft(grandparent);

std::swap(parent->color, grandparent->color);

ptr = parent;
}

setColor(root, 1);

// Виправлення після видалення

void fixDelete(RBNode*& node) {

if (node == nullptr)

return;

if (node == root) {

root = nullptr;

return;

if (getColor(node) == 0 || getColor(node->left) == 0 || getColor(node->right) == 0) {

RBNode* child = node->left != nullptr ? node->left : node->right;

if (node == node->parent->left) {

node->parent->left = child;

if (child != nullptr)

child->parent = node->parent;

setColor(child, 1);

delete (node);

else {

node->parent->right = child;

if (child != nullptr)

child->parent = node->parent;

setColor(child, 1);

delete (node);

else {

RBNode* sibling = nullptr;

RBNode* parent = nullptr;

RBNode* ptr = node;


setColor(ptr, 2);

while (ptr != root && getColor(ptr) == 2) {

parent = ptr->parent;

if (ptr == parent->left) {

sibling = parent->right;

if (getColor(sibling) == 0) {

setColor(sibling, 1);

setColor(parent, 0);

rotateLeft(parent);

else {

if (getColor(sibling->left) == 1 && getColor(sibling->right) == 1) {

setColor(sibling, 0);

if (getColor(parent) == 0)

setColor(parent, 1);

else

setColor(parent, 2);

ptr = parent;

else {

if (getColor(sibling->right) == 1) {

setColor(sibling->left, 1);

setColor(sibling, 0);

rotateRight(sibling);

sibling = parent->right;

setColor(sibling, parent->color);

setColor(parent, 1);

setColor(sibling->right, 1);

rotateLeft(parent);

break;

else {

sibling = parent->left;
if (getColor(sibling) == 0) {

setColor(sibling, 1);

setColor(parent, 0);

rotateRight(parent);

else {

if (getColor(sibling->left) == 1 && getColor(sibling->right) == 1) {

setColor(sibling, 0);

if (getColor(parent) == 0)

setColor(parent, 1);

else

setColor(parent, 2);

ptr = parent;

else {

if (getColor(sibling->left) == 1) {

setColor(sibling->right, 1);

setColor(sibling, 0);

rotateLeft(sibling);

sibling = parent->left;

setColor(sibling, parent->color);

setColor(parent, 1);

setColor(sibling->left, 1);

rotateRight(parent);

break;

if (node == node->parent->left)

node->parent->left = nullptr;

else

node->parent->right = nullptr;

delete(node);

setColor(root, 1);
}

// Отримання кольору вузла

int getColor(RBNode*& t) {

if (t == nullptr) return 1;

return t->color;

// Встановлення кольору вузла

void setColor(RBNode*& t, int col) {

if (t == nullptr) return;

t->color = col;

// Вставка нового вузла

RBNode* _insert(RBNode*& root, RBNode*& ptr) {

if (root == nullptr)

return ptr;

if (ptr->key < root->key) {

root->left = _insert(root->left, ptr);

root->left->parent = root;

else if (ptr->key > root->key) {

root->right = _insert(root->right, ptr);

root->right->parent = root;

return root;

// Видалення вузла

RBNode* _erase(RBNode*& root, long long data) {

if (root == nullptr)

return root;

if (data < root->key)

return _erase(root->left, data);


if (data > root->key)

return _erase(root->right, data);

if (root->left == nullptr || root->right == nullptr)

return root;

RBNode* temp = minValueNode(root->right);

root->key = temp->key;

auto _tmp = root->node;

root->node = temp->node;

temp->node = _tmp;

return _erase(root->right, temp->key);

// Знаходження вузла з мінімальним значенням

RBNode* minValueNode(RBNode*& node) {

RBNode* ptr = node;

while (ptr->left != nullptr)

ptr = ptr->left;

return ptr;

// Пошук вузла за значенням

RBNode* _find(RBNode*& head, long long x, QString& path) {

if (head == nullptr) return nullptr;

long long k = head->key;

path += QString::number(k) + " "; // Додаємо поточний ключ до шляху

if (k > x) {

path += "(Left) -> "; // Додаємо напрямок пошуку (ліворуч)

return _find(head->left, x, path); // Переміщуємося вліво і додаємо ключ до шляху

if (k < x) {

path += "(Right) -> "; // Додаємо напрямок пошуку (праворуч)

return _find(head->right, x, path); // Переміщуємося вправо і додаємо ключ до шляху

}
// Якщо знайдено потрібний елемент, додаємо його ключ і повертаємо його

path += "|Found (" + QString::number(k) + ")";

return head;

// Отримання висоти вузла

int height(RBNode* head) {

if (head == nullptr) return 0;

return head->height;

public:

RBTree() {

root = nullptr;

n = 0;

// Функція вставки

void insert(long long n) {

RBNode* node = new RBNode(n);

root = _insert(root, node);

fixInsert(node);

this->n++;

// Функція видалення

void erase(long long data) {

if (n == 2) {

if (data == root->key) {

auto t = root;

root = t->left == nullptr ? t->right : t->left;

delete t;

root->color = 1;

root->parent = nullptr;

else {

RBNode* node = _erase(root, data);


fixDelete(node);

else {

RBNode* node = _erase(root, data);

fixDelete(node);

n--;

// Функція пошуку

RBNode* find(long long x, QString& path) {

return _find(root, x, path);

// Отримання кореневого вузла

RBNode* getRoot() {

return root;

// Оновлення висоти вузлів

void updateHeight(RBNode* head) {

if (head == nullptr) return;

updateHeight(head->left);

updateHeight(head->right);

head->height = 1 + std::max(height(head->left), height(head->right));

};
Перевірка програми.
Додавання елемента: Видалення
3

2,5

1,5
Час (с)

0,5

0
0 5 10 15 20 25
-0,5
Кількість елементів

Червоний графік – збалансоване дерево


Фіолетовий графік – червоно-чорне дерево

Видалення елемента: Видалення


3

2,5

1,5
Час (с)

0,5

0
0 5 10 15 20 25
-0,5
Кількість елементів

Червоний графік – збалансоване дерево


Фіолетовий графік – червоно-чорне дерево

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

2,5

1,5
Час (с)

0,5

0
0 5 10 15 20 25
-0,5
Кількість елементів

Червоний графік – збалансоване дерево


Фіолетовий графік – червоно-чорне дерево

Таблиця тестування.

Дані Результат Призначення

Обрання “АВЛ- Програма виконується, будуючи АВЛ-


Відомий
дерева” дерево.
Обрання “Червоно- Програма виконується, будуючи червоно-
Відомий
чорного дерева” чорне дерево.
Натискання кнопки
“Додати прилад…” Вузол з відповідною потужністю буде
Відомий
після введення додано у дерево
потужності
Натискання кнопки
“Видалення Якщо існує вузол з введеною потужністю,
зламаного приладу” Відомий його буде видалено, дерево буде
після введення перебалансовано
потужності
Натискання кнопки
Якщо існує вузол з введеною потужністю,
“Знайти прилад,
Відомий його буде підсвічено жовтим кольором, а
який…” після
також буде виведено шлях до нього.
введення потужності
Скріншоти роботи програми.
Контрольні питання
1) Перерахуйте та прокоментуйте переваги та недоліки 2-3-4 дерев. Поясніть
основні принципи їх побудови.
Переваги 2-3-4 дерев:
Ефективність вставки та видалення: Дякуючи збалансованості дерева,
операції вставки та видалення можуть бути виконані ефективно з витратою
часу O(log n). Збалансованість: 2-3-4 дерево завжди знаходиться у
збалансованому стані, що означає, що всі гілки мають однакову довжину від
кореня до листків. Це дозволяє швидко здійснювати доступ до будь-якого
елемента дерева, оскільки пошук відбувається на глибині O(log n).
Швидкий доступ до даних: Завдяки збалансованості і можливості
збереження даних в листках дерева, доступ до будь-якого елемента
відбувається швидко з витратою часу O(log n).
Недоліки 2-3-4 дерев:
Складність реалізації: реалізація збалансованого 2-3-4 дерева – складне
завдання порівняно з іншими видами бінарних дерев.
Висока вартість пам'яті: зберігання додаткових вузлів, збільшує вартість
пам'яті.
Можна замінити іншими структурами даних: існують інші структури
даних, такі як B-дерева та хеш-таблиці, які можуть бути більш ефективними в
певних сценаріях використання.
Основні принципи побудови 2-3-4-дерев:
Всі листки рівномірно розподілені по всій висоті дерева.
У дерева немає повторень, тобто кожний вузол містить унікальний ключ.
Кожен вузол може мати 2, 3 або 4 нащадки, тому дерево може мати 2, 3
або 4 дочірніх вузла на кожному рівні.
Ключі всіх вузлів у лівому піддереві менші, ніж ключ вузла, а ключі в
правому піддереві більші.
Якщо вузол має три нащадки, то середній ключ буде батьківським
ключем для двох нащадків, які знаходяться по його боки, і він буде зберігатися
в самому вузлі. Таким чином, він розділяє ключі, які менші за нього, від ключів,
які більші за нього.
Якщо вузол має чотири нащадки, то два ключі будуть знаходитися в
самому вузлі, а два інших будуть батьківськими ключами для двох пар
нащадків, які знаходяться по його боки.

2. Перерахуйте та прокоментуйте переваги та недоліки червоно чорних дерев.


Поясніть основні принципи їх побудови.
Червоно-чорні дерева - це вид самобалансуючихся бінарних пошукових
дерев, який забезпечує більш оптимальну операційну складність для основних
операцій (додавання, видалення, пошук) за рахунок підтримки балансу дерева.
Переваги червоно-чорних дерев:
Забезпечують операційну складність O(log n) для операцій додавання,
видалення та пошуку, що робить їх ефективними для роботи з великими
об'ємами даних.
Гарантовано збалансовані, тому гарантовано максимальна операційна
швидкість.
Можуть використовуватися для різноманітних операцій з даними,
включаючи знайдення максимального та мінімального значення, додавання та
видалення вузлів, пошук, ітерацію тощо.
Недоліки червоно-чорних дерев:
Потребують більше пам'яті для зберігання кольору кожного вузла.
Операції додавання, видалення та пошуку мають більшу складність
порівняно з бінарними деревами, оскільки потрібні додаткові перевірки на
збереження правил кольорів вузлів.
Основні принципи побудови червоно-чорних дерев:
Кожен вузол дерева має колір - червоний або чорний.
Корінь дерева завжди є чорним.
Кожний лист (NULL) - чорний.
Якщо вузол червоний, то його дочірні вузли завжди чорні.
Усі прості шляхи від вузла до його листя мають однакову кількість
чорних вузлів. Загалом, червоно-чорні дерева є ефективними та гнучкими
структурами даних, які можуть використовуватися для різних задач.

3. Перерахуйте та прокоментуйте переваги та недоліки навантажених дерев.


Навантажені дерева - це різновид бінарних дерев пошуку, де кожен вузол
містить значення (навантаження) та може мати два або більше дочірніх вузли.
Основні переваги навантажених дерев:
Можуть зберігати та шукати довільні типи даних, а не тільки цілі числа
або рядки;
Можуть містити декілька значень з однаковим ключем;
Забезпечують більш ефективний пошук, вставку та видалення даних порівняно
зі звичайними масивами.
Основні недоліки навантажених дерев:
1) Потребують більше пам'яті, ніж звичайні масиви;
2) Складність реалізації та розуміння може бути вищою порівняно зі
звичайними масивами.

You might also like