Туторіал · Алгоритми · JavaScript · Canvas 2D
📅 Липень 2026 ⏱ ≈ 25 хв 🎯 Середній рівень

Sorting visualizer за 100 рядків JavaScript

Класична стовпчикова анімація алгоритму сортування виглядає так, ніби потребує повноцінної машини станів, щоб призупинятись і відновлюватись посеред порівняння. Насправді ні — функції-генератори JavaScript дозволяють писати алгоритми точно так, як у підручнику, і отримати покадрову анімацію майже безкоштовно. Цей туторіал будує робочий візуалізатор із чотирма алгоритмами приблизно за 100 рядків чистого JavaScript і Canvas 2D.

1. Основний прийом: генератори як алгоритми з паузою

Звичайна функція JavaScript виконується до кінця миттєво, щойно ви її викликали — немає вбудованого способу призупинити Bubble Sort посеред обміну і відновити його через 16 мілісекунд на наступному кадрі анімації. Функція-генератор (оголошена як function*) розв'язує це точно: виклик yield призупиняє виконання і повертає значення викликачу, який може відновити його пізніше через .next() — продовжуючи з наступного рядка, з усіма локальними змінними в цілості.

function* countUp(n) {
  for (let i = 0; i < n; i++) yield i; // призупиняється тут щоразу
}
const gen = countUp(3);
gen.next(); // { value: 0, done: false }
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: undefined, done: true }

Весь візуалізатор побудований на одній ідеї: написати кожен алгоритм сортування як генератор, що виконує yield щоразу, коли порівнює або обмінює два елементи, а потім дати циклу requestAnimationFrame витягувати по одному кроку і перемальовувати масив між кроками.

2. Дані та налаштування Canvas

Дані — це просто масив чисел, що представляють висоти стовпчиків, а рендерер малює кожне число як кольоровий вертикальний стовпчик. Два індекси — compare — підсвічуються, щоб було видно точно, на що дивиться алгоритм на кожному кроці:

const canvas = document.getElementById('sortCanvas');
const ctx = canvas.getContext('2d');
const N = 80;
let arr = Array.from({ length: N }, () => Math.floor(Math.random() * 100) + 5);

function drawBars(highlight = []) {
  const w = canvas.width / arr.length;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  arr.forEach((val, i) => {
    ctx.fillStyle = highlight.includes(i) ? '#f87171' : '#60a5fa';
    const h = (val / 105) * canvas.height;
    ctx.fillRect(i * w, canvas.height - h, w - 1, h);
  });
}

3. Bubble Sort як генератор

Bubble Sort повторно проходить масив, обмінюючи сусідні елементи, що стоять не в тому порядку. Записаний як генератор, він ідентичний підручниковій версії, за винятком одного доданого yield після кожного порівняння:

function* bubbleSort(a) {
  for (let i = 0; i < a.length - 1; i++) {
    for (let j = 0; j < a.length - i - 1; j++) {
      yield [j, j + 1]; // збираємось порівняти ці два індекси
      if (a[j] > a[j + 1]) {
        [a[j], a[j + 1]] = [a[j + 1], a[j]]; // обмін
        yield [j, j + 1]; // показуємо і стан після обміну
      }
    }
  }
}

4. Insertion Sort як генератор

Insertion Sort вирощує відсортований префікс по одному елементу, зсуваючи більші елементи праворуч, щоб звільнити місце. Виконання yield при кожному зсуві візуально показує «вставку» у сповільненому відтворенні:

function* insertionSort(a) {
  for (let i = 1; i < a.length; i++) {
    const key = a[i];
    let j = i - 1;
    yield [i, j];
    while (j >= 0 && a[j] > key) {
      a[j + 1] = a[j]; // зсув праворуч
      j--;
      yield [j, j + 1];
    }
    a[j + 1] = key; // вставляємо key у своє місце
    yield [j + 1];
  }
}

5. Quick Sort як генератор

Quick Sort рекурсивний, тому його версія-генератор потребує yield* («делегування yield»), щоб передати кожне значення, повернуте рекурсивним викликом, до верхнього викликача — без цього були б видні лише yield із найзовнішнього виклику:

function* quickSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const pivot = a[hi];
  let i = lo - 1;
  for (let j = lo; j < hi; j++) {
    yield [j, hi]; // порівнюємо a[j] з опорним
    if (a[j] < pivot) {
      i++;
      [a[i], a[j]] = [a[j], a[i]];
      yield [i, j];
    }
  }
  [a[i + 1], a[hi]] = [a[hi], a[i + 1]]; // ставимо опорний на його остаточне місце
  yield [i + 1, hi];
  yield* quickSort(a, lo, i);       // делегуємо: передаємо кожен yield з лівої половини
  yield* quickSort(a, i + 2, hi); // … і з правої половини
}
Чому yield*, а не просто виклик функції: звичайний рекурсивний виклик quickSort(a, lo, i) миттєво виконав би всю ліву половину до кінця, оскільки ніхто не витягує з неї значень. yield* перетворює рекурсивний виклик на під-ітератор, чиї значення проходять через батьківський генератор по одному, точно чергуючись із верхньорівневим циклом, що витягує кадри.

6. Merge Sort як генератор

Merge Sort розбиває масив, рекурсивно сортує обидві половини, а потім зливає їх назад за допомогою допоміжного масиву. Виконання yield як під час розбиття, так і під час фази злиття, показує структуру «розділяй і володарюй» прямо на екрані:

function* mergeSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const mid = (lo + hi) >> 1;
  yield* mergeSort(a, lo, mid);
  yield* mergeSort(a, mid + 1, hi);

  const left = a.slice(lo, mid + 1), right = a.slice(mid + 1, hi + 1);
  let i = 0, j = 0, k = lo;
  while (i < left.length && j < right.length) {
    a[k] = left[i] <= right[j] ? left[i++] : right[j++];
    yield [k];  // індекс k щойно отримав злите значення
    k++;
  }
  while (i < left.length)  { a[k] = left[i++];  yield [k++]; }
  while (j < right.length) { a[k] = right[j++]; yield [k++]; }
}

7. Цикл рендерингу

Цикл витягує одну пару {value, done} з активного генератора за кадр анімації, перемальовує стовпчики з підсвіченими повернутими індексами і зупиняється, коли done дорівнює true. Витягування кількох кроків за кадр контролює швидкість відтворення, зовсім не торкаючись коду алгоритму:

let sorter = bubbleSort(arr);
let stepsPerFrame = 3; // вище значення = швидше відтворення

function animate() {
  let result;
  for (let s = 0; s < stepsPerFrame; s++) {
    result = sorter.next();
    if (result.done) break;
  }
  drawBars(result.done ? [] : result.value);
  if (!result.done) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Заміна алгоритмів: оскільки кожен алгоритм — це просто генератор з однаковим контрактом «поверни масив індексів для підсвітки», перехід від Bubble Sort до Quick Sort — це рівно один рядок — sorter = quickSort(arr) — без жодних змін у циклі рендерингу.

8. Повний візуалізатор на ~100 рядків

Об'єднавши всі частини в один самодостатній скрипт — робочий, анімований візуалізатор сортування з чотирма алгоритмами на вибір:

// ── Sorting visualizer на ~100 рядків: генератори + Canvas 2D ──
const canvas = document.getElementById('sortCanvas');
const ctx = canvas.getContext('2d');
const N = 80;
let arr = [];
let sorter = null;
const stepsPerFrame = 3;

function resetArray() {
  arr = Array.from({ length: N }, () => Math.floor(Math.random() * 100) + 5);
}

function drawBars(highlight = []) {
  const w = canvas.width / arr.length;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  arr.forEach((val, i) => {
    ctx.fillStyle = highlight.includes(i) ? '#f87171' : '#60a5fa';
    const h = (val / 105) * canvas.height;
    ctx.fillRect(i * w, canvas.height - h, w - 1, h);
  });
}

function* bubbleSort(a) {
  for (let i = 0; i < a.length - 1; i++)
    for (let j = 0; j < a.length - i - 1; j++) {
      yield [j, j + 1];
      if (a[j] > a[j + 1]) { [a[j], a[j + 1]] = [a[j + 1], a[j]]; yield [j, j + 1]; }
    }
}

function* insertionSort(a) {
  for (let i = 1; i < a.length; i++) {
    const key = a[i]; let j = i - 1;
    yield [i, j];
    while (j >= 0 && a[j] > key) { a[j + 1] = a[j]; j--; yield [j, j + 1]; }
    a[j + 1] = key; yield [j + 1];
  }
}

function* quickSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const pivot = a[hi]; let i = lo - 1;
  for (let j = lo; j < hi; j++) {
    yield [j, hi];
    if (a[j] < pivot) { i++; [a[i], a[j]] = [a[j], a[i]]; yield [i, j]; }
  }
  [a[i + 1], a[hi]] = [a[hi], a[i + 1]]; yield [i + 1, hi];
  yield* quickSort(a, lo, i);
  yield* quickSort(a, i + 2, hi);
}

function* mergeSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const mid = (lo + hi) >> 1;
  yield* mergeSort(a, lo, mid);
  yield* mergeSort(a, mid + 1, hi);
  const left = a.slice(lo, mid + 1), right = a.slice(mid + 1, hi + 1);
  let i = 0, j = 0, k = lo;
  while (i < left.length && j < right.length) { a[k] = left[i] <= right[j] ? left[i++] : right[j++]; yield [k++]; }
  while (i < left.length)  { a[k] = left[i++];  yield [k++]; }
  while (j < right.length) { a[k] = right[j++]; yield [k++]; }
}

const ALGORITHMS = { bubble: bubbleSort, insertion: insertionSort, quick: quickSort, merge: mergeSort };

function start(name) {
  resetArray();
  sorter = ALGORITHMS[name](arr);
  requestAnimationFrame(animate);
}

function animate() {
  let result;
  for (let s = 0; s < stepsPerFrame; s++) {
    result = sorter.next();
    if (result.done) break;
  }
  drawBars(result.done ? [] : result.value);
  if (!result.done) requestAnimationFrame(animate);
}

start('bubble'); // викличте start('quick'), start('merge'), start('insertion'), щоб перемкнутись
Наступні кроки: Додайте випадний список <select>, підключений до start(select.value), щоб перемикати алгоритми наживо, відстежуйте лічильник порівнянь/обмінів, щоб емпірично показати Big-O у дії, або додайте осцилятор Web Audio, чия частота керується порівнюваним значенням для класичного ефекту «звук алгоритму сортування».

Часті запитання

Чого я навчуся в цьому уроці?

Створіть анімований візуалізатор алгоритмів сортування приблизно за 100 рядків чистого JavaScript і Canvas 2D: генератори як алгоритми з паузою, Bubble/Insertion/Quick/Merge Sort та кольорове виділення порівнянь і обмінів.

Які теми розглядаються в цьому уроці?

Цей урок охоплює такі теми: Основний прийом: генератори як алгоритми з паузою, Дані та налаштування Canvas, Bubble Sort як генератор, Insertion Sort як генератор, Quick Sort як генератор, Merge Sort як генератор, Цикл рендерингу, Повний візуалізатор на ~100 рядків.

Скільки часу займає цей урок?

Цей урок займає приблизно 25 хв.

Які попередні знання потрібні?

Це урок рівня «Середній рівень» — окрема попередня підготовка, крім базового JavaScript, не потрібна.