Đệ quy đuôi là gì?


1695

Trong khi bắt đầu học lisp, tôi đã bắt gặp thuật ngữ đệ quy đuôi . điều đó có chính xác?


155
Đối với tò mò: cả trong và trong khi đã ở trong ngôn ngữ trong một thời gian rất dài. Trong khi được sử dụng trong tiếng Anh cổ; Trong khi đó là một sự phát triển của tiếng Anh trong khi. Là liên từ, chúng có thể hoán đổi cho nhau về ý nghĩa, nhưng trong khi vẫn không tồn tại trong tiếng Anh Mỹ chuẩn.
Filip Bartuzi

14
Có lẽ nó là muộn, nhưng đây là một bài viết khá tốt về đuôi đệ quy: programmerinterview.com/index.php/recursion/tail-recursion
Sam003

5
Một trong những lợi ích tuyệt vời của việc xác định hàm đệ quy đuôi là nó có thể được chuyển đổi thành một dạng lặp và do đó làm sống lại thuật toán từ phương thức-stack-over. Có thể muốn truy cập phản hồi từ @Kyle Cronin và một vài người khác bên dưới
KGhatak

Liên kết này từ @yesudeep là mô tả chi tiết nhất, tốt nhất tôi đã tìm thấy - lua.org/pil/6.3.html
Jeff Fischer

1
Ai đó có thể cho tôi biết, Do hợp nhất sắp xếp và sắp xếp nhanh chóng sử dụng đệ quy đuôi (TRO)?
Majurageerthan

Câu trả lời:


1721

Hãy xem xét một hàm đơn giản có thêm N số tự nhiên đầu tiên. (ví dụ sum(5) = 1 + 2 + 3 + 4 + 5 = 15).

Đây là một triển khai JavaScript đơn giản sử dụng đệ quy:

function recsum(x) {
    if (x === 1) {
        return x;
    } else {
        return x + recsum(x - 1);
    }
}

Nếu bạn đã gọi recsum(5), đây là những gì trình thông dịch JavaScript sẽ đánh giá:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15

Lưu ý cách mọi cuộc gọi đệ quy phải hoàn thành trước khi trình thông dịch JavaScript bắt đầu thực sự thực hiện công việc tính tổng.

Đây là phiên bản đệ quy đuôi của cùng chức năng:

function tailrecsum(x, running_total = 0) {
    if (x === 0) {
        return running_total;
    } else {
        return tailrecsum(x - 1, running_total + x);
    }
}

Đây là chuỗi các sự kiện sẽ xảy ra nếu bạn gọi tailrecsum(5), (có hiệu quả sẽ làtailrecsum(5, 0) , vì đối số thứ hai mặc định).

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

Trong trường hợp đệ quy đuôi, với mỗi đánh giá của lệnh gọi đệ quy, running_total được cập nhật.

Lưu ý: Câu trả lời ban đầu được sử dụng ví dụ từ Python. Chúng đã được thay đổi thành JavaScript, vì trình thông dịch Python không hỗ trợ tối ưu hóa cuộc gọi đuôi . Tuy nhiên, trong khi tối ưu hóa cuộc gọi đuôi là một phần của thông số ECMAScript 2015 , hầu hết các trình thông dịch JavaScript không hỗ trợ nó .


32
Tôi có thể nói rằng với đệ quy đuôi, câu trả lời cuối cùng được tính bằng cách gọi LAST của phương thức không? Nếu nó không phải là đệ quy đuôi, bạn cần tất cả các kết quả cho tất cả các phương pháp để tính toán câu trả lời.
chrisapotek

2
Đây là một phụ lục trình bày một vài ví dụ trong Lua: lua.org/pil/6.3.html Có thể hữu ích để vượt qua điều đó! :)
yesudeep

2
Ai đó có thể vui lòng giải quyết câu hỏi của chrisapotek? Tôi bối rối làm thế nào tail recursioncó thể đạt được bằng một ngôn ngữ không tối ưu hóa các cuộc gọi đuôi.
Kevin Meredith

3
@KevinMeredith "đệ quy đuôi" có nghĩa là câu lệnh cuối cùng trong một hàm, là một lệnh gọi đệ quy đến cùng hàm đó. Bạn đúng rằng không có điểm nào trong việc thực hiện điều này bằng ngôn ngữ không tối ưu hóa đệ quy đó. Tuy nhiên, câu trả lời này cho thấy khái niệm (gần như) chính xác. Nó rõ ràng hơn là một cuộc gọi đuôi, nếu "khác:" bị bỏ qua. Sẽ không thay đổi hành vi, nhưng sẽ thực hiện cuộc gọi đuôi như một tuyên bố độc lập. Tôi sẽ gửi nó như là một chỉnh sửa.
ToolmakerSteve 12/12/13

2
Vì vậy, trong python không có lợi thế bởi vì với mỗi lệnh gọi hàm tailrecsum, một khung stack mới được tạo - phải không?
Quazi Irfan

707

Trong đệ quy truyền thống , mô hình điển hình là bạn thực hiện các cuộc gọi đệ quy trước, sau đó bạn lấy giá trị trả về của cuộc gọi đệ quy và tính kết quả. Theo cách này, bạn không nhận được kết quả tính toán cho đến khi bạn quay lại sau mỗi cuộc gọi đệ quy.

Trong đệ quy đuôi , bạn thực hiện các tính toán của mình trước, và sau đó bạn thực hiện lệnh gọi đệ quy, chuyển kết quả của bước hiện tại của bạn sang bước đệ quy tiếp theo. Điều này dẫn đến tuyên bố cuối cùng ở dạng (return (recursive-function params)). Về cơ bản, giá trị trả về của bất kỳ bước đệ quy đã cho nào cũng giống như giá trị trả về của lệnh gọi đệ quy tiếp theo .

Hậu quả của việc này là một khi bạn đã sẵn sàng thực hiện bước đệ quy tiếp theo, bạn không cần khung ngăn xếp hiện tại nữa. Điều này cho phép một số tối ưu hóa. Trong thực tế, với một trình biên dịch bằng văn bản một cách thích hợp, bạn nên không bao giờ có một chồng tràn snicker với một cuộc gọi đệ quy đuôi. Chỉ cần sử dụng lại khung stack hiện tại cho bước đệ quy tiếp theo. Tôi khá chắc chắn Lisp làm điều này.


