lektsii_OP / T15
.pdf
Частина із специфічних функцій класу list є перевантаженими, зокрема, функція splice(). Її можливі реалізації:
|
splice(it, іlist) |
|
переміщення всіх елементів з списку іlist у поточний список перед |
|
|
|
|
ітератором it |
|
|
splice(it, іlist, it1) |
переміщення елемента *it1 з списку іlist у поточний список перед |
||
|
|
|
ітератором it |
|
|
splice(it, іlist, b,e) |
переміщення елементів з діапазону [b, e) списку іlist у поточний |
||
|
|
|
список перед ітератором it |
|
Наприклад, |
|
|
|
|
// ----------------- |
створення списка1 --------------------------- |
|
||
list<int> list1; |
|
|
|
|
for (int i=1; i<=5; i++) |
|
|||
|
list1.push_back(i); |
|
||
cout << "list1: "; |
|
|||
copy(list1.begin(), list1.end(), ostream_iterator<int>(cout," ")); |
// 1 2 3 4 5 |
|||
// ----------------- |
створення списка2 --------------------------- |
|
||
list<int> list2; |
|
|
|
|
for (int i=1; i<=5; i++) |
|
|||
|
list2.push_back(i*10); |
|
||
cout << "\nlist2: "; |
|
|||
copy(list2.begin(), list2.end(), ostream_iterator<int>(cout," ")); |
// 10 20 30 40 50 |
|||
// ----------------- |
переміщення елементів списка2 у список1 --------------------------- |
|
||
list<int>:: iterator it = list1.begin(); |
|
|||
advance(it, 2); |
|
|
|
|
list1.splice(it, list2); |
|
|||
cout << "\nlist1: "; |
|
|||
copy(list1.begin(), list1.end(), ostream_iterator<int>(cout," ")); |
// 1 2 10 20 30 40 50 3 4 5 |
|||
cout << "\nlist2: "; |
|
|||
copy(list2.begin(), list2.end(), ostream_iterator<int>(cout," ")); |
// порожній список |
|||
// ----------------- |
переміщення елементів списка1 у список2 --------------------------- |
|
||
list2.splice(list2.begin(), list1, it, list1.end()); |
|
|||
cout << "\nlist1: "; |
|
|||
copy(list1.begin(), list1.end(), ostream_iterator<int>(cout," ")); |
// 1 2 10 20 30 40 50 |
|||
cout << "\nlist2: "; |
|
|||
copy(list2.begin(), list2.end(), ostream_iterator<int>(cout," ")); |
// 3 4 5 |
|||
Відеокопія результата:
Контейнер <list> зручний при частих вставках і видаленнях елементів (складність О(1)). Його недоліком є відсутність доступа по індексу, а також повільний пошук і доступ (складність О(n)).
41
Контейнер <deque>. Даний контейнерний шаблон реалізує концепцію дека (черги з двома кінцями). Передбачає роботу з елементами на обох кінцях (як у списку), а також доступ по індексу [ ] (як у векторі). Тобто, він схожий на vector, але з можливістю швидкої вставки і видалення елементів на обох кінцях.
Для організації доступу до елементів використовуються як ітератори (прямі і зворотні), так і операція індексації []. Також можна безпосередньо звертатися до першого і до останнього елемента списку (функції front() і back() відповідно).
Вставка елемента у дек <deque> виконується аналогічно списку <list>: за допомогою функцій push_back(), push_front() та перевантаженої функції insert().
Для видалення елементів можна використовувати |
тільки функції pop_front(), |
pop_back(), erase() та clear(). |
|
Наприклад, |
|
deque<int> deq; |
// порожній дек |
for (int i=0; i<10; i++) |
|
deq.push_back(i); |
// додавання елемента в дек |
cout << "deque1: "; |
|
copy(deq.begin(),deq.end(), ostream_iterator<int>(cout," ")); |
// 0 1 2 3 4 5 6 7 8 9 |
deq .erase(deq .begin()); |
// видалення першого елемента |
cout << "\ndeque2: "; |
|
copy(deq.begin(),deq.end(), ostream_iterator<int>(cout," ")); |
// 1 2 3 4 5 6 7 8 9 |
deq .erase(deq.begin()+2, deq.begin()+5); |
// видалення елементів3,4,5 |
cout << "\ndeque3: "; |
|
copy(deq.begin(),deq.end(), ostream_iterator<int>(cout," ")); |
// 1 2 6 7 8 9 |
Відеокопія результата: |
|
Перевагами контейнера <deque> є швидке виконання операцій вставки і видалення першого та останнього елемента, а також швидкий доступ по індексу; недоліком - повільні операції з внутрішніми елементами (вставка, видалення).
Контейнер <stack>. Клас <stack> реалізує концепцію стека елементів, в якому додавання і видалення елементів здійснюється з одного кінця – вершини стека.
Даний контейнерний клас є адаптером контейнерів, оскільки використовує (тобто адаптує) для реалізації інший контейнер. Контейнер, на базі якого будуються стек, є параметром шаблона даного класу. За замовчуванням таким контейнером для стека є дек. Однак, це можна поміняти, вказавши другим (необов’язковим) параметром тип іншого контейнера: для стека це може бути список або вектор (обидва підтримують push_back). Наприклад,
stack <int, vector<int> > s;
42
Доступ до вершини стеку здійснюється за допомогою функції top(), вставка та видалення елементів - за допомогою функцій push() і pop() відповідно.
Наприклад,
stack<int> st; |
// порожній стек |
st .push( 2); |
// додавання елемента в стек |
st .push( 6); |
// додавання елемента в стек |
st .push( 51); |
// додавання елемента в стек |
cout << st .size() << " elements in stack\n"; |
//3 elements in stack |
cout << "Top element: "<< st .top() << "\n"; |
// Top element: 51 |
st .pop(); |
// видалення елемента із стека |
cout << st .size() << " elements on stack\n"; |
//2 elements on stack |
cout << "Top element: " << st .top() << "\n"; |
// Top element: 6 |
Контейнер <queue>. Клас <queue> реалізує концепцію черги елементів, в якій з одного кінця (хвоста) можна додавати елементи, а з іншого (голови) — виймати.
Даний контейнерний клас також є адаптером контейнерів і також використовує за замовчуванням для реалізації контейнер дека. Однак контейнером для адаптації може бути список (підтримує pop_front). Наприклад,
queue <double, list<double> > q;
Для доступу до голови черги використовується функція front(), до хвоста – функція back(). Вставка елементів (в кінець черги) та їх видалення (з голови черги) здійснюється за допомогою функцій push() і pop() відповідно.
Наприклад,
queue <int> q; |
// порожня черга |
q.push( 2); |
// додавання елемента ( черга: 2 ) |
q.push( 6); |
// додавання елемента ( черга: 2 6 ) |
q.push( 51); |
// додавання елемента ( черга: 2 6 51 ) |
cout << q.size() << " elements in queue \n"; |
//3 elements in queue |
cout << "First element: "<< q.front() << "\n"; |
// First element: 2 |
cout << "Last element: " << q.back() << "\n"; |
// Last element: 51 |
q.pop(); |
// видалення елемента ( черга: 6 51 ) |
cout << q.size() << " elements in queue \n"; |
//2 elements in queue |
cout << "First element: "<< q.front() << "\n"; |
// First element: 6 |
q.push( 15); |
// додавання елемента ( черга: 6 51 15 ) |
cout << q.size() << " elements in queue \n"; |
//3 elements in queue |
cout << "Last element: " << q.back() << "\n"; |
// Last element: 15 |
Приклад. На основі вектора цілих чисел створити список із елементів вектора з парними значеннями.
# include <iostream> |
|
# include <vector> |
|
# include <list> |
|
using namespace std; |
|
void input (vector <int> &, int); |
// створення вектора |
void output (vector <int> &); |
// виведення вектора |
void output (list <int> &); |
// виведення списку |
|
43 |
void conversion (vector <int> &, list <int> &); |
// перетворення вектора у список |
||
int main() |
|
|
|
{ int num=15; |
// кількість елементів вектора |
||
vector<int> arr; |
// порожній вектор |
||
list <int> myLlst; |
// порожній список |
||
srand(time(NULL)); |
|
|
|
input (arr, num); |
//створити вектор |
||
output (arr); |
// вивести створений вектор |
||
conversion(arr, myLlst); |
// сформувати список |
||
output (myLlst); |
// вивести сформований список |
||
system("pause"); |
|
|
|
} |
|
|
|
// ----------------- |
генерація вектора |
--------------------------- |
|
void input (vector <int> &v, int n) |
|
|
|
{ for (int |
i=0; i<n; i++) |
|
|
v.push_back(rand()%20); |
//генерація і додавання елемента у вектор |
||
} |
|
|
|
// ----------------- |
виведення вектора --------------------------- |
|
|
void output (vector <int> & v)
{cout<<"Array: ";
copy(v.begin(), v.end(), ostream_iterator<int>(cout," "));
cout<<endl; |
|
} |
|
// ----------------- |
перетворення вектора у список --------------------------- |
void conversion (vector <int> & v, list <int> & lst)
{int i;
for (i=0; i<v.size(); i++)
|
if (!(v[i]%2)) |
//якщо елемент вектора має парне значення, |
|
lst .push_back(v[i]); |
//додати елемент у список |
} |
|
|
// |
----------------- виведення списку |
--------------------------- |
void output (list <int> & lst) |
|
|
{ |
cout<<"List: "; |
|
copy(lst .begin(), lst .end(), ostream_iterator<int>(cout," ")); cout<<endl;
}
Відеокопія результата:
44
45
Дерева
Основні визначення
Зв'язні списки не охоплюють весь спектр можливих представлень даних. Наприклад, за їх допомогою важко описати ієрархічні структури подібні каталогам і файлам або структури для зберігання інформації генеалогічного дерева.
Для цього краще підходить модель відома як дерево.
Деревом називають структуру даних, кожен елемент якої позв'язний з декількома іншими її елементами. Елементи дерева називають його вузлами (або вершинами).
У спискових структурах за поточною вершиною (якщо вона не остання) завжди слідує тільки одна вершина, тоді як у деревовидних структурах таких вершин може бути декілька. Тому, на відміну від спискових структур, дерева відносяться до нелінійних структур даних.
Дерево характеризується наступними властивостями:
у дереві існує єдиний вузол, на який не посилається ніякий інший вузол, і який називається коренем дерева;
починаючи з кореня і слідуючи по певному ланцюжку покажчиків, що містяться у вузлах, можна здійснити доступ до будь-якого елемента дерева;
на кожен вузол дерева, окрім кореня, є єдине посилання (тобто кожен вузол адресується єдиним покажчиком).
Вузли дерева (за виключенням кореня) розподілені серед m ≥ 0 непересічних множин, кожна із яких в свою чергу є деревом. Такі дерева називають піддеревами (subtrees) даного кореня. Тобто вузол дерева може бути в свою чергу коренем деякого піддерева.
Серед будь-якої пари безпосередньо зв’язаних вузлів дерева можна виділити предка та нащадка: вузол, на який посилається інший вузол є нащадком цього вузла; вузол, який містить таке посилання – предок відповідного вузла. Нащадок є коренем певного піддерева предка. Якщо вузол не має нащадків, то такий вузол називають
листом (або термінальною вершиною) дерева.
Вважається, що корінь дерева розташований на першому рівні. Кожний вузолнащадок рівня k має предка на рівні k–1. Максимальний рівень дерева називають його глибиною (або висотою). Глибина піддерева - відстань від кореня дерева до кореня піддерева. Висота піддерева - довжина найдовшої гілки в цьому піддереві, де довжина гілки піддерева - це відстань від кореня піддерева до листа цієї гілки.
Кількість безпосередніх нащадків вузла має назву степеня (degree) цього вузла. Максимальну степінь вузла у певному дереві називають степенем дерева.
Шлях між початковою і кінцевою вершиною дерева – це послідовність прохідних вершин. У цьому сенсі висота дерева – це найдовший шлях від кореневої вершини до термінальних.
Математично дерево розглядається як окремий випадок графа, в якому відсутні замкнуті шляхи (цикли).
46
Найчастіше дерева зображують з коренем, який знаходиться зверху:
|
|
A |
|
|
корінь |
|
B |
|
C |
|
вузол |
D |
E |
F |
G |
H |
лист |
Класифікація дерев
Класифікацію дерев можна провести за різними ознаками.
1)За кількістю можливих нащадків у вершин розрізняють двійкові (бінарні) або недвійкові (сильнорозгалужені) дерева. У двійковому дереві кожна вершина може мати не більше двох нащадків; у недвійковому дереві вершини можуть мати будьяку кількість нащадків.
2)Якщо в дереві важливий порядок слідування нащадків, то такі дерева називають впорядкованими. Для них вводиться поняття лівий і правий нащадок (для двійкових дерев) або більше лівий / правий (для недвійковий дерев). У цьому сенсі два наступних найпростіших упорядкованих дерева з однаковими елементами вважаються різними:
Дерево вважається впорядкованим, якщо порядок його елементів є фіксованим.
Двійкові дерева використовуються достатньо часто і тому представляють найбільший практичний інтерес.
Бінарне дерево (binary tree) - це впорядковане дерево, кожна вершина якого має не більше двох піддерев, причому для кожного вузла виконується правило: у лівих вершинах повинні знаходитися менші значення, ніж у правих (рис. 13 (a)). Якщо бінарне дерево має точно по два нащадки з кожного вузла, то воно вважається
повним бінарним деревом (рис. 13 (б)).
47
а) |
б) |
Рис.13. Приклади бінарних дерев
Збалансоване дерево - різновид бінарного дерева, яке автоматично підтримує свою висоту, тобто кількість рівнів вершин під коренем, мінімальною.
Процедура зменшення (балансування) висоти дерева виконується за допомогою трансформацій, відомих як обернення дерева, в певні моменти часу (переважно при видаленні або додаванні нових елементів).
Ідеально збалансоване дерево — це дерево, у якого для кожної вершини різниця між висотами лівого та правого піддерев не перевищує одиниці (рис. 14).
Бінарне дерево називають ідеально збалансованим, якщо для кожної його вершини кількість вершин у лівому і правому піддереві різниться не більше ніж на 1.
Рис.14. Приклади ідеально збалансованих бінарних дерев
Однак, така умова доволі складна для виконання на практиці і може вимагати значної перебудови дерева при додаванні або видаленні елементів. Тому було запропоноване менш строге визначення, яке отримало назву умови АВЛ(AVL)- збалансованості. Згідно нього, бінарне дерево є збалансованим, якщо висоти лівого та правого піддерев різняться не більше ніж на одиницію. Дерева, що задовольняють таким умовам, називаються AVL-деревами. Зрозуміло, що кожне ідеально збалансоване дерево є також АВЛ-збалансованим, але не навпаки.
Найбільшого розповсюдження ці структури даних набули в тих задачах, де необхідне маніпулювання з ієрархічними даними, ефективний пошук в даних, їхнє структуроване зберігання та модифікація. Бінарні дерева забезпечують пошук конкретного значення, максимуму, мінімуму, попереднього, наступного, операції вставки та видалення елемента
48
Бінарні дерева
При розгляді дерева як структури даних необхідно чітко розуміти наступні два моменти:
1)всі вершини дерева, що розглядаються як змінні мови програмування, повинні бути одного і того ж типу, більше того - записами з деяким інформаційним наповненням і необхідною кількістю сполучаючих полів;
2)в силу природної логічної розгалуженості дерев і відсутності єдиного правила розміщення вершин в порядку один за одним, їх логічна організація не співпадає з фізичним розміщенням вершин дерева в оперативній пам'яті.
Дерево є типовим прикладом рекурсивно визначеної структури даних, оскільки воно визначається в термінах самого себе.
Над структурами типу дерева виконують такі основні дії:
створення дерева;
додавання вузла у визначене місце в дереві;
додавання цілого фрагмента дерева;
видалення вузла з дерева;
видалення цілого фрагмента дерева;
обхід дерева (всіх його вершин);
пошук у дереві (заданої вершини).
перенумерація вершин дерева;
трансформації (повороти) фрагментів дерева тощо.
Створення бінарного дерева. Найпростіший спосіб побудови бінарного дерева полягає у створенні дерева симетричної структури із наперед відомою кількістю вузлів. Усі вузли-нащадки, що створюються, рівномірно розподіляються зліва та справа від кожного вузла-предка. Правило рівномірного розподілу n вузлів можна визначити рекурсивно:
1)перший вузол вважати коренем дерева;
2)створити ліве піддерево з кількістю вузлів numberleft = n/2;
3)створити праве піддерево з кількістю вузлів numberright = n–numberleft–1. При цьому досягається мінімально можлива глибина для заданої кількості вузлів дерева.
Тип вузла бінарного дерева у мовах С/C++ оголошують так:
struct TREE |
// тип вузла дерева |
{ char inf; |
// інформаційне поле вузла |
TREE *left, |
// покажчик на ліве піддерево |
*right; |
// покажчик на праве піддерево |
}; |
|
Для листків покажчики left та right мають значення NULL (рис. 15).
49
Data
left right
Data
left right
Data
left right
Data |
|
Data |
|
Data |
|
Data |
||||
|
|
|
|
|
|
|
|
|
|
|
NULL |
NULL |
|
NULL |
NULL |
|
NULL |
NULL |
|
NULL |
NULL |
|
|
|
|
|
|
|
|
|
|
|
Рис.15. Приклади ідеально збалансованих бінарних дерев
Для обробки дерева досить знати адресу кореневої вершини. Для зберігання цієї адреси треба ввести посилальну змінну, наприклад, TREE *root. Тоді пусте дерево визначається просто установкою змінної root в нульове значення:
root = NULL;
Для формування дерева шляхом включення в нього нового вузла треба мати адресу батьківського вузла, до якого буде додаватися новостворений вузол (parent), а також тип піддерева, що створюється (ліве або праве).
Наприклад, якщо
typedef enum tag_type {RIGHT, LEFT} TYPE; TYPE type;
то функція створення бінарного дерева може мати вид:
TREE *curr = new TREE; |
// створення нового вузла дерева |
|
cin>> curr -> inf; |
// заповнення його інформаційного поля |
|
curr ->left = NULL; |
// обнулення покажчика на ліве піддерево створеного вузла |
|
curr -> right = NULL; |
// обнулення покажчика на праве піддерево створеного вузла |
|
if (parent) |
// if (parent != NULL) |
|
{ if (type == LEFT) parent ->left = curr; else parent ->right = curr;
else root = curr;
Обхід дерева. Покроковий перебір елементів дерева по зв'язках між вузламипредками і вузлами-нащадками називається обходом дерева.
Оскільки дерево є нелінійної структурою, то не існує єдиної схеми обходу дерева. Класично виділяють наступні основні схеми:
обхід в прямому порядку (обхід зверху вниз);
обхід у зворотному порядку (обхід знизу вверх).
симетричний обхід (обхід зліва направо).
Обхід, при якому кожен вузол-предок переглядається перед його нащадками називають обходом дерева в прямому порядку. Схема обходу дерева в прямому порядку на прикладі бінарного дерева із трьох вершин (рис. 16):
1)обробити кореневу вершину поточного піддерева;
2)перейти до обробки лівого піддерева таким же чином;
3)обробити праве піддерево таким же чином.
50
