Skip to main content

Упражнения: Линейни Структури от Данни, Масиви и Двоично Търсене

Напредък

0%
✅ Завършени: 0 / 0📊 Напредък: 0%

💡 Напредъкът се записва локално в браузъра


ℹ️За упражненията

Тези упражнения са организирани в три нива на трудност:

  • 🟢 Лесни (1-10): Основни концепции и дефиниции
  • 🟡 Средни (11-20): Практически задачи и проследяване на алгоритми
  • 🔴 Трудни (21-30): Имплементация, дебъгване и анализ на сложност

Препоръчваме да решавате задачите последователно за най-добро усвояване на материала.


🟢 Лесно Ниво (1-10)

10 minЛЕСНО

Задача 1: Структура от данни - дефиниция и значение

Въпрос: Какво е структура от данни? С ваши думи, обяснете защо изборът на правилната структура от данни има значение в програмирането.

Отговор: Структурата от данни е организиран начин за съхранение и управление на данни в компютъра, който позволява ефективно извършване на операции като добавяне, изтриване, търсене и достъп до данни.

Изборът на правилната структура има значение, защото:

  • Производителност: Различните структури имат различна времева сложност за операции (O(1), O(log n), O(n))
  • Памет: Ефективното използване на паметта зависи от структурата
  • Скалируемост: Правилният избор осигурява добра производителност при нарастване на данните
  • Четимост: Подходящата структура прави кода по-разбираем и поддържаем

10 minЛЕСНО

Задача 2: Хомогенност на масивите - еднотипност на елементите

Вярно или Невярно: В масив всички елементи трябва да са от един и същи тип данни. Обяснете отговора си.

Отговор: Вярно. В масив всички елементи трябва да са от един и същи тип данни (хомогенни елементи). Това е фундаментално свойство на масивите, което позволява:

  • Всеки елемент заема еднакво количество памет
  • Директното изчисляване на адреси: адрес = база + (индекс × размер)
  • Константен O(1) достъп до всеки елемент по индекс

Ако елементите бяха с различен размер, директното изчисляване на адреси нямаше да е възможно.


10 minЛЕСНО

Задача 3: Индексиране на масиви - достъп до елементи

Задача: Даден е масивът int numbers[5] = {10, 20, 30, 40, 50};, каква е стойността на numbers[2]?

Отговор: numbers[2] = 30

Обяснение: Масивите в C++ използват индексиране, базирано на нула:

  • numbers[0] = 10 (първи елемент)
  • numbers[1] = 20 (втори елемент)
  • numbers[2] = 30 (трети елемент) ← Търсеният отговор
  • numbers[3] = 40 (четвърти елемент)
  • numbers[4] = 50 (пети елемент)

10 minЛЕСНО

Задача 4: Непрекъснато разпределение на паметта и кеш ефективност

Въпрос: Какво означава "непрекъснато разпределение на паметта" в контекста на масивите? Защо е това важно?

Отговор: Непрекъснато разпределение означава, че всички елементи на масива се съхраняват последователно в паметта, без пропуски между тях.

Защо е важно:

  • Бърз достъп: Позволява директно изчисляване на адрес на всеки елемент
  • Кеш ефективност: Процесорът може да зарежда съседни елементи в кеша заедно
  • Предсказуемост: Простота при управление на паметта
  • Без фрагментация: Целият масив е в един непрекъснат блок

10 minЛЕСНО

Задача 5: Фиксиран размер на масивите и динамични алтернативи

Въпрос: Ако създадете масив с int grades[100];, можете ли по-късно да промените размера му на 150 по време на изпълнението на програмата? Защо или защо не?

Отговор: НЕ, не можете да промените размера на статичен масив по време на изпълнение.

Причини:

  • Статичните масиви имат фиксиран размер, определен при компилация
  • Паметта се заделя на стека и размерът е константа
  • След създаване, размерът не може да се променя

Алтернативи:

  • Използвайте динамично заделена памет (new[]) и пресъздайте масива
  • Използвайте std::vector, който автоматично управлява размера

10 minЛЕСНО

Задача 6: Времева сложност O(1) за достъп по индекс

Въпрос: Каква е времевата сложност (Big O нотация) за достъп до елемент в масив по неговия индекс? Обяснете защо.

