$ sudo teach IT
Модуль 9 · Сортировка

Урок 9.1 — Алгоритмы сортировки

Разберёмся, что такое алгоритмы сортировки, познакомимся с Big O нотацией и узнаем, как работает встроенная сортировка C#.

Представь, что у тебя есть колода карт, и ты хочешь разложить их по порядку: от двойки до туза. Если карт всего 5 — ты сделаешь это за пару секунд. А если карт 1 000 000? Вот здесь и приходят на помощь алгоритмы сортировки — чёткие инструкции, как упорядочить данные максимально эффективно.

🧮

Что такое алгоритм сортировки

Сортировка — это процесс упорядочивания элементов по определённому правилу: по возрастанию, по убыванию, по алфавиту, по дате и так далее. Без сортировки не обходится ни одна программа:

  • Магазин: товары отсортированы по цене или рейтингу
  • Соцсеть: посты отсортированы по дате публикации
  • Поисковик: результаты отсортированы по релевантности
  • Банк: транзакции отсортированы по дате и сумме
  • Игры: таблица лидеров отсортирована по очкам

Алгоритм сортировки — это пошаговая инструкция, которая гарантированно упорядочивает массив. Алгоритмов десятки, и каждый имеет свои сильные и слабые стороны.

Основные критерии, по которым сравнивают алгоритмы:

  1. Скорость работы — сколько операций нужно выполнить
  2. Потребление памяти — нужна ли дополнительная память
  3. Стабильность — сохраняется ли порядок равных элементов
  4. In-place vs not-in-place — сортируется ли массив на месте

Чтобы оценивать скорость алгоритмов, компьютерные учёные придумали Big O нотацию. Давай разберёмся, как она работает.

📈

Нотация Big O

Big O (О-большое) — это математическая нотация, которая описывает, как растёт время выполнения алгоритма при увеличении количества входных данных. Она показывает асимптотическую сложность — то есть поведение на больших объёмах данных.

Представь, что n — это количество элементов. Big O отвечает на вопрос: «Если я увеличу n в 10 раз, во сколько раз медленнее станет алгоритм?»

Основные классы сложности (от лучшего к худшему):

O(1) — Константная сложность

Время не зависит от размера данных. Всегда выполняется за одно и то же время, хоть для 10 элементов, хоть для 10 миллионов.

Примеры: array[0] — доступ к элементу по индексу; dict[key] — доступ к значению словаря по ключу.

O(n) — Линейная сложность

Время растёт пропорционально количеству элементов. В 10 раз больше данных → в 10 раз медленнее.

Примеры: проход по массиву циклом for; поиск элемента в неотсортированном массиве; нахождение максимума/минимума.

O(n log n) — Линейно-логарифмическая

Лучшее, чего можно достичь для сортировки сравнением. Быстрая сортировка, сортировка слиянием, Introsort.

В 10 раз больше данных → примерно в 17 раз медленнее (n log n растёт чуть быстрее n).

O(n²) — Квадратичная сложность

Время растёт квадратично. В 10 раз больше данных → в 100 раз медленнее. Катастрофа для больших массивов.

Примеры: пузырьковая сортировка, вложенные циклы. ⚠️ Избегай для > 1000 элементов.

Визуализация роста сложности (n = 1 000 000):

Таблица · Сравнение сложности
Сложность   n = 10     n = 100    n = 1000    n = 1 000 000
─────────────────────────────────────────────────────────────────
O(1)         1 оп      1 оп       1 оп        1 оп
O(log n)     4 оп      7 оп       10 оп       20 оп
O(n)         10 оп     100 оп     1000 оп     1 000 000 оп
O(n log n)   33 оп     664 оп     9966 оп     19 931 569 оп
O(n²)        100 оп    10 000 оп  1 000 000 оп ≈ 1 000 000 000 000 оп

💡 Обрати внимание: для n = 1 000 000 разница между O(n log n) и O(n²) — это ~20 млн операций против ~1 триллиона. Быстрая сортировка выполнится за секунды, пузырёк — за часы (или дни).

Big O нотация отбрасывает константы и младшие слагаемые. Например, 5n² + 3n + 10 — это всё равно O(n²). Нас интересует только самый быстрорастущий член.

⏱️

Сравнение алгоритмов по времени и памяти

