Урок 9.2 — Пузырьковая сортировка (Bubble Sort)
Изучим самый простой алгоритм сортировки — пузырёк. Разберём визуализацию, реализацию на C# и оптимизацию.
Bubble Sort (сортировка пузырьком) — это самый простой для понимания алгоритм сортировки. Он настолько прост, что обычно его изучают первым. Но за эту простоту приходится платить — он очень медленный на больших массивах. Однако он отлично подходит, чтобы понять, как вообще работают алгоритмы сортировки.
Принцип работы — сравнение соседей
Название «пузырёк» происходит от того, как элементы «всплывают» на свои места, как пузырьки воздуха в воде. Самые большие элементы постепенно смещаются в конец массива.
Алгоритм состоит из одного простого действия, которое повторяется много раз:
Базовый шаг:
Идём по массиву слева направо. Сравниваем два соседних элемента. Если левый больше правого — меняем их местами. Переходим к следующей паре.
После одного полного прохода (называется итерация или проход) самый большой элемент оказывается в конце массива. Потому что он будет «всплывать» — каждый раз, когда мы встречаем меньший элемент, мы меняем их местами, и большой элемент продвигается вправо.
После первого прохода мы знаем, что последний элемент уже на своём месте. Поэтому следующий проход делаем на один элемент короче. И так далее, пока весь массив не станет отсортированным.
Пошаговая визуализация
Рассмотрим массив [5, 3, 8, 1, 2]. Отсортируем его по возрастанию Bubble Sort.
Проход 1 (i = 0):
[5, 3, 8, 1, 2] → 5 > 3? Да! Меняем → [3, 5, 8, 1, 2]
[3, 5, 8, 1, 2] → 5 > 8? Нет → [3, 5, 8, 1, 2]
[3, 5, 8, 1, 2] → 8 > 1? Да! Меняем → [3, 5, 1, 8, 2]
[3, 5, 1, 8, 2] → 8 > 2? Да! Меняем → [3, 5, 1, 2, 8] ✓ 8 на месте
Проход 2 (i = 1) — последний элемент уже не трогаем:
[3, 5, 1, 2, 8] → 3 > 5? Нет → [3, 5, 1, 2, 8]
[3, 5, 1, 2, 8] → 5 > 1? Да! Меняем → [3, 1, 5, 2, 8]
[3, 1, 5, 2, 8] → 5 > 2? Да! Меняем → [3, 1, 2, 5, 8] ✓ 5 на месте
Проход 3 (i = 2):
[3, 1, 2, 5, 8] → 3 > 1? Да! Меняем → [1, 3, 2, 5, 8]
[1, 3, 2, 5, 8] → 3 > 2? Да! Меняем → [1, 2, 3, 5, 8] ✓ 3 на месте
Проход 4 (i = 3):
[1, 2, 3, 5, 8] → 1 > 2? Нет → [1, 2, 3, 5, 8] ✓ Отсортировано!
Обрати внимание: после каждого прохода самый большой из оставшихся элементов оказывается в конце. Как пузырёк, который всплывает на поверхность. После n-1 проходов массив гарантированно отсортирован.
Реализация на C#
Напишем базовую реализацию Bubble Sort на C#. Она будет сортировать массив int[] по возрастанию.
static void BubbleSort(int[] arr)
{
int n = arr.Length;
// Внешний цикл — количество проходов
for (int i = 0; i < n - 1; i++)
{
// Внутренний цикл — сравнение соседних элементов
// n - 1 - i: с каждым проходом «всплывших» элементов становится больше
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1]) // если левый больше правого
{
// Меняем местами
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
Разберём код по строкам:
- Внешний цикл
for (int i = 0; i < n - 1; i++)— выполняет n-1 проходов. i — номер прохода (начиная с 0). - Внутренний цикл
for (int j = 0; j < n - 1 - i; j++)— проходит по ещё неотсортированной части.n - 1 - i— потому что последние i элементов уже на своих местах. - Сравнение и обмен — если arr[j] > arr[j+1], меняем их через временную переменную temp.
Полная программа с демонстрацией:
using System;
class Program
{
static void Main()
{
int[] arr = { 64, 34, 25, 12, 22, 11, 90 };
Console.WriteLine("Исходный массив: " + string.Join(", ", arr));
BubbleSort(arr);
Console.WriteLine("Отсортированный: " + string.Join(", ", arr));
}
static void BubbleSort(int[] arr)
{
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
// Отладочный вывод после каждого прохода
Console.WriteLine($"Проход {i + 1}: {string.Join(", ", arr)}");
}
}
}
// Вывод:
// Исходный массив: 64, 34, 25, 12, 22, 11, 90
// Проход 1: 34, 25, 12, 22, 11, 64, 90
// Проход 2: 25, 12, 22, 11, 34, 64, 90
// Проход 3: 12, 22, 11, 25, 34, 64, 90
// Проход 4: 12, 11, 22, 25, 34, 64, 90
// Проход 5: 11, 12, 22, 25, 34, 64, 90
// Проход 6: 11, 12, 22, 25, 34, 64, 90
// Отсортированный: 11, 12, 22, 25, 34, 64, 90
Оптимизация — флаг раннего выхода
Если во время очередного прохода не было ни одной замены — это значит, что массив уже отсортирован. Можно сразу завершить алгоритм, не делая лишних проходов.
Добавим булевый флаг swapped:
static void OptimizedBubbleSort(int[] arr)
{
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
bool swapped = false; // флаг — были ли обмены в этом проходе
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true; // обмен был!
}
}
// Если не было ни одного обмена — массив отсортирован
if (!swapped)
break; // досрочный выход
}
}
Сравни работу двух версий на почти отсортированном массиве:
int[] nearlySorted = { 2, 3, 5, 1, 4 };
// Базовая версия: сделает 4 прохода (всегда)
// Оптимизированная:
// Проход 1: [2, 3, 1, 4, 5] — была 1 замена
// Проход 2: [2, 1, 3, 4, 5] — была 1 замена
// Проход 3: [1, 2, 3, 4, 5] — была 1 замена
// Проход 4: swapped = false → break! // досрочный выход!
Можно добавить ещё одну оптимизацию: запоминать позицию последнего обмена. После этой позиции массив уже отсортирован, и там проверять нечего:
static void DoubleOptimizedBubbleSort(int[] arr)
{
int n = arr.Length;
int lastSwap = n - 1;
while (lastSwap > 0)
{
int currentLast = 0; // последняя позиция, где был обмен
for (int j = 0; j < lastSwap; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
currentLast = j; // запомнили позицию обмена
}
}
lastSwap = currentLast; // дальше этой позиции ничего не проверяем
}
}
Такой подход автоматически сокращает диапазон проверки на каждом проходе, причём иногда — больше чем на 1 элемент.
Сложность алгоритма
Временная сложность
- Худший случай O(n²) — массив отсортирован в обратном порядке. Каждый элемент нужно двигать через весь массив.
- Средний случай O(n²) — случайный порядок. Примерно n²/2 сравнений и n²/2 обменов.
- Лучший случай O(n) — массив уже отсортирован (с оптимизацией). Один проход без обменов.
Пространственная сложность
O(1) — сортировка in-place. Мы используем только одну временную переменную temp, не считая счётчиков циклов. Дополнительная память не зависит от размера массива.
💡 Сравни: для n = 50 000, Bubble Sort сделает ~2.5 миллиарда операций (2.5 × 10⁹). Быстрая сортировка — ~500 000 (5 × 10⁵). Разница в 5000 раз. На практике Bubble Sort редко используется — в основном в учебных целях.
Плюсы и минусы Bubble Sort
✅ Плюсы
- Очень простой для понимания
- Простая реализация (10 строк кода)
- Стабильная сортировка (порядок равных сохраняется)
- In-place (O(1) дополнительной памяти)
- Быстрый на почти отсортированных данных (O(n) с оптимизацией)
- Легко отлаживать и модифицировать
❌ Минусы
- Очень медленный на больших данных (O(n²))
- Всегда делает n-1 проходов (в базовой версии)
- Количество обменов = количеству сравнений (в худшем случае)
- Проигрывает Insertion Sort в аналогичных условиях
- Никогда не используется в реальных проектах
- Не масштабируется — для 100 000 элементов уже неприменим
Bubble Sort — это отличный учебный инструмент, но в реальном коде используй встроенный Introsort или Merge Sort.
Обмен через кортежи (C# 7+)
Вместо временной переменной для обмена можно использовать деконструкцию кортежа. Это выглядит чище и короче:
// Старый способ с temp
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
// Новый способ с кортежем (C# 7+)
(arr[j], arr[j + 1]) = (arr[j + 1], arr[j]);
Компилятор C# сам создаст временную переменную. Код становится компактнее без потери производительности. Используй этот способ в современных проектах.
Мини-тест
Проверь понимание Bubble Sort.
Вопрос 1
Сколько проходов сделает базовая Bubble Sort для массива из 10 элементов?
A) 10 B) 9 C) 100
Вопрос 2
Что делает оптимизация Bubble Sort с флагом swapped?
A) Уменьшает количество проходов, если массив уже отсортирован B) Ускоряет обмен элементов C) Меняет порядок сравнения
Вопрос 3
Какая временная сложность Bubble Sort в худшем случае?
A) O(n) B) O(n log n) C) O(n²)
Вопрос 4
Какой элемент оказывается на своём месте после первого прохода Bubble Sort?
A) Самый маленький B) Самый большой C) Средний
Вопрос 5
Является ли Bubble Sort стабильной сортировкой?
A) Да B) Нет C) Зависит от реализации
Задача
Задание: Сортировка строк по длине
Напиши программу, которая:
- Создаёт массив строк:
"яблоко", "груша", "слива", "арбуз", "дыня", "вишня" - Сортирует его по длине строки (от короткой к длинной) с помощью Bubble Sort
- При одинаковой длине — сохраняет алфавитный порядок (чтобы сделать сортировку стабильной)
- Выводит массив после каждого прохода (как в примере выше)
- Использует оптимизированную версию с флагом
Дополнительно: реализуй сортировку по убыванию длины (три слова разной длины, 3-4-5).
Подсказка: длина строки — s.Length. Для сравнения: arr[j].Length > arr[j + 1].Length.
Что важно запомнить
Тест: 9.2: Пузырьковая сортировка (Bubble Sort)
3 вопроса