17
"Tôi khá chắc chắn Lisp làm điều này" - Scheme làm, nhưng Common Lisp không luôn như vậy.
Aaron

2
@Daniel "Về cơ bản, giá trị trả về của bất kỳ bước đệ quy nào cũng giống như giá trị trả về của lệnh gọi đệ quy tiếp theo." - Tôi không thấy đối số này đúng với đoạn mã được đăng bởi Lorin Hochstein. Bạn có thể vui lòng giải thích?
Geek

8
@Geek Đây là một phản hồi thực sự muộn, nhưng điều đó thực sự đúng trong ví dụ của Lorin Hochstein. Việc tính toán cho từng bước được thực hiện trước cuộc gọi đệ quy, thay vì sau cuộc gọi. Kết quả là, mỗi điểm dừng chỉ trả về giá trị trực tiếp từ bước trước đó. Cuộc gọi đệ quy cuối cùng kết thúc việc tính toán và sau đó trả về kết quả cuối cùng không được sửa đổi trong suốt quá trình quay lại ngăn xếp cuộc gọi.
thiệu lại vào

3
Scala có nhưng bạn cần @tailrec được chỉ định để thực thi nó.
SilentDirge

2
"Theo cách này, bạn không nhận được kết quả tính toán cho đến khi bạn quay lại sau mỗi cuộc gọi đệ quy." - có thể tôi đã hiểu nhầm điều này, nhưng điều này không đặc biệt đúng đối với các ngôn ngữ lười biếng trong đó phép đệ quy truyền thống là cách duy nhất để thực sự có được kết quả mà không gọi tất cả các lần truy cập (ví dụ: xếp một danh sách Bools vô hạn với &&).
hasufell

206

Một điểm quan trọng là đệ quy đuôi về cơ bản tương đương với vòng lặp. Đây không chỉ là vấn đề tối ưu hóa trình biên dịch, mà còn là một thực tế cơ bản về tính biểu cảm. Điều này đi cả hai cách: bạn có thể thực hiện bất kỳ vòng lặp của biểu mẫu

while(E) { S }; return Q

trong đó EQlà các biểu thức và Slà một chuỗi các câu lệnh, và biến nó thành một hàm đệ quy đuôi

f() = if E then { S; return f() } else { return Q }

Tất nhiên, E, S, và Qphải được xác định để tính toán một số giá trị thú vị trên một số biến. Ví dụ, chức năng lặp

sum(n) {
  int i = 1, k = 0;
  while( i <= n ) {
    k += i;
    ++i;
  }
  return k;
}

tương đương với (các) hàm đệ quy đuôi

sum_aux(n,i,k) {
  if( i <= n ) {
    return sum_aux(n,i+1,k+i);
  } else {
    return k;
  }
}

sum(n) {
  return sum_aux(n,1,0);
}

(Việc "gói" chức năng đệ quy đuôi này với một hàm có ít tham số là thành ngữ chức năng phổ biến.)


Trong câu trả lời của @LorinHochstein, tôi đã hiểu, dựa trên lời giải thích của anh ấy, rằng đệ quy đuôi là khi phần đệ quy theo sau "Trả về", tuy nhiên trong phần của bạn, phần đệ quy đuôi thì không. Bạn có chắc chắn ví dụ của bạn được xem xét đệ quy đuôi đúng không?
CodyBugstein

1
@Imray Phần đệ quy đuôi là câu lệnh "return sum_aux" bên trong sum_aux.
Chris Conway

1
@lmray: Mã của Chris về cơ bản là tương đương. Thứ tự của if / then và kiểu của bài kiểm tra giới hạn ... nếu x == 0 so với if (i <= n) ... không phải là thứ gì đó bị treo lên. Vấn đề là mỗi lần lặp lại chuyển kết quả của nó sang lần tiếp theo.
Taylor

else { return k; }có thể được đổi thànhreturn k;
c0der

144

Đoạn trích này từ cuốn sách Lập trình trong Lua chỉ ra cách thực hiện đệ quy đuôi thích hợp (trong Lua, nhưng cũng nên áp dụng cho Lisp) và tại sao nó tốt hơn.

Một cuộc gọi đuôi [đệ quy đuôi] là một loại goto mặc quần áo như một cuộc gọi. Một cuộc gọi đuôi xảy ra khi một chức năng gọi một hành động khác là hành động cuối cùng của nó, vì vậy nó không có gì khác để làm. Chẳng hạn, trong đoạn mã sau, cuộc gọi đến glà cuộc gọi đuôi:

function f (x)
  return g(x)
end

Sau fcuộc gọig , nó không có gì khác để làm. Trong các tình huống như vậy, chương trình không cần quay lại chức năng gọi khi chức năng được gọi kết thúc. Do đó, sau cuộc gọi đuôi, chương trình không cần giữ bất kỳ thông tin nào về chức năng gọi trong ngăn xếp. ...

Bởi vì một cuộc gọi đuôi thích hợp sử dụng không có không gian ngăn xếp, không có giới hạn về số lượng các cuộc gọi đuôi "lồng nhau" mà một chương trình có thể thực hiện. Chẳng hạn, chúng ta có thể gọi hàm sau với bất kỳ số nào làm đối số; nó sẽ không bao giờ tràn vào ngăn xếp:

function foo (n)
  if n > 0 then return foo(n - 1) end
end

... Như tôi đã nói trước đó, một cuộc gọi đuôi là một loại goto. Như vậy, một ứng dụng khá hữu ích của các cuộc gọi đuôi thích hợp trong Lua dành cho các máy trạng thái lập trình. Các ứng dụng như vậy có thể đại diện cho mỗi tiểu bang bằng một chức năng; để thay đổi trạng thái là đi đến (hoặc gọi) một chức năng cụ thể. Ví dụ, chúng ta hãy xem xét một trò chơi mê cung đơn giản. Mê cung có một số phòng, mỗi phòng có tới bốn cửa: bắc, nam, đông và tây. Ở mỗi bước, người dùng đi vào một hướng di chuyển. Nếu có một cánh cửa theo hướng đó, người dùng sẽ đi đến phòng tương ứng; mặt khác, chương trình in một cảnh báo. Mục tiêu là đi từ phòng ban đầu đến phòng cuối cùng.

