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

lektsii_OP / T15

.pdf
Скачиваний:
95
Добавлен:
17.03.2016
Размер:
1.5 Mб
Скачать

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

Наприклад, одновимірний масив з 5-ти елементів (приміром з цілих чисел 4, –5, 0, 1, 8) розміщується у пам’яті в такий спосіб:

Список же з тими самими значеннями може бути розташований у пам’яті так:

У наведеному прикладі елемент зі значенням 0 “знає”, де розташовано елемент зі значенням 1 і не “знає”, де розташована решта елементів. Щоб “дістатися” елемента зі значенням 0, треба звернутися до першого елемента (4), довідатися в нього адресу другого (–5), а вже у другого – довідатися адресу третього (0). Безпосередньо одразу до третього елемента звернутися неможливо.

Список для зручності і наочності можна зобразити в такий спосіб:

Як видно, елементи такого списку і зв’язки поміж ними можна розташувати у пряму лінію і рухатися по елементам у напрямку від першого елемента до останнього, тобто лінійно. Тому такі списки називають також лінійними однозв'язними списками.

Зрозуміло, що окрім даних, кожен елемент списку повинен зберігати ще одне значення - адресу наступного елемента. Тому природним є подання елемента списку у вигляді структури, яка складається з двох частин: інформаційної і вказівної (рис. 4). Інформаційна частина містить одне чи декілька інформаційних полів (полів даних); вказівна - інформацію про місцезнаходження пов'язаного з ним елемента або елементів списку (один або два покажчика на наступний та/або попередній елемент структури). Саме за допомогою полів-покажчиків і здійснюється зв'язування елементів списку в єдину структуру.

Data - інформаційна частина

next - вказівна частина

Рис. 4. Структура елемента списку

2 Представлення у пам'яті раніше розглянутих статичних структур було суміжним.

21

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

Отже, основними характеристиками спискових структур є:

зв'язність їх елементів, яка передбачає, що кожен із елементів містить окрім власне даних, посилання на наступний елемент списку;

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

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

Для списків характерні специфічні операції обробки елементів: вставка, видалення, пошук і сортування. Щоб змінити порядок елементів списку, досить поміняти значення відповідних посилань. При цьому фізичне розташування самих елементів не зміняюється.

Коли зв'язне представлення даних використовується для вирішення прикладної задачі, для користувача "видимим" є тільки вміст інформаційного поля, а полезв'язок використовується тільки програмістом-розробником.

Можливість зміни структури та розміру в процесі роботи програми істотно підвищує гнучкість програм, які використовують динамічні структури даних, оскільки розмір структури обмежується тільки доступним розміром пам'яті ПК. Окрім того, при зміні логічної послідовності елементів такої структури потрібне не переміщення даних в пам'яті, а тільки корекція покажчиків.

Разом з тим, зв'язне представлення даних не позбавлене і недоліків, основними з яких є наступні:

робота з покажчиками вимагає, як правило, більшої кваліфікації від програміста;

на поля, що містять покажчики для зв'язування елементів один з одним, витрачається додаткова пам'ять;

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

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

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

22

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

ін.).

Існує декілька можливих типів зв’язних списків. Вони різняться як кількістю зв'язувань між елементами, так і принципом обробки. Зокрема, за кількістю зв’язків між елементами списки поділяють на однозв'язні і двозв'язні (деки); за топологією – на лінійні і циклічні.

Однозв’язні лінійні списки

Список, кожен елемент якого містить покажчик лише на один наступний елемент, називають однозв’язним (рис. 5). Кожен елемент такого списку складається з інформаційної частини (Data) і вказівника на наступний елемент списку (next). Останній елемент списку містить у полі-покажчику порожній покажчик (NULL), що інтерпретується як кінець списку. Перший елемент списку називають його головою (або вершиною). Адреса вершини списку зберігається у спеціальному покажчику (наприклад, head) з якого і починається робота з списком.

head

 

. . .

 

 

Data next

Data next

Data next

Data NULL

Рис. 5. Структура лінійного однозв'язного списку

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

Розглянемо, як приклад, однозв'язний список, схематично представлений на рис. 6:

head

73

28

21

85

NULL

Рис. 6. Схематичне представлення динамічної структури

