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

⏱ 45 мин · 🧩 заданий: 5

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

Простые сортировки O(n²)

def bubble_sort(a):
    a = a[:]
    for end in range(len(a) - 1, 0, -1):
        swapped = False
        for i in range(end):
            if a[i] > a[i + 1]:                    # «всплываем» большой элемент вправо
                a[i], a[i + 1] = a[i + 1], a[i]
                swapped = True
        if not swapped:                            # проход без обменов — уже отсортировано
            break
    return a

def insertion_sort(a):
    a = a[:]
    for i in range(1, len(a)):
        key, j = a[i], i - 1
        while j >= 0 and a[j] > key:               # сдвигаем большие вправо
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = key                             # вставляем на место
    return a

def selection_sort(a):
    a = a[:]
    for i in range(len(a)):
        m = min(range(i, len(a)), key=a.__getitem__)    # ищем минимум в хвосте
        a[i], a[m] = a[m], a[i]
    return a

data = [5, 2, 9, 1, 5, 6]
print(bubble_sort(data), insertion_sort(data), selection_sort(data))
Лучший Средний Худший Устойчивая Когда хороша
Пузырьком O(n) O(n²) O(n²) да только для обучения
Вставками O(n) O(n²) O(n²) да маленькие и почти отсортированные массивы
Выбором O(n²) O(n²) O(n²) нет минимум обменов

Пройдите insertion_sort 👣 Пошагово — видно, как элемент «проезжает» влево на своё место.

Сортировка слиянием — O(n log n)

«Разделяй и властвуй»: разделить пополам, отсортировать половины рекурсивно, слить два отсортированных списка за O(n).

def merge(a, b):
    result, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:              # <= сохраняет устойчивость
            result.append(a[i]); i += 1
        else:
            result.append(b[j]); j += 1
    return result + a[i:] + b[j:]

def merge_sort(a, depth=0):
    if len(a) <= 1:
        return a
    mid = len(a) // 2
    left, right = merge_sort(a[:mid], depth + 1), merge_sort(a[mid:], depth + 1)
    merged = merge(left, right)
    print("  " * depth + f"слияние {left} + {right} → {merged}")
    return merged

merge_sort([38, 27, 43, 3, 9, 82, 10])

Уровней деления — log n, на каждом уровне слияния суммарно O(n) → O(n log n) всегда. Минус — O(n) дополнительной памяти. Идея слияния нужна в задачах: слить k отсортированных списков, посчитать инверсии, внешняя сортировка огромных файлов.

Быстрая сортировка — O(n log n) в среднем

Выбрать опорный элемент, разбить массив: меньшие — влево, большие — вправо, рекурсивно отсортировать части.

import random

def quick_sort_simple(a):
    if len(a) <= 1:
        return a
    pivot = random.choice(a)
    less = [x for x in a if x < pivot]
    equal = [x for x in a if x == pivot]
    greater = [x for x in a if x > pivot]
    return quick_sort_simple(less) + equal + quick_sort_simple(greater)

print(quick_sort_simple([3, 6, 8, 10, 1, 2, 1]))

def partition_lomuto(a, lo, hi):
    pivot = a[hi]
    i = lo                                 # граница «меньших»
    for j in range(lo, hi):
        if a[j] < pivot:
            a[i], a[j] = a[j], a[i]
            i += 1
    a[i], a[hi] = a[hi], a[i]              # опорный встаёт на своё итоговое место
    return i

arr = [7, 2, 1, 8, 6, 3, 5, 4]
p = partition_lomuto(arr, 0, len(arr) - 1)
print(arr, "опорный на позиции", p)
  • Средний случай O(n log n), на месте (O(log n) стека), на практике очень быстрая.
  • Худший случай O(n²) — если опорный всегда минимальный/максимальный (например, первый элемент уже отсортированного массива). Защита — случайный опорный или медиана трёх.
  • Неустойчивая.

Quickselect — k-й элемент за O(n)

Разбиение позволяет найти k-й по величине элемент без полной сортировки: после partition опорный стоит на своём месте, рекурсия идёт только в одну половину.

import random

def quickselect(a, k):
    """k-й наименьший (k от 0), в среднем O(n)."""
    a = a[:]
    lo, hi = 0, len(a) - 1
    while True:
        p = random.randint(lo, hi)
        a[p], a[hi] = a[hi], a[p]
        pivot, i = a[hi], lo
        for j in range(lo, hi):
            if a[j] < pivot:
                a[i], a[j] = a[j], a[i]
                i += 1
        a[i], a[hi] = a[hi], a[i]
        if i == k:
            return a[i]
        if i < k:
            lo = i + 1
        else:
            hi = i - 1

data = [3, 2, 1, 5, 6, 4]
print(quickselect(data, len(data) - 2))     # второй по величине

Сортировки без сравнений — O(n + k)

Любая сортировка сравнениями не может быть быстрее O(n log n) в худшем случае (нужно различить n! перестановок, а каждое сравнение даёт 1 бит). Но если ключи — небольшие целые, можно обойтись без сравнений:

def counting_sort(a, max_value):
    counts = [0] * (max_value + 1)
    for x in a:
        counts[x] += 1
    result = []
    for value, c in enumerate(counts):
        result.extend([value] * c)
    return result

