Cube Sort (Кубическая сортировка)
Краткое описание
Параллельный алгоритм сортировки, разработанный для многопроцессорных систем с кубической топологией (гиперкуб). Делит данные на блоки, распределяет по процессорам, локально сортирует каждый блок, затем объединяет с помощью параллельного слияния. В последовательной версии работает аналогично Merge Sort с оптимизациями для cache locality.
Когда использовать
- Параллельные вычисления — разработан специально для многопроцессорных систем
- Распределённые системы — эффективен при обработке больших данных на кластерах
- GPU вычисления — может быть адаптирован для CUDA/OpenCL
- Очень большие массивы — где параллелизм даёт значительное ускорение
- Системы с гиперкубической топологией — оптимизирован для такой архитектуры
Когда не стоит использовать:
- Однопроцессорные системы — последовательная версия не лучше обычного Merge Sort
- Маленькие массивы — накладные расходы на параллелизм не окупятся
- Простота важна — очень сложная реализация для production
- Ограниченная память — требует дополнительную память как Merge Sort
- Нестабильная нагрузка — может быть дисбаланс между процессорами
С объяснением логики работы
/**
* Cube Sort (последовательная версия)
*
* Это упрощённая версия для одного процессора.
* Настоящий Cube Sort использует параллельную обработку на множестве процессоров.
*
* Алгоритм:
* 1. Делим массив на блоки (в параллельной версии — по числу процессоров)
* 2. Сортируем каждый блок локально
* 3. Выполняем последовательное слияние отсортированных блоков
*
* В параллельной версии:
* - Каждый процессор получает свой блок
* - Все процессоры сортируют одновременно
* - Слияние происходит параллельно по схеме гиперкуба
*/
/**
* Слияние двух отсортированных массивов
*/
function merge(left, right) {
const result = [];
let leftIndex = 0;
let rightIndex = 0;
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] <= right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
/**
* Insertion Sort для малых блоков
* В параллельной версии каждый процессор запускает это локально
*/
function insertionSort(arr, left = 0, right = arr.length - 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;
}
return arr;
}
/**
* Cube Sort (последовательная версия)
*
* @param {Array} arr - массив для сортировки
* @param {number} blockSize - размер блока (в параллельной версии = размер данных на процессор)
*/
function cubeSort(arr, blockSize = 32) {
if (arr.length <= 1) return arr;
// ШАГ 1: Делим массив на блоки
// В параллельной версии каждый блок обрабатывается отдельным процессором
const blocks = [];
for (let i = 0; i < arr.length; i += blockSize) {
const end = Math.min(i + blockSize, arr.length);
blocks.push(arr.slice(i, end));
}
// ШАГ 2: Сортируем каждый блок локально
// ПАРАЛЛЕЛЬНАЯ ФАЗА: все процессоры работают одновременно
for (let i = 0; i < blocks.length; i++) {
insertionSort(blocks[i], 0, blocks[i].length - 1);
}
// ШАГ 3: Слияние блоков
// В параллельной версии используется схема гиперкуба
// Процессоры объединяются попарно, затем четвёрками, и т.д.
while (blocks.length > 1) {
const mergedBlocks = [];
for (let i = 0; i < blocks.length; i += 2) {
if (i + 1 < blocks.length) {
// Сливаем пары блоков
mergedBlocks.push(merge(blocks[i], blocks[i + 1]));
} else {
// Если блоков нечётное число, последний переносим как есть
mergedBlocks.push(blocks[i]);
}
}
blocks.length = 0;
blocks.push(...mergedBlocks);
}
// ШАГ 4: Копируем результат обратно
const sorted = blocks[0];
for (let i = 0; i < arr.length; i++) {
arr[i] = sorted[i];
}
return arr;
}
// Пример использования
const numbers = [64, 34, 25, 12, 22, 11, 90, 88, 45, 50, 33, 77];
console.log("Исходный массив:", numbers);
cubeSort(numbers, 4); // Блоки по 4 элемента
console.log("Отсортированный массив:", numbers);
/**
* ВИЗУАЛИЗАЦИЯ РАБОТЫ на примере [8, 3, 5, 1, 9, 6, 7, 2] с blockSize=2:
*
* ШАГ 1: Деление на блоки
* [8, 3] | [5, 1] | [9, 6] | [7, 2]
*
* ШАГ 2: Локальная сортировка (ПАРАЛЛЕЛЬНО)
* Процессор 0: [8, 3] → [3, 8]
* Процессор 1: [5, 1] → [1, 5]
* Процессор 2: [9, 6] → [6, 9]
* Процессор 3: [7, 2] → [2, 7]
*
* Результат: [3, 8] | [1, 5] | [6, 9] | [2, 7]
*
* ШАГ 3: Слияние (схема гиперкуба)
*
* Раунд 1 (ПАРАЛЛЕЛЬНО):
* Процессоры 0-1: merge([3, 8], [1, 5]) → [1, 3, 5, 8]
* Процессоры 2-3: merge([6, 9], [2, 7]) → [2, 6, 7, 9]
*
* Результат: [1, 3, 5, 8] | [2, 6, 7, 9]
*
* Раунд 2:
* Процессоры 0-3: merge([1, 3, 5, 8], [2, 6, 7, 9]) → [1, 2, 3, 5, 6, 7, 8, 9]
*
* Финальный результат: [1, 2, 3, 5, 6, 7, 8, 9]
*/
/**
* СХЕМА ГИПЕРКУБА для 8 процессоров:
*
* Гиперкуб 3-го порядка (8 вершин = 2³):
*
* 100 -------- 101
* /| /|
* / | / |
* 110--------111 |
* | 000------|--001
* | / | /
* |/ |/
* 010---------011
*
* Фазы слияния:
* Фаза 1: пары по оси X (000↔001, 010↔011, 100↔101, 110↔111)
* Фаза 2: пары по оси Y (000↔010, 001↔011, 100↔110, 101↔111)
* Фаза 3: пары по оси Z (000↔100, 001↔101, 010↔110, 011↔111)
*
* Каждая фаза выполняется ПАРАЛЛЕЛЬНО!
*/
/**
* ПОЧЕМУ "CUBE" SORT:
*
* Название происходит от топологии гиперкуба:
* - Для n = 2^k процессоров используется k-мерный гиперкуб
* - 2 процессора = линия (1D)
* - 4 процессора = квадрат (2D)
* - 8 процессоров = куб (3D)
* - 16 процессоров = гиперкуб 4D
*
* Преимущества гиперкуба:
* - Логарифмический диаметр: O(log n) шагов для связи
* - Хорошая масштабируемость
* - Эффективная балансировка нагрузки
*/С анализом сложностей
/**
* Cube Sort с анализом сложности
*
* ПОСЛЕДОВАТЕЛЬНАЯ ВЕРСИЯ:
* - Временная сложность: O(n log n) — как Merge Sort
* - Пространственная: O(n) — для временных блоков
*
* ПАРАЛЛЕЛЬНАЯ ВЕРСИЯ (p процессоров):
* - Временная сложность: O((n/p) log(n/p) + n log p)
* - Локальная сортировка: O((n/p) log(n/p))
* - Слияние log p раундов: O(n log p)
* - При p = O(n / log n): O(log² n) — почти линейная!
*
* ОСОБЕННОСТИ:
* - Показанная последовательная реализация стабильна (insertionSort + merge с `<=`);
* нестабильность характерна для параллельных/гиперкубических схем слияния
* - Не in-place
* - Требует специальную архитектуру для полной эффективности
*/
function merge(left, right) {
// ВРЕМЕННАЯ СЛОЖНОСТЬ: O(n + m)
// где n = left.length, m = right.length
const result = [];
let leftIndex = 0;
let rightIndex = 0;
// Сравниваем и объединяем: O(n + m)
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] <= right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
function insertionSort(arr, left = 0, right = arr.length - 1) {
// ВРЕМЕННАЯ СЛОЖНОСТЬ: O(k²) где k = размер блока
// Для малых блоков (k ≤ 32): практически O(k) = 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;
}
return arr;
}
/**
* Cube Sort — последовательная версия
*
* ДЕТАЛЬНЫЙ АНАЛИЗ:
*
* Пусть:
* - n = размер массива
* - b = размер блока (blockSize)
* - p = n/b = количество блоков (в параллельной версии = число процессоров)
*
* ФАЗА 1: Создание блоков — O(n)
* ФАЗА 2: Локальная сортировка
* - Последовательно: p блоков * O(b log b) = O((n/b) * b log b) = O(n log b)
* - ПАРАЛЛЕЛЬНО: O(b log b) — все процессоры работают одновременно!
*
* ФАЗА 3: Слияние
* - Количество раундов: log₂(p) = log₂(n/b)
* - Каждый раунд: O(n)
* - Последовательно: O(n log(n/b))
* - ПАРАЛЛЕЛЬНО: каждый раунд тоже параллелится → O(n/p * log p)
*
* ИТОГО:
* Последовательная: O(n log b + n log(n/b)) = O(n log n)
* Параллельная: O(b log b + n/p * log p)
*/
function cubeSort(arr, blockSize = 32) {
if (arr.length <= 1) return arr;
const n = arr.length;
const p = Math.ceil(n / blockSize); // Количество "процессоров"
// ФАЗА 1: Деление на блоки
// Время: O(n)
// Память: O(n) для хранения блоков
const blocks = [];
for (let i = 0; i < n; i += blockSize) {
const end = Math.min(i + blockSize, n);
blocks.push(arr.slice(i, end));
}
// ФАЗА 2: Локальная сортировка
// ПОСЛЕДОВАТЕЛЬНО: O(n log b)
// ПАРАЛЛЕЛЬНО: O(b log b) — все p процессоров работают одновременно
for (let i = 0; i < blocks.length; i++) {
insertionSort(blocks[i], 0, blocks[i].length - 1);
}
// ФАЗА 3: Слияние (схема гиперкуба)
// Количество раундов: log₂(p)
//
// АНАЛИЗ РАУНДОВ:
// Раунд 1: p/2 слияний блоков размера b
// Время одного слияния: O(2b)
// ПОСЛЕДОВАТЕЛЬНО: O(p/2 * 2b) = O(p*b) = O(n)
// ПАРАЛЛЕЛЬНО: O(2b)
//
// Раунд 2: p/4 слияний блоков размера 2b
// ПОСЛЕДОВАТЕЛЬНО: O(n)
// ПАРАЛЛЕЛЬНО: O(4b)
//
// ...
//
// Раунд log p: 1 слияние блоков размера n/2
// ПОСЛЕДОВАТЕЛЬНО: O(n)
// ПАРАЛЛЕЛЬНО: O(n)
//
// ИТОГО для фазы 3:
// ПОСЛЕДОВАТЕЛЬНО: log₂(p) раундов * O(n) = O(n log p)
// ПАРАЛЛЕЛЬНО: O(b) + O(2b) + O(4b) + ... + O(n) = O(n)
// (геометрическая прогрессия, доминирует последний член)
let round = 1;
while (blocks.length > 1) {
const mergedBlocks = [];
// В каждом раунде объединяем пары блоков
for (let i = 0; i < blocks.length; i += 2) {
if (i + 1 < blocks.length) {
mergedBlocks.push(merge(blocks[i], blocks[i + 1]));
} else {
mergedBlocks.push(blocks[i]);
}
}
blocks.length = 0;
blocks.push(...mergedBlocks);
round++;
}
// ФАЗА 4: Копирование — O(n)
const sorted = blocks[0];
for (let i = 0; i < arr.length; i++) {
arr[i] = sorted[i];
}
return arr;
}
/**
* ОПТИМАЛЬНЫЙ ВЫБОР blockSize:
*
* Последовательная версия:
* - blockSize должен помещаться в L1 cache (~32-64 элемента)
* - Insertion Sort эффективен на малых данных
* - Рекомендуется: 16-64
*
* Параллельная версия:
* - blockSize = n/p, где p = число процессоров
* - Должен быть достаточно большим для окупаемости параллелизма
* - Рекомендуется: n/p ≥ 1000
*/
/**
* СРАВНЕНИЕ ПРОИЗВОДИТЕЛЬНОСТИ:
*
* Для n = 1,000,000, p = 1000 процессоров:
*
* ПОСЛЕДОВАТЕЛЬНЫЕ АЛГОРИТМЫ:
* - Merge Sort: O(n log n) = O(20,000,000) операций
* - Quick Sort: O(n log n) ≈ O(15,000,000) операций (лучшие константы)
* - Cube Sort: O(n log n) = O(20,000,000) операций (аналогично Merge Sort)
*
* ПАРАЛЛЕЛЬНЫЙ CUBE SORT:
* - Локальная сортировка: O((n/p) log(n/p)) = O(1000 * 10) = O(10,000)
* - Слияние: O(n) = O(1,000,000)
* - ИТОГО: O(1,010,000) операций
* - Ускорение: ~20x по сравнению с последовательным!
*
* При оптимальном p = O(n / log n):
* - Сложность: O(log² n)
* - Для n = 1,000,000: O(400) операций
* - Ускорение: ~50,000x (теоретически)
*/
console.log("Cube Sort (последовательная версия)");
console.log("Работает как оптимизированный Merge Sort");
console.log("Для реального ускорения нужна параллельная реализация");