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

Урок 9.2 — Пузырьковая сортировка (Bubble Sort)

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

Bubble Sort (сортировка пузырьком) — это самый простой для понимания алгоритм сортировки. Он настолько прост, что обычно его изучают первым. Но за эту простоту приходится платить — он очень медленный на больших массивах. Однако он отлично подходит, чтобы понять, как вообще работают алгоритмы сортировки.

🫧

Принцип работы — сравнение соседей

Название «пузырёк» происходит от того, как элементы «всплывают» на свои места, как пузырьки воздуха в воде. Самые большие элементы постепенно смещаются в конец массива.

Алгоритм состоит из одного простого действия, которое повторяется много раз:

Базовый шаг:

Идём по массиву слева направо. Сравниваем два соседних элемента. Если левый больше правого — меняем их местами. Переходим к следующей паре.

После одного полного прохода (называется итерация или проход) самый большой элемент оказывается в конце массива. Потому что он будет «всплывать» — каждый раз, когда мы встречаем меньший элемент, мы меняем их местами, и большой элемент продвигается вправо.

После первого прохода мы знаем, что последний элемент уже на своём месте. Поэтому следующий проход делаем на один элемент короче. И так далее, пока весь массив не станет отсортированным.

👣

Пошаговая визуализация

Рассмотрим массив [5, 3, 8, 1, 2]. Отсортируем его по возрастанию Bubble Sort.

Проход 1 (i = 0):

Начинаем с пары (0,1)
[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 проходов массив гарантированно отсортирован.

💡 Для массива из 5 элементов нужно максимум 4 прохода (n-1), потому что после того, как 4 элемента на своих местах, последний автоматически тоже на месте.
💻

Реализация на C#

Напишем базовую реализацию Bubble Sort на C#. Она будет сортировать массив int[] по возрастанию.

C# · Bubble Sort (базовая версия)
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.

Полная программа с демонстрацией:

C# · Bubble Sort — полный пример
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
💡 Обрати внимание: на проходе 5 массив уже отсортирован, но алгоритм всё равно делает последний проход. Это неэффективно. Далее мы это исправим.
⚡

Оптимизация — флаг раннего выхода

Если во время очередного прохода не было ни одной замены — это значит, что массив уже отсортирован. Можно сразу завершить алгоритм, не делая лишних проходов.

Добавим булевый флаг swapped:

C# · Bubble Sort (оптимизированный)
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;  // досрочный выход
    }
}

Сравни работу двух версий на почти отсортированном массиве:

C# · Сравнение версий
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!  // досрочный выход!
💡 Для уже отсортированного массива оптимизированная версия сделает всего 1 проход (проверит, что всё на месте, и завершится). Базовая — все n-1 проходов. Разница — O(n) против O(n²).

Можно добавить ещё одну оптимизацию: запоминать позицию последнего обмена. После этой позиции массив уже отсортирован, и там проверять нечего:

C# · Bubble Sort (двойная оптимизация)
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+)

Вместо временной переменной для обмена можно использовать деконструкцию кортежа. Это выглядит чище и короче:

C# · Обмен через кортеж
// Старый способ с 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) Зависит от реализации

💡 Ответы: 1 — B (n-1 проходов), 2 — A (досрочный выход), 3 — C, 4 — B, 5 — A (да, стабильная, так как мы меняем только когда строго больше, а не больше или равно)
🎮

Задача

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

Напиши программу, которая:

  1. Создаёт массив строк: "яблоко", "груша", "слива", "арбуз", "дыня", "вишня"
  2. Сортирует его по длине строки (от короткой к длинной) с помощью Bubble Sort
  3. При одинаковой длине — сохраняет алфавитный порядок (чтобы сделать сортировку стабильной)
  4. Выводит массив после каждого прохода (как в примере выше)
  5. Использует оптимизированную версию с флагом

Дополнительно: реализуй сортировку по убыванию длины (три слова разной длины, 3-4-5).

Подсказка: длина строки — s.Length. Для сравнения: arr[j].Length > arr[j + 1].Length.

💡 Сортировка строк по длине — хорошая практическая задача. Обрати внимание: при одинаковой длине (например, «слива» и «вишня») порядок сохранится, потому что мы меняем только когда Length строго больше.
📌

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

1️⃣
Пузырёк сравнивает соседние элементы и меняет их, если левый больше правого. Самые большие «всплывают» в конец.
2️⃣
Внешний цикл — количество проходов (n-1). Внутренний — сравнение пар (n-1-i).
3️⃣
Оптимизация с флагом — если за проход не было обменов, массив отсортирован, выходим досрочно.
4️⃣
Сложность: O(n²) в среднем и худшем, O(n) в лучшем (с оптимизацией). Память O(1). Стабильная сортировка.
5️⃣
Bubble Sort — учебный алгоритм. В реальных проектах используй Array.Sort() (Introsort).

Тест: 9.2: Пузырьковая сортировка (Bubble Sort)

3 вопроса

Быстрая сортировка

Premium