Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Пособие

.pdf
Скачиваний:
66
Добавлен:
22.03.2015
Размер:
1.35 Mб
Скачать

20.5 Бінарні дерева Бінарне дерево – це динамічна структура даних, що складається

з вузлів, кожен з яких містить, крім даних, не більше двох посилань на різні бінарні дерева. На кожен вузол є рівно одне посилання. Початковий вузол називається коренем дерева [19].

На рисунку 20.6 наведено приклад бінарного дерева. Вузол, що не має піддерев, називається листом. Вихідні вузли називаються предками, вхідні - нащадками. Висота дерева визначається кількістю рівнів, на яких розташовуються його вузли.

Рис. 20.6 Бінарне дерево

Якщо дерево організовано таким чином, що для кожного вузла всі ключі його лівого піддерева менше ключа цього вузла, а всі ключі його правого піддерева - більше, воно називається деревом пошуку. Однакові ключі не допускаються.

У дереві пошуку можна знайти елемент по ключу, рухаючись від кореня і переходячи на ліве або праве піддерево в залежності від значення ключа в кожному вузлі. Такий пошук набагато ефективніше пошуку за списком, оскільки час пошуку визначається висотою дерева, а вона пропорційна двійковому логарифму кількості вузлів.

Дерево є рекурсивною структурою даних, оскільки кожне піддерево також є деревом. Дії із такими структурами доцільно виконува-

161

ти за допомогою рекурсивних алгоритмів. Наприклад, функцію обходу всіх вузлів дерева в загальному вигляді можна описати так:

function way_arouncd ( дерево )

{

way_around ( ліве піддерево ) опрацювання кореня

way_around ( праве піддерево )

}

Можна обходити дерево і в іншому порядку, наприклад, спочатку корінь, потім піддерева, але наведена функція дозволяє отримати на виході відсортовану послідовність ключів, оскільки спочатку відвідуються вершини з меншими ключами, розташованими в лівому піддереві. Результат обходу дерева, зображеного на рис. 20.6: 1, 6, 8, 10, 20, 21, 25, 30.

Якщо у функції обходу перше звернення йде до правого піддерева, результат обходу буде іншим: 30, 25, 21, 20, 10, 8, 6, 1.

Таким чином, дерева пошуку можна застосовувати для сортування значень. При обході дерева вузли не видаляються.

Для бінарних дерев визначені операції:

-включення вузла в дерево;

-пошук по дереву;

-обхід дерева;

-видалення вузла.

Для кожного рекурсивного алгоритму можна створити його нерекурсивний еквівалент. У наведеній нижче програмі реалізована нерекурсивна функція пошуку по дереву з включенням і рекурсивна функція обходу дерева. Перша функція здійснює пошук елемента із заданим ключем. Якщо елемент знайдено, вона повертає покажчик на нього, а якщо ні - включає елемент у відповідне місце дерева і повертає вказівник на нього. Для включення елемента необхідно пам’ятати пройдений по дереву шлях на один крок назад і знати, чи виконується включення нового елемента в ліве або праве піддерево його предка.

Програма формує дерево з масиву цілих чисел і виводить його на екран.

#include <iostream.h> struct Node

{

162

int d;

Node *left; Node *right;

};

Node * first (int d);

Node * search_insert(Node *root, int d); void print_tree(Node *root, int l);

int main()

{

int b[] = {10, 25, 20, 6, 21, 8, 1, 30}; Node *root = first(b[0]);

for (int i=1; i<8; i++) search_insert(root, b[i]);

print_tree(root, 0); _getch();

return 0;

}

//Формування першого елемента дерева

Node * first(int d)

{

Node *pv = (Node*) malloc(sizeof(Node)); pv->d = d;

pv->left = 0; pv->right = 0; return pv;

}

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

Node * search_insert(Node *root, int d)

{

Node *pv = root, *prev; bool found = false; while (pv && !found)

{

prev = pv;

if (d == pv->d) found = true;

else

if (d < pv->d)

pv =pv->left;

else

163

pv= pv->right;

}

if (found) return pv;

//Створення нового вузла

Node *pnew = (Node*) malloc(sizeof(Node)); pnew->d = d;

pnew->left = 0; pnew->right = 0; if (d < prev->d)

// Приєднання до лівого піддерева предка prev->left = pnew;

else

// Приєднання до правого піддерева предка prev->right = pnew;

return pnew;

}

// Обхід дерева

void print_tree(Node *p, int level)

{

if(p)

{

print_tree(p->left, level +1); // друк лівого піддерева

for (int i = 0; i<level; i++) printf(" ");

printf("%d\n",p->d);

// друк кореня піддерева print_tree(p->right, level +1); // друк правого піддерева

}

}

