Đệ quy có phải là một tính năng tự nó không?


116

... hay chỉ là luyện tập?

Tôi hỏi điều này vì một cuộc tranh cãi với giáo sư của tôi: Tôi đã mất tín nhiệm khi gọi một hàm đệ quy trên cơ sở chúng tôi không đề cập đến đệ quy trong lớp và lập luận của tôi là chúng tôi đã học nó một cách ngầm định bằng cách học returnvà phương pháp.

Tôi đang hỏi ở đây vì tôi nghi ngờ ai đó có câu trả lời dứt khoát.

Ví dụ, sự khác biệt giữa hai phương pháp sau là gì:

public static void a() {
    return a();
    }

public static void b() {
    return a();
    }

Ngoài " atiếp tục mãi mãi" (trong chương trình thực tế, nó được sử dụng đúng cách để nhắc người dùng một lần nữa khi được cung cấp đầu vào không hợp lệ), có sự khác biệt cơ bản nào giữa abkhông? Đối với một trình biên dịch chưa được tối ưu hóa, chúng được xử lý khác nhau như thế nào?

Cuối cùng, nó phụ thuộc vào việc liệu bằng cách học return a()từ bđó chúng ta cũng học return a()từ đó a. Chúng tôi đã?


24
Tranh luận xuất sắc. Tôi tự hỏi nếu bạn giải thích nó như thế này với giáo sư của bạn. Nếu bạn đã làm vậy, tôi nghĩ anh ấy nên trả lại công trạng đã mất cho bạn.
Michael Yaworski

57
Đệ quy thậm chí không phải là một khái niệm dành riêng cho khoa học máy tính. Hàm Fibonacci, toán tử giai thừa và rất nhiều thứ khác từ toán học (và có thể là các lĩnh vực khác) được (hoặc ít nhất có thể được) biểu diễn một cách đệ quy. Giáo sư có yêu cầu rằng bạn cũng không biết những điều này không?
Theodoros Chatzigiannakis

34
Thay vào đó, giáo sư nên ghi công cho anh ta vì đã nghĩ ra một cách giải quyết vấn đề một cách thanh lịch, hoặc để nói lên suy nghĩ sáng suốt.

11
Nhiệm vụ là gì? Đây là một vấn đề mà tôi thường thắc mắc, khi bạn gửi một nhiệm vụ lập trình, những gì được đánh dấu, khả năng giải quyết vấn đề của bạn hoặc khả năng sử dụng những gì bạn đã học. Hai điều này không nhất thiết phải giống nhau.
Leon

35
FWIW, nhắc nhập cho đến khi đúng không phải là nơi tốt để sử dụng đệ quy, quá dễ làm tràn ngăn xếp. Đối với trường hợp cụ thể này, sẽ tốt hơn nếu sử dụng một cái gì đó như a() { do { good = prompt(); } while (!good); }.
Kevin,

Câu trả lời:


113

Để trả lời câu hỏi cụ thể của bạn: Không, từ quan điểm của việc học một ngôn ngữ, đệ quy không phải là một tính năng. Nếu giáo sư của bạn thực sự cho điểm bạn vì đã sử dụng một "tính năng" mà ông ấy chưa dạy, điều đó đã sai.

Đọc giữa các dòng, một khả năng là bằng cách sử dụng đệ quy, bạn đã tránh sử dụng một tính năng được cho là kết quả học tập cho khóa học của anh ta. Ví dụ: có thể bạn hoàn toàn không sử dụng phép lặp hoặc có thể bạn chỉ sử dụng forcác vòng lặp thay vì sử dụng cả forwhile. Thông thường, một bài tập nhằm mục đích kiểm tra khả năng của bạn để làm một số việc nhất định và nếu bạn tránh làm chúng, giáo sư của bạn sẽ không thể cấp cho bạn điểm dành riêng cho tính năng đó. Tuy nhiên, nếu đó thực sự là nguyên nhân khiến bạn bị mất điểm, giáo sư nên coi đây là kinh nghiệm học tập của bản thân - nếu việc chứng minh kết quả học tập nhất định là một trong những tiêu chí cho một bài tập, thì cần giải thích rõ ràng cho sinh viên. .

Phải nói rằng, tôi đồng ý với hầu hết các ý kiến ​​và câu trả lời khác rằng phép lặp là một lựa chọn tốt hơn đệ quy ở đây. Có một vài lý do, và trong khi những người khác đã chạm vào chúng ở một mức độ nào đó, tôi không chắc họ đã giải thích đầy đủ suy nghĩ đằng sau chúng.

Tràn ngăn xếp

Rõ ràng hơn là bạn có nguy cơ gặp lỗi tràn ngăn xếp. Trên thực tế, phương pháp bạn đã viết rất khó thực sự dẫn đến một phương pháp, vì người dùng sẽ phải nhập sai nhiều lần để thực sự kích hoạt tràn ngăn xếp.

Tuy nhiên, một điều cần lưu ý là không chỉ bản thân phương thức, mà các phương thức khác cao hơn hoặc thấp hơn trong chuỗi cuộc gọi sẽ nằm trên ngăn xếp. Bởi vì điều này, việc vô tình chiếm đoạt không gian ngăn xếp có sẵn là một điều khá bất lịch sự đối với bất kỳ phương pháp nào. Không ai muốn phải thường xuyên lo lắng về không gian ngăn xếp trống bất cứ khi nào họ viết mã vì rủi ro rằng mã khác có thể đã sử dụng quá nhiều không cần thiết.

Đây là một phần của nguyên tắc chung hơn trong thiết kế phần mềm được gọi là trừu tượng hóa. Về cơ bản, khi bạn gọi DoThing(), tất cả những gì bạn cần quan tâm là Điều đã xong. Bạn không cần phải lo lắng về các chi tiết triển khai về cách nó được thực hiện. Nhưng việc sử dụng ngăn xếp một cách tham lam đã phá vỡ nguyên tắc này, bởi vì mỗi bit mã phải lo lắng về số lượng ngăn xếp mà nó có thể an toàn giả định rằng nó đã để lại cho nó bằng mã ở nơi khác trong chuỗi lệnh gọi.

Dễ đọc

Lý do khác là tính dễ đọc. Lý tưởng mà mã nên mong muốn là trở thành một tài liệu có thể đọc được của con người, trong đó mỗi dòng mô tả đơn giản những gì nó đang làm. Thực hiện hai cách tiếp cận sau:

private int getInput() {
    int input;
    do {
        input = promptForInput();
    } while (!inputIsValid(input))
    return input;
}

đấu với

private int getInput() {
    int input = promptForInput();
    if(inputIsValid(input)) {
        return input;
    }
    return getInput();
}

Vâng, cả hai đều hoạt động và có, cả hai đều khá dễ hiểu. Nhưng hai cách tiếp cận có thể được mô tả bằng tiếng Anh như thế nào? Tôi nghĩ nó sẽ giống như:

Tôi sẽ nhắc nhập cho đến khi đầu vào hợp lệ rồi trả lại

đấu với

Tôi sẽ nhắc nhập đầu vào, sau đó nếu đầu vào hợp lệ tôi sẽ trả lại, nếu không, tôi sẽ nhận đầu vào và trả về kết quả của điều đó

Có lẽ bạn có thể nghĩ về những từ ngữ ít rườm rà hơn cho phần sau, nhưng tôi nghĩ bạn sẽ luôn thấy rằng từ đầu tiên sẽ là một mô tả chính xác hơn, về mặt khái niệm, về những gì bạn thực sự đang cố gắng làm. Điều này không có nghĩa là đệ quy luôn luôn khó đọc hơn. Đối với các tình huống mà nó tỏa sáng, chẳng hạn như duyệt qua cây, bạn có thể thực hiện cùng một loại phân tích song song giữa đệ quy và một cách tiếp cận khác và gần như chắc chắn bạn sẽ thấy đệ quy cung cấp mã tự mô tả rõ ràng hơn, từng dòng một.

Riêng biệt, cả hai đều là những điểm nhỏ. Rất ít khả năng điều này sẽ thực sự dẫn đến tràn ngăn xếp và khả năng đọc đạt được là rất nhỏ. Nhưng bất kỳ chương trình nào cũng sẽ là tập hợp của nhiều quyết định nhỏ này, vì vậy, ngay cả khi chúng tách rời nhau không quan trọng nhiều, điều quan trọng là phải tìm hiểu các nguyên tắc đằng sau việc làm cho chúng đúng.


8
Bạn có thể mở rộng khẳng định của mình rằng đệ quy không phải là một tính năng không? Tôi đã lập luận trong câu trả lời của mình rằng nó là như vậy, bởi vì không phải tất cả các trình biên dịch đều nhất thiết phải hỗ trợ nó.
Harry Johnston

