Download as pdf or txt
Download as pdf or txt
You are on page 1of 55

НАЦІОНАЛЬНИЙ ТЕХНІЧНИЙ УНІВЕРСИТЕТ УКРАЇНИ

«КИЇВСЬКИЙ ПОЛІТЕХНІЧНИЙ ІНСТИТУТ ім. І. Сікорського»


Кафедра АВТОМАТИЗАЦІЇ ТЕПЛОЕНЕРГЕТИЧНИХ ПРОЦЕСІВ

“ЗАТВЕРДЖУЮ”
Декан ТЕФ
_________ Є. М. Письменний
(підпис) (ініціали, прізвище)

“____”___________ 20__ р.
_________ ______________
(підпис) (ініціали, прізвище)

“____”___________ 20__ р.

АЛГОРИТМИ ТА СТРУКТУРИ ДАНИХ


(назва навчальної дисципліни)

МЕТОДИЧНІ ВКАЗІВКИ ТА ЗАВДАННЯ


ДО ЛАБОРАТОРНИХ ЗАНЯТЬ

спеціальність 151. Автоматизація та комп’ютерно-інтегровані технології


(шифр і назва спеціальності)

спеціалізація _ Автоматизоване управління технологічними процесами


(шифр і назва спеціальності)

освітньо-кваліфікаційний рівень ___бакалавр____________


форма навчання ____денна_та заочна___________________

Ухвалено методичною комісією ТЕФ ________________________________


Протокол від “__”____________20__ року № _____

Голова ___________________ ( Шевель Є. В )


(підпис) (прізвище та
ініціали)

“__” _________ 20__ року

Київ – 2021
Методичні вказівки та завдання до лабораторних занять кредитного модуля
«Алгоритми та структури даних» для бакалаврів напрямку підготовки «Автоматизація
та комп'ютерно-інтегровані технології» // Укладач Грудзинський Ю.Є. – Київ: НТУУ "КПІ
ім. І. Сікорського". – 2021 р. – 55 с.

Укладач Грудзинський Юліан Євгенович

Відповідальний

редактор ______________________________

Пропоновані методичні вказівки містять пояснення та рекомендації щодо


виконання лабораторних робіт з кредитного модулю «Алгоритми та структури даних».
Приведено завдання та пояснювальні матеріали для виконання циклу лабораторних
робіт з програмування алгоритмів сортування та роботи з абстрактними структурами
даних типу зв'язаний список, стек, дека, черга. Всі лабораторні роботи виконуються за
допомогою мови програмування Сі в інтегрованому середовищі розробки Microsoft
Visual Studio.

Київ, НТУУ "КПІ ім. І. Сікорського", 2021

2
ЗМІСТ
ВИМОГИ ДО ВИКОНАННЯ, ОФОРМЛЕННЯ І ЗДАЧІ ЛАБОРАТОРНИХ
РОБІТ ............................................................................................................................ 5
Оформлення.............................................................................................................. 5
Нарахування балів ................................................................................................... 6
Самостійне виконання завдань ............................................................................... 6
Допуск до лабораторної роботи ............................................................................. 6
Захист завдань .......................................................................................................... 6
Угоди про стиль кодування .................................................................................... 6
Додаткові вимоги до написання програм .............................................................. 6
Рекомендації по створенню власних проектів в середовищі Visual Studio ....... 7
ЛАБОРАТОРНА РОБОТА №1. АЛГОРИТМИ СОРТУВАННЯ ......................... 9
1.1. Алгоритми порядку складності O(N2) ......................................................... 9
1.2. Алгоритми порядку складності O(N*log(N)) ............................................ 10
1.3. Алгоритми порядку складності O(N)......................................................... 13
1.4. Опис роботи ................................................................................................. 15
1.5. Вимоги до виконання роботи ..................................................................... 15
1.6. Зміст звіту по лабораторній роботі ............................................................ 15
1.7. Варіанти завдань .......................................................................................... 16
1.8. Контрольні питання ..................................................................................... 17
ЛАБОРАТОРНА РОБОТА №2. ЛІНІЙНІ АБСТРАКТНІ СТРУКТУРИ ДАНИХ
(АТД) 18
2.1. Алгоритми роботи з АТД............................................................................ 18
2.1.1. Робота з двонапрямленим списком ........................................................ 18
2.1.2. Робота з циклічним (кільцевим) списком .............................................. 19
2.2. Опис виконання роботи .............................................................................. 20
2.3. Вимоги до виконання роботи ..................................................................... 21
2.4. Зміст звіту по лабораторній роботі ............................................................ 21
2.5. Варіанти завдань .......................................................................................... 21
2.6. Контрольні питання ..................................................................................... 22
ЛАБОРАТОРНА РОБОТА №3. БІНАРНІ ДЕРЕВА ............................................ 23
3.1. Представлення бінарного дерева в пам'яті комп'ютера ........................... 23
3.2. Траверс (обхід) бінарних дерев .................................................................. 23
3.2.1. Обхід в прямому порядку ........................................................................ 24
3.2.2. Симетричний обхід .................................................................................. 25
3.2.3. Обхід у зворотному порядку ................................................................... 26
3.2.4. Обхід в ширину......................................................................................... 27
3.3. Створення дерева ......................................................................................... 29
3.4. Пошук вершини у бінарному дереві .......................................................... 30
3.5. Включення нового елемента у бінарне дерево ......................................... 31
3.6. Видалення вершини з бінарного дерева пошуку ..................................... 33
3.6.1. Випадок 1: видалення вершини, яка не має нащадків. ......................... 33
3.6.2. Випадок 2: видалення вершини з одним нащадком. ............................ 33

3
3.6.3. Випадок 3: видалення вершини з двома нащадками. ........................... 34
3.7. Видалення бінарного дерева....................................................................... 37
3.8. Опис виконання роботи .............................................................................. 37
3.9. Вимоги до виконання роботи ..................................................................... 38
3.10. Зміст звіту по лабораторній роботі ......................................................... 38
3.11. Варіанти завдань ....................................................................................... 38
3.12. Контрольні питання.................................................................................. 38
ЛАБОРАТОРНА РОБОТА №4. АЛГОРИТМИ І ПРИНЦИПИ ХЕШУВАННЯ.
РОЗВ'ЯЗАННЯ КОЛІЗІЙ 40
4.1. Загальні поняття і визначення .................................................................... 40
4.2. Приклад організації хеш-таблиці ............................................................... 40
4.3. Функції хешування ...................................................................................... 41
4.3.1. Отримання залишку ................................................................................. 41
4.3.2. Мультиплікативний метод....................................................................... 42
4.3.3. Метод середини квадрата ........................................................................ 42
4.3.4. Перетворення символьних ключів.......................................................... 42
4.4. Колізії і їх розв'язання ................................................................................. 42
4.5. Основні операції з хеш-таблицею .............................................................. 43
4.6. Опис виконання роботи .............................................................................. 43
4.7. Вимоги до виконання роботи ..................................................................... 44
4.8. Зміст звіту по лабораторній роботі ............................................................ 44
4.9. Варіанти завдань .......................................................................................... 44
4.10. Контрольні питання.................................................................................. 46
РЕКОМЕНДОВАНА ЛІТЕРАТУРА ........................................................................ 47
Базова ...................................................................................................................... 47
Допоміжна .............................................................................................................. 47
Додаткова література, видана на українській мові ............................................ 48
ДОДАТОК 1. ВИМОГИ ДО ОФОРМЛЕННЯ КОДУ ПРОГРАМ НА МОВІ СІ 49
Угоди по ідентифікаторам .................................................................................... 49
Підбір ідентифікаторів .................................................................................... 49
Написання ідентифікаторів............................................................................. 49
Угоди по самодокументованих програмах ......................................................... 50
Коментарі........................................................................................................... 50
Специфікація функцій........................................................................................ 51
Специфікація програмного файлу або модуля ................................................ 52
Угоди по читабельності програм ......................................................................... 52
"Драбинка" .......................................................................................................... 52
Довжина рядків програмного тексту ............................................................. 54
Інші рекомендації ............................................................................................... 54

4
ВИМОГИ ДО ВИКОНАННЯ, ОФОРМЛЕННЯ І ЗДАЧІ ЛАБОРАТОРНИХ
РОБІТ
Оформлення

Звіти по всіх лабораторних роботах, необхідні ілюстративні малюнки в них, таблиці


повинні оформлятися відповідно до вимог ГОСТ 2.105-95 "Загальні вимоги до
оформлення текстових документів".
Малюнки схем алгоритмів у звіті повинні зображуватися відповідно до вимог ГОСТ
19.701-90 "ЕСПД. Схеми алгоритмів, програм, даних і систем. Умовні позначення і
правила виконання".
Перелік необхідних розділів звіту наведено в описі кожної лабораторної роботи в
пункті "Зміст звіту".
Приклад оформлення титульного аркуша звіту наведено на малюнку нижче:

5
Нарахування балів
Завдання кожної лабораторної роботи оцінюється в задану кількість балів залежно
від складності завдань. Бали, фактично отримані студентом за виконання роботи,
залежать від ступеня відповідності виконаної програми вимогам завдання, оформлення
та власних знань студента.
Доречне прислів’я: «що посієш, те й пожнеш».
Для кожної лабораторної роботи встановлені терміни її виконання. В залежності від
фактичних термінів здачі роботи від балів, набраних студентом, віднімається штраф,
встановлений викладачем.
Доречне прислів’я: «куй залізо, поки гаряче».
Самостійне виконання завдань
Робота повинна бути виконана студентом самостійно. Випадки виявлення плагіату
будуть каратися виставленням балів з негативним коефіцієнтом.
Доречне прислів’я: «на чужому горбі в рай не потрапиш».
Допуск до лабораторної роботи
Перед тим, як приступити до виконання відповідної лабораторної роботи згідно
свого варіанту кожний студент бригади повинен відповісти на контрольні питання
викладача. Тільки після цього бригада студентів буде допущена до виконання роботи.
Студент(и) бригади, недопущені до виконання лабораторної роботи, відмічаються у
журналі групи як відсутні.
Перед виконанням лабораторної роботи у кожного студента бригади повинен бути:
- протокол лабораторної роботи, написаний власноруч (без висновків та
результатів роботи);
- в протоколі повинна бути присутня схема алгоритму, виконана згідно діючого
стандарту;
- в протоколі повинен бути присутнім текст програм на відповідних мовах,
згідно завдання;
- в протоколі повинен бути присутнім розділ "Очікувані результати роботи".
Захист завдань
Для контролю засвоєння студентом навчальних матеріалів та контролю
самостійності виконання лабораторної роботи, робота, що здається повинна бути
захищена викладачу. Нерозуміння студентом принципів роботи алгоритму або
програми своєї роботи може бути розцінено, як спроба здачі чужої роботи (див.
попередній розділ).
Угоди про стиль кодування
При написанні програмного коду слід дотримуватися певних правил оформлення,
про які докладно написано в Додатку 1. Невідповідність програми даним вимогам може
з'явитися причиною зниження балів за роботу.
Додаткові вимоги до написання програм
При виконанні кожної лабораторної роботи студент повинен передбачати у своєму
програмному коді відповідні перевірки на допустимість вхідних даних реальному
діапазону значень.
Кожна з написаних студентом програм повинна бути інтерактивною і на кожному
етапі свого виконання активно взаємодіяти з користувачем, інформуючи його про

