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

lektsii_OP / T15

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

Рис. 4. Структура динамічної пам’яті при розміщенні двовимірного масиву

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

float **а = new float* [3];

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

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

 

a[i] = new float [5];

// створення рядків матриці

Після цього можна працювати з матрицею як з звичайним двовимірним масивом, звертаючись до кожного елемента за його індексом (a[i][j]), що є більш природним і зручним, аніж попередній спосіб. Зокрема, ініціалізація такого масиву може мати вид:

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

 

for (int j=0; j<5; j++)

 

a[i][j]=i*j;

// ініціалізація елементів масиву

Ще один приклад створення двовимірного динамічного масиву цілих чисел:

int n; const m=5;

cout<<"input the number"; cin>>n;

int **р;

// р - покажчик на масив покажчиків

р = new int*[n];

// виділення пам’яті для масиву покажчиків

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

 

р[i] = new int[m];

// виділення пам’яті для рядків масиву n х m

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

 

for (int j=0; j<m; j++)

 

р[i][j] = i*j;

// ініціалізація елементів масиву

У даному прикладі визначаємо змінну р як адресу масиву покажчиків. Виділяємо область пам'яті для масиву з n покажчиків на тип int і присвоюємо адресу початку цієї пам'яті покажчику р. У циклі пробігаємо по масиву покажчиків р[], присвоюючи кожному покажчику р[i] адресу знову виділенної пам'яті під масив з m чисел типу int.

Відмінність описаної схеми від схеми статичного двомірного масиву полягає в тому, що для адрес покажчика на багатовимірний масив р та адрес масиву покажчиків на інші одновимірні масиви р[0], р[1], ..., р[n-1] має бути відведений реальний фізичний простір пам'яті. У той час як для статичного двовимірного масиву вирази виду р, р[0], р[1], ..., р[n-1] були всього лише можливими конструкціями для посилань на реально існуючі елементи масиву, але самі ці покажчики не існували як об'єкти в пам'яті комп'ютера.

11

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

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

 

delete[] p[i];

// звільнення пам’яті від кожного рядка масива n х m

delete[] p;

// звільнення пам’яті від масиву покажчиків

Таким чином, операції new [] і delete [] дозволяють створювати і видаляти динамічні масиви, підтримуючи при цьому ілюзію їх довільної розмірності.

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

int **p;

//покажчик на масив

int n, m;

//кількість рядків і стовпців масиву

int** input();

 

//створення матриці

void output(int **);

//виведення масиву

void processing(int **);

//обробка масиву

void remove(int **);

//видалення масиву

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

int main()

{ srand(time(NULL)); p= input(); output(p); processing(p); remove(p); system("pause");

}

//======================= створення матриці ======================

int** input()

{ cout<<"n="; cin>>n; cout<<"m="; cin>>m; int **a= new int *[n]; for (int i=0; i<n; i++)

a[i]= new int [m]; for (int i=0; i<n; i++)

for (int j=0; j<m; j++) a[i][j]= rand()%21-5;

return a;

}

//======================= виведення матриці =======================

void output(int **a) { for (i=0; i<n; i++)

{for (j=0; j<m; j++)

cout<<setw(4)<<a[i][j];

cout<<endl;

}

}

// ======================== обробка матриці ========================

void processing(int **a)

12

{int num=-1; bool flag;

for (int j=0; j<m; j++)

{flag=true;

for (int i=0; i<n; i++) if (a[i][j]<0)

{flag=false; break;

}

if (flag)

{ num=j; break;

}

}

if (num==-1) cout<<"no"<<endl; else cout<<"n="<<num+1<<endl;

}

// ================== видалення матриці (вивільнення пам'яті ) ==================

void remove (int **a) { for (int i=0; i<n; i++) delete[] a[i];

delete []a;

}

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

Динамічні масиви можна створювати у С++ і засобами STL–бібліотеки. Дана бібліотека містить контейнер vector, що зберігає скінчену множину однотипних об'єктів і реалізує довільний доступ до них. Даний контейнер по суті є звичайним динамічним масивом, але з низкою додаткових функцій, зокрема, з автоматичною зміною розміру при додаванні/видаленні елемента. Щоб мати змогу використовувати дану колекцію, слід підключити до програми заголовний файл <vector> і використовувати простір імен std.

Створити STL–вектор, як об’єкт шаблонного класу vector, можна декількома способами, використовуючи різні види конструктора. Основні із них:

vector <T> v;

vector <T> v(number); vector <T> v(number, value); vector <T> v(v1);

13