5
Không phải tất cả các ngôn ngữ đều nhất thiết phải hỗ trợ đệ quy, vì vậy nó không nhất thiết chỉ là vấn đề chọn trình biên dịch phù hợp - nhưng bạn hoàn toàn đúng khi nói rằng "tính năng" là một mô tả vốn không rõ ràng, vậy là đủ công bằng. Điểm thứ hai của bạn cũng công bằng từ quan điểm của một người học lập trình (như bây giờ bình thường) mà không có bất kỳ nền tảng nào về lập trình mã máy trước tiên. :-)
Harry Johnston

2
Lưu ý rằng vấn đề 'khả năng đọc' là vấn đề với cú pháp. Không có gì là "không thể đọc được" về đệ quy. Trên thực tế, quy nạp là cách dễ nhất để thể hiện cấu trúc dữ liệu quy nạp, như vòng lặp, danh sách và trình tự, v.v. Và hầu hết các cấu trúc dữ liệu là quy nạp.
nomen

6
Tôi nghĩ bạn đã xếp chồng lên nhau bộ bài với từ ngữ của bạn. Bạn đã mô tả phiên bản lặp lại theo chức năng và ngược lại. Tôi nghĩ rằng mô tả từng dòng một công bằng hơn về cả hai sẽ là “Tôi sẽ nhắc bạn nhập liệu. Nếu đầu vào không hợp lệ, tôi sẽ tiếp tục lặp lại lời nhắc cho đến khi tôi nhận được đầu vào hợp lệ. Sau đó tôi sẽ trả lại nó ”. vs “Tôi sẽ nhắc đầu vào. Nếu đầu vào hợp lệ, tôi sẽ trả lại. Nếu không tôi sẽ trả về kết quả của một lần làm lại. ” (Các con tôi đã hiểu khái niệm chức năng của việc thực hiện khi chúng còn học mầm non, vì vậy tôi nghĩ đó là bản tóm tắt tiếng Anh hợp pháp về khái niệm đệ quy ở đây.)
pjs

2
@HarryJohnston Thiếu hỗ trợ đệ quy sẽ là một ngoại lệ đối với các tính năng hiện có chứ không phải là thiếu một tính năng mới. Đặc biệt, trong ngữ cảnh của câu hỏi này, "tính năng mới" có nghĩa là "hành vi hữu ích mà chúng tôi chưa dạy tồn tại", điều này không đúng với đệ quy vì đó là phần mở rộng hợp lý của các tính năng đã được dạy (cụ thể là các thủ tục chứa hướng dẫn và cuộc gọi thủ tục là hướng dẫn). Nó giống như thể giáo sư dạy một học sinh phép cộng, và sau đó mắng anh ta vì đã cộng cùng một giá trị nhiều hơn một lần vì "chúng tôi chưa bao gồm phép nhân".
nmclean

48

Để trả lời câu hỏi nghĩa đen, thay vì câu hỏi meta: đệ quy một tính năng, theo nghĩa không phải tất cả các trình biên dịch và / hoặc ngôn ngữ nhất thiết phải cho phép nó. Trong thực tế, nó được mong đợi ở tất cả các trình biên dịch hiện đại (thông thường) - và chắc chắn là tất cả các trình biên dịch Java! - nhưng nó không đúng trên toàn cầu .

Như một ví dụ tiếp theo về lý do tại sao đệ quy có thể không được hỗ trợ, hãy xem xét một trình biên dịch lưu trữ địa chỉ trả về cho một hàm ở một vị trí tĩnh; đây có thể là trường hợp, ví dụ, đối với trình biên dịch cho bộ vi xử lý không có ngăn xếp.

Đối với một trình biên dịch như vậy, khi bạn gọi một hàm như thế này

a();

nó được thực hiện như

move the address of label 1 to variable return_from_a
jump to label function_a
label 1

và định nghĩa của a (),

function a()
{
   var1 = 5;
   return;
}

được thực hiện như

label function_a
move 5 to variable var1
jump to the address stored in variable return_from_a

Hy vọng rằng vấn đề khi bạn cố gắng gọi a()đệ quy trong trình biên dịch như vậy là rõ ràng; trình biên dịch không còn biết cách trả về từ lệnh gọi bên ngoài nữa, vì địa chỉ trả về đã bị ghi đè.