Trò chơi này là một máy trạng thái điển hình, trong đó phòng hiện tại là trạng thái. Chúng tôi có thể thực hiện mê cung như vậy với một chức năng cho mỗi phòng. Chúng tôi sử dụng các cuộc gọi đuôi để di chuyển từ phòng này sang phòng khác. Một mê cung nhỏ với bốn phòng có thể trông như thế này:

function room1 ()
  local move = io.read()
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
       return room1()   -- stay in the same room
  end
end

function room2 ()
  local move = io.read()
  if move == "south" then return room4()
  elseif move == "west" then return room1()
  else print("invalid move")
       return room2()
  end
end

function room3 ()
  local move = io.read()
  if move == "north" then return room1()
  elseif move == "east" then return room4()
  else print("invalid move")
       return room3()
  end
end

function room4 ()
  print("congratulations!")
end

Vì vậy, bạn thấy, khi bạn thực hiện một cuộc gọi đệ quy như:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

Đây không phải là đệ quy đuôi vì bạn vẫn còn việc phải làm (thêm 1) trong chức năng đó sau khi thực hiện cuộc gọi đệ quy. Nếu bạn nhập một số rất cao, nó có thể sẽ gây ra tràn ngăn xếp.


9
Đây là một câu trả lời tuyệt vời vì nó giải thích ý nghĩa của các cuộc gọi đuôi theo kích thước ngăn xếp.
Andrew Swan

@AndrewSwan Thật vậy, mặc dù tôi tin rằng người hỏi ban đầu và người đọc thỉnh thoảng có thể vấp phải câu hỏi này có thể được phục vụ tốt hơn với câu trả lời được chấp nhận (vì anh ta có thể không biết stack thực sự là gì.) Theo cách tôi sử dụng Jira, lớn quạt.
Hoffmann

1
Câu trả lời yêu thích của tôi là do bao gồm cả hàm ý cho kích thước ngăn xếp.
njk2015

80

Sử dụng đệ quy thông thường, mỗi cuộc gọi đệ quy sẽ đẩy một mục khác vào ngăn xếp cuộc gọi. Khi đệ quy hoàn tất, ứng dụng sẽ phải bật từng mục xuống.

Với đệ quy đuôi, tùy thuộc vào ngôn ngữ, trình biên dịch có thể thu gọn ngăn xếp xuống một mục, vì vậy bạn tiết kiệm không gian ngăn xếp ... Một truy vấn đệ quy lớn thực sự có thể gây ra tràn ngăn xếp.

Về cơ bản thu hồi đuôi có thể được tối ưu hóa thành lặp.


1
"Một truy vấn đệ quy lớn thực sự có thể gây ra tràn ngăn xếp." nên ở đoạn 1 chứ không phải ở đoạn 2 (đệ quy đuôi)? Ưu điểm lớn của đệ quy đuôi là nó có thể (ví dụ: Lược đồ) được tối ưu hóa theo cách không "tích lũy" các cuộc gọi trong ngăn xếp, do đó, hầu như sẽ tránh tràn ngăn xếp!
Olivier Dulac

70

Tệp biệt ngữ có điều này để nói về định nghĩa của đệ quy đuôi:

đệ quy đuôi /n./

Nếu bạn chưa phát bệnh, hãy xem đệ quy đuôi.


68

Thay vì giải thích nó bằng lời nói, đây là một ví dụ. Đây là phiên bản Scheme của chức năng giai thừa:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

Đây là một phiên bản của giai thừa được đệ quy đuôi:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

Bạn sẽ nhận thấy trong phiên bản đầu tiên rằng cuộc gọi đệ quy đến thực tế được đưa vào biểu thức nhân, và do đó trạng thái phải được lưu trên ngăn xếp khi thực hiện cuộc gọi đệ quy. Trong phiên bản đệ quy đuôi, không có biểu thức S nào khác đang chờ giá trị của lệnh gọi đệ quy và vì không có việc gì để làm nữa, trạng thái không phải được lưu trên ngăn xếp. Theo quy định, các hàm đệ quy đuôi Scheme sử dụng không gian ngăn xếp không đổi.


4
+1 để đề cập đến khía cạnh quan trọng nhất của việc thu hồi đuôi mà chúng có thể được chuyển đổi thành dạng lặp và do đó biến nó thành dạng phức tạp bộ nhớ O (1).
KGhatak

1
@KGhatak không chính xác; câu trả lời nói chính xác về "không gian ngăn xếp không đổi", không phải bộ nhớ nói chung. không được gây nghiện, chỉ để đảm bảo không có sự hiểu lầm. ví dụ, quy list-reversetrình đột biến danh sách đuôi đệ quy sẽ chạy trong không gian ngăn xếp không đổi nhưng sẽ tạo và phát triển cấu trúc dữ liệu trên heap. Một giao dịch cây có thể sử dụng một ngăn xếp mô phỏng, trong một đối số bổ sung. vv
Will Ness

45

Đệ quy đuôi đề cập đến lệnh gọi đệ quy là cuối cùng trong lệnh logic cuối cùng trong thuật toán đệ quy.

Thông thường trong đệ quy, bạn có một trường hợp cơ sở là trường hợp dừng các cuộc gọi đệ quy và bắt đầu bật ngăn xếp cuộc gọi. Để sử dụng một ví dụ cổ điển, mặc dù nhiều C-ish hơn Lisp, hàm giai thừa minh họa đệ quy đuôi. Cuộc gọi đệ quy xảy ra sau khi kiểm tra điều kiện trường hợp cơ sở.

factorial(x, fac=1) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

Cuộc gọi ban đầu đến giai thừa sẽ là factorial(n)nơi fac=1(giá trị mặc định) và n là số mà giai thừa được tính.


Tôi thấy giải thích của bạn dễ hiểu nhất, nhưng nếu có bất cứ điều gì xảy ra, thì đệ quy đuôi chỉ hữu ích cho các hàm với một trường hợp cơ sở câu lệnh. Hãy xem xét một phương pháp như postimg.cc/5Yg3Cdjn này . Lưu ý: bên ngoài elselà bước bạn có thể gọi là "trường hợp cơ sở" nhưng trải dài trên một số dòng. Tôi đang hiểu lầm bạn hay giả định của tôi là đúng? Đệ quy đuôi chỉ tốt cho một lớp lót?
Tôi muốn trả lời

2
@IWantAnswers - Không, phần thân của hàm có thể lớn tùy ý. Tất cả những gì cần thiết cho một cuộc gọi đuôi là chi nhánh mà nó gọi hàm là điều cuối cùng nó thực hiện và trả về kết quả của việc gọi hàm. Các factorialví dụ chỉ là ví dụ đơn giản cổ điển, đó là tất cả.
TJ Crowder

28

Điều đó có nghĩa là thay vì cần phải đẩy con trỏ lệnh trên ngăn xếp, bạn có thể chỉ cần nhảy lên đỉnh của hàm đệ quy và tiếp tục thực hiện. Điều này cho phép các chức năng lặp lại vô thời hạn mà không làm tràn ngăn xếp.

Tôi đã viết một bài đăng trên blog về chủ đề này, trong đó có các ví dụ đồ họa về các khung stack trông như thế nào.


21

Đây là một đoạn mã nhanh so sánh hai chức năng. Đầu tiên là đệ quy truyền thống để tìm giai thừa của một số đã cho. Thứ hai sử dụng đệ quy đuôi.

Rất đơn giản và trực quan để hiểu.

Một cách dễ dàng để biết nếu một hàm đệ quy là một đệ quy đuôi là nếu nó trả về một giá trị cụ thể trong trường hợp cơ sở. Có nghĩa là nó không trả về 1 hoặc đúng hoặc bất cứ điều gì tương tự. Nó nhiều khả năng sẽ trả về một số biến thể của một trong các tham số phương thức.

Một cách khác để nói là nếu cuộc gọi đệ quy không có bất kỳ bổ sung, số học, sửa đổi, v.v ... Có nghĩa là không có gì ngoài một cuộc gọi đệ quy thuần túy.

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}

3
0! là 1. Vậy "mynumber == 1" phải là "mynumber == 0".
lịch trình

19

Cách tốt nhất để tôi hiểu tail call recursionlà một trường hợp đệ quy đặc biệt trong đó cuộc gọi cuối cùng (hoặc cuộc gọi đuôi) là chính chức năng.

So sánh các ví dụ được cung cấp trong Python:

def recsum(x):
 if x == 1:
  return x
 else:
  return x + recsum(x - 1)

^ TUYỂN DỤNG

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

^ TUYỂN DỤNG

Như bạn có thể thấy trong phiên bản đệ quy chung, cuộc gọi cuối cùng trong khối mã là x + recsum(x - 1). Vì vậy, sau khi gọi recsumphương thức, có một hoạt động khác đó là x + ...

Tuy nhiên, trong phiên bản đệ quy đuôi, cuộc gọi cuối cùng (hoặc cuộc gọi đuôi) trong khối mã tailrecsum(x - 1, running_total + x)có nghĩa là cuộc gọi cuối cùng được thực hiện cho chính phương thức đó và không có thao tác nào sau đó.

Điểm này rất quan trọng vì đệ quy đuôi như đã thấy ở đây không làm cho bộ nhớ tăng lên vì khi VM bên dưới thấy một hàm tự gọi ở vị trí đuôi (biểu thức cuối cùng được đánh giá trong hàm), nó sẽ loại bỏ khung stack hiện tại được gọi là Tối ưu hóa cuộc gọi đuôi (TCO).

BIÊN TẬP

Lưu ý Hãy nhớ rằng ví dụ trên được viết bằng Python có thời gian chạy không hỗ trợ TCO. Đây chỉ là một ví dụ để giải thích điểm. TCO được hỗ trợ trong các ngôn ngữ như Scheme, Haskell, v.v.


12

Trong Java, đây là một triển khai đệ quy đuôi có thể có của hàm Fibonacci:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

Tương phản điều này với việc thực hiện đệ quy tiêu chuẩn:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}

1
Điều này đang trả về kết quả sai cho tôi, đối với đầu vào 8 tôi nhận được 36, nó phải là 21. Tôi có thiếu điều gì không? Tôi đang sử dụng java và sao chép nó.
Alberto Zaccagni

1
Điều này trả về SUM (i) cho i trong [1, n]. Không có gì để làm với Fftimeacci. Đối với một Fibbo, bạn cần có một bài kiểm tra mà substracts iterđến acckhi iter < (n-1).
Askolein

10

Tôi không phải là lập trình viên Lisp, nhưng tôi nghĩ điều này sẽ giúp ích.

Về cơ bản đó là một phong cách lập trình sao cho cuộc gọi đệ quy là điều cuối cùng bạn làm.


10

Dưới đây là một ví dụ Lisp phổ biến thực hiện giai thừa sử dụng đệ quy đuôi. Do tính chất không có chồng, người ta có thể thực hiện các tính toán nhân tử cực lớn ...

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

Và sau đó để giải trí, bạn có thể thử (format nil "~R" (! 25))


9

Nói tóm lại, một đệ quy đuôi có lệnh gọi đệ quy là câu lệnh cuối cùng trong hàm để nó không phải chờ cuộc gọi đệ quy.

Vì vậy, đây là một đệ quy đuôi, tức là N (x - 1, p * x) là câu lệnh cuối cùng trong hàm mà trình biên dịch thông minh để tìm ra rằng nó có thể được tối ưu hóa thành một vòng lặp for (giai thừa). Tham số thứ hai p mang giá trị sản phẩm trung gian.

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

Đây là cách viết đệ quy không hàm đệ quy ở trên (mặc dù một số trình biên dịch C ++ có thể có thể tối ưu hóa nó bằng mọi cách).

function N(x) {
   return x == 1 ? 1 : x * N(x - 1);
}

nhưng đây không phải là:

function F(x) {
  if (x == 1) return 0;
  if (x == 2) return 1;
  return F(x - 1) + F(x - 2);
}

Tôi đã viết một bài viết dài có tiêu đề " Hiểu về đệ quy đuôi - Visual Studio C ++ - Chế độ xem hội "

nhập mô tả hình ảnh ở đây


1
Làm thế nào là chức năng của bạn N đuôi đệ quy?
Fabian Pijcke

N (x-1) là câu lệnh cuối cùng trong hàm mà trình biên dịch thông minh để tìm ra rằng nó có thể được tối ưu hóa thành một vòng lặp for (giai thừa)
Doctorlai