Отговор: Времевата сложност е O(1) - константно време.

Защо:

  • Адресът се изчислява директно по формулата: адрес = база + (индекс × размер_елемент)
  • Не е необходимо да се обхожда масивът
  • Времето за достъп е независимо от размера на масива
  • Едно просто аритметично изчисление + един memory read

Това е едно от най-големите предимства на масивите спрямо други структури като свързани списъци (O(n) достъп).


10 minЛЕСНО

Задача 7: Инкапсулация в ООП с масиви като пример

Въпрос: В обектно-ориентираното програмиране, какво означава "инкапсулация"? Дайте пример, свързан с масиви.

Отговор: Инкапсулацията е принцип на ООП, при който данните (атрибути) и методите за работа с тях се обединяват в един клас, като вътрешните детайли се скриват от външния свят.

Пример с масиви:

class CustomArray {
private:
int* data; // Скрит - external code не може да достъпи директно
size_t size; // Скрит

public:
int get(size_t i) { // Контролиран достъп
if (i < size) return data[i];
throw std::out_of_range("Невалиден индекс");
}
};

Инкапсулацията защитава данните от некоректни операции (напр. достъп извън границите).


10 minЛЕСНО

Задача 8: Изискване за сортираност при двоично търсене

Вярно или Невярно: Двоичното търсене работи както върху сортирани, така и върху несортирани масиви. Обяснете отговора си.

Отговор: НЕВЯРНО! Двоичното търсене работи САМО върху сортирани масиви.

Защо:

  • Алгоритъмът разчита на факта, че елементите са подредени
  • Сравнението със среден елемент показва в коя половина е целевата стойност
  • Ако масивът не е сортиран, тази логика не работи - може да елиминираме половината, в която всъщност е елементът

За несортирани масиви се използва линейно търсене (O(n)).


10 minЛЕСНО

Задача 9: Проследяване на двоично търсене - първа стъпка

Задача: В масива {2, 4, 6, 8, 10, 12, 14}, ако търсите стойността 10 използвайки двоично търсене, кой е първият среден индекс, който ще бъде проверен?

Отговор: Средният индекс е 3.

Изчисление:

  • Масивът има 7 елемента (индекси 0-6)
  • low = 0, high = 6
  • mid = low + (high - low) / 2 = 0 + (6 - 0) / 2 = 0 + 3 = 3
  • На индекс 3 е стойността 8
  • Тъй като 8 < 10, ще търсим в дясната половина след това

10 minЛЕСНО

Задача 10: Роля на конструктора в C++ класове

Въпрос: Каква е целта на конструктора в C++ клас?

Отговор: Конструкторът е специален метод, който се извиква автоматично при създаване на обект от класа. Неговите цели са:

  • Инициализация: Задава начални стойности на членовете на класа
  • Заделяне на ресурси: Заделя динамична памет, отваря файлове и др.
  • Валидация: Проверява входни параметри преди създаване на обекта
  • Гарантира валидно състояние: Обектът е винаги в коректно състояние след създаването му

Пример:

CustomArray(size_t n) : capacity(n) {
data = new int[capacity]; // Заделя памет
}

🟡 Средно Ниво (11-20)

15 minСРЕДНО

Задача 11: Деклариране и инициализация на масив от double стойности

Кодиране: Напишете C++ код за деклариране и инициализация на масив от 7 double стойности, представляващи дневни температури. Включете код за отпечатване на всички температури.

#include <iostream>

int main() {
// Деклариране и инициализация
double temperatures[7] = {22.5, 24.0, 23.8, 21.2, 25.1, 26.3, 24.7};

// Отпечатване на всички температури
std::cout << "Дневни температури:" << std::endl;
for (int i = 0; i < 7; ++i) {
std::cout << "Ден " << (i + 1) << ": "
<< temperatures[i] << "°C" << std::endl;
}

return 0;
}

15 minСРЕДНО

Задача 12: Разлика между статични и динамични масиви в C++

Обяснение: Обяснете разликата между тези две декларации на масиви в C++:

int arr1[10];
int* arr2 = new int[10];

Какви са последиците за управлението на паметта при всяка от тях?

Отговор:

int arr1[10];

Статичен масив на стека

  • Заделя се на стека
  • Автоматично време на живот
  • Освобождава се автоматично
  • Фиксиран размер при компилация
  • Риск от stack overflow при голям размер

