Suốt 11 bài trước, mỗi bài đều có 1 sandbox tương tác riêng — cây AVL, đồ thị Dijkstra, mảng sắp xếp, cây Huffman... Bài cuối này lùi lại một bước và đặt câu hỏi khác: những sandbox đó có điểm chung gì? Câu trả lời là có — rất nhiều. Và khi tách phần chung ra thành 1 framework tối giản, việc dựng visualizer cho một thuật toán hoàn toàn mới chỉ còn tốn vài chục dòng code, thay vì viết lại từ đầu.
1. Vấn Đề: Mỗi Sandbox Đều Tự Viết Lại Cùng Một Thứ
Nhìn lại 11 sandbox đã xây (từ Bài 1 tới Bài 11), chúng đều có chung 3 thành phần, dù thuật toán bên trong khác nhau hoàn toàn:
- Điều khiển bước: chạy thuật toán từng bước một, có nút play/pause, tốc độ tuỳ chỉnh — không "nhảy" thẳng tới kết quả cuối.
- Vẽ trạng thái: mỗi bước cần vẽ lại node/cạnh (cây, đồ thị) hoặc ô/thanh (mảng, bảng băm) lên canvas.
- Log giải thích: một dòng mô tả tiếng Việt đi kèm mỗi bước, luôn đồng bộ với hình vẽ đang hiển thị.
Nếu tách 3 phần này thành 1 engine dùng chung, phần còn lại riêng cho từng thuật toán chỉ là: sinh ra danh sách các bước và vẽ 1 bước cụ thể trông như thế nào.
2. Framework Điều Khiển Bước: Biến Thuật Toán Thành Danh Sách "Step"
Công cụ tự nhiên nhất trong JavaScript để "tạm dừng" một thuật toán giữa chừng và lấy ra từng trạng
thái là generator function (function* / yield). Mỗi lần
yield, thuật toán "đóng băng" và trả về đúng 1 khung trạng thái — giống hệt 1 khung hình
của video:
// Bất kỳ thuật toán nào cũng có thể viết thành 1 generator sinh step
function* bubbleSortSteps(input) {
const arr = input.slice();
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
yield { array: arr.slice(), compare: [j, j + 1], desc: `So sánh a[${j}] và a[${j + 1}]` };
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
yield { array: arr.slice(), swap: [j, j + 1], desc: `Hoán đổi vì a[${j}] > a[${j + 1}]` };
}
}
}
}
// Engine dùng chung: chạy generator tới hết, LƯU LẠI toàn bộ step vào mảng
function collectSteps(generator) {
return [...generator]; // materialize toàn bộ — không lazy nữa
}
.next() mỗi lần tiến 1 bước, muốn
lùi lại 1 bước sẽ phải chạy lại thuật toán từ đầu tới bước đó — tốn CPU và phức
tạp. Vì hầu hết bài toán minh hoạ trong series có kích thước nhỏ (vài chục node/phần tử), vật chất
hoá toàn bộ mảng step vào bộ nhớ ngay từ đầu ([...generator]) cho phép
tua tới/lùi bất kỳ bước nào tức thì chỉ bằng cách đổi currentIndex —
đánh đổi một chút bộ nhớ để lấy trải nghiệm điều khiển mượt hơn nhiều.
3. Lớp Vẽ Node/Cạnh Tái Sử Dụng
Phần vẽ cũng tách thành các hàm nguyên tử nhỏ, dùng chung cho mọi thuật toán loại "đồ thị/cây" (node + cạnh) hoặc loại "mảng/bảng" (ô/thanh) — thuật toán mới chỉ cần gọi lại đúng những hàm này với dữ liệu khác:
// Dùng lại cho MỌI thuật toán dạng đồ thị/cây: AVL, Dijkstra, Trie, B-Tree, Huffman...
function drawNode(ctx, x, y, label, color) {
ctx.beginPath();
ctx.arc(x, y, 16, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(label, x, y);
}
function drawEdge(ctx, x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
// Dùng lại cho MỌI thuật toán dạng mảng: Sorting, DP, Memory Allocator, Hash Table...
function drawBar(ctx, x, y, w, h, label, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText(label, x + w / 2, y + h / 2);
}
4. Đồng Bộ Log Giải Thích Với Từng Bước
Vì mỗi step đã mang sẵn 1 chuỗi desc, việc "đồng bộ" chỉ đơn giản là:
1 hàm renderStep(index) duy nhất vừa vẽ lại canvas theo
steps[index], vừa cập nhật log hiển thị đúng mô tả của step đó — không có 2 luồng cập
nhật riêng biệt nên không bao giờ bị lệch pha:
let steps = [];
let currentIndex = 0;
function renderStep(index) {
currentIndex = Math.max(0, Math.min(index, steps.length - 1));
const step = steps[currentIndex];
draw(step); // hàm vẽ RIÊNG cho từng thuật toán (dùng drawNode/drawBar ở trên)
logEl.textContent = step.desc; // luôn khớp 100% với những gì vừa vẽ
progressEl.textContent = `Bước ${currentIndex + 1} / ${steps.length}`;
}
draw() và cập nhật log ở
2 hàm callback khác nhau, mỗi hàm có setTimeout riêng. Nếu tốc độ
người dùng chọn thay đổi giữa chừng, 2 timer có thể trôi lệch nhau — hình vẽ đang ở bước 5 nhưng log
lại hiện mô tả bước 4. Gộp cả vẽ và log vào cùng 1 hàm nhận
currentIndex làm nguồn sự thật duy nhất (single source of truth) loại bỏ hoàn toàn khả
năng lệch pha này.
5. Chạy Thử: Cùng 1 Engine, 2 Thuật Toán Khác Hẳn Nhau
Sân chơi bên dưới dùng đúng 1 engine (play/pause/lùi/tiến/tốc độ/log) cho cả
Bubble Sort (dữ liệu dạng mảng, vẽ bằng drawBar) lẫn
BFS trên đồ thị (dữ liệu dạng node/cạnh, vẽ bằng
drawNode/drawEdge) — chỉ khác nhau ở hàm sinh step và hàm vẽ, phần điều
khiển hoàn toàn giống hệt nhau.
Chọn thuật toán
Nhật ký (đồng bộ với hình vẽ)
Đổi thuật toán để thấy CÙNG 1 bộ điều khiển chạy 2 loại dữ liệu khác nhau.
6. Tổng Kết Series: Nhìn Lại 11 Bài Đã Đi Qua
Mỗi bài trong series đều có thể mô tả lại vừa đúng bằng framework 3 phần ở trên — chỉ khác nhau ở cách sinh step và cách vẽ:
| Bài | Chủ đề | Ý chính |
|---|---|---|
| 01 | AVL & Red-Black Tree | Xoay cây để giữ cân bằng chiều cao O(log n) |
| 02 | Dijkstra & A* | Lan vùng theo trọng số, heuristic định hướng tìm kiếm |
| 03 | Sorting Visualizer | So sánh trực quan độ phức tạp các thuật toán sắp xếp |
| 04 | Trie | Cây tiền tố cho tra cứu/gợi ý từ theo từng ký tự |
| 05 | Union-Find | Path compression & union by rank gần như O(1) |
| 06 | Segment Tree / Fenwick Tree | Truy vấn khoảng & cập nhật điểm trong O(log n) |
| 07 | Quy Hoạch Động | Memoization/tabulation cho bài toán con chồng lấp |
| 08 | B-Tree Database Index | Cây bậc cao tối ưu số lần đọc/ghi ổ đĩa |
| 09 | Memory Allocator | Free list, First/Best/Worst-Fit, coalescing |
| 10 | Hash Table & Va Chạm | Chaining vs Open Addressing, tombstone, rehashing |
| 11 | Huffman Data Compression | Mã tiền tố tối ưu theo tần suất bằng thuật toán tham lam |
drawNode/drawEdge cho cây, Sorting cần
drawBar cho mảng, Hash Table cần cả hai (bucket dạng cột + chain dạng node nối tiếp).
Khác biệt thực tế giữa các sandbox trong series chủ yếu nằm ở
tương tác trực tiếp (click để xoá 1 khối nhớ, click để chèn 1 đỉnh đồ thị...) —
phần này khó tổng quát hoá tuyệt đối vì mỗi thao tác người dùng mang ý nghĩa khác nhau tuỳ thuật
toán. Framework "điều khiển bước" phù hợp nhất khi minh hoạ
một lần chạy trọn vẹn của thuật toán (như 2 demo phía trên); các sandbox có tương
tác click-để-thao-tác tuỳ ý (Bài 1, 8, 9, 10) vẫn cần thêm phần xử lý sự kiện chuột riêng bên ngoài
engine step.
Trắc nghiệm ôn tập
Câu 1: Vì sao generator function (function*/yield) phù hợp tự nhiên để
biến 1 thuật toán thành danh sách các "step"?
Trắc nghiệm ôn tập
Câu 2: Vì sao vật chất hoá (materialize) toàn bộ step vào 1 mảng lại giúp "lùi bước" (step back) gần như miễn phí?
Trắc nghiệm ôn tập
Câu 3: Lợi ích chính của việc gộp vẽ canvas và cập nhật log vào chung 1 hàm
renderStep(index) là gì?