Тут v, v1 – вектори, T - тип компонент вектора, number – кількість елементів вектора, value – значення, яким ініціалізуються елементи вектора.

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

vector <float> A;

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

vector <int> В(30);

// створення цілочисельного вектора В з 30 елементів

Слід звернути увагу, що конструкція

vector <int> v [10];

створить масив із 10 векторів цілих чисел, кожен з яких спочатку порожній.

Вектор можна створити і з наперед встановленими значеннями. Для цього використовується наступна конструкція:

vector <T> v(n, value);

В результаті створюється вектор, елементи якого заповнені значенням val. Наприклад,

vector <int> В(3, 10); // створення цілочисельного вектора В з трьома значеннями 10

STLматрицю можна описати так:

vector< vector<int> > matrix;

Оскільки дві кутові дужки, що йдуть підряд без пробілу, більшість трансляторів сприймають як операцію "<<" або ">>", то, при наявності в коді вкладених STLконструкцій, між кутовими дужками треба ставити пробіл.

Наприклад, створення матриці розмірності n х m, заповненої -1:

int n, m;

vector< vector<int> > matrix(n, vector<int>(m, -1));

Для спрощення описів можна визначити типи, наприклад так:

typedef vector <int> tvector;

// тип вектор

typedef vector <tvector> tmatrix;

// тип матриця

Доступ до елементів вектора можна здійснювати так само, як і в звичайному масиві, використовуючи оператор індексації [], наприклад, v[і]. Окрім того, можна скористатися методами об'єкта класу vector, які повертають посилання на певні елементи масиву:

Метод

Опис

 

 

at(n)

Повертає посилання на n-й елемент масиву

front()

Повертає посилання на перший елемент масиву

back()

Повертає посилання на останній елемент масиву

Наприклад,

vector v(5, 10); v[0]=0; v[4]=4;

cout << v .front() << " " << v .back() << endl;

14

Основна функціональність STL-векторів реалізується через специфічні методи класу vector. Даний клас дозволяє досить ефективно і легко вирішувати наступні задачі:

1)вставляти елементи в масив;

2)видаляти елементи із масиву;

3)визначати розмір масива і змінювати його;

4)порівнювати елементи масивів.

Деякі із методів класу vector передбачають використання ітераторів - об'єктів STL, які відіграють роль вказівників і дозволяють одержати доступ до елементів масиву. Щоб мати змогу працювати з ітераторами слід підключити заголовний файл

<iterator>.

Вставка елементів в STL-масив здійснюється наступними методами:

Метод

Опис

 

 

push_back(x)

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

insert(it,x)

Вставляє елемент x перед елементом, на який посилається ітератор it. Повертає

 

ітератор вставленого елемента.

insert(it,n,x)

Вставляє n елементів x перед елементом, на який посилається ітератор it.

 

Повертає ітератор першого вставленого елемента.

insert(it,b,e)

Вставляє елементи з діапазону, обмеженого ітераторами [b,e), перед

 

елементом, на який посилається ітератор it. Повертає ітератор першого

 

вставленого елемента

Наприклад,

vector <int> v;

 

 

v .push_back(3);

// помістити 3 у кінець вектора - v = (3)

v .push_back(1);

// додати 1

- v = (3, 1)

v .push_back(2);

// додати 2

- v = (3, 1, 2)

for (int i= 0; i< v .size(); i++)

 

 

cout << v[i] << “ “;

// вивести вектор ( виводиться: 3 1 2 )

Вставити елемент val перед i-м елементом вектора A можна за допомогою

A.insert(A.begin()+ i, val).

Якщо ж передати в якості покажчика два ітератора, то можна вставити весь фрагмент між ітераторами. Наприклад,

A.insert(A.begin(), B.begin(), B.end())

вставляє на початок вектора A весь вміст вектора B.

Наведемо ще один приклад роботи з вектором і матрицею з використанням бібліотеки STL.

vector<int> v(10);

// створюється вектор v з 10 елементів типу int

for (int i=0; i<10; i++)

 

v[i] = i;

// ініціалізація елементів вектора v значеннями від 0 до 9

vector<int> m[10];

// створюється матриця m із елементів типу int

for (int i=0; i < 10; i++)

 

for (int j=0; j < 10; j++)

 

m[i].push_back(j);

// ініціалізація елементів поточного рядка матриці m

 

15

 

for (int

i=0; i < 10; i++)

 

 

{ for (int j=0; j < 10; j++)

 

 

cout<<m[i][j]<<" ";

// виведення елементів поточного рядка матриці m

 

cout<<endl;

 

}

 

 

 

Для видалення елементів з STL-масиву служать наступні методи:

 

 

 

 

 

 