6
необхідні вхідні дані, результати свого виконання і варіантах подальшої поведінки
користувача.
Програмний код написаних студентом лабораторних робіт не повинен містити
фрагменти, що не стосуються теми лабораторної роботи, варіанту завдання
лабораторної роботи або невживані фрагменти.
Рекомендації по створенню власних проектів в середовищі Visual Studio
Для того, щоб забезпечити безпомилкову компіляцію програми при використанні
зовнішніх функцій мови С і з використанням символів кирилиці кодової таблиці 1251 в
середовищі Windows студентам слід виконати наступні чотири дії для кожного свого
програмного проекта:
1. Використовувати наступну структуру програмного коду програми main() в своїх
лабораторних роботах:
#define _CRT_SECURE_NO_WARNINGS
#include "stdlib.h"
#include "stdio.h"
#include "conio.h"
#include "math.h"
#include "locale.h"
#include "string.h"
#include "windows.h"

<тип_значення_що_повертається> main (<список_параметрів>) {

<розділ_оголошень_програми>

SetConsoleCP(1251); /* перший оператор програми, */


SetConsoleOutputCP(1251); /*який виконується! */


<виконуване_тіло_програми_(код)>

}
2. Далі слід вказати курсором миші на ім'я проекту у вікні Solution Explorer, клацнути
правою клавішею миші і вибрати у випадаючому меню пункт "Properties". У вікні
властивостей, ліворуч, зробити наступний вибір:
"Configuration Properties" -> "General"
і на вкладці праворуч перейти до властивості "Character Set". В цій властивості,
за допомогою меню, що випадає встановити набір символів "Not Set".

7
3. Далі слід, вказавши курсором мишки на ім'я проекту у вікні Solution Explorer,
клацнути правою клавішею миші та обрати у випадаючому меню пункт
"Properties". У вікні властивостей зліва, зробити наступний вибір:
"Configuration Properties" -> "C/C++" -> "Pre-compiled headers"
і на вкладці праворуч перейти до властивості "Pre-compiled header". У цій
властивості, за допомогою меню, що випадає встановити режим "Do not use
precompiled headers".
4. У властивостях нового проекту для даних лабораторних робіт слід вказати явно
використання стандарту мови C++ при компіляції, що дозволить повноцінно
використовувати нові можливості С++ при написанні коду С. Для цього слід
вказати курсором мишки на ім'я проекту у вікні Solution Explorer, клацнути
правою клавішею миші і вибрати в меню, що випало, пункт "Properties". У вікні
властивостей, ліворуч, зробити наступний вибір:
Configuration Properties -> C/C++ -> Advanced
У правій частині вікна, що відкрилося, в рядку вибору "Compile as" вибрати
режим "Compile as C++ code (/ TC)" і натиснути внизу кнопку ОК. Виконати цю
процедуру для кожного проекту в рішенні.

У деяких лабораторних роботах потрібно вводити за допомогою функції scanf()


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

scanf ("%[^\n]s", str);

У багатьох лабораторних роботах потрібно виконати кілька завдань, тобто написати


кілька непов'язаних між собою програм. У цьому випадку студенту необхідно створити
в межах одного рішення Visual Studio кілька проектів (див. [7]), а не створювати кілька
рішень. У цьому випадку він отримує можливість простого переходу між проектами, не
закриваючи створеного рішення і не "плодячи" рішеннь, тим самим, спрощуючи собі
роботу і не "захаращуючи" жорсткий диск даними.
При виконанні кожної лабораторної роботи студент повинен передбачати у
своєму програмному коді відповідні перевірки на допустимість вхідних даних
реальному діапазону значень.
Кожна з написаних студентом програм повинна бути інтерактивною і на
кожному етапі свого виконання активно взаємодіяти з користувачем, інформуючи
його про необхідні вхідні дані, результати свого виконання і варіантах подальшої
поведінки користувача.
Програмний код написаних студентом лабораторних робіт не повинен містити
фрагменти, що не стосуються теми лабораторної роботи, варіанту завдання
лабораторної роботи або невживані фрагменти, (окрім тексту коментарів в
програмі).

8
ЛАБОРАТОРНА РОБОТА №1. АЛГОРИТМИ СОРТУВАННЯ
Мета роботи: Вивчити і реалізувати алгоритми внутрішнього сортування трьох
видів складності: O(N2), O(N*log(N)) та O(N). Розібратися з принципами побудови
алгоритмів сортування та їх подальшою реалізацією на мові процедурного
програмування Сі.
До виконання лабораторної роботи студенту слід ознайомитися з матеріалом
відповідного розділу "Сортування" [5]. Усі нижчеописані алгоритми здійснюють
сортування за збільшенням. При необхідності організувати сортування за зменшенням
– алгоритми слід відповідно (додаткове питання на захисті!) змінити.

1.1. Алгоритми порядку складності O(N2)

Сортування бульбашкою та включеннями детально викладено в роботі [5]. Тут ми


коротко опишемо сортування вибором. Сортування вибором — простий алгоритм
сортування лінійного масиву, на основі включень, але з деякими відмінностями. Має
ефективність O(N2), що робить його неефективним при сортуванні великих масивів, і в
цілому, менш ефективним за подібний алгоритм сортування включенням. Сортування
вибором характерне більшою простотою самого алгоритму, ніж сортування
включенням, і в деяких випадках, вищою продуктивністю.
Алгоритм працює таким чином:
1. знаходить у списку найменше значення;
2. міняє його місцями із першим значеннями у списку;
3. повторює два попередніх кроки, доки список не завершиться (починаючи з
НАСТУПНОЇ позиції);
Фактично, таким чином ми ділимо список на дві частини: перша (ліва) — повністю
відсортована, а друга (права) — ще ні.

Рис. 1.1. Приклад сортування вибором восьмиелементного масиву

Приклад 1.1. Реалізація функції сортування вибором

// Функція отримує на вході несортований масив цілих чисел і повертає його вже
// отсортованим.
// Ім'я функції sortChoice повертає код завершення функції:
// 0 – функція виконалась успішно;
// 1 – невірний розмір масиву, сортування не відбулось.

int sortChoice (int array[], // вхідний масив


int n) { // кількість елементів масиву
int iter, jter, kter ; // "бігунки"-ітератори для перебору масиву
int temp ; // змінна, в якій тимчасово зберігається значення елементу масива

9
/* Перевірка корректності розміру масиву */
if (n<1)
return 1;

/* Реалізація основного алгоритму */


for (iter=0; iter<n-1; iter++) { // зовнішній цикл
kter = iter ; // ініціалізовуємо kter в позицію iter
temp = array[iter] ; // і зберігаємо значення елементу
for (jter=iter+1; jter<n; jter++) { // внутрішній цикл
if (array[jter] < temp) { //перевіряємо, чи значення елементу
менше
kter = jter ; // зберігаємо індекс найменшого елементу
temp = array[kter] ; // зберігаємо його значення
}
}
/* обмінюємо значення найменшого елементу масиву з поточним */
array [kter] = array [iter] ;
array [iter] = temp ;
}
return 0; // нормальне завершення функції
}

1.2. Алгоритми порядку складності O(N*log(N))

Сортування Шелла та швидке сортування детально викладені в роботі [5]. Тут ми


коротко опишемо сортування злиттям. Сортування злиттям (merge sort) - разом із
алгоритмами швидкого сортування є одним з найефективніших алгоритмів сортування.
Цей алгоритм, як і алгоритм швидкого сортування керується принципом "розділяй та
володарюй", тобто він ділить множину елементів, які потрібно відсортувати, на
підмножини, які сортуються цим же алгоритмом. Як і швидке сортування, цей алгоритм
базується на рекурсії.
Саму роботу алгоритму можна розбити на два кроки:
- поділити масив на дві рівні частини і застосувати сортування злиттям до кожної з
них;
- злити два відсортовані масиви з попереднього кроку у один таким чином, щоб він
був відсортований.

Рис. 1.2.А. Розділення початкового масиву на підмасиви

10
Рис. 1.2.Б. Сортування підмасивів з одночасним злиттям

Подивитися в динаміці роботу алгоритму можливо за посиланням


https://upload.wikimedia.org/wikipedia/commons/c/cc/Merge-sort-example-300px.gif

Складність методу злиття оцінюється як O(N*logN). Але об'єм пам'яті, яка буде
використана при цьому, дорівнює 2*N.

Приклад 1.2 Реалізація функції сортування злиттям

/* функція ділить вхідний масив на дві половини, потім викликає себе для ділення
масивів на наступні дві половини і так далі
Значення, що повертається функцією:
0 – сортування виконалось успішно:
1 – недостатньо пам'яті для виконання */

int sortMerge (int arr[], // масив, що сортується,


int beg, // початок лівого підмасиву масиву arr для сортування
int end) { // кінець правого підмасиву масива arr)

int mid; // індекс серединки

if (beg<end) {
mid = (beg+end)/2; // визначаємо серединку
sortMerge(arr, beg, mid); // сортуємо лівий та правий підмасиви
sortMerge(arr, mid+1, end); // об'єднуємо відсортовані підмасиви
if (!merge(arr, beg, mid, end))
return 0; // якщо все в порядку, повертаємо 0
else
return 1; // якщо нестача пам'яті, повертаємо 1
}
}

/* Функція merge() використовується для злиття двох відсортованих підмасивів в


єдиний відсортований масив. Злиття є ключовим процесом, який передбачає, що
підмасиви arr [beg..mid] та arr [mid + 1..end] вже відсортовані і об'єднує ці два підмасива
в масив arr [beg..end].

Значення, що повертається функцією:


0 – об'єднання виконалось успішно:
1 – недостатньо пам'яті для виконання

Зміст передаваємих параметрів:

11
Лівий підмасив arr[beg..mid]
Правий підмасив arr[mid+1..end] */

int merge(int arr[], // масив, що сортується


int beg, // початок лівого підмасиву
int mid, // серединка
int end) { // кінець правого підмасиву

int i=beg, // початок лівого підмасиву


j=mid+1, // початок правого підмасиву
index=beg,
k,
*temp, // адреса допоміжного масиву
size; // розмір допоміжного масиву

// створення допоміжного масиву


size = end+1;
temp = (int*) malloc (sizeof(int)*size);
if (!temp) return 1; // повернення при нестачі пам'яті!

// заповнюємо допоміжний масив в правильному порядку


while((i<=mid) && (j<=end)) {
if(arr[i] < arr[j]) {
temp[index] = arr[i];
i++;
}
else {
temp[index] = arr[j];
j++;
}
index++;
}

if(i>mid) { // переписуємо нескопійовані елементи правого підмасиву,


// якщо такі лишилися
while(j<=end) {
temp[index] = arr[j];
j++;
index++;
}
}
else { // переписуємо нескопійовані елементи лівого підмасиву,
// якщо такі лишилися
while(i<=mid) {
temp[index] = arr[i];
i++;
index++;
}
}

// копіюємо допоміжний масив в початковий масив arr

12
for(k=beg;k<index;k++)
arr[k] = temp[k];

return 0; // нормальне повернення з функціі – підмасиви злиті!


}

1.3. Алгоритми порядку складності O(N)

Сортування підрахунком та сортування змішуванням детально викладені в роботі


[5]. Тут ми коротко опишемо сортування комірками. Сортування комірками (блоками,
корзинами, bucket sort) - стабільний алгоритм впорядкування, що доцільно
використовувати, якщо вхідні дані розподілені рівномірно. В основі алгоритму лежить
розподілення всіх елементів по скінченній кількості комірок. Кожна комірка
впорядковується окремо іншим алгоритмом впорядкування або ж рекурсивно
алгоритмом впорядкування комірками. Сортування комірками є узагальненням
сортування підрахунком.

Рис. 1.3.А. Етап 1. Елементи масиву розміщуються по комірках

Рис. 1.3.Б. Етап 2. Елементи в кожній комірці відсортовуються

Алгоритм працює за час O(N), оскільки використовує додаткову інформацію про


елементи.

Приклад 1.3 Реалізація функції сортування комірками