У каждого алгоритма сортировки есть две важные характеристики: временная сложность (сколько операций) и пространственная сложность (сколько дополнительной памяти).

Алгоритмы можно разделить на три категории:

  • Квадратичные O(n²) — простые, но медленные: пузырёк, вставка, выбор
  • Линейно-логарифмические O(n log n) — быстрые: быстрая сортировка, слияние, пирамидальная
  • Линейные O(n) — особые случаи: сортировка подсчётом (для чисел с ограниченным диапазоном)

Сводная таблица популярных алгоритмов:

Алгоритм Лучший случай Средний Худший случай Память Стабильный In-place
Bubble Sort O(n) O(n²) O(n²) O(1) ✅ Да ✅ Да
Quick Sort O(n log n) O(n log n) O(n²) O(log n) ❌ Нет ✅ Да
Merge Sort O(n log n) O(n log n) O(n log n) O(n) ✅ Да ❌ Нет
Selection Sort O(n²) O(n²) O(n²) O(1) ❌ Нет ✅ Да
Insertion Sort O(n) O(n²) O(n²) O(1) ✅ Да ✅ Да
Heap Sort O(n log n) O(n log n) O(n log n) O(1) ❌ Нет ✅ Да

💡 Нет «идеального» алгоритма. Быстрая сортировка быстра в среднем, но деградирует до O(n²). Сортировка слиянием стабильна и гарантирует O(n log n), но требует O(n) памяти. Выбор зависит от задачи.

⚖️

Стабильная vs нестабильная сортировка

Стабильная сортировка сохраняет относительный порядок элементов с одинаковыми ключами. Звучит сложно, но на примере всё просто.

Представь, что у нас есть список студентов, отсортированный по имени, и мы хотим отсортировать его по возрасту:

Пример · Стабильная сортировка
Исходный список (отсортирован по имени):
Анна, 25
Борис, 22
Вика, 25
Глеб, 22

После стабильной сортировки по возрасту:
Борис, 22    // первым из 22-летних
Глеб, 22     // вторым из 22-летних (сохранился порядок из исходного)
Анна, 25     // первой из 25-летних
Вика, 25     // второй из 25-летних (сохранился порядок)

После нестабильной сортировки по возрасту:
Глеб, 22     // или Борис — порядок не гарантирован
Борис, 22
Вика, 25     // или Анна — порядок не гарантирован
Анна, 25

Когда важна стабильность? Когда ты сортируешь данные по нескольким полям последовательно. Например, сначала по дате, потом по статусу. Со стабильной сортировкой вторая сортировка не «сломает» порядок первой.

✅ Стабильные

  • Bubble Sort
  • Merge Sort
  • Insertion Sort
  • C# Array.Sort() для малых массивов

❌ Нестабильные

  • Quick Sort (типичная реализация)
  • Selection Sort
  • Heap Sort
  • Introsort (встроенная в C#)

Важно: встроенная сортировка C# (Introsort) нестабильна. Если нужна стабильная сортировка — используй LINQ OrderBy() (она стабильна) или Merge Sort.

📦

In-place vs Not-in-place сортировка

In-place (сортировка на месте) — алгоритм использует только O(1) или O(log n) дополнительной памяти. Массив сортируется прямо в той же области памяти.

Not-in-place (с внешней памятью) — алгоритм создаёт новые массивы для промежуточных результатов. Требует O(n) дополнительной памяти.

✅ In-place

  • Bubble Sort
  • Quick Sort
  • Selection Sort
  • Insertion Sort
  • Heap Sort

Плюс: экономия памяти. Минус: исходные данные теряются (перезаписываются).

❌ Not-in-place

  • Merge Sort (требует O(n) памяти)
  • Сортировка подсчётом
  • LINQ OrderBy() (создаёт копию)

Плюс: исходные данные сохраняются нетронутыми. Минус: лишняя память.

Для большинства практических задач C# использует Introsort — гибридный алгоритм, который работает in-place. Но если тебе нужно сохранить исходный порядок, используй OrderBy(), который создаёт копию.

🔄

Встроенная сортировка C# — Introsort

В C# для сортировки не нужно писать свой алгоритм (если, конечно, это не учебная задача). В стандартной библиотеке есть две основные точки входа:

C# · Встроенная сортировка
int[] arr = { 5, 2, 8, 1, 9 };

// Array.Sort() — in-place сортировка массива (Introsort)
Array.Sort(arr);
Console.WriteLine(string.Join(", ", arr));  // 1, 2, 5, 8, 9

// List.Sort() — in-place сортировка списка (Introsort)
var list = new List<int> { 5, 2, 8, 1, 9 };
list.Sort();
Console.WriteLine(string.Join(", ", list)); // 1, 2, 5, 8, 9

// LINQ OrderBy() — not-in-place, возвращает новую коллекцию
var sorted = arr.OrderBy(x => x).ToArray();
Console.WriteLine(string.Join(", ", sorted)); // 1, 2, 5, 8, 9
// Исходный arr при этом не меняется!

Introsort (Introspective Sort) — это гибридный алгоритм, который используется в Array.Sort() и List.Sort(). Он был разработан Дэвидом Массером в 1997 году и объединяет три алгоритма:

  1. Quick Sort — основной алгоритм. Работает в среднем за O(n log n).
  2. Heap Sort — подключается, если Quick Sort начинает деградировать (глубина рекурсии слишком большая). Гарантирует O(n log n) в худшем случае.
  3. Insertion Sort — для маленьких подмассивов (< 16 элементов). Быстрее всех на малых данных.

Благодаря этой комбинации Introsort гарантирует O(n log n) в худшем случае, но при этом быстр в среднем — как Quick Sort.

💡 Introsort — нестабильная сортировка. Если нужна стабильность (порядок равных элементов сохраняется), используй OrderBy() из LINQ. Она реализована через Merge Sort (стабильная, O(n log n)).

✅

Когда встроенной сортировки достаточно

В 99% случаев тебе не нужно писать свою сортировку. Встроенный Introsort (через Array.Sort() или List.Sort()) справляется отлично. Вот когда его достаточно:

  1. Обычные данные — числа, строки, даты, простые объекты
  2. Любой размер — от 10 до 10 000 000 элементов (Introsort оптимизирован для всех размеров)
  3. Не нужна стабильность — если порядок равных элементов не важен
  4. Сортировка по нескольким полям — используй LINQ OrderBy().ThenBy()
  5. Стандартные типы — int, double, string, DateTime уже умеют сравниваться

Когда нужно писать свою сортировку:

  • Учебные задачи — чтобы понять, как работают алгоритмы
  • Особые структуры данных — например, частично отсортированные массивы
  • Ограничения по памяти — если доступно меньше O(log n) дополнительной памяти
  • Специфические требования — сортировка на GPU, внешняя сортировка для файлов

💡 Главный совет: используй Array.Sort() для массивов и list.Sort() для списков. Если нужна сортировка без изменения оригинала или сортировка по полю объекта — используй OrderBy(). И только если ты учишься или у тебя супер-специфическая задача — пиши свою сортировку.

🧩

Сортировка объектов и кастомное сравнение

Часто нужно сортировать не числа, а объекты. Например, список пользователей по возрасту или товаров по цене. Для этого используется либо LINQ, либо интерфейс IComparable<T>:

C# · Сортировка объектов
class Product
{
    public string Name { get; set; }
    public double Price { get; set; }
    public int Rating { get; set; }
}

var products = new List<Product>
{
    new Product { Name = "Ноутбук",  Price = 120000, Rating = 5 },
    new Product { Name = "Мышь",     Price = 2500,   Rating = 4 },
    new Product { Name = "Клавиатура", Price = 5000, Rating = 5 },
};

// Сортировка по цене (LINQ OrderBy)
var byPrice = products.OrderBy(p => p.Price);
foreach (var p in byPrice)
    Console.WriteLine($"{p.Price} — {p.Name}");
// 2500 — Мышь
// 5000 — Клавиатура
// 120000 — Ноутбук

// Сортировка по рейтингу, а потом по цене (ThenBy)
var byRatingThenPrice = products
    .OrderByDescending(p => p.Rating)
    .ThenBy(p => p.Name);
foreach (var p in byRatingThenPrice)
    Console.WriteLine($"{p.Name}: рейтинг {p.Rating}, цена {p.Price}");

// In-place сортировка с компаратором (лямбда)
products.Sort((a, b) => a.Price.CompareTo(b.Price));
// Теперь products отсортирован по цене

Разберём, как работает компаратор: Sort((a, b) => a.Price.CompareTo(b.Price)).

  • Если a.Price меньше b.Price → CompareTo вернёт < 0 → a будет раньше b
  • Если равны → вернёт 0 → порядок не меняется
  • Если больше → вернёт > 0 → a будет позже b
  • Для сортировки по убыванию: products.Sort((a, b) => b.Price.CompareTo(a.Price))

💡 OrderBy() — это LINQ-метод, который возвращает IEnumerable<T>. Он ленивый (отложенное выполнение). Чтобы получить реальный массив/список, вызови .ToArray() или .ToList().

🔍

Сортировка по убыванию и Reverse

Иногда нужно отсортировать данные в обратном порядке (по убыванию). Вот как это сделать:

C# · Сортировка по убыванию
int[] arr = { 5, 2, 8, 1, 9 };

// Способ 1: отсортировать по возрастанию, затем развернуть
Array.Sort(arr);
Array.Reverse(arr);
Console.WriteLine(string.Join(", ", arr));  // 9, 8, 5, 2, 1

// Способ 2: через LINQ OrderByDescending
var desc = arr.OrderByDescending(x => x).ToArray();
// arr не изменился, desc = { 9, 8, 5, 2, 1 }

// Способ 3: обратный компаратор
Array.Sort(arr, (a, b) => b.CompareTo(a));
// arr отсортирован по убыванию
💡 Array.Reverse() просто переворачивает массив задом наперёд, O(n). Не путай с сортировкой по убыванию — это разные операции.
🧪

Мини-тест

Проверь, как ты усвоил основы алгоритмов сортировки.

Вопрос 1

Какая сложность у алгоритма, который выполняет n² + 3n + 10 операций?

Варианты: A) O(n)   B) O(n²)   C) O(1)

Вопрос 2

Какая сложность у Array.Sort() в C#?

Варианты: A) O(n²)   B) O(n log n)   C) O(n)

Вопрос 3

Что значит, что алгоритм сортировки стабильный?

A) Он всегда работает за O(n log n)   B) Он сохраняет порядок равных элементов   C) Он не использует дополнительную память

Вопрос 4

Что такое in-place сортировка?

A) Сортировка, не требующая интернета   B) Сортировка, которая использует O(1) или O(log n) дополнительной памяти   C) Сортировка, работающая за O(log n)

Вопрос 5

Какой гибридный алгоритм используется в Array.Sort()?

A) Quick Sort   B) Merge Sort   C) Introsort

💡 Ответы: 1 — B, 2 — B, 3 — B, 4 — B, 5 — C
🎮

Задача

Задание: Сортировка массива объектов

Создай массив (или список) из 10 книг. У каждой книги есть название (string), автор (string), год издания (int) и цена (double).

Выполни:

  1. Отсортируй книги по году издания (LINQ OrderBy) и выведи
  2. Отсортируй по цене по убыванию (OrderByDescending) и выведи
  3. Отсортируй по автору, а потом по названию (ThenBy) и выведи
  4. Используй list.Sort() с компаратором для сортировки по году издания
  5. Посчитай среднюю цену книг после 2000 года

Подсказка: используй LINQ — Where(), Average(), OrderBy(), ThenBy().

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

💡 Это задача на практическое применение встроенной сортировки C#. В следующих уроках мы научимся писать алгоритмы сортировки вручную.
📌

Что важно запомнить

1️⃣
Big O — способ оценить скорость алгоритма при росте данных. Основные классы: O(1), O(log n), O(n), O(n log n), O(n²).
2️⃣
Стабильная сортировка сохраняет порядок равных элементов. In-place — не требует много дополнительной памяти.
3️⃣
C# Array.Sort() использует Introsort — гибрид QuickSort, HeapSort и InsertionSort. Гарантирует O(n log n).
4️⃣
OrderBy() из LINQ — стабильная сортировка, не изменяет оригинал. Array.Sort() и list.Sort() — in-place, нестабильные.
5️⃣
O(n²) — плохо для больших массивов. Bubble Sort и другие квадратичные алгоритмы подходят только для обучения или < 1000 элементов.

Тест: 9.1: Алгоритмы сортировки

3 вопроса

Пузырьковая сортировка

Premium