Метод

 

 

Опис

 

 

 

 

 

pop_back()

 

Видаляє останній елемент вектора

 

clear()

 

Видаляє всі елементи з вектора (його розмір стає рівним 0)

 

erase(it)

 

Видаляє елемент у масиві в позиції ітератора it. Повертає ітератор елемента, що

 

 

 

слідує за вилученим

 

erase(b,e)

 

Видаляє елементи з діапазону, обмеженого ітераторами [b,e). Повертає ітератор

 

 

 

елемента, що слідує за вилученими

 

swap(А)

 

Міняє місцями елементи викликаючого вектора і вектора А

Наприклад,

v.erase(v.begin()+ i); v.erase(v.begin()+ i, v.begin()+ j); v.erase(v.begin() + i, v.end()); v.erase(v.end() - k, v.end());

// видалити із вектора v елемент з індексом i //видалити з v елементи з i (включно) до j (не включно)

//видалити з v елементи з i-го до кінця

//видалити k останніх елементів вектора v

Маніпуляції з розміром об'єкта класу vector визначаються наступними методами:

Метод

Опис

 

 

size()

Визначає кількість елементів у векторі в поточний момент

resize(n)

Змінює розмір вектора. Новий розмір задається параметром n.

resize(n, c)

Змінює розмір вектора. Новий розмір задається параметром n. Якщо вектор

 

збільшується в розмірі, нові елементи заповнюються значенням c.

capacity()

Повертає поточну місткість колекції - кількість елементів, які може містити

 

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

 

колекції, а не те, скільки в ній зберігається елементів)

reserve(n);

Виділяє пам'ять для n елементів вектора. Якщо поточний розмір вектора менше

 

величини, що повертається функцією reserve(), відбувається додаткове виділення

 

пам'яті

empty()

Визначає, чи є вектор порожнім (якщо вектор порожній, повертає значення true,

 

інакше — false)

Після виклику методу resize(n) вектор буде містити рівно n елементів. Якщо параметр n менше, ніж розмір вектора до виклику resize(n), то вектор зменшиться і «зайві» елементи будуть видалені. Якщо ж n більше, ніж розмір вектора, то вектор збільшить свій розмір і заповнить елементи, що з'явилися, нулями. Наприклад,

vector<int> v(20);

// створюється вектор v з 20 елементів типу int

for (int i=0; i<20; i++)

 

v[i] = i+1;

// ініціалізація елементів вектора v значеннями від 1 до 19

v.resize(25);

// збільшення розміру вектора v

for (int i=20; i<25; i++)

 

v[i] = i*2;

// ініціалізація нових елементів v

16

Важливо пам'ятати, що якщо використовувати push_back() після resize(n), то елементи будуть додані після області, виділеної resize(n), а не в неї. Наприклад,

vector<int> v(20); for (int i=0; i<20; i++)

v[i] = i+1; v.resize(25);

for (int i=20; i<25; i++)

v.push_back(i*2); // запис здійснюється в елементи [25..30), а не [20..25) !

Після виконання даного фрагмента коду розмір вектора буде дорівнює 30, а не 25.

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

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

Фрагмент програми нижче демонструє, що розмір і місткість колекції - дві різні сутності:

vector<int> v;

cout << "Real size of array in vector: " << vec.capacity ()<< endl; for (int i= 0; i< 10; i++)

vec.push_back (10);

cout << "Real size of array in vector: " << vec.capacity ()<< endl;

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

Розглянемо ще наступний приклад. Нехай є vector з 1000 елементів і обсяг виділеної їм пам'яті становить 1024 елемента. Ми збираємося додати в нього 50 елементів за допомогою методу push_back(). Оскільки вектор збільшується на 50 елементів, то місткість наявної пам'яті буде недостатньою. Тому буфер під розміщення вектора розширюється в два рази і його розмір у пам'яті по завершенні цієї операції становитиме 2048 елементів, тобто майже в два рази більше, ніж це реально необхідно. Однак, якщо перед серією викликів методу v.push_back () додати виклик

v.reserve (1050);

то пам'ять буде використовуватися ефективно.

Подібно іншим типам, векторам можна присвоювати значення з використанням операції "=" і виконувати над ними операцію порівняння "==". Наприклад,

vector <int> vecA, vecB; vecA.push_back(3); vecA.push_back(2);

17

vecB = vecA;

// vecB = (3, 2)

vecB[1] = 0;

// vecB = (3, 0)

if (vecA == vecB) cout << “Рівні!” << endl;

Операція "==" виконує поелементне порівняння двох векторів. У даному випадку вектори не рівні між собою.