/* Декларування структури для комірок */


struct bucket {
int count; // лічильник елементів
int* values; // Вказівник на елемент масиву
};

/* Функція для порівняння двох елементів. Ця функція використовується


бібліотечною функцією qsort
Значення, що повертається функцією:
0 - елементи рівні
1 - перший елемент більший
-1 - перший елемент меньший */

13
int compareIntegers(const void* first, const void* second) {
int a = *((int*)first), b = *((int*)second);
if (a == b) return 0;
else if (a < b) return -1;
else return 1;
}

/* Функція реалізації алгоритма сортування комірками


Значення, що повертається функцією:
0 – сортування виконалось успішно:
1 – недостатньо пам'яті для виконання

Зміст параметрів що передаються:


Вказівник на масив array[]
Кількість елементів масиву n */

int sortBucket(int array[],int n) {


struct bucket buckets[3];
int i, j, k;

for (i = 0; i < 3; i++) {


buckets[i].count = 0; // скидання лічильника
buckets[i].values = (int*)malloc(sizeof(int) * n); // виділення блока пам'яті
if (!buckets[i].values)
return 1; // повернення при нестачі пам'яті!
}

// Розділяємо несортовані елементи по трьох комірках


for (i = 0; i < n; i++) {
if (array[i] < 0) { // якщо елемент < 0 : до першої комірки
buckets[0].values[buckets[0].count++] = array[i];
}
else if (array[i] > 10) { // якщо елемент > 10 : до третьої комірки
buckets[2].values[buckets[2].count++] = array[i];
}
else { // якщо елемент 0 - 10 : до другої комірки
buckets[1].values[buckets[1].count++] = array[i];
}
}

for (k = 0, i = 0; i < 3; i++) {

/* Використовуємо Quicksort для сортування кожної комірки */


qsort(buckets[i].values, buckets[i].count, sizeof(int), &compareIntegers);
for (j = 0; j < buckets[i].count; j++) { // переносимо відсортовані елементи з
// кожної комірки до основного масиву
array[k + j] = buckets[i].values[j];
}
k += buckets[i].count; // коригування індекса відсортованого масиву
free(buckets[i].values); // звільнення пам'яті

14
}
return 0; // нормальне повернення з функціі – масив відсортований
}

1.4. Опис роботи

У відповідності зі своїм варіантом реалізувати алгоритми трьох видів сортування


для одного і того ж масиву чисел зі 100, 500, 1000, 2000, 3000, ..., 10000 елементів.
Масиви заповнювати за допомогою генератора випадкових чисел і ручним введенням
(тестовий режим). Заміряти швидкість роботи алгоритмів і в протоколі побудувати
графіки залежності швидкості роботи від числа елементів масиву для всіх трьох видів
сортувань свого варіанту. Зробити порівнювальні висновки по роботі.

1.5. Вимоги до виконання роботи

1. Всі програми повинні бути написані на мові Сі.


2. Реалізовувати алгоритми сортування у вигляді функцій, з передачею їм масивів, що
підлягають сортуванню, у вигляді параметрів.
3. Програма повинна працювати у діалозі та взаємодії с користувачем.
4. Програма повинна працювати з одним і тим же масивом трьома видами сортування
з видачею по кожному виду загальної кількості елементів та часу, який було
затрачено на їх сортування.
5. При роботі у тестовому режимі заповнювати лише один масив. Кінець заповнення –
введення пустого елементу масиву.
6. Текст програми повинен відповідати правилам оформлення програм на мові Сі,
наведеними в ДОДАТОК 1. ВИМОГИ ДО ОФОРМЛЕННЯ КОДУ ПРОГРАМ НА МОВІ
СІ .

1.6. Зміст звіту по лабораторній роботі

