IntroSort (Introspective Sort / Интроспективная сортировка)
Краткое описание
Гибридный алгоритм сортировки, комбинирующий Quick Sort, Heap Sort и Insertion Sort. Начинает с Quick Sort, но следит за глубиной рекурсии — если она превышает 2×log(n), переключается на Heap Sort, избегая худшего случая O(n²). Для малых подмассивов использует Insertion Sort. Используется в C++ STL std::sort.
Когда использовать
- Production код на C++ — это алгоритм за std::sort в стандартной библиотеке
- Нужна гарантированная O(n log n) БЕЗ дополнительной памяти — сочетает скорость Quick Sort с гарантиями Heap Sort
- Универсальный выбор — работает эффективно на любых данных, не имеет худшего случая O(n²)
- Большие массивы в памяти — очень быстрый на практике благодаря Quick Sort как основному алгоритму
- Критична производительность — оптимален для большинства реальных сценариев
Когда не стоит использовать:
- Нужна стабильность — IntroSort не стабильный, как и Quick Sort
- Маленькие массивы — для массивов < 16 элементов обычный Insertion Sort может быть проще
- Почти отсортированные данные — TimSort будет эффективнее благодаря адаптивности
- Простота реализации важна — один из самых сложных алгоритмов для имплементации
С объяснением логики работы
/**
* IntroSort — гибридная сортировка
*
* Стратегия:
* 1. Начинаем с Quick Sort (быстрый на практике)
* 2. Следим за глубиной рекурсии
* 3. Если глубина > 2*log(n) → переключаемся на Heap Sort (избегаем O(n²))
* 4. Для малых подмассивов (< 16 элементов) → Insertion Sort
*/
// Порог для переключения на Insertion Sort
const INSERTION_SORT_THRESHOLD = 16;
/**
* Insertion Sort для малых подмассивов
*/
function insertionSort(arr, left, right) {
for (let i = left + 1; i <= right; i++) {
const key = arr[i];
let j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
/**
* Heapify для Heap Sort
*/
function heapify(arr, n, i, offset) {
let largest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
if (left < n && arr[offset + left] > arr[offset + largest]) {
largest = left;
}
if (right < n && arr[offset + right] > arr[offset + largest]) {
largest = right;
}
if (largest !== i) {
[arr[offset + i], arr[offset + largest]] =
[arr[offset + largest], arr[offset + i]];
heapify(arr, n, largest, offset);
}
}
/**
* Heap Sort для подмассива
*/
function heapSort(arr, left, right) {
const n = right - left + 1;
// Строим max-heap
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i, left);
}
// Извлекаем элементы из кучи
for (let i = n - 1; i > 0; i--) {
[arr[left], arr[left + i]] = [arr[left + i], arr[left]];
heapify(arr, i, 0, left);
}
}
/**
* Partition для Quick Sort (Lomuto scheme)
*/
function partition(arr, left, right) {
// Медиана из трёх для выбора pivot (оптимизация)
const mid = Math.floor((left + right) / 2);
// Упорядочиваем arr[left], arr[mid], arr[right]
if (arr[mid] < arr[left]) {
[arr[left], arr[mid]] = [arr[mid], arr[left]];
}
if (arr[right] < arr[left]) {
[arr[left], arr[right]] = [arr[right], arr[left]];
}
if (arr[right] < arr[mid]) {
[arr[mid], arr[right]] = [arr[right], arr[mid]];
}
// Используем медиану как pivot
const pivot = arr[mid];
[arr[mid], arr[right - 1]] = [arr[right - 1], arr[mid]];
let i = left;
let j = right - 1;
while (true) {
while (arr[++i] < pivot) {}
while (arr[--j] > pivot) {}
if (i >= j) break;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
[arr[i], arr[right - 1]] = [arr[right - 1], arr[i]];
return i;
}
/**
* Внутренняя рекурсивная функция IntroSort
*/
function introSortHelper(arr, left, right, maxDepth) {
const size = right - left + 1;
// ОПТИМИЗАЦИЯ 1: Для малых массивов используем Insertion Sort
// Insertion Sort эффективнее на малых данных из-за меньших накладных расходов
if (size <= INSERTION_SORT_THRESHOLD) {
insertionSort(arr, left, right);
return;
}
// ОПТИМИЗАЦИЯ 2: Если глубина рекурсии слишком большая
// значит Quick Sort деградирует до O(n²)
// Переключаемся на Heap Sort с гарантированной O(n log n)
if (maxDepth === 0) {
heapSort(arr, left, right);
return;
}
// Основной случай: используем Quick Sort
const pivotIndex = partition(arr, left, right);
// Рекурсивно сортируем части, уменьшая maxDepth
introSortHelper(arr, left, pivotIndex - 1, maxDepth - 1);
introSortHelper(arr, pivotIndex + 1, right, maxDepth - 1);
}
/**
* Основная функция IntroSort
*/
function introSort(arr) {
if (arr.length <= 1) return arr;
// Вычисляем максимальную глубину рекурсии: 2 * log₂(n)
// Это эмпирически оптимальное значение
const maxDepth = Math.floor(2 * Math.log2(arr.length));
introSortHelper(arr, 0, arr.length - 1, maxDepth);
return arr;
}
// Пример использования
const numbers = [64, 34, 25, 12, 22, 11, 90, 88, 45, 50, 22, 3, 99, 1];
console.log("Исходный массив:", numbers);
introSort(numbers);
console.log("Отсортированный массив:", numbers);
/**
* ПОЧЕМУ INTROSORT ЭФФЕКТИВЕН:
*
* 1. БЫСТРЫЙ В СРЕДНЕМ
* Quick Sort — один из самых быстрых на практике
*
* 2. ГАРАНТИРОВАННАЯ ПРОИЗВОДИТЕЛЬНОСТЬ
* Heap Sort предотвращает деградацию до O(n²)
*
* 3. ОПТИМАЛЕН НА МАЛЫХ ДАННЫХ
* Insertion Sort для подмассивов < 16 элементов
*
* 4. МЕДИАНА ИЗ ТРЁХ
* Лучший выбор pivot, чем случайный или первый элемент
*/С анализом сложностей
/**
* IntroSort с анализом сложности
*
* ВРЕМЕННАЯ СЛОЖНОСТЬ:
* - Лучший случай: O(n log n) — Quick Sort с хорошими разбиениями
* - Средний случай: O(n log n) — типичная работа Quick Sort
* - Худший случай: O(n log n) — ГАРАНТИРОВАННО! Heap Sort предотвращает O(n²)
*
* ПРОСТРАНСТВЕННАЯ СЛОЖНОСТЬ:
* - O(log n) — стек рекурсии для Quick Sort
* - O(1) дополнительной памяти (in-place)
*
* ОСОБЕННОСТИ:
* - НЕ стабильный (как Quick Sort)
* - Гибридный: комбинирует 3 алгоритма
* - Production-ready: используется в C++ STL
*/
const INSERTION_SORT_THRESHOLD = 16;
function insertionSort(arr, left, right) {
// ВРЕМЕННАЯ СЛОЖНОСТЬ: O((right-left)²)
// Для малых массивов (≤16): константное время
// O(16²) = O(256) = O(1)
for (let i = left + 1; i <= right; i++) {
const key = arr[i];
let j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
function heapify(arr, n, i, offset) {
// O(log n) — высота дерева
let largest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
if (left < n && arr[offset + left] > arr[offset + largest]) {
largest = left;
}
if (right < n && arr[offset + right] > arr[offset + largest]) {
largest = right;
}
if (largest !== i) {
[arr[offset + i], arr[offset + largest]] =
[arr[offset + largest], arr[offset + i]];
heapify(arr, n, largest, offset);
}
}
function heapSort(arr, left, right) {
// ВРЕМЕННАЯ СЛОЖНОСТЬ: O(n log n) гарантированно
// Используется как "страховка" от худшего случая Quick Sort
const n = right - left + 1;
// Build heap: O(n)
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i, left);
}
// Extract elements: O(n log n)
for (let i = n - 1; i > 0; i--) {
[arr[left], arr[left + i]] = [arr[left + i], arr[left]];
heapify(arr, i, 0, left);
}
}
function partition(arr, left, right) {
// ВРЕМЕННАЯ СЛОЖНОСТЬ: O(right - left) = O(n)
//
// ОПТИМИЗАЦИЯ: Медиана из трёх
// Вместо случайного pivot или первого элемента,
// выбираем медиану из arr[left], arr[mid], arr[right]
// Это значительно снижает вероятность плохих разбиений
const mid = Math.floor((left + right) / 2);
// Сортируем три элемента: O(1)
if (arr[mid] < arr[left]) {
[arr[left], arr[mid]] = [arr[mid], arr[left]];
}
if (arr[right] < arr[left]) {
[arr[left], arr[right]] = [arr[right], arr[left]];
}
if (arr[right] < arr[mid]) {
[arr[mid], arr[right]] = [arr[right], arr[mid]];
}
const pivot = arr[mid];
[arr[mid], arr[right - 1]] = [arr[right - 1], arr[mid]];
let i = left;
let j = right - 1;
// Partition: O(n)
while (true) {
while (arr[++i] < pivot) {}
while (arr[--j] > pivot) {}
if (i >= j) break;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
[arr[i], arr[right - 1]] = [arr[right - 1], arr[i]];
return i;
}
function introSortHelper(arr, left, right, maxDepth) {
const size = right - left + 1;
// ВЕТВЛЕНИЕ 1: Малые массивы
// ПОЧЕМУ 16? Эмпирически найденное оптимальное значение
// - Insertion Sort быстрее на малых данных
// - Меньше накладных расходов на рекурсию
// - Лучшая cache locality
if (size <= INSERTION_SORT_THRESHOLD) {
insertionSort(arr, left, right); // O(size²) но size ≤ 16
return;
}
// ВЕТВЛЕНИЕ 2: Превышена глубина рекурсии
// ПОЧЕМУ maxDepth = 2*log(n)?
// - log(n) — ожидаемая глубина для хорошо сбалансированного Quick Sort
// - 2*log(n) — запас на небольшие дисбалансы
// - Если превышено → плохие разбиения → переключаемся на Heap Sort
if (maxDepth === 0) {
heapSort(arr, left, right); // O(n log n) гарантированно
return;
}
// ОСНОВНОЙ СЛУЧАЙ: Quick Sort
// АНАЛИЗ:
// - Partition: O(n)
// - Две рекурсии: T(k) + T(n-k-1)
// - При хороших разбиениях: T(n) = 2T(n/2) + O(n) = O(n log n)
// - При плохих: защищены maxDepth и переключением на Heap Sort
const pivotIndex = partition(arr, left, right);
introSortHelper(arr, left, pivotIndex - 1, maxDepth - 1);
introSortHelper(arr, pivotIndex + 1, right, maxDepth - 1);
}
function introSort(arr) {
if (arr.length <= 1) return arr;
// КЛЮЧЕВОЙ ПАРАМЕТР: maxDepth = 2 * log₂(n)
//
// Примеры:
// n = 100: maxDepth = 2 * 6.64 ≈ 13
// n = 1000: maxDepth = 2 * 9.97 ≈ 20
// n = 1000000: maxDepth = 2 * 19.93 ≈ 40
//
// Это позволяет Quick Sort работать нормально,
// но предотвращает деградацию до O(n²)
const maxDepth = Math.floor(2 * Math.log2(arr.length));
introSortHelper(arr, 0, arr.length - 1, maxDepth);
return arr;
}
// ИТОГОВАЯ СЛОЖНОСТЬ:
//
// Время: O(n log n) в ЛЮБОМ случае
// Память: O(log n) для стека рекурсии
//
// СРАВНЕНИЕ С ДРУГИМИ АЛГОРИТМАМИ:
//
// IntroSort vs Quick Sort:
// ✓ IntroSort гарантирует O(n log n), Quick Sort может O(n²)
// ✓ IntroSort использует Insertion Sort для малых массивов
// ✓ IntroSort использует медиану из трёх для pivot
// = Оба in-place и нестабильные
//
// IntroSort vs Heap Sort:
// ✓ IntroSort быстрее на практике (большую часть времени Quick Sort)
// ✓ IntroSort лучшая cache locality
// = Оба гарантируют O(n log n)
// = Оба in-place и нестабильные
//
// IntroSort vs Merge Sort:
// ✓ IntroSort не требует O(n) памяти
// ✗ IntroSort не стабильный
// = Оба гарантируют O(n log n)
//
// IntroSort vs TimSort:
// ✓ IntroSort быстрее на случайных данных
// ✓ IntroSort использует меньше памяти
// ✗ IntroSort не адаптивный
// ✗ IntroSort не стабильный
//
// ПОЧЕМУ C++ STL ИСПОЛЬЗУЕТ INTROSORT:
// 1. Гарантированная O(n log n) — нет худшего случая
// 2. Очень быстрый на практике — Quick Sort как основа
// 3. In-place — O(log n) памяти
// 4. Оптимизирован для разных сценариев
// 5. Проверен временем в production
// Примеры производительности:
// Случайные данные: работает как Quick Sort — очень быстро
const random = Array.from({length: 1000}, () => Math.random());
// Отсортированные данные: не деградирует благодаря медиане из трёх
const sorted = Array.from({length: 1000}, (_, i) => i);
// Обратный порядок: не деградирует благодаря maxDepth и Heap Sort
const reversed = Array.from({length: 1000}, (_, i) => 1000 - i);
// Все одинаковые: медиана из трёх помогает
const same = Array.from({length: 1000}, () => 42);