ages = [25, 31, 18, 25, 40, 18, 22]
print(counting_sort(ages, 120))              # O(n + k), k — диапазон значений

def radix_sort(a):
    """Поразрядная сортировка неотрицательных чисел: устойчиво по каждой цифре."""
    exp = 1
    while max(a, default=0) // exp > 0:
        buckets = [[] for _ in range(10)]
        for x in a:
            buckets[(x // exp) % 10].append(x)
        a = [x for b in buckets for x in b]
        exp *= 10
    return a

print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))

Сортировка в Python: Timsort

sorted() и list.sort() используют Timsort (в 3.11+ — его улучшение Powersort): гибрид слияния и вставок, который находит уже отсортированные участки («пробеги») и сливает их.

  • O(n log n) в худшем, O(n) на почти отсортированных данных;
  • устойчивая;
  • list.sort() — на месте, sorted() — новый список.
people = [("Анна", "HR", 150), ("Боря", "IT", 250), ("Вера", "IT", 200), ("Глеб", "HR", 150)]

print(sorted(people, key=lambda p: p[2], reverse=True))
print(sorted(people, key=lambda p: (p[1], -p[2])))        # отдел ↑, зарплата ↓

by_name = sorted(people, key=lambda p: p[0], reverse=True)   # устойчивость: сортируем по
by_dept = sorted(by_name, key=lambda p: p[1])                 # второстепенному, потом по главному
print(by_dept)

words = ["banana", "apple", "Cherry", "date"]
print(sorted(words), sorted(words, key=str.lower), sorted(words, key=len))

Сортировка по убыванию строкового поля

Минус работает только для чисел. Для строк по убыванию внутри составного ключа используйте два прохода устойчивой сортировки (как выше) или functools.cmp_to_key.

Вопрос на собеседовании

«Расскажите о быстрой сортировке». Опорный элемент, разбиение на меньшие и большие, рекурсия; в среднем O(n log n), в худшем O(n²) — при плохом выборе опорного, защищаются случайным выбором; сортирует на месте, неустойчивая. Сравнить со слиянием: всегда O(n log n), устойчивая, но O(n) памяти.

Итог

  • O(n²): пузырьком, выбором, вставками (вставки хороши для маленьких и почти отсортированных).
  • Слиянием: всегда O(n log n), устойчивая, O(n) памяти.
  • Быстрая: O(n log n) в среднем, O(n²) в худшем, на месте; quickselect — k-й элемент за O(n).
  • Без сравнений: подсчётом и поразрядная — O(n + k).
  • В Python — Timsort: устойчивая, O(n) на почти отсортированных; сортировка по нескольким ключам через кортеж.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Сортировка слиянием. Реализуйте merge_sort(nums), возвращающую новый отсортированный список. Без sorted и .sort(). Сначала напишите функцию слияния двух отсортированных списков.

Какие тесты проверяют код (3)
assert merge_sort([5, 2, 9, 1, 5, 6]) == [1, 2, 5, 5, 6, 9] and merge_sort([]) == [] and merge_sort([1]) == [1]
data = [3, 1, 2]; result = merge_sort(data); assert data == [3, 1, 2] and result == [1, 2, 3], "Исходный список не должен меняться"
import random; r = random.Random(1); data = [r.randint(-1000, 1000) for _ in range(20_000)]; assert merge_sort(data) == sorted(data)
Задание 2 Программа на Python

Быстрая сортировка на месте. Реализуйте quick_sort(nums), сортирующую список на месте (функция ничего не возвращает). Используйте разбиение Ломуто и случайный опорный элемент.

Какие тесты проверяют код (4)
a = [5, 2, 9, 1, 5, 6]; assert quick_sort(a) is None and a == [1, 2, 5, 5, 6, 9]
a = []; quick_sort(a); b = [7]; quick_sort(b); assert a == [] and b == [7]
import random; r = random.Random(3); a = [r.randint(0, 10_000) for _ in range(20_000)]; b = sorted(a); quick_sort(a); assert a == b
a = list(range(5_000)); quick_sort(a); assert a == list(range(5_000)), "Уже отсортированный массив — худший случай для фиксированного опорного; выбирайте его случайно"
Задание 3 Программа на Python

Сортировка по нескольким полям. Отсортируйте сотрудников (dict с ключами name, dept, salary): по отделу по алфавиту, внутри отдела — по зарплате по убыванию, при равной зарплате — по имени. Верните список имён.

Какие тесты проверяют код (1)
staff = [
    {"name": "Вера", "dept": "IT", "salary": 200},
    {"name": "Анна", "dept": "HR", "salary": 150},
    {"name": "Борис", "dept": "IT", "salary": 250},
    {"name": "Глеб", "dept": "IT", "salary": 200},
    {"name": "Дина", "dept": "HR", "salary": 180},
]
assert sort_staff(staff) == ["Дина", "Анна", "Борис", "Вера", "Глеб"]

Задание 4 Заполнить таблицу

Свойства сортировок

O(n log n) в худшемустойчиваяна месте (O(1) памяти)
сортировка вставками
слиянием
быстрая (quicksort)
пирамидальная (heapsort)
Timsort (sorted в Python)
Отметьте ячейку — «+», пустая — «−».
Задание 5 Один вариант ответа

Что значит «устойчивая сортировка»?

Войдите, чтобы отмечать пройденные уроки.