Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Posibnyk_C_sum.doc
Скачиваний:
9
Добавлен:
29.08.2019
Размер:
1.63 Mб
Скачать

10 Динамічні конструкції даних

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

10.1 – Зв’язані списки

Зв’язаний список – одна з найважливіших структур даних, у якій елементи лінійно впорядковані, але порядок визначається не номерами елементів, як у масивах, а вказівниками, що входять до складу елементів списку. Список має “голову” – перший та “хвіст” – останній елемент. Зв’язаний список – не новий тип, а спосіб організації даних відомих типів. Для побудови зв’язаного списку найкраще підходять структури, тоді кожна з них є одним елементом списку, крім власне даних, вони містять один або більше вказівників для запам’ятовування адрес сусідніх структур. Ці вказівники мусять мати тип тієї структури, на яку вони вказують. Нагадаємо, що вказівник – це змінна, призначена для запам’ятовування адреси, а його тип визначає крок розміщення даних у пам’яті. Таким чином, одержуємо ніби ланцюжок певної кількості структур, зв’язаних між собою за допомогою адрес.

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

Однобічно зв’язаний список є найпростішим, кожний його елемент (структура) містить один вказівник, який запам’ятовує адресу, назвемо її next, наступного елемента. Якщо вказівник не вказує на інший елемент, тобто next = NULL, то вважається, що даний елемент є останнім у списку. Крім вказівника next, програма має мати ще й вказівник на перший елемент списку, назвемо його head, який служить для заходу в список. Якщо список порожній, то цьому вказівнику теж присвоюють значення NULL. Однобічно зв’язаний список забезпечує послідовний доступ до своїх елементів у одну сторону – від початку до кінця. Під час гортання списку (наприклад, під час пошуку потрібного елемента) ні вказівник head, ні кожний із вказівників next не змінюють своїх значень (адрес), тому для запам’ятовування адреси поточного елемента теж використовується вказівник, назвемо його curr. Гортання списку відбувається в циклі, умовою завершення якого є curr == NULL.

Двобічно зв’язаний список подібно до вищерозгляненого однобічно зв’язаного теж забезпечує послідовний доступ до своїх елементів, але, на відміну від нього, дозволяє гортати список як від початку до кінця, так і навпаки. Кожний його елемент містить два вказівники: один next, який вказує на наступний елемент, а другий, назвемо його prev, який вказує на попередній. Якщо prev=NULL, то в елемента немає попередника, тобто він є “головою” списку, якщо next=NULL, то в нього немає наступника – “хвіст” списку.

Кільцевий список може бути як однобічно, так і двобічно зв’язаним. У ньому зв’язується перший елемент з останнім. Тобто, в однобічно зв’язаному списку вказівник next “хвоста” дорівнює head. У двобічно зв’язаному вказівник prev “голови” вказує на “хвіст”, а вказівник next “хвоста” – на “голову” списку.

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

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

Таблиця 10.1 – Приклад масиву структур

Код

підприємства

Назва

підприємства

Статутний фонд,

млн у.о

1

Харкiвтрансгаз

34,60

2

Прикарпаттрансгаз

22,15

3

Київтрансгаз

75,00

4

Львiвтрансгаз

42,12

5

Експорттрансгаз

1,85

6

Гадячгазпром

12,48

Утворення і редагування зв’язаного списку та звертання до його елементів (вивід на екран) показано в програмі прикладу 10.1.

Приклад 10.1 – Створення та редагування зв’язаного списку

#include<stdio.h>

#include<stdlib.h> /* Для функції malloc() */

#include<string.h> /* Для функції strcpy() */

struct pidpr{int kod;

char naz[32];

float stf;

struct pidpr *dali;};

int main(void)

{

int i; FILE *zv;

struct pidpr dano[6]={{1, " Харківтрансгаз ", 34.60, NULL},

{2, " Прикарпаттрансгаз ", 22.15, NULL},

{3, " Київтрансгаз ", 75.00, NULL},

{4, " Львiвтрансгаз ", 42.12, NULL},

{5, " Експорттрансгаз ", 1.85, NULL},

{6, " Гадячгазпром ", 12.48, NULL}};

struct pidpr *head=NULL, *prev, *curr ,*vstv;

zv=fopen("zv_sp", "a");

/* Створення зв’язаного списку */

for(i=0; i<5; i++)

{curr=malloc(sizeof(struct pidpr));

if(head==NULL)head=curr; else prev->dali=curr;

curr->kod=dano[i].kod;

strcpy(curr->naz, dano[i].naz);

curr->stf=dano[i].stf;

curr->dali=NULL;

prev=curr;

}

fputs("Вивід новоствореного списку:\n",zv);

curr=head; i=0;

while(curr!=NULL)

{fprintf(zv,"adr=%p kod=%d naz=%s stf=%5.2f dali=%p\n",

curr, curr->kod, curr->naz, curr->stf, curr->dali);

curr=curr->dali;

}

/* Пошук та вилучення структури, де код=3 */

curr=head; i=0; prev=curr;

while(curr!=NULL)

{

if(curr->kod==3)

{

if(head==curr)head=head->dali; else prev->dali=curr->dali;

free(curr); curr=prev;

}

prev=curr; curr=curr->dali;

}

fputs("Вивід списку з вилученим елементом, де kod=3:\n",zv);

curr=head; i=0;

while(curr!=NULL)

{fprintf(zv,"adr=%p kod=%d naz=%s stf=%5.2f dali=%p\n",

curr, curr->kod, curr->naz, curr->stf, curr->dali);

curr=curr->dali;

}

/* Вставка структури перед тією, де kod=4 */

vstv=malloc(sizeof(struct pidpr));

strcpy(vstv->naz, dano[5].naz); vstv->kod=dano[5].kod; vstv->stf=dano[5].stf;

curr=prev=head;

while(curr!=NULL)

{

if(curr->kod==4)

{

if(curr==head)head=vstv; else prev->dali=vstv;

vstv->dali=curr;

}

prev=curr; curr=curr->dali;

}

fputs("Вивід списку із вставленою структурою:\n",zv);

curr=head; i=0;

while(curr!=NULL)

{fprintf(zv,"adr=%p kod=%d naz=%s stf=%5.2f dali=%p\n",

curr, curr->kod, curr->naz, curr->stf, curr->dali);

curr=curr->dali;

}

/* Додавання структури в кінець списку */

curr=prev=head;

vstv=malloc(sizeof(struct pidpr));

strcpy(vstv->naz, dano[2].naz); vstv->kod=dano[2].kod; vstv->stf=dano[2].stf;

while(curr!=NULL)

{

prev=curr; curr=curr->dali;

}

if(curr==head)head=vstv; else prev->dali=vstv;

vstv->dali=NULL;

fputs("Вивід списку із доданою структурою:\n",zv);

curr=head; i=0;

while(curr!=NULL)

{fprintf(zv,"adr=%p kod=%d naz=%s stf=%5.2f dali=%p\n",

curr, curr->kod, curr->naz, curr->stf, curr->dali);

curr=curr->dali;

}

/* Знищення списку і звільнення пам’яті, вивід знищених адрес */

fputs("Вивід знищених адрес:\n",zv);

curr=head; i=0;

while(curr!=NULL)

{

fprintf(zv,"i=%d adr=%p\n", i++, curr);

free(curr);

curr=curr->dali;

}

puts("Закінчення роботи.");

getch();

return 0;

}

Результати виконання програми (вміст файлу zv_sp) такі:

Вивід новоствореного списку:

adr=0F84 kod=1 naz=Харківтрансгаз stf=34.60 dali=0FB4

adr=0FB4 kod=2 naz=Прикарпаттрансгаз stf=22.15 dali=0FE4

adr=0FE4 kod=3 naz= Київтрансгаз stf=75.00 dali=1014

adr=1014 kod=4 naz= Львiвтрансгаз stf=42.12 dali=1044

adr=1044 kod=5 naz= Експорттрансгаз stf= 1.85 dali=0000

Вивід списку з вилученим елементом, де kod=3:

adr=0F84 kod=1 naz= Харківтрансгаз stf=34.60 dali=0FB4

adr=0FB4 kod=2 naz= Прикарпаттрансгаз stf=22.15 dali=1014

adr=1014 kod=4 naz= Львiвтрансгаз stf=42.12 dali=1044