Даний список складається з 4 елементів. Його перший елемент має поле Data, рівне 73, і зв'язаний за допомогою свого поля next з другим елементом, поле Data якого дорівнює 28, і так далі до останнього, четвертого елемента, поле Data якого рівне 85, а поле next є NULL, тобто нульовою адресою, що є ознакою завершення списку. Тут head є покажчиком, який вказує на голову (перший елемент) списка.

23

Оголошення елемента однозв'язного списку у С/С++ має вид:

struct тип_елемента { тип поле_1;

тип поле_n;

тип_елемента *покажчик_наступний_елемент;

};

Наприклад,

struct TList

 

{ int data;

// інформаційне поле

TList *next;

// адресне поле

};

 

Покажчик на перший елемент - вершину списку - оголошується так:

TList *head=NULL;

Його початкове значення завжди дорівнює NULL (тобто списку поки що немає). Після створення списку, head набуває значення адреси пам’яті, починаючи з якої розташовано перший елемент цього списку.

Маючи покажчик на перший елемент списку, до другого його елемента можна звернутися як head ->next, до третього - head ->next->next і т.д. Щоб отримати числове значення першого елемента, звертання повинне бути таким: head ->data.

Слід звернути увагу на те, що head є не елементом списку, а лише покажчиком на нього, тобто є покажчиком на структуру. Тому звертання до полів структури відбувається не через крапку ".", а через операцію "->".

Для створення однозв’язного списку слід створити його перший елемент:

1)виділити під нього місце у пам’яті;

2)занести змістовні дані;

3)ініціалізувати адресне поле ознакою кінця списку (NULL);

4)визначити покажчик на голову списку (значенням адреси створеного елемента).

Наприклад,

TList *head= NULL; current = new TList; current ->data = 4; current ->next=NULL; head = current;

//створити пустий покажчик на список

//виділити пам’ять під новий елемент

//занести змістовні дані елемента

//ініціалізувати адресне поле елемента значенням NULL

//визначити покажчик на голову списку

Для долучення нового елемента в кінець списку слід виконати такі дії:

1)виділити місце в пам’яті під новий елемент;

2)занести до нього змістовні дані;

3)ініціалізувати його адресне поле ознакою кінця списку (NULL);

4)долучити створений елемент до списку після останнього елемента (адресу нового елемента записати у адресне поле останнього елемента);

5)перевизначити покажчик на останній елемент.

24

Наприклад, якщо tail - покажчик на останній елемент, то дії по долученню елемента у кінець списку можна задати так:

current = new TList;

 

current -> data = 5;

 

current ->next= NULL;

 

tail -> next = current;

// зв’язати новий елемент з останнім

tail = current;

// перевизначити покажчик на останній елемент

Загальний алгоритм створення однозв’язного списку може бути таким:

1)ініціалізувати покажчик голови списку, як покажчик на порожній список (значенням NULL);

2)виділити пам'ять для нового елемента;

3)ввести змістовні дані нового елемента;

4)ініціалізувати адресне поле нового елемента ознакою кінця списку (значенням

NULL);

5)якщо список порожній, встановити покажчик на голову списку як посилання на новий елемент; якщо ж список не порожній - встановити посилання на новий елемент у вказівну частину останнього елемента списку;

6)перевизначити покажчик на останній елемент списку;

7)якщо потрібно ввести ще один елемент, перейти на п.2, інакше – кінець.

Наприклад,

TList *head= NULL; char c;

do

{current = new TList; cin >> current ->data; current ->next = NULL;

if (head == NULL) head = current; else tail -> next = current;

tail = current;

cout << "Continue? (Y/N)"; cin>>c;

}

while((c!='n')&&(c!='N'));

Основними операціями над однозв’язними списками є:

переміщення по списку;

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

видалення елемента із списку.

Розглянемо реалізацію цих базових алгоритмів.

Переміщення по списку. Якщо current – покажчик на поточний елемент, то переміщення до наступного елемента списку здійснюється за допомогою присвоєння виду

current = current -> next;

Проходження за списком виконується переважно у циклі while допоки потрібний елемент не знайдений або допоки список не завершено:

25

while (current != NULL)

// допоки список не завершено

{

...

// виконання дій з current ->data

 

current = current -> next;

// перехід до наступного елемента списку

}

 

 

Наприклад, пошук елемента цілочисельного списку, що має значення 7:

current = head;

while (current != NULL)