int* arr2 = new int[10];

Динамичен масив на heap

  • Заделя се на heap-а
  • Ръчно време на живот
  • Трябва delete[] arr2;
  • Размер може да е runtime променлива
  • Риск от memory leak ако забравите delete[]

15 minСРЕДНО

Задача 13: Изчисляване на адрес в паметта по индекс

Изчисление: Дадена е формулата адрес = базов_адрес + (индекс × размер_на_елемента), изчислете адреса на паметта на елемента с индекс 5 в масив от цели числа, ако базовият адрес е 1000 и всяко цяло число заема 4 байта.

Отговор:

адрес = базов_адрес + (индекс × размер_на_елемента)
адрес = 1000 + (5 × 4)
адрес = 1000 + 20
адрес = 1020

Обяснение: Елементът на индекс 5 е 6-ият елемент (индексите започват от 0). Той е отместен с 5 × 4 = 20 байта от началото на масива.


15 minСРЕДНО

Задача 14: Важност на проверката на границите в масиви

Въпрос: В custom array клас, защо е важно да се имплементира проверка на границите в методите get() и set()? Какво би могло да се случи без нея?

Отговор: Проверката на границите е критична за безопасност и коректност.

Без проверка могат да се случат:

  • Buffer overflow: Писане извън масива, презаписване на друга памет
  • Segmentation fault: Опит за достъп до невалидна памет → crash
  • Undefined behavior: Непредсказуемо поведение на програмата
  • Security vulnerabilities: Възможност за атаки
  • Data corruption: Повреждане на други данни в паметта

С проверка:

T get(size_t index) const {
if (index >= capacity) {
throw std::out_of_range("Индекс извън границите");
}
return data[index];
}

15 minСРЕДНО

Задача 15: Стъпка по стъпка проследяване на двоично търсене

Проследяване: Проследете двоично търсене за стойността 17 в сортирания масив {3, 7, 11, 17, 23, 31, 39, 47}. Покажете стойностите на low, high и mid на всяка стъпка.

Отговор:

Стъпка 1:

  • low = 0, high = 7
  • mid = 0 + (7-0)/2 = 3
  • arr[3] = 17 ← Намерен!

Резултат: Елементът е намерен веднага на индекс 3 след само 1 сравнение, защото 17 е точно в средата на масива.


15 minСРЕДНО

Задача 16: Сравнение на линейно и двоично търсене - 1024 елемента

Сравнение: Сравнете максималния брой сравнения, необходими за линейно търсене спрямо двоично търсене в масив от 1,024 елемента. Покажете изчисленията си.

Отговор:

Линейно търсене (O(n)):

  • Максимален брой сравнения = размер на масива
  • За 1024 елемента: 1024 сравнения

Двоично търсене (O(log n)):

  • Максимален брой сравнения = ⌈log₂(n)⌉
  • log₂(1024) = log₂(2¹⁰) = 10
  • За 1024 елемента: 10 сравнения

Заключение: Двоичното търсене е 102 пъти по-бързо (1024 / 10 ≈ 102) за този размер масив!


15 minСРЕДНО

Задача 17: Сигнатура на функция за двоично търсене в вектор от низове

Сигнатура на функция: Напишете C++ сигнатура на функция (само заглавието) за функция за двоично търсене, която търси целеви низ в сортиран вектор от низове. Включете подходящи const квалификатори и тип на връщане.

Отговор:

int binarySearch(const std::vector<std::string>& arr, const std::string& target);

Обяснение на всяка част:

  • int - връща индекс (или -1 ако не е намерен)
  • const std::vector<std::string>& - const препратка (не копира, не модифицира)
  • const std::string& - целевият низ също const препратка

Алтернативен вариант с size_t:

std::optional<size_t> binarySearch(const std::vector<std::string>& arr,
const std::string& target);

15 minСРЕДНО

Задача 18: Дълбоко копиране в конструктора за копиране

Обяснение: Обяснете какво означава "дълбоко копиране" в контекста на конструктора за копиране за custom array клас. Защо е необходимо?

Отговор: Дълбокото копиране означава създаване на истинско, независимо копие на всички данни, не само копиране на указатели.