Mối quan tâm của tôi là hàm N của bạn chính xác là hàm recsum từ câu trả lời được chấp nhận của chủ đề này (ngoại trừ đó là một tổng và không phải là sản phẩm), và recsum được cho là không đệ quy?
Fabian Pijcke

8

đây là phiên bản Perl 5 của tailrecsumchức năng được đề cập trước đó.

sub tail_rec_sum($;$){
  my( $x,$running_total ) = (@_,0);

  return $running_total unless $x;

  @_ = ($x-1,$running_total+$x);
  goto &tail_rec_sum; # throw away current stack frame
}

8

Đây là một đoạn trích từ Cấu trúc và Giải thích các Chương trình Máy tính về đệ quy đuôi.

Ngược lại với phép lặp và đệ quy, chúng ta phải cẩn thận không nhầm lẫn giữa khái niệm quy trình đệ quy với khái niệm thủ tục đệ quy. Khi chúng tôi mô tả một thủ tục là đệ quy, chúng tôi đề cập đến thực tế cú pháp rằng định nghĩa thủ tục đề cập (trực tiếp hoặc gián tiếp) đến chính thủ tục đó. Nhưng khi chúng ta mô tả một quy trình theo mô hình đệ quy tuyến tính, chúng ta đang nói về cách quá trình phát triển, chứ không phải về cú pháp của một thủ tục được viết như thế nào. Có vẻ đáng lo ngại khi chúng ta đề cập đến một quy trình đệ quy như fact-iter như tạo ra một quy trình lặp. Tuy nhiên, quá trình thực sự lặp đi lặp lại: Trạng thái của nó được nắm bắt hoàn toàn bởi ba biến trạng thái của nó và một trình thông dịch cần theo dõi chỉ ba biến để thực hiện quy trình.

Một lý do khiến sự khác biệt giữa quy trình và thủ tục có thể gây nhầm lẫn là hầu hết việc triển khai các ngôn ngữ phổ biến (bao gồm Ada, Pascal và C) được thiết kế theo cách mà việc giải thích bất kỳ thủ tục đệ quy nào tiêu tốn một lượng bộ nhớ phát triển cùng với số lượng các cuộc gọi thủ tục, ngay cả khi quy trình được mô tả, về nguyên tắc, lặp lại. Kết quả là, các ngôn ngữ này chỉ có thể mô tả các quy trình lặp bằng cách sử dụng các cấu trúc vòng lặp có mục đích đặc biệt, như làm, lặp lại, cho đến khi, cho và trong khi. Việc thực hiện Đề án không chia sẻ khuyết điểm này. Nó sẽ thực hiện một quy trình lặp trong không gian không đổi, ngay cả khi quy trình lặp được mô tả bằng thủ tục đệ quy. Một triển khai với thuộc tính này được gọi là đệ quy đuôi. Với việc thực hiện đệ quy đuôi, phép lặp có thể được thể hiện bằng cơ chế gọi thủ tục thông thường, do đó các cấu trúc lặp đặc biệt chỉ hữu ích như đường cú pháp.


1
Tôi đã đọc qua tất cả các câu trả lời ở đây, và đây là lời giải thích rõ ràng nhất chạm đến cốt lõi thực sự sâu sắc của khái niệm này. Nó giải thích nó một cách thẳng thắn khiến mọi thứ trông thật đơn giản và rõ ràng. Xin hãy tha thứ cho sự thô lỗ của tôi. Nó bằng cách nào đó khiến tôi cảm thấy như những câu trả lời khác chỉ không đâm vào đầu đinh. Tôi nghĩ đó là lý do tại sao SICP quan trọng.
englealuze

8

Hàm đệ quy là một hàm tự gọi

Nó cho phép các lập trình viên viết các chương trình hiệu quả bằng cách sử dụng một lượng mã tối thiểu .

Nhược điểm là chúng có thể gây ra các vòng lặp vô hạn và các kết quả không mong muốn khác nếu không được viết đúng .

Tôi sẽ giải thích cả chức năng đệ quy đơn giản và chức năng đệ quy đuôi

Để viết một hàm đệ quy đơn giản

  1. Điểm đầu tiên cần xem xét là khi nào bạn nên quyết định ra khỏi vòng lặp đó là vòng lặp if
  2. Thứ hai là quá trình phải làm gì nếu chúng ta là chức năng của chính mình

Từ ví dụ đã cho:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

Từ ví dụ trên

if(n <=1)
     return 1;

Là yếu tố quyết định khi thoát khỏi vòng lặp

else 
     return n * fact(n-1);

Là xử lý thực tế phải được thực hiện

Hãy để tôi phá vỡ nhiệm vụ từng cái một để dễ hiểu.

Hãy cho chúng tôi xem những gì xảy ra trong nội bộ nếu tôi chạy fact(4)

  1. Thay thế n = 4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

Ifvòng lặp thất bại để nó đi vào elsevòng lặp để nó trở lại4 * fact(3)

  1. Trong bộ nhớ ngăn xếp, chúng ta có 4 * fact(3)

    Thay thế n = 3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

Ifvòng lặp thất bại để nó đi vào elsevòng lặp

để nó trở lại 3 * fact(2)