{if (current ->data == 7 )

{cout << "Item is found" << endl; break;

}

current = current -> next;

}

Вставка нового елемента у список. Алгоритми вставки елемента у список різняться в залежності від місця вставки. Розрізняють три основні алгоритми такої вставки: на початок, в середину і в кінець списку. Алгоритм вставки елемента в кінець однозв’язного списку наведений вище. Розглянемо алгоритм вставки елемента на початок існуючого однозв’язного списку.

Вважаємо, що на новий створюваний елемент буде посилатися покажчик current. Тоді алгоритм вставки може бути наступним:

1)виділити пам'ять для нового елемента;

2)ввести його змістовні дані;

3)долучити новий елемент до списку (в його адресне поле записати значення покажчика голови списку);

4)встановити покажчик голови списку на новий елемент (перевизначити його значенням покажчика на новий елемент).

Наприклад,

TList *current = new TList;

// виділити пам’ять під новий елемент

cin >> current -> Data;

// ввести змістовні дані нового елемента

current ->Next = head ;

// вставити новий елемент перед першим

head = current;

// перевизначити покажчик на голову списку

Для вставки нового елемента у середину списку, треба мати покажчик на елемент, за яким здійснюється вставка. Тоді алгоритм такої вставки може бути наступним:

1)виділити пам'ять для нового елемента;

2)ввести його змістовні дані;

3)зв'язати новий елемент з наступним за знайденим (в адресне поле нового елемента переписати значення адресного поля елемента, за яким здійснюється вставка);

4)зв'язати новий елемент із знайденим (в адресне поле елемента, за яким здійснюється вставка, записати значенням покажчика на новий елемент).

Наприклад, вставимо новий елемент у цілочисельний список після елемента із значенням 7. Вважаємо, що на елемент, за яким здійснюється вставка, вказує покажчик previous.

26

TList *previous = head;

// встановити покажчик на голову списку

while (previous!= NULL)

// допоки список не завершено

{ if (previous ->data == 7)

// якщо шуканий елемент знайдено

{ TList *current = new TList;

// виділити пам’ять під новий елемент

cin >> current ->data;

// ввести змістовні дані нового елемента

current ->next = previous ->next; // зпозиціонувати елемент на наступний за знайденим

previous ->next = current;

// зпозиціонувати знайдений елемент на новий

}

 

previous = previous -> next;

// перейти до наступного елемента списку

}

 

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

Алгоритм видалення елемента з початку списку може бути таким:

1)зберегти значення голови списку у допоміжному покажчику;

2)перемістити покажчик голови списка на другий елемент (перевизначити його значенням адресного поля першого елемента);

3)видалити перший елемент списку.

Наприклад,

current = head; head = head -> next; delete current;

//зберегти значення вершини списка //зробити вершиною другий елемент списку //видалити попередню вершину списку

Алгоритм видалення елемента з середини списку можна представити так:

1)зберегти посилання на елемент, що видаляється, у допоміжному покажчику;

2)перевизначити адресне поле елемента, що передує тому, який видаляється, значенням адреси наступного за тим, що видаляється, елемента;

3)видалити вказаний елемент.

Наприклад,

current = previous ->next; previous ->next = current ->next; delete current;

//зберегти покажчик на елемент, що видаляється //зв’язати попередній елемент з наступним за заданим //видалити заданий елемент

Тут current – покажчик на елемент, що видаляється, previous – покажчик на елемент списку, за яким здійснюється видалення.

Встановлення нового зв’язку між елементами у даному прикладі можна здійснити також наступним чином:

previous ->next = previous ->next ->next;

Схема реалізації даного алгоритму представлена на рис. 7.

27

previous current

head

Data next

Data next

Data next

Data NULL

previous -> next = current -> next

Рис. 7. Схема реалізації алгоритма видалення елемента зсередини списку

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

1)зберегти посилання на останній елемент (значення адресного поля передостаннього елемента) у допоміжному покажчику;

2)записати у адресне поле передостаннього елемента ознаку кінця списку

(NULL);

3)видалити колишній останній елемент.

Наприклад,

current = previous -> next; previous -> next = NULL; delete current;

//зберегти покажчик на останній елемент //визначити передостанній елемент як останній //видалити останній елемент

Якщо ж на останній елемент списку вказує окремий покажчик (наприклад, tail), то можливий інший алгоритм видалення елемента з кінця списку:

