Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
АМ_1_LR_2.doc
Скачиваний:
3
Добавлен:
23.08.2019
Размер:
196.1 Кб
Скачать

Міністерство освіти і науки, молоді та спорту України

Прикарпатський національний університет

імені Василя Стефаника

Кафедра радіофізики і електроніки

Лабораторна робота №1

з дисципліни

"Алгоритми та методи обчислень"

ОДНОСПРЯМОВАНІ і ДВохСПРЯМОВАНІ СПИСКИ

Івано-Франківськ – 2011

Мета роботи:

- оволодіти навичками доступу до даних і роботу з пам'яттю при використанні односпрямованих і двохспрямованих списків;

- навчитися вирішувати завдання з використанням списків.

Теоретичні відомості

Ключові терміни:

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

Довжина списку - це величина, рівна числу елементів у списку.

Лінійний список - це список, що відбиває відносини сусідства між елементами.

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

Порожній список - це список нульової довжини.

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

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

Вказівник голови списку (голова списку) - це вказівник на перший елемент списку.

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

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

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

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

Лінійні зв'язні списки є найпростішими динамічними структурами даних. Із усього різноманіття зв'язаних списків можна виділити наступні основні:

- односпрямовані (однозв'язні) списки;

- двохспрямовані (двохзв'язні) списки;

- циклічні (кільцеві) списки.

В основному вони відрізняються видом взаємозв'язку елементів і/або припустимими операціями.

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

Односпрямований (однозв'язний) список - це структура даних, що представляє собою послідовність елементів, у кожному з яких зберігається значення і вказівник на наступний елемент списку (рис. 1). В останньому елементі вказівник на наступний елемент дорівнює NULL.

Рисунок 1.  Лінійний односпрямований список

Опис найпростішого елемента такого списку виглядає в такий спосіб:

struct ім’я_типу { інформаційне поле; адресне поле; };

де

- інформаційне поле - це поле кожного, раніше оголошеного або стандартного, типу;

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

Наприклад:

struct Node {

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

Node*next;//адресне поле

};

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

Наприклад:

struct point {

char*name;//інформаційне поле

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

point*next;//адресне поле

};

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

Основними операціями, які здійснюються з односпрямованими списками, є:

- створення списку;

- друк (перегляд) списку;

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

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

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

- перевірка чи порожній список;

" вилучення списку.

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

Розглянемо докладніше кожну з наведених операцій.

Для опису алгоритмів цих основних операцій використовується наступне оголошення:

struct Single_List {//структура даних

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

Single_List *Next; //адресне поле

};

. . . . . . . . . .

Single_List *Head; //вказівник на перший елемент списку

. . . . . . . . . .

Single_List *Current;

//вказівник на поточний елемент списку (при необхідності)

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

// створення односпрямованого списку (додавання в кінець)

void Make_Single_List(int n,Single_List** Head){

if (n > 0) {

(*Head) = new Single_List();

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

cout << "Введіть значення ";

cin >> (*Head)->Data;

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

(*Head)->Next=NULL;//обнуління адресного поля

Make_Single_List(n-1,&((*Head)->Next));

}

}

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

//друк односпрямованого списку

void Print_Single_List(Single_List* Head) {

if (Head != NULL) {

cout << Head->Data << "\t";

Print_Single_List(Head->Next);

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

}

else cout << "\n";

}

У динамічні структури легко додавати елементи, тому що для цього досить змінити значення адресних полів. Вставка першого і наступного елементів списку відрізняються одна від одної. Тому у функції, що реалізує дану операцію, спочатку здійснюється перевірка, на яке місце вставляється елемент. Далі реалізується відповідний алгоритм додавання (рис. 2).

Рисунок 2.  Вставка елемента в односпрямований список

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

Single_List* Insert_Item_Single_List(Single_List* Head,

int Number, int DataItem){

Number--;

Single_List *NewItem=new(Single_List);

NewItem->Data=DataItem;

NewItem->Next = NULL;

if (Head == NULL) {//список порожній

Head = NewItem;// створюємо перший елемент списку

}

else {//список не порожній

Single_List *Current=Head;

for(int i=1; i < Number && Current->Next!=NULL; i++)

Current=Current->Next;

if (Number == 0){

// вставляємо новий елемент на перше місце

NewItem->Next = Head;

Head = NewItem;

}

else {// вставляємо новий елемент на не перше місце

if (Current->Next != NULL)

NewItem->Next = Current->Next;

Current->Next = NewItem;

}

}

return Head;

}

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

Алгоритми вилучення першого і наступного елементів списку відрізняються один від одного. Тому у функції, що реалізує дану операцію, здійснюється перевірка, який елемент вілучається. Далі реалізується відповідний алгоритм вилучення (рис. 3).

Рисунок 3.  Вилучення елемента з односпрямованого списку

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

Single_List* Delete_Item_Single_List(Single_List* Head,

int Number){

Single_List *ptr;// допоміжний вказівник

Single_List *Current = Head;

for (int i = 1; i < Number && Current != NULL; i++)

Current = Current->Next;

if (Current != NULL){// перевірка на коректність

if (Current == Head){// вилучаємо перший елемент

Head = Head->Next;

delete(Current);

Current = Head;

}

else {// вилучаємо не перший елемент

ptr = Head;

while (ptr->Next != Current)

ptr = ptr->Next;

ptr->Next = Current->Next;

delete(Current);

Current=ptr;

}

}

return Head;

}

Операція пошуку елемента в списку полягає в послідовному перегляді всіх елементів списку доти, поки поточний елемент не буде містити задане значення або поки не буде досягнутий кінець списку. В останньому випадку фіксується відсутність шуканого елемента в списку (функція приймає значення false).

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

bool Find_Item_Single_List(Single_List* Head, int DataItem){

Single_List *ptr; // допоміжним покажчик

ptr = Head;

while (ptr != NULL){//поки не кінець списку

if (DataItem == ptr->Data) return true;

else ptr = ptr->Next;

}

return false;

}

Операція вилучення списку полягає у звільненні динамічної пам'яті. Для даної операції організується функція, у якій потрібно переставляти вказівник на наступний елемент списку доти, поки вказівник не стане дорівнювати NULL, тобто не буде досягнутий кінець списку. Реалізуємо рекурсивну функцію.

/*звільнення пам'яті, виділеної під односпрямований список*/

void Delete_Single_List(Single_List* Head){

if (Head != NULL){

Delete_Single_List(Head->Next);

delete Head;

}

}

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

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

Двохспрямований (двохзв’язний) список - це структура даних, що складається з послідовності елементів, кожний з яких містить інформаційну частину і два вказівника на сусідні елементи (рис. 4). При цьому два сусідніх елементи повинні містити взаємні посилання один на одний.

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

Рисунок 4.  Двохспрямований список

Опис найпростішого елемента такого списку виглядає в такий спосіб:

struct имя_типа {

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

адресне поле 1;

адресне поле 2;

};

де

інформаційне поле - це поле кожного, раніше оголошеного або стандартного, типу;

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

адресне поле 2 - це вказівник на об'єкт того ж типу, що і обумовлена структура, у нього записується адреса попереднього елемента списку.

Наприклад:

struct list {

type elem ;

list *next, *pred ;

}

list *headlist ;

де type - тип інформаційного поля елемента списку;

*next, *pred - вказівники на наступний і попередній елементи цієї структури відповідно.

Змінна-вказівник headlіst задає список як єдиний програмний об'єкт, його значення - вказівник на перший (або заголовний) елемент списку.

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

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

- створення списку;

- друк (перегляд) списку;

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

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

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

- перевірка чи порожній список;

- вилучення списку.

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

Для опису алгоритмів цих основних операцій використовується наступне оголошення:

struct Double_List {//структура даних

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

Double_List *Next, //адресне поле

*Prior; //адресне поле

};

. . . . . . . . . .

Double_List *Head; // вказівник на перший елемент списку

. . . . . . . . . .

Double_List *Current;

// вказівник на поточний елемент списку (при необхідності)

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

// створення двохнаправленого списку (додавання в кінець)

void Make_Double_List(int n,Double_List** Head,

Double_List* Prior){

if (n > 0) {

(*Head) = new Double_List();

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

cout << "Введіть значення ";

cin >> (*Head)->Data;

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

(*Head)->Prior = Prior;

(*Head)->Next=NULL;//обнуління адресного поля

Make_Double_List(n-1,&((*Head)->Next),(*Head));

}

else (*Head) = NULL;

}

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

// друк двохспрямованого списку

void Print_Double_List(Double_List* Head) {

if (Head != NULL) {

cout << Head->Data << "\t";

Print_Double_List(Head->Next);

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

}

else cout << "\n";

}

У динамічні структури легко додавати елементи, тому що для цього досить змінити значення адресних полів. Операція вставки реалізовується аналогічно функції вставки для односпрямованого списку, тільки з урахуванням особливостей двохспрямованого списку (рис. 5).

Рисунок 5.  Додавання елемента у двохспрямований список

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

Double_List* Insert_Item_Double_List(Double_List* Head,

int Number, int DataItem){

Number--;

Double_List *NewItem=new(Double_List);

NewItem->Data=DataItem;

NewItem->Prior=NULL;

NewItem->Next = NULL;

if (Head == NULL) {//список ппорожній

Head = NewItem;

}

else {//список не порожній

Double_List *Current=Head;

for(int i=1; i < Number && Current->Next!=NULL; i++)

Current=Current->Next;

if (Number == 0){

// вставляємо новий елемент на перше місце

NewItem->Next = Head;

Head->Prior = NewItem;

Head = NewItem;

}

else {// вставляємо новий елемент на не перше місце

if (Current->Next != NULL) Current->Next->Prior = NewItem;

NewItem->Next = Current->Next;

Current->Next = NewItem;

NewItem->Prior = Current;

Current = NewItem;

}

}

return Head;

}

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

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

/* вилучення елемента із заданим номером із двохспрямованого списку */

Double_List* Delete_Item_Double_List(Double_List* Head,

int Number){

Double_List *ptr;// допоміжний покажчик

Double_List *Current = Head;

for (int i = 1; i < Number && Current != NULL; i++)

Current = Current->Next;

if (Current != NULL){// перевірка на коректність

if (Current->Prior == NULL){// вилучаємо перший елемент

Head = Head->Next;

delete(Current);

Head->Prior = NULL;

Current = Head;

}

else {// вилучаємо не перший елемент

if (Current->Next == NULL) {

// вилучаємо останній елемент

Current->Prior->Next = NULL;

delete(Current);

Current = Head;

}

else {// вилучаємо не перший і не останній елемент

ptr = Current->Next;

Current->Prior->Next =Current->Next;

Current->Next->Prior =Current->Prior;

delete(Current);

Current = ptr;

}

}

}

return Head;

}

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

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

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

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

// пошук елемента у двохспрямованому списку

bool Find_Item_Double_List(Double_List* Head,

int DataItem){

Double_List *ptr; // допоміжний вказівник

ptr = Head;

while (ptr != NULL){// поки не кінець списку

if (DataItem == ptr->Data) return true;

else ptr = ptr->Next;

}

return false;

}

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

// перевірка чи порожній двохспрямований список

bool Empty_Double_List(Double_List* Head){

if (Head!=NULL) return false;

else return true;

}

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

// звільнення пам'яті, виділеної під двохспрямований список

void Delete_Double_List(Double_List* Head){

if (Head != NULL){

Delete_Double_List(Head->Next);

delete Head;

}

}

Методичні вказівки

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

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

При написанні програми виконуйте коментування коду.

Література

1. Ахо А., Хопкрофт Дж., Ульман Дж. Структуры данных и алгоритмы. – М.: Издательский дом “Вильямс”, 2001. – 384 с.

2. Вирт Н. Структуры данных и алгоритмы. С примерами на Паскале. Издание 2. – СПб.: Невский диалект, 2008. – 352 с.

3. Кнут Д. Искусство программирования для ЭВМ / В 3-х томах. – М.: “Мир”, 2008.

4. Новиков Ф. А. Дискретная математика для программистов. Учебник для вузов. 2-е изд. – СПб.: Питер, 2005. – 364 с.

Завдання

1. Вивчіть завдання, виділивши при цьому всі види даних;

2. Сформулюйте математичну постановку завдання;

3. Виберіть метод рішення завдання, якщо це необхідно;

4. Розробіть алгоритм рішення завдання;

5. Напишіть програму обраною мовою програмування;

6. Підготуйте тестові дані для перевірки різних режимів роботи програми;

7. Виконайте тестування програми;

8. Порівняйте отриманий результат з підготовленим для тестування;

9. Здійсніть відлагодження програми;

10. Підготуйте звіт.

Вимоги до звіту

Звіт до лабораторної роботи повинен відповідати наступній структурі:

- титульний лист.

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

- математична модель. У цьому підрозділі вводяться математичні описи даних і математичний опис їхньої взаємодій. Ціль підрозділу - подати розв'язуване завдання у математичному формулюванні.

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

- лістинг програми. Підрозділ повинен містити текст програми обраною мовою програмування.

- контрольний тест. Підрозділ містить набори вихідних даних і отримані в ході виконання програми результати.

- висновки по лабораторній роботі.

- відповіді на контрольні питання.

Приклад виконання роботи

Приклад 1. N-натуральних чисел є елементами двохспрямованого списку L, обчислити: X1*Xn+X2*Xn-1+...+Xn*X1. Вивести на екран кожний добуток і підсумкову суму.

Алгоритм:

1. Створюємо структуру.

2. Формуємо список цілих чисел.

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

4. Підсумуємо отримані результати.

5. Виводимо на друк

Створення структури, формування списку і виведення на друк розглянуті раніше. Приведемо функції реалізації просування за списком в обох напрямках і знаходження підсумкової суми.

// пошук останнього елемента списку

Double_List* Find_End_Item_Double_List(Double_List* Head){

Double_List *ptr; // додатковий вказівник

ptr = Head;

while (ptr->Next != NULL){

ptr = ptr->Next;

}

return ptr;

}

// підсумкова сума добутків

void Total_Sum(Double_List* Head) {

Double_List* lel = Head;

Double_List* mel = Find_End_Item_Double_List(Head);

int mltp,sum=0;

while(lel != NULL) {

mltp = (lel->Data)*(mel->Data);// множення елементів

printf("\n\n%d * %d = %d",lel->Data,mel->Data,mltp);

sum = sum + mltp;// підсумовування добутків

lel = lel->Next;

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

mel = mel->Prior;

// ідемо за списком з останнього елемента в перший

}

printf("\n\n Підсумкова сума дорівнює %d",sum);

}

}

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