Без дълбоко копиране (плитко копиране):

  • Двата обекта споделят същата памет
  • Промяна в единия засяга другия
  • Двойно освобождаване на паметта при унищожаване → crash

С дълбоко копиране:

CustomArray(const CustomArray& other) : capacity(other.capacity) {
data = new T[capacity]; // Нова памет
for (size_t i = 0; i < capacity; ++i) {
data[i] = other.data[i]; // Копиране на стойности
}
}

Всеки обект има своя собствена независима памет.


15 minСРЕДНО

Задача 19: Роля на деструктора и memory leaks

Въпрос: Каква е целта на деструктора в класа CustomArray? Какво би се случило, ако забравите да го имплементирате?

Отговор: Деструкторът освобождава динамично заделената памет когато обектът бива унищожен.

~CustomArray() {
delete[] data; // Освобождава паметта
data = nullptr;
}

Без деструктор:

  • Memory leak: Заделената памет никога не се освобождава
  • При много създавания/унищожавания паметта се изчерпва
  • Програмата може да стане бавна или да се срине
  • Особено проблематично в дългосрочни приложения

15 minСРЕДНО

Задача 20: Избягване на integer overflow при изчисляване на mid

Въпрос: При двоично търсене, защо mid = low + (high - low) / 2 се предпочита пред mid = (low + high) / 2?

Отговор: За да се избегне integer overflow.

Проблем с (low + high) / 2:

  • Ако low и high са много големи числа, low + high може да надхвърли максималната стойност на int
  • Например: low = 2,000,000,000 и high = 2,000,000,000
  • low + high = 4,000,000,000 > INT_MAX (обикновено 2,147,483,647)
  • Резултат: overflow → неправилна стойност

С low + (high - low) / 2:

  • Първо изчисляваме разликата: high - low
  • Делим я на 2
  • Добавяме към low
  • Никога не надхвърляме лимитите

🔴 Трудно Ниво (21-30)

20 minТРУДНО

Задача 21: Имплементация на reverse() функция на място

Имплементация: Имплементирайте пълна C++ член-функция void CustomArray::reverse(), която обръща елементите в масива на място (без създаване на нов масив). Обмислете времева и пространствена сложност.

template <typename T>
void CustomArray<T>::reverse() {
size_t left = 0;
size_t right = capacity - 1;

// Разменяме елементи от двата края към центъра
while (left < right) {
// Swap data[left] и data[right]
T temp = data[left];
data[left] = data[right];
data[right] = temp;

left++;
right--;
}
}

// Алтернативно с std::swap:
template <typename T>
void CustomArray<T>::reverse() {
for (size_t i = 0; i < capacity / 2; ++i) {
std::swap(data[i], data[capacity - 1 - i]);
}
}

Сложност:

  • Времева: O(n) - обхождаме половината от масива
  • Пространствена: O(1) - използваме само временна променлива

20 minТРУДНО

Задача 22: Намиране на първото срещане с двоично търсене

Дизайн и Имплементация: Проектирайте и имплементирайте C++ функция, която използва двоично търсене за намиране на първото срещане на целева стойност в сортиран масив, който може да съдържа дубликати.

Пример: В {1, 2, 2, 2, 3, 4}, търсенето за 2 трябва да върне индекс 1.