1. Титульний лист.
2. Мета роботи та завдання.
3. Теоретичні відомості (необов'язково).
4. Схема алгоритму програми.
5. Оригінальний текст програми з коментарями.
6. Набори тестових значень з основними результатами (скрини екранів та текст).
7. Побудова трьох швидкісних графіків залежності часу сортування від розміру масиву
на одному малюнку.
8. Висновки по роботі.

15
1.7. Варіанти завдань


Завдання: три алгоритми сортування для реалізації
варіанта

1 Сортування вставкою, швидке сортування, сортування підрахунком

2 Сортування бульбашкою, швидке сортування, сортування підрахунком.

3 Сортування вибором, сортування злиттям, сортування підрахунком

4 Сортування змішуванням, сортування злиттям, сортування підрахунком

5 Сортування вставкою, сортування Шелла, сортування комірками

6 Сортування вибором, сортування Шелла, сортування комірками

7 Сортування змішуванням, швидке сортування, сортування комірками

8 Сортування бульбашкою, швидке сортування, сортування комірками

9 Сортування вставкою, сортування злиттям, сортування підрахунком

10 Сортування бульбашкою, сортування злиттям, сортування підрахунком

11 Сортування вибором, сортування Шелла, сортування підрахунком

12 Сортування змішуванням, сортування Шелла, сортування підрахунком

13 Сортування вставкою, швидке сортування, сортування комірками

14 Сортування бульбашкою, швидке сортування, сортування комірками

15 Сортування вибором, сортування злиттям, сортування комірками

16 Сортування змішуванням, сортування злиттям, сортування комірками

17 Сортування вставкою, сортування Шелла, сортування підрахунком

18 Сортування бульбашкою, сортування Шелла, сортування підрахунком

19 Сортування вибором, швидке сортування, сортування підрахунком

20 Сортування змішуванням, швидке сортування, сортування підрахунком

21 Сортування вставкою, швидке сортування, сортування підрахунком

22 Сортування вибором, сортування злиттям, сортування підрахунком

23 Сортування вставкою, сортування Шелла, сортування комірками

24 Сортування змішуванням, швидке сортування, сортування комірками

16

Завдання: три алгоритми сортування для реалізації
варіанта

25 Сортування вставкою, сортування злиттям, сортування підрахунком

26 Сортування вибором, сортування Шелла, сортування підрахунком

27 Сортування вставкою, швидке сортування, сортування комірками

28 Сортування вибором, сортування злиттям, сортування комірками

29 Сортування вставкою, сортування Шелла, сортування підрахунком

30 Сортування вибором, швидке сортування, сортування підрахунком

1.8. Контрольні питання

1. Скільки існує груп алгоритмів сортування?


2. За якими ознаками характеризуються алгоритми сортування?
3. Що потрібно враховувати при виборі алгоритму сортування?
4. Який алгоритм сортування вважається найпростішим?
5. Який алгоритм сортування вважається найефективнішим?
6. Що означає поняття «швидкість сортування»?
7. У чому полягає метод сортування бульбашкою?
8. У чому полягає метод сортування вибором?
9. У чому полягає метод сортування вставкою?
10. У чому полягає метод сортування злиттям?
11. У чому полягає метод швидкого сортування?
12. У чому полягає метод сортування Шелла?
13. У чому полягає метод сортування комірками?
14. Як залежить швидкість сортування від розміру структури даних для різних
алгоритмів?
15. Як визначити, яким алгоритмам сортування віддати перевагу при вирішенні
задачі?

17
ЛАБОРАТОРНА РОБОТА №2. ЛІНІЙНІ АБСТРАКТНІ СТРУКТУРИ
ДАНИХ (АТД)

Мета роботи: Вивчити принципи побудови лінійних абстрактних типів даних та


алгоритми їх обробки мовою процедурного програмування Сі.
До виконання лабораторної роботи студенту слід ознайомитися з матеріалом
відповідних розділів "Зв'язані списки" та "Черга і стек" [5].

2.1. Алгоритми роботи з АТД

В даній лабораторній роботі студент повинен реалізувати, згідно з завданням свого


варіанту, наступні типи лінійних АТД:
• однонапрямлений список (описано в [5]);
• двонапрямлений список (див. п. 2.1.1);
• кільцевий (циклічний) список (див. п. 2.1.2);
• черга (описано в [5]);
• стек (описано в [5]).

2.1.1. Робота з двонапрямленим списком

Двонапрямлений список є більш складним типом пов'язаного списку, який містить в


кожному вузлі два покажчика: покажчик на наступний вузол і покажчик на попередній
вузол в послідовності вузлів списку. Таким чином, вузол складається щонайменше з
трьох частин: інформаційної, покажчика на наступний і покажчика на попередній вузол:
Приклад 2.1 Структура вузла двонапрямленного списку
struct node {
int data;
struct node *next;
struct node *prev;
};

Двонапрямлений список виглядає наступним чином:

Рис. 2.1 Двонапрямлений список


Поле prev першого вузла і поле next останнього вузла будуть містити покажчики
NULL. Поле prev використовується для зберігання адреси попереднього вузла, що
дозволяє реалізувати перегляд списку в зворотньому напрямку. Двонаправлений
список вимагає більше місця в пам'яті під вузол і більш складних базових операцій
маніпулювання списком. Проте, двонаправлений список забезпечує легкість
маніпулювання елементами списку, оскільки він підтримує покажчики на вузли в обох
напрямках (вперед і назад). Наявність двох покажчиків замість одного надає кілька
переваг. Найбільш важливе з них полягає в тому, що переміщення по списку можливо
в обох напрямках. Це спрощує роботу зі списком, зокрема, вставку і видалення вузла.
Крім цього, користувач може переглядати список в будь-якому напрямку. Ще одна
перевага має значення тільки при деяких збоях. Оскільки весь список можна пройти не

18
лише за прямими, але і за зворотними посиланнями, то в разі, якщо якесь із посилань
стане невірним, цілісність списку можна відновити за іншим посиланням.
Для ще більшої ефективності списку використовується наступна структура
дескриптора списку START:
• назва списку;
• покажчик (адреса) початку списку;
• покажчик (адреса) кінця списку;
• поточне число елементів списку;
• опис елемента (довжина, тип і т.п.).
Тобто, до дескриптора списку додається покажчик кінця списку (END). Для
відсортованих списків це дозволяє домогтися більшої ефективності пошуку. Алгоритми
для роботи з двонапрямленими і однонапрямленими списками дуже схожі, за винятком
того, що перші повинні виконувати додаткові дії для управління ще одним набором
посилань.
Детальний опис роботи алгоритмів обробки двонапрямлених списків наведено в
розділі 6.4 роботи [6]. При виконанні відповідного завдання лабораторної роботи
студент може взяти за основу програмну реалізацію алгоритмів, наведену в кінці цього
ж розділу (с.с. 194-198).

2.1.2. Робота з циклічним (кільцевим) списком

У циклічному (кільцевому) пов'язаному списку, останній вузол містить покажчик на


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

Рис. 2.2 Циклічний однонапрямлений список


Єдиним недоліком циклічного пов'язаного списку є складність навігації при
проходженні списку. Справа в тому, що в циклічному списку відсутній останній вузол і,
отже, немає значення NULL в адресній частині будь-якого з вузлів списку. Це ускладнює
індикацію моменту завершення перегляду списку.
Структура вузла циклічного списку може мати вигляд структури вузла
однонапрямленого (наведена в [5]), чи двонапрямленого списку (див. Приклад 2.1).
Структура дескриптора циклічного списку START може мати структуру дескриптора
відповідного однонапрямленого чи двонапрямленого списку, з включенням покажчика
на умовний початок циклічного списку.
Зате циклічний список може бути дуже корисний у тому випадку, коли в рамках
виконання завдання потрібно нескінченне число разів проходити через послідовність
будь-яких елементів і виконувати пов'язані з ними завдання. Циклічні списки широко
використовуються в багатозадачних ОС для обслуговування задач. Так, ОС здатна
повторювати цикл завдань, щоб по черзі запускати і зупиняти кожну з них, виділяючи
заданий число квантів часу процесора для її виконання. Якщо почався новий процес,
він може бути доданий в будь-яке місце списку, наприклад за дескриптором, що

19
дозволить йому відразу-ж і запуститися.
Ще один приклад використання циклічного списку - це Інтернет. Коли ми працюємо
в Інтернеті, ми можемо використовувати кнопку Назад і кнопку Уперед для переходу до
попередніх / наступних сторінках, на яких ми вже раніше побували. Для цього циклічний
список використовується для підтримки послідовності відвіданих Web сторінок.
Алгоритми для роботи з циклічними списками повинні враховувати відсутність
термінальних вузлів в списку, для чого використовувати маркування вузлів списку. Це
ускладнює їх реалізацію.
Детальний опис роботи алгоритмів обробки циклічних списків наведено в розділі 6.3
роботи [6]. При виконанні відповідного завдання лабораторної роботи студент може
взяти за основу програмну реалізацію алгоритмів, наведену в кінці цього ж розділу (с.с.
184-187).

2.2. Опис виконання роботи

У відповідності зі своїм варіантом реалізувати алгоритми на мові Сі для власної


структури АТД. Взяти до уваги наступні вимоги.
1. Для побудови структури даних АТД забороняється використовувати готові та
стандартні структури АТД мови Сі.
2. Для розміщення АТД використовувати механізм динамічної пам'яті. Для
доступу к АТД використовувати виключно відповідний дескриптор.
3. Для АТД типу список необхідно передбачити наступний функціонал:
• вивести на екран елемент із заданою властивістю;
• вивести на екран перший елемент списку (для кільцевого списку не
виконується);
• вивести на екран останній елемент списку (для кільцевого списку не
виконується);
• для кільцевого списку відображати поточне положення маркера списку і
значення поточного елемента;
• вставити додатковий елемент до або після зазначеного вузла;
• вставити додатковий елемент в відсортованому порядку;
• виключити певний елемент зі списку.
• перевірка списку на порожнечу;
• отримання розміру списку;
• сортування списку;
• відображення списку на екрані.
4. Для АТД типу черга (стек) необхідно передбачити наступний функціонал:
• додавання елемента до черги (розміщення в стеку);
• читання елемента з черги (стека) з видаленням;
• перевірка черги (стека) на порожнечу;
• перевірка черги (стека) на переповнення;
• отримання поточного розміру черги (стека);
• відображення вмісту черги (стека) на екрані.
5. Організувати тестування розроблених алгоритмів і структур даних за
допомогою консолі. При виконанні кожної операції над АТД здійснювати вивід
стану структури даних на консоль (в файл).
6. Число елементів в АТД повинно бути не менше 100. Передбачити, що
заповнення елементів АТД проводити за допомогою генератора випадкових
чисел, чи вручну (для тестування).
7. Зробити висновки по роботі.

20
2.3. Вимоги до виконання роботи

1. Всі програми повинні бути написані на мові Сі.


2. Реалізовувати відповідні алгоритми обробки АТД у вигляді функцій, з
передачею їм дескрипторів АТД у вигляді параметрів.
3. Глобальні змінні використовувати заборонено.
4. Програма повинна працювати у діалозі та взаємодії з користувачем.
5. Текст програми повинен відповідати правилам оформлення програм на мові
Сі, наведеними в ДОДАТОК 1. ВИМОГИ ДО ОФОРМЛЕННЯ КОДУ ПРОГРАМ
НА МОВІ СІ

2.4. Зміст звіту по лабораторній роботі

1. Титульний лист.
2. Мета роботи та завдання.
3. Теоретичні відомості (необов'язково).
4. Схема алгоритму програми.
5. Оригінальний текст програми з коментарями.
6. Набори тестових значень з основними результатами (скрини екранів та текст).
7. Висновки по роботі.

2.5. Варіанти завдань

Тип елементу структури Структура АТД, яку


Номер варіанту
даних вузла АТД потрібно реалізувати
1 Комплексне число Однонапрямлений список
2 Рядок Стек
3 Дійсне число Двонапрямлений список
4 Точка в просторі R3 Черга
Кільцевий
5 Рядок
однонапрямлений список
6 Ціле число Однонапрямлений список
7 Комплексне число Стек
8 Дійсне число Однонапрямлений список
9 Точка в просторі R3 Стек
10 Рядок Двонапрямлений список
11 Ціле число Двонапрямлений список
12 Комплексне число Черга
Кільцевий
13 Дійсне число
однонапрямлений список
14 Точка в просторі R3 Однонапрямлений список
15 Рядок Черга
16 Ціле число Стек
17 Комплексне число Двонапрямлений список
18 Дійсне число Черга

21
Тип елементу структури Структура АТД, яку
Номер варіанту
даних вузла АТД потрібно реалізувати
Кільцевий
19 Точка в просторі R3
однонапрямлений список
20 Рядок Однонапрямлений список
21 Комплексне число Однонапрямлений список
22 Дійсне число Двонапрямлений список
Кільцевий
23 Рядок
однонапрямлений список
24 Комплексне число Стек
25 Точка в просторі R3 Стек
26 Ціле число Двонапрямлений список
Кільцевий
27 Дійсне число
однонапрямлений список
28 Рядок Черга
29 Комплексне число Двонапрямлений список
Кільцевий
30 Точка в просторі R3
однонапрямлений список

2.6. Контрольні питання

1. Скільки полів може містити вузол списку? Від чого залежить кількість полів?
Наведіть приклади.
2. Якого типу можуть бути поля вузлів списку? Наведіть приклади.
3. Чи обов'язково застосовувати процедуру звільнення пам'яті, зайнятої вузлом,
коли ми позбавляємося від цього вузла в списку? Яким чином це впливає на
роботу програми?
4. Чи може вузол списку бути такого типу, щоб утримувати кілька полів типу
покажчика? Якщо - так, то наведіть приклад для чого це може бути потрібно.
5. Чи можна одночасно працювати з декількома списками відразу?
6. Як Ви вважаєте, на що потрібно звертати особливу увагу при роботі зі
списками?
7. Поняття двонапрямленого списку. Які можливі структури двонапрямленого
списку?
8. Як задавати двонапрямлений список?
9. Які основні операції над двонапрямленим списком?
10. Що таке АТД стек та черга?
11. Як представляються в пам'яті АТД стек та черга?
12. Які основні операції над АТД стек та черга?
13. У чому полягають відмінності між АТД стек та черга?
14. Переваги та недоліки різного представлення (списком, масивом) в пам'яті
АТД стек та черга.
15. Що таке кільцевий список? Представлення АТД кільцевий список у пам'яті.
16. В чому полягають відмінності при роботі з кільцевим списком та
двонапрямленим чи однонапрямленими списками?

22
ЛАБОРАТОРНА РОБОТА №3. БІНАРНІ ДЕРЕВА
Мета роботи: Вивчити принципи побудови абстрактних типів даних типу бінарне
дерево та алгоритми їх обробки мовою процедурного програмування Сі. До виконання
лабораторної роботи студенту слід ознайомитися з матеріалом відповідних розділів
теми "Дерева" [5]

3.1. Представлення бінарного дерева в пам'яті комп'ютера

У зв'язаному поданні бінарного дерева, кожна вершина буде складатися з


трьох частин: елемент даних, покажчик на лівий вузол і покажчик на правий
вузол. Таким чином, в мові програмування Сі бінарне дерево будується зі
структурою вершини, вказаною нижче на прикладі:
Приклад 3.1. Структура вершини бінарного дерева на мові Сі
struct BTnode {
struct BTnode *left; /* адреса лівого нащадка */
int data; /* дані, що зберігаються на вершині */
struct BTnode *right; /* адреса правого нащадка */
};

Кожне бінарне дерево має свій дескриптор, що містить покажчик root на


кореневий елемент (самий верхній елемент) дерева. Якщо root = NULL, то
дерево порожньо. Розглянемо бінарне дерево, наведене на малюнку нижче.
Схематичне представлення такого бінарного дерева наведено на малюнку
нижче:

Рис. 3.1. Схематичне представлення бінарного дерева в пам'яті

3.2. Траверс (обхід) бінарних дерев

Траверсом (обходом) бінарного дерева називається систематична процедура


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

23
3.2.1. Обхід в прямому порядку

При такому обході алгоритм спочатку обробляє вершину, потім її лівий


дочірній вузол, а після правий. Алгоритм прямого обходу також відомий як
алгоритм NLR (Node-Left-Right). Розглянемо дерево, представлене на малюнку
нижче, і пояснимо хід виконання процедури обходу:

Рис. 3.2 Початкове дерево для обходу в прямому порядку

Насамперед алгоритм звернеться до кореня і виведе значення D. Потім він


переміститься до лівої дочірньої вершині кореня, виведе B і розгляне вже її ліву
дочірню вершину, тобто A. Більше дочірніх вершин немає, тому алгоритм
повернеться у вершину B і пройде до його правої дочірньої вершині, в нашому
випадку до С. У неї теж немає нащадків, отже, відбудеться ще одне повернення
до вершини B. Раз дочірніх вершин у вершини B більше немає, то програма
підніметься вгору по дереву до кореня D і пройде до його правої дочірньої
вершини E. Не знайшовши у неї нащадків, алгоритм знову повернеться до
кореня, констатує відсутність будь-яких інших дочірніх вершин і завершить
обхід. В результаті порядок обходу буде виглядати так: D, B, A, C, E.
Алгоритм вивчає (проходить) вершини в одному порядку, а підсумки
виконаної роботи виводить в іншому. У наступному списку представлені кроки,
які виконуються під час прямого обходу для дерева, зображеного на Рис. 3..

1. Пройти D.
2. Вивести D.
3. Пройти B.
4. Вивести B.
5. Пройти A.
6. Вивести A.
7. Пройти B.
8. Пройти C.
9. Вивести C.
10. Пройти B.
11. Пройти D.
12. Пройти E.
13. Вивести E.
14. Пройти D.

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


програми:

24
Приклад 3.2 Алгоритм прямого обходу бінарного дерева
typedef struct BTnode {
struct BTnode *left;
int data;
struct BTnode *right;
};

void preOrder (BTnode* tree) {


if (tree ->!= NULL) { /* якщо вершина існує */
printf ("%d\n", tree-> data); /* обробка даних на вершині дерева */
preOrder (tree -> left); /* "пірнаємо" в ліве піддерево вузла */
preOrder (tree -> right); /* "пірнаємо" в праве піддерево вузла */
}
return;
}

Обхід в прямому порядку використовується не тільки для бінарних дерев, а


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

При симетричному обході алгоритм обробляє ліву дочірню вершину, потім


її саму і тільки після цього праву дочірню вершину. Алгоритм симетричного
обходу також відомий як алгоритм LNR (Left-Node-Right).
Працюючи з деревом, зображеним на Рис. 3., програма почне рухатися з
кореня, переміститься відразу до лівої дочірньої вершини B, а через неї до лівої
дочірньої вершини А. У цій вершині вже немає лівого нащадка, тому програма
виведе А і, не знайшовши правого нащадка , повернеться до батьківської
вершини B, щоб вивести її. Після цього алгоритм пройде до правої дочірньої
вершини С, у якій також відсутній лівий нащадок. Вивівши С, програма
упевниться, що немає і правого нащадка, і знову повернеться до батьківської
вершини B. Оскільки робота з лівою частиною дерева вже закінчена, алгоритм
підніметься до кореня D, виведе його і звернеться до правої частини з дочірньою
вершиною E. У цієї вершини немає лівого дочірнього вузла, значить, алгоритм
зразу виведе E і не знайшовши правого дочірнього вузла, повернеться до кореня
D. Підсумковий порядок симетричного обходу буде таким: A, B, C, D, E.
Слід звернути увагу на те, що виведені (оброблені) вершини
розташовуються в відсортованому порядку. Впорядковані дерева якраз і
будуються таким чином, щоб симетричний обхід дав на виході відсортовані
значення.
У наступному списку представлені кроки, які виконуються під час
симетричного обходу для дерева, зображеного на Рис. 3..

1. Пройти D.

25
2. Пройти B.
3. Пройти A.
4. Вивести A.
5. Пройти B.
6. Вивести B.
7. Пройти C.
8. Вивести C.
9. Пройти B.
10. Пройти D.
11. Вивести D.
12. Пройти E.
13. Вивести E.
14. Пройти D.

Рекурсивна реалізація описаного алгоритму представлена нижче у вигляді Сі-


програми:
Приклад 3.3 Алгоритм симетричного обходу бінарного дерева
typedef struct BTnode {
struct BTnode *left;
int data;
struct BTnode *right;
};

void inOrder (BTnode* tree) {


if (tree ->!= NULL) { /* якщо вершина існує */
inOrder (tree -> left); /* "пірнаємо" в ліве піддерево вузла */
printf ("%d\n", tree-> data); /* обробка даних в вершині дерева */
inOrder (tree -> right); /* "пірнаємо"в праве піддерево вузла */
}
return;
}

3.2.3. Обхід у зворотному порядку

Алгоритм обходу в зворотному порядку також відомий як алгоритм LRN (Left –


Right - Node).
В даному випадку алгоритм обробляє спочатку лівий дочірній вузол
вершини, потім правий і тільки після цього саму вершину. Як і в попередніх
обходах, алгоритм почне розглядати дерево, зображене на Рис. 3., з кореня,
переміститься у ліву частину до дочірньої вершини B, а через неї - до лівої
дочірньої вершини A. Не знайшовши подальших нащадків, він виведе A,
повернеться до батьківської вершини B і перейде до правої дочірньої вершини
C. У неї теж немає нащадків, тому програма виведе саму вершину C і знову
звернеться до батьківської вершини B. Оскільки робота з дочірніми вершинами
на цьому рівні закінчена, алгоритм виведе B, підніметься до кореня D і пройде

26
до правої дочірньої вершини E. Не виявивши пов'язаних з нею нащадків,
алгоритм зразу виведе E і повернеться до D. Раз обхід дочірніх вершин
закінчено, залишається вивести D і завершити траверс. В цілому порядок
проходження алгоритму буде таким: A, C, B, E, D.
У наступному списку представлені кроки, які виконуються під час
зворотного обходу для дерева, зображеного на Рис. 3..

1. Пройти D.
2. Пройти B.
3. Пройти A.
4. Вивести A.
5. Пройти B.
6. Пройти C.
7. Вивести C.
8. Пройти B.
9. Вивести B.
10. Пройти D.
11. Пройти E.
12. Вивести E.
13. Пройти D.
14. Вивести D.

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


Сі- програми:
Приклад 3.4 Алгоритм обходу у зворотному порядку бінарного дерева
typedef struct BTnode {
struct BTnode *left;
int data;
struct BTnode *right;
};

void postOrder (BTnode* tree) {


postOrder (tree -> left); /* "пірнаємо" в ліве піддерево вузла */
postOrder (tree -> right); /* "пірнаємо" в праве піддерево вузла */ printf
("%d\n", tree-> data); /* обробка даних в вершині дерева */
}
return;
}

Обхід в зворотному порядку також легко застосувати до дерев ступеня


більше 2: алгоритм повинен пройти по всім дочірнім вузлам вершини перед тим,
як обробити її саму.
3.2.4. Обхід в ширину

Здійснюючи обхід в ширину, алгоритм обробляє всі вершини дерева на

27
поточному рівні в порядку зліва направо, а потім переходить до вершин
наступного рівня. У випадку з деревом, зображеним на Рис. 3., програма
насамперед звернеться до кореня і виведе D, потім перейде до наступного рівня
і виведе вершини B і E, а на завершальному етапі опуститься на нижній рівень і
виведе A і С. Повний обхід буде виглядати так: D, B, E, A, C.
Цей алгоритм також називають BFT (breadth-first traversal) алгоритмом
обходу. Даний алгоритм не дотримується структури дерева, на відміну від
алгоритмів, розглянутих вище. У використовуваному прикладі немає дочірньої
гілки від вершини E до вершини A, тому неясно, як алгоритм до неї перейде.
Щоб вирішити це завдання, потрібно додати дочірні вершини в чергу (яку
створюємо додатково!), а потім обробити їх, коли завершиться обробка
батьківського рівня.
Приклад такого алгоритму на мові Сі наведено нижче:
Приклад 3.5 Алгоритм обходу бінарного дерева в ширину
typedef struct tQueueNode { /* структура вузла черги */
BTnode data; /* дані елемента черги - вершини дерева */
tQueueNode *next;
};

typedef struct tQD { /* дескриптор черги */


tQueueNode *front;
tQueueNode *rear;
int size;
};

typedef struct BTnode { /* вершина дерева */


struct BTnode *left;
int data;
struct BTnode *right;
};

typedef struct tBTD { /* дескриптор дерева */


int size; /* число вершин */
BTnode *root; /* корінь дерева */
};

tQD* CreateQueue(void); /* прототип створення простої черги */


int InQueue (tQD*, BTnode*); /* прототип постановки вершини дерева в чергу */
int ExQueue (tQD*, BTnode*); /* прототип вилучення вершини дерева з черги */
int ReleaseQueue (tQD*) /* прототип видалення черги */

int depthFirst(tBTD *dsTree) {

tQD *dsChildren;
BTnode *node;

28
// Створюємо чергу для зберігання дочірніх вершин при подальшій обробці.
dsChildren = CreateQueue();
if (!dsChildren) return -1; /* немає пам'яті в купі */

// Розміщуємо корінь в черзі.


InQueue (dsChildren, dsTree -> root);

// Обробляємо чергу, поки вона не стане порожньою.


While (dsChildren -> size !=0)
// Отримуємо наступну вершину з черги.
if (!ExQueue (dsChildren, node)) return -2; /* внутрішня помилка */

< обробляємо вершину.> /* тут знаходяться оператори обробки


даних в вершині node*/
// Додаємо дочірні вершини вузла node в чергу.
If (node -> left != null)
if (!InQueue (dsChildren, node -> left)) return -1;
If (node -> right != null)
if (!InQueue (dsChildren, node -> right) return -1;
}
if (!ReleaseQueue (dsChildren))
return -2;
else
return 0;
}

Спершу алгоритм створює чергу і розміщує в ній кореневу вершину. Потім


починається цикл, який працює до тих пір, поки черга не спорожніє. Усередині
циклу програма видаляє з черги першу вершину, обробляє її, а після додає в
чергу її дочірні вершини. У такому випадку всі елементи розглядаються в
порядку «Першим прийшов, першим пішов», тобто вершини поточного рівня
будуть оброблені повністю, перед тим як почнеться обробка будь-яких дочірніх
вершин. Оскільки алгоритм ставить в чергу відразу лівий дочірній вузол
вершини, а потім правий, на певному рівні всі вершини обробляються в порядку
зліва направо.
3.3. Створення дерева

Для управління деревом як динамічною структурою необхідна наявність


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

29
Рис. 3.3 Доступ до дерева за допомогою дескриптора
У найпростішому випадку дескриптор не використовується, доступ до
дерева здійснюється за покажчиком, оголошеному в програмі як змінна.
Алгоритм створення бінарного дерева зводиться до створення і ініціалізації
дескриптора. На мові Сі це виглядає так:
Приклад 3.6 Алгоритм створення бінарного дерева
typedef struct BTnode { /* вершина дерева */
struct BTnode *left;
int data;
struct BTnode *right;
};

typedef struct tBST { /* дескриптор дерева */


int size; /* число вершин */
BTnode *root; /* корінь дерева */
};

tBST* createTree (void) {


tBST *dsBST;
dsBST = (tBST*) malloc(sizeof(tBST));
if (!dsBST) return NULL; /* немає доступної пам'яті в купі,
дескриптор не створений */
dsBST -> root = NULL;
return dsBST;
}

3.4. Пошук вершини у бінарному дереві

Рис. 3.4 Алгоритм пошуку значення 17 в бінарному дереві пошуку

Процес пошуку починається з кореневої вершини. Алгоритм перевіряє, чи є

30
бінарне дерево пошуку порожнім. Якщо воно порожнє, то значення, яке ми
шукаємо відсутнє в дереві. Таким чином, алгоритм пошуку завершується
шляхом повернення значення NULL в якості знайденої вершини до програми,
що викликала. Якщо ж є вершини в дереві, то алгоритм перевіряє, чи є ключове
значення поточної вершини рівним значенню пошуку. Якщо ТАК, алгоритм
повертає в викликану програму адресу знайденої вершини і завершується. Якщо
НІ, то алгоритм перевіряє, чи є значення для пошуку меншим, ніж значення
поточної вершини, і в цьому випадку він повинен рекурсивно викликати сам
себе з лівої дочірньої вершини в якості параметра. У разі, якщо значення пошуку
більше, ніж значення ключа поточної вершини, алгоритм повинен рекурсивно
викликати себе з правої дочірньої вершини.
Алгоритм пошуку на мові Сі може виглядати так:
Приклад 3.7 Алгоритм пошуку в бінарному дереві пошуку
typedef struct BTnode { /* вершина дерева */
struct BTnode *left;
int data;
struct BTnode *right;
};

BTnode *searchElement (BTnode *root, int key) {


if (root -> data == key || root == NULL)
return (root);
else if (key < root -> data)
return ( searchElement(root -> left, key) );
else
return ( searchElement(root -> right, key) );
}

3.5. Включення нового елемента у бінарне дерево

Функція включення (вставки) використовується для додавання нової


вершини із заданим значенням ключа в правильну позицію бінарного дерева
пошуку.
Алгоритм включення в дерево окремих елементів складається з трьох дій:
1. пошук місця включення;
2. отримання динамічної пам'яті для нової вершини і створення
зв'язку вершини з деревом за допомогою покажчика;
3. занесення інформаційних даних елемента до новоствореної
вершини.
Додавання вершини в правильну позицію означає, що нова вершина не
повинна порушувати властивості бінарного дерева. На малюнку нижче
показаний приклад вставки ключа 42 в бінарне дерево пошуку:

31
Рис. 3.5 Приклад вставки ключа 42 у бінарне дерево пошуку

В прикладі нижче показано алгоритм включення заданого значення ключа в


бінарне дерево пошуку.
Приклад 3.8 Алгоритм включення заданого значення в бінарне дерево пошуку
typedef struct BTnode { /* вершина дерева */
struct BTnode *left;
int data;
struct BTnode *right;
};

typedef struct tBST { /* дескриптор дерева */


int size; /* число вершин */
BTnode *root; /* корінь дерева */
};

BTnode* insertTree (tBST *dsTree, BTnode *subTree, int key) {


BTnode *tempNode; /* тимчасова вершина */

/* місце вставки знайдено (поточна вершина вже термінальна), виконується


створення вершини і її заповнення */
if (subTree == NULL) {
subTree = (BTnode*) malloc(sizeof(BTnode));
if (!subTree) return NULL; // не вистачає місця в "купі" під нову вершину
subTree -> data = key;
subTree -> left = subTree -> right = NULL;
dsTree -> size++;
return (subTree);
}

// місце вставки ще не знайдено,

// 1. Продовжуємо пошук місця вставки по лівій гілці

32
else if (key < subTree -> data)
tempNode = insertTree(dsTree, subTree -> left, key);

// 2. Продовжуємо пошук місця вставки по правій гілці


else
tempNode = insertTree(dsTree, subTree -> right, key);

return tempNode;
}

3.6. Видалення вершини з бінарного дерева пошуку

Функція видалення вершини є найскладнішою з операцій над BST. Вся


справа в тому, що властивості двійкового дерева пошуку не повинні порушитися
при видаленні однієї з вершин. Існує три різних випадки видалення вершин з
BST (в залежності від наявності у вершини дочірніх нащадків), кожен з яких ми
розглянемо окремо.
3.6.1. Випадок 1: видалення вершини, яка не має нащадків.

Подивимося на бінарне дерево пошуку, наведене на малюнку нижче. Якщо


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

Рис. 3.6 Приклад видалення вершини 78, яка не має нащадків

3.6.2. Випадок 2: видалення вершини з одним нащадком.

Щоб впоратися з таким завданням єдиний нащадок вершини, що


видаляється, встановлюється в якості нащадка її батьківської вершини. Іншими
словами, видалена вершина в дереві замінюється її нащадком. Наприклад,
видалимо вершину 5 з дерева нижче:

33
Рис. 3.7 Видалити вершину 5 з бінарного дерева пошуку

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

Рис. 3.8 Бінарне дерево пошуку після видалення з нього вершини 5

3.6.3. Випадок 3: видалення вершини з двома нащадками.

Видаляємо знову вершину 5, але трохи з іншого дерева:

Рис. 3.9 Видалення вершини 5 з двома нащадками

Зробимо тепер по-іншому: знайдемо в правому піддереві мінімум. Ясно, що


його можна знайти, якщо почати в правому нащадку вузла 5 і йти до упору вліво,

34
по лівим гілкам. Оскільки у знайденому мінімуму немає лівого сина, можна
вирізати його за аналогією з випадком 1 і вставити його замість видаляємої
вершини. Через те що він був мінімальним в правому піддереві, властивість
впорядкованості не порушується:

Рис. 3.10 Бінарне дерево пошуку після видалення з нього вершини 5

Алгоритм, який реалізує в собі всі три вищевказаних випадки, наведено у


прикладі нижче:
Приклад 3.9 Алгоритм видалення вершини дерева
typedef struct BTnode { /* вершина дерева */
struct BTnode *left;
int data;
struct BTnode *right;
};

typedef struct tBST { /* дескриптор дерева */


int size; /* кількість вершин */
BTnode *root; /* корінь дерева */
};

// рекурсивна функція знаходження мінімального елемента у дереві


BTnode *findSmallestElement(BTnode *tree) {
if (tree == NULL || tree -> left = NULL)
return tree;
else
return findSmallestElement(tree -> left);
}

// функція видалення елемента


BTnode *deleteElement(tBST* dsTree, BTnode *subTree, int key) {

BTnode *current, /* адреса поточної вершини */


*parent, /* адреса батьківської вершини */
*temp; /* адреса тимчасової вершини */

// випадок виродженого дерева

35
if (!subTree)
return (subTree);

// пошук вершини, що видаляється, в лівому (правому) піддереві


parent = current = subTree;
while (current != NULL && key != current–>data) {
parent = current;
current = (key < current –> data) ? current –> left : current–>right;
}
if (!current) return NULL; // вершина, що видаляється, в дереві відсутня

/******************** вершина, що видаляється, знайдена ****************************/

// Випадок 3, присутність двох нащадків у вершини, що видаляється

if (current -> left && current -> right) {


/* пошук мінімального елемента у правому піддереві з переносом його на
місце видаленої вершини */
current -> data = findSmallestNode(current -> right) -> data;
// видалення мінімального елемента у правому піддереві
return deleteElement (current -> right, current -> data);
}

// Випадок 1, у вершини, що видаляється, відсутні нащадки

else if (current -> left == NULL && current -> right == NULL) {
// якщо вершина, що видаляється стоїть зліва від батьківської
if (parent -> left == current)
parent - > left = NULL;
// якщо справа від батьківської
else
parent -> right = NULL;
temp = current;
dsTree -> size--;
free (current);
return temp;
}

// Випадок 2, один нащадок у вершини, що видаляється

else if (current -> left != NULL) // якщо нащадок лівий


parent -> left = current -> left;
else // якщо нащадок правий
parent -> right = current -> right;
temp = current;
dsTree -> size--;
free (current);
return temp;
}

36
3.7. Видалення бінарного дерева

Щоб видалити дерево бінарного пошуку з пам'яті, ми спершу повинні


видалити всі вершини в лівому піддереві, а потім видалити всі вершини в
правому піддереві. Після цього можна видалити і дескриптор дерева. Алгоритм
в прикладі нижче дає рекурсивну процедуру повного видалення бінарного
дерева пошуку.
Приклад 3.10 Алгоритм повного видалення бінарного дерева пошуку
typedef struct BTnode { /* вершина дерева */
struct BTnode *left;
int data;
struct BTnode *right;
};

int deleteTree (BTnode *tree) {


if (tree) {
deleteTree (tree -> left);
deleteTree (tree -> right);
free (tree);
return 0;
}
else
return 1;
}

3.8. Опис виконання роботи

У відповідності зі своїм варіантом реалізувати алгоритми на мові Сі для власної


структури АТД. Взяти до уваги наступні вимоги.
1. Для побудови структури даних АТД забороняється використовувати готові та
стандартні структури АТД мови Сі.
2. Для розміщення АТД використовувати механізм динамічної пам'яті. Для
доступу к АТД використовувати виключно відповідний дескриптор.
3. Для АТД типу бінарне дерево необхідно передбачити наступний функціонал:
• вивести на екран елемент із заданою властивістю;
• відображати поточне положення у дереві;
• вставити додатковий елемент у дерево;
• виключити певний елемент з дерева;
• перевірка дерева на порожнечу;
• отримання розміру дерева та його висоти;
• сортування дерева;
• відображення дерева на екрані.
4. Організувати тестування розроблених алгоритмів і структур даних за
допомогою консолі. При виконанні кожної операції над АТД здійснювати вивід
стану структури даних на консоль (у файл).
5. Число елементів в дереві повинно бути не менше 20. Передбачити, що
заповнення елементів дерева проводити за допомогою генератора
випадкових чисел, чи вручну (для тестування).
6. Зробити висновки по роботі.

37
3.9. Вимоги до виконання роботи

1. Всі програми повинні бути написані на мові Сі.


2. Реалізовувати відповідні алгоритми обробки АТД у вигляді функцій, з
передачею їм дескрипторів АТД, у вигляді параметрів.
3. Глобальні змінні використовувати заборонено.
4. Програма повинна працювати у діалозі та взаємодії с користувачем.
5. Текст програми повинен відповідати правилам оформлення програм на мові
Сі, наведеними в ДОДАТОК 1. ВИМОГИ ДО ОФОРМЛЕННЯ КОДУ ПРОГРАМ
НА МОВІ СІ

3.10. Зміст звіту по лабораторній роботі

1. Титульний лист.
2. Мета роботи та завдання.
3. Теоретичні відомості (необов'язково).
4. Схеми алгоритмів обробки дерева.
5. Оригінальний текст програми з коментарями.
6. Набори тестових значень з основними результатами (скрини екранів та текст).
7. Висновки по роботі.

3.11. Варіанти завдань

1. Для організації двійкового дерева пошуку визначити відповідний


структурний тип, який містить поля, використані в лабораторній роботі № 2.
Для отримання ключового поля звернутися до викладача.
2. Розробити такі функції:
 створення дерева;
 вставки вузла в дерево;
 видалення вузла з дерева;
 проходження дерева (показувати на екрані послідовність вузлів):
a. в прямому порядку;
b. в зворотному порядку;
c. симетричним обходом.
 пошук вузла в дереві;
 видалення дерева;
 зберігання дерева в двійковому файлі на диску;
 читання дерева з двійкового файлу на диску.
3. Виконати пошук інформації в дереві. Перевірити варіанти успішного і не
успішного пошуку.
4. Зробити висновки по роботі.

3.12. Контрольні питання

1. Що таке дерево? Охарактеризуйте основні поняття, пов'язані з деревом


2. Яке дерево називається бінарним деревом пошуку? Охарактеризуйте

38
основні поняття, пов'язані з бінарним деревом
3. Які способи обходу бінарного дерева ви знаєте?
4. У чому полягає особливість бінарних дерев пошуку?
5. Для яких цілей застосовуються бінарні дерева? Які види бінарних
дерев ви знаєте?
6. Як бінарні дерева представляються у пам'яті ЕОМ?
7. Як здійснюється вставка і видалення елемента бінарного дерева
пошуку?
8. Як здійснюється створення і видалення бінарного дерева?
9. Як здійснюється пошук елемента в бінарному дереві?

39
ЛАБОРАТОРНА РОБОТА №4. АЛГОРИТМИ І ПРИНЦИПИ
ХЕШУВАННЯ. РОЗВ'ЯЗАННЯ КОЛІЗІЙ
Мета роботи:
– вивчити особливості організації хеш-таблиць для різних типів алгоритмів
хешування;
– отримати практичні навички використання функцій рехешування для
розширення колізій в хеш-таблицях;
– створити консольний додаток, в якому реалізовані операції вставки,
видалення і пошуку елементів в хеш-таблиці.

4.1. Загальні поняття і визначення

Часто бувають потрібні динамічні множини, що підтримують тільки словникові


операції: додавання, пошук і видалення елемента. Основне завдання організації такої
множини - знайти структуру даних, що забезпечує мінімальні витрати на пошук.
У цьому випадку застосовують так зване хешування, а відповідна структура
даних називається «хеш-таблиця». Задана множина елементів при цьому
організовується як звичайний масив. Однак місце розташування окремого елемента
множини в масиві визначається не місцем розташування (порядку проходження) ключа
відносно інших ключів, як у випадку відсортованої множини, а власним значенням
(вмістом) ключа цього елемента.
Таким чином, занесення нового елемента в масив виконується за допомогою
функції перетворення ключів (розстановки, хешування), яка перетворює ключ елемента
в індекс масиву. У цьому випадку говорять про хешовані таблиці, таблиці з
обчислюваними входами або асоціативні масиви чи словники.
Хешування (англ. Hashing) - перетворення вхідного масиву даних довільної
довжини в вихідний бітовий рядок фіксованої довжини. Такі перетворення також
називаються хеш-функціями або функціями згортки, а їх результати називають хешем,
хеш-адресою.
Хеш-функція - функція, яка перетворює ключ в певний індекс в масиві.
Хеш-таблиця - це звичайний масив з адресацією, що задається хеш-функцією.

4.2. Приклад організації хеш-таблиці

Припустимо, що деяка фірма випускає деталі і кодує їх семизначними числами.


Для застосування прямої індексації з використанням повного семизначного ключа
потрібен був би масив з 10 мільйонів елементів. Зрозуміло, що це призвело б до втрати
дуже великого простору, оскільки реально фірма випускає не більше 1000 деталей.
Таким чином, для зберігання інформації про деталі досить мати масив для зберігання
1000 елементів з діапазоном індексів від 0 до 999.
Для того, щоб мати можливість прямого доступу до елементу масиву по ключу,
необхідний деякий метод перетворення ключа у відповідний індекс масиву всередині
діапазону 0 - 999. В якості такого методу можна використовувати три останні цифри
номера вироба, для чого слід поділити націло номер виробу на 10 000. В даному
випадку хеш-функція для занесення записів про деталі в таблицю може мати наступний
вигляд:
Приклад 4.1
int Hesh(int key)
{return key%10000}

40
Структура самої хеш-таблиці для розглянутого прикладу приведена на рисунку нижче:
Таблиця 4.1
Індекс Ключ (код деталі) Запис
0 4967000
1 6584001
2 8421002

...
396 4618396
397 4759397
398 9572398
399 1286399

...
990 0000990
991 0000991
992 1200992
993 0047993
...

998 5682998
999 0001999

4.3. Функції хешування

Хеш-функція має вирішальний вплив на продуктивність хеш-таблиці. Гарна хеш-


функція повинна якомога рівномірніше розподіляти ключі по всьому діапазону значень
індексів. Це дозволяє скоротити довжину ланцюжків розміщення і час, що витрачається
на пошук елементів в таблиці.
Найбільш прості і відомі алгоритми хешування ґрунтуються на таких функціях
хешування:
- отримання залишку;
- мультиплікативна;
- метод середини квадрата;
- перетворення символьних ключів.
4.3.1. Отримання залишку

Отримання хеш-адреси елемента в таблиці засноване на використанні залишку


від ділення числового еквівалента ключа K на число M, що дорівнює або близьке до
числа елементів (сегментів) хеш-таблиці N.
Hach (K) = K % M
Отриманий цілий залишок приймається за індекс сегмента в таблиці. Для
зменшення колізій, як правило, краще всього вибирати M простим числом, тому що
просте число збільшує ймовірність того, що генерація хеш-значення по ключу
відбуватиметься рівномірно по всій області значень функції. M також не повинно бути

41
занадто близьким до степеня числа 2. Тому що у цьому випадку функція буде просто
видавати молодші K біт двійкового представлення ключа K.

4.3.2. Мультиплікативний метод

Алгоритм мультиплікативного методу є наступним:


Приклад 4.2
Крок 1: Вибрати константу A таку, що 0 <A <1.
Крок 2: Помножити ключ К на А.
Крок 3: Взяти дробову частину добутку кроку 2.
Крок 4: Помножити результат кроку 3 на розмір хеш-таблиці (M).
Отже, хеш-функцію можна визначити як:
Hach (K) = 𝑀 ∗ ⌈𝐾 ∗ 𝐴⌉,
Найголовніша перевага цього методу полягає в тому, що він працює практично з
будь-яким значенням А. Хоча оптимальний вибір значення A все ж існує. Дональд Кнут
в своїй знаменитій багатотомній праці, присвяченій алгоритмам, припустив, що
найкращим вибором для А є вибір числа φ = 0.6180339887 ("золотий" переріз).

4.3.3. Метод середини квадрата

Метод середини квадрата є хорошою хеш-функцією, яка працює за два кроки:


Приклад 4.3
Крок 1: Обчислюється квадрат значення ключа K. Тобто, знаходимо K2.
Крок 2: З отриманого цілого числа беремо R середніх цифр. Це і буде індекс в
хеш-таблиці.
Алгоритм добре працює, тому що більшість або всі цифри цілочисельного
представлення ключа впливають на результат. Це відбувається тому, що всі цифри в
цілочисельному поданні ключа сприяють породженню середніх цифр значення його
квадрата. Таким чином, в результаті не домінує розподіл останніх цифр або перших
цифр значення ключа. У методі середини квадрата, одні і ті ж R цифр повинні
вибиратися для всіх ключів.

4.3.4. Перетворення символьних ключів

Ключ розбивається на групи по 4-ри символи. Неповна група доповнюється зліва


пробілами. Кожна група, наприклад група, що складається з 4-х символів abcd,
обробляється як число на основі розміру таблиці кодування. Для повної таблиці
символів ASCII (або ANSI) розміром в 256 символів отримаємо:
Приклад 4.4
a * 2563 + b * 2562 + c * 2561 + d * 2560,
де символ, наприклад а, замінюється його кодом у відповідній таблиці символів.
Після перетворення всіх блоків в цілі числа вони підсумовуються, потім отримана
сума зводиться в квадрат і з результату вибираються середні цифри.

4.4. Колізії і їх розв'язання

Основна проблема організації хеш-таблиць полягає в тому, що хеш-адреси двох

42
різних ключів можуть збігатися. Наприклад, при додаванні в Таблиця 4.1, наведену
вище, запису з ключем 0596998 виникне така ситуація, що дана комірка вже зайнята.
Ситуація, коли два або більше ключа асоціюються з однією і тією ж коміркою
таблиці називається колізією при хешуванні. Слід зазначити, що гарною хеш-
функцією є така функція, яка мінімізує колізії і розподіляє записи рівномірно по всій
таблиці.
Для вирішення проблеми колізій необхідно визначити:
 якого виду хеш-функцію потрібно використовувати, щоб зменшити
число колізій?
 що робити в разі виникнення колізій?
 використовувати іншу хеш-функцію або послідовність функцій;
 використовувати додаткову структуру даних (найчастіше
упорядкований список), в якому розміщуються всі елементи множини за
ключами, які претендують на один і той самий індекс.
Зазначені два підходи вирішення колізій відображаються в різних типах
алгоритмів хешування. Існує дві різні форми хешування. Одна з них називається
відкритою, або зовнішнім хешуванням і дозволяє зберігати множини в потенційно
нескінченному просторі, знімаючи тим самим обмеження на розмір множин. Інша
називається закритою, або внутрішнім хешуванням і використовує обмежений розмір
пам'яті для зберігання даних, обмежуючи таким чином розмір елементів хеш-масиву.
Алгоритми хешування цих двох способів наведено у відповідному розділі [5].

4.5. Основні операції з хеш-таблицею

До основних операцій з хеш-таблицею відносяться наступні:


 вставка елемента.
 пошук елемента.
 видалення елемента
 розширення хеш-таблиці
Приклади відповідних алгоритмів наведено у відповідних розділах [5].

4.6. Опис виконання роботи

У відповідності зі своїм варіантом реалізувати алгоритми хешування на мові Сі для


власної структури АТД типу хеш-таблиці. Взяти до уваги наступні вимоги.
1. Для побудови структури даних АТД забороняється використовувати готові та
стандартні структури АТД мови Сі.
2. Для розміщення хеш-таблиці використовувати виключно механізм динамічної
пам'яті.
3. Для АТД типу хеш-таблиці необхідно передбачити наступний функціонал:
• ініціалізація хеш-таблиці;
• пошук елемента в хеш-таблиці;
• відображати поточний зміст хеш-таблиці чи окремого елементу;
• додавання нового елемента у хеш-таблицю;
• видалення певного елемента з хеш-таблиці;
• отримання поточного розміру розміру хеш-таблиці;
• збільшення розміру хеш-таблиці.
4. Організувати тестування розроблених алгоритмів і структур даних за
допомогою консолі. При виконанні кожної операції над АТД здійснювати вивід

43
стану структури даних на консоль (у файл).
5. Число елементів у хеш-таблиці повинно бути змінним, але не меншим від 20.
Передбачити, що заповнення елементів хеш-таблиці проводити за
допомогою генератора випадкових чисел, чи вручну (для тестування).
6. Зробити висновки по роботі.

4.7. Вимоги до виконання роботи

1. Всі програми повинні бути написані на мові Сі.


2. Реалізовувати відповідні алгоритми обробки АТД у вигляді функцій, з
передачею їм дескрипторів АТД, у вигляді параметрів.
3. Глобальні змінні використовувати заборонено.
4. Програма повинна працювати у діалозі та взаємодії з користувачем.
5. Текст програми повинен відповідати правилам оформлення програм на мові
Сі, наведеними в ДОДАТОК 1. ВИМОГИ ДО ОФОРМЛЕННЯ КОДУ ПРОГРАМ
НА МОВІ СІ

4.8. Зміст звіту по лабораторній роботі

1. Титульний лист.
2. Мета роботи та завдання.
3. Теоретичні відомості (необов'язково).
4. Схеми алгоритмів обробки хеш-таблиці.
5. Оригінальний текст програми з коментарями.
6. Набори тестових значень з основними результатами (скрини екранів та текст).
7. Висновки по роботі.

4.9. Варіанти завдань

У якості структури (типу) даних, що буде зберігатися у хеш-таблиці, слід взяти тип
даних та ключ, який використовувався у 3 лабораторній роботі. При використанні у
якості ключа символьного рядку – використовувати функцію хешування, описану у п.
4.3.4, при використанні у якості ключа числового цілочисельного значення –
використовувати у якості функції хешування метод середини квадрата (п. 4.3.3).
Таблиця 4.2
Варіант Алгоритм Функція Початковий Розмір
хешування пробірування розмір блоку
хештаблиці розширення

метод
1 - 100 -
ланцюжків
відкрита
2 лінійна 10 15
адресація
метод
3 - 100 -
ланцюжків

44
Варіант Алгоритм Функція Початковий Розмір
хешування пробірування розмір блоку
хештаблиці розширення

відкрита
4 квадратична 10 10
адресація
метод
5 - 100 -
ланцюжків
відкрита подвійне
6 10 13
адресація хешування
метод
7 - 100 -
ланцюжків
відкрита
8 лінійна 10 13
адресація
метод
9 - 100 -
ланцюжків
відкрита
10 квадратична 10 14
адресація
метод
11 - 120 -
ланцюжків
відкрита подвійне
12 12 12
адресація хешування
метод
13 - 120 -
ланцюжків
відкрита
14 лінійна 12 16
адресація
метод
15 - 120 -
ланцюжків
відкрита
16 квадратична 12 17
адресація
метод
17 - 120 -
ланцюжків
відкрита подвійне
18 12 20
адресація хешування
метод
19 - 12 -
ланцюжків
відкрита
20 квадратична 12 18
адресація
відкрита
21 квадратична 10 14
адресація
метод
22 - 100 -
ланцюжків

45
Варіант Алгоритм Функція Початковий Розмір
хешування пробірування розмір блоку
хештаблиці розширення

відкрита
23 лінійна 10 13
адресація
метод
24 - 100 -
ланцюжків
відкрита подвійне
25 10 13
адресація хешування
метод
26 - 100 -
ланцюжків
відкрита
27 квадратична 10 10
адресація
метод
28 - 100 -
ланцюжків
відкрита
29 лінійна 10 15
адресація
метод
30 - 100 -
ланцюжків

4.10. Контрольні питання

1. Що записується в хеш-таблицю?
2. Чим визначається індекс запису в хеш-таблиці?
3. Які основні проблеми хешування і в чому вони полягають?
4. Скільки операцій порівняння виконується при пошуку по ключу із
застосуванням таблиць прямого доступу?
5. Які недоліки алгоритма пошука по ключу з використанням відкритої
адресації?
6. Яке призначення хеш-функції?
7. Назвіть базові функції хешування.
8. Як створюється хеш-таблиця при реалізації метода відкритої адресації? Що
зберігається в елементах такої хеш-таблиці?
9. Назовіть алгоритми розв’язку колізій при відкритій адресації.
10. В чому суть метода розв’язку колізій при використанні розподілених ланцюгів
переповнень?

46
РЕКОМЕНДОВАНА ЛІТЕРАТУРА
Базова

1. Хаггарти Р. Дискретная математика для программистов, 2-е изд. - Москва:


Техносфера, 2012. – 400 с.
2. Стивенс Р. Алгоритмы. Теория и практическое применение. – Москва:
Издательство "Э", 2016. — 544 с.
3. Седжвик Р. Фундаментальные алгоритмы на С. Анализ / Структуры данных /
Сортировка /Поиск / Алгоритмы на графах. - СПб: ООО «ДиаСофтЮП», 2003.-
1136 с.
4. Хусаинов Б.С. Структуры и алгоритмы обработки данных. Примеры на языке Си:
Учеб. пособие. — Москва: Финансы и статистика, 2004.— 464 с.: ил.
5. Конспект лекций кредитного модуля «Алгоритмы и структуры данных» для
студентов направления подготовки «Автоматизация и компьютерно-
интегрированные технологии» // Составитель Грудзинский Ю.Е. – Киев: НТУУ
"КПИ". – 2017 г. – 55 с.

Допоміжна

6. Thareja R. Data Structures Using C, 2nd Edition. - New Delhi: Oxford University Press,
2014. – 557 p.
7. Bhasin H. Algorithms. Design and Analysis. - New Delhi: Oxford University Press,
2015. – 727 p.
8. Головешкин В.А., Ульянов М.В. Теория рекурсии для программистов. - М.:
ФИЗМАТЛИТ, 2006. - 296 с.
9. Ворожцов А.В. Алгоритмы, анализ, построение и реализация на языке Си:
Лекции. – Москва: МФТИ, 2007. – 452 с.
10. Хэзфилд Р., Кирби Л. Искусство программирования на Си. Фундаментальные
алгоритмы, структуры данных и примеры приложений: Энциклопедия
программиста. – Киев: Диасофт, 2001. – 736 с.
11. Клейнберг Дж., Тардос Е. Алгоритмы. Разработка и применение. - СПб.: Питер,
2016. — 800 с.
12. Кормен Т.Х. Алгоритмы: вводный курс. — М.: ООО “И.Д. Вильямс”, 2014. — 208
с.
13. Скиена С. Алгоритмы. Руководство по разработке, 2-е изд. — СПб.: БХВ-
Петербург, 2011. — 720 с.
14. Tiwari N.K., Agrawal J., Shandilya Sh.K. DATA STRUCTURES. - New Delhi: I.K.
International Publishing House Pvt. Ltd., 2016. – 275 p.
15. Алексеев В.Е., Таланов В.А. Графы. Модели вычислений. Структуры данных:
Учебник. – Нижний Новгород: Изд-во ННГУ, 2005. 307 с.
16. Валединский В.Д., Пронкин Ю.Н. Вычислительные системы и
программирование. Схемы хранения данных. – Москва: Изд-во ЦПИ при
механико-математическом факультете МГУ, 2006. – 168 с.

47
Додаткова література, видана на українській мові

17. Єжова Л. Ф. Алгоритмізація і програмування процедур обробки інформації


(2000): Навчально-методичний посібник. – Київ: КНЕУ, 2000. – 152 с.
18. Трофименко О.Г. С++. Основи програмування. Теорія та практика: Підручник. -
Одеса: Фенікс, 2010. – 544 с.
19. Ковалюк Т.В. Основи програмування. – Київ: Видавнича група BHV, 2005. – 384
с.
20. Андрійчук В.І., Комарницький М.Я., Іщук Ю.Б. Вступ до дискретної математики. –
Львів: Видавничий центр ЛНУ ім І. Франка , 2003. – 254 с.

48
ДОДАТОК 1. ВИМОГИ ДО ОФОРМЛЕННЯ КОДУ ПРОГРАМ НА МОВІ СІ
Угоди по ідентифікаторам

Підбір ідентифікаторів

А. Всі ідентифікатори повинні вибиратися з міркувань читабельності і


максимального семантичного навантаження.
Наприклад:

const float Eps = 0.0001; // Точність


unsigned short Sum; // Сума
unsigned char Message [20]; // повідомлення

Невдалими можна вважати ідентифікатори:

const float UU = 0.0001; // Точність


unsigned short Kk; // Сума
unsigned char Zz [20]; // повідомлення

Б. Ідентифікатори рекомендується підбирати з слів англійської мови. наприклад:


// Видає звуковий сигнал заданої частоти і тривалості
void Beep (unsigned short Hertz, unsigned short MSec);

// Видає True (1), якщо файл з ім'ям FName існує


unsigned char ExistFile (unsigned char * FName);

// Ознака закінчення роботи з програмою


unsigned char Done;

// Розміри виробу (ширина, висота)


unsigned short Width, Height;
Не дуже вдалими можна вважати ідентифікатори:
// Видає звуковий сигнал заданої частоти і тривалості
void Zvuk (unsigned short Chast, unsigned short Dlit);

// Видає True (1), якщо файл з ім'ям Im існує


unsigned char EstFile (unsigned char * Im);

// Ознака закінчення роботи з програмою


unsigned char Konec;

// Розміри виробу (ширина, висота)


unsigned short Shirina, Vysota;

Написання ідентифікаторів

Існує два основних способи написання ідентифікаторів.

49
А. У будь-якому ідентифікаторі кожне слово, що входить в ідентифікатор, писати,
починаючи з великої літери, інші літери - маленькі.
Наприклад:
float NextX, LastX; // Наступна і попередня ітерація
char BeepOnError; // Звуковий сигнал при
// неправильних діях користувача
unsigned char FileName [20];
Б. У будь-яких ідентифікаторах кожне слово, що входить в ідентифікатор, розділяти
символом _, при цьому всі букви - маленькі.
Наприклад:
float next_x, last_x; // Наступна і попередня ітерація
char beep_on_error; // Звуковий сигнал при
// неправильних діях користувача
unsigned char file_name [20];
В. Для ідентифікаторів імен функцій прийнято використовувати перше слово
ідентифікатора з маленької літери, а усі наступні – з великої. Символ _ при цьому не
застосовується.
Наприклад:
// Стандартна функція модуля Graph; видає опис
// помилки використання графіки по її коду
unsigned char * graphErrorMsg (short err_code);
Г. Усі літери ідентифікаторів макросів, макровизначень у програмі прийнято писати
використовуючи виключно великі літери.
Наприклад:
#define SIZE 100
#define ABS(x) ((x)<0?-(x):(x))


double Arr(SIZE);


c = ABS(a-b);

Угоди по самодокументованих програмах

Коментарі

А. Коментарі в тілі програми слід писати виключно українською чи російською мовою


і по суті так, щоб програміст, який не брав участі в розробці програми (але має досвід
роботи на мовах Сі/Асемблер), міг без особливих зусиль розібратися в логіці програми,
і, при необхідності, супроводжувати даний програмний продукт.
Б. Рекомендується коментарі в програмі писати після символів //, а /* і */
використовувати при налагодженні програми як "заглушки" ділянок програмного коду.

50
Специфікація функцій

Для кожної користувальницької функції повинна бути описана у вигляді коментаря


специфікація, що містить наступну інформацію:
- призначення функції;
- опис семантики параметрів-значень (параметрів, що передаються за
значенням);
- опис семантики параметрів-змінних (параметрів, що передаються по
посиланню);
- опис семантики, що повертається.
Наприклад:
////////////////////////////////////// Gauss /////////////////////////////////////////////////////////////////////////
// Рішення системи лінійних алгебраїчних рівнянь
// Методом Гаусса.
//
// Вхід:
// A - матриця коефіцієнтів системи;
// B - стовпець вільних членів системи;
// Eps - точність обчислень.
//
// Вихід:
// X - вектор рішення
// HasSolution - прапорець, що встановлюється в True, якщо рішення
// системи існує, і в False у всіх інших випадках
// NumOfRoots - число коренів в рішенні системи, може приймати
// значення:
// 0 - якщо рішення системи не існує,
// MaxN - якщо рішення системи існує і єдино,
// MaxInt - якщо існує нескінченна безліч рішень;
// Det - значення визначника матриці A
// AForReverse - нижня трикутна матриця, отримана з A
// в результаті виконання прямого ходу алгоритму Гауса
// BForReverse - стовпець вільних членів, отриманий
// з B в результаті виконання прямого ходу
// алгоритму Гаусса
//
// Результат Gauss: 0 - успішно,1 – помилка.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

unsigned char Gauss( Matrix A, Vector B, float Eps,


Vector* X, char* HasSolution, short* NumOfRoots,
float* Det, Matrix* AForReverse, Vector* BForReverse
){

return 0;

return 1;
}

Примітка:

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

Специфікація програмного файлу або модуля

Програмний файл або модуль повинен починатися зі специфікації у вигляді


коментаря, що містить наступну інформацію:
- ідентифікація проекту, до якого належить файл;
- призначення (назва) та ім'я файлу;
- версія файлу;
- прізвище автора;
- опис модуля;
- історія змін модуля.
Наприклад:
/*-------------------------------------------------------------------------------------------------------------------
Проект: ABC-1.0
Назва: Математичні розрахунки
Файл: primes.c
Версія: 1.0.2
Автор: Наливайко І.М.
Опис: Підрахунок кількості простих чисел.
Зміни:
---------------------------------------------------------------------------------------------------------------------
№ Дата Версія Автор Опис
----------------------------------------------------------------------------------------------------------------------
1 01.03.16 1.0.1 Наливайко І.М.. Розрахунок на проміжку [1..200].
2 05.07.16 1.0.2 Підрахуйко І.М. Розрахунок на проміжку користувача.
------------------------------------------------------------------------------------------------------------------*/
Примітка:
Після специфікації програмного файлу рекомендується помістити коментар до
вказівок щодо запуску програми і роботі з нею (вказівками щодо використання модуля
іншими програмістами) або посилання на джерело, яке використано при написанні
програми (модуля).

Угоди по читабельності програм

"Драбинка"

"Драбинка" повинна відображати структурну вкладеність мовних конструкцій.


Рекомендується відступ не менше 2-х і не більше 4-х пропусків (пробілів). Прийнятого
відступу потрібно дотримуватися у всьому тексті програми. Правила написання деяких
конструкцій:
if ( <умова> ) {
<оператори>
}

if ( <умова> )
<оператор>;

52
if ( <умова> ) {
<оператори>
}
else {
<оператори>
}

while ( <умова> ) {
<операторы>
}

while ( <умова> )
<оператор>;

for ( <ініц.. лічильника>; <умова>; <зміна лічильника> ) {


<оператори>
}

for ( <ініц.. лічильника>; <умова>; <зміна лічильника> ) {


<оператор>;

switch ( <вираз> ) {
case <вираз>:
<оператори>;
break;
........
default:
<оператори>;
}

short Sign( float X ) {


// повертає знак числа X
if ( X > 0 )
return 1;
else if ( X < 0 )
return -1;
else
return 0;
}

void Equation( float A, float B, float C,


float* X1, float* X2, char* Num ) {
// знаходження дійсних коренів квадратного рівняння;
// A, B, C -- коефіцієнти
// X1, X2 -- корені (якщо дійсного розв'язання не має, то
// прирівнюються до 0);
// Num -- кількість коренів (0, 1, чи 2)

float D;
D = sqr( B ) -4 * A * C;

53
if ( D < 0 ) {
*Num = 0;
*X1 = 0;
*X2 = 0;
}
else {
*X1 = ( -B + sqrt( D ) ) / ( 2 * A );
*X2 = ( -B - sqrt( D ) ) / ( 2 * A );
if ( *X1 == *X2 )
*Num = 1;
else
*Num = 2;
}
}

Довжина рядків програмного тексту

Довжина рядків програми не повинна перевищувати ширини екрану (80 символів).

Інші рекомендації

А. Рекомендується операнди бінарних операцій (+, = та інші) відокремлювати від


знака операції одною прогалиною.
Наприклад:
Sum = A + B;

Б. Рекомендується при перерахуванні ідентифікаторів після коми "," ставити одну


прогалину.
Наприклад:
printf ( "Сума:% d; Різниця:% d.", A + B, A - B);
unsigned short Day, Month, Year;
unsigned char i, j, k, l, m, n;

В. Рекомендується завжди писати символ-роздільник операторів ";" безпосередньо


після оператора.
Наприклад:
switch (Num) {
case 1:
printf ( "один ...");
break;
case 2:
printf ( "два ...");
break;
case 3:
printf ( "три ...");
break;
default:

54
printf ( "багато!");
}

Г. Рекомендується шістьнадцяткові числа писати великими літерами. наприклад:


#define BadDate 0xFFFF
#define kbEnter 0x0D // код клавіші <Enter>

55

You might also like