Поточний покажчик для пошуку по дереву позначений pv, покажчик на предка pv позначений prev, змінна pnew використовується для виділення пам’яті для включення в дерево вузла. Рекурсії вдалося уникнути, зберігши лише одну змінну (prev) і повторивши при включенні оператори, що визначають, до якого піддерева приєднується новий вузол. Результат роботи програми для дерева, зображеного на рис. 20.6:

164

Рис. 20.6. Сформоване дерево

Розглянемо докладніше функцію обходу дерева. Другим параметром в неї передається ціла змінна, що визначає, на якому рівні знаходиться вузол. Корінь знаходиться на рівні 0. Дерево друкується по горизонталі так, що корінь знаходиться зліва. Перед значенням вузла для імітації структури дерева виводиться кількість пропусків, що пропорційно рівню вузла. Якщо закоментувати цикл друку пропусків, відсортований за зростанням масив буде виведений у стовпчик. Функція обходу дерева завдовжки всього в кілька рядків може надрукувати дерево будь-якого розміру - обмеженням є тільки розмір стека.

Видалення вузла з дерева являє собою не таку просту задачу, оскільки вузол, що видаляється, може бути кореневим, зберігати два, одне або жодного посилання на піддерева. Щоб зберегти впорядкованість дерева при видаленні вузла з двома посиланнями, його замінюють на вузол з найближчим до нього ключем. Це може бути самий лівий вузол його правого піддерева або самий правий вузол лівого піддерева (наприклад, щоб видалити з дерева на рис. 20.6 вузол з ключем 25, його потрібно замінити на 21 або 30, вузол 10 замінюється на 20 або 8, і т. д.).

20.6 Реалізація динамічних структур за допомогою масивів

Операції виділення і звільнення пам’яті – складне завдання, тому у випадках, коли максимальний розмір даних можна визначити до початку використання і в процесі роботи він не змінюється (наприклад, при сортуванні вмісту файлу), більш ефективним може виявитися одноразове виділення безперервній області пам’яті. Зв’язки

165

елементів при цьому реалізуються не через покажчики, а через допоміжні змінні або масиви, в яких зберігаються номери елементів.

Найпростіше реалізувати таким чином стек. Крім масиву елементів, що відповідають типу даних стека, достатньо мати одну змінну цілого типу для зберігання індексу елемента масиву, що є вершиною стека. При додаванні елемента в стек індекс збільшується на одиницю, а при вибірці - зменшується.

Для реалізації черги потрібні дві змінні цілого типу - для зберігання індексу елементів масиву, що є початком і кінцем черги.

Для реалізації лінійного списку необхідний допоміжний масив цілих чисел і ще одна змінна, наприклад:

10 25 20 6 21 8 1 30 - масив даних

1 2 3 4 5 6 7 -1 - допоміжний масив 0 – індекс першого елемента в списку

1-й елемент допоміжного масиву містить для кожного i-ro елемента масиву даних індекс наступного за ним елемента. Від’ємне число використовується як ознака кінця списку. Той же масив після сортування:

10 25 20 6 21 8 1 30 - масив даних

2 7 4 5 1 0 3 -1 - допоміжний масив 6 - індекс першого елемента в списку

Для створення бінарного дерева можна використовувати два допоміжних масиву (індекси вершин його правого і лівого піддерева). Від’ємне число використовується як ознака порожнього посилання. Наприклад, дерево, наведене на рис. 20.6, можна представити таким чином:

10 25 20 6 21 8 1 30 - масив даних 3 2 -1 6 -1 -1 -1 -1 - ліве посилання

1 7 4 5 -1 -1 -1 -1 - праве посилання Пам’ять під такі структури можна виділити або на етапі компі-

ляції, якщо розмір можна задати константою, або під час виконання програми.

166

21 ІНДИВІДУАЛЬНІ ЗАВДАННЯ ДО П. 20

Завдання:

У роботі необхідно організувати однонапрямлений список зі структурою, взятою із попередньої індивідуальної роботи згідно з варіантом, та передбачити наступні дії :

додавання елементів до списку;

вилучення елементів зі списку;

додавання елементів у кінець списку;

пошук за заданим полем;

друк списку.

167

22 ДИРЕКТИВИ ПРЕПРОЦЕСОРА

Препроцесор – це програма, що попереднє опрацьовує текст програми до компіляції. Вона виконує макропідстановку і включення іменованих файлів. Кожна з цих операцій кодується у вигляді особливого оператора (директиви), що починається символом #. Як правило, препроцесор використовується для включення в програму файлів, у яких визначені стандартні функції й інші сутності.