int findFirstOccurrence(const std::vector<int>& arr, int target) {
int low = 0;
int high = arr.size() - 1;
int result = -1; // Запазваме резултата

while (low <= high) {
int mid = low + (high - low) / 2;

if (arr[mid] == target) {
result = mid; // Запазваме позицията
high = mid - 1; // Продължаваме да търсим НАЛЯВО
} else if (arr[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}

return result;
}

Ключова идея: Когато намерим елемента, не спираме веднага, а продължаваме да търсим в лявата половина (high = mid - 1), за да намерим по-ранно срещане.


20 minТРУДНО

Задача 23: Анализ на сортиране + двоично търсене срещу линейно търсене

Анализ: Анализирайте времевата сложност на следната операция: Имате несортиран масив от n елемента и искате да търсите стойност използвайки двоично търсене. Включете времето, необходимо за сортиране на масива първо. Кога този подход става по-изгоден от просто линейно търсене?

Отговор:

Обща сложност:

  • Сортиране: O(n log n) - например QuickSort или MergeSort
  • Двоично търсене: O(log n)
  • Обща: O(n log n) + O(log n) = O(n log n)

Сравнение с линейно търсене:

  • Линейно търсене: O(n)
  • С сортиране + двоично: O(n log n)

Кога си струва:

  • За едно търсене: НЕ си струва (O(n log n) > O(n))
  • За k търсения: O(n log n) + k×O(log n) vs k×O(n)
  • Изгодно когато k > log n (приблизително)
  • Например: 1000 елемента, >10 търсения → си струва да сортираме

Практическо правило:

Ако броят търсения k > n / log n, сортирането и двоичното търсене стават по-ефективни.


20 minТРУДНО

Задача 24: Template клас ResizableArray с resize() метод

Template Клас: Напишете C++ template клас ResizableArray<T>, който разширява концепцията на CustomArray чрез добавяне на метод resize(size_t newCapacity). Методът трябва да работи както при увеличаване, така и при намаляване на масива, запазвайки съществуващите елементи.

template <typename T>
class ResizableArray {
private:
T* data;
size_t capacity;
size_t size; // Брой използвани елементи

public:
ResizableArray(size_t cap = 10)
: capacity(cap), size(0) {
data = new T[capacity];
}

~ResizableArray() {
delete[] data;
}

void resize(size_t newCapacity) {
if (newCapacity == capacity) return;

// Заделяме нова памет
T* newData = new T[newCapacity];

// Копираме колкото се побират елементи
size_t elementsToCopy = (newCapacity < size) ? newCapacity : size;
for (size_t i = 0; i < elementsToCopy; ++i) {
newData[i] = data[i];
}

// Освобождаваме старата памет
delete[] data;

// Актуализираме указателя и капацитета
data = newData;
capacity = newCapacity;

// Ако сме смалили, актуализираме size
if (size > newCapacity) {
size = newCapacity;
}
}

void add(const T& value) {
if (size >= capacity) {
resize(capacity * 2); // Удвояване при нужда
}
data[size++] = value;
}

T& operator[](size_t i) { return data[i]; }
const T& operator[](size_t i) const { return data[i]; }
size_t getSize() const { return size; }
size_t getCapacity() const { return capacity; }
};

20 minТРУДНО

Задача 25: Намиране на позиция за вмъкване със запазване на сортираност

Модифицирано Търсене: Имплементирайте модифицирана функция за двоично търсене, която намира позицията за вмъкване на целева стойност в сортиран масив (където стойността трябва да бъде вмъкната, за да се запази сортираният ред, дори ако не е в масива).

size_t findInsertPosition(const std::vector<int>& arr, int target) {
size_t low = 0;
size_t high = arr.size(); // Забележка: size(), не size()-1

// Търсим най-лявата позиция, където target <= arr[pos]
while (low < high) {
size_t mid = low + (high - low) / 2;

if (arr[mid] < target) {
low = mid + 1; // Целта е вдясно
} else {
high = mid; // Целта е вляво или на mid
}
}

return low; // low е правилната позиция за вмъкване
}

// Пример: arr = {1, 3, 5, 7, 9}, target = 6
// Резултат: 3 (между 5 и 7)

Забележки: Тази техника се нарича "lower_bound" и е имплементирана в std::lower_bound.


20 minТРУДНО

Задача 26: Амортизирана сложност при чести вмъквания в сортиран масив

Сценарий и Анализ: Разгледайте сценарий, при който трябва често да вмъквате елементи в сортиран масив, като поддържате сортирания ред. Изчислете и обяснете амортизираната времева сложност за n вмъквания. Ще бъде ли двоичното търсене полезно тук? Предложете алтернативна структура от данни, която може да е по-подходяща.

Отговор:

Анализ за n вмъквания:

  • Двоично търсене за позиция: O(log n)
  • Местене на елементи за вмъкване: O(n) в най-лош случай
  • Обща сложност за едно вмъкване: O(n)
  • За n вмъквания: O(n²)

Двоичното търсене помага ли?

  • Да, за намиране на позицията (O(log n))
  • НО местенето на елементи остава O(n)
  • Общата сложност е доминирана от местенето

По-добри алтернативи:

Балансирано Дърво

AVL или Red-Black Tree

  • Insert: O(log n)
  • Delete: O(log n)
  • Search: O(log n)

Skip List

Вероятностна структура

  • Insert: O(log n) средно
  • Delete: O(log n) средно
  • Search: O(log n) средно

B-Tree

За дискови операции

  • Оптимизирано за I/O
  • Минимални disk seeks
  • Подходящо за бази данни

20 minТРУДНО

Задача 27: Debugging на бъгната имплементация на двоично търсене

Отстраняване на Грешки: Отстранете и поправете тази бъгната имплементация на двоично търсене:

int binarySearch(const vector<int>& arr, int target) {
int low = 0;
int high = arr.size();
while (low < high) {
int mid = (low + high) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) low = mid;
else high = mid;
}
return -1;
}

Идентифицирайте всички грешки и обяснете защо всяка корекция е необходима.

Грешки и корекции:

1. Грешка: int high = arr.size();

  • Проблем: high трябва да е последният валиден индекс
  • Корекция: int high = arr.size() - 1;

2. Грешка: while (low < high)

  • Проблем: Пропуска случая когато low == high
  • Корекция: while (low <= high)

3. Грешка: low = mid;

  • Проблем: Безкраен цикъл! Не напредваме
  • Корекция: low = mid + 1;

4. Грешка: high = mid;

  • Проблем: Безкраен цикъл!
  • Корекция: high = mid - 1;

5. Потенциална грешка: (low + high) / 2

  • Проблем: Integer overflow за големи стойности
  • Корекция: low + (high - low) / 2

Коректна версия:

int binarySearch(const vector<int>& arr, int target) {
int low = 0;
int high = arr.size() - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) low = mid + 1;
else high = mid - 1;
}
return -1;
}