Đối với trình biên dịch mà tôi thực sự sử dụng (tôi nghĩ là cuối những năm 70 hoặc đầu những năm 80) không hỗ trợ đệ quy, vấn đề hơi phức tạp hơn thế: địa chỉ trả về sẽ được lưu trữ trên ngăn xếp, giống như trong các trình biên dịch hiện đại, nhưng các biến cục bộ thì không. 't. (Về mặt lý thuyết, điều này có nghĩa là có thể thực hiện đệ quy đối với các hàm không có biến cục bộ không tĩnh, nhưng tôi không nhớ liệu trình biên dịch có hỗ trợ rõ ràng điều đó hay không. Nó có thể cần các biến cục bộ ngầm định vì một số lý do.)

Nhìn về phía trước, tôi có thể tưởng tượng ra các kịch bản chuyên biệt - có lẽ là các hệ thống song song - nơi mà việc không phải cung cấp một ngăn xếp cho mọi luồng có thể có lợi, và do đó, đệ quy chỉ được phép nếu trình biên dịch có thể cấu trúc lại nó thành một vòng lặp. (Tất nhiên các trình biên dịch sơ khai mà tôi thảo luận ở trên không có khả năng thực hiện các tác vụ phức tạp như tái cấu trúc mã.)


Ví dụ, bộ tiền xử lý C không hỗ trợ đệ quy trong macro. Các định nghĩa macro hoạt động tương tự như các hàm, nhưng bạn không thể gọi chúng một cách đệ quy.
sth

7
"Ví dụ giả quy" của bạn không phải là tất cả những gì được tạo ra: Tiêu chuẩn Fortran 77 không cho phép các hàm tự gọi đệ quy - lý do là khá nhiều những gì bạn mô tả. (Tôi tin rằng địa chỉ để chuyển đến khi chức năng được thực hiện được lưu trữ ở cuối mã chức năng hoặc một cái gì đó tương đương với sự sắp xếp này.) Xem tại đây để biết một chút về điều đó.
alexis

5
Ví dụ như ngôn ngữ Shader hoặc ngôn ngữ GPGPU (giả sử, GLSL, Cg, OpenCL C) không hỗ trợ đệ quy. Trong chừng mực, lập luận "không phải tất cả các ngôn ngữ đều hỗ trợ nó" chắc chắn là hợp lệ. Đệ quy giả định tương đương với một ngăn xếp (nó không nhất thiết phải là một ngăn xếp , nhưng cần phải có một phương tiện để lưu trữ địa chỉ trả về và khung hàm bằng cách nào đó ).
Damon

Một trình biên dịch Fortran mà tôi đã làm việc một chút vào đầu những năm 1970 không có ngăn xếp cuộc gọi. Mỗi chương trình con hoặc hàm có các vùng nhớ tĩnh cho địa chỉ trả về, các tham số và các biến riêng của nó.
Patricia Shanahan

2
Ngay cả một số phiên bản của Turbo Pascal cũng vô hiệu hóa đệ quy theo mặc định và bạn phải thiết lập chỉ thị trình biên dịch để kích hoạt nó.
dan04

17

Giáo viên muốn biết bạn đã học hay chưa. Rõ ràng là bạn đã không giải quyết vấn đề theo cách mà anh ấy đã dạy bạn ( cách tốt ; lặp đi lặp lại), và do đó, cho rằng bạn đã không làm như vậy. Tôi là tất cả vì các giải pháp sáng tạo nhưng trong trường hợp này, tôi phải đồng ý với giáo viên của bạn vì một lý do khác:
Nếu người dùng cung cấp đầu vào không hợp lệ quá nhiều lần (tức là bằng cách nhấn enter), bạn sẽ có ngoại lệ tràn ngăn xếp và giải pháp sẽ sụp đổ. Ngoài ra, giải pháp lặp đi lặp lại hiệu quả hơn và dễ bảo trì hơn. Tôi nghĩ đó là lý do mà giáo viên của bạn nên đưa ra cho bạn.


2
Chúng tôi không được yêu cầu thực hiện nhiệm vụ này theo bất kỳ cách cụ thể nào; và chúng tôi đã học về các phương thức, không chỉ là phép lặp. Ngoài ra, tôi sẽ để lại cái nào dễ đọc hơn tùy theo sở thích cá nhân: Tôi đã chọn những gì có vẻ tốt với tôi. Lỗi SO là mới đối với tôi, mặc dù ý tưởng rằng đệ quy là một tính năng trong bản thân nó dường như vẫn chưa được thành lập.

