Nó phụ thuộc vào cách bạn xác định "đệ quy".
Nếu chúng tôi thực sự yêu cầu nó liên quan đến ngăn xếp cuộc gọi (hoặc bất kỳ cơ chế nào để duy trì trạng thái chương trình được sử dụng), thì chúng tôi luôn có thể thay thế nó bằng một cái gì đó không. Thật vậy, các ngôn ngữ dẫn đến việc sử dụng đệ quy một cách tự nhiên có xu hướng có các trình biên dịch sử dụng tối ưu hóa cuộc gọi đuôi, vì vậy những gì bạn viết là đệ quy nhưng những gì bạn chạy là lặp đi lặp lại.
Nhưng hãy xem xét trường hợp chúng tôi thực hiện cuộc gọi đệ quy và sử dụng kết quả của cuộc gọi đệ quy cho cuộc gọi đệ quy đó.
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
if (m == 0)
return n+1;
if (n == 0)
return Ackermann(m - 1, 1);
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
Thực hiện cuộc gọi đệ quy đầu tiên rất dễ dàng:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
if (m == 0)
return n+1;
if (n == 0)
{
m--;
n = 1;
goto restart;
}
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
Sau đó, chúng tôi có thể dọn sạch goto
để tránh xa Velociraptors và bóng râm của Dijkstra:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
while(m != 0)
{
if (n == 0)
{
m--;
n = 1;
}
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
return n+1;
}
Nhưng để loại bỏ các cuộc gọi đệ quy khác, chúng ta sẽ phải lưu các giá trị của một số cuộc gọi vào một ngăn xếp:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
Stack<BigInteger> stack = new Stack<BigInteger>();
stack.Push(m);
while(stack.Count != 0)
{
m = stack.Pop();
if(m == 0)
n = n + 1;
else if(n == 0)
{
stack.Push(m - 1);
n = 1;
}
else
{
stack.Push(m - 1);
stack.Push(m);
--n;
}
}
return n;
}
Bây giờ, khi chúng tôi xem xét mã nguồn, chúng tôi chắc chắn đã biến phương thức đệ quy của mình thành phương pháp lặp.
Xem xét những gì đã được biên dịch, chúng tôi đã biến mã sử dụng ngăn xếp cuộc gọi để thực hiện đệ quy thành mã không (và khi làm như vậy đã biến mã sẽ ném ngoại lệ ngăn xếp cho các giá trị khá nhỏ thành mã sẽ chỉ đơn thuần là mã mất nhiều thời gian để quay trở lại [xem Làm cách nào tôi có thể ngăn chức năng Ackerman của mình tràn ra khỏi ngăn xếp? để biết thêm một số tối ưu hóa khiến nó thực sự quay trở lại cho nhiều đầu vào khả dĩ hơn]).
Xem xét cách thức đệ quy được triển khai nói chung, chúng tôi đã biến mã sử dụng ngăn xếp cuộc gọi thành mã sử dụng một ngăn xếp khác để giữ các hoạt động đang chờ xử lý. Do đó, chúng tôi có thể lập luận rằng nó vẫn còn đệ quy, khi được xem xét ở mức thấp đó.
Và ở cấp độ đó, thực sự không có cách nào khác xung quanh nó. Vì vậy, nếu bạn coi phương pháp đó là đệ quy, thì thực sự có những điều chúng ta không thể làm nếu không có nó. Nói chung mặc dù chúng tôi không dán nhãn đệ quy như vậy. Thuật ngữ đệ quy rất hữu ích vì nó bao gồm một tập hợp các cách tiếp cận nhất định và cho chúng ta một cách để nói về chúng, và chúng ta không còn sử dụng một trong số chúng.
Tất nhiên, tất cả những điều này giả định rằng bạn có một sự lựa chọn. Có cả hai ngôn ngữ cấm các cuộc gọi đệ quy và các ngôn ngữ thiếu các cấu trúc vòng lặp cần thiết để lặp lại.