1)записати до передостаннього елемента ознаку кінця списку (NULL);

2)звільнити пам’ять із-під колишнього останнього елемента;

3)вважати останнім колишній передостанній елемент.

Наприклад,

previous -> next = NULL;

//визначити передостанній елемент як останній

delete tail;

//видалити останній елемент

tail = previous;

//зробити останнім передостанній елемент

Зоднозв’язними списками також можна виконувати такі дії:

шукати елемент, що задовільняє певним критеріям;

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

об'єднувати в одному списку два або більше лінійних списків;

розбивати список на два або більше фрагментів;

сортувати елемент списку тощо.

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

Важливими окремими випадками лінійних однозв’язних списків, в яких операції додавання та вилучення значень можуть бути виконані лише для певних елементів, є

стеки і черги.

28

Черги

Черга – це різновид однозв’язного списку, елементи якого вносяться з одного кінця, а вилучаються з іншого. Такий принцип обробки елементів формулюється як «першим прийшов - першим пішов» і позначається абревіатурою FIFO (від англ. First Input First Output).

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

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

Для роботи з чергою слід зберігати покажчики на її вершину (наприклад, head) і її хвіст (наприклад, tail). Кожен елемент черги повинен мати поле-покажчик на наступний елемент (який був включений у чергу за поточним). Останній елемент, занесений у чергу, вважається її хвостом (містить у адресному полі порожній покажчик NULL). Структурна організація черги наведена на рис. 9.

head

Inf

Next

Inf

. . .

Inf

Next

 

Next

tail

Inf

NULL

Рис. 9. Структура черги

Алгоритм створення черги аналогічний алгоритму створення однозв’язного списку з тією лише різницею, що в результаті зберігається не тільки покажчик на вершину, а й на хвіст черги. Алгоритм додавання елемента у чергу (часто цю операцію називають enqueue - "поставити в чергу") відповідає розглянутому раніше алгоритму вставки елемента в кінець (хвіст) однозв’язного списку. Видалення елемента із черги (часто цю операцію називають dequeue - "отримання з черги") виконується так же, як і видалення елемента із початку однозв’язного списку. Можна також зчитувати дані із черги, знищити чергу тощо.

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

(queue underflow).

Приклад створення черги елементів і її очищення.

struct TQueue

// елемент черги

{ int info;

// інформаційне поле

TQueue *next; // покажчик на наступний елемент в черзі

};

TQueue *head = NULL; // покажчик на голову черги (спочатку NULL, оскільки черга порожня)

29

TQueue *tail = NULL;

// покажчик на хвіст черги (спочатку NULL, оскільки черга порожня)

void Add (int);

// додавання елемента в чергу

char Del(int &);

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

// ============== головна функція =====================

int main()

{int n= 5;

for (int i=1; i<n; i++) Add(i*2);

int а = 0;

for (int i=0; i<n+1; i++)

{ if (Del(a)) cout << а << endl;

else cout << "Queue is empty"<< endl;

}

system("pause");

}

// ================= додавання елемента в чергу ======================

void Add (int value)

// value - значення, яке потрібно помістити в чергу

{ TQueue *current = new TQueue;

// створення нового елемента черги

current -> info = value;

 

// заповнення його інформаційного поля

current -> next = NULL;

 

// після current елементів немає

if (tail)

 

// якщо черга не порожня,

 

tail ->next = current;

 

// наступним елементом після хвоста черги стає current

else head = current;

 

// інакше current стає головою черги

tail = current;

 

// current стає хвостом черги

}

 

 

 

 

 

// ================ видалення елемента з черги ==================

char

Del(int &value)

// в value поміщається прочитане значення

{ if (head)

 

// якщо черга не порожня,

{ value = head -> info;

 

// читання інформаційного поля з голови черги

 

TQueue *temp = head;

// запам'ятовування покажчика на голову черги

 

head = head ->next;

 

// встановлення як голови черги наступного елемента

 

delete temp;

 

//видалення елемента, який був головою черги

 

if (! head)

 

// якщо був видалений останній елемент черги,

 

 

tail = NULL;

 

// то обнуляємо хвіст

 

 

return

1;

 

// читання успішне

}

 

 

 

 

else return 0;

 

// спроба читання з порожньої черги

}

 

 

 

 

 

Відеокопія результата:

30

Соседние файлы в папке lektsii_OP