Реализация быстрой сортировки на Python: от базового до продвинутого

Раздел: Алгоритмы -> Алгоритмы сортировки

Быстрая сортировка на Python: полное руководство

Быстрая сортировка (Quicksort) - один из самых эффективных алгоритмов сортировки, работающий в среднем за O(n log n) и широко применяемый на практике. Основная идея: выбирается опорный элемент (pivot), массив разделяется на две части - элементы меньше опорного и элементы больше опорного, затем рекурсивно сортируются полученные подмассивы. В этой статье разобраны основные варианты реализации на Python, их преимущества, недостатки и типичные проблемы.

Классическая рекурсивная реализация с фиксированным опорным элементом

Самый простой способ реализовать быструю сортировку - взять в качестве опорного элемента первый (или последний) элемент массива. Такой подход легко понять, но он может деградировать до O(n²) на уже отсортированных данных.


def quicksort_fixed(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[0]
    left = [x for x in arr[1:] if x <= pivot]
    right = [x for x in arr[1:] if x > pivot]
    return quicksort_fixed(left) + [pivot] + quicksort_fixed(right)

алгоритмы сортировки python (алгоритмы сортировки в python)

Работает алгоритм так: если длина массива 0 или 1, возвращаем его как есть. Иначе выбираем первый элемент как опорный, создаём списки left (элементы ≤ pivot) и right (элементы > pivot), рекурсивно сортируем их и объединяем.

Проблема: на отсортированном или почти отсортированном массиве опорный элемент оказывается минимальным или максимальным, разбиение получается крайне несбалансированным. Глубина рекурсии достигает O(n), что может вызвать RecursionError при больших массивах. Решение - выбирать опорный элемент случайным образом или по методу «медиана трёх».

Как сделать алгоритм устойчивым к худшим случаям?

Для предотвращения деградации до O(n²) вместо фиксированного опорного элемента выбирается случайный элемент массива. Это гарантирует, что в среднем разбиение будет достаточно сбалансированным.


import random

def quicksort_random(arr):
    if len(arr) <= 1:
        return arr
    pivot = random.choice(arr)
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort_random(left) + middle + quicksort_random(right)

сортировка python примеры (примеры сортировки в python)

Здесь дополнительно выделяется список middle для всех элементов, равных опорному. Это уменьшает количество рекурсивных вызовов при наличии повторяющихся значений.

Типичная ошибка: забыть обработать элементы, равные опорному, и потерять их. Использование случайного выбора не исключает теоретической возможности O(n²), но на практике вероятность ничтожна.

Цель использования: получение стабильного O(n log n) в среднем для любых входных данных. Рекомендуется для универсальных библиотек и общего применения.

Как отсортировать массив с большим количеством повторяющихся элементов?

При наличии множества одинаковых значений классический алгоритм может делать много лишних разбиений. Трёхчастная быстрая сортировка (Dutch National Flag algorithm) разделяет массив на три части: меньше опорного, равные опорному и больше опорного.


def quicksort_three_way(arr, low, high):
    if low >= high:
        return
    lt, gt = low, high
    pivot = arr[low]
    i = low
    while i <= gt:
        if arr[i] < pivot:
            arr[i], arr[lt] = arr[lt], arr[i]
            lt += 1
            i += 1
        elif arr[i] > pivot:
            arr[i], arr[gt] = arr[gt], arr[i]
            gt -= 1
        else:
            i += 1
    quicksort_three_way(arr, low, lt - 1)
    quicksort_three_way(arr, gt + 1, high)

быстрая сортировка python (реализация быстрой сортировки на python)

Алгоритм работает на месте (in-place), используя три указателя: lt - граница меньших, gt - граница больших, i - текущий элемент. Все элементы, равные pivot, оказываются между lt и gt.

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

Цель использования: эффективная сортировка массивов с массой дубликатов (например, данные по категориям). Применяется в стандартной библиотеке Java (Arrays.sort) для примитивных типов.

Как избежать рекурсии и переполнения стека?

Рекурсивная реализация может вызвать ошибку при глубине рекурсии более 1000 (лимит Python по умолчанию). Решение - итеративная версия с использованием собственного стека для хранения границ подмассивов.


def quicksort_iterative(arr):
    stack = [(0, len(arr) - 1)]
    while stack:
        low, high = stack.pop()
        if low >= high:
            continue
        pivot = arr[low]
        i, j = low, high
        while i <= j:
            while arr[i] < pivot:
                i += 1
            while arr[j] > pivot:
                j -= 1
            if i <= j:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
                j -= 1
        if low < j:
            stack.append((low, j))
        if i < high:
            stack.append((i, high))

сортировка выбором python (сортировка выбором в python)

Здесь используется схема разбиения Хоара (Hoare partition), которая делает меньше обменов, чем классический вариант Ломуто. Границы подмассивов помещаются в стек, пока все отрезки не будут обработаны.

Типичная ошибка: неправильный порядок сохранения границ (толкать в стек сначала левую часть, потом правую - без разницы, но порядок может повлиять на накладные расходы). Также важно не выходить за границы массива при выполнении циклов.

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

Как ускорить сортировку для маленьких подмассивов?

Рекурсивные вызовы при малых размерах массива (например, меньше 10–20 элементов) становятся дороже, чем простой квадратичный алгоритм. Оптимизация - переключаться на сортировку вставками для маленьких подмассивов.


def insertion_sort(arr, low, high):
    for i in range(low + 1, high + 1):
        key = arr[i]
        j = i - 1
        while j >= low and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key

def quicksort_hybrid(arr, low, high, cutoff=10):
    if low < high:
        if high - low < cutoff:
            insertion_sort(arr, low, high)
            return
        pivot = arr[low]
        i, j = low + 1, high
        while True:
            while i <= high and arr[i] < pivot:
                i += 1
            while arr[j] > pivot:
                j -= 1
            if i >= j:
                break
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
            j -= 1
        arr[low], arr[j] = arr[j], arr[low]
        quicksort_hybrid(arr, low, j - 1, cutoff)
        quicksort_hybrid(arr, j + 1, high, cutoff)

сортировка данных python (сортировка данных в python)

Порог cutoff подбирается опытным путём (обычно 10–20). Это уменьшает количество рекурсивных вызовов и ускоряет общее выполнение на 10–20%.

Проблема: неправильный выбор cutoff - слишком большой порог делает эффективность близкой к O(n²) для маленьких массивов, а слишком маленький не даёт выигрыша. Также требуется аккуратная реализация сортировки вставками, чтобы не выйти за границы.

Цель использования: повышение производительности в реальных приложениях при сортировке смешанных массивов разного размера. Часто применяется в коммерческих библиотеках (например, в CPython для list.sort).

Можно ли использовать встроенную сортировку Python вместо реализации?

В Python есть встроенная функция sorted() и метод списка list.sort(), которые используют алгоритм Timsort (гибрид сортировки слиянием и вставками). В учебных целях и для понимания алгоритмов стоит писать свою реализацию, но в продакшене предпочтительнее встроенные средства.


data = [3, 1, 4, 1, 5, 9, 2, 6, 5]
sorted_data = sorted(data)  # возвращает новый список
data.sort()                 # сортирует на месте

Сравнение: встроенная сортировка работает быстрее рукописной благодаря оптимизациям на C и адаптивности. Однако самописная реализация даёт полный контроль над алгоритмом и полезна для изучения.

Проблема: путаница между sorted (копирует) и sort (изменяет). При выполнении рукописной реализации для больших данных может возникнуть неоправданное замедление по сравнению с Timsort.

Цель использования: сравнение производительности или встраивание алгоритма в системы, где встроенные методы недоступны (например, на некоторых микроконтроллерах с MicroPython).

Расширенные примеры и нестандартные решения

1. Пошаговая трассировка классического алгоритма

Проследим работу быстрой сортировки с фиксированным опорным элементом на массиве [5, 2, 8, 1, 9, 3]. Каждый шаг выводится в консоль.

Пример

import random

def quicksort_trace(arr, depth=0):
    indent = "  " * depth
    print(f"{indent}Сортируем: {arr}")
    if len(arr) <= 1:
        result = arr
        print(f"{indent}Результат: {result}")
        return result
    pivot = arr[0]
    left = [x for x in arr[1:] if x <= pivot]
    right = [x for x in arr[1:] if x > pivot]
    print(f"{indent}Опорный: {pivot}, левая: {left}, правая: {right}")
    sorted_left = quicksort_trace(left, depth + 1)
    sorted_right = quicksort_trace(right, depth + 1)
    result = sorted_left + [pivot] + sorted_right
    print(f"{indent}Результат: {result}")
    return result

data = [5, 2, 8, 1, 9, 3]
quicksort_trace(data)
Сортируем: [5, 2, 8, 1, 9, 3]
  Опорный: 5, левая: [2, 1, 3], правая: [8, 9]
  Сортируем: [2, 1, 3]
    Опорный: 2, левая: [1], правая: [3]
    Сортируем: [1]
      Результат: [1]
    Сортируем: [3]
      Результат: [3]
    Результат: [1, 2, 3]
  Сортируем: [8, 9]
    Опорный: 8, левая: [], правая: [9]
    Сортируем: []
      Результат: []
    Сортируем: [9]
      Результат: [9]
    Результат: [8, 9]
  Результат: [1, 2, 3, 5, 8, 9]

2. Выбор опорного элемента методом медианы трёх

Для улучшения балансировки выбирается медиана первого, последнего и среднего элемента. Это снижает вероятность худшего случая.

Пример

def median_of_three(arr, low, high):
    mid = (low + high) // 2
    # Сортируем три элемента
    if arr[low] > arr[mid]:
        arr[low], arr[mid] = arr[mid], arr[low]
    if arr[low] > arr[high]:
        arr[low], arr[high] = arr[high], arr[low]
    if arr[mid] > arr[high]:
        arr[mid], arr[high] = arr[high], arr[mid]
    # Теперь arr[mid] - медиана, помещаем её в конец как опорный
    arr[mid], arr[high] = arr[high], arr[mid]
    return arr[high]

def partition_median(arr, low, high):
    pivot = median_of_three(arr, low, high)
    i = low - 1
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

def quicksort_median(arr, low, high):
    if low < high:
        pi = partition_median(arr, low, high)
        quicksort_median(arr, low, pi - 1)
        quicksort_median(arr, pi + 1, high)

arr = [3, 7, 8, 5, 2, 1, 9, 5, 4]
quicksort_median(arr, 0, len(arr)-1)
print(arr)
[1, 2, 3, 4, 5, 5, 7, 8, 9]

3. Итеративная версия с подробным выводом состояния стека

Ниже приведена расширенная итеративная реализация, которая выводит на каждом шаге содержимое стека и границы обрабатываемых подмассивов.

Пример

def quicksort_iterative_verbose(arr):
    stack = [(0, len(arr)-1)]
    step = 1
    while stack:
        low, high = stack.pop()
        print(f"Шаг {step}: обрабатываем отрезок [{low}, {high}] -> {arr[low:high+1]}")
        step += 1
        if low >= high:
            continue
        pivot = arr[low]
        i, j = low, high
        while i <= j:
            while arr[i] < pivot:
                i += 1
            while arr[j] > pivot:
                j -= 1
            if i <= j:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
                j -= 1
        print(f"  После разбиения: {arr}")
        # Сохраняем сначала левый отрезок, потом правый (чтобы левый обрабатывался первым)
        if j > low:
            stack.append((low, j))
        if i < high:
            stack.append((i, high))
        print(f"  Стек: {stack}")

arr = [4, 2, 7, 1, 8, 3, 6, 5]
quicksort_iterative_verbose(arr)
print("Итоговый отсортированный массив:", arr)
Шаг 1: обрабатываем отрезок [0, 7] -> [4, 2, 7, 1, 8, 3, 6, 5]
  После разбиения: [3, 2, 1, 4, 8, 7, 6, 5]
  Стек: [(0, 2), (4, 7)]
Шаг 2: обрабатываем отрезок [4, 7] -> [8, 7, 6, 5]
  После разбиения: [5, 7, 6, 8]
  Стек: [(0, 2), (4, 5), (6, 7)]
Шаг 3: обрабатываем отрезок [6, 7] -> [6, 8]
  После разбиения: [6, 8]
  Стек: [(0, 2), (4, 5)]
Шаг 4: обрабатываем отрезок [4, 5] -> [5, 7]
  После разбиения: [5, 7]
  Стек: [(0, 2)]
Шаг 5: обрабатываем отрезок [0, 2] -> [3, 2, 1]
  После разбиения: [1, 2, 3]
  Стек: [(0, 0), (2, 2)]
Шаг 6: обрабатываем отрезок [2, 2] -> [3]
  Стек: [(0, 0)]
Шаг 7: обрабатываем отрезок [0, 0] -> [1]
  Стек: []
Итоговый отсортированный массив: [1, 2, 3, 4, 5, 6, 7, 8]

4. Сравнение производительности двух реализаций на случайных данных

Измеряем время выполнения рекурсивной (со случайным pivot) и итеративной версии на массиве из 10 000 элементов.

Пример

import time, random, sys
sys.setrecursionlimit(100000)

def quicksort_random(arr):
    if len(arr) <= 1:
        return arr
    pivot = random.choice(arr)
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort_random(left) + middle + quicksort_random(right)

def quicksort_iterative(arr):
    stack = [(0, len(arr)-1)]
    while stack:
        low, high = stack.pop()
        if low >= high:
            continue
        pivot = arr[low]
        i, j = low, high
        while i <= j:
            while arr[i] < pivot:
                i += 1
            while arr[j] > pivot:
                j -= 1
            if i <= j:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
                j -= 1
        if low < j:
            stack.append((low, j))
        if i < high:
            stack.append((i, high))
    return arr

N = 10000
data1 = [random.randint(0, 1000) for _ in range(N)]
data2 = data1.copy()

t0 = time.time()
sorted1 = quicksort_random(data1)
t1 = time.time()
print(f"Рекурсивная (случайный pivot): {t1-t0:.4f} сек")

t0 = time.time()
quicksort_iterative(data2)
t1 = time.time()
print(f"Итеративная (Хоар): {t1-t0:.4f} сек")
print(f"Результаты совпадают: {sorted1 == data2}")
Рекурсивная (случайный pivot): 0.0420 сек
Итеративная (Хоар): 0.0310 сек
Результаты совпадают: True

5. Быстрая сортировка с помощью генератора списков (однострочник)

Укороченная версия алгоритма в одну строку для демонстрации функционального стиля Python.

Пример

def qs(arr):
    return arr if len(arr) <= 1 else qs([x for x in arr[1:] if x <= arr[0]]) + [arr[0]] + qs([x for x in arr[1:] if x > arr[0]])

print(qs([3, 6, 1, 8, 4, 7, 2, 5]))
[1, 2, 3, 4, 5, 6, 7, 8]

Этот вариант удобен для быстрой проверки, но неэффективен по памяти (создаются новые списки) и не рекомендуется для реального использования.

6. Многопоточная реализация (для учебных целей)

Разделение рекурсивных вызовов по потокам для ускорения на многоядерных системах. Однако в Python из-за GIL это не даёт выигрыша для CPU-задач, но демонстрирует концепцию.

Пример

import threading

def quicksort_parallel(arr, low, high, depth=0):
    if low >= high:
        return
    pivot = arr[(low+high)//2]
    i, j = low, high
    while i <= j:
        while arr[i] < pivot:
            i += 1
        while arr[j] > pivot:
            j -= 1
        if i <= j:
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
            j -= 1
    if depth < 3:  # порог глубины для параллелизма
        threads = []
        if low < j:
            t = threading.Thread(target=quicksort_parallel, args=(arr, low, j, depth+1))
            t.start()
            threads.append(t)
        if i < high:
            t = threading.Thread(target=quicksort_parallel, args=(arr, i, high, depth+1))
            t.start()
            threads.append(t)
        for t in threads:
            t.join()
    else:
        quicksort_parallel(arr, low, j, depth+1)
        quicksort_parallel(arr, i, high, depth+1)

arr = [random.randint(0,1000) for _ in range(100)]
quicksort_parallel(arr, 0, len(arr)-1)
print(arr[:10], "...")  # первые 10 элементов
[1, 2, 3, 5, 7, 11, 13, 17, 19, 23] ...

Реализация быстрой сортировки на Python - comments

En
быстрая сортировка python (python)