Hãy nhớ rằng chúng tôi đã gọi `` `4 * fact (3)` `

Đầu ra cho fact(3) = 3 * fact(2)

Cho đến nay ngăn xếp có 4 * fact(3) = 4 * 3 * fact(2)

  1. Trong bộ nhớ ngăn xếp, chúng ta có 4 * 3 * fact(2)

    Thay thế n = 2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

Ifvòng lặp thất bại để nó đi vào elsevòng lặp

để nó trở lại 2 * fact(1)

Hãy nhớ rằng chúng tôi đã gọi 4 * 3 * fact(2)

Đầu ra cho fact(2) = 2 * fact(1)

Cho đến nay ngăn xếp có 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. Trong bộ nhớ ngăn xếp, chúng ta có 4 * 3 * 2 * fact(1)

    Thay thế n = 1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If vòng lặp là đúng

để nó trở lại 1

Hãy nhớ rằng chúng tôi đã gọi 4 * 3 * 2 * fact(1)

Đầu ra cho fact(1) = 1

Cho đến nay ngăn xếp có 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

Cuối cùng, kết quả thực tế (4) = 4 * 3 * 2 * 1 = 24

nhập mô tả hình ảnh ở đây

Các Đệ quy Tail sẽ

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. Thay thế n = 4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

Ifvòng lặp thất bại để nó đi vào elsevòng lặp để nó trở lạifact(3, 4)

  1. Trong bộ nhớ ngăn xếp, chúng ta có fact(3, 4)

    Thay thế n = 3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

Ifvòng lặp thất bại để nó đi vào elsevòng lặp

để nó trở lại fact(2, 12)

  1. Trong bộ nhớ ngăn xếp, chúng ta có fact(2, 12)

    Thay thế n = 2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

Ifvòng lặp thất bại để nó đi vào elsevòng lặp

để nó trở lại fact(1, 24)

  1. Trong bộ nhớ ngăn xếp, chúng ta có fact(1, 24)

    Thay thế n = 1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If vòng lặp là đúng

để nó trở lại running_total

Đầu ra cho running_total = 24

Cuối cùng, kết quả thực tế (4,1) = 24

nhập mô tả hình ảnh ở đây


7

Đuôi đệ quy là cuộc sống bạn đang sống ngay bây giờ. Bạn liên tục tái chế cùng một khung ngăn xếp, lặp đi lặp lại, bởi vì không có lý do hoặc phương tiện nào để quay lại khung "trước đó". Quá khứ đã qua và được thực hiện với nó để có thể được loại bỏ. Bạn nhận được một khung hình, mãi mãi di chuyển đến tương lai, cho đến khi quá trình của bạn chắc chắn chết.

Sự tương tự bị phá vỡ khi bạn xem xét một số quy trình có thể sử dụng các khung bổ sung nhưng vẫn được coi là đệ quy đuôi nếu ngăn xếp không phát triển vô hạn.


1
nó không phá vỡ dưới sự giải thích rối loạn nhân cách chia . :) Một xã hội của tâm trí; một tâm trí như một xã hội. :)
Will Ness

Ồ Bây giờ đó là một cách khác để suy nghĩ về nó
sutanu dalui

7

Đệ quy đuôi là một hàm đệ quy trong đó hàm tự gọi nó ở cuối ("đuôi") của hàm trong đó không tính toán được thực hiện sau khi trả về lệnh gọi đệ quy. Nhiều trình biên dịch tối ưu hóa để thay đổi một cuộc gọi đệ quy thành một cuộc gọi đệ quy đuôi hoặc một cuộc gọi lặp.

Hãy xem xét các vấn đề của giai thừa tính toán của một số.

Một cách tiếp cận đơn giản sẽ là:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

Giả sử bạn gọi giai thừa (4). Cây đệ quy sẽ là:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

Độ sâu đệ quy tối đa trong trường hợp trên là O (n).

Tuy nhiên, hãy xem xét ví dụ sau:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

Cây đệ quy cho factTail (4) sẽ là:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

Ở đây cũng vậy, độ sâu đệ quy tối đa là O (n) nhưng không có cuộc gọi nào thêm bất kỳ biến phụ nào vào ngăn xếp. Do đó trình biên dịch có thể làm mất đi với một ngăn xếp.


7

Đuôi đệ quy khá nhanh so với đệ quy thông thường. Nó nhanh vì đầu ra của cuộc gọi tổ tiên sẽ không được viết thành chồng để theo dõi. Nhưng trong đệ quy bình thường, tất cả các cuộc gọi tổ tiên được viết trong ngăn xếp để theo dõi.


6

Hàm đệ quy đuôi là một hàm đệ quy trong đó thao tác cuối cùng thực hiện trước khi quay lại thực hiện lệnh gọi hàm đệ quy. Đó là, giá trị trả về của lệnh gọi hàm đệ quy ngay lập tức được trả về. Ví dụ: mã của bạn sẽ trông như thế này:

def recursiveFunction(some_params):
    # some code here
    return recursiveFunction(some_args)
    # no code after the return statement

Trình biên dịch và trình thông dịch thực hiện tối ưu hóa cuộc gọi đuôi hoặc loại bỏ cuộc gọi đuôi có thể tối ưu hóa mã đệ quy để ngăn chặn tràn ngăn xếp. Nếu trình biên dịch hoặc trình thông dịch của bạn không thực hiện tối ưu hóa cuộc gọi đuôi (chẳng hạn như trình thông dịch CPython), thì không có lợi ích bổ sung nào khi viết mã theo cách này.

Ví dụ, đây là một hàm giai đoạn đệ quy chuẩn trong Python:

def factorial(number):
    if number == 1:
        # BASE CASE
        return 1
    else:
        # RECURSIVE CASE
        # Note that `number *` happens *after* the recursive call.
        # This means that this is *not* tail call recursion.
        return number * factorial(number - 1)

Và đây là phiên bản đệ quy đuôi của hàm giai thừa:

def factorial(number, accumulator=1):
    if number == 0:
        # BASE CASE
        return accumulator
    else:
        # RECURSIVE CASE
        # There's no code after the recursive call.
        # This is tail call recursion:
        return factorial(number - 1, number * accumulator)
print(factorial(5))

(Lưu ý rằng mặc dù đây là mã Python, trình thông dịch CPython không thực hiện tối ưu hóa cuộc gọi đuôi, do đó, việc sắp xếp mã của bạn như thế này không có lợi ích thời gian chạy.)

Bạn có thể phải làm cho mã của mình khó đọc hơn một chút để sử dụng tối ưu hóa cuộc gọi đuôi, như trong ví dụ giai thừa. (Ví dụ, trường hợp cơ sở bây giờ hơi không trực quan và accumulatortham số được sử dụng hiệu quả như một loại biến toàn cục.)

Nhưng lợi ích của việc tối ưu hóa cuộc gọi đuôi là nó ngăn ngừa lỗi tràn ngăn xếp. (Tôi sẽ lưu ý rằng bạn có thể nhận được lợi ích tương tự bằng cách sử dụng thuật toán lặp thay vì thuật toán đệ quy.)

Tràn ngăn xếp được tạo ra khi ngăn xếp cuộc gọi có quá nhiều đối tượng khung được đẩy lên. Một đối tượng khung được đẩy lên ngăn xếp cuộc gọi khi một chức năng được gọi và bật ra khỏi ngăn xếp cuộc gọi khi chức năng đó trở lại. Các đối tượng khung chứa thông tin như các biến cục bộ và dòng mã nào sẽ trở về khi hàm trả về.

Nếu chức năng đệ quy của bạn thực hiện quá nhiều cuộc gọi đệ quy mà không trả về, ngăn xếp cuộc gọi có thể vượt quá giới hạn đối tượng khung của nó. (Số lượng thay đổi theo nền tảng; trong Python là 1000 đối tượng khung theo mặc định.) Điều này gây ra lỗi tràn ngăn xếp . (Này, đó là tên của trang web này đến từ đâu!)

Tuy nhiên, nếu điều cuối cùng mà hàm đệ quy của bạn thực hiện là thực hiện cuộc gọi đệ quy và trả về giá trị trả về của nó, thì không có lý do gì nó cần giữ đối tượng khung hiện tại cần ở lại ngăn xếp cuộc gọi. Xét cho cùng, nếu không có mã sau lệnh gọi hàm đệ quy, không có lý do gì để bám vào các biến cục bộ của đối tượng khung hiện tại. Vì vậy, chúng ta có thể thoát khỏi đối tượng khung hiện tại ngay lập tức thay vì giữ nó trên ngăn xếp cuộc gọi. Kết quả cuối cùng của việc này là ngăn xếp cuộc gọi của bạn không tăng kích thước và do đó không thể chồng tràn.

Trình biên dịch hoặc trình thông dịch phải có tối ưu hóa cuộc gọi đuôi như một tính năng để nó có thể nhận ra khi tối ưu hóa cuộc gọi đuôi có thể được áp dụng. Thậm chí sau đó, bạn có thể sắp xếp lại mã trong chức năng đệ quy của mình để sử dụng tối ưu hóa cuộc gọi đuôi và tùy thuộc vào khả năng đọc dễ giảm này có giá trị tối ưu hóa.


"Đệ quy đuôi (còn gọi là tối ưu hóa cuộc gọi đuôi hoặc loại bỏ cuộc gọi đuôi)". Không; loại bỏ cuộc gọi đuôi hoặc tối ưu hóa cuộc gọi đuôi là điều bạn có thể áp dụng cho chức năng đệ quy đuôi, nhưng chúng không giống nhau. Bạn có thể viết các hàm đệ quy đuôi trong Python (như bạn hiển thị), nhưng chúng không hiệu quả hơn hàm không đệ quy đuôi, bởi vì Python không thực hiện tối ưu hóa cuộc gọi đuôi.
giữ trẻ

Điều đó có nghĩa là nếu ai đó quản lý để tối ưu hóa trang web và hiển thị cuộc gọi đệ quy đuôi đệ quy thì chúng ta sẽ không có trang web StackOverflow nữa?! Điều đó thật kinh khủng.
Nadjib Mami

5

Để hiểu một số khác biệt cốt lõi giữa đệ quy gọi đuôi và đệ quy không gọi đuôi, chúng ta có thể khám phá các triển khai .NET của các kỹ thuật này.

Dưới đây là một bài viết với một số ví dụ trong C #, F # và C ++ \ CLI: Adventures in Tail Recursion in C #, F # và C ++ \ CLI .

C # không tối ưu hóa cho đệ quy cuộc gọi đuôi trong khi F # thì không.

Sự khác biệt của nguyên tắc liên quan đến các vòng lặp so với phép tính Lambda. C # được thiết kế với các vòng lặp trong khi F # được xây dựng từ các nguyên tắc tính toán Lambda. Để có một cuốn sách rất hay (và miễn phí) về các nguyên tắc tính toán Lambda, hãy xem Cấu trúc và diễn giải các chương trình máy tính, của Abelson, Sussman và Sussman .

Về các cuộc gọi đuôi trong F #, để có một bài viết giới thiệu rất hay, hãy xem Giới thiệu chi tiết về các cuộc gọi đuôi trong F # . Cuối cùng, đây là một bài viết đề cập đến sự khác biệt giữa đệ quy không đuôi và đệ quy gọi đuôi (trong F #): Đệ quy đuôi so với đệ quy không đuôi trong F sắc nét .

Nếu bạn muốn đọc về một số khác biệt về thiết kế của đệ quy cuộc gọi đuôi giữa C # và F #, hãy xem Tạo Mã số gọi cho cuộc gọi đuôi trong C # và F # .

Nếu bạn quan tâm đủ để muốn biết những điều kiện nào ngăn trình biên dịch C # thực hiện tối ưu hóa cuộc gọi đuôi, hãy xem bài viết này: Điều kiện gọi đuôi của JIT CLR .


4

Có hai loại đệ quy cơ bản: đệ quy đầuđệ quy đuôi.

Trong đệ quy đầu , một hàm thực hiện cuộc gọi đệ quy của nó và sau đó thực hiện một số tính toán khác, có thể sử dụng kết quả của cuộc gọi đệ quy, ví dụ.

Trong một hàm đệ quy đuôi , tất cả các tính toán xảy ra đầu tiên và cuộc gọi đệ quy là điều cuối cùng xảy ra.

Lấy từ bài siêu tuyệt vời này. Hãy xem xét việc đọc nó.


4

Đệ quy có nghĩa là một hàm gọi chính nó. Ví dụ:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

Đuôi đệ quy có nghĩa là đệ quy kết luận hàm:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

Xem, điều cuối cùng chức năng chưa kết thúc (thủ tục, trong thuật ngữ Scheme) là gọi chính nó. Một ví dụ khác (hữu ích hơn) là:

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

Trong thủ tục của người trợ giúp, việc LAST thực hiện nếu bên trái không phải là tự gọi (SAU một cái gì đó và cdr một cái gì đó). Về cơ bản, đây là cách bạn lập bản đồ.

Đệ quy đuôi có một lợi thế lớn là trình thông dịch (hoặc trình biên dịch, phụ thuộc vào ngôn ngữ và nhà cung cấp) có thể tối ưu hóa nó và biến nó thành một cái gì đó tương đương với một vòng lặp while. Thực tế, theo truyền thống Scheme, hầu hết các vòng lặp "for" và "while" được thực hiện theo cách đệ quy đuôi (không có trong và theo thời gian, theo như tôi biết).


3

Câu hỏi này có rất nhiều câu trả lời tuyệt vời ... nhưng tôi không thể không đồng ý với cách thay thế về cách định nghĩa "đệ quy đuôi", hoặc ít nhất là "đệ quy đuôi thích hợp". Cụ thể: người ta có nên xem nó như một tính chất của một biểu thức cụ thể trong một chương trình không? Hay người ta nên xem nó như một tài sản của việc thực hiện ngôn ngữ lập trình ?

Để biết thêm về quan điểm sau, có một bài báo cổ điển của Will Clinger, "Đệ quy thích hợp và hiệu quả không gian" (PLDI 1998), định nghĩa "đệ quy đuôi thích hợp" là một đặc tính của việc thực hiện ngôn ngữ lập trình. Định nghĩa được xây dựng để cho phép một người bỏ qua các chi tiết triển khai (chẳng hạn như liệu ngăn xếp cuộc gọi có thực sự được biểu diễn thông qua ngăn xếp thời gian chạy hoặc thông qua danh sách các khung được liên kết phân bổ heap không).

Để thực hiện điều này, nó sử dụng phân tích tiệm cận: không phải là thời gian thực hiện chương trình như người ta thường thấy, mà là sử dụng không gian chương trình . Theo cách này, việc sử dụng không gian của danh sách được liên kết phân bổ heap so với ngăn xếp cuộc gọi thời gian chạy kết thúc tương đương với tiệm cận; vì vậy người ta phải bỏ qua chi tiết triển khai ngôn ngữ lập trình đó (một chi tiết chắc chắn có vấn đề khá nhiều trong thực tế, nhưng có thể làm vẩn đục nước khá nhiều khi cố gắng xác định liệu một triển khai đã cho có thỏa mãn yêu cầu "đệ quy đuôi tài sản" không )

Bài viết đáng để nghiên cứu cẩn thận vì một số lý do:

  • Nó đưa ra một định nghĩa quy nạp của các biểu thức đuôicác lệnh gọi đuôi của một chương trình. (Định nghĩa như vậy và tại sao các cuộc gọi như vậy lại quan trọng, dường như là chủ đề của hầu hết các câu trả lời khác được đưa ra ở đây.)

    Dưới đây là những định nghĩa, chỉ để cung cấp một hương vị của văn bản:

    Định nghĩa 1 Các biểu thức đuôi của một chương trình được viết trong Core Scheme được định nghĩa theo quy nạp như sau.

    1. Phần thân của biểu thức lambda là biểu thức đuôi
    2. Nếu (if E0 E1 E2)là biểu thức đuôi, thì cả hai E1E2là biểu thức đuôi.
    3. Không có gì khác là một biểu hiện đuôi.

    Định nghĩa 2 Một cuộc gọi đuôi là một biểu thức đuôi là một cuộc gọi thủ tục.

(một cuộc gọi đệ quy đuôi, hoặc như bài báo nói, "cuộc gọi tự đuôi" là một trường hợp đặc biệt của một cuộc gọi đuôi trong đó thủ tục được gọi chính nó.)

  • Nó cung cấp các định nghĩa chính thức cho sáu "máy" khác nhau để đánh giá Lõi Đề án, trong đó mỗi máy có thể quan sát được hành vi tương tự , ngoại trừ cho các tiệm cận lớp không gian phức tạp cho mỗi tên là trong.

    Ví dụ: sau khi đưa ra định nghĩa cho các máy tương ứng, 1. quản lý bộ nhớ dựa trên ngăn xếp, 2. thu gom rác nhưng không có cuộc gọi đuôi, 3. thu gom rác và gọi đuôi, bài báo tiếp tục với các chiến lược quản lý lưu trữ tiên tiến hơn, chẳng hạn như 4. "evlis đệ quy đuôi", nơi mà môi trường không cần phải được bảo tồn qua việc đánh giá của các đối số tiểu biểu hiện cuối cùng trong một cuộc gọi đuôi, 5. giảm môi trường của một đóng cửa để chỉ các biến miễn đóng cửa đó, và 6. cái gọi là ngữ nghĩa "an toàn cho không gian" theo định nghĩa của Appel và Shao .

  • Để chứng minh rằng các máy thực sự thuộc sáu lớp phức tạp không gian riêng biệt, bài báo, cho mỗi cặp máy được so sánh, cung cấp các ví dụ cụ thể về các chương trình sẽ làm nổ tung không gian tiệm cận trên một máy chứ không phải máy kia.


(Đọc qua câu trả lời của tôi bây giờ, tôi không chắc liệu mình có thực sự nắm bắt được những điểm quan trọng của bài báo Clinger hay không . Nhưng, than ôi, tôi không thể dành nhiều thời gian hơn để phát triển câu trả lời này ngay bây giờ.)


1

Nhiều người đã giải thích đệ quy ở đây. Tôi muốn trích dẫn một vài suy nghĩ về một số lợi thế mà đệ quy mang lại từ cuốn sách Đồng thời trong .NET, Các mô hình lập trình song song và lập trình hiện đại của Riccardo Terrell:

Đệ quy chức năng là cách tự nhiên để lặp lại trong FP vì nó tránh được sự đột biến của trạng thái. Trong mỗi lần lặp, một giá trị mới được truyền vào hàm tạo vòng lặp thay vào đó để được cập nhật (bị thay đổi). Ngoài ra, một chức năng đệ quy có thể được tạo ra, làm cho chương trình của bạn trở nên mô đun hơn, cũng như giới thiệu các cơ hội để khai thác song song. "

Đây cũng là một số lưu ý thú vị từ cùng một cuốn sách về đệ quy đuôi:

Đệ quy cuộc gọi đuôi là một kỹ thuật chuyển đổi chức năng đệ quy thông thường thành phiên bản tối ưu hóa có thể xử lý các đầu vào lớn mà không có bất kỳ rủi ro và tác dụng phụ nào.

LƯU Ý Lý do chính cho một cuộc gọi đuôi là tối ưu hóa là để cải thiện vị trí dữ liệu, sử dụng bộ nhớ và sử dụng bộ đệm. Bằng cách thực hiện một cuộc gọi đuôi, callee sử dụng cùng một không gian ngăn xếp như người gọi. Điều này làm giảm áp lực bộ nhớ. Nó cải thiện nhẹ bộ đệm vì cùng một bộ nhớ được sử dụng lại cho những người gọi tiếp theo và có thể ở trong bộ đệm, thay vì xóa một dòng bộ đệm cũ hơn để nhường chỗ cho dòng bộ đệm mới.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.