Препроцесор не виконує синтаксичний аналіз тексту. Він просто розпізнає макропідстановки і виконує їх. Директиви препроцесора не залежать від синтаксису мови, за одним виключенням – їх імена чуттєві до регістра букв (табл. 22.1).

 

Директиви препроцесора

Таблиця 22.1.

 

 

#define

#elif

#else

#endif

#error

#if

#ifdef

#ifndef

#include

#line

#pragma

#undef

Кожна директива повинна розташовуватися в окремому рядку програми. При цьому необхідно уважно стежити за пробілами усередині директив. Деякі з них не мають значення, а інші приводять до помилок.

22.1 Директива # include

Директива #include є найпростішим засобом забезпечення узгодженості оголошень в різних файлах, вона включає в них інформацію про інтерфейс з заголовних файлів.

Заголовки зазвичай мають розширення .h і можуть містити:

визначення типів, констант, вбудованих функцій, шаблонів, перерахувань;

оголошення функцій, даних, імен, шаблонів;

простору імен;

директиви препроцесора;

коментарі.

У заголовному файлі не повинно бути визначень функцій і даних. Ці правила не є вимогою мови, а відображають розумний спосіб використання директиви.

Розглянемо як приклад фрагмент програми, що містить дуже розповсюджену директиву #include.

168

#include <iostream.h> #include <math.h>

# include <string.h> #include < conio.h> #include <stdlib.h > #INCLUDE <alloc.h>

#include<INCLUDE\IOSTREAM.H>

Перший рядок не правильний, оскільки, як зазначено вище, кожна директива повинна знаходитися в окремому рядку. Третій рядок помилковий, тому що усередині кутових дужок повинне знаходитися ім’я заголовка. У даному випадку ім’я заголовного файлу, зазначеного в третьому рядку, починається з пробілу, що, безсумнівно, є помилкою. Пробіл після імені заголовкового файлу, як показано в четвертому рядку, сучасні компілятори також вважають помилкою, хоча старі компілятори розглядали його як незначущий символ і правильно розпізнавали ім’я файлу.

Другий рядок правильний, оскільки символ # є окремою лексемою. Між першою і другою лексемами директиви препроцесора можуть розташовуватися лише пробіли і символи горизонтальної табуляції. Будь-які інші роздільники заборонені. Незважаючи на те, що символ # – окрема лексема, він є невід’ємною частиною імені директиви.

П’ятий рядок не правильний, тому що ім’я директиви #include повинне складатися лише з малих літер. Шостий рядок також є абсолютно правильним, хоча і виглядає досить незвично.

Зверніть увагу на одну обставину: оскільки директиву препроцесора можна розміщати в довільному місці програми, а директива #include замінюється зазначеним файлом, можна «згортати» вихідний текст програми, розподіляючи його фрагменти по різних файлах.

#include <stdio.h> int main()

{

#include "file1.txt" #include "c:\text\file2.txt" return 0;

}

Цей фрагмент ілюструє одну цікаву властивість директиви #include. З її допомогою можна вставляти в текст програми не

169

тільки стандартні, але і власні текстові файли. Для цього слід застосовувати лапки, а не кутові дужки. У цьому випадку пошук файлу виконується в поточному каталозі, а не в каталозі INCLUDE. Якщо такого файлу в поточному каталозі немає, виконується пошук у каталозі INCLUDE. Регістр букв при наборі імені файлу не має значення. Крім того, у директиві #include можна явно вказувати шлях до файлу. Імена каталогів у системі Windows розділяються зворотною косою рисою, а в системі UNIX — косою рисою.

22.2 Директива #define

Директива #define визначає підстановку в тексті програми. Вона використовується для визначення:

– символічних констант:

#define ім’я текст_підстановки

(всі входження імені замінюються на текст підстановки);

– макросів, що виглядають як функції, але реалізуються підстановкою їх тексту в текст програми:

#define ім’я (параметри) текст_підстановки

– символів керуючих умовною компіляцією. Вони використовуються разом з директивами #ifdef та #ifndef [15].

Формат:

# define ім’я

Приклади:

#define VERSION 1

#def1ne VASIA "Василь Іванович" #define МАХ (х,у) ((x)> (y)? (X): (y)) #define MUX

Імена рекомендується записувати великими літерами, щоб візуально відрізняти їх від імен змінних і функцій. Параметри макросу використовуються при макропідстановці, наприклад, якщо в тексті програми використовується виклик макросу

у

=

MAX (suml, sum2);. він буде замінений на

у

=

((suml)> (sum2)? (suml): (sum2));

170