🔍 Алгоритми за Сортиране: Пълно Ръководство
Сортирането е фундаментална операция в информатиката, която подрежда елементите на списък в определен ред (най-често възходящ или низходящ). Изборът на правилния алгоритъм зависи от размера на данните (\(N\)), ограниченията за памет и спецификите на елементите.
1. Прости алгоритми: \(O(N^2)\)
Тези алгоритми са лесни за имплементация, но неефективни за големи масиви (\(N > 2000\)).
1.1. Метод на мехурчето (Bubble Sort)
Многократно обхожда масива, като разменя съседни елементи, ако са в грешен ред. Най-тежкият елемент "изплува" като мехурче накрая.
void bubbleSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
bool swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
swapped = true;
}
}
if (!swapped) break; // Оптимизация
}
}
1.2. Сортиране чрез вмъкване (Insertion Sort)
Изгражда сортирания масив елемент по елемент. Взима следващия и го "вмъква" на правилното място сред вече сортираните. * Предимство: Изключително бърз (\(O(N)\)) за почти сортирани данни. * Употреба: Често се използва като базов случай в по-сложни алгоритми (напр. Timsort, който е в основата на сортирането в Python и Java, използва Insertion Sort за малки парчета).
2. Ефективни алгоритми: \(O(N \log N)\)
Задължителни за състезания, където \(N\) достига \(10^5\) или \(10^6\).
2.1. Сортиране чрез сливане (Merge Sort)
Класически пример за стратегията "Разделяй и владей" (Divide and Conquer). 1. Разделяне: Масивът се дели на две половини. 2. Владеене: Всяка половина се сортира рекурсивно. 3. Сливане: Двете сортирани половини се обединяват (merge) в един сортиран масив.
- Стабилност: Да (запазва реда на равните елементи).
- Памет: Изисква \(O(N)\) допълнителна памет.
void merge(vector<int>& arr, int l, int m, int r) {
vector<int> temp;
int i = l, j = m + 1;
while (i <= m && j <= r) {
if (arr[i] <= arr[j]) temp.push_back(arr[i++]);
else temp.push_back(arr[j++]);
}
while (i <= m) temp.push_back(arr[i++]);
while (j <= r) temp.push_back(arr[j++]);
for (int k = 0; k < temp.size(); k++) arr[l + k] = temp[k];
}
void mergeSort(vector<int>& arr, int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
2.2. Бързо сортиране (Quick Sort)
Също използва "Разделяй и владей", но разделянето става чрез Pivot (опорен елемент). Всички по-малки от пивота отиват вляво, а по-големите – вдясно.
* Бързина: В практиката е по-бърз от Merge Sort, защото работи "in-place" (без допълнителна памет) и има добра кеш-локалност.
* Най-лош случай: \(O(N^2)\), ако пивотът е лошо избран (напр. сортиран масив).
* std::sort в C++ използва хибриден вариант (Introsort: QuickSort + HeapSort), за да гарантира \(O(N \log N)\).
3. Линейни сортировки: \(O(N)\)
Възможни са само ако знаем нещо повече за данните (напр. че са цели числа в малък диапазон).
3.1. Counting Sort (Сортиране чрез броене)
Ако числата са в интервала \([0, K]\):
1. Създаваме масив cnt[K+1] с нули.
2. За всяко число x от входа, увеличаваме cnt[x].
3. Извеждаме индекса i точно cnt[i] пъти.
Сложност: \(O(N + K)\).
3.2. Radix Sort (Поразредно сортиране)
Сортира числата цифра по цифра (обикновено от последната към първата), използвайки стабилен Counting Sort за всяка позиция. Сложност: \(O(D \cdot (N+K))\), където \(D\) е броят цифри.
4. Използване на std::sort в C++
В 99% от случаите в състезания трябва да използвате вградения sort.
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
struct Student {
string name;
int score;
};
// Сравнителна функция (Comparator)
bool compareStudents(const Student& a, const Student& b) {
if (a.score != b.score)
return a.score > b.score; // Низходящо по точки
return a.name < b.name; // Възходящо по име при равни точки
}
int main() {
vector<int> v = {5, 1, 4, 2, 8};
// Стандартно сортиране (възходящо)
sort(v.begin(), v.end());
// Низходящо с ламбда функция
sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
vector<Student> students = {{"Ivan", 90}, {"Ani", 95}, {"Bobi", 90}};
sort(students.begin(), students.end(), compareStudents);
return 0;
}
std::stable_sort
Ако искате да запазите реда на елементите, които са равни според критерия за сравнение, използвайте stable_sort. Той обикновено е базиран на Merge Sort и е малко по-бавен, но понякога е необходим.
5. Практически съвети и често срещани грешки
5.1. Проблеми със сравнителната функция
Една от най-честите грешки при използване на sort е написване на невалидна функция за сравнение. Тя трябва да:
* Бъде транзитивна: Ако \(A < B\) и \(B < C\), то \(A < C\).
* Бъде антисиметрична: Ако \(A < B\), то НЕ \(B < A\).
* Ако \(A\) НЕ е по-малко от \(B\) и \(B\) НЕ е по-малко от \(A\), то са равни.
Грешна функция може да доведе до Segmentation Fault или непредсказуеми резултати.
5.2. Сортиране на структури с operator<
Вместо да подавате функция, можете да дефинирате operator< за вашата структура:
struct Point {
int x, y;
bool operator<(const Point& other) const {
if (x != other.x) return x < other.x;
return y < other.y;
}
};
vector<Point> points = {{3,5}, {1,2}, {3,1}};
sort(points.begin(), points.end()); // Вече работи директно
5.3. Сортиране на част от масива
Можете да сортирате само определен участък:
vector<int> arr = {5, 2, 9, 1, 5, 6};
sort(arr.begin() + 1, arr.begin() + 4); // Сортира индекси [1, 4)
// Резултат: {5, 1, 2, 9, 5, 6}
5.4. Частично сортиране (partial_sort)
Ако ви трябват само първите \(K\) най-малки елемента, partial_sort е по-бърз от пълното сортиране:
vector<int> v = {5, 1, 9, 3, 7, 2};
partial_sort(v.begin(), v.begin() + 3, v.end());
// Първите 3 са сортирани: {1, 2, 3, ...}
6. Задачи за упражнение
- CSES Distinct Numbers: Използвайте сортиране за намиране на уникални елементи.
- Codeforces 339D: Изисква сортиране с персонализирана функция.
- AtCoder ABC128C: Комбинация на сортиране и структури.
🏁 Заключение
Изборът на правилен алгоритъм зависи от контекста:
* \(N \le 2000\): Bubble Sort или Insertion Sort са достатъчни (ако времето позволява).
* \(N \le 10^6\): Използвайте std::sort (Introsort).
* Стабилност е критична: std::stable_sort.
* Специфични ограничения: Counting Sort или Radix Sort.
Запомнете: в 99% от случаите в състезания просто пишете sort(arr.begin(), arr.end()) – не изобретявайте колелото наново!