20 minТРУДНО

Задача 28: Имплементация на operator= със strong exception guarantee

Exception Safety: Имплементирайте operator= (оператор за присвояване) за класа CustomArray с пълна безопасност при изключения (strong exception guarantee). Гарантирайте, че ако заделянето на памет се провали по време на присвояването, оригиналният обект остава непроменен.

template <typename T>
CustomArray<T>& CustomArray<T>::operator=(const CustomArray& other) {
// Проверка за самоприсвояване
if (this == &other) {
return *this;
}

// Copy-and-swap idiom за exception safety
// Заделяме НОВА памет преди да променим текущия обект
T* newData = new T[other.capacity]; // Може да хвърли bad_alloc

// Ако new успее, копираме данните
for (size_t i = 0; i < other.capacity; ++i) {
newData[i] = other.data[i];
}

// Сега сме сигурни, че всичко е наред
// Освобождаваме старата памет
delete[] data;

// Актуализираме this
data = newData;
capacity = other.capacity;

return *this;
}

// Алтернативно - copy-and-swap idiom:
template <typename T>
CustomArray<T>& CustomArray<T>::operator=(CustomArray other) { // Copy по стойност
std::swap(data, other.data);
std::swap(capacity, other.capacity);
return *this; // other се унищожава и освобождава старата памет
}

Strong exception guarantee: Ако new хвърли изключение, оригиналният обект остава непроменен.


20 minТРУДНО

Задача 29: Проектиране на клас SortedArray с автоматично поддържане на сортираност

Дизайн на Клас: Проектирайте C++ клас SortedArray, който поддържа елементите в сортиран ред автоматично. Имплементирайте методи за вмъкване, изтриване и търсене. Анализирайте времевата сложност на всяка операция и обяснете компромисите в сравнение с несортиран масив.

template <typename T>
class SortedArray {
private:
std::vector<T> data;

public:
// Вмъкване - O(n)
void insert(const T& value) {
// Намираме позиция за вмъкване - O(log n)
auto pos = std::lower_bound(data.begin(), data.end(), value);
data.insert(pos, value); // Вмъкване - O(n)
}

// Търсене - O(log n)
bool search(const T& value) const {
return std::binary_search(data.begin(), data.end(), value);
}

// Намиране на индекс - O(log n)
int find(const T& value) const {
auto it = std::lower_bound(data.begin(), data.end(), value);
if (it != data.end() && *it == value) {
return std::distance(data.begin(), it);
}
return -1;
}

// Изтриване - O(n)
bool remove(const T& value) {
auto it = std::lower_bound(data.begin(), data.end(), value);
if (it != data.end() && *it == value) {
data.erase(it); // O(n) - мести елементи
return true;
}
return false;
}

T get(size_t index) const { return data[index]; }
size_t size() const { return data.size(); }
};

Компромиси спрямо несортиран масив:

ОперацияSortedArrayНесортиран
ТърсенеO(log n) ✅O(n)
ВмъкванеO(n) ⚠️O(1) в края
ИзтриванеO(n) ⚠️O(n)
ДостъпO(1) ✅O(1) ✅

20 minТРУДНО

Задача 30: Сравнителен анализ: масиви срещу други структури от данни

Есе: Напишете цялостно сравнение (поне 300 думи), обсъждащо кога да използвате масиви спрямо други структури от данни (свързани списъци, динамични масиви като std::vector, hash таблици) за различни приложения от реалния свят. Включете конкретни примери като: системи в реално време, индексиране на бази данни, текстови редактори и социални медии фийдове. Обмислете фактори като шаблони на достъп, честота на модификация, ограничения на паметта и изисквания за производителност.

Сравнение на Структури от Данни за Реални Приложения

1. Системи в Реално Време

В критични за времето системи като управление на самолети, автомобили или медицинско оборудване, статичните масиви са предпочитани. Тяхното константно време за достъп O(1) и предсказуемо използване на памет са критични. Динамичното заделяне (new/malloc) е често забранено поради непредсказуемото време за изпълнение и риск от фрагментация. std::vector може да предизвика неочаквани забавяния при преоразмеряване.

2. Индексиране на Бази Данни

За индексиране се използват специализирани структури като B-Tree и B+ Tree вместо прости масиви. Причините са: (1) Базите данни са динамични - чести INSERT/DELETE операции; (2) Масивите изискват O(n) за вмъкване/изтриване; (3) B-Trees поддържат баланс автоматично с O(log n) за всички операции; (4) Оптимизирани за дисков I/O с големи възли. Масиви се използват само за малки, статични lookup таблици.

3. Текстови Редактори

Модерни редактори като VS Code използват сложни структури - не прости масиви. Rope data structure или Gap Buffer са предпочитани защото: (1) Текстът се променя често - INSERT/DELETE операции; (2) Масивите изискват O(n) за вмъкване на символ в средата; (3) Rope позволява O(log n) операции чрез дървовидна структура; (4) Gap buffer оптимизира cursor movement. За малки текстове (< 1000 символа) std::vector е достатъчен.

4. Социални Медии Фийдове

За фийдове (Facebook, Twitter) се използват комбинация от структури: (1) std::vector или std::deque за текущо видимите постове в паметта; (2) Hash tables (std::unordered_map) за бърз lookup на потребители/постове по ID; (3) Приоритетни опашки за ranking алгоритми; (4) Linked lists понякога за historical scrolling. Прости масиви са неподходящи поради: променлив размер, чести актуализации, нужда от бързо търсене по ключ (не по индекс).

Общи Фактори за Решение:

  • Шаблони на достъп: Произволен → масиви; Секвенциален → списъци
  • Честота на модификация: Редки → масиви; Чести → дървета/хеш таблици
  • Ограничения на паметта: Строги → масиви; Гъвкави → динамични структури
  • Изисквания за производителност: O(1) достъп → масиви; O(log n) баланс → дървета

Заключение

Няма "най-добра" структура - изборът зависи от конкретните нужди на приложението. Разбирането на характеристиките и trade-offs на всяка структура е ключово за правилния дизайн.


Обобщение

Поздравления!

Ако сте решили всички упражнения, вие вече имате:

  • Солидно разбиране на свойствата и предимствата на масивите
  • Практически умения за обектно-ориентирана имплементация в C++
  • Дълбоко познаване на двоичното търсене и неговата сложност
  • Критично мислене за избора на подходящи структури от данни

Продължавайте да практикувате и да изследвате по-сложни структури от данни!

ℹ️Следващи стъпки
  • Практикувайте имплементирането на CustomArray от нулата
  • Експериментирайте с различни варианти на двоично търсене
  • Изследвайте std::vector и std::array в STL
  • Сравнете производителността на различни структури с benchmarks
  • Четете код от open source проекти за реални примери

Успех с ученето! 🚀