đệ quy so với lặp lại


109

Có đúng không khi nói rằng mọi nơi đều sử dụng đệ quy, một forvòng lặp có thể được sử dụng? Và nếu đệ quy thường chậm hơn thì lý do kỹ thuật cho việc sử dụng nó qua forlặp vòng lặp là gì?

Và nếu luôn có thể chuyển đổi một đệ quy thành một forvòng lặp thì có quy tắc ngón tay cái để làm điều đó không?


3
recursionvs iteration? iteration = for loopTôi nghĩ.
chiêngzhitaao

4
Blog của Tom Moertel của có bốn bài viết xuất sắc về chuyển đổi code đệ quy để mã lặp đi lặp lại: blog.moertel.com/tags/recursion.html
cjohnson318

Câu trả lời:


147

Đệ quy thường chậm hơn nhiều vì tất cả các lệnh gọi hàm phải được lưu trữ trong một ngăn xếp để cho phép trả về các hàm của người gọi. Trong nhiều trường hợp, bộ nhớ phải được cấp phát và sao chép để thực hiện cách ly phạm vi.

Một số tối ưu hóa, như tối ưu hóa cuộc gọi đuôi , làm cho các đệ quy nhanh hơn nhưng không phải lúc nào cũng có thể thực hiện được và không được triển khai bằng tất cả các ngôn ngữ.

Những lý do chính để sử dụng đệ quy là

  • rằng nó trực quan hơn trong nhiều trường hợp khi nó bắt chước cách tiếp cận vấn đề của chúng tôi
  • rằng một số cấu trúc dữ liệu như cây dễ khám phá hơn bằng cách sử dụng đệ quy (hoặc sẽ cần ngăn xếp trong mọi trường hợp)

Tất nhiên mọi đệ quy có thể được mô hình hóa như một loại vòng lặp: đó là điều cuối cùng CPU sẽ làm. Và bản thân đệ quy, trực tiếp hơn, có nghĩa là đặt các lệnh gọi hàm và phạm vi trong một ngăn xếp. Nhưng việc thay đổi thuật toán đệ quy của bạn thành một thuật toán lặp có thể cần nhiều công việc và làm cho mã của bạn ít khả năng bảo trì hơn: đối với mọi tối ưu hóa, chỉ nên thử khi một số hồ sơ hoặc bằng chứng cho thấy nó là cần thiết.


10
Thêm vào đó - đệ quy có liên quan chặt chẽ với thuật ngữ rút gọn đóng vai trò trung tâm trong nhiều thuật toán và trong CS nói chung.
SomeWittyUsername

3
Bạn có thể vui lòng cung cấp cho tôi một ví dụ trong đó đệ quy làm cho mã dễ bảo trì hơn không? Theo kinh nghiệm của tôi, nó luôn luôn ngược lại. Cảm ơn bạn
Yeikel

@Yeikel Viết một hàm f(n)trả về số Fibonacci thứ n .
Matt,

54

Có chính xác không khi nói rằng mọi nơi đệ quy được sử dụng một vòng lặp for có thể được sử dụng?

Có, bởi vì đệ quy trong hầu hết các CPU được mô hình hóa bằng các vòng lặp và cấu trúc dữ liệu ngăn xếp.

Và nếu đệ quy thường chậm hơn thì lý do kỹ thuật để sử dụng nó là gì?

Nó không phải là "thường chậm hơn": đó là đệ quy được áp dụng không chính xác sẽ chậm hơn. Trên hết, các trình biên dịch hiện đại rất giỏi trong việc chuyển đổi một số đệ quy thành vòng lặp mà không cần hỏi.

Và nếu luôn có thể chuyển đổi một đệ quy thành một vòng lặp for thì có quy tắc ngón tay cái để làm điều đó không?

Viết chương trình lặp cho các thuật toán được hiểu rõ nhất khi được giải thích lặp lại; viết chương trình đệ quy cho các thuật toán được giải thích một cách đệ quy tốt nhất.

Ví dụ, tìm kiếm cây nhị phân, chạy quicksort và phân tích cú pháp biểu thức trong nhiều ngôn ngữ lập trình thường được giải thích một cách đệ quy. Chúng cũng được mã hóa đệ quy tốt nhất. Mặt khác, tính toán thừa số và tính toán số Fibonacci dễ giải thích hơn nhiều về số lần lặp. Sử dụng đệ quy đối với chúng giống như đánh ruồi bằng một chiếc búa tạ: đó không phải là một ý kiến ​​hay, ngay cả khi chiếc búa tạ thực hiện rất tốt + .


+ Tôi mượn phép ví von chiếc búa tạ từ cuốn "Kỷ luật lập trình" của Dijkstra.


7
Đệ quy thường đắt hơn (chậm hơn / nhiều bộ nhớ hơn), vì tạo khung ngăn xếp và như vậy. Sự khác biệt có thể nhỏ khi được áp dụng đúng cách cho một vấn đề đủ phức tạp, nhưng nó vẫn đắt hơn. Có thể có những trường hợp ngoại lệ như tối ưu hóa đệ quy đuôi.
Bernhard Barker

