Đệ quy
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 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,
indextă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
- 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?
- 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.
- 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?
- 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.
- 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?
- 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.
- 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?
- index 3 == 3: hết list, đây là điểm dừng. Trả 0, không gọi tiếp nữa.
- Tầng Sum(prices, 3) gỡ khỏi call stack, giá trị 0 trả về tầng dưới.
- Có kết quả 0: 7000 + 0 = 7000.
- Tầng Sum(prices, 2) gỡ khỏi call stack, trả 7000 về tầng dưới.
- Có kết quả 7000: 12000 + 7000 = 19000.
- Tầng Sum(prices, 1) gỡ khỏi call stack, trả 19000 về tầng dưới.
- Có kết quả 19000: 5000 + 19000 = 24000.
- 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 3Cá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.
Bình luận
Đang tải bình luận…