adr=1044 kod=5 naz= Експорттрансгаз stf= 1.85 dali=0000

Вивід списку із вставленою структурою:

adr=0F84 kod=1 naz= Харківтрансгаз stf=34.60 dali=0FB4

adr=0FB4 kod=2 naz= Прикарпаттрансгаз stf=22.15 dali=0FE4

adr=0FE4 kod=6 naz= Гадячгазпром stf=12.48 dali=1014

adr=1014 kod=4 naz= Львiвтрансгаз stf=42.12 dali=1044

adr=1044 kod=5 naz= Експорттрансгаз stf= 1.85 dali=0000

Вивід списку із доданою структурою:

adr=0F84 kod=1 naz= Харківтрансгаз stf=34.60 dali=0FB4

adr=0FB4 kod=2 naz= Прикарпаттрансгаз stf=22.15 dali=0FE4

adr=0FE4 kod=6 naz= Гадячгазпром stf=12.48 dali=1014

adr=1014 kod=4 naz= Львiвтрансгаз stf=42.12 dali=1044

adr=1044 kod=5 naz= Експорттрансгаз stf= 1.85 dali=1074

adr=1074 kod=3 naz= Київтрансгаз stf=75.00 dali=0000

Вивід знищених адрес:

i=0 adr=0F84

i=1 adr=0FB4

i=2 adr=0FE4

i=3 adr=1014

i=4 adr=1044

i=5 adr=1074

На її початку оголошено тег структури pidpr, яка містить чотири такі елементи (за даними таблиці 10.1 про підприємства, які експлуатують газопроводи): змінну kod цілого типу, масив літер naz[32], змінну stf дійсного типу та адресу *dali типу структури pidpr. Ця адреса служить для зберігання наступної структури списку після поточної.

Всередині програмного блока оголошений та ініціалізований масив структур dano[6] типу pidpr. У програмі він служить як хранилище структур для демонстрації робіт у зв’язаному списку. Для утворення списку використаємо перші п’ять з них, а шосту – для вставки нової структури в список. Адреси *dali масиву структур ініціалізовані значенням NULL, зауважимо, що це зроблено просто “для порядку”, кожна з них одержить своє значення в процесі виконання програми. Змінна i – допоміжна, вона служить у якості лічильника структур. Для маніпуляції структурами списку служать п’ять вказівників типу структури pidpr, вони мають таке призначення:

  • *head – адреса початку списку, вона не змінюється в ході виконання програми, спочатку вона приймає значення NULL, оскільки список ще порожній;

  • *prev – адреса попередньої структури відносно поточної;

  • *curr – адреса поточної структури;

  • *vstv – допоміжна адреса для вставки структури в список.

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

Для забезпечення “читабельності” програми ті групи її операторів, які виконують певні види робіт, розділені (озаглавлені) коментарами. Нижче подано їх пояснення.

Створення списку відбувається в циклі, де його параметр i змінюється від 0 до 5. На початку циклу за допомогою відомої з розділу 7.1 функції malloc() відводиться пам’ять під чергову структуру типу pidpr, вона одержує адресу *curr. Якщо список ще порожній, то цю адресу приймає вказівник head, інакше – вказівник на адресу попередньої структури prev->dali, тобто відбувається її прив’язування до попередніх структур списку.

Після цього відбувається ініціалізація елементів новоутвореної структури. Масив naz[] та змінні kod і stf забирають свої значення з масиву dano[]. Адреса curr->dali приймає значення NULL, це означає, що тут список закінчується, адже кожна поточна структура є останньою в списку до тих пір, поки не буде додана наступна. Значення змінних kod і stf передаються в структуру шляхом присвоєння. Рядок naz[] копіюється за допомогою функції strcpy(curr->naz,dano[i].naz), тут не можна застосувати операцію присвоєння, бо, оскільки ім’я масиву є його адресою, то відбудеться переприсвоєння адрес, а не значень.

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

Читання списку відбувається в циклі типу while, поки curr!=NULL, тобто поки адреса попередньої структури curr->dali не дорівнює нулю. Гортання структур у списку відбувається за допомогою оператора curr=curr->dali. Звернемо увагу на те, що, на відміну від програми прикладу 7.2.3 під час гортання списку не відбувається нарощення адреси curr++. Структури в списку не обов’язково будуть розміщені послідовно одна за другою і їхні адреси не будуть відрізнятися з заданим кроком. Особливо це стосується багатокористувацьких систем, коли одночасно працює декілька програм, які виділяють пам’ять, кожна під свої дані.

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

Вилучення структури зі списку спричиняє зміну адрес сусідніх структур. Пошук потрібної структури виконується так же, як при редагуванні елементів. Коли структура знайдена, то вона стає поточною, для її вилучення спочатку необхідно з’єднати попередню структуру з наступною, тобто попередню зробити поточною. Оскільки поточна структура не містить своєї адреси, то подібно до того, як відбувалося створення списку, паралельно з нею змінюється й адреса попередньої структури prev. Зв’язування попередньої структури з наступною, обминаючи поточну, відбувається шляхом присвоєння prev->dali=curr->dali. Зауважимо, що адреса наступної структури міститься в поточній. Після цього треба звільнити ще зайняту вже непотрібною структурою пам’ять за допомогою відомої з розділу 7 функції free(curr). У прикладі 10.1 вилучена структура зі значенням kod=3.

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

У фрагменті нашого прикладу ставилася задача вставити останню (з індексом 5) структуру масиву dano[] перед структурою, де kod=4. Нова структура одержала адресу vstv. Під час гортання списку відомим уже з попередніх прикладів способом була знайдена поточна структура з адресою curr, де curr->kod==4. Зрозуміло, що під час виконання операції вставки подібно до вилучення структури мусить бути відомою й адреса prev. Зв’язування нової структури відбувається шляхом переприсвоєння адрес: prev->dali=vstv, а vstv->dali=curr.

Аналізуючи одержані результати, зауважимо, що нова структура дістала адресу 0FE4, тобто ту ж адресу, яку мала щойно вилучена структура. Вилучення структури спричинило наявність прогалини в пам’яті (вона стала фрагментованою), а компілятор ощадливо її використав і відвів під нові дані першу ж прогалину, яка підходила за розмірами.

Додавання структури до списку виконується подібно до вставки, різниця полягає лише в тому, що додана структура стає останньою, тому має посилатися на нульову адресу, тобто vstv->dali=NULL.

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

Знищення списку виконується спочатку від адреси head (оскільки маємо послідовний доступ до його структур) тоді, коли він уже непотрібний, а програма ще працює з іншими даними. У фрагменті нашої програми для знищення списку застосовується вже відома з розділу 7 функція free().

Зауважимо, що запропонована вище програма має учбовий характер і може бути вдосконалена. Наприклад, у ній слід передбачити обробку виняткової ситуації при відсутності пам’яті під час додавання чергового елемента зв’язаного списку подібно до того, як це було зроблено в програмі прикладу 7.1.4.

Запитання для перевірки

  1. Що таке зв’язаний список? Чи можна назвати його типом даних?

  2. Які переваги має зв’язаний список перед масивом?

  3. Перелічіть та поясніть види зв’язаних списків.

  4. Яким чином зв’язуються між собою дані у зв’язаному списку?

  5. Що таке прямий та послідовний доступи до даних у зв’язаному списку?

  6. Поясніть виконані вище в програмах операції над даними зв’язаного списку.

  7. Чи можна додати до зв’язаного списку за один раз (за один сеанс виділення пам’яті) декілька різних структур? Як можна тоді їх використовувати в програмах та звертатися до кожної з них?

  8. Чи можна прочитати зв’язаний список не від початку до кінця, як це було зроблено у вищенаведених прикладах, а навпаки – від кінця до початку?

  9. Чи обов’язково потрібно звільняти пам’ять, зайняту зв’язанним списком? Що станеться, якщо цього не робити?

  10. Запропонуйте алгоритм вставки в список структури не перед поточною, як це зроблено в вищенаведеному прикладі, а за нею.

  11. Додайте до програми прикладу 10.1 обробку виняткової ситуації – попередження аварійного її завершення через відсутність вільної ділянки оперативної пам’яті.

  12. Чому використання зв’язаного списку може спричинити фрагментацію пам’яті?

10.1 Стеки

10.2 Черги

10.3 Дерева

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]