Приклад. В одномірному масиві, що складається з n елементів, обчислити cуму від’ємних елементів масиву.

#include <iostream> #include <vector> #include <stdlib.h> #include <stdio.h> using namespace std;

void input (vector <int> &);

void output (vector <int> &, string ); int suma(vector <int> &);

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

int main()

{ vector <int> v; srand(time(NULL)); input (v);

output(v, "\nNew vector:" );

cout << "\n sum elementiv below 0 is "<<suma(v)<<endl; system("pause");

}

//======================= генерація масиву ======================

void input (vector<int> &vec) { int n=0;

cout << "\n Enter number of element : "; cin >> n; for (int i=0; i<n; i++)

vec.push_back(rand()%20);

}

//======================= виведення масиву ======================

void output (vector<int> &vec, string str) { cout<<str;

for (int i= 0; i<vec.size(); i++) cout << vec[i] << " ";

cout<<endl;

}

//=========== знаходження суми елементів масиву =================

int suma(vector<int> &vec) { int sum=0;

for (int i= 0; i<vec.size(); i++) if (vec[i]<0)

sum +=vec[i]; return sum;

}

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

18

Зручною можливістю є використання для обробки STL-масивів глобальних STL- алгоритмів. Вони представляють набір готових функцій, які можуть бути застосовані до STL-контейнерів, зокрема об’єктів класу vector. Визначені ці алгоритми в заголовному файлі <algorithm>. Перелік основних STL-алгоритмів, які можуть застосовуватися для масивів:

Алгоритм

Призначення

 

 

binary_search()

визначає, є чи даний елемент у відсортованому масиві

copy()

копіює масив

count()

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

 

 

count_if()

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

 

предикату

 

 

find()

знаходить перше входження значення в масив

includes()

визначає, чи включає один масив всі елементи іншого масиву

 

 

lower_bound()

знаходить перше входження значення у відсортованому масиві

 

 

make_heap()

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

 

 

max()

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

 

 

min()

повертає мінімальне із двох значень

 

 

max_element()

знаходить найбільше значення в масиві

min_element()

знаходить найменше значення в масиві

merge()

зливає два відсортованих масива (результат розміщає в третій масив)

 

 

partial_sort()

сортує частину масиву

 

 

random_shuffle()

переміщає елементи відповідно до випадкового рівномірному розподілу

 

(безладно перемішує масив)

remove()

видаляє елементи з даним значенням

 

 

replace()

заміняє елементи із зазначеним значенням

 

 

reverse()

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

 

 

remove_copy()

копіює масив, видаляючи елементи із зазначеним значенням

 

 

rotate()

виконує циклічне переміщення вліво елементів у діапазоні

 

 

search ()

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

 

 

sort()

сортує масив

stable_sort()

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

swap()

міняє місцями два значення

 

 

unique()

видаляє повторювані сусідні елементи

 

 

unique_copy()

копіює масив, видаляючи рівні сусідні елементи

 

 

upper_bound()

знаходить перший елемент, більший ніж задане значення

 

 

Одним із таких алгоритмів є сортування. Для сортування можна застосувати стандартний алгоритм sort. Можна сортувати, як весь вектор, так і окремі його частини:

void input (vector <int> &);

void output (vector <int> &, string ); int suma(vector <int> &);

19

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

int main()

{ vector <int> v1(10);

 

vector<int> v2(10);

 

input (v1);

 

output(v1, "\nvector1:" );

// 10 9 8 7 6 5 4 3 2 1

v2= v1;

 

output(v2, "\nvector2:" );

// 10 9 8 7 6 5 4 3 2 1

sort(v1.begin(), v1.end());

 

output(v1, "\nsorted vector1:" );

// 1 2 3 4 5 6 7 8 9 10

sort(v2.begin()+1, v2.end() -1);

 

output(v2, "\nsorted fragment vector2:" );

// 10 2 3 4 5 6 7 8 9 1

system("pause");

 

}

 

//======================= ініціалізація масиву ======================

void input (vector<int> &v)

{ for (int i=0; i< v.capacity(); i++) v[і]= 10-і;

}

//======================= виведення масиву ======================

void output (vector<int> &v, string str) { cout<<str;

for (int i= 0; i<v.size(); i++) cout << v[i] << " ";

cout<<endl;

}

З використанням алгоритмів можливе створення дуже могутніх і ефективних програм. По компактності такий код перевершує код, написаний на таких сучасних мовах, як Java і С#, і в значній мірі ефективніше останнього.

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

Спискові структури

Поняття списку

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

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

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

20

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