← Назад к вопросам
Сортировка слиянием
2.0 Middle🔥 131 комментариев
#Python Core#Архитектура и паттерны
Условие
Реализуйте алгоритм сортировки слиянием (Merge Sort).
Это рекурсивный алгоритм "разделяй и властвуй" со сложностью O(n log n).
Пример
merge_sort([38, 27, 43, 3, 9, 82, 10]) → [3, 9, 10, 27, 38, 43, 82]
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Сортировка слиянием (Merge Sort)
Сортировка слиянием — это эффективный рекурсивный алгоритм сортировки, основанный на парадигме «разделяй и властвуй». Алгоритм разделяет массив пополам, рекурсивно сортирует каждую половину, а затем объединяет отсортированные половины в один отсортированный массив.
Решение 1: Классическая реализация
def merge_sort(arr: list) -> list:
"""
Сортировка слиянием.
Args:
arr: Массив для сортировки
Returns:
Отсортированный массив
Временная сложность: O(n log n)
Пространственная сложность: O(n)
"""
if len(arr) <= 1:
return arr
# Разделяем массив пополам
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
# Рекурсивно сортируем обе половины
left_sorted = merge_sort(left)
right_sorted = merge_sort(right)
# Объединяем отсортированные половины
return merge(left_sorted, right_sorted)
def merge(left: list, right: list) -> list:
"""
Объединяет два отсортированных массива в один отсортированный.
"""
result = []
i = j = 0
# Сравниваем элементы из левого и правого массивов
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# Добавляем оставшиеся элементы
result.extend(left[i:])
result.extend(right[j:])
return result
# Тестирование
print(merge_sort([38, 27, 43, 3, 9, 82, 10])) # [3, 9, 10, 27, 38, 43, 82]
print(merge_sort([5, 2, 8, 1, 9])) # [1, 2, 5, 8, 9]
print(merge_sort([1])) # [1]
print(merge_sort([])) # []
Решение 2: Инplace сортировка (минимизация памяти)
def merge_sort_inplace(arr: list, left: int = 0, right: int = None) -> None:
"""
Сортировка слиянием с минимальным использованием дополнительной памяти.
Модифицирует массив на месте.
"""
if right is None:
right = len(arr) - 1
if left < right:
mid = (left + right) // 2
# Рекурсивно сортируем обе половины
merge_sort_inplace(arr, left, mid)
merge_sort_inplace(arr, mid + 1, right)
# Объединяем отсортированные части
merge_inplace(arr, left, mid, right)
def merge_inplace(arr: list, left: int, mid: int, right: int) -> None:
"""
Объединяет два отсортированных подмассива на месте.
"""
# Создаём временные копии
left_half = arr[left:mid + 1]
right_half = arr[mid + 1:right + 1]
i = j = 0
k = left
# Объединяем половины
while i < len(left_half) and j < len(right_half):
if left_half[i] <= right_half[j]:
arr[k] = left_half[i]
i += 1
else:
arr[k] = right_half[j]
j += 1
k += 1
# Копируем оставшиеся элементы
while i < len(left_half):
arr[k] = left_half[i]
i += 1
k += 1
while j < len(right_half):
arr[k] = right_half[j]
j += 1
k += 1
# Тестирование
arr = [38, 27, 43, 3, 9, 82, 10]
merge_sort_inplace(arr)
print(arr) # [3, 9, 10, 27, 38, 43, 82]
Решение 3: С визуализацией процесса
def merge_sort_verbose(arr: list, depth: int = 0) -> list:
"""
Сортировка слиянием с визуализацией процесса.
"""
indent = " " * depth
print(f"{indent}Разделяем: {arr}")
if len(arr) <= 1:
print(f"{indent}Базовый случай: {arr}")
return arr
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
print(f"{indent}Левая половина: {left}")
print(f"{indent}Правая половина: {right}")
left_sorted = merge_sort_verbose(left, depth + 1)
right_sorted = merge_sort_verbose(right, depth + 1)
result = merge(left_sorted, right_sorted)
print(f"{indent}Объединили: {result}")
return result
# Пример
merge_sort_verbose([38, 27, 43, 3])
Решение 4: Сортировка списков объектов
from typing import List, Callable, Any
def merge_sort_by_key(arr: List[Any], key: Callable = None) -> List[Any]:
"""
Сортировка слиянием с кастомной функцией сравнения.
"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
left_sorted = merge_sort_by_key(left, key)
right_sorted = merge_sort_by_key(right, key)
return merge_by_key(left_sorted, right_sorted, key)
def merge_by_key(left: list, right: list, key: Callable = None) -> list:
"""
Объединение с кастомной функцией сравнения.
"""
result = []
i = j = 0
while i < len(left) and j < len(right):
left_val = key(left[i]) if key else left[i]
right_val = key(right[j]) if key else right[j]
if left_val <= right_val:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# Тестирование с объектами
students = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'score': 75},
{'name': 'Charlie', 'score': 95},
]
sorted_students = merge_sort_by_key(students, key=lambda x: x['score'])
for student in sorted_students:
print(f"{student['name']}: {student['score']}")
# Bob: 75
# Alice: 85
# Charlie: 95
Решение 5: Сортировка с подсчётом инверсий
def merge_sort_count_inversions(arr: list) -> tuple:
"""
Сортировка слиянием с подсчётом количества инверсий.
Инверсия — пара индексов (i, j) где i < j, но arr[i] > arr[j].
"""
def merge_and_count(left: list, right: list) -> tuple:
result = []
inversions = 0
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
# Все элементы в left[i:] больше right[j]
inversions += len(left) - i
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result, inversions
def sort_and_count(arr: list) -> tuple:
if len(arr) <= 1:
return arr, 0
mid = len(arr) // 2
left, left_inv = sort_and_count(arr[:mid])
right, right_inv = sort_and_count(arr[mid:])
merged, merge_inv = merge_and_count(left, right)
return merged, left_inv + right_inv + merge_inv
return sort_and_count(arr)
# Тестирование
arr = [1, 3, 5, 2, 4, 6]
sorted_arr, inversions = merge_sort_count_inversions(arr)
print(f"Отсортировано: {sorted_arr}")
print(f"Инверсии: {inversions}") # 3 инверсии: (3,2), (5,2), (5,4)
Полное решение с тестами
def merge_sort(arr: list) -> list:
"""
Сортировка слиянием.
Использует парадигму "разделяй и властвуй":
1. Разделяем массив пополам
2. Рекурсивно сортируем обе половины
3. Объединяем отсортированные половины
Временная сложность: O(n log n) во всех случаях
Пространственная сложность: O(n)
Стабильна: Да (равные элементы сохраняют порядок)
"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left: list, right: list) -> list:
"""Объединяет два отсортированных массива."""
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def test_merge_sort():
# Основные тесты
assert merge_sort([38, 27, 43, 3, 9, 82, 10]) == [3, 9, 10, 27, 38, 43, 82]
assert merge_sort([5, 2, 8, 1, 9]) == [1, 2, 5, 8, 9]
# Граничные случаи
assert merge_sort([]) == []
assert merge_sort([1]) == [1]
assert merge_sort([2, 1]) == [1, 2]
# Уже отсортированный
assert merge_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]
# Обратный порядок
assert merge_sort([5, 4, 3, 2, 1]) == [1, 2, 3, 4, 5]
# Дубликаты
assert merge_sort([3, 1, 2, 1, 3]) == [1, 1, 2, 3, 3]
# Отрицательные числа
assert merge_sort([3, -1, 2, -2, 0]) == [-2, -1, 0, 2, 3]
print("✓ Все тесты пройдены!")
test_merge_sort()
Анализ сложности
| Параметр | Значение |
|---|---|
| Лучший случай | O(n log n) |
| Средний случай | O(n log n) |
| Худший случай | O(n log n) |
| Пространство | O(n) |
| Стабильность | Да |
| Адаптивность | Нет |
Сравнение с другими алгоритмами
| Алгоритм | Лучше | Худше | Память |
|---|---|---|---|
| Quick Sort | O(n log n) | O(n²) | O(log n) |
| Merge Sort | O(n log n) | O(n log n) | O(n) |
| Heap Sort | O(n log n) | O(n log n) | O(1) |
| Bubble Sort | O(n) | O(n²) | O(1) |
Преимущества и недостатки
Преимущества:
- Гарантированная O(n log n) — всегда стабильная производительность
- Стабильна — сохраняет порядок равных элементов
- Параллелизм — можно распараллелить
- Предсказуемо — не зависит от входных данных
Недостатки:
- Требует O(n) дополнительной памяти — неинplace алгоритм
- Медленнее Quick Sort в среднем — на реальных данных
- Не адаптивна — одинаково медленна для отсортированных данных
Практическое применение
- Встроенная сортировка в Python — TimSort основан на Merge Sort
- Сортировка в Java — Arrays.sort() для объектов
- Дисковые файлы — если данные не помещаются в памяти
- Подсчёт инверсий — в анализе данных
- Внешняя сортировка — для больших объёмов данных