Tôi không chắc về một vòng lặp for trong mọi trường hợp. Hãy xem xét một đệ quy phức tạp hơn hoặc một đệ quy có nhiều hơn một biến
SomeWittyUsername

@dasblinkenlight Về mặt lý thuyết, có thể giảm nhiều vòng lặp xuống một vòng duy nhất, nhưng không chắc chắn về điều này.
SomeWittyUsername

@icepack Có, nó có thể. Nó có thể không đẹp, nhưng nó có thể.
Bernhard Barker

Tôi không chắc tôi đồng ý với tuyên bố đầu tiên của bạn. Bản thân các CPU không thực sự mô hình hóa đệ quy, nó là các lệnh đang được chạy trên CPU mô hình hóa đệ quy. thứ hai, một cấu trúc vòng lặp không (nhất thiết) phải có một tập dữ liệu đang phát triển và thu nhỏ một cách linh động, trong đó một thuật toán đệ quy thường sẽ cho mỗi cấp sâu mà đệ quy phải bước.
trumpetlicks

28

Câu hỏi:

Và nếu đệ quy thường chậm hơn thì lý do kỹ thuật nào cho việc sử dụng nó để lặp lại vòng lặp?

Câu trả lời :

Bởi vì trong một số thuật toán khó có thể giải nó lặp đi lặp lại. Cố gắng giải quyết tìm kiếm theo chiều sâu theo cả cách đệ quy và lặp lại. Bạn sẽ có ý tưởng rằng thật khó để giải quyết DFS với sự lặp lại.

Một điều tốt khác để thử: Cố gắng viết sắp xếp Hợp nhất lặp đi lặp lại. Bạn sẽ mất khá nhiều thời gian.

Câu hỏi:

Có chính xác không khi nói rằng mọi nơi đệ quy được sử dụng một vòng lặp for có thể được sử dụng?

Câu trả lời :

Đúng. Đây chủ đề có một câu trả lời rất tốt cho việc này.

Câu hỏi:

Và nếu luôn có thể chuyển đổi một đệ quy thành một vòng lặp for thì có quy tắc ngón tay cái để làm điều đó không?

Câu trả lời :

Hãy tin tôi. Cố gắng viết phiên bản của riêng bạn để giải quyết tìm kiếm theo chiều sâu lặp đi lặp lại. Bạn sẽ nhận thấy rằng một số vấn đề dễ dàng hơn để giải quyết nó một cách đệ quy.

Gợi ý: Đệ quy tốt khi bạn đang giải một bài toán có thể được giải bằng kỹ thuật chia và chinh phục .


3
Tôi đánh giá cao nỗ lực đưa ra câu trả lời có thẩm quyền và tôi chắc rằng tác giả thông minh nhưng "tin tôi đi" không phải là câu trả lời hữu ích cho một câu hỏi có ý nghĩa mà câu trả lời không rõ ràng ngay lập tức. Có những thuật toán rất đơn giản để thực hiện tìm kiếm theo chiều sâu lặp đi lặp lại. Xem ví dụ ở cuối trang này để biết mô tả về một thuật toán trong mã giả: csl.mtu.edu/cs2321/www/newLectures/26_Depth_First_Search.html
jdelman

3

Bên cạnh việc chậm hơn, đệ quy cũng có thể dẫn đến lỗi tràn ngăn xếp tùy thuộc vào độ sâu của nó.


3

Để viết một phương thức tương đương bằng cách sử dụng phép lặp, chúng ta phải sử dụng một cách rõ ràng một ngăn xếp. Thực tế là phiên bản lặp lại yêu cầu một ngăn xếp cho giải pháp của nó cho thấy rằng vấn đề đủ khó để nó có thể hưởng lợi từ đệ quy. Theo nguyên tắc chung, đệ quy phù hợp nhất cho các vấn đề không thể giải quyết với một lượng bộ nhớ cố định và do đó yêu cầu một ngăn xếp khi được giải theo cách lặp lại. Phải nói rằng, đệ quy và lặp lại có thể hiển thị cùng một kết quả trong khi chúng tuân theo các mẫu khác nhau.

Ví dụ, để tìm số tam giác thứ n của dãy Tam giác: 1 3 6 10 15… Một chương trình sử dụng thuật toán lặp để tìm số tam giác thứ n:

Sử dụng thuật toán lặp lại:

//Triangular.java
import java.util.*;
class Triangular {
   public static int iterativeTriangular(int n) {
      int sum = 0;
      for (int i = 1; i <= n; i ++)
         sum += i;
      return sum;
   }
   public static void main(String args[]) {
      Scanner stdin = new Scanner(System.in);
      System.out.print("Please enter a number: ");
      int n = stdin.nextInt();
      System.out.println("The " + n + "-th triangular number is: " + 
                            iterativeTriangular(n));
   }
}//enter code here

Sử dụng thuật toán đệ quy:

//Triangular.java
import java.util.*;
class Triangular {
   public static int recursiveTriangular(int n) {
      if (n == 1)
     return 1;  
      return recursiveTriangular(n-1) + n; 
   }

   public static void main(String args[]) {
      Scanner stdin = new Scanner(System.in);
      System.out.print("Please enter a number: ");
      int n = stdin.nextInt();
      System.out.println("The " + n + "-th triangular number is: " + 
                             recursiveTriangular(n)); 
   }
}

1

Hầu hết các câu trả lời dường như giả định rằng iterative= for loop. Nếu vòng lặp for của bạn không bị hạn chế ( a la C, bạn có thể làm bất cứ điều gì bạn muốn với bộ đếm vòng lặp của mình), thì điều đó là chính xác. Nếu đó là một vòng lặp thực for (ví dụ như trong Python hoặc hầu hết các ngôn ngữ chức năng mà bạn không thể sửa đổi bộ đếm vòng lặp theo cách thủ công), thì nó không chính xác.

Tất cả các hàm (có thể tính toán được) có thể được thực hiện cả đệ quy và sử dụng whilecác vòng lặp (hoặc các bước nhảy có điều kiện, về cơ bản là giống nhau). Nếu bạn thực sự hạn chế bản thânfor loops , bạn sẽ chỉ nhận được một tập con của các hàm đó (các hàm đệ quy nguyên thủy, nếu các phép toán cơ bản của bạn là hợp lý). Đúng là như vậy, đó là một tập hợp con khá lớn chứa mọi hàm đơn lẻ mà bạn có thể sử dụng trong thực tế.

Điều quan trọng hơn nhiều là rất nhiều hàm rất dễ triển khai đệ quy và rất khó triển khai lặp đi lặp lại (quản lý thủ công ngăn xếp cuộc gọi của bạn không được tính).


1

Vâng, như lời của Thanakron Tandavas ,

Đệ quy tốt khi bạn đang giải quyết một vấn đề có thể được giải quyết bằng kỹ thuật chia và chinh phục.

Ví dụ: Towers of Hanoi

  1. N vòng tăng kích thước
  2. 3 cực
  3. Các vòng bắt đầu xếp chồng lên nhau trên cực 1. Mục tiêu là di chuyển các vòng sao cho chúng xếp chồng lên nhau trên cực 3 ... Nhưng
    • Chỉ có thể di chuyển một vòng tại một thời điểm.
    • Không thể đặt vòng lớn hơn trên vòng nhỏ hơn.
  4. Giải pháp lặp lại là "mạnh mẽ nhưng xấu xí"; giải pháp đệ quy là "thanh lịch".

Một ví dụ thú vị. Tôi đoán bạn biết bài báo của MC Er "The Towers of Hanoi and Binary Numerals". Cũng được xem trong một video tuyệt vời của 3brown1blue.
Andrestand

0

Dường như tôi nhớ giáo sư khoa học máy tính của tôi đã nói hồi ngày rằng tất cả các bài toán có giải pháp đệ quy cũng có giải pháp lặp lại. Ông nói rằng một giải pháp đệ quy thường chậm hơn, nhưng chúng thường được sử dụng khi chúng dễ lập luận và viết mã hơn các giải pháp lặp lại.

Tuy nhiên, trong trường hợp các giải pháp đệ quy nâng cao hơn, tôi không tin rằng nó sẽ luôn có thể thực hiện chúng bằng cách sử dụng một forvòng lặp đơn giản .


Luôn có thể chuyển đổi một thuật toán đệ quy thành một thuật toán lặp (sử dụng ngăn xếp). Bạn có thể không kết thúc với một vòng lặp đặc biệt đơn giản, nhưng nó hoàn toàn có thể.
Bernhard Barker

-4

đệ quy + ghi nhớ có thể dẫn đến giải pháp hiệu quả hơn so với cách tiếp cận lặp lại thuần túy, ví dụ: hãy kiểm tra điều này: http://jsperf.com/fibonacci-memoized-vs-iterative-for-large-n


3
Bất kỳ mã đệ quy nào cũng có thể được chuyển đổi thành mã lặp lại giống hệt nhau về mặt chức năng bằng cách sử dụng ngăn xếp. Sự khác biệt mà bạn đang thể hiện là sự khác biệt giữa hai cách tiếp cận để giải quyết cùng một vấn đề, không phải sự khác biệt giữa đệ quy và lặp lại.
Bernhard Barker

-6

Câu trả lời ngắn gọn: sự cân bằng là đệ quy nhanh hơn và vòng lặp for chiếm ít bộ nhớ hơn trong hầu hết các trường hợp. Tuy nhiên, thường có nhiều cách để thay đổi vòng lặp for hoặc đệ quy để làm cho nó chạy nhanh hơn

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.