Recursion & Backtracking — Nghệ thuật gọi tên chính mình
Vòng lặp for và while rất mạnh mẽ để duyệt mảng phẳng. Nhưng nếu bạn cần quét toàn bộ ổ cứng máy tính để tìm một file .txt?
Thư mục gốc chứa thư mục con, thư mục con lại chứa thư mục cháu... Bạn không thể biết trước cần lồng bao nhiêu vòng for cho đủ.
Khi cấu trúc dữ liệu bị phân nhánh vô tận (như Tree hay Graph), vòng lặp thông thường chính thức đầu hàng. Chào mừng bạn đến với Recursion (Đệ quy) và kỹ thuật đi kèm không thể tách rời: Backtracking (Quay lui).
📋 Agenda
Thời gian đọc ước tính: ~8 phút
Sau bài này, bạn sẽ:
- ✅ Hiểu được tại sao Đệ quy không phải là ma thuật, mà nó được điều khiển bằng Call Stack.
- ✅ Nhận diện được 2 thành phần BẮT BUỘC để một hàm đệ quy không làm sập Server.
- ✅ Trực quan hóa khái niệm Backtracking (Quay lui) qua ví dụ tìm đường trong mê cung.
- ✅ Tự tay implement bài toán Tổ hợp bằng Backtracking trong TypeScript.
Yêu cầu đầu vào:
- 🔹 Có kiến thức về Stack (Ngăn xếp).
❓ Tại sao hàm lại tự gọi chính nó?
Định nghĩa: Đệ quy (Recursion) là quá trình một hàm tự gọi lại chính nó bên trong phần thân của nó.
Vấn đề (Problem Statement):
Hãy tính Giai thừa của 5 (5! = 5 * 4 * 3 * 2 * 1).
Dùng vòng for chạy lùi rất dễ. Nhưng nhìn dưới góc độ toán học: 5! = 5 * 4!.
Và 4! = 4 * 3!.
... Thay vì lặp, chúng ta đang biến một bài toán lớn (Giai thừa 5) thành bài toán giống y hệt nhưng NHỎ HƠN (Giai thừa 4). Cứ bóc dần lớp vỏ như bóc củ hành cho đến khi đụng cái lõi. Đụng lõi xong mới quay ngược ra nhân kết quả lại.
📖 Trực quan hóa Call Stack của Đệ quy
JavaScript / TypeScript quản lý việc gọi hàm bằng Call Stack (Ngăn xếp). Ai gọi trước sẽ xếp dưới đáy, ai gọi sau xếp chồng lên trên. Đứa trên cùng chạy xong rút ra (Pop) thì đứa bên dưới mới được chạy tiếp.
function factorial(num: number): number {
if (num === 1) return 1; // 💡 ĐIỂM DỪNG (Base Case)
return num * factorial(num - 1); // 💡 GỌI LẠI CHÍNH NÓ VỚI INPUT NHỎ HƠN
}
factorial(4);
Mô phỏng Call Stack chạy hàm factorial(4):
- Call
factorial(4). Chờ kết quả của4 * factorial(3). -> Tạm dừng hàm, nhét vào Stack. - Call
factorial(3). Chờ kết quả của3 * factorial(2). -> Tạm dừng, nhét vào Stack. - Call
factorial(2). Chờ kết quả của2 * factorial(1). -> Tạm dừng, nhét vào Stack. - Call
factorial(1). Nhận thấynum === 1-> Trả về 1. Hàm này CHẠY XONG! Rút nó khỏi Stack.
Bây giờ, hiệu ứng Domino quay ngược lại (Pop Stack):
5. Hàm factorial(2) nhận được số 1. Tính 2 * 1 = 2. Trả về 2. Xong, rút!
6. Hàm factorial(3) nhận được số 2. Tính 3 * 2 = 6. Trả về 6. Xong, rút!
7. Hàm factorial(4) nhận được số 6. Tính 4 * 6 = 24. Trả về 24. Ngăn xếp trống rỗng. Xong nhiệm vụ!
⚠️ Định luật sinh tử của Đệ quy
Bất kì hàm đệ quy nào cũng bắt buộc phải có 2 thứ:
- Base Case (Điểm dừng): Nếu không có lệnh
if (num === 1) return 1, hàm sẽ gọi mãi xuống -1, -2, -3... cho đến khi Call Stack chứa không nổi nữa. Server sập với lỗi kinh điển: Stack Overflow. - Different Input (Thay đổi Input): Mỗi lần gọi lại chính nó, bạn phải thay đổi input (
num - 1). Nếu gọi lại với input cũ, nó sẽ lặp vô hạn.
🔨 Cảnh giới cao nhất: Backtracking (Quay lui)
Đệ quy tính giai thừa thì dễ, vì nó là một đường thẳng (như Array). Nhưng Đệ quy giải bài toán Tổ hợp hoặc Tìm đường thì nó là nhiều ngã rẽ. Khi bạn đi vào ngã rẽ 1, phát hiện nó là Ngõ cụt (hoặc đụng Base Case). Bạn lùi lại ngã 3 lúc nãy, và bắt đầu đi vào ngã rẽ 2. Quá trình "Lùi lại 1 bước, làm sạch vết chân, thử đường khác" đó gọi là Backtracking.