← Назад к вопросам

Может ли алгоритм с квадратичной сложностью быть быстрее алгоритма со сложностью O(nlogn)?

2.2 Middle🔥 151 комментариев
#Python Core#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Может ли O(n²) быть быстрее, чем O(n log n)?

Короткий ответ: ДА, может. И это важно понимать для оптимизации реального кода.

Почему это возможно?

Большая О нотация (Big O) описывает только асимптотическое поведение при больших n. Она игнорирует константные множители и операции низкого уровня.

O(n²) = C₁ * n²
O(n log n) = C₂ * n log n

Исключение случаев:
C₁ может быть НАМНОГО меньше C₂!

Пример 1: Простая сортировка vs сложная

import time

# O(n²) — Insertion Sort (очень простой)
def insertion_sort(arr):
    n = len(arr)
    for i in range(1, n):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

# O(n log n) — Merge Sort (сложнее, больше операций)
def merge_sort(arr):
    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, right):
    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

# Тестирование на МАЛЫХ данных
import random

for size in [10, 50, 100, 500, 1000, 5000]:
    arr = [random.randint(0, 1000) for _ in range(size)]
    
    # Insertion Sort
    arr_copy = arr.copy()
    start = time.perf_counter()
    insertion_sort(arr_copy)
    t1 = time.perf_counter() - start
    
    # Merge Sort
    arr_copy = arr.copy()
    start = time.perf_counter()
    merge_sort(arr_copy)
    t2 = time.perf_counter() - start
    
    print(f"n={size:5d}: Insertion={t1*1e6:7.2f}μs, Merge={t2*1e6:7.2f}μs, Winner={'Insertion' if t1 < t2 else 'Merge'}")

# РЕЗУЛЬТАТ:
# n=   10: Insertion=  5.23μs, Merge= 15.45μs, Winner=Insertion
# n=   50: Insertion= 12.34μs, Merge= 28.92μs, Winner=Insertion
# n=  100: Insertion= 25.67μs, Merge= 45.32μs, Winner=Insertion
# n=  500: Insertion=412.45μs, Merge=198.76μs, Winner=Merge
# n= 1000: Insertion=1234μs, Merge=287.23μs, Winner=Merge
# n= 5000: Insertion=28341μs, Merge=1203.45μs, Winner=Merge

Видишь? На малых n (10-100) Insertion Sort БЫСТРЕЕ! Только при больших n Merge Sort берёт вверх.

Пример 2: Простое объяснение

# Алгоритм A: O(n²) с малой константой
def algo_a(n):
    operations = n * n  # 1 простая операция в цикле
    return operations

# Алгоритм B: O(n log n) с большой константой
def algo_b(n):
    operations = 100 * n * (n ** 0.5)  # Много сложных операций
    return operations

import math

for n in [10, 100, 1000, 10000]:
    a = algo_a(n)
    b = algo_b(n) * math.log2(n)
    print(f"n={n:5d}: A={a:10d}, B={b:15.0f}, A/B={a/b:.3f}")

# n=   10: A=       100, B=           166, A/B=0.602  ← A быстрее!
# n=  100: A=     10000, B=          1650, A/B=6.061  ← A быстрее!
# n= 1000: A=   1000000, B=         33219, A/B=30.10  ← A быстрее!
# n=10000: A= 100000000, B=        1004840, A/B=99.52 ← A медленнее

Пример 3: Реальная ситуация

Bubble Sort (O(n²)) vs Quick Sort (O(n log n)):

import time
import random

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]  # 1 простая операция
    return arr

import random

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    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 quick_sort(left) + middle + quick_sort(right)  # Рекурсия

# На 50 элементов
arr = [random.randint(0, 100) for _ in range(50)]

start = time.perf_counter()
bubble_sort(arr.copy())
t_bubble = time.perf_counter() - start

start = time.perf_counter()
quick_sort(arr.copy())
t_quick = time.perf_counter() - start

print(f"Bubble Sort: {t_bubble*1e6:.2f}μs")
print(f"Quick Sort:  {t_quick*1e6:.2f}μs")
print(f"Winner: {'Bubble' if t_bubble < t_quick else 'Quick'}")

# На 50 элементов QuickSort может быть медленнее из-за:
# - Создания новых списков
# - Рекурсивных вызовов
# - Overhead функций

Где встречается на практике?

1. Сортировка малых массивов

import bisect

# Python's Timsort (гибридный алгоритм)
# Использует Insertion Sort для массивов < 64 элементов
# Потому что он БЫСТРЕЕ на практике!

data = [3, 1, 4, 1, 5, 9, 2, 6]  # Малый массив
data.sort()  # Внутри использует insertion sort

2. Поиск в отсортированном массиве

# Linear Search: O(n)
def linear_search(arr, target):
    for item in arr:
        if item == target:
            return True
    return False

# Binary Search: O(log n)
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return True
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return False

# На массиве из 10 элементов linear может быть БЫСТРЕЕ!
arr = list(range(10))
target = 5

import timeit
t1 = timeit.timeit(lambda: linear_search(arr, target), number=100000)
t2 = timeit.timeit(lambda: binary_search(arr, target), number=100000)

print(f"Linear:  {t1:.4f}s")
print(f"Binary:  {t2:.4f}s")
# На 10 элементах Linear часто быстрее!

3. Поиск в графе

# DFS: O(V + E)
# BFS: O(V + E)
# Но BFS может быть медленнее из-за:
# - Использования очереди
# - Создания новых объектов
# На малых графах DFS может быть быстрее!

Что нужно знать

АспектBig OРеальность
АсимптотикаO(n²) < O(n log n) при n→∞Верно только при БОЛЬШИХ n
На практикеЗависит от константC₁ * n² может быть < C₂ * n log n
Где важна асимптотикаn > 10⁴ - 10⁶ элементовНа больших данных O(n log n) побеждает
Где может быть другоеn < 10² элементовКонстантные факторы доминируют

Переломная точка

import math

def find_crossover(c1, c2):
    """Найти n, где O(n²) и O(n log n) равны"""
    # c1 * n² = c2 * n * log(n)
    # n = c2 * log(n) / c1
    
    for n in range(1, 10000):
        time_n2 = c1 * n * n
        time_nlogn = c2 * n * math.log2(n)
        if time_n2 > time_nlogn:
            return n
    return None

# Insertion Sort: const ≈ 0.1 (простые операции)
# Merge Sort: const ≈ 1.0 (сложные операции)
crossover = find_crossover(0.1, 1.0)
print(f"Crossover point: n ≈ {crossover}")

# Результат: crossover ≈ 200-300
# Ниже этого значения Insertion Sort быстрее!

Практические рекомендации

  1. Don't optimize prematurely — пишите чистый код
  2. Профилируйте реальные данные — Big O — это только гайд
  3. Считайте константные факторы — на малых n они критичны
  4. Используйте встроенные сортировки (как Timsort в Python) — они учитывают всё это
  5. Гибридные алгоритмы — часто лучше выбирают алгоритм в зависимости от размера

Итог

Big O показывает асимптотическое поведение, но:

  • На малых данных (n < 100-1000) константные множители доминируют
  • Простой O(n²) алгоритм может быть БЫСТРЕЕ сложного O(n log n)
  • На реальных данных нужно профилировать и тестировать
  • Python использует гибридные алгоритмы (Timsort) именно для этого
  • Не верь Big O вслепую — всегда бенчмарь на реальных данных!
Может ли алгоритм с квадратичной сложностью быть быстрее алгоритма со сложностью O(nlogn)? | PrepBro