Пузырьковая сортировка
Пузырьковая сортировка — простейший алгоритм, с которого начинается изучение сортировок. Он медленный, но идеально иллюстрирует принцип: постепенно «выталкивать» большие элементы на нужное место. Разберём пошагово с визуализацией и напишем код.
🫧 Принцип: «пузыри» всплывают вправо
Представьте аквариум с пузырьками разного размера. Большие пузырьки поднимаются быстрее, маленькие — медленнее. Вот именно по этому принципу работает пузырьковая сортировка — большие элементы «всплывают» вправо с каждым проходом.
Алгоритм очень простой. Мы идём по массиву слева направо и сравниваем каждую пару соседних элементов. Если левый больше правого — меняем их местами. Так самый большой элемент за один проход окажется в самом конце. Потом повторяем для оставшейся части.
🔑 Ключевая идея: за каждый полный проход по массиву самый большой из оставшихся элементов занимает своё правильное место в конце. n элементов — нужно не более n проходов.
📊 Пошаговая визуализация: [5, 3, 8, 1, 4]
Разберём пошагово. Проход 1: сравниваем пары соседей и меняем местами, если нужно. После первого прохода максимальный элемент (8) встаёт на своё место.
| Шаг | Сравниваем | Действие | Массив после |
|---|---|---|---|
| Начало | — | — | [5, 3, 8, 1, 4] |
| Проход 1, шаг 1 | 5 и 3 | 5 > 3 → меняем | [3, 5, 8, 1, 4] |
| Проход 1, шаг 2 | 5 и 8 | 5 < 8 → не меняем | [3, 5, 8, 1, 4] |
| Проход 1, шаг 3 | 8 и 1 | 8 > 1 → меняем | [3, 5, 1, 8, 4] |
| Проход 1, шаг 4 | 8 и 4 | 8 > 4 → меняем | [3, 5, 1, 4, 8] |
| Итог прохода 1 | 8 встал на своё место ✅ | [3, 5, 1, 4, 8] | |
| Проход 2 | Сравниваем первые 4 элемента, 5 «всплывает» вправо | [3, 1, 4, 5, 8] | |
| Проход 3 | Сравниваем первые 3 элемента | [1, 3, 4, 5, 8] | |
| Проход 4 | Сравниваем первые 2 элемента | [1, 3, 4, 5, 8] | |
| Готово! ✅ | Массив отсортирован за 4 прохода | [1, 3, 4, 5, 8] | |
💻 Базовая реализация
Код пузырьковой сортировки — один из самых коротких и понятных в алгоритмике. Два вложенных цикла: внешний считает проходы, внутренний идёт по элементам.
function bubbleSort(arr) {
const n = arr.length;
for (let i = 0; i < n; i++) { // внешний: n проходов
for (let j = 0; j < n - i - 1; j++) { // внутренний: каждый раз на 1 короче
if (arr[j] > arr[j + 1]) {
// меняем местами (деструктуризация ES6)
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
console.log(bubbleSort([5, 3, 8, 1, 4]));
// [1, 3, 4, 5, 8]
Почему внутренний цикл идёт до n - i - 1? После каждого прохода i последних элементов уже отсортированы и стоят на своих местах. Нет смысла проверять их снова.
🔄 Замена через деструктуризацию: [a, b] = [b, a] — современный ES6-способ поменять два значения местами без временной переменной. Раньше писали: let tmp = a; a = b; b = tmp;
🚀 Оптимизация: флаг для раннего выхода
Базовая реализация всегда делает n² сравнений — даже если массив уже отсортирован! Это расточительно. Добавим простую оптимизацию: если за целый проход не было ни одной замены — массив уже отсортирован, выходим досрочно.
function bubbleSortOptimized(arr) {
const n = arr.length;
for (let i = 0; i < n; i++) {
let swapped = false; // флаг: были ли замены в этом проходе?
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true; // была замена — отмечаем
}
}
if (!swapped) {
// Если ни одной замены — массив уже отсортирован!
break;
}
}
return arr;
}
// Для уже отсортированного массива — всего один проход!
console.log(bubbleSortOptimized([1, 2, 3, 4, 5]));
// [1, 2, 3, 4, 5] — быстро! O(n) в лучшем случае
Эта оптимизация меняет лучший случай с O(n²) на O(n). Если передать уже отсортированный массив — алгоритм сделает всего один проход и выйдет.
Массив уже отсортирован → один проход → O(n)
Случайный порядок → около n/2 проходов → O(n²)
Массив отсортирован в обратном порядке → n проходов → O(n²)
🔢 Почему сложность именно O(n²)
Посчитаем количество сравнений для массива из n элементов:
- Проход 1: n−1 сравнений
- Проход 2: n−2 сравнений
- Проход 3: n−3 сравнений
- ... и так до последнего прохода: 1 сравнение
Итого: (n−1) + (n−2) + ... + 1 = n(n−1)/2. При больших n это примерно n²/2. В Big O константы отбрасываем — получаем O(n²).
// Для n = 5 элементов:
// Проход 0: 4 сравнения (j от 0 до 3)
// Проход 1: 3 сравнения (j от 0 до 2)
// Проход 2: 2 сравнения (j от 0 до 1)
// Проход 3: 1 сравнение (j = 0)
// Итого: 4 + 3 + 2 + 1 = 10 сравнений = 5*4/2
// Для n = 1000:
// 1000 * 999 / 2 = 499 500 сравнений
// Для n = 1 000 000:
// ~500 000 000 000 сравнений = миллиард!
⚠️ Вывод: пузырьковая сортировка хороша для обучения и понимания принципов, но в реальном коде никогда не используйте её для больших массивов. При 10 000+ элементов — только встроенный .sort() или быстрая/слиянием.
✅ Когда пузырьковая сортировка полезна
Несмотря на все ограничения, есть ситуации, где пузырьковая сортировка — разумный выбор:
Идеально для объяснения концепции сортировки — простой, понятный, легко визуализировать
До 10–20 элементов разница между алгоритмами незначительна. Простота кода важнее
С флагом early-exit алгоритм работает за O(n) — лучше многих конкурентов в этом частном случае
✅ Итоги урока
- Пузырьковая сортировка сравнивает соседние элементы и переставляет их — большие «всплывают» вправо
- За каждый полный проход самый большой из оставшихся элементов встаёт на своё место
- Два вложенных цикла → сложность O(n²) в среднем и худшем случае
- Флаг
swappedпозволяет выйти досрочно — лучший случай становится O(n) - Для массива [5, 3, 8, 1, 4] нужно 4 прохода и 10 сравнений
- На практике не используется для больших данных — только учебные цели и маленькие массивы
В следующем уроке — быстрая сортировка: умный алгоритм «разделяй и властвуй», который работает в O(n log n). ⚡
Пузырьковая сортировка
6 вопросов