3
"Tôi sẽ để lại cái nào dễ đọc hơn theo sở thích cá nhân". Đã đồng ý. Đệ quy không phải là một tính năng của Java. Đây là.
mike

2
@Vality: Loại bỏ cuộc gọi đuôi? Một số JVM có thể làm điều đó, nhưng hãy nhớ rằng nó cũng cần duy trì dấu vết ngăn xếp cho các trường hợp ngoại lệ. Nếu nó cho phép loại bỏ cuộc gọi đuôi, dấu vết ngăn xếp, được tạo một cách ngây thơ, có thể trở nên không hợp lệ, vì vậy một số JVM không thực hiện TCE vì lý do đó.
icktoofay

5
Dù bằng cách nào, việc dựa vào tối ưu hóa để làm cho mã hỏng của bạn ít bị hỏng hơn, là một hình thức khá tệ.
cHao

7
+1, hãy thấy rằng trong Ubuntu gần đây, màn hình đăng nhập đã bị hỏng khi người dùng nhấn nút Enter liên tục, điều tương tự đã xảy ra với XBox
Sebastian

13

Trừ điểm vì "chúng tôi đã không đề cập đến đệ quy trong lớp" thật là khủng khiếp. Nếu bạn đã học cách gọi hàm A gọi hàm B gọi hàm C, hàm này trả về B, hàm này trả về A, hàm này trả về trở lại trình gọi và giáo viên không nói rõ ràng với bạn rằng đây phải là các hàm khác nhau (mà chẳng hạn như trong các phiên bản FORTRAN cũ), không có lý do gì mà A, B và C không thể cùng một chức năng.

Mặt khác, chúng tôi sẽ phải xem mã thực tế để quyết định xem trong trường hợp cụ thể của bạn, sử dụng đệ quy có thực sự là điều cần làm hay không. Không có nhiều chi tiết, nhưng nghe có vẻ sai.


10

Có rất nhiều quan điểm để xem xét về câu hỏi cụ thể mà bạn đã hỏi nhưng điều tôi có thể nói là từ quan điểm học một ngôn ngữ, đệ quy không phải là một tính năng của riêng nó. Nếu giáo sư của bạn thực sự cho điểm bạn vì đã sử dụng một "tính năng" mà ông ấy chưa dạy, điều đó là sai nhưng như tôi đã nói, có những quan điểm khác cần xem xét ở đây thực sự khiến giáo sư đúng khi trừ điểm.

Từ những gì tôi có thể suy ra từ câu hỏi của bạn, việc sử dụng một hàm đệ quy để yêu cầu đầu vào trong trường hợp đầu vào bị lỗi không phải là một phương pháp hay vì mọi lệnh gọi của hàm đệ quy đều được đẩy vào ngăn xếp. Vì đệ quy này được điều khiển bởi đầu vào của người dùng nên có thể có một hàm đệ quy vô hạn và do đó dẫn đến một StackOverflow.

Không có sự khác biệt giữa 2 ví dụ mà bạn đã đề cập trong câu hỏi của mình theo nghĩa những gì chúng làm (nhưng khác nhau theo những cách khác) - Trong cả hai trường hợp, một địa chỉ trả về và tất cả thông tin phương thức đang được tải vào ngăn xếp. Trong trường hợp đệ quy, địa chỉ trả về chỉ đơn giản là dòng ngay sau khi gọi phương thức (tất nhiên, nó không chính xác như những gì bạn thấy trong chính mã, mà là trong mã mà trình biên dịch đã tạo). Trong Java, C và Python, đệ quy khá đắt so với lặp (nói chung) vì nó yêu cầu cấp phát một khung ngăn xếp mới. Chưa kể bạn có thể nhận được ngoại lệ tràn ngăn xếp nếu đầu vào không hợp lệ quá nhiều lần.

Tôi tin rằng giáo sư đã trừ điểm vì đệ quy được coi là một chủ đề của riêng nó và không chắc ai đó không có kinh nghiệm lập trình sẽ nghĩ đến đệ quy. (Tất nhiên không có nghĩa là họ sẽ không, nhưng không chắc).

IMHO, tôi nghĩ giáo sư đã đúng khi trừ điểm cho bạn. Bạn có thể dễ dàng chuyển phần xác thực sang một phương pháp khác và sử dụng nó như thế này:

public bool foo() 
{
  validInput = GetInput();
  while(!validInput)
  {
    MessageBox.Show("Wrong Input, please try again!");
    validInput = GetInput();
  }
  return hasWon(x, y, piece);
}

Nếu những gì bạn đã làm thực sự có thể được giải quyết theo cách đó thì những gì bạn đã làm là một hành vi xấu và nên tránh.


Mục đích của chính phương thức là xác thực đầu vào, sau đó gọi và trả về kết quả của một phương thức khác (đó là lý do tại sao nó tự trả về). Cụ thể, nó kiểm tra xem nước đi trong trò chơi Tic-Tac-Toe có hợp lệ hay không, sau đó trả về hasWon(x, y, piece)(chỉ kiểm tra hàng và cột bị ảnh hưởng).

bạn có thể dễ dàng CHỈ lấy phần xác thực và đặt nó vào một phương thức khác có tên là "GetInput" chẳng hạn và sau đó sử dụng nó như tôi đã viết trong câu trả lời của mình. Tôi đã chỉnh sửa câu trả lời của mình với cách nó sẽ như thế nào. Tất nhiên, bạn có thể làm cho GetInput trả về một kiểu chứa thông tin bạn cần.
Yonatan Nir

1
Yonatan Nir: Khi nào thì đệ quy là một thực hành xấu? Có thể JVM sẽ nổ tung vì Hotspot VM không thể tối ưu hóa do bảo mật mã byte và mọi thứ sẽ là một đối số tốt. Làm thế nào là mã của bạn bất kỳ khác với nó sử dụng một cách tiếp cận khác?

1
Đệ quy không phải lúc nào cũng là một thực hành xấu nhưng nếu nó có thể tránh được và giữ cho mã sạch và dễ bảo trì thì nên tránh. Trong Java, C và Python, đệ quy khá đắt so với lặp (nói chung) vì nó yêu cầu cấp phát một khung ngăn xếp mới. Trong một số trình biên dịch C, người ta có thể sử dụng cờ trình biên dịch để loại bỏ chi phí này, nó biến đổi một số kiểu đệ quy nhất định (thực tế là một số kiểu gọi đuôi) thành các bước nhảy thay vì gọi hàm.
Yonatan Nir

1
Nó không rõ ràng, nhưng nếu bạn thay thế một vòng lặp với số lần lặp vô hạn bằng đệ quy, thì điều đó thật tệ. Java không đảm bảo tối ưu hóa cuộc gọi đuôi, vì vậy bạn có thể dễ dàng hết dung lượng ngăn xếp. Trong Java, không sử dụng đệ quy, trừ khi bạn được đảm bảo có số lần lặp hạn chế (thường là lôgarit so với tổng kích thước của dữ liệu).
hyde

6

Có thể giáo sư của bạn chưa dạy nó, nhưng có vẻ như bạn đã sẵn sàng để tìm hiểu những ưu và nhược điểm của đệ quy.

Ưu điểm chính của đệ quy là các thuật toán đệ quy thường dễ viết và nhanh hơn nhiều.

Nhược điểm chính của đệ quy là các thuật toán đệ quy có thể gây tràn ngăn xếp, vì mỗi mức đệ quy yêu cầu thêm một khung ngăn xếp bổ sung vào ngăn xếp.

Đối với mã sản xuất, trong đó việc mở rộng quy mô có thể dẫn đến nhiều cấp độ đệ quy trong quá trình sản xuất hơn so với các thử nghiệm đơn vị của lập trình viên, nhược điểm thường vượt trội hơn lợi thế và mã đệ quy thường bị tránh khi thực tế.


1
Bất kỳ thuật toán đệ quy tiềm ẩn rủi ro nào luôn có thể được viết lại một cách đáng kể để sử dụng một ngăn xếp rõ ràng - ngăn xếp cuộc gọi xét cho cùng cũng chỉ là một ngăn xếp. Trong trường hợp này, nếu bạn viết lại giải pháp để sử dụng ngăn xếp, nó sẽ trông rất nực cười - thêm bằng chứng cho thấy câu trả lời đệ quy không phải là một câu trả lời rất tốt.
Aaronaught

1
Nếu tràn ngăn xếp là vấn đề, bạn nên sử dụng ngôn ngữ / thời gian chạy hỗ trợ tối ưu hóa cuộc gọi đuôi, chẳng hạn như .NET 4.0 hoặc bất kỳ ngôn ngữ lập trình chức năng nào
Sebastian

Không phải tất cả các đệ quy đều là lời gọi đuôi.
Warren Dew

6

Về câu hỏi cụ thể, đệ quy có phải là một tính năng không, tôi có xu hướng nói có, nhưng sau khi giải thích lại câu hỏi. Có những lựa chọn thiết kế chung về ngôn ngữ và trình biên dịch giúp khả năng đệ quy và các ngôn ngữ hoàn chỉnh Turing tồn tại không cho phép đệ quy . Nói cách khác, đệ quy là một khả năng được kích hoạt bởi các lựa chọn nhất định trong thiết kế ngôn ngữ / trình biên dịch.

  • Hỗ trợ các hàm hạng nhất làm cho phép đệ quy có thể thực hiện được với các giả định rất nhỏ; xem ví dụ viết các vòng lặp trong Unlambda hoặc biểu thức Python khó hiểu này không chứa tham chiếu tự, vòng lặp hoặc phép gán:

    >>> map((lambda x: lambda f: x(lambda g: f(lambda v: g(g)(v))))(
    ...   lambda c: c(c))(lambda R: lambda n: 1 if n < 2 else n * R(n - 1)),
    ...   xrange(10))
    [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
    
  • Các ngôn ngữ / trình biên dịch sử dụng liên kết trễ hoặc xác định các khai báo chuyển tiếp , làm cho khả năng đệ quy. Ví dụ: trong khi Python cho phép mã bên dưới, đó là lựa chọn thiết kế (liên kết muộn), không phải là yêu cầu đối với hệ thống hoàn chỉnh Turing . Các hàm đệ quy lẫn nhau thường phụ thuộc vào sự hỗ trợ cho các khai báo chuyển tiếp.

    factorial = lambda n: 1 if n < 2 else n * factorial(n-1)
    
  • Các ngôn ngữ được định kiểu tĩnh cho phép các kiểu được định nghĩa đệ quy góp phần kích hoạt đệ quy. Xem triển khai này của Bộ kết hợp Y trong Go . Nếu không có các kiểu được định nghĩa đệ quy, vẫn có thể sử dụng đệ quy trong cờ vây, nhưng tôi tin rằng bộ tổ hợp Y cụ thể là không thể.


1
Điều này khiến đầu tôi bùng nổ, đặc biệt là Unlambda +1
John Powell

Tổ hợp điểm cố định rất khó. Khi tôi quyết định học lập trình hàm, tôi buộc mình phải nghiên cứu bộ tổ hợp Y cho đến khi tôi hiểu nó, và sau đó áp dụng nó để viết các hàm hữu ích khác. Đã lấy tôi một lúc, nhưng rất đáng giá.
wberry

5

Từ những gì tôi có thể suy luận từ câu hỏi của bạn, sử dụng một hàm đệ quy để yêu cầu đầu vào trong trường hợp đầu vào bị lỗi không phải là một phương pháp hay. Tại sao?

Bởi vì mọi lệnh gọi hàm đệ quy đều được đẩy vào ngăn xếp. Vì đệ quy này được điều khiển bởi đầu vào của người dùng nên có thể có một hàm đệ quy vô hạn và do đó dẫn đến một StackOverflow :-p

Có một vòng lặp không đệ quy để làm điều này là cách để đi.


Phần lớn của phương thức được đề cập và mục đích của chính phương pháp này là xác thực đầu vào thông qua các lần kiểm tra khác nhau. Quá trình bắt đầu lại từ đầu nếu đầu vào không hợp lệ cho đến khi đầu vào đúng (như hướng dẫn).

4
@fay Nhưng nếu đầu vào không hợp lệ quá nhiều lần, bạn sẽ gặp lỗi StackOverflowError. Đệ quy đẹp hơn, nhưng trong mắt tôi, thường là một vấn đề hơn là một vòng lặp thông thường (do các ngăn xếp).
Michael Yaworski

1
Vậy thì đó là một điểm thú vị và tốt. Tôi đã không coi đó là lỗi. Mặc dù vậy, liệu có thể đạt được hiệu quả tương tự bằng cách while(true)gọi cùng một phương thức không? Nếu vậy, tôi sẽ không nói điều này hỗ trợ bất kỳ sự khác biệt nào giữa đệ quy, tốt nên biết như vậy.

1
@fay while(true)là một vòng lặp vô hạn. Trừ khi bạn có một breaktuyên bố, tôi không thấy điểm trong đó, trừ khi bạn đang cố gắng làm hỏng chương trình của mình lol. Quan điểm của tôi là, nếu bạn gọi cùng một phương thức (đó là đệ quy), đôi khi nó sẽ cung cấp cho bạn một lỗi StackOverflowError , nhưng nếu bạn sử dụng một whilehoặc forvòng lặp, nó sẽ không. Vấn đề chỉ đơn giản là không tồn tại với một vòng lặp thông thường. Có lẽ tôi đã hiểu lầm bạn, nhưng câu trả lời của tôi dành cho bạn là không.
Michael Yaworski

4
Thành thật mà nói thì điều này đối với tôi có lẽ là lý do thực sự khiến giáo sư bị trừ điểm =) Ông ấy có thể không giải thích rõ lắm, nhưng đó là một lời phàn nàn hợp lệ để nói rằng bạn đã sử dụng nó theo cách sẽ bị coi là rất kém phong cách nếu không. hoàn toàn sai sót trong mã nghiêm trọng hơn.
Chỉ huy Co ngò Salamander

3

Đệ quy là một khái niệm lập trình , một tính năng (như lặp) và một thực hành . Như bạn có thể thấy từ liên kết, có một phạm vi nghiên cứu lớn dành riêng cho chủ đề này. Có lẽ chúng ta không cần phải đi sâu vào chủ đề để hiểu những điểm này.

Đệ quy như một tính năng

Nói một cách dễ hiểu, Java hỗ trợ nó một cách ngầm định, bởi vì nó cho phép một phương thức (về cơ bản là một hàm đặc biệt) có "kiến thức" về chính nó và về các phương thức khác tạo nên lớp mà nó thuộc về. Hãy xem xét một ngôn ngữ không đúng như vậy: bạn có thể viết phần nội dung của phương thức đó a, nhưng bạn sẽ không thể bao gồm lời gọi đến abên trong nó. Giải pháp duy nhất sẽ là sử dụng phép lặp để thu được cùng một kết quả. Trong một ngôn ngữ như vậy, bạn sẽ phải phân biệt giữa các hàm nhận thức được sự tồn tại của chính chúng (bằng cách sử dụng mã thông báo cú pháp cụ thể) và những hàm nào không nhận thức được! Trên thực tế, toàn bộ nhóm ngôn ngữ tạo ra sự khác biệt đó (xem ví dụ như họ LispML ). Điều thú vị là Perl thậm chí còn cho phép các chức năng ẩn danh (được gọi làlambdas ) để gọi chính chúng một cách đệ quy (một lần nữa, với một cú pháp dành riêng).

không đệ quy?

Đối với các ngôn ngữ thậm chí không hỗ trợ khả năng đệ quy, thường có một giải pháp khác, ở dạng tổ hợp Điểm cố định , nhưng nó vẫn yêu cầu ngôn ngữ hỗ trợ các chức năng được gọi là đối tượng lớp đầu tiên (tức là các đối tượng có thể được thao tác trong chính ngôn ngữ).

Đệ quy như một thực hành

Việc có sẵn tính năng đó trong một ngôn ngữ không có nghĩa là nó có tính thành ngữ. Trong Java 8, các biểu thức lambda đã được đưa vào, do đó, việc áp dụng phương pháp tiếp cận chức năng để lập trình có thể trở nên dễ dàng hơn. Tuy nhiên, có những cân nhắc thực tế:

  • cú pháp vẫn không thân thiện với đệ quy
  • trình biên dịch có thể không phát hiện được phương pháp đó và tối ưu hóa nó

Điểm mấu chốt

May mắn thay (hay chính xác hơn là để dễ sử dụng), Java cho phép các phương thức tự nhận thức theo mặc định và do đó hỗ trợ đệ quy, vì vậy đây không thực sự là một vấn đề thực tế, nhưng nó vẫn là một vấn đề lý thuyết và tôi cho rằng giáo viên của bạn muốn giải quyết nó một cách cụ thể. Bên cạnh đó, với sự phát triển gần đây của ngôn ngữ, nó có thể trở thành một thứ quan trọng trong tương lai.

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.