🔄 Đệ Quy - Recursion
Chào mừng đến với bài học về Recursion - phương thức tự gọi chính nó trong Café!
🎯 Món Ăn Hôm Nay
Tưởng tượng bạn đang đếm số ly cà phê trong kho:
- Đếm từng ly một → Gọi lại chính mình
- Dừng khi hết ly → Điều kiện dừng
- Giống gương soi gương → Lặp vô tận nếu không có điểm dừng
Đó chính là Recursion - hàm tự gọi chính nó!
🔄 Recursion Là Gì?
Recursion = Đệ quy (method tự gọi chính nó)
public static void demNguoc(int n) {
if (n == 0) { // Điều kiện dừng
System.out.println("Hết!");
return;
}
System.out.println(n);
demNguoc(n - 1); // Gọi lại chính nó
}
// Sử dụng
demNguoc(5); // 5, 4, 3, 2, 1, Hết!
☕ Ẩn Dụ Café:
- Recursion = Barista dạy barista mới (cứ truyền lại)
- Base case = Điều kiện dừng (khi nào xong)
- Recursive case = Bước tiếp theo (làm gì tiếp)
- Stack = Chồng ly (xếp chồng lên nhau)
📋 Cấu Trúc Recursion
Mọi hàm đệ quy cần 2 thành phần:
- Base Case (Điều kiện dừng): Khi nào dừng
- Recursive Case (Bước đệ quy): Gọi lại chính nó
public static int giaiThua(int n) {
// Base case: Điều kiện dừng
if (n <= 1) {
return 1;
}
// Recursive case: Gọi lại chính nó
return n * giaiThua(n - 1);
}
👨🍳 Câu Chuyện Trong Quán
Tình huống 1: Đếm Ngược
public class DemNguoc {
public static void demNguoc(int n) {
// Base case
if (n == 0) {
System.out.println("☕ Bắt đầu pha!");
return;
}
// In ra số hiện tại
System.out.println("Chuẩn bị... " + n);
// Recursive case
demNguoc(n - 1);
}
public static void main(String[] args) {
System.out.println("⏰ ĐẾM NGƯỢC PHA CÀ PHÊ:\n");
demNguoc(5);
}
}
Output:
⏰ ĐẾM NGƯỢC PHA CÀ PHÊ:
Chuẩn bị... 5
Chuẩn bị... 4
Chuẩn bị... 3
Chuẩn bị... 2
Chuẩn bị... 1
☕ Bắt đầu pha!
Tình huống 2: Tính Giai Thừa
public class GiaiThua {
public static int tinhGiaiThua(int n) {
// Base case
if (n <= 1) {
return 1;
}
// Recursive case
return n * tinhGiaiThua(n - 1);
}
public static void main(String[] args) {
System.out.println("📊 TÍNH GIAI THỪA:\n");
for (int i = 1; i <= 5; i++) {
int ketQua = tinhGiaiThua(i);
System.out.printf("%d! = %d%n", i, ketQua);
}
}
}
Output:
📊 TÍNH GIAI THỪA:
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
📝 Công Thức Nấu (Code Examples)
Ví Dụ 1: Fibonacci - Dãy Số
public class Fibonacci {
public static int fibonacci(int n) {
// Base case
if (n <= 1) {
return n;
}
// Recursive case
return fibonacci(n - 1) + fibonacci(n - 2);
}
public static void main(String[] args) {
System.out.println("🔢 DÃY FIBONACCI:\n");
for (int i = 0; i < 10; i++) {
System.out.print(fibonacci(i) + " ");
}
System.out.println();
}
}
Output:
🔢 DÃY FIBONACCI:
0 1 1 2 3 5 8 13 21 34
Giải thích:
fibonacci(5)
= fibonacci(4) + fibonacci(3)
= (fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1))
= ((fibonacci(2) + fibonacci(1)) + (fibonacci(1) + fibonacci(0))) + ...
Ví Dụ 2: Tính Tổng Từ 1 Đến N
public class TinhTong {
public static int tongSo(int n) {
// Base case
if (n == 1) {
return 1;
}
// Recursive case
return n + tongSo(n - 1);
}
public static void main(String[] args) {
int n = 10;
int ketQua = tongSo(n);
System.out.printf("Tổng từ 1 đến %d = %d%n", n, ketQua);
}
}
Output:
Tổng từ 1 đến 10 = 55
Ví Dụ 3: Đảo Ngược String
public class DaoNguocChuoi {
public static String daoNguoc(String str) {
// Base case: Chuỗi rỗng hoặc 1 ký tự
if (str.isEmpty()) {
return str;
}
// Recursive case: Lấy ký tự cuối + đảo ngược phần còn lại
return str.charAt(str.length() - 1)
+ daoNguoc(str.substring(0, str.length() - 1));
}
public static void main(String[] args) {
String tenMon = "Latte";
String daoNguoc = daoNguoc(tenMon);
System.out.println("Gốc: " + tenMon);
System.out.println("Đảo: " + daoNguoc);
}
}
Output:
Gốc: Latte
Đảo: ettaL
Ví Dụ 4: Tính Lũy Thừa
public class LuyThua {
public static int luyThua(int coSo, int soMu) {
// Base case
if (soMu == 0) {
return 1;
}
// Recursive case
return coSo * luyThua(coSo, soMu - 1);
}
public static void main(String[] args) {
System.out.println("📐 TÍNH LŨY THỪA:\n");
System.out.println("2^0 = " + luyThua(2, 0));
System.out.println("2^3 = " + luyThua(2, 3));
System.out.println("5^4 = " + luyThua(5, 4));
}
}
Output:
📐 TÍNH LŨY THỪA:
2^0 = 1
2^3 = 8
5^4 = 625
Ví Dụ 5: Đếm Số Chữ Số
public class DemChuSo {
public static int demChuSo(int n) {
// Base case
if (n < 10) {
return 1;
}
// Recursive case
return 1 + demChuSo(n / 10);
}
public static void main(String[] args) {
int[] soLuong = {5, 50, 500, 5000, 50000};
System.out.println("🔢 ĐếM CHỮ SỐ:\n");
for (int so : soLuong) {
int dem = demChuSo(so);
System.out.printf("%,6d → %d chữ số%n", so, dem);
}
}
}
Output:
🔢 ĐếM CHỮ SỐ:
5 → 1 chữ số
50 → 2 chữ số
500 → 3 chữ số
5,000 → 4 chữ số
50,000 → 5 chữ số
Ví Dụ 6: Tìm Ước Chung Lớn Nhất (GCD)
public class UocChung {
public static int gcd(int a, int b) {
// Base case
if (b == 0) {
return a;
}
// Recursive case: Thuật toán Euclid
return gcd(b, a % b);
}
public static void main(String[] args) {
System.out.println("🔢 ƯỚC CHUNG LỚN NHẤT:\n");
System.out.println("GCD(48, 18) = " + gcd(48, 18));
System.out.println("GCD(100, 50) = " + gcd(100, 50));
System.out.println("GCD(17, 13) = " + gcd(17, 13));
}
}
Output:
🔢 ƯỚC CHUNG LỚN NHẤT:
GCD(48, 18) = 6
GCD(100, 50) = 50
GCD(17, 13) = 1
🔥 Thực Hành Trong Quán
Bài Tập 1: In Menu Lồng Nhau
public class MenuLong {
public static void inMenu(String[] items, int index, int level) {
// Base case
if (index >= items.length) {
return;
}
// In thụt lề theo level
for (int i = 0; i < level; i++) {
System.out.print(" ");
}
System.out.println("- " + items[index]);
// Recursive case
inMenu(items, index + 1, level + 1);
}
public static void main(String[] args) {
String[] menu = {"Cà phê", "Espresso", "Latte", "Mocha", "Cappuccino"};
System.out.println("☕ MENU PHÂN CẤP:\n");
inMenu(menu, 0, 0);
}
}
Output:
☕ MENU PHÂN CẤP:
- Cà phê
- Espresso
- Latte
- Mocha
- Cappuccino
Bài Tập 2: Tính Tổng Các Chữ Số
public class TongChuSo {
public static int tongChuSo(int n) {
// Base case
if (n == 0) {
return 0;
}
// Recursive case
return (n % 10) + tongChuSo(n / 10);
}
public static void main(String[] args) {
int[] giaTri = {12345, 50000, 99999};
System.out.println("➕ TỔNG CÁC CHỮ SỐ:\n");
for (int gia : giaTri) {
int tong = tongChuSo(gia);
System.out.printf("%,6d → Tổng: %d%n", gia, tong);
}
}
}
Output:
➕ TỔNG CÁC CHỮ SỐ:
12,345 → Tổng: 15
50,000 → Tổng: 5
99,999 → Tổng: 45
Bài Tập 3: Kiểm Tra Palindrome (Đối Xứng)
public class KiemTraDoiXung {
public static boolean laPalindrome(String str) {
// Base case: Chuỗi rỗng hoặc 1 ký tự
if (str.length() <= 1) {
return true;
}
// Kiểm tra ký tự đầu và cuối
if (str.charAt(0) != str.charAt(str.length() - 1)) {
return false;
}
// Recursive case: Kiểm tra phần giữa
return laPalindrome(str.substring(1, str.length() - 1));
}
public static void main(String[] args) {
String[] ten = {"Latte", "madam", "noon", "Mocha", "level"};
System.out.println("🔄 KIỂM TRA PALINDROME:\n");
for (String t : ten) {
boolean ketQua = laPalindrome(t.toLowerCase());
System.out.printf("%-8s: %s%n", t, ketQua ? "✅ Đối xứng" : "❌ Không");
}
}
}
Output:
🔄 KIỂM TRA PALINDROME:
Latte : ❌ Không
madam : ✅ Đối xứng
noon : ✅ Đối xứng
Mocha : ❌ Không
level : ✅ Đối xứng
Bài Tập 4: Tìm Max Trong Mảng
public class TimMax {
public static int timMax(int[] arr, int index) {
// Base case: Phần tử cuối
if (index == arr.length - 1) {
return arr[index];
}
// Recursive case
int maxConLai = timMax(arr, index + 1);
return Math.max(arr[index], maxConLai);
}
public static void main(String[] args) {
int[] gia = {45000, 60000, 50000, 55000, 48000};
int max = timMax(gia, 0);
System.out.println("💰 BẢNG GIÁ:");
for (int g : gia) {
System.out.printf("%,d VND%n", g);
}
System.out.printf("%n🏆 Giá cao nhất: %,d VND%n", max);
}
}
⚠️ Lỗi Thường Gặp
Lỗi 1: Thiếu Base Case (StackOverflowError)
// ❌ SAI: Không có điều kiện dừng
public static void demMaiMai(int n) {
System.out.println(n);
demMaiMai(n - 1); // ❌ Lỗi! Chạy vô tận
}
// ✅ ĐÚNG: Có base case
public static void dem(int n) {
if (n == 0) { // ✅ Điều kiện dừng
return;
}
System.out.println(n);
dem(n - 1);
}
Lỗi 2: Base Case Sai
// ❌ SAI: Base case không bao giờ đạt được
public static int giaiThua(int n) {
if (n == 0) { // Đúng
return 1;
}
return n * giaiThua(n - 2); // ❌ Lỗi! Bỏ qua n=1
}
// ✅ ĐÚNG: Base case phù hợp
public static int giaiThua(int n) {
if (n <= 1) { // ✅ Bắt cả n=0 và n=1
return 1;
}
return n * giaiThua(n - 1);
}
Lỗi 3: Quá Nhiều Đệ Quy (Hiệu Năng Kém)
// ❌ SAI: Fibonacci đệ quy thuần túy (chậm)
public static int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2); // Tính lại nhiều lần
}
// ✅ ĐÚNG: Dùng memoization hoặc iteration
public static int fibIterative(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
Lỗi 4: Không Trả Về Giá Trị Trong Base Case
// ❌ SAI: Base case không return
public static int tinhTong(int n) {
if (n == 1) {
// Thiếu return!
}
return n + tinhTong(n - 1);
}
// ✅ ĐÚNG: Base case có return
public static int tinhTong(int n) {
if (n == 1) {
return 1; // ✅ Có return
}
return n + tinhTong(n - 1);
}
💡 Bí Quyết Của Barista
- Luôn có base case: Điều kiện dừng rõ ràng
- Tiến về base case: Mỗi lần gọi phải gần base case hơn
- Đơn giản hóa vấn đề: Chia nhỏ bài toán
- Kiểm tra hiệu năng: Recursion có thể chậm
- Tránh đệ quy sâu: StackOverflowError nếu quá sâu
- Cân nhắc iteration: Đôi khi vòng lặp tốt hơn
🎓 Bạn Đã Học Được
- ✅ Recursion = Hàm tự gọi chính nó
- ✅ Base case = Điều kiện dừng (bắt buộc)
- ✅ Recursive case = Bước đệ quy
- ✅ Phải tiến về base case
- ✅ Ứng dụng: Giai thừa, Fibonacci, đảo ngược, tìm kiếm
- ✅ Lưu ý: StackOverflowError, hiệu năng
- ✅ So sánh: Recursion vs Iteration
☕ Món Tiếp Theo
Đã biết recursion! Giờ học về kế thừa (extends):
💡 Lời Khuyên Cuối: Recursion như công thức truyền miệng - chia sẻ từng bước cho đến khi ai cũng biết!