VieTopikKhoá học
  • Games
  • Cá nhân
Tải app
© 2026 VieTopikChính sách bảo mậtĐiều khoản sử dụngCài appEnglish
  • Trang chủ
  • Học
  • Luyện thi
  • Cẩm nang
  • Games
  • Cá nhân
Cấu trúc dữ liệu và giải thuậtĐo độ hiệu quả
Bài 2/18
6 phút

Đệ quy

Bình luận
Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt

Danh mục sản phẩm có danh mục con, danh mục con lại có danh mục con nữa. Viết vòng lặp cho dữ liệu lồng nhau không biết bao nhiêu tầng thì rất rối. Đệ quy giải những bài như vậy bằng cách cho method tự gọi lại chính nó. Merge sort, cây và đồ thị ở các chương sau đều dựa vào kỹ thuật này.

Khái niệm

🌀 Đệ quy (recursion): method tự gọi lại chính nó, mỗi lần với một bài toán nhỏ hơn.

🛑 Điểm dừng (base case): trường hợp nhỏ nhất mà method trả lời thẳng, không gọi lại chính nó nữa.

📚 Call stack: vùng nhớ ghi các lời gọi method đang chờ kết quả.

Gọi method thì call stack chồng thêm một tầng, return thì gỡ tầng trên cùng.

Method đệ quy luôn có hai phần: điểm dừng, và lời gọi lại với bài toán nhỏ hơn để tiến dần tới điểm dừng.

Ví dụ

Tính tổng giá: tổng tính từ vị trí index bằng giá ở index cộng tổng phần còn lại.

List<decimal> prices = new List<decimal>
{
    5000m, 12000m, 7000m
};
Console.WriteLine(Sum(prices, 0));   // 24000

decimal Sum(List<decimal> items, int index)
{
    if (index == items.Count)
    {
        return 0;   // điểm dừng: hết list
    }
    return items[index] + Sum(items, index + 1);
}
  • Điểm dừng là khi index đã qua phần tử cuối: không còn gì để cộng.
  • Mỗi lần gọi lại, index tăng 1, nên phần còn lại ngắn dần.
  • Sum(prices, 0) chờ Sum(prices, 1), Sum(prices, 1) lại chờ Sum(prices, 2). Tới điểm dừng, các kết quả cộng dồn ngược lên.

▶ Mô phỏng: Call stack của sum

List<decimal> prices = new List<decimal>
{
    5000m, 12000m, 7000m
};
Console.WriteLine(Sum(prices, 0));

decimal Sum(List<decimal> items, int index)
{
    if (index == items.Count)
    {
        return 0;   // điểm dừng: hết list
    }
    return items[index] + Sum(items, index + 1);
}
Xem từng bước
  1. Vào Sum(prices, 0): chồng thêm một tầng, call stack có 2 tầng. Kiểm tra index 0 == 3?
  2. Cần 5000 + Sum(prices, 1). Tầng này phải chờ kết quả của lời gọi bên trong.
  3. Vào Sum(prices, 1): chồng thêm một tầng, call stack có 3 tầng. Kiểm tra index 1 == 3?
  4. Cần 12000 + Sum(prices, 2). Tầng này phải chờ kết quả của lời gọi bên trong.
  5. Vào Sum(prices, 2): chồng thêm một tầng, call stack có 4 tầng. Kiểm tra index 2 == 3?
  6. Cần 7000 + Sum(prices, 3). Tầng này phải chờ kết quả của lời gọi bên trong.
  7. Vào Sum(prices, 3): chồng thêm một tầng, call stack có 5 tầng. Kiểm tra index 3 == 3?
  8. index 3 == 3: hết list, đây là điểm dừng. Trả 0, không gọi tiếp nữa.
  9. Tầng Sum(prices, 3) gỡ khỏi call stack, giá trị 0 trả về tầng dưới.
  10. Có kết quả 0: 7000 + 0 = 7000.
  11. Tầng Sum(prices, 2) gỡ khỏi call stack, trả 7000 về tầng dưới.
  12. Có kết quả 7000: 12000 + 7000 = 19000.
  13. Tầng Sum(prices, 1) gỡ khỏi call stack, trả 19000 về tầng dưới.
  14. Có kết quả 19000: 5000 + 19000 = 24000.
  15. Sum(prices, 0) trả 24000 về main. Call stack đã chồng tới 5 tầng: list càng dài, call stack càng cao.

Tính tổng thế này viết bằng vòng lặp cũng được, lại gọn hơn. Đệ quy thật sự có ích khi dữ liệu lồng nhau, như cây ở chương 5.

Thử ngay

Thay dòng in Sum(prices, 0) bằng đoạn dưới đây. Method CountDown in một dòng trước và một dòng sau lời gọi lại:

CountDown(3);

void CountDown(int n)
{
    if (n == 0)
    {
        return;
    }
    Console.WriteLine("Vào " + n);
    CountDown(n - 1);
    Console.WriteLine("Ra " + n);
}

Đoán trước khi chạy: sáu dòng in ra theo thứ tự nào?

Xem kết quả
Vào 3
Vào 2
Vào 1
Ra 1
Ra 2
Ra 3

Các dòng "Vào" in ra khi lời gọi chồng lên call stack. Dòng "Ra" chỉ chạy khi lời gọi bên trong đã xong, nên tầng vào sau cùng lại ra đầu tiên.

Lỗi hay gặp

Thiếu điểm dừng, hoặc gọi lại mà bài toán không nhỏ đi. Lời gọi chồng lên mãi tới khi call stack đầy. Chương trình sập với dòng Stack overflow., và try/catch cũng không bắt được lỗi này.

// SAI — index không tăng, không bao giờ tới điểm dừng
decimal Sum(List<decimal> items, int index)
{
    if (index == items.Count)
    {
        return 0;
    }
    return items[index] + Sum(items, index);
}
// ĐÚNG — mỗi lần gọi, phần còn lại ngắn đi một
decimal Sum(List<decimal> items, int index)
{
    if (index == items.Count)
    {
        return 0;
    }
    return items[index] + Sum(items, index + 1);
}

Tóm tắt

  • Đệ quy: method tự gọi lại chính nó với bài toán nhỏ hơn.
  • Luôn có điểm dừng, và mỗi lời gọi phải tiến gần điểm dừng hơn.
  • Call stack giữ các lời gọi đang chờ, vào sau thì ra trước.
  • Đệ quy không dừng làm sập chương trình với Stack overflow.
Big-OArray và List bên trong

Bình luận